Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/docs_pre_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ jobs:
ROGUE_DOCS_SITE_ROOT: /${{ github.event.repository.name }}
steps:
- name: Checkout source
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
token: ${{ secrets.GH_TOKEN }}

- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
cache: "pip"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/docs_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
REPO_NAME: ${{ github.event.repository.name }}
steps:
- name: Checkout source
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ inputs.target_ref || github.ref }}
Expand Down Expand Up @@ -118,7 +118,7 @@ jobs:
echo "DOCS_LATEST_TARGET_VERSION=$latest_tag" >> "$GITHUB_ENV"

- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
cache: "pip"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/perf_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
ROGUE_SITE_ROOT: /${{ github.event.repository.name }}
steps:
- name: Checkout source
uses: actions/checkout@v6
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha }}
Expand All @@ -46,7 +46,7 @@ jobs:
- name: Download perf artifact
id: perf_artifact
continue-on-error: true
uses: actions/download-artifact@v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: perf-results
path: perf-results
Expand Down
26 changes: 13 additions & 13 deletions .github/workflows/rogue_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ jobs:
steps:

# This step checks out a copy of your repository.
- uses: actions/checkout@v6
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

- name: Check for trailing whitespace and tabs
run: ./scripts/check_whitespace.sh

- uses: actions/setup-python@v6
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: 3.12
cache: 'pip'
Expand Down Expand Up @@ -116,7 +116,7 @@ jobs:
# Dual-language Codecov upload; non-blocking
- name: Upload Coverage to Codecov
if: always()
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
continue-on-error: true
with:
files: ./coverage-py.xml,./coverage-cpp.xml
Expand All @@ -132,7 +132,7 @@ jobs:
steps:

# This step checks out a copy of your repository.
- uses: actions/checkout@v6
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

Expand Down Expand Up @@ -161,11 +161,11 @@ jobs:
runs-on: ubuntu-24.04
steps:

- uses: actions/checkout@v6
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

- uses: actions/setup-python@v6
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: 3.12
cache: 'pip'
Expand Down Expand Up @@ -226,12 +226,12 @@ jobs:
steps:

# This step checks out a copy of your repository.
- uses: actions/checkout@v6
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1

- name: Setup Miniforge
uses: conda-incubator/setup-miniconda@v3
uses: conda-incubator/setup-miniconda@8ee1f361103df19b6f8c8655fd3967a8ecb162d5 # v4.0.1
with:
miniforge-variant: Miniforge3
miniforge-version: latest
Expand All @@ -242,14 +242,14 @@ jobs:
use-only-tar-bz2: true

- name: Cache conda packages
uses: actions/cache@v4
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/conda_pkgs_dir
key: conda-pkgs-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('conda.yml') }}

- name: Cache conda environment
id: conda-env-cache
uses: actions/cache@v4
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.CONDA }}/envs/rogue_build
key: conda-env-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('conda.yml') }}
Expand Down Expand Up @@ -291,11 +291,11 @@ jobs:
runs-on: ubuntu-24.04
steps:

- uses: actions/checkout@v6
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

- uses: actions/setup-python@v6
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: 3.12
cache: 'pip'
Expand Down Expand Up @@ -333,7 +333,7 @@ jobs:

- name: Upload Perf Results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: perf-results
path: perf-results/*.json
Expand Down
169 changes: 169 additions & 0 deletions docs/src/pyrogue_tree/core/device.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,175 @@ The built-in ``enable`` Variable is especially important because it lets a tree
keep its full structure visible while disabling hardware interaction for one
subtree.

.. _pyrogue_tree_device_enable_dependencies:

Dependency-Controlled Device Enable
===================================

Some Devices are reachable only while another part of the system is ready. For
example, a front-end board accessed through a serial link must not receive
register transactions while that link is down. Likewise, a peripheral subtree
should not be accessed while its power rail is off.

Pass those readiness Variables to the Device's ``enableDeps`` argument. The
Device remains present in the tree, but its memory Blocks are enabled only when
its own ``enable`` setting, its parent Device, and every enable dependency all
permit access.

The following example models a front-end FPGA reached through a PGP link. The
``PgpFrontEndLane`` Device contains the PGP4 AXI-L status registers and a
``FrontEnd`` Device whose enable state depends on the remote-link status.

.. code-block:: python

import pyrogue as pr
import surf.protocols.pgp

class FrontEnd(pr.Device):
"""Registers implemented by the FPGA at the far end of the link."""

def __init__(self, **kwargs):
super().__init__(description='Remote front-end FPGA', **kwargs)

self.add(pr.RemoteVariable(
name='FpgaVersion',
description='Front-end firmware version',
offset=0x0000,
bitSize=32,
mode='RO',
))

self.add(pr.RemoteVariable(
name='AcquisitionEnable',
description='Enable front-end data acquisition',
offset=0x0004,
bitSize=1,
base=pr.Bool,
mode='RW',
))

class PgpFrontEndLane(pr.Device):
"""One local PGP4 link and the front end reached through that link."""

def __init__(self, *, pgpMemBase, frontEndMemBase, **kwargs):
super().__init__(description='PGP-connected front end', **kwargs)

# Local PGP4 control and status registers.
self.add(surf.protocols.pgp.Pgp4AxiL(
name='Pgp4',
memBase=pgpMemBase,
offset=0x0000,
numVc=4,
))

# Registers implemented by the FPGA at the far end of the link.
self.add(FrontEnd(
name='FrontEnd',
memBase=frontEndMemBase,
enableDeps=[self.Pgp4.RxStatus.RemRxLinkReady],
))

The separate ``pgpMemBase`` and ``frontEndMemBase`` arguments reflect one
common PGP/SRP topology, but they are not an ``enableDeps`` requirement. A
dependency Variable may use the same memory interface as the dependent Device,
a different interface, or no memory interface at all. ``enableDeps`` only
requires a Variable object that emits updates.

``Pgp4.RxStatus.RemRxLinkReady`` is a Boolean RemoteVariable provided by
SURF's ``surf.protocols.pgp.Pgp4AxiL``. SURF polls it once per second,
so link transitions automatically update ``FrontEnd.enable``. When the remote
receiver is not ready, PyRogue disables the ``FrontEnd`` Blocks, and recursive
reads, writes, and configuration loads do not issue memory transactions to that
Device. When the remote receiver becomes ready again, PyRogue re-enables those
Blocks automatically.

Polling The Link Status Is Required
-----------------------------------

``enableDeps`` does not read or poll its dependency Variables. It registers for
their update notifications and re-evaluates the Device enable state only when
an update is processed. A cached link-ready value therefore cannot protect a
Device from later link transitions unless something continues to read that
status register.

In this example, SURF gives ``RemRxLinkReady`` a one-second ``pollInterval``.
That polling occurs only while Root polling is enabled. The default
``Root(pollEn=True)`` behavior is appropriate; constructing the Root with
``pollEn=False`` or setting ``Root.PollEn`` false stops the periodic reads and
prevents ``FrontEnd.enable`` from following subsequent link changes.

Keep the dependency Variable outside the Device that it gates, as shown above.
The local ``Pgp4`` status Device remains readable while ``FrontEnd`` is
disabled, so its next poll can detect link recovery and re-enable the remote
Blocks. Placing the link-status register inside ``FrontEnd`` would create a
deadlock in the design: disabling ``FrontEnd`` would also disable the Block
that must be read to discover recovery.

If an application intentionally runs with polling disabled, it must explicitly
read the dependency whenever it needs to refresh the enable state. Updates are
processed asynchronously, so wait for the queued update before using the
effective state:

.. code-block:: python

lane.Pgp4.RxStatus.RemRxLinkReady.get(read=True)
root.waitOnUpdate()

if lane.FrontEnd.enable.value() is True:
lane.FrontEnd.readBlocks()

This explicit read is only a snapshot. Repeat it whenever the link might have
changed, or prefer continuous polling for automatic protection. Until the first
poll or explicit read is processed, a Device with ``enableDeps`` remains in the
``'deps'`` state and its Blocks stay disabled.

Effective Enable States
-----------------------

``device.enable`` represents both the user's enable choice and any automatic
gating. Its effective value is:

.. list-table::
:header-rows: 1
:widths: 30 25 45

* - Condition
- ``device.enable.value()``
- Display value
* - The Device was explicitly disabled
- ``False``
- ``False``
* - One or more enable dependencies are false
- ``'deps'``
- ``ExtDepFalse``
* - An ancestor Device is not effectively enabled
- ``'parent'``
- ``ParentFalse``
* - All conditions permit access
- ``True``
- ``True``

An explicit ``device.enable.set(False)`` takes precedence over dependency and
parent state. Setting it back to ``True`` restores dependency-controlled
behavior; it does not bypass a false dependency or disabled parent.

Practical Rules
---------------

* Pass Variable objects, not paths or names, and construct each dependency
before constructing the dependent Device.
* Multiple entries use AND semantics: every dependency value must be truthy.
Boolean status Variables are clearest, although integer ``0``/``1``
Variables work as well.
* A dependency controls hardware access; it does not remove or hide the Device,
change the user's stored enable choice, or write the dependency Variable.
* Dependency changes take effect through PyRogue's Variable update mechanism.
Ensure hardware status Variables are read or polled so their updates reach
the dependent Device.
* Use ``device.enable.value() is True`` when testing whether a Device is
effectively enabled. A general truthiness check is incorrect because the
explanatory states ``'deps'`` and ``'parent'`` are non-empty strings.

Composition And Tree Structure
==============================

Expand Down
6 changes: 4 additions & 2 deletions python/pyrogue/_Device.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,10 @@ class Device(pr.Node,rim.Hub):
Initial enable state.
defaults : dict, optional
Default variable values keyed by name.
enableDeps : iterable, optional
Enable dependency variables.
enableDeps : iterable of BaseVariable, optional
Variables that gate this Device's effective enable state. All
dependency values must be truthy before the Device's memory Blocks are
enabled. See :ref:`pyrogue_tree_device_enable_dependencies`.
hubMin : int, optional (default = 0)
Hub minimum access size.
hubMax : int, optional (default = 0)
Expand Down
15 changes: 8 additions & 7 deletions python/pyrogue/_HelperFunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,22 +336,23 @@ class PyrogueDumper(yaml.Dumper):
pass

def _var_representer(dumper: yaml.Dumper, data: pr.VariableValue) -> Any:
"""Represent ``VariableValue`` using display formatting and YAML typing."""
if isinstance(data.value, bool):
"""Represent floats natively and other values by display text."""
if data.valueDisp is None:
return dumper.represent_scalar('tag:yaml.org,2002:null',u'null')
elif isinstance(data.value, bool):
enc = 'tag:yaml.org,2002:bool'
elif data.enum is not None:
enc = 'tag:yaml.org,2002:str'
elif isinstance(data.value, int):
# Preserve readable integer formats such as hexadecimal in saved
# configs; unsafe display formats need a separate compatibility policy.
enc = 'tag:yaml.org,2002:int'
elif isinstance(data.value, float):
enc = 'tag:yaml.org,2002:float'
return dumper.represent_float(float(data.value))
else:
enc = 'tag:yaml.org,2002:str'

if data.valueDisp is None:
return dumper.represent_scalar('tag:yaml.org,2002:null',u'null')
else:
return dumper.represent_scalar(enc, data.valueDisp)
return dumper.represent_scalar(enc, data.valueDisp)

def _dict_representer(dumper: yaml.Dumper, data: odict) -> Any:
"""Represent ``OrderedDict`` values while preserving key order."""
Expand Down
Loading
Loading