diff --git a/.copier-answers.yml b/.copier-answers.yml
new file mode 100644
index 00000000..d4625839
--- /dev/null
+++ b/.copier-answers.yml
@@ -0,0 +1,14 @@
+# Changes here will be overwritten by Copier
+_commit: 1.0.2-87-g16b7b0e
+_src_path: gh:DiamondLightSource/python-copier-template
+author_email: giles.knap@diamond.ac.uk
+author_name: Giles Knap
+component_owner: group:default/sscc
+description: A GUI for low level interaction with turbo pmacs and power pmacs
+distribution_name: dls-pmac-control
+docker: true
+docs_type: sphinx
+git_platform: github.com
+github_org: DiamondLightSource
+package_name: dls_pmac_control
+repo_name: dls-pmac-control
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..79b85ff4
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,46 @@
+// For format details, see https://containers.dev/implementors/json_reference/
+{
+ "name": "Python 3 Developer Container",
+ "build": {
+ "dockerfile": "../Dockerfile",
+ "target": "developer"
+ },
+ "remoteEnv": {
+ // Allow X11 apps to run inside the container
+ "DISPLAY": "${localEnv:DISPLAY}"
+ },
+ "customizations": {
+ "vscode": {
+ // Set *default* container specific settings.json values on container create.
+ "settings": {
+ "python.defaultInterpreterPath": "/venv/bin/python"
+ },
+ // Add the IDs of extensions you want installed when the container is created.
+ "extensions": [
+ "ms-python.python",
+ "github.vscode-github-actions",
+ "tamasfe.even-better-toml",
+ "redhat.vscode-yaml",
+ "ryanluker.vscode-coverage-gutters",
+ "charliermarsh.ruff",
+ "ms-azuretools.vscode-docker"
+ ]
+ }
+ },
+ "features": {
+ // Some default things like git config
+ "ghcr.io/devcontainers/features/common-utils:2": {
+ "upgradePackages": false
+ }
+ },
+ "runArgs": [
+ // Allow the container to access the host X11 display and EPICS CA
+ "--net=host",
+ // Make sure SELinux does not disable with access to host filesystems like tmp
+ "--security-opt=label=disable"
+ ],
+ // Mount the parent as /workspaces so we can pip install peers as editable
+ "workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind",
+ // After the container is created, install the python project in editable form
+ "postCreateCommand": "pip install $([ -f dev-requirements.txt ] && echo '-c dev-requirements.txt') -e '.[dev]' && pre-commit install"
+}
\ No newline at end of file
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 00000000..892c71ee
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,27 @@
+# Contribute to the project
+
+Contributions and issues are most welcome! All issues and pull requests are
+handled through [GitHub](https://github.com/DiamondLightSource/dls-pmac-control/issues). Also, please check for any existing issues before
+filing a new one. If you have a great idea but it involves big changes, please
+file a ticket before making a pull request! We want to make sure you don't spend
+your time coding something that might not fit the scope of the project.
+
+## Issue or Discussion?
+
+Github also offers [discussions](https://github.com/DiamondLightSource/dls-pmac-control/discussions) as a place to ask questions and share ideas. If
+your issue is open ended and it is not obvious when it can be "closed", please
+raise it as a discussion instead.
+
+## Code Coverage
+
+While 100% code coverage does not make a library bug-free, it significantly
+reduces the number of easily caught bugs! Please make sure coverage remains the
+same or is improved by a pull request!
+
+## Developer Information
+
+It is recommended that developers use a [vscode devcontainer](https://code.visualstudio.com/docs/devcontainers/containers). This repository contains configuration to set up a containerized development environment that suits its own needs.
+
+This project was created using the [Diamond Light Source Copier Template](https://github.com/DiamondLightSource/python-copier-template) for Python projects.
+The template's [Developer Guide](https://diamondlightsource.github.io/python-copier-template) contains detailed information on setting up a development environment, running the tests and what standards the code and documentation
+should follow.
diff --git a/.github/actions/install_requirements/action.yml b/.github/actions/install_requirements/action.yml
new file mode 100644
index 00000000..d33e0805
--- /dev/null
+++ b/.github/actions/install_requirements/action.yml
@@ -0,0 +1,34 @@
+name: Install requirements
+description: Install a version of python then call pip install and report what was installed
+inputs:
+ python-version:
+ description: Python version to install, default is from Dockerfile
+ default: "dev"
+ pip-install:
+ description: Parameters to pass to pip install
+ default: "$([ -f dev-requirements.txt ] && echo '-c dev-requirements.txt') -e .[dev]"
+
+runs:
+ using: composite
+ steps:
+ - name: Get version of python
+ run: |
+ PYTHON_VERSION="${{ inputs.python-version }}"
+ if [ $PYTHON_VERSION == "dev" ]; then
+ PYTHON_VERSION=$(sed -n "s/ARG PYTHON_VERSION=//p" Dockerfile)
+ fi
+ echo "PYTHON_VERSION=$PYTHON_VERSION" >> "$GITHUB_ENV"
+ shell: bash
+
+ - name: Setup python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+
+ - name: Install packages
+ run: pip install ${{ inputs.pip-install }}
+ shell: bash
+
+ - name: Report what was installed
+ run: pip freeze
+ shell: bash
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..184ba363
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,24 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ groups:
+ actions:
+ patterns:
+ - "*"
+
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ groups:
+ dev-dependencies:
+ patterns:
+ - "*"
diff --git a/.github/pages/index.html b/.github/pages/index.html
new file mode 100644
index 00000000..80f0a009
--- /dev/null
+++ b/.github/pages/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Redirecting to main branch
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.github/pages/make_switcher.py b/.github/pages/make_switcher.py
new file mode 100755
index 00000000..e2c8e6f6
--- /dev/null
+++ b/.github/pages/make_switcher.py
@@ -0,0 +1,92 @@
+import json
+import logging
+from argparse import ArgumentParser
+from pathlib import Path
+from subprocess import CalledProcessError, check_output
+from typing import List, Optional
+
+
+def report_output(stdout: bytes, label: str) -> List[str]:
+ ret = stdout.decode().strip().split("\n")
+ print(f"{label}: {ret}")
+ return ret
+
+
+def get_branch_contents(ref: str) -> List[str]:
+ """Get the list of directories in a branch."""
+ stdout = check_output(["git", "ls-tree", "-d", "--name-only", ref])
+ return report_output(stdout, "Branch contents")
+
+
+def get_sorted_tags_list() -> List[str]:
+ """Get a list of sorted tags in descending order from the repository."""
+ stdout = check_output(["git", "tag", "-l", "--sort=-v:refname"])
+ return report_output(stdout, "Tags list")
+
+
+def get_versions(ref: str, add: Optional[str]) -> List[str]:
+ """Generate the file containing the list of all GitHub Pages builds."""
+ # Get the directories (i.e. builds) from the GitHub Pages branch
+ try:
+ builds = set(get_branch_contents(ref))
+ except CalledProcessError:
+ builds = set()
+ logging.warning(f"Cannot get {ref} contents")
+
+ # Add and remove from the list of builds
+ if add:
+ builds.add(add)
+
+ # Get a sorted list of tags
+ tags = get_sorted_tags_list()
+
+ # Make the sorted versions list from main branches and tags
+ versions: List[str] = []
+ for version in ["master", "main"] + tags:
+ if version in builds:
+ versions.append(version)
+ builds.remove(version)
+
+ # Add in anything that is left to the bottom
+ versions += sorted(builds)
+ print(f"Sorted versions: {versions}")
+ return versions
+
+
+def write_json(path: Path, repository: str, versions: str):
+ org, repo_name = repository.split("/")
+ struct = [
+ {"version": version, "url": f"https://{org}.github.io/{repo_name}/{version}/"}
+ for version in versions
+ ]
+ text = json.dumps(struct, indent=2)
+ print(f"JSON switcher:\n{text}")
+ path.write_text(text, encoding="utf-8")
+
+
+def main(args=None):
+ parser = ArgumentParser(
+ description="Make a versions.json file from gh-pages directories"
+ )
+ parser.add_argument(
+ "--add",
+ help="Add this directory to the list of existing directories",
+ )
+ parser.add_argument(
+ "repository",
+ help="The GitHub org and repository name: ORG/REPO",
+ )
+ parser.add_argument(
+ "output",
+ type=Path,
+ help="Path of write switcher.json to",
+ )
+ args = parser.parse_args(args)
+
+ # Write the versions file
+ versions = get_versions("origin/gh-pages", args.add)
+ write_json(args.output, args.repository, versions)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/workflows/_check.yml b/.github/workflows/_check.yml
new file mode 100644
index 00000000..a6139c19
--- /dev/null
+++ b/.github/workflows/_check.yml
@@ -0,0 +1,27 @@
+on:
+ workflow_call:
+ outputs:
+ branch-pr:
+ description: The PR number if the branch is in one
+ value: ${{ jobs.pr.outputs.branch-pr }}
+
+jobs:
+ pr:
+ runs-on: "ubuntu-latest"
+ outputs:
+ branch-pr: ${{ steps.script.outputs.result }}
+ steps:
+ - uses: actions/github-script@v7
+ id: script
+ if: github.event_name == 'push'
+ with:
+ script: |
+ const prs = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ head: context.repo.owner + ':${{ github.ref_name }}'
+ })
+ if (prs.data.length) {
+ console.log(`::notice ::Skipping CI on branch push as it is already run in PR #${prs.data[0]["number"]}`)
+ return prs.data[0]["number"]
+ }
diff --git a/.github/workflows/_container.yml b/.github/workflows/_container.yml
new file mode 100644
index 00000000..4857ee9e
--- /dev/null
+++ b/.github/workflows/_container.yml
@@ -0,0 +1,56 @@
+on:
+ workflow_call:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ # Need this to get version number from last tag
+ fetch-depth: 0
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to GitHub Docker Registry
+ if: github.event_name != 'pull_request'
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and export to Docker local cache
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ # Need load and tags so we can test it below
+ load: true
+ tags: tag_for_testing
+
+ - name: Test cli works in cached runtime image
+ run: docker run --rm tag_for_testing --version
+
+ - name: Create tags for publishing image
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ghcr.io/${{ github.repository }}
+ tags: |
+ type=ref,event=tag
+ type=raw,value=latest
+
+ - name: Push cached image to container registry
+ if: github.ref_type == 'tag'
+ uses: docker/build-push-action@v5
+ # This does not build the image again, it will find the image in the
+ # Docker cache and publish it
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/_dist.yml b/.github/workflows/_dist.yml
new file mode 100644
index 00000000..b1c4c93c
--- /dev/null
+++ b/.github/workflows/_dist.yml
@@ -0,0 +1,36 @@
+on:
+ workflow_call:
+
+jobs:
+ build:
+ runs-on: "ubuntu-latest"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ # Need this to get version number from last tag
+ fetch-depth: 0
+
+ - name: Build sdist and wheel
+ run: >
+ export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) &&
+ pipx run build
+
+ - name: Upload sdist and wheel as artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist
+
+ - name: Check for packaging errors
+ run: pipx run twine check --strict dist/*
+
+ - name: Install produced wheel
+ uses: ./.github/actions/install_requirements
+ with:
+ pip-install: dist/*.whl
+
+ - name: Test module --version works using the installed wheel
+ # If more than one module in src/ replace with module name to test
+ run: python -m $(ls --hide='*.egg-info' src | head -1) --version
diff --git a/.github/workflows/_docs.yml b/.github/workflows/_docs.yml
new file mode 100644
index 00000000..40446e33
--- /dev/null
+++ b/.github/workflows/_docs.yml
@@ -0,0 +1,54 @@
+on:
+ workflow_call:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Avoid git conflicts when tag and branch pushed at same time
+ if: github.ref_type == 'tag'
+ run: sleep 60
+
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ # Need this to get version number from last tag
+ fetch-depth: 0
+
+ - name: Install system packages
+ run: sudo apt-get install graphviz
+
+ - name: Install python packages
+ uses: ./.github/actions/install_requirements
+
+ - name: Build docs
+ run: tox -e docs
+
+ - name: Remove environment.pickle
+ run: rm build/html/.doctrees/environment.pickle
+
+ - name: Upload built docs artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: docs
+ path: build
+
+ - name: Sanitize ref name for docs version
+ run: echo "DOCS_VERSION=${GITHUB_REF_NAME//[^A-Za-z0-9._-]/_}" >> $GITHUB_ENV
+
+ - name: Move to versioned directory
+ run: mv build/html .github/pages/$DOCS_VERSION
+
+ - name: Write switcher.json
+ run: python .github/pages/make_switcher.py --add $DOCS_VERSION ${{ github.repository }} .github/pages/switcher.json
+
+ - name: Publish Docs to gh-pages
+ if: github.ref_type == 'tag' || github.ref_name == 'main'
+ # We pin to the SHA, not the tag, for security reasons.
+ # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions
+ uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: .github/pages
+ keep_files: true
\ No newline at end of file
diff --git a/.github/workflows/_pypi.yml b/.github/workflows/_pypi.yml
new file mode 100644
index 00000000..f2ead1bc
--- /dev/null
+++ b/.github/workflows/_pypi.yml
@@ -0,0 +1,22 @@
+on:
+ workflow_call:
+ secrets:
+ PYPI_TOKEN:
+ required: true
+
+jobs:
+ upload:
+ runs-on: ubuntu-latest
+ environment: release
+
+ steps:
+ - name: Download dist artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: dist
+ path: dist
+
+ - name: Publish to PyPI using trusted publishing
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ password: ${{ secrets.PYPI_TOKEN }}
diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml
new file mode 100644
index 00000000..b49fa7dc
--- /dev/null
+++ b/.github/workflows/_release.yml
@@ -0,0 +1,32 @@
+on:
+ workflow_call:
+
+jobs:
+ artifacts:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ merge-multiple: true
+
+ - name: Zip up docs
+ run: |
+ set -vxeuo pipefail
+ if [ -d html ]; then
+ mv html $GITHUB_REF_NAME
+ zip -r docs.zip $GITHUB_REF_NAME
+ rm -rf $GITHUB_REF_NAME
+ fi
+
+ - name: Create GitHub Release
+ # We pin to the SHA, not the tag, for security reasons.
+ # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions
+ uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
+ with:
+ prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }}
+ files: "*"
+ generate_release_notes: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml
new file mode 100644
index 00000000..9b1f21f2
--- /dev/null
+++ b/.github/workflows/_test.yml
@@ -0,0 +1,62 @@
+on:
+ workflow_call:
+ inputs:
+ python-version:
+ type: string
+ description: The version of python to install
+ required: true
+ runs-on:
+ type: string
+ description: The runner to run this job on
+ required: true
+ secrets:
+ CODECOV_TOKEN:
+ required: true
+
+env:
+ # https://github.com/pytest-dev/pytest/issues/2042
+ PY_IGNORE_IMPORTMISMATCH: "1"
+
+jobs:
+ run:
+ runs-on: ${{ inputs.runs-on }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ # Need this to get version number from last tag
+ fetch-depth: 0
+
+ - if: inputs.python-version == 'dev'
+ name: Install dev versions of python packages
+ uses: ./.github/actions/install_requirements
+
+ - if: inputs.python-version == 'dev'
+ name: Write the requirements as an artifact
+ run: pip freeze --exclude-editable > /tmp/dev-requirements.txt
+
+ - if: inputs.python-version == 'dev'
+ name: Upload dev-requirements.txt
+ uses: actions/upload-artifact@v4
+ with:
+ name: dev-requirements
+ path: /tmp/dev-requirements.txt
+
+ - if: inputs.python-version != 'dev'
+ name: Install latest versions of python packages
+ uses: ./.github/actions/install_requirements
+ with:
+ python-version: ${{ inputs.python-version }}
+ pip-install: ".[dev]"
+
+ - name: Run tests
+ run: tox -e pytest
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ name: ${{ inputs.python-version }}/${{ inputs.runs-on }}
+ files: cov.xml
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/_tox.yml b/.github/workflows/_tox.yml
new file mode 100644
index 00000000..a13536d3
--- /dev/null
+++ b/.github/workflows/_tox.yml
@@ -0,0 +1,22 @@
+on:
+ workflow_call:
+ inputs:
+ tox:
+ type: string
+ description: What to run under tox
+ required: true
+
+
+jobs:
+ run:
+ runs-on: "ubuntu-latest"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install python packages
+ uses: ./.github/actions/install_requirements
+
+ - name: Run tox
+ run: tox -e ${{ inputs.tox }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..c5610ddc
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,68 @@
+name: CI
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ check:
+ uses: ./.github/workflows/_check.yml
+
+ lint:
+ needs: check
+ if: needs.check.outputs.branch-pr == ''
+ uses: ./.github/workflows/_tox.yml
+ with:
+ tox: pre-commit,pyright
+
+ test:
+ needs: check
+ if: needs.check.outputs.branch-pr == ''
+ strategy:
+ matrix:
+ runs-on: ["ubuntu-latest"] # can add windows-latest, macos-latest
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
+ include:
+ # Include one that runs in the dev environment
+ - runs-on: "ubuntu-latest"
+ python-version: "dev"
+ fail-fast: false
+ uses: ./.github/workflows/_test.yml
+ with:
+ runs-on: ${{ matrix.runs-on }}
+ python-version: ${{ matrix.python-version }}
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+
+ container:
+ needs: check
+ if: needs.check.outputs.branch-pr == ''
+ uses: ./.github/workflows/_container.yml
+ permissions:
+ packages: write
+
+ docs:
+ needs: check
+ if: needs.check.outputs.branch-pr == ''
+ uses: ./.github/workflows/_docs.yml
+
+ dist:
+ needs: check
+ if: needs.check.outputs.branch-pr == ''
+ uses: ./.github/workflows/_dist.yml
+
+ pypi:
+ if: github.ref_type == 'tag'
+ needs: dist
+ uses: ./.github/workflows/_pypi.yml
+ permissions:
+ id-token: write
+ secrets:
+ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
+
+ release:
+ if: github.ref_type == 'tag'
+ needs: [dist, docs]
+ uses: ./.github/workflows/_release.yml
+ permissions:
+ contents: write
diff --git a/.github/workflows/periodic.yml b/.github/workflows/periodic.yml
new file mode 100644
index 00000000..e2a0fd1b
--- /dev/null
+++ b/.github/workflows/periodic.yml
@@ -0,0 +1,13 @@
+name: Periodic
+
+on:
+ workflow_dispatch:
+ schedule:
+ # Run weekly to check URL links still resolve
+ - cron: "0 8 * * WED"
+
+jobs:
+ linkcheck:
+ uses: ./.github/workflows/_tox.yml
+ with:
+ tox: docs build -- -b linkcheck
diff --git a/.gitignore b/.gitignore
index de048497..2593ec75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,28 +1,70 @@
-*~
-.cproject
-.project
-.vscode
-O.*
-bin
-include
-lib
-db
-dbd
-data
-iocs/lab
-iocs/gui
-iocs/lab-ppmac
-.idea
-cmake-build-debug/
-*.pyc
-deleteme
-.pytest_cache
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+**/_version.py
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
.cache
-*.out
-*.o
+nosetests.xml
+coverage.xml
+cov.xml
+.pytest_cache/
+.mypy_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
*.log
-#etc/PowerClipper/
-#etc/LabPowerPmac/
-#etc/LabPowerPmac_Backup/
-.nfs*
-**/venv27
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# likely venv names
+.venv*
+venv*
+
+# further build artifacts
+lockfiles/
+
+# ruff cache
+.ruff_cache/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..5a4cbf7b
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,23 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.5.0
+ hooks:
+ - id: check-added-large-files
+ - id: check-yaml
+ - id: check-merge-conflict
+
+ - repo: local
+ hooks:
+ - id: ruff
+ name: lint with ruff
+ language: system
+ entry: ruff check --force-exclude
+ types: [python]
+ require_serial: true
+
+ - id: ruff-format
+ name: format with ruff
+ language: system
+ entry: ruff format --force-exclude
+ types: [python]
+ require_serial: true
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..66ad6324
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "ms-vscode-remote.remote-containers",
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..fe2b6dc3
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,19 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Debug Unit Test",
+ "type": "python",
+ "request": "launch",
+ "justMyCode": false,
+ "program": "${file}",
+ "purpose": [
+ "debug-test"
+ ],
+ "console": "integratedTerminal",
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..c129d991
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "python.testing.unittestEnabled": false,
+ "python.testing.pytestEnabled": true,
+ "editor.formatOnSave": true,
+ "editor.codeActionsOnSave": {
+ "source.organizeImports": "explicit"
+ },
+ "[python]": {
+ "editor.defaultFormatter": "charliermarsh.ruff",
+ },
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..946e69d4
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,16 @@
+// See https://go.microsoft.com/fwlink/?LinkId=733558
+// for the documentation about the tasks.json format
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "shell",
+ "label": "Tests, lint and docs",
+ "command": "tox -p",
+ "options": {
+ "cwd": "${workspaceRoot}"
+ },
+ "problemMatcher": [],
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..6c754acc
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+# The devcontainer should use the developer target and run as root with podman
+# or docker with user namespaces.
+ARG PYTHON_VERSION=3.11
+FROM python:${PYTHON_VERSION} as developer
+
+# Add any system dependencies for the developer/build environment here
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ graphviz \
+ && rm -rf /var/lib/apt/lists/*
+
+# Set up a virtual environment and put it in PATH
+RUN python -m venv /venv
+ENV PATH=/venv/bin:$PATH
+
+# The build stage installs the context into the venv
+FROM developer as build
+COPY . /context
+WORKDIR /context
+RUN pip install .
+
+# The runtime stage copies the built venv into a slim runtime container
+FROM python:${PYTHON_VERSION}-slim as runtime
+# Add apt-get system dependecies for runtime here if needed
+COPY --from=build /venv/ /venv/
+ENV PATH=/venv/bin:$PATH
+
+# change this entrypoint if it is not the same as the repo
+ENTRYPOINT ["dls-pmac-control"]
+CMD ["--version"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..8dada3ed
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 9e7adae7..46fbd4a3 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,19 @@
-# pmac
-EPICS driver and support module for the Delta Tau PMAC motion controller family.
+[![CI](https://github.com/DiamondLightSource/dls-pmac-control/actions/workflows/ci.yml/badge.svg)](https://github.com/DiamondLightSource/dls-pmac-control/actions/workflows/ci.yml)
+[![Coverage](https://codecov.io/gh/DiamondLightSource/dls-pmac-control/branch/main/graph/badge.svg)](https://codecov.io/gh/DiamondLightSource/dls-pmac-control)
+[![PyPI](https://img.shields.io/pypi/v/dls-pmac-control.svg)](https://pypi.org/project/dls-pmac-control)
+[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
+
+# dls_pmac_control
+
+A GUI for low level interaction with turbo pmacs and power pmacs
+
+
+Source |
+:---: | :---:
+PyPI | `pip install dls-pmac-control`
+Documentation |
+Releases |
+
+
+
+See https://diamondlightsource.github.io/dls-pmac-control for more detailed documentation.
diff --git a/catalog-info.yaml b/catalog-info.yaml
new file mode 100644
index 00000000..062f54ee
--- /dev/null
+++ b/catalog-info.yaml
@@ -0,0 +1,10 @@
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: dls-pmac-control
+ title: dls-pmac-control
+ description: A GUI for low level interaction with turbo pmacs and power pmacs
+spec:
+ type: documentation
+ lifecycle: experimental
+ owner: group:default/sscc
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..f254507c
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,188 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+import sys
+from pathlib import Path
+from subprocess import check_output
+
+import requests
+
+import dls_pmac_control
+
+# -- General configuration ------------------------------------------------
+
+# General information about the project.
+project = "dls-pmac-control"
+
+# The full version, including alpha/beta/rc tags.
+release = dls_pmac_control.__version__
+
+# The short X.Y version.
+if "+" in release:
+ # Not on a tag, use branch name
+ root = Path(__file__).absolute().parent.parent
+ git_branch = check_output("git branch --show-current".split(), cwd=root)
+ version = git_branch.decode().strip()
+else:
+ version = release
+
+extensions = [
+ # Use this for generating API docs
+ "sphinx.ext.autodoc",
+ # This can parse google style docstrings
+ "sphinx.ext.napoleon",
+ # For linking to external sphinx documentation
+ "sphinx.ext.intersphinx",
+ # Add links to source code in API docs
+ "sphinx.ext.viewcode",
+ # Adds the inheritance-diagram generation directive
+ "sphinx.ext.inheritance_diagram",
+ # Add a copy button to each code block
+ "sphinx_copybutton",
+ # For the card element
+ "sphinx_design",
+ # So we can write markdown files
+ "myst_parser",
+]
+
+# So we can use the ::: syntax
+myst_enable_extensions = ["colon_fence"]
+
+# If true, Sphinx will warn about all references where the target cannot
+# be found.
+nitpicky = True
+
+# A list of (type, target) tuples (by default empty) that should be ignored when
+# generating warnings in "nitpicky mode". Note that type should include the
+# domain name if present. Example entries would be ('py:func', 'int') or
+# ('envvar', 'LD_LIBRARY_PATH').
+nitpick_ignore = [
+ ("py:class", "NoneType"),
+ ("py:class", "'str'"),
+ ("py:class", "'float'"),
+ ("py:class", "'int'"),
+ ("py:class", "'bool'"),
+ ("py:class", "'object'"),
+ ("py:class", "'id'"),
+ ("py:class", "typing_extensions.Literal"),
+]
+
+# Both the class’ and the __init__ method’s docstring are concatenated and
+# inserted into the main body of the autoclass directive
+autoclass_content = "both"
+
+# Order the members by the order they appear in the source code
+autodoc_member_order = "bysource"
+
+# Don't inherit docstrings from baseclasses
+autodoc_inherit_docstrings = False
+
+# Output graphviz directive produced images in a scalable format
+graphviz_output_format = "svg"
+
+# The name of a reST role (builtin or Sphinx extension) to use as the default
+# role, that is, for text marked up `like this`
+default_role = "any"
+
+# The master toctree document.
+master_doc = "index"
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# These patterns also affect html_static_path and html_extra_path
+exclude_patterns = ["_build"]
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = "sphinx"
+
+# This means you can link things like `str` and `asyncio` to the relevant
+# docs in the python documentation.
+intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
+
+# A dictionary of graphviz graph attributes for inheritance diagrams.
+inheritance_graph_attrs = {"rankdir": "TB"}
+
+# Ignore localhost links for periodic check that links in docs are valid
+linkcheck_ignore = [r"http://localhost:\d+/"]
+
+# Set copy-button to ignore python and bash prompts
+# https://sphinx-copybutton.readthedocs.io/en/latest/use.html#using-regexp-prompt-identifiers
+copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: "
+copybutton_prompt_is_regexp = True
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = "pydata_sphinx_theme"
+github_repo = "dls-pmac-control"
+github_user = "DiamondLightSource"
+switcher_json = f"https://{github_user}.github.io/{github_repo}/switcher.json"
+switcher_exists = requests.get(switcher_json).ok
+if not switcher_exists:
+ print(
+ "*** Can't read version switcher, is GitHub pages enabled? \n"
+ " Once Docs CI job has successfully run once, set the "
+ "Github pages source branch to be 'gh-pages' at:\n"
+ f" https://github.com/{github_user}/{github_repo}/settings/pages",
+ file=sys.stderr,
+ )
+
+# Theme options for pydata_sphinx_theme
+# We don't check switcher because there are 3 possible states for a repo:
+# 1. New project, docs are not published so there is no switcher
+# 2. Existing project with latest skeleton, switcher exists and works
+# 3. Existing project with old skeleton that makes broken switcher,
+# switcher exists but is broken
+# Point 3 makes checking switcher difficult, because the updated skeleton
+# will fix the switcher at the end of the docs workflow, but never gets a chance
+# to complete as the docs build warns and fails.
+html_theme_options = {
+ "logo": {
+ "text": project,
+ },
+ "use_edit_page_button": True,
+ "github_url": f"https://github.com/{github_user}/{github_repo}",
+ "icon_links": [
+ {
+ "name": "PyPI",
+ "url": f"https://pypi.org/project/{project}",
+ "icon": "fas fa-cube",
+ }
+ ],
+ "switcher": {
+ "json_url": switcher_json,
+ "version_match": version,
+ },
+ "check_switcher": False,
+ "navbar_end": ["theme-switcher", "icon-links", "version-switcher"],
+ "external_links": [
+ {
+ "name": "Release Notes",
+ "url": f"https://github.com/{github_user}/{github_repo}/releases",
+ }
+ ],
+ "navigation_with_keys": False,
+}
+
+# A dictionary of values to pass into the template engine’s context for all pages
+html_context = {
+ "github_user": github_user,
+ "github_repo": project,
+ "github_version": version,
+ "doc_path": "docs",
+}
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+html_show_sphinx = False
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+html_show_copyright = False
+
+# Logo
+html_logo = "images/dls-logo.svg"
+html_favicon = html_logo
diff --git a/docs/explanations.md b/docs/explanations.md
new file mode 100644
index 00000000..73ab289b
--- /dev/null
+++ b/docs/explanations.md
@@ -0,0 +1,10 @@
+# Explanations
+
+Explanations of how it works and why it works that way.
+
+```{toctree}
+:maxdepth: 1
+:glob:
+
+explanations/*
+```
diff --git a/docs/explanations/decisions.md b/docs/explanations/decisions.md
new file mode 100644
index 00000000..f97c2f9f
--- /dev/null
+++ b/docs/explanations/decisions.md
@@ -0,0 +1,18 @@
+% This Source Code Form is subject to the terms of the Mozilla Public
+
+% License, v. 2.0. If a copy of the MPL was not distributed with this
+
+% file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Architectural Decision Records
+
+We record major architectural decisions in Architecture Decision Records (ADRs),
+as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
+Below is the list of our current ADRs.
+
+```{toctree}
+:glob: true
+:maxdepth: 1
+
+decisions/*
+```
diff --git a/docs/explanations/decisions/0001-record-architecture-decisions.md b/docs/explanations/decisions/0001-record-architecture-decisions.md
new file mode 100644
index 00000000..44d234ef
--- /dev/null
+++ b/docs/explanations/decisions/0001-record-architecture-decisions.md
@@ -0,0 +1,18 @@
+# 1. Record architecture decisions
+
+## Status
+
+Accepted
+
+## Context
+
+We need to record the architectural decisions made on this project.
+
+## Decision
+
+We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
+
+## Consequences
+
+See Michael Nygard's article, linked above. To create new ADRs we will copy and
+paste from existing ones.
diff --git a/docs/explanations/decisions/0002-switched-to-python-copier-template.md b/docs/explanations/decisions/0002-switched-to-python-copier-template.md
new file mode 100644
index 00000000..66fe5d8b
--- /dev/null
+++ b/docs/explanations/decisions/0002-switched-to-python-copier-template.md
@@ -0,0 +1,28 @@
+# 2. Adopt python-copier-template for project structure
+
+## Status
+
+Accepted
+
+## Context
+
+We should use the following [python-copier-template](https://github.com/DiamondLightSource/python-copier-template).
+The template will ensure consistency in developer
+environments and package management.
+
+## Decision
+
+We have switched to using the template.
+
+## Consequences
+
+This module will use a fixed set of tools as developed in `python-copier-template`
+and can pull from this template to update the packaging to the latest techniques.
+
+As such, the developer environment may have changed, the following could be
+different:
+
+- linting
+- formatting
+- pip venv setup
+- CI/CD
diff --git a/docs/genindex.md b/docs/genindex.md
new file mode 100644
index 00000000..73f1191b
--- /dev/null
+++ b/docs/genindex.md
@@ -0,0 +1,3 @@
+# Index
+
+
diff --git a/docs/how-to.md b/docs/how-to.md
new file mode 100644
index 00000000..6b161417
--- /dev/null
+++ b/docs/how-to.md
@@ -0,0 +1,10 @@
+# How-to Guides
+
+Practical step-by-step guides for the more experienced user.
+
+```{toctree}
+:maxdepth: 1
+:glob:
+
+how-to/*
+```
diff --git a/docs/how-to/contribute.md b/docs/how-to/contribute.md
new file mode 100644
index 00000000..f9c4ca1d
--- /dev/null
+++ b/docs/how-to/contribute.md
@@ -0,0 +1,2 @@
+```{include} ../../.github/CONTRIBUTING.md
+```
\ No newline at end of file
diff --git a/docs/how-to/run-container.md b/docs/how-to/run-container.md
new file mode 100644
index 00000000..8676b841
--- /dev/null
+++ b/docs/how-to/run-container.md
@@ -0,0 +1,14 @@
+# Run in a container
+
+Pre-built containers with {\{repo_name}} and its dependencies already
+installed are available on [Github Container Registry](https://ghcr.io/DiamondLightSource/dls-pmac-control).
+
+## Starting the container
+
+To pull the container from github container registry and run:
+
+```
+$ docker run ghcr.io/DiamondLightSource/dls-pmac-control:main --version
+```
+
+To get a released version, use a numbered release instead of `main`.
diff --git a/docs/images/dls-logo.svg b/docs/images/dls-logo.svg
new file mode 100644
index 00000000..0af1a177
--- /dev/null
+++ b/docs/images/dls-logo.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..730b3fdc
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,56 @@
+---
+html_theme.sidebar_secondary.remove: true
+---
+
+```{include} ../README.md
+:end-before:
+
+::::{grid} 2
+:gutter: 4
+
+:::{grid-item-card} {material-regular}`directions_walk;2em`
+```{toctree}
+:maxdepth: 2
+tutorials
+```
++++
+Tutorials for installation and typical usage. New users start here.
+:::
+
+:::{grid-item-card} {material-regular}`directions;2em`
+```{toctree}
+:maxdepth: 2
+how-to
+```
++++
+Practical step-by-step guides for the more experienced user.
+:::
+
+:::{grid-item-card} {material-regular}`info;2em`
+```{toctree}
+:maxdepth: 2
+explanations
+```
++++
+Explanations of how it works and why it works that way.
+:::
+
+:::{grid-item-card} {material-regular}`menu_book;2em`
+```{toctree}
+:maxdepth: 2
+reference
+```
++++
+Technical reference material including APIs and release notes.
+:::
+
+::::
diff --git a/docs/reference.md b/docs/reference.md
new file mode 100644
index 00000000..6a7d085a
--- /dev/null
+++ b/docs/reference.md
@@ -0,0 +1,11 @@
+# Reference
+
+Technical reference material including APIs and release notes.
+
+```{toctree}
+:maxdepth: 1
+:glob:
+
+reference/*
+genindex
+```
diff --git a/docs/reference/api.md b/docs/reference/api.md
new file mode 100644
index 00000000..82732e95
--- /dev/null
+++ b/docs/reference/api.md
@@ -0,0 +1,17 @@
+# API
+
+```{eval-rst}
+.. automodule:: dls_pmac_control
+
+ ``dls_pmac_control``
+ -----------------------------------
+```
+
+This is the internal API reference for {\{package_name}}
+
+```{eval-rst}
+.. data:: dls_pmac_control.__version__
+ :type: str
+
+ Version number as calculated by https://github.com/pypa/setuptools_scm
+```
diff --git a/docs/tutorials.md b/docs/tutorials.md
new file mode 100644
index 00000000..1fe66c54
--- /dev/null
+++ b/docs/tutorials.md
@@ -0,0 +1,10 @@
+# Tutorials
+
+Tutorials for installation and typical usage. New users start here.
+
+```{toctree}
+:maxdepth: 1
+:glob:
+
+tutorials/*
+```
diff --git a/docs/tutorials/installation.md b/docs/tutorials/installation.md
new file mode 100644
index 00000000..836070d9
--- /dev/null
+++ b/docs/tutorials/installation.md
@@ -0,0 +1,42 @@
+# Installation
+
+## Check your version of python
+
+You will need python 3.8 or later. You can check your version of python by
+typing into a terminal:
+
+```
+$ python3 --version
+```
+
+## Create a virtual environment
+
+It is recommended that you install into a “virtual environment” so this
+installation will not interfere with any existing Python software:
+
+```
+$ python3 -m venv /path/to/venv
+$ source /path/to/venv/bin/activate
+```
+
+## Installing the library
+
+You can now use `pip` to install the library and its dependencies:
+
+```
+$ python3 -m pip install dls-pmac-control
+```
+
+If you require a feature that is not currently released you can also install
+from github:
+
+```
+$ python3 -m pip install git+https://github.com/DiamondLightSource/dls-pmac-control.git
+```
+
+The library should now be installed and the commandline interface on your path.
+You can check the version that has been installed by typing:
+
+```
+$ dls-pmac-control --version
+```
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..360bdfc2
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,110 @@
+[build-system]
+requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "dls-pmac-control"
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "License :: OSI Approved :: Apache Software License",
+ "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",
+]
+description = "A GUI for low level interaction with turbo pmacs and power pmacs"
+dependencies = [] # Add project dependencies here, e.g. ["click", "numpy"]
+dynamic = ["version"]
+license.file = "LICENSE"
+readme = "README.md"
+requires-python = ">=3.7"
+
+[project.optional-dependencies]
+dev = [
+ "copier",
+ "myst-parser",
+ "pipdeptree",
+ "pre-commit",
+ "pydata-sphinx-theme>=0.12",
+ "pyright",
+ "pytest",
+ "pytest-cov",
+ "ruff",
+ "sphinx-autobuild",
+ "sphinx-copybutton",
+ "sphinx-design",
+ "tox-direct",
+ "types-mock",
+]
+
+[project.scripts]
+dls-pmac-control = "dls_pmac_control.__main__:main"
+
+[project.urls]
+GitHub = "https://github.com/DiamondLightSource/dls-pmac-control"
+
+[[project.authors]] # Further authors may be added by duplicating this section
+email = "giles.knap@diamond.ac.uk"
+name = "Giles Knap"
+
+
+[tool.setuptools_scm]
+write_to = "src/dls_pmac_control/_version.py"
+
+[tool.pyright]
+reportMissingImports = false # Ignore missing stubs in imported modules
+
+[tool.pytest.ini_options]
+# Run pytest with all our checkers, and don't spam us with massive tracebacks on error
+addopts = """
+ --tb=native -vv --doctest-modules --doctest-glob="*.rst"
+ """
+# https://iscinumpy.gitlab.io/post/bound-version-constraints/#watch-for-warnings
+filterwarnings = "error"
+# Doctest python code in docs, python code in src docstrings, test functions in tests
+testpaths = "docs src tests"
+
+[tool.coverage.run]
+data_file = "/tmp/dls_pmac_control.coverage"
+
+[tool.coverage.paths]
+# Tests are run from installed location, map back to the src directory
+source = ["src", "**/site-packages/"]
+
+# tox must currently be configured via an embedded ini string
+# See: https://github.com/tox-dev/tox/issues/999
+[tool.tox]
+legacy_tox_ini = """
+[tox]
+skipsdist=True
+
+[testenv:{pre-commit,pyright,pytest,docs}]
+# Don't create a virtualenv for the command, requires tox-direct plugin
+direct = True
+passenv = *
+allowlist_externals =
+ pytest
+ pre-commit
+ pyright
+ sphinx-build
+ sphinx-autobuild
+commands =
+ pytest: pytest --cov=dls_pmac_control --cov-report term --cov-report xml:cov.xml {posargs}
+ pyright: pyright src tests {posargs}
+ pre-commit: pre-commit run --all-files {posargs}
+ docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html
+"""
+
+[tool.ruff]
+src = ["src", "tests"]
+line-length = 88
+lint.select = [
+ "B", # flake8-bugbear - https://docs.astral.sh/ruff/rules/#flake8-bugbear-b
+ "C4", # flake8-comprehensions - https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4
+ "E", # pycodestyle errors - https://docs.astral.sh/ruff/rules/#error-e
+ "F", # pyflakes rules - https://docs.astral.sh/ruff/rules/#pyflakes-f
+ "W", # pycodestyle warnings - https://docs.astral.sh/ruff/rules/#warning-w
+ "I", # isort - https://docs.astral.sh/ruff/rules/#isort-i
+ "UP", # pyupgrade - https://docs.astral.sh/ruff/rules/#pyupgrade-up
+]
diff --git a/src/dls_pmac_control/__init__.py b/src/dls_pmac_control/__init__.py
new file mode 100644
index 00000000..26d23bad
--- /dev/null
+++ b/src/dls_pmac_control/__init__.py
@@ -0,0 +1,3 @@
+from ._version import __version__
+
+__all__ = ["__version__"]
diff --git a/src/dls_pmac_control/__main__.py b/src/dls_pmac_control/__main__.py
new file mode 100644
index 00000000..823f3b98
--- /dev/null
+++ b/src/dls_pmac_control/__main__.py
@@ -0,0 +1,16 @@
+from argparse import ArgumentParser
+
+from . import __version__
+
+__all__ = ["main"]
+
+
+def main(args=None):
+ parser = ArgumentParser()
+ parser.add_argument("-v", "--version", action="version", version=__version__)
+ args = parser.parse_args(args)
+
+
+# test with: python -m dls_pmac_control
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 00000000..9d950201
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,9 @@
+import subprocess
+import sys
+
+from dls_pmac_control import __version__
+
+
+def test_cli_version():
+ cmd = [sys.executable, "-m", "dls_pmac_control", "--version"]
+ assert subprocess.check_output(cmd).decode().strip() == __version__