diff --git a/.github/workflows/wfssrv-tests.yml b/.github/workflows/wfssrv-tests.yml index 669bac3..3eb420a 100644 --- a/.github/workflows/wfssrv-tests.yml +++ b/.github/workflows/wfssrv-tests.yml @@ -10,12 +10,11 @@ jobs: matrix_tests: runs-on: ${{ matrix.os }} - if: "!contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: os: [ubuntu-latest] - python-ver: [7, 8] - tox-env: [cov, astropylts, astropydev, numpydev] + python-ver: [13] + tox-env: [cov, astropydev, numpydev] steps: - uses: actions/checkout@v1 - name: Set up python 3.${{ matrix.python-ver }} with tox environment ${{ matrix.tox-env }} on ${{ matrix.os }} @@ -39,13 +38,12 @@ jobs: doc_test: runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: - uses: actions/checkout@v1 - name: Set up Python to build docs with sphinx uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.13 - name: Install base dependencies run: | python -m pip install --upgrade pip @@ -57,13 +55,12 @@ jobs: codestyle: runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: - uses: actions/checkout@v1 - name: Python codestyle check uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.13 - name: Install base dependencies run: | python -m pip install --upgrade pip diff --git a/Dockerfile b/Dockerfile index 40bc902..009b003 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,16 @@ -FROM mmtobservatory/mmtwfs:latest +FROM python:3.13 -MAINTAINER T. E. Pickering "te.pickering@gmail.com" +LABEL maintainer="te.pickering@gmail.com" COPY . . -RUN python -m pip install --upgrade pip -RUN python -m pip install git+https://github.com/MMTObservatory/camsrv.git#egg=camsrv -RUN python -m pip install git+https://github.com/MMTObservatory/cwfs.git#egg=cwfs -RUN python -m pip install -e .[all] +RUN apt-get update +RUN apt-get install -y git + +RUN python -m pip install --upgrade pip setuptools setuptools_scm +RUN python -m pip install git+https://github.com/MMTObservatory/camsrv#egg=camsrv +RUN python -m pip install git+https://github.com/MMTObservatory/mmtwfs#egg=mmtwfs +RUN python -m pip install . EXPOSE 8080 diff --git a/LICENSE.rst b/LICENSE.rst deleted file mode 100644 index 91bf16b..0000000 --- a/LICENSE.rst +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2018, MMT Observatory -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. -* Neither the name of the MMT Observatory nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wfssrv/cwfssrv.py b/OLD/cwfssrv.py similarity index 100% rename from wfssrv/cwfssrv.py rename to OLD/cwfssrv.py diff --git a/.github/workflows/docker.yml b/OLD/docker.yml similarity index 100% rename from .github/workflows/docker.yml rename to OLD/docker.yml diff --git a/README.md b/README.md index 7f5c9f5..d9eb268 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,4 @@ MMT Wavefront Sensor Analysis Interface ![Python Tests](https://github.com/MMTObservatory/WFSsrv/workflows/Python%20Tests/badge.svg) -![Publish to Docker](https://github.com/MMTObservatory/WFSsrv/workflows/Publish%20to%20Docker/badge.svg) - [![codecov](https://codecov.io/gh/MMTObservatory/WFSsrv/branch/master/graph/badge.svg)](https://codecov.io/gh/MMTObservatory/WFSsrv) diff --git a/docs/conf.py b/docs/conf.py index 6568bf6..9ae0109 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,13 +36,6 @@ print('ERROR: the documentation requires the sphinx-astropy package to be installed') sys.exit(1) -# Get configuration information from setup.cfg -from configparser import ConfigParser -conf = ConfigParser() - -conf.read([os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')]) -setup_cfg = dict(conf.items('metadata')) - # -- General configuration ---------------------------------------------------- # By default, highlight as Python 3. @@ -67,17 +60,17 @@ # -- Project information ------------------------------------------------------ # This does not *have* to match the package name, but typically does -project = setup_cfg['name'] -author = setup_cfg['author'] +project = "wfssrv" +author = "T. E. Pickering" copyright = '{0}, {1}'.format( - datetime.datetime.now().year, setup_cfg['author']) + datetime.datetime.now().year, "T. E. Pickering") # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -import_module(setup_cfg['name']) -package = sys.modules[setup_cfg['name']] +import_module("wfssrv") +package = sys.modules["wfssrv"] # The short X.Y version. version = package.__version__.split('-', 1)[0] @@ -106,7 +99,7 @@ html_theme_options = { - 'logotext1': 'mmtwfs', # white, semi-bold + 'logotext1': 'wfssrv', # white, semi-bold 'logotext2': '', # orange, light 'logotext3': ':docs' # white, light } @@ -151,21 +144,8 @@ man_pages = [('index', project.lower(), project + u' Documentation', [author], 1)] - -# -- Options for the edit_on_github extension --------------------------------- - -if setup_cfg.get('edit_on_github').lower() == 'true': - - extensions += ['sphinx_astropy.ext.edit_on_github'] - - edit_on_github_project = setup_cfg['github_project'] - edit_on_github_branch = "master" - - edit_on_github_source_root = "" - edit_on_github_doc_root = "docs" - # -- Resolving issue number to links in changelog ----------------------------- -github_issues_url = 'https://github.com/{0}/issues/'.format(setup_cfg['github_project']) +github_issues_url = 'https://github.com/{0}/issues/'.format("wfssrv") # -- Turn on nitpicky mode for sphinx (to warn about references not found) ---- # diff --git a/pyproject.toml b/pyproject.toml index 7e7daea..38fd7ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,111 @@ -[build-system] +[project] +name = "wfssrv" +dynamic = ["version"] +authors = [ + { name = "T. E. Pickering", email = "te.pickering@gmail.com"} +] +license = {file = "licenses/LICENSE.rst"} +readme = "README.rst" +description = "MMTO Wavefront Sensor Analysis Server" +requires-python = ">=3.12" +dependencies = [ + "tornado", + "redis", + "matplotlib", + "camsrv@git+https://github.com/MMTObservatory/camsrv", + "mmtwfs@git+https://github.com/MMTObservatory/mmtwfs", +] + +[project.optional-dependencies] +test = [ + "tox", + "coverage", + "pytest-astropy", + "black", + "flake8", + "codecov", +] +docs = [ + "sphinx-astropy", +] + +[project.scripts] +"wfs_header.py" = "wfssrv.scripts.wfs_header:main" +wfssrv = "wfssrv.wfssrv:main" + +[project.urls] +Repository = "https://github.com/mmtobservatory/wfssrv.git" +Documentation = "https://wfssrv.readthedocs.io/" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.package-data] +"wfssrv.templates" = ["**"] +"wfssrv.static" = ["**"] + +[tool.setuptools.packages] +find = {} -requires = ["setuptools", - "setuptools_scm", - "wheel"] +[tool.setuptools_scm] +version_file = "wfssrv/version.py" + +[build-system] +requires = [ + "setuptools", + "setuptools_scm", +] build-backend = 'setuptools.build_meta' + +[tool.pytest.ini_options] +minversion = 7.0 +testpaths = [ + "wfssrv/tests", +] +astropy_header = true +doctest_plus = "enabled" +text_file_format = "rst" +addopts = [ + "--color=yes", + "--doctest-rst", +] +xfail_strict = true +filterwarnings = [ + "error", + "ignore:numpy\\.ufunc size changed:RuntimeWarning", + "ignore:numpy\\.ndarray size changed:RuntimeWarning", + # weird no event loop deprecation warning + "ignore:.*There is no current event loop:DeprecationWarning", +] + +[tool.coverage] + + [tool.coverage.run] + omit = [ + "wfssrv/_astropy_init*", + "wfssrv/conftest.py", + "wfssrv/tests/*", + "wfssrv/version*", + "*/wfssrv/_astropy_init*", + "*/wfssrv/conftest.py", + "*/wfssrv/tests/*", + "*/wfssrv/version*", + ] + + [tool.coverage.report] + exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't complain about packages we have installed + "except ImportError", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # Don't complain about script hooks + "'def main(.*):'", + # Ignore branches that don't pertain to this version of Python + "pragma: py{ignore_python_version}", + # Don't complain about IPython completion helper + "def _ipython_key_completions_", + ] diff --git a/scripts/sao-wfs-server b/scripts/sao-wfs-server index 421fdd2..fd7c2f4 100755 --- a/scripts/sao-wfs-server +++ b/scripts/sao-wfs-server @@ -13,7 +13,7 @@ if {[info exists env(WFSROOT)]} { if {[info exists env(CONDA_PREFIX)]} { set header_script [file join $env(CONDA_PREFIX) bin wfs_header.py] } else { - set header_script /mmt/conda/bin/wfs_header.py + set header_script /mmt/condaforge/envs/mmtwfs/bin/wfs_header.py } # nominally the port should be 9876, but query for it via DNS to be sure and in case it gets moved diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 96b6985..0000000 --- a/setup.cfg +++ /dev/null @@ -1,100 +0,0 @@ -[metadata] -name = wfssrv -description = MMTO Wavefront Sensor Analysis Server -long_description = file: README.md -author = T. E. Pickering (MMT Observatory) -author_email = tim@mmto.org -license = BSD-3 -license_file = LICENSE.rst -url = https://github.com/MMTObservatory/WFSsrv -edit_on_github = True -github_project = MMTObservatory/WFSsrv - -[options] -zip_safe = False -packages = find: -python_requires = >=3.7 -setup_requires = setuptools_scm -install_requires = - tornado - astropy - mmtwfs - camsrv - -include_package_data = True - -[options.extras_require] -docs = - sphinx-astropy -test = - tox - pytest - pytest-cov - pytest-astropy - nose - coverage - codecov -all = - matplotlib - scipy - astropy - photutils - scikit-image - dnspython - poppy - lmfit - ccdproc - astroscrappy - redis - -[options.entry_points] -console_scripts = - wfs_header.py = wfssrv.scripts.wfs_header:main - wfssrv = wfssrv.wfssrv:main - -[options.package_data] -wfssrv.templates = *.html -wfssrv.static = */* -wfssrv.tests = coveragerc - -[tool:pytest] -testpaths = "wfssrv" "docs" -astropy_header = true -doctest_plus = enabled -text_file_format = rst -addopts = --doctest-rst - -[coverage:run] -parallel = True -branch = True -omit = - wfssrv/_astropy_init* - wfssrv/conftest.py - wfssrv/*setup_package* - wfssrv/tests/* - wfssrv/*/tests/* - wfssrv/extern/* - wfssrv/version* - */wfssrv/_astropy_init* - */wfssrv/conftest.py - */wfssrv/*setup_package* - */wfssrv/tests/* - */wfssrv/*/tests/* - */wfssrv/extern/* - */wfssrv/version* - -[coverage:report] -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - # Don't complain about packages we have installed - except ImportError - # Don't complain if tests don't hit assertions - raise AssertionError - raise NotImplementedError - # Don't complain about script hooks - def main\(.*\): - # Ignore branches that don't pertain to this version of Python - pragma: py{ignore_python_version} - # Don't complain about IPython completion helper - def _ipython_key_completions_ diff --git a/setup.py b/setup.py deleted file mode 100755 index 8fb0105..0000000 --- a/setup.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python -# Licensed under a 3-clause BSD style license - see LICENSE.rst - -# NOTE: The configuration for the package, including the name, version, and -# other information are set in the setup.cfg file. - -import os -import sys - -from setuptools import setup -from setuptools.config import read_configuration - - -# First provide helpful messages if contributors try and run legacy commands -# for tests or docs. - -TEST_HELP = """ -Note: running tests is no longer done using 'python setup.py test'. Instead -you will need to run: - - tox -e test - -If you don't already have tox installed, you can install it with: - - pip install tox - -If you only want to run part of the test suite, you can also use pytest -directly with:: - - pip install -e .[test] - pytest - -For more information, see: - - http://docs.astropy.org/en/latest/development/testguide.html#running-tests -""" - -if 'test' in sys.argv: - print(TEST_HELP) - sys.exit(1) - -DOCS_HELP = """ -Note: building the documentation is no longer done using -'python setup.py build_docs'. Instead you will need to run: - - tox -e build_docs - -If you don't already have tox installed, you can install it with: - - pip install tox - -You can also build the documentation with Sphinx directly using:: - - pip install -e .[docs] - cd docs - make html - -For more information, see: - - http://docs.astropy.org/en/latest/install.html#builddocs -""" - -if 'build_docs' in sys.argv or 'build_sphinx' in sys.argv: - print(DOCS_HELP) - sys.exit(1) - -VERSION_TEMPLATE = """ -# Note that we need to fall back to the hard-coded version if either -# setuptools_scm can't be imported or setuptools_scm can't determine the -# version, so we catch the generic 'Exception'. -try: - from setuptools_scm import get_version - version = get_version(root='..', relative_to=__file__) -except Exception: - version = '{version}' -""".lstrip() - - -setup(use_scm_version={'write_to': os.path.join('wfssrv', 'version.py'), - 'write_to_template': VERSION_TEMPLATE}) diff --git a/tox.ini b/tox.ini index 945f4f6..7839a6b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,7 @@ [tox] envlist = - py{37,38}{,-test}{,-cov} - py{37,38}-astropylts - py{37,38}-{numpy,astropy}dev + py{312,313}{,-test}{,-cov} + py{312,313}-{numpy,astropy}dev build_docs linkcheck codestyle @@ -16,7 +15,7 @@ isolated_build = true usedevelop = True # Pass through the following environemnt variables which may be needed for the CI -passenv = HOME WINDIR LC_ALL LC_CTYPE CC CI +passenv = HOME,WINDIR,LC_ALL,LC_CTYPE,CC,CI # Run the tests in a temporary directory to make sure that we don't import # astropy from the source tree @@ -33,16 +32,10 @@ changedir = {toxworkdir}/tox_testing description = test: run tests cov: with coverage enabled - astropylts: with astropy LTS {numpy,astropy}dev: with latest master from github repo cov_report: generate HTML coverage report deps = - git+https://github.com/MMTObservatory/cwfs.git#egg=cwfs - git+https://github.com/MMTObservatory/mmtwfs.git#egg=mmtwfs - git+https://github.com/MMTObservatory/camsrv.git#egg=camsrv - git+https://github.com/MMTObservatory/indiclient.git#egg=indiclient - astropylts: astropy==4.0.* numpydev: git+https://github.com/numpy/numpy.git#egg=numpy astropydev: git+https://github.com/astropy/astropy.git#egg=astropy @@ -53,19 +46,17 @@ depends = # The following indicates which extras_require from setup.cfg will be installed extras = test - all commands = pip freeze !cov: pytest --pyargs wfssrv {toxinidir}/docs {posargs} - cov: pytest --pyargs wfssrv {toxinidir}/docs --cov wfssrv --cov-config={toxinidir}/setup.cfg {posargs} + cov: pytest --pyargs wfssrv {toxinidir}/docs --cov wfssrv --cov-config={toxinidir}/pyproject.toml {posargs} cov: coverage xml -o {toxinidir}/coverage.xml [testenv:build_docs] changedir = docs description = invoke sphinx-build to build the HTML docs extras = - all docs commands = pip freeze @@ -75,7 +66,6 @@ commands = changedir = docs description = check the links in the HTML docs extras = - all docs commands = pip freeze diff --git a/wfssrv/__init__.py b/wfssrv/__init__.py index 43f77e9..000a5a2 100644 --- a/wfssrv/__init__.py +++ b/wfssrv/__init__.py @@ -1,25 +1,8 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst -# Affiliated packages may add whatever they like to this file, but -# should keep this content at the top. -# ---------------------------------------------------------------------------- -from ._astropy_init import * # noqa -# ---------------------------------------------------------------------------- - -# Enforce Python version check during package import. -# This is the same check as the one at the top of setup.py -import sys -from distutils.version import LooseVersion - -__minimum_python_version__ = "3.7" - __all__ = [] - -class UnsupportedPythonError(Exception): - pass - - -if LooseVersion(sys.version) < LooseVersion(__minimum_python_version__): - raise UnsupportedPythonError("wfssrv does not support Python < {}" - .format(__minimum_python_version__)) +try: + from .version import version as __version__ +except ImportError: + __version__ = "" diff --git a/wfssrv/_astropy_init.py b/wfssrv/_astropy_init.py deleted file mode 100644 index 2dffe8f..0000000 --- a/wfssrv/_astropy_init.py +++ /dev/null @@ -1,52 +0,0 @@ -# Licensed under a 3-clause BSD style license - see LICENSE.rst - -__all__ = ['__version__'] - -# this indicates whether or not we are in the package's setup.py -try: - _ASTROPY_SETUP_ -except NameError: - import builtins - builtins._ASTROPY_SETUP_ = False - -try: - from .version import version as __version__ -except ImportError: - __version__ = '' - - -if not _ASTROPY_SETUP_: # noqa - import os - from warnings import warn - from astropy.config.configuration import ( - update_default_config, - ConfigurationDefaultMissingError, - ConfigurationDefaultMissingWarning) - - # Create the test function for self test - from astropy.tests.runner import TestRunner - test = TestRunner.make_test_runner_in(os.path.dirname(__file__)) - test.__test__ = False - __all__ += ['test'] - - # add these here so we only need to cleanup the namespace at the end - config_dir = None - - if not os.environ.get('ASTROPY_SKIP_CONFIG_UPDATE', False): - config_dir = os.path.dirname(__file__) - config_template = os.path.join(config_dir, __package__ + ".cfg") - if os.path.isfile(config_template): - try: - update_default_config( - __package__, config_dir, version=__version__) - except TypeError as orig_error: - try: - update_default_config(__package__, config_dir) - except ConfigurationDefaultMissingError as e: - wmsg = (e.args[0] + - " Cannot install default profile. If you are " - "importing from source, this is expected.") - warn(ConfigurationDefaultMissingWarning(wmsg)) - del e - except Exception: - raise orig_error diff --git a/wfssrv/conftest.py b/wfssrv/conftest.py index 672b273..b164677 100644 --- a/wfssrv/conftest.py +++ b/wfssrv/conftest.py @@ -3,21 +3,12 @@ # get picked up when running the tests inside an interpreter using # packagename.test -import os +try: + from pytest_astropy_header.display import TESTED_VERSIONS -from astropy.version import version as astropy_version - -# For Astropy 3.0 and later, we can use the standalone pytest plugin -if astropy_version < '3.0': - from astropy.tests.pytest_plugins import * # noqa - del pytest_report_header ASTROPY_HEADER = True -else: - try: - from pytest_astropy_header.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS - ASTROPY_HEADER = True - except ImportError: - ASTROPY_HEADER = False +except ImportError: + ASTROPY_HEADER = False def pytest_configure(config): @@ -26,24 +17,6 @@ def pytest_configure(config): config.option.astropy_header = True - # Customize the following lines to add/remove entries from the list of - # packages for which version numbers are displayed when running the tests. - PYTEST_HEADER_MODULES.pop('Pandas', None) - PYTEST_HEADER_MODULES['scikit-image'] = 'skimage' - - from . import __version__ - packagename = os.path.basename(os.path.dirname(__file__)) - TESTED_VERSIONS[packagename] = __version__ - -# Uncomment the last two lines in this block to treat all DeprecationWarnings as -# exceptions. For Astropy v2.0 or later, there are 2 additional keywords, -# as follow (although default should work for most cases). -# To ignore some packages that produce deprecation warnings on import -# (in addition to 'compiler', 'scipy', 'pygments', 'ipykernel', and -# 'setuptools'), add: -# modules_to_ignore_on_import=['module_1', 'module_2'] -# To ignore some specific deprecation warning messages for Python version -# MAJOR.MINOR or later, add: -# warnings_to_ignore_by_pyver={(MAJOR, MINOR): ['Message to ignore']} -# from astropy.tests.helper import enable_deprecations_as_exceptions # noqa -# enable_deprecations_as_exceptions() + from wfssrv import __version__ + + TESTED_VERSIONS["camsrv"] = __version__ diff --git a/wfssrv/templates/wfs.html b/wfssrv/templates/wfs.html index be688ef..9048feb 100644 --- a/wfssrv/templates/wfs.html +++ b/wfssrv/templates/wfs.html @@ -4,8 +4,10 @@ - - + + + + @@ -41,7 +43,7 @@ } - + @@ -84,6 +86,7 @@ // The HTML element in which to place the figure $('div#{{ figure }}') ); + {% end %} getZerns(); diff --git a/wfssrv/tests/test_app.py b/wfssrv/tests/test_app.py index 03a3c38..1ffc9c4 100644 --- a/wfssrv/tests/test_app.py +++ b/wfssrv/tests/test_app.py @@ -6,4 +6,4 @@ def test_wfs(): app = wfs() - assert(app is not None) + assert app is not None diff --git a/wfssrv/wfssrv.py b/wfssrv/wfssrv.py index dbd1e29..bb09688 100644 --- a/wfssrv/wfssrv.py +++ b/wfssrv/wfssrv.py @@ -9,6 +9,10 @@ import os import json import pathlib +import signal +import socket +from pathlib import Path + import redis import numpy as np @@ -35,16 +39,20 @@ import matplotlib import matplotlib.pyplot as plt -from matplotlib.backends.backend_webagg_core import (FigureCanvasWebAggCore, new_figure_manager_given_figure) +from matplotlib.backends.backend_webagg import ( + FigureCanvasWebAgg, + FigureManagerWebAgg, + new_figure_manager_given_figure, +) from mmtwfs.wfs import WFSFactory from mmtwfs.zernike import ZernikeVector from mmtwfs.telescope import MMT -matplotlib.use('webagg') -glog = logging.getLogger('') -log = logging.getLogger('WFSsrv') +matplotlib.use("webagg") +glog = logging.getLogger("") +log = logging.getLogger("WFSsrv") log.setLevel(logging.DEBUG) @@ -53,43 +61,59 @@ def create_default_figures(): figures = {} ax = {} data = np.zeros((512, 512)) - tel = MMT(secondary='f5') # secondary doesn't matter, just need methods for mirror forces/plots + tel = MMT( + secondary="f5" + ) # secondary doesn't matter, just need methods for mirror forces/plots forces = tel.bending_forces(zv=zv) # stub for plot showing bkg-subtracted WFS image with aperture positions - figures['slopes'], ax['slopes'] = plt.subplots() - figures['slopes'].set_label("Aperture Positions and Spot Movement") - ax['slopes'].imshow(data, cmap='Greys', origin='lower', interpolation='None') + figures["slopes"], ax["slopes"] = plt.subplots() + figures["slopes"].set_label("Aperture Positions and Spot Movement") + ax["slopes"].imshow(data, cmap="Greys", origin="lower", interpolation="None") # stub for plot showing bkg-subtracted WFS image and residuals slopes of wavefront fit - figures['residuals'], ax['residuals'] = plt.subplots() - figures['residuals'].set_label("Zernike Fit Residuals") - ax['residuals'].imshow(data, cmap='Greys', origin='lower', interpolation='None') + figures["residuals"], ax["residuals"] = plt.subplots() + figures["residuals"].set_label("Zernike Fit Residuals") + ax["residuals"].imshow(data, cmap="Greys", origin="lower", interpolation="None") # stub for zernike bar chart - figures['barchart'] = zv.bar_chart() + figures["barchart"] = zv.bar_chart() # stub for zernike fringe bar chart - figures['fringebarchart'] = zv.fringe_bar_chart() + figures["fringebarchart"] = zv.fringe_bar_chart() # stubs for mirror forces - figures['forces'] = tel.plot_forces(forces) - figures['forces'].set_label("Requested M1 Actuator Forces") + figures["forces"] = tel.plot_forces(forces) + figures["forces"].set_label("Requested M1 Actuator Forces") # stubs for mirror forces - figures['totalforces'] = tel.plot_forces(forces) - figures['totalforces'].set_label("Total M1 Actuator Forces") + figures["totalforces"] = tel.plot_forces(forces) + figures["totalforces"].set_label("Total M1 Actuator Forces") return figures class WFSsrv(tornado.web.Application): + + class MplJs(tornado.web.RequestHandler): + def get(self): + self.set_header("Content-Type", "application/javascript") + + js_content = FigureManagerWebAgg.get_javascript() + + self.write(js_content) + class HomeHandler(tornado.web.RequestHandler): """ Serves the main HTML page. """ + def get(self): - self.render("home.html", current=self.application.wfs, wfslist=self.application.wfs_names) + self.render( + "home.html", + current=self.application.wfs, + wfslist=self.application.wfs_names, + ) class SelectHandler(tornado.web.RequestHandler): def get(self): @@ -118,7 +142,9 @@ def get(self): manager = self.application.managers[k] fig_ids.append(manager.num) figkeys.append(k) - ws_uri = "ws://{req.host}/{figdiv}/ws".format(req=self.request, figdiv=k) + ws_uri = "ws://{req.host}/{figdiv}/ws".format( + req=self.request, figdiv=k + ) ws_uris.append(ws_uri) self.render( @@ -132,7 +158,7 @@ def get(self): default_mode=self.application.wfs.default_mode, m1_gain=self.application.wfs.m1_gain, m2_gain=self.application.wfs.m2_gain, - log_uri=log_uri + log_uri=log_uri, ) except Exception as e: log.warning(f"Must specify valid wfs: {wfs}. ({e})") @@ -146,7 +172,9 @@ def get(self): if self.application.wfs.connected: log.info(f"{self.application.wfs.name} is connected.") else: - log.warning(f"Couldn't connect to {self.application.wfs.name}. Offline?") + log.warning( + f"Couldn't connect to {self.application.wfs.name}. Offline?" + ) else: log.info(f"{self.application.wfs.name} already connected") self.finish() @@ -173,42 +201,44 @@ def async_plot(self, func, *args): def make_barchart(self, zernikes, zrms, residual): log.debug("Making bar chart...") waves = zrms.value / 550.0 - self.application.figures['barchart'] = zernikes.bar_chart( + self.application.figures["barchart"] = zernikes.bar_chart( residual=residual, - title=f"Total Wavefront RMS: {zrms.round(1)} ({np.round(waves, 2)} waves)" + title=f"Total Wavefront RMS: {zrms.round(1)} ({np.round(waves, 2)} waves)", ) - return 'barchart' + return "barchart" @run_on_executor def make_fringebarchart(self, zernikes, focus, cc_x, cc_y): log.debug("Making fringe bar chart...") - self.application.figures['fringebarchart'] = zernikes.fringe_bar_chart( + self.application.figures["fringebarchart"] = zernikes.fringe_bar_chart( title="Focus: {0:0.1f} CC_X: {1:0.1f} CC_Y: {2:0.1f}".format( focus, cc_x, cc_y, ), - max_c=1500*u.nm, + max_c=1500 * u.nm, ) - return 'fringebarchart' + return "fringebarchart" @run_on_executor def make_totalforces(self, telescope, forces, m1focus): log.debug("Making total forces plot...") - self.application.figures['totalforces'] = telescope.plot_forces(forces, m1focus) - self.application.figures['totalforces'].set_label("Unmasked M1 Actuator Forces") - return 'totalforces' + self.application.figures["totalforces"] = telescope.plot_forces( + forces, m1focus + ) + self.application.figures["totalforces"].set_label( + "Unmasked M1 Actuator Forces" + ) + return "totalforces" @run_on_executor def make_pendingforces(self, telescope, forces, m1focus, limit): log.debug("Making pending forces plot...") - self.application.figures['forces'] = telescope.plot_forces( - forces, - m1focus, - limit=limit + self.application.figures["forces"] = telescope.plot_forces( + forces, m1focus, limit=limit ) - self.application.figures['forces'].set_label("Requested M1 Actuator Forces") - return 'forces' + self.application.figures["forces"].set_label("Requested M1 Actuator Forces") + return "forces" def complete_refresh(self, key): self.application.refresh_figure(key, self.application.figures[key]) @@ -226,11 +256,13 @@ def get(self): spher = self.get_argument("spher", default=False) if spher == "true": - spher_mask = ['Z11', 'Z22'] + spher_mask = ["Z11", "Z22"] log.info(f"Ignoring all spherical terms {str(spher_mask)}...") else: - spher_mask = ['Z22'] - log.info(f"Only ignoring the high-order spherical terms {str(spher_mask)}...") + spher_mask = ["Z22"] + log.info( + f"Only ignoring the high-order spherical terms {str(spher_mask)}..." + ) if os.path.isfile(filename) and not self.application.busy: self.application.busy = True @@ -240,102 +272,140 @@ def get(self): self.application.wfs.disconnect() log.debug("Measuring slopes...") - results = self.application.wfs.measure_slopes(filename, mode=mode, plot=True) - if results['slopes'] is not None: - if 'seeing' in results: - log.info(f"Seeing (zenith): {results['seeing'].round(2)}") - log.info(f"Seeing (raw): {results['raw_seeing'].round(2)}") + results = self.application.wfs.measure_slopes( + filename, mode=mode, plot=True + ) + if results["slopes"] is not None: + if "vlt_seeing" in results: + log.info(f"Seeing (zenith): {results['vlt_seeing'].round(2)}") + log.info(f"Seeing (raw): {results['raw_vlt_seeing'].round(2)}") if self.application.wfs.connected: log.info("Publishing seeing values to redis.") self.application.update_seeing(results) tel = self.application.wfs.telescope log.debug("Making slopes plot...") - self.application.figures['slopes'] = results['figures']['slopes'] - self.application.refresh_figure('slopes', self.application.figures['slopes']) + self.application.figures["slopes"] = results["figures"]["slopes"] + self.application.refresh_figure( + "slopes", self.application.figures["slopes"] + ) log.debug("Fitting wavefront...") zresults = self.application.wfs.fit_wavefront(results, plot=True) - if zresults['fit_report'].success: + if zresults["fit_report"].success: log.info(f"Residual RMS: {zresults['residual_rms'].round(2)}") - self.application.figures['residuals'] = zresults['resid_plot'] - self.application.refresh_figure('residuals', self.application.figures['residuals']) + self.application.figures["residuals"] = zresults["resid_plot"] + self.application.refresh_figure( + "residuals", self.application.figures["residuals"] + ) - zvec = zresults['zernike'] - zvec_raw = zresults['rot_zernike'] - zvec_ref = zresults['ref_zernike'] + zvec = zresults["zernike"] + zvec_raw = zresults["rot_zernike"] + zvec_ref = zresults["ref_zernike"] self.application.wavefront_fit = zvec.copy() m1gain = self.application.wfs.m1_gain # this is the total if we try to correct everything as fit - totforces, totm1focus, zv_totmasked = tel.calculate_primary_corrections(zvec.copy(), gain=m1gain) + totforces, totm1focus, zv_totmasked = ( + tel.calculate_primary_corrections(zvec.copy(), gain=m1gain) + ) - self.async_plot(self.make_barchart, zvec.copy(), zresults['zernike_rms'], zresults['residual_rms']) - self.async_plot(self.make_totalforces, tel, totforces, totm1focus) + self.async_plot( + self.make_barchart, + zvec.copy(), + zresults["zernike_rms"], + zresults["residual_rms"], + ) + self.async_plot( + self.make_totalforces, tel, totforces, totm1focus + ) log.debug("Saving files and calculating corrections...") zvec_file = self.application.datadir / (filename + ".zernike") - zvec_raw_file = self.application.datadir / (filename + ".raw.zernike") - zvec_ref_file = self.application.datadir / (filename + ".ref.zernike") + zvec_raw_file = self.application.datadir / ( + filename + ".raw.zernike" + ) + zvec_ref_file = self.application.datadir / ( + filename + ".ref.zernike" + ) zvec.save(filename=zvec_file) zvec_raw.save(filename=zvec_raw_file) zvec_ref.save(filename=zvec_ref_file) # check the RMS of the wavefront fit and only apply corrections if the fit is good enough. # M2 can be more lenient to take care of large amounts of focus or coma. - if zresults['residual_rms'] < 4000 * u.nm: + if zresults["residual_rms"] < 4000 * u.nm: self.application.has_pending_m1 = True self.application.has_pending_coma = True self.application.has_pending_focus = True log.info(f"{filename}: all proposed corrections valid.") - elif zresults['residual_rms'] <= 7000 * u.nm: + elif zresults["residual_rms"] <= 7000 * u.nm: self.application.has_pending_focus = True log.warning(f"{filename}: only focus corrections valid.") - elif zresults['residual_rms'] > 7000 * u.nm: - log.error(f"{filename}: wavefront fit too poor; no valid corrections") + elif zresults["residual_rms"] > 7000 * u.nm: + log.error( + f"{filename}: wavefront fit too poor; no valid corrections" + ) self.application.has_pending_recenter = True - self.application.pending_focus = self.application.wfs.calculate_focus(zvec.copy()) + self.application.pending_focus = ( + self.application.wfs.calculate_focus(zvec.copy()) + ) # only allow M1 corrections if we are reasonably close to good focus... if self.application.pending_focus > 150 * u.um: self.application.has_pending_m1 = False - self.application.pending_cc_x, self.application.pending_cc_y = self.application.wfs.calculate_cc(zvec.copy()) + self.application.pending_cc_x, self.application.pending_cc_y = ( + self.application.wfs.calculate_cc(zvec.copy()) + ) self.async_plot( self.make_fringebarchart, zvec.copy(), self.application.pending_focus, self.application.pending_cc_x, - self.application.pending_cc_y + self.application.pending_cc_y, ) log.debug("Calculating pending forces...") - self.application.pending_az, self.application.pending_el = self.application.wfs.calculate_recenter(results) - self.application.pending_forces, self.application.pending_m1focus, zv_masked = \ - self.application.wfs.calculate_primary(zvec.copy(), mask=spher_mask) - self.application.pending_forcefile = self.application.datadir / (filename + ".forces") - zvec_masked_file = self.application.datadir / (filename + ".masked.zernike") + self.application.pending_az, self.application.pending_el = ( + self.application.wfs.calculate_recenter(results) + ) + ( + self.application.pending_forces, + self.application.pending_m1focus, + zv_masked, + ) = self.application.wfs.calculate_primary( + zvec.copy(), mask=spher_mask + ) + self.application.pending_forcefile = ( + self.application.datadir / (filename + ".forces") + ) + zvec_masked_file = self.application.datadir / ( + filename + ".masked.zernike" + ) zv_masked.save(filename=zvec_masked_file) - limit = np.round(np.abs(self.application.pending_forces['force']).max()) + limit = np.round( + np.abs(self.application.pending_forces["force"]).max() + ) self.async_plot( self.make_pendingforces, tel, self.application.pending_forces, self.application.pending_m1focus, - limit + limit, ) else: log.error(f"Wavefront fit failed: {filename}") figures = create_default_figures() - figures['slopes'] = results['figures']['slopes'] + figures["slopes"] = results["figures"]["slopes"] self.application.refresh_figures(figures=figures) else: log.error(f"Wavefront measurement failed: {filename}") figures = create_default_figures() - figures['slopes'] = results['figures']['slopes'] + figures["slopes"] = results["figures"]["slopes"] self.application.refresh_figures(figures=figures) else: @@ -352,15 +422,23 @@ def get(self): self.application.wfs.telescope.correct_primary( self.application.pending_forces, self.application.pending_m1focus, - filename=self.application.pending_forcefile + filename=self.application.pending_forcefile, ) - max_f = self.application.pending_forces['force'].max() - min_f = self.application.pending_forces['force'].min() + max_f = self.application.pending_forces["force"].max() + min_f = self.application.pending_forces["force"].min() log.info(f"Maximum force {round(max_f, 2)} N") log.info(f"Minimum force {round(min_f, 2)} N") - log.info("Adjusting M1 focus by {0:0.1f}".format(self.application.pending_m1focus)) + log.info( + "Adjusting M1 focus by {0:0.1f}".format( + self.application.pending_m1focus + ) + ) self.application.has_pending_m1 = False - self.write("Sending forces to cell and {0:0.1f} focus to secondary...".format(self.application.pending_m1focus)) + self.write( + "Sending forces to cell and {0:0.1f} focus to secondary...".format( + self.application.pending_m1focus + ) + ) else: log.info("No M1 corrections sent") self.write("No M1 corrections sent") @@ -386,11 +464,12 @@ class ComaCorrectHandler(tornado.web.RequestHandler): def get(self): log.info("M2 coma corrections...") if self.application.has_pending_coma and self.application.wfs.connected: - self.application.wfs.secondary.correct_coma(self.application.pending_cc_x, self.application.pending_cc_y) + self.application.wfs.secondary.correct_coma( + self.application.pending_cc_x, self.application.pending_cc_y + ) self.application.has_pending_coma = False log_str = "Sending {0:0.1f}/{1:0.1f} CC_X/CC_Y to secondary...".format( - self.application.pending_cc_x, - self.application.pending_cc_y + self.application.pending_cc_x, self.application.pending_cc_y ) log.info(log_str) self.write(log_str) @@ -410,9 +489,8 @@ def get(self): # self.application.pending_el # ) log_str = "Hexapod recentering disabled. " - log_str += "Please apply mount offsets of EL={0:0.1f}\" and AZ={1:0.1f}\"...".format( - self.application.pending_el, - self.application.pending_az + log_str += 'Please apply mount offsets of EL={0:0.1f}" and AZ={1:0.1f}"...'.format( + self.application.pending_el, self.application.pending_az ) log.warning(log_str) self.write(log_str) @@ -424,7 +502,7 @@ def get(self): class RestartHandler(tornado.web.RequestHandler): def get(self): try: - wfs = self.get_argument('wfs') + wfs = self.get_argument("wfs") self.application.restart_wfs(wfs) log.info(f"restarting {wfs}") except Exception as e: @@ -454,7 +532,7 @@ def get(self): def post(self): self.set_header("Content-Type", "text/plain") try: - gain = float(self.get_body_argument('gain')) + gain = float(self.get_body_argument("gain")) if self.application.wfs is not None: if gain >= 0.0 and gain <= 1.0: self.application.wfs.m1_gain = gain @@ -476,7 +554,7 @@ def get(self): def post(self): self.set_header("Content-Type", "text/plain") try: - gain = float(self.get_body_argument('gain')) + gain = float(self.get_body_argument("gain")) if self.application.wfs is not None: if gain >= 0.0 and gain <= 1.0: self.application.wfs.m2_gain = gain @@ -510,7 +588,9 @@ def get(self): except PermissionError as e: # started getting weird permission errors on hacksaw that looks like NFS race bug. # running 'ls' in the directory clears the error... - log.warning(f"Permission error, {e.__class__}, while listing files in {p}...") + log.warning( + f"Permission error, {e.__class__}, while listing files in {p}..." + ) os.system(f"ls {p} > /dev/null") fullfiles = [] files = [] @@ -551,14 +631,14 @@ def get(self): class CompMirrorStatus(tornado.web.RequestHandler): def get(self): - compmirror = self.application.wfs_systems['newf9'].compmirror + compmirror = self.application.wfs_systems["newf9"].compmirror status = compmirror.get_mirror() self.write(json.dumps(status)) self.finish() class CompMirrorToggle(tornado.web.RequestHandler): def get(self): - compmirror = self.application.wfs_systems['newf9'].compmirror + compmirror = self.application.wfs_systems["newf9"].compmirror status = compmirror.toggle_mirror() self.write(json.dumps(status)) self.finish() @@ -567,21 +647,22 @@ class Download(tornado.web.RequestHandler): """ Handles downloading of the figure in various file formats. """ + def get(self, fig, fmt): managers = self.application.managers mimetypes = { - 'ps': 'application/postscript', - 'eps': 'application/postscript', - 'pdf': 'application/pdf', - 'svg': 'image/svg+xml', - 'png': 'image/png', - 'jpeg': 'image/jpeg', - 'tif': 'image/tiff', - 'emf': 'application/emf' + "ps": "application/postscript", + "eps": "application/postscript", + "pdf": "application/pdf", + "svg": "image/svg+xml", + "png": "image/png", + "jpeg": "image/jpeg", + "tif": "image/tiff", + "emf": "application/emf", } - self.set_header('Content-Type', mimetypes.get(fmt, 'binary')) + self.set_header("Content-Type", mimetypes.get(fmt, "binary")) buff = io.BytesIO() managers[fig].canvas.print_figure(buff, format=fmt) @@ -592,15 +673,18 @@ class LogStreamer(tornado.websocket.WebSocketHandler): """ A websocket for streaming log messages from log file to the browser. """ + def open(self): - if hasattr(self, 'set_nodelay'): + if hasattr(self, "set_nodelay"): self.set_nodelay(False) filename = self.application.logfile - self.proc = Subprocess(["tail", "-f", "-n", "0", filename], - stdout=Subprocess.STREAM, - stdin=Subprocess.STREAM, - stderr=Subprocess.STREAM, - bufsize=1) + self.proc = Subprocess( + ["tail", "-f", "-n", "0", filename], + stdout=Subprocess.STREAM, + stdin=Subprocess.STREAM, + stderr=Subprocess.STREAM, + bufsize=1, + ) self.proc.set_exit_callback(self._close) tornado.ioloop.IOLoop.current().spawn_callback(self.stream_output) @@ -621,7 +705,7 @@ def stream_output(self): try: while True: line = yield self.proc.stdout.read_until(b"\n") - self.write_line(line.decode('utf-8')) + self.write_line(line.decode("utf-8")) except StreamClosedError: pass @@ -632,10 +716,16 @@ def write_line(self, html): color = "text-danger" else: color = "text-success" - if "tornado.access" not in html and "poppy" not in html and "DEBUG" not in html: + if ( + "tornado.access" not in html + and "poppy" not in html + and "DEBUG" not in html + ): html = "%s" % (color, html) - html += "" - self.write_message(html.encode('utf-8')) + html += ( + '' + ) + self.write_message(html.encode("utf-8")) class WebSocket(tornado.websocket.WebSocketHandler): """ @@ -654,6 +744,7 @@ class WebSocket(tornado.websocket.WebSocketHandler): - ``send_binary(blob)`` is called to send binary image data to the browser. """ + supports_binary = True def open(self, figname): @@ -661,7 +752,7 @@ def open(self, figname): self.figname = figname manager = self.application.managers[figname] manager.add_web_socket(self) - if hasattr(self, 'set_nodelay'): + if hasattr(self, "set_nodelay"): self.set_nodelay(True) def on_close(self): @@ -677,11 +768,14 @@ def on_message(self, message): # Every message has a "type" and a "figure_id". message = json.loads(message) - if message['type'] == 'supports_binary': - self.supports_binary = message['value'] + if message["type"] == "supports_binary": + self.supports_binary = message["value"] else: - manager = self.application.fig_id_map[message['figure_id']] - manager.handle_json(message) + manager = self.application.fig_id_map[message["figure_id"]] + try: + manager.handle_json(message) + except Exception: + pass def send_json(self, content): self.write_message(json.dumps(content)) @@ -691,7 +785,8 @@ def send_binary(self, blob): self.write_message(blob, binary=True) else: data_uri = "data:image/png;base64,{0}".format( - blob.encode('base64').replace('\n', '')) + blob.encode("base64").replace("\n", "") + ) self.write_message(data_uri) def restart_wfs(self, wfs): @@ -703,7 +798,7 @@ def restart_wfs(self, wfs): def close_figures(self): if self.figures is not None: - plt.close('all') + plt.close("all") def refresh_figure(self, k, figure): if k not in self.managers: @@ -711,10 +806,9 @@ def refresh_figure(self, k, figure): self.managers[k] = new_figure_manager_given_figure(fignum, figure) self.fig_id_map[fignum] = self.managers[k] else: - canvas = FigureCanvasWebAggCore(figure) + canvas = FigureCanvasWebAgg(figure) self.managers[k].canvas = canvas self.managers[k].canvas.manager = self.managers[k] - self.managers[k]._get_toolbar(canvas) self.managers[k]._send_event("refresh") self.managers[k].canvas.draw() @@ -742,20 +836,28 @@ def set_redis(self, key, value): def update_seeing(self, results): try: - wfs_seeing = results['seeing'].round(2).value - wfs_raw_seeing = results['raw_seeing'].round(2).value - r1 = self.set_redis('wfs_seeing', wfs_seeing) - r2 = self.set_redis('wfs_raw_seeing', wfs_raw_seeing) + wfs_seeing = results["vlt_seeing"].round(2).value + wfs_raw_seeing = results["raw_vlt_seeing"].round(2).value + wfs_ellipticity = results["ellipticity"].round(3) + r1 = self.set_redis("wfs_seeing", wfs_seeing) + r2 = self.set_redis("wfs_raw_seeing", wfs_raw_seeing) if None not in r1 and None not in r2: - log.info(f"Set redis values wfs_seeing={wfs_seeing} and wfs_raw_seeing={wfs_raw_seeing}") + log.info( + f"Set redis values wfs_seeing={wfs_seeing} and wfs_raw_seeing={wfs_raw_seeing}" + ) else: log.warning("Problem sending seeing values to redis...") + r3 = self.set_redis("wfs_ellipticity", wfs_ellipticity) + if None not in r3: + log.info(f"Set redis values wfs_ellipticity={wfs_ellipticity}") + else: + log.warning("Problem sending ellipticity value to redis...") except Exception as e: - log.warning(f'Error connecting to MMTO API server... : {e}') + log.warning(f"Error connecting to MMTO API server... : {e}") def __init__(self): - if 'WFSROOT' in os.environ: - self.datadir = pathlib.Path(os.environ['WFSROOT']) + if "WFSROOT" in os.environ: + self.datadir = pathlib.Path(os.environ["WFSROOT"]) else: self.datadir = pathlib.Path.cwd() @@ -768,7 +870,9 @@ def __init__(self): if os.path.isdir(self.datadir): self.logfile = self.datadir / "wfs.log" - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) handler = logging.handlers.WatchedFileHandler(self.logfile) handler.setFormatter(formatter) glog.addHandler(handler) @@ -778,11 +882,13 @@ def __init__(self): self.wfs = None self.wfs_systems = {} - self.wfs_keys = ['newf9', 'f5', 'mmirs', 'binospec'] + self.wfs_keys = ["newf9", "f5", "mmirs", "binospec"] self.wfs_names = {} for w in self.wfs_keys: self.wfs_systems[w] = WFSFactory(wfs=w) - self.wfs_systems[w].nzern = 10 # hard-code this for now, but should be configurable/settable + self.wfs_systems[w].nzern = ( + 10 # hard-code this for now, but should be configurable/settable + ) self.wfs_names[w] = self.wfs_systems[w].name self.busy = False @@ -797,15 +903,27 @@ def __init__(self): self.refresh_figures() self.wavefront_fit = ZernikeVector(Z04=1) - if 'REDISHOST' in os.environ: - redis_host = os.environ['REDISHOST'] + if "REDISHOST" in os.environ: + redis_host = os.environ["REDISHOST"] else: - redis_host = 'redis.mmto.arizona.edu' + redis_host = "redis.mmto.arizona.edu" self.redis_server = redis.StrictRedis(host=redis_host, port=6379, db=0) handlers = [ + # Static files for the CSS and JS + ( + r"/_static/(.*)", + tornado.web.StaticFileHandler, + {"path": FigureManagerWebAgg.get_static_file_path()}, + ), + # Static images for the toolbar + ( + r"/_images/(.*)", + tornado.web.StaticFileHandler, + {"path": Path(matplotlib.get_data_path(), "images")}, + ), (r"/", self.HomeHandler), - (r"/mpl\.js", tornado.web.RedirectHandler, dict(url="static/js/mpl.js")), + (r"/mpl.js", self.MplJs), (r"/select", self.SelectHandler), (r"/wfspage", self.WFSPageHandler), (r"/connect", self.ConnectHandler), @@ -827,15 +945,15 @@ def __init__(self): (r"/clearm2", self.ClearM2Handler), (r"/compmirror", self.CompMirrorStatus), (r"/compmirrortoggle", self.CompMirrorToggle), - (r'/download_([a-z]+).([a-z0-9.]+)', self.Download), - (r'/log', self.LogStreamer), - (r'/([a-z0-9.]+)/ws', self.WebSocket) + (r"/download_([a-z]+).([a-z0-9.]+)", self.Download), + (r"/log", self.LogStreamer), + (r"/([a-z0-9.]+)/ws", self.WebSocket), ] settings = dict( template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), - debug=True + debug=True, ) super(WFSsrv, self).__init__(handlers, **settings) @@ -844,12 +962,30 @@ def main(): application = WFSsrv() http_server = tornado.httpserver.HTTPServer(application) - http_server.listen(8080) - - print("http://127.0.0.1:8080/") + sockets = tornado.netutil.bind_sockets(8080, "") + http_server.add_sockets(sockets) + + for s in sockets: + addr, port = s.getsockname()[:2] + if s.family is socket.AF_INET6: + addr = f"[{addr}]" + print(f"WFSSrv listening on http://{addr}:{port}/") print("Press Ctrl+C to quit") - tornado.ioloop.IOLoop.instance().start() + ioloop = tornado.ioloop.IOLoop.instance() + + def shutdown(): + ioloop.stop() + print("Server stopped") + + old_handler = signal.signal( + signal.SIGINT, lambda sig, frame: ioloop.add_callback_from_signal(shutdown) + ) + + try: + ioloop.start() + finally: + signal.signal(signal.SIGINT, old_handler) if __name__ == "__main__":