Fixed merge conflict

This commit is contained in:
Joe Knapper 2025-06-20 15:14:35 +00:00
commit 9dac080566
31 changed files with 522 additions and 114 deletions

3
.gitignore vendored
View file

@ -91,3 +91,6 @@ openflexure_settings/
# Files created by simulator # Files created by simulator
openflexure/ openflexure/
# js version file
/webapp/src/version.js

View file

@ -44,7 +44,7 @@ meta-build-image:
- web - web
.javascript: .javascript:
image: node:15 image: node:18
before_script: before_script:
- cd webapp - cd webapp
- npm install - npm install
@ -58,6 +58,19 @@ meta-build-image:
- tags - tags
- web - web
# Check webapp and server have matching version string
version-check:
stage: analysis
#use a base python image as nothing needs to be installed
image: python:3.11
script:
- ./check_version_sync.py
only:
- branches
- merge_requests
- tags
- web
# Python static analysis with ruff # Python static analysis with ruff
ruff-lint: ruff-lint:
stage: analysis stage: analysis
@ -145,6 +158,7 @@ build:
stage: build stage: build
extends: .javascript extends: .javascript
script: script:
- export NODE_OPTIONS=--openssl-legacy-provider
# Build JS application for production # Build JS application for production
- npm run build - npm run build
artifacts: artifacts:

View file

@ -2,31 +2,35 @@
## Summary ## Summary
(Summarize the bug encountered concisely) <!-- Summarise the bug encountered concisely -->
## Configuration ## Configuration
<!-- Adjust as appropriate. -->
I'm using: * **Sever version**: v2 or v3
* **Release/Branch**: e.g. v3.0.0-alpha1
**Camera:** (E.g. Raspberry Pi camera v2) * **Hardware Configuration:**
* Raspberry Pi Camera v2
**Motor controller:** (E.g. Sangaboard, or Arduino Nano) * Sangabouard v0.5
## Steps to reproduce ## Steps to reproduce
(How one can reproduce the issue - this is very important) <!--How to reproduce the issue - this is very important-->
## Relevant logs and/or screenshots ## Relevant logs and/or screenshots
To access your microscope log, either: <!-- To access your microscope log, either:
* In the webapp, open the "Logging" tab, and click "Download log file" in the top right corner
* Run `ofm log` on your microscope, and copy/paste the output here * Run `ofm log` on your microscope, and copy/paste the output here
* Go to `http://<your microscope IP>:5000/logfile`, download the log file, and attach it here using the "Attach a File" button below * Go to `http://<your microscope IP>:5000/logfile`, download the log file, and attach it here using the "Attach a File" button below
* In most setups, `http://microscope.local:5000/logfile` should work * In most setups, `http://microscope.local:5000/logfile` should work-->
## Additional details ## Additional details
(Anything else you think might be relevant to mention) <!--Anything else you think might be relevant to mention-->
/label ~bug /label ~bug <!-- ~"Severity::Medium" ~"sever version::v3" -->

View file

@ -2,18 +2,18 @@
## Summary ## Summary
(Summarize the bug encountered concisely) <!-- Summarise the feature concisely -->
## Configuration
I'm using:
**Camera:** (E.g. Raspberry Pi camera v2)
**Motor controller:** (E.g. Sangaboard, or Arduino Nano)
## Additional details ## Additional details
(Anything else you think might be relevant to mention) <!--Anything else you think might be relevant to mention, such as who would benefit from the change,
/label ~feature This could include sections such as:
## Implementation strategy
-->
/label ~feature <!-- ~"Severity::Medium" ~"sever version::v3" -->

View file

@ -1,3 +1,90 @@
# [v3.0.0-alpha1](https://gitlab.com/openflexure/openflexure-microscope-server/compare/v2.11.0...v3.0.0-alpha1) (2025-06-09)
This is the first alpha release of an almost total overhaul of the server, and the underlying LabThings framework. Some highlights are:
* Moved to underlying architecture based on [labthings-fastapi](https://github.com/labthings/labthings-fastapi). This allows each component of the microscope to be a "Thing", which can interact with each other.
* Current "Things" are camera, stage, autofocus, smart_scan background_detect, auto_recentre_stage, capture, settings_manager, system_control and test
* Overhaul of scanning, removing the predetermined, rectangular area in favour of exploring the sample indefinitely by appending neighbouring xy sites after each capture
* Background detect Thing optionally uses the colour distribution of a user-set "background" image to determine if the field of view is sample or background, and plan future scanning accordingly
* Stitching from openflexure-stitching now part of the scanning protocol, displaying a live updating preview of the current scan, and optionally producing a high res stitched JPEG, DZI and/or TIFF at the end of the scan
* A DZI viewer in the scan tab, allowing stitched images to be opened from within the GUI
* Webapp updates - scan tab added showing thumbnail preview + image count + scan times, gallery removed, tour removed, power tab added,
* Updated to numpy 2
* Automatically delete empty scans
V3 was started in a branch (MR !159)
The following merge requests have been merged into v3:
* !162 Scanning and recentring
* !163 Web app compatibility with v3 API
* !164 Modal progress dialog for TaskSubmitter
* !166 Pseudo spiral scan path and fixed dx-dy inversion
* !167 Restored old log path
* !168 Refactor task submitter
* !169 Run `openflexure-stitch` on the server
* !170 custom scan name from gui
* !171 Make it possible to manually save all settings to disk
* !172 Updates based on feedback from Rwanda trip
* !173 Autofocus efficiency
* !174 Parallel acquisition and moves
* !175 Improvements to autofocus and stitching in `smart_scan`.
* !176 Replace tasksubmitter with action-button
* !181 Add configurability to the server
* !191 Migrate to new Blob type and update imports
* !192 Formal Protocol for camera and stage interfaces
* !194 Add more unit tests and fix CI
* !226 Split up ruff format and lint
* !225 Rename exposure (with limit)
* !232 Purge all windows line endings and add check to formatter.
* !231 Reorder tabs
* !211 Use the UV channels only to determine if an area is background or sample
* !228 Stop uvicorn.run disabling existing loggers
* !227 CSM always returns to start on error
* !236 Inverse colours
* !239 Scanning cleanup
* !240 Refactor scanning
* !241 Add some extra rules to ruff, also add a more complete ruff config that fails
* !242 Further scan refactor
* !245 Add a scan path planner that prioritises shorter moves
* !247 Looping autofocus at the start of the scan
* !249 Remove focus_change_acceptable test
* !244 Remove correlation only stitching, now covered by preview stitch
* !250 Thumbnails and accurate durations in scan-list tab
* !253 Regex to count captured images
* !238 Disable tour
* !246 z stacking Thing
* !248 Cleanup zipping
* !255 Remove autofocus test
* !256 New comments on scanning
* !259 Update scan data JSON at end of scan
* !260 Scan tab updates
* !258 Use NumPy v2 and new wheel for opencv
* !230 Lazier regex for finding logs with square brackets
* !263 Clean up of serve_static_files
* !265 Update serve static files
* !267 Update theme.less to prevent automatic changed colour of uk-alert-success
* !268 GUI clean up - button cases, features tab and redo tour
* !269 Action buttons raise an Error from the last message in the log for the Actio
* !257 Added purge_empty_scans() method
* !270 Import openflexure-stitching from pip
* !272 Settling time as global, settle before taking first image in stack
* !261 CSM info in webapp tab
* !275 GUI shutdown clean up
* !276 Ui fixes
* !277 Fix error in purge_empty_scans if the `images` dir hadn't been created
* !266 Gui dzi viewer
* !278 update labthings-picamera to most recent release
* !279 Simulation fixes
* !281 Change from building with node 15 to node 18
* !282 Defocus and realign simulation axes
* !280 Custom `from_index()` function in webapp for backwards compatibility before javascrpt `at()`
* !284 Update labthings-api dep and also the version number for alpha release
* !286 Use same version number for server and webapp, add CI check
* !285 Swapped the order of CSM so y axis is completed first
* !283 Add scan directory to json config
* !287 Update depedencies and changelog prior to v3.0.0-alpha1 release
# [v2.11.0](https://gitlab.com/openflexure/openflexure-microscope-server/compare/v2.10.1...v2.11.0) (2022-08-08) # [v2.11.0](https://gitlab.com/openflexure/openflexure-microscope-server/compare/v2.10.1...v2.11.0) (2022-08-08)
## New features ## New features
* Background detection can now be used in scans, if you have the background-detect extension. ([!153](https://gitlab.com/openflexure/openflexure-microscope-server/-/merge_requests/153)) * Background detection can now be used in scans, if you have the background-detect extension. ([!153](https://gitlab.com/openflexure/openflexure-microscope-server/-/merge_requests/153))

View file

@ -97,19 +97,11 @@ sudo -u openflexure-ws PATH="/var/openflexure/application/openflexure-microscope
### Set up the Javascript environment and build ### Set up the Javascript environment and build
* The Flask web application, written in Python, serves a web application written in `Vue.js`. This is distributed as part of the built version of the server, hosted on our [build server](https://build.openflexure.org/openflexure-microscope-server/).
* You could extract the pre-built web app from this tarball, which saves you having to set up Node.js. However, it also means you're not able to change the interface, and it's possible your interface will get out of sync with your server. The Labthings-FastAPI server, written in Python, serves a web application written in `Vue.js` (Vue2). This is distributed with the SD card image for the microscope.
* Building the web interface will require a valid Node.js installation. If you don't have Node.js (including `npm`) the [Node.js website](https://nodejs.org/en/) offers downloads for all platforms, though see the instructions below for Raspberry Pi.
* To install Node.js on a Raspberry Pi: For more details on the web app see the ReadMe in the [webapp](./webapp) directory.
* `curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -`
* `sudo apt install nodejs`
* To build the web application (this produces a set of static files, that are served by the Flask webserver)
* `cd webapp`
* `npm install`
* `npm run build`
* To create a Node.js development server (this will help various development tools to display more information, and auto-rebuilds when you change the source files)
* `npm run serve`
* You access the development server on a different port (it's printed on the command line when you run the above command). This means that when it starts up you will need to tell it where the microscope server is, using the "override API origin" field in the page that pops up. If you are running a test server on your computer, this is most likely `http://localhost:5000/`.
## Formatting, linting, and tests ## Formatting, linting, and tests
All of the commands below assume that you are running in the OFM virtual environment, i.e. you have run `ofm activate` on an OpenFlexure SD card, or `source .venv/bin/activate` on Linux, or `.venv/Scripts/activate`on Windows. All of the commands below assume that you are running in the OFM virtual environment, i.e. you have run `ofm activate` on an OpenFlexure SD card, or `source .venv/bin/activate` on Linux, or `.venv/Scripts/activate`on Windows.

26
check_version_sync.py Executable file
View file

@ -0,0 +1,26 @@
#! /usr/bin/env python3
"""
A script to check that the npm version string matches the python
version string exactly.
"""
import json
import tomllib
import os
with open("pyproject.toml", "rb") as toml_f:
pyproject_data = tomllib.load(toml_f)
with open(os.path.join("webapp", "package.json"), "r", encoding="utf-8") as json_f:
webapp_data = json.load(json_f)
py_ver = pyproject_data["project"]["version"]
js_ver = webapp_data["version"]
assert py_ver == js_ver, (
"The python and javascript version numbers should match.\n"
f"Python version: {py_ver}\n"
f"Javascript version: {js_ver}\n"
""
)

View file

@ -7,7 +7,12 @@
"/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
"/system_control/": "openflexure_microscope_server.things.system_control:SystemControlThing", "/system_control/": "openflexure_microscope_server.things.system_control:SystemControlThing",
"/settings/": "openflexure_microscope_server.things.settings_manager:SettingsManager", "/settings/": "openflexure_microscope_server.things.settings_manager:SettingsManager",
"/smart_scan/": "openflexure_microscope_server.things.smart_scan:SmartScanThing", "/smart_scan/": {
"class": "openflexure_microscope_server.things.smart_scan:SmartScanThing",
"kwargs": {
"scans_folder": "/var/openflexure/scans/"
}
},
"/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing", "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing",
"/capture/": "openflexure_microscope_server.things.capture:CaptureThing" "/capture/": "openflexure_microscope_server.things.capture:CaptureThing"
}, },

View file

@ -7,7 +7,12 @@
"/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
"/system_control/": "openflexure_microscope_server.things.system_control:SystemControlThing", "/system_control/": "openflexure_microscope_server.things.system_control:SystemControlThing",
"/settings/": "openflexure_microscope_server.things.settings_manager:SettingsManager", "/settings/": "openflexure_microscope_server.things.settings_manager:SettingsManager",
"/smart_scan/": "openflexure_microscope_server.things.smart_scan:SmartScanThing", "/smart_scan/": {
"class": "openflexure_microscope_server.things.smart_scan:SmartScanThing",
"kwargs": {
"scans_folder": "./openflexure/scans/"
}
},
"/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing", "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing",
"/capture/": "openflexure_microscope_server.things.capture:CaptureThing" "/capture/": "openflexure_microscope_server.things.capture:CaptureThing"
}, },

View file

@ -7,7 +7,12 @@
"/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "/camera_stage_mapping/": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
"/system_control/": "openflexure_microscope_server.things.system_control:SystemControlThing", "/system_control/": "openflexure_microscope_server.things.system_control:SystemControlThing",
"/settings/": "openflexure_microscope_server.things.settings_manager:SettingsManager", "/settings/": "openflexure_microscope_server.things.settings_manager:SettingsManager",
"/smart_scan/": "openflexure_microscope_server.things.smart_scan:SmartScanThing", "/smart_scan/": {
"class": "openflexure_microscope_server.things.smart_scan:SmartScanThing",
"kwargs": {
"scans_folder": "./openflexure/scans/"
}
},
"/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing", "/background_detect/": "openflexure_microscope_server.things.background_detect:BackgroundDetectThing",
"/capture/": "openflexure_microscope_server.things.capture:CaptureThing" "/capture/": "openflexure_microscope_server.things.capture:CaptureThing"
}, },

