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..821b133d 100644
--- a/openflexure_microscope/camera/base.py
+++ b/openflexure_microscope/camera/base.py
@@ -6,12 +6,16 @@ import threading
import datetime
import logging
+import threading
+import gevent
+
from abc import ABCMeta, abstractmethod
from collections import OrderedDict
from .capture import CaptureObject, build_captures_from_exif
from openflexure_microscope.utilities import entry_by_uuid
from labthings.core.lock import StrictLock
+from labthings.core.event import ClientEvent
from openflexure_microscope.paths import data_file_path
@@ -44,51 +48,6 @@ def generate_numbered_basename(obj_list: list) -> str:
return basename
-class CameraEvent(object):
- """
- A frame-signaller object used by any instances or subclasses of BaseCamera.
-
- An event-like class that signals all active clients when a new frame is available.
- """
-
- def __init__(self):
- self.events = {}
-
- def wait(self, timeout: int = 5):
- """Wait for the next frame (invoked from each client's thread)."""
- ident = threading.get_ident()
- if ident not in self.events:
- # 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()]
- return self.events[ident][0].wait(timeout)
-
- def set(self):
- """Signal that a new frame is available."""
- now = time.time()
- remove = None
- for ident, event in self.events.items():
- if not event[0].isSet():
- # if this client's event is not set, then set it
- # also update the last set timestamp to now
- event[0].set()
- event[1] = now
- else:
- # if the client's event is already set, it means the client
- # did not process a previous frame
- # if the event stays set for more than 5 seconds, then assume
- # the client is gone and remove it
- if now - event[1] > 5:
- remove = ident
- if remove:
- del self.events[remove]
-
- def clear(self):
- """Clear frame event, once processed."""
- self.events[threading.get_ident()][0].clear()
-
-
class BaseCamera(metaclass=ABCMeta):
"""
Base implementation of StreamingCamera.
@@ -98,11 +57,11 @@ class BaseCamera(metaclass=ABCMeta):
self.thread = None
self.camera = None
- self.lock = StrictLock(timeout=1)
+ self.lock = StrictLock(timeout=1, name="Camera")
self.frame = None
self.last_access = 0
- self.event = CameraEvent()
+ self.event = ClientEvent()
self.stop = False # Used to indicate that the stream loop should break
self.stream_timeout = 20
@@ -181,65 +140,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 +255,64 @@ 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:
+ # Spawn a greenlet to handle stream
+ # start background frame thread
+ self.thread = gevent.spawn(self._thread)
+
+ # 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):
@@ -367,7 +325,6 @@ class BaseCamera(metaclass=ABCMeta):
for frame in self.frames_iterator:
self.frame = frame
self.event.set() # send signal to clients
- time.sleep(0)
# Handle timeout
if (
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:
diff --git a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py
index 90534d6c..6e993d3e 100644
--- a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py
+++ b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py
@@ -191,15 +191,10 @@ class ExtensibleSerialInstrument(object):
will keep reading until a termination phrase is reached.
"""
with self.communications_lock:
- logging.debug("Flushing input buffer...")
self.flush_input_buffer()
- logging.debug(f"Writing query: {queryString}")
self.write(queryString)
- logging.debug("Query written")
if self.ignore_echo == True: # Needs Implementing for a multiline read!
- logging.debug("Reading first line...")
first_line = self.readline(timeout).strip()
- logging.debug(f"Read finished. Got {first_line}")
if first_line == queryString:
return self.readline(timeout).strip()
else:
@@ -209,14 +204,11 @@ class ExtensibleSerialInstrument(object):
if termination_line is not None:
multiline = True
if multiline:
- logging.debug("Reading multiline...")
return self.read_multiline(termination_line)
else:
- logging.debug("Reading response...")
line = self.readline(
timeout
).strip() # question: should we strip the final newline?
- logging.debug(f"Read finished. Got {line}")
return line
def parsed_query(
diff --git a/pyproject.toml b/pyproject.toml
index 0e431ab1..c9f02312 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry]
name = "openflexure_microscope"
-version = "2.0.1"
+version = "2.1.0-dev"
description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope."
authors = [