ISISNeutronMuon / ISISNeutronMuon/p4pillon
IOCMimicServer does not serve all NT fields like real IOC
- Dominant language
- Python
- Stars
- 2
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
## `SharedPV.open()` sends an incomplete structure, breaking strict pvAccess clients
A `SharedPV` opened with a partially-marked value never transmits the unmarked fields — not to the first client, and not to any client thereafter. Clients that zero-fill absent leaves hide this; clients that don't, break.
### Background
pvAccess puts only *marked* fields on the wire. A `SharedPV`'s stored change mask is the **union** of everything marked since `open()`, and `open()` resets that union. A field that is never marked is therefore never sent at all.
`p4p.nt.NTScalar(...).wrap(v)` marks only `value`. So a PV that is opened and not subsequently posted to — or one whose posts only ever touch `value` and `timeStamp` — serves a structure in which `alarm.*`, `display.*`, `control.*` and `valueAlarm.*` are permanently absent from the wire.
### Impact
The EPICS Archiver Appliance throws on connect to any such channel:
```
ERROR pv.EPICS_V4_PV - exception when reading pv
java.lang.NullPointerException
at EPICSEvent$FieldValue$Builder.setVal(EPICSEvent.java:909)
at PBScalarString.setFieldValues(PBScalarString.java:285)
at EPICS_V4_PV.saveAllMetaData(EPICS_V4_PV.java:594)
at EPICS_V4_PV.subscribe(EPICS_V4_PV.java:439)
```
The pure-Java `org.epics.pva` client constructs a `PVAString` with a `null` payload from the introspection descriptor and only `decode()`s the bits set in the change BitSet, so an untransmitted string stays `null`. The archiver's `FieldValuesCache.currentFieldValues()` flattens the structure with no null check, and `saveAllMetaData()`'s `getEverything` path bypasses `v3NamedValues()` — the only place that drops nulls — so the `null` reaches protobuf's `setVal`.
Only *strings* throw; `PVAInt` initialises to `0`. `alarm.message`, `display.description` and `display.units` are the exposed leaves.
The exception is caught by `subscribe()`, so the channel still connects — what is lost is the initial sample and all saved metadata, on every connect. The same client library backs Phoebus/CS-Studio, so the archiver is unlikely to be the only affected consumer.
### Which PVs are affected
Exposure is entirely "does anything ever mark this field":
| PV | marks `alarm.message`? |
|---|---|
| plain `p4pillon.server.thread.SharedPV`, no handler | never |
| `SharedNT`, `NTScalar`, no valueAlarm | never |
| `SharedNT`, `NTScalar`, valueAlarm active | only from the first alarm transition onward |
| `SharedNT`, `NTEnum` | at open (`AlarmNTEnumRule.init_rule` writes it unconditionally) |
Rows 2 and 3 are the same defect with a timing dependence: a client that connects before the first alarm transition receives the incomplete structure, one that connects after does not. `AlarmRule` does not help — it is read-only enforcement for puts and never writes a value.
`display.*` is owned by no rule at all, so an `NTScalar('d', display=True)` opened without display metadata never transmits `display.description` or `display.units` regardless of which rules are attached.
### This is not how a real IOC behaves
A live QSRV channel (`HELREC::DEWPOINT:READ`) marks **all 27 leaves** on a plain first get, including `display.limitLow`/`limitHigh` and `control.*` at their default `0.0`:
```
changedSet: alarm.message, alarm.severity, alarm.status, control.limitHigh,
control.limitLow, control.minStep, display.description, display.form.choices,
display.form.index, display.limitHigh, display.limitLow, display.precision,
display.units, timeStamp.nanoseconds, timeStamp.secondsPastEpoch,
timeStamp.userTag, value, valueAlarm.active, valueAlarm.highAlarmLimit,
valueAlarm.highAlarmSeverity, valueAlarm.highWarningLimit,
valueAlarm.highWarningSeverity, valueAlarm.hysteresis, valueAlarm.lowAlarmLimit,
valueAlarm.lowAlarmSeverity, valueAlarm.lowWarningLimit,
valueAlarm.lowWarningSeverity
```
QSRV does not use transmission-absence to signal "unset" — it carries that in-band (`valueAlarm.active = False`, `highAlarmSeverity = 0`, and
`highAlarmLimit = NaN`).
### Reproducing
```python
from p4p.nt import NTScalar
from p4p.server import Server, StaticProvider
from p4p.server.thread import SharedPV
from p4p.client.thread import Context
nt = NTScalar('d', display=True, valueAlarm=True)
pv = SharedPV(nt=nt, initial=nt.wrap(1.0))
prov = StaticProvider("probe")
prov.add("PROBE:PV", pv)
with Server(providers=[prov], isolate=True) as S, \
Context('pva', conf=S.conf(), useenv=False) as ctx:
raw = ctx.get("PROBE:PV").raw
print(sorted(raw.changedSet(expand=True))) # ['value']
print(repr(raw['alarm']['message'])) # '' -- null to a Java client
```
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.