ADR 0074: A reading is published only if the block stands behind it¶
Status: Accepted
Date: 2026-08-21
Issue: #1738
Related: ADR 0007 (the TRK/TRK_VAL
back-calculation this reuses), ADR 0009
(the fail-safe posture this deliberately does not duplicate),
ADR 0064
(the rule that a reference is refused where it is written),
#1734 (the
bench investigation this came out of)
Context¶
On 2026-08-21, between 16:12:41 and 16:13:12, bench-loop-01's analog channel 1
stopped carrying its signal. The coupler's own output readback held at 19623
counts, which is 59.9 % of span. The input register read 3 counts for the same
thirty-one seconds. The loop-pid control module treated those 3 counts as a
genuine process value of 0.009 %, computed an error of 60 %, and wound its
control output to 100 %. Throughout, the unit reported Running and the tag
reported Good quality.
The bench fault was a measurement artefact, and #1734 records it. The reason it reached the actuator is ours, and that is what this record is about.
Three links in the chain carried the reading, and not one of them carried quality.
The Modbus driver invents it. pkg/driver/modbus stamps Quality: "Good" on
every successful read. A broken wire is a successful read of a register that
happens to hold an underrange value, so it is Good by construction.
pkg/driver/ethernetip does the same. OPC UA does not, because it passes the
server's own quality through, and the simulation driver's wire-break fault
produces Bad. A genuine bad quality therefore already existed on two of the
four drivers, and nothing in the runtime looked at it.
The AI block threw it away. It read the tag value, used tv.Value, and never
touched tv.Quality. A reading that arrived Bad was scaled into engineering
units and published exactly like a good one.
The bus could not carry it in any case. ctx.Bus("PV") hands the PID block a
bare float, so the controller had nowhere for a quality to ride along. It had no
bad-process-variable branch, because it could not have had one.
The single place in the runtime that read quality at all was
interlock.evaluate, and it read it from its own direct driver read rather than
off the bus.
The failure direction is what makes this worse than a general loss of information. A 4-20 mA live zero exists so that a failed transmitter is distinguishable from a real zero. A dead input therefore reads at the bottom of span, the error against a setpoint anywhere above it is close to maximal, and a reverse-acting loop answers by driving its final element to the opposite rail. On the bench that element is a resistor. On a customer's plant it is a valve.
It also crosses ADR 0008. A safe-state posture is written against what the runtime believes the process is doing, and the runtime could not tell the difference between a process at zero and an instrument that had stopped answering.
Decision¶
A block publishes a reading only if it stands behind it. Four parts.
The AI block holds, flags and faults¶
When the AI block cannot trust a reading it holds OUT at the last trusted
engineering value, raises a new PV_BAD boolean output, and returns an error so
the program reports Degraded with the block named in FaultedBlocks. All
three are needed and none substitutes for the others.
Holding OUT is what protects a downstream that is not wired for any of this. A
held process variable yields a stale error. The distance between that and a
maximal one is the distance between a loop that drifts and a loop that rails.
PV_BAD is what makes the condition actionable. It can be wired into a
controller, into an output block's interlock input, or declared as a tag with
role: alarm so the HMI annunciates it.
The error is what makes the condition visible, through machinery that already
exists. The scan loop skips a faulted block, holds its outputs, and surfaces
Degraded to the control module health topic, the metrics and the HMI. Without
it the condition would live only on a tag that nothing is obliged to read, which
is how the bench event ran for thirty-one seconds unremarked.
RAW deliberately does not hold. OUT is a claim about the process and must
not be made from a reading the block rejects. RAW is the diagnostic, and an
engineer looking at a faulted input block wants the number the device actually
returned. The two disagreeing is the finding.
Two sources make a reading untrustworthy, and they reach different drivers. The
first is the driver reporting Bad, which is default-on and carries no
false-trip risk because the source itself said so. The second is a configured
fail band, described below.
Uncertain is treated as usable. It means the source has doubts it could not
resolve. The value may still be perfectly good. interlock.evaluate already trips on
Bad alone. A control block that dropped its process variable on every
Uncertain would hold the loop through an OPC UA server's ordinary startup
transients.
The PID block gains a PV_BAD input and reuses the tracking it already has¶
PID gains a PV_BAD input port. While it is true the loop stops acting on its
process variable and holds. With TRK_VAL wired from the downstream analog
output, which every shipped PID template already does, the integrator is
back-calculated so the loop holds at the value it actually wrote and resumes
from there when the instrument comes back. With TRK_VAL unwired there is
nothing to track, so the output is held outright at its last computed value.
It is a separate port. The runtime resolves a data
connection by scanning for the first entry matching the destination block and
port, and nothing refuses a duplicate destination. A second wire into TRK
would not combine with the first. It would be silently dropped.
A manual override still wins over a PV_BAD hold. An operator who can see that
the instrument is dead is entitled to command the output anyway, so the #1715
tracking path is unchanged.
The fail band is opt-in, and it is not the general answer¶
AI gains optional rawFailLow and rawFailHigh parameters naming the raw
interval outside which a reading is rejected. Both bounds or neither, which is
the rule the engineering range already follows for the same reason. They are
absent by default, so no existing document changes behaviour.
This is the only detection available on a driver with no quality channel. That means Modbus and EtherNet/IP, and therefore most of the field. It must not be documented as the general answer, for a reason the bench made concrete.
WAGO's 4-20 mA input profile declares RawMin: 0. The card maps 4 mA onto raw
zero. The live zero that NAMUR NE 43 exists to provide has therefore already
been normalised away before the driver sees it. IOModuleFailSafe records the
same fact about the output side, measured at 4.00 mA on a 750-554. What survives
below 4 mA is whatever the card chooses to report there. Each vendor decides
that for itself and no standard fixes it.
The bench numbers give a narrow but real gap on a 750-454. An open loop read raw count 3 and a live zero read counts 32 to 40 on a span of 32767, so a band opening at 16 separates them. That gap is a property of one card and not a rule.
The general answer is the card's own NE 43 underrange diagnostic, which no device profile models today. That is separate work and is filed separately.
The Modbus driver keeps saying Good¶
A successful read is a successful read. Modbus has no quality channel on the
wire, and the driver knows the register but not the engineering range, the live
zero convention, or the process. Manufacturing a Bad from a value would put
the judgement in the layer with the least information. The judgement belongs to
the AI block, which knows the raw span and can be given a fail band.
pkg/driver/ethernetip is unchanged for the same reason.
The three quality strings are now declared once, as constants in pkg/driver,
because a control decision wants one declaration to be audited against. The
field stays a plain string so that every existing producer keeps compiling.
Consolidating the five duplicate declarations onto the constants is follow-up
work, deliberately not folded into a safety fix.
The corpus gate, and why the wire needs one¶
The two halves of the fix buy different things, and writing the control test is what made that clear.
The AI-side default stops the rail. It needs no wiring and it protects a document nobody has touched.
The PV_BAD wire stops the drift. A held process variable is still a stale
error, and a controller that is not told keeps integrating against it until it
arrives at the same rail slowly. A slower rail is harder to see rather than
safer.
Only an author can add that wire, and it is exactly the kind of thing that gets left out. The document works without it, nothing fails, and the loss is invisible until an instrument dies. That is the shape #1648 records, and this tree's answer to it is a gate over the corpus.
make lint-pv-quality-wired states the rule without naming a block. For every
data connection into a destination block's PV port, if the source block's type
publishes a PV_BAD output and the destination block's type declares a PV_BAD
input port, then a connection from that source's PV_BAD to that destination's
PV_BAD must exist too. Both halves are answered by the runtime itself, through
Outputs() on the source type and InputPorts() on the destination type. A
block that grows a PV_BAD output later is therefore covered the day it does.
There is no allowlist. An entry saying that one loop need not be told would be a
claim about a plant this repository cannot see.
Alternatives Considered¶
Growing the bus to carry quality alongside every value. This is the
invasive option, and it turned out to be unnecessary. The shipped pid-loop
template already wires write_out.ILCK_ACTIVE into pid.TRK, which is a
boolean condition on one block driving a hold on another over the existing bus.
A PV_BAD output and a PV_BAD input are the same shape. Propagating a
companion quality automatically would also have to key on a port naming
convention, and ADR 0016 refuses that pattern elsewhere in the tree.
Publishing PV_BAD and changing nothing else. This leaves OUT scaling the
untrusted value, so a document that does not wire the new port still rails. The
capability would be offered everywhere and executed nowhere. That is the #1648
shape all over again.
Holding and flagging without faulting the block. The values would be right and the condition would be quiet. Nothing would reach the health topic, the metrics or the HMI unless a second surface were built for it, and quiet is what let the bench event pass unremarked.
Having the Modbus driver report Bad below a configured floor. This is the
same detection in the wrong layer, and it is answered above.
Consequences¶
A program that wires nothing new is safer than it was, and its behaviour on a
bad reading changes. An analog input on OPC UA or the simulation driver now
holds its last trusted value when a reading arrives Bad. Its program reports
Degraded where it previously reported Running. That is the intended change and it is the reason
the driver-reported half is default-on. On Modbus and EtherNet/IP nothing
changes until a fail band is configured, because those drivers still report
Good.
A cold start against a dead instrument has no good answer available. The block
has been asked for a reading and has never had one, so OUT holds the zero
value. That is a number it has no business standing behind. The error says so in
different words in that case, and a loop wired for PV_BAD holds instead of
acting on it.
The cascade template's setpoint comes from an analog input too, and a
supervisory setpoint that dies is a real hazard this does not cover. PID has
one PV_BAD port, and wiring two sources into it needs an OR block and a
decision about whether holding is the right answer to a dead setpoint. That is
deliberately left out of a safety fix and noted in the template.
Amendment: a dead supervisory setpoint is annunciated (#1741)¶
The consequence above left a question open. pid-cascade reads its supervisory
setpoint from a tag address, so read_sp is an AI block and it dies exactly
the way read_pv does. The wire this ADR added covers one of the two, and the
shape of the gap invites a session to close it by ORing the two flags into the
one PV_BAD port. That is the wrong answer. This amendment records the ruling
and the reasoning. The absent wire is a decision, and it should read as one.
The two failures are not the same question¶
A dead process variable blinds the loop. It can no longer tell what the process is doing, so it has no basis on which to act. Holding the output is the only answer available to it, and that is what this ADR is about.
A dead supervisory setpoint leaves the loop able to see and able to act. What it has lost is the target. The measurement is live, the final element is live, and the last setpoint the supervisor asked for is a value that was valid moments ago. Holding the output there would throw both of those working things away and leave the process open loop against whatever disturbs it. The trigger for doing so is usually a supervisory link failure, which is often only communications and often transient.
So the loop keeps regulating on the setpoint read_sp is holding. This is the
standard cascade answer. A slave whose remote setpoint stops arriving falls back
to its last cascade setpoint and keeps controlling. It does not fall back to
manual.
Annunciation is the whole of the response, so it has to reach somewhere¶
Refusing to act means the operator has to be told. pid-cascade declares a
second role: alarm tag, SP_BAD, bound to read_sp.PV_BAD, beside the
PV_BAD tag this ADR added. There are two tags because one combined flag would
tell an operator that an instrument had died without saying which one to go and
look at. The Degraded program state was already the only evidence available,
and it has the same problem.
The template also grows a second fail band. spRawFailLow and spRawFailHigh
are the setpoint input's, and rawFailLow and rawFailHigh stay the process
variable's under the name pid-loop uses. Without the second pair SP_BAD
could never go true on Modbus or EtherNet/IP, which is where a broken wire is a
successful read of an underrange register, so the alarm would have been a
constant false across most of the field. That is the failure mode this ADR
already records about the driver half, arriving one template over.
The gate is deliberately not widened¶
make lint-pv-quality-wired asks about PV and not about SP. Widening the
destination port set is a two-line change and reads like an oversight to close.
It is not. A gate demanding that read_sp.PV_BAD be wired into pid.PV_BAD
would demand the behaviour this amendment rejects. The scope note in
hack/lint-pv-quality-wired/main.go says so at the point where a session would
make the change.
What holds the ruling¶
pkg/fbruntime/sp_quality_chain_test.go states it as an executable difference
against a real Runtime and the simulation driver's own wire-break fault. The
setpoint instrument dies, the process is then disturbed, and the loop answers
the disturbance. A second test asserts that the flag and the Degraded state
both arrive with only read_sp named. The third is the
control, and it is what keeps the other two honest: the same rig and the same
program, with the process variable dying instead, and an output that does not
move.
Wiring the setpoint flag into pid.PV_BAD makes the first test fail with the
loop frozen at its pre-disturbance output, which is the rejected design stated
in numbers.
What was not built¶
A configured safe setpoint was the third option and it was not taken. It would
need a new parameter and a SEL block, and the parameter has no sensible
default. An unset one resolves to zero, which is a rail. A rail is the failure
this ADR exists to prevent. Making it genuinely opt-in is not available
either, because pkg/templatecompiler drops an unset parameter but a block and
a connection are structural. A deployment that wants this behaviour can author
it, and the template does not ship it.
Amendment: the card's own range diagnostic, where the card publishes one (#1740)¶
The record above names the NAMUR NE 43 underrange diagnostic as the general answer to a dead 4-20 mA transmitter, says no device profile models one, and files it separately. This amendment closes that. The vendor manuals and the bench between them made the question smaller than it looked, and one of the things they settled was a premise the original record got wrong.
There is no second address, because the bits are inside the number¶
The open question was where a card exposes underrange, and whether reaching it
means a new parameter on AI or a driver that folds the bit into quality.
Neither. A WAGO standard analog input card carries a 12-bit measured value in
B14 ... B3 of the word and hands the low bits to its own range detection. The
750-454 manual, § 2.1.1.7: "Bit B0 = 1 is set when the range of measurement is
overranged. Bit B0 and B1 = 1 are set in case of measurement range underflow or
broken wire." The 750-459 manual says the same of B1 and B0 and calls B2
reserved with no defined value.
Both manuals also describe an optional status byte, which is a richer surface than these two bits. A Modbus coupler does not carry it. The live bench node settles that: the 750-352's analog input process image register measures 32 bits for the 750-454's two channels, where a mapped status byte would make it 48. That surface is unreachable over this protocol, and it does not matter, because the two bits that decide the question are already in the register the io-probe reads.
The bench number was never a measurement¶
The record above cites the #1734 open loop at raw count 3 and a live zero at 32 to 40, and reasons from the gap between them to a fail band opening at 16. The gap is real and the reasoning from it was wrong. Every measurement that card can produce is a multiple of eight, because the measurement occupies B14 ... B3. Three is not one. Sixty consecutive readings taken off the bench card through the product's own read path on 2026-08-21 were multiples of eight without exception, with the low bits clear on every one.
So raw 3 was not a small number near zero that a band could be placed under. It
was B0 | B1, which is the card stating a broken wire. It arrived in a field the
product was scaling into engineering units. The fail band #1738 shipped happens
to catch it. It catches it as a value, which is not what it is.
The driver reports it, and that is not the thing this ADR refused¶
The Modbus driver stamps Bad when a read's value carries the card's declared
fault bits. The section above rules that this driver does not manufacture a
quality out of a value, and that rule is intact. Manufacturing would be reading
a number and deciding, from the engineering range or the live zero convention or
the process, that it looks wrong. That judgement is still the AI block's and
still needs a fail band to make it. Here the card set a bit to say the channel
is outside the range it can measure, and reporting what a device said about
itself is the one thing a driver is unambiguously for.
Four things follow from putting it there.
The mask belongs to the device profile. It is declared per
catalogued module in pkg/deviceprofile/wago750, carried on
driver.DiscoveredChannel, and resolved by the driver against the layout it
already discovers to serve TagMetadata. No CRD field, no authoring, no
existing document edited. A capability that had to be switched on per channel
would be offered everywhere and executed nowhere, which is the #1648 shape this
tree keeps meeting.
A mask, and only bits the manual defines. B2 is out on both cards, because
one manual calls it unused and the other calls it reserved with no defined
value. Reading a bit a vendor declines to define invents a fault out of
whatever the card leaves there. A card whose manual documents no such bits
declares no mask, which is why the 750-469 and the 750-464 have none. An RTD
word's low bits are tenths of a degree, and a mask on one of those cards would
read Bad at most temperatures.
Only a single-register read. A width suffix that spans registers puts the low bits of the next register where these bits would be, so the mask stops naming anything. The driver answers zero there and guesses at nothing.
Resolving it must not hammer the device. The read path now reaches discovery, discovery force-reconnects before it probes, and most Modbus devices match no profile at all. A failure is therefore cached against the connection generation it happened on, so such a device is asked once per connection rather than once per scan. A reconnect is also when the answer could have changed, since a re-racked node comes back on a new connection.
What it reaches, and what it does not¶
Quality is read by everything, so this is wider than the AI block. A broken
wire now reaches five surfaces. The AI block holds, raises PV_BAD and reports
Degraded. The tag reads Bad on the HMI and over MQTT. So does dcs io read.
And interlock.evaluate trips, because it has always tripped on Bad alone. A
device interlock on a dead transmitter tripping is the intended reading of ADR
0007 and not a side effect to be trimmed.
It does not reach a card that publishes nothing below its range, a device family whose profile this product does not carry, or EtherNet/IP. Those are still covered by the fail band alone, and the section above still governs what that band is and is not.
That narrows one line of the #1741 amendment above, which says a SP_BAD or
PV_BAD flag could never go true on Modbus without a band. On a card whose
profile knows its in-band diagnostic it now can. The reason that amendment
gives for declaring the second band stands, because the cards it does not
reach are most of the field.
Measured on the card, and what the measurement left open¶
The account above was written from the vendor manuals and from sixty readings
that were all multiples of eight. It was afterwards driven on a real 750-454, on
2026-08-21. An operator lifted one leg at the field terminals while the product
read the channel through its own path. A generator at its floor read 0x0000 at
Good. The lifted wire read 0x0003 at Bad. Each verdict carried nineteen
seconds of the previous state on either side of the transition. Both words are
the bottom of the range to anything reading the number alone, and the card told
them apart. That is the claim of this amendment, and it is now measured.
The comparison also disposes of the fail band as a substitute here, and the
multiple-of-eight argument never reached this far. The card maps 4 mA onto raw
zero, so its live zero IS 0x0000. The fault sentinel 0x0003 sits numerically
ABOVE it. A rawFailLow placed to catch the sentinel therefore rejects the live
zero as well. A band placed under the live zero cannot exist at all.
The two facts together settle it more strongly than either does alone. A genuine
reading is a multiple of eight, so the sentinel 0x0003 lies strictly between
the genuine readings 0x0000 and 0x0008. A fail band accepts a closed
interval and rejects everything outside it. No choice of rawFailLow and
rawFailHigh can therefore reject the sentinel while accepting both of its
neighbours. The band cannot express this condition at any setting.
The gap between the #1734 numbers that suggested a band at 16 separated the sentinel from a reading slightly above the live zero. It never separated the sentinel from the live zero itself.
What the run did not settle is the trip CURRENT. Finding where the card starts flagging needs a source below 3.6 mA. The bench signal generator floors at 4 mA. A lifted wire is therefore a step to zero, and it is never a controlled current. The manual bounds the fault band only as 4 mA minus 0.1 to 2.0 mA.
That leaves a real blind spot. Between 4 mA and the trip point the card reports
0x0000 at Good, so a transmitter that has failed PARTWAY is
indistinguishable from a process sitting at zero. Nothing in this amendment
closes that. A fail band cannot close it either, for the reason two paragraphs
above. The diagnostic answers a dead transmitter and says nothing about a
drifting one.
A correction the same work forced¶
The catalog described the 750-464 as a 2-channel 4-20 mA card. It is a 2-or-4
channel RTD and resistance card, and it claimed the unipolar 0-32767 span that
the comment on Module.RawMin already says such a card must leave unclaimed. A
claimed span is worse than an absent one, because an AI block with autoRaw
takes it as authoritative where an absent one leaves the block on its own
declared fallback. The description mattered a second time here: a mask applied
to "the 4-20 mA cards" would have been applied to that one.
Its channel count and process image footprint were left alone at two, which is the card's 2-channel parametrisation. A 4-channel one occupies four words and would shift every analog input after it in the lineup. Choosing between them looked impossible here, because the module identification word carries the part number and says nothing about the parametrisation, and nobody has one of these cards to measure. That was filed as #1744, and the amendment below answers it from a register the coupler was publishing all along.
Amendment: a configurable card's footprint comes from the node total (#1744)¶
Status: Accepted Date: 2026-08-21 Issue: #1744
The paragraph above is now wrong in its conclusion and right in its premise. The type code really does say nothing about the parametrisation. It is not the only thing the coupler publishes.
The manual settles the first question, and it settles it twice¶
The coupler's complete Modbus register table is Tables 121 and 122 of the 750-881 manual, and the same table serves the 750-352 on the bench. It carries no per-module process image size anywhere. The one per-module surface is the module identification table at 0x2030, and Table 149 defines its word exactly: for a digital module a bit code whose bits 8 to 14 are the module's size in bits, and for everything else the item number. WAGO says why the digital case is different, which is that item numbers cannot be read out of digital modules.
Two facts fall out of that one sentence. A digital module needs no reconciling at all, because the coupler publishes its footprint directly. And a part number in that table is never a digital module, so an unidentified one occupies word-based process image and no digital bits.
What the coupler does publish is the node total, at 0x1022 for the analog outputs and 0x1023 for the analog inputs, both in bits. That is enough whenever one module in an image is unresolved. Subtract the cards whose type code fixes their width and the residual is the remaining module's footprint. That number is measured, and nothing about it is assumed. Two unresolved modules in one image cannot be apportioned, and the code does not try.
Only the analog images are reconciled. The digital pair needs nothing, per above, and the analog pair is also the one the bench 750-352 was measured against: 32 bits for the 750-454's two words and 32 for the 750-554's, exactly the sum with nothing added for the coupler. The digital pair reading 8 for an 8-channel card cannot distinguish an exact bit count from one padded to a byte, so it is no basis for a gate.
The conservative reading was never conservative¶
The issue asked whether the reading decodeDigitalModule already takes was
available here. That reading claims no channels for a module it cannot decode,
and the comment on it called claiming nothing the safe answer.
It is not, and it was not there either. A process image address is the running sum of the footprints of the modules before it, so a module that claims zero does not leave the modules behind it where they were. It moves them by its whole real footprint. Claiming nothing and claiming the wrong width are the same defect, and the first one is quieter.
That reached three paths. It reached the undecodable digital module. It reached every part number outside a catalog of fourteen entries, which is most of what WAGO sells. And it reached the table's own first word, which identifies the controller.
So the answer is to stop publishing addresses downstream of an ambiguity rather
than to skip past it. An unresolved slot closes the images it sits in, the
channels ahead of it are published because they are correct, and the reasons
come back with them as a deviceprofile.PartialDiscovery. The driver logs those
and keeps the channels, because a caller that discovers nothing is worse off
than one that discovers most of a node and is told what is missing.
Where the refusal lands mattered more than the refusal¶
Three things stop this from trading a wrong address for a dead node, which is the #1665 shape.
The controller entry is read as the controller. The manual says the table's first word is one. A controller occupies no process image, so the profile records a resolved zero there. Read the other way it would close both analog images at slot 0 of every node there is.
An unidentified module has its footprint recovered from the residual whenever the image holds only that one unknown. Its channels stay unnamed, which is honest. The layout still steps over it by the right number of words. Without this half, a single catalog miss would truncate a real lineup.
A size register answering zero is treated as no answer. A Modbus server that maps its whole address space answers zero for a register it does not implement, and that is the same word a node with nothing in the image returns. Reading it as a total closed the analog image on the repository's own device fake, which is how this was found. Where the node truly has none, the identified cards claim none either, so nothing was lost by declining to reconcile.
A block that costs no channel is not reported. An unidentified module leaves both analog images open until each is separately accounted for, so on a node with no analog outputs its output-side ambiguity is permanent and free. Naming it would train a reader to ignore the message that matters.
What is still not answered¶
Nobody has a 750-464. The reconciliation is proven against planted lineups and against the bench node's real register values, where it is exactly a no-op, and that is the strongest evidence available without the card. A four-channel one in a real lineup has never been read.
The third question the issue asked, whether this shape reaches other cards, has
an answer that is larger than the 750-464. Every card outside the catalog is the
same problem, and the same machinery now handles it. No per-card entry is
needed for one. Module.Widths is what a card declares when its type code fixes
no width, and the 750-464 is the only entry carrying one today.
Amendment: a boundary carries the quality of what it carries (#1746)¶
Date: 2026-08-21 Issue: #1746
The gate above is what carries the wired half of this decision across the whole corpus. Two templates shipped with the wire. Every other document has it because the gate asked for it. That gate did not see through a composite, and the line it printed about that was wrong about why it was safe.
The class it could not see¶
A composite function block type is deliberately not registered (#1658, ADR
0065). It is flattened at load and has no Execute. The block registry
therefore answers nothing about a composite instance, and nothing about the
__SELF_IN and __SELF_OUT boundary inside a type. Both were reported
UNANALYSED and skipped. The message attached to the instance said the
composite's insides are checked as their own document.
That is true of a composite that reads its own input, which is what
regulated-valve does. It is false of one whose PV arrives on its boundary.
Inside the type the source of that PV is __SELF_IN, and the registry does not
know that either. Such a loop was checked at neither level. A fixture of exactly
that shape, an AI feeding a PID through a composite boundary with no
PV_BAD anywhere, passed the gate and exited 0.
Resolving the composite would not have closed it¶
The obvious repair is to resolve a composite's ports from the interface its
own document declares. That interface is spec.dataInputs and
spec.dataOutputs, which is what Flatten itself works from. Resolving them
makes every connection analysable and removes both UNANALYSED lines.
It also leaves the fixture passing. The rule above is phrased as a companion:
where the source publishes PV_BAD and the destination declares it, the edge
must exist. Nothing obliges a type to declare such a port, and a type that
declares none satisfies the rule by having nowhere to put the answer. The gate
would have stopped saying it could not tell while still not telling, which is
worse than the line it replaced.
The rule is about the boundary¶
A boundary forwards what it carries. Where a PV routed from __SELF_IN reaches
a block that can be told the reading is untrusted, the type declares a PV_BAD
input and routes it. Where a PV exported to __SELF_OUT comes from a block
that publishes PV_BAD, the type exports that too, because a consumer of the
composite's PV is otherwise in the position this ADR describes, one level of
packaging away.
The two rules compose without a third. Declaring the port and routing it nowhere is caught by the companion rule already stated, since the boundary then carries the port like any other block.
What it changed, and what it deliberately did not¶
regulated-valve exports a position from an AI that can hold. It now exports
PV_BAD beside it. Its own inner loop was already wired.
deviation-monitor routes its boundary PV into a SUB. A SUB declares no
PV_BAD input, so nothing is demanded of it. That case is why the rule asks
what the destination can be told. A type whose PV reaches only blocks that
cannot act on quality has nothing to forward, and demanding a port there would
make the gate a nuisance for no safety.
A type that no document in the tree declares still resolves to nothing and is still reported UNANALYSED. The gate reads a corpus. A type it cannot see is a thing it did not check.
Amendment: an empty channel is declared (#1920)¶
The Consequences above say a cold start against a dead instrument has no
good answer and that the block faults in that case too. The bench measured
what the fault costs when the instrument is not dead but absent. cal-input
on bench-loop-01 is a calibrator port with nothing wired to it, and its own
description says so. Its block faulted on every scan from the day it was
deployed. The program was Degraded for the life of every container. The
High equipment alarm from #1754 stood for six days. The program-level counter
read 1 from the first scan onward. A standing alarm on a port the plant knows
is empty is the stale alarm ISA-18.2 warns about, and it trains an operator
out of the alarms that matter.
The inference the issue proposed, and why it does not survive the bench¶
The issue asked that Degraded mean a block that was working has stopped,
and not a block that has never had a reading. The block already remembers
whether it has ever taken a trusted reading, and the proposal keys on that
memory. It does not survive the port it was written for. The memory persists
across restarts on purpose (ADR 0080). A calibrator is plugged in for one
calibration and unplugged after it, and from that first unplug the block is
one that was working and has stopped, for good. The inference makes a channel
nobody has ever touched quiet and nothing else. It also silences the case this
record chose to fault: a fresh deploy against an instrument that is dead.
There is no precedent for the inference either. Foundation Fieldbus and DeltaV
report a never-connected channel and a broken wire as the same Bad status.
The answer to a configured channel that is expected to carry no signal is to
declare it out of service. This tree already holds that line: the corpus gate
above refuses an allowlist because an entry saying one loop need not be told
would be a claim about a plant this repository cannot see. The plant makes
that claim in its own document.
The declaration¶
ControlModule.spec.unconnectedInputs names the template inputs the instance
expects to carry nothing, keyed by input name, with the reason as the value.
The template compiler hands the declaration to every AI block whose address
is written as {{.inputs.<name>}} for a declared name, as the block params
unconnected and unconnectedReason. On such a block a rejected reading is
still held and still raises PV_BAD, and it is not a fault. The program
stays Running, FaultedBlocks is empty, and no alarm stands. A reading
that arrives Good is published normally, which is what lets a calibrator be
plugged into a declared port and read. A read that does not complete still
faults, because the declaration is about the channel and not about the bus.
The declaration is refused where it cannot be honoured. It is refused when it
names a port that is not a template input, because a typo that silences
nothing reads as if it had. It is refused when no AI block reads the port.
A discrete input judges no quality, and a device interlock trips on Bad
because that is the safe answer for a protection. Neither is what the
declaration licenses. The refusal lands where a binding fault already does, on the
module's TemplateResolved condition, and dcs lint cmtemplates reads it
from a tree of documents.
An undeclared channel is unchanged. An input somebody configured and nobody wired is a commissioning fault, and the plant wants it annunciated.
The tag carries the quality, because the fault no longer does¶
Every tag bound to a function block output was published at Good quality,
whatever the block had said about the value. On a dead instrument a held
process variable therefore reached the HMI and the historian as a Good
number, and the Degraded program state was the only thing that said
otherwise. On a declared input there is no Degraded state, so the tag has
to say it. A tag bound to a block's OUT is now published at Bad quality
for as long as that block raises PV_BAD. RAW is unchanged, because it is
what the device said. Every other port is the block's own claim. The port
name is the same contract the corpus gate above already reads.
What was not built¶
ISA-18.2 provides a per-alarm out-of-service state, and it was not taken. It
would silence the annunciation and leave the fault, the Degraded state and
the saturated counter in place. That is the shape #1920 found and not a
remedy for it. The fault is where the declaration has to land.
A gateway surface for the declaration is separate work. The gateway carries the field through create, update and read. A document that declares it therefore survives an edit from the UI. Where an engineer declares it from the UI, and how the module's page shows an empty channel, is a UI ruling and is filed as #1923.
Amendment: a structure is published as no value (#1943)¶
The rule above is written for the block. A block that cannot stand behind a
reading publishes none, and the tag it feeds carries Bad. The driver read
the block never sees was not held to it.
On the capture stack, Boiler #1 on the opc-plc simulator exposes one
variable, BoilerStatus, declared as the simulator's own BoilerDataType.
That type is a subtype of Structure, and the value is served as an
ExtensionObject the client library holds no registration for. Two things
went wrong with it, one on each side of the wire.
Discovery called it a String. The type judgement asks for the declared
built-in, then for the namespace-0 ancestor of a declared type outside
namespace 0, then reads the served value. A structure answers none of those,
and the last fallback, on the Go type of the sample, had no arm for an
ExtensionObject and answered String by default. The wizard wrote a type the
node does not have into the ControlModule, and nothing on its variables panel
said the runtime could not read the variable.
The runtime published the object. ReadValue forwarded the decoded value
unchanged. An undecoded ExtensionObject is a type id and a nil body. That
object went through the driver, the process image, the runtime's tags
endpoint, the gateway and the JSON encoder, and the ControlModule page
printed it as [object Object] beside a red Bad dot.
The decision¶
A structure is named. Discovery types it Structure, which is namespace 0's
own name for it. That name is deliberately not one of the built-ins a write
can be encoded to, so a write to such a tag is refused by name before the
wire. The discovered variable carries unreadable with the reason, and the
wizard prints the reason on the row. The tag is still emitted, because the
address is real and the module is where an engineer will look for it.
A structure is not published. The OPC UA client answers a Read or a
subscription notification that decoded an ExtensionObject with no value, at
Bad quality, with the reason beside it. That holds for a structure the
library did decode as much as for one it did not: the process image holds
scalars, and nothing downstream can act on a record. The reason is a new
field on the driver's value, Error. It rides the runtime's read and tags
endpoints and the gateway's tag payload to the page. The page renders the
absence and the reason in the runtime's own words. Error is set only when
the driver declined to publish what it read. A Bad the device itself
reported carries no reason on this field, because the device's status code
is the device's, and reporting it here is separate work.
The page renders no object in any case. The value cell prints a dash for anything that is not a scalar, so a value of that shape reaching it by some road this record did not walk would still not print as one.
What was not built¶
The wizard does not drop the variable. An engineer who asked for every variable under a node is told what the runtime will do with each of them before Generate, which is better than a tag that vanished. The simulator's boiler exposes that one variable. Dropping it would have emitted a tagless module, and that is the empty stub the wizard exists to replace.
The client does not decode vendor structures. Registering BoilerDataType
would read one simulator's record and no controller's. A structure the plant
needs read is a per-field variable on the server, or a driver that knows the
type, and either is a different feature.
Amendment: a simulated channel nobody modelled is not a reading (#1955)¶
The rule above is written for the block, and the amendment before this one
extended it to a driver that read a value it declined to publish. The
simulation driver was held to neither. Asked for an address its store did not
hold, it answered 0.0 at quality Good with no error, for every address,
forever.
Two shapes fell into that branch, and only the second is common. One is an
address the driver was never told about. That is the
ADR 0076 hazard,
arriving from the module driver's side. The other is a channel an IOModule
declares and the simulation does not model. That one is ordinary authoring, and
it is everywhere. examples/distillery's wash-sim declared
DI-AgitatorFault at discrete.1, no behaviour named that address, and the
channel read 0 at Good from the day the site was written. On a fault input
that is the reading that says all is well.
It also made a real defect unreadable.
#1953 was a
hot-added module whose tick loop died on arrival, so its modelled channels never
reached the store. Every one of them read 0.0 at Good, the module reported
Online, its channels reported healthy, and the ControlModule binding them
reached Running. Nothing separated a simulation that had never run from one
running correctly.
The decision¶
The driver answers for an address its store does not hold by direction, because the two directions are not the same claim.
A declared output keeps 0.0 at Good. It models a register, a register
nothing has written holds zero, and a Modbus device answers the same way. The
ADR 0080
commanded-state restore reads exactly this before the first write of a restarted
network lands.
A declared input with no behaviour answers Bad, and the reason says the
simulation models no value for that channel. This is the case worth changing.
An address the module never declared answers Bad too, and names itself as one
the module does not carry.
The reason travels in the Error field the amendment above added. A returned
error would be dropped at the runtime's own read, and the tag would then vanish
from every surface. That says less than a Bad that names itself.
Direction reaches the driver on every configuration path. The auto-realistic one always carried it. The profile path never did. A profile carries behaviours and says nothing about direction, and a module with a profile is exactly the one whose unmodelled inputs had no way to be found.
The corpus is held to more than the driver refuses¶
The refusal is the whole answer for a plant. It is not the whole answer for this
repository's examples/, because every screenshot, clip and demo is filmed
against those files, and an unmodelled input that reaches a capture stack shows
as -- in a take that then has to be re-shot. make lint-example-sim-channels
holds every declared input on a simulated IOModule to having a behaviour, a
preset mapping or a written sim-unmodelled-ok reason. Outputs are out of its
scope for the same reason they keep their Good.
The first sweep found nine channels across five sites, and every one of them was a drive fault bit. Each now carries a behaviour saying the simulated drive is healthy. That is a claim the simulation makes, where the old zero was one the driver invented.
What was not built¶
The HMI does not distinguish an unmodelled simulated input from a genuinely
failed read. Both are Bad with a reason, the reason is rendered, and a
separate treatment would be a second vocabulary for the same fact.
The gate does not read outputs. An output with no behaviour is the ordinary case. Demanding a behaviour for one would ask the corpus to model every register it writes.
Amendment: a bit is a reading too (#1998)¶
Everything above this line was built for the analog path. The AI block judges
what it reads. The amendments carried that judgement to a composite boundary, to
a declared-empty channel, to a vendor structure and to the simulation driver.
The DI block is the other half of every plant's field I/O, and it read tv
and used tv.Value for the whole of it. The driver's verdict arrived and was
discarded one line later.
The consequence is worse than the analog one it mirrors, in the one way that matters. An analog leaves evidence of its own failure. A dead transmitter reads at a rail, and where the card normalises that away the fail band above exists to be configured. A bit leaves none. There is no out-of-range for a contact: a broken wire on a normally-open switch reads open, which is what a switch that is simply not made reads, and no band can be configured over a two-valued signal. Quality is the only evidence a digital failure ever produces, and the block was throwing it away.
What that reached is everything downstream of fbOutputQuality, which publishes
a block-output tag Good unless the block raises PV_BAD. So a limit switch
whose wire had broken reached the HMI as a switch that was not made, the
historian recorded it as such, and a phase guard reading the tag through
READ() got a plain FALSE at Good quality. examples/fermentation's
isolate-vessel phase confirms that a vessel is isolated on exactly two such
contacts, and would have confirmed it the same way with both wires cut.
The decision¶
DI judges its reading on the terms AI already does, minus the one that
cannot apply.
It holds OUT at the last trusted value. A stale reading is a claim there was
once evidence for. A fabricated one is not. The hold also keeps the
solenoid-valve template's MISMATCH from annunciating a device fault whose
only evidence is a reading the block has just refused.
It raises PV_BAD, which is what makes the condition actionable on the bus and
what the runtime reads to publish the tag at Bad quality.
It returns an error. That faults the block, names it in FaultedBlocks and
reports the program Degraded through machinery that already exists.
There is no fail band, for the reason above. Uncertain stays usable, matching
AI and interlock.evaluate.
The unconnectedInputs declaration reaches DI now. It could not before, and
the compiler said so in as many words. It refused a declaration no AI block
read, because a DI judged no quality and there was nothing for the declaration
to license. There is now.
The held value is an operating point, so DI implements StatefulBlock
(ADR 0080). The
interface's own contract used to name IX as the block that deliberately did
not, on the grounds that it made no held-value promise for a restart to keep.
Holding is exactly that promise. Without the restore, a restart under a dead
contact reconstructs the block fresh and publishes false on its first scan,
which on a limit switch is not a neutral default: it is the reading that says
the valve has left the position it was commanded to.
The reason travels with the refusal¶
A Bad reading reached a phase guard as the words "bad quality" and nothing else.
Two hops were dropping what they had. The runtime's FB-output read never looked
at FaultedBlocks, though the block's own message is already kept there against
its name, and the ST bridge's response struct had no field for the error the
runtime does send. Both carry it now.
That is what a wedge diagnosis is made of. When a guard fails on quality for long enough, the SFC engine ends the run and the phase writes the failing transition, the duration, the consecutive scan count and the read error onto its own chart. Until this change the last of those four named neither the channel nor the cause.
The quality was reaching nothing (#2014)¶
Found by driving this amendment on a capture stack. The block refused the
reading, the runtime logged the refusal, the program went Degraded, and the
tag came back Good anyway.
Adapter.ReadTag tries a variable-backed entry before a block-output one, and
the quality ADR 0074's #1920 amendment
added lived on the second branch. A template-compiled tag carries BOTH refs.
All 70 entries in the fermenter's tag map do, and not one is blockRef-only. On
any plant built from templates the quality was therefore decided by code
nothing reached. The batch path the HMI polls had the same hole in both of its
own branches. That is the third time that merge has lost something ReadValue
carries, after the observation-time contract and the read-side faults.
Both paths judge quality on the same evidence now. The lesson is the fixture's. The #1920 test built an entry the template compiler does not emit, so it passed against a branch the product does not run.
What was not built¶
DI publishes no RAW. On an AI the pair exists because OUT is scaled and
RAW is what the device returned. The two disagreeing is the finding. A digital
has no scaling. RAW would differ from OUT only while the block is holding.
PV_BAD already says as much, in a form a downstream can act on.
The interlock path is unchanged. interlock.evaluate trips on Bad, which is
the safe answer for a protection and is not what a hold would be.