From 7ad48da054553ca26cfaadeaaac9b211cfa445e2 Mon Sep 17 00:00:00 2001 From: Romain Bazile Date: Fri, 18 Jun 2021 11:21:35 +0200 Subject: [PATCH] imager: move and split into submodules (cherry picked from commit 0716f25fb7b78140ed1960445810557ad9e6840c) --- .../{imager.py => imager/__init__.py} | 111 +++--------------- .../planktoscope/{ => imager}/raspimjpeg.py | 12 +- .../state_machine.py} | 0 scripts/planktoscope/imager/streamer.py | 60 ++++++++++ 4 files changed, 85 insertions(+), 98 deletions(-) rename scripts/planktoscope/{imager.py => imager/__init__.py} (88%) rename scripts/planktoscope/{ => imager}/raspimjpeg.py (98%) rename scripts/planktoscope/{imager_state_machine.py => imager/state_machine.py} (100%) create mode 100644 scripts/planktoscope/imager/streamer.py diff --git a/scripts/planktoscope/imager.py b/scripts/planktoscope/imager/__init__.py similarity index 88% rename from scripts/planktoscope/imager.py rename to scripts/planktoscope/imager/__init__.py index a8608f0..ca02410 100644 --- a/scripts/planktoscope/imager.py +++ b/scripts/planktoscope/imager/__init__.py @@ -26,10 +26,13 @@ import planktoscope.mqtt import planktoscope.light # import planktoscope.streamer -import planktoscope.imager_state_machine +import planktoscope.imager.state_machine # import raspimjpeg module -import planktoscope.raspimjpeg +import planktoscope.imager.raspimjpeg + +# import streamer module +import planktoscope.imager.streamer # Integrity verification module import planktoscope.integrity @@ -38,89 +41,11 @@ import planktoscope.integrity import planktoscope.uuidName -################################################################################ -# Streaming PiCamera over server -################################################################################ -import socketserver -import http.server +# Libraries for the streaming server import threading import functools -################################################################################ -# Classes for the PiCamera Streaming -################################################################################ -class StreamingHandler(http.server.BaseHTTPRequestHandler): - # Webpage content containing the PiCamera Streaming - PAGE = """\ - - - PlanktonScope v2 | PiCamera Streaming - - - - - - """ - - def __init__(self, delay, *args, **kwargs): - self.delay = delay - super(StreamingHandler, self).__init__(*args, **kwargs) - - @logger.catch - def do_GET(self): - if self.path == "/": - self.send_response(301) - self.send_header("Location", "/index.html") - self.end_headers() - elif self.path == "/index.html": - content = self.PAGE.encode("utf-8") - self.send_response(200) - self.send_header("Content-Type", "text/html") - self.send_header("Content-Length", len(content)) - self.end_headers() - self.wfile.write(content) - elif self.path == "/stream.mjpg": - self.send_response(200) - self.send_header("Age", 0) - self.send_header("Cache-Control", "no-cache, private") - self.send_header("Pragma", "no-cache") - self.send_header( - "Content-Type", "multipart/x-mixed-replace; boundary=FRAME" - ) - - self.end_headers() - try: - while True: - try: - with open("/dev/shm/mjpeg/cam.jpg", "rb") as jpeg: # nosec - frame = jpeg.read() - except FileNotFoundError as e: - logger.error(f"Camera has not been started yet") - time.sleep(5) - except Exception as e: - logger.exception(f"An exception occured {e}") - else: - self.wfile.write(b"--FRAME\r\n") - self.send_header("Content-Type", "image/jpeg") - self.send_header("Content-Length", len(frame)) - self.end_headers() - self.wfile.write(frame) - self.wfile.write(b"\r\n") - time.sleep(self.delay) - - except BrokenPipeError as e: - logger.info(f"Removed streaming client {self.client_address}") - else: - self.send_error(404) - self.end_headers() - - -class StreamingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): - allow_reuse_address = True - daemon_threads = True - - logger.info("planktoscope.imager is loaded") ################################################################################ @@ -158,7 +83,7 @@ class ImagerProcess(multiprocessing.Process): self.__camera_type = configuration.get("camera_type", self.__camera_type) self.stop_event = stop_event - self.__imager = planktoscope.imager_state_machine.Imager() + self.__imager = planktoscope.imager.state_machine.Imager() self.__img_goal = 0 self.__img_done = 0 self.__sleep_before = None @@ -170,7 +95,7 @@ class ImagerProcess(multiprocessing.Process): # Initialise the camera and the process # Also starts the streaming to the temporary file - self.__camera = planktoscope.raspimjpeg.raspimjpeg() + self.__camera = planktoscope.imager.raspimjpeg.raspimjpeg() try: self.__camera.start() @@ -179,7 +104,7 @@ class ImagerProcess(multiprocessing.Process): f"An exception has occured when starting up raspimjpeg: {e}" ) try: - self.__camera.start(true) + self.__camera.start(True) except Exception as e: logger.exception( f"A second exception has occured when starting up raspimjpeg: {e}" @@ -281,7 +206,7 @@ class ImagerProcess(multiprocessing.Process): logger.error(f"The received message has the wrong argument {last_message}") self.imager_client.client.publish("status/imager", '{"status":"Error"}') return - self.__imager.change(planktoscope.imager_state_machine.Imaging) + self.__imager.change(planktoscope.imager.state_machine.Imaging) # Get duration to wait before an image from the different received arguments self.__sleep_before = float(last_message["sleep"]) @@ -496,7 +421,7 @@ class ImagerProcess(multiprocessing.Process): ) if self.__imager.state.name == "waiting": if self.imager_client.msg["payload"]["status"] == "Done": - self.__imager.change(planktoscope.imager_state_machine.Capture) + self.__imager.change(planktoscope.imager.state_machine.Capture) self.imager_client.client.unsubscribe("status/pump") else: logger.info( @@ -634,7 +559,7 @@ class ImagerProcess(multiprocessing.Process): self.__pump_message() - self.__imager.change(planktoscope.imager_state_machine.Waiting) + self.__imager.change(planktoscope.imager.state_machine.Waiting) def __state_capture(self): planktoscope.light.imaging() @@ -695,7 +620,7 @@ class ImagerProcess(multiprocessing.Process): self.__pump_message() - self.__imager.change(planktoscope.imager_state_machine.Waiting) + self.__imager.change(planktoscope.imager.state_machine.Waiting) def __capture_error(self, message=""): logger.error(f"An error occurred during the capture: {message}") @@ -709,7 +634,7 @@ class ImagerProcess(multiprocessing.Process): self.__img_done = 0 self.__img_goal = 0 self.__error = 0 - self.__imager.change(planktoscope.imager_state_machine.Stop) + self.__imager.change(planktoscope.imager.state_machine.Stop) else: self.__error += 1 self.imager_client.client.publish( @@ -762,10 +687,12 @@ class ImagerProcess(multiprocessing.Process): logger.info("Starting the streaming server thread") address = ("", 8000) - fps = 16 + fps = 15 refresh_delay = 1 / fps - handler = functools.partial(StreamingHandler, refresh_delay) - server = StreamingServer(address, handler) + handler = functools.partial( + planktoscope.imager.streamer.StreamingHandler, refresh_delay + ) + server = planktoscope.imager.streamer.StreamingServer(address, handler) self.streaming_thread = threading.Thread( target=server.serve_forever, daemon=True ) diff --git a/scripts/planktoscope/raspimjpeg.py b/scripts/planktoscope/imager/raspimjpeg.py similarity index 98% rename from scripts/planktoscope/raspimjpeg.py rename to scripts/planktoscope/imager/raspimjpeg.py index 22e3a12..2e0903f 100644 --- a/scripts/planktoscope/raspimjpeg.py +++ b/scripts/planktoscope/imager/raspimjpeg.py @@ -53,12 +53,7 @@ class raspimjpeg(object): if force: # let's kill all rogue Raspimjpeg first try: - subprocess.run( # nosec - "sudo killall -9 raspimjpeg".split(), - shell=True, - timeout=1, - check=True, - ) + self.killall() except Exception as e: logger.exception(f"Killing Raspimjpeg failed because of {e}") # The input to this call are perfectly controlled @@ -494,3 +489,8 @@ class raspimjpeg(object): """Kill the process.""" logger.debug("Killing raspimjpeg in a very dirty way") self.__process.terminate() + + def killall(self): + """Literally erases the raspimjpeg process(es)""" + logger.debug("Killing raspimjpeg in a very ugly dirty way") + subprocess.run("sudo killall -q -9 raspimjpeg".split(), timeout=1) # nosec diff --git a/scripts/planktoscope/imager_state_machine.py b/scripts/planktoscope/imager/state_machine.py similarity index 100% rename from scripts/planktoscope/imager_state_machine.py rename to scripts/planktoscope/imager/state_machine.py diff --git a/scripts/planktoscope/imager/streamer.py b/scripts/planktoscope/imager/streamer.py new file mode 100644 index 0000000..06185e3 --- /dev/null +++ b/scripts/planktoscope/imager/streamer.py @@ -0,0 +1,60 @@ +from loguru import logger + +import time + +import socketserver +import http.server + + +################################################################################ +# Classes for the PiCamera Streaming +################################################################################ +class StreamingHandler(http.server.BaseHTTPRequestHandler): + def __init__(self, delay, *args, **kwargs): + self.delay = delay + super(StreamingHandler, self).__init__(*args, **kwargs) + + @logger.catch + def do_GET(self): + if self.path == "/": + self.send_response(301) + self.send_header("Location", "/stream.mjpg") + self.end_headers() + elif self.path == "/stream.mjpg": + self.send_response(200) + self.send_header("Age", 0) + self.send_header("Cache-Control", "no-cache, private") + self.send_header("Pragma", "no-cache") + self.send_header( + "Content-Type", "multipart/x-mixed-replace; boundary=FRAME" + ) + self.end_headers() + try: + while True: + try: + with open("/dev/shm/mjpeg/cam.jpg", "rb") as jpeg: # nosec + frame = jpeg.read() + except FileNotFoundError as e: + logger.error(f"Camera has not been started yet") + time.sleep(5) + except Exception as e: + logger.exception(f"An exception occured {e}") + else: + self.wfile.write(b"--FRAME\r\n") + self.send_header("Content-Type", "image/jpeg") + self.send_header("Content-Length", len(frame)) + self.end_headers() + self.wfile.write(frame) + self.wfile.write(b"\r\n") + time.sleep(self.delay) + + except BrokenPipeError as e: + logger.info(f"Removed streaming client {self.client_address}") + else: + self.send_error(404) + self.end_headers() + + +class StreamingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): + allow_reuse_address = True + daemon_threads = True