From d770a6a0913f8f82ae4f16838bd490872c36b42f Mon Sep 17 00:00:00 2001 From: ramjayakumar21 Date: Fri, 25 Apr 2025 15:47:47 -0700 Subject: [PATCH 01/20] Add inital functionality for controlling pump --- .../server/features/aeac_water_delivery.py | 68 +++++++++++++++++++ src_pymav/server/httpserver.py | 24 ++++++- src_pymav/server/operations/rc_channel_cmd.py | 54 +++++++++++++++ 3 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 src_pymav/server/operations/rc_channel_cmd.py diff --git a/src_pymav/server/features/aeac_water_delivery.py b/src_pymav/server/features/aeac_water_delivery.py index aac9678..519f858 100644 --- a/src_pymav/server/features/aeac_water_delivery.py +++ b/src_pymav/server/features/aeac_water_delivery.py @@ -1,7 +1,19 @@ from server.common.wpqueue import WaypointQueue, Waypoint +from server.operations.rc_channel_cmd import send_rc_channel_value +from pymavlink import mavutil +AEAC_PUMP_CHANNEL = 7 + +''' +Generates a water delivery mission with the following waypoints: +1. Start at current location +2. Loiter down to delivery altitude for a specified duration (deliver_duration_secs) +3. Send signal to deliver water +3. Return to previous location altitude +''' def generate_water_wps( + mav_connection: mavutil.mavfile, current_alt: float, deliver_alt: float, deliver_duration_secs: int, @@ -10,6 +22,7 @@ def generate_water_wps( ) -> WaypointQueue: landing_mission = WaypointQueue() + # Set the current altitude to the current location wp_1 = Waypoint( "Start", "curr_wp", @@ -18,6 +31,7 @@ def generate_water_wps( current_alt, ) + # TODO how do we send a message here? wp_2 = Waypoint( "stay", "curr_wp", @@ -42,3 +56,57 @@ def generate_water_wps( landing_mission.push(wp_3) return landing_mission + +def send_payload_command(mav_connection: mavutil.mavfile, value: int, command: str): + channel = AEAC_PUMP_CHANNEL + result = send_rc_channel_value(mav_connection=mav_connection, channel=channel, value=value) + + if result == -1: + print(f"Failed to send command '{command}' to channel {channel}.") + return -1 + else: + print(f"Successfully sent command '{command}' to channel {channel}.") + return 1 + +def set_payload_mode(mav_connection: mavutil.mavfile, valve_one_open: bool, + valve_two_open: bool, pump_on: bool): + + # Two switches are used on the payload + # SWITCH 1 represents the state of valve one and valve two + # Three possible states: + # 1. UP (100) - Valve one open and valve two closed + # 2. MID (300) - Both valves closed + # 3. DOWN (500) - Valve one closed and valve two open + + # SWITCH 2 represents the state of the pump + # Two possible states: + # 1. ON (1) - Pump on + # 2. OFF (-1) - Pump off + + # value = 1500 + SWITCH 1 * SWITCH 2 + + print(f"Setting payload mode with valve_one_open: {valve_one_open}, " + f"valve_two_open: {valve_two_open}, pump_on: {pump_on}") + + if pump_on and valve_one_open and not valve_two_open: + print("PAYLOAD: Set to intake water") + value = 1500 + 100 * 1 + elif not pump_on and not valve_one_open and not valve_two_open: + print("PAYLOAD: Set to transport water") + value = 1500 + 300 * -1 + elif not pump_on and not valve_one_open and valve_two_open: + print("PAYLOAD: Set to release water") + value = 1500 + 500 * -1 + elif not pump_on and valve_one_open and valve_two_open: + print("PAYLOAD: Set to refill reservoir") + value = 1500 + 100 * -1 + else: + print("Invalid combination of valve and pump states.") + return -1 + + result = send_rc_channel_value(mav_connection=mav_connection, channel=AEAC_PUMP_CHANNEL, value=value) + + if result == -1: + print(f"Failed to set payload value to {value}") + else: + print(f"Sucessfully set payload value to {value}") \ No newline at end of file diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 50c9f0c..137d796 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -11,7 +11,7 @@ from server.operations.land import land_in_place, land_at_position from server.features.aeac_scan import scan_area -from server.features.aeac_water_delivery import generate_water_wps +from server.features.aeac_water_delivery import generate_water_wps, set_payload_mode from server.utilities.request_message_streaming import set_parameter @@ -330,7 +330,7 @@ def deliver_water_down(): deliver_duration_secs = input["deliver_duration_secs"] curr_lat = input["curr_lat"] curr_lon = input["curr_lon"] - wpq = generate_water_wps(current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) + wpq = generate_water_wps(self.mav_connection, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) if new_mission(self.mav_connection, wpq): @@ -339,6 +339,26 @@ def deliver_water_down(): return "Mission request failed", 400 else: return f"Invalid input, missing a parameter.", 400 + + @app.route("/aeac_payload", methods=["POST"]) + def change_aeac_payload(): + input = request.get_json() + + if ("valve_one_open" in input and "valve_two_open" in input and "pump_on" in input): + + # Extract values from JSON input + valve_one_open = input["valve_one_open"] + valve_two_open = input["valve_two_open"] + pump_on = input["pump_on"] + + result = set_payload_mode(self.mav_connection, valve_one_open, valve_two_open, pump_on) + + if result != -1: + return f"Payload mode changed", 200 + else: + return "Payload mode failed to change", 400 + else: + return f"Invalid input, missing a parameter.", 400 socketio.run(app, host="0.0.0.0", port=PORT, debug=True, use_reloader=False) diff --git a/src_pymav/server/operations/rc_channel_cmd.py b/src_pymav/server/operations/rc_channel_cmd.py new file mode 100644 index 0000000..6120da9 --- /dev/null +++ b/src_pymav/server/operations/rc_channel_cmd.py @@ -0,0 +1,54 @@ +import math + +from pymavlink import mavutil + +from server.common.status import Status +from server.common.wpqueue import WaypointQueue, Waypoint +from server.common.encoders import command_int_to_string +from server.utilities.request_message_streaming import request_messages + +""" + Sends a specified value to the drone's RC channels. + @input mav_connection: The MAVLink connection object. + @input channel: The channel number to send the value to. + Value between 1 and 18, inclusive. + NOTE: channel 5 and 8 is used for flight mode, DO NOT use these channels. + @input value: The value to send to the specified channel. + Normally 1000 ~ 2000, 0 to release channel back to the RC radio, + UINT16_MAX (e.g 65535) to ignore this field + +""" +def send_rc_channel_value(mav_connection: mavutil.mavfile, channel: int, value: int, + tgt_sys_id: int = 1, tgt_comp_id: int = 1, timeout = 10) -> str: + if channel < 1 or channel > 18: + print(f"Channel {channel} is out of range. Must be between 1 and 18, inclusive.") + return -1 + if channel == 5 or channel == 8: + print(f"Channel {channel} is used for flight mode. Do not use this channel.") + return -1 + if value < 0 or value > 65535: + print(f"Value {value} is out of range. Must be between 0 and 65535, inclusive.") + return -1 + + channel_values = [0] * 18 + channel_values[channel - 1] = value + + print(f"DEBUG: channel_values: {channel_values}") + + mav_connection.mav.command_long_send( + tgt_comp_id, + tgt_sys_id, + mavutil.mavlink.RC_CHANNELS_OVERRIDE, + *channel_values, + ) + + ack = mav_connection.recv_match(type='COMMAND_ACK', blocking=True, timeout=timeout) + if ack is None: + print('No acknowledgment received within the timeout period.') + return -1 + + print(f"RC Channel value of {value} sent to channel {channel} successfully.") + return 1 + + + From 9ea92d78d4db85648ace6121bc8d04927702122f Mon Sep 17 00:00:00 2001 From: ramjayakumar21 Date: Fri, 25 Apr 2025 16:26:07 -0700 Subject: [PATCH 02/20] update api spec --- api_spec.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/api_spec.yml b/api_spec.yml index 5d357cc..9cdfbc7 100644 --- a/api_spec.yml +++ b/api_spec.yml @@ -349,7 +349,7 @@ paths: - delivery summary: Deliver water operation description: > - For AEAC 2025, while in autonomous mode, hovers down to specific altitude for 'deliver_duration' seconds, and then returns to original altitude ready for reentering manual control. + For AEAC 2025, while in autonomous mode, hovers down to specific altitude for 'deliver_duration' seconds and then returns to original altitude ready for reentering manual control. requestBody: content: application/json: @@ -376,6 +376,27 @@ paths: description: Commencing Deliver operation "400": description: Invalid input, missing a parameter or Mission request failed + tags: + - delivery + /aeac_payload: + summary: Control AEAC Pump and water payload + description: > + For AEAC 2025 allows controlling pump, valve 1 and valve 2 + requestBody: + content: + application/json: + schema: + type: object + properties: + pump_on: + type: boolean + description: Turn the pump on or off + valve_one_open: + type: boolean + description: Open/Close valve one + valve_two_open: + type: boolean + description: Open/Close valve two components: schemas: Waypoint: From 3ddde13dcc2dc8c0f44c79c6535ab88d932d406b Mon Sep 17 00:00:00 2001 From: Hansen Dan Date: Sun, 27 Apr 2025 17:55:16 -0700 Subject: [PATCH 03/20] prelim callback system impl --- src_pymav/server/common/callback.py | 81 +++++++++++++++++++ .../server/features/aeac_water_delivery.py | 12 +++ src_pymav/server/httpserver.py | 8 +- src_pymav/server/operations/get_info.py | 7 +- 4 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src_pymav/server/common/callback.py diff --git a/src_pymav/server/common/callback.py b/src_pymav/server/common/callback.py new file mode 100644 index 0000000..af7c6d1 --- /dev/null +++ b/src_pymav/server/common/callback.py @@ -0,0 +1,81 @@ +from pymavlink import mavutil + +class Callback(): + def __init__( + self, + name: str, + trigger_message_type: str, + trigger_condition: function = (lambda curr_msg, prev_msg: True), + payload: function = (lambda msg, conn, state: print(msg)), + only_once: bool = True + ): + """ + A callback instance. Callbacks are composed of two components - the trigger, which controls + when and how the callback fires, and the payload, which contains the code to run. + + Parameters: + `name`: A name to help disambiguate the callback. + `trigger_message_type`: mavlink message type that triggers the callback, + e.g. `'GLOBAL_POSITION_INT'` + `trigger_condition`: a function that should take in mavlink messages of the designated + trigger type and return a boolean that indicates whether or not the callback should + fire on that message. + Also has access to the previous message of that time seen by the callback system. An + example of how this can be used is triggering specifically on the transition between + waypoints 2 -> 3. + Functions must be able to handle cases where prev_msg is `None`. + If unspecified, a function that will always return `True` will be used - i.e., the + callback will fire on every message of the correct type. + `payload`: a function that contains the desired logic to run when the callback fires. + Has access to the message it was triggered on, the `mavlink_connection` object, and + a state dictionary owned by the MPS server instance itself (for passing information + back to the wider server context.) + If unspecified, a default function will be used that simply prints the message. + `only_once`: indicates that the callback should only execute once, i.e. it will be removed + after being triggered for the first and only time. + """ + + self.name = name + self.trigger_message_type = trigger_message_type + self.trigger_condition = trigger_condition + self.payload = payload + self.only_once = only_once + +class CallbackSystem(): + def __init__(self, mav_connection: mavutil.mavfile, state: dict): + self.conn = mav_connection + self.state = state + + self.callbacks: dict[str, list[Callback]] = {} + self.prev_messages: dict[str, object] = {} + + def update_and_check(self, latest_messages: dict): + # traverse through all callbacks and check each of their triggers + for callback_type, callback_list in self.callbacks.items(): + prev_msg = self.prev_messages.get(callback_type, None) + curr_msg = latest_messages.get(callback_type, None) + + if curr_msg == None: + # no point testing + continue + + for callback in callback_list: + if callback.trigger_condition(curr_msg, prev_msg): + + if callback.only_once: + # remove callback before firing it + self.callbacks[callback_type].remove(callback) + + # fire callback + callback.payload(curr_msg, self.conn, self.state) + + print(f"DEBUG: Fired Callback {callback.name}") + + # update prev_messages + self.prev_messages.update(latest_messages) + + def register_callback(self, callback: Callback): + self.callbacks[callback.trigger_message_type].append(callback) + + # def unregister_callback(self, name: str, trigger_message_type: int): + # pass \ No newline at end of file diff --git a/src_pymav/server/features/aeac_water_delivery.py b/src_pymav/server/features/aeac_water_delivery.py index 519f858..bd68431 100644 --- a/src_pymav/server/features/aeac_water_delivery.py +++ b/src_pymav/server/features/aeac_water_delivery.py @@ -1,4 +1,5 @@ from server.common.wpqueue import WaypointQueue, Waypoint +from server.common.callback import CallbackSystem, Callback from server.operations.rc_channel_cmd import send_rc_channel_value from pymavlink import mavutil @@ -14,6 +15,7 @@ ''' def generate_water_wps( mav_connection: mavutil.mavfile, + callback_sys: CallbackSystem, current_alt: float, deliver_alt: float, deliver_duration_secs: int, @@ -32,6 +34,16 @@ def generate_water_wps( ) # TODO how do we send a message here? + + # TODO test: setting up a callback to trigger on waypoint 2 + callback_sys.register_callback(Callback( + "Water Delivery Callback", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq == 2), + lambda msg, conn, state: send_payload_command(conn, 0, 'TODO'), # TODO !!! + True + )) + wp_2 = Waypoint( "stay", "curr_wp", diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 137d796..f554715 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -18,11 +18,17 @@ from server.common.wpqueue import WaypointQueue, Waypoint from server.common.status import Status from server.common.encoders import command_string_to_int, command_int_to_string +from server.common.callback import CallbackSystem class HTTP_Server: def __init__(self, mav_connection): self.mav_connection: mavfile = mav_connection + self.miscellaneous_state = {} + + self.callback_sys = CallbackSystem(self.mav_connection, self.miscellaneous_state) + + # TODO Handle Camera Protocol via Callbacks? def serve_forever(self, production=True, HOST="localhost", PORT=9000): print("GCOM HTTP Server starting...") @@ -330,7 +336,7 @@ def deliver_water_down(): deliver_duration_secs = input["deliver_duration_secs"] curr_lat = input["curr_lat"] curr_lon = input["curr_lon"] - wpq = generate_water_wps(self.mav_connection, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) + wpq = generate_water_wps(self.mav_connection, self.callback_sys, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) if new_mission(self.mav_connection, wpq): diff --git a/src_pymav/server/operations/get_info.py b/src_pymav/server/operations/get_info.py index e986c25..c10ab70 100644 --- a/src_pymav/server/operations/get_info.py +++ b/src_pymav/server/operations/get_info.py @@ -5,14 +5,16 @@ from server.common.status import Status from server.common.wpqueue import WaypointQueue, Waypoint from server.common.encoders import command_int_to_string +from server.common.callback import CallbackSystem from server.utilities.request_message_streaming import request_messages + """ Get current status of a drone Type of message can be found on https://mavlink.io/en/messages/common.html """ -def get_status(mav_connection: mavutil.mavfile) -> Status: +def get_status(mav_connection: mavutil.mavfile, callback_sys: CallbackSystem = None) -> Status: # trigger an update # mav_connection.recv_match(blocking=True) @@ -57,6 +59,9 @@ def get_status(mav_connection: mavutil.mavfile) -> Status: winddirection = math.degrees(math.atan(status_wind.wind_x / status_wind.wind_y)) if status_wind.wind_y != 0 else (0 if status_wind.wind_x > 0 else 180) windvelocity = math.sqrt(status_wind.wind_x * status_wind.wind_x + status_wind.wind_y * status_wind.wind_y) + # trigger / callback mechanism + callback_sys.update_and_check(mav_connection.messages) + return Status( system_time.time_unix_usec / 1000000, # seconds From f821bef554ce6485806c8a5fa304eed5825d481a Mon Sep 17 00:00:00 2001 From: Hansen Dan Date: Sun, 27 Apr 2025 17:55:16 -0700 Subject: [PATCH 04/20] prelim callback system impl --- src_pymav/server/common/callback.py | 81 +++++++++++++++++++ .../server/features/aeac_water_delivery.py | 12 +++ src_pymav/server/httpserver.py | 16 ++-- src_pymav/server/operations/get_info.py | 7 +- 4 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 src_pymav/server/common/callback.py diff --git a/src_pymav/server/common/callback.py b/src_pymav/server/common/callback.py new file mode 100644 index 0000000..af7c6d1 --- /dev/null +++ b/src_pymav/server/common/callback.py @@ -0,0 +1,81 @@ +from pymavlink import mavutil + +class Callback(): + def __init__( + self, + name: str, + trigger_message_type: str, + trigger_condition: function = (lambda curr_msg, prev_msg: True), + payload: function = (lambda msg, conn, state: print(msg)), + only_once: bool = True + ): + """ + A callback instance. Callbacks are composed of two components - the trigger, which controls + when and how the callback fires, and the payload, which contains the code to run. + + Parameters: + `name`: A name to help disambiguate the callback. + `trigger_message_type`: mavlink message type that triggers the callback, + e.g. `'GLOBAL_POSITION_INT'` + `trigger_condition`: a function that should take in mavlink messages of the designated + trigger type and return a boolean that indicates whether or not the callback should + fire on that message. + Also has access to the previous message of that time seen by the callback system. An + example of how this can be used is triggering specifically on the transition between + waypoints 2 -> 3. + Functions must be able to handle cases where prev_msg is `None`. + If unspecified, a function that will always return `True` will be used - i.e., the + callback will fire on every message of the correct type. + `payload`: a function that contains the desired logic to run when the callback fires. + Has access to the message it was triggered on, the `mavlink_connection` object, and + a state dictionary owned by the MPS server instance itself (for passing information + back to the wider server context.) + If unspecified, a default function will be used that simply prints the message. + `only_once`: indicates that the callback should only execute once, i.e. it will be removed + after being triggered for the first and only time. + """ + + self.name = name + self.trigger_message_type = trigger_message_type + self.trigger_condition = trigger_condition + self.payload = payload + self.only_once = only_once + +class CallbackSystem(): + def __init__(self, mav_connection: mavutil.mavfile, state: dict): + self.conn = mav_connection + self.state = state + + self.callbacks: dict[str, list[Callback]] = {} + self.prev_messages: dict[str, object] = {} + + def update_and_check(self, latest_messages: dict): + # traverse through all callbacks and check each of their triggers + for callback_type, callback_list in self.callbacks.items(): + prev_msg = self.prev_messages.get(callback_type, None) + curr_msg = latest_messages.get(callback_type, None) + + if curr_msg == None: + # no point testing + continue + + for callback in callback_list: + if callback.trigger_condition(curr_msg, prev_msg): + + if callback.only_once: + # remove callback before firing it + self.callbacks[callback_type].remove(callback) + + # fire callback + callback.payload(curr_msg, self.conn, self.state) + + print(f"DEBUG: Fired Callback {callback.name}") + + # update prev_messages + self.prev_messages.update(latest_messages) + + def register_callback(self, callback: Callback): + self.callbacks[callback.trigger_message_type].append(callback) + + # def unregister_callback(self, name: str, trigger_message_type: int): + # pass \ No newline at end of file diff --git a/src_pymav/server/features/aeac_water_delivery.py b/src_pymav/server/features/aeac_water_delivery.py index 519f858..bd68431 100644 --- a/src_pymav/server/features/aeac_water_delivery.py +++ b/src_pymav/server/features/aeac_water_delivery.py @@ -1,4 +1,5 @@ from server.common.wpqueue import WaypointQueue, Waypoint +from server.common.callback import CallbackSystem, Callback from server.operations.rc_channel_cmd import send_rc_channel_value from pymavlink import mavutil @@ -14,6 +15,7 @@ ''' def generate_water_wps( mav_connection: mavutil.mavfile, + callback_sys: CallbackSystem, current_alt: float, deliver_alt: float, deliver_duration_secs: int, @@ -32,6 +34,16 @@ def generate_water_wps( ) # TODO how do we send a message here? + + # TODO test: setting up a callback to trigger on waypoint 2 + callback_sys.register_callback(Callback( + "Water Delivery Callback", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq == 2), + lambda msg, conn, state: send_payload_command(conn, 0, 'TODO'), # TODO !!! + True + )) + wp_2 = Waypoint( "stay", "curr_wp", diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 137d796..40c201a 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -18,11 +18,17 @@ from server.common.wpqueue import WaypointQueue, Waypoint from server.common.status import Status from server.common.encoders import command_string_to_int, command_int_to_string +from server.common.callback import CallbackSystem class HTTP_Server: def __init__(self, mav_connection): self.mav_connection: mavfile = mav_connection + self.miscellaneous_state = {} + + self.callback_sys = CallbackSystem(self.mav_connection, self.miscellaneous_state) + + # TODO Handle Camera Protocol via Callbacks? def serve_forever(self, production=True, HOST="localhost", PORT=9000): print("GCOM HTTP Server starting...") @@ -36,7 +42,7 @@ def index(): @app.route("/queue", methods=["GET"]) def get_queue(): - curr = get_status(self.mav_connection)._wpn + curr = get_status(self.mav_connection, self.callback_sys)._wpn wpq = get_current_mission(self.mav_connection) formatted = [] @@ -56,7 +62,7 @@ def get_queue(): def post_queue(): payload = request.get_json() - ret = get_status(self.mav_connection) + ret = get_status(self.mav_connection, self.callback_sys) last_altitude = ret.as_dictionary().get("altitude", 50) wpq = [] @@ -103,7 +109,7 @@ def post_queue(): def post_insert_wp(): payload = request.get_json() - ret: Status = get_status(self.mav_connection) + ret: Status = get_status(self.mav_connection, self.callback_sys) last_altitude = ret._alt if ret != () else 50 curr = max(ret._wpn, 1) @@ -165,7 +171,7 @@ def get_clear_queue(): @app.route("/status", methods=["GET"]) def get_status_handler(): print("Status sent to GCOM") - s = get_status(self.mav_connection).as_dictionary() + s = get_status(self.mav_connection, self.callback_sys).as_dictionary() return s, 200 @app.route("/takeoff", methods=["POST"]) @@ -330,7 +336,7 @@ def deliver_water_down(): deliver_duration_secs = input["deliver_duration_secs"] curr_lat = input["curr_lat"] curr_lon = input["curr_lon"] - wpq = generate_water_wps(self.mav_connection, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) + wpq = generate_water_wps(self.mav_connection, self.callback_sys, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) if new_mission(self.mav_connection, wpq): diff --git a/src_pymav/server/operations/get_info.py b/src_pymav/server/operations/get_info.py index e986c25..c10ab70 100644 --- a/src_pymav/server/operations/get_info.py +++ b/src_pymav/server/operations/get_info.py @@ -5,14 +5,16 @@ from server.common.status import Status from server.common.wpqueue import WaypointQueue, Waypoint from server.common.encoders import command_int_to_string +from server.common.callback import CallbackSystem from server.utilities.request_message_streaming import request_messages + """ Get current status of a drone Type of message can be found on https://mavlink.io/en/messages/common.html """ -def get_status(mav_connection: mavutil.mavfile) -> Status: +def get_status(mav_connection: mavutil.mavfile, callback_sys: CallbackSystem = None) -> Status: # trigger an update # mav_connection.recv_match(blocking=True) @@ -57,6 +59,9 @@ def get_status(mav_connection: mavutil.mavfile) -> Status: winddirection = math.degrees(math.atan(status_wind.wind_x / status_wind.wind_y)) if status_wind.wind_y != 0 else (0 if status_wind.wind_x > 0 else 180) windvelocity = math.sqrt(status_wind.wind_x * status_wind.wind_x + status_wind.wind_y * status_wind.wind_y) + # trigger / callback mechanism + callback_sys.update_and_check(mav_connection.messages) + return Status( system_time.time_unix_usec / 1000000, # seconds From b9ee198b7992cdc2a317457b4f38e42ee281f37d Mon Sep 17 00:00:00 2001 From: ramjayakumar21 Date: Mon, 28 Apr 2025 21:04:40 -0700 Subject: [PATCH 05/20] rewrite aeac water delivery to use water delivery callback --- .DS_Store | Bin 0 -> 6148 bytes api_spec.yml | 3 + src/.DS_Store | Bin 0 -> 6148 bytes .../server/features/aeac_water_delivery.py | 74 ++++++++++++------ src_pymav/server/httpserver.py | 8 +- src_pymav/server/operations/rc_channel_cmd.py | 3 +- 6 files changed, 60 insertions(+), 28 deletions(-) create mode 100644 .DS_Store create mode 100644 src/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..41a5c19576e87dca849ef571f8c74edbebdea593 GIT binary patch literal 6148 zcmeHKJxc>Y5S>j96BH$dg;-rJ7|ah4PB{s7LMl7WhtZI5lBo4rSX)`zS_gYOKmLNC zrIlbK*!yO8)m_inDIznl`|f6Dciz3@c5jJDE$=rgL?t3BqA>>J=)N$HbIVxHdZvMj z=jgU8wMNq0&18y06c7deq5}Nw22`dt?a-e0{Z6h2i@;c_V7K8{_SUonSI^YoGrCZn@FJ``daEOHG> z=zv<(r%fo*@)>bTHpk_3dei7s?=`2)TrR8c@_b&pW`VZwcG}RW4{igGRWSE}oKG1p zr+9wt?4)OP^5$_Fjn4A;1Pe?kPXjP%cs$hI`bWgA-+37xSD$r#JI<|}je7NRL#`+G zGYVkOW-Cq@lwK4N1w?^&1$cc3(HJ9(sX@JTpwmYHU;wu^T*Md~2(@8}Jr56Q60bhZguEooEB)hj}9*+0g5N!jEjq_52dI&nZ9qS9;if7QYAr|rlFtV5$ Rga;-+0$K*?M1fyb;1%NMsAK>D literal 0 HcmV?d00001 diff --git a/api_spec.yml b/api_spec.yml index 9cdfbc7..9a13b56 100644 --- a/api_spec.yml +++ b/api_spec.yml @@ -397,6 +397,9 @@ paths: valve_two_open: type: boolean description: Open/Close valve two + reset: + type: boolean + description: Reset channel back to operator control, if true ignores other parameters components: schemas: Waypoint: diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e53fa6398a636e76c7c2c0b44cb0fc7928250842 GIT binary patch literal 6148 zcmeHKOHRWu5S?jD)v8F89m`yyCm>Ye1YNQS^dq`xl!^ipyYvVw5#k)2fJ49$SaAg2 zc&rkW1_>4jp&4oBCH9Q{^4!KT5%KV9Iw0y3Q3o#A=pt+}=@+k9!?)}L*^M!wGn&wx zW|UU4t>G^!AZypdYNfQK8?4>O`lXBW!Dy0aOQqf2tIy2xBpEOAF)})bC-e0~_H^`B z_VgRs%Xyhagkh;oEBwMP=vrdHlJoWF{(#%8} zSWxpd>@9O>@4r0XD~D2jnZfpqV1D<16$)mifGJ=K{73=p*8b zas15ePblnWM|Vv$nH!5Q{Z1Iz^!DI4Dm>QZ>>C> v?6nd66fP!th2o|J7j7#?u585zaA# int: # Two switches are used on the payload # SWITCH 1 represents the state of valve one and valve two @@ -96,10 +123,13 @@ def set_payload_mode(mav_connection: mavutil.mavfile, valve_one_open: bool, # 2. OFF (-1) - Pump off # value = 1500 + SWITCH 1 * SWITCH 2 - + print(f"Setting payload mode with valve_one_open: {valve_one_open}, " f"valve_two_open: {valve_two_open}, pump_on: {pump_on}") + if reset: + print("PAYLOAD: Resetting channel back to pilot control") + value = 0 if pump_on and valve_one_open and not valve_two_open: print("PAYLOAD: Set to intake water") value = 1500 + 100 * 1 diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 40c201a..22d3963 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -337,8 +337,7 @@ def deliver_water_down(): curr_lat = input["curr_lat"] curr_lon = input["curr_lon"] wpq = generate_water_wps(self.mav_connection, self.callback_sys, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) - - + if new_mission(self.mav_connection, wpq): return f"Commencing Deliver operation", 200 else: @@ -350,14 +349,15 @@ def deliver_water_down(): def change_aeac_payload(): input = request.get_json() - if ("valve_one_open" in input and "valve_two_open" in input and "pump_on" in input): + if ("valve_one_open" in input and "valve_two_open" in input and "pump_on" in input and "reset" in input): # Extract values from JSON input valve_one_open = input["valve_one_open"] valve_two_open = input["valve_two_open"] pump_on = input["pump_on"] + reset = input["reset"] - result = set_payload_mode(self.mav_connection, valve_one_open, valve_two_open, pump_on) + result = set_payload_mode(self.mav_connection, valve_one_open, valve_two_open, pump_on, reset) if result != -1: return f"Payload mode changed", 200 diff --git a/src_pymav/server/operations/rc_channel_cmd.py b/src_pymav/server/operations/rc_channel_cmd.py index 6120da9..f148255 100644 --- a/src_pymav/server/operations/rc_channel_cmd.py +++ b/src_pymav/server/operations/rc_channel_cmd.py @@ -30,7 +30,7 @@ def send_rc_channel_value(mav_connection: mavutil.mavfile, channel: int, value: print(f"Value {value} is out of range. Must be between 0 and 65535, inclusive.") return -1 - channel_values = [0] * 18 + channel_values = [65535] * 18 channel_values[channel - 1] = value print(f"DEBUG: channel_values: {channel_values}") @@ -51,4 +51,3 @@ def send_rc_channel_value(mav_connection: mavutil.mavfile, channel: int, value: return 1 - From 9cda67f53127da234207368ce70c4ae676602e06 Mon Sep 17 00:00:00 2001 From: Hansen Date: Mon, 28 Apr 2025 22:52:12 -0700 Subject: [PATCH 06/20] SITL testing adjustments --- api_spec.yml | 18 +++++++----------- src_pymav/server/common/callback.py | 13 ++++++++++--- src_pymav/server/httpserver.py | 11 ++++++----- src_pymav/server/operations/rc_channel_cmd.py | 19 +++++++------------ 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/api_spec.yml b/api_spec.yml index 9a13b56..9b374ef 100644 --- a/api_spec.yml +++ b/api_spec.yml @@ -356,29 +356,19 @@ paths: schema: type: object properties: - current_alt: - type: number - description: Current altitude of the drone deliver_alt: type: number description: Altitude to lower to for delivery deliver_duration_secs: type: number description: Duration in seconds to stay at the delivery altitude - curr_lat: - type: number - description: Current latitude of the drone - curr_lon: - type: number - description: Current longitude of the drone responses: "200": description: Commencing Deliver operation "400": description: Invalid input, missing a parameter or Mission request failed - tags: - - delivery /aeac_payload: + post: summary: Control AEAC Pump and water payload description: > For AEAC 2025 allows controlling pump, valve 1 and valve 2 @@ -400,6 +390,12 @@ paths: reset: type: boolean description: Reset channel back to operator control, if true ignores other parameters + responses: + "200": + description: Payload mode changed + "400": + description: Invalid input, missing a parameter or Payload mode failed to change + components: schemas: Waypoint: diff --git a/src_pymav/server/common/callback.py b/src_pymav/server/common/callback.py index af7c6d1..20232a6 100644 --- a/src_pymav/server/common/callback.py +++ b/src_pymav/server/common/callback.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from pymavlink import mavutil class Callback(): @@ -5,8 +7,8 @@ def __init__( self, name: str, trigger_message_type: str, - trigger_condition: function = (lambda curr_msg, prev_msg: True), - payload: function = (lambda msg, conn, state: print(msg)), + trigger_condition: Callable = (lambda curr_msg, prev_msg: True), + payload: Callable = (lambda msg, conn, state: print(msg)), only_once: bool = True ): """ @@ -51,6 +53,7 @@ def __init__(self, mav_connection: mavutil.mavfile, state: dict): def update_and_check(self, latest_messages: dict): # traverse through all callbacks and check each of their triggers + print(f"DEBUG: update_and_check called") for callback_type, callback_list in self.callbacks.items(): prev_msg = self.prev_messages.get(callback_type, None) curr_msg = latest_messages.get(callback_type, None) @@ -60,6 +63,7 @@ def update_and_check(self, latest_messages: dict): continue for callback in callback_list: + print(f"DEBUG: testing callback {callback.name}") if callback.trigger_condition(curr_msg, prev_msg): if callback.only_once: @@ -75,7 +79,10 @@ def update_and_check(self, latest_messages: dict): self.prev_messages.update(latest_messages) def register_callback(self, callback: Callback): - self.callbacks[callback.trigger_message_type].append(callback) + print(f"DEBUG: registering {callback.name}") + if callback.trigger_message_type not in self.callbacks.keys(): + self.callbacks[callback.trigger_message_type] = [] + self.callbacks.get(callback.trigger_message_type).append(callback) # def unregister_callback(self, name: str, trigger_message_type: int): # pass \ No newline at end of file diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 22d3963..c86174c 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -327,15 +327,16 @@ def generate_scan_points(): def deliver_water_down(): input = request.get_json() - if ("current_alt" in input and "deliver_alt" in input and - "deliver_duration_secs" in input and "curr_lat" in input and "curr_lon" in input): + if ("deliver_alt" in input and "deliver_duration_secs" in input): + + ret: Status = get_status(self.mav_connection, self.callback_sys) # Extract values from JSON input - current_alt = input["current_alt"] + current_alt = ret._alt deliver_alt = input["deliver_alt"] deliver_duration_secs = input["deliver_duration_secs"] - curr_lat = input["curr_lat"] - curr_lon = input["curr_lon"] + curr_lat = ret._lat + curr_lon = ret._lng wpq = generate_water_wps(self.mav_connection, self.callback_sys, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) if new_mission(self.mav_connection, wpq): diff --git a/src_pymav/server/operations/rc_channel_cmd.py b/src_pymav/server/operations/rc_channel_cmd.py index f148255..b989d88 100644 --- a/src_pymav/server/operations/rc_channel_cmd.py +++ b/src_pymav/server/operations/rc_channel_cmd.py @@ -17,9 +17,10 @@ Normally 1000 ~ 2000, 0 to release channel back to the RC radio, UINT16_MAX (e.g 65535) to ignore this field + NOTE: Parameter RC_OVERRIDE_TIME should be set to -1 (so overrides never expire) """ def send_rc_channel_value(mav_connection: mavutil.mavfile, channel: int, value: int, - tgt_sys_id: int = 1, tgt_comp_id: int = 1, timeout = 10) -> str: + tgt_sys_id: int = 1, tgt_comp_id: int = 1, timeout = 10) -> int: if channel < 1 or channel > 18: print(f"Channel {channel} is out of range. Must be between 1 and 18, inclusive.") return -1 @@ -30,23 +31,17 @@ def send_rc_channel_value(mav_connection: mavutil.mavfile, channel: int, value: print(f"Value {value} is out of range. Must be between 0 and 65535, inclusive.") return -1 - channel_values = [65535] * 18 + channel_values = [0] * 18 channel_values[channel - 1] = value print(f"DEBUG: channel_values: {channel_values}") - mav_connection.mav.command_long_send( - tgt_comp_id, - tgt_sys_id, - mavutil.mavlink.RC_CHANNELS_OVERRIDE, - *channel_values, + result = mav_connection.mav.rc_channels_override_send( + mav_connection.target_system, + mav_connection.target_component, + *channel_values ) - ack = mav_connection.recv_match(type='COMMAND_ACK', blocking=True, timeout=timeout) - if ack is None: - print('No acknowledgment received within the timeout period.') - return -1 - print(f"RC Channel value of {value} sent to channel {channel} successfully.") return 1 From a0b5040dbe896be86fab412d2513dc85c15db5a0 Mon Sep 17 00:00:00 2001 From: Hansen Date: Mon, 28 Apr 2025 23:01:06 -0700 Subject: [PATCH 07/20] camera callback --- src_pymav/server/httpserver.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index c86174c..68479b9 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -18,7 +18,7 @@ from server.common.wpqueue import WaypointQueue, Waypoint from server.common.status import Status from server.common.encoders import command_string_to_int, command_int_to_string -from server.common.callback import CallbackSystem +from server.common.callback import CallbackSystem, Callback class HTTP_Server: @@ -29,6 +29,13 @@ def __init__(self, mav_connection): self.callback_sys = CallbackSystem(self.mav_connection, self.miscellaneous_state) # TODO Handle Camera Protocol via Callbacks? + self.callback_sys.register_callback( + Callback( + "Print all CAMERA_FEEDBACK messages", + 'CAMERA_FEEDBACK', + only_once=False + ) + ) def serve_forever(self, production=True, HOST="localhost", PORT=9000): print("GCOM HTTP Server starting...") From 3fea3cc08ef37a2aa29817667c9a7e239d638161 Mon Sep 17 00:00:00 2001 From: ramjayakumar21 Date: Sat, 3 May 2025 12:13:31 -0700 Subject: [PATCH 08/20] add cam functionality --- src_pymav/server/features/aeac_scan.py | 32 +++++++++++++- src_pymav/server/httpserver.py | 58 ++++++++++++++++++++++++-- src_pymav/server/operations/camera.py | 48 +++++++++++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 src_pymav/server/operations/camera.py diff --git a/src_pymav/server/features/aeac_scan.py b/src_pymav/server/features/aeac_scan.py index 70a91ce..06d20ab 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -3,6 +3,9 @@ from server.common.conversion import * from server.common.wpqueue import Waypoint, WaypointQueue +from server.common.callback import CallbackSystem, Callback + +from server.operations.camera import activate_camera, deactivate_camera # ALL UNITS IN METERS UNLESS SPECIFIED SPLINE_WAYPOINT_TYPE = "SPLINE_WAYPOINT" @@ -38,7 +41,7 @@ def plot_shape(points, color, close_loop=False, scatter=True) -> None: next = points[(i + 1) % len(points)] plt.plot([curr[0], next[0]], [curr[1], next[1]], color=color, alpha=0.7, linewidth=1, zorder=2) -def scan_area(center_lat, center_lng, altitude, target_area_radius) -> WaypointQueue: +def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, target_area_radius) -> WaypointQueue: wpq = WaypointQueue() center_we, center_sn = convert_gps_to_utm(center_lat, center_lng) zone = convert_gps_to_utm_zone(center_lng) @@ -56,6 +59,20 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius) -> WaypointQ record.append((center_we, center_sn)) # # wpq.append((0, "", center_lat, center_lng, altitude)) + callback_sys.register_callback(Callback( + "Scan Mission - Start Camera", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq == 1), + lambda msg, conn, state: activate_camera( + mav_connection=mav_connection, + cam_id=0, + time_between_pics_secs=0.5, + num_of_pics=0 + ) + ), + only_once=True + ) + # transit from center to edge, turning gently so that drone is tangent when reaching the edge tmp_lat, tmp_lng = convert_utm_to_gps(center_we + target_area_radius / 2, center_sn - target_area_radius / 2, zone, hemisphere) record.append((center_we + target_area_radius / 2, center_sn - target_area_radius / 2)) @@ -66,6 +83,18 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius) -> WaypointQ record.append((center_we + target_area_radius, center_sn)) wpq.push(Waypoint(count, "", tmp_lat, tmp_lng, altitude)) count += 1 + + callback_sys.register_callback(Callback( + "Scan Mission - Stop Camera", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq < count - 1), + lambda msg, conn, state: deactivate_camera( + mav_connection=mav_connection, + cam_id=0, + ) + ), + only_once=True + ) # generate spiral decrease_per_radian = 0.75 * (scan_radius) / (2 * math.pi) @@ -100,5 +129,6 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius) -> WaypointQ # TODO handle deadzone + if __name__ == '__main__': scan_area(0,0,100, 100) \ No newline at end of file diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 68479b9..20bd732 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -9,6 +9,7 @@ from server.operations.get_info import get_status, get_current_mission from server.operations.change_modes import change_flight_mode from server.operations.land import land_in_place, land_at_position +from server.operations.camera import activate_camera, deactivate_camera from server.features.aeac_scan import scan_area from server.features.aeac_water_delivery import generate_water_wps, set_payload_mode @@ -38,7 +39,7 @@ def __init__(self, mav_connection): ) def serve_forever(self, production=True, HOST="localhost", PORT=9000): - print("GCOM HTTP Server starting...") + print("GCOM HTTP Server running...") app = Flask(__name__) socketio = SocketIO(app) @@ -312,6 +313,44 @@ def put_flight_mode(): return f"OK! Changed mode: {input['mode']}", 200 else: return f"Unrecognized mode: {input['mode']}", 400 + + @app.route("/activate_camera", methods=["POST"]) + def activate_cam(): + response: dict = request.get_json() + + if ("cam_id" not in response + or "time_between_pics_secs" not in response + or "num_of_pics" not in response + ): + return "Missing params", 400 + + cam_id = response["cam_id"] + time_between_pics_secs = response["time_between_pics_secs"] + num_of_pics = response["num_of_pics"] + + if activate_camera(mav_connection=self.mav_connection, cam_id=cam_id, + time_between_pics_secs=time_between_pics_secs, + num_of_pics=num_of_pics): + return "Activated Camera", 200 + else: + return "Failed to Activate Camera", 400 + + @app.route("/deactivate_camera", methods=["POST"]) + def deactivate_cam(): + response: dict = request.get_json() + + if ("cam_id" not in response): + return "Missing params", 400 + cam_id = response["cam_id"] + + if deactivate_camera(mav_connection=self.mav_connection, cam_id=cam_id) + return "Deactivated Camera", 200 + else: + return "Failed to Deactivate Camera", 400 + + @app.route("/flightmode", methods=["PUT"]) + + ### AEAC 2025 COMMANDS ### @app.route("/aeac_scan", methods=["POST"]) def generate_scan_points(): @@ -320,8 +359,21 @@ def generate_scan_points(): # TODO Trigger CameraVision system to begin scanning if (input["center_lat"] and input["center_lng"] and input["altitude"] and input["target_area_radius"]): - wpq = scan_area(center_lat=input["center_lat"], center_lng=input["center_lng"], - altitude=input["altitude"], target_area_radius=input["target_area_radius"]) + + center_lat = input["center_lat"] + center_lon = input["center_lon"] + altitude = input["altitude"] + target_area_radius = input["target_area_radius"] + + ret: Status = get_status(self.mav_connection, self.callback_sys) + + # If given lat lon is 0, then base spiral off of current lat lon + if (center_lat == 0 and center_lon == 0): + center_lat = ret._lat + center_lon = ret._lon + + wpq = scan_area(self.mav_connection, self.callback_sys, + center_lat, center_lon, altitude, target_area_radius) if new_mission(self.mav_connection, wpq): return f"Scan Mission Set", 200 diff --git a/src_pymav/server/operations/camera.py b/src_pymav/server/operations/camera.py new file mode 100644 index 0000000..ad81b81 --- /dev/null +++ b/src_pymav/server/operations/camera.py @@ -0,0 +1,48 @@ +from pymavlink.mavutil import mavfile, mavlink + + +""" + +cam_id: 0 is all cameras, 1 is camera 1, 2 is camera 2 +num_of_pics: 0 is unlimited pictures, else limit to num_of_pics +""" + +def activate_camera(mav_connection: mavfile, cam_id: int = 0, time_between_pics_secs: float = 1.0, num_of_pics: int = 0, + timeout: int = 5, tgt_sys_id: int = 1, tgt_comp_id: int = 1) -> int: + + mav_connection.mav.command_long_send( + tgt_sys_id, + tgt_comp_id, + mavfile.mavlink.MAV_CMD_IMAGE_START_CAPTURE, + cam_id, time_between_pics_secs, num_of_pics + ) + + # Wait for the acknowledgment + ack = mav_connection.recv_match(type='COMMAND_ACK', blocking=True, timeout=timeout) + if ack is None: + print('No acknowledgment received within the timeout period.') + return -1 + + print(f"ACTIVATE camera ack: {ack}") + + return ack.result + +def deactivate_camera(mav_connection: mavfile, tgt_sys_id: int = 1, tgt_comp_id: int = 1, + cam_id: int = 0, timeout: int = 5) -> int: + + mav_connection.mav.command_long_send( + tgt_sys_id, + tgt_comp_id, + mavfile.mavlink.MAV_CMD_IMAGE_STOP_CAPTURE, + cam_id + ) + + # Wait for the acknowledgment + ack = mav_connection.recv_match(type='COMMAND_ACK', blocking=True, timeout=timeout) + if ack is None: + print('No acknowledgment received within the timeout period.') + return -1 + + print(f"DEACTIVATE camera ack: {ack}") + + return ack.result \ No newline at end of file From d0edfa2fe8944a79ad3ff18e9305d7049b8e0707 Mon Sep 17 00:00:00 2001 From: Hansen Date: Sat, 3 May 2025 15:16:25 -0700 Subject: [PATCH 09/20] updated README --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ec04aec..0fe8c93 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ mavproxy --master=tcp:127.0.0.1:5760 --out=udp:127.0.0.1:14550 --out=udp:127.0.0 > If using WSL2, get the IP of host machine using `ip route show default` > If using Windows, make sure to run mavproxy as an Administrator. -When running mavproxy, point master to the SITL instance connection and specify 2 outputs, one for connecting with Mission Planner for visualization and one to interface with pymavlink. +When running mavproxy, point master to the SITL instance connection and specify 2 outputs, one for connecting with Mission Planner for visualization and one to interface with pymavlink. If connecting to an actual drone, set the `--baudrate=xxx` option as well. ### Using MissionPlanner-Scripts @@ -73,11 +73,14 @@ When running mavproxy, point master to the SITL instance connection and specify 2. Launch the application: ```c - poetry run python src/main.py + poetry run python src_pymav/main.py ``` The server will listen on the specified port (default 9000) for HTTP requests. +> [!IMPORTANT] +> Parameter RC_OVERRIDE_TIME should be set to -1 (so overrides never expire) + ### Visualization Connect Mission Planner to one of output of mavproxy. From 14bb7dd5ebc4fc87c3bcbdeb8970d6ebb7b2143e Mon Sep 17 00:00:00 2001 From: Ram Jayakumar Date: Sun, 4 May 2025 12:49:44 -0700 Subject: [PATCH 10/20] computer changes --- postman_collection.json | 147 ++++++++++++++++++++++++- src_pymav/main.py | 2 +- src_pymav/server/features/aeac_scan.py | 4 +- src_pymav/server/httpserver.py | 11 +- src_pymav/server/operations/camera.py | 15 +-- 5 files changed, 161 insertions(+), 18 deletions(-) diff --git a/postman_collection.json b/postman_collection.json index 932eb06..77c1b65 100644 --- a/postman_collection.json +++ b/postman_collection.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "20ebd461-c68a-4ab5-99d5-5a4b03c40827", + "_postman_id": "69e0a2f4-ba23-4182-91c7-ea3290f0fd81", "name": "MissionPlannerScripts", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "30667343" + "_exporter_id": "26491029" }, "item": [ { @@ -428,6 +428,149 @@ ], "description": "Access to flight options." }, + { + "name": "aeac", + "item": [ + { + "name": "start_cam", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"cam_id\": 0,\r\n \"time_between_pics_secs\": 0.5,\r\n \"num_of_pics\": 0\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/activate_camera", + "host": [ + "{{base_url}}" + ], + "path": [ + "activate_camera" + ] + } + }, + "response": [] + }, + { + "name": "stop_cam", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"cam_id\": 0\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/deactivate_camera", + "host": [ + "{{base_url}}" + ], + "path": [ + "deactivate_camera" + ] + } + }, + "response": [] + }, + { + "name": "aeac_scan", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"center_lat\": 0,\r\n \"center_lon\": 0,\r\n \"altitude\": 50,\r\n \"target_area_radius\": 110\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/aeac_scan", + "host": [ + "{{base_url}}" + ], + "path": [ + "aeac_scan" + ] + } + }, + "response": [] + }, + { + "name": "aeac_deliver", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"deliver_alt\": 1,\r\n \"deliver_duration_secs\": 20\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/aeac_deliver", + "host": [ + "{{base_url}}" + ], + "path": [ + "aeac_deliver" + ] + } + }, + "response": [] + }, + { + "name": "aeac_payload_OFF", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"valve_one_open\": false,\r\n \"valve_two_open\": false,\r\n \"pump_on\": false,\r\n \"reset\": false\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{base_url}}/aeac_payload", + "host": [ + "{{base_url}}" + ], + "path": [ + "aeac_payload" + ] + } + }, + "response": [] + }, + { + "name": "New Request", + "request": { + "method": "GET", + "header": [] + }, + "response": [] + } + ] + }, { "name": "Test Endpoint", "request": { diff --git a/src_pymav/main.py b/src_pymav/main.py index 389936c..45e68c0 100644 --- a/src_pymav/main.py +++ b/src_pymav/main.py @@ -11,7 +11,7 @@ production = True HOST, PORT, SOCKET_PORT = "localhost", 9000, 9001 STATUS_HOST, STATUS_PORT = "localhost", 1323 -DISABLE_STATUS = False +DISABLE_STATUS = True MAVLINK_CONNECTION_STRING = 'udpin:localhost:14551' if __name__ == "__main__": diff --git a/src_pymav/server/features/aeac_scan.py b/src_pymav/server/features/aeac_scan.py index 06d20ab..d6fbdd1 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -70,7 +70,7 @@ def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, ta num_of_pics=0 ) ), - only_once=True + True ) # transit from center to edge, turning gently so that drone is tangent when reaching the edge @@ -93,7 +93,7 @@ def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, ta cam_id=0, ) ), - only_once=True + True ) # generate spiral diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 20bd732..8a3b01e 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -343,22 +343,21 @@ def deactivate_cam(): return "Missing params", 400 cam_id = response["cam_id"] - if deactivate_camera(mav_connection=self.mav_connection, cam_id=cam_id) + if deactivate_camera(mav_connection=self.mav_connection, cam_id=cam_id): return "Deactivated Camera", 200 else: return "Failed to Deactivate Camera", 400 - - @app.route("/flightmode", methods=["PUT"]) ### AEAC 2025 COMMANDS ### @app.route("/aeac_scan", methods=["POST"]) def generate_scan_points(): input = request.get_json() + print(input) # TODO Trigger CameraVision system to begin scanning - if (input["center_lat"] and input["center_lng"] and - input["altitude"] and input["target_area_radius"]): + if ("center_lat" in input and"center_lon" in input and + "altitude" in input and "target_area_radius" in input): center_lat = input["center_lat"] center_lon = input["center_lon"] @@ -370,7 +369,7 @@ def generate_scan_points(): # If given lat lon is 0, then base spiral off of current lat lon if (center_lat == 0 and center_lon == 0): center_lat = ret._lat - center_lon = ret._lon + center_lon = ret._lng wpq = scan_area(self.mav_connection, self.callback_sys, center_lat, center_lon, altitude, target_area_radius) diff --git a/src_pymav/server/operations/camera.py b/src_pymav/server/operations/camera.py index ad81b81..af8f046 100644 --- a/src_pymav/server/operations/camera.py +++ b/src_pymav/server/operations/camera.py @@ -1,6 +1,5 @@ from pymavlink.mavutil import mavfile, mavlink - """ cam_id: 0 is all cameras, 1 is camera 1, 2 is camera 2 @@ -8,13 +7,14 @@ """ def activate_camera(mav_connection: mavfile, cam_id: int = 0, time_between_pics_secs: float = 1.0, num_of_pics: int = 0, - timeout: int = 5, tgt_sys_id: int = 1, tgt_comp_id: int = 1) -> int: + timeout: int = 5, tgt_sys_id: int = 0, tgt_comp_id: int = 0) -> int: mav_connection.mav.command_long_send( tgt_sys_id, tgt_comp_id, - mavfile.mavlink.MAV_CMD_IMAGE_START_CAPTURE, - cam_id, time_between_pics_secs, num_of_pics + mavlink.MAV_CMD_IMAGE_START_CAPTURE, + 0, cam_id, time_between_pics_secs, num_of_pics, + 0, 0, 0, 0 ) # Wait for the acknowledgment @@ -27,14 +27,15 @@ def activate_camera(mav_connection: mavfile, cam_id: int = 0, time_between_pics_ return ack.result -def deactivate_camera(mav_connection: mavfile, tgt_sys_id: int = 1, tgt_comp_id: int = 1, +def deactivate_camera(mav_connection: mavfile, tgt_sys_id: int = 0, tgt_comp_id: int = 0, cam_id: int = 0, timeout: int = 5) -> int: mav_connection.mav.command_long_send( tgt_sys_id, tgt_comp_id, - mavfile.mavlink.MAV_CMD_IMAGE_STOP_CAPTURE, - cam_id + mavlink.MAV_CMD_IMAGE_STOP_CAPTURE, + 0, cam_id, + 0, 0, 0, 0, 0, 0 ) # Wait for the acknowledgment From ff56105565adae49efebe828d50d5417ff440a7d Mon Sep 17 00:00:00 2001 From: ramjayakumar21 Date: Mon, 5 May 2025 09:57:59 -0700 Subject: [PATCH 11/20] add toggle to enable cam --- src_pymav/server/features/aeac_scan.py | 53 ++++++++++++++------------ src_pymav/server/httpserver.py | 6 ++- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src_pymav/server/features/aeac_scan.py b/src_pymav/server/features/aeac_scan.py index d6fbdd1..0b2b0c1 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -41,7 +41,7 @@ def plot_shape(points, color, close_loop=False, scatter=True) -> None: next = points[(i + 1) % len(points)] plt.plot([curr[0], next[0]], [curr[1], next[1]], color=color, alpha=0.7, linewidth=1, zorder=2) -def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, target_area_radius) -> WaypointQueue: +def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, target_area_radius, enable_cam) -> WaypointQueue: wpq = WaypointQueue() center_we, center_sn = convert_gps_to_utm(center_lat, center_lng) zone = convert_gps_to_utm_zone(center_lng) @@ -59,19 +59,21 @@ def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, ta record.append((center_we, center_sn)) # # wpq.append((0, "", center_lat, center_lng, altitude)) - callback_sys.register_callback(Callback( - "Scan Mission - Start Camera", - 'MISSION_CURRENT', - lambda curr_msg, prev_msg: (curr_msg.seq == 1), - lambda msg, conn, state: activate_camera( - mav_connection=mav_connection, - cam_id=0, - time_between_pics_secs=0.5, - num_of_pics=0 - ) - ), - True - ) + + if (enable_cam): + callback_sys.register_callback(Callback( + "Scan Mission - Start Camera", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq == 1), + lambda msg, conn, state: activate_camera( + mav_connection=mav_connection, + cam_id=0, + time_between_pics_secs=0.5, + num_of_pics=0 + ) + ), + True + ) # transit from center to edge, turning gently so that drone is tangent when reaching the edge tmp_lat, tmp_lng = convert_utm_to_gps(center_we + target_area_radius / 2, center_sn - target_area_radius / 2, zone, hemisphere) @@ -84,17 +86,18 @@ def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, ta wpq.push(Waypoint(count, "", tmp_lat, tmp_lng, altitude)) count += 1 - callback_sys.register_callback(Callback( - "Scan Mission - Stop Camera", - 'MISSION_CURRENT', - lambda curr_msg, prev_msg: (curr_msg.seq < count - 1), - lambda msg, conn, state: deactivate_camera( - mav_connection=mav_connection, - cam_id=0, - ) - ), - True - ) + if (enable_cam): + callback_sys.register_callback(Callback( + "Scan Mission - Stop Camera", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq < count - 1), + lambda msg, conn, state: deactivate_camera( + mav_connection=mav_connection, + cam_id=0, + ) + ), + True + ) # generate spiral decrease_per_radian = 0.75 * (scan_radius) / (2 * math.pi) diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 8a3b01e..a2d061b 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -357,12 +357,14 @@ def generate_scan_points(): # TODO Trigger CameraVision system to begin scanning if ("center_lat" in input and"center_lon" in input and - "altitude" in input and "target_area_radius" in input): + "altitude" in input and "target_area_radius" in input and + "enable_camera" in input): center_lat = input["center_lat"] center_lon = input["center_lon"] altitude = input["altitude"] target_area_radius = input["target_area_radius"] + enable_camera = input["enable_camera"] ret: Status = get_status(self.mav_connection, self.callback_sys) @@ -372,7 +374,7 @@ def generate_scan_points(): center_lon = ret._lng wpq = scan_area(self.mav_connection, self.callback_sys, - center_lat, center_lon, altitude, target_area_radius) + center_lat, center_lon, altitude, target_area_radius, enable_camera) if new_mission(self.mav_connection, wpq): return f"Scan Mission Set", 200 From c2807fc4fdef81f42ead37a9d55a3168cffdf85b Mon Sep 17 00:00:00 2001 From: Hansen Date: Mon, 5 May 2025 13:34:01 -0700 Subject: [PATCH 12/20] fix callbacks --- src_pymav/server/features/aeac_scan.py | 10 ++++------ src_pymav/server/httpserver.py | 14 +++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src_pymav/server/features/aeac_scan.py b/src_pymav/server/features/aeac_scan.py index 0b2b0c1..fcc4c9d 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -70,10 +70,9 @@ def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, ta cam_id=0, time_between_pics_secs=0.5, num_of_pics=0 - ) ), - True - ) + only_once=True + )) # transit from center to edge, turning gently so that drone is tangent when reaching the edge tmp_lat, tmp_lng = convert_utm_to_gps(center_we + target_area_radius / 2, center_sn - target_area_radius / 2, zone, hemisphere) @@ -94,10 +93,9 @@ def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, ta lambda msg, conn, state: deactivate_camera( mav_connection=mav_connection, cam_id=0, - ) ), - True - ) + only_once=True + )) # generate spiral decrease_per_radian = 0.75 * (scan_radius) / (2 * math.pi) diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index a2d061b..39907c7 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -356,12 +356,12 @@ def generate_scan_points(): print(input) # TODO Trigger CameraVision system to begin scanning - if ("center_lat" in input and"center_lon" in input and + if ("center_lat" in input and"center_lng" in input and "altitude" in input and "target_area_radius" in input and "enable_camera" in input): center_lat = input["center_lat"] - center_lon = input["center_lon"] + center_lng = input["center_lng"] altitude = input["altitude"] target_area_radius = input["target_area_radius"] enable_camera = input["enable_camera"] @@ -369,12 +369,12 @@ def generate_scan_points(): ret: Status = get_status(self.mav_connection, self.callback_sys) # If given lat lon is 0, then base spiral off of current lat lon - if (center_lat == 0 and center_lon == 0): + if (center_lat == 0 and center_lng == 0): center_lat = ret._lat - center_lon = ret._lng + center_lng = ret._lng wpq = scan_area(self.mav_connection, self.callback_sys, - center_lat, center_lon, altitude, target_area_radius, enable_camera) + center_lat, center_lng, altitude, target_area_radius, enable_camera) if new_mission(self.mav_connection, wpq): return f"Scan Mission Set", 200 @@ -396,8 +396,8 @@ def deliver_water_down(): deliver_alt = input["deliver_alt"] deliver_duration_secs = input["deliver_duration_secs"] curr_lat = ret._lat - curr_lon = ret._lng - wpq = generate_water_wps(self.mav_connection, self.callback_sys, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) + curr_lng = ret._lng + wpq = generate_water_wps(self.mav_connection, self.callback_sys, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lng) if new_mission(self.mav_connection, wpq): return f"Commencing Deliver operation", 200 From bc27078d71c258866fa5beefbfacfb255f54822d Mon Sep 17 00:00:00 2001 From: Hansen Date: Mon, 5 May 2025 17:06:21 -0700 Subject: [PATCH 13/20] correctly test all callbacks even if one is removed --- src_pymav/server/common/callback.py | 47 ++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src_pymav/server/common/callback.py b/src_pymav/server/common/callback.py index 20232a6..0074f85 100644 --- a/src_pymav/server/common/callback.py +++ b/src_pymav/server/common/callback.py @@ -50,10 +50,31 @@ def __init__(self, mav_connection: mavutil.mavfile, state: dict): self.callbacks: dict[str, list[Callback]] = {} self.prev_messages: dict[str, object] = {} + + def __check_exec_indicate_removal(self, callback: Callback, curr_msg, prev_msg) -> bool: + """ + Checks the callback's trigger condition and executes if it passes. + Returns a boolean that indicates if the callback should be removed. + """ + print(f"DEBUG: testing callback {callback.name}") + + keep = True + + if callback.trigger_condition(curr_msg, prev_msg): + + if callback.only_once: + # remove callback before firing it + keep = False + + # fire callback + callback.payload(curr_msg, self.conn, self.state) + + print(f"DEBUG: Fired Callback {callback.name}") + + return keep def update_and_check(self, latest_messages: dict): # traverse through all callbacks and check each of their triggers - print(f"DEBUG: update_and_check called") for callback_type, callback_list in self.callbacks.items(): prev_msg = self.prev_messages.get(callback_type, None) curr_msg = latest_messages.get(callback_type, None) @@ -62,18 +83,11 @@ def update_and_check(self, latest_messages: dict): # no point testing continue - for callback in callback_list: - print(f"DEBUG: testing callback {callback.name}") - if callback.trigger_condition(curr_msg, prev_msg): - - if callback.only_once: - # remove callback before firing it - self.callbacks[callback_type].remove(callback) - - # fire callback - callback.payload(curr_msg, self.conn, self.state) - - print(f"DEBUG: Fired Callback {callback.name}") + # check every callback in the list, filtering out those that get removed + self.callbacks[callback_type] = list(filter( + lambda callback: self.__check_exec_indicate_removal(callback, curr_msg, prev_msg), + callback_list + )) # update prev_messages self.prev_messages.update(latest_messages) @@ -84,5 +98,8 @@ def register_callback(self, callback: Callback): self.callbacks[callback.trigger_message_type] = [] self.callbacks.get(callback.trigger_message_type).append(callback) - # def unregister_callback(self, name: str, trigger_message_type: int): - # pass \ No newline at end of file + def deregister_callback(self, name: str): + self.deregister_callback_by_condition(lambda callback: (name == callback.name)) + + def deregister_callback_by_condition(self, condition: Callable = (lambda callback: False)): + pass From b09c232abcf2bbb3a2f133973d9362e61a28d5e0 Mon Sep 17 00:00:00 2001 From: Hansen Date: Mon, 5 May 2025 18:49:49 -0700 Subject: [PATCH 14/20] complete callback removal system --- src_pymav/server/common/callback.py | 51 ++++++++++++++++--- src_pymav/server/features/aeac_scan.py | 24 ++++++--- .../server/features/aeac_water_delivery.py | 21 +++++--- src_pymav/server/httpserver.py | 21 ++++---- src_pymav/server/operations/get_info.py | 3 +- src_pymav/server/operations/queue.py | 8 ++- 6 files changed, 95 insertions(+), 33 deletions(-) diff --git a/src_pymav/server/common/callback.py b/src_pymav/server/common/callback.py index 0074f85..a4dd88d 100644 --- a/src_pymav/server/common/callback.py +++ b/src_pymav/server/common/callback.py @@ -9,7 +9,11 @@ def __init__( trigger_message_type: str, trigger_condition: Callable = (lambda curr_msg, prev_msg: True), payload: Callable = (lambda msg, conn, state: print(msg)), - only_once: bool = True + removable_flags: dict[str, bool] = { + "on_payload_fired": True, + "on_mission_switched": True, + "on_deregister_called": True, + } ): """ A callback instance. Callbacks are composed of two components - the trigger, which controls @@ -33,15 +37,30 @@ def __init__( a state dictionary owned by the MPS server instance itself (for passing information back to the wider server context.) If unspecified, a default function will be used that simply prints the message. - `only_once`: indicates that the callback should only execute once, i.e. it will be removed - after being triggered for the first and only time. + `removable`: a dictionary with boolean flags indicating the conditions under which the + callback can be deleted/removed/deregistered. + `on_payload_fired`: indicates that the callback will be removed after being triggered + for the first and only time, i.e., it will only execute once. + `on_mission_switched`: indicates that the callback will be removed when the mission + is switched/updated, i.e., the callback is mission-specific and it would be + invalid for that callback to persist if the mission was switched before it was + executed. + `on_deregister_called`: indicates that the callback can be removed through the + Callback System's deregistration functions. + A Callback with all three of these flags set to `False` will be effectively permanent. """ self.name = name self.trigger_message_type = trigger_message_type self.trigger_condition = trigger_condition self.payload = payload - self.only_once = only_once + + self.removable_flags = { + "on_payload_fired": True, + "on_mission_switched": True, + "on_deregister_called": True, + } + self.removable_flags.update(removable_flags) class CallbackSystem(): def __init__(self, mav_connection: mavutil.mavfile, state: dict): @@ -62,7 +81,7 @@ def __check_exec_indicate_removal(self, callback: Callback, curr_msg, prev_msg) if callback.trigger_condition(curr_msg, prev_msg): - if callback.only_once: + if callback.removable_flags.get("on_payload_fired"): # remove callback before firing it keep = False @@ -102,4 +121,24 @@ def deregister_callback(self, name: str): self.deregister_callback_by_condition(lambda callback: (name == callback.name)) def deregister_callback_by_condition(self, condition: Callable = (lambda callback: False)): - pass + for callback_type, callback_list in self.callbacks.items(): + # filter the list of callbacks, keeping only those who cannot be deregistered directly + # through methods and those for whom the condition evaluates to False. + self.callbacks[callback_type] = list(filter( + lambda callback: (callback.removable_flags.get("on_deregister_called") and not condition(callback)), + callback_list + )) + + def mission_switched(self): + """ + The mission switched, so deregister all callbacks for which `self.removable_flags["on_mission_switched"]` is `True`. + """ + + print("DEBUG: Clearing Callbacks on Mission Switch") + + for callback_type, callback_list in self.callbacks.items(): + # filter out all callbacks that are set to expire on mission switch + self.callbacks[callback_type] = list(filter( + lambda callback: not callback.removable_flags.get("on_mission_switched"), + callback_list + )) \ No newline at end of file diff --git a/src_pymav/server/features/aeac_scan.py b/src_pymav/server/features/aeac_scan.py index fcc4c9d..eaae9e6 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -41,8 +41,10 @@ def plot_shape(points, color, close_loop=False, scatter=True) -> None: next = points[(i + 1) % len(points)] plt.plot([curr[0], next[0]], [curr[1], next[1]], color=color, alpha=0.7, linewidth=1, zorder=2) -def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, target_area_radius, enable_cam) -> WaypointQueue: +def scan_area(center_lat, center_lng, altitude, target_area_radius, enable_cam) -> tuple[WaypointQueue, list[Callback]]: wpq = WaypointQueue() + callbacks = [] + center_we, center_sn = convert_gps_to_utm(center_lat, center_lng) zone = convert_gps_to_utm_zone(center_lng) hemisphere = 1 # +1 for North, -1 for South @@ -61,17 +63,20 @@ def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, ta if (enable_cam): - callback_sys.register_callback(Callback( + callbacks.append(Callback( "Scan Mission - Start Camera", 'MISSION_CURRENT', lambda curr_msg, prev_msg: (curr_msg.seq == 1), lambda msg, conn, state: activate_camera( - mav_connection=mav_connection, + mav_connection=conn, cam_id=0, time_between_pics_secs=0.5, num_of_pics=0 ), - only_once=True + removable_flags = { + "on_payload_fired": True, + "on_mission_switched": True, + } )) # transit from center to edge, turning gently so that drone is tangent when reaching the edge @@ -86,15 +91,18 @@ def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, ta count += 1 if (enable_cam): - callback_sys.register_callback(Callback( + callbacks.append(Callback( "Scan Mission - Stop Camera", 'MISSION_CURRENT', lambda curr_msg, prev_msg: (curr_msg.seq < count - 1), lambda msg, conn, state: deactivate_camera( - mav_connection=mav_connection, + mav_connection=conn, cam_id=0, ), - only_once=True + removable_flags = { + "on_payload_fired": True, + "on_mission_switched": True, + } )) # generate spiral @@ -126,7 +134,7 @@ def scan_area(mav_connection, callback_sys, center_lat, center_lng, altitude, ta # ax.set_aspect('equal', adjustable='box') # plt.show() - return wpq + return wpq, callbacks # TODO handle deadzone diff --git a/src_pymav/server/features/aeac_water_delivery.py b/src_pymav/server/features/aeac_water_delivery.py index bac7bb2..4a82e46 100644 --- a/src_pymav/server/features/aeac_water_delivery.py +++ b/src_pymav/server/features/aeac_water_delivery.py @@ -14,15 +14,14 @@ 3. Return to previous location altitude ''' def generate_water_wps( - mav_connection: mavutil.mavfile, - callback_sys: CallbackSystem, current_alt: float, deliver_alt: float, deliver_duration_secs: int, curr_lat: float, curr_lon: float, -) -> WaypointQueue: +) -> tuple[WaypointQueue, list[Callback]]: landing_mission = WaypointQueue() + callbacks = [] # Set the current altitude to the current location wp_1 = Waypoint( @@ -42,7 +41,7 @@ def generate_water_wps( ) # Add a callback to set the payload mode to release water after getting to loiter wp - callback_sys.register_callback(Callback( + callbacks.append(Callback( "Water Delivery - Release water", 'MISSION_CURRENT', lambda curr_msg, prev_msg: (curr_msg.seq == 3), @@ -53,7 +52,10 @@ def generate_water_wps( pump_on=False, reset=False ), - only_once=True + removable_flags = { + "on_payload_fired": True, + "on_mission_switched": True, + } )) wp_3 = Waypoint( @@ -76,7 +78,7 @@ def generate_water_wps( ) # Add a callback to set the payload mode to stop releasing water after getting to loiter wp - callback_sys.register_callback(Callback( + callbacks.append(Callback( "Water Delivery - Stop releasing water", 'MISSION_CURRENT', lambda curr_msg, prev_msg: (curr_msg.seq == 5), @@ -87,7 +89,10 @@ def generate_water_wps( pump_on=False, reset=False ), - only_once=True + removable_flags = { + "on_payload_fired": True, + "on_mission_switched": True, + } )) wp_5 = Waypoint( @@ -105,7 +110,7 @@ def generate_water_wps( landing_mission.push(wp_4) landing_mission.push(wp_5) - return landing_mission + return landing_mission, callbacks def set_payload_mode(mav_connection: mavutil.mavfile, valve_one_open: bool, valve_two_open: bool, pump_on: bool, reset: bool = False) -> int: diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 39907c7..8ddc6fa 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -34,7 +34,11 @@ def __init__(self, mav_connection): Callback( "Print all CAMERA_FEEDBACK messages", 'CAMERA_FEEDBACK', - only_once=False + removable_flags={ + "on_payload_fired": False, + "on_mission_switched": False, + "on_deregister_called": True, + } ) ) @@ -104,7 +108,7 @@ def post_queue(): ) wpq.append(wp) - success = new_mission(self.mav_connection, WaypointQueue(wpq.copy())) + success = new_mission(self.mav_connection, self.callback_sys, WaypointQueue(wpq.copy())) copy = WaypointQueue(wpq.copy()).aslist() wpq.clear() @@ -158,7 +162,7 @@ def post_insert_wp(): # start list with new waypoints, extend with current mission at the end new_waypoints.extend(curr_wpq.aslist()[curr:]) - success = new_mission(self.mav_connection, WaypointQueue(new_waypoints.copy())) + success = new_mission(self.mav_connection, self.callback_sys, WaypointQueue(new_waypoints.copy())) copy = WaypointQueue(new_waypoints.copy()).aslist() new_waypoints.clear() @@ -277,7 +281,7 @@ def post_land(): landing_mission.push(Waypoint(0, "Approach", land.get('latitude'), land.get('longitude'), land.get('altitude', 35))) landing_mission.push(Waypoint(1, "Landing", land.get('latitude'), land.get('longitude'), 0, "LAND")) - if new_mission(self.mav_connection, landing_mission): + if new_mission(self.mav_connection, self.callback_sys, landing_mission): return "Landing at Specified Location", 200 else: return "Landing failed", 400 @@ -373,10 +377,9 @@ def generate_scan_points(): center_lat = ret._lat center_lng = ret._lng - wpq = scan_area(self.mav_connection, self.callback_sys, - center_lat, center_lng, altitude, target_area_radius, enable_camera) + wpq, callbacks = scan_area(center_lat, center_lng, altitude, target_area_radius, enable_camera) - if new_mission(self.mav_connection, wpq): + if new_mission(self.mav_connection, self.callback_sys, wpq, callbacks): return f"Scan Mission Set", 200 else: return "Mission request failed", 400 @@ -397,9 +400,9 @@ def deliver_water_down(): deliver_duration_secs = input["deliver_duration_secs"] curr_lat = ret._lat curr_lng = ret._lng - wpq = generate_water_wps(self.mav_connection, self.callback_sys, current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lng) + wpq, callbacks = generate_water_wps(current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lng) - if new_mission(self.mav_connection, wpq): + if new_mission(self.mav_connection, self.callback_sys, wpq, callbacks): return f"Commencing Deliver operation", 200 else: return "Mission request failed", 400 diff --git a/src_pymav/server/operations/get_info.py b/src_pymav/server/operations/get_info.py index c10ab70..6ec8049 100644 --- a/src_pymav/server/operations/get_info.py +++ b/src_pymav/server/operations/get_info.py @@ -60,7 +60,8 @@ def get_status(mav_connection: mavutil.mavfile, callback_sys: CallbackSystem = N windvelocity = math.sqrt(status_wind.wind_x * status_wind.wind_x + status_wind.wind_y * status_wind.wind_y) # trigger / callback mechanism - callback_sys.update_and_check(mav_connection.messages) + if callback_sys is not None: + callback_sys.update_and_check(mav_connection.messages) return Status( system_time.time_unix_usec / 1000000, # seconds diff --git a/src_pymav/server/operations/queue.py b/src_pymav/server/operations/queue.py index be89aea..3a9b775 100644 --- a/src_pymav/server/operations/queue.py +++ b/src_pymav/server/operations/queue.py @@ -2,6 +2,7 @@ from server.common.wpqueue import WaypointQueue, Waypoint from server.common.encoders import command_string_to_int, command_int_to_string +from server.common.callback import CallbackSystem, Callback def set_home(mavlink_connection: mavutil.mavlink_connection, latitude: float, longitude: float, altitude: float): # -> int | None: # Send a set home command @@ -20,7 +21,7 @@ def set_home(mavlink_connection: mavutil.mavlink_connection, latitude: float, lo return ack.result -def new_mission(mavlink_connection: mavutil.mavlink_connection, waypoint_queue: WaypointQueue) -> bool: +def new_mission(mavlink_connection: mavutil.mavlink_connection, callback_sys: CallbackSystem, waypoint_queue: WaypointQueue, callbacks: list[Callback] = []) -> bool: # Clear any existing mission from vehicle print('Clearing mission') mavlink_connection.mav.mission_clear_all_send(mavlink_connection.target_system, mavlink_connection.target_component) @@ -28,6 +29,11 @@ def new_mission(mavlink_connection: mavutil.mavlink_connection, waypoint_queue: if not verify_ack(mavlink_connection, 'Error clearing mission'): return False + # register callbacks after mission is cleared + callback_sys.mission_switched() + for callback in callbacks: + callback_sys.register_callback(callback) + # Insert the home waypoint wp_list = [] seq = 0 From 4c2ed27da6618304f298c9ed814449108d220a6f Mon Sep 17 00:00:00 2001 From: Ram Jayakumar Date: Tue, 6 May 2025 16:47:11 -0700 Subject: [PATCH 15/20] add bruno and status script --- MissionPlannerScripts/Test Endpoint.bru | 11 + MissionPlannerScripts/aeac/New Request.bru | 11 + MissionPlannerScripts/aeac/aeac_deliver.bru | 18 ++ .../aeac/aeac_payload_OFF.bru | 20 ++ MissionPlannerScripts/aeac/aeac_scan.bru | 21 ++ MissionPlannerScripts/aeac/folder.bru | 7 + MissionPlannerScripts/aeac/start_cam.bru | 19 ++ MissionPlannerScripts/aeac/stop_cam.bru | 17 ++ MissionPlannerScripts/bruno.json | 9 + MissionPlannerScripts/collection.bru | 7 + MissionPlannerScripts/environments/main.bru | 3 + MissionPlannerScripts/landing/folder.bru | 11 + MissionPlannerScripts/landing/home.bru | 29 +++ MissionPlannerScripts/landing/land.bru | 15 ++ MissionPlannerScripts/landing/land_1.bru | 25 +++ MissionPlannerScripts/landing/rtl.bru | 15 ++ MissionPlannerScripts/landing/rtl_1.bru | 21 ++ MissionPlannerScripts/options/altstandard.bru | 15 ++ MissionPlannerScripts/options/flightmode.bru | 21 ++ MissionPlannerScripts/options/folder.bru | 11 + MissionPlannerScripts/queue/clear.bru | 19 ++ MissionPlannerScripts/queue/diversion.bru | 36 ++++ MissionPlannerScripts/queue/folder.bru | 11 + MissionPlannerScripts/queue/insert.bru | 25 +++ MissionPlannerScripts/queue/queue.bru | 19 ++ MissionPlannerScripts/queue/queue_1.bru | 88 ++++++++ MissionPlannerScripts/status/folder.bru | 11 + MissionPlannerScripts/status/status.bru | 32 +++ MissionPlannerScripts/takeoff/arm.bru | 21 ++ MissionPlannerScripts/takeoff/folder.bru | 11 + MissionPlannerScripts/takeoff/takeoff.bru | 25 +++ logs/main.log | 15 ++ logs/status_loop.log | 189 ++++++++++++++++++ logs/status_loop.pid | 1 + status_loop.py | 23 +++ 35 files changed, 832 insertions(+) create mode 100644 MissionPlannerScripts/Test Endpoint.bru create mode 100644 MissionPlannerScripts/aeac/New Request.bru create mode 100644 MissionPlannerScripts/aeac/aeac_deliver.bru create mode 100644 MissionPlannerScripts/aeac/aeac_payload_OFF.bru create mode 100644 MissionPlannerScripts/aeac/aeac_scan.bru create mode 100644 MissionPlannerScripts/aeac/folder.bru create mode 100644 MissionPlannerScripts/aeac/start_cam.bru create mode 100644 MissionPlannerScripts/aeac/stop_cam.bru create mode 100644 MissionPlannerScripts/bruno.json create mode 100644 MissionPlannerScripts/collection.bru create mode 100644 MissionPlannerScripts/environments/main.bru create mode 100644 MissionPlannerScripts/landing/folder.bru create mode 100644 MissionPlannerScripts/landing/home.bru create mode 100644 MissionPlannerScripts/landing/land.bru create mode 100644 MissionPlannerScripts/landing/land_1.bru create mode 100644 MissionPlannerScripts/landing/rtl.bru create mode 100644 MissionPlannerScripts/landing/rtl_1.bru create mode 100644 MissionPlannerScripts/options/altstandard.bru create mode 100644 MissionPlannerScripts/options/flightmode.bru create mode 100644 MissionPlannerScripts/options/folder.bru create mode 100644 MissionPlannerScripts/queue/clear.bru create mode 100644 MissionPlannerScripts/queue/diversion.bru create mode 100644 MissionPlannerScripts/queue/folder.bru create mode 100644 MissionPlannerScripts/queue/insert.bru create mode 100644 MissionPlannerScripts/queue/queue.bru create mode 100644 MissionPlannerScripts/queue/queue_1.bru create mode 100644 MissionPlannerScripts/status/folder.bru create mode 100644 MissionPlannerScripts/status/status.bru create mode 100644 MissionPlannerScripts/takeoff/arm.bru create mode 100644 MissionPlannerScripts/takeoff/folder.bru create mode 100644 MissionPlannerScripts/takeoff/takeoff.bru create mode 100644 logs/main.log create mode 100644 logs/status_loop.log create mode 100644 logs/status_loop.pid create mode 100644 status_loop.py diff --git a/MissionPlannerScripts/Test Endpoint.bru b/MissionPlannerScripts/Test Endpoint.bru new file mode 100644 index 0000000..dbbc9e2 --- /dev/null +++ b/MissionPlannerScripts/Test Endpoint.bru @@ -0,0 +1,11 @@ +meta { + name: Test Endpoint + type: http + seq: 1 +} + +get { + url: {{ base_url }}/ + body: none + auth: none +} diff --git a/MissionPlannerScripts/aeac/New Request.bru b/MissionPlannerScripts/aeac/New Request.bru new file mode 100644 index 0000000..ae12dc6 --- /dev/null +++ b/MissionPlannerScripts/aeac/New Request.bru @@ -0,0 +1,11 @@ +meta { + name: New Request + type: http + seq: 6 +} + +get { + url: + body: none + auth: none +} diff --git a/MissionPlannerScripts/aeac/aeac_deliver.bru b/MissionPlannerScripts/aeac/aeac_deliver.bru new file mode 100644 index 0000000..57ecb6b --- /dev/null +++ b/MissionPlannerScripts/aeac/aeac_deliver.bru @@ -0,0 +1,18 @@ +meta { + name: aeac_deliver + type: http + seq: 4 +} + +post { + url: {{base_url}}/aeac_deliver + body: json + auth: none +} + +body:json { + { + "deliver_alt": 1, + "deliver_duration_secs": 20 + } +} diff --git a/MissionPlannerScripts/aeac/aeac_payload_OFF.bru b/MissionPlannerScripts/aeac/aeac_payload_OFF.bru new file mode 100644 index 0000000..c84f078 --- /dev/null +++ b/MissionPlannerScripts/aeac/aeac_payload_OFF.bru @@ -0,0 +1,20 @@ +meta { + name: aeac_payload_OFF + type: http + seq: 5 +} + +post { + url: {{base_url}}/aeac_payload + body: json + auth: none +} + +body:json { + { + "valve_one_open": false, + "valve_two_open": false, + "pump_on": false, + "reset": false + } +} diff --git a/MissionPlannerScripts/aeac/aeac_scan.bru b/MissionPlannerScripts/aeac/aeac_scan.bru new file mode 100644 index 0000000..9162881 --- /dev/null +++ b/MissionPlannerScripts/aeac/aeac_scan.bru @@ -0,0 +1,21 @@ +meta { + name: aeac_scan + type: http + seq: 3 +} + +post { + url: {{base_url}}/aeac_scan + body: json + auth: none +} + +body:json { + { + "center_lat": 0, + "center_lon": 0, + "altitude": 50, + "target_area_radius": 110, + "enable_camera": false + } +} diff --git a/MissionPlannerScripts/aeac/folder.bru b/MissionPlannerScripts/aeac/folder.bru new file mode 100644 index 0000000..cecf5c0 --- /dev/null +++ b/MissionPlannerScripts/aeac/folder.bru @@ -0,0 +1,7 @@ +meta { + name: aeac +} + +auth { + mode: none +} diff --git a/MissionPlannerScripts/aeac/start_cam.bru b/MissionPlannerScripts/aeac/start_cam.bru new file mode 100644 index 0000000..c952936 --- /dev/null +++ b/MissionPlannerScripts/aeac/start_cam.bru @@ -0,0 +1,19 @@ +meta { + name: start_cam + type: http + seq: 1 +} + +post { + url: {{base_url}}/activate_camera + body: json + auth: none +} + +body:json { + { + "cam_id": 0, + "time_between_pics_secs": 0.5, + "num_of_pics": 0 + } +} diff --git a/MissionPlannerScripts/aeac/stop_cam.bru b/MissionPlannerScripts/aeac/stop_cam.bru new file mode 100644 index 0000000..89d57ed --- /dev/null +++ b/MissionPlannerScripts/aeac/stop_cam.bru @@ -0,0 +1,17 @@ +meta { + name: stop_cam + type: http + seq: 2 +} + +post { + url: {{base_url}}/deactivate_camera + body: json + auth: none +} + +body:json { + { + "cam_id": 0 + } +} diff --git a/MissionPlannerScripts/bruno.json b/MissionPlannerScripts/bruno.json new file mode 100644 index 0000000..a8d2646 --- /dev/null +++ b/MissionPlannerScripts/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "MissionPlannerScripts", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/MissionPlannerScripts/collection.bru b/MissionPlannerScripts/collection.bru new file mode 100644 index 0000000..d2687cd --- /dev/null +++ b/MissionPlannerScripts/collection.bru @@ -0,0 +1,7 @@ +meta { + name: MissionPlannerScripts +} + +auth { + mode: none +} diff --git a/MissionPlannerScripts/environments/main.bru b/MissionPlannerScripts/environments/main.bru new file mode 100644 index 0000000..894c580 --- /dev/null +++ b/MissionPlannerScripts/environments/main.bru @@ -0,0 +1,3 @@ +vars { + base_url: http://localhost:9000 +} diff --git a/MissionPlannerScripts/landing/folder.bru b/MissionPlannerScripts/landing/folder.bru new file mode 100644 index 0000000..c982a90 --- /dev/null +++ b/MissionPlannerScripts/landing/folder.bru @@ -0,0 +1,11 @@ +meta { + name: landing +} + +auth { + mode: none +} + +docs { + Access to landing, RTL, and home waypoint. +} diff --git a/MissionPlannerScripts/landing/home.bru b/MissionPlannerScripts/landing/home.bru new file mode 100644 index 0000000..7878f08 --- /dev/null +++ b/MissionPlannerScripts/landing/home.bru @@ -0,0 +1,29 @@ +meta { + name: home + type: http + seq: 5 +} + +post { + url: {{ base_url }}/home + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "id": 0, + "name": "Alpha", + "latitude": -35.3627798, + "longitude": 149.1651830, + "altitude": 583 + } +} + +docs { + Set the home waypoint of the drone. +} diff --git a/MissionPlannerScripts/landing/land.bru b/MissionPlannerScripts/landing/land.bru new file mode 100644 index 0000000..d23e604 --- /dev/null +++ b/MissionPlannerScripts/landing/land.bru @@ -0,0 +1,15 @@ +meta { + name: land + type: http + seq: 3 +} + +get { + url: {{ base_url }}/land + body: none + auth: none +} + +docs { + Immediately descend and land. +} diff --git a/MissionPlannerScripts/landing/land_1.bru b/MissionPlannerScripts/landing/land_1.bru new file mode 100644 index 0000000..d7e961f --- /dev/null +++ b/MissionPlannerScripts/landing/land_1.bru @@ -0,0 +1,25 @@ +meta { + name: land_1 + type: http + seq: 4 +} + +post { + url: {{ base_url }}/land + body: json + auth: none +} + +body:json { + { + "id": 0, + "name": "string", + "latitude": 38.3171058, + "longitude": -76.5517151, + "altitude": 100 + } +} + +docs { + Land at designated location. +} diff --git a/MissionPlannerScripts/landing/rtl.bru b/MissionPlannerScripts/landing/rtl.bru new file mode 100644 index 0000000..f4540f3 --- /dev/null +++ b/MissionPlannerScripts/landing/rtl.bru @@ -0,0 +1,15 @@ +meta { + name: rtl + type: http + seq: 1 +} + +get { + url: {{ base_url }}/rtl + body: none + auth: none +} + +docs { + Return to launch. +} diff --git a/MissionPlannerScripts/landing/rtl_1.bru b/MissionPlannerScripts/landing/rtl_1.bru new file mode 100644 index 0000000..36b89ad --- /dev/null +++ b/MissionPlannerScripts/landing/rtl_1.bru @@ -0,0 +1,21 @@ +meta { + name: rtl_1 + type: http + seq: 2 +} + +post { + url: {{ base_url }}/rtl + body: json + auth: none +} + +body:json { + { + "altitude": 0 + } +} + +docs { + Return to launch at specified altitude. +} diff --git a/MissionPlannerScripts/options/altstandard.bru b/MissionPlannerScripts/options/altstandard.bru new file mode 100644 index 0000000..d0f0f32 --- /dev/null +++ b/MissionPlannerScripts/options/altstandard.bru @@ -0,0 +1,15 @@ +meta { + name: altstandard + type: http + seq: 2 +} + +put { + url: {{ base_url }}/altstandard + body: none + auth: none +} + +docs { + UNIMPLEMENTED +} diff --git a/MissionPlannerScripts/options/flightmode.bru b/MissionPlannerScripts/options/flightmode.bru new file mode 100644 index 0000000..8d2c5b4 --- /dev/null +++ b/MissionPlannerScripts/options/flightmode.bru @@ -0,0 +1,21 @@ +meta { + name: flightmode + type: http + seq: 1 +} + +put { + url: {{ base_url }}/flightmode + body: json + auth: none +} + +body:json { + { + "mode": "loiter" + } +} + +docs { + Change flight mode ofm the aircraft to Loiter, Stabilize, Auto or Guided. +} diff --git a/MissionPlannerScripts/options/folder.bru b/MissionPlannerScripts/options/folder.bru new file mode 100644 index 0000000..e7aebb1 --- /dev/null +++ b/MissionPlannerScripts/options/folder.bru @@ -0,0 +1,11 @@ +meta { + name: options +} + +auth { + mode: none +} + +docs { + Access to flight options. +} diff --git a/MissionPlannerScripts/queue/clear.bru b/MissionPlannerScripts/queue/clear.bru new file mode 100644 index 0000000..bb3d96e --- /dev/null +++ b/MissionPlannerScripts/queue/clear.bru @@ -0,0 +1,19 @@ +meta { + name: clear + type: http + seq: 4 +} + +get { + url: {{ base_url }}/clear + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +docs { + Clear the waypoint queue. +} diff --git a/MissionPlannerScripts/queue/diversion.bru b/MissionPlannerScripts/queue/diversion.bru new file mode 100644 index 0000000..d2be8d1 --- /dev/null +++ b/MissionPlannerScripts/queue/diversion.bru @@ -0,0 +1,36 @@ +meta { + name: diversion + type: http + seq: 5 +} + +post { + url: {{ base_url }}/diversion + body: json + auth: none +} + +body:json { + { + "exclude": [ + { + "id": 0, + "name": "string", + "longitude": 0, + "latitude": 0, + "altitude": 0 + } + ], + "rejoin_at": { + "id": 0, + "name": "string", + "longitude": 0, + "latitude": 0, + "altitude": 0 + } + } +} + +docs { + Divert the aircraft around an exclusion zone. +} diff --git a/MissionPlannerScripts/queue/folder.bru b/MissionPlannerScripts/queue/folder.bru new file mode 100644 index 0000000..26480b1 --- /dev/null +++ b/MissionPlannerScripts/queue/folder.bru @@ -0,0 +1,11 @@ +meta { + name: queue +} + +auth { + mode: none +} + +docs { + Access to aircraft waypoint queue +} diff --git a/MissionPlannerScripts/queue/insert.bru b/MissionPlannerScripts/queue/insert.bru new file mode 100644 index 0000000..a79f7e6 --- /dev/null +++ b/MissionPlannerScripts/queue/insert.bru @@ -0,0 +1,25 @@ +meta { + name: insert + type: http + seq: 3 +} + +post { + url: {{ base_url }}/insert + body: json + auth: none +} + +body:json { + [{ + "id": 0, + "name": "Inserted", + "latitude": 38.3096384, + "longitude": -76.5514048, + "altitude": 100 + }] +} + +docs { + Insert a waypoint onto the front of the queue. +} diff --git a/MissionPlannerScripts/queue/queue.bru b/MissionPlannerScripts/queue/queue.bru new file mode 100644 index 0000000..286fae0 --- /dev/null +++ b/MissionPlannerScripts/queue/queue.bru @@ -0,0 +1,19 @@ +meta { + name: queue + type: http + seq: 1 +} + +get { + url: {{ base_url }}/queue + body: json + auth: none +} + +headers { + Content-Type: multipart/form-data +} + +docs { + Returns the current list of queue of waypoints to hit. Passed or removed waypoints are not displayed. +} diff --git a/MissionPlannerScripts/queue/queue_1.bru b/MissionPlannerScripts/queue/queue_1.bru new file mode 100644 index 0000000..76ca15d --- /dev/null +++ b/MissionPlannerScripts/queue/queue_1.bru @@ -0,0 +1,88 @@ +meta { + name: queue_1 + type: http + seq: 2 +} + +post { + url: {{ base_url }}/queue + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + [ + { + "id": 0, + "name": "Alpha", + "latitude": 38.3143531, + "longitude": -76.5594292, + "altitude": 100 + }, + { + "id": 6, + "name": "Alpha", + "latitude": 38.3012197, + "longitude": -76.5467262, + "altitude": 100, + "command":"DO_CHANGE_SPEED", + "param1":1, + "param2":10 + }, + { + "id": 1, + "name": "Alpha", + "latitude": 38.3180233, + "longitude": -76.5576053, + "altitude": 100, + "command":"WAYPOINT", + "param1":3, + "param2":4, + "param3":5, + "param4":6 + }, + { + "id": 5, + "name": "Alpha", + "latitude": 38.3035098, + "longitude": -76.5376282, + "altitude": 100, + "command":"DO_VTOL_TRANSITION", + "param1":3, + "param4":4 + }, + { + "id": 2, + "name": "Alpha", + "latitude": 38.3200772, + "longitude": -76.5527773, + "altitude": 100, + "command":"LOITER_UNLIM", + "param3":0 + }, + { + "id": 3, + "name": "Alpha", + "latitude": 38.3195385, + "longitude": -76.5394735, + "altitude": 100 + }, + { + "id": 4, + "name": "Alpha", + "latitude": 38.3112889, + "longitude": -76.5240669, + "altitude": 100, + "param3":-1 + } + + ] +} + +docs { + Overwrite the queue with a new list of waypoints. +} diff --git a/MissionPlannerScripts/status/folder.bru b/MissionPlannerScripts/status/folder.bru new file mode 100644 index 0000000..4eb740b --- /dev/null +++ b/MissionPlannerScripts/status/folder.bru @@ -0,0 +1,11 @@ +meta { + name: status +} + +auth { + mode: none +} + +docs { + Access to aircraft status. +} diff --git a/MissionPlannerScripts/status/status.bru b/MissionPlannerScripts/status/status.bru new file mode 100644 index 0000000..2ec7704 --- /dev/null +++ b/MissionPlannerScripts/status/status.bru @@ -0,0 +1,32 @@ +meta { + name: status + type: http + seq: 1 +} + +get { + url: {{base_url}}/status + body: json + auth: none +} + +body:json { + { + "airspeed": 0, + "groundspeed": 0, + "latitude": 0, + "longitude": 0, + "altitude": 0, + "heading": 0, + "batteryvoltage": 0, + "winddirection": 0, + "windvelocity": 0, + "current_wpn": 0, + "date": 0, + "time": 0 + } +} + +docs { + Obtain the aircraft status. +} diff --git a/MissionPlannerScripts/takeoff/arm.bru b/MissionPlannerScripts/takeoff/arm.bru new file mode 100644 index 0000000..3e10c25 --- /dev/null +++ b/MissionPlannerScripts/takeoff/arm.bru @@ -0,0 +1,21 @@ +meta { + name: arm + type: http + seq: 2 +} + +put { + url: {{ base_url }}/arm + body: json + auth: none +} + +body:json { + { + "arm": 1 + } +} + +docs { + Arm or disarm the motors. +} diff --git a/MissionPlannerScripts/takeoff/folder.bru b/MissionPlannerScripts/takeoff/folder.bru new file mode 100644 index 0000000..72b1f29 --- /dev/null +++ b/MissionPlannerScripts/takeoff/folder.bru @@ -0,0 +1,11 @@ +meta { + name: takeoff +} + +auth { + mode: none +} + +docs { + Access to takeoff and arming motors. +} diff --git a/MissionPlannerScripts/takeoff/takeoff.bru b/MissionPlannerScripts/takeoff/takeoff.bru new file mode 100644 index 0000000..f0914d4 --- /dev/null +++ b/MissionPlannerScripts/takeoff/takeoff.bru @@ -0,0 +1,25 @@ +meta { + name: takeoff + type: http + seq: 1 +} + +post { + url: {{ base_url }}/takeoff + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "altitude": 25 + } +} + +docs { + Lift off to a given altitude. +} diff --git a/logs/main.log b/logs/main.log new file mode 100644 index 0000000..98f3b1a --- /dev/null +++ b/logs/main.log @@ -0,0 +1,15 @@ +Starting server in production +Starting... HTTP server listening at localhost:9000. Status WS connecting to localhost:1323. + +Starting threads... + +MissionPlanner Server starting... +GCOM HTTP Server starting... +Status Websocket Client starting... + * Serving Flask app 'server.gcomhandler' + * Debug mode: on +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:9000 + * Running on http://10.42.0.51:9000 +Press CTRL+C to quit diff --git a/logs/status_loop.log b/logs/status_loop.log new file mode 100644 index 0000000..3d099cb --- /dev/null +++ b/logs/status_loop.log @@ -0,0 +1,189 @@ +[Sat May 3 13:48:48 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:48:53 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:48:57 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:02 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:06 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:11 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:15 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:20 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:25 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:29 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:34 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:38 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:43 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:47 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:52 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:49:56 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:01 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:06 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:10 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:15 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:19 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:24 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:28 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:33 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:38 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:42 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:47 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:51 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:50:56 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:00 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:05 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:09 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:14 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:19 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:23 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:28 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:32 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:37 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:41 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:46 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:51 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:51:55 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:00 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:04 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:09 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:13 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:18 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:23 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:27 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:32 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:36 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:41 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:45 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:50 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:54 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:52:59 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:04 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:08 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:13 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:17 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:22 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:26 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:31 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:36 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:40 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:45 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:49 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:54 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:53:58 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:03 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:07 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:12 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:17 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:21 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:26 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:30 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:35 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:39 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:44 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:48 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:53 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:54:58 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:02 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:07 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:11 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:16 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:20 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:25 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:29 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:34 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:39 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:43 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:48 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:52 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:55:57 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:01 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:06 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:11 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:15 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:20 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:24 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:29 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:33 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:38 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:42 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:47 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:52 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:56:56 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:01 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:05 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:10 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:14 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:19 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:23 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:28 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:33 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:37 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:42 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:46 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:51 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:57:55 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:00 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:04 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:09 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:14 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:18 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:23 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:27 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:32 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:36 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:41 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:46 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:50 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +[Sat May 3 13:58:55 2025] Error contacting http://localhost:8000/status: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /status (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] No connection could be made because the target machine actively refused it')) +Traceback (most recent call last): + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\urllib3\util\connection.py", line 73, in create_connection + sock.connect(sa) +ConnectionRefusedError: [WinError 10061] No connection could be made because the target machine actively refused it + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\status_loop.py", line 23, in + main() + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\status_loop.py", line 19, in main + check_status() + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\status_loop.py", line 11, in check_status + response = requests.get(url) + ^^^^^^^^^^^^^^^^^ + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\requests\api.py", line 73, in get + return request("get", url, params=params, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\requests\api.py", line 59, in request + return session.request(method=method, url=url, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\requests\sessions.py", line 589, in request + resp = self.send(prep, **send_kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\requests\sessions.py", line 703, in send + r = adapter.send(request, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\requests\adapters.py", line 667, in send + resp = conn.urlopen( + ^^^^^^^^^^^^^ + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen + response = self._make_request( + ^^^^^^^^^^^^^^^^^^^ + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\urllib3\connectionpool.py", line 493, in _make_request + conn.request( + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\urllib3\connection.py", line 445, in request + self.endheaders() + File "C:\Python312\Lib\http\client.py", line 1331, in endheaders + self._send_output(message_body, encode_chunked=encode_chunked) + File "C:\Python312\Lib\http\client.py", line 1091, in _send_output + self.send(msg) + File "C:\Python312\Lib\http\client.py", line 1035, in send + self.connect() + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\urllib3\connection.py", line 276, in connect + self.sock = self._new_conn() + ^^^^^^^^^^^^^^^^ + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\urllib3\connection.py", line 198, in _new_conn + sock = connection.create_connection( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\jay_r\uas\MissionPlanner-Scripts\venv\Lib\site-packages\urllib3\util\connection.py", line 81, in create_connection + sock.close() + File "C:\Python312\Lib\socket.py", line 501, in close + def close(self): + +KeyboardInterrupt diff --git a/logs/status_loop.pid b/logs/status_loop.pid new file mode 100644 index 0000000..058fbb1 --- /dev/null +++ b/logs/status_loop.pid @@ -0,0 +1 @@ +1713 diff --git a/status_loop.py b/status_loop.py new file mode 100644 index 0000000..7d14b69 --- /dev/null +++ b/status_loop.py @@ -0,0 +1,23 @@ +import time +import requests + +# Configuration +HTTP_SERVER = "http://localhost:8000" # Replace with your actual server URL +INTERVAL = 0.5 # Time in seconds between requests + +def check_status(): + url = f"{HTTP_SERVER}/status" + try: + response = requests.get(url) + response.raise_for_status() # Raise an exception for HTTP errors + print(f"[{time.ctime()}] Status: {response.status_code} - {response.text}") + except requests.RequestException as e: + print(f"[{time.ctime()}] Error contacting {url}: {e}") + +def main(): + while True: + check_status() + time.sleep(INTERVAL) + +if __name__ == "__main__": + main() From 9e99b460f33405c20715185d763866f9a2a39415 Mon Sep 17 00:00:00 2001 From: Hansen Date: Tue, 6 May 2025 17:26:22 -0700 Subject: [PATCH 16/20] SITL testing changes --- .../server/features/aeac_water_delivery.py | 5 ++--- src_pymav/server/httpserver.py | 9 +++++---- src_pymav/server/operations/get_info.py | 2 +- src_pymav/server/operations/queue.py | 20 ++++++++++--------- .../utilities/request_message_streaming.py | 3 ++- status_loop.py | 2 +- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src_pymav/server/features/aeac_water_delivery.py b/src_pymav/server/features/aeac_water_delivery.py index 4a82e46..3938960 100644 --- a/src_pymav/server/features/aeac_water_delivery.py +++ b/src_pymav/server/features/aeac_water_delivery.py @@ -14,7 +14,6 @@ 3. Return to previous location altitude ''' def generate_water_wps( - current_alt: float, deliver_alt: float, deliver_duration_secs: int, curr_lat: float, @@ -29,7 +28,7 @@ def generate_water_wps( "curr_wp", curr_lat, curr_lon, - current_alt, + deliver_alt + 5, ) wp_2 = Waypoint( @@ -100,7 +99,7 @@ def generate_water_wps( "curr_wp", curr_lat, curr_lon, - current_alt, + deliver_alt + 5, command="LOITER_UNLIM", ) diff --git a/src_pymav/server/httpserver.py b/src_pymav/server/httpserver.py index 8ddc6fa..e2a8d69 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -182,7 +182,7 @@ def get_clear_queue(): @app.route("/status", methods=["GET"]) def get_status_handler(): - print("Status sent to GCOM") + # print("Status sent to GCOM") s = get_status(self.mav_connection, self.callback_sys).as_dictionary() return s, 200 @@ -374,12 +374,13 @@ def generate_scan_points(): # If given lat lon is 0, then base spiral off of current lat lon if (center_lat == 0 and center_lng == 0): + print(f"{ret._lat} {ret._lng}") center_lat = ret._lat center_lng = ret._lng wpq, callbacks = scan_area(center_lat, center_lng, altitude, target_area_radius, enable_camera) - if new_mission(self.mav_connection, self.callback_sys, wpq, callbacks): + if new_mission(self.mav_connection, self.callback_sys, wpq, callbacks, frame=3): # frame - RELATIVE TO HOME ALT return f"Scan Mission Set", 200 else: return "Mission request failed", 400 @@ -400,9 +401,9 @@ def deliver_water_down(): deliver_duration_secs = input["deliver_duration_secs"] curr_lat = ret._lat curr_lng = ret._lng - wpq, callbacks = generate_water_wps(current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lng) + wpq, callbacks = generate_water_wps(deliver_alt, deliver_duration_secs, curr_lat, curr_lng) - if new_mission(self.mav_connection, self.callback_sys, wpq, callbacks): + if new_mission(self.mav_connection, self.callback_sys, wpq, callbacks, frame=3): # frame - RELATIVE TO HOME ALT return f"Commencing Deliver operation", 200 else: return "Mission request failed", 400 diff --git a/src_pymav/server/operations/get_info.py b/src_pymav/server/operations/get_info.py index 6ec8049..39f2985 100644 --- a/src_pymav/server/operations/get_info.py +++ b/src_pymav/server/operations/get_info.py @@ -53,7 +53,7 @@ def get_status(mav_connection: mavutil.mavfile, callback_sys: CallbackSystem = N status_wind = mav_connection.messages.get('WIND_COV', Object(wind_x = 0, wind_y = 0)) latency_wind = mav_connection.time_since('WIND_COV') - print(f"Latencies: {latency_time:2f}s, {latency_gps:2f}s, {latency_att:2f}s, {latency_vfr:2f}s, {latency_sys:2f}s, {latency_wpn:2f}s, {latency_wind:2f}s") + # print(f"Latencies: {latency_time:2f}s, {latency_gps:2f}s, {latency_att:2f}s, {latency_vfr:2f}s, {latency_sys:2f}s, {latency_wpn:2f}s, {latency_wind:2f}s") # wind calculations in the horizontal plane TODO determine if vertical windspeed is needed winddirection = math.degrees(math.atan(status_wind.wind_x / status_wind.wind_y)) if status_wind.wind_y != 0 else (0 if status_wind.wind_x > 0 else 180) diff --git a/src_pymav/server/operations/queue.py b/src_pymav/server/operations/queue.py index 3a9b775..e4307fb 100644 --- a/src_pymav/server/operations/queue.py +++ b/src_pymav/server/operations/queue.py @@ -21,7 +21,7 @@ def set_home(mavlink_connection: mavutil.mavlink_connection, latitude: float, lo return ack.result -def new_mission(mavlink_connection: mavutil.mavlink_connection, callback_sys: CallbackSystem, waypoint_queue: WaypointQueue, callbacks: list[Callback] = []) -> bool: +def new_mission(mavlink_connection: mavutil.mavlink_connection, callback_sys: CallbackSystem, waypoint_queue: WaypointQueue, callbacks: list[Callback] = [], frame = 0) -> bool: # Clear any existing mission from vehicle print('Clearing mission') mavlink_connection.mav.mission_clear_all_send(mavlink_connection.target_system, mavlink_connection.target_component) @@ -29,16 +29,11 @@ def new_mission(mavlink_connection: mavutil.mavlink_connection, callback_sys: Ca if not verify_ack(mavlink_connection, 'Error clearing mission'): return False - # register callbacks after mission is cleared - callback_sys.mission_switched() - for callback in callbacks: - callback_sys.register_callback(callback) - # Insert the home waypoint wp_list = [] seq = 0 wp_list.append(mavutil.mavlink.MAVLink_mission_item_int_message( - 0, 0, seq, 0, 16, 0, 0, 0, 0, 0, 0, + 0, 0, seq, frame, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0 @@ -50,7 +45,7 @@ def new_mission(mavlink_connection: mavutil.mavlink_connection, callback_sys: Ca wp_list.append(mavutil.mavlink.MAVLink_mission_item_int_message( mavlink_connection.target_system, mavlink_connection.target_component, seq, - 0, command_string_to_int(wp._com), 0, 1, + frame, command_string_to_int(wp._com), 0, 1, float(wp._param1), float(wp._param2), float(wp._param3), float(wp._param4), int(wp._lat * 10000000), int(wp._lng * 10000000), int(wp._alt) @@ -60,7 +55,14 @@ def new_mission(mavlink_connection: mavutil.mavlink_connection, callback_sys: Ca mavlink_connection.waypoint_count_send(len(wp_list)) # Upload waypoints to the UAV - return send_waypoints(mavlink_connection, wp_list) + ret = send_waypoints(mavlink_connection, wp_list) + + # register callbacks after mission is cleared + callback_sys.mission_switched() + for callback in callbacks: + callback_sys.register_callback(callback) + + return ret def send_waypoints(mavlink_connection: mavutil.mavlink_connection, wp_list: list) -> bool: """ diff --git a/src_pymav/server/utilities/request_message_streaming.py b/src_pymav/server/utilities/request_message_streaming.py index 38782a7..42cecb2 100644 --- a/src_pymav/server/utilities/request_message_streaming.py +++ b/src_pymav/server/utilities/request_message_streaming.py @@ -162,7 +162,8 @@ def request_messages(connection, message_types: list) -> bool: response = connection.recv_match(type='COMMAND_ACK', blocking=True) if (response and response.command == mavutil.mavlink.MAV_CMD_REQUEST_MESSAGE and response.result == mavutil.mavlink.MAV_RESULT_ACCEPTED): - print(f"Request for Message of type {message_type} ACCEPTED") + # print(f"Request for Message of type {message_type} ACCEPTED") + pass else: print(f"Request for Message of type {message_type} DENIED") diff --git a/status_loop.py b/status_loop.py index 7d14b69..926754e 100644 --- a/status_loop.py +++ b/status_loop.py @@ -2,7 +2,7 @@ import requests # Configuration -HTTP_SERVER = "http://localhost:8000" # Replace with your actual server URL +HTTP_SERVER = "http://localhost:9000" # Replace with your actual server URL INTERVAL = 0.5 # Time in seconds between requests def check_status(): From 789df88c5204153e46c7eaea6347728feba68c43 Mon Sep 17 00:00:00 2001 From: Hansen Dan Date: Fri, 9 May 2025 10:25:24 -0700 Subject: [PATCH 17/20] changed scan behaviour --- src_pymav/server/features/aeac_scan.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src_pymav/server/features/aeac_scan.py b/src_pymav/server/features/aeac_scan.py index eaae9e6..9800dff 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -80,14 +80,17 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius, enable_cam) )) # transit from center to edge, turning gently so that drone is tangent when reaching the edge - tmp_lat, tmp_lng = convert_utm_to_gps(center_we + target_area_radius / 2, center_sn - target_area_radius / 2, zone, hemisphere) - record.append((center_we + target_area_radius / 2, center_sn - target_area_radius / 2)) - wpq.push(Waypoint(count, "", tmp_lat, tmp_lng, altitude)) - count += 1 + # tmp_lat, tmp_lng = convert_utm_to_gps(center_we + target_area_radius / 2, center_sn - target_area_radius / 2, zone, hemisphere) + # record.append((center_we + target_area_radius / 2, center_sn - target_area_radius / 2)) + # wpq.push(Waypoint(count, "", tmp_lat, tmp_lng, altitude)) + # count += 1 + + spiral_wps = [] tmp_lat, tmp_lng = convert_utm_to_gps(center_we + target_area_radius, center_sn, zone, hemisphere) record.append((center_we + target_area_radius, center_sn)) - wpq.push(Waypoint(count, "", tmp_lat, tmp_lng, altitude)) + # wpq.push(Waypoint(count, "", tmp_lat, tmp_lng, altitude)) + spiral_wps.append(Waypoint(count, "", tmp_lat, tmp_lng, altitude)) count += 1 if (enable_cam): @@ -124,9 +127,14 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius, enable_cam) # place waypoint record.append((center_we + current_radius * math.cos(current_angle), center_sn + current_radius * math.sin(current_angle))) tmp_lat, tmp_lng = convert_utm_to_gps(center_we + current_radius * math.cos(current_angle), center_sn + current_radius * math.sin(current_angle), zone, hemisphere) - wpq.push(Waypoint(count, "", tmp_lat, tmp_lng, altitude, command=SPLINE_WAYPOINT_TYPE, p2=2)) + # wpq.push(Waypoint(count, "", tmp_lat, tmp_lng, altitude, command=SPLINE_WAYPOINT_TYPE, p2=2)) + spiral_wps.append(Waypoint(count, "", tmp_lat, tmp_lng, altitude, command=SPLINE_WAYPOINT_TYPE, p2=2)) count += 1 + spiral_wps.reverse() + for wp in spiral_wps: + wpq.push(wp) + # plot_shape(record, color="green", close_loop=False, scatter=True) # plot_shape([(wp._lng, wp._lat) for wp in wpq.aslist()], color="blue", close_loop=False, scatter=True) # plt.grid() From 18e20717a300d33af245da28eb369cb72c58d11c Mon Sep 17 00:00:00 2001 From: Hansen Dan Date: Fri, 9 May 2025 10:50:53 -0700 Subject: [PATCH 18/20] change speed during scan --- src_pymav/server/features/aeac_scan.py | 26 ++++++++++++++++++++- src_pymav/server/operations/change_modes.py | 18 ++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src_pymav/server/features/aeac_scan.py b/src_pymav/server/features/aeac_scan.py index 9800dff..6fdec1a 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -6,11 +6,13 @@ from server.common.callback import CallbackSystem, Callback from server.operations.camera import activate_camera, deactivate_camera +from server.operations.change_modes import change_speed # ALL UNITS IN METERS UNLESS SPECIFIED SPLINE_WAYPOINT_TYPE = "SPLINE_WAYPOINT" TURNING_RADIUS = 20 EARTH_RADIUS = 6378 * 1000 # 6378 km +SPEED = 4 # m/s def calculate_scan_radius(altitude, vertical_fov_deg, horizontal_fov_deg) -> int: # Convert FOV angles from degrees to radians @@ -54,6 +56,17 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius, enable_cam) scan_radius = calculate_scan_radius(altitude, 44, 57) # from v1226-mpz 20MP Lens (12 mm focal) print(scan_radius) + + callbacks.append(Callback( + "Scan Mission - Set Speed", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq == 1), + lambda msg, conn, state: change_speed(conn, speed=SPEED), + removable_flags={ + "on_payload_fired": True, + "on_mission_switched": True, + } + )) # go to center waypoint (with generous slack) wpq.push(Waypoint(0, "", center_lat, center_lng, altitude, command=SPLINE_WAYPOINT_TYPE)) @@ -97,7 +110,7 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius, enable_cam) callbacks.append(Callback( "Scan Mission - Stop Camera", 'MISSION_CURRENT', - lambda curr_msg, prev_msg: (curr_msg.seq < count - 1), + lambda curr_msg, prev_msg: (curr_msg.seq < count - 1), # TODO check this? will it just fire immediately? lambda msg, conn, state: deactivate_camera( mav_connection=conn, cam_id=0, @@ -135,6 +148,17 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius, enable_cam) for wp in spiral_wps: wpq.push(wp) + callbacks.append(Callback( + "Scan Mission - Unset Speed", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq >= count - 1), + lambda msg, conn, state: change_speed(conn, speed=-2), + removable_flags={ + "on_payload_fired": True, + "on_mission_switched": True, + } + )) + # plot_shape(record, color="green", close_loop=False, scatter=True) # plot_shape([(wp._lng, wp._lat) for wp in wpq.aslist()], color="blue", close_loop=False, scatter=True) # plt.grid() diff --git a/src_pymav/server/operations/change_modes.py b/src_pymav/server/operations/change_modes.py index ad03cc3..125df19 100644 --- a/src_pymav/server/operations/change_modes.py +++ b/src_pymav/server/operations/change_modes.py @@ -29,6 +29,24 @@ def change_flight_mode( return True +def change_speed(mav_connection: mavfile, tgt_sys_id=1, tgt_comp_id=1, speed: float = -2): + mav_connection.mav.command_long_send( + target_system=tgt_sys_id, + target_component=tgt_comp_id, + command=mavlink.MAV_CMD_DO_CHANGE_SPEED, + confirmation=0, + param1=0, # SPEED_TYPE_AIRSPEED + param2=speed, # -2 is default + param3=-1, # -1 indicates no change + param4=0, + param5=0, + param6=0, + param7=0, + ) + + verify_ack(mav_connection, "Failed ACK after change_speed") + + return True def change_aircraft_type(mav_connection: mavfile): # TODO investigate whether to deprecate From 5ca818457c2bd18536a7abcf252c9db4b02f140c Mon Sep 17 00:00:00 2001 From: Hansen Dan Date: Fri, 9 May 2025 11:13:09 -0700 Subject: [PATCH 19/20] additional scan changes --- src_pymav/server/features/aeac_scan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src_pymav/server/features/aeac_scan.py b/src_pymav/server/features/aeac_scan.py index 6fdec1a..3ad0138 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -55,6 +55,7 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius, enable_cam) count = 0 scan_radius = calculate_scan_radius(altitude, 44, 57) # from v1226-mpz 20MP Lens (12 mm focal) + scan_radius += 5 # fudge radius print(scan_radius) callbacks.append(Callback( @@ -158,6 +159,8 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius, enable_cam) "on_mission_switched": True, } )) + + print(f"DEBUG: {count = }") # plot_shape(record, color="green", close_loop=False, scatter=True) # plot_shape([(wp._lng, wp._lat) for wp in wpq.aslist()], color="blue", close_loop=False, scatter=True) From 75fd33ba938bbc905e9f30b378db75b9d413cad8 Mon Sep 17 00:00:00 2001 From: Hansen Date: Fri, 9 May 2025 12:19:16 -0700 Subject: [PATCH 20/20] adjustments from testing --- src_pymav/server/features/aeac_scan.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src_pymav/server/features/aeac_scan.py b/src_pymav/server/features/aeac_scan.py index 3ad0138..938039e 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -146,13 +146,19 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius, enable_cam) count += 1 spiral_wps.reverse() + + # duplicate last waypoint + last_wp = spiral_wps[-1] + spiral_wps.append(last_wp) + count += 1 + for wp in spiral_wps: wpq.push(wp) callbacks.append(Callback( "Scan Mission - Unset Speed", 'MISSION_CURRENT', - lambda curr_msg, prev_msg: (curr_msg.seq >= count - 1), + lambda curr_msg, prev_msg: (curr_msg.seq >= count), lambda msg, conn, state: change_speed(conn, speed=-2), removable_flags={ "on_payload_fired": True,