Merge branch 'master' into flatten_lst_route

This commit is contained in:
Richard Bowman 2020-03-26 13:26:23 +00:00
commit 0c84053834
21 changed files with 11071 additions and 63 deletions

View file

@ -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)

View file

@ -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

View file

@ -241,6 +241,7 @@ def dynamic_form():
return {
"icon": "sd_storage",
"title": "Storage",
"viewPanel": "gallery",
"forms": [
{
"name": "Autostorage",

View file

@ -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)
)

View file

@ -0,0 +1,2 @@
> 1%
last 2 versions

View file

@ -0,0 +1 @@
dist/* filter=lfs diff=lfs merge=lfs -text

View 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

View file

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:18020f0209272ab81d01b2d68da517fcff98dd07d2ad7fe738c66df75003f7cf
size 197

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94cc5b4e5b3ddb8c6b94ef9b81b49060c582935aedf6c7d1626f4166c1827442
size 288006

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba98b02d051bf6f7d2d4302154512a72ae6ddc1eff261e171fd92a067e1bdf84
size 92018

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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>

View file

@ -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);

View file

@ -0,0 +1,3 @@
module.exports = {
productionSourceMap: false
};

View file

@ -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

View file

@ -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):

View file

@ -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()

View file

@ -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: