diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..41a5c19 Binary files /dev/null and b/.DS_Store differ 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/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. diff --git a/api_spec.yml b/api_spec.yml index 5d357cc..9b374ef 100644 --- a/api_spec.yml +++ b/api_spec.yml @@ -349,33 +349,53 @@ 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: 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 + /aeac_payload: + post: + 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 + 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/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/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/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..e53fa63 Binary files /dev/null and b/src/.DS_Store differ 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/common/callback.py b/src_pymav/server/common/callback.py new file mode 100644 index 0000000..a4dd88d --- /dev/null +++ b/src_pymav/server/common/callback.py @@ -0,0 +1,144 @@ +from collections.abc import Callable + +from pymavlink import mavutil + +class Callback(): + def __init__( + self, + name: str, + trigger_message_type: str, + trigger_condition: Callable = (lambda curr_msg, prev_msg: True), + payload: Callable = (lambda msg, conn, state: print(msg)), + 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 + 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. + `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.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): + self.conn = mav_connection + self.state = state + + 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.removable_flags.get("on_payload_fired"): + # 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 + 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 + + # 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) + + def register_callback(self, callback: 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 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)): + 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 70a91ce..938039e 100644 --- a/src_pymav/server/features/aeac_scan.py +++ b/src_pymav/server/features/aeac_scan.py @@ -3,11 +3,16 @@ 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 +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 @@ -38,8 +43,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(center_lat, center_lng, altitude, target_area_radius) -> 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 @@ -48,7 +55,19 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius) -> WaypointQ 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( + "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)) @@ -56,16 +75,52 @@ 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)) + + if (enable_cam): + 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=conn, + cam_id=0, + time_between_pics_secs=0.5, + num_of_pics=0 + ), + 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 - 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): + callbacks.append(Callback( + "Scan Mission - Stop Camera", + 'MISSION_CURRENT', + 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, + ), + removable_flags = { + "on_payload_fired": True, + "on_mission_switched": True, + } + )) # generate spiral decrease_per_radian = 0.75 * (scan_radius) / (2 * math.pi) @@ -86,9 +141,33 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius) -> WaypointQ # 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() + + # 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), + lambda msg, conn, state: change_speed(conn, speed=-2), + removable_flags={ + "on_payload_fired": True, + "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) # plt.grid() @@ -96,9 +175,10 @@ def scan_area(center_lat, center_lng, altitude, target_area_radius) -> WaypointQ # ax.set_aspect('equal', adjustable='box') # plt.show() - return wpq + return wpq, callbacks # TODO handle deadzone + if __name__ == '__main__': scan_area(0,0,100, 100) \ 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 aac9678..3938960 100644 --- a/src_pymav/server/features/aeac_water_delivery.py +++ b/src_pymav/server/features/aeac_water_delivery.py @@ -1,24 +1,63 @@ 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 +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( - 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( "Start", "curr_wp", curr_lat, curr_lon, - current_alt, + deliver_alt + 5, ) wp_2 = Waypoint( + "pre-loiter", + "curr_wp", + curr_lat, + curr_lon, + deliver_alt, + ) + + # Add a callback to set the payload mode to release water after getting to loiter wp + callbacks.append(Callback( + "Water Delivery - Release water", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq == 3), + lambda msg, conn, state: set_payload_mode( + mav_connection=conn, + valve_one_open=False, + valve_two_open=True, + pump_on=False, + reset=False + ), + removable_flags = { + "on_payload_fired": True, + "on_mission_switched": True, + } + )) + + wp_3 = Waypoint( "stay", "curr_wp", curr_lat, @@ -27,18 +66,93 @@ def generate_water_wps( command="LOITER_TIME", p1=deliver_duration_secs, ) + - wp_3 = Waypoint( + wp_4 = Waypoint( + "post-loiter", + "curr_wp", + curr_lat, + curr_lon, + deliver_alt, + ) + + # Add a callback to set the payload mode to stop releasing water after getting to loiter wp + callbacks.append(Callback( + "Water Delivery - Stop releasing water", + 'MISSION_CURRENT', + lambda curr_msg, prev_msg: (curr_msg.seq == 5), + lambda msg, conn, state: set_payload_mode( + mav_connection=conn, + valve_one_open=False, + valve_two_open=False, + pump_on=False, + reset=False + ), + removable_flags = { + "on_payload_fired": True, + "on_mission_switched": True, + } + )) + + wp_5 = Waypoint( "Return", "curr_wp", curr_lat, curr_lon, - current_alt, + deliver_alt + 5, command="LOITER_UNLIM", ) landing_mission.push(wp_1) landing_mission.push(wp_2) landing_mission.push(wp_3) + landing_mission.push(wp_4) + landing_mission.push(wp_5) + + 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: + + # 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 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 + 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) - return landing_mission + 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..e2a8d69 100644 --- a/src_pymav/server/httpserver.py +++ b/src_pymav/server/httpserver.py @@ -9,23 +9,41 @@ 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 +from server.features.aeac_water_delivery import generate_water_wps, set_payload_mode from server.utilities.request_message_streaming import set_parameter 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, Callback 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? + self.callback_sys.register_callback( + Callback( + "Print all CAMERA_FEEDBACK messages", + 'CAMERA_FEEDBACK', + removable_flags={ + "on_payload_fired": False, + "on_mission_switched": False, + "on_deregister_called": True, + } + ) + ) 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) @@ -36,7 +54,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 +74,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 = [] @@ -90,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() @@ -103,7 +121,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) @@ -144,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() @@ -164,8 +182,8 @@ 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() + # print("Status sent to GCOM") + s = get_status(self.mav_connection, self.callback_sys).as_dictionary() return s, 200 @app.route("/takeoff", methods=["POST"]) @@ -263,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 @@ -299,18 +317,70 @@ 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 + + ### 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"]): - wpq = scan_area(center_lat=input["center_lat"], center_lng=input["center_lng"], - altitude=input["altitude"], target_area_radius=input["target_area_radius"]) + 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_lng = input["center_lng"] + 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) + + # 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, wpq): + 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 @@ -321,24 +391,45 @@ 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"] - wpq = generate_water_wps(current_alt, deliver_alt, deliver_duration_secs, curr_lat, curr_lon) - - - if new_mission(self.mav_connection, wpq): + curr_lat = ret._lat + curr_lng = ret._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, frame=3): # frame - RELATIVE TO HOME ALT return f"Commencing Deliver operation", 200 else: 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 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, reset) + + 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/camera.py b/src_pymav/server/operations/camera.py new file mode 100644 index 0000000..af8f046 --- /dev/null +++ b/src_pymav/server/operations/camera.py @@ -0,0 +1,49 @@ +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 = 0, tgt_comp_id: int = 0) -> int: + + mav_connection.mav.command_long_send( + tgt_sys_id, + tgt_comp_id, + mavlink.MAV_CMD_IMAGE_START_CAPTURE, + 0, cam_id, time_between_pics_secs, num_of_pics, + 0, 0, 0, 0 + ) + + # 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 = 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, + mavlink.MAV_CMD_IMAGE_STOP_CAPTURE, + 0, cam_id, + 0, 0, 0, 0, 0, 0 + ) + + # 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 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 diff --git a/src_pymav/server/operations/get_info.py b/src_pymav/server/operations/get_info.py index e986c25..39f2985 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) @@ -51,12 +53,16 @@ def get_status(mav_connection: mavutil.mavfile) -> Status: 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) windvelocity = math.sqrt(status_wind.wind_x * status_wind.wind_x + status_wind.wind_y * status_wind.wind_y) + # trigger / callback mechanism + 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..e4307fb 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] = [], 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) @@ -32,7 +33,7 @@ def new_mission(mavlink_connection: mavutil.mavlink_connection, waypoint_queue: 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 @@ -44,7 +45,7 @@ def new_mission(mavlink_connection: mavutil.mavlink_connection, waypoint_queue: 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) @@ -54,7 +55,14 @@ def new_mission(mavlink_connection: mavutil.mavlink_connection, waypoint_queue: 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/operations/rc_channel_cmd.py b/src_pymav/server/operations/rc_channel_cmd.py new file mode 100644 index 0000000..b989d88 --- /dev/null +++ b/src_pymav/server/operations/rc_channel_cmd.py @@ -0,0 +1,48 @@ +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 + + 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) -> int: + 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}") + + result = mav_connection.mav.rc_channels_override_send( + mav_connection.target_system, + mav_connection.target_component, + *channel_values + ) + + print(f"RC Channel value of {value} sent to channel {channel} successfully.") + return 1 + + 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 new file mode 100644 index 0000000..926754e --- /dev/null +++ b/status_loop.py @@ -0,0 +1,23 @@ +import time +import requests + +# Configuration +HTTP_SERVER = "http://localhost:9000" # 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()