Merge branch 'master' into flatten_lst_route
This commit is contained in:
commit
0c84053834
21 changed files with 11071 additions and 63 deletions
|
|
@ -1,4 +1,8 @@
|
|||
#!/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 atexit
|
||||
|
|
@ -60,6 +64,7 @@ app, labthing = create_app(
|
|||
prefix="/api/v2",
|
||||
title=f"OpenFlexure Microscope {api_microscope.name}",
|
||||
description="Test LabThing-based API for OpenFlexure Microscope",
|
||||
types=["org.openflexure.microscope"],
|
||||
version=pkg_resources.get_distribution("openflexure_microscope").version,
|
||||
flask_kwargs={"static_url_path": ""},
|
||||
)
|
||||
|
|
@ -167,5 +172,9 @@ def cleanup():
|
|||
|
||||
atexit.register(cleanup)
|
||||
|
||||
# Start the app
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -13,3 +13,5 @@ except Exception as e:
|
|||
logging.error(
|
||||
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 {
|
||||
"icon": "sd_storage",
|
||||
"title": "Storage",
|
||||
"viewPanel": "gallery",
|
||||
"forms": [
|
||||
{
|
||||
"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", "dist"),
|
||||
)
|
||||
|
||||
# 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)
|
||||
)
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
1
openflexure_microscope/api/example_extensions/custom_element/static/.gitattributes
vendored
Normal file
1
openflexure_microscope/api/example_extensions/custom_element/static/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
dist/* filter=lfs diff=lfs merge=lfs -text
|
||||
23
openflexure_microscope/api/example_extensions/custom_element/static/.gitignore
vendored
Normal file
23
openflexure_microscope/api/example_extensions/custom_element/static/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Override and include dist
|
||||
!/dist
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
3
openflexure_microscope/api/example_extensions/custom_element/static/dist/demo.html
vendored
Normal file
3
openflexure_microscope/api/example_extensions/custom_element/static/dist/demo.html
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18020f0209272ab81d01b2d68da517fcff98dd07d2ad7fe738c66df75003f7cf
|
||||
size 197
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:94cc5b4e5b3ddb8c6b94ef9b81b49060c582935aedf6c7d1626f4166c1827442
|
||||
size 288006
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba98b02d051bf6f7d2d4302154512a72ae6ddc1eff261e171fd92a067e1bdf84
|
||||
size 92018
|
||||
10783
openflexure_microscope/api/example_extensions/custom_element/static/package-lock.json
generated
Normal file
10783
openflexure_microscope/api/example_extensions/custom_element/static/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "vue-web-component-project",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vue-cli-service build --target wc --inline-vue --name my-custom-element ./src/components/VueWebComponent.vue"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"core-js": "^3.4.4",
|
||||
"vue": "^2.6.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.1.0",
|
||||
"@vue/cli-service": "^4.1.0",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div class="my-component-class">
|
||||
<h1>My Vue Web Component</h1>
|
||||
<div>Base URL: {{ componentBaseURL }}</div>
|
||||
<p>Host microscope name: {{ hostDeviceName }}</p>
|
||||
<p>{{ message }}</p>
|
||||
<br />
|
||||
<p>This cheeky little counter has all of its logic contained in a server-side component:</p>
|
||||
<button type="button" v-on:click="decrement()">-</button>
|
||||
<span>{{ value }}</span>
|
||||
<button type="button" v-on:click="increment()">+</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
'componentBaseURL': {
|
||||
required: false,
|
||||
default: null,
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
value: 0,
|
||||
message: "",
|
||||
hostDeviceName: null
|
||||
};
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.componentBaseURL){
|
||||
axios
|
||||
.get(`${this.componentBaseURL}`)
|
||||
.then(response => {
|
||||
console.log(response.data)
|
||||
this.hostDeviceName = response.data.title
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.log("Error reading test json, probabl because of cors or something")
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.message = "No componentBaseURL given"
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
decrement: function() {
|
||||
this.value = this.value -1
|
||||
},
|
||||
|
||||
increment: function() {
|
||||
this.value = this.value +1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-component-class {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
productionSourceMap: false
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue