Merge branch 'master' of https://gitlab.com/openflexure/openflexure-microscope-server
This commit is contained in:
commit
877b2210ed
8 changed files with 68 additions and 64 deletions
|
|
@ -9,7 +9,7 @@ import logging, logging.handlers
|
||||||
import os
|
import os
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
from flask import Flask, send_file
|
from flask import Flask, send_file, abort
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
@ -33,31 +33,42 @@ from openflexure_microscope.api.microscope import default_microscope as api_micr
|
||||||
from openflexure_microscope.api.v2 import views
|
from openflexure_microscope.api.v2 import views
|
||||||
|
|
||||||
# Handle logging
|
# Handle logging
|
||||||
DEFAULT_LOGFILE = logs_file_path("openflexure_microscope.log")
|
ROOT_LOGFILE = logs_file_path("openflexure_microscope.log")
|
||||||
|
ACCESS_LOGFILE = logs_file_path("openflexure_microscope.access.log")
|
||||||
|
|
||||||
logger = logging.getLogger()
|
# Basic log format
|
||||||
|
formatter = logging.Formatter(
|
||||||
error_formatter = logging.Formatter(
|
|
||||||
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
|
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
|
||||||
)
|
)
|
||||||
|
|
||||||
rotating_logfile = logging.handlers.RotatingFileHandler(
|
|
||||||
DEFAULT_LOGFILE, maxBytes=1_000_000, backupCount=7
|
|
||||||
)
|
|
||||||
|
|
||||||
error_handlers = [rotating_logfile, logging.StreamHandler()]
|
|
||||||
|
|
||||||
for handler in error_handlers:
|
|
||||||
handler.setFormatter(error_formatter)
|
|
||||||
logger.addHandler(handler)
|
|
||||||
|
|
||||||
|
# Get root logger
|
||||||
|
logger = logging.getLogger()
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# Create file handler
|
||||||
|
fh = logging.handlers.RotatingFileHandler(
|
||||||
|
ROOT_LOGFILE, maxBytes=1_000_000, backupCount=5
|
||||||
|
)
|
||||||
|
fh.setFormatter(formatter)
|
||||||
|
fh.setLevel(logging.INFO)
|
||||||
|
fh.propagate = False
|
||||||
|
|
||||||
|
# Create access log file handler
|
||||||
|
afh = logging.handlers.RotatingFileHandler(
|
||||||
|
ACCESS_LOGFILE, maxBytes=1_000_000, backupCount=5
|
||||||
|
)
|
||||||
|
afh.setFormatter(formatter)
|
||||||
|
afh.setLevel(logging.INFO)
|
||||||
|
afh.propagate = False
|
||||||
|
|
||||||
|
# Add file handler to root logger
|
||||||
|
logger.addHandler(fh)
|
||||||
|
|
||||||
# Log server paths being used
|
# Log server paths being used
|
||||||
logging.info(f"Running with data path {OPENFLEXURE_VAR_PATH}")
|
logging.info(f"Running with data path {OPENFLEXURE_VAR_PATH}")
|
||||||
|
|
||||||
print("Creating app")
|
logging.info("Creating app")
|
||||||
# Create flask app
|
# Create flask app
|
||||||
app, labthing = create_app(
|
app, labthing = create_app(
|
||||||
__name__,
|
__name__,
|
||||||
|
|
@ -141,11 +152,15 @@ def err_log():
|
||||||
"""
|
"""
|
||||||
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
return send_file(
|
return send_file(
|
||||||
DEFAULT_LOGFILE,
|
ROOT_LOGFILE,
|
||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
attachment_filename="openflexure_microscope_{}.log".format(timestamp),
|
attachment_filename="openflexure_microscope_{}.log".format(timestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route('/api/v1/', defaults={'path': ''})
|
||||||
|
@app.route('/api/v1/<path:path>')
|
||||||
|
def api_v1_catch_all(path):
|
||||||
|
abort(410, 'API v1 is no longer in use. Please upgrade your client.')
|
||||||
|
|
||||||
# Automatically clean up microscope at exit
|
# Automatically clean up microscope at exit
|
||||||
def cleanup():
|
def cleanup():
|
||||||
|
|
@ -176,6 +191,9 @@ atexit.register(cleanup)
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from labthings.server.wsgi import Server
|
from labthings.server.wsgi import Server
|
||||||
|
|
||||||
print("Starting OpenFlexure Microscope Server...")
|
# Block the access logs from propagating up to the root logger
|
||||||
server = Server(app, log=logger, error_log=logger)
|
logging.getLogger("labthings.server.wsgi.handler").propagate = False
|
||||||
|
|
||||||
|
logging.info("Starting OpenFlexure Microscope Server...")
|
||||||
|
server = Server(app, log=afh, error_log=None)
|
||||||
server.run(host="::", port=5000, debug=False, zeroconf=True)
|
server.run(host="::", port=5000, debug=False, zeroconf=True)
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class JPEGSharpnessMonitor:
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"Stop the background thread"
|
"Stop the background thread"
|
||||||
self.stop_event.set()
|
self.stop_event.set()
|
||||||
print("Joining JPEG thread")
|
logging.info("Joining JPEG thread")
|
||||||
self.background_thread.join()
|
self.background_thread.join()
|
||||||
|
|
||||||
def _measure_jpegs(self):
|
def _measure_jpegs(self):
|
||||||
|
|
@ -75,7 +75,7 @@ class JPEGSharpnessMonitor:
|
||||||
time_now = time.time()
|
time_now = time.time()
|
||||||
self.jpeg_sizes.append(size_now)
|
self.jpeg_sizes.append(size_now)
|
||||||
self.jpeg_times.append(time_now)
|
self.jpeg_times.append(time_now)
|
||||||
print("Exited JPEG measure loop")
|
logging.info("Exited JPEG measure loop")
|
||||||
if self.stop_event.is_set():
|
if self.stop_event.is_set():
|
||||||
logging.debug("Cleanly stopped sharpness measurement in background thread")
|
logging.debug("Cleanly stopped sharpness measurement in background thread")
|
||||||
if self.should_stop():
|
if self.should_stop():
|
||||||
|
|
|
||||||
|
|
@ -73,13 +73,13 @@ def calibrate_xy_grid(tracker, move, step=100, n_steps=4, backlash_compensation=
|
||||||
transformed_image_positions = np.dot(image_positions, A)
|
transformed_image_positions = np.dot(image_positions, A)
|
||||||
residuals = transformed_image_positions - stage_positions
|
residuals = transformed_image_positions - stage_positions
|
||||||
fractional_error = norm(residuals) / stage_positions.shape[0]
|
fractional_error = norm(residuals) / stage_positions.shape[0]
|
||||||
print(f"Ratio of residuals to displacement is {fractional_error})")
|
logging.debug(f"Ratio of residuals to displacement is {fractional_error})")
|
||||||
if fractional_error > 0.05: # Check it was a reasonably good fit
|
if fractional_error > 0.05: # Check it was a reasonably good fit
|
||||||
print(
|
logging.warning(
|
||||||
"Warning: the error fitting measured displacements was %.1f%%"
|
"Warning: the error fitting measured displacements was %.1f%%"
|
||||||
% (fractional_error * 100)
|
% (fractional_error * 100)
|
||||||
)
|
)
|
||||||
print(
|
logging.info(
|
||||||
f"Calibrated the pixel-location matrix.\nResiduals were {fractional_error*100:.1f}% of the shift."
|
f"Calibrated the pixel-location matrix.\nResiduals were {fractional_error*100:.1f}% of the shift."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,19 @@ def flat_lens_shading_table(camera):
|
||||||
|
|
||||||
def adjust_exposure_to_setpoint(camera, setpoint):
|
def adjust_exposure_to_setpoint(camera, setpoint):
|
||||||
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>."""
|
"""Adjust the camera's exposure time until the maximum pixel value is <setpoint>."""
|
||||||
print("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
|
logging.info("Adjusting shutter speed to hit setpoint {}".format(setpoint), end="")
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
print(".", end="")
|
print(".", end="")
|
||||||
camera.shutter_speed = int(
|
camera.shutter_speed = int(
|
||||||
camera.shutter_speed * setpoint / np.max(rgb_image(camera))
|
camera.shutter_speed * setpoint / np.max(rgb_image(camera))
|
||||||
)
|
)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print("done")
|
logging.info("done")
|
||||||
|
|
||||||
|
|
||||||
def auto_expose_and_freeze_settings(camera):
|
def auto_expose_and_freeze_settings(camera):
|
||||||
"""Freeze the settings after auto-exposing to white illumination"""
|
"""Freeze the settings after auto-exposing to white illumination"""
|
||||||
print("Allowing the camera to auto-expose")
|
logging.info("Allowing the camera to auto-expose")
|
||||||
camera.awb_mode = "auto"
|
camera.awb_mode = "auto"
|
||||||
camera.exposure_mode = "auto"
|
camera.exposure_mode = "auto"
|
||||||
camera.iso = (
|
camera.iso = (
|
||||||
|
|
@ -49,18 +49,18 @@ def auto_expose_and_freeze_settings(camera):
|
||||||
for i in range(6):
|
for i in range(6):
|
||||||
print(".", end="")
|
print(".", end="")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
print("done")
|
logging.info("done")
|
||||||
|
|
||||||
print("Freezing the camera settings...")
|
logging.info("Freezing the camera settings...")
|
||||||
camera.shutter_speed = camera.exposure_speed
|
camera.shutter_speed = camera.exposure_speed
|
||||||
print("Shutter speed = {}".format(camera.shutter_speed))
|
logging.info("Shutter speed = {}".format(camera.shutter_speed))
|
||||||
camera.exposure_mode = "off"
|
camera.exposure_mode = "off"
|
||||||
print("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
|
||||||
print("Auto white balance disabled, gains are {}".format(g))
|
logging.info("Auto white balance disabled, gains are {}".format(g))
|
||||||
print(
|
logging.info(
|
||||||
"Analogue gain: {}, Digital gain: {}".format(
|
"Analogue gain: {}, Digital gain: {}".format(
|
||||||
camera.analog_gain, camera.digital_gain
|
camera.analog_gain, camera.digital_gain
|
||||||
)
|
)
|
||||||
|
|
@ -90,7 +90,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.
|
||||||
print("Generating a lens shading table at {}x{}".format(*lst_resolution))
|
logging.info("Generating a lens shading table at {}x{}".format(*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, :, :]
|
||||||
|
|
@ -107,7 +107,7 @@ def lst_from_channels(channels):
|
||||||
padded_image_channel = np.pad(
|
padded_image_channel = np.pad(
|
||||||
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
|
||||||
print(
|
logging.info(
|
||||||
"Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(
|
"Channel shape: {}x{}, shading table shape: {}x{}, after padding {}".format(
|
||||||
iw, ih, lw * 32, lh * 32, padded_image_channel.shape
|
iw, ih, lw * 32, lh * 32, padded_image_channel.shape
|
||||||
)
|
)
|
||||||
|
|
@ -185,7 +185,7 @@ if __name__ == "__main__":
|
||||||
with PiCamera() as camera:
|
with PiCamera() as camera:
|
||||||
camera.start_preview()
|
camera.start_preview()
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
print("Recalibrating...")
|
logging.info("Recalibrating...")
|
||||||
recalibrate_camera(camera)
|
recalibrate_camera(camera)
|
||||||
print("Done.")
|
logging.info("Done.")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,8 @@ def build_gui_from_dict(gui_description, extension_object):
|
||||||
# Make a working copy of GUI description
|
# Make a working copy of GUI description
|
||||||
api_gui = copy.deepcopy(gui_description)
|
api_gui = copy.deepcopy(gui_description)
|
||||||
|
|
||||||
print("")
|
logging.debug(extension_object)
|
||||||
print(extension_object)
|
logging.debug(extension_object._rules)
|
||||||
print(extension_object._rules)
|
|
||||||
|
|
||||||
# Expand shorthand routes into full relative URLs
|
# Expand shorthand routes into full relative URLs
|
||||||
if "forms" in gui_description and isinstance(api_gui["forms"], list):
|
if "forms" in gui_description and isinstance(api_gui["forms"], list):
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class Microscope:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
### Detector
|
### Detector
|
||||||
print("Creating camera")
|
logging.info("Creating camera")
|
||||||
if configuration.get("camera"):
|
if configuration.get("camera"):
|
||||||
camera_type = configuration["camera"].get("type")
|
camera_type = configuration["camera"].get("type")
|
||||||
if camera_type in ("PiCamera", "PiCameraStreamer"):
|
if camera_type in ("PiCamera", "PiCameraStreamer"):
|
||||||
|
|
@ -94,7 +94,7 @@ class Microscope:
|
||||||
logging.warning("No compatible camera hardware found.")
|
logging.warning("No compatible camera hardware found.")
|
||||||
|
|
||||||
### Stage
|
### Stage
|
||||||
print("Creating stage")
|
logging.info("Creating stage")
|
||||||
if configuration.get("stage"):
|
if configuration.get("stage"):
|
||||||
stage_type = configuration["stage"].get("type")
|
stage_type = configuration["stage"].get("type")
|
||||||
stage_port = configuration["stage"].get("port")
|
stage_port = configuration["stage"].get("port")
|
||||||
|
|
@ -105,7 +105,7 @@ class Microscope:
|
||||||
logging.error(e)
|
logging.error(e)
|
||||||
logging.warning("No compatible Sangaboard hardware found.")
|
logging.warning("No compatible Sangaboard hardware found.")
|
||||||
|
|
||||||
print("Handling fallbacks")
|
logging.info("Handling fallbacks")
|
||||||
### Fallbacks
|
### Fallbacks
|
||||||
if not self.camera:
|
if not self.camera:
|
||||||
self.camera = MissingCamera()
|
self.camera = MissingCamera()
|
||||||
|
|
@ -113,7 +113,7 @@ class Microscope:
|
||||||
self.stage = MissingStage()
|
self.stage = MissingStage()
|
||||||
|
|
||||||
### Locks
|
### Locks
|
||||||
print("Creating locks")
|
logging.info("Creating locks")
|
||||||
if hasattr(self.camera, "lock"):
|
if hasattr(self.camera, "lock"):
|
||||||
self.lock.locks.append(self.camera.lock)
|
self.lock.locks.append(self.camera.lock)
|
||||||
if hasattr(self.stage, "lock"):
|
if hasattr(self.stage, "lock"):
|
||||||
|
|
|
||||||
27
poetry.lock
generated
27
poetry.lock
generated
|
|
@ -290,7 +290,7 @@ description = "Python implementation of LabThings, based on the Flask microframe
|
||||||
name = "labthings"
|
name = "labthings"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6,<4.0"
|
python-versions = ">=3.6,<4.0"
|
||||||
version = "0.6.2"
|
version = "0.6.3"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
Flask = ">=1.1.1,<2.0.0"
|
Flask = ">=1.1.1,<2.0.0"
|
||||||
|
|
@ -379,7 +379,7 @@ description = "Core utilities for Python packages"
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
version = "20.3"
|
version = "20.4"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pyparsing = ">=2.0.2"
|
pyparsing = ">=2.0.2"
|
||||||
|
|
@ -466,19 +466,6 @@ all = ["Sphinx (>=1.5.1)", "check-manifest (>=0.25)", "coverage (>=4.0)", "isort
|
||||||
docs = ["Sphinx (>=1.5.1)"]
|
docs = ["Sphinx (>=1.5.1)"]
|
||||||
tests = ["check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)"]
|
tests = ["check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Python interface to your NPM and package.json."
|
|
||||||
name = "pynpm"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "0.1.2"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
all = ["Sphinx (>=1.5.1)", "check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)"]
|
|
||||||
docs = ["Sphinx (>=1.5.1)"]
|
|
||||||
tests = ["check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest (>=2.8.0)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Python parsing module"
|
description = "Python parsing module"
|
||||||
|
|
@ -773,7 +760,7 @@ ifaddr = "*"
|
||||||
rpi = ["RPi.GPIO"]
|
rpi = ["RPi.GPIO"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "4fbf4240ac033cff8c0879b570792626e29a5aa2c5ffa2ec08b825ef8c2b700c"
|
content-hash = "1c923fc71ff343293798e1bd187df98791716e15e4d970fef1a8d77bf25de1e2"
|
||||||
python-versions = "^3.6"
|
python-versions = "^3.6"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
|
|
@ -942,8 +929,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-0.6.2-py3-none-any.whl", hash = "sha256:b83997a7dd115e0bfe20c2d5c52741c0b747fe8044e12db7bf82a433f8493d8e"},
|
{file = "labthings-0.6.3-py3-none-any.whl", hash = "sha256:63ef2a575d008b8e260dc5b64e7691b99d74a9dcdbea4263fb7975aeb50ce74b"},
|
||||||
{file = "labthings-0.6.2.tar.gz", hash = "sha256:6373e2d0fdd731752a6fabd09708adeaf233b67f0cdcabee669642f647e9d95d"},
|
{file = "labthings-0.6.3.tar.gz", hash = "sha256:f5756ad4622210923511b690a8deb378bddc7d1e300ebd9e42cca8bc562ef032"},
|
||||||
]
|
]
|
||||||
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"},
|
||||||
|
|
@ -1084,8 +1071,8 @@ opencv-python-headless = [
|
||||||
{file = "opencv_python_headless-4.2.0.34-cp38-cp38-win_amd64.whl", hash = "sha256:20c450943acebd9aa04566fdef4a658b94eec80a0c39a8f480c9aa1696034fcb"},
|
{file = "opencv_python_headless-4.2.0.34-cp38-cp38-win_amd64.whl", hash = "sha256:20c450943acebd9aa04566fdef4a658b94eec80a0c39a8f480c9aa1696034fcb"},
|
||||||
]
|
]
|
||||||
packaging = [
|
packaging = [
|
||||||
{file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
|
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
|
||||||
{file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
|
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
|
||||||
]
|
]
|
||||||
picamera = []
|
picamera = []
|
||||||
pillow = [
|
pillow = [
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ opencv-python-headless = [
|
||||||
{version = "4.1.0.25", python = "~3.7"}, # PiWheels build for RPi running Py37
|
{version = "4.1.0.25", python = "~3.7"}, # PiWheels build for RPi running Py37
|
||||||
{version = "4.2.0.34", python = "^3.8"} # Latest for Py38 systems
|
{version = "4.2.0.34", python = "^3.8"} # Latest for Py38 systems
|
||||||
]
|
]
|
||||||
labthings = "0.6.2"
|
labthings = "0.6.3"
|
||||||
pynpm = "^0.1.2"
|
pynpm = "^0.1.2"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue