diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..6152ad93
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,55 @@
+// For format details, see https://containers.dev/implementors/json_reference/
+{
+ "name": "PandABlocks-server Developer Container",
+ "build": {
+ "dockerfile": "../Dockerfile",
+ "target": "developer"
+ },
+ "remoteEnv": {
+ // Allow X11 apps to run inside the container
+ "DISPLAY": "${localEnv:DISPLAY}",
+ // Put things that allow it in the persistent cache
+ "PRE_COMMIT_HOME": "/cache/pre-commit",
+ "UV_CACHE_DIR": "/cache/uv",
+ "UV_PYTHON_CACHE_DIR": "/cache/uv-python",
+ },
+ "customizations": {
+ "vscode": {
+ // Set *default* container specific settings.json values on container create.
+ "settings": {
+ // Workaround to prevent garbled python REPL in the terminal
+ // https://github.com/microsoft/vscode-python/issues/25505
+ "python.terminal.shellIntegration.enabled": false
+ },
+ // Add the IDs of extensions you want installed when the container is created.
+ "extensions": [
+ "ms-python.python",
+ "ms-azuretools.vscode-docker"
+ ]
+ }
+ },
+ // Create the config folder for the bash-config feature and uv cache
+ "initializeCommand": "mkdir -p ${localEnv:HOME}/.config/terminal-config",
+ "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"
+ ],
+ "mounts": [
+ // Mount in the user terminal config folder so it can be edited
+ {
+ "source": "${localEnv:HOME}/.config/terminal-config",
+ "target": "/user-terminal-config",
+ "type": "bind"
+ },
+ // Keep a persistent cross container cache for uv, pre-commit, and the venvs
+ {
+ "source": "devcontainer-shared-cache",
+ "target": "/cache",
+ "type": "volume"
+ }
+ ],
+ // Mount the parent as /workspaces so we can pip install peers as editable
+ "workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind",
+}
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 00000000..6eaa9eee
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,17 @@
+# Contributing
+
+Contributions and issues are most welcome! All issues and pull requests are handled
+through [GitHub](https://github.com/PandABlocks/PandABlocks-server/issues). Please check
+for any existing issues before filing a new one.
+
+## Issue or Pull Request?
+
+Raise an issue if you want to report a bug or request a feature. Open a pull request if
+you already have a change ready to propose. If you are unsure, raise an issue first to
+discuss the change.
+
+## Documentation
+
+Documentation is built with [MyST](https://mystmd.org) and published to GitHub Pages.
+The docs follow the [Diátaxis](https://diataxis.fr) framework — tutorials, how-to
+guides, reference and explanations.
diff --git a/.github/pages/index.html b/.github/pages/index.html
deleted file mode 100644
index cc33127d..00000000
--- a/.github/pages/index.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
- Redirecting to master branch
-
-
-
-
-
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..4517592d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,67 @@
+name: Docs CI
+
+# Build + verify the docs on every event, then publish on INTERNAL events. The
+# build (the shared `docs.yml`) runs for PRs (including forks), pushes to main, and
+# tags, and uploads each build's `docs` artifact. Publishing is nested here (the
+# `publish` job -> the shared `publish.yml`) so its status is visible on the
+# PR/commit, but ONLY for internal events on this repo: a fork PR's build runs with a
+# read-only token and must never deploy.
+#
+# Both halves are consumed from DiamondLightSource/myst-version-switcher-plugin at a
+# pinned tag — this repo carries no docs-build/publish workflow of its own.
+# publish.yml reconstructs the whole versioned site from durable sources (main's
+# latest build persisted at _sources/main.zip, each release's docs.zip asset, every
+# open PR's build artifact) and deploys it to Pages directly; there is no gh-pages
+# branch.
+on:
+ pull_request:
+ push:
+ branches: [main]
+ tags: ['*'] # '*' never matches '/'
+
+jobs:
+ docs:
+ uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/docs.yml@v0.7.0
+ with:
+ # `make docs` drives npx mystmd (pinned by MYSTMD_VERSION) and reads build
+ # settings from CONFIG, so create it first. eval runs the compound command.
+ build-command: cp CONFIG.example CONFIG && make docs
+ # The fork-PR warning links here so a maintainer can publish a fork preview.
+ preview-workflow: preview-fork.yml
+
+ # Tag-only: attach this build's docs.zip (bare html/ root) to the GitHub Release
+ # so `assemble` can reconstruct that released version on future deploys.
+ docs-release:
+ needs: [docs]
+ if: github.ref_type == 'tag'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: docs
+ - env:
+ GH_TOKEN: ${{ github.token }}
+ run: gh release upload "${{ github.ref_name }}" docs.zip --clobber --repo "${{ github.repository }}"
+
+ # Internal events only: an internal PR, or a push to main/tag, has a same-repo
+ # build we can trust + deploy. Fork PRs (head repo != this repo) are excluded —
+ # docs.yml's build job warns them instead. publish.yml defaults guard-default-branch
+ # to true, so every deploy asserts main is present and can never silently drop
+ # /main/ (main is self-durable via _sources/main.zip).
+ publish:
+ needs: [docs]
+ if: >-
+ github.repository == 'PandABlocks/PandABlocks-server' &&
+ ( github.event_name != 'pull_request' ||
+ github.event.pull_request.head.repo.full_name == github.repository )
+ uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/publish.yml@v0.7.0
+ with:
+ version-name: ${{ needs.docs.outputs.version-name }}
+ permissions:
+ contents: read
+ actions: read
+ pages: write
+ id-token: write
+ statuses: write
diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml
index 103ec395..872a6335 100644
--- a/.github/workflows/code.yml
+++ b/.github/workflows/code.yml
@@ -42,7 +42,7 @@ jobs:
cd PandABlocks-rootfs
ln -s CONFIG.example CONFIG
make kernel PLATFORM=${{matrix.platform}}
-
+
- name: Make zpkg
run: |
cd PandABlocks-server
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
deleted file mode 100644
index c5227cd3..00000000
--- a/.github/workflows/docs.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-name: Docs CI
-
-on:
- push:
- pull_request:
-
-jobs:
- build:
- runs-on: ubuntu-latest
- # Use container from ghcr
- container:
- image: ghcr.io/pandablocks/pandablocks-dev-container:latest
- credentials:
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- volumes:
- - ${{ github.workspace }}:/repos
- - ${{ github.workspace }}/build:/build
-
- steps:
- # Git repositories
- - name: Checkout Source
- uses: actions/checkout@v2
- with:
- path: PandABlocks-server
- # require history to get back to last tag for version number of branches
- fetch-depth: 0
-
- # Build
- - name: Build Docs
- run: |
- cd PandABlocks-server
- ln -s CONFIG.example CONFIG
- make docs
-
- # Verification of build files
- #- name: List build files
- # run: ls ${{ github.workspace }}/build/html
-
- # Publish
- - name: Move to versioned directory
- # e.g. master or 0.1.2
- run: mv build/build-server/html "PandABlocks-server/.github/pages/${GITHUB_REF##*/}"
-
- - name: Publish Docs to gh-pages
- # Only master and tags are published
- if: "${{ github.repository_owner == 'PandABlocks' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')) }}"
- # We pin to the SHA, not the tag, for security reasons.
- # https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions
- uses: peaceiris/actions-gh-pages@bbdfb200618d235585ad98e965f4aafc39b4c501 # v3.7.3
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: PandABlocks-server/.github/pages
- keep_files: true
\ No newline at end of file
diff --git a/.github/workflows/preview-fork.yml b/.github/workflows/preview-fork.yml
new file mode 100644
index 00000000..fbdad8ae
--- /dev/null
+++ b/.github/workflows/preview-fork.yml
@@ -0,0 +1,31 @@
+name: Preview fork PR
+
+# Maintainer opt-in to publish a versioned docs preview for an EXTERNAL fork PR.
+# Fork PRs build + verify in Docs CI but never auto-publish (their build runs with a
+# read-only token), so this is the only path that deploys one. A reusable workflow
+# can't be workflow_dispatch'd cross-repo, so this thin wrapper lives here and passes
+# the PR number through to the shared publish.yml via workflow_call.
+#
+# It approves the fork PR's CURRENT head SHA (a `preview-approved` commit status) and
+# assembles the site with that PR's artifact included. A later push to the PR (new
+# SHA) drops the preview until you re-run this. version-name is empty: there is no
+# in-run build to inject, so publish.yml does a pure durable gather.
+on:
+ workflow_dispatch:
+ inputs:
+ pr:
+ description: Fork PR number to approve + preview.
+ required: true
+
+jobs:
+ preview:
+ uses: DiamondLightSource/myst-version-switcher-plugin/.github/workflows/publish.yml@v0.7.0
+ with:
+ version-name: ""
+ pr: ${{ inputs.pr }}
+ permissions:
+ contents: read
+ actions: read
+ pages: write
+ id-token: write
+ statuses: write
diff --git a/.gitignore b/.gitignore
index 6677454f..39686957 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,6 @@
*.pyc
tests/fpga_sequences
/venv
+
+# MyST build output
+docs/_build/
diff --git a/CONFIG.example b/CONFIG.example
index 7dfaac73..e160582e 100644
--- a/CONFIG.example
+++ b/CONFIG.example
@@ -15,9 +15,11 @@ BUILD_DIR = /build/build-server
#
PYTHON = python3
-# Sphinx build for documentation.
+# Version of mystmd (https://mystmd.org) used to build the documentation with
+# `make docs` / `make docs-dev`. Run on demand via npx, so no global install is
+# needed.
#
-SPHINX_BUILD = sphinx-build
+MYSTMD_VERSION = 1.10.1
# Compiler settings. Default cross compile prefix given here. If BINUTILS_DIR
# is specified it will be prepended to the path for building the driver and
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..991306b4
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+# The devcontainer should use the developer target and run as root with podman
+# or docker with user namespaces.
+FROM ghcr.io/diamondlightsource/ubuntu-devcontainer:resolute AS developer
+
+# Add any system dependencies for the developer/build environment here.
+# Candidates: an ARMv7-A cross-compiler toolchain for on-PandA builds; the
+# native toolchain below is enough for the simulation server and docs.
+RUN apt-get update -y && apt-get install -y --no-install-recommends \
+ npm \
+ && apt-get dist-clean
diff --git a/Makefile b/Makefile
index 19461b05..04f3b8a8 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,7 @@ TOP := $(CURDIR)
BUILD_DIR = $(TOP)/build
PYTHON = python3
-SPHINX_BUILD = sphinx-build
+MYSTMD_VERSION = 1.10.1
COMPILER_PREFIX = $(COMPILER_PREFIX_$(PLATFORM))
KERNEL_DIR = $(error Define KERNEL_DIR in CONFIG file)
PANDA_ROOTFS = $(error Define PANDA_ROOTFS in CONFIG file)
@@ -140,16 +140,33 @@ slow_load: $(SLOW_LOAD)
# ------------------------------------------------------------------------------
# Documentation
+#
+# Docs are built with MyST (mystmd), run through npx so no global install is
+# needed; pin the version with MYSTMD_VERSION (see CONFIG.example). `make docs`
+# mirrors `myst build --html --strict` and `make docs-dev` mirrors `myst start`,
+# matching the docs/ task runner. MyST writes its output into docs/_build/html.
+# --strict exits non-zero on any error-severity message (e.g. an unresolved
+# cross-repo xref) so CI fails rather than publishing broken links.
-$(DOCS_BUILD_DIR)/index.html: $(wildcard docs/*.rst docs/*/*.rst docs/conf.py)
- $(SPHINX_BUILD) -b html docs $(DOCS_BUILD_DIR)
+MYST = npx --yes --package mystmd@$(MYSTMD_VERSION) myst
-docs: $(DOCS_BUILD_DIR)/index.html
+docs:
+ cd docs && $(MYST) build --html --strict
+
+docs-dev:
+ cd docs && $(MYST) start
clean-docs:
- rm -rf $(DOCS_BUILD_DIR)
+ rm -rf $(TOP)/docs/_build
-.PHONY: docs clean-docs
+.PHONY: docs docs-dev clean-docs
+
+# The zpkg packaging installs the built docs from $(DOCS_BUILD_DIR) (the `html`
+# entry in etc/panda-server.list), so stage the MyST output there for zpkg.
+$(DOCS_BUILD_DIR)/index.html: docs
+ rm -rf $(DOCS_BUILD_DIR)
+ mkdir -p $(DOCS_BUILD_DIR)
+ cp -r $(TOP)/docs/_build/html/. $(DOCS_BUILD_DIR)
# ------------------------------------------------------------------------------
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..6506d048
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# PandABlocks-server
+
+The PandA socket server provides a bridge between the register interface to the
+FPGA firmware controlling the PandA hardware and users and other software. The
+interface is designed to be simple and robust.
+
+The PandA firmware is structured into numerous functional blocks, each
+configured via a number of fields. This structure is directly reflected in the
+functional interface provided by this server: most commands read or write
+specific fields.
+
+The server publishes two socket end points: one for configuration control
+(simple ASCII commands, ASCII responses) and one for streamed data capture
+(no commands, a lightly structured binary stream).
+
+
+
+## Where to find things
+
+| | |
+|---|---|
+| Source code | |
+| Documentation | |
+| Releases | |
diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md
new file mode 100644
index 00000000..dd316dfb
--- /dev/null
+++ b/docs/PROGRESS.md
@@ -0,0 +1,72 @@
+# Docs Rewrite Progress — PandABlocks-server
+
+Tracks every target page for this repo. Update the relevant line **in the same commit** as the file
+it refers to. Statuses: ☐ todo · ◐ stub · ✅ converted · 🔍 needs-review · ⛔ blocked (→ issue #).
+Page list expanded from `06-source-provenance-map.md`.
+
+## Stage A — scaffold
+- ✅ skeleton instantiated (title/github/logo swapped, apidoc + pip-tutorial + run-container removed)
+- ✅ TOC + stubs for every target page present (12 stubs + 4 landing + index)
+- ✅ `index.md` README-include + `how-to/contribute` CONTRIBUTING-include resolve
+- ✅ xref/intersphinx wired + cross-link probe resolves in built output (reference/capture → PandABlocks-client)
+- ☐ (FPGA only) — N/A
+- ✅ `myst build` green (exit 0, no warnings/errors)
+- ✅ Pages deploy wired — `.github/workflows/docs.yml` switched to `myst build` (copier-aligned)
+
+## Tutorials
+- (none in v4 — empty `tutorials.md` landing kept for parity)
+
+## How-to
+- ✅ how-to/startup — converted from startup.rst (Prompt D)
+- ✅ how-to/building — converted from building.rst; devcontainer pointer added; CONFIG syntax note = verify (Prompt D)
+- ✅ how-to/contribute — `{include} .github/CONTRIBUTING.md` — scaffold include resolves
+
+## Explanations
+- ✅ explanations/architecture — skeleton authored from server code (Prompt D); depth = blocked: author
+
+## Reference
+- ✅ reference/commands — source: commands.rst — converted (Prompt E); Configuration + System command tables preserved, *IDN? kept; *CAPTURE.OPTIONS? points to capture-options
+- ✅ reference/fields — source: fields.rst — converted (Prompt E); MODE matrix + Summary-of-Sub-Types + Summary-of-Attributes tables kept; table-streaming detail points to reference/streaming-tables
+- ✅ reference/capture — source: capture.rst — converted (Prompt E); ~60 MB/s + "not installed" webcontrol caveat kept; Capture Options extracted to capture-options; data-out links meta-panda integrate-with-a-panda; holds the Stage A client xref probe
+- ✅ reference/capture-options — source: capture.rst ⊃ + Interview5 §4 — written (Prompt E); field + connection options; "run *CAPTURE.OPTIONS? live" note added; superset still blocked: verify
+- ✅ reference/config — converted from config.rst (Prompt D)
+- ⛔ reference/extension — source: extension.rst — blocked: verify
+- ✅ reference/streaming-tables — authored from Interview5 §8 + fields.rst streaming section (Prompt D)
+- ✅ reference/support — converted from support.rst (Prompt D)
+- ⛔ reference/c-standard — source: NEW (Interview1 §4) — blocked: author
+
+## Blocked (issues raised)
+Issues not yet created — Stage B (Prompt B) will create and link them.
+- ⛔ reference/extension — issue #80 — verify
+- ⛔ reference/c-standard — issue #83 — author
+
+- ⛔ reference/capture-options (live confirmation) — issue #78 — verify
+
+- ⛔ reference/capture (60 MB/s figure) — issue #79 — verify
+
+- ⛔ reference/commands (*IDN? v4.x) — issue #81 — verify
+
+- ⛔ how-to/building (CONFIG syntax) — issue #82 — verify
+
+- ⛔ explanations/architecture (depth) — issue #84 — author
+
+## Notes
+- **Legacy sources preserved.** Original Sphinx/RST tree moved to `docs/_legacy_rst/` (not in TOC,
+ ignored by `myst build`) so Stage D/E conversion can read sources in-tree; also on `origin/main`.
+ `presentation-2016-05/*` is dropped per 06 (kept under `_legacy_rst/` for now; delete with the rest
+ of `_legacy_rst/` once conversion is complete).
+- **Default branch is `main`** — publish gated on `main`/tags. The root `index.html` redirect is
+ generated by the version-switcher `switcher` action: it points at the preferred (newest stable
+ tag) version, falling back to `main` when no stable tag is deployed.
+- **xref/intersphinx prototype.** Only `PandABlocks-client` (deployed, Sphinx, stays on /main) is
+ active; the probe in `reference/capture` resolves in built output. meta-panda + FPGA + fastcs kept
+ commented until published; uncomment in Stage F and upstream into python-copier-template. Interim
+ inventory target for those MyST repos is each repo's `docs`-branch build (`/docs/objects.inv`),
+ switching to `/stable` once the upstream switcher action publishes a `stable` symlink to the
+ latest release (Pages tars with `--dereference`, so the symlink materialises if its target version
+ is retained).
+- **CI** mirrors python-copier-template-example `_docs.yml` (npm+mystmd build, upload-artifact of
+ `docs/_build` minus the templates cache, versioned move, peaceiris v4). The switcher.json + root
+ redirect are written by the `DiamondLightSource/myst-version-switcher-plugin/switcher@v0.2.0`
+ action; the in-page dropdown is the matching `version-switcher` MyST plugin (release-asset `.mjs`
+ in `myst.yml` `plugins`, rendered via `navbar_end.md`).
diff --git a/docs/PandA-logo-for-black-background.svg b/docs/_legacy_rst/PandA-logo-for-black-background.svg
similarity index 100%
rename from docs/PandA-logo-for-black-background.svg
rename to docs/_legacy_rst/PandA-logo-for-black-background.svg
diff --git a/docs/_static/theme_overrides.css b/docs/_legacy_rst/_static/theme_overrides.css
similarity index 100%
rename from docs/_static/theme_overrides.css
rename to docs/_legacy_rst/_static/theme_overrides.css
diff --git a/docs/_templates/layout.html b/docs/_legacy_rst/_templates/layout.html
similarity index 100%
rename from docs/_templates/layout.html
rename to docs/_legacy_rst/_templates/layout.html
diff --git a/docs/building.rst b/docs/_legacy_rst/building.rst
similarity index 100%
rename from docs/building.rst
rename to docs/_legacy_rst/building.rst
diff --git a/docs/capture.rst b/docs/_legacy_rst/capture.rst
similarity index 100%
rename from docs/capture.rst
rename to docs/_legacy_rst/capture.rst
diff --git a/docs/commands.rst b/docs/_legacy_rst/commands.rst
similarity index 100%
rename from docs/commands.rst
rename to docs/_legacy_rst/commands.rst
diff --git a/docs/conf.py b/docs/_legacy_rst/conf.py
similarity index 100%
rename from docs/conf.py
rename to docs/_legacy_rst/conf.py
diff --git a/docs/config.rst b/docs/_legacy_rst/config.rst
similarity index 100%
rename from docs/config.rst
rename to docs/_legacy_rst/config.rst
diff --git a/docs/extension.rst b/docs/_legacy_rst/extension.rst
similarity index 100%
rename from docs/extension.rst
rename to docs/_legacy_rst/extension.rst
diff --git a/docs/favicon.ico b/docs/_legacy_rst/favicon.ico
similarity index 100%
rename from docs/favicon.ico
rename to docs/_legacy_rst/favicon.ico
diff --git a/docs/fields.rst b/docs/_legacy_rst/fields.rst
similarity index 100%
rename from docs/fields.rst
rename to docs/_legacy_rst/fields.rst
diff --git a/docs/index.rst b/docs/_legacy_rst/index.rst
similarity index 100%
rename from docs/index.rst
rename to docs/_legacy_rst/index.rst
diff --git a/docs/presentation-2016-05/.gitignore b/docs/_legacy_rst/presentation-2016-05/.gitignore
similarity index 100%
rename from docs/presentation-2016-05/.gitignore
rename to docs/_legacy_rst/presentation-2016-05/.gitignore
diff --git a/docs/presentation-2016-05/Makefile b/docs/_legacy_rst/presentation-2016-05/Makefile
similarity index 100%
rename from docs/presentation-2016-05/Makefile
rename to docs/_legacy_rst/presentation-2016-05/Makefile
diff --git a/docs/presentation-2016-05/building.rst b/docs/_legacy_rst/presentation-2016-05/building.rst
similarity index 100%
rename from docs/presentation-2016-05/building.rst
rename to docs/_legacy_rst/presentation-2016-05/building.rst
diff --git a/docs/presentation-2016-05/docutils.conf b/docs/_legacy_rst/presentation-2016-05/docutils.conf
similarity index 100%
rename from docs/presentation-2016-05/docutils.conf
rename to docs/_legacy_rst/presentation-2016-05/docutils.conf
diff --git a/docs/presentation-2016-05/images/admin.png b/docs/_legacy_rst/presentation-2016-05/images/admin.png
similarity index 100%
rename from docs/presentation-2016-05/images/admin.png
rename to docs/_legacy_rst/presentation-2016-05/images/admin.png
diff --git a/docs/presentation-2016-05/images/architecture.png b/docs/_legacy_rst/presentation-2016-05/images/architecture.png
similarity index 100%
rename from docs/presentation-2016-05/images/architecture.png
rename to docs/_legacy_rst/presentation-2016-05/images/architecture.png
diff --git a/docs/presentation-2016-05/images/software.png b/docs/_legacy_rst/presentation-2016-05/images/software.png
similarity index 100%
rename from docs/presentation-2016-05/images/software.png
rename to docs/_legacy_rst/presentation-2016-05/images/software.png
diff --git a/docs/presentation-2016-05/images/system.png b/docs/_legacy_rst/presentation-2016-05/images/system.png
similarity index 100%
rename from docs/presentation-2016-05/images/system.png
rename to docs/_legacy_rst/presentation-2016-05/images/system.png
diff --git a/docs/presentation-2016-05/panda.rst b/docs/_legacy_rst/presentation-2016-05/panda.rst
similarity index 100%
rename from docs/presentation-2016-05/panda.rst
rename to docs/_legacy_rst/presentation-2016-05/panda.rst
diff --git a/docs/presentation-2016-05/server.rst b/docs/_legacy_rst/presentation-2016-05/server.rst
similarity index 100%
rename from docs/presentation-2016-05/server.rst
rename to docs/_legacy_rst/presentation-2016-05/server.rst
diff --git a/docs/presentation-2016-05/styles/blank.gif b/docs/_legacy_rst/presentation-2016-05/styles/blank.gif
similarity index 100%
rename from docs/presentation-2016-05/styles/blank.gif
rename to docs/_legacy_rst/presentation-2016-05/styles/blank.gif
diff --git a/docs/presentation-2016-05/styles/dlsfooterpad.png b/docs/_legacy_rst/presentation-2016-05/styles/dlsfooterpad.png
similarity index 100%
rename from docs/presentation-2016-05/styles/dlsfooterpad.png
rename to docs/_legacy_rst/presentation-2016-05/styles/dlsfooterpad.png
diff --git a/docs/presentation-2016-05/styles/framing.css b/docs/_legacy_rst/presentation-2016-05/styles/framing.css
similarity index 100%
rename from docs/presentation-2016-05/styles/framing.css
rename to docs/_legacy_rst/presentation-2016-05/styles/framing.css
diff --git a/docs/presentation-2016-05/styles/html4css1.css b/docs/_legacy_rst/presentation-2016-05/styles/html4css1.css
similarity index 100%
rename from docs/presentation-2016-05/styles/html4css1.css
rename to docs/_legacy_rst/presentation-2016-05/styles/html4css1.css
diff --git a/docs/presentation-2016-05/styles/iepngfix.htc b/docs/_legacy_rst/presentation-2016-05/styles/iepngfix.htc
similarity index 100%
rename from docs/presentation-2016-05/styles/iepngfix.htc
rename to docs/_legacy_rst/presentation-2016-05/styles/iepngfix.htc
diff --git a/docs/presentation-2016-05/styles/opera.css b/docs/_legacy_rst/presentation-2016-05/styles/opera.css
similarity index 100%
rename from docs/presentation-2016-05/styles/opera.css
rename to docs/_legacy_rst/presentation-2016-05/styles/opera.css
diff --git a/docs/presentation-2016-05/styles/outline.css b/docs/_legacy_rst/presentation-2016-05/styles/outline.css
similarity index 100%
rename from docs/presentation-2016-05/styles/outline.css
rename to docs/_legacy_rst/presentation-2016-05/styles/outline.css
diff --git a/docs/presentation-2016-05/styles/pretty.css b/docs/_legacy_rst/presentation-2016-05/styles/pretty.css
similarity index 100%
rename from docs/presentation-2016-05/styles/pretty.css
rename to docs/_legacy_rst/presentation-2016-05/styles/pretty.css
diff --git a/docs/presentation-2016-05/styles/print.css b/docs/_legacy_rst/presentation-2016-05/styles/print.css
similarity index 100%
rename from docs/presentation-2016-05/styles/print.css
rename to docs/_legacy_rst/presentation-2016-05/styles/print.css
diff --git a/docs/presentation-2016-05/styles/pygments.css b/docs/_legacy_rst/presentation-2016-05/styles/pygments.css
similarity index 100%
rename from docs/presentation-2016-05/styles/pygments.css
rename to docs/_legacy_rst/presentation-2016-05/styles/pygments.css
diff --git a/docs/presentation-2016-05/styles/s5-core.css b/docs/_legacy_rst/presentation-2016-05/styles/s5-core.css
similarity index 100%
rename from docs/presentation-2016-05/styles/s5-core.css
rename to docs/_legacy_rst/presentation-2016-05/styles/s5-core.css
diff --git a/docs/presentation-2016-05/styles/slides.css b/docs/_legacy_rst/presentation-2016-05/styles/slides.css
similarity index 100%
rename from docs/presentation-2016-05/styles/slides.css
rename to docs/_legacy_rst/presentation-2016-05/styles/slides.css
diff --git a/docs/presentation-2016-05/styles/slides.js b/docs/_legacy_rst/presentation-2016-05/styles/slides.js
similarity index 100%
rename from docs/presentation-2016-05/styles/slides.js
rename to docs/_legacy_rst/presentation-2016-05/styles/slides.js
diff --git a/docs/startup.rst b/docs/_legacy_rst/startup.rst
similarity index 100%
rename from docs/startup.rst
rename to docs/_legacy_rst/startup.rst
diff --git a/docs/support.rst b/docs/_legacy_rst/support.rst
similarity index 100%
rename from docs/support.rst
rename to docs/_legacy_rst/support.rst
diff --git a/docs/explanations.md b/docs/explanations.md
new file mode 100644
index 00000000..a1dca5ac
--- /dev/null
+++ b/docs/explanations.md
@@ -0,0 +1,7 @@
+# Explanations
+
+Explanations of how it works and why it works that way.
+
+```{toc}
+:context: children
+```
diff --git a/docs/explanations/architecture.md b/docs/explanations/architecture.md
new file mode 100644
index 00000000..ea86d78f
--- /dev/null
+++ b/docs/explanations/architecture.md
@@ -0,0 +1,55 @@
+# Server architecture
+
+:::{admonition} Skeleton — depth coming later
+:class: note
+
+This page provides an orientation skeleton. Detailed internals (locking
+strategy, DMA pipeline, persistence state machine) are tracked in the issue
+backlog as **blocked: author**.
+:::
+
+The PandABlocks server is a C daemon that presents two TCP sockets and bridges
+them to the PandA FPGA hardware (or a software simulation of it).
+
+## Sockets
+
+**Configuration port (default 8888)**
+: An ASCII command/response interface used to read and write block fields and
+ execute system commands. Described in [](/reference/commands.md).
+
+**Data port (default 8889)**
+: A binary streaming interface for captured experiment data. Described in
+ [](/reference/capture.md).
+
+## Internal structure
+
+The server is organised around three cooperating layers.
+
+**Configuration thread** (`config_server.c`)
+: Accepts connections on the configuration port. Parses incoming ASCII commands,
+ dispatches them to the block/field database (`config_command.c`, `fields.c`),
+ and formats responses. One thread per client connection.
+
+**Data thread** (`data_server.c`)
+: Accepts connections on the data port. Reads captured data from the central
+ circular buffer and streams it to connected clients. Handles framing,
+ base-64 encoding, and metadata headers.
+
+**Hardware layer** (`hardware.c` / `sim_hardware.c`)
+: Abstracts register reads/writes and DMA transfers to the FPGA. In simulation
+ mode (`sim_hardware.c`) the hardware layer is replaced by an in-process
+ emulator.
+
+## Block model
+
+The server loads its block and field topology from the `config_d` configuration
+files at startup (see [](/reference/config.md)). Each block is a named,
+potentially multi-instance hardware component (e.g. `PULSE[4]`). Fields within
+a block map to registers and expose typed operations (read, write, capture, etc.)
+as documented in [](/reference/fields.md).
+
+## Persistence
+
+Field values that survive a server restart are written to a persistence file
+(see the `-f` and `-t` options in [](/how-to/startup.md)). The file is updated
+on a poll/holdoff/backoff schedule to reduce write pressure.
diff --git a/docs/how-to.md b/docs/how-to.md
new file mode 100644
index 00000000..ba801fd7
--- /dev/null
+++ b/docs/how-to.md
@@ -0,0 +1,7 @@
+# How-to Guides
+
+Practical step-by-step guides for the more experienced user.
+
+```{toc}
+:context: children
+```
diff --git a/docs/how-to/building.md b/docs/how-to/building.md
new file mode 100644
index 00000000..02e049bb
--- /dev/null
+++ b/docs/how-to/building.md
@@ -0,0 +1,77 @@
+# Build and test the server
+
+:::{tip}
+For local development the devcontainer defined in this repository
+(`.devcontainer/devcontainer.json` and the top-level `Dockerfile`) provides a
+pre-configured environment and is the recommended starting point. The
+instructions below cover a native build for those who need it.
+:::
+
+## Dependencies
+
+The following must be available before building the server.
+
+**Zynq cross-compiler toolchain**
+: Available as part of the Xilinx Vivado build environment, or any ARMv7-A
+ cross-compiler. Required for all applications running on PandA.
+
+**PandABlocks-FPGA**
+: Must be available before building the server; it contains the configuration
+ file defining the low-level register interface to the PandA firmware.
+
+## Setting up the `CONFIG` file
+
+Copy `CONFIG.example` to `CONFIG` in the base directory and edit as needed,
+commenting out lines that are not required.
+
+:::{note}
+The exact syntax accepted by the server Makefile for `CONFIG` values is pending
+verification (tracked in the issue backlog). Use the form shown in
+`CONFIG.example`.
+:::
+
+The following symbols must point to the appropriate dependencies:
+
+`BINUTILS_DIR`
+: Path to the Zynq cross-compiler toolchain if it is not on `PATH`. Not required
+ for the simulation server or documentation builds.
+
+The following symbols can be left at their defaults:
+
+`BUILD_DIR`
+: Path for built files.
+
+`PYTHON`
+: Path for python interpreter used during the build
+
+`MYSTMD_VERSION`
+: `mystmd version No.` Version of MyST required to build docs. defaults to 1.10.1.
+
+`DEFAULT_TARGETS`
+: Makefile targets built by `make` or `make default`. Default list: `driver`,
+ `server`, `sim_server`, `docs`.
+
+## Build targets
+
+| Target | Description |
+|--------------|----------------------------------------------------------|
+| `default` | Builds all targets listed in `$(DEFAULT_TARGETS)` |
+| `driver` | Kernel driver module |
+| `server` | Server binary to run on PandA |
+| `sim_server` | Simulation server to run on the local PC |
+| `docs` | HTML documentation |
+| `clean` | Removes the entire `$(BUILD_DIR)` directory |
+
+## Generated files
+
+After a successful build, `$(BUILD_DIR)` contains:
+
+`driver/`
+: Kernel module for hardware access.
+
+`server/` and `sim_server/`
+: The on-PandA server and the local simulation server respectively.
+
+`html/`
+: HTML documentation.
+
diff --git a/docs/how-to/contribute.md b/docs/how-to/contribute.md
new file mode 100644
index 00000000..6e419797
--- /dev/null
+++ b/docs/how-to/contribute.md
@@ -0,0 +1,2 @@
+```{include} ../../.github/CONTRIBUTING.md
+```
diff --git a/docs/how-to/startup.md b/docs/how-to/startup.md
new file mode 100644
index 00000000..f72ba4d7
--- /dev/null
+++ b/docs/how-to/startup.md
@@ -0,0 +1,63 @@
+# Start the PandA server
+
+The PandA socket server normally starts automatically at boot time or when the
+`zpkg-daemon` script is run. It is started and stopped by the script
+`etc/panda-server` installed at `/opt/etc/init.d`.
+
+The server can also be started from the command line with the following options.
+
+`-h`
+: Display help text listing all available command-line options.
+
+`-p` *port*
+: TCP port for configuration commands. Default: `8888`.
+
+`-d` *port*
+: TCP port for data capture. Default: `8889`.
+
+`-R`
+: Allow socket reuse via the `SO_REUSEADDR` socket option.
+
+`-c` *config-dir*
+: Directory from which the `config`, `registers`, and `description` files are
+ loaded. **This argument must be specified.**
+
+`-f` *persistence-file*
+: File from which persistence state is loaded on startup and to which it is saved
+ during operation. If not specified, persistence state is not saved.
+
+`-t` *[poll][:holdoff[:backoff]]*
+: Three parameters (in seconds) controlling writes to the persistence file.
+
+ Every *poll* seconds the server checks for configuration changes. If a change
+ is found, it waits a further *holdoff* seconds before writing. It then waits
+ *backoff* seconds before resuming polling.
+
+ Defaults: `poll` = 2, `holdoff` = 10, `backoff` = 60. With these defaults all
+ parameters reach the persistence file within 72 seconds.
+
+ The colon syntax lets you set any subset: for example `-t:20` sets *holdoff* = 20
+ while leaving *poll* and *backoff* unchanged.
+
+`-D`
+: Run the server as a daemon. This is the normal production mode; omit for
+ debugging.
+
+`-P` *pid-file*
+: Write the server process ID to the given file; the file is deleted on exit.
+
+`-T`
+: Config-file validation mode: the server exits immediately after loading
+ configuration files without accepting connections.
+
+`-M` *MAC-list*
+: Initialise up to four MAC address registers from the given file. The file may
+ contain comment lines (starting with `#`) and up to four MAC address lines,
+ each either blank or a six-octet address in `XX:XX:XX:XX:XX:XX` format.
+
+`-X` *port*
+: Connect to an extension server running locally on the specified port. See
+ [](/reference/extension.md) for details.
+
+`-r` *rootfs-version*
+: Specify the rootfs version string reported by the `*IDN?` command.
diff --git a/docs/images/PandA-logo-for-black-background.svg b/docs/images/PandA-logo-for-black-background.svg
new file mode 100644
index 00000000..f52449b4
--- /dev/null
+++ b/docs/images/PandA-logo-for-black-background.svg
@@ -0,0 +1,331 @@
+
+
+
+
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..2cd059c2
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,41 @@
+---
+site:
+ hide_outline: true
+---
+
+```{include} ../README.md
+:end-before:
+The downside to this mode is that if capture fails for any reason, then the last
+framed block of data that the server sent should be discarded, as it will have
+been corrupted while being sent.
+
+## Examples
+
+Some examples of data capture for different options follow.
+
+Default:
+
+```
+arm_time: 2021-05-26T10:34:06.133Z
+missed: 0
+process: Scaled
+format: ASCII
+fields:
+ PCAP.CAPTURE_TS double Trigger
+ COUNTER1.OUT double Triggered scale: 1 offset: 0 units:
+ COUNTER2.OUT double Triggered scale: 1 offset: 0 units:
+ PGEN1.OUT double Triggered scale: 1 offset: 0 units:
+
+ 1e-06 0 0 262143
+ 3e-06 0 0 262142
+ 5e-06 0 0 262141
+ 7e-06 0 0 262140
+ 9e-06 0 0 262139
+END 5 Ok
+```
+
+`BASE64`:
+
+```
+arm_time: 2021-05-26T10:34:06.133Z
+missed: 0
+process: Scaled
+format: Base64
+sample_bytes: 32
+fields:
+ PCAP.CAPTURE_TS double Trigger
+ COUNTER1.OUT double Triggered scale: 1 offset: 0 units:
+ COUNTER2.OUT double Triggered scale: 1 offset: 0 units:
+ PGEN1.OUT double Triggered scale: 1 offset: 0 units:
+
+ ju21oPfGsD4AAAAAAAAAAAAAAAAAAAAAAAAAAPj/D0FU5BBxcyrJPgAAAAAAAAAAAAAAAAAAAAAA
+ AAAA8P8PQfFo44i1+NQ+AAAAAAAAAAAAAAAAAAAAAAAAAADo/w9BuF8+WTFc3T4AAAAAAAAAAAAA
+ AAAAAAAAAAAAAOD/D0E/q8yU1t/iPgAAAAAAAAAAAAAAAAAAAAAAAAAA2P8PQQ==
+END 5 Ok
+```
+
+`XML`:
+
+```
+
+
+
+
+
+
+
+
+
+
+ 1e-06 0 0 262143
+ 3e-06 0 0 262142
+ 5e-06 0 0 262141
+ 7e-06 0 0 262140
+ 9e-06 0 0 262139
+END 5 Ok
+```
+
+## Getting captured data out
+
+This page describes the wire format on the data port. For end-to-end ways of
+retrieving captured data — reading the binary stream directly, using the Python
+client, or via EPICS/Tango — see the meta-panda how-to
+[Integrate with a PandA](xref:meta-panda/how-to/integrate-with-a-panda).
+
+
+As a starting point, captured data can be read out programmatically with the
+Python client's
+[`BlockingClient`](xref:PandABlocks-client#pandablocks.blocking.BlockingClient).
diff --git a/docs/reference/commands.md b/docs/reference/commands.md
new file mode 100644
index 00000000..ef23867e
--- /dev/null
+++ b/docs/reference/commands.md
@@ -0,0 +1,318 @@
+# Command interface
+
+The default server port for the command interface is port 8888. All commands and
+responses are in ASCII with lines separated by newline characters (ASCII
+character `0x0A`).
+
+All commands can be grouped into three forms (query, assignment, table
+assignment) and two targets (system and fields). There are exactly four possible
+response formats (ok, ok with value, error, multiple value). This section
+describes the command interface.
+
+The three basic command forms are:
+
+| Name | Format | Description |
+|---|---|---|
+| Query | *target*`?` | Interrogates *target* for the current value; can return an error, a single value, or a list of multiple values. |
+| Assignment | *target*`=`*value* | Updates *target* with the given value; can return an error or success. |
+| Table | *target*`<`*format* | Command may be followed by lines of text, and *must* be terminated by a blank line. |
+
+The four basic command responses are:
+
+| Name | Format | Description |
+|---|---|---|
+| Success | `OK` | Returned by assignment and table commands to report a successful update. |
+| Value | `OK =`*value* | Successful return of a single value from a query command. |
+| Error | `ERR` *error* | Error string returned on any command failure. |
+| Multi value | `!`*value* … `.` | Any number of values can be returned, each preceded by `!`, finally `.` by itself indicates end of input. |
+
+Command forms and their possible responses:
+
+| Form | Responses |
+|---|---|
+| Query | Error, Value, Multi value |
+| Assignment | Error, Success |
+| Table | Error, Success |
+
+Each individual query target will either return a single value or multi-value,
+as documented below.
+
+Finally, there are two basic types of target: configuration commands and system
+commands.
+
+## Configuration commands
+
+The entire hardware interface to PandA is structured into "blocks" and "fields",
+and each field may have a number of "attributes" depending on the field type.
+This structure is reflected in the form of the configuration commands tabulated
+below:
+
+| Command syntax | Description |
+|---|---|
+| *block*[*number*]`.`*field*`?` | Return current value of field. |
+| *block*[*number*]`.`*field*`=`*value* | Assign value to field. |
+| *block*[*number*]`.`*field*`<`[[`<`][`\|`]][`B`] | Write table data to field. `<` writes a fixed table, `<<` writes a streaming table and `<<\|` writes the last streaming table. |
+| *block*[*number*]`.`*field*`.`*attr*`?` | Return current value of field attribute. |
+| *block*[*number*]`.`*field*`.`*attr*`=`*value* | Assign value to field attribute. |
+| *block*[*number*]`.*?` | Returns list of fields. |
+| *block*[*number*]`.`*field*`.*?` | Returns list of field attributes. |
+
+In all of these commands the number after the block is optional if there is only
+one instance of that block, and is ignored for the two `.*?` commands. See the
+description of the `.TABLE` fields in [](/reference/fields.md) for an explanation
+of the optional format characters in the table write command.
+
+## System commands
+
+All system commands are prefixed with a leading `*` character. The simplest
+command is `*IDN?`, which returns a system identification string:
+
+```
+< *IDN?
+> OK =PandA SW: 4.1-1-g2a34851 FPGA: 4.2.0 04e81f37 8a2b0249 rootfs: 2023.2+snapshot-bb27e6aef4dafd501bb5d72c95facd178c92dc48 (langdale)
+```
+
+The available system commands are tabulated here and listed in more detail
+below:
+
+| Command | Description |
+|---|---|
+| `*IDN?` | Device identification. |
+| `*ECHO `*string*`?` | Echo. |
+| `*WHO?` | List connected clients. |
+| `*BLOCKS?` | List device blocks. |
+| `*DESC.`*block*`.`*field*[`.`*attr*]`?` `*DESC.`*block*`.`*field*`[].`*subfield*`?` | Show description for field, attribute, or table subfield. |
+| `*ENUMS.`*block*`.`*field*[`.`*attr*]`?` `*ENUMS.`*block*`.`*field*`[].`*subfield*`?` | List enumerations for field, attribute, or table subfield. |
+| `*CHANGES`[`.`*group*]`?` | Report changes to values. *group* can be any of `CONFIG`, `BITS`, `POSN`, `READ`, `ATTR`, `TABLE`, or `METADATA`. |
+| `*CHANGES`[`.`*group*]`=`[`E`\|`S`] | Reset reported changes; *group* as above. |
+| `*CAPTURE?` | Report fields configured for capture. |
+| `*CAPTURE.*?` | List all fields that can be captured. |
+| `*CAPTURE.`*name*`?` | Interrogate capture options; *name* can be `OPTIONS` or `ENUMS`. |
+| `*CAPTURE=` | Reset data capture. |
+| `*POSITIONS?` | Enumerate possible capture positions. |
+| `*BITS?` | Enumerate possible bit bus positions. |
+| `*VERBOSE=`*value* | Control command logging. |
+| `*PCAP.`*field*`?` | Special position capture status fields. *field* can be any of `STATUS`, `CAPTURED`, or `COMPLETION`. |
+| `*PCAP.`*field*`=` | Position capture actions. *field* can be either `ARM` or `DISARM`. |
+| `*SAVESTATE=` | Triggers immediate save to file of the persistence file state. |
+| `*CLOCK_FREQ?` | Returns currently configured system clock frequency. |
+
+`*IDN?`
+: Returns the system identification string, for example:
+
+ ```
+ OK =PandA SW: 1.1 FPGA: 0.1.9 d1275f61 00000000 rootfs: PandA 1.1
+ ```
+
+ The first field after "PandA" is the software version, the second is the FPGA
+ version, the third the firmware build number, and the fourth identifies the
+ supporting firmware. The final fields (prefixed `rootfs:`) identify the
+ underlying system on which the server is running.
+
+ Note that the `rootfs:` identification is new to version 1.1 of PandA.
+
+`*ECHO `*string*`?`
+: Returns the string back to the caller. Not terribly useful. Note that the
+ echoed string cannot contain any of `?`, `=` or `<`, as this would cause the
+ command to be mistaken for another command format. Example usage:
+
+ ```
+ < *ECHO This is a test?
+ > OK =This is a test
+ ```
+
+`*WHO?`
+: Returns a list of client connections, for example:
+
+ ```
+ < *WHO?
+ > !2015-12-04T14:30:40.403Z config 127.0.0.1:34185
+ > .
+ ```
+
+ The first field is the time the connection was made, the second is either
+ `config` or `data` depending on whether the configuration or data port is
+ connected, and the third is the remote IP address and socket.
+
+`*BLOCKS?`
+: Returns a list of all the top-level blocks in the system. The order in which
+ the blocks are returned is somewhat arbitrary. For example (here the list has
+ been shortened in the middle):
+
+ ```
+ < *BLOCKS?
+ > !TTLIN 6
+ > !OUTENC 4
+ ...
+ > !CLOCKS 1
+ > !BITS 1
+ > !QDEC 4
+ > .
+ ```
+
+ Block and field commands can be used to interrogate each block. The number
+ after each block records the number of instances of each block.
+
+`*DESC.`*block*`?` `*DESC.`*block*`.`*field*`?` `*DESC.`*block*`.`*field*`.`*attr*`?` `*DESC.`*block*`.`*field*`[].`*subfield*`?`
+: Returns the description string for the specified block, field, attribute, or
+ table subfield, e.g.:
+
+ ```
+ < *DESC.TTLIN?
+ > OK =TTL input
+ < *DESC.TTLIN.TERM?
+ > OK =Select TTL input termination
+ < *DESC.TTLIN.TERM.INFO?
+ > OK =Class information for field
+ ```
+
+`*ENUMS.`*block*`.`*field*`?` `*ENUMS.`*block*`.`*field*`.`*attr*`?` `*ENUMS.`*block*`.`*field*`[].`*subfield*`?`
+: Returns the list of enumerations for the given field, attribute, or table
+ subfield, if appropriate.
+
+`*CHANGES?` `*CHANGES.CONFIG?` `*CHANGES.BITS?` `*CHANGES.POSN?` `*CHANGES.READ?` `*CHANGES.ATTR?` `*CHANGES.TABLE?` `*CHANGES.METADATA?`
+: Reports changes to the appropriate group of values. Changes are reported since
+ the last request on the connection, and on the first request the current value
+ for every field will be reported. `*CHANGES?` reports changes for all groups;
+ otherwise one of the following groups can be selected:
+
+ | Group | Description |
+ |---|---|
+ | CONFIG | Configuration settings |
+ | BITS | Bits on the system bus |
+ | POSN | Positions |
+ | READ | Polled read values |
+ | ATTR | Attributes (including capture enable flags) |
+ | TABLE | Table changes |
+ | METADATA | Metadata field changes |
+
+ For example:
+
+ ```
+ < *CHANGES.CONFIG?
+ > !TTLIN1.TERM=High-Z
+ > !TTLIN2.TERM=50-Ohm
+ > !TTLIN3.TERM=High-Z
+ ...
+ > !QDEC2.B=TTLIN1.VAL
+ > !QDEC3.B=TTLIN1.VAL
+ > !QDEC4.B=TTLIN1.VAL
+ > .
+ ```
+
+ Here 804 (at the time of writing) lines have been deleted from the transcript.
+ If we repeat the call we see that no further changes have happened until
+ something is actually changed:
+
+ ```
+ < *CHANGES.CONFIG?
+ > .
+ < TTLOUT4.VAL=TTLIN3.VAL
+ > OK
+ < *CHANGES.CONFIG?
+ > !TTLOUT4.VAL=TTLIN3.VAL
+ > .
+ ```
+
+ Note that for tables only the fact that the table has changed is shown; no
+ attempt is made to show the current table value:
+
+ ```
+ < *CHANGES.TABLE?
+ > !PCOMP1.TABLE<
+ > !PCOMP2.TABLE<
+ > !PCOMP3.TABLE<
+ > !PCOMP4.TABLE<
+ > !PGEN1.TABLE<
+ > !PGEN2.TABLE<
+ > !SEQ1.TABLE<
+ > !SEQ2.TABLE<
+ > !SEQ3.TABLE<
+ > !SEQ4.TABLE<
+ > .
+ ```
+
+`*CHANGES=`[`E`\|`S`] `*CHANGES.CONFIG=`[`E`\|`S`] `*CHANGES.BITS=`[`E`\|`S`] `*CHANGES.POSN=`[`E`\|`S`] `*CHANGES.READ=`[`E`\|`S`] `*CHANGES.ATTR=`[`E`\|`S`] `*CHANGES.TABLE=`[`E`\|`S`] `*CHANGES.METADATA=`[`E`\|`S`]
+: These commands reset the change information for the corresponding group so that
+ only changes occurring after the reset are reported, or so that all changes are
+ reported. If `=` or `=E` (for End) is specified then only new changes are
+ reported; if `=S` (for Start) then change reporting is reset to the start as
+ for a new connection. For example:
+
+ ```
+ < TTLIN1.TERM=50-Ohm
+ > OK
+ < *CHANGES=
+ > OK
+ < *CHANGES.CONFIG?
+ > .
+ ```
+
+`*CAPTURE?`
+: Returns a list of all positions and bit masks that will be written to the data
+ capture port. This list is controlled by setting the `.CAPTURE` attribute on
+ the corresponding position fields.
+
+`*CAPTURE.*?`
+: Returns a list of all fields that can be configured for capture. This includes
+ all `pos_out` and `ext_out` fields.
+
+`*CAPTURE.OPTIONS?`
+: Lists the available capture options for `pos_out` fields. See
+ [](/reference/capture-options.md) for the full set of options.
+
+`*CAPTURE.ENUMS?`
+: Generates a curated list of capture option selections, designed for presenting
+ lists of available capture options as an enumeration. Returns the same as
+ calling `*ENUMS.`*name*`.`*field*`.CAPTURE?` on any `pos_out` field.
+
+`*CAPTURE=`
+: Resets all `.CAPTURE` flags to zero so that no data will be captured.
+
+`*POSITIONS?`
+: Lists all available position capture fields in order.
+
+`*BITS?`
+: Lists all available bit bus positions, but not including the special values
+ `ZERO` and `ONE`.
+
+`*VERBOSE=`*value*
+: If `*VERBOSE=1` is set then every command will be echoed to the server's log.
+ Set `*VERBOSE=0` to restore normal quiet behaviour.
+
+`*PCAP.STATUS?` `*PCAP.CAPTURED?` `*PCAP.COMPLETION?`
+: Interrogates the status of position capture:
+
+ | Field | Description |
+ |---|---|
+ | STATUS | Returns a string with three fields: "Busy" or "Idle", followed by the number of connected readers, and the number taking data. |
+ | CAPTURED | Returns the number of samples captured in the current or most recent data capture. |
+ | COMPLETION | Returns the completion status from the most recent data capture, as listed in the table below. |
+
+ The completion codes have the following meaning:
+
+ | Code | Meaning |
+ |---|---|
+ | Busy | Capture in progress. |
+ | Ok | Capture completed without error or intervention. |
+ | Disarmed | Capture was manually disarmed by `*PCAP.DISARM=` command. |
+ | Framing error | Data capture framing error, probably due to incorrectly configured capture. |
+ | DMA data error | Internal data error, should not occur. |
+ | Driver data overrun | Data capture too fast, internal buffers overrun. Can also occur if the PandA processor is overloaded. |
+
+`*PCAP.ARM=` `*PCAP.DISARM=`
+: Top-level capture control:
+
+ | Field | Description |
+ |---|---|
+ | ARM | Initiates data capture. Will fail if capture is already in progress, or no fields are configured for capture. |
+ | DISARM | Halts ongoing data capture. |
+
+`*SAVESTATE=`
+: Updates the persistence state file (as configured on the command line when
+ launched) with the current state. Returns after a filesystem `sync` call, so it
+ is safe to power off the system after this command has completed.
+
+`*CLOCK_FREQ?`
+: Returns the currently configured FPGA clock frequency, as used to convert
+ between times in natural units and times in clock ticks.
diff --git a/docs/reference/config.md b/docs/reference/config.md
new file mode 100644
index 00000000..c5b99e42
--- /dev/null
+++ b/docs/reference/config.md
@@ -0,0 +1,145 @@
+# Config, registers and description files
+
+On startup the PandA server loads its configuration from three files: `config`,
+`registers`, and `description`. When running the simulation these are loaded from
+the `config_d` directory in the build directory; on PandA they are loaded from
+`/opt/share/panda/config_d`.
+
+The syntax of each file mirrors the field definitions described in
+[](/reference/fields.md). All three files share a common structure: indentation
+indicates hierarchy, `#` starts a comment, block names appear in the first
+column, and fields are indented one level.
+
+| File | Purpose |
+|---------------|--------------------------------------------------------------|
+| `config` | Defines all blocks, their fields, and each field's behaviour |
+| `registers` | Maps each block and field to hardware register offsets |
+| `description` | Optionally provides human-readable descriptions |
+
+## Configuration file `config`
+
+This file defines all blocks and fields available to this PandA instance and is
+processed first.
+
+**Block definition syntax:**
+
+```
+block-name [ "[" count "]" ]
+ [ field-definition ]*
+```
+
+A block definition gives the block a name and optionally a repeat *count* (the
+number of hardware instances). It is followed by indented field definitions.
+
+**Field definition syntax:**
+
+```
+field-name field-type [ field-type-data ]
+```
+
+The *field-type* determines the basic function of the field, what operations are
+permitted on it, and how it maps to hardware registers.
+
+### Field types
+
+| Field type | Description |
+|-----------------------------------------|-----------------------------------------------------|
+| `param` *subtype* [`=` *value*] | Single 32-bit value written to a register; optional initial value |
+| `read` *subtype* | Read-only register |
+| `write` *subtype* | Write-only register (triggers an immediate action) |
+| `time` | Like `param` but 64-bit, spanning two registers |
+| `bit_out` | Output bit |
+| `pos_out` [*scale* [*offset* [*units*]]]| Position bus output; optional default scale/offset/units |
+| `ext_out` (`timestamp` \| `samples` \| `bits` *group*) | Extension bus entry needing special treatment |
+| `bit_mux` [`=` *value*] | Bit bus input selector; optionally defaults to `ONE` (129) |
+| `pos_mux` | Position bus input selector |
+| `table` [*row-words*] | Long table of numbers with special access methods |
+
+`ext_out` *ext-extra* values:
+
+| Value | Description |
+|-----------------|----------------------------------------------------------|
+| `timestamp` | Captures a 64-bit timestamp |
+| `samples` | Captures the sample count for data capture |
+| `bits` *group* | Captures 32 bits of the 128-bit bit bus; *group* selects which 32-bit slice |
+
+### Field subtypes
+
+`param`, `read`, and `write` fields require one of the following subtypes:
+
+| Subtype | Description |
+|-----------------------------------------|-----------------------------------------------------|
+| `uint` [*max-value*] | Unsigned integer, optionally bounded |
+| `int` | Signed integer |
+| `scalar` *scale* [*offset* [*units*]] | Fixed-point value with scale and optional offset/units |
+| `bit` | Single bit |
+| `action` | Write-only trigger with no data payload |
+| `lut` | 5-input lookup table function |
+| `enum` | Enumeration; followed by indented `number string` pairs |
+| `position` | Position value |
+| `time` | Time value |
+
+## Register file `registers`
+
+This file assigns hardware registers to each block and field. Its structure
+mirrors `config` but replaces field type specifications with register
+assignments.
+
+**Block definition syntax:**
+
+```
+block-name { [ "S" ] block-register | "X" } [ extension-module ]
+ [ field-definition ]*
+```
+
+- Prefix `S` on *block-register* allows the same register to be shared across
+ multiple blocks.
+- `X` instead of a register number marks a block with no register-mapped fields
+ (extension-only fields only).
+- An optional *extension-module* name enables the extension register syntax for
+ this block.
+
+**Field register syntax by type:**
+
+| Class | Register syntax |
+|------------------------|-------------------------------------------------------|
+| `param` | *register* \| *write-extension* |
+| `read` | *register* \| *read-extension* |
+| `write` | *register* \| *write-extension* |
+| `time` | *low-register* *high-register* |
+| `bit_out` | `(`*bit-index*`)`N |
+| `pos_out` | `(`*pos-index*`)`N |
+| `ext_out timestamp` | *ext-index* *ext-index* |
+| `ext_out` other | *ext-index* |
+| `bit_mux` | *register* |
+| `pos_mux` | *register* |
+| `table` (short) | `short` *size* *init-reg* *fill-reg* *length-reg* |
+| `table` (long/DMA) | `long` `2^`*size* *nbuf* *base-reg* *length-reg* |
+
+The notation `(...)`N means the register number is repeated N times, once per
+hardware instance of the block.
+
+### Extension register syntax
+
+When an extension server is enabled (see [](/how-to/startup.md) `-X` option) and
+an extension module is associated with a block, `param`, `read`, and `write`
+fields may be redirected to it:
+
+```
+read-extension = [ read-reg ]* "X" field-spec
+write-extension = [ read-reg ]* [ "W" [ write-reg ]* ] "X" field-spec
+```
+
+*field-spec* is passed to the extension module to bind the field. See
+[](/reference/extension.md) for details on extension fields.
+
+## Description file `description`
+
+The entire file is optional. Its syntax is:
+
+```
+block-name block-description
+ field-name field-description
+```
+
+Descriptions are newline-terminated UTF-8 strings.
diff --git a/docs/reference/extension.md b/docs/reference/extension.md
new file mode 100644
index 00000000..62ee4d47
--- /dev/null
+++ b/docs/reference/extension.md
@@ -0,0 +1,13 @@
+# Extension server
+
+:::{admonition} 🚧 TODO — documentation stub
+:class: warning
+
+This page is a Stage A scaffold stub and has not yet been written.
+
+**Status:** blocked: verify
+
+**Source:** extension.rst (739w: Extension Modules, Injected Values) — verify vs implementation
+
+**Blocked:** [PandABlocks/PandABlocks-server#80](https://github.com/PandABlocks/PandABlocks-server/issues/80)
+:::
diff --git a/docs/reference/fields.md b/docs/reference/fields.md
new file mode 100644
index 00000000..f1e0303e
--- /dev/null
+++ b/docs/reference/fields.md
@@ -0,0 +1,500 @@
+(fields)=
+# Blocks, fields and attributes
+
+The set of hardware blocks can be interrogated with the `*BLOCKS?` command:
+
+```
+< *BLOCKS?
+> !TTLIN 6
+> !OUTENC 4
+> !PCAP 1
+> !PCOMP 4
+> !TTLOUT 10
+> !ADC 8
+> !DIV 4
+> !INENC 4
+> !SLOW 1
+> !PGEN 2
+> !LVDSIN 2
+> !POSITIONS 1
+> !POSENC 4
+> !SEQ 4
+> !PULSE 4
+> !SRGATE 4
+> !LUT 8
+> !LVDSOUT 2
+> !COUNTER 8
+> !ADDER 1
+> !CLOCKS 1
+> !BITS 1
+> !QDEC 4
+> .
+```
+
+For each block the number after the block tells us how many instances there are
+of the block. Each block is controlled and interrogated through a number of
+fields, and the `block.*?` command can be used to interrogate the list of
+fields:
+
+```
+< TTLIN.*?
+> !VAL 1 bit_out
+> !TERM 0 param enum
+> .
+```
+
+This tells us that block `TTLIN` has two fields, `TTLIN.VAL` and `TTLIN.TERM`.
+The first field after the field name is a sequence number for user interface
+display, and the rest of each response describes the "type" of the field. In
+this case we see that `TTLIN.VAL` is a `bit_out` field, which means it can be
+used for bit data capture and can be connected to any `param bit_mux` field as a
+data source.
+
+Each field has one or more attributes depending on the field type. The list of
+attributes can be interrogated with the `block.field.*?` command:
+
+```
+< TTLIN.VAL.*?
+> !CAPTURE_WORD
+> !OFFSET
+> !INFO
+> .
+< TTLIN.TERM.*?
+> !INFO
+> .
+```
+
+All fields have the `.INFO` attribute, which just repeats the type information
+already reported, e.g. `TTLIN1.VAL.INFO?` returns `bit_out` (note that a block
+number must be specified when interrogating fields and attributes).
+
+## Field types
+
+Each field type determines the set of attributes available for the field. The
+types and their attributes are documented below.
+
+| Field type | Description |
+|---|---|
+| `param` *subtype* | Configurable parameter. The *subtype* determines the precise behaviour and the available attributes. |
+| `read` *subtype* | A read-only hardware field, used for monitoring status. Again, *subtype* determines available attributes. |
+| `write` *subtype* | A write-only field; *subtype* determines possible values and attributes. |
+| `time` | Configurable timer parameter. |
+| `bit_out` | Bit output, can be configured as bit input for `bit_mux` fields. |
+| `pos_out` | Position output, can be configured for data capture and as position input for `pos_mux` fields. |
+| `ext_out` *extra* | Extended output values, can be configured for data capture, but not available on position bus. |
+| `bit_mux` | Bit input with configurable delay. |
+| `pos_mux` | Position input multiplexer selection. |
+| `table` | Table data with special access methods. |
+
+`param` *subtype*
+: All fields of this type contribute to the `*CHANGES.CONFIG` change group and
+ are used to configure the behaviour of the corresponding block. Fields of this
+ type are used for input configuration and other behavioural settings.
+
+`read` *subtype*
+: All fields of this type contribute to the `*CHANGES.READ` change group, but
+ are only checked when either the field is read or the change group is polled.
+ Fields of this type are used for monitoring the internal status of a block,
+ and they cannot be written to.
+
+`write` *subtype*
+: Fields of this type can only be written and are used for immediate actions on
+ a block. The `action` subtype is used to support actions without any
+ parameters; for example the following command forces a soft reset on the given
+ pulse block:
+
+ ```
+ < PULSE1.FORCE_RESET=
+ > OK
+ ```
+
+`time`
+: Fields of this type are used for configuring delays. They also contribute to
+ `*CHANGES.CONFIG`. The following attributes are supported:
+
+ `UNITS`
+ : Can be set to any of the strings `min`, `s`, `ms`, or `us`, and is used to
+ interpret how values read and written to the field are interpreted.
+
+ `RAW`
+ : Can be read or written to report or set the delay in FPGA ticks.
+
+ The `UNITS` attribute determines how numbers read or written to the field are
+ interpreted. For example:
+
+ ```
+ < PULSE1.DELAY.UNITS=s
+ > OK
+ < PULSE1.DELAY=2.5
+ > OK
+ < PULSE1.DELAY.RAW?
+ > OK =312500000
+ < PULSE1.DELAY.UNITS=ms
+ > OK
+ < PULSE1.DELAY?
+ > OK =2500
+ ```
+
+ Note that changing `UNITS` doesn't change the delay, only how it is reported
+ and interpreted.
+
+`bit_out`
+: Fields of this type are used for block outputs which contribute to the
+ internal bit system bus, and they contribute to the `*CHANGES.BITS` change
+ group. They can be captured via the appropriate `PCAP.BITS`*n* block as
+ reported by the `CAPTURE_WORD` attribute.
+
+ `CAPTURE_WORD`
+ : Identifies which `pos_out` value can be used to capture this bit.
+
+ `OFFSET`
+ : The bit offset into the captured word of this particular bit.
+
+ For example:
+
+ ```
+ < TTLIN1.VAL.CAPTURE_WORD?
+ > OK =PCAP.BITS0
+ < TTLIN1.VAL.OFFSET?
+ > OK =2
+ ```
+
+ This tells us that if `PCAP.BITS0` is captured then `TTLIN1.VAL` can be read as
+ bit 2 of this word, counting from the least significant bit. The field itself
+ can be read to return the current value of the bit.
+
+`pos_out`
+: Fields of this type are used for block outputs which contribute to the
+ internal position bus, and they contribute to the `*CHANGES.POSN` change group.
+ The following attribute supports capture control:
+
+ `CAPTURE`
+ : Manages capture of this field. One of the following enumeration values can be
+ written:
+
+ | Value | Description |
+ |---|---|
+ | No | Capture is disabled for this field. |
+ | Value | The value at the time of trigger will be captured. |
+ | Diff | The difference of values is captured. |
+ | Sum | The sum of all valid values is captured. This is a 64-bit value, and may be further scaled if `PCAP.SHIFT_SUM` is set. |
+ | Mean | The average of all valid values is captured. |
+ | Min | The minimum of all valid values is captured. |
+ | Max | The maximum of all valid values is captured. |
+ | Min Max | Both minimum and maximum values are captured. |
+ | Min Max Mean | All three values — minimum, maximum, average — are captured. |
+ | StdDev | The standard deviation of valid values is captured. Only available if supported by the FPGA configuration. |
+ | Mean StdDev | Both average and standard deviation are captured. Only available if supported by the FPGA configuration. |
+
+ Combinations of the individual options can also be written as a
+ space-separated list — see [](/reference/capture-options.md).
+
+ The following attributes support formatting of the field when reading it; the
+ current value is returned subject to the formatting rules described below.
+
+ `OFFSET`, `SCALE`
+ : Configure the conversion from the underlying position to the value captured
+ when scaling is enabled and read from the `SCALED` attribute.
+
+ `UNITS`
+ : Can be set to any UTF-8 string, provided for the convenience of the user
+ interface and returned as part of the data capture heading.
+
+ `SCALED`
+ : Returns the scaled value computed as `value * scale + offset`.
+
+`ext_out` *extra*
+: Fields of this type represent values that can be captured but which are not
+ present on the position bus. These fields also support one capture control
+ field:
+
+ `CAPTURE`
+ : As for `pos_out`, can be set to control capture of this field:
+
+ | Value | Description |
+ |---|---|
+ | No | This field will not be captured. |
+ | Value | This field will be captured. |
+
+ The *extra* field determines the detailed behaviour of this field, and will be
+ one of the following values:
+
+ | extra value | Description |
+ |---|---|
+ | `timestamp` | Timestamps in clock ticks with optional scaling to seconds on data capture. |
+ | `samples` | Special internal field for counting captured samples. |
+ | `bits` | Used to implement bit-bus readout fields. Fields of this sub-type implement an extra `BITS` field. |
+
+ Fields of type `ext_out bits` implement an extra attribute:
+
+ `BITS`
+ : Returns a list of all bit fields associated with this field. Fields of this
+ type can be used to capture a snapshot of the bit bus at the trigger time.
+
+`bit_mux`
+: Bit input selectors for blocks. Each of these fields can be set to the name of
+ a corresponding `bit_out` field, for example:
+
+ ```
+ < TTLOUT1.VAL=TTLIN1.VAL
+ > OK
+ ```
+
+ There are two attributes:
+
+ `DELAY`
+ : Can be set to any value between 0 and `MAX_DELAY` to delay the bit input to
+ the block by the specified number of clock ticks.
+
+ `MAX_DELAY`
+ : Returns the maximum delay that can be set for this input.
+
+`pos_mux`
+: Position input selectors for blocks. Each of these fields can be set to the
+ name of a corresponding `pos_out` field, for example:
+
+ ```
+ < ADDER1.INPA=ADC2.OUT
+ > OK
+ ```
+
+`table`
+: Values of this type are used for long tables of numbers. This server imposes
+ no structure on these values apart from treating them as an array of 32-bit
+ integers.
+
+ Table values are written with the special `<` syntax:
+
+ | Operator | Description |
+ |---|---|
+ | `block`*number*`.field<` | Normal table write, fixed table |
+ | `block`*number*`.field<<` | Normal table write, streaming table |
+ | `block`*number*`.field<<\|` | Normal table write, last streaming table |
+ | `block`*number*`.field OK
+ < SEQ3.TABLE.LENGTH?
+ > OK =12
+ ```
+
+ Note that when data is sent in base-64 format, each individual line must
+ encode a multiple of four bytes, otherwise the write will be rejected. For full
+ details of fixed vs. streaming (`<<` / `<<|`) table writes and DMA buffer
+ sizing, see [](/reference/streaming-tables.md).
+
+ The following attributes are provided by this field type:
+
+ `MAX_LENGTH`
+ : The maximum number of 32-bit words which can be stored in the table.
+
+ `LENGTH`
+ : The current number of words in the table.
+
+ `B`
+ : This read-only attribute returns the content of the table in base-64.
+
+ `FIELDS`
+ : Returns a list of strings which can be used to interpret the content of the
+ table. Each line returned is of the following format:
+
+ ```
+ left:right field-name subtype
+ ```
+
+ Here *left* and *right* are bit field indices into a single table row,
+ consisting of a number of 32-bit words concatenated (in little-endian order)
+ with bits numbered from 0 in the least significant position up to
+ 32×`ROW_WORDS`−1, and *left* ≥ *right*. The name of the field is given by
+ *field-name*, and *subtype* can be one of `int`, `uint`, or `enum`. If
+ *subtype* is `enum` then the list of enums can be interrogated through the
+ command:
+
+ ```
+ *ENUMS.block.table[].field?
+ ```
+
+ where *block*, *table*, *field* are appropriate names.
+
+ `ROW_WORDS`
+ : Returns the number of 32-bit words in a single row of the table. This can be
+ used to help interpret the `FIELDS` result.
+
+ `QUEUED_LINES`
+ : When a fixed table is written, returns the number of lines in that table.
+ When streaming tables are written, returns the number of lines that have been
+ scheduled, including the ones currently being used by the FPGA.
+
+ `MODE`
+ : Indicates the mode that the table is in as a consequence of the last table
+ write. The possible values are `INIT` (no table), `FIXED` (fixed table),
+ `STREAMING` (streaming table) and `STREAMING_LAST` (last streaming table).
+ Writing an empty table always moves the table to `INIT` state. In addition to
+ this mode, there is also an implicit completed state in the FPGA that happens
+ either when there is a sudden error or when the streaming is finished; it will
+ cause any future writes to be rejected and the `MODE` attribute will keep the
+ last value until a reset is done, to ensure the client is aware of any error.
+
+ The following table summarises the mode transitions for each table command,
+ where `<0` represents writing an empty table:
+
+ | MODE \ command | `<` | `<<` | `<<\|` | `<0` |
+ |---|---|---|---|---|
+ | `INIT` | `FIXED` | `STREAMING` | `STREAMING_LAST` | `INIT` |
+ | `FIXED` | `FIXED` | `STREAMING` | `STREAMING_LAST` | `INIT` |
+ | `STREAMING` | Reject | `STREAMING` | `STREAMING_LAST` | `INIT` |
+ | `STREAMING_LAST` | Reject | Reject | Reject | `INIT` |
+ | `COMPLETED[HEALTH]` | Reject | Reject | Reject | `INIT` |
+
+ Writing empty tables with `<<` or `<<|` is rejected to avoid accidental
+ mistakes in streaming mode.
+
+## Field sub-types
+
+The following field sub-types can be used for `param`, `read` and `write`
+fields.
+
+`uint` [*max-value*]
+: The most basic type: the value read or written is an unsigned 32-bit number.
+ There is one fixed attribute:
+
+ `MAX`
+ : Returns the maximum value that can be written to this field.
+
+`int`
+: Similar to `uint`, but signed, and there is no upper limit on the value.
+
+`scalar` *scale* [*offset* [*units*]]
+: Floating point values can be read or written, and are converted from and to the
+ underlying signed integer type via the equations below:
+
+ ```
+ value = scale * raw + offset
+ raw = (value - offset) / scale
+ ```
+
+ The following attributes are supported:
+
+ `UNITS`
+ : Returns the configured units string.
+
+ `RAW`
+ : Returns the underlying unconverted integer value.
+
+ `SCALE`
+ : Returns the configured scaling factor.
+
+ `OFFSET`
+ : Returns the configured scaling offset.
+
+`bit`
+: A value which is 0 or 1; there are no extra attributes.
+
+`action`
+: A value which cannot be read and always writes as 0. Only useful for `write`
+ fields.
+
+`lut`
+: This field sub-type is used for the 5-input lookup table function calculation
+ field. This field can be set to any valid logical expression generated from
+ inputs `A` to `E` using the standard operators `&`, `|`, `^`, `~`, `?:` from C
+ together with `=` for equality and `=>` for implication (`A=>B` abbreviates
+ `~A|B`). All operations have C precedence, `=` has the same precedence as `==`
+ in C, and `=>` has precedence between `|` and `?:`.
+
+ The following attribute is supported:
+
+ `RAW`
+ : Returns the corresponding lookup table assignment as a 32-bit number.
+
+ For example:
+
+ ```
+ < LUT2.FUNC=A=>B?C:D
+ > OK
+ < LUT2.FUNC?
+ > OK =A=>B?C:D
+ < LUT2.FUNC.RAW?
+ > OK =0xF0CCF0F0
+ ```
+
+`enum`
+: Enumeration fields define a list of valid strings which can be written to the
+ field. To interrogate the list of valid enumeration values use the `*ENUMS`
+ command, for example:
+
+ ```
+ < *ENUMS.TTLIN1.TERM?
+ > !High-Z
+ > !50-Ohm
+ > .
+ ```
+
+`time`
+: Converts between time in specified units and time in FPGA clock ticks. The
+ following attributes are supported:
+
+ `UNITS`
+ : Can be set to any of the strings `min`, `s`, `ms`, or `us`, and is used to
+ interpret how values read and written to the field are interpreted.
+
+ `RAW`
+ : Can be read or written to report or set the delay in FPGA ticks.
+
+## Summary of sub-types
+
+| Sub-type | Attributes | Description |
+|---|---|---|
+| uint | MAX | Possibly bounded 32-bit unsigned integer value |
+| int | | Unbounded 32-bit signed integer value |
+| scalar | RAW, UNITS, SCALE, OFFSET | Scaled signed floating point value |
+| bit | | Bit: 0 or 1 |
+| action | | Write only, no value |
+| lut | RAW | 5-input lookup table logical formula |
+| enum | | Enumeration selection (labels listed via the `*ENUMS` command) |
+| time | RAW, UNITS | Time intervals converted to FPGA ticks |
+
+## Summary of attributes
+
+| Field (sub)type | Attribute | Description | R | W | C | M |
+|---|---|---|:-:|:-:|:-:|:-:|
+| (all) | INFO | Returns type of field | R | | | |
+| uint | MAX | Maximum allowed integer value | R | | | |
+| scalar | RAW | Underlying integer value | R | W | | |
+| | UNITS | Configured units for scalar | R | | | |
+| | SCALE | Configured scaling factor for scalar | R | | | |
+| | OFFSET | Configured scaling offset for scalar | R | | | |
+| lut | RAW | Computed lookup table 32-bit value | R | | | |
+| time | UNITS | Units and scaling selection for time | R | W | C | |
+| | RAW | Raw time in FPGA clock cycles | R | W | | |
+| bit_out | CAPTURE_WORD | Capturable word containing this bit | R | | | |
+| | OFFSET | Offset of this bit in captured word | R | | | |
+| bit_mux | DELAY | Bit input delay in FPGA ticks | R | W | C | |
+| | MAX_DELAY | Maximum valid delay | R | | | |
+| pos_out | CAPTURE | Position capture control | R | W | C | |
+| | OFFSET | Position offset | R | W | C | |
+| | SCALE | Position scaling | R | W | C | |
+| | UNITS | Position units | R | W | C | |
+| | SCALED | Position after applying scaling | R | | | |
+| ext_out bits | BITS | List of bit_out fields | R | | | M |
+| table | MAX_LENGTH | Maximum table length in 32-bit words | R | | | |
+| | LENGTH | Current table length in 32-bit words | R | | | |
+| | B | Table data in base-64 | R | | | M |
+| | FIELDS | Table field descriptions | R | | | M |
+| | ROW_WORDS | Number of words in a table row | R | | | |
+
+Key:
+
+- **R** — Attribute can be read.
+- **W** — Attribute can be written.
+- **C** — Attribute contributes to the `*CHANGES.ATTR` change set.
+- **M** — Attribute returns a multiple-value result.
diff --git a/docs/reference/streaming-tables.md b/docs/reference/streaming-tables.md
new file mode 100644
index 00000000..835ba1d6
--- /dev/null
+++ b/docs/reference/streaming-tables.md
@@ -0,0 +1,88 @@
+# Streaming tables
+
+Tables in PandA operate in two modes: **fixed** (a single write replaces the
+whole table) and **streaming** (a sequence of writes fills a DMA-backed ring
+buffer that the hardware consumes continuously). Streaming is used for long,
+DMA-driven tables — for example the SEQ sequencer table — where the host needs
+to supply data faster than a single write can deliver.
+
+## Write operators
+
+The operator appended to the field name on the configuration port determines
+which mode is used:
+
+| Operator | Encoding | Meaning |
+|----------|----------|--------------------------------------------------|
+| `<` | text | Fixed table write |
+| `<<` | text | Streaming table write (more data follows) |
+| `<<\|` | text | Streaming table write, last chunk |
+| `