Merge branch 'v2.1.0-dev' into stage-calibration
This commit is contained in:
commit
8a2a2abbec
12 changed files with 170 additions and 81 deletions
|
|
@ -32,6 +32,8 @@ The root of your ``gui`` dictionary expects 2 properties:
|
||||||
|
|
||||||
``icon`` - The name of a Material Design icon to use for your plugin
|
``icon`` - The name of a Material Design icon to use for your plugin
|
||||||
|
|
||||||
|
``viewPanel`` *(optional)* - Content to display to the right of the extension form. Either ``stream`` (default), ``gallery``, or ``settings``.
|
||||||
|
|
||||||
``forms`` - An array of forms as described below
|
``forms`` - An array of forms as described below
|
||||||
|
|
||||||
Form level
|
Form level
|
||||||
|
|
@ -53,6 +55,8 @@ Each form is described by a JSON object, with the following properties:
|
||||||
|
|
||||||
``schema`` - List of dictionaries. Each dictionary element describes a form component.
|
``schema`` - List of dictionaries. Each dictionary element describes a form component.
|
||||||
|
|
||||||
|
``emitOnResponse`` *(optional)* - OpenFlexure eV event to emit when a response is recieved from the extension (generally avoid unless you know you need this.)
|
||||||
|
|
||||||
Component level
|
Component level
|
||||||
+++++++++++++++
|
+++++++++++++++
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
from gevent import monkey
|
||||||
|
|
||||||
|
# Patch most system modules. Leave threads untouched so we can still use them normally if needed.
|
||||||
|
monkey.patch_all(thread=False)
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import atexit
|
import atexit
|
||||||
|
|
@ -60,6 +64,7 @@ app, labthing = create_app(
|
||||||
prefix="/api/v2",
|
prefix="/api/v2",
|
||||||
title=f"OpenFlexure Microscope {api_microscope.name}",
|
title=f"OpenFlexure Microscope {api_microscope.name}",
|
||||||
description="Test LabThing-based API for OpenFlexure Microscope",
|
description="Test LabThing-based API for OpenFlexure Microscope",
|
||||||
|
types=["org.openflexure.microscope"],
|
||||||
version=pkg_resources.get_distribution("openflexure_microscope").version,
|
version=pkg_resources.get_distribution("openflexure_microscope").version,
|
||||||
flask_kwargs={"static_url_path": ""},
|
flask_kwargs={"static_url_path": ""},
|
||||||
)
|
)
|
||||||
|
|
@ -167,5 +172,9 @@ def cleanup():
|
||||||
|
|
||||||
atexit.register(cleanup)
|
atexit.register(cleanup)
|
||||||
|
|
||||||
|
# Start the app
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port="5000", threaded=True, debug=True, use_reloader=False)
|
from labthings.server.wsgi import Server
|
||||||
|
|
||||||
|
server = Server(app)
|
||||||
|
server.run(host="0.0.0.0", port=5000, debug=False)
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,5 @@ except Exception as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Exception loading builtin extension picamera_autocalibrate: \n{traceback.format_exc()}"
|
f"Exception loading builtin extension picamera_autocalibrate: \n{traceback.format_exc()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ..example_extensions.custom_element import customelement_extension_v2
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ def dynamic_form():
|
||||||
return {
|
return {
|
||||||
"icon": "sd_storage",
|
"icon": "sd_storage",
|
||||||
"title": "Storage",
|
"title": "Storage",
|
||||||
|
"viewPanel": "gallery",
|
||||||
"forms": [
|
"forms": [
|
||||||
{
|
{
|
||||||
"name": "Autostorage",
|
"name": "Autostorage",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
from labthings.server.extensions import BaseExtension
|
||||||
|
from labthings.server.view import View
|
||||||
|
from labthings.server.decorators import ThingProperty, PropertySchema, use_args
|
||||||
|
from labthings.server import fields
|
||||||
|
from labthings.server.find import find_component
|
||||||
|
from labthings.core.utilities import path_relative_to
|
||||||
|
|
||||||
|
from openflexure_microscope.paths import settings_file_path, check_rw
|
||||||
|
from openflexure_microscope.config import OpenflexureSettingsFile
|
||||||
|
from openflexure_microscope.camera.base import BASE_CAPTURE_PATH
|
||||||
|
from openflexure_microscope.camera.capture import build_captures_from_exif
|
||||||
|
|
||||||
|
from openflexure_microscope.api.utilities.gui import build_gui
|
||||||
|
|
||||||
|
from flask import abort, url_for
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
from sys import platform
|
||||||
|
|
||||||
|
|
||||||
|
class CustomElementExtension(BaseExtension):
|
||||||
|
def __init__(self):
|
||||||
|
BaseExtension.__init__(
|
||||||
|
self,
|
||||||
|
"org.openflexure.customelement",
|
||||||
|
description="Testing HTML components in an extension",
|
||||||
|
static_folder=path_relative_to(__file__, "static"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register the on_microscope function to run when the microscope is attached
|
||||||
|
self.on_component("org.openflexure.microscope", self.on_microscope)
|
||||||
|
|
||||||
|
def on_microscope(self, microscope_obj):
|
||||||
|
"""Function to automatically call when the parent LabThing has a microscope attached."""
|
||||||
|
logging.debug(f"CustomElementExtension found microscope {microscope_obj}")
|
||||||
|
|
||||||
|
|
||||||
|
customelement_extension_v2 = CustomElementExtension()
|
||||||
|
|
||||||
|
|
||||||
|
def wc_func():
|
||||||
|
return {
|
||||||
|
"href": customelement_extension_v2.static_file_url("my-custom-element.min.js"),
|
||||||
|
"name": "my-custom-element",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def gui_func():
|
||||||
|
return {"icon": "pets", "title": "Element", "viewPanel": "stream", "wc": wc_func()}
|
||||||
|
|
||||||
|
|
||||||
|
customelement_extension_v2.add_meta("wc", wc_func)
|
||||||
|
customelement_extension_v2.add_meta(
|
||||||
|
"gui", build_gui(gui_func, customelement_extension_v2)
|
||||||
|
)
|
||||||
7
openflexure_microscope/api/example_extensions/custom_element/static/my-custom-element.min.js
vendored
Normal file
7
openflexure_microscope/api/example_extensions/custom_element/static/my-custom-element.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -207,7 +207,7 @@ class CaptureTags(View):
|
||||||
|
|
||||||
return jsonify(capture_obj.tags)
|
return jsonify(capture_obj.tags)
|
||||||
|
|
||||||
def delete(self, capture_id):
|
def delete(self, id):
|
||||||
"""
|
"""
|
||||||
Delete tags from a single image capture
|
Delete tags from a single image capture
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,16 @@ class MjpegStream(View):
|
||||||
@doc_response(200, mimetype="multipart/x-mixed-replace")
|
@doc_response(200, mimetype="multipart/x-mixed-replace")
|
||||||
def get(self):
|
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")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
# Restart stream worker thread
|
# Restart stream worker thread
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import time
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
|
import gevent.event
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
@ -61,7 +62,7 @@ class CameraEvent(object):
|
||||||
# this is a new client
|
# this is a new client
|
||||||
# add an entry for it in the self.events dict
|
# add an entry for it in the self.events dict
|
||||||
# each entry has two elements, a threading.Event() and a timestamp
|
# 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)
|
return self.events[ident][0].wait(timeout)
|
||||||
|
|
||||||
def set(self):
|
def set(self):
|
||||||
|
|
@ -181,65 +182,6 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
def rebuild_captures(self):
|
def rebuild_captures(self):
|
||||||
self.images = build_captures_from_exif(self.paths["default"])
|
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
|
# RETURNING CAPTURES
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -355,6 +297,65 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
return output
|
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
|
# WORKER THREAD
|
||||||
|
|
||||||
def _thread(self):
|
def _thread(self):
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image, ImageFont, ImageDraw
|
from PIL import Image, ImageFont, ImageDraw
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import gevent
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
@ -224,7 +225,7 @@ class MissingCamera(BaseCamera):
|
||||||
# While the iterator is not closed
|
# While the iterator is not closed
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1) # Only serve frames at 1fps
|
gevent.sleep(1) # Only serve frames at 1fps
|
||||||
# Reset stream
|
# Reset stream
|
||||||
self.stream.seek(0)
|
self.stream.seek(0)
|
||||||
self.stream.truncate()
|
self.stream.truncate()
|
||||||
|
|
|
||||||
30
poetry.lock
generated
30
poetry.lock
generated
|
|
@ -265,25 +265,20 @@ i18n = ["Babel (>=0.8)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = ""
|
description = "Python implementation of LabThings, based on the Flask microframework"
|
||||||
name = "labthings"
|
name = "labthings"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "^3.6"
|
python-versions = ">=3.6,<4.0"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
Flask = "^1.1.1"
|
Flask = ">=1.1.1,<2.0.0"
|
||||||
apispec = "^3.2.0"
|
apispec = ">=3.2.0,<4.0.0"
|
||||||
flask-cors = "^3.0.8"
|
flask-cors = ">=3.0.8,<4.0.0"
|
||||||
gevent = "^1.4.0"
|
gevent = ">=1.4.0,<2.0.0"
|
||||||
gevent-websocket = "^0.10.1"
|
gevent-websocket = ">=0.10.1,<0.11.0"
|
||||||
marshmallow = "^3.4.0"
|
marshmallow = ">=3.4.0,<4.0.0"
|
||||||
webargs = "^5.5.3"
|
webargs = ">=5.5.3,<6.0.0"
|
||||||
|
|
||||||
[package.source]
|
|
||||||
reference = "028e21cb5b53cc8feeea3af7a0579fde67a54daf"
|
|
||||||
type = "git"
|
|
||||||
url = "https://github.com/labthings/python-labthings.git"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
|
|
@ -713,7 +708,7 @@ version = "1.11.2"
|
||||||
rpi = ["picamera", "RPi.GPIO"]
|
rpi = ["picamera", "RPi.GPIO"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "c32b040f9990c0acaf6d1c984d503a6fa86a6d2f6eee51301dbdde7a5b176d82"
|
content-hash = "3a3a3bd4ffd7e0fe4c4c418a1b191c33afd375e6f06bb298189fd84ff75ee4cc"
|
||||||
python-versions = "^3.6"
|
python-versions = "^3.6"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
|
|
@ -876,7 +871,10 @@ jinja2 = [
|
||||||
{file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
|
{file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
|
||||||
{file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"},
|
{file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"},
|
||||||
]
|
]
|
||||||
labthings = []
|
labthings = [
|
||||||
|
{file = "labthings-0.2.0-py3-none-any.whl", hash = "sha256:e2235c0db45c7134403bd78b2aa55ae5aa1bfee677735c9674f52bfcb605e998"},
|
||||||
|
{file = "labthings-0.2.0.tar.gz", hash = "sha256:f77391bbb02676ac7c28de59c425c4dc8d6de63031f2a2db8bffd26bc691cb5e"},
|
||||||
|
]
|
||||||
lazy-object-proxy = [
|
lazy-object-proxy = [
|
||||||
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
|
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
|
||||||
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
|
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,10 @@ picamera = { git = "https://github.com/rwb27/picamera.git", branch = "master", o
|
||||||
"RPi.GPIO" = { version = "^0.6.5", optional = true }
|
"RPi.GPIO" = { version = "^0.6.5", optional = true }
|
||||||
pyserial = "^3.4" # Used for sangaboard (basic_serial_instrument) until we move to sangaboard pip library
|
pyserial = "^3.4" # Used for sangaboard (basic_serial_instrument) until we move to sangaboard pip library
|
||||||
|
|
||||||
labthings = { git = "https://github.com/labthings/python-labthings.git", branch = "master"} # TODO: Pin to a fixed version before 2.0.0 stable
|
|
||||||
python-dateutil = "^2.8"
|
python-dateutil = "^2.8"
|
||||||
psutil = "^5.6.7" # Autostorage extension
|
psutil = "^5.6.7" # Autostorage extension
|
||||||
opencv-python-headless = "3.4.7.28"
|
opencv-python-headless = "3.4.7.28"
|
||||||
|
labthings = "0.2.0"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
rpi = ["picamera", "RPi.GPIO"]
|
rpi = ["picamera", "RPi.GPIO"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue