Skip to content

Commit

Permalink
Polish UI, polish greengrass recipe (#31)
Browse files Browse the repository at this point in the history
* 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 <maxmckelvey@Maxs-MBP.positronix.local>
  • Loading branch information
max-at-groundlight and Max McKelvey committed Aug 29, 2023
1 parent 06c232d commit 920d161
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 81 deletions.
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/api/gl_config.json
/api/*.json
/api/*.json
/api/external
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ yarn-error.log*
.env*.local
/api/gl_config.json
/api/*.json
/api/external

# vercel
.vercel
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions api/gl_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
86 changes: 74 additions & 12 deletions api/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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))})

Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion api/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
58 changes: 28 additions & 30 deletions app/detectors/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -16,6 +16,7 @@ export default function Home() {
const [showEditOverlay, setShowEditOverlay] = useState<boolean>(false);
const [editOverlayIndex, setEditOverlayIndex] = useState<number>(0);
const [lastButtonWasAdd, setLastButtonWasAdd] = useState<boolean>(false);
const [lastAddButtonWasNew, setLastAddButtonWasNew] = useState<boolean>(false);
const [imageSources, setImageSources] = useState<CameraType[]>([]);
const [camerasWaiting, setCamerasWaiting] = useState<boolean[]>([]); // images that are waiting for a response
const [showEditNotificationsOverlay, setShowEditNotificationsOverlay] = useState<boolean>(false);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -184,18 +184,16 @@ export default function Home() {
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-4 rounded-lg flex items-center text-sm" onClick={() => {
setShowEditOverlay(true);
setLastButtonWasAdd(true);
setLastAddButtonWasNew(true);
setEditOverlayIndex(detectors.length);
setDetectors((detectors) => detectors.concat({
name: "New Detector",
query: "New Query?",
id: "",
config: {
enabled: false,
imgsrc_idx: 0,
vid_config: detectors[0]?.config?.vid_config ? detectors[0].config.vid_config : {
name: "",
},
image: detectors[0]?.config?.image ? detectors[0].config.image : "",
imgsrc_idx: -1,
image: "",
trigger_type: "time",
cycle_time: 30,
}
Expand All @@ -208,18 +206,16 @@ export default function Home() {
<div className="w-52">
<Dropdown options={availableDetectors.map(d => d.name)} selected="Add Existing Detector" setSelected={(e, idx) => {
setLastButtonWasAdd(true);
setLastAddButtonWasNew(false);
setEditOverlayIndex(detectors.length);
let detectors_copy = detectors.concat({
name: availableDetectors[idx].name,
query: availableDetectors[idx].query,
id: availableDetectors[idx].id,
config: {
enabled: false,
imgsrc_idx: 0,
vid_config: detectors[0]?.config?.vid_config ? detectors[0].config.vid_config : {
name: "webcam",
},
image: detectors[0]?.config?.image ? detectors[0].config.image : "",
imgsrc_idx: -1,
image: "",
trigger_type: "time",
cycle_time: 30,
}
Expand All @@ -235,20 +231,22 @@ export default function Home() {
{detectorsByGroup && detectorsByGroup.map((group, indexA) => (
<div className="flex flex-col items-start" key={indexA}>
<div className="p-1"></div>
{/* <div className="grid grid-cols-2 gap-4 w-full px-4 py-1 border-y-[1px] border-black"> */}
<div className="grid grid-cols-[minmax(0,1fr),minmax(0,1fr),56px] gap-4 w-full px-4 py-1 border-y-[1px] border-black">
<h2 className="text-lg">{group[0].name}</h2>
<h2 className="text-lg">{group[0].query}</h2>
<a
href={"https://app.groundlight.ai/reef/detectors/" + group[0].id}
target="_blank"
className="text-lg hover:bg-gray-200 hover:text-gray-700 rounded-md px-4 py-1 mr-auto"
>
{group[0].name}
<ArrowUpRightIcon className="ml-2 w-5 h-5 inline-block" />
</a>
<h2 className="text-lg py-1">{group[0].query}</h2>
<button className="hover:bg-gray-200 hover:text-gray-700 rounded-md px-2 py-1 font-bold" onClick={() => {
setShowEditNotificationsOverlay(true);
setEditNotificationsOverlayIndex(0);
setEditNotificationsOverlayGroupIndex(indexA);
}}>
<Cog6ToothIcon className="w-6 h-6 m-auto" onClick={() => {
// setShowEditNotificationsOverlay(true);
// setEditNotificationsOverlayIndex(0);
// setEditNotificationsOverlayGroupIndex(indexA);
}} />
<Cog6ToothIcon className="w-6 h-6 m-auto"/>
</button>
</div>
{group.map((detector, indexB) => (
Expand All @@ -272,7 +270,8 @@ export default function Home() {
{ source.config.name }
</div>
)
} 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);
}} />
</div>
Expand Down Expand Up @@ -301,6 +300,7 @@ export default function Home() {
setEditOverlayIndex(index);
setShowEditOverlay(true);
setLastButtonWasAdd(false);
setLastAddButtonWasNew(false);
}}>
<Cog6ToothIcon className="w-6 h-6" />
</button>
Expand All @@ -318,18 +318,16 @@ export default function Home() {
<button className="w-full rounded-lg border-2 bg-gray-200 border-gray-300 text-gray-400 hover:bg-gray-300 h-full flex items-center justify-center" onClick={() => {
setShowEditOverlay(true);
setLastButtonWasAdd(true);
setLastAddButtonWasNew(false);
setEditOverlayIndex(detectors.length);
setDetectors((detectors) => detectors.concat({
name: group[0]?.name ? group[0].name : "New Detector",
query: group[0]?.query ? group[0].query : "New Query?",
id: group[0]?.id ? group[0].id : "",
name: group[0].name,
query: group[0].query,
id: group[0].id,
config: {
enabled: false,
imgsrc_idx: 0,
vid_config: detectors[0]?.config?.vid_config ? detectors[0].config.vid_config : {
name: "webcam",
},
image: detectors[0]?.config?.image ? detectors[0].config.image : "",
imgsrc_idx: -1,
image: "",
trigger_type: "time",
cycle_time: 30,
}
Expand All @@ -343,7 +341,7 @@ export default function Home() {
))}
</div>
{detectors.length > 0 && showEditOverlay &&
<EditDetectorOverlay detector={detectors[editOverlayIndex]} detectors={availableDetectors} index={0} onSave={async (e) => {
<EditDetectorOverlay detector={detectors[editOverlayIndex]} detectors={availableDetectors} index={0} startWithNew={lastAddButtonWasNew} onSave={async (e) => {
if (e.isNewDetector) {
const id = await makeNewDetector(e.detector);
if (id === "Failed") {
Expand Down
2 changes: 1 addition & 1 deletion app/sources/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default function VideoPage() {
}

return (
<main className="flex flex-col items-start px-10 py-5 gap-2 relative">
<main className="flex flex-col items-start px-10 py-5 gap-2 relative overflow-scroll max-h-full">
<h1 className="text-3xl font-semibold">Configure your Image Sources</h1>
<Spinner hidden={cameras !== undefined} />
<div className="flex flex-wrap items-stretch gap-8 mx-10 my-5">
Expand Down
Loading

0 comments on commit 920d161

Please sign in to comment.