LinkVariable
LinkVariable exposes a derived value backed by one or more
dependency Variables. It is useful for unit conversion, bit-field composition,
and user-friendly computed views.
The most common application use case is converting raw ADC or DAC register values into real engineering units while still preserving direct access to the underlying raw Variables.
When To Use LinkVariable
Use LinkVariable when the important user-facing value is derived from one
or more other Variables rather than owned directly by software or mapped
directly to hardware.
That makes it the presentation-oriented counterpart to the other Variable types:
LocalVariableowns software stateRemoteVariablemaps hardware stateLinkVariablecomputes or transforms a value from dependencies
This is often the best way to expose engineering units, calibrated views, composite bit fields, or operator-friendly abstractions without hiding the raw registers that developers may still need.
What You Usually Set
Most LinkVariable definitions use the shared Variable parameters from
Variable, plus the linked-logic parameters that make
the node derived rather than directly stored:
dependenciesfor the source Variables.linkedGetfor read-side conversion.linkedSetfor write-side conversion.
Some code also uses the variable=... shortcut to mirror another Variable
directly and then override only selected presentation properties or callback
behavior.
When you pass variable=some_var, PyRogue automatically uses that
Variable’s get() and set() methods as the linked access behavior and
adds it as a dependency. In other words, this is the quick way to build a
single-source LinkVariable without spelling out both dependencies and
the default callbacks yourself.
This is most useful when you want a second view of one Variable that differs mainly in presentation, naming, grouping, or a small override to the default linked behavior.
Callbacks And Dependencies
LinkVariable typically uses:
dependencies: source Variables used by linked logiclinkedGet: compute displayed value from dependencieslinkedSet: convert user value back to dependency writes
In the current implementation, the callback wrappers support keyword arguments matching:
linkedGet(dev=None, var=None, read=True, index=-1, check=True)linkedSet(dev=None, var=None, value=..., write=True, index=-1, verify=True, check=True)
The callback may accept any subset of those parameters. This gives the linked logic
access to the surrounding Device, the LinkVariable itself, and the
normal read/write control flags when needed.
The read and write controls are especially important:
readtellslinkedGetwhether the caller wants fresh data from the dependencies or only the currently cached values.writetellslinkedSetwhether dependency updates should actually be committed or only staged locally.
That means a good LinkVariable implementation usually preserves the
caller’s intent. If a caller requests get(read=False), the linked logic
should usually avoid forcing extra hardware reads. If a caller requests
set(write=False), the linked logic should usually avoid committing the
dependency writes immediately.
This is one of the most important design points for LinkVariable callbacks:
they should usually forward read and write into dependency accesses
rather than hard-code read=True or write=True.
LinkVariable Chaining
LinkVariable dependencies can include other LinkVariable instances, so
you can build layered conversions (for example raw ADC counts -> volts ->
engineering quantity such as power, temperature, or calibrated sensor units).
This is useful when:
One conversion step is reused by multiple higher-level views
You want to separate low-level scaling from calibration or application logic
Derived values should remain readable and testable as independent Nodes
Design Guidance
Good LinkVariable usage usually follows a simple rule: keep the raw
Variables visible, and use LinkVariable to add a clearer operational view
rather than to hide the hardware interface entirely.
In practice:
Use
RemoteVariablefor the raw register surfaceAdd one or more
LinkVariableNodes for engineering units or derived viewsKeep the conversion logic small, readable, and easy to test
Prefer helper functions over dense lambdas once the math or policy becomes non-trivial
Preserve caller intent by forwarding
readandwriteinto dependency accesses unless the linked behavior intentionally overrides that policyConsider subclassing
LinkVariablewhen the same linked-access pattern is reused across many nodes
Examples
Mirroring A Single Variable With variable=
The variable=... shortcut is useful when you want a second tree-facing
view of one Variable without writing explicit dependencies,
linkedGet, and linkedSet boilerplate.
import pyrogue as pr
class PowerMonitor(pr.Device):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add(pr.RemoteVariable(
name='VoltageRaw',
offset=0x00,
bitSize=16,
mode='RO',
base=pr.UInt,
hidden=True,
))
self.add(pr.LinkVariable(
name='VoltageCounts',
variable=self.VoltageRaw,
mode='RO',
disp='{:#06x}',
description='Mirror of VoltageRaw with operator-facing formatting',
))
In this pattern, VoltageCounts reuses VoltageRaw for storage and read
behavior, but presents it as a separate node with its own name, description,
and formatting. This is a good fit when you want an alternate tree-facing view
without introducing any new conversion logic.
Engineering-Unit View Of A Raw Register
One common pattern is to pair a hidden raw RemoteVariable with a
user-facing LinkVariable that converts the raw count into engineering
units.
import pyrogue as pr
class TempMonitor(pr.Device):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add(pr.RemoteVariable(
name='TempRaw',
offset=0x100,
bitSize=12,
mode='RO',
base=pr.UInt,
))
self.add(pr.LinkVariable(
name='Temperature',
units='degC',
mode='RO',
dependencies=[self.TempRaw], # add()ed Nodes can be accessed as attributes
linkedGet=lambda var, read=True: (var.dependencies[0].get(read=read) * 0.1) - 40.0,
))
The important detail is that linkedGet forwards the caller’s read
choice to the dependency. That preserves the normal Variable behavior: callers
can choose between a fresh read and the currently cached value.
Composite Value Built From Multiple Register Fields
Another common pattern is assembling one logical value from several underlying register fields.
import pyrogue as pr
class MyAdc(pr.Device):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add(pr.RemoteVariable(name='MaskLow', offset=0x10, bitSize=4, mode='RW'))
self.add(pr.RemoteVariable(name='MaskHigh', offset=0x14, bitSize=4, mode='RW'))
self.add(pr.RemoteVariable(name='MaskDf', offset=0x14, bitSize=2, bitOffset=4, mode='RW'))
def _get_mask(var, read=True):
low = var.dependencies[0].get(read=read)
high = var.dependencies[1].get(read=read)
df = var.dependencies[2].get(read=read)
return (df << 8) | (high << 4) | low
def _set_mask(var, value, write=True):
var.dependencies[0].set(value & 0xF, write=False)
var.dependencies[1].set((value >> 4) & 0xF, write=False)
var.dependencies[2].set((value >> 8) & 0x3, write=False)
if write:
var.parent.writeBlocks()
self.add(pr.LinkVariable(
name='DeviceMask',
disp='{:#b}',
dependencies=[self.MaskLow, self.MaskHigh, self.MaskDf],
linkedGet=_get_mask,
linkedSet=_set_mask,
))
This pattern is useful when the hardware register map is fragmented but the user-facing control should look like one coherent value.
ADC Counts to Input Voltage (Read-Only)
import pyrogue as pr
class AdcMonitor(pr.Device):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add(pr.RemoteVariable(
name='AdcRaw',
offset=0x200,
bitSize=16,
mode='RO',
base=pr.UInt,
))
# 16-bit ADC, 2.5 V full-scale, no offset
self.add(pr.LinkVariable(
name='InputVoltage',
units='V',
mode='RO',
disp='{:.6f}',
dependencies=[self.AdcRaw],
linkedGet=lambda var, read=True: (
var.dependencies[0].get(read=read) * (2.5 / 65535.0)
),
))
DAC Voltage Setpoint to Raw Code (Read/Write)
import pyrogue as pr
class DacControl(pr.Device):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add(pr.RemoteVariable(
name='DacRaw',
offset=0x300,
bitSize=14,
mode='RW',
base=pr.UInt,
))
full_scale = 1.8
max_code = (1 << 14) - 1
self.add(pr.LinkVariable(
name='DacSetpoint',
units='V',
mode='RW',
disp='{:.5f}',
dependencies=[self.DacRaw],
linkedGet=lambda var, read=True: (
var.dependencies[0].get(read=read) * (full_scale / max_code)
),
linkedSet=lambda var, value, write=True: (
var.dependencies[0].set(
max(0, min(max_code, int(round((float(value) / full_scale) * max_code)))),
write=write,
)
),
))
ADC Conversion with Multiple Dependencies (Read-Only)
import pyrogue as pr
class AdcWithGain(pr.Device):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add(pr.RemoteVariable(
name='AdcRaw',
offset=0x400,
bitSize=16,
mode='RO',
base=pr.UInt,
))
self.add(pr.RemoteVariable(
name='GainRaw',
offset=0x404,
bitSize=4,
mode='RO',
base=pr.UInt,
))
def _scaled_input(var, read=True):
adc_raw = var.dependencies[0].get(read=read)
gain_raw = var.dependencies[1].get(read=read)
adc_volts = adc_raw * (2.5 / 65535.0)
gain = 1 << gain_raw # 0->1x, 1->2x, 2->4x, ...
return adc_volts / gain
self.add(pr.LinkVariable(
name='InputVoltageScaled',
mode='RO',
units='V',
disp='{:.6f}',
dependencies=[self.AdcRaw, self.GainRaw],
linkedGet=_scaled_input,
))
Subclassing LinkVariable
An occasional pattern is to subclass LinkVariable when the same
linked-access behavior needs to be reused many times. This is useful when the
linked logic is more like a reusable Variable type than a one-off callback.
Two common examples are:
An indexed view into one element of an array-like dependency
A grouped Variable that reads or writes a coordinated set of dependencies
import pyrogue as pr
class IndexedLinkVariable(pr.LinkVariable):
def __init__(self, dep, index, **kwargs):
super().__init__(linkedGet=self._get, linkedSet=self._set, **kwargs)
self.dep = dep
self.index = index
def _get(self, *, read):
return self.dep.get(read=read, index=self.index)
def _set(self, *, value, write):
self.dep.set(value=value, index=self.index, write=write)
This pattern is a good fit when many nodes should expose the same linked behavior with different dependencies or indices. It keeps the tree definition cleaner and avoids large repeated lambdas.
What To Explore Next
Software-owned Variables: LocalVariable
Hardware-backed Variables: RemoteVariable
Shared Variable behavior: Variable
API Reference
See LinkVariable for generated API details.