This cheeky little counter has all of its logic contained in a server-side component:
+
+ {{ value }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openflexure_microscope/api/example_extensions/custom_element/static/src/main.js b/openflexure_microscope/api/example_extensions/custom_element/static/src/main.js
new file mode 100644
index 00000000..e9e54c9b
--- /dev/null
+++ b/openflexure_microscope/api/example_extensions/custom_element/static/src/main.js
@@ -0,0 +1,7 @@
+import Vue from 'vue';
+import wrap from '@vue/web-component-wrapper';
+import VueWebComponent from './components/VueWebComponent';
+
+const CustomElement = wrap(Vue, VueWebComponent);
+
+window.customElements.define('my-custom-element', CustomElement);
\ No newline at end of file
diff --git a/openflexure_microscope/api/example_extensions/custom_element/static/vue.config.js b/openflexure_microscope/api/example_extensions/custom_element/static/vue.config.js
new file mode 100644
index 00000000..0ed27336
--- /dev/null
+++ b/openflexure_microscope/api/example_extensions/custom_element/static/vue.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ productionSourceMap: false
+};
\ No newline at end of file
diff --git a/openflexure_microscope/api/v2/views/streams.py b/openflexure_microscope/api/v2/views/streams.py
index 4382e2ab..73ffd258 100644
--- a/openflexure_microscope/api/v2/views/streams.py
+++ b/openflexure_microscope/api/v2/views/streams.py
@@ -18,7 +18,16 @@ class MjpegStream(View):
@doc_response(200, mimetype="multipart/x-mixed-replace")
def get(self):
"""
- MJPEG stream from the microscope camera
+ MJPEG stream from the microscope camera.
+
+ Note: While the code actually getting frame data from a camera and storing it in
+ camera.frame runs in a thread, the gen(microscope.camera) generator does not.
+ This response is therefore blocking. The generator just yields the most recent
+ frame from the camera object, passed to the Flask response, and then repeats until
+ the connection is closed.
+
+ Without monkey patching with gevent, or using a native threaded server, the stream
+ will block all proceeding requests.
"""
microscope = find_component("org.openflexure.microscope")
# Restart stream worker thread
diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py
index e3da4131..e203f44f 100644
--- a/openflexure_microscope/camera/base.py
+++ b/openflexure_microscope/camera/base.py
@@ -3,6 +3,7 @@ import time
import os
import shutil
import threading
+import gevent.event
import datetime
import logging
@@ -61,7 +62,7 @@ class CameraEvent(object):
# this is a new client
# add an entry for it in the self.events dict
# each entry has two elements, a threading.Event() and a timestamp
- self.events[ident] = [threading.Event(), time.time()]
+ self.events[ident] = [gevent.event.Event(), time.time()]
return self.events[ident][0].wait(timeout)
def set(self):
@@ -181,65 +182,6 @@ class BaseCamera(metaclass=ABCMeta):
def rebuild_captures(self):
self.images = build_captures_from_exif(self.paths["default"])
- # START AND STOP WORKER THREAD
-
- def start_worker(self, timeout: int = 5) -> bool:
- """Start the background camera thread if it isn't running yet."""
- timeout_time = time.time() + timeout
-
- self.last_access = time.time()
- self.stop = False
-
- if not self.stream_active:
- # start background frame thread
- self.thread = threading.Thread(target=self._thread)
- self.thread.daemon = True
- self.thread.start()
-
- # wait until frames are available
- logging.info("Waiting for frames")
- while self.get_frame() is None:
- if time.time() > timeout_time:
- raise TimeoutError("Timeout waiting for frames.")
- else:
- time.sleep(0.1)
- return True
-
- def stop_worker(self, timeout: int = 5) -> bool:
- """Flag worker thread for stop. Waits for thread close or timeout."""
- logging.debug("Stopping worker thread")
- timeout_time = time.time() + timeout
-
- if self.stream_active:
- self.stop = True
- self.thread.join() # Wait for stream thread to exit
- logging.debug("Waiting for stream thread to exit.")
-
- while self.stream_active:
- if time.time() > timeout_time:
- logging.debug("Timeout waiting for worker thread close.")
- raise TimeoutError("Timeout waiting for worker thread close.")
- else:
- time.sleep(0.1)
- return True
-
- # HANDLE STREAM FRAMES
-
- def get_frame(self):
- """Return the current camera frame."""
- self.last_access = time.time()
-
- # wait for a signal from the camera thread
- self.event.wait()
- self.event.clear()
-
- return self.frame
-
- @abstractmethod
- def frames(self):
- """Create generator that returns frames from the camera."""
- pass
-
# RETURNING CAPTURES
@property
@@ -355,6 +297,65 @@ class BaseCamera(metaclass=ABCMeta):
return output
+ # START AND STOP WORKER THREAD
+
+ def start_worker(self, timeout: int = 5) -> bool:
+ """Start the background camera thread if it isn't running yet."""
+ timeout_time = time.time() + timeout
+
+ self.last_access = time.time()
+ self.stop = False
+
+ if not self.stream_active:
+ # start background frame thread
+ self.thread = threading.Thread(target=self._thread)
+ self.thread.daemon = True
+ self.thread.start()
+
+ # wait until frames are available
+ logging.info("Waiting for frames")
+ while self.get_frame() is None:
+ if time.time() > timeout_time:
+ raise TimeoutError("Timeout waiting for frames.")
+ else:
+ time.sleep(0.1)
+ return True
+
+ def stop_worker(self, timeout: int = 5) -> bool:
+ """Flag worker thread for stop. Waits for thread close or timeout."""
+ logging.debug("Stopping worker thread")
+ timeout_time = time.time() + timeout
+
+ if self.stream_active:
+ self.stop = True
+ self.thread.join() # Wait for stream thread to exit
+ logging.debug("Waiting for stream thread to exit.")
+
+ while self.stream_active:
+ if time.time() > timeout_time:
+ logging.debug("Timeout waiting for worker thread close.")
+ raise TimeoutError("Timeout waiting for worker thread close.")
+ else:
+ time.sleep(0.1)
+ return True
+
+ # HANDLE STREAM FRAMES
+
+ def get_frame(self):
+ """Return the current camera frame."""
+ self.last_access = time.time()
+
+ # wait for a signal from the camera thread
+ self.event.wait()
+ self.event.clear()
+
+ return self.frame
+
+ @abstractmethod
+ def frames(self):
+ """Create generator that returns frames from the camera."""
+ pass
+
# WORKER THREAD
def _thread(self):
diff --git a/openflexure_microscope/camera/mock.py b/openflexure_microscope/camera/mock.py
index 322bae58..65e65a32 100644
--- a/openflexure_microscope/camera/mock.py
+++ b/openflexure_microscope/camera/mock.py
@@ -10,6 +10,7 @@ import time
import numpy as np
from PIL import Image, ImageFont, ImageDraw
from datetime import datetime
+import gevent
import logging
@@ -224,7 +225,7 @@ class MissingCamera(BaseCamera):
# While the iterator is not closed
try:
while True:
- time.sleep(1) # Only serve frames at 1fps
+ gevent.sleep(1) # Only serve frames at 1fps
# Reset stream
self.stream.seek(0)
self.stream.truncate()
diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py
index 990767d0..29ab938b 100644
--- a/openflexure_microscope/camera/pi.py
+++ b/openflexure_microscope/camera/pi.py
@@ -531,6 +531,7 @@ class PiCameraStreamer(BaseCamera):
# Set resolution and stop stream recording if necessary
if not use_video_port:
self.stop_stream_recording()
+ time.sleep(0.1)
self.camera.capture(
target,
@@ -540,6 +541,7 @@ class PiCameraStreamer(BaseCamera):
bayer=(not use_video_port) and bayer,
use_video_port=use_video_port,
)
+ time.sleep(0.1)
# Set resolution and start stream recording if necessary
if not use_video_port: