From e7d27842f8e7186d66c04770571f621c3ac2c425 Mon Sep 17 00:00:00 2001 From: Krishnan Shankar Date: Sat, 4 Jun 2022 10:48:32 -0400 Subject: [PATCH] Update Image retreival to use new HTTP-based system + more More: Use better type hints for GroundStation-dependent classes, attempt filter for groundstation vs autopilot logs, other bugfixes --- .gitignore | 1 + client/src/pages/FlightData/tabs/Logs.js | 11 +++-- server/app.py | 9 +++- server/groundstation.py | 26 +++++++--- server/handlers/image.py | 63 +++++++++--------------- server/handlers/interop.py | 11 +++-- server/handlers/uav/dummy.py | 7 ++- server/handlers/uav/prod.py | 13 +++-- server/handlers/ugv/dummy.py | 15 ++++-- server/handlers/ugv/prod.py | 7 ++- server/handlers/utils.py | 2 +- server/sample.config.json | 3 +- 12 files changed, 103 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index 58838e6d..c2a67abf 100644 --- a/.gitignore +++ b/.gitignore @@ -152,6 +152,7 @@ cython_debug/ !/server/assets/odlc_images/sample.png /server/handlers/uav/uav_mission.txt /server/handlers/ugv/ugv_mission.txt +/client/public/map/*/*/* odlc.json log.txt debug_log.txt diff --git a/client/src/pages/FlightData/tabs/Logs.js b/client/src/pages/FlightData/tabs/Logs.js index 5cd780b9..4b17c94a 100644 --- a/client/src/pages/FlightData/tabs/Logs.js +++ b/client/src/pages/FlightData/tabs/Logs.js @@ -18,6 +18,7 @@ const Logs = () => { const [logs, setLogs] = useState([]) const [autoScroll, setAutoScroll] = useState(true) const [filters, setFilters] = useState(["[INFO ]", "[IMPORTANT]", "[WARNING ]", "[ERROR ]", "[CRITICAL ]"]) + const [types, setTypes] = useState(["(groundstation)", "(autopilot)"]) const autoScrollRef = useRef() const scrollDiv = useRef() const container = useRef() @@ -86,6 +87,7 @@ const Logs = () => { return ( { + console.log(e.target.value) if (e.target.checked) { if (!filters.includes(e.target.value)) { setFilters([...filters, e.target.value]) @@ -109,9 +111,10 @@ const Logs = () => { Error Critical -
+ + window.open("http://localhost:5000/logs")}>Open Log File { scrollDiv.current.scrollIntoView(); setAutoScroll(true) }}>Scroll To End -
+
@@ -144,9 +147,9 @@ const StyledLog = ({ content }) => { } const ScrollButton = styled(Button)` - margin: 2em 0 0 auto; + margin: auto 0 0 auto; width: 75%; - height: 2.5em; + height: 1.5em; ` const StyledContainer = styled.div` diff --git a/server/app.py b/server/app.py index 1f2e7d74..3ee988cc 100644 --- a/server/app.py +++ b/server/app.py @@ -42,7 +42,11 @@ def log_root(message, *args, **kwargs): logger = logging.getLogger("groundstation") logger.setLevel(logging.DEBUG) -formatter = logging.Formatter("[%(levelname)-9s] %(asctime)s %(message)s") + +autopilot = logging.getLogger("autopilot") +autopilot.setLevel(logging.DEBUG) + +formatter = logging.Formatter("[%(levelname)-9s] (%(name)s) %(asctime)s %(message)-500s") # console_handler = logging.StreamHandler(sys.stdout) # console_handler.setLevel(logging.IMPORTANT) @@ -53,17 +57,20 @@ def log_root(message, *args, **kwargs): file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) logger.addHandler(file_handler) +autopilot.addHandler(file_handler) debug_file_handler = logging.FileHandler("logs/debug.log", mode="w") debug_file_handler.setLevel(logging.DEBUG) debug_file_handler.setFormatter(formatter) logger.addHandler(debug_file_handler) +autopilot.addHandler(debug_file_handler) LOG_STREAM = StringIO() string_handler = logging.StreamHandler(LOG_STREAM) string_handler.setLevel(logging.DEBUG) string_handler.setFormatter(formatter) logger.addHandler(string_handler) +autopilot.addHandler(string_handler) logger.info("STARTED LOGGING") diff --git a/server/groundstation.py b/server/groundstation.py index 9d99d24c..9cb7682b 100644 --- a/server/groundstation.py +++ b/server/groundstation.py @@ -3,6 +3,8 @@ import time from threading import Thread +import requests + import errors from handlers import DummyUAV, ProdUAV from handlers import DummyUGV, ProdUGV @@ -92,14 +94,26 @@ def ugv_thread(self): time.sleep(0.1) def image_thread(self): - if not self.config["uav"]["images"]["type"] == "prod": # Initialize a socket connection - self.image.socket_connect() + if self.config["uav"]["images"]["type"] == "prod": # Initialize a socket connection + while True: + time.sleep(1) + try: + res = requests.get(f"{self.config['uav']['images']['url']}/last_image") + except Exception: + self.logger.error("[Image] Cannot connect to FlightSoftware, retrying in 5 seconds") + time.sleep(4) + continue + if res.status_code != 200: + self.logger.error("[Image] Unable to retreive image count, retrying in 5 seconds") + time.sleep(4) + continue + img_cnt = res.json()["result"] + if img_cnt != self.image.img_count: + self.image.retreive_image(img_cnt) else: # Use a dummy connection while True: - run = self.image.retreive_images() - if run: - self.logger.info("[Image] Successfully identified ODLC from Image") - time.sleep(0.1) + self.image.dummy_retreive_image() + time.sleep(2) def async_calls(self): print("╔═══ STARTING ASYNC THREADS") diff --git a/server/handlers/image.py b/server/handlers/image.py index a76c68f6..aa945403 100644 --- a/server/handlers/image.py +++ b/server/handlers/image.py @@ -1,9 +1,13 @@ +from __future__ import annotations import base64 import logging import string +import time +import typing from random import random, randint, choice import eventlet +import requests import socketio from dotenv import load_dotenv @@ -11,53 +15,35 @@ load_dotenv() +if typing.TYPE_CHECKING: + from groundstation import GroundStation + @decorate_all_functions(log, logging.getLogger("groundstation")) class ImageHandler: def __init__(self, gs, config): self.logger = logging.getLogger("groundstation") - self.gs = gs + self.gs: GroundStation = gs self.config = config - self.sio = self.app = None + self.img_count = -1 print("╠ CREATED IMAGE HANDLER") self.logger.info("CREATED IMAGE HANDLER") def initialize(self): - self.sio = socketio.Server(max_http_buffer_size=40_000_000) - self.app = socketio.WSGIApp(self.sio) - - @self.sio.event - def connect(sid, _): - self.logger.important("[Image] Socketio connection established (sid: %s)", sid) - - @self.sio.event - def image(sid, data): - self.logger.debug( - "[Image] Successfully retreived image from socketio connection (sid: %s)", sid - ) - img = data["image"] - if self.process_image(img): - self.logger.info("[Image] Successfully identified ODLC from Image (sid: %s)", sid) - - @self.sio.event - def disconnect(sid): - self.logger.warning("[Image] Lost socketio connection (sid: %s)", sid) - print("╠ INITIALIZED IMAGE HANDLER") self.logger.info("INITIALIZED IMAGE HANDLER") - def socket_connect(self): - eventlet.wsgi.server( - eventlet.listen( - (self.config["uav"]["images"]["host"], self.config["uav"]["images"]["port"]) - ), - self.app, - log_output=False, - ) + def retreive_image(self, img_cnt): + for i in range(self.img_count, img_cnt): + self.logger.info("Retreiving image %s", i) + img_res = requests.get(self.config["uav"]["images"]["url"] + f"/image/{i}") + if img_res.status_code == 200 and self.process_image(img_res.content): + self.logger.info("[Image] Successfully identified ODLC from Image") + time.sleep(1) + self.img_count = img_cnt - # When socket connection is not used - def retreive_images(self): - # Retreives Image from UAV + def dummy_retreive_image(self): + # Retrieves Image from UAV if random() < 1: # Every image (until CV implementation) with open("assets/odlc_images/sample.png", "rb") as image_file: img = base64.b64encode(image_file.read()) @@ -67,14 +53,14 @@ def retreive_images(self): return False def process_image(self, image): - if random() < 0.05: # 5% chance that the image is of an ODLC + if random() < 1: # 5% chance that the image is of an ODLC # Dummy Data - self.gs.interop.odlc.add_to_queue( + self.gs.interop.odlc_add_to_queue( image, "standard", - random() * 90, - random() * -90, - randint(0, 360), + self.gs.uav.lat, + self.gs.uav.lon, + self.gs.uav.orientation["yaw"], choice( [ "circle", @@ -119,7 +105,6 @@ def process_image(self, image): "orange", ] ), - log=False, ) return True return False diff --git a/server/handlers/interop.py b/server/handlers/interop.py index cced59bb..2711384c 100644 --- a/server/handlers/interop.py +++ b/server/handlers/interop.py @@ -1,7 +1,9 @@ +from __future__ import annotations import base64 import json import logging import os +import typing from datetime import datetime, timedelta, date from auvsi_suas.client import client @@ -12,6 +14,9 @@ from errors import InvalidRequestError, InvalidStateError, GeneralError, ServiceUnavailableError from handlers.utils import decorate_all_functions, log +if typing.TYPE_CHECKING: + from groundstation import GroundStation + def json_serial(obj): if isinstance(obj, (datetime, date)): @@ -55,7 +60,7 @@ def __init__(self, gs, config): self.logger = logging.getLogger("groundstation") print("╠ CREATED INTEROP HANDLER") self.logger.info("CREATED INTEROP ERROR") - self.gs = gs + self.gs: GroundStation = gs self.config = config self.mission_id = self.config["interop"]["mission_id"] self.login_status = False @@ -206,7 +211,7 @@ def odlc_get_queue(self, filter_val=3): def odlc_add_to_queue( self, - image: str = None, + image: bytes = None, type_: str = None, lat: float = None, lon: float = None, @@ -219,7 +224,7 @@ def odlc_add_to_queue( ): try: with open(f"assets/odlc_images/{len(self.odlc_queued_data)}.png", "wb") as file: - file.write(base64.b64decode(image)) + file.write(image) base_obj = { "created": datetime.now(), "auto_submit": datetime.now() + timedelta(minutes=5), diff --git a/server/handlers/uav/dummy.py b/server/handlers/uav/dummy.py index db0cd3b2..853357f4 100644 --- a/server/handlers/uav/dummy.py +++ b/server/handlers/uav/dummy.py @@ -1,8 +1,10 @@ +from __future__ import annotations import json import logging import math import os import random +import typing from dronekit import Command from pymavlink import mavutil as uavutil @@ -10,6 +12,9 @@ from errors import GeneralError, ServiceUnavailableError, InvalidRequestError from handlers.utils import decorate_all_functions, log +if typing.TYPE_CHECKING: + from groundstation import GroundStation + COMMANDS = { "WAYPOINT": uavutil.mavlink.MAV_CMD_NAV_WAYPOINT, "GEOFENCE": uavutil.mavlink.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_INCLUSION, @@ -68,7 +73,7 @@ class DummyUAVHandler: def __init__(self, gs, config): self.logger = logging.getLogger("groundstation") - self.gs = gs + self.gs: GroundStation = gs self.config = config self.port = self.config["uav"]["telemetry"]["port"] self.serial = self.config["uav"]["telemetry"]["serial"] diff --git a/server/handlers/uav/prod.py b/server/handlers/uav/prod.py index dc8902f8..0149ac6a 100644 --- a/server/handlers/uav/prod.py +++ b/server/handlers/uav/prod.py @@ -1,7 +1,9 @@ +from __future__ import annotations import json import logging import math import os +import typing from typing import Optional from dronekit import connect, Command, VehicleMode, Vehicle @@ -10,6 +12,9 @@ from errors import GeneralError, InvalidRequestError, InvalidStateError from handlers.utils import decorate_all_functions, log +if typing.TYPE_CHECKING: + from groundstation import GroundStation + BAUDRATE = 57600 COMMANDS = { @@ -139,7 +144,7 @@ class UAVHandler: def __init__(self, gs, config): self.logger = logging.getLogger("groundstation") - self.gs = gs + self.gs: GroundStation = gs self.config = config self.port = self.config["uav"]["telemetry"]["port"] self.serial = self.config["uav"]["telemetry"]["serial"] @@ -271,11 +276,11 @@ def set_home(self): def calibrate(self): try: - self.vehicle.send_calibrate_accelerometer() + self.vehicle.send_calibrate_accelerometer(simple=True) self.vehicle.send_calibrate_barometer() self.vehicle.send_calibrate_gyro() - self.vehicle.send_calibrate_magnetometer() - self.vehicle.send_calibrate_vehicle_level() + # self.vehicle.send_calibrate_magnetometer() + # self.vehicle.send_calibrate_vehicle_level() return {} except Exception as e: raise GeneralError(str(e)) from e diff --git a/server/handlers/ugv/dummy.py b/server/handlers/ugv/dummy.py index 3d4dc1f9..dc53de55 100644 --- a/server/handlers/ugv/dummy.py +++ b/server/handlers/ugv/dummy.py @@ -1,8 +1,10 @@ +from __future__ import annotations import json import logging import math import os import random +import typing from dronekit import Command from pymavlink import mavutil as uavutil @@ -10,6 +12,9 @@ from errors import GeneralError, ServiceUnavailableError, InvalidRequestError from handlers.utils import decorate_all_functions, log +if typing.TYPE_CHECKING: + from groundstation import GroundStation + COMMANDS = { # Takeoff will be initiated using a Flight Mode # "TAKEOFF": uavutil.mavlink.MAV_CMD_NAV_TAKEOFF, @@ -69,7 +74,7 @@ def readmission(filename): class DummyUGVHandler: def __init__(self, gs, config): self.logger = logging.getLogger("groundstation") - self.gs = gs + self.gs: GroundStation = gs self.config = config self.port = self.config["ugv"]["telemetry"]["port"] self.serial = self.config["ugv"]["telemetry"]["serial"] @@ -116,8 +121,12 @@ def update(self): if not self.droppos: self.droppos = self.gs.interop.get_data("ugv") self.droppos = self.droppos["result"] - self.lat = self.droppos["drop"]["latitude"] + (random.random() - 0.5) / 2000 - self.lon = self.droppos["drop"]["longitude"] + (random.random() - 0.5) / 2000 + if not self.lat: + self.lat = self.droppos["drop"]["latitude"]# + (random.random() - 0.5) / 2000 + self.lon = self.droppos["drop"]["longitude"]# + (random.random() - 0.5) / 2000 + self.lat += (random.random() - 0.5) / 80000 + self.lon += (random.random() - 0.5) / 80000 + # ^^ remove after frr x_dist = self.droppos["drop"]["latitude"] - self.lat y_dist = self.droppos["drop"]["longitude"] - self.lon angle = math.atan2(y_dist, x_dist) diff --git a/server/handlers/ugv/prod.py b/server/handlers/ugv/prod.py index c1c2171b..e4bb5f84 100644 --- a/server/handlers/ugv/prod.py +++ b/server/handlers/ugv/prod.py @@ -1,7 +1,9 @@ +from __future__ import annotations import json import logging import math import os +import typing from typing import Optional from dronekit import connect, Command, VehicleMode, Vehicle @@ -10,6 +12,9 @@ from errors import GeneralError, InvalidRequestError, InvalidStateError from handlers.utils import decorate_all_functions, log +if typing.TYPE_CHECKING: + from groundstation import GroundStation + SERIAL_PORT = "/dev/ttyACM0" BAUDRATE = 115200 @@ -89,7 +94,7 @@ class UGVHandler: def __init__(self, gs, config): self.logger = logging.getLogger("groundstation") - self.gs = gs + self.gs: GroundStation = gs self.config = config self.port = self.config["ugv"]["telemetry"]["port"] self.serial = self.config["ugv"]["telemetry"]["serial"] diff --git a/server/handlers/utils.py b/server/handlers/utils.py index b7ddf68a..c07b4532 100644 --- a/server/handlers/utils.py +++ b/server/handlers/utils.py @@ -4,7 +4,7 @@ from logging import Logger from typing import Callable -log_exempt = ("update", "stats", "quick", "get_armed", "submit_telemetry", "odlc_get_queue") +log_exempt = ("update", "stats", "quick", "get_armed", "submit_telemetry", "odlc_get_queue", "odlc_add_to_queue", "process_image") def log(func: Callable, logger: Logger) -> Callable: diff --git a/server/sample.config.json b/server/sample.config.json index 3e1e34b0..0e2ce851 100644 --- a/server/sample.config.json +++ b/server/sample.config.json @@ -7,8 +7,7 @@ }, "images": { "type": "dummy", - "host": "", - "port": 4000 + "url": "http://192.168.1.99:4000" } }, "ugv": {