Skip to content

Commit

Permalink
added notification capabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
Max McKelvey authored and Max McKelvey committed Aug 1, 2023
1 parent 40e9c2e commit f301ed9
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 32 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ services:
1. Install [Node.js](https://nodejs.org/en/download/) and [Python 3.8+](https://www.python.org/downloads/).

```bash
git clone git@github.com:max-at-groundlight/linux-edgelight-server.git
cd linux-edgelight-server
git clone https://github.com/groundlight/docker-edgelight-server.git
cd docker-edgelight-server
npm install
npm run dev
```
Expand Down
34 changes: 6 additions & 28 deletions api/gl_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import cv2
import groundlight
import multiprocessing
from api.notifications import send_notifications

def frame_to_base64(frame) -> str:
# return str(base64.b64encode(cv2.imencode(".jpg", frame)[1]))
# encode image as jpeg
_, buffer = cv2.imencode('.jpg', frame)
# encode the image as base64
Expand Down Expand Up @@ -54,7 +54,6 @@ def run_process(idx: int, detector: dict, api_key: str, endpoint: str,
if detector["config"]["cycle_time"] < poll_delay:
poll_delay = detector["config"]["cycle_time"]
cycle_time = detector["config"]["cycle_time"]
# delay = lambda: time.sleep(detector["config"]["cycle_time"])
elif trigger_type == "pin":
# TODO: implement
raise ValueError(f"Trigger type [{trigger_type}] not yet supported.")
Expand All @@ -64,8 +63,6 @@ def run_process(idx: int, detector: dict, api_key: str, endpoint: str,
else:
raise ValueError(f"Invalid trigger type: {trigger_type}")

# if "endpoint" in detector and detector.endpoint is not None:
# endpoint = detector.endpoint
gl = groundlight.Groundlight(api_token=api_key, endpoint=endpoint)
det = gl.get_detector(detector["id"])
conf = det.confidence_threshold if det.confidence_threshold is not None else 0.9
Expand All @@ -81,8 +78,6 @@ def run_process(idx: int, detector: dict, api_key: str, endpoint: str,
continue
uuid = uuid4().hex

# websocket_metadata_queue.put(detector)

# send to groundlight
query = gl.submit_image_query(det, frame, 0) # default wait is 30s

Expand All @@ -103,7 +98,7 @@ def run_process(idx: int, detector: dict, api_key: str, endpoint: str,
# poll for result until timeout
while should_continue():
query = gl.get_image_query(query.id)
if (query.result.confidence is not None and query.result.confidence > conf and not has_cancelled) or (query.result.confidence is None and query.result.label is not None and query.result.label != "QUERY_FAIL" and not has_cancelled):
if not has_cancelled and ((query.result.confidence is not None and query.result.confidence > conf) or (query.result.confidence is None and query.result.label is not None and query.result.label != "QUERY_FAIL" )):
websocket_cancel_queue.put({
"cancel": True,
"confidence": query.result.confidence,
Expand All @@ -116,30 +111,14 @@ def run_process(idx: int, detector: dict, api_key: str, endpoint: str,
"label": query.result.label,
})
has_cancelled = True
if "notifications" in detector:
send_notifications(detector["name"], detector["query"], query.result.label, detector["notifications"], frame)
delay()

retry_time = time.time() + cycle_time

# wait for next cycle
# delay()
# if not websocket_response_queue.empty():
# res = websocket_response_queue.get()
# label: str = res["label"].upper()
# res_uuid: str = res["uuid"]
# if res_uuid == uuid:
# if label == "YES" or label == "NO":
# gl.add_label(query, label)
# elif label == "PASS":
# gl.add_label(query, "YES")
# elif label == "FAIL":
# gl.add_label(query, "NO")
# else:
# print(f"Invalid response: {label}")
# else:
# print(f"UUID mismatch: {res_uuid} != {uuid}")

# query = gl.get_image_query(query.id)d

if not has_cancelled:
# Cancel previous query if it hasn't been cancelled yet
websocket_cancel_queue.put({
"cancel": True,
"confidence": query.result.confidence,
Expand All @@ -149,5 +128,4 @@ def run_process(idx: int, detector: dict, api_key: str, endpoint: str,
"det_query": detector["query"],
"det_idx": idx,
"imgsrc_idx": detector["config"]["imgsrc_idx"],
# "label": query.result.label,
})
5 changes: 4 additions & 1 deletion api/index.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass
import multiprocessing
import time
from typing import List, Union
from typing import List, Optional, Union
from fastapi import FastAPI, WebSocket
import json
from fastapi.websockets import WebSocketState
Expand All @@ -15,6 +15,8 @@
import base64
import asyncio

from api.notifications import send_notifications

class Config(pydantic.BaseModel):
enabled: bool
imgsrc_idx: int
Expand All @@ -24,6 +26,7 @@ class Config(pydantic.BaseModel):
cycle_time: Union[int, None]
pin: Union[int, None]
pin_active_state: Union[int, None]
notifications: Optional[dict]

class Detector(pydantic.BaseModel):
name: str
Expand Down
134 changes: 134 additions & 0 deletions api/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import smtplib, ssl
import cv2

from email import encoders
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

from twilio.rest import Client
from slack_sdk import WebClient

import requests
import os

def send_notifications(det_name: str, query: str, label: str, options: dict, image):
if "condition" not in options:
return
condition = options["condition"] # "pass", "fail"
if not ((condition == "pass" and label == "YES") or (condition == "fail" and label == "NO")):
return
if "email" in options:
email_options = options["email"]
send_email(det_name, query, image, label, email_options)
if "twilio" in options:
twilio_options = options["twilio"]
send_sms(det_name, query, label, twilio_options)
if "slack" in options:
slack_options = options["slack"]
send_slack(det_name, query, label, slack_options)
if "stacklight" in options:
stacklight_options = options["stacklight"]
post_to_stacklight(det_name, query, label, stacklight_options)

def send_email(det_name: str, query: str, image, label: str, options: dict):
subject = f"Your detector [{det_name}] detected an anomaly"
body = f"Your detector [{det_name}] returned a \"{label}\" result to the query [{query}].\n\nThe image of the anomaly is attached below."
sender_email = options["from_email"]
receiver_email = options["to_email"]
app_password = options["email_password"]

# Create a multipart message and set headers
message = MIMEMultipart()
message["From"] = sender_email
message["To"] = receiver_email
message["Subject"] = subject

# Add body to email
message.attach(MIMEText(body, "plain"))

filename = "test.jpg" # In same directory as script

_, im_buf_arr = cv2.imencode(".jpg", image)
byte_im = im_buf_arr.tobytes()
part = MIMEImage(byte_im)

# Encode file in ASCII characters to send by email
encoders.encode_base64(part)

# Add header as key/value pair to attachment part
part.add_header(
"Content-Disposition",
f"attachment; filename= {filename}",
)

# Add attachment to message and convert message to string
message.attach(part)
text = message.as_string()

# 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:
server.login(sender_email, app_password)
server.sendmail(sender_email, receiver_email, text)

def send_sms(det_name: str, query: str, label: str, options: dict):
account_sid = options["account_sid"]
auth_token = options["auth_token"]
client = Client(account_sid, auth_token)

message = client.messages \
.create(
body=f"Your detector [{det_name}] returned a \"{label}\" result to the query [{query}].",
from_=options["from_number"],
to=options["to_number"]
)

def send_slack(det_name: str, query: str, label: str, options: dict):
client = WebClient(token=options["token"])
response = client.chat_postMessage(
channel=options["channel_id"],
text=f"Your detector [{det_name}] returned a \"{label}\" result to the query [{query}]."
)
assert response["message"]["text"] == f"Your detector [{det_name}] returned a \"{label}\" result."

def post_to_stacklight(det_name: str, query: str, label: str, options: dict):
if "ip" not in options:
id = options["id"]
ap_name = "GL_STACKLIGHT_" + id
ap_password = "gl_stacklight_password_" + id
if "ssid" not in options or "password" not in options:
print("No ssid or password provided")
return
ssid = options["ssid"]
password = options["password"]
try:
wifi_networks = os.popen("nmcli -t -f ssid dev wifi list").read()
if ap_name not in wifi_networks:
return
os.popen(f"nmcli dev wifi connect {ap_name} password {ap_password}")

# push ssid and password to stacklight
res = requests.post(f"http://192.168.4.1:8080", json={"ssid": ssid, "password": password})
if res.status_code != 200:
print("Could not connect to stacklight")
return
res = requests.get(f"http://192.168.4.1:8080/ip")
if res.status_code != 200:
print("Could not connect to stacklight")
return
ip = res.text
except:
try:
os.popen(f"nmcli dev wifi connect {options['ssid']} password {options['password']}")
except:
print("Could not connect to wifi network")
return

else:
ip: str = options["ip"]

port = "8080"

# http post to stacklight
requests.post(f"http://{ip}:{port}/display", data=label)
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ fastapi==0.95.2
uvicorn[standard]
groundlight
framegrab
pypylon
pypylon
slack_sdk
twilio

0 comments on commit f301ed9

Please sign in to comment.