Repository: BYUCamachoLab/pyrolab (language: python)

Issue:
v0.3.0 merge in



Discussion / hints:

Fix patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index e3085da..9b4171d 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 0.2.1
+current_version = 0.3.0
 commit = True
 tag = True
 parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
@@ -8,10 +8,12 @@ serialize =
 
 [bumpversion:part:build]
 
-[bumpversion:file:pyrolab/__init__.py]
+[bumpversion:file:src/pyrolab/__init__.py]
 
-[bumpversion:file:docs/conf.py]
+[bumpversion:file:src/pyromonitor/__init__.py]
 
-[bumpversion:file:./setup.cfg]
+[bumpversion:file:docs/conf.py]
 
 [bumpversion:file:./README.md]
+
+[bumpversion:file:./pyproject.toml]
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a612323..6c4445b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,9 +12,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout code
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
       - name: Set up Python
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: '3.x'
       - name: Install dependencies
@@ -25,17 +25,18 @@ jobs:
       - name: Build
         run: |
           python -m build
+          pip install -e .
       - name: Get version number
         id: get_version
-        run: echo ::set-output name=VERSION::$(python3 -c "import pyrolab; print(pyrolab.__version__)")
+        run: echo "VERSION=$(python3 -c 'import pyrolab; print(pyrolab.__version__)')" >> $GITHUB_ENV
       - name: Load Release text
         id: get_text
         run: |
-          BODY=$(cat ./docs/changelog/${{ steps.get_version.outputs.VERSION }}-changelog.md)
+          BODY=$(cat ./docs/changelog/${{ env.VERSION }}-changelog.md)
           BODY="${BODY//'%'/'%25'}"
           BODY="${BODY//$'\n'/'%0A'}"
           BODY="${BODY//$'\r'/'%0D'}"
-          echo "::set-output name=BODY::$BODY"
+          echo "BODY=$BODY" >> $GITHUB_OUTPUT
       - name: Publish to PyPI
         uses: pypa/gh-action-pypi-publish@v1.5.0
         with:
@@ -48,7 +49,7 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
         with:
           tag_name: ${{ github.ref }}
-          release_name: PyroLab ${{ steps.get_version.outputs.VERSION }}
-          body: ${{ steps.get_text.outputs.BODY }}
+          release_name: PyroLab ${{ env.VERSION }}
+          body: ${{ env.BODY }}
           draft: false
           prerelease: false
diff --git a/.gitignore b/.gitignore
index 0fcda0b..f95cd13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,6 +70,7 @@ instance/
 
 # Sphinx documentation
 docs/_build/
+docs/reference/api/
 
 # PyBuilder
 target/
@@ -132,3 +133,8 @@ dmypy.json
 .vscode/
 sandbox/
 tests/data/config_test.yaml
+.DS_Store
+
+# Local data
+src/pyrolab/data/local/
+src/pyromonitor/localdata/
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..83f23ea
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,1 @@
+include LICENSE README.md NOTICE.txt AUTHORS.txt pyproject.toml
diff --git a/README.md b/README.md
index db410e7..0a56e2f 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 ---
 
 <p align="center">
-<img alt="Development version" src="https://img.shields.io/badge/master-v0.2.1-informational">
+<img alt="Development version" src="https://img.shields.io/badge/master-v0.3.0-informational">
 <a href="https://pypi.python.org/pypi/pyrolab"><img alt="PyPI Version" src="https://img.shields.io/pypi/v/pyrolab.svg"></a>
 <img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/pyrolab">
 <a href="https://pyrolab.readthedocs.io/"><img alt="Documentation Status" src="https://readthedocs.org/projects/pyrolab/badge/?version=latest"></a>
@@ -50,6 +50,7 @@ The full list of supported extras is:
 - tsl550
 - oscope
 - arduino
+- monitor
 
 You can also clone the repository, navigate to the toplevel, and install in 
 editable mode (make sure you have pip >= 21.1):
@@ -58,20 +59,16 @@ editable mode (make sure you have pip >= 21.1):
 pip install -e .
 ```
 
-## Uninstallation
+## Web Monitor
 
-PyroLab creates data and configuration directories that aren't deleted when
-pip uninstalled. You can find their locations by running (before 
-uninstallation):
+PyroLab comes with a web application that can monitor your nameserver and 
+provide an easy-to-access status board. It's a Flask app that can be served
+using a production grade server. To run:
 
 ```
-import pyrolab
-print(pyrolab.PYROLAB_DATA_DIR)
-print(pyrolab.PYROLAB_CONFIG_DIR)
+pyromonitor up
 ```
 
-These folders can be safely deleted after uninstallation.
-
 ## Example
 
 ### Local Instruments
@@ -142,7 +139,7 @@ configurations change, PyroLab will automatically work when started, every
 time!
 
 For an example of how a new PyroLab instrument server should be configured the
-first time it's run, see ``examples/302.resource-manager/prep.py``.
+first time it's run, see ``examples/library-catalog/config.yaml``.
 
 ## FAQ's
 1. **Another instrument library? What about all the others?**  
@@ -187,7 +184,7 @@ corrresponding version number and commit the modified files (where version
 numbers were updated). Pushing the tags (a manual process) to the remote will 
 automatically create a new release. Releases are automatically published to 
 PyPI and GitHub when git tags matching the "v*" pattern are created 
-(e.g. "v0.2.1"), as bumpversion does.
+(e.g. "v0.3.0"), as bumpversion does.
 
 After bumping version, you can view the tags on the local machine by running 
 ``git tag``. To push the tags to the remote server and trigger the release
@@ -199,6 +196,19 @@ it's safest to run it separately through another terminal).
 
 ## Building the Docs
 
-Much of the API documentation is autogenerated from the source code. Due to
-this, to maintain a clean repository, make sure to never commit the directory
-``/docs/reference/api`` to Git. Always delete this folder before committing.
+Much of the API documentation is autogenerated from the source code. Gitignores
+are in place to prevent you from committing autogenerated pages.
+
+To build the docs, navigate to the ``docs/`` directory and run:
+
+```
+pip install -r requirements.txt
+make html
+```
+
+## Building the Package
+
+You can test the build and view the bundled artifacts using 
+[build](https://pypi.org/project/build/). It's recommended you build locally
+before pushing to PyPI. In particular, double check the included files and make
+sure only the required files are there by modifying MANIFEST.in as necessary.
diff --git a/docs/_static/images/architecture.png b/docs/_static/images/architecture.png
new file mode 100644
index 0000000..b08e2a7
Binary files /dev/null and b/docs/_static/images/architecture.png differ
diff --git a/docs/_static/images/ohnoanyway.png b/docs/_static/images/ohnoanyway.png
new file mode 100644
index 0000000..2d62b9a
Binary files /dev/null and b/docs/_static/images/ohnoanyway.png differ
diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst
index 5b0d7e7..1c2bade 100644
--- a/docs/_templates/autosummary/class.rst
+++ b/docs/_templates/autosummary/class.rst
@@ -1,27 +1,31 @@
-{% extends "!autosummary/class.rst" %}
+{{ fullname | escape | underline}}
 
-{% block methods %}
-{% if methods %}
-   .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages.
-      .. autosummary::
-         :toctree:
-      {% for item in methods %}
-         {%- if not item.startswith('_') or item in ['__call__'] %}
-         {{ name }}.{{ item }}
-         {%- endif -%}
-      {%- endfor %}
-{% endif %}
-{% endblock %}
+.. currentmodule:: {{ module }}
+
+.. autoclass:: {{ objname }}
 
-{% block attributes %}
-{% if attributes %}
+   {% block methods %}
+   {% if methods %}
    .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages.
-      .. autosummary::
-         :toctree:
-      {% for item in attributes %}
-         {%- if not item.startswith('_') %}
-         {{ name }}.{{ item }}
-         {%- endif -%}
-      {%- endfor %}
-{% endif %}
-{% endblock %}
\ No newline at end of file
+       .. autosummary::
+           :toctree:
+       {% for item in all_methods %}
+           {%- if not item.startswith('_') or item in ['__call__'] %}
+           {{ name }}.{{ item }}
+           {%- endif -%}
+       {%- endfor %}
+   {% endif %}
+   {% endblock %}
+
+   {% block attributes %}
+   {% if attributes %}
+      .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages.
+         .. autosummary::
+            :toctree:
+         {% for item in all_attributes %}
+            {%- if not item.startswith('_') %}
+            {{ name }}.{{ item }}
+            {%- endif -%}
+         {%- endfor %}
+   {% endif %}
+   {% endblock %}
diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst
index e1f428d..fffa76b 100644
--- a/docs/_templates/autosummary/module.rst
+++ b/docs/_templates/autosummary/module.rst
@@ -1,40 +1,66 @@
-{% extends "!autosummary/module.rst" %}
-
 {# This file is almost the same as the default, but adds :toctree: to the autosummary directives.
    The original can be found at `sphinx/ext/autosummary/templates/autosummary/module.rst`. #}
 
-{% block attributes %}
-{% if attributes %}
-   .. rubric:: Module Attributes
+{{ fullname | escape | underline}}
 
-   .. autosummary::
-      :toctree:
-   {% for item in attributes %}
-      {{ item }}
-   {%- endfor %}
-{% endif %}
-{% endblock %}
+.. automodule:: {{ fullname }}
 
-{% block functions %}
-{% if functions %}
-   .. rubric:: Functions
+   {% block attributes %}
+   {% if attributes %}
+      .. rubric:: Module Attributes
+   
+      .. autosummary::
+         :toctree:
+      {% for item in attributes %}
+         {{ item }}
+      {%- endfor %}
+   {% endif %}
+   {% endblock %}
+   
+   {% block functions %}
+   {% if functions %}
+      .. rubric:: Functions
+   
+      .. autosummary::
+         :toctree:
+      {% for item in functions %}
+         {{ item }}
+      {%- endfor %}
+   {% endif %}
+   {% endblock %}
+   
+   {% block classes %}
+   {% if classes %}
+      .. rubric:: Classes
+   
+      .. autosummary::
+         :toctree:
+      {% for item in classes %}
+         {{ item }}
+      {%- endfor %}
+   {% endif %}
+   {% endblock %}
+
+   {% block exceptions %}
+   {% if exceptions %}
+   .. rubric:: {{ _('Exceptions') }}
 
    .. autosummary::
-      :toctree:
-   {% for item in functions %}
+   {% for item in exceptions %}
       {{ item }}
    {%- endfor %}
-{% endif %}
-{% endblock %}
+   {% endif %}
+   {% endblock %}
 
-{% block classes %}
-{% if classes %}
-   .. rubric:: Classes
+{% block modules %}
+{% if modules %}
+.. rubric:: Modules
 
-   .. autosummary::
-      :toctree:
-   {% for item in classes %}
-      {{ item }}
-   {%- endfor %}
+.. autosummary::
+   :toctree:
+   :recursive:
+{% for item in modules %}
+   {{ item }}
+{%- endfor %}
 {% endif %}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/docs/_templates/sidebar-nav-bs.html b/docs/_templates/sidebar-nav-bs.html
deleted file mode 100644
index 7e0043e..0000000
--- a/docs/_templates/sidebar-nav-bs.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<nav class="bd-links" id="bd-docs-nav" aria-label="Main navigation">
-  <div class="bd-toc-item active">
-    {% if pagename.startswith("reference") %}
-    {{ generate_nav_html("sidebar", maxdepth=4, collapse=True, includehidden=True, titles_only=True) }}
-    {% else %}
-    {{ generate_nav_html("sidebar", maxdepth=4, collapse=False, includehidden=True, titles_only=True) }}
-    {% endif %}
-  </div>
-</nav>
diff --git a/docs/changelog/0.3.0-changelog.md b/docs/changelog/0.3.0-changelog.md
new file mode 100644
index 0000000..d40a9bc
--- /dev/null
+++ b/docs/changelog/0.3.0-changelog.md
@@ -0,0 +1,22 @@
+## 0.3.0 - 2023-02-26
+
+Significant restructuring, bug fixes, new features!
+
+### Added
+- There's a new Flask web app called pyromonitor that can be configured to
+  ping a nameserver and present instrument status in an easy-to-read table.
+- PyroLab now pings PyPI to check for updates and notifies the user if a newer
+  version is available.
+
+### Changed
+- Fixed connection function for PPCL55x lasers so they don't alternate between
+  connecting and failing if they're already connected.
+- All PyroLab errors are now builtin Python errors in an attempt to make them
+  serializable over the network. This way, more useful information appears
+  on the client side than "unsupported serialized class."
+- Configuration files are now stored in the same directory as the PyroLab
+  package installation. This means that when it's uninstalled, it doesn't 
+  orphan configuration files in some obscure directory on your computer.
+
+### Removed
+- None
diff --git a/docs/conf.py b/docs/conf.py
index 5fcbaca..6c8138c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -15,7 +15,7 @@
 import os
 import sys
 sys.path.insert(0, os.path.abspath('.'))
-sys.path.insert(0, os.path.abspath('..'))
+sys.path.insert(0, os.path.abspath('../src'))
 
 
 # -- Project information -----------------------------------------------------
@@ -25,7 +25,19 @@
 author = 'Sequoia Ploeg'
 
 # The full version, including alpha/beta/rc tags
-release = '0.2.1'
+release = '0.3.0'
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+language = "en"
 
 
 # -- General configuration ---------------------------------------------------
@@ -34,29 +46,34 @@
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
-    'sphinx.ext.autodoc',
-    'numpydoc',
+    "sphinx.ext.autodoc",
+    "numpydoc",
     # "sphinx.ext.intersphinx",
     # "sphinx.ext.coverage",
     # "sphinx.ext.doctest",
-    'sphinx.ext.autosummary',
-    # 'sphinx_autodoc_typehints',
+    "sphinx.ext.autosummary",
+    "sphinx.ext.autodoc.typehints",
+    "sphinx.ext.viewcode",
     # "sphinx.ext.graphviz",
     # "sphinx.ext.ifconfig",
-    # 'matplotlib.sphinxext.plot_directive',
     # 'IPython.sphinxext.ipython_console_highlighting',
     # 'IPython.sphinxext.ipython_directive',
     # "sphinx.ext.imgmath",
-    # 'sphinx.ext.todo', #
-    'sphinx.ext.mathjax',
+    "sphinx.ext.todo",
+    "sphinx.ext.mathjax",
     # 'sphinx.ext.viewcode', #
+    # "sphinxext.rediraffe",
     # 'sphinx.ext.githubpages', #
-    # 'sphinx_autodoc_typehints', #
-    'sphinx_panels',
+    "sphinx_design",
+    "sphinx_copybutton",
+    # "matplotlib.sphinxext.mathmpl",
+    # "matplotlib.sphinxext.plot_directive",
+    # "myst_nb",
+    "sphinx_togglebutton",
+    # "sphinxcontrib.bibtex",
 ]
 
 autosummary_generate = True  # Turn on sphinx.ext.autosummary
-# autosummary_generate = True if include_api else ["index"]
 autodoc_typehints = "none"
 autoclass_content = "class"  # Add __init__ doc (ie. params) to class summaries
 # html_show_sourcelink = False  # Remove 'view source code' from top of page (for html, not python)
@@ -76,24 +93,39 @@
     'sacher_tec',
     'pyvisa',
     'pyfirmata',
+    'cv2',
 ]
 
-panels_add_bootstrap_css = False
+# -- MyST-NB settings --------------------------------------------------------
+# https://myst-nb.readthedocs.io/en/latest/configuration.html#execution
+# source_suffix = {
+#     '.rst': 'restructuredtext',
+#     '.ipynb': 'myst-nb',
+#     '.myst': 'myst-nb',
+#     # '.md': 'markdown',
+# }
+# nb_execution_mode = "cache"
+# nb_execution_excludepatterns = [
+#     'cpn_fixer.ipynb',
+#     'eo_fixer.ipynb',
+# ]
+
+# -- Matplotlib plot directive -----------------------------------------------
+
+# For speedup, decide which plot_formats to build based on build targets:
+#     html only -> png
+#     latex only -> pdf
+#     all other cases, including html + latex -> png, pdf
+# For simplicity, we assume that the build targets appear in the command line.
+# We're falling back on using all formats in case that assumption fails.
+# formats = {'html': ('png', 100), 'latex': ('pdf', 100)}
+# plot_formats = [formats[target] for target in ['html', 'latex']
+#                 if target in sys.argv] or list(formats.values())
 
 # numpydoc
-numpydoc_attributes_as_param_list = False
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-language = "en"
+# numpydoc_attributes_as_param_list = False
+# numpydoc_show_class_members = False 
+# numpydoc_class_members_toctree = False
 
 
 # -- Options for HTML output -------------------------------------------------
@@ -114,6 +146,8 @@
             "name": "GitHub",
             "url": "https://github.com/BYUCamachoLab/pyrolab",
             "icon": "fab fa-github-square",
+            "type": "fontawesome",
+            "attributes": {"target": "_blank"},
         },
         # {
         #     "name": "GitLab",
@@ -127,32 +161,49 @@
         # },
     ],
     "navigation_with_keys": False,
+    # "logo": {
+    #     "text": "PyData Theme",
+    #     "image_dark": "logo-dark.svg",
+    #     "alt_text": "PyData Theme",
+    # },
+    "use_edit_page_button": True,
+    "show_toc_level": 1,
+    "navbar_align": "left",  # [left, content, right] For testing that the navbar items align properly
+    # "navbar_center": ["version-switcher", "navbar-nav"],
+    # "announcement": "https://raw.githubusercontent.com/pydata/pydata-sphinx-theme/main/docs/_templates/custom-template.html",
+    "show_nav_level": 1,
+    # "navbar_start": ["navbar-logo"],
+    # "navbar_end": ["theme-switcher", "navbar-icon-links"],
+    # "navbar_persistent": ["search-button"],
+    # "primary_sidebar_end": ["custom-template.html", "sidebar-ethical-ads.html"],
+    # "footer_items": ["copyright"],
+    # "secondary_sidebar_items": ["page-toc.html"],  # Remove the source buttons
+    # "switcher": {
+    #     "json_url": json_url,
+    #     "version_match": version_match,
+    # },
+    "external_links": [
+      {"name": "Changelog", "url": "https://github.com/BYUCamachoLab/pyrolab/releases"},
+    ],
 }
 
-# Add any paths that contain custom themes here, relative to this directory.
-# html_theme_path = ["themes"]
-
-# The name for this set of Sphinx documents.  If None, it defaults to
-# "<project> v<release> documentation".
-# html_title = None
-
-# A shorter title for the navigation bar.  Default is the same as html_title.
-# html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-# html_logo = "_static/img/logo.svg"
-
-# You can remove the sidebar from some pages by listing them here
-# html_sidebars = {
-#   "pagename": []
-# }
+html_sidebars = {
+    "index": [],
+}
 
 html_css_files = [
     "css/getting_started.css",
     "css/pyrolab.css",
 ]
 
+html_context = {
+    "github_url": "https://github.com",
+    "github_user": "BYUCamachoLab",
+    "github_repo": "pyrolab",
+    "github_version": "master",
+    "doc_path": "docs/",
+}
+
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
@@ -165,9 +216,3 @@
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
 # html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-# html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst
new file mode 100644
index 0000000..46e6b6d
--- /dev/null
+++ b/docs/development/contributing.rst
@@ -0,0 +1,25 @@
+.. _development_contributing:
+
+
+============
+Contributing
+============
+
+Have an instrument driver you've written for Python code? Why not add it to 
+PyroLab and expand our set of supported devices?
+
+We're always looking to expand the number of devices included in the driver
+library. We'd love to have yours, too!
+
+To submit your driver, fork PyroLab on GitHub, add your driver (inheriting 
+from the right parent classes, and creating a new driver category if it doesn't
+fall under any existing categories), make sure it's well documented and 
+following the numpydoc standard, test it to make sure it works over a network
+(pay particular attention to the types of the arguments and the return values),
+and then submit a pull request! One of the maintainers will then look at it
+as soon as possible, work with you to make sure it's up to standards, and then
+merge it in to the main repository and release a new version to PyPI.
+
+You can also contribute by submitting issues, improvement suggestions, or bug
+reports. We manage these through `GitHub Issues
+<https://github.com/BYUCamachoLab/pyrolab/issues>`_.
diff --git a/docs/development/index.rst b/docs/development/index.rst
index 8acfcd3..cee32fc 100644
--- a/docs/development/index.rst
+++ b/docs/development/index.rst
@@ -13,12 +13,13 @@ request with a fix!
 More information about the development of PyroLab can be found at our 
 `project webpage <https://camacholab.byu.edu/research/computational-photonics>`__.
 
-.. The documentation is hosted for free at https://simphonyphotonics.readthedocs.io/.
+The documentation is hosted for free at https://pyrolab.readthedocs.io/.
 
 The source for this documentation can be found in the master branch of the source repository.
 
 .. toctree::
    :maxdepth: 2
 
+   contributing
    documenting
-   code_standard
\ No newline at end of file
+   code_standard
diff --git a/docs/getting_started/architecture.rst b/docs/getting_started/architecture.rst
index 328871f..bb1e8bc 100644
--- a/docs/getting_started/architecture.rst
+++ b/docs/getting_started/architecture.rst
@@ -1,17 +1,111 @@
 .. _getting_started_architecture:
 
 
-Architecture
-============
+Network Architecture
+====================
 
-Typical setup of a PyroLab network
-----------------------------------
 
-Nameservers exist,
-Daemons start up and ping them.
-Nameservers continually check in with daemons.
-Clients ping nameservers.
-Clients communicate directly with daemons.
-Clients communicate directly with nameservers.
+On the shoulders of giants...
+-----------------------------
 
-Make a diagram.
\ No newline at end of file
+Yes, we created PyroLab from scratch. No, we did not create the network
+communication library from scratch.
+
+`Pyro5`_ is a pure Python Remote Procedure Call (`RPC`_) library, licensed
+under the MIT license.
+
+.. _RPC: https://en.wikipedia.org/wiki/Remote_procedure_call
+.. _Pyro5: http://pyro5.readthedocs.org/en/latest/
+
+
+Definitions
+-----------
+
+There are four important concepts from Pyro5 that are important in PyroLab:
+
+#. Services (API's for remote resources, instrument drivers, data processing routines)
+#. Servers (machines that host remote resources, are physically connected to instruments, etc.)
+#. Nameservers (an optional "phonebook," making locating servers and services easy)
+#. Clients (machines that aggregate and access remote resources)
+
+PyroLab **services** are objects that are remotely accessible. These might be
+hardware devices, long running calculations, or perhaps an interface providing
+an API for interacting with filesystems on a remote machine; it all depends on
+what methods your services provide. 
+
+The machine that hosts your services is called a **server** and runs one or more
+"daemon" processes that listen for requests on a port (one port per daemon).
+Each daemon runs in its own process, so the failure of one daemon doesn't
+affect another. Each daemon controls one or more services. Therefore, multiple
+services can be accessed on the same IP address and the same port, but they
+each have a unique Pyro URI that you connect with in order to send commands.
+One computer may therefore have services accessible through multiple ports,
+depending on how many daemons are configured. There are different run
+configurations for daemons, which are described in detail in the `user guide
+<user_guide_servers>`_.
+
+A **nameserver** acts as a phonebook. Daemons can be configured to register
+themselves with a nameserver when they're started up. In essence, they send a
+notification to the server that they're up, and they send over a list of
+services that can now be accessed. They provide the name of the service, as
+well as a URI that can be fed to a Proxy, the object in PyroLab that a client
+uses to directly connect to a service and call its methods. The nameserver does
+nothing more than store names to URI's, and provide them upon request from a
+client. Note that a nameserver doesn't have to be running on a separate 
+machine; all parts of a PyroLab network could in theory be running on a single
+machine. The fact that they interact by making requests that are handled on 
+certain ports is what makes them a network.
+
+A **client** is your local machine, when you're using it to connect to remote
+resources. You access remote resources by using a
+:py:class:`~pyrolab.api.Proxy`. A Proxy takes a URI that identifies a service
+to be connected to. Usually, you will ask the nameserver for the location of
+some service by using its "human-readable" name, but it will return a
+(unwieldy) URI that is then fed into the Proxy. You can now call all the
+publicly exposed methods of the service as if it were a local object running on
+your own machine.
+
+
+Network Architecture
+--------------------
+
+.. figure:: /_static/images/architecture.png
+   :width: 600
+   :alt: PyroLab architecture in the wild
+   
+   a. An example hardware setup. b. The interactions between objects in a 
+   PyroLab network. A resource server is a "daemon."
+
+When configuring a PyroLab network, you basically think about it in the 
+following order:
+
+#. Set up a nameserver to track available services.
+#. Create some server daemons (called resource servers in the figure above)
+   on your machines connected to specific hardware or providing specific
+   computation functionality. 
+
+   * The daemons will ping the nameserver to register themselves when they
+     start up.
+
+#. The nameserver will periodically check in with the daemons to make sure they
+   haven't disappeared or gone offline. 
+
+   * If they have gone offline, the nameserver can be configured to 
+     automatically unregister them. Or, you might wish to keep a record of them 
+     and their last known address in the nameserver registry.
+
+#. Clients ping nameservers to get the URI address of services they wish to
+   interact with.
+#. Clients then communicate **directly** with daemons, which relay requests to
+   services/hardware they host.
+
+   * At this point, the nameserver is out of the picture! It only gives the URI
+     to the client, which then *directly contacts* the service. The nameserver
+     does not relay any of the requests.
+
+You can see there is a triangle of communciation going on; nameservers and
+daemons communicate, nameservers and clients communicate, and clients and
+daemons communicate. None of the client communication with the daemon passes
+through the nameserver, though. And finally, all communication with the service
+itself is over-the-wire via the daemon; the nameserver and client never 
+directly interact with a service, except through the daemon.
diff --git a/docs/getting_started/cli.rst b/docs/getting_started/cli.rst
new file mode 100644
index 0000000..4783c02
--- /dev/null
+++ b/docs/getting_started/cli.rst
@@ -0,0 +1,101 @@
+.. _getting_started_cli:
+
+
+Command Line Interface
+======================
+
+PyroLab infrastructure is managed through a command line interface (CLI) that
+is inspired by Docker. If you are familiar, PyroLab's CLI may feel quite 
+familiar. Knowledge of Docker's CLI is definitely not a requirement, though.
+
+You can view all options in the PyroLab CLI by running ``pyrolab --help``:
+
+.. code-block:: bash
+
+    Usage: pyrolab [OPTIONS] COMMAND [ARGS]...
+
+    Options:
+    -v, --version                   Show the version and exit.
+    --data-dir                      Show the data directories and exit.
+    --install-completion [bash|zsh|fish|powershell|pwsh]
+                                    Install completion for the specified shell.
+    --show-completion [bash|zsh|fish|powershell|pwsh]
+                                    Show completion for the specified shell, to
+                                    copy it or customize the installation.
+    --help                          Show this message and exit.
+
+    Commands:
+    config  Configure PyroLab nameservers, daemons, and services.
+    down    Stop the background PyroLab daemon.
+    info    Print details about a nameserver, daemon, or service.
+    logs
+    ps      Process status: list all running PyroLab nameservers, daemons,...
+    reload  Reload the PyroLab daemon using the latest configuration file.
+    rename  Rename a nameserver, daemon or service.
+    start   Start a nameserver or daemon (and its services).
+    stop    Stop a nameserver or daemon (and its services).
+    up      Start the background PyroLab daemon.
+
+PyroLab has an always-running daemon that manages services and other server
+instances. The daemon can be started by running ``pyrolab up``. Any services
+defined in the ``autolaunch`` section of the `YAML file
+<getting_started_yaml>`_ will automatically be started at this time.
+
+PyroLab also maintains an internal global configuration. It is in the same
+format as the configuration file we wrote in the last section. You have to
+explicitly load this configuration file into PyroLab before launching the
+main daemon. You can do this with the ``pyrolab config`` utility:
+
+.. code-block:: bash
+
+    Usage: pyrolab config [OPTIONS] COMMAND [ARGS]...
+
+    Configure PyroLab nameservers, daemons, and services.
+
+    Options:
+    --help  Show this message and exit.
+
+    Commands:
+    export  Export the configuration file
+    reset   Reset the configuration file
+    update  Update the configuration file
+
+In our ``library-catalog`` example, you could load the configuration file:
+
+.. code-block:: bash
+
+    pyrolab config update config.yaml
+
+Then you could start the monitoring PyroLab daemon by running:
+
+.. code-block:: bash
+
+    pyrolab up
+
+Assuming you don't get any errors in the terminal, you can then run the process
+status command:
+
+.. code-block:: bash
+
+    pyrolab ps
+
+It will print to the terminal a nicely formatted table detailing all the 
+running processes daemons and nameservers:
+
+.. code-block:: text
+
+    NAMESERVER    CREATED              STATUS        URI
+    ------------  -------------------  ------------  -----
+    production    2023-02-27 15:58:36  Up 7 seconds
+
+    DAEMON    CREATED              STATUS        URI
+    --------  -------------------  ------------  -----
+    lockable  2023-02-27 15:58:36  Up 7 seconds
+
+The PyroLab daemon monitors the server processes it starts up, automatically 
+relaunching them if they fail or encounter an error at some point. 
+
+Although PyroLab will just happily keep humming along in the background, if you
+want to shutdown the server daemons and main PyroLab daemon, you can by running
+``pyrolab down``. Don't do that yet, though--in the next section, we'll look at 
+how to connect to your custom services from a remote client.
diff --git a/docs/getting_started/client.rst b/docs/getting_started/client.rst
index faf0223..dc2b341 100644
--- a/docs/getting_started/client.rst
+++ b/docs/getting_started/client.rst
@@ -1,2 +1,157 @@
+.. _getting_started_client:
+
+
 Client Tutorial
 ===============
+
+
+Freeing resources
+-----------------
+
+While it's nice to have PyroLab installed on all the computers you plan to use
+it from, it's not technically required; PyroLab simply wraps the default
+Pyro5 :py:class:`~pyrolab.api.Proxy` object. It knows how to communicate with
+PyroLab drivers just as well as the version imported from PyroLab. In all our
+examples, however, we'll import the Proxy from ``pyrolab.api``.
+
+At this point, we have a nameserver running on the default ``localhost`` 
+address. The configuration file also used the default port, 9090. Because 
+PyroLab daemons generate human-undreadable URI's for each service, we'll
+use the nameserver to find our library service.
+
+To connect to any PyroLab object, we use a Proxy. You use a Proxy even when 
+you don't realize it, like connecting to the nameserver:
+
+.. code-block:: python
+
+    ns = locate_ns(host="localhost")
+
+``locate_ns()`` returns a Proxy object. Proxies track a remote Pyro5 URI, but
+they do not connect to the remote server until the first function call is made.
+Servers have limited resources, in terms of sockets--they can only support a
+limited number of connections at a time. If more clients connect to the name
+server, or any Proxy, than the pool size allows, they will get a connection
+error.
+
+It is suggested you apply the following pattern when using the name server in
+your code:
+
+#. obtain a proxy for the NS
+#. look up the stuff you need, store it
+#. free the NS proxy
+#. use the URI's/proxies you've just looked up
+
+This makes sure your client code doesn't consume resources in the name server
+for an excessive amount of time, and more importantly, frees up the limited
+connection pool to let other clients get their turn. If you have a proxy to the
+name server and you let it live for too long, it may eventually deny other
+clients access to the name server because its connection pool is exhausted. So
+if you don't need the proxy anymore, make sure to free it up.
+
+The best way to do this is to use a context manager, which automatically 
+handles disconnecting the connection for you:
+
+.. code-block:: python
+
+    from pyrolab.api import locate_ns
+
+    with locate_ns(host="localhost") as ns:
+        uri = ns.lookup("library")
+
+    print(uri)
+    # this URI will change every time, but this time it printed:
+    # PYRO:obj_a488557cc38f4d149cdc1fdb38d75e27@localhost:50328
+
+.. note::
+
+    You can still use the proxy object when it is disconnected: Pyro will
+    reconnect it for you as soon as it's needed again.
+
+A similar pattern is recommended for drivers to which you make infrequent
+calls:
+
+.. code-block:: python
+
+    from pyrolab.api import Proxy
+
+    with Proxy(uri) as obj:
+        obj.method()
+
+
+Back to the library
+-------------------
+
+We can get our library instance and load it with our catalog to start:
+
+.. code-block:: python
+
+    from pyrolab.api import locate_ns, Proxy
+
+    with locate_ns(host="localhost") as ns:
+        uri = ns.lookup("library")
+
+    with Proxy(uri) as catalog:
+        books = {
+            "The Hobbit": 3,
+            "The Lord of the Rings": 2,
+            "Harry Potter": 3,
+            "The Lightning Thief": 2,
+            "C Programming": 2,
+        }
+
+        for book, number in books.items():
+            catalog.add_book(book, number)
+
+Note that we freed our connection to the nameserver by using it as a context
+manager, which automatically releases the connection upon exiting the block.
+This is best practice, since once we get the URI the first time, we won't be
+needing the nameserver again.
+
+We then use the Proxy object, again in a context manager, to make several calls
+with the same connection. No need to disconnect and reconnect between every
+API call. It's less likely multiple clients will be trying to access the same
+service simultaneously, anyway--especially in the context of PyroLab, which is
+geared towards the remote control of hardware devices. Only one person will
+be using them at a time.
+
+We can now perform operations against our library. The Proxy acts like a local
+Python object, exposing all the public methods that were marked with
+``@expose`` (or all non-private functions if the entire class was decorated).
+Because we used builtin Python exceptions in our class, we can also catch
+exceptions we know will be raised if we perform illegal operations.
+
+.. code-block:: python
+
+    with Proxy(uri) as catalog:
+        
+        catalog.checkout("The Hobbit")
+
+        catalog.checkout("C Programming", 2)
+
+        # Check out a book when all books are already checked out
+        try:
+            catalog.checkout("C Programming")
+        except ValueError as e:
+            # handle this exception...
+
+        # Return a book not in the library's catalog
+        try:
+            catalog.checkin("Dune")
+        except LookupError as e:
+            # handle this exception...
+
+        # Check out a book not in the library's catalog
+        try: 
+            catalog.checkout("Coding for Dummies")
+        except LookupError as e:
+            # handle this exception...
+
+Voila! We now have remote resources available on any client computer as a local
+object. This opens up a world of possibilities when it comes to making multiple
+hardware sources cooperate as a single operation. As a completely random
+example, you might create a photonic chip testing station where one computer
+controls motion stages, another controls a laser source, while yet another
+provides a microscope image of the alignment, all being aggregated and
+controlled by an untethered, underpowered laptop in a laboratory setting.
+
+Continue on for a summary of what we've done in this getting started guide.
diff --git a/docs/getting_started/configuration.rst b/docs/getting_started/configuration.rst
index 07b7265..c10ae53 100644
--- a/docs/getting_started/configuration.rst
+++ b/docs/getting_started/configuration.rst
@@ -1,50 +1,139 @@
-Combined YAML Configuration
-===========================
+.. _getting_started_configuration:
 
-Below is a fully functioning example that you could test on your own computer.
-It uses only included dependencies and sample services and runs on localhost.
-It automatically starts up the nameserver and background services and daemons
-when you run ``pyrolab up``.
+Launch Configuration
+====================
+
+
+Making our services discoverable
+--------------------------------
+
+In order to use your own custom drivers and services with PyroLab, they have to
+be visible to the Python interpreter. This means they must be either
+installable (using pip install) or be in one way or another discoverable by the
+Python interpreter; for example, being placed in the environment variable
+``$PYTHONPATH``. On \*nix systems, this can be done by placing a statement in
+your ``~/.bashrc`` file similar to the following:
+
+.. code-block:: bash
+
+    export PYTHONPATH="/path/to/your/services:$PYTHONPATH"
+
+Another way of doing this is with a simple script that modifies your
+environment variables for the lifetime of that shell. Then it's not globally
+available all of the time. A simple script which does this is in the 
+``library-catalog`` example directory:
+
+.. literalinclude:: /../examples/library-catalog/updatepath.sh
+    :language: bash
+
+This prepends the directory containing the script (and presumably the Python
+module ``service.py`` which contains the library catalog code) to the
+``$PYTHONPATH`` variable. You'll now need to make sure you source the
+``updatepath.sh`` script so that our service is discoverable. In the terminal,
+navigate to the directory containing the library-catalog example and source the
+script:
+
+.. code-block:: bash
+
+    source updatepath.sh
+
+.. note::
+
+    For the sake of this example, it's easiest to run these scripts on Windows
+    Subsystem for Linux (WSL). Paths are much easier to manage in Linux. If you
+    want to figure out how to do it on Windows, have fun!
+
+
+.. _getting_started_yaml:
+
+YAML launch configuration
+-------------------------
+
+PyroLab reads a YAML configuration file in order to start up server daemons and 
+locate nameservers. In addition to a version number for future compatability
+changes, it has four sections:
+
+* nameservers
+* daemons
+* services
+* autolaunch
+
+Below is a fully functioning configuration example continuing from our library
+example (see ``examples/library-catalog`` in the PyroLab repository) that you
+could test on your own computer. It uses only included dependencies, our toy
+library service, and runs on localhost. It automatically starts up the
+nameserver and background services and daemons when you run ``pyrolab up``
+(we'll discuss the command line interface more in the next section). 
+
+This configuration will start the nameserver on localhost, although to make it
+publicly available on the network you could simply change the key "host" from 
+"localhost" to "public". Even though everything is running locally on your
+computer, it simulates computers connecting over the network by binding to 
+specific exposed ports.
+
+An explanation of the sections follows the YAML file (here's ``config.yaml``): 
+
+.. literalinclude:: /../examples/library-catalog/config.yaml
+    :language: yaml
+
+The ``nameservers`` option defines named nameservers either for starting up a
+nameserver or for registering daemons and services with. Since the nameserver
+is just a phonebook and doesn't manage daemons or services in any way, daemons
+and services can be registered with any number of nameservers. Hence, the 
+``nameservers`` section can contain multiple nameserver definitions.
+
+The ``daemons`` section sets up a series of server daemons. Each daemon binds
+to a different port on your system and runs as its own process. All services
+hosted by a given daemon pass their communications through the one port. This
+allows you to distribute your services across multiple daemons, so if one fails
+not all are affected, or if one is particularly communication heavy, you can 
+reduce latency to other services by having them on different interfaces where
+they won't be blocked by some long running communication. Additionally, there
+are specialty daemons that support different features, like device connection
+locking. You might set up two daemons where one doesn't allow its devices to be
+locked while the other does.
+
+The ``services`` section defines services or hardware devices and the daemon
+they should be hosted on. Each service belongs to a single daemon, but can be
+registered with multiple nameservers. Since the class that defines the service
+is dynamically loaded, this is why the Python module must be visible to the
+interpreter.
+
+The name of each service you provide is the name that the daemon will use to
+register the service with the nameserver. 
+
+.. warning::
+
+    The nameserver doesn't care if you register a second service with the same
+    name! It will happily overwrite the previous entry with the "updated"
+    address. Make sure you're not reusing names with a nameserver, or you'll
+    orphan your services--they'll be undiscoverable.
+
+For the above reason, it may be useful to invent some sort of standardized
+naming scheme for naming services. For example, in our setting, each computer
+has a name (in our case, taken from the Marvel Cinematic Universe) and each
+hardware device has a name, so we might end up with an entry like:
 
 .. code-block:: yaml
-    
-    version: '1.0'
-
-    nameservers:
-    production:
-        host: localhost
-        ns_port: 9090
-        broadcast: false
-        ns_bchost: null
-        ns_bcport: 9091
-        ns_autoclean: 0.0
-        storage: memory
-
-    daemons:
-    lockable:
-        module: pyrolab.server
-        classname: LockableDaemon
-        host: localhost
-        port: 0
-        unixsocket: null
-        nathost: null
-        natport: 0
-        servertype: thread
-        nameservers:
-        - production
 
     services:
-    pymtech.thething:
-        module: pyrolab.drivers.sample
-        classname: SampleService
-        description: Sample service
+      westview.scarletwitch:
+        module: pyrolab.drivers.lasers.tsl550
+        classname: TSL550
+        parameters:
+          port: ...
+        description: Santec Laser
         instancemode: single
-        daemon: lockable
+        daemon: westview.daemon
         nameservers:
-        - production
+        - ...
+
+The ``autolaunch`` section defines servers to launch when the main PyroLab
+daemon is started. It's possible to define services but launch and quit them
+independently, similar to Docker containers. Having the configuration available
+to PyroLab makes different servers known to PyroLab but doesn't automatically
+mean they launch at startup. The ``autolaunch`` section allows us to mass
+launch servers instead of starting them up one at a time.
 
-    autolaunch:
-    nameservers: 
-    - production
-    daemons:
-    - lockable
\ No newline at end of file
+In the next section, we'll use the PyroLab CLI to actually start up our 
+services.
diff --git a/docs/getting_started/fullexample.rst b/docs/getting_started/fullexample.rst
deleted file mode 100644
index 6e89591..0000000
--- a/docs/getting_started/fullexample.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-Full Example
-============
-
-Pure Code
----------
-
-Configuration Files and CLI
----------------------------
-
diff --git a/docs/getting_started/index.rst b/docs/getting_started/index.rst
index 68b8750..f5ae77d 100644
--- a/docs/getting_started/index.rst
+++ b/docs/getting_started/index.rst
@@ -9,26 +9,20 @@ features of PyroLab. It includes plug-and-play tutorials and examples to help
 you become get hands on experience and become more familiar with the mechanisms 
 and terminology of PyroLab.
 
-For more detailed information, see the user guide.
+For more detailed information, like the nitty-gritty details and other
+advanced usage, see the `user guide <user_guide>`_.
 
 .. toctree::
     :maxdepth: 2
 
-    install
     overview
-    pyro5_essentials
+    install
     architecture
     services
     server
     nameserver
     configuration
+    cli
     client
-    fullexample
-
-.. getting_started/usage
-.. getting_started/examples
-.. getting_started/tutorials
-.. getting_started/faq
-.. getting_started/contributing
-.. getting_started/citing
-.. getting_started/license
+    summary
+    license
diff --git a/docs/getting_started/install.rst b/docs/getting_started/install.rst
index 2863178..598081c 100644
--- a/docs/getting_started/install.rst
+++ b/docs/getting_started/install.rst
@@ -11,15 +11,15 @@ users.
 Considerations
 --------------
 
-1. Is this installation serving as a client?
+1. **Is this installation serving as a client (accessing remote resources)?** 
    The PyroLab client is designed to be platform agnostic. It should work on
    any OS, regardless of the OS running the server.
-2. Is this installation serving as a server?
+2. **Is this installation serving as a server (providing services/instruments)?** 
    You need to make sure that the drivers needed to connect to your devices are
    available on the OS of choice. For example, certain drivers are only 
    available on Windows. Check the documentation for the driver before 
    installing.
-3. Is this installation acting as a nameserver?
+3. **Is this installation acting as a nameserver (server phonebook)?** 
    It is recommended you use a Linux installation for the nameserver, due to 
    the simplicity of security, opening ports, and running servers on Linux.
    However, the nameserver is the simplest mechanism in PyroLab, and can run
@@ -28,7 +28,7 @@ Considerations
 Python Version Support
 ----------------------
 
-PyroLab officially supports Python 3.7 to 3.9.
+PyroLab officially supports Python 3.7 to 3.11.
 
 
 Installing PyroLab
@@ -50,8 +50,9 @@ visit each driver's documentation for a listing of packages you may need to
 preinstall.
 
 For drivers or services that have packages installable from PyPI, a pip install
-with extras is sufficient (see below). Some drivers may require additional 
-3rd party software to be installed, see the respetive drivers' documentation.
+with extras is sufficient (see below). Some drivers may require additional 3rd
+party software to be installed (such as ThorLabs' Kinesis DLL's), so see the
+respective drivers' documentation.
 
 
 Installing with pip
@@ -59,7 +60,7 @@ Installing with pip
 
 PyroLab supports many different drivers but aims to avoid clogging up your 
 system with all the potential dependencies. Therefore, installation "extras" 
-are provided for installing the dependencies of each desired feature.
+are provided for installing the Python dependencies of each desired feature.
 
 For a basic installation, run:
 
@@ -74,18 +75,19 @@ You can install the extras (or multiple of them) using the following command:
    pip install pyrolab[feature]
    pip install pyrolab[feature1,feature2,feature3]
 
-Available extras:
+Presently available extras are:
 
-* ``tsl550``
-* ``ppcl55x``
-* ``rto``
+* ``tsl550`` (Santec TSL-550 Laser)
+* ``ppcl55x`` (Pure Photonics Lasers)
+* ``rto`` (Rohde-Schwarz Oscilloscopes)
 * ``arduino``
+* ``monitor`` (Web interface for monitoring services registered with nameservers) 
 
 
 Installing from git
 ^^^^^^^^^^^^^^^^^^^
 
-You can also install directly from git by first cloning the repository. After
+You can also install from git by first cloning the repository. After
 cloning, we still recommend using an "editable" pip install to setup all the
 paths and register the command line program. That looks like this:
 
@@ -97,7 +99,7 @@ paths and register the command line program. That looks like this:
    # Or, with some extras:
    pip install -e .[tsl550,ppcl55x,rto,arduino]
 
-If deep down inside you, you truly hate pip installing and just want to add
+If deep down in your soul, you truly hate pip installing and just want to add
 things to your PATH to install them, you can add the directory containing
 PyroLab to your PATH. Additionally, the command line program can be invoked
 by executing:
@@ -110,12 +112,12 @@ by executing:
 Data Directories
 ----------------
 
-PyroLab stores a fair amount of data on the user's computer. This includes
-configuration files that allow you to specify hosted services or instruments, 
-log files, and other data files. However, performing a pip uninstall (or, if
-you simply added the directory to your PATH, simply deleting the directory)
-will not remove the data. To see where PyroLab stores its data, run one of the
-following:
+PyroLab stores a fair amount of installation-specific data on the user's
+computer. This includes configuration files that allow you to specify hosted
+services or instruments, log files, and other data files. This data is stored
+in the same directory as your PyroLab installation (which in turn depends on
+how PyroLab was installed). If you wish, you can see where PyroLab stores its
+data by running one of the following:
 
 .. code-block:: bash
 
@@ -125,18 +127,5 @@ following:
    # If running from the source directory
    python -m pyrolab.cli --show-data-dir
 
-
-On Linux, you might get something directories like:
-
-.. code-block:: python
-
-   # If running from an interactive shell
-   >>> from pyrolab import PYROLAB_DATA_DIR, PYROLAB_CONFIG_DIR
-   >>> print(PYROLAB_DATA_DIR)
-   /home/user/.local/share/PyroLab
-   >>> print(PYROLAB_CONFIG_DIR)
-   /home/user/.config/PyroLab
-
-If you ever uninstall PyroLab and want to purge all the files it might have
-left around, delete those directories after uninstalling (but be sure to find 
-them before uninstalling, or each time PyroLab is run, it will recreate them!).
+You never really should manage these files yourself, though. But if you see 
+them show up, there's no need for concern.
diff --git a/docs/getting_started/license.rst b/docs/getting_started/license.rst
new file mode 100644
index 0000000..98bbda6
--- /dev/null
+++ b/docs/getting_started/license.rst
@@ -0,0 +1,9 @@
+.. _getting_started_license:
+
+
+License
+=======
+
+
+.. literalinclude:: /../LICENSE
+    :language: text
diff --git a/docs/getting_started/nameserver.rst b/docs/getting_started/nameserver.rst
index ba0bd6d..03c6a97 100644
--- a/docs/getting_started/nameserver.rst
+++ b/docs/getting_started/nameserver.rst
@@ -1,11 +1,17 @@
+.. _getting_started_nameserver:
+
+
 Nameserver Configuration
 ========================
 
-A nameserver configuration is necessary whether it's on the machine actually
-hosting a nameserver or on a machine that will be hosting services that will 
-be registered with the nameserver. In either case, it uses the information
-stored in a :py:class:`NameServerConfiguration` object to setup or locate 
-nameservers.
+A nameserver configuration is necessary when:
+
+* The machine is hosting a nameserver.
+* The machine is hosting services that will be registered with a nameserver. 
+
+In either case, it uses the information stored in a
+:py:class:`~pyrolab.configure.NameServerConfiguration` object to setup or
+locate nameservers.
 
 Let's take a look at the nameserver configuration class.
 
@@ -24,8 +30,8 @@ As with the server configuration, in the YAML representation of the
 nameserver's configuration, only the keys that are different from the defaults
 need to be define. Undefined fields assume their default values.
 
-We can work with NameServerConfiguration objects either through a text YAML
-file, or by using the actual Python object. Let's see how that would look:
+We can work with :py:class:`~pyrolab.configure.NameServerConfiguration`
+objects either through a text YAML file, or by using the actual Python object:
 
 .. code-block:: python
    :caption: The default nameserver configuration, interacting directly with 
@@ -45,10 +51,9 @@ file, or by using the actual Python object. Let's see how that would look:
    >>> config.host, config.ns_port
    ('public', 2022)
 
-These objects have a :py:func:`pyrolab.configure.YAMLMixin.yaml` method that 
+These objects have a :py:func:`~pyrolab.configure.YAMLMixin.yaml` method that 
 can be used to dump the configuration to a valid YAML file. It accepts a 
-parameter that allows you to dump only the values that differ from the default. 
-Let's see how that would look:
+parameter that allows you to dump only the values that differ from the default:
 
 .. code-block:: python
    :caption: The default nameserver configuration, interacting with the YAML
@@ -77,8 +82,8 @@ Let's see how that would look:
    ns_autoclean: 30.0
    storage: sql
 
-You can run a simple nameserver by simply passing one of these objects to 
-:py:func:`pyrolab.nameserver.start_ns_loop`.
+You can launch a simple nameserver by simply passing one of these objects to 
+:py:func:`pyrolab.api.start_ns_loop`.
 
 .. code-block:: python
    :caption: Starting a nameserver, continuing using the config from above.
diff --git a/docs/getting_started/overview.rst b/docs/getting_started/overview.rst
index cb07db5..26038bc 100644
--- a/docs/getting_started/overview.rst
+++ b/docs/getting_started/overview.rst
@@ -7,7 +7,7 @@ Overview
 Why another instrumentation library?
 ------------------------------------
 
-There already exists several instrumentation libraries for Python. Like any
+Several instrumentation libraries already exist for Python. Like any
 good developer, the original author sought after an existing library for his
 particular use case and, upon finding his quest unproductive, created his own
 from scratch: PyroLab.
@@ -24,10 +24,12 @@ from scratch: PyroLab.
 In this case, the specific issues that PyroLab was trying to solve were:
 
 #. PyroLab was created within an academic institution environment, where:
-#. All the computers we get from the computer support resource office are the 
-   old and crappy leftovers from computing labs.
-#. Every subgroup and setup has its own custom software for the same hardware,
-   and it's not very transferrable.
+#. Many of the computers we get from the computer support resource office are 
+   the old and crappy leftovers from computing labs.
+#. Every subgroup and experimental setup has its own custom software for the
+   same hardware, and it's not very transferrable.
+#. We have different hardware connected to differen computers, either for lack
+   of ports or cords or physical distance between hardware and computer.
 #. Sometimes our drivers need to transfer large amounts of data.
 #. The aforementioned old and crappy computers can't handle being connected
    to multiple hardware devices *and* acquire data simultaneously; they freeze,
@@ -40,6 +42,8 @@ PyroLab attempts to solve this problem by:
 
 #. Standardizing the API for controlling our hardware across all projects and
    setups.
+#. Well documented API's make transfer of knowledge and training new users much
+   easier (reading code is much harder than writing it).
 #. Allowing local machines to host specific hardware devices that can be 
    accessed remotely by any number of machines.
 #. Allowing remote machines to connect to any number of local machines, thereby
@@ -48,20 +52,22 @@ PyroLab attempts to solve this problem by:
    wholeheartedly to running one particularly resource hungry hardware device
    without having to also deal with 4 other hardware devices.
 
-In other words, PyroLab is both a distributed hardware device network manager 
-**and** a one-stop repository for hardware drivers. It allows researchers (in 
-our context, perhaps you will use this in other settings) to use a single 
+In other words, PyroLab is both a distributed hardware device network manager
+**and** a one-stop repository for hardware drivers. It allows researchers (in
+our context, perhaps you will use this in other settings) to use a single
 language, Python, a very popular language for prototyping and scientific
 computing, to control a whole variety of devices from a script-based approach.
-In an academic research lab setting, it allows users to "bring your own 
+In an academic research lab setting, it allows users to "bring your own
 computer" and access experiments and hardware hosted from any number of "host"
 computers. No more cable management issues, no more arguing over "who has the
-laser today," and no more issues with slow computers that can only manage
-one frame per second from the microscope while running Windows 10.
+laser today" because remoting into the computer kicks off everyone else and is
+a general pain to switch back and forth, and no more issues with slow computers
+that can only manage one frame per second from the microscope while running
+Windows 10.
 
 Yes, it was written tailored to our use case. But we tried to keep it general
 enough so that you, too, will find it so invaluable that you'll want to use it,
 develop it, and contribute new drivers to it.
 
-So, welcome to PyroLab. Read on to learn about the "philosophy" of the
-framework and how to use it properly.
+So, welcome to PyroLab. Read on through our getting started guide to learn
+about the "philosophy" of the framework and how to use it properly.
diff --git a/docs/getting_started/pyro5_essentials.rst b/docs/getting_started/pyro5_essentials.rst
deleted file mode 100644
index 9079705..0000000
--- a/docs/getting_started/pyro5_essentials.rst
+++ /dev/null
@@ -1,36 +0,0 @@
-.. _getting_started_pyro5:
-
-
-Pyro5 Essentials
-================
-
-On the shoulders of giants...
------------------------------
-
-Yes, we created PyroLab from scratch. No, we did not create the network
-communication library from scratch.
-
-`Pyro5`_ is essentially a pure Python Remote Procedure Call (`RPC`_) library, 
-licensed under the MIT license.
-
-.. _RPC: https://en.wikipedia.org/wiki/Remote_procedure_call
-.. _Pyro5: http://pyro5.readthedocs.org/en/latest/
-
-
-There are three important concepts from Pyro5 that are important in PyroLab:
-
-#. Services
-#. Servers
-#. Nameservers
-
-PyroLab "services" are objects that are remotely accessible. These might be
-representative of hardware devices, long running calculations, or perhaps an
-object providing an API for interacting with filesystems on a remote machine;
-it all depends on what methods your services provide. 
-
-The program that hosts your services is called a "server" and runs daemon
-processes that listen for requests. Services are registered with a daemon, and
-a single machine could host multiple daemons. There are different run
-configurations for daemons, which are described in detail in the `user guide
-<user_guide_servers>`_.
-
diff --git a/docs/getting_started/server.rst b/docs/getting_started/server.rst
index a93af82..ac28ab1 100644
--- a/docs/getting_started/server.rst
+++ b/docs/getting_started/server.rst
@@ -1,17 +1,21 @@
-Server (and Service) Configuration
-==================================
+.. _getting_started_daemon:
+
+
+Daemon Configuration
+====================
 
 While servers, often referred to as daemons in the PyroLab docs, and services
 registered through them can be thought about and developed separately, in
-deployment, their configurations are inextricably linked. Hence this section
-will again reference services, in addition to servers.
+deployment, their configurations are linked. Hence this section will again
+reference services, in addition to servers.
 
 Server daemons and service configurations are only important for host machines
 providing services to a PyroLab network. For a client simply connecting to the
 network to utilize available PyroLab services, there is no need to configure
 servers or services.
 
-Let's take a look at the server configuration class, ``DaemonConfiguration``.
+Let's take a look at the server configuration class,
+:py:class:`~pyrolab.configure.DaemonConfiguration`.
 
 ----
 
@@ -32,7 +36,8 @@ model that loads the file will automatically populate the missing fields with
 the default values.
 
 We can work with DaemonConfiguration objects either through a text YAML file,
-or by constructing the actual Python object. Let's see how that would look:
+or by constructing the actual Python object. For now, we'll use Python
+directly:
 
 .. code-block:: python
    :caption: The default server configuration, interacting directly with 
@@ -52,10 +57,9 @@ or by constructing the actual Python object. Let's see how that would look:
    >>> config.module, config.classname, config.host, config.port
    ('pyrolab.daemon', 'LockableDaemon', 'public', 2022)
 
-These objects have a :py:func:`pyrolab.configure.YAMLMixin.yaml` method that 
-can be used to dump the configuration to a valid YAML file. It can accept a 
-parameter that allows you to dump only the values that differ from the default. 
-Let's see how that would look:
+These objects have a :py:func:`~pyrolab.configure.YAMLMixin.yaml` method that
+can be used to dump the configuration to a YAML file. It accepts a parameter
+that allows you to dump only the values that differ from the default:
 
 .. code-block:: python
    :caption: The default server configuration, interacting with the YAML
@@ -71,7 +75,7 @@ Let's see how that would look:
    unixsocket: null
    nathost: null
    natport: 0
-   servertype: thre
+   servertype: thread
 
 .. code-block:: python
    :caption: Modified server configuration, interacting with the YAML 
@@ -83,24 +87,3 @@ Let's see how that would look:
    classname: LockableDaemon
    host: public
    port: 2022
-
-.. You can run a simple server by simply passing one of these objects to 
-.. :py:func:`pyrolab.nameserver.start_ns_loop`.
-
-.. .. code-block:: python
-..    :caption: Starting a nameserver, continuing using the config from above.
-
-..    >>> from pyrolab.nameserver import start_ns_loop
-..    >>> start_ns_loop(config)
-
-.. So a complete script for starting up a nameserver that is visible to other 
-.. machines on the network would look like this:
-
-.. .. code-block:: python
-..    :caption: Simplest, complete script for running a nameserver visible to 
-..       other machines on the network.
-
-..    >>> from pyrolab.configure import NameServerConfiguration
-..    >>> from pyrolab.nameserver import start_ns_loop
-..    >>> config = NameServerConfiguration(host="public")
-..    >>> start_ns_loop(config)
diff --git a/docs/getting_started/services.rst b/docs/getting_started/services.rst
index 5653f20..9b0f325 100644
--- a/docs/getting_started/services.rst
+++ b/docs/getting_started/services.rst
@@ -1,22 +1,28 @@
 .. _getting_started_services:
 
 
-Services
-========
+Writing Services
+================
 
-In this example, we'll write a simple service that performs a potential "real-
-world" application in a very bad, overly simplistic way. But the goal is to
-learn the API of services, not actually implement a good service.
+In this example, we'll write a simple service that performs a potential
+"real-world" application in a very bad, overly simplistic way. But the goal is
+to learn the API of services, not actually implement a good service.
 
 
 The Library Problem
 -------------------
 
-For this demonstration, let's assume there's a service that acts as a
-library registry. It'll maintain a list of all available books and their
-quantities. We won't use a real database; we'll just an in-memory Python
-dictionary. Every time your service is shutdown and restarted, you'll lose
-the state of the "database." Oh well. Let's get started!
+Let's assume there's a service that acts as a library catalog. It'll maintain
+a list of all available books and their quantities. We won't use a real
+database; we'll just an in-memory Python dictionary. Every time your service is
+shutdown and restarted, you'll lose the state of the "database." Oh well. Let's
+get started!
+
+.. figure:: /_static/images/ohnoanyway.png
+   :width: 300
+   :alt: Oh no! Anyway... (Top Gear)
+   
+   *Infinite books, if the power keeps going out.*
 
 This example can also be found in the ``examples/library-catalog`` directory in
 the PyroLab repository.
@@ -34,7 +40,8 @@ Requirements:
 * Add new books to the registry
 * Remove books from the registry
 
-Let's write a simple Python class that can do this:
+Let's write a simple Python class that can do this. We'll name the module
+``libservice.py``:
 
 .. code-block:: python
    
@@ -76,7 +83,7 @@ Let's write a simple Python class that can do this:
                 The number of copies to check out (default 1).
             """
             if title not in self.catalog:
-                raise ValueError(f"Cannot checkout: Book '{title}' not found.")
+                raise LookupError(f"Cannot checkout: Book '{title}' not found.")
             if self.catalog[title] < quantity:
                 raise ValueError(f"Cannot checkout: Not enough copies of {title}.")
             self.catalog[title] -= quantity
@@ -93,7 +100,7 @@ Let's write a simple Python class that can do this:
                 The number of copies to check in (default 1).
             """
             if title not in self.catalog:
-                raise ValueError(f"Cannot checkin: Book '{title}' not found.")
+                raise LookupError(f"Cannot checkin: Book '{title}' not found.")
             self.catalog[title] += quantity
 
         def add_book(self, title: str, quantity: int = 1) -> None:
@@ -119,9 +126,14 @@ Let's write a simple Python class that can do this:
                 The title of the book to remove.
             """
             if title not in self.catalog:
-                raise ValueError(f"Cannot remove: Book '{title}' not found.")
+                raise LookupError(f"Cannot remove: Book '{title}' not found.")
             del self.catalog[title]
 
+.. warning::
+
+    When naming your modules, take care to make sure no module names overlap
+    with submodules of PyroLab, or the dynamic module loader may mistakenly
+    load the wrong file!
    
 
 Pyro-ing it
@@ -131,31 +143,51 @@ To make a class Pyro compatible, there's a few things you should note:
 
 * Only accept and return standard Python builtin types. These are the only 
   things the Pyro5 serializer knows how to handle and pass across a network.
+  (Even numpy arrays should be converted to a regular list.)
 * You can expose certain methods and properties from the class, or you can
   expose an entire class and all its PUBLIC methods and properties will be
   exposed.
 * Private methods and properties CANNOT be exposed (methods that start with an
-  underscore). You CAN expose dunder methods (special methods that start and
+  underscore). You CAN expose "dunder" methods (special methods that start and
   end with a double underscore, e.g. ``__len__``).
-
-You need to decorate your class with ``@expose`` to make it Pyro-compatible.
-
-PyroLab also provides a :py:class:`Service` class that should be used as a base
-object that your service inherits from. While the base class won't expose any
-of your methods for you, it does add some functionality that PyroLab can use in
-the background, as well as some convenience methods for you (such as a function
-for setting the instance mode behavior of the class, see the `user guide
-<user_guide_servers>`_ for advanced usage).
+* If you're going to use exceptions, only use the `built-in Python exceptions
+  <https://docs.python.org/3/library/exceptions.html>`_. These are the only
+  ones that PyroLab knows how to serialize.
+
+You need to decorate your class (or just the specific methods you want to make
+publicly available) with ``@``:py:func:`~pyrolab.api.expose` to make it
+Pyro-compatible.
+
+PyroLab also provides a :py:class:`~pyrolab.service.Service` class that should
+be used as a base object that your service inherits from. While the base class
+won't expose any of your methods for you, it does add some functionality that
+PyroLab can use in the background, as well as some convenience methods for you,
+including:
+
+* A :py:meth:`~pyrolab.service.Service.ping` method, so you can test the
+  ability to connect with it quickly (the web monitor also uses this to check
+  on services' availability, if you choose to set up the server).
+* A function for setting the instance mode behavior of the class (see the `user
+  guide <user_guide_instance_mode>`_ for advanced usage).
 
 Our class from above is already safe in the sense that it only accepts and
 returns regular Python types. Now, we can simply change its base and add the
-``@expose`` decorator so that a PyroLab server could host it. Additionally,
-since we don't want to create a new object for every incoming Proxy connection,
-but one object for all incoming connections, we'll use the ``@behavior``
-decorator to set the instance mode to "single". (If we were to create a new
-object for every Proxy connection, our libraries wouldn't actually be in sync
-with each other.) Again, see the `user guide <user_guide_servers>`_ for more
-information on behaviors.
+``@``:py:func:`~pyrolab.api.expose` decorator so that a PyroLab server could
+host it. Additionally, since we don't want to create a new object for every
+incoming Proxy connection, but one object for all incoming connections, we'll
+use the ``@``:py:func:`~pyrolab.api.behavior` decorator to set the instance
+mode to "single". (If we were to create a new object for every Proxy
+connection, our libraries wouldn't actually be in sync with each other--every
+new connection would get the default initialization catalog.) This would be
+especially important, for example, if you're working with physical hardware, of
+which there only exists one and creating a new object for every connection
+doesn't map to physical reality (unless we live in a multiverse where every
+time you create a new connection with PyroLab it also duplicates a new piece of
+that hardware in your lab). Again, see the `user guide
+<user_guide_instance_mode>`_ for more information on behaviors.
+
+Let's modify our class from above by adding the decorators and inheriting from 
+the :py:class:`~pyrolab.service.Service` class:
 
 .. code-block:: python
 
@@ -166,5 +198,5 @@ information on behaviors.
     class LibraryCatalog(Service):
         ...
 
-We now have a PyroLab service class! Continue reading to learn how to host it
-for remote clients.
+We now have a new PyroLab service! In the next section, we'll learn how to host
+it to allow remote clients to access it.
diff --git a/docs/getting_started/summary.rst b/docs/getting_started/summary.rst
new file mode 100644
index 0000000..bfc7544
--- /dev/null
+++ b/docs/getting_started/summary.rst
@@ -0,0 +1,25 @@
+.. _getting_started_summary:
+
+
+Summary
+=======
+
+Congratulations! You've successfully: 
+
+* set up a PyroLab network on your own computer, 
+* launched the PyroLab monitoring daemon, 
+* registered a service, 
+* connected to that service, and 
+* interacted with it using a Proxy client!
+
+The User Guide contains some advanced tips and tricks, but for the general
+user of PyroLab, you now know enough to set up your own functioning network
+and use the included drivers for controlling hardware. 
+
+As you create your own generic drivers, we'd love for you to share them with us
+to be included in core PyroLab. See the `Contributing
+<development_contributing>`_ page for more information.
+
+We'd love to get your feedback to make PyroLab better. Issues and feature
+requests can be made through our `Issue Tracker
+<https://github.com/BYUCamachoLab/pyrolab/issues>`_.
diff --git a/docs/index.rst b/docs/index.rst
index e7936c8..c074e83 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,8 +1,3 @@
-.. PyroLab documentation master file, created by
-   sphinx-quickstart on Wed Nov 10 19:21:42 2021.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
 =====================
 PyroLab Documentation
 =====================
@@ -18,84 +13,102 @@ A framework for using remote lab instruments as local resources, built on `Pyro5
 Key Features
 ------------
 
-- Free and open-source software provided under the GPLv3+ License
+- Free and open-source software provided under the GPLv3+ License.
 - Compatible with Python 3.7+.
 - Cross-platform: runs on Windows, MacOS, and Linux.
 - Included device drivers for several scopes and ThorLabs motion controllers.
 - Remotely controlled devices can be accessed through a `Pyro5`_ proxy.
 
-.. panels::
-    :card: + intro-card text-center
-    :column: col-lg-6 col-md-6 col-sm-6 col-xs-12 d-flex
+If you use PyroLab, we'd appreciate you citing our work:
+
+
+.. tab-set::
+
+    .. tab-item:: Text
+
+        Coming soon
+
+    .. tab-item:: BibTeX
 
-    ---
-    :img-top: _static/images/index_getting_started.svg
+        .. code-block:: 
 
-    Getting started
-    ^^^^^^^^^^^^^^^
+            Coming soon
 
-    New to *PyroLab*? Check out the getting started guides. They contain an
-    introduction to *PyroLab's* main concepts and a step-by-step tutorial for
-    beginners.
 
-    +++
+.. grid:: 1 2 2 2
+    :gutter: 3
 
-    .. link-button:: getting_started
-            :type: ref
-            :text: To the getting started guides
-            :classes: btn-block btn-secondary stretched-link
+    .. grid-item-card::  Getting Started
 
-    ---
-    :img-top: _static/images/index_user_guide.svg
+        .. image:: /_static/images/index_getting_started.svg
+            :align: center
+            :height: 100
+            :width: 100
 
-    User guide
-    ^^^^^^^^^^
+        New to *PyroLab*? Check out the getting started guides. They contain an
+        introduction to *PyroLab's* main concepts and a step-by-step tutorial for
+        beginners.
 
-    The user guide provides in-depth information on the key concepts of PyroLab
-    with useful background information and explanation.
+        .. button-ref:: getting_started
+            :color: primary
+            :shadow:
+            :expand:
 
-    +++
+            To the getting started guides
 
-    .. link-button:: user_guide
-            :type: ref
-            :text: To the user guide
-            :classes: btn-block btn-secondary stretched-link
+    .. grid-item-card::  User Guide
 
-    ---
-    :img-top: _static/images/index_api.svg
+        .. image:: /_static/images/index_user_guide.svg
+            :align: center
+            :height: 100
+            :width: 100
 
-    API reference
-    ^^^^^^^^^^^^^
+        The user guide provides in-depth information on the key concepts of PyroLab
+        with useful background information and explanation.
 
-    The reference guide contains a detailed description of the PyroLab API. The
-    reference describes how the methods work and which parameters can be used.
-    It also contains a listing of provided hardware drivers. It assumes that
-    you have an understanding of the key concepts.
+        .. button-ref:: user_guide
+            :color: primary
+            :shadow:
+            :expand:
 
-    +++
+            To the user guide
 
-    .. link-button:: api_reference
-            :type: ref
-            :text: To the reference guide
-            :classes: btn-block btn-secondary stretched-link
+    .. grid-item-card::  API Reference
 
-    ---
-    :img-top: _static/images/index_contribute.svg
+        .. image:: /_static/images/index_api.svg
+            :align: center
+            :height: 100
+            :width: 100
 
-    Developer guide
-    ^^^^^^^^^^^^^^^
+        The reference guide contains a detailed description of the PyroLab API. The
+        reference describes how the methods work and which parameters can be used.
+        It also contains a listing of provided hardware drivers. It assumes that
+        you have an understanding of the key concepts.
 
-    Want to add your own device driver? Saw a typo in the documentation? Want to improve
-    existing functionalities? The contributing guidelines will guide
-    you through the process of improving PyroLab.
+        .. button-ref:: api_reference
+            :color: primary
+            :shadow:
+            :expand:
 
-    +++
+            To the reference guide
 
-    .. link-button:: development
-            :type: ref
-            :text: To the development guide
-            :classes: btn-block btn-secondary stretched-link
+    .. grid-item-card::  Developer Guide
 
+        .. image:: /_static/images/index_contribute.svg
+            :align: center
+            :height: 100
+            :width: 100
+
+        Want to add your own device driver? Saw a typo in the documentation? Want to improve
+        existing functionalities? The contributing guidelines will guide
+        you through the process of improving PyroLab.
+
+        .. button-ref:: development
+            :color: primary
+            :shadow:
+            :expand:
+
+            To the development guide
 
 
 .. toctree::
@@ -106,11 +119,3 @@ Key Features
    user_guide/index
    reference/index
    development/index
-
-
-.. Indices and tables
-.. ------------------
-
-.. * :ref:`genindex`
-.. * :ref:`modindex`
-.. * :ref:`search`
diff --git a/docs/reference/analysis.rst b/docs/reference/analysis.rst
deleted file mode 100644
index ff14365..0000000
--- a/docs/reference/analysis.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-.. currentmodule:: pyrolab
-.. autosummary::
-   :toctree: api/
-
-   analysis
\ No newline at end of file
diff --git a/docs/reference/api.rst b/docs/reference/api.rst
index e4c119b..fed446c 100644
--- a/docs/reference/api.rst
+++ b/docs/reference/api.rst
@@ -1,43 +1,76 @@
-.. currentmodule:: pyrolab.api
+.. .. currentmodule:: pyrolab.api
 
 
 pyrolab.api
 ===========
 
 Importing ``pyrolab.api`` or any object from it has one (useful) side effect.
-If a configuration file exists (has been set through the CLI), it will load
-and set PyroLab to use the first listed nameserver configuration. Functions
-such as :py:func:`pyrolab.nameserver.locate_ns` will find the nameserver without needing to
-pass any arguments, such as the hostname of the nameserver.
+If a configuration file exists (has been set through the CLI), it will load and
+set PyroLab to use the first listed nameserver configuration. Functions such as
+:py:func:`pyrolab.api.locate_ns` will therefore find the nameserver
+without needing to pass any arguments, such as the hostname of the nameserver.
 
 
 Services
 --------
 
-* :py:func:`pyrolab.server.expose`
-* :py:func:`pyrolab.server.behavior`
-* :py:func:`pyrolab.server.oneway`
+Wrappers
+~~~~~~~~
+
+.. autofunction:: pyrolab.api.expose
+
+.. autofunction:: pyrolab.api.behavior
+
+.. autofunction:: pyrolab.api.oneway
+
+Classes
+~~~~~~~
 
 * :py:class:`pyrolab.service.Service`
-* :py:class:`pyrolab.api.Proxy`
 
-Server
-------
 
-* :py:func:`pyrolab.api.locate_ns`
-* :py:func:`pyrolab.nameserver.start_ns`
-* :py:func:`pyrolab.nameserver.start_ns_loop`
-* :py:func:`pyrolab.server.serve`
+Server Daemon
+-------------
+
+Functions
+~~~~~~~~~
+
+.. autofunction:: pyrolab.api.locate_ns
+
+.. autofunction:: pyrolab.api.start_ns
+
+.. autofunction:: pyrolab.api.start_ns_loop
+
+.. autofunction:: pyrolab.api.serve
+
+Classes
+~~~~~~~
 
 * :py:class:`pyrolab.server.Daemon`
 * :py:class:`pyrolab.server.LockableDaemon`
 
+
+Client
+------
+
+Classes
+~~~~~~~
+
+.. autoclass:: pyrolab.api.Proxy
+
+
 Configuration
 -------------
 
+Functions
+~~~~~~~~~
+
 * :py:func:`pyrolab.configure.update_config`
 * :py:func:`pyrolab.configure.reset_config`
 
+Classes
+~~~~~~~
+
 * :py:class:`pyrolab.configure.NameServerConfiguration`
 * :py:class:`pyrolab.configure.DaemonConfiguration`
 * :py:class:`pyrolab.configure.ServiceConfiguration`
diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst
index b7ac2dd..3fe8bd2 100644
--- a/docs/reference/cli.rst
+++ b/docs/reference/cli.rst
@@ -27,15 +27,3 @@ pyrolab.cli
      start   Start a nameserver or daemon (and its services).
      stop    Stop a nameserver or daemon (and its services).
      up      Start the background PyroLab daemon.
-
-
-Where did my instances go?
---------------------------
-
-You should be able to find all background pyrolab daemons by running (on Unix):
-
-.. code-block:: bash
-
-   ps aux | grep pyrolabd
-
-Output of ``pyrolab --help``:
\ No newline at end of file
diff --git a/docs/reference/drivers.rst b/docs/reference/drivers.rst
index f6552ac..6d482c9 100644
--- a/docs/reference/drivers.rst
+++ b/docs/reference/drivers.rst
@@ -101,7 +101,7 @@ become available to the :py:class:`Proxy`. So, a list of mixins is maintained
 here, as these functions are not documented with each driver but may be 
 available to them.
 
-* :py:class:`pyrolab.daemon.Lockable`
+* :py:class:`~pyrolab.server.Lockable`
 
 Debugging/Sample Services
 -------------------------
diff --git a/docs/reference/errors.rst b/docs/reference/errors.rst
deleted file mode 100644
index 15976b9..0000000
--- a/docs/reference/errors.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-.. currentmodule:: pyrolab
-.. autosummary::
-   :toctree: api/
-
-   errors
\ No newline at end of file
diff --git a/docs/reference/index.rst b/docs/reference/index.rst
index 4326293..7b7d7aa 100644
--- a/docs/reference/index.rst
+++ b/docs/reference/index.rst
@@ -16,7 +16,6 @@ elsewhere, or for machines using PyroLab drivers locally.
    :maxdepth: 2
 
    api
-   analysis
    drivers
 
 Deployment API
@@ -30,7 +29,6 @@ For configuring and deploying PyroLab instances.
    cli
    configure
    server
-   errors
    nameserver
    service
 
diff --git a/docs/requirements.txt b/docs/requirements.txt
index c72f6ed..b525a49 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,10 +1,12 @@
-sphinx==4.3.0
-pydata-sphinx-theme==0.7.2
-numpydoc==1.1.0
-sphinx-panels==0.6.0
-appdirs==1.4.4
+sphinx==5.3.0
+pydata-sphinx-theme==0.12.0
+numpydoc==1.5.0
+numpy==1.24.2
+scipy==1.10.1
+sphinx_design==0.3.0
+sphinx-copybutton==0.5.1
+sphinx-togglebutton==0.3.2
+linkify-it-py==2.0.0
+pydantic==1.10.5
 pyyaml==6.0
-pydantic==1.9.0
-numpy==1.22.0
-scipy==1.7.3
-Pyro5==5.13.1
\ No newline at end of file
+Pyro5==5.14
\ No newline at end of file
diff --git a/docs/user_guide/clients.rst b/docs/user_guide/clients.rst
index 0f73925..4117149 100644
--- a/docs/user_guide/clients.rst
+++ b/docs/user_guide/clients.rst
@@ -2,3 +2,30 @@
 Clients
 =======
 
+Since clients are simply Proxy objects that receive a list of available
+functions when they first connect, they are PyroLab version-independent.
+
+Note that it's always a good idea to use proxies in a context-manager to clean
+up the connection:
+
+.. code-block:: python
+
+    with locate_ns(host="...") as ns:
+        uri = ns.lookup("...")
+
+    with Proxy(uri) as remote_object:
+        remote_object.useful_method()
+
+Assuming drivers and services have been written correctly, they only raise
+builtin Python exceptions. You can also catch different levels of exceptions if
+you know what exceptions an object can raise. Therefore you can use them in
+try-except blocks:
+
+.. code-block:: python
+
+    try:
+        remote_object.useful_method()
+    except ConnectionError as e:
+        # handle this kind of error
+    except Exception as e:
+        # handle the generic exception
diff --git a/docs/user_guide/configuration.rst b/docs/user_guide/configuration.rst
index d67455f..712b774 100644
--- a/docs/user_guide/configuration.rst
+++ b/docs/user_guide/configuration.rst
@@ -1,5 +1,9 @@
+Configuration
+=============
+
+
 Nameservers
-===========
+-----------
 
 Note that, regardless of whether it is present in the file, the default
 configuration is always available. It has the following settings:
@@ -7,7 +11,7 @@ configuration is always available. It has the following settings:
 .. code-block:: yaml
     
     nameservers:
-        - default:
+        default:
             host: localhost
             broadcast: false
             ns_port: 9090
@@ -23,6 +27,7 @@ careful and make sure you actually want to change the default.
 
 
 Daemons
+-------
 
 Note that, regardless of whether it is present in the file, the default
 configuration is always available. It has the following settings:
@@ -30,7 +35,7 @@ configuration is always available. It has the following settings:
 .. code-block:: yaml
     
     daemons:
-        - default:
+        default:
             classname: Daemon
             host: localhost
             servertype: thread
@@ -40,6 +45,9 @@ overridden. Many places in PyroLab read the default configuration, so be
 careful and make sure you actually want to change the default.
 
 
+Services
+--------
+
+.. todo::
 
-You can also specify "auto n" where n is an integer to generate a
-random name of n words for any kind of object.
+    Coming soon.
diff --git a/docs/user_guide/deployment.rst b/docs/user_guide/deployment.rst
index 562284b..6efaa46 100644
--- a/docs/user_guide/deployment.rst
+++ b/docs/user_guide/deployment.rst
@@ -81,15 +81,32 @@ Using the Startup folder
 6. Click "Next".
 7. Type a name for the shortcut (such as "pyrolabd"), and click "Finish".
 
-macOS
-^^^^^
+Linux and macOS
+^^^^^^^^^^^^^^^
 
-Your mac os crontab as well.
+We recommend using crontab on MacOS as well. You'll want to write an executable
+script that activates whatever necessary environments you have, if PyroLab
+isn't installed in the global Python interpreter (which, for the record is
+usually a bad idea). Here's an example ``autolaunch.sh``:
 
-Linux
-^^^^^
+.. code-block:: bash
+
+   source env/bin/activate 
+   pyrolab up
+
+Then you'll register it with cron to run it, for example, on startup. To launch
+cron:
+
+.. code-block:: bash
+
+   sudo crontab -e
+
+Then enter a line similar to the one below:
+
+.. code-block:: bash
+
+   @reboot /path/to/autolaunch.sh
 
-The easiest way to run pyrolabd is by running a cron job set to run at startup.
 
 Starting the Daemon
 -------------------
@@ -101,6 +118,7 @@ on some localhost port, and the CLI simply sends commands to the daemon. The
 CLI is not the instance of PyroLab that is actually managing what would be 
 considered the "PyroLab program."
 
+
 Launching from the command line
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -115,6 +133,7 @@ automatically load that configuration. (This does not imply that all the
 listed services will be started; for that to happen, they must be listed in
 the ``autolaunch`` section of the configuration file.)
 
+
 Force launching the daemon
 ^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -142,3 +161,20 @@ perhaps in the terminal). To force start a new daemon, simply run:
 .. code-block:: python
 
    pyrolab up --force
+
+You might consider making sure that your process has actually died before 
+forcing a new one to launch. On Windows, you can simply reference the Task
+Manager. On \*nix based systems, you can follow the instructions below.
+
+
+Where did my instances go?
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+PyroLab runs a daemon in the background that monitor and spawns all daemon
+servers as their own process. If you're concerned the master daemon was
+orphaned, you should be able to find all background pyrolab daemons by running
+(on \*nix):
+
+.. code-block:: bash
+
+   ps aux | grep pyrolabd
diff --git a/docs/user_guide/index.rst b/docs/user_guide/index.rst
index 2527c5e..a6a894d 100644
--- a/docs/user_guide/index.rst
+++ b/docs/user_guide/index.rst
@@ -8,19 +8,20 @@ User Guide
    :maxdepth: 2
    :hidden:
 
-   pyro5_concepts
    nameservers
    servers
-   clients
    services
+   clients
    configuration
    deployment
    libraries
+   webmonitor
    
 A few notes on simply starting PyroLab. When launched from the terminal,
 PyroLab reads a few environment variables for convenient configuration.
 
 .. todo::
+   
    Link to where the data directories is discussed, for appdirs.
 
 ``PYROLAB_LOGFILE``
@@ -35,7 +36,3 @@ This can be very helpful when debugging to see how the program is progressing,
 and when the program is being too verbose, it's easy to silence. Note that
 including debugging will also include log statements from all of PyroLab's
 dependencies.
-
-Nameservers, daemons, or services (known as PyroLab "entities") can be
-configured in a few ways. The most common (and easiest) way is to use a
-PyroLab configuration YAML file.
diff --git a/docs/user_guide/nameservers.rst b/docs/user_guide/nameservers.rst
index 8d8ce2d..e529016 100644
--- a/docs/user_guide/nameservers.rst
+++ b/docs/user_guide/nameservers.rst
@@ -36,10 +36,11 @@ storage_type
     Specify the storage mechanism to use. You have several options:
 
     * ``memory``: fast, volatile in-memory database. This is the default.
-    * ``dbm``: dbm-style persistent database table. Filename must be provided
-      (see the ``storage_file`` option). This storage type does not support metadata.
-    * ``sql``: sqlite persistent database. Filename must be provided 
-      (see the ``storage_file`` option).
+    * ``dbm``: persistent database using dbm. Optionally provide the filename 
+      to use (ignore for PyroLab to create automatically). This storage type 
+      does not support metadata.
+    * ``sql``: persistent database using sqlite. Optionally provide the 
+      filename to use (ignore for PyroLab to create automatically).
 storage_file
     The filename (full path) to use for the storage if the mechanism is not 
     ``memory``. Default is an automatic program data directory, so does not 
@@ -55,21 +56,21 @@ this:
 .. code-block:: yaml
 
     nameservers:
-        - default:
+        default:
             host: localhost
             ns_port: 9090
             ns_bchost: localhost
             ns_bcport: 9091
             ns_autoclean: 0.0
             storage_type: memory
-        - production:
+        production:
             host: camacholab.ee.byu.edu
             ns_port: 9090
             ns_bchost: null
             ns_bcport: 9091
             ns_autoclean: 15.0
             storage_type: sql
-        - development:
+        development:
             host: localhost
             ns_port: 9090
 
@@ -79,13 +80,14 @@ Launching a nameserver
 
 Using the command line interface
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-If PyroLab is installed in the system path and a configuration file exists 
-(either internally within PyroLab or at a location you specify), you can launch 
-a nameserver with the following command:
+If PyroLab is installed in the system path and a configuration file exists, you
+can import the configuration and launch a nameserver with the following
+commands:
 
 .. code-block:: bash
 
-    $ pyrolab nameserver -c ./ns_cfg.yml -l production
+    $ pyrolab config update ./ns_cfg.yml
+    $ pyrolab start nameserver production
 
 
 Using a script
@@ -112,6 +114,10 @@ sometimes the software (or the hardware) may crash without the chance to
 handle housekeeping calls. This is why the nameserver can periodically ping
 all known services to check if they are still alive.
 
+The "autoclean" configuration value is a polling period, in seconds, to check
+in with registered services. If it's set to ``0.0``, autoclean is turned off.
+Any other value indicates the frequency with which to check connectivity.
+
 Additionally, because services can come back online with the same name and
 notify the nameserver of their availability, the nameserver will not block
 registrations of new services with the same name. Be sure when you're writing
@@ -123,7 +129,7 @@ none of the names you're using are already used by the register, or you may
 Free connections to the nameserver quickly
 ------------------------------------------
 
-From the `Pyro5 docs <https://pyro5.readthedocs.io/en/latest/nameserver.html#free-connections-to-the-ns-quickly>`_`:
+From the `Pyro5 docs <https://pyro5.readthedocs.io/en/latest/nameserver.html#free-connections-to-the-ns-quickly>`_:
 
     By default the Name server uses a Pyro socket server based on whatever
     configuration is the default. Usually that will be a threadpool based
diff --git a/docs/user_guide/pyro5_concepts.rst b/docs/user_guide/pyro5_concepts.rst
deleted file mode 100644
index 245bb24..0000000
--- a/docs/user_guide/pyro5_concepts.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-
-The Briefest Introduction to Pyro5
-==================================
\ No newline at end of file
diff --git a/docs/user_guide/servers.rst b/docs/user_guide/servers.rst
index c9dc621..ca81308 100644
--- a/docs/user_guide/servers.rst
+++ b/docs/user_guide/servers.rst
@@ -30,9 +30,9 @@ having to run lengthy Python scripts. Failures that result in the termination
 of PyroLab processes, such as power outages, are easily recovered from; since
 the connected instruments are known to the program, it can reload them
 automatically upon startup, registering them again with the appropriate
-`nameserver <user_guide_nameservers>`_ (see how to configure your machine to
-automatically start PyroLab on startup at `Deployment
-<user_guide_deployment>`_).
+:ref:`nameserver <user_guide_nameservers>` (see how to configure your machine
+to automatically start PyroLab on startup at :ref:`Deployment
+<user_guide_deployment>`).
 
 Another reason to split services up between different daemons is because any
 given daemon can run in one of two ``servertype`` modes.
@@ -88,12 +88,11 @@ simultaneously. But if you have multiple services on a multiplexed server,
 calls will execute one after the other, and will wait for all previous calls
 to execute, too.
 
-**Thought experiment**
-   
-   Suppose you have 
-
 Be aware when you define your daemons that you don't define too many; each 
 will be started as a separate process, so if you're running a lot of daemons,
 they won't actually be running simultaneously; your computer doesn't have
-enough processors. Most of the time, you can just put all the services on a
-threaded server.
\ No newline at end of file
+enough processors. *Most of the time*, you can just put all the services on a
+threaded server. It's in the situation of a long-running function that blocks
+where you might consider separating services between daemons so that you don't
+lose the ability to contact other services or functions while awaiting the
+return value of a blocking function.
diff --git a/docs/user_guide/services.rst b/docs/user_guide/services.rst
index a086397..e5ed5f2 100644
--- a/docs/user_guide/services.rst
+++ b/docs/user_guide/services.rst
@@ -1,3 +1,5 @@
+.. _user_guide_services:
+
 
 Services
 ========
@@ -14,18 +16,43 @@ that can easily be transmitted and reinstantiated from the dictionary.
 Autoconnecting Services
 -----------------------
 
-Some services have a "connect" function. Typically, this is for hardware 
-devices that need to be connected to the computer. This function should be
-called before any other functions are called.
+Some services have a "connect" function. Typically, this is for hardware
+devices that need to be connected to the computer using a serial connection or
+some other protocol. Therefore, this function will need to be called before any
+other functions can be called.
+
+.. note::
+
+    Something to consider when writing drivers is that in the connect()
+    function, you should do some checking to see if it's already connected
+    because if multiple connections request access, you want to be able to 
+    handle them all gracefully instead of giving false unavailable errors.
 
 In some cases, those connection parameters are computer-specific and not known
-by the end user. This is because the drivers are written to be generic and not
-necessarily used with remote PyroLab connections (i.e., they can be imported
-and used locally in a script). Because of this, PyroLab services can be 
-configured with autoconnect parameters, allowing remote clients to simply call
-"autoconnect()" to connect to the service (which in turn simply forwards the
-call to "connect()" with the appropriate parameters, never exposing them to
-the client).
+by the end user. This is because the drivers are written to be generic and
+usableeven without remote PyroLab connections (i.e., they can be imported and
+used locally in a script). Because of this, PyroLab services can be configured
+with autoconnect parameters, allowing remote clients who do not know the local
+connection details to simply call "autoconnect()" to connect to the service
+(which in turn simply forwards the call to "connect()" with the appropriate
+parameters, never exposing them to the client). They are specified in the 
+YAML configuration file for the computing acting as a server. Here's an example
+for a laser that takes a single parameter different from the default, the
+COM port on which to connect:
+
+.. code-block:: yaml
+
+    services:
+        westview.scarletwitch:
+            module: pyrolab.drivers.lasers.tsl550
+            classname: TSL550
+            parameters:
+                port: COM4
+            description: Santec Laser
+            instancemode: single
+            daemon: westview.daemon
+            nameservers:
+            - production
 
 Because of this mechanism, when writing services, all arguments to the function
 should be keyword arguments (i.e. no positional arguments). They don't 
@@ -34,6 +61,7 @@ None or an empty list, after which you have code in the connect function that
 checks for bad arguments and raises exceptions like ValueError for required
 arguments.
 
+
 Availability
 ------------
 
@@ -41,4 +69,43 @@ Some services have drivers that may only be available on certain operating
 systems. The beauty of PyroLab is that, while the computer hosting the service
 must run the correct OS and provide the appropriate drivers, any client
 (regardless of OS or the availability of drivers) can then connect to that
-host via a Proxy and make requests, send control inputs, and retrieve data.
\ No newline at end of file
+host via a Proxy and make requests, send control inputs, and retrieve data.
+
+
+Exposing Methods
+----------------
+
+When writing drivers for instruments or other services, you should (as much
+as possible) import from PyroLab's native API instead of directly importing 
+from Pyro5. Some Pyro5 objects have had functions added to them to make them
+work with PyroLab, and by using PyroLab versions of functions and objects, you
+are future-proofing your code against potential changes that PyroLab might 
+make. If you use the PyroLab versions, your code is sure to inherit whatever
+functionality may be added.
+
+
+.. _user_guide_instance_mode:
+
+Instance Modes
+--------------
+
+.. todo:: 
+
+    This section is under construction.
+
+
+Exceptions
+----------
+
+Pyro5 only supports the serialization of builtin Python exception classes.
+There is technically a way to register new serialization/deserialization 
+functions with Pyro5, but since different versions of the software may be
+running on each computer, the client would have to duplicate code for 
+deserializing the attributes returned by the Proxy. Therefore, it is simpler
+to restrict exceptions to builtins. 
+
+The `Python builtin exception
+<https://docs.python.org/3/library/exceptions.html>`_ list is pretty extensive,
+and if you truly cannot find an existing exception that encapsulates the
+essence of your error, you can always raise a generic exception with a specific
+message and then compare error strings on the handling side.
diff --git a/docs/user_guide/webmonitor.rst b/docs/user_guide/webmonitor.rst
new file mode 100644
index 0000000..b7ac3f9
--- /dev/null
+++ b/docs/user_guide/webmonitor.rst
@@ -0,0 +1,79 @@
+.. _user_guide_webmonitor:
+
+
+Web Monitor
+===========
+
+PyroLab includes a web monitoring app that can be configured to display 
+devices registered with a nameserver, along with their status (such as 
+availability or locked state). It installs with PyroLab as an extra if you use
+the command:
+
+.. code-block:: bash
+
+    pip install pyrolab[monitor]
+
+It comes with a web server that can be configured to host the webapp on 
+localhost or any other interface. It's launched using the command line, and you
+can see it's options by typing ``pyromonitor --help``:
+
+.. code-block:: text
+
+    Usage: pyromonitor [OPTIONS] COMMAND [ARGS]...
+
+    Options:
+    -v, --version                   Show the version and exit.
+    --install-completion [bash|zsh|fish|powershell|pwsh]
+                                    Install completion for the specified shell.
+    --show-completion [bash|zsh|fish|powershell|pwsh]
+                                    Show completion for the specified shell, to
+                                    copy it or customize the installation.
+    --help                          Show this message and exit.
+
+    Commands:
+    configure  Load a server configuration file.
+    up         Launch the monitoring web app.
+
+The server configuration file is a YAML file with the following keys and 
+defaults:
+
+.. code-block:: yaml
+
+    host: localhost
+    port: 8080
+    nameserver: localhost
+    ns_port: 9090
+    polling: 300
+
+* ``host`` is the location to serve the web app, either localhost or your own
+  IP address.
+* ``port`` is the port to serve the web app on.
+* ``nameserver`` is the domain of the nameserver to monitor.
+* ``ns_port`` is the port the nameserver is exposed on.
+* ``polling`` is the period in seconds between refreshes. The web monitor works
+  by attempting to ping every listed service at a set refresh rate.
+
+You can write your own configuration file and persist it within pyromonitor.
+Only non-default keys need to be specified in this file, although you may 
+specify all keys and values if you wish. Load the config file in pyromonitor
+using the ``pyromonitor configure`` command:
+
+.. code-block:: text
+
+    Usage: pyromonitor configure [OPTIONS] FILENAME
+
+    Load a server configuration file.
+
+    Arguments:
+    FILENAME  [required]
+
+    Options:
+    --help  Show this message and exit.
+
+You can launch the monitor on the host and port in the configuration by using
+the ``pyromonitor up`` command:
+
+.. code-block:: text
+
+    Serving on http://localhost:8080
+    (press CTRL+C to quit)
diff --git a/examples/905.pp-laser/ppcl550_example.py b/examples/905.pp-laser/ppcl550_example.py
deleted file mode 100644
index f8f73fd..0000000
--- a/examples/905.pp-laser/ppcl550_example.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Example of how to use the Pure Photonics pp550 laser locally
-
-# import the the pyrolab class
-from pyrolab.drivers.lasers.ppcl550 import PPCL55x
-
-import time
-
-# The 550 laser has a range of 1515-1570 nm, power range of 6-13.5 dBm
-# choose the correct COM port 
-laser = PPCL55x(port="COM6")
-
-# set the power wavelength and channel
-laser.setPower(13.5)
-laser.setWavelength(1550)
-laser.setChannel(1)
-
-# turn the laser on
-laser.on()
-time.sleep(10)
-
-# adjust the power to 10 dBm
-print("setting the power to 10 dBm")
-laser.setPower(10)
-time.sleep(10)
-
-# change the wavelength to 1570 nm
-print("setting wavelength to 1570 nm")
-laser.setWavelength(1570)
-time.sleep(10)
-laser.off()
-laser.close()
\ No newline at end of file
diff --git a/examples/905.pp-laser/ppcl551_example.py b/examples/905.pp-laser/ppcl551_example.py
deleted file mode 100644
index 4159593..0000000
--- a/examples/905.pp-laser/ppcl551_example.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Example of how to use the Pure Photonics pp551 laser locally
-
-# import the the pyrolab class
-from pyrolab.drivers.lasers.ppcl550 import PPCL55x
-
-import time
-
-# The 551 laser has a range of 1569-1625 nm, power range of 6-13.5 dBm
-# choose the correct COM port 
-laser = PPCL55x(minWL=1569, maxWL=1625, port="COM5")
-
-# set the power wavelength and channel
-laser.setPower(13.5)
-laser.setWavelength(1570)
-laser.setChannel(1)
-
-# turn the laser on
-laser.on()
-time.sleep(10)
-
-# adjust the power to 10 dBm
-print("setting the power to 10 dBm")
-laser.setPower(10)
-time.sleep(10)
-
-# change the wavelength to 1600 nm
-print("setting wavelength to 1600 nm")
-laser.setWavelength(1600)
-time.sleep(10)
-laser.off()
-laser.close()
\ No newline at end of file
diff --git a/examples/221.autoconnect/README.txt b/examples/autoconnect/README.txt
similarity index 100%
rename from examples/221.autoconnect/README.txt
rename to examples/autoconnect/README.txt
diff --git a/examples/221.autoconnect/client.py b/examples/autoconnect/client.py
similarity index 100%
rename from examples/221.autoconnect/client.py
rename to examples/autoconnect/client.py
diff --git a/examples/221.autoconnect/nameserver.py b/examples/autoconnect/nameserver.py
similarity index 100%
rename from examples/221.autoconnect/nameserver.py
rename to examples/autoconnect/nameserver.py
diff --git a/examples/221.autoconnect/server.py b/examples/autoconnect/server.py
similarity index 63%
rename from examples/221.autoconnect/server.py
rename to examples/autoconnect/server.py
index 7e1f0ae..aa7fc7c 100644
--- a/examples/221.autoconnect/server.py
+++ b/examples/autoconnect/server.py
@@ -18,22 +18,19 @@
 not be able to reconnect to release the lock if not for this feature.)
 """
 
-from pyrolab.api import locate_ns
+from pyrolab.api import locate_ns, Daemon, DaemonConfiguration
 from pyrolab.drivers.sample import SampleAutoconnectInstrument
-from pyrolab.server.locker import create_lockable
-from pyrolab.server import AutoconnectLockableDaemon, srv_profile, DaemonConfiguration
 
 cfg = DaemonConfiguration(servertype="multiplex")
-srv_profile.use(cfg)
+cfg.update_pyro_config()
 
-daemon = AutoconnectLockableDaemon()
+daemon = Daemon()
 ns = locate_ns(host="localhost")
-SS = create_lockable(SampleAutoconnectInstrument)
-uri = daemon.register(SS, connect_params={"address": "0.0.0.0", "port": 8080})
-ns.register("test.SampleAutoconnectInstrument", uri, metadata={"This instrument supports autoconnect()."})
+uri = daemon.register(SampleAutoconnectInstrument, connect_params={"address": "0.0.0.0", "port": 8080})
+ns.register("test.autoconnectable", uri, metadata={"This instrument supports autoconnect()."})
 
 print("SERVER READY")
 try:
     daemon.requestLoop()
 finally:
-    ns.remove("test.SampleAutoconnectInstrument")
+    ns.remove("test.autoconnectable")
diff --git a/examples/library-catalog/client.py b/examples/library-catalog/client.py
new file mode 100644
index 0000000..3058c0a
--- /dev/null
+++ b/examples/library-catalog/client.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © PyroLab Project Contributors
+# Licensed under the terms of the GNU GPLv3+ License
+# (see pyrolab/__init__.py for details)
+
+"""
+2-Way SSL Server
+----------------
+
+...
+"""
+
+from pyrolab.api import locate_ns, Proxy
+
+with locate_ns(host="localhost") as ns:
+    uri = ns.lookup("library")
+
+with Proxy(uri) as catalog:
+    books = {
+        "The Hobbit": 3,
+        "The Lord of the Rings": 2,
+        "Harry Potter": 3,
+        "The Lightning Thief": 2,
+        "C Programming": 2,
+    }
+
+    for book, number in books.items():
+        catalog.add_book(book, number)
+
+    print("Initial Catalog...")
+    print(catalog.get_catalog(), "\n")
+    
+    print("Checking out The Hobbit...")
+    catalog.checkout("The Hobbit")
+    print(catalog.get_catalog(), "\n")
+
+    print("Checkout out two copies of C Programming...")
+    catalog.checkout("C Programming", 2)
+    print(catalog.get_catalog(), "\n")
+
+    # Check out a book when all books are already checked out
+    try:
+        print("Checking out when all copies are already checked out...")
+        catalog.checkout("C Programming")
+    except ValueError as e:
+        print(e, "\n")
+
+    # Return a book not in the library's catalog
+    try:
+        print("Returning out a book not in the catalog...")
+        catalog.checkin("Dune")
+    except LookupError as e:
+        print(e, "\n")
+
+    # Check out a book not in the library's catalog
+    try: 
+        print("Checking out a book not in the catalog...")
+        catalog.checkout("Coding for Dummies")
+    except LookupError as e:
+        print(e)
diff --git a/examples/library-catalog/config.yaml b/examples/library-catalog/config.yaml
new file mode 100644
index 0000000..391ec7e
--- /dev/null
+++ b/examples/library-catalog/config.yaml
@@ -0,0 +1,40 @@
+version: '1.0'
+
+nameservers:
+  production:
+    host: localhost
+    ns_port: 9090
+    broadcast: false
+    ns_bchost: null
+    ns_bcport: 9091
+    ns_autoclean: 0.0
+    storage: memory
+
+daemons:
+  lockable:
+    module: pyrolab.server
+    classname: LockableDaemon
+    host: localhost
+    port: 0
+    unixsocket: null
+    nathost: null
+    natport: 0
+    servertype: thread
+    nameservers:
+    - production
+
+services:
+  library:
+    module: libservice
+    classname: LibraryCatalog
+    description: Library catalog system
+    instancemode: single
+    daemon: lockable
+    nameservers:
+    - production
+
+autolaunch:
+  nameservers:
+  - production
+  daemons:
+  - lockable
diff --git a/examples/library-catalog/service.py b/examples/library-catalog/libservice.py
similarity index 92%
rename from examples/library-catalog/service.py
rename to examples/library-catalog/libservice.py
index 6bc8f40..689fa59 100644
--- a/examples/library-catalog/service.py
+++ b/examples/library-catalog/libservice.py
@@ -16,6 +16,7 @@
 from pyrolab.api import Service, behavior, expose
 
 
+
 @behavior(instance_mode="single")
 @expose
 class LibraryCatalog(Service):
@@ -56,7 +57,7 @@ def checkout(self, title: str, quantity: int = 1) -> None:
             The number of copies to check out (default 1).
         """
         if title not in self.catalog:
-            raise ValueError(f"Cannot checkout: Book '{title}' not found.")
+            raise LookupError(f"Cannot checkout: Book '{title}' not found.")
         if self.catalog[title] < quantity:
             raise ValueError(f"Cannot checkout: Not enough copies of {title}.")
         self.catalog[title] -= quantity
@@ -73,7 +74,7 @@ def checkin(self, title: str, quantity: int = 1) -> None:
             The number of copies to check in (default 1).
         """
         if title not in self.catalog:
-            raise ValueError(f"Cannot checkin: Book '{title}' not found.")
+            raise LookupError(f"Cannot checkin: Book '{title}' not found.")
         self.catalog[title] += quantity
 
     def add_book(self, title: str, quantity: int = 1) -> None:
@@ -99,11 +100,12 @@ def remove_book(self, title: str) -> None:
             The title of the book to remove.
         """
         if title not in self.catalog:
-            raise ValueError(f"Cannot remove: Book '{title}' not found.")
+            raise LookupError(f"Cannot remove: Book '{title}' not found.")
         del self.catalog[title]
 
 
 if __name__ == "__main__":
+    # Test the library code outside of the context of pyrolab
     books = {
         "The Hobbit": 3,
         "The Lord of the Rings": 2,
diff --git a/examples/library-catalog/updatepath.sh b/examples/library-catalog/updatepath.sh
new file mode 100644
index 0000000..4c3cb5c
--- /dev/null
+++ b/examples/library-catalog/updatepath.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+# To update the PYTHONPATH in your shell environment to make custom modules 
+# in this directory visible to the Python interpreter, run:
+#
+# $ source updatepath.sh
+
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+export PYTHONPATH="$SCRIPT_DIR:$PYTHONPATH"
diff --git a/examples/222.lockable-services/README.txt b/examples/locking/README.txt
similarity index 100%
rename from examples/222.lockable-services/README.txt
rename to examples/locking/README.txt
diff --git a/examples/222.lockable-services/client.py b/examples/locking/client.py
similarity index 100%
rename from examples/222.lockable-services/client.py
rename to examples/locking/client.py
diff --git a/examples/222.lockable-services/nameserver.py b/examples/locking/nameserver.py
similarity index 80%
rename from examples/222.lockable-services/nameserver.py
rename to examples/locking/nameserver.py
index f1abe93..48a4970 100644
--- a/examples/222.lockable-services/nameserver.py
+++ b/examples/locking/nameserver.py
@@ -12,7 +12,8 @@
 """
 
 from pyrolab.api import start_ns_loop
+from pyrolab.configure import NameServerConfiguration
 
 # Let's run our nameserver! It will use the default profile, which provides
 # sensible defaults--running on 'localhost', default port 9090, etc.
-start_ns_loop()
+start_ns_loop(NameServerConfiguration())
diff --git a/examples/222.lockable-services/server.py b/examples/locking/server.py
similarity index 77%
rename from examples/222.lockable-services/server.py
rename to examples/locking/server.py
index f853614..62c5c7f 100644
--- a/examples/222.lockable-services/server.py
+++ b/examples/locking/server.py
@@ -18,19 +18,17 @@
 not be able to reconnect to release the lock if not for this feature.)
 """
 
-from pyrolab.api import locate_ns
+from pyrolab.api import locate_ns, LockableDaemon, DaemonConfiguration
 from pyrolab.drivers.sample import SampleService
-from pyrolab.server.locker import create_lockable
-from pyrolab.server import LockableDaemon, srv_profile, DaemonConfiguration
 
 cfg = DaemonConfiguration(servertype="multiplex")
-srv_profile.use(cfg)
+cfg.update_pyro_config()
 
-daemon = LockableDaemon()
 ns = locate_ns(host="localhost")
-SS = create_lockable(SampleService)
-# SS = behavior(SS, instance_mode="single")
-SS._pyroInstancing = ("single", None)
+
+daemon = LockableDaemon()
+SS = SampleService
+SS.set_behavior("single")
 uri = daemon.register(SS)
 ns.register("test.SampleService", uri, metadata={"You can put lists of strings here!"})
 
diff --git a/pyproject.toml b/pyproject.toml
index dcb7132..3959e7d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,13 +1,125 @@
-[build-system]
-requires = [
-    "setuptools>=51",
-    "wheel"
+[project]
+name = "PyroLab"
+version = "0.3.0"
+description = "A framework for using remote lab instruments as local resources built on Pyro5"
+readme = "README.md"
+requires-python = ">=3.7"
+license = {file = "LICENSE"}
+keywords = [
+  "PyroLab", 
+  "Pyro5", 
+  "Pyro", 
+  "lab", 
+  "instrument", 
+  "remote", 
+  "resources", 
+  "framework", 
+  "laboratory", 
+  "instrumentation", 
+  "hardware", 
+  "science", 
+  "remote", 
+  "network", 
+  "integration",
+]
+
+# This should be your name or the name of the organization who originally
+# authored the project, and a valid email address corresponding to the name
+# listed.
+authors = [
+  {name = "Sequoia Ploeg", email = "sequoiap4@gmail.com" },
+  {name = "BYU CamachoLab", email = "camacho@byu.edu"}
+]
+
+# This should be your name or the names of the organization who currently
+# maintains the project, and a valid email address corresponding to the name
+# listed.
+maintainers = [
+  {name = "Sequoia Ploeg", email = "sequoiap4@gmail.com" },
+  {name = "BYU CamachoLab", email = "camacho@byu.edu"}
+]
+
+classifiers = [
+  "Development Status :: 4 - Beta",
+  "Intended Audience :: Education",
+  "Intended Audience :: Science/Research",
+  "Intended Audience :: Developers",
+  "Topic :: Scientific/Engineering",
+  "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3.7",
+  "Programming Language :: Python :: 3.8",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Programming Language :: Python :: 3 :: Only",
+  "Operating System :: MacOS",
+  "Operating System :: Microsoft :: Windows",
+  "Operating System :: POSIX :: Linux",
+  "Operating System :: OS Independent",
+]
+
+dependencies = [
+  "numpy",
+  "scipy",
+  "Pyro5",
+  "pydantic",
+  "pyyaml",
+  "deprecation",
+  "typer",
+  "colorama",
+  "tabulate",
+  "requests",
+  "packaging>=23.0",
 ]
+
+# List additional groups of dependencies here (e.g. development
+# dependencies). Users will be able to install these using the "extras"
+# syntax, for example:
+#
+#   $ pip install sampleproject[dev]
+[project.optional-dependencies]
+tsl550 = ["pyserial"]
+ppcl55x = ["pyserial"]
+rto = ["pyvisa", "pyvisa-py"]
+arduino = ["pyfirmata"]
+monitor = ["flask", "omegaconf", "waitress"]
+test = ["pytest", "coverage"]
+
+[project.urls]
+"Homepage" = "https://pyrolab.readthedocs.io/en/latest/"
+"Documentation" = "https://pyrolab.readthedocs.io/en/latest/"
+"Source" = "https://github.com/BYUCamachoLab/pyrolab"
+"Bug Reports" = "https://github.com/BYUCamachoLab/pyrolab/issues"
+"Research Group" = "https://camacholab.byu.edu/"
+
+[project.scripts]
+pyrolab = "pyrolab.cli:app"
+pyromonitor = "pyromonitor.cli:app"
+
+[tool.setuptools.packages.find]
+where = ["src"]
+include = ["pyrolab", "pyromonitor"]
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+addopts = "--maxfail=2 -rf"
+testpaths = ["tests"]
+console_output_style = "progress"
+filterwarnings = ["ignore::UserWarning", "ignore::DeprecationWarning"]
+
+[build-system]
+requires = ["setuptools>=62.0.0", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [tool.isort]
 profile = "black"
 extend_skip_glob = "api.py"
+multi_line_output = 3
+include_trailing_comma = true
+force_grid_wrap = 0
+use_parentheses = true
+line_length = 88
 known_third_party = [
     "Pyro5",
     "matplotlib",
diff --git a/pyrolab/drivers/motion/kinesis/exceptions.py b/pyrolab/drivers/motion/kinesis/exceptions.py
deleted file mode 100644
index e1ecb3d..0000000
--- a/pyrolab/drivers/motion/kinesis/exceptions.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © PyroLab Project Contributors
-# Licensed under the terms of the GNU GPLv3+ License
-# (see pyrolab/__init__.py for details)
-
-"""
-Kinesis Exceptions
-==================
-
-Exceptions for the Kinesis driver.
-"""
-
-class KinesisError(Exception):
-    def __init__(self, *args: object, errcode=0) -> None:
-        super().__init__(*args)
-        self.errcode = errcode
-
-class KinesisCommunicationError(KinesisError):
-    def __init__(self, *args: object, errcode=0) -> None:
-        super().__init__(*args, errcode=errcode)
-
-class KinesisDLLError(KinesisError):
-    def __init__(self, *args: object, errcode=0) -> None:
-        super().__init__(*args, errcode=errcode)
-
-class KinesisMotorError(KinesisError):
-    def __init__(self, *args: object, errcode=0) -> None:
-        super().__init__(*args, errcode=errcode)
-        
\ No newline at end of file
diff --git a/pyrolab/errors.py b/pyrolab/errors.py
deleted file mode 100644
index 69432ac..0000000
--- a/pyrolab/errors.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © PyroLab Project Contributors
-# Licensed under the terms of the GNU GPLv3+ License
-# (see pyrolab/__init__.py for details)
-
-"""
-Errors
-======
-
-Custom errors for PyroLab.
-
-PyroLab will only ever raise Python builtin exceptions or the errors listed
-here. Remote tracebacks, however, may contain other exceptions. See each 
-service's documentation for exceptions that may be raised by that service.
-"""
-
-
-class PyroLabError(Exception):
-    """Base class for all PyroLab exceptions."""
-    pass
-
-
-class CommunicationError(PyroLabError):
-    """
-    Error raised when there is a problem communicating with device
-    """
-    def __init__(self, message="Communication failed with device"):
-        super().__init__(message)
-    pass
diff --git a/requirements.txt b/requirements.txt
index 1fad6a8..a92c89f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,10 +10,15 @@ pytest>=6.0.1
 bump2version>=1.0.0 
 pyfirmata
 pyyaml
-appnope
 pydantic
 typer
 tabulate
 colorama
 black
-isort>=5.0.0
\ No newline at end of file
+isort>=5.0.0
+tabulate==0.9.0
+flask==2.2.3
+jinja2==3.1.2
+omegaconf==2.3.0
+build==0.10.0
+waitress==2.1.2
\ No newline at end of file
diff --git a/scripts/run_monitor.py b/scripts/run_monitor.py
new file mode 100644
index 0000000..414ca16
--- /dev/null
+++ b/scripts/run_monitor.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+# Copyright © 2023 PyroLab Project Contributors and others (see AUTHORS.txt).
+# The resources, libraries, and some source files under other terms (see NOTICE.txt).
+#
+# This file is part of PyroLab.
+#
+# PyroLab is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyroLab is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with PyroLab. If not, see <https://www.gnu.org/licenses/>.
+
+# This file is a development server only, not fit for production use! To learn
+# about running flask apps in production, see
+# https://flask.palletsprojects.com/en/2.2.x/deploying/
+
+
+from pyromonitor import create_app
+
+
+if __name__ == '__main__':
+    app = create_app()
+    app.run(debug=True, use_debugger=False, use_reloader=True, passthrough_errors=True)
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index baac96a..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,72 +0,0 @@
-[metadata]
-name = PyroLab
-version = 0.2.1
-description = A framework for using remote lab instruments as local resources built on Pyro5
-long_description = file: README.md
-long_description_content_type = text/markdown
-author = Sequoia Ploeg
-author_email = sequoia.ploeg@byu.edu
-url = https://github.com/BYUCamachoLab/pyrolab
-classifiers = 
-    Development Status :: 4 - Beta
-    Intended Audience :: Science/Research
-    Intended Audience :: Education
-    Intended Audience :: Developers
-    License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
-    Operating System :: Microsoft :: Windows
-    Operating System :: MacOS
-    Operating System :: POSIX :: Linux
-    Operating System :: OS Independent
-    Programming Language :: Python :: 3
-    Programming Language :: Python :: 3.7
-    Programming Language :: Python :: 3.8
-    Programming Language :: Python :: 3.9
-    Programming Language :: Python :: 3.10
-    Programming Language :: Python :: 3 :: Only
-    Programming Language :: Python :: Implementation :: CPython
-    Topic :: Scientific/Engineering
-    Topic :: Scientific/Engineering :: Physics
-    Topic :: Scientific/Engineering :: Visualization
-    Topic :: Software Development :: Libraries :: Python Modules
-license = GPLv3+
-keywords = PyroLab, Pyro5, Pyro, lab, instrument, remote, resources, framework, laboratory, instrumentation, hardware, science, remote, network, integration
-platforms = Windows, Linux, Mac OS-X
-project_urls = 
-    Source = https://github.com/BYUCamachoLab/pyrolab
-    Documentation = https://pyrolab.readthedocs.io/en/latest/
-    Tracker = https://github.com/BYUCamachoLab/pyrolab/issues
-    Research Group = https://camacholab.byu.edu/
-
-[options]
-python_requires = >=3.7
-packages = find:
-setup_requires = 
-    pip >= 21.1
-install_requires = 
-    numpy
-    scipy
-    Pyro5
-    pydantic
-    pyyaml
-    appdirs
-    deprecation
-    appnope
-    typer
-    colorama
-    tabulate
-
-[options.entry_points]
-console_scripts =
-    pyrolab = pyrolab.cli:app
-
-[options.extras_require]
-tsl550 =
-    pyserial
-ppcl55x =
-    pyserial
-rto =
-    pyvisa
-    pyvisa-py
-arduino =
-    pyfirmata
-    
\ No newline at end of file
diff --git a/pyrolab/__init__.py b/src/pyrolab/__init__.py
similarity index 66%
rename from pyrolab/__init__.py
rename to src/pyrolab/__init__.py
index 10152f5..a04adf4 100644
--- a/pyrolab/__init__.py
+++ b/src/pyrolab/__init__.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 # Copyright © 2020- PyroLab Project Contributors and others (see AUTHORS.txt).
 # The resources, libraries, and some source files under other terms (see NOTICE.txt).
 #
@@ -29,17 +28,21 @@
 import platform
 import sys
 
-if sys.version_info < (3, 7, 0):
+# Check if Python version is supported
+pyversion = sys.version_info
+if pyversion < (3, 7, 0):
     raise Exception(
         "PyroLab requires Python 3.7+ (version "
         + platform.python_version()
         + " detected)."
     )
 
+
+# Metadata
 __name__ = "PyroLab"
 __author__ = "CamachoLab"
 __copyright__ = "Copyright 2020, The PyroLab Project"
-__version__ = "0.2.1"
+__version__ = "0.3.0"
 __license__ = "GPLv3+"
 __maintainer__ = "Sequoia Ploeg"
 __maintainer_email__ = "sequoia.ploeg@byu.edu"
@@ -49,6 +52,7 @@
 __website_url__ = "https://camacholab.byu.edu/"
 
 
+# Filter warnings
 import warnings
 
 warnings.filterwarnings("default", category=DeprecationWarning)
@@ -56,42 +60,28 @@
     warnings.filterwarnings("ignore", category=DeprecationWarning)
 
 
-# Hide a very annoying warnings from appnope about Python 3.12
-with warnings.catch_warnings():
-    warnings.filterwarnings("ignore", category=DeprecationWarning)
-    import appnope
-    appnope.nope()
-
-
-from appdirs import AppDirs
+# Configuration directories
+# Old api deprecated in 3.11, new api added in 3.9
+if pyversion < (3, 9, 0):
+    base_path = pathlib.Path(__file__).resolve().parent
+else:
+    from importlib.resources import files
+    base_path = files("pyrolab")
+base_path = base_path / "data" / "local"
 
-_dirs = AppDirs(__name__, __author__)
-
-# Data Directories
-PYROLAB_DATA_DIR = pathlib.Path(_dirs.user_data_dir)
+# Data directories
+PYROLAB_DATA_DIR = pathlib.Path(base_path)
 PYROLAB_DATA_DIR.mkdir(parents=True, exist_ok=True)
 
-# Configuration Directories
-PYROLAB_CONFIG_DIR = PYROLAB_DATA_DIR / "config"
-PYROLAB_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
-
-# User config file directory
-CONFIG_DIR = PYROLAB_CONFIG_DIR / "user"
-CONFIG_DIR.mkdir(parents=True, exist_ok=True)
-
-# Background daemon data directory
-PYROLABD_DATA = PYROLAB_DATA_DIR / "pyrolabd"
-PYROLABD_DATA.mkdir(parents=True, exist_ok=True)
-
-NAMESERVER_STORAGE = PYROLAB_DATA_DIR / "nameservers"
+NAMESERVER_STORAGE = PYROLAB_DATA_DIR / "nameserver"
 NAMESERVER_STORAGE.mkdir(parents=True, exist_ok=True)
 
 PYROLAB_LOGDIR = PYROLAB_DATA_DIR / "logs"
 PYROLAB_LOGDIR.mkdir(parents=True, exist_ok=True)
 
-LOCKFILE = PYROLABD_DATA / "pyrolabd.lock"
-USER_CONFIG_FILE = CONFIG_DIR / "user_configuration.yaml"
-RUNTIME_CONFIG = PYROLABD_DATA / "runtime_config.yaml"
+LOCKFILE = PYROLAB_DATA_DIR / "pyrolabd.lock"
+USER_CONFIG_FILE = PYROLAB_DATA_DIR / "user_configuration.yaml"
+RUNTIME_CONFIG = PYROLAB_DATA_DIR / "runtime_config.yaml"
 
 
 # Set up logging to file
@@ -107,6 +97,7 @@ def get_loglevel() -> int:
         loglevel = logging.INFO
     return loglevel
 
+
 if len(logging.root.handlers) == 0:    
     logfile = PYROLAB_LOGDIR / f"pyrolab_{os.getpid()}.log"
     root = logging.getLogger()
@@ -122,3 +113,29 @@ def get_loglevel() -> int:
 import Pyro5.errors
 
 sys.excepthook = Pyro5.errors.excepthook
+
+
+# Check for updates to PyroLab
+try:
+    import json
+    import requests
+    from requests.adapters import HTTPAdapter
+    from packaging.version import parse
+
+    url = "https://pypi.org/pypi/pyrolab/json"
+    with requests.Session() as s:
+        s.mount('https://pypi.org', HTTPAdapter(max_retries=3))
+        resp = s.get(url).text
+
+    v = json.loads(resp)['info']['version']
+    curver = parse(__version__)
+    latest = parse(v)
+
+    log = logging.getLogger(__name__)
+
+    if curver < latest:
+        message = f"A new version of PyroLab is available (latest is {latest}, but {curver} is installed)."
+        warnings.warn(message, stacklevel=2)
+        log.info(message)
+except:
+    pass
diff --git a/pyrolab/api.py b/src/pyrolab/api.py
similarity index 89%
rename from pyrolab/api.py
rename to src/pyrolab/api.py
index 5185422..280d96b 100644
--- a/pyrolab/api.py
+++ b/src/pyrolab/api.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -13,8 +11,8 @@
 
 from Pyro5.client import Proxy
 from Pyro5.core import locate_ns
+from Pyro5.server import behavior, expose, oneway, serve
 from pyrolab import USER_CONFIG_FILE
-
 from pyrolab.configure import (
     PyroLabConfiguration, 
     NameServerConfiguration, 
@@ -23,14 +21,7 @@
     reset_config, 
     update_config,
 )
-from pyrolab.server import (
-    Daemon,
-    LockableDaemon,
-    behavior,
-    expose,
-    oneway,
-    serve,
-)
+from pyrolab.server import Daemon, LockableDaemon
 from pyrolab.nameserver import start_ns, start_ns_loop
 from pyrolab.service import Service
 
diff --git a/pyrolab/cli.py b/src/pyrolab/cli.py
similarity index 96%
rename from pyrolab/cli.py
rename to src/pyrolab/cli.py
index 3d7ef6f..aa6b9d9 100644
--- a/pyrolab/cli.py
+++ b/src/pyrolab/cli.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -19,15 +17,14 @@
 import textwrap
 import fileinput
 import re
-from datetime import date
 from pathlib import Path
-from typing import Optional
+from typing import Callable, Iterable, Optional
 from time import sleep, strptime
 
 import pkg_resources
 import typer
 
-from pyrolab import LOCKFILE, PYROLAB_LOGDIR, RUNTIME_CONFIG, USER_CONFIG_FILE # PYROLAB_LOGFILE
+from pyrolab import LOCKFILE, PYROLAB_LOGDIR, RUNTIME_CONFIG, USER_CONFIG_FILE
 from pyrolab.api import Proxy
 from pyrolab.configure import (
     PyroLabConfiguration,
@@ -68,13 +65,7 @@ def get_daemon(abort=True, suppress_reload_message=False) -> PyroLabDaemon:
 # pyrolab --version
 ###############################################################################
 
-app = typer.Typer()
-
-print(f"PyroLab  Copyright © 2020-{date.today().year}  BYU CamachoLab, PyroLab Project Contributors")
-print("This program comes with ABSOLUTELY NO WARRANTY.")
-print("This is free software, and you are welcome to redistribute it under certain conditions.")
-print("See the documentation for more information (https://pyrolab.readthedocs.io).")
-print()
+app = typer.Typer(no_args_is_help=True)
 
 
 def _version_callback(value: bool=True) -> None:
@@ -332,6 +323,17 @@ def logs_clean():
     """
     [f.unlink() for f in PYROLAB_LOGDIR.glob("*.*")]
 
+def try_itr(func: Callable, itr: Iterable, *exceptions, **kwargs):
+    """
+    Tests a function on an iterable, yields iterable if no exception is raised.
+    """
+    for elem in itr:
+        try:
+            func(elem, **kwargs)
+            yield elem
+        except exceptions:
+            pass
+
 @logs_app.command("export")
 def logs_export(filename: str):
     """
@@ -341,6 +343,7 @@ def logs_export(filename: str):
     lines = list(fileinput.input(f_names))
     t_fmt = "%Y-%m-%d %H:%M:%S.%f" # format of time stamps
     t_pat = re.compile(r'\[(.+?)\]') # pattern to extract timestamp
+    lines = list(try_itr(lambda l: strptime(t_pat.search(l).group(1), t_fmt), lines, AttributeError))
     with Path(filename).open(mode='w') as f:
         for l in sorted(lines, key=lambda l: strptime(t_pat.search(l).group(1), t_fmt)):
             f.write(l)
diff --git a/pyrolab/configure.py b/src/pyrolab/configure.py
similarity index 95%
rename from pyrolab/configure.py
rename to src/pyrolab/configure.py
index 11fe511..9f9a241 100644
--- a/pyrolab/configure.py
+++ b/src/pyrolab/configure.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -34,13 +32,13 @@
 """
 
 from __future__ import annotations
-import importlib
 
+import uuid
+import importlib
 import logging
 from pathlib import Path
-import sys
 from typing import IO, Any, Dict, List, Optional, Type, Union
-import uuid
+
 
 import Pyro5
 from pydantic import BaseModel, BaseSettings, validator
@@ -48,18 +46,17 @@
 from yaml import dump, load
 from yaml.constructor import ConstructorError
 from yaml.nodes import MappingNode
-
-from pyrolab.server import Daemon
-from pyrolab.service import Service
-
 try:
     from yaml import CLoader as Loader
 except ImportError:
     from yaml import Loader
 
+from pyrolab.server import Daemon
+from pyrolab.service import Service
 from pyrolab import NAMESERVER_STORAGE, USER_CONFIG_FILE
 from pyrolab.utils import generate_random_name, get_ip
 
+
 log = logging.getLogger(__name__)
 
 
@@ -414,26 +411,37 @@ class DaemonConfiguration(BaseSettings, PyroConfigMixin, YAMLMixin):
     classname : str, optional
         The name of the Daemon class to use (default is basic "Daemon").
     host : str, optional
-        The hostname of the local server, or the string "public", which 
-        is converted to the host's public IP address (default "localhost").
-    ns_host : str, optional
-        The hostname of the nameserver (default "localhost").
-    ns_port : int, optional
-        The port of the nameserver (default 9090).
-    ns_bcport : int, optional
-        The port of the broadcast server (default 9091).
-    ns_bchost : bool, optional
-        Whether to broadcast the nameserver (default None).
+        The hostname of the local server, or the string "public", which is
+        converted to the host's public IP address (default "localhost").
+    port : int, optional
+        Port to bind the server on (default 0, which means to pick a random
+        port).
+    unixsocket : str, optional
+        The name of a Unix domain socket to use instead of a TCP/IP socket
+        (default None, which means don't use).
+    nathost : str, optional
+        Hostname to use in published addresses (useful when running behind a
+        NAT firewall/router). Default is None which means to just use the
+        normal host. For more details about NAT, see the Pyro5 docs about using
+        Pyro5 behind a NAT router/firewall.
+    natport : int, optional
+        Port to use in published addresses (useful when running behind a NAT
+        firewall/router). If you use 0 here (default), Pyro will replace the
+        NAT-port by the internal port number to facilitate one-to-one NAT port
+        mappings.
     servertype : str, optional
         Either ``thread`` or ``multiplex`` (default "thread").
     nameservers : List[str], optional
-        Whether to register the daemon itself with known nameservers. Useful
+        Whether to register the *daemon itself* with known nameservers. Useful
         if the daemon provides functions for managing local instruments that
-        would be useful to remote clients.
+        would be useful to remote clients. Services declare their own
+        nameserver registrations; belonging to a daemon that registers itself
+        with a specific nameserver does not mean that its services will also be
+        registered with that nameserver.
 
     Examples
     --------
-    The following is an example of a valid configuration file "daemons" 
+    The following is an example of a valid configuration file "daemons"
     section. Keys not defined assume the default values.
 
     .. code-block:: yaml
@@ -491,10 +499,6 @@ class ServiceConfiguration(BaseSettings, PyroConfigMixin, YAMLMixin):
 
     Parameters
     ----------
-    name : str
-        A unique human-readable name for identifying the instrument. If you're
-        not creative, you can use :py:func:`pyrolab.utils.generate_random_name`
-        to generate a random name.
     module : str
         The PyroLab module the class belongs to, as a string.
     classname : str
diff --git a/pyrolab/data/wordlist.txt b/src/pyrolab/data/wordlist.txt
similarity index 100%
rename from pyrolab/data/wordlist.txt
rename to src/pyrolab/data/wordlist.txt
diff --git a/pyrolab/drivers/__init__.py b/src/pyrolab/drivers/__init__.py
similarity index 99%
rename from pyrolab/drivers/__init__.py
rename to src/pyrolab/drivers/__init__.py
index a28078c..2579880 100644
--- a/pyrolab/drivers/__init__.py
+++ b/src/pyrolab/drivers/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/arduino/__init__.py b/src/pyrolab/drivers/arduino/__init__.py
similarity index 92%
rename from pyrolab/drivers/arduino/__init__.py
rename to src/pyrolab/drivers/arduino/__init__.py
index b35fa6b..37a70a9 100644
--- a/pyrolab/drivers/arduino/__init__.py
+++ b/src/pyrolab/drivers/arduino/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/arduino/arduino.py b/src/pyrolab/drivers/arduino/arduino.py
similarity index 92%
rename from pyrolab/drivers/arduino/arduino.py
rename to src/pyrolab/drivers/arduino/arduino.py
index 3fac97b..b97b0fe 100644
--- a/pyrolab/drivers/arduino/arduino.py
+++ b/src/pyrolab/drivers/arduino/arduino.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -25,26 +23,18 @@
 
    pyfirmata
 """
+
 import logging
 
 from pyfirmata import ANALOG, INPUT, OUTPUT, PWM, SERVO, Arduino, ArduinoDue, ArduinoMega, ArduinoNano, util
-from Pyro5.api import expose
 
 from pyrolab.drivers.arduino import Arduino as PyroArduino
-from pyrolab.errors import PyroLabError
+from pyrolab.api import expose
 
 
 log = logging.getLogger(__name__)
 
 
-class UnknownBoardException(PyroLabError):
-    """
-    Error raised when the arduino board name is unknown or not supported
-    """
-    def __init__(self, message="Unknown board"):
-        super().__init__(message)
-
-
 @expose
 class BaseArduinoDriver(PyroArduino):
     """
@@ -65,6 +55,11 @@ def connect(self, port: str, board: str="uno") -> None:
             | ``mega``: `Arduino Mega <https://store.arduino.cc/products/arduino-mega-2560-rev3>`_
             | ``due``: `Arduino Due <https://store.arduino.cc/products/arduino-due>`_
             | ``nano``: `Arduino Nano <https://store.arduino.cc/products/arduino-nano>`_
+
+        Raises
+        ------
+        ValueError
+            If the board type is not supported.
         """
         if hasattr(self, "board"):
             log.debug("Already connected")
@@ -81,7 +76,7 @@ def connect(self, port: str, board: str="uno") -> None:
         elif board == "nano":
             self.board = ArduinoNano(self.port)
         else:
-            raise UnknownBoardException("Unknown board " + board)
+            raise ValueError(f"Unknown board '{board}'")
         
         self.it = util.Iterator(self.board)
         self.it.start()
diff --git a/pyrolab/drivers/cameras/__init__.py b/src/pyrolab/drivers/cameras/__init__.py
similarity index 92%
rename from pyrolab/drivers/cameras/__init__.py
rename to src/pyrolab/drivers/cameras/__init__.py
index dd77e90..04110aa 100644
--- a/pyrolab/drivers/cameras/__init__.py
+++ b/src/pyrolab/drivers/cameras/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/cameras/sciTSI.py b/src/pyrolab/drivers/cameras/sciTSI.py
similarity index 99%
rename from pyrolab/drivers/cameras/sciTSI.py
rename to src/pyrolab/drivers/cameras/sciTSI.py
index 416dd77..f1fa579 100644
--- a/pyrolab/drivers/cameras/sciTSI.py
+++ b/src/pyrolab/drivers/cameras/sciTSI.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/cameras/thorcam.py b/src/pyrolab/drivers/cameras/thorcam.py
similarity index 99%
rename from pyrolab/drivers/cameras/thorcam.py
rename to src/pyrolab/drivers/cameras/thorcam.py
index 4de97f3..672b417 100644
--- a/pyrolab/drivers/cameras/thorcam.py
+++ b/src/pyrolab/drivers/cameras/thorcam.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -41,10 +39,10 @@
 
 import numpy as np
 import cv2 as cv
-from Pyro5.api import locate_ns, Proxy
 
 from pyrolab.api import expose
 from pyrolab.drivers.cameras import Camera
+from pyrolab.api import locate_ns, Proxy
 
 
 log = logging.getLogger(__name__)
diff --git a/pyrolab/drivers/cameras/uc480.py b/src/pyrolab/drivers/cameras/uc480.py
similarity index 97%
rename from pyrolab/drivers/cameras/uc480.py
rename to src/pyrolab/drivers/cameras/uc480.py
index 67094e0..892a775 100644
--- a/pyrolab/drivers/cameras/uc480.py
+++ b/src/pyrolab/drivers/cameras/uc480.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -42,7 +40,6 @@
 
 from pyrolab.api import expose
 from pyrolab.drivers.cameras.thorcam import ThorCamBase, ThorCamClient
-from pyrolab.errors import PyroLabError
 
 
 log = logging.getLogger(__name__)
@@ -79,7 +76,7 @@ def pixelclock(self, clockspeed: int) -> None:
         if hasattr(self, "handle"):
             tc.PixelClock(self.handle, 6, byref(pixelclock), sizeof(pixelclock))
         else:
-            raise PyroLabError("Cannot set pixelclock before connecting to device.")
+            raise ConnectionError("Cannot set pixelclock before connecting to device.")
 
     @property
     @expose
@@ -96,7 +93,7 @@ def exposure(self, exposure: int) -> None:
         if hasattr(self, "handle"):
             tc.SetExposure(self.handle, 12 , exposure_c, sizeof(exposure_c))
         else:
-            raise PyroLabError("Cannot set exposure before connecting to device.")
+            raise ConnectionError("Cannot set exposure before connecting to device.")
 
     @property
     @expose
@@ -113,7 +110,7 @@ def framerate(self, framerate: int) -> None:
         if hasattr(self, "handle"):
             tc.SetFrameRate(self.handle, c_double(framerate), byref(s_framerate))
         else:
-            raise PyroLabError("Cannot set framerate before connecting to device.")
+            raise ConnectionError("Cannot set framerate before connecting to device.")
 
     def connect(self, 
                 serialno: str, 
@@ -202,7 +199,7 @@ def connect(self,
                 self.handle = handle
                 break
             elif(i == num.value - 1):
-                raise PyroLabError("Camera not found")
+                raise ConnectionError("Camera not found")
             else:
                 error = tc.ExitCamera(handle)
                 if error != 0:
diff --git a/pyrolab/drivers/lasers/__init__.py b/src/pyrolab/drivers/lasers/__init__.py
similarity index 91%
rename from pyrolab/drivers/lasers/__init__.py
rename to src/pyrolab/drivers/lasers/__init__.py
index 8a9df7a..855fcb1 100644
--- a/pyrolab/drivers/lasers/__init__.py
+++ b/src/pyrolab/drivers/lasers/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/lasers/ppcl55x.py b/src/pyrolab/drivers/lasers/ppcl55x.py
similarity index 89%
rename from pyrolab/drivers/lasers/ppcl55x.py
rename to src/pyrolab/drivers/lasers/ppcl55x.py
index e70cb31..f50e034 100644
--- a/pyrolab/drivers/lasers/ppcl55x.py
+++ b/src/pyrolab/drivers/lasers/ppcl55x.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -31,13 +29,14 @@
 
 import serial
 import numpy as np
-from Pyro5.api import expose
 from scipy.constants import speed_of_light as C_SPEED
 
+from pyrolab.drivers.lasers import Laser
+from pyrolab.api import expose, behavior
+
+
 log = logging.getLogger(__name__)
 
-from pyrolab.drivers.lasers import Laser
-from pyrolab.errors import CommunicationError
 
 ITLA_NOERROR=0x00
 ITLA_EXERROR=0x01
@@ -99,6 +98,7 @@
 WRITE_READ=1
 
 
+@behavior(instance_mode="single")
 @expose
 class PPCL55xBase(Laser):
     """
@@ -120,9 +120,9 @@ class PPCL55xBase(Laser):
     # MAXIMUM_POWER_DBM : float
     #     The maximum power of the laser in dBm (value 13.5).
 
-    MINIMUM_WAVELENGTH = 1500
-    MAXIMUM_WAVELENGTH = 1600
-    MINIMUM_POWER_DBM = 7
+    MINIMUM_WAVELENGTH = 1500.0
+    MAXIMUM_WAVELENGTH = 1600.0
+    MINIMUM_POWER_DBM = 7.0
     MAXIMUM_POWER_DBM = 13.5
 
     MINIMUM_FREQUENCY = C_SPEED / MAXIMUM_WAVELENGTH
@@ -141,20 +141,20 @@ def connect(self, port: str="", baudrate: int=9600) -> None:
         baudrate : int
             baudrate of the serial connection (default 9600)
         """
+        log.debug("Entering connect()")
 
         self.latest_register = 0
         self.queue = []
-        self.laser_on = False
         self.max_row_ticket = 0
 
+        if hasattr(self, "device") and self.device.is_open:
+            log.debug("Already connected")
+            return True
         try:
-            self.device = serial.Serial(port,baudrate,timeout=1,
-                parity=serial.PARITY_NONE) #attempt connection with given baudrate
+            self.device = serial.Serial(port, baudrate, timeout=1, parity=serial.PARITY_NONE)
         except serial.SerialException as e:
             self.device.close()
-            raise CommunicationError("Could not connect to laser on port " + port
-            + " with baudrate " + str(baudrate))
-            raise e
+            raise ConnectionError(f"Could not connect to laser on port {port} with baudrate {baudrate}.")
 
     def close(self) -> None:
         """
@@ -529,8 +529,39 @@ class PPCL550(PPCL55xBase):
         The minimum power of the laser in dBm (value 7).
     MAXIMUM_POWER_DBM : float
         The maximum power of the laser in dBm (value 13.5).
-    """
 
+    Examples
+    --------
+    .. code-block:: python
+
+        import time
+        from pyrolab.drivers.lasers.ppcl550 import PPCL550
+
+        # choose the correct COM port 
+        laser = PPCL55x(port="COM6")
+
+        # set the power wavelength and channel
+        laser.setPower(13.5)
+        laser.setWavelength(1550)
+        laser.setChannel(1)
+
+        # turn the laser on
+        laser.on()
+        time.sleep(10)
+
+        # adjust the power to 10 dBm
+        laser.setPower(10)
+        time.sleep(10)
+
+        # change the wavelength to 1570 nm
+        laser.setWavelength(1570)
+        time.sleep(10)
+        
+        laser.off()
+        laser.close()
+    """
+    # The 550 laser has a range of 1515-1570 nm, power range of 6-13.5 dBm
+    # TODO: Verify the above statement
     MINIMUM_WAVELENGTH = 1529
     MAXIMUM_WAVELENGTH = 1566
     MINIMUM_POWER_DBM = 7
@@ -557,8 +588,39 @@ class PPCL551(PPCL55xBase):
         The minimum power of the laser in dBm (value 7).
     MAXIMUM_POWER_DBM : float
         The maximum power of the laser in dBm (value 13.5).
-    """
 
+    Examples
+    --------
+    .. code-block:: python
+
+        import time
+        from pyrolab.drivers.lasers.ppcl550 import PPCL551
+        
+        # choose the correct COM port 
+        laser = PPCL551(minWL=1569, maxWL=1625, port="COM5")
+
+        # set the power wavelength and channel
+        laser.setPower(13.5)
+        laser.setWavelength(1570)
+        laser.setChannel(1)
+
+        # turn the laser on
+        laser.on()
+        time.sleep(10)
+
+        # adjust the power to 10 dBm
+        laser.setPower(10)
+        time.sleep(10)
+
+        # change the wavelength to 1600 nm
+        laser.setWavelength(1600)
+        time.sleep(10)
+        
+        laser.off()
+        laser.close()
+    """
+    # The 551 laser has a range of 1569-1625 nm, power range of 6-13.5 dBm
+    # TODO: Verify the above statement
     MINIMUM_WAVELENGTH = 1572
     MAXIMUM_WAVELENGTH = 1609
     MINIMUM_POWER_DBM = 7
diff --git a/pyrolab/drivers/lasers/sachertec.py b/src/pyrolab/drivers/lasers/sachertec.py
similarity index 99%
rename from pyrolab/drivers/lasers/sachertec.py
rename to src/pyrolab/drivers/lasers/sachertec.py
index bd13d82..6e1165c 100644
--- a/pyrolab/drivers/lasers/sachertec.py
+++ b/src/pyrolab/drivers/lasers/sachertec.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/lasers/tsl550.py b/src/pyrolab/drivers/lasers/tsl550.py
similarity index 99%
rename from pyrolab/drivers/lasers/tsl550.py
rename to src/pyrolab/drivers/lasers/tsl550.py
index e7e27ce..076c918 100644
--- a/pyrolab/drivers/lasers/tsl550.py
+++ b/src/pyrolab/drivers/lasers/tsl550.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -33,7 +31,7 @@
 
 from pyrolab.api import behavior, expose
 from pyrolab.drivers.lasers import Laser
-from pyrolab.errors import CommunicationError
+
 
 log = logging.getLogger(__name__)
 
@@ -185,7 +183,7 @@ def connect(self, port: str="", baudrate: int=9600, terminator: str="\r",
         try:
             self.device = serial.Serial(port, baudrate=baudrate, timeout=timeout)
         except serial.SerialException as e:
-            raise CommunicationError("Could not connect to laser on port " + port)
+            raise ConnectionError("Could not connect to laser on port " + port)
         self.device.flushInput()
         self.device.flushOutput()
         self.query_delay = query_delay
diff --git a/pyrolab/drivers/motion/__init__.py b/src/pyrolab/drivers/motion/__init__.py
similarity index 91%
rename from pyrolab/drivers/motion/__init__.py
rename to src/pyrolab/drivers/motion/__init__.py
index fee8c59..cff5d09 100644
--- a/pyrolab/drivers/motion/__init__.py
+++ b/src/pyrolab/drivers/motion/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/motion/kinesis/__init__.py b/src/pyrolab/drivers/motion/kinesis/__init__.py
similarity index 98%
rename from pyrolab/drivers/motion/kinesis/__init__.py
rename to src/pyrolab/drivers/motion/kinesis/__init__.py
index de7b2b4..a56f34d 100644
--- a/pyrolab/drivers/motion/kinesis/__init__.py
+++ b/src/pyrolab/drivers/motion/kinesis/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/motion/kinesis/bpc303.py b/src/pyrolab/drivers/motion/kinesis/bpc303.py
similarity index 99%
rename from pyrolab/drivers/motion/kinesis/bpc303.py
rename to src/pyrolab/drivers/motion/kinesis/bpc303.py
index 7e30122..8d69d2b 100644
--- a/pyrolab/drivers/motion/kinesis/bpc303.py
+++ b/src/pyrolab/drivers/motion/kinesis/bpc303.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -21,10 +19,9 @@
 from ctypes import c_char_p, c_int, c_short
 from typing import Any, Dict, List
 
-from Pyro5.server import oneway
 from thorlabs_kinesis import benchtop_piezo as bp
 
-from pyrolab.api import expose
+from pyrolab.api import expose, oneway
 from pyrolab.drivers.motion.kinesis import KinesisInstrument
 
 
diff --git a/pyrolab/drivers/motion/kinesis/kdc101.py b/src/pyrolab/drivers/motion/kinesis/kdc101.py
similarity index 98%
rename from pyrolab/drivers/motion/kinesis/kdc101.py
rename to src/pyrolab/drivers/motion/kinesis/kdc101.py
index 70fcc8a..983356c 100644
--- a/pyrolab/drivers/motion/kinesis/kdc101.py
+++ b/src/pyrolab/drivers/motion/kinesis/kdc101.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -38,20 +36,14 @@
     cast,
     create_string_buffer,
 )
-from ctypes.wintypes import DWORD, WORD
 from typing import Any, Dict, List
 
-from Pyro5.server import oneway
 from thorlabs_kinesis import kcube_dcservo as kcdc
 from thorlabs_kinesis._utils import c_dword, c_word
 
 from pyrolab.api import expose
 from pyrolab.drivers.motion.kinesis import ERROR_CODES, KinesisInstrument
-from pyrolab.drivers.motion.kinesis.exceptions import (
-    KinesisCommunicationError,
-    KinesisDLLError,
-    KinesisMotorError,
-)
+
 
 log = logging.getLogger(__name__)
 
@@ -62,11 +54,11 @@
 def check_error(status):
     if status != 0:
         if status > 0 and status <= 21:
-            raise KinesisCommunicationError(ERROR_CODES[status], errcode=status)
+            raise ConnectionError(f"{ERROR_CODES[status]} ({status})")
         elif (status >= 32 and status <= 36) or (status >= 41 and status <= 43):
-            raise KinesisDLLError(ERROR_CODES[status], errcode=status)
+            raise OSError(f"{ERROR_CODES[status]} ({status})")
         elif( status >= 37 and status <= 39) or (status >= 44 and status <= 47):
-            raise KinesisMotorError(ERROR_CODES[status], errcode=status)
+            raise RuntimeError(f"{ERROR_CODES[status]} ({status})")
         raise RuntimeError(ERROR_CODES[status])
 
 
@@ -91,7 +83,6 @@ def home(self, block=True):
 # message from previous non-blocking calls don't get consumed by new requestors.
 
 
-@expose
 class KDC101(KinesisInstrument):
     """
     A KCube DC Servo motor. 
@@ -726,7 +717,7 @@ def move_continuous(self, direction="forward"):
         status = kcdc.CC_MoveAtVelocity(self._serialno, direction)
         try:
             check_error(status)
-        except KinesisMotorError as e:
+        except RuntimeError as e:
             # If it's just an invalid position error, we can ignore it
             if e.errcode == 38:
                 pass
diff --git a/pyrolab/drivers/motion/max31x.py b/src/pyrolab/drivers/motion/max31x.py
similarity index 99%
rename from pyrolab/drivers/motion/max31x.py
rename to src/pyrolab/drivers/motion/max31x.py
index 411fadc..c0537f6 100644
--- a/pyrolab/drivers/motion/max31x.py
+++ b/src/pyrolab/drivers/motion/max31x.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/motion/prm1z8.py b/src/pyrolab/drivers/motion/prm1z8.py
similarity index 98%
rename from pyrolab/drivers/motion/prm1z8.py
rename to src/pyrolab/drivers/motion/prm1z8.py
index 38256d4..aa1bef8 100644
--- a/pyrolab/drivers/motion/prm1z8.py
+++ b/src/pyrolab/drivers/motion/prm1z8.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/motion/z8xx.py b/src/pyrolab/drivers/motion/z8xx.py
similarity index 99%
rename from pyrolab/drivers/motion/z8xx.py
rename to src/pyrolab/drivers/motion/z8xx.py
index 2306869..501abfe 100644
--- a/pyrolab/drivers/motion/z8xx.py
+++ b/src/pyrolab/drivers/motion/z8xx.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/sample.py b/src/pyrolab/drivers/sample.py
similarity index 99%
rename from pyrolab/drivers/sample.py
rename to src/pyrolab/drivers/sample.py
index ce2d8a5..9122732 100644
--- a/pyrolab/drivers/sample.py
+++ b/src/pyrolab/drivers/sample.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/scopes/VISAResourceExtentions.py b/src/pyrolab/drivers/scopes/VISAResourceExtentions.py
similarity index 99%
rename from pyrolab/drivers/scopes/VISAResourceExtentions.py
rename to src/pyrolab/drivers/scopes/VISAResourceExtentions.py
index 2473576..64e9ef3 100644
--- a/pyrolab/drivers/scopes/VISAResourceExtentions.py
+++ b/src/pyrolab/drivers/scopes/VISAResourceExtentions.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/scopes/__init__.py b/src/pyrolab/drivers/scopes/__init__.py
similarity index 92%
rename from pyrolab/drivers/scopes/__init__.py
rename to src/pyrolab/drivers/scopes/__init__.py
index 439bded..d791289 100644
--- a/pyrolab/drivers/scopes/__init__.py
+++ b/src/pyrolab/drivers/scopes/__init__.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/drivers/scopes/rohdeschwarz.py b/src/pyrolab/drivers/scopes/rohdeschwarz.py
similarity index 99%
rename from pyrolab/drivers/scopes/rohdeschwarz.py
rename to src/pyrolab/drivers/scopes/rohdeschwarz.py
index 359fdf7..40c3ba8 100644
--- a/pyrolab/drivers/scopes/rohdeschwarz.py
+++ b/src/pyrolab/drivers/scopes/rohdeschwarz.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/manager.py b/src/pyrolab/manager.py
similarity index 98%
rename from pyrolab/manager.py
rename to src/pyrolab/manager.py
index 71583eb..a286a60 100644
--- a/pyrolab/manager.py
+++ b/src/pyrolab/manager.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -26,7 +24,6 @@
 
 from __future__ import annotations
 
-import importlib
 import logging
 import multiprocessing
 import threading
@@ -34,7 +31,7 @@
 from datetime import datetime
 from multiprocessing import current_process
 from multiprocessing.queues import Queue
-from typing import TYPE_CHECKING, Dict, List, Tuple, Type
+from typing import TYPE_CHECKING, Dict, Tuple
 
 from Pyro5.core import locate_ns
 
@@ -51,7 +48,6 @@
         ServiceConfiguration,
     )
     from pyrolab.server import Daemon
-    from pyrolab.service import Service
 
 
 log = logging.getLogger(__name__)
@@ -222,9 +218,6 @@ def setup_daemon(self) -> Tuple[Daemon, Dict[str, URI]]:
         for sname, sconfig in self.serviceconfigs.items():
             log.info(f"Registering service '{sname}'")
             service = sconfig._get_service()
-
-            log.debug("Preparing daemon class")
-            service = daemon.prepare_class(service)
             
             log.debug("Getting service uri")
             uri = daemon.register(service)
diff --git a/pyrolab/nameserver.py b/src/pyrolab/nameserver.py
similarity index 95%
rename from pyrolab/nameserver.py
rename to src/pyrolab/nameserver.py
index 5fc8b83..a9d8489 100644
--- a/pyrolab/nameserver.py
+++ b/src/pyrolab/nameserver.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -18,15 +16,13 @@
 
 from Pyro5.nameserver import BroadcastServer, NameServerDaemon, start_ns
 
-from pyrolab import PYROLAB_DATA_DIR
+from pyrolab import NAMESERVER_STORAGE
 from pyrolab.configure import NameServerConfiguration
 
 log = logging.getLogger(__name__)
 
 
-NAMESERVER_DATA_DIR = PYROLAB_DATA_DIR / "nameserver" / "data"
-NAMESERVER_DATA_DIR.mkdir(parents=True, exist_ok=True)
-STORAGE_FILE = NAMESERVER_DATA_DIR / "storage"
+STORAGE_FILE = NAMESERVER_STORAGE / "storage"
 
 
 # # Inheriting from the Nameserver
diff --git a/pyrolab/pyrolabd.py b/src/pyrolab/pyrolabd.py
similarity index 99%
rename from pyrolab/pyrolabd.py
rename to src/pyrolab/pyrolabd.py
index 2ddd337..c5009bd 100644
--- a/pyrolab/pyrolabd.py
+++ b/src/pyrolab/pyrolabd.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/pyrolab/server.py b/src/pyrolab/server.py
similarity index 85%
rename from pyrolab/server.py
rename to src/pyrolab/server.py
index 5627454..14f930e 100644
--- a/pyrolab/server.py
+++ b/src/pyrolab/server.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -19,7 +17,7 @@
 
 import Pyro5
 from Pyro5.core import URI
-from Pyro5.server import behavior, expose, oneway, serve
+from Pyro5.server import expose
 
 if TYPE_CHECKING:
     from Pyro5.socketutil import SocketConnection
@@ -85,6 +83,14 @@ class Lockable:
     for use with local instruments. Additionally, any service registered with
     a :py:class:`LockableDaemon` will automatically have this mixin added to 
     it.
+
+    Examples
+    --------
+    .. code-block:: python
+
+        class MyCustomService(Service, Lockable):
+            def __init__(self, *args, **kwargs):
+                pass
     """
     def lock(self, user: str="") -> bool:
         """
@@ -165,15 +171,34 @@ class Daemon(Pyro5.server.Daemon):
     def __init__(self, *args, **kwargs) -> None:
         super().__init__(*args, **kwargs)
 
+    def register(self, obj_or_class, objectId=None, force=False, weak=False):
+        """
+        Register a Pyro object under the given id. 
+        
+        Note that this object is now only known inside this daemon, it is not
+        automatically available in a name server. This method returns a URI for
+        the registered object. Pyro checks if an object is already registered,
+        unless you set force=True. You can register a class or an object
+        (instance) directly. For a class, Pyro will create instances of it to
+        handle the remote calls according to the instance_mode (set via @expose
+        on the class). The default there is one object per session (=proxy
+        connection). If you register an object directly, Pyro will use that
+        single object for all remote calls. With weak=True, only weak reference
+        to the object will be stored, and the object will get unregistered from
+        the daemon automatically when garbage-collected.
+        """
+        obj_or_class = self._prepare_class(obj_or_class)
+        return super().register(obj_or_class, objectId=objectId, force=force, weak=weak)
+
     @staticmethod
-    def prepare_class(cls) -> Type[Service]:
+    def _prepare_class(cls) -> Type[Service]:
         """
         Performs any actions on the class required for the given Daemon.
 
         Some classes require mixins to be added in order for certain 
-        functionality to work. When the ProcessManager prepares to run a Daemon,
-        the child process will call this method on the class before registering
-        the class.
+        functionality to work. This method is called by 
+        :py:meth:`pyrolab.server.Daemon.register` before passing the 
+        registration on to the Pyro5 daemon.
 
         Parameters
         ----------
@@ -186,6 +211,27 @@ def prepare_class(cls) -> Type[Service]:
             The prepared class.
         """
         return cls
+    
+    @expose
+    def ping(self) -> bool:
+        """
+        Returns a bool (True) to indicate that the Daemon is alive and can be
+        communicated with.
+
+        Returns
+        -------
+        result : bool
+            True, meaning communication was established.
+        """
+        return True
+    
+    @expose
+    def pyrolab_version(self) -> str:
+        """
+        Return the version of PyroLab running the device.
+        """
+        from pyrolab import __version__
+        return __version__
 
 
 class LockableDaemon(Daemon):
@@ -233,7 +279,7 @@ def __init__(self, *args, **kwargs) -> None:
         self.locked_instances = {}
 
     @staticmethod
-    def prepare_class(cls) -> Type[Service]:
+    def _prepare_class(cls) -> Type[Service]:
         """
         Dynamically create a new class that is also based on Lockable.
 
@@ -248,6 +294,9 @@ def prepare_class(cls) -> Type[Service]:
         class
             A subclass that inherits from the original class and ``Lockable``.
         """
+        if issubclass(type(cls), Daemon):
+            return cls
+        
         DynamicLockable = type(
             cls.__name__ + "Lockable",
             (cls, Lockable, ),
@@ -255,6 +304,7 @@ def prepare_class(cls) -> Type[Service]:
         )
         return DynamicLockable
 
+
     def _lock(self, pyroId: str, conn: SocketConnection, user: str="") -> bool:
         """
         Logs a "lock" action on a Pyro object.
@@ -353,18 +403,6 @@ def release(self, uri: str) -> str:
         else:
             return True
 
-    @expose
-    def ping(self) -> str:
-        """
-        Returns a string to indicate that the Daemon is alive.
-
-        Returns
-        -------
-        result : str
-            A string to indicate that the Daemon is alive.
-        """
-        return "pong"
-
     def _getInstance(self, clazz, conn):
         """
         Find or create a new instance of the class.
@@ -385,7 +423,7 @@ def _getInstance(self, clazz, conn):
         
         Raises
         ------
-        Exception
+        ConnectionRefusedError
             If an instance exists but is locked by a different connection.
         """
         self._last_requestor = conn
@@ -395,7 +433,8 @@ def _getInstance(self, clazz, conn):
             if lock_owner is None or lock_owner == conn:
                 return obj
             if lock_owner != conn:
-                raise Exception(f"Pyro object is locked (by '{username or lock_owner}').")
+                raise ConnectionRefusedError(f"Pyro object is locked (by '{username or lock_owner}')")
+        return obj
 
     def clientDisconnect(self, conn):
         """
diff --git a/pyrolab/service.py b/src/pyrolab/service.py
similarity index 79%
rename from pyrolab/service.py
rename to src/pyrolab/service.py
index c23e58c..9205e64 100644
--- a/pyrolab/service.py
+++ b/src/pyrolab/service.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
@@ -16,6 +14,9 @@
 import logging
 from typing import Callable, Optional
 
+from pyrolab.server import expose
+
+
 log = logging.getLogger(__name__)
 
 
@@ -58,3 +59,24 @@ def set_behavior(cls, instance_mode: str="session", instance_creator: Optional[C
         if instance_creator and not callable(instance_creator):
             raise TypeError("instance_creator must be a callable")
         cls._pyroInstancing = (instance_mode, instance_creator)
+
+    @expose
+    def ping(self) -> bool:
+        """
+        Returns a bool (True) to indicate that the Daemon is alive and can be
+        communicated with.
+
+        Returns
+        -------
+        result : bool
+            True, meaning communication was established.
+        """
+        return True
+    
+    @expose
+    def pyrolab_version(self) -> str:
+        """
+        Return the version of PyroLab running the device.
+        """
+        from pyrolab import __version__
+        return __version__
diff --git a/pyrolab/utils.py b/src/pyrolab/utils.py
similarity index 97%
rename from pyrolab/utils.py
rename to src/pyrolab/utils.py
index f38cedb..278a831 100644
--- a/pyrolab/utils.py
+++ b/src/pyrolab/utils.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright © PyroLab Project Contributors
 # Licensed under the terms of the GNU GPLv3+ License
 # (see pyrolab/__init__.py for details)
diff --git a/src/pyromonitor/__init__.py b/src/pyromonitor/__init__.py
new file mode 100644
index 0000000..15b4784
--- /dev/null
+++ b/src/pyromonitor/__init__.py
@@ -0,0 +1,68 @@
+import os
+import sys
+import platform
+import pathlib
+
+from flask import Flask
+
+
+# Check if Python version is supported
+pyversion = sys.version_info
+if pyversion < (3, 7, 0):
+    raise Exception(
+        "PyroLab requires Python 3.7+ (version "
+        + platform.python_version()
+        + " detected)."
+    )
+
+
+# Metadata
+__name__ = "pyromonitor"
+__author__ = "BYU CamachoLab"
+__copyright__ = "Copyright 2023, The PyroLab Project"
+__version__ = "0.3.0"
+__license__ = "GPLv3+"
+__maintainer__ = "Sequoia Ploeg"
+__maintainer_email__ = "sequoiac@byu.edu"
+__status__ = "Development" # "Production"
+__project_url__ = "https://github.com/sequoiap/pyrolab"
+__forum_url__ = "https://github.com/sequoiap/pyrolab/issues"
+__website_url__ = "https://camacholab.byu.edu/"
+
+
+# Configuration directories
+# Old api deprecated in 3.11, new api added in 3.9
+if sys.version_info < (3, 9, 0):
+    base_path = pathlib.Path(__file__).resolve().parent
+else:
+    from importlib.resources import files
+    base_path = files("pyromonitor")
+base_path = base_path / "localdata"
+
+# Data directories
+DATA_DIR = pathlib.Path(base_path)
+DATA_DIR.mkdir(parents=True, exist_ok=True)
+CONFIG_FILE = DATA_DIR / "config.yaml"
+
+
+def create_app(test_config=None):
+    # create and configure the app
+    app = Flask(__name__)
+
+    if test_config is None:
+        # load the instance config, if it exists, when not testing
+        app.config.from_pyfile('config.py', silent=True)
+    else:
+        # load the test config if passed in
+        app.config.from_mapping(test_config)
+
+    # ensure the instance folder exists
+    try:
+        os.makedirs(app.instance_path)
+    except OSError:
+        pass
+    
+    from . import lister
+    app.register_blueprint(lister.bp)
+
+    return app
diff --git a/src/pyromonitor/cli.py b/src/pyromonitor/cli.py
new file mode 100644
index 0000000..a241411
--- /dev/null
+++ b/src/pyromonitor/cli.py
@@ -0,0 +1,77 @@
+# Copyright © PyroLab Project Contributors
+# Licensed under the terms of the GNU GPLv3+ License
+# (see pyrolab/__init__.py for details)
+
+"""
+Command Line Interface
+======================
+
+Usage: pyrolab [OPTIONS] COMMAND [ARGS]...
+
+Try ``pyrolab --help`` for help.
+"""
+import shutil
+from pathlib import Path
+
+import typer
+
+from pyromonitor import CONFIG_FILE
+from pyromonitor.config import CONFIG
+
+
+app = typer.Typer(no_args_is_help=True)
+
+
+def _version_callback(value: bool=True) -> None:
+    if value:
+        from pyromonitor import __version__
+        typer.echo(f"PyroMonitor {__version__}")
+        raise typer.Exit()
+
+@app.callback()
+def main(
+    version: bool = typer.Option(False, "--version", "-v", help="Show the version and exit.", callback=_version_callback, is_eager=True),
+):
+    return
+
+@app.command()
+def configure(
+    filename: str,
+):
+    """
+    Load a server configuration file.
+    """
+    filename = Path(filename)
+    if not filename.exists():
+        raise FileNotFoundError(f"File does not: '{filename}'")
+    shutil.copy(filename, CONFIG_FILE)
+
+@app.command()
+def up(
+    host: str = typer.Option(None, "--host", "-h", help="Host IP to run app on (default localhost)"),
+    port: int = typer.Option(None, "--port", "-p", help="Host port to run app on (default 8080)"),
+    nameserver: str = typer.Option(None, "--nameserver", "-n", help="Nameserver to use"),
+    ns_port: int = typer.Option(None, "--ns_port", "-p", help="Nameserver port"),
+):
+    """
+    Launch the monitoring web app.
+    """
+    if host:
+        CONFIG.host = host
+    if port:
+        CONFIG.port = port
+    if nameserver:
+        CONFIG.nameserver = nameserver
+    if ns_port:
+        CONFIG.ns_port = ns_port
+    
+    from pyromonitor import create_app
+    webapp = create_app()
+
+    from waitress import serve
+    uri = f'{CONFIG.host}:{CONFIG.port}'
+    
+    print(f"\n    Serving on http://{uri}/{CONFIG.subfolder}")
+    print(f"    (press CTRL+C to quit)\n")
+
+    serve(webapp, listen=uri, threads=4, clear_untrusted_proxy_headers=True)
diff --git a/src/pyromonitor/config.py b/src/pyromonitor/config.py
new file mode 100644
index 0000000..05cc4e0
--- /dev/null
+++ b/src/pyromonitor/config.py
@@ -0,0 +1,19 @@
+from omegaconf import OmegaConf
+
+from pyromonitor import CONFIG_FILE
+
+
+_defaultcfg = {
+    "host": "localhost",
+    "port": 8080,
+    "nameserver": "localhost",
+    "ns_port": 9090,
+    "polling": 300,
+    "subfolder": "",
+}
+
+
+CONFIG = OmegaConf.create(_defaultcfg)
+if CONFIG_FILE.exists():
+    userconf = OmegaConf.load(CONFIG_FILE)
+    CONFIG.merge_with(userconf)
diff --git a/src/pyromonitor/lister.py b/src/pyromonitor/lister.py
new file mode 100644
index 0000000..07b9a31
--- /dev/null
+++ b/src/pyromonitor/lister.py
@@ -0,0 +1,131 @@
+from __future__ import annotations
+
+import threading
+from typing import List, NamedTuple
+from datetime import datetime
+
+from pyrolab.api import locate_ns, Proxy
+from flask import Blueprint, flash, g, redirect, render_template, request, url_for
+
+from pyromonitor.config import CONFIG
+
+
+# List subapp
+bp = Blueprint('lister', __name__)
+
+
+# Globals
+_timer = None
+_status: List[InstrumentStatus] = []
+_last_updated = None
+
+AVAILABLE = "Available"
+LOCKED = "Locked"
+UNREACHABLE = "Unreachable"
+
+
+class InstrumentStatus(NamedTuple):
+    name: str
+    uri: str
+    description: str
+    version: str
+    status: str
+
+
+def time_elapsed_human_readable(start: datetime=None, end: datetime=None) -> str:
+    """
+    Return the time delta of two times (or one and now) in plain English.
+
+    Parameters
+    ----------
+    start : datetime
+        The start time.
+    end : datetime, optional
+        The end time. Defaults to now.
+
+    Returns
+    -------
+    str
+        The time delta in plain English.
+    """
+    if not start:
+        return "∞"
+    elif end:
+        delta = end - start
+    else:
+        delta = datetime.now() - start
+
+    if delta.days > 0:
+        return f"{delta.days} days"
+    elif delta.seconds > 3600:
+        return f"{delta.seconds // 3600} hours"
+    elif delta.seconds > 120:
+        return f"{delta.seconds // 60} minutes"
+    elif delta.seconds > 60:
+        return f"1 minute"
+    else:
+        return f"{delta.seconds} seconds"
+
+
+def reset_timer() -> None:
+    """
+    Reset the timer that triggers a status refresh.
+
+    Polling period is read from the configuration.
+    """
+    global _timer
+
+    _timer = threading.Timer(CONFIG.polling, refresh_status)
+    _timer.daemon = True
+    _timer.start()
+
+
+def refresh_status() -> None:
+    """
+    Refresh the availability status of every instrument known to the nameserver.
+
+    This function has side effects, as it updates global variables that are
+    made available to the page API.
+    """
+    global _last_updated
+    global _status
+
+    with locate_ns(CONFIG.nameserver) as ns:
+        instruments: dict = ns.list(return_metadata=True) # {name: (uri, description)}
+        instruments.pop("Pyro.NameServer") # no need to display the nameserver
+        
+    status = []
+    for name, (uri, description) in instruments.items():
+        try:
+            description = list(description)[0]
+        except:
+            description = 'Not available'
+        if len(description) > 80:
+            description = description[:77] + "..."
+        try:
+            with Proxy(ns.lookup(name)) as p:
+                version = p.pyrolab_version()
+                status.append(InstrumentStatus(name, uri, description, version, AVAILABLE))
+        except ConnectionRefusedError as e:
+            status.append(InstrumentStatus(name, uri, description, "", LOCKED))
+        except Exception as e:
+            status.append(InstrumentStatus(name, uri, description, "", UNREACHABLE))
+
+    _last_updated = datetime.now()
+    _status = status
+    reset_timer()
+
+
+@bp.route(f'/{CONFIG.subfolder}', methods=["GET"])
+def listall():
+    global _last_updated
+    global _status
+
+    updated = f"{time_elapsed_human_readable(_last_updated)} ago"
+    status = sorted([stat for stat in _status if stat.status in [AVAILABLE, LOCKED]], key=lambda x: x.name) + \
+             sorted([stat for stat in _status if stat.status == UNREACHABLE], key=lambda x: x.name)
+
+    return render_template('base.html', status=status, update_time=updated)
+
+
+refresh_status()
diff --git a/src/pyromonitor/static/style.css b/src/pyromonitor/static/style.css
new file mode 100644
index 0000000..a5478c2
--- /dev/null
+++ b/src/pyromonitor/static/style.css
@@ -0,0 +1,19 @@
+.abbreviated-description {
+  max-width: 25%;
+  display: inline-block;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+.bg-blue {
+  background: linear-gradient(to right, #2980b9, #2c3e50);
+}
+.center {
+  text-align: center;
+}
+.text-font {
+  font-family: 'Outfit', sans-serif;
+}
+.title {
+  font-size: 3em;
+}
\ No newline at end of file
diff --git a/src/pyromonitor/templates/base.html b/src/pyromonitor/templates/base.html
new file mode 100644
index 0000000..fc60585
--- /dev/null
+++ b/src/pyromonitor/templates/base.html
@@ -0,0 +1,91 @@
+<!doctype html>
+<html>
+  <head>
+    <title>{% block title %}{% endblock %} PyroLab Monitor</title>
+    <!-- <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> -->
+    <link rel="stylesheet" href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css">
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300&display=swap" rel="stylesheet">
+    <style>
+      .abbreviated-description {
+        max-width: 25%;
+        display: inline-block;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+      .bg-blue {
+        background: linear-gradient(to right, #2980b9, #2c3e50);
+      }
+      .center {
+        text-align: center;
+      }
+      .text-font {
+        font-family: 'Outfit', sans-serif;
+      }
+      .title {
+        font-size: 3em;
+      }
+    </style>
+  </head>
+  <!-- Table based off https://bbbootstrap.com/snippets/tailwind-css-responsive-table-45362330# -->
+  <body class="bg-blue">
+    <nav class="center">
+      <h1 class="text-font title">PyroLab Monitor</h1>
+    </nav>
+    <section class="content">
+      {% for message in get_flashed_messages() %}
+        <div class="flash">{{ message }}</div>
+      {% endfor %}
+
+      <p class="center text-font">Last updated: {{ update_time }} </p>
+
+      <div class="min-h-screen py-5">
+        <div class='overflow-x-auto w-full'>
+          <table class='mx-auto max-w-4xl w-full whitespace-nowrap rounded-lg bg-white divide-y divide-gray-300 overflow-hidden'>
+            <thead class="bg-gray-900">
+                <tr class="text-white text-left">
+                    <th class="font-semibold text-sm uppercase px-6 py-4"> Name </th>
+                    <th class="font-semibold text-sm uppercase px-6 py-4 text-center"> Status </th>
+                    <th class="font-semibold text-sm uppercase px-6 py-4"> Description </th>
+                </tr>
+            </thead>
+            <tbody class="divide-y divide-gray-200">
+              {% for instrument in status  %}
+                <tr>
+                  <td class="px-6 py-4">
+                    <div class="flex items-center space-x-3">
+                      <div>
+                          <p> {{ instrument.name }} </p>
+                          <p class="text-gray-500 text-sm font-semibold tracking-wide"> PYROLAB {{ instrument.version }} </p>
+                      </div>
+                    </div>
+                  </td>
+                  {% if instrument.status == "Available" %}
+                  <td class="px-6 py-4 text-center"> <span class="text-white text-sm w-1/3 pb-1 bg-green-600 font-semibold px-2 rounded-full"> {{ instrument.status }} </span> </td>
+                  {% elif instrument.status == "Locked" %}
+                  <td class="px-6 py-4 text-center"> <span class="text-white text-sm w-1/3 pb-1 bg-red-600 font-semibold px-2 rounded-full"> {{ instrument.status }} </span> </td>
+                  {% else %}
+                  <td class="px-6 py-4 text-center"> <span class="text-white text-sm w-1/3 pb-1 bg-yellow-600 font-semibold px-2 rounded-full"> {{ instrument.status }} </span> </td>
+                  {% endif %}
+                  <td class="px-6 py-4">
+                    <p class=""> {{ instrument.description }} </p>
+                    <p class="text-gray-500 text-sm font-semibold tracking-wide"> {{ instrument.uri }} </p>
+                  </td>
+                </tr>
+              {% endfor %}
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </section>
+
+    <footer class="center">
+      <p>BYU CamachoLab</p>
+    </footer>
+  </body>
+  
+  <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
+</html>
\ No newline at end of file


Test patch:
diff --git a/pyrolab/drivers/motion/kinesis/tests/test_bpc303.py b/src/pyrolab/drivers/motion/kinesis/tests/test_bpc303.py
similarity index 100%
rename from pyrolab/drivers/motion/kinesis/tests/test_bpc303.py
rename to src/pyrolab/drivers/motion/kinesis/tests/test_bpc303.py
diff --git a/tests/data/config.yaml b/tests/pyrolab/data/config.yaml
similarity index 100%
rename from tests/data/config.yaml
rename to tests/pyrolab/data/config.yaml
diff --git a/tests/data/devconfig.yaml b/tests/pyrolab/data/devconfig.yaml
similarity index 100%
rename from tests/data/devconfig.yaml
rename to tests/pyrolab/data/devconfig.yaml
diff --git a/tests/drivers/lasers/test_tsl550.py b/tests/pyrolab/drivers/lasers/test_tsl550.py
similarity index 100%
rename from tests/drivers/lasers/test_tsl550.py
rename to tests/pyrolab/drivers/lasers/test_tsl550.py
diff --git a/tests/drivers/motion/test_nanomax300.py b/tests/pyrolab/drivers/motion/test_nanomax300.py
similarity index 100%
rename from tests/drivers/motion/test_nanomax300.py
rename to tests/pyrolab/drivers/motion/test_nanomax300.py
diff --git a/tests/drivers/test_sample.py b/tests/pyrolab/drivers/test_sample.py
similarity index 100%
rename from tests/drivers/test_sample.py
rename to tests/pyrolab/drivers/test_sample.py
diff --git a/tests/include_drivers.py b/tests/pyrolab/include_drivers.py
similarity index 100%
rename from tests/include_drivers.py
rename to tests/pyrolab/include_drivers.py
diff --git a/tests/test_code_interface.py b/tests/pyrolab/test_code_interface.py
similarity index 100%
rename from tests/test_code_interface.py
rename to tests/pyrolab/test_code_interface.py
diff --git a/tests/test_configure.py b/tests/pyrolab/test_configure.py
similarity index 100%
rename from tests/test_configure.py
rename to tests/pyrolab/test_configure.py
diff --git a/tests/test_daemon.py b/tests/pyrolab/test_daemon.py
similarity index 100%
rename from tests/test_daemon.py
rename to tests/pyrolab/test_daemon.py
diff --git a/tests/test_errors.py b/tests/pyrolab/test_errors.py
similarity index 100%
rename from tests/test_errors.py
rename to tests/pyrolab/test_errors.py
diff --git a/tests/test_manager.py b/tests/pyrolab/test_manager.py
similarity index 100%
rename from tests/test_manager.py
rename to tests/pyrolab/test_manager.py
diff --git a/tests/test_nameserver.py b/tests/pyrolab/test_nameserver.py
similarity index 100%
rename from tests/test_nameserver.py
rename to tests/pyrolab/test_nameserver.py
diff --git a/tests/test_pyrolabd.py b/tests/pyrolab/test_pyrolabd.py
similarity index 100%
rename from tests/test_pyrolabd.py
rename to tests/pyrolab/test_pyrolabd.py
diff --git a/tests/test_service.py b/tests/pyrolab/test_service.py
similarity index 100%
rename from tests/test_service.py
rename to tests/pyrolab/test_service.py
diff --git a/tests/test_utils.py b/tests/pyrolab/test_utils.py
similarity index 100%
rename from tests/test_utils.py
rename to tests/pyrolab/test_utils.py
diff --git a/tests/testsrv.py b/tests/pyrolab/testsrv.py
similarity index 100%
rename from tests/testsrv.py
rename to tests/pyrolab/testsrv.py
diff --git a/tests/pyromonitor/test_app.py b/tests/pyromonitor/test_app.py
new file mode 100644
index 0000000..e69de29