Merge branch 'cleanup' into 'master'

The grand cleanup

See merge request openflexure/openflexure-microscope-server!82
This commit is contained in:
Joel Collins 2020-11-13 13:42:56 +00:00
commit 5b3d46ff98
60 changed files with 465 additions and 1180 deletions

0
.gitattributes vendored
View file

11
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,11 @@
repos:
- repo: https://github.com/psf/black
rev: 19.10b0
hooks:
- id: black
- repo: https://github.com/timothycrosley/isort
rev: 5.4.2
hooks:
- id: isort
additional_dependencies: [toml]
exclude: ^.*/?setup\.py$

View file

@ -1,20 +0,0 @@
FROM python:3.6
ENV PYTHONUNBUFFERED=1 \
POETRY_VERSION=1.0.3 \
POETRY_HOME="/opt/poetry" \
POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_NO_INTERACTION=1
ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH"
WORKDIR /app
RUN curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python
COPY . ./
RUN poetry install --no-dev --no-interaction
EXPOSE 5000
CMD ["poetry", "run", "python", "-m", "openflexure_microscope.api.app"]

View file

@ -14,6 +14,17 @@ This includes installing the server in a mode better suited for active developme
* `poetry install` * `poetry install`
* `poetry run build_static` * `poetry run build_static`
### Pre-commit hooks
We make use of pre-commit hooks to run code analysis before committing to the codebase.
The simplest way to ensure this works is to install pre-commits into your current Python environment:
* `pip3 install pre-commit`
* `pre-commit install`
To run pre-commit analysis manually, you can run `pre-commit run`.
### Node installation ### Node installation
* Note, building the static interface will require a valid Node.js installation * Note, building the static interface will require a valid Node.js installation

View file

@ -38,7 +38,7 @@ sys.modules.update((mod_name, Mock()) for mod_name in mock_imports)
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = "OpenFlexure Microscope Software" project = "OpenFlexure Microscope Software"
copyright = "2018, Bath Open Instrumentation Group" copyright = "2018, Bath Open Instrumentation Group" # pylint: disable=redefined-builtin
author = "Bath Open Instrumentation Group" author = "Bath Open Instrumentation Group"
# The short X.Y version # The short X.Y version

View file

@ -1,6 +1,6 @@
from labthings import Schema, fields, find_component from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
from labthings.views import PropertyView, View from labthings.views import PropertyView
## Extension methods ## Extension methods

View file

@ -3,7 +3,7 @@ import io # Used in our capture action
from flask import send_file # Used to send images from our server from flask import send_file # Used to send images from our server
from labthings import Schema, fields, find_component from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
from labthings.views import ActionView, PropertyView, View from labthings.views import ActionView, PropertyView
## Extension methods ## Extension methods

View file

@ -1,16 +1,8 @@
import io # Used in our capture action
import time # Used in our timelapse function import time # Used in our timelapse function
from flask import send_file # Used to send images from our server from labthings import current_action, fields, find_component, update_action_progress
from labthings import (
Schema,
current_action,
fields,
find_component,
update_action_progress,
)
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
from labthings.views import ActionView, PropertyView, View from labthings.views import ActionView
# Used in our timelapse function # Used in our timelapse function
from openflexure_microscope.captures.capture_manager import generate_basename from openflexure_microscope.captures.capture_manager import generate_basename

View file

@ -1,16 +1,8 @@
import io # Used in our capture action
import time # Used in our timelapse function import time # Used in our timelapse function
from flask import send_file # Used to send images from our server from labthings import current_action, fields, find_component, update_action_progress
from labthings import (
Schema,
current_action,
fields,
find_component,
update_action_progress,
)
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
from labthings.views import ActionView, PropertyView, View from labthings.views import ActionView
# Used to convert our GUI dictionary into a complete eV extension GUI # Used to convert our GUI dictionary into a complete eV extension GUI
from openflexure_microscope.api.utilities.gui import build_gui from openflexure_microscope.api.utilities.gui import build_gui

View file

@ -24,13 +24,13 @@ import pkg_resources
from flask import abort, send_file from flask import abort, send_file
from flask_cors import CORS, cross_origin from flask_cors import CORS, cross_origin
from labthings import create_app from labthings import create_app
from labthings.views import View
from labthings.extensions import find_extensions from labthings.extensions import find_extensions
from labthings.views import View
from openflexure_microscope.api.microscope import default_microscope as api_microscope from openflexure_microscope.api.microscope import default_microscope as api_microscope
from openflexure_microscope.api.utilities import init_default_extensions, list_routes from openflexure_microscope.api.utilities import init_default_extensions, list_routes
from openflexure_microscope.api.v2 import views from openflexure_microscope.api.v2 import views
from openflexure_microscope.config import JSONEncoder from openflexure_microscope.json import JSONEncoder
from openflexure_microscope.paths import ( from openflexure_microscope.paths import (
OPENFLEXURE_EXTENSIONS_PATH, OPENFLEXURE_EXTENSIONS_PATH,
OPENFLEXURE_VAR_PATH, OPENFLEXURE_VAR_PATH,

View file

@ -83,7 +83,7 @@ class JPEGSharpnessMonitor:
raise e raise e
if stop < 1: if stop < 1:
stop = len(jpeg_times) stop = len(jpeg_times)
logging.debug("changing stop to {}".format(stop)) logging.debug("changing stop to %s", (stop))
jpeg_times = jpeg_times[start:stop] jpeg_times = jpeg_times[start:stop]
jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs) jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs)
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop] return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
@ -258,13 +258,12 @@ def fast_up_down_up_autofocus(
# Ensure the MJPEG stream has started # Ensure the MJPEG stream has started
microscope.camera.start_stream_recording() microscope.camera.start_stream_recording()
df = dz # TODO: refactor so I actually use dz in the code below!
logging.debug("Initial move") logging.debug("Initial move")
if initial_move_up: if initial_move_up:
m.focus_rel(df / 2) m.focus_rel(dz / 2)
# move down # move down
logging.debug("Move down") logging.debug("Move down")
i, z = m.focus_rel(-df) i, z = m.focus_rel(-dz)
# now inspect where the sharpest point is, and estimate the sharpness # now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack # (JPEG size) that we should find at the start of the Z stack
_, jz, js = m.move_data(i) _, jz, js = m.move_data(i)
@ -285,16 +284,14 @@ def fast_up_down_up_autofocus(
inow = np.argmax( inow = np.argmax(
js < current_js js < current_js
) # use the curve we recorded to estimate our position ) # use the curve we recorded to estimate our position
# TODO: fancy interpolation stuff
# So, the Z position corresponding to our current sharpness value is zs[inow] # So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow] # That means we should move forwards, by best_z - zs[inow]
logging.debug("Correction move") logging.debug("Correction move")
correction_move = best_z + target_z - jz[inow] correction_move = best_z + target_z - jz[inow]
logging.debug( logging.debug(
"Fast autofocus scan: correcting backlash by moving {} steps".format( "Fast autofocus scan: correcting backlash by moving %s steps",
correction_move (correction_move),
)
) )
m.focus_rel(correction_move) m.focus_rel(correction_move)
return m.data_dict() return m.data_dict()

View file

@ -63,17 +63,15 @@ def auto_expose_and_freeze_settings(camera):
logging.info("Freezing the camera settings...") logging.info("Freezing the camera settings...")
camera.shutter_speed = camera.exposure_speed camera.shutter_speed = camera.exposure_speed
logging.info("Shutter speed = {}".format(camera.shutter_speed)) logging.info("Shutter speed = %s", (camera.shutter_speed))
camera.exposure_mode = "off" camera.exposure_mode = "off"
logging.info("Auto exposure disabled") logging.info("Auto exposure disabled")
g = camera.awb_gains g = camera.awb_gains
camera.awb_mode = "off" camera.awb_mode = "off"
camera.awb_gains = g camera.awb_gains = g
logging.info("Auto white balance disabled, gains are {}".format(g)) logging.info("Auto white balance disabled, gains are %s", (g))
logging.info( logging.info(
"Analogue gain: {}, Digital gain: {}".format( "Analogue gain: %s, Digital gain: %s", camera.analog_gain, camera.digital_gain
camera.analog_gain, camera.digital_gain
)
) )
adjust_exposure_to_setpoint(camera, 215) adjust_exposure_to_setpoint(camera, 215)
@ -100,7 +98,7 @@ def lst_from_channels(channels):
# lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int)) # lst_resolution = list(np.ceil(full_resolution / 64.0).astype(int))
lst_resolution = [(r // 64) + 1 for r in full_resolution] lst_resolution = [(r // 64) + 1 for r in full_resolution]
# NB the size of the LST is 1/64th of the image, but rounded UP. # NB the size of the LST is 1/64th of the image, but rounded UP.
logging.info("Generating a lens shading table at {}x{}".format(*lst_resolution)) logging.info("Generating a lens shading table at %sx%s", *lst_resolution)
lens_shading = np.zeros([channels.shape[0]] + lst_resolution, dtype=np.float) lens_shading = np.zeros([channels.shape[0]] + lst_resolution, dtype=np.float)
for i in range(lens_shading.shape[0]): for i in range(lens_shading.shape[0]):
image_channel = channels[i, :, :] image_channel = channels[i, :, :]
@ -118,9 +116,12 @@ def lst_from_channels(channels):
image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge" image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge"
) # Pad image to the right and bottom ) # Pad image to the right and bottom
logging.info( logging.info(
"Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format( "Channel shape: %sx%s, shading table shape: %sx%s, after padding %s",
iw, ih, lw * 32, lh * 32, padded_image_channel.shape iw,
) ih,
lw * 32,
lh * 32,
padded_image_channel.shape,
) )
# Next, fill the shading table (except edge pixels). Please excuse the # Next, fill the shading table (except edge pixels). Please excuse the
# for loop - I know it's not fast but this code needn't be! # for loop - I know it's not fast but this code needn't be!

View file

@ -16,6 +16,7 @@ from labthings import (
from labthings.extensions import BaseExtension from labthings.extensions import BaseExtension
from labthings.views import ActionView from labthings.views import ActionView
from openflexure_microscope.api.v2.views.actions.camera import FullCaptureArgs
from openflexure_microscope.captures.capture_manager import generate_basename from openflexure_microscope.captures.capture_manager import generate_basename
from openflexure_microscope.devel import abort from openflexure_microscope.devel import abort
@ -216,7 +217,7 @@ class ScanExtension(BaseExtension):
for x_y in line: for x_y in line:
# Move to new grid position without changing z # Move to new grid position without changing z
logging.debug("Moving to step {}".format([x_y[0], x_y[1], next_z])) logging.debug("Moving to step %s", ([x_y[0], x_y[1], next_z]))
microscope.stage.move_abs([x_y[0], x_y[1], next_z]) microscope.stage.move_abs([x_y[0], x_y[1], next_z])
# Refocus # Refocus
if autofocus_enabled: if autofocus_enabled:
@ -270,7 +271,7 @@ class ScanExtension(BaseExtension):
# Make sure we use our current best estimate of focus (i.e. the current position) next point # Make sure we use our current best estimate of focus (i.e. the current position) next point
next_z = microscope.stage.position[2] next_z = microscope.stage.position[2]
logging.debug("Returning to {}".format(initial_position)) logging.debug("Returning to %s", (initial_position))
microscope.stage.move_abs(initial_position) microscope.stage.move_abs(initial_position)
end = time.time() end = time.time()
@ -328,34 +329,29 @@ class ScanExtension(BaseExtension):
return return
if i != steps - 1: if i != steps - 1:
logging.debug("Moving z by {}".format(step_size)) logging.debug("Moving z by %s", (step_size))
microscope.stage.move_rel([0, 0, step_size]) microscope.stage.move_rel([0, 0, step_size])
if return_to_start: if return_to_start:
logging.debug("Returning to {}".format(initial_position)) logging.debug("Returning to %s", (initial_position))
microscope.stage.move_abs(initial_position) microscope.stage.move_abs(initial_position)
scan_extension_v2 = ScanExtension() scan_extension_v2 = ScanExtension()
class TileScanArgs(FullCaptureArgs):
namemode = fields.String(missing="coordinates", example="coordinates")
grid = fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3])
style = fields.String(missing="raster")
autofocus_dz = fields.Integer(missing=50)
fast_autofocus = fields.Boolean(missing=False)
stride_size = fields.List(
fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
)
class TileScanAPI(ActionView): class TileScanAPI(ActionView):
args = { args = TileScanArgs()
"filename": fields.String(missing=None, example=None),
"namemode": fields.String(missing="coordinates", example="coordinates"),
"temporary": fields.Boolean(missing=False),
"stride_size": fields.List(
fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
),
"grid": fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3]),
"style": fields.String(missing="raster"),
"autofocus_dz": fields.Integer(missing=50),
"fast_autofocus": fields.Boolean(missing=False),
"use_video_port": fields.Boolean(missing=False),
"bayer": fields.Boolean(missing=False),
"annotations": fields.Dict(missing={}, example={"Foo": "Bar"}),
"tags": fields.List(fields.String, missing=[]),
"resize": fields.Dict(missing=None), # TODO: Validate keys
}
# Allow 10 seconds to stop upon DELETE request # Allow 10 seconds to stop upon DELETE request
# Gives fast-autofocus time to finish if it's running # Gives fast-autofocus time to finish if it's running

View file

@ -11,8 +11,6 @@ from labthings.schema import Schema, pre_dump
from labthings.utilities import description_from_view from labthings.utilities import description_from_view
from labthings.views import ActionView, PropertyView, View from labthings.views import ActionView, PropertyView, View
from openflexure_microscope.devel import JsonResponse, request
class ZipObjectSchema(Schema): class ZipObjectSchema(Schema):
id = fields.String() id = fields.String()
@ -128,12 +126,15 @@ default_zip_manager = ZipManager()
class ZipBuilderAPIView(ActionView): class ZipBuilderAPIView(ActionView):
def post(self): args = fields.List(fields.String(), required=True)
ids = list(JsonResponse(request).json)
def post(self, args):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Return a handle on the autofocus task # Return a handle on the autofocus task
return default_zip_manager.marshaled_build_zip_from_capture_ids(microscope, ids) return default_zip_manager.marshaled_build_zip_from_capture_ids(
microscope, args
)
class ZipListAPIView(PropertyView): class ZipListAPIView(PropertyView):

View file

@ -184,7 +184,6 @@ export default {
this.checkConnection(); this.checkConnection();
// Handle guided tour // Handle guided tour
// If the user has already completed or skipped the guided tour // If the user has already completed or skipped the guided tour
// TODO: Only run this if connected to the API
var completedTour = this.getLocalStorageObj("completedTour") || false; var completedTour = this.getLocalStorageObj("completedTour") || false;
if (!completedTour) { if (!completedTour) {
this.$tours["guidedTour"].start(); this.$tours["guidedTour"].start();
@ -207,8 +206,6 @@ export default {
); );
// Keyboard shortcuts // Keyboard shortcuts
// TODO: Shortcut guide
Mousetrap.bind("?", () => { Mousetrap.bind("?", () => {
console.log(this.keyboardManual); console.log(this.keyboardManual);
this.toggleModalElement(this.$refs["keyboardManualModal"]); // Calls the mixin this.toggleModalElement(this.$refs["keyboardManualModal"]); // Calls the mixin

View file

@ -16,63 +16,27 @@
id="switcher-left" id="switcher-left"
class="uk-flex uk-flex-column uk-padding-remove uk-width-auto uk-height-1-1 uk-text-center" class="uk-flex uk-flex-column uk-padding-remove uk-width-auto uk-height-1-1 uk-text-center"
> >
<tabIcon <!-- For each top tab -->
id="view-tab-icon" <template v-for="(item, index) in enabledTopTabs">
tab-i-d="view" <!-- Render the tab icon -->
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
<i class="material-icons">visibility</i>
</tabIcon>
<tabIcon
id="gallery-tab-icon"
tab-i-d="gallery"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
<i class="material-icons">photo_library</i>
</tabIcon>
<hr />
<tabIcon
id="navigate-tab-icon"
tab-i-d="navigate"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
<i class="material-icons">gamepad</i>
</tabIcon>
<tabIcon
id="capture-tab-icon"
tab-i-d="capture"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
<i class="material-icons">camera_alt</i>
</tabIcon>
<template v-if="$store.state.globalSettings.IHIEnabled">
<hr id="extension-tab-divider" />
<tabIcon <tabIcon
id="slidescan-tab-icon" :id="item.id + '-tab-icon'"
tab-i-d="slidescan" :key="item.id + '-tab-icon'"
:tab-i-d="item.id"
:require-connection="true" :require-connection="true"
:current-tab="currentTab" :current-tab="currentTab"
:class="item.class"
@set-tab="setTab" @set-tab="setTab"
> >
<i class="material-icons">settings_overscan</i> <i class="material-icons">{{ item.icon }}</i>
</tabIcon> </tabIcon>
<!-- Add a divider if item.divide is true -->
<hr v-if="item.divide" :key="'tab-divider-' + index" />
</template> </template>
<hr id="extension-tab-divider" /> <hr id="extension-tab-divider" />
<!-- For each plugin tab -->
<tabIcon <tabIcon
v-for="plugin in pluginsGuiList" v-for="plugin in pluginsGuiList"
:key="plugin.id" :key="plugin.id"
@ -88,36 +52,23 @@
<hr id="extension-tab-divider" /> <hr id="extension-tab-divider" />
<tabIcon <!-- For each bottom tab -->
id="settings-tab-icon" <template v-for="(item, index) in bottomTabs">
class="uk-margin-auto-top" <!-- Render the tab icon -->
tab-i-d="settings" <tabIcon
:require-connection="false" :id="item.id + '-tab-icon'"
:current-tab="currentTab" :key="item.id + '-tab-icon'"
@set-tab="setTab" :tab-i-d="item.id"
> :require-connection="true"
<i class="material-icons">settings</i> :current-tab="currentTab"
</tabIcon> :class="item.class"
@set-tab="setTab"
<tabIcon >
id="logging-tab-icon" <i class="material-icons">{{ item.icon }}</i>
tab-i-d="logging" </tabIcon>
:require-connection="false" <!-- Add a divider if item.divide is true -->
:current-tab="currentTab" <hr v-if="item.divide" :key="'tab-divider-' + index" />
@set-tab="setTab" </template>
>
<i class="material-icons">assignment_late</i>
</tabIcon>
<tabIcon
id="about-tab-icon"
tab-i-d="about"
:require-connection="false"
:current-tab="currentTab"
@set-tab="setTab"
>
<i class="material-icons">info</i>
</tabIcon>
</div> </div>
</div> </div>
@ -126,69 +77,19 @@
id="container-left" id="container-left"
class="uk-padding-remove uk-height-1-1 uk-width-expand" class="uk-padding-remove uk-height-1-1 uk-width-expand"
> >
<!-- For each top tab -->
<tabContent <tabContent
tab-i-d="view" v-for="item in enabledTopTabs"
:id="item.id + '-tab-content'"
:key="item.id + '-tab-content'"
:tab-i-d="item.id"
:require-connection="true" :require-connection="true"
:current-tab="currentTab" :current-tab="currentTab"
> >
<viewContent /> <component :is="item.component"></component>
</tabContent>
<tabContent
tab-i-d="gallery"
:require-connection="false"
:current-tab="currentTab"
>
<galleryContent />
</tabContent>
<tabContent
tab-i-d="navigate"
:require-connection="true"
:current-tab="currentTab"
>
<navigateContent />
</tabContent>
<tabContent
tab-i-d="capture"
:require-connection="true"
:current-tab="currentTab"
>
<captureContent />
</tabContent>
<template v-if="$store.state.globalSettings.IHIEnabled">
<tabContent
tab-i-d="slidescan"
:require-connection="true"
:current-tab="currentTab"
>
<slideScanContent />
</tabContent>
</template>
<tabContent
tab-i-d="settings"
:require-connection="false"
:current-tab="currentTab"
>
<settingsContent class="section-content" />
</tabContent>
<tabContent
tab-i-d="logging"
:require-connection="false"
:current-tab="currentTab"
>
<loggingContent />
</tabContent>
<tabContent
tab-i-d="about"
:require-connection="false"
:current-tab="currentTab"
>
<aboutContent class="section-content" />
</tabContent> </tabContent>
<!-- For each plugin tab -->
<tabContent <tabContent
v-for="plugin in pluginsGuiList" v-for="plugin in pluginsGuiList"
:key="plugin.id" :key="plugin.id"
@ -204,6 +105,18 @@
@reloadForms="updatePlugins()" @reloadForms="updatePlugins()"
/> />
</tabContent> </tabContent>
<!-- For each bottom tab -->
<tabContent
v-for="item in bottomTabs"
:id="item.id + '-tab-content'"
:key="item.id + '-tab-content'"
:tab-i-d="item.id"
:require-connection="true"
:current-tab="currentTab"
>
<component :is="item.component"></component>
</tabContent>
</div> </div>
</div> </div>
</template> </template>
@ -252,14 +165,69 @@ export default {
return { return {
plugins: [], plugins: [],
currentTab: "view", currentTab: "view",
unwatchStoreFunction: null unwatchStoreFunction: null,
topTabs: [
{
id: "view",
icon: "visibility",
component: viewContent
},
{
id: "gallery",
icon: "photo_library",
component: galleryContent,
divide: true // Add a divider after this tab icon
},
{
id: "navigate",
icon: "gamepad",
component: navigateContent
},
{
id: "capture",
icon: "camera_alt",
component: captureContent,
divide: true // Add a divider after this tab icon
}
],
bottomTabs: [
{
id: "settings",
icon: "settings",
component: settingsContent,
class: "uk-margin-auto-top"
},
{
id: "logging",
icon: "assignment_late",
component: loggingContent
},
{
id: "about",
icon: "info",
component: aboutContent
}
]
}; };
}, },
computed: { computed: {
enabledTopTabs: function() {
var enabledTabs = this.topTabs;
if (this.$store.state.globalSettings.IHIEnabled) {
enabledTabs.push({
id: "slidescan",
icon: "settings_overscan",
component: slideScanContent
});
}
return enabledTabs;
},
pluginsUri: function() { pluginsUri: function() {
return `${this.$store.getters.baseUri}/api/v2/extensions`; return `${this.$store.getters.baseUri}/api/v2/extensions`;
}, },
pluginsGuiList: function() { pluginsGuiList: function() {
// List of plugin GUIs, obtained from this.plugins values // List of plugin GUIs, obtained from this.plugins values
console.log("Recalculating plugins"); console.log("Recalculating plugins");
@ -271,15 +239,21 @@ export default {
} }
return pluginGuis; return pluginGuis;
}, },
tabOrder: function() { tabOrder: function() {
// TODO: There must be a better way to do this, somehow reading the order of the HTML elements? var ind = [];
var ind = ["view", "gallery", "navigate", "capture", "settings"]; for (const tab of this.enabledTopTabs) {
ind.push(tab.id);
}
for (const plugin of this.pluginsGuiList) { for (const plugin of this.pluginsGuiList) {
ind.push(plugin.id); ind.push(plugin.id);
} }
ind.push("about"); for (const tab of this.bottomTabs) {
ind.push(tab.id);
}
return ind; return ind;
}, },
currentTabIndex: function() { currentTabIndex: function() {
return this.tabOrder.indexOf(this.currentTab); return this.tabOrder.indexOf(this.currentTab);
} }

View file

@ -131,7 +131,6 @@ export default {
}, },
submitApiUri: function() { submitApiUri: function() {
// TODO: This could probably be handled more explicitally
return this.pluginApiUri + this.route; return this.pluginApiUri + this.route;
} }
}, },

View file

@ -24,54 +24,6 @@ def blueprint_name_for_module(module_name, api_ver=2, suffix=""):
return f"v{api_ver}_{bp_name}_blueprint{suffix}" return f"v{api_ver}_{bp_name}_blueprint{suffix}"
class JsonResponse:
def __init__(self, request):
"""
Object to wrap up simple functionality for parsing a JSON response.
"""
# Try to load as json
try:
self.json = (
request.get_json()
) #: dict: Dictionary representation of request JSON
# If malformed JSON is passed, make an empty dictionary
except BadRequest as e:
logging.error(e)
self.json = {}
if self.json is None:
self.json = {}
# Store raw response data
self.data = request.get_data() #: str: String representation of request data
if self.data is None:
self.data = ""
def param(self, key, default=None, convert=None):
"""
Check if a key exists in a JSON/dictionary payload, and returns it.
Args:
key (str): JSON key to look for
default: Value to return if no matching key-value pair is found.
convert: Converter function. By passing a type, the value will be converted to that type.
**Use with caution!**
"""
# If no JSON payload exists, make an empty dictionary
if not self.json:
self.json = {}
if key in self.json:
val = self.json[key]
else:
val = default
if convert and (val is not None):
val = convert(val)
return val
def gen(camera): def gen(camera):
"""Video streaming generator function.""" """Video streaming generator function."""
while True: while True:
@ -121,12 +73,10 @@ def init_default_extensions(extension_dir):
default_ext_path = os.path.join(extension_dir, "defaults.py") default_ext_path = os.path.join(extension_dir, "defaults.py")
if not os.path.isfile(default_ext_path): # If user extensions file doesn't exist if not os.path.isfile(default_ext_path): # If user extensions file doesn't exist
logging.warning( logging.warning("No extension file found at %s. Creating...", (extension_dir))
"No extension file found at {}. Creating...".format(extension_dir)
)
create_file(default_ext_path) create_file(default_ext_path)
logging.info("Populating {}...".format(default_ext_path)) logging.info("Populating %s...", (default_ext_path))
with open(default_ext_path, "w") as outfile: with open(default_ext_path, "w") as outfile:
outfile.write(_DEFAULT_EXTENSION_INIT) outfile.write(_DEFAULT_EXTENSION_INIT)

View file

@ -1,33 +1,39 @@
import io import io
import logging import logging
from flask import abort, send_file from flask import send_file
from labthings import fields, find_component from labthings import Schema, fields, find_component
from labthings.views import ActionView from labthings.views import ActionView
from openflexure_microscope.api.v2.views.captures import CaptureSchema from openflexure_microscope.api.v2.views.captures import CaptureSchema
class CaptureResizeSchema(Schema):
width = fields.Integer(example=640, required=True)
height = fields.Integer(example=480, required=True)
class BasicCaptureArgs(Schema):
use_video_port = fields.Boolean(missing=False)
bayer = fields.Boolean(
missing=False, description="Include raw bayer data in capture"
)
resize = fields.Nested(CaptureResizeSchema(), required=False)
class FullCaptureArgs(BasicCaptureArgs):
filename = fields.String(example="MyFileName")
temporary = fields.Boolean(missing=False, description="Delete capture on shutdown")
annotations = fields.Dict(missing={}, example={"Client": "SwaggerUI"})
tags = fields.List(fields.String, missing=[], example=["docs"])
class CaptureAPI(ActionView): class CaptureAPI(ActionView):
""" """
Create a new image capture. Create a new image capture.
""" """
args = { args = FullCaptureArgs()
"filename": fields.String(example="MyFileName"),
"temporary": fields.Boolean(
missing=False, description="Delete capture on shutdown"
),
"use_video_port": fields.Boolean(missing=False),
"bayer": fields.Boolean(
missing=False, description="Store raw bayer data in file"
),
"annotations": fields.Dict(missing={}, example={"Client": "SwaggerUI"}),
"tags": fields.List(fields.String, missing=[], example=["docs"]),
"resize": fields.Dict(
missing=None, example={"width": 640, "height": 480}
), # TODO: Validate keys
}
schema = CaptureSchema() schema = CaptureSchema()
def post(self, args): def post(self, args):
@ -38,13 +44,10 @@ class CaptureAPI(ActionView):
resize = args.get("resize", None) resize = args.get("resize", None)
if resize: if resize:
if ("width" in resize) and ("height" in resize): resize = (
resize = ( int(resize["width"]),
int(resize["width"]), int(resize["height"]),
int(resize["height"]), ) # Convert dict to tuple
) # Convert dict to tuple
else:
abort(404)
# Explicitally acquire lock (prevents empty files being created if lock is unavailable) # Explicitally acquire lock (prevents empty files being created if lock is unavailable)
with microscope.camera.lock: with microscope.camera.lock:
@ -64,16 +67,7 @@ class RAMCaptureAPI(ActionView):
Take a non-persistant image capture. Take a non-persistant image capture.
""" """
args = { args = BasicCaptureArgs()
"use_video_port": fields.Boolean(missing=True),
"bayer": fields.Boolean(
missing=False, description="Return with raw bayer data"
),
"resize": fields.Dict(
missing=None, example={"width": 640, "height": 480}
), # TODO: Validate keys
}
responses = {200: {"content_type": "image/jpeg"}} responses = {200: {"content_type": "image/jpeg"}}
def post(self, args): def post(self, args):
@ -84,13 +78,10 @@ class RAMCaptureAPI(ActionView):
resize = args.get("resize", None) resize = args.get("resize", None)
if resize: if resize:
if ("width" in resize) and ("height" in resize): resize = (
resize = ( int(resize["width"]),
int(resize["width"]), int(resize["height"]),
int(resize["height"]), ) # Convert dict to tuple
) # Convert dict to tuple
else:
abort(404)
# Open a BytesIO stream to be destroyed once request has returned # Open a BytesIO stream to be destroyed once request has returned
with microscope.camera.lock, io.BytesIO() as stream: with microscope.camera.lock, io.BytesIO() as stream:
@ -133,8 +124,6 @@ class GPUPreviewStartAPI(ActionView):
window = [int(w) for w in window] window = [int(w) for w in window]
microscope.camera.start_preview(fullscreen=fullscreen, window=window) microscope.camera.start_preview(fullscreen=fullscreen, window=window)
# TODO: Make schema for microscope state
return microscope.state return microscope.state
@ -145,5 +134,4 @@ class GPUPreviewStopAPI(ActionView):
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
microscope.camera.stop_preview() microscope.camera.stop_preview()
# TODO: Make schema for microscope state
return microscope.state return microscope.state

View file

@ -25,11 +25,11 @@ class MoveStageAPI(ActionView):
# Handle absolute positioning (calculate a relative move from current position and target) # Handle absolute positioning (calculate a relative move from current position and target)
if (args.get("absolute")) and (microscope.stage): # Only if stage exists if (args.get("absolute")) and (microscope.stage): # Only if stage exists
target_position = axes_to_array(args, ["x", "y", "z"]) target_position = axes_to_array(args, ["x", "y", "z"])
logging.debug("TARGET: {}".format(target_position)) logging.debug("TARGET: %s", (target_position))
position = [ position = [
target_position[i] - microscope.stage.position[i] for i in range(3) target_position[i] - microscope.stage.position[i] for i in range(3)
] ]
logging.debug("DELTA: {}".format(position)) logging.debug("DELTA: %s", (position))
else: else:
# Get coordinates from payload # Get coordinates from payload
@ -45,7 +45,6 @@ class MoveStageAPI(ActionView):
else: else:
logging.warning("Unable to move. No stage found.") logging.warning("Unable to move. No stage found.")
# TODO: Make schema for microscope state
return microscope.state["stage"]["position"] return microscope.state["stage"]["position"]
@ -60,5 +59,4 @@ class ZeroStageAPI(ActionView):
with microscope.stage.lock(timeout=1): with microscope.stage.lock(timeout=1):
microscope.stage.zero_position() microscope.stage.zero_position()
# TODO: Make schema for microscope state
return microscope.state["stage"] return microscope.state["stage"]

View file

@ -1,13 +1,13 @@
import logging
from flask import abort, redirect, request, send_file, url_for from flask import abort, redirect, request, send_file, url_for
from labthings import Schema, fields, find_component from labthings import Schema, fields, find_component
from labthings.marshalling import marshal_with from labthings.marshalling import marshal_with, use_args
from labthings.utilities import description_from_view from labthings.utilities import description_from_view
from labthings.views import PropertyView, View from labthings.views import PropertyView, View
from marshmallow import pre_dump from marshmallow import pre_dump
from openflexure_microscope.api.utilities import JsonResponse, get_bool from openflexure_microscope.api.utilities import get_bool
# SCHEMAS
class InstrumentSchema(Schema): class InstrumentSchema(Schema):
@ -23,17 +23,25 @@ class ImageSchema(Schema):
format = fields.String() format = fields.String()
name = fields.String() name = fields.String()
tags = fields.List(fields.String()) tags = fields.List(fields.String())
annotations = fields.Dict() annotations = fields.Dict(keys=fields.Str(), values=fields.Str())
class CaptureMetadataSchema(Schema): class CaptureMetadataSchema(Schema):
experimenter = fields.Dict() # TODO: Make schema # Full dataset dictionary will change depending on the type of
experimenterGroup = fields.Dict() # TODO: Make schema # dataset, so we can't make a specific schema in this case.
dataset = fields.Dict() # TODO: Make schema dataset = fields.Dict()
# Nested schema for Image data
image = fields.Nested(ImageSchema()) image = fields.Nested(ImageSchema())
# Nested schema for instrument data
instrument = fields.Nested(InstrumentSchema()) instrument = fields.Nested(InstrumentSchema())
class BasicDatasetSchema(Schema):
id = fields.UUID()
name = fields.String()
type = fields.String()
class CaptureSchema(ImageSchema): class CaptureSchema(ImageSchema):
""" """
Schema containing only basic attributes required Schema containing only basic attributes required
@ -41,10 +49,15 @@ class CaptureSchema(ImageSchema):
are returned by using FullCaptureSchema are returned by using FullCaptureSchema
""" """
dataset = fields.Dict() # TODO: Make schema # We need dataset information in the capture array
# so that client applications can sort data into folders
# without the server having to do a tonne of file IO
dataset = fields.Nested(BasicDatasetSchema())
file = fields.String( file = fields.String(
data_key="path", description="Path of file on microscope device" data_key="path", description="Path of file on microscope device"
) )
# No need to make a schema for links as we only ever
# create the dictionary right here in `generate_links`
links = fields.Dict() links = fields.Dict()
@pre_dump @pre_dump
@ -104,6 +117,9 @@ class FullCaptureSchema(CaptureSchema):
metadata = fields.Nested(CaptureMetadataSchema()) metadata = fields.Nested(CaptureMetadataSchema())
# VIEWS
class CaptureList(PropertyView): class CaptureList(PropertyView):
tags = ["captures"] tags = ["captures"]
schema = CaptureSchema(many=True) schema = CaptureSchema(many=True)
@ -203,7 +219,8 @@ class CaptureTags(View):
return capture_obj.tags return capture_obj.tags
def put(self, id_): @use_args(fields.List(fields.String(), required=True))
def put(self, args, id_):
""" """
Add tags to a single image capture Add tags to a single image capture
""" """
@ -213,17 +230,12 @@ class CaptureTags(View):
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
# TODO: Replace with normal Flask request JSON thing capture_obj.put_tags(args)
data_dict = JsonResponse(request).json
if type(data_dict) != list:
return abort(400)
capture_obj.put_tags(data_dict)
return capture_obj.tags return capture_obj.tags
def delete(self, id_): @use_args(fields.List(fields.String(), required=True))
def delete(self, args, id_):
""" """
Delete tags from a single image capture Delete tags from a single image capture
""" """
@ -233,12 +245,7 @@ class CaptureTags(View):
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
data_dict = JsonResponse(request).json for tag in args:
if type(data_dict) != list:
return abort(400)
for tag in data_dict:
capture_obj.delete_tag(str(tag)) capture_obj.delete_tag(str(tag))
return capture_obj.tags return capture_obj.tags
@ -259,7 +266,8 @@ class CaptureAnnotations(View):
return capture_obj.annotations return capture_obj.annotations
def put(self, id_): @use_args(fields.Dict())
def put(self, args, id_):
""" """
Update metadata for a single image capture Update metadata for a single image capture
""" """
@ -269,12 +277,6 @@ class CaptureAnnotations(View):
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
data_dict = JsonResponse(request).json capture_obj.put_annotations(args)
logging.debug(data_dict)
if type(data_dict) != dict:
return abort(400)
capture_obj.put_annotations(data_dict)
return capture_obj.annotations return capture_obj.annotations

View file

@ -1,12 +1,11 @@
import logging import logging
from flask import abort, request from flask import abort
from labthings import find_component from labthings import fields, find_component
from labthings.marshalling import use_args
from labthings.utilities import create_from_path, get_by_path, set_by_path from labthings.utilities import create_from_path, get_by_path, set_by_path
from labthings.views import PropertyView, View from labthings.views import PropertyView, View
from openflexure_microscope.api.utilities import JsonResponse
class SettingsProperty(PropertyView): class SettingsProperty(PropertyView):
def get(self): def get(self):
@ -16,17 +15,17 @@ class SettingsProperty(PropertyView):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
return microscope.read_settings() return microscope.read_settings()
def put(self): @use_args(fields.Dict())
def put(self, args):
""" """
Update current microscope settings, including camera and stage Update current microscope settings, including camera and stage
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
payload = JsonResponse(request)
logging.debug("Updating settings from PUT request:") logging.debug("Updating settings from PUT request:")
logging.debug(payload.json) logging.debug(args)
microscope.update_settings(payload.json) microscope.update_settings(args)
microscope.save_settings() microscope.save_settings()
return self.get() return self.get()
@ -50,16 +49,16 @@ class NestedSettingsProperty(View):
return value return value
def put(self, route): @use_args(fields.Dict())
def put(self, args, route):
""" """
Update a nested section of the current microscope settings Update a nested section of the current microscope settings
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
keys = route.split("/") keys = route.split("/")
payload = JsonResponse(request)
dictionary = create_from_path(keys) dictionary = create_from_path(keys)
set_by_path(dictionary, keys, payload.json) set_by_path(dictionary, keys, args)
microscope.update_settings(dictionary) microscope.update_settings(dictionary)
microscope.save_settings() microscope.save_settings()

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import io
import logging import logging
import threading import threading
import time import time
import io
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from collections import namedtuple from collections import namedtuple
@ -113,10 +113,10 @@ class BaseCamera(metaclass=ABCMeta):
def close(self): def close(self):
"""Close the BaseCamera and all attached StreamObjects.""" """Close the BaseCamera and all attached StreamObjects."""
logging.info("Closing {}".format(self)) logging.info("Closing %s", (self))
# Stop worker thread # Stop worker thread
self.stop_worker() self.stop_worker()
logging.info("Closed {}".format(self)) logging.info("Closed %s", (self))
# START AND STOP WORKER THREAD # START AND STOP WORKER THREAD

View file

@ -156,10 +156,10 @@ class PiCameraStreamer(BaseCamera):
for key in PiCameraStreamer.picamera_settings_keys: for key in PiCameraStreamer.picamera_settings_keys:
try: try:
value = getattr(self.camera, key) value = getattr(self.camera, key)
logging.debug("Reading PiCamera().{}: {}".format(key, value)) logging.debug("Reading PiCamera().%s: %s", key, value)
conf_dict["picamera"][key] = value conf_dict["picamera"][key] = value
except AttributeError: except AttributeError:
logging.debug("Unable to read PiCamera attribute {}".format(key)) logging.debug("Unable to read PiCamera attribute %s", (key))
# Include a serialised lens shading table # Include a serialised lens shading table
if ( if (
@ -243,26 +243,22 @@ class PiCameraStreamer(BaseCamera):
# Set exposure mode # Set exposure mode
if "exposure_mode" in settings_dict: if "exposure_mode" in settings_dict:
logging.debug( logging.debug(
"Applying exposure_mode: {}".format(settings_dict["exposure_mode"]) "Applying exposure_mode: %s", (settings_dict["exposure_mode"])
) )
self.camera.exposure_mode = settings_dict["exposure_mode"] self.camera.exposure_mode = settings_dict["exposure_mode"]
# Apply gains and let them settle # Apply gains and let them settle
if "analog_gain" in settings_dict: if "analog_gain" in settings_dict:
logging.debug( logging.debug("Applying analog_gain: %s", (settings_dict["analog_gain"]))
"Applying analog_gain: {}".format(settings_dict["analog_gain"])
)
set_analog_gain(self.camera, float(settings_dict["analog_gain"])) set_analog_gain(self.camera, float(settings_dict["analog_gain"]))
if "digital_gain" in settings_dict: if "digital_gain" in settings_dict:
logging.debug( logging.debug("Applying digital_gain: %s", (settings_dict["digital_gain"]))
"Applying digital_gain: {}".format(settings_dict["digital_gain"])
)
set_digital_gain(self.camera, float(settings_dict["digital_gain"])) set_digital_gain(self.camera, float(settings_dict["digital_gain"]))
# Apply shutter speed # Apply shutter speed
if "shutter_speed" in settings_dict: if "shutter_speed" in settings_dict:
logging.debug( logging.debug(
"Applying shutter_speed: {}".format(settings_dict["shutter_speed"]) "Applying shutter_speed: %s", (settings_dict["shutter_speed"])
) )
self.camera.shutter_speed = int(settings_dict["shutter_speed"]) self.camera.shutter_speed = int(settings_dict["shutter_speed"])
@ -272,17 +268,17 @@ class PiCameraStreamer(BaseCamera):
if "awb_gains" in settings_dict: if "awb_gains" in settings_dict:
logging.debug("Applying awb_mode: off") logging.debug("Applying awb_mode: off")
self.camera.awb_mode = "off" self.camera.awb_mode = "off"
logging.debug("Applying awb_gains: {}".format(settings_dict["awb_gains"])) logging.debug("Applying awb_gains: %s", (settings_dict["awb_gains"]))
self.camera.awb_gains = settings_dict["awb_gains"] self.camera.awb_gains = settings_dict["awb_gains"]
elif "awb_mode" in settings_dict: elif "awb_mode" in settings_dict:
logging.debug("Applying awb_mode: {}".format(settings_dict["awb_mode"])) logging.debug("Applying awb_mode: %s", (settings_dict["awb_mode"]))
self.camera.awb_mode = settings_dict["awb_mode"] self.camera.awb_mode = settings_dict["awb_mode"]
# Handle some properties that can be quickly applied # Handle some properties that can be quickly applied
batched_keys = ["framerate", "saturation"] batched_keys = ["framerate", "saturation"]
for key in batched_keys: for key in batched_keys:
if (key in settings_dict) and hasattr(self.camera, key): if (key in settings_dict) and hasattr(self.camera, key):
logging.debug("Applying {}: {}".format(key, settings_dict[key])) logging.debug("Applying %s: %s", key, settings_dict[key])
setattr(self.camera, key, settings_dict[key]) setattr(self.camera, key, settings_dict[key])
# Final optional pause to settle # Final optional pause to settle
@ -307,7 +303,7 @@ class PiCameraStreamer(BaseCamera):
for i in range(2): for i in range(2):
if np.abs(centre[i] - 0.5) + size / 2 > 0.5: if np.abs(centre[i] - 0.5) + size / 2 > 0.5:
centre[i] = 0.5 + (1.0 - size) / 2 * np.sign(centre[i] - 0.5) centre[i] = 0.5 + (1.0 - size) / 2 * np.sign(centre[i] - 0.5)
logging.info("setting zoom, centre {}, size {}".format(centre, size)) logging.info("setting zoom, centre %s, size %s", centre, size)
new_fov = (centre[0] - size / 2, centre[1] - size / 2, size, size) new_fov = (centre[0] - size / 2, centre[1] - size / 2, size, size)
self.camera.zoom = new_fov self.camera.zoom = new_fov
@ -329,13 +325,12 @@ class PiCameraStreamer(BaseCamera):
self.preview_active = True self.preview_active = True
except picamerax.exc.PiCameraMMALError as e: except picamerax.exc.PiCameraMMALError as e:
logging.error( logging.error(
"Suppressed a MMALError in start_preview. Exception: {}".format(e) "Suppressed a MMALError in start_preview. Exception: %s", (e)
) )
except picamerax.exc.PiCameraValueError as e: except picamerax.exc.PiCameraValueError as e:
logging.error( logging.error(
"Suppressed a ValueError exception in start_preview. Exception: {}".format( "Suppressed a ValueError exception in start_preview. Exception: %s",
e (e),
)
) )
def stop_preview(self): def stop_preview(self):
@ -364,7 +359,7 @@ class PiCameraStreamer(BaseCamera):
if not self.record_active: if not self.record_active:
# Start the camera video recording on port 2 # Start the camera video recording on port 2
logging.info("Recording to {}".format(output)) logging.info("Recording to %s", (output))
self.camera.start_recording( self.camera.start_recording(
output, output,
@ -413,12 +408,12 @@ class PiCameraStreamer(BaseCamera):
try: try:
self.camera.stop_recording(splitter_port=splitter_port) self.camera.stop_recording(splitter_port=splitter_port)
except picamerax.exc.PiCameraNotRecording: except picamerax.exc.PiCameraNotRecording:
logging.info("Not recording on splitter_port {}".format(splitter_port)) logging.info("Not recording on splitter_port %s", (splitter_port))
else: else:
logging.info( logging.info(
"Stopped MJPEG stream on port {1}. Switching to {0}.".format( "Stopped MJPEG stream on port %s. Switching to %s.",
self.image_resolution, splitter_port splitter_port,
) self.image_resolution,
) )
# Increase the resolution for taking an image # Increase the resolution for taking an image
@ -470,9 +465,9 @@ class PiCameraStreamer(BaseCamera):
) )
else: else:
logging.debug( logging.debug(
"Started MJPEG stream at {} on port {}".format( "Started MJPEG stream at %s on port %s",
self.stream_resolution, splitter_port self.stream_resolution,
) splitter_port,
) )
def capture( def capture(
@ -501,7 +496,7 @@ class PiCameraStreamer(BaseCamera):
output_object (str/BytesIO): Target object. output_object (str/BytesIO): Target object.
""" """
with self.lock: with self.lock:
logging.info("Capturing to {}".format(output)) logging.info("Capturing to %s", (output))
# Set resolution and stop stream recording if necessary # Set resolution and stop stream recording if necessary
if not use_video_port: if not use_video_port:
@ -537,7 +532,7 @@ class PiCameraStreamer(BaseCamera):
logging.debug("Creating PiRGBArray") logging.debug("Creating PiRGBArray")
with picamerax.array.PiRGBArray(self.camera) as output: with picamerax.array.PiRGBArray(self.camera) as output:
logging.info("Capturing to {}".format(output)) logging.info("Capturing to %s", (output))
self.camera.capture(output, format="rgb", use_video_port=True) self.camera.capture(output, format="rgb", use_video_port=True)

View file

@ -55,26 +55,17 @@ if __name__ == "__main__":
# fix the shutter speed # fix the shutter speed
cam.shutter_speed = cam.exposure_speed cam.shutter_speed = cam.exposure_speed
logging.info( logging.info("Current a/d gains: %s, %s", cam.analog_gain, cam.digital_gain)
"Current a/d gains: {}, {}".format(cam.analog_gain, cam.digital_gain)
)
logging.info("Attempting to set analogue gain to 1") logging.info("Attempting to set analogue gain to 1")
set_analog_gain(cam, 1) set_analog_gain(cam, 1)
logging.info("Attempting to set digital gain to 1") logging.info("Attempting to set digital gain to 1")
set_digital_gain(cam, 1) set_digital_gain(cam, 1)
# The old code is left in here in case it is a useful example...
# ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port,
# MMAL_PARAMETER_DIGITAL_GAIN,
# to_rational(1))
# print("Return code: {}".format(ret))
try: try:
while True: while True:
logging.info( logging.info(
"Current a/d gains: {}, {}".format( "Current a/d gains: %s, %s", cam.analog_gain, cam.digital_gain
cam.analog_gain, cam.digital_gain
)
) )
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:

View file

@ -1,3 +1,3 @@
from . import capture, capture_manager from . import capture, capture_manager
from .capture import CaptureObject, THUMBNAIL_SIZE from .capture import THUMBNAIL_SIZE, CaptureObject
from .capture_manager import CaptureManager from .capture_manager import CaptureManager

View file

@ -6,13 +6,13 @@ import logging
import os import os
import uuid import uuid
from collections import OrderedDict from collections import OrderedDict
from PIL import Image
import dateutil.parser import dateutil.parser
from PIL import Image
from openflexure_microscope.camera import piexif from openflexure_microscope.captures import piexif
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError from openflexure_microscope.captures.piexif import InvalidImageDataError
from openflexure_microscope.config import JSONEncoder from openflexure_microscope.json import JSONEncoder
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"] EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
THUMBNAIL_SIZE = (200, 150) THUMBNAIL_SIZE = (200, 150)
@ -25,23 +25,23 @@ def make_file_list(directory, formats):
glob.glob("{}/**/*.{}".format(directory, fmt.lower()), recursive=True) glob.glob("{}/**/*.{}".format(directory, fmt.lower()), recursive=True)
) )
logging.info("{} capture files found on disk".format(len(files))) logging.info("%s capture files found on disk", (len(files)))
return files return files
def build_captures_from_exif(capture_path): def build_captures_from_exif(capture_path):
logging.debug("Reloading captures from {}...".format(capture_path)) logging.debug("Reloading captures from %s...", (capture_path))
files = make_file_list(capture_path, EXIF_FORMATS) files = make_file_list(capture_path, EXIF_FORMATS)
captures = OrderedDict() captures = OrderedDict()
for f in files: for f in files:
logging.debug("Reloading capture {}...".format(f)) logging.debug("Reloading capture %s...", (f))
capture = capture_from_path(f) capture = capture_from_path(f)
if capture: if capture:
captures[capture.id] = capture captures[capture.id] = capture
logging.info("{} capture files successfully reloaded".format(len(captures))) logging.info("%s capture files successfully reloaded", (len(captures)))
return captures return captures
@ -58,7 +58,7 @@ def capture_from_path(path):
capture.sync_basic_metadata() capture.sync_basic_metadata()
return capture return capture
except (InvalidImageDataError, json.decoder.JSONDecodeError): except (InvalidImageDataError, json.decoder.JSONDecodeError):
logging.error("Invalid metadata at {}.".format(path)) logging.error("Invalid metadata at %s.", (path))
return None return None
@ -75,7 +75,7 @@ class CaptureObject(object):
# Store a nice ID # Store a nice ID
self.id = uuid.uuid4() #: str: Unique capture ID self.id = uuid.uuid4() #: str: Unique capture ID
logging.debug("Created CaptureObject {}".format(self.id)) logging.debug("Created CaptureObject %s", (self.id))
self.time = datetime.datetime.now() self.time = datetime.datetime.now()
@ -316,7 +316,7 @@ class CaptureObject(object):
""" """
if self.exists: # If data file exists if self.exists: # If data file exists
logging.info("Opening from file {}".format(self.file)) logging.info("Opening from file %s", (self.file))
with open(self.file, "rb") as f: with open(self.file, "rb") as f:
d = io.BytesIO(f.read()) # Load bytes from file d = io.BytesIO(f.read()) # Load bytes from file
d.seek(0) # Rewind loaded bytestream d.seek(0) # Rewind loaded bytestream
@ -364,7 +364,7 @@ class CaptureObject(object):
"""If the StreamObject has been saved, delete the file.""" """If the StreamObject has been saved, delete the file."""
if os.path.isfile(self.file): if os.path.isfile(self.file):
logging.info("Deleting file {}".format(self.file)) logging.info("Deleting file %s", (self.file))
os.remove(self.file) os.remove(self.file)
return True return True

View file

@ -61,7 +61,7 @@ class CaptureManager:
self.close() self.close()
def close(self): def close(self):
logging.info("Closing {}".format(self)) logging.info("Closing %s", (self))
# Close all StreamObjects # Close all StreamObjects
for capture_list in [self.images.values(), self.videos.values()]: for capture_list in [self.images.values(), self.videos.values()]:
for stream_object in capture_list: for stream_object in capture_list:
@ -75,9 +75,9 @@ class CaptureManager:
""" """
if os.path.isdir(self.paths["temp"]): if os.path.isdir(self.paths["temp"]):
logging.info("Clearing {}...".format(self.paths["temp"])) logging.info("Clearing %s...", (self.paths["temp"]))
shutil.rmtree(self.paths["temp"]) shutil.rmtree(self.paths["temp"])
logging.debug("Cleared {}.".format(self.paths["temp"])) logging.debug("Cleared %s.", (self.paths["temp"]))
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"])
@ -118,25 +118,7 @@ class CaptureManager:
# CREATING NEW CAPTURES # CREATING NEW CAPTURES
def new_image( def _new_output(self, temporary, filename, folder, fmt):
self,
temporary: bool = True,
filename: str = None,
folder: str = "",
fmt: str = "jpeg",
):
"""
Create a new image capture object.
Args:
temporary (bool): Should the data be deleted after session ends.
Creating the capture with a content manager sets this to true.
filename (str): Name of the stored file. Defaults to timestamp.
folder (str): Name of the folder in which to store the capture.
fmt (str): Format of the capture.
"""
# Generate file name # Generate file name
if not filename: if not filename:
filename = generate_numbered_basename(self.images.values()) filename = generate_numbered_basename(self.images.values())
@ -156,10 +138,32 @@ class CaptureManager:
if temporary: if temporary:
output.put_tags(["temporary"]) output.put_tags(["temporary"])
return output
def new_image(
self,
temporary: bool = True,
filename: str = None,
folder: str = "",
fmt: str = "jpeg",
):
"""
Create a new image capture object.
Args:
temporary (bool): Should the data be deleted after session ends.
Creating the capture with a content manager sets this to true.
filename (str): Name of the stored file. Defaults to timestamp.
folder (str): Name of the folder in which to store the capture.
fmt (str): Format of the capture.
"""
# Create a new output object
output = self._new_output(temporary, filename, folder, fmt)
# Update capture list # Update capture list
capture_key = str(output.id) capture_key = str(output.id)
logging.debug("Adding image %s with key %s", output, capture_key) logging.debug("Adding image %s with key %s", output, capture_key)
self.images[capture_key] = output self.images[capture_key] = output
return output return output
@ -182,26 +186,8 @@ class CaptureManager:
folder (str): Name of the folder in which to store the capture. folder (str): Name of the folder in which to store the capture.
fmt (str): Format of the capture. fmt (str): Format of the capture.
""" """
# TODO: Remove the redundancy here # Create a new output object
output = self._new_output(temporary, filename, folder, fmt)
# Generate file name
if not filename:
filename = generate_numbered_basename(self.videos.values())
logging.debug(filename)
filename = "{}.{}".format(filename, fmt)
# Generate folder
base_folder = self.paths["temp"] if temporary else self.paths["default"]
folder = os.path.join(base_folder, folder)
# Generate file path
filepath = os.path.join(folder, filename)
# Create capture object
output = CaptureObject(filepath=filepath)
# Insert a temporary tag if temporary
if temporary:
output.put_tags(["temporary"])
# Update capture list # Update capture list
capture_key = str(output.id) capture_key = str(output.id)

View file

@ -3,18 +3,9 @@ import json
import logging import logging
import os import os
import shutil import shutil
from fractions import Fraction
from uuid import UUID
import numpy as np from .json import JSONEncoder
from labthings.json import LabThingsJSONEncoder from .paths import CONFIGURATION_FILE_PATH, SETTINGS_FILE_PATH
from .paths import (
CONFIGURATION_FILE_PATH,
DEFAULT_CONFIGURATION_FILE_PATH,
DEFAULT_SETTINGS_FILE_PATH,
SETTINGS_FILE_PATH,
)
class OpenflexureSettingsFile: class OpenflexureSettingsFile:
@ -33,7 +24,12 @@ class OpenflexureSettingsFile:
self.path = path self.path = path
# Initialise basic config file with defaults if it doesn't exist # Initialise basic config file with defaults if it doesn't exist
initialise_file(self.path, populate=defaults) initialise_file(
self.path,
# Populate with default dictionary, or empty JSON if empty
populate=json.dumps(defaults, cls=JSONEncoder, indent=2, sort_keys=True)
or "{}\n",
)
def load(self) -> dict: def load(self) -> dict:
""" """
@ -78,32 +74,6 @@ class OpenflexureSettingsFile:
return settings return settings
class JSONEncoder(LabThingsJSONEncoder):
"""
A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
"""
def default(self, o):
if isinstance(o, UUID):
return str(o)
# PiCamera fractions
elif isinstance(o, Fraction):
return float(o)
# Numpy integers
elif isinstance(o, np.integer):
return int(o)
# Numpy arrays
elif isinstance(o, np.ndarray):
return o.tolist()
# UUIDs
elif isinstance(o, UUID):
return str(o)
else:
# call base class implementation which takes care of
# raising exceptions for unsupported types
return LabThingsJSONEncoder.default(self, o)
# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES # HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES
@ -116,7 +86,7 @@ def load_json_file(config_path) -> dict:
""" """
config_path = os.path.expanduser(config_path) config_path = os.path.expanduser(config_path)
logging.info("Loading {}...".format(config_path)) logging.info("Loading %s...", config_path)
with open(config_path) as config_file: with open(config_path) as config_file:
try: try:
@ -139,7 +109,7 @@ def save_json_file(config_path: str, config_dict: dict):
""" """
config_path = os.path.expanduser(config_path) config_path = os.path.expanduser(config_path)
logging.info("Saving {}...".format(config_path)) logging.info("Saving %s...", config_path)
logging.debug(config_dict) logging.debug(config_dict)
with open(config_path, "w") as outfile: with open(config_path, "w") as outfile:
@ -172,33 +142,26 @@ def initialise_file(config_path, populate: str = "{}\n"):
""" """
config_path = os.path.expanduser(config_path) config_path = os.path.expanduser(config_path)
logging.debug("Initialising {}".format(config_path)) logging.debug("Initialising %s", (config_path))
logging.debug("Exists: {}".format(os.path.exists(config_path))) logging.debug("Exists: %s", (os.path.exists(config_path)))
if not os.path.exists(config_path): # If user config file doesn't exist if not os.path.exists(config_path): # If user config file doesn't exist
logging.warning("No config file found at {}. Creating...".format(config_path)) logging.warning("No config file found at %s. Creating...", (config_path))
create_file(config_path) create_file(config_path)
logging.info("Populating {}...".format(config_path)) logging.info("Populating %s...", (config_path))
with open(config_path, "w") as outfile: with open(config_path, "w") as outfile:
outfile.write(populate) outfile.write(populate)
# Load the default settings
with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings:
DEFAULT_SETTINGS = default_settings.read()
#: Default user settings object #: Default user settings object
user_settings = OpenflexureSettingsFile( user_settings = OpenflexureSettingsFile(path=SETTINGS_FILE_PATH)
path=SETTINGS_FILE_PATH, defaults=DEFAULT_SETTINGS
)
# Load the default configuration
with open(DEFAULT_CONFIGURATION_FILE_PATH, "r") as default_configuration:
DEFAULT_CONFIGURATION = default_configuration.read()
#: Default user settings object #: Default user settings object
user_configuration = OpenflexureSettingsFile( user_configuration = OpenflexureSettingsFile(
path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION path=CONFIGURATION_FILE_PATH,
defaults={
"camera": {"type": "PiCamera"},
"stage": {"type": "SangaStage", "port": None},
},
) )

View file

@ -13,8 +13,6 @@ from labthings import current_action as current_task
from labthings import update_action_data as update_task_data from labthings import update_action_data as update_task_data
from labthings import update_action_progress as update_task_progress from labthings import update_action_progress as update_task_progress
from openflexure_microscope.api.utilities import JsonResponse
__all__ = [ __all__ = [
"current_task", "current_task",
"update_task_progress", "update_task_progress",

View file

@ -0,0 +1,31 @@
from fractions import Fraction
from uuid import UUID
import numpy as np
from labthings.json import LabThingsJSONEncoder
class JSONEncoder(LabThingsJSONEncoder):
"""
A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
"""
def default(self, o):
if isinstance(o, UUID):
return str(o)
# PiCamera fractions
elif isinstance(o, Fraction):
return float(o)
# Numpy integers
elif isinstance(o, np.integer):
return int(o)
# Numpy arrays
elif isinstance(o, np.ndarray):
return o.tolist()
# UUIDs
elif isinstance(o, UUID):
return str(o)
else:
# call base class implementation which takes care of
# raising exceptions for unsupported types
return LabThingsJSONEncoder.default(self, o)

View file

@ -10,7 +10,7 @@ import pkg_resources
from expiringdict import ExpiringDict from expiringdict import ExpiringDict
from openflexure_microscope.camera.mock import MissingCamera from openflexure_microscope.camera.mock import MissingCamera
from openflexure_microscope.captures import CaptureManager, THUMBNAIL_SIZE from openflexure_microscope.captures import THUMBNAIL_SIZE, CaptureManager
from openflexure_microscope.stage.mock import MissingStage from openflexure_microscope.stage.mock import MissingStage
from openflexure_microscope.stage.sanga import SangaDeltaStage, SangaStage from openflexure_microscope.stage.sanga import SangaDeltaStage, SangaStage
@ -71,7 +71,7 @@ class Microscope:
def close(self): def close(self):
"""Shut down the microscope hardware.""" """Shut down the microscope hardware."""
logging.info("Closing {}".format(self)) logging.info("Closing %s", (self))
if self.camera: if self.camera:
try: try:
self.camera.close() self.camera.close()
@ -83,7 +83,7 @@ class Microscope:
except TimeoutError as e: except TimeoutError as e:
logging.error(e) logging.error(e)
self.captures.close() self.captures.close()
logging.info("Closed {}".format(self)) logging.info("Closed %s", (self))
def setup(self, configuration): def setup(self, configuration):
""" """
@ -196,32 +196,34 @@ class Microscope:
Applies a settings dictionary to the microscope. Missing parameters will be left untouched. Applies a settings dictionary to the microscope. Missing parameters will be left untouched.
""" """
with self.lock: with self.lock:
logging.debug("Microscope: Applying settings: {}".format(settings)) logging.debug("Microscope: Applying settings: %s", (settings))
# If attached to a camera # If attached to a camera
if ("camera" in settings) and self.camera: if ("camera" in settings) and self.camera:
self.camera.update_settings(settings.get("camera", {})) self.camera.update_settings(settings.pop("camera", {}))
# If attached to a stage # If attached to a stage
if ("stage" in settings) and self.stage: if ("stage" in settings) and self.stage:
self.stage.update_settings(settings.get("stage", {})) self.stage.update_settings(settings.pop("stage", {}))
# Capture manager # Capture manager
self.captures.update_settings(settings.get("captures", {})) self.captures.update_settings(settings.pop("captures", {}))
# Microscope settings # Microscope settings
if "id" in settings: if "id" in settings:
self.id = settings["id"] self.id = settings.pop("id")
if "name" in settings: if "name" in settings:
self.name = settings["name"] self.name = settings.pop("name")
if "fov" in settings: if "fov" in settings:
self.fov = settings["fov"] self.fov = settings.pop("fov")
# Extension settings # Extension settings
if "extensions" in settings: if "extensions" in settings:
self.extension_settings.update(settings["extensions"]) self.extension_settings.update(settings.pop("extensions"))
# TODO: warn if there are settings that we silently ignore # Warn about any superfluous keys
for key in settings.keys():
logging.warning("Key %s is unused and was ignored", key)
def read_settings(self, full: bool = True): def read_settings(self, full: bool = True):
""" """

View file

@ -1,9 +0,0 @@
{
"camera": {
"type": "PiCamera"
},
"stage": {
"type": "SangaStage",
"port": null
}
}

View file

@ -1,7 +0,0 @@
{
"fov": [
4100,
3146
],
"jpeg_quality": 100
}

View file

@ -39,17 +39,6 @@ def logs_file_path(filename: str):
return os.path.join(logs_dir, filename) return os.path.join(logs_dir, filename)
# HANDLE DEFAULTS FILES STORED IN THIS APPLICATION
HERE = os.path.abspath(os.path.dirname(__file__))
#: Path of default (first-run) microscope settings
DEFAULT_SETTINGS_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json")
#: Path of default (first-run) microscope configuration
DEFAULT_CONFIGURATION_FILE_PATH = os.path.join(
HERE, "microscope_configuration.default.json"
)
# BASE PATHS # BASE PATHS
if os.name == "nt": if os.name == "nt":

View file

@ -1,10 +1,9 @@
import logging import logging
import os import os
import sys
import platform import platform
import pkg_resources import sys
from .error_sources import bcolors import pkg_resources
from openflexure_microscope.paths import ( from openflexure_microscope.paths import (
FALLBACK_OPENFLEXURE_VAR_PATH, FALLBACK_OPENFLEXURE_VAR_PATH,
@ -13,11 +12,12 @@ from openflexure_microscope.paths import (
from . import ( from . import (
check_capture_reload, check_capture_reload,
check_settings,
check_picamera, check_picamera,
check_sangaboard, check_sangaboard,
check_settings,
check_system, check_system,
) )
from .error_sources import bcolors
# Paths for suggestions # Paths for suggestions
LOGS_PATHS = [ LOGS_PATHS = [
@ -46,9 +46,7 @@ else:
logger.setLevel(logging.WARNING) logger.setLevel(logging.WARNING)
if __name__ == "__main__": def main():
spoof = False
print() print()
print(bcolors.HEADER + "OpenFlexure Rescue" + bcolors.ENDC) print(bcolors.HEADER + "OpenFlexure Rescue" + bcolors.ENDC)
print() print()
@ -88,3 +86,7 @@ if __name__ == "__main__":
print() print()
for err in error_sources: for err in error_sources:
print(err.message) print(err.message)
if __name__ == "__main__":
main()

View file

@ -1,9 +1,9 @@
import logging import logging
from openflexure_microscope.captures.capture import ( from openflexure_microscope.captures.capture import (
EXIF_FORMATS,
build_captures_from_exif, build_captures_from_exif,
make_file_list, make_file_list,
EXIF_FORMATS,
) )
from openflexure_microscope.captures.capture_manager import BASE_CAPTURE_PATH from openflexure_microscope.captures.capture_manager import BASE_CAPTURE_PATH
from openflexure_microscope.config import user_settings from openflexure_microscope.config import user_settings

View file

@ -1,7 +1,7 @@
from .error_sources import ErrorSource
from openflexure_microscope.config import user_configuration from openflexure_microscope.config import user_configuration
from .error_sources import ErrorSource
def main(): def main():
error_sources = [] error_sources = []

View file

@ -1,34 +1,49 @@
import json import json
import logging import logging
from .error_sources import ErrorSource
ERROR_SOURCES = [] ERROR_SOURCES = []
def trace_config_exceptions(): def trace_config_exceptions():
error_sources = [] error_sources = []
from openflexure_microscope.paths import ( from openflexure_microscope.paths import CONFIGURATION_FILE_PATH, SETTINGS_FILE_PATH
DEFAULT_CONFIGURATION_FILE_PATH,
SETTINGS_FILE_PATH,
)
try: try:
default_config = json.load(DEFAULT_CONFIGURATION_FILE_PATH) default_config = json.load(CONFIGURATION_FILE_PATH)
if not default_config: if not default_config:
error_sources.append("default_config_empty") error_sources.append(
ErrorSource(
"Configuration file is missing or empty. This may occur if the server has never been started."
)
)
except Exception as e: # pylint: disable=W0703 except Exception as e: # pylint: disable=W0703
logging.error("Error parsing config:") logging.error("Error parsing config:")
logging.error(e) logging.error(e)
error_sources.append("default_config_error") error_sources.append(
ErrorSource(
f"Configuration file is malformed. You can reset to the default configuration by deleting {CONFIGURATION_FILE_PATH}."
)
)
try: try:
default_settings = json.load(SETTINGS_FILE_PATH) default_settings = json.load(SETTINGS_FILE_PATH)
if not default_settings: if not default_settings:
error_sources.append("default_settings_empty") error_sources.append(
ErrorSource(
"Settings file is missing or empty. This may occur if the server has never been started."
)
)
except Exception as e: # pylint: disable=W0703 except Exception as e: # pylint: disable=W0703
logging.error("Error parsing settings:") logging.error("Error parsing settings:")
logging.error(e) logging.error(e)
error_sources.append("default_settings_error") error_sources.append(
ErrorSource(
f"Settings file is malformed. You can reset to the default settings by deleting {SETTINGS_FILE_PATH}."
)
)
return error_sources return error_sources
@ -39,7 +54,11 @@ def main():
try: try:
from openflexure_microscope import config as _ from openflexure_microscope import config as _
except Exception as e: # pylint: disable=W0703 except Exception as e: # pylint: disable=W0703
error_sources.append("config_settings_import_error") error_sources.append(
ErrorSource(
"Error importing configuration submodule. This could be an error in our code."
)
)
logging.error("Error importing config:") logging.error("Error importing config:")
logging.error(e) logging.error(e)
error_sources.extend(trace_config_exceptions()) error_sources.extend(trace_config_exceptions())

View file

@ -1,5 +1,6 @@
from urllib.request import urlopen
from urllib.error import URLError from urllib.error import URLError
from urllib.request import urlopen
import psutil import psutil
from .error_sources import WarningSource from .error_sources import WarningSource

View file

@ -81,7 +81,7 @@ class SangaStage(BaseStage):
@backlash.setter @backlash.setter
def backlash(self, blsh): def backlash(self, blsh):
logging.debug("Setting backlash to {}".format(blsh)) logging.debug("Setting backlash to %s", (blsh))
if blsh is None: if blsh is None:
self._backlash = None self._backlash = None
elif isinstance(blsh, Iterable): elif isinstance(blsh, Iterable):
@ -262,7 +262,7 @@ class SangaDeltaStage(SangaStage):
# Transform into delta coordinates # Transform into delta coordinates
displacement = np.dot(self.Tdv, displacement) displacement = np.dot(self.Tdv, displacement)
logging.debug("Delta displacement: {}".format(displacement)) logging.debug("Delta displacement: %s", (displacement))
# Do the move # Do the move
SangaStage.move_rel(self, displacement, axis=None, backlash=backlash) SangaStage.move_rel(self, displacement, axis=None, backlash=backlash)
@ -274,7 +274,7 @@ class SangaDeltaStage(SangaStage):
# Transform into delta coordinates # Transform into delta coordinates
final = np.dot(self.Tdv, final) final = np.dot(self.Tdv, final)
logging.debug("Delta final: {}".format(final)) logging.debug("Delta final: %s", (final))
# Do the move # Do the move
SangaStage.move_abs(self, final, **kwargs) SangaStage.move_abs(self, final, **kwargs)

36
poetry.lock generated
View file

@ -79,7 +79,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
[[package]] [[package]]
name = "babel" name = "babel"
version = "2.8.1" version = "2.9.0"
description = "Internationalization utilities" description = "Internationalization utilities"
category = "dev" category = "dev"
optional = false optional = false
@ -283,7 +283,7 @@ i18n = ["Babel (>=0.8)"]
[[package]] [[package]]
name = "labthings" name = "labthings"
version = "1.1.2" version = "1.1.3"
description = "Python implementation of LabThings, based on the Flask microframework" description = "Python implementation of LabThings, based on the Flask microframework"
category = "main" category = "main"
optional = false optional = false
@ -483,7 +483,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.24.0" version = "2.25.0"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
category = "dev" category = "dev"
optional = false optional = false
@ -493,7 +493,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
certifi = ">=2017.4.17" certifi = ">=2017.4.17"
chardet = ">=3.0.2,<4" chardet = ">=3.0.2,<4"
idna = ">=2.5,<3" idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" urllib3 = ">=1.21.1,<1.27"
[package.extras] [package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
@ -557,7 +557,7 @@ python-versions = "*"
[[package]] [[package]]
name = "sphinx" name = "sphinx"
version = "3.3.0" version = "3.3.1"
description = "Python documentation generator" description = "Python documentation generator"
category = "dev" category = "dev"
optional = false optional = false
@ -687,7 +687,7 @@ python-versions = "*"
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "1.25.11" version = "1.26.1"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev" category = "dev"
optional = false optional = false
@ -752,8 +752,8 @@ rpi = ["RPi.GPIO"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.6" python-versions = "^3.6.1"
content-hash = "fc03c76cedfd54d4d6be2ced9eb236dfdd1ffb986fdc731f96ee560053f7aa8a" content-hash = "e0aad912d5cb388d3ff395dc8a517f701ba2cbe4c2169771080a889906e91b41"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [
@ -781,8 +781,8 @@ attrs = [
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
] ]
babel = [ babel = [
{file = "Babel-2.8.1-py2.py3-none-any.whl", hash = "sha256:be432f50d6c38c705ea45a0c05a4bbb22a70742a93888fbae5e7948da1635d23"}, {file = "Babel-2.9.0-py2.py3-none-any.whl", hash = "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5"},
{file = "Babel-2.8.1.tar.gz", hash = "sha256:820c195271534e8a86f564ba9ef2c207f356cfeb7e94d2bdc6b57897c4233837"}, {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"},
] ]
black = [ black = [
{file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"}, {file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"},
@ -855,8 +855,8 @@ jinja2 = [
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
] ]
labthings = [ labthings = [
{file = "labthings-1.1.2-py3-none-any.whl", hash = "sha256:1e17e20c3585dbb2293ca1e75c46c4b9a21ee3aa30b71a2d839e16f40c21f600"}, {file = "labthings-1.1.3-py3-none-any.whl", hash = "sha256:51f0bc897fcbacbcbb911658d3daafd41a35373bca4ef2156423cdb728fa3d78"},
{file = "labthings-1.1.2.tar.gz", hash = "sha256:0d456f11920c4cd84e94afae2e7f005c3ea65346da5523d51aeec17698808156"}, {file = "labthings-1.1.3.tar.gz", hash = "sha256:3d68c638866f1c525a8b5285570c2d4599005d227b928e78088c0a29e6544007"},
] ]
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"},
@ -1058,8 +1058,8 @@ pyyaml = [
{file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
] ]
requests = [ requests = [
{file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"},
{file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"},
] ]
rope = [ rope = [
{file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"}, {file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"},
@ -1103,8 +1103,8 @@ snowballstemmer = [
{file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
] ]
sphinx = [ sphinx = [
{file = "Sphinx-3.3.0-py3-none-any.whl", hash = "sha256:3abdb2c57a65afaaa4f8573cbabd5465078eb6fd282c1e4f87f006875a7ec0c7"}, {file = "Sphinx-3.3.1-py3-none-any.whl", hash = "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"},
{file = "Sphinx-3.3.0.tar.gz", hash = "sha256:1c21e7c5481a31b531e6cbf59c3292852ccde175b504b00ce2ff0b8f4adc3649"}, {file = "Sphinx-3.3.1.tar.gz", hash = "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300"},
] ]
sphinxcontrib-applehelp = [ sphinxcontrib-applehelp = [
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
@ -1162,8 +1162,8 @@ typed-ast = [
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
] ]
urllib3 = [ urllib3 = [
{file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, {file = "urllib3-1.26.1-py2.py3-none-any.whl", hash = "sha256:61ad24434555a42c0439770462df38b47d05d9e8e353d93ec3742900975e3e65"},
{file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, {file = "urllib3-1.26.1.tar.gz", hash = "sha256:097116a6f16f13482d2a2e56792088b9b2920f4eb6b4f84a2c90555fb673db74"},
] ]
webargs = [ webargs = [
{file = "webargs-6.1.1-py2.py3-none-any.whl", hash = "sha256:2ead6ce38559152043ab4ada4d389aef6c804b0c169482e7c92b3f498f690b2c"}, {file = "webargs-6.1.1-py2.py3-none-any.whl", hash = "sha256:2ead6ce38559152043ab4ada4d389aef6c804b0c169482e7c92b3f498f690b2c"},

View file

@ -29,7 +29,7 @@ packages = [
] ]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.6" python = "^3.6.1"
Flask = "^1.0" Flask = "^1.0"
numpy = "1.19.2" # Exact version so we can guarantee a wheel numpy = "1.19.2" # Exact version so we can guarantee a wheel
Pillow = "^7.2.0" Pillow = "^7.2.0"
@ -44,13 +44,14 @@ expiringdict = "^1.2.1"
camera-stage-mapping = "0.1.4" camera-stage-mapping = "0.1.4"
picamerax = ">=20.9.1" picamerax = ">=20.9.1"
pyyaml = "^5.3.1" pyyaml = "^5.3.1"
labthings = "^1.1.2" labthings = "^1.1.3"
[tool.poetry.extras] [tool.poetry.extras]
rpi = ["RPi.GPIO"] rpi = ["RPi.GPIO"]
[tool.poetry.scripts] [tool.poetry.scripts]
build_static = 'openflexure_microscope.install:build' build_static = 'openflexure_microscope.install:build'
rescue = 'openflexure_microscope.rescue.auto:main'
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pynpm = "^0.1.2" pynpm = "^0.1.2"
@ -71,5 +72,5 @@ ensure_newline_before_comments = true
line_length = 88 line_length = 88
[tool.pylint.'MESSAGES CONTROL'] [tool.pylint.'MESSAGES CONTROL']
disable = "C0330, C0326, W1202, W0511, C, R" disable = "fixme,C,R"
max-line-length = 88 max-line-length = 88

View file

@ -1,101 +0,0 @@
from io import BytesIO
import numpy as np
import requests
from PIL import Image
class APIconnection:
def __init__(self, host="localhost", port=5000, api_ver="v1"):
self.base = self.build_base(host=host, port=port, api_ver=api_ver)
def build_base(self, host, port, api_ver):
return "http://{}:{}/api/{}".format(host, port, api_ver)
def uri(self, suffix):
return self.base + suffix
def get(self, route, json=True, timeout=5):
r = requests.get(self.uri(route), timeout=timeout)
if json:
return r.json()
else:
return r
def post(self, route, json=None, timeout=5):
r = requests.post(self.uri(route), json=json, timeout=timeout)
return r.json()
def delete(self, route, timeout=5):
r = requests.delete(self.uri(route), timeout=timeout)
return r.json()
def set_overlay(self, message="", size=50):
json = {"text": message, "size": size}
return self.post("/camera/overlay", json=json)
def get_overlay(self):
return self.get("/camera/overlay")
def get_config(self):
return self.get("/config")
def set_config(self, config_dict):
return self.post("/config", json=config_dict)
def get_state(self):
return self.get("/state")
def start_preview(self):
return self.post("/camera/preview/start")
def stop_preview(self):
return self.post("/camera/preview/stop")
def move_by(self, x=0, y=0, z=0):
json = {"x": x, "y": y, "z": z}
return self.post("/stage/position", json=json)
def new_capture(self, use_video_port=True, keep_on_disk=False, resize=None):
json = {"keep_on_disk": keep_on_disk, "use_video_port": use_video_port}
if resize:
json["size"] = {"width": resize[0], "height": resize[1]}
return self.post("/camera/capture", json=json)
def get_capture(self, capture_id):
uri_route = "/camera/capture/{}/download".format(capture_id)
r = self.get(uri_route, json=False)
img = Image.open(BytesIO(r.content))
array = np.asarray(img, dtype=np.int32)
return array
def del_capture(self, capture_id):
uri_route = "/camera/capture/{}".format(capture_id)
return self.delete(uri_route)
def capture(
self,
use_video_port=True,
keep_on_disk=False,
delete_after_use=True,
resize=None,
):
p = self.new_capture(
use_video_port=use_video_port, keep_on_disk=keep_on_disk, resize=resize
)
capture_id = p["metadata"]["id"]
img_array = self.get_capture(capture_id)
if delete_after_use:
self.del_capture(capture_id)
return img_array
def set_zoom(self, zoom_value=1.0):
json = {"zoom_value": zoom_value}
return self.post("/camera/zoom", json=json)
def get_zoom(self):
return self.get("/camera/zoom")

View file

@ -1,112 +0,0 @@
#!/usr/bin/env python
import logging
import sys
import unittest
import numpy as np
from api_client import APIconnection
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
class TestCapture(unittest.TestCase):
def test_capture_config(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
config = connection.get_config()
expected_keys = ["image_resolution", "stream_resolution", "numpy_resolution"]
for key in expected_keys:
self.assertTrue(key in config)
def test_capture_videoport(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
resolution = connection.get_config()["stream_resolution"]
for resize in [None, (640, 480)]:
if resize:
resolution = resize
capture_array = connection.capture(
use_video_port=True,
keep_on_disk=False,
delete_after_use=True,
resize=resize,
)
self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3))
def test_capture_full(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
resolution = connection.get_config()["image_resolution"]
for resize in [None, (640, 480)]:
if resize:
resolution = resize
capture_array = connection.capture(
use_video_port=False,
keep_on_disk=False,
delete_after_use=True,
resize=resize,
)
self.assertTrue(capture_array.shape == (resolution[1], resolution[0], 3))
class TestStage(unittest.TestCase):
def test_stage_config(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
config = connection.get_config()
expected_keys = ["backlash"]
for key in expected_keys:
self.assertTrue(key in config)
def test_stage_state(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
state = connection.get_state()
self.assertTrue("stage" in state)
expected_keys = ["position"]
for key in expected_keys:
self.assertTrue(key in state["stage"])
def test_stage_movement(self):
connection = APIconnection(host="localhost", port=5000, api_ver="v1")
move_distance = 500
for axis in range(3):
for direction in [1, -1]:
pos_i_dict = connection.get_state()["stage"]["position"]
pos_i = [pos_i_dict["x"], pos_i_dict["y"], pos_i_dict["z"]]
move = [0, 0, 0]
move[axis] = move_distance * direction
connection.move_by(*move)
pos_f_dict = connection.get_state()["stage"]["position"]
pos_f = [pos_f_dict["x"], pos_f_dict["y"], pos_f_dict["z"]]
diff = np.subtract(pos_f, pos_i)
logging.debug("{} > {}".format(pos_i, pos_f))
self.assertTrue(np.array_equal(diff, move))
if __name__ == "__main__":
suites = [
unittest.TestLoader().loadTestsFromTestCase(TestCapture),
unittest.TestLoader().loadTestsFromTestCase(TestStage),
]
alltests = unittest.TestSuite(suites)
result = unittest.TextTestRunner(verbosity=2).run(alltests)

View file

@ -1,266 +0,0 @@
#!/usr/bin/env python
import io
import logging
import os
import sys
import time
import unittest
import numpy as np
from PIL import Image
from openflexure_microscope.camera.pi import CaptureObject, PiCameraStreamer
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
class TestCaptureMethods(unittest.TestCase):
def test_still_capture(self):
"""Tests capturing still images to a BytesIO stream."""
global camera
for use_video_port in [True, False]:
for resize in [None, (640, 480)]:
# Wait for camera
camera.wait_for_camera()
# Capture to a context (auto-deletes files when done)
with camera.new_image() as output:
camera.capture(output, use_video_port=use_video_port, resize=resize)
# Ensure file deletion fails and returns False
self.assertFalse(output.delete_file())
# Ensure capture not stored to file
self.assertFalse(os.path.isfile(output.file))
# BEFORE DELETE: Ensure StreamObject 'stream' has
# a valid BytesIO object and byte string
self.assertTrue(isinstance(output.data, io.IOBase))
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
# Save capture to file
output.save_file()
# Check file got saved
self.assertTrue(os.path.isfile(output.file))
# Delete file
output.delete_file()
# Check file got deleted
self.assertFalse(os.path.isfile(output.file))
# AFTER DELETE: Ensure StreamObject 'stream' has
# a valid BytesIO object and byte string
self.assertTrue(isinstance(output.data, io.IOBase))
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
# Create a PIL image from stream
image = Image.open(output.data)
# Ensure a valid PIL image was created
self.assertTrue(isinstance(image, Image.Image))
# Calculate expected dimensions
if resize:
dims = resize
else:
if use_video_port:
dims = camera.stream_resolution
else:
dims = camera.image_resolution
# Ensure PIL image size matches expected size
print(image.size, dims)
self.assertTrue(image.size == dims)
def test_still_store(self):
"""Tests capturing still images to a file on disk."""
global camera
for use_video_port in [True, False]:
for resize in [None, (640, 480)]:
# Wait for camera
camera.wait_for_camera()
# Capture
output = camera.capture(
camera.new_image().file,
use_video_port=use_video_port,
resize=resize,
)
# Check file got saved
self.assertTrue(os.path.isfile(output.file))
statinfo = os.stat(output.file)
self.assertTrue(statinfo.st_size > 0)
# Ensure StreamObject 'stream' has
# a valid BytesIO object and byte string
self.assertTrue(isinstance(output.data, io.IOBase))
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
# Ensure file deletion completes and returns True
self.assertTrue(output.delete_file())
# Check file got deleted
self.assertFalse(os.path.isfile(output.file))
class TestUnencodedMethods(unittest.TestCase):
def test_yuv_array(self):
"""Tests capturing unencoded YUV data to a Numpy array."""
global camera
for use_video_port in [True, False]:
for resize in [None, (640, 480)]:
print("{}, {}, {}".format(id, resize, use_video_port))
# Wait for camera
camera.wait_for_camera()
# Capture RGB array
yuv = camera.yuv(use_video_port=use_video_port, resize=resize)
# Ensure capture output is a valid numpy array
self.assertTrue(isinstance(yuv, np.ndarray))
# Calculate expected dimensions
if resize:
dims = resize
else:
if use_video_port:
dims = camera.stream_resolution
else:
dims = camera.numpy_resolution
# Ensure array shape matches expected dimensions
self.assertTrue(yuv.shape == (dims[1], dims[0], 3))
def test_rgb_array(self):
"""Tests capturing unencoded YUV/RGB data to a Numpy array."""
global camera
for use_video_port in [True, False]:
for resize in [None, (640, 480)]:
print("{}, {}, {}".format(id, resize, use_video_port))
# Wait for camera
camera.wait_for_camera()
# Capture RGB array
rgb = camera.array(use_video_port=use_video_port, resize=resize)
# Ensure capture output is a valid numpy array
self.assertTrue(isinstance(rgb, np.ndarray))
# Calculate expected dimensions
if resize:
dims = resize
else:
if use_video_port:
dims = camera.stream_resolution
else:
dims = camera.numpy_resolution
# Ensure array shape matches expected dimensions
self.assertTrue(rgb.shape == (dims[1], dims[0], 3))
class TestRecordMethods(unittest.TestCase):
def test_video_record(self):
"""Tests recording videos to BytesIO stream, and to file on disk."""
global camera
# Wait for camera
camera.wait_for_camera()
with camera.new_video() as output:
# Start recording
camera.start_recording(output)
# Record for 2 seconds
time.sleep(2)
# Stop recording
camera.stop_recording()
# Check stream
self.assertTrue(isinstance(output.data, io.IOBase))
self.assertTrue(isinstance(output.binary, (bytes, bytearray)))
# Check file
statinfo = os.stat(output.file)
self.assertTrue(statinfo.st_size > 0)
# Log path
temp_path = output.file
# Check file got deleted on __exit__
self.assertFalse(os.path.isfile(temp_path))
time.sleep(0.25)
def test_video_store(self):
"""Tests recording videos to file on disk, without context manager."""
global camera
# Wait for camera
camera.wait_for_camera()
# Start recording
output = camera.start_recording(camera.new_video())
# Record for 2 seconds
time.sleep(2)
# Stop recording
camera.stop_recording()
# Check file
statinfo = os.stat(output.file)
self.assertTrue(statinfo.st_size > 0)
# Ensure file deletion completes and returns True
self.assertTrue(output.delete_file())
# Check file got deleted
self.assertFalse(os.path.isfile(output.file))
class TestThreadStarting(unittest.TestCase):
def test_restarting_stream(self):
"""Tests that a capture call restarts the camera worker thread."""
global camera
# Wait for camera
camera.wait_for_camera()
# Force-stop the camera worker thread (should return True)
self.assertTrue(camera.stop_worker())
# Check Pi camera has been disconnected
self.assertTrue(camera.camera)
# Restart worker thread
self.assertTrue(camera.start_worker())
self.assertTrue(camera.camera)
self.assertTrue(camera.thread)
self.assertIsInstance(camera.stream, io.IOBase)
if __name__ == "__main__":
with PiCameraStreamer() as camera:
suites = [
unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods),
unittest.TestLoader().loadTestsFromTestCase(TestUnencodedMethods),
unittest.TestLoader().loadTestsFromTestCase(TestThreadStarting),
unittest.TestLoader().loadTestsFromTestCase(TestRecordMethods),
]
alltests = unittest.TestSuite(suites)
result = unittest.TextTestRunner(verbosity=2).run(alltests)

View file

@ -1,44 +0,0 @@
#!/usr/bin/env python
import atexit
import logging
import sys
import unittest
from openflexure_stage import OpenFlexureStage
from openflexure_microscope import Microscope, config
from openflexure_microscope.camera.pi import PiCameraStreamer
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
class TestPluginMethods(unittest.TestCase):
def test_plugin_load(self):
plugin_arr = microscope.plugins.plugins
plugin_names = [plugin[0] for plugin in plugin_arr]
self.assertTrue("testing" in plugin_names)
def test_camera_access(self):
identify = microscope.plugins.testing.identify()
self.assertTrue(identify[0] is microscope.camera)
def test_stage_access(self):
identify = microscope.plugins.testing.identify()
self.assertTrue(identify[1] is microscope.stage)
if __name__ == "__main__":
with Microscope() as microscope:
microscope.attach(PiCameraStreamer(), OpenFlexureStage())
microscope.plugins.attach("openflexure_microscope.plugins.testing:Plugin")
suites = [unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods)]
alltests = unittest.TestSuite(suites)
result = unittest.TextTestRunner(verbosity=2).run(alltests)

View file

@ -1,42 +0,0 @@
#!/usr/bin/env python
import logging
import sys
import unittest
import numpy as np
from openflexure_stage import OpenFlexureStage
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
class TestMicroscope(unittest.TestCase):
def test_movement(self):
move_distance = 500
for axis in range(3):
for direction in [1, -1]:
pos_i = stage.position
logging.debug(pos_i)
logging.info(
"Moving axis {} by {}".format(axis, move_distance * direction)
)
move = [0, 0, 0]
move[axis] = move_distance * direction
stage.move_rel(move)
pos_f = stage.position
diff = np.subtract(pos_f, pos_i)
logging.debug("{} > {}".format(pos_i, pos_f))
self.assertTrue(np.array_equal(diff, move))
if __name__ == "__main__":
with OpenFlexureStage("/dev/ttyUSB0") as stage:
suites = [unittest.TestLoader().loadTestsFromTestCase(TestMicroscope)]
alltests = unittest.TestSuite(suites)
result = unittest.TextTestRunner(verbosity=2).run(alltests)