View file

@ -1,6 +1,6 @@
[project] [project]
name = "openflexure-microscope-server" name = "openflexure-microscope-server"
version = "3.0.0.dev0" version = "3.0.0-alpha1"
authors = [ authors = [
{ name="Richard Bowman", email="richard.bowman@cantab.net" }, { name="Richard Bowman", email="richard.bowman@cantab.net" },
{ name="Joel Collins" }, { name="Joel Collins" },
@ -15,7 +15,7 @@ classifiers = [
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
dependencies = [ dependencies = [
"labthings-fastapi[server] == 0.0.7", "labthings-fastapi[server] == 0.0.9",
"labthings-sangaboard", "labthings-sangaboard",
"camera-stage-mapping ~= 0.1.10", "camera-stage-mapping ~= 0.1.10",
"opencv-python ~= 4.11.0", "opencv-python ~= 4.11.0",
@ -102,22 +102,21 @@ select = [
# "PL", # Pylint (a subset of, catches far less than pylint does) # "PL", # Pylint (a subset of, catches far less than pylint does)
# "B", # Flake8 bugbear # "B", # Flake8 bugbear
"A", # Flake8 builtins checker "A", # Flake8 builtins checker
# "C", # Flake8 comprehensions "C", # Flake8 comprehensions
# "FIX", #TODO/FIXME's in code. These should be added during develoment for ongoing tasks. # "FIX", #TODO/FIXME's in code. These should be added during develoment for ongoing tasks.
# If they are not fixed before merge they should be converted to GitLab issues # If they are not fixed before merge they should be converted to GitLab issues
# "LOG", # Flake8 logging issues (pedantic logger formatting issues can be added with "G") # "LOG", # Flake8 logging issues (pedantic logger formatting issues can be added with "G")
# "T20", # Warns for print statments, production code should log # "T20", # Warns for print statments, production code should log
# "PT", # pytest linting # "PT", # pytest linting
# "RET", # Consistent clear return statments "RET", # Consistent clear return statements
"RSE", # Raise parentheses "RSE", # Raise parentheses
# "SIM", # Simplifications detected # "SIM", # Simplifications detected
# "ARG", # unused arguments "ARG", # unused arguments
# "C90", # McCabe complexity! "C90", # McCabe complexity!
"NPY", # Numpy linting "NPY", # Numpy linting
"N", # PEP8 naming "N", # PEP8 naming
# "DOC", # Enforce argument, return, and raise to be mentioned in docstrings there is # "D", # Docstring checks these may need to be added gradually
# also "D" but this even warns about docstring punctuation. Perhaps a step too "ERA001", # Commented out code!
# far?
] ]
ignore = [ ignore = [

View file

@ -24,16 +24,15 @@ select = [
"LOG", # Flake8 logging issues (pedantic logger formatting issues can be added with "G") "LOG", # Flake8 logging issues (pedantic logger formatting issues can be added with "G")
"T20", # Warns for print statments, production code should log "T20", # Warns for print statments, production code should log
"PT", # pytest linting "PT", # pytest linting
"RET", # Consistent clear return statments "RET", # Consistent clear return statements
"RSE", # Raise parentheses "RSE", # Raise parentheses
"SIM", # Simplifications detected "SIM", # Simplifications detected
"ARG", # unused arguments "ARG", # unused arguments
"C90", # McCabe complexity! "C90", # McCabe complexity!
"NPY", # Numpy linting "NPY", # Numpy linting
"N", # PEP8 naming "N", # PEP8 naming
"DOC", # Enforce argument, return, and raise to be mentioned in docstrings there is "D", # Docstring checks these may need to be added gradually
# also "D" but this even warns about docstring punctuation. Perhaps a step too "ERA001", # Commented out code!
# far?
] ]
# Line length is set to 88 for ruff. This is what the formatter aims for # Line length is set to 88 for ruff. This is what the formatter aims for

View file

@ -10,17 +10,33 @@ from .legacy_api import add_v2_endpoints
from ..logging import configure_logging, retrieve_log, retrieve_log_from_file from ..logging import configure_logging, retrieve_log, retrieve_log_from_file
def customise_server(server: ThingServer, log_folder: str): def customise_server(server: ThingServer, log_folder: str, scans_folder: Optional[str]):
"""Customise the server with additional endpoints, etc.""" """Customise the server with additional endpoints, etc."""
configure_logging(log_folder) configure_logging(log_folder)
add_v2_endpoints(server) add_v2_endpoints(server)
add_static_files(server.app) add_static_files(server.app, scans_folder)
# Add an endpoint to get the logs - (directly calling the FastAPI decorator) # Add an endpoint to get the logs - (directly calling the FastAPI decorator)
server.app.get("/log/")(retrieve_log) server.app.get("/log/")(retrieve_log)
server.app.get("/logfile/")(retrieve_log_from_file) server.app.get("/logfile/")(retrieve_log_from_file)
def _get_scans_dir(config: dict) -> Optional[str]:
"""
Read the config and return the scans directory.
Return is None if there is no /smart_scan/ thing loaded.
"""
if "/smart_scan/" in config["things"]:
try:
return config["things"]["/smart_scan/"]["kwargs"]["scans_folder"]
except KeyError as e:
msg = "Configuration error smart scan should have scans_folder kwarg set"
raise RuntimeError(msg) from e
return None
def serve_from_cli(argv: Optional[list[str]] = None): def serve_from_cli(argv: Optional[list[str]] = None):
"""Start the server from the command line""" """Start the server from the command line"""
args = cli.parse_args(argv) args = cli.parse_args(argv)
@ -29,11 +45,16 @@ def serve_from_cli(argv: Optional[list[str]] = None):
log_config["loggers"]["uvicorn"]["propagate"] = True log_config["loggers"]["uvicorn"]["propagate"] = True
log_config["loggers"]["uvicorn.access"]["propagate"] = True log_config["loggers"]["uvicorn.access"]["propagate"] = True
# Create server and config vars before trying to configure so they are defined
# if fallback is needed before they are set.
config = None
server = None
try: try:
config = cli.config_from_args(args) config = cli.config_from_args(args)
log_folder = config.get("log_folder", "./openflexure/logs") log_folder = config.get("log_folder", "./openflexure/logs")
scans_folder = _get_scans_dir(config)
server = cli.server_from_config(config) server = cli.server_from_config(config)
customise_server(server, log_folder) customise_server(server, log_folder, scans_folder)
uvicorn.run( uvicorn.run(
server.app, server.app,
host=args.host, host=args.host,

View file

@ -1,4 +1,5 @@
import os import os
from typing import Optional
from fastapi.responses import FileResponse, RedirectResponse from fastapi.responses import FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@ -22,7 +23,7 @@ def add_static_file(app: FastAPI, fname: str, folder: str) -> None:
) )
def add_static_files(app: FastAPI) -> None: def add_static_files(app: FastAPI, scans_folder: Optional[str]) -> None:
"""Add the static files responsible for the webapp app to the FastAPI app """Add the static files responsible for the webapp app to the FastAPI app
app: The FastAPI app to add to, in this case the OpenFlexure server app: The FastAPI app to add to, in this case the OpenFlexure server
@ -45,9 +46,13 @@ def add_static_files(app: FastAPI) -> None:
name=f"static_{fname}", name=f"static_{fname}",
) )
# Mount the scan directory to .../scans/, to allow dzi viewing # If scans folder is None, there is not smart scan thing. So nothing to mount.
app.mount( if scans_folder is not None:
"/scans/", # Mount the scan directory to .../scans/, to allow dzi viewing
StaticFiles(directory="/var/openflexure/scans/"), if not os.path.isdir(scans_folder):
name="scans", os.makedirs(scans_folder)
) app.mount(
"/scans/",
StaticFiles(directory=scans_folder),
name="scans",
)

View file

@ -118,20 +118,12 @@ class RecentringThing(Thing):
logging.info( logging.info(
f"Breaking because the highest point is at {np.argmax(sorted_all_heights)} in the list" f"Breaking because the highest point is at {np.argmax(sorted_all_heights)} in the list"
) )
# plt.plot(sorted_lateral, sorted_all_heights,'.')
# plt.plot(sorted_lateral, quad_fit_func(sorted_lateral))
# plt.show()
break break
else: if turning_loc < np.min(sorted_lateral):
if turning_loc < np.min(sorted_lateral): moves = -1
moves = -1 elif turning_loc > np.max(sorted_lateral):
elif turning_loc > np.max(sorted_lateral): moves = 1
moves = 1
else:
# plt.plot(sorted_lateral, sorted_all_heights,'.')
# plt.plot(sorted_lateral, quad_fit_func(sorted_lateral))
# plt.show()
pass
# Centre value is replaced by the maximum value recorded in that axis # Centre value is replaced by the maximum value recorded in that axis
centre[direction] = focused_pos[direction][np.argmax(all_heights)][ centre[direction] = focused_pos[direction][np.argmax(all_heights)][

View file

@ -105,11 +105,12 @@ class JPEGSharpnessMonitor:
stop: int = int(np.argmax(jpeg_times > stage_times[1])) stop: int = int(np.argmax(jpeg_times > stage_times[1]))
except ValueError as e: except ValueError as e:
if np.sum(jpeg_times > stage_times[0]) == 0: if np.sum(jpeg_times > stage_times[0]) == 0:
raise ValueError( errmsg = (
"No images were captured during the move of the stage. Perhaps the camera is not streaming images?" "No images were captured during the move of the stage. "
) from e "Perhaps the camera is not streaming images?"
else: )
raise e raise ValueError(errmsg) from e
raise e
if stop < 1: if stop < 1:
stop = len(jpeg_times) stop = len(jpeg_times)
logging.debug("changing stop to %s", (stop)) logging.debug("changing stop to %s", (stop))

View file

@ -23,8 +23,7 @@ class BackgroundDetectThing(Thing):
bd = self.thing_settings.get("background_distributions", None) bd = self.thing_settings.get("background_distributions", None)
if bd: if bd:
return ChannelDistributions(**bd) return ChannelDistributions(**bd)
else: return None
return None
@background_distributions.setter @background_distributions.setter
def background_distributions(self, value: Optional[ChannelDistributions]) -> None: def background_distributions(self, value: Optional[ChannelDistributions]) -> None:

View file

@ -83,6 +83,7 @@ class OpenCVCamera(BaseCamera):
It's likely to be highly inefficient - raw and/or uncompressed captures using It's likely to be highly inefficient - raw and/or uncompressed captures using
binary image formats will be added in due course. binary image formats will be added in due course.
""" """
logging.warning(f"OpenCV camera doen't respect {resolution} setting")
ret, frame = self.cap.read() ret, frame = self.cap.read()
if not ret: if not ret:
raise RuntimeError( raise RuntimeError(
@ -100,6 +101,7 @@ class OpenCVCamera(BaseCamera):
This function will produce a JPEG image. This function will produce a JPEG image.
""" """
logging.warning(f"OpenCV camera doen't respect {resolution} setting")
frame = self.capture_array() frame = self.capture_array()
jpeg = cv2.imencode(".jpg", frame)[1].tobytes() jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
exif_dict = { exif_dict = {

View file

@ -104,15 +104,15 @@ class SimulatedCamera(BaseCamera):
"""Generate an image with blobs based on supplied coordinates""" """Generate an image with blobs based on supplied coordinates"""
canvas_width, canvas_height, _ = self.canvas_shape canvas_width, canvas_height, _ = self.canvas_shape
image_width, image_height, _ = self.shape image_width, image_height, _ = self.shape
pos = [x * RATIO for x in pos] pos = tuple(x * RATIO for x in pos)
top_left = ( top_left = (
int(pos[0]) - image_width // 2 - canvas_width // 2, int(pos[0]) - image_width // 2 - canvas_width // 2,
int(pos[1]) - image_height // 2 - canvas_height // 2, int(pos[1]) - image_height // 2 - canvas_height // 2,
) )
focused_image = self.canvas[ x_slice = slice(top_left[0], top_left[0] + self.shape[0])
tuple(slice(top_left[i], top_left[i] + self.shape[i]) for i in range(2)) y_slice = slice(top_left[1], top_left[1] + self.shape[1])
+ (slice(None),) z_slice = slice(None)
] focused_image = self.canvas[(x_slice, y_slice, z_slice)]
image = gaussian_filter( image = gaussian_filter(
focused_image, focused_image,
sigma=np.abs(pos[2]) / 5, sigma=np.abs(pos[2]) / 5,
@ -189,6 +189,7 @@ class SimulatedCamera(BaseCamera):
It's likely to be highly inefficient - raw and/or uncompressed captures using It's likely to be highly inefficient - raw and/or uncompressed captures using
binary image formats will be added in due course. binary image formats will be added in due course.
""" """
logging.warning(f"Simulation camera doen't respect {resolution} setting")
return self.generate_frame() return self.generate_frame()
@thing_action @thing_action
@ -201,6 +202,7 @@ class SimulatedCamera(BaseCamera):
This function will produce a JPEG image. This function will produce a JPEG image.
""" """
logging.warning(f"Simulation camera doen't respect {resolution} setting")
frame = self.capture_array() frame = self.capture_array()
jpeg = cv2.imencode(".jpg", frame)[1].tobytes() jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
exif_dict = { exif_dict = {

View file

@ -87,7 +87,7 @@ def make_hardware_interface(
axes = stage.axis_names axes = stage.axis_names
def pos2dict(pos: Sequence[float]) -> Mapping[str, float]: def pos2dict(pos: Sequence[float]) -> Mapping[str, float]:
return {k: p for k, p in zip(axes, pos)} return dict(zip(axes, pos))
def dict2pos(posd: Mapping[str, float]) -> Sequence[float]: def dict2pos(posd: Mapping[str, float]) -> Sequence[float]:
return tuple(posd[k] for k in axes if k in posd) return tuple(posd[k] for k in axes if k in posd)
@ -228,10 +228,12 @@ class CameraStageMapper(Thing):
This performs two 1d calibrations in x and y, then combines their results. This performs two 1d calibrations in x and y, then combines their results.
""" """
logger.info("Calibrating X axis:") # Calibrate y-axis first as it is more likely to fail.
cal_x: dict = self.calibrate_1d(hw, stage, logger, (1, 0, 0)) # The x-y difference is due to the camera aspect ratio, not the stage hardware.
logger.info("Calibrating Y axis:") logger.info("Calibrating Y axis:")
cal_y: dict = self.calibrate_1d(hw, stage, logger, (0, 1, 0)) cal_y: dict = self.calibrate_1d(hw, stage, logger, (0, 1, 0))
logger.info("Calibrating X axis:")
cal_x: dict = self.calibrate_1d(hw, stage, logger, (1, 0, 0))
logger.info("Calibration complete, updating metadata.") logger.info("Calibration complete, updating metadata.")
# Combine X and Y calibrations to make a 2D calibration # Combine X and Y calibrations to make a 2D calibration

View file

@ -114,7 +114,8 @@ def _scan_running(method):
class SmartScanThing(Thing): class SmartScanThing(Thing):
def __init__(self): def __init__(self, scans_folder):
self._scans_folder = scans_folder
self._preview_stitch_popen = None self._preview_stitch_popen = None
self._preview_stitch_popen_lock = threading.Lock() self._preview_stitch_popen_lock = threading.Lock()
self._scan_lock = threading.Lock() self._scan_lock = threading.Lock()
@ -270,11 +271,8 @@ class SmartScanThing(Thing):
@property @property
def base_scan_dir(self) -> str: def base_scan_dir(self) -> str:
"""This directory will hold all the scans we do.""" """The path of the directory where the scans are saved."""
# TODO: This should be determined using sensible configuration. return self._scans_folder
# If the working directory is `/var/openflexure` this will result
# in scans being saved at `/var/openflexure/scans/`
return "scans"
@thing_property @thing_property
def latest_scan_name(self) -> Optional[str]: def latest_scan_name(self) -> Optional[str]:

View file

@ -80,7 +80,7 @@ class BaseStage(Thing):
position = PropertyDescriptor( position = PropertyDescriptor(
Mapping[str, int], Mapping[str, int],
{k: 0 for k in _axis_names}, dict.fromkeys(_axis_names, 0),
description="Current position of the stage", description="Current position of the stage",
readonly=True, readonly=True,
observable=True, observable=True,

View file

@ -91,5 +91,5 @@ class DummyStage(BaseStage):
It is intended for use after manually or automatically recentring the It is intended for use after manually or automatically recentring the
stage. stage.
""" """
self.position = {k: 0 for k in self.axis_names} self.position = dict.fromkeys(self.axis_names, 0)
self.instantaneous_position = self.position self.instantaneous_position = self.position

View file

@ -189,8 +189,7 @@ def get_expected_result_for_example_smart_spiral(
""" """
pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl") pkl_fname = os.path.join(THIS_DIR, f"example_smart_spiral_{sample_name}.pkl")
with open(pkl_fname, "rb") as pkl_file_obj: with open(pkl_fname, "rb") as pkl_file_obj:
planner = pickle.load(pkl_file_obj) return pickle.load(pkl_file_obj)
return planner
def load_sample_points(sample_name: str): def load_sample_points(sample_name: str):

View file

@ -18,11 +18,20 @@
* Install Node.js (and npm) * Install Node.js (and npm)
* Install dependencies with `npm install` * Install dependencies with `npm install`
* Node v18 changes SSL, and so you need `$env:NODE_OPTIONS = "--openssl-legacy-provider"` on Windows or `export NODE_OPTIONS=--openssl-legacy-provider` on Linux/MacOS for compatibility. * Node v18 changes SSL, and so you need a legacy version for compatibility:
* On Windows: `$env:NODE_OPTIONS = "--openssl-legacy-provider"`
* On Linux/MacOS `export NODE_OPTIONS=--openssl-legacy-provider`
* Build the static web app with `npm run build` * Build the static web app with `npm run build`
* Serve a development version with `npm run serve`
We generally run this on the Raspberry Pi (as that is where the Webapp is hosted). If this isn't suitable - for example, if you can't install Node on your microscope due to version conflicts or no internet connection - you can build it on your computer instead, and then copy over the contents of `..\src\openflexure_microscope_server\static` to your Pi (using scp or another file transfer method). # Developing
When developing it is useful to use the development server so Vue changes happen instantly without the need to rebuild. To start the development server run:
npm run serve
The development server is accessed on a different port from the microscope API. The port to access the development server is printed on the command line when you run the above command.
When the development webapp starts it cannot locate the microscope API as this is served by the microscope on port 5000. Instead it will present you with a page giving you the option to "override API origin". If you are running a development server on your computer, you should enter `http://localhost:5000/`.
## VS Code and ESLint ## VS Code and ESLint

214
webapp/package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "openflexure-microscope-jsclient", "name": "openflexure-microscope-jsclient",
"version": "0.0.0", "version": "3.0.0-alpha1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "openflexure-microscope-jsclient", "name": "openflexure-microscope-jsclient",
"version": "0.0.0", "version": "3.0.0-alpha1",
"license": "GNU General Public License v3.0 ", "license": "GNU General Public License v3.0 ",
"dependencies": { "dependencies": {
"imjoy-core": "^0.13", "imjoy-core": "^0.13",
@ -27,6 +27,7 @@
"eslint": "^6.8", "eslint": "^6.8",
"eslint-plugin-prettier": "^3.4", "eslint-plugin-prettier": "^3.4",
"eslint-plugin-vue": "^6.2", "eslint-plugin-vue": "^6.2",
"genversion": "^3.2.0",
"less": "^3.13", "less": "^3.13",
"less-loader": "^5.0", "less-loader": "^5.0",
"mdns-js": "^1.0", "mdns-js": "^1.0",
@ -6867,6 +6868,39 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.0.1"
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/filesize": { "node_modules/filesize": {
"version": "3.6.1", "version": "3.6.1",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
@ -6984,6 +7018,16 @@
"url": "https://github.com/avajs/find-cache-dir?sponsor=1" "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
} }
}, },
"node_modules/find-package": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/find-package/-/find-package-1.0.0.tgz",
"integrity": "sha512-yVn71XCCaNgxz58ERTl8nA/8YYtIQDY9mHSrgFBfiFtdNNfY0h183Vh8BRkKxD8x9TUw3ec290uJKhDVxqGZBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"parents": "^1.0.1"
}
},
"node_modules/find-up": { "node_modules/find-up": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@ -7245,6 +7289,50 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/genversion": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/genversion/-/genversion-3.2.0.tgz",
"integrity": "sha512-OIYSX6XYA8PHecLDCTri30hadSZfAjZ8Iq1+BBDXqLWP4dRLuJNLoNjsSWtTpw97IccK2LDWzkEstxAB8GdN7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^7.2.0",
"ejs": "^3.1.9",
"find-package": "^1.0.0"
},
"bin": {
"genversion": "bin/genversion.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/genversion/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/genversion/node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
},
"bin": {
"ejs": "bin/cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/get-caller-file": { "node_modules/get-caller-file": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -9047,6 +9135,108 @@
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"dev": true "dev": true
}, },
"node_modules/jake": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.3",
"chalk": "^4.0.2",
"filelist": "^1.0.4",
"minimatch": "^3.1.2"
},
"bin": {
"jake": "bin/cli.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/jake/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jake/node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"dev": true,
"license": "MIT"
},
"node_modules/jake/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jake/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/jake/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/jake/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/jake/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/javascript-stringify": { "node_modules/javascript-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
@ -10798,6 +10988,16 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parents": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz",
"integrity": "sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-platform": "~0.11.15"
}
},
"node_modules/parse-asn1": { "node_modules/parse-asn1": {
"version": "5.1.6", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
@ -10924,6 +11124,16 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true "dev": true
}, },
"node_modules/path-platform": {
"version": "0.11.15",
"resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz",
"integrity": "sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",

View file

@ -1,14 +1,15 @@
{ {
"name": "openflexure-microscope-jsclient", "name": "openflexure-microscope-jsclient",
"version": "0.0.0", "version": "3.0.0-alpha1",
"private": true, "private": true,
"description": "User client for the OpenFlexure Microscope Server", "description": "User client for the OpenFlexure Microscope Server",
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)", "author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
"scripts": { "scripts": {
"serve": "vue-cli-service serve --mode development", "gv": "genversion src/version.js",
"serve-without-imjoy": "VUE_APP_ENABLE_IMJOY=false vue-cli-service serve --mode development", "serve": "npm run gv; vue-cli-service serve --mode development",
"build": "vue-cli-service build --openssl-legacy-provider --mode production", "serve-without-imjoy": "npm run gv; VUE_APP_ENABLE_IMJOY=false vue-cli-service serve --mode development",
"build-without-imjoy": "VUE_APP_ENABLE_IMJOY=false vue-cli-service build --mode production", "build": "npm run gv; vue-cli-service build --openssl-legacy-provider --mode production",
"build-without-imjoy": "npm run gv; VUE_APP_ENABLE_IMJOY=false vue-cli-service build --mode production",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint",
"lint:fix": "vue-cli-service lint --fix" "lint:fix": "vue-cli-service lint --fix"
}, },
@ -31,6 +32,7 @@
"eslint": "^6.8", "eslint": "^6.8",
"eslint-plugin-prettier": "^3.4", "eslint-plugin-prettier": "^3.4",
"eslint-plugin-vue": "^6.2", "eslint-plugin-vue": "^6.2",
"genversion": "^3.2.0",
"less": "^3.13", "less": "^3.13",
"less-loader": "^5.0", "less-loader": "^5.0",
"mdns-js": "^1.0", "mdns-js": "^1.0",

View file

@ -171,6 +171,26 @@ export default {
}, },
mounted() { mounted() {
//Define .from_index() as a custom function, working similar to .at()
//For backwards compatibility, not using in-built .at() until updates to Connect
function from_index(n) {
// ToInteger() abstract op
n = Math.trunc(n) || 0;
// Allow negative indexing from the end
if (n < 0) n += this.length;
// OOB access is guaranteed to return undefined
if (n < 0 || n >= this.length) return undefined;
// Otherwise, this is just normal property access
return this[n];
}
const TypedArray = Reflect.getPrototypeOf(Int8Array);
for (const C of [Array, String, TypedArray]) {
Object.defineProperty(C.prototype, "from_index",
{ value: from_index,
writable: true,
enumerable: false,
configurable: true });
}
// Check for already running tasks // Check for already running tasks
if (this.taskStarted != true) { if (this.taskStarted != true) {
this.checkExistingTasks(); this.checkExistingTasks();
@ -299,14 +319,14 @@ export default {
if (!this.progress) this.progress = 1; if (!this.progress) this.progress = 1;
// Test whether the log is empty or the most recent message is not from an error // Test whether the log is empty or the most recent message is not from an error
// If so, return a default message // If so, return a default message
if (response.data.log.length == 0 | response.data.log.at(-1).levelname != "ERROR") { if (response.data.log.length == 0 | response.data.log.from_index(-1).levelname != "ERROR") {
var message = "Unexpected error, please check the logs"; var message = "Unexpected error, please check the logs";
} }
// As LabThings Actions add the message from any raised exception to the log, the // As LabThings Actions add the message from any raised exception to the log, the
// last message in the log is the message from the Exception. // last message in the log is the message from the Exception.
//If the Exception was raised with no message, use a default. //If the Exception was raised with no message, use a default.
else { else {
message = response.data.log.at(-1).message||"Unexpected error, please check the logs"; message = response.data.log.from_index(-1).message||"Unexpected error, please check the logs";
} }
// Raise an Error with the chosen message // Raise an Error with the chosen message
reject(new Error(message)); reject(new Error(message));

View file

@ -25,7 +25,7 @@
<div> <div>
<b>Server Version:</b> <br /> <b>Server Version:</b> <br />
V3 (pre-alpha) v{{ app_version() }}
</div> </div>
<hr /> <hr />

View file

@ -386,6 +386,9 @@ export default {
margin-left: 5px; margin-left: 5px;
margin-right: 5px; margin-right: 5px;
width: 3em; width: 3em;
/* Stop Firefox showing input spinners, other
browsers set with block below */
-moz-appearance: textfield;
} }
/* Chrome, Safari, Edge, Opera */ /* Chrome, Safari, Edge, Opera */
.numeric-setting-line-input::-webkit-outer-spin-button, .numeric-setting-line-input::-webkit-outer-spin-button,

View file

@ -11,6 +11,7 @@ require("vue-tour/dist/vue-tour.css");
// Import MD icons // Import MD icons
import "material-symbols/outlined.css"; import "material-symbols/outlined.css";
import version from './version.js'
// UIKit overrides // UIKit overrides
UIkit.mixin( UIkit.mixin(
@ -35,6 +36,9 @@ Vue.config.productionTip = false;
Vue.mixin({ Vue.mixin({
methods: { methods: {
app_version() {
return version;
},
thingDescription(thing) { thingDescription(thing) {
return this.$store.getters["wot/thingDescription"](thing); return this.$store.getters["wot/thingDescription"](thing);
}, },