From 920d1618c4936c49ddd883cf8e38bd9b2c435149 Mon Sep 17 00:00:00 2001 From: max-at-groundlight <136360589+max-at-groundlight@users.noreply.github.com> Date: Tue, 29 Aug 2023 07:05:36 -0700 Subject: [PATCH] Polish UI, polish greengrass recipe (#31) * added link to detector on page, new greengrass recipe * update readme * added smtp host, fixed img overflow * now new detectors and sources start with a -1 source * added install instructions back, added volume arg * kept backend from failing without config * non failing backend, removed useless log from detectors page --------- Co-authored-by: Max McKelvey --- .dockerignore | 3 +- .gitignore | 1 + README.md | 2 +- api/gl_process.py | 8 +++ api/index.py | 86 +++++++++++++++++++++---- api/notifications.py | 3 +- app/detectors/page.tsx | 58 ++++++++--------- app/sources/page.tsx | 2 +- components/EditDetectorOverlay.tsx | 17 ++--- components/EditNotificationsOverlay.tsx | 11 +++- deploy/greengrass-recipe.yaml | 44 ++++++------- utils/types.ts | 3 +- 12 files changed, 157 insertions(+), 81 deletions(-) diff --git a/.dockerignore b/.dockerignore index 16c7c6a..1d9806a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ /api/gl_config.json -/api/*.json \ No newline at end of file +/api/*.json +/api/external \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3085fd3..c0b049e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ yarn-error.log* .env*.local /api/gl_config.json /api/*.json +/api/external # vercel .vercel diff --git a/README.md b/README.md index 919dae2..cde31d7 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ There are several ways to deploy the code: ### Running with AWS Greengrass -Before creating the component, you must run `sudo usermod -aG docker ggc_user` on your Greengrass device to allow the Greengrass service to access the host's Docker daemon. +Before creating the component, run `sudo usermod -aG docker ggc_user` on your Greengrass device to allow the Greengrass service to access the host's Docker daemon. 1. Create a new Greengrass Component 2. Select "Enter recipe as YAML" diff --git a/api/gl_process.py b/api/gl_process.py index c26d345..2958495 100644 --- a/api/gl_process.py +++ b/api/gl_process.py @@ -37,6 +37,10 @@ def run_process(idx: int, logger: logging.Logger, detector: dict, api_key: str, websocket_cancel_queue: multiprocessing.Queue, websocket_response_queue: multiprocessing.Queue): + if detector["config"]["imgsrc_idx"] == -1: + logger.error(f"Detector {detector['id']} has no image source.") + return + trigger_type = detector["config"]["trigger_type"] poll_delay = 0.5 @@ -79,6 +83,10 @@ def run_process(idx: int, logger: logging.Logger, detector: dict, api_key: str, continue uuid = uuid4().hex + if frame is None: + logger.warn("Frame recieved as None.") + continue + # send to groundlight query = gl.submit_image_query(det, frame, 0) # default wait is 30s diff --git a/api/index.py b/api/index.py index 05a1d22..8561710 100644 --- a/api/index.py +++ b/api/index.py @@ -20,7 +20,7 @@ class Config(pydantic.BaseModel): enabled: bool imgsrc_idx: int - vid_config: dict + # vid_config: dict image: Optional[str] trigger_type: str cycle_time: Union[int, None] @@ -62,6 +62,14 @@ def get_base64_img(g: FrameGrabber) -> Union[str, None]: def fetch_config() -> dict: with open("./api/gl_config.json", "r") as f: return json.load(f) + +def fetch_external_config_yaml() -> dict: + with open("./api/external/config.yaml", "r") as f: + return yaml.safe_load(f) + +def fetch_external_config_json() -> dict: + with open("./api/external/config.json", "r") as f: + return json.load(f) def store_config(config: dict): with open("./api/gl_config.json", "w") as f: @@ -120,10 +128,34 @@ def make_grabbers(): return grabbers +def make_images_for_detectors(): + config = fetch_config() + if "detectors" not in config: + return + for d in config["detectors"]: + try: + if "imgsrc_idx" in d["config"] and "image" not in d["config"] and d["config"]["imgsrc_idx"] != "-1": + img = get_base64_img(app.ALL_GRABBERS[d["config"]["imgsrc_idx"]]) + if img: + d["config"]["image"] = img + except Exception as e: + logger.logger.error("Failed to load image, error: " + str(e)) + + store_config(config) + def startup(): logger.logger.info("Loading config...") try: - config = fetch_config() + try: + config = fetch_config() + except: + try: + config = fetch_external_config_yaml() + logger.logger.info("Loaded external yaml config") + except: + config = fetch_external_config_json() + logger.logger.info("Loaded external json config") + store_config(config) detectors = config["detectors"] if "detectors" in config else [] api_key = config["api_key"] if "api_key" in config else None endpoint = config["endpoint"] if "endpoint" in config else None @@ -141,12 +173,17 @@ def startup(): try: for d in detectors: - if "imgsrc_idx" in d["config"] and "image" not in d["config"] and d["config"]["imgsrc_idx"] != "-1": - img = get_base64_img(app.ALL_GRABBERS[d["config"]["imgsrc_idx"]]) - if img: - d["config"]["image"] = img - except Exception as e: - logger.logger.error("Failed to load images, error: " + str(e)) + try: + if "imgsrc_idx" in d["config"] and "image" not in d["config"] and d["config"]["imgsrc_idx"] != "-1": + img = get_base64_img(app.ALL_GRABBERS[d["config"]["imgsrc_idx"]]) + if img: + d["config"]["image"] = img + except Exception as e: + logger.logger.error("Failed to load image, error: " + str(e)) + + store_config(config) + except: + pass startup() @@ -200,10 +237,12 @@ def make_new_detector(detector: Detector): return "Failed" @app.post("/api/config/detectors") -def post_detectors(detectors: DetectorList): +# def post_detectors(detectors: DetectorList): +def post_detectors(detectors: dict): config = fetch_config() try: - config["detectors"] = json.loads(detectors.json())["detectors"] + # config["detectors"] = json.loads(detectors.json())["detectors"] + config["detectors"] = detectors["detectors"] endpoint = config["endpoint"] if "endpoint" in config else None api_key = config["api_key"] if "api_key" in config else None except Exception as e: @@ -247,7 +286,21 @@ def get_cameras(): @app.post("/api/cameras/autodetect") def autodetect_cameras(): - new_grabbers: List[FrameGrabber] = framegrab.FrameGrabber.autodiscover().values() + new_grabbers: List[FrameGrabber] = list(framegrab.FrameGrabber.autodiscover().values()) + time.sleep(0.2) + indicies_to_remove = [] + for i in range(len(new_grabbers)): + try: + new_grabbers[i].grab() + except: + indicies_to_remove.append(i) + for i in indicies_to_remove[::-1]: + try: + new_grabbers[i].release() + except: + pass + new_grabbers.pop(i) + app.ALL_GRABBERS.extend(new_grabbers) set_in_config({"image_sources": list(map(lambda g: g.config, app.ALL_GRABBERS))}) @@ -304,7 +357,10 @@ async def websocket_queue_runner(logger): logger.info("Taking photo") app.DETECTOR_GRAB_NOTIFY_QUEUES[i].get_nowait() d = list(filter(lambda d: d["config"]["enabled"], app.DETECTOR_CONFIG["detectors"]))[i] - img = app.ALL_GRABBERS[d["config"]["imgsrc_idx"]].grab() + try: + img = app.ALL_GRABBERS[d["config"]["imgsrc_idx"]].grab() + except: + img = None app.DETECTOR_PHOTO_QUEUES[i].put_nowait(img) while not app.WEBSOCKET_RESPONSE_QUEUE.empty(): res = app.WEBSOCKET_RESPONSE_QUEUE.get() @@ -330,6 +386,12 @@ async def app_startup(): @app.on_event("shutdown") async def app_shutdown(): + logger.logger.info("Shutting down grabbers and detectors...") + for g in app.ALL_GRABBERS: + try: + g.release() + except: + pass for p in app.DETECTOR_PROCESSES: if p.is_alive(): p.kill() diff --git a/api/notifications.py b/api/notifications.py index 8598dbe..2b03099 100644 --- a/api/notifications.py +++ b/api/notifications.py @@ -82,7 +82,8 @@ def send_email(det_name: str, query: str, image, label: str, options: dict): # Log in to server using secure context and send email context = ssl.create_default_context() - with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server: + host = "host" in options and options["host"] or "smtp.gmail.com" + with smtplib.SMTP_SSL(host, 465, context=context) as server: server.login(sender_email, app_password) server.sendmail(sender_email, receiver_email, text) diff --git a/app/detectors/page.tsx b/app/detectors/page.tsx index fc7aa80..c0923ed 100644 --- a/app/detectors/page.tsx +++ b/app/detectors/page.tsx @@ -4,7 +4,7 @@ import { Dropdown } from "@/components/Dropdown"; import { EditDetectorOverlay } from "@/components/EditDetectorOverlay"; import { EditNotificationsOverlay } from "@/components/EditNotificationsOverlay"; import { BASE_SERVER_URL } from "@/utils/config"; -import { ArrowPathIcon, Cog6ToothIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; +import { ArrowPathIcon, ArrowTrendingUpIcon, ArrowUpLeftIcon, ArrowUpRightIcon, Cog6ToothIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; import { useEffect, useState } from "react"; import ReactSwitch from "react-switch"; @@ -16,6 +16,7 @@ export default function Home() { const [showEditOverlay, setShowEditOverlay] = useState(false); const [editOverlayIndex, setEditOverlayIndex] = useState(0); const [lastButtonWasAdd, setLastButtonWasAdd] = useState(false); + const [lastAddButtonWasNew, setLastAddButtonWasNew] = useState(false); const [imageSources, setImageSources] = useState([]); const [camerasWaiting, setCamerasWaiting] = useState([]); // images that are waiting for a response const [showEditNotificationsOverlay, setShowEditNotificationsOverlay] = useState(false); @@ -118,7 +119,6 @@ export default function Home() { let detectors_copy = [...detectors]; detectors_copy[det_idx].config.imgsrc_idx = imgsrc_idx; detectors_copy[det_idx].config.image = imageSources[imgsrc_idx].image; - detectors_copy[det_idx].config.vid_config = imageSources[imgsrc_idx].config; setDetectors(detectors_copy); saveDetectors(detectors_copy); } @@ -184,6 +184,7 @@ export default function Home() { {group.map((detector, indexB) => ( @@ -272,7 +270,8 @@ export default function Home() { { source.config.name } ) - } selected={detector.config.vid_config.name != "" ? detector.config.vid_config.name : "Choose Img Src"} setSelected={(e, idx) => { + // } selected={detector.config.vid_config.name != "" ? detector.config.vid_config.name : "Choose Img Src"} setSelected={(e, idx) => { + } selected={detector.config.imgsrc_idx >= 0 && imageSources.length > 0 ? imageSources[detector.config.imgsrc_idx].config.name : "Choose Img Src"} setSelected={(e, idx) => { changeDetectorImgSrc(detector, idx); }} /> @@ -301,6 +300,7 @@ export default function Home() { setEditOverlayIndex(index); setShowEditOverlay(true); setLastButtonWasAdd(false); + setLastAddButtonWasNew(false); }}> @@ -318,18 +318,16 @@ export default function Home() {