fujilib.devices¶
The Analyzer facade and open_device (design §7.1, §7.2), the session every
call goes through (design §6), discovery (design §7.5) and device profiles; the
frozen data models (design §8), the pure decoders from register banks to
models, the read procedures that join a read plan to a decoder, safety tiers
and capability flags, and the unified-API snapshots; and the write side:
encoding a setting, writing it and reading it back, settings documents, and
the operation commands (design §6.1-§6.4, Safety); and the
front panel's manual calibrations, planned, watched and driven from the host
(design §6.5).
Opening and using an analyzer¶
fujilib.devices.factory ¶
open_device: the entry point (design §7.1, unified API §A).
It opens the serial port (or takes the caller's open transport), binds one Modbus bus to it, attaches a session to the station, and by default identifies the analyzer. If anything fails or is cancelled on the way, what this call opened is closed again; a transport the caller passed in is never closed by it.
A port opened by name can be opened again the same way after a connection
failure (Analyzer.reopen()); a transport the caller passed in cannot.
open_device
async
¶
open_device(
port,
*,
profile=ZP_PROFILE,
protocol=None,
address=1,
serial_settings=None,
timeout=DEFAULTS.request_timeout_s,
identify=True,
channel_map=None,
options=Capability.NONE,
write_warn_per_minute=DEFAULTS.write_warn_per_minute,
settle_after_reopen_s=DEFAULTS.settle_after_reopen_s,
)
Open the analyzer at station address on port.
Use the result as an async context manager, or call close()::
async with await open_device("COM8", channel_map={"CH3": "o2"}) as anz:
frame = await anz.poll()
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
port
|
str | Transport
|
A serial port name ( |
required |
profile
|
DeviceProfile
|
The analyzer family. |
ZP_PROFILE
|
protocol
|
ProtocolKind | str | None
|
The wire protocol; the profile's (MODBUS RTU) when |
None
|
address
|
int
|
The station number, 1-31, as set on the front panel. |
1
|
serial_settings
|
SerialSettings | None
|
The serial framing, with |
None
|
timeout
|
float
|
Seconds to wait for each reply. A per-call |
request_timeout_s
|
identify
|
bool
|
Identify the analyzer before returning (six transactions). |
True
|
channel_map
|
Mapping[ChannelId | str, Gas | str] | None
|
The gas on each channel, e.g. |
None
|
options
|
Capability
|
Options the analyzer has, whatever its type code says, e.g.
|
NONE
|
write_warn_per_minute
|
int
|
Setting writes a minute above which a warning is logged; 0 for never (design §6.3). |
write_warn_per_minute
|
settle_after_reopen_s
|
float
|
Seconds for which readings are |
settle_after_reopen_s
|
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
an argument is invalid; nothing was opened. |
FujiConnectionError
|
the port cannot be opened, or the transport is closed. |
FujiConfigurationError
|
the transport already carries an open analyzer. |
FujiProtocolUnsupportedError
|
the station does not identify as a ZP analyzer. |
FujiError
|
identification failed. |
Source code in src/fujilib/devices/factory.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |
fujilib.devices.analyzer ¶
The :class:Analyzer facade: one ZP-series analyzer on one station (design §7.2).
Every method that reads from the analyzer runs as one operation of the
:class:~fujilib.devices.session.Session, under the port's operation lock and
one operation deadline. Arguments are checked first, so a bad channel or
parameter name is refused before anything is sent.
- Every I/O method takes a keyword-only
timeout: a deadline for the whole operation, including the wait for the port and any retries.None(the default) leaves it to the per-transaction timeout and the retries. - Channel arguments accept a :class:
~fujilib.registry.channels.ChannelIdor a string such as"CH3". - Everything that changes the analyzer takes
confirm, and is refused before anything is sent unless it isTrue(design §6.2). A setting write is written once and read back (:class:~fujilib.devices.writes.WriteResult); only the reviewed subset of the register map is writable (design §5.4).
Example::
async with await open_device(
"COM8", channel_map={"CH1": "co2", "CH2": "co", "CH3": "o2"}
) as anz:
frame = await anz.poll()
o2 = frame.channel("CH3") # value, unit, state, label source, ...
Analyzer ¶
A Fuji ZP-series analyzer. Created by :func:~fujilib.devices.factory.open_device.
Wrap an open session; :func:~fujilib.devices.factory.open_device does this.
Source code in src/fujilib/devices/analyzer.py
options
property
¶
The options taken as fitted: asserted when opening, or listed by the type code.
apply_settings
async
¶
apply_settings(
document,
*,
confirm=False,
any_analyzer=False,
max_tier=SafetyTier.DANGEROUS,
timeout=None,
)
Write the settings of a document that differ from the analyzer's.
The whole document is compared first (:meth:diff_settings); if any
setting is refused, nothing is written. confirm must then be
True; the CLI also asks for its destructive flag when a write is
DANGEROUS. The writes go one at a time in dependency order, each read
back; the first that fails stops the rest, and nothing is rolled back.
max_tier refuses a document whose writes go above it, judged on
the comparison made here, not on an earlier one: the CLI passes
PERSISTENT unless its destructive flag is given. timeout bounds
the whole apply.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
the document is not a settings document, or is refused; nothing was written. |
FujiConfirmationRequiredError
|
there is something to write and
|
FujiError
|
the comparison's reads failed. A failed write does not raise: it is in the report. |
Source code in src/fujilib/devices/analyzer.py
calibration_status
async
¶
What is calibrating, held or failed. Two transactions, the poll's status blocks.
Source code in src/fujilib/devices/analyzer.py
channel_status
async
¶
One measured channel's status: range, calibration, hold and errors. Two transactions.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
Source code in src/fujilib/devices/analyzer.py
close
async
¶
Close the analyzer and the port it opened. Idempotent.
Waits for an operation in progress to finish. A transport the caller
passed to open_device is left open.
Source code in src/fujilib/devices/analyzer.py
diff_settings
async
¶
Compare a settings document (fujilib-settings/1) with the analyzer's settings.
Read-only: the range tables and every setting are read (four
transactions), and each setting of the document is found unchanged, to
be written, or refused, with the reason (:mod:fujilib.devices.settings).
A document from another analyzer is refused unless any_analyzer.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
the document is not a settings document. |
FujiError
|
a transaction failed. |
Source code in src/fujilib/devices/analyzer.py
identify
async
¶
Read the type code, serial number, ranges and readings, and probe the capabilities.
Six transactions (design §4.3). channel_map replaces the asserted
gas labels; an asserted label is the only one fit for calculation
(design §2.9).
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiProtocolUnsupportedError
|
the type code does not name a ZP model. |
FujiError
|
a transaction failed. |
Source code in src/fujilib/devices/analyzer.py
manual_calibration ¶
manual_calibration(
plan,
*,
gas,
confirm=False,
rule=None,
adc=False,
interval=0.5,
key_timeout=2.0,
run_timeout=30.0,
cleanup_timeout=30.0,
)
A manual zero or span of plan, driven from the host with the panel's keys.
Use it as an async context manager (design §6.5)::
plan = await anz.plan_manual_calibration("CH3", "span")
gas = CalibrationGas(20.95, "vol%", label="20.95 % O2 in N2")
async with anz.manual_calibration(plan, gas=gas, confirm=True) as run:
await run.wait_steady() # the operator opens the span-gas valve
event = await run.calibrate(confirm=True)
Entering it checks, before any key, that the panel is on the
measurement screen with no calibration flag set, key lock and output
hold are off, the analyzer reports no instrument error, the plan reads
the same again and calibrates no channel on both ranges, and gas
(one for every established channel of the plan, or one for all) is
each channel's calibration-gas setting. It then presses ZERO or SPAN,
moves the cursor and selects the channel: the wait step, where the
gas settles. These keys are STATEFUL; calibrate(confirm=True)
sends the ENT that calibrates, which is DANGEROUS. Leaving the
block returns the panel to measurement for the step it is on (ESC on
the wait step), shielded from cancellation.
With adc the raw A/D values are read with each read on the wait
step and kept in the record. interval is the time between those
reads; the port is free between them, so a recording goes on.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
a gas is missing or malformed, or a time is not positive; nothing was sent. |
Source code in src/fujilib/devices/analyzer.py
plan_auto_calibration
async
¶
What :meth:start_auto_calibration would calibrate, against which gases, for how long.
Read-only: the channels enabled for it, their ranges (both where the calibration range is "both"), the calibration gases, whether the outputs are held, and an estimated duration inferred from the flow times. Two transactions, plus the range tables if needed.
Source code in src/fujilib/devices/analyzer.py
plan_auto_zero_calibration
async
¶
What :meth:start_auto_zero_calibration would zero; as :meth:plan_auto_calibration.
Source code in src/fujilib/devices/analyzer.py
plan_manual_calibration
async
¶
What a manual zero or span of channel at the front panel would calibrate.
Read-only. A zero of a channel set to "at once" zeroes every channel so set, and a channel set to "both" is calibrated on both its ranges; the plan lists every channel and range with its calibration gas (design §6.5). Two transactions, plus the range tables if needed.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiDecodeError
|
a range setting does not read as a range. |
Source code in src/fujilib/devices/analyzer.py
poll
async
¶
Read every established channel and the analyzer's status: two transactions.
Without detail only the concentrations are read (one transaction),
and every reading's validity is unknown rather than assumed (design
§4.3). A channel that reads non-zero for the first time joins the
established channels, in this frame and every later one (design §2.9).
Raises:
| Type | Description |
|---|---|
FujiError
|
a transaction failed. When the status block fails after
the concentrations were read, the error's |
Source code in src/fujilib/devices/analyzer.py
read_adc
async
¶
The 21 raw A/D counts of the service manual's table (undocumented; design §6.6).
A service diagnostic, not a calibrated or higher-resolution gas measurement.
Raises:
| Type | Description |
|---|---|
FujiCapabilityError
|
the analyzer has no A/D block; nothing was sent once known. |
Source code in src/fujilib/devices/analyzer.py
read_calibration_log
async
¶
Calibration-log records, newest first per channel; firmware 2.24 or later.
channel None reads every established measured channel, in order.
Each channel is seven transactions.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiFirmwareError
|
the analyzer has no calibration log (older firmware); nothing was sent once known. |
Source code in src/fujilib/devices/analyzer.py
read_channel
async
¶
One established channel's reading, from a full poll.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
Source code in src/fujilib/devices/analyzer.py
read_clock
async
¶
The analyzer's real-time clock and when it was read (undocumented; design §6.6).
Naive local time with a two-digit year; it drifts, and it never replaces host timestamps.
Raises:
| Type | Description |
|---|---|
FujiCapabilityError
|
the analyzer has no clock; nothing was sent once known. |
FujiDecodeError
|
the words are not a date. |
Source code in src/fujilib/devices/analyzer.py
read_error_log
async
¶
The error log, newest first; up to 14 entries with day, hour and minute only.
Source code in src/fujilib/devices/analyzer.py
read_metadata
async
¶
The settings snapshot a consumer records with its data (design §2.11, §7.2).
Response times, averaging, calibration gases and scope, hold, the
automatic schedules, the current ranges and the analyzer's clock. Reads
the identity first if identify() has not run, and the range tables
if a poll saw a range change.
Raises:
| Type | Description |
|---|---|
FujiError
|
a transaction failed. |
Source code in src/fujilib/devices/analyzer.py
read_parameter
async
¶
One register by its name in the register map (docs/registers.md).
A range-scaled value uses the range tables, read first if needed. An
alarm limit needs alarm_targets (alarm number to channel), because
the target register's encoding is contested (design §5.2); without it
the value is None and only the raw word is given.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
Source code in src/fujilib/devices/analyzer.py
read_parameters
async
¶
Several registers by name, read in the fewest blocks, in the order given.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
a name is not in the register map; nothing was sent. |
Source code in src/fujilib/devices/analyzer.py
read_ranges
async
¶
Read the range tables of channels 1-5; the session keeps them. One transaction.
Source code in src/fujilib/devices/analyzer.py
read_settings
async
¶
Every holding register, decoded, by name. Three transactions, plus the ranges if needed.
Raises:
| Type | Description |
|---|---|
FujiError
|
a transaction failed. |
Source code in src/fujilib/devices/analyzer.py
reopen
async
¶
Open the port again and identify the analyzer, after a connection failure.
A connection failure breaks the session (every later call is refused);
this is the way back without losing what the session learned. Only a
port that open_device opened by name can be reopened. The station
must answer as the same analyzer: the same serial number and type code.
Raises:
| Type | Description |
|---|---|
FujiConfigurationError
|
the port came from the caller; or another analyzer answers on the station. |
FujiConnectionError
|
the analyzer is closed, or the port cannot be opened. |
FujiError
|
identification failed; the analyzer stays unusable. |
Source code in src/fujilib/devices/analyzer.py
reprobe
async
¶
Probe capability again and return what was found (design §6.6).
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
Source code in src/fujilib/devices/analyzer.py
return_to_measurement
async
¶
Put the front panel back on the measurement screen (42002). STATEFUL.
It takes an operator out of whatever menu they are in, and out of a manual calibration's channel selection, so it is not refused there. It does not stop or cancel a calibration, and is refused while one is under way: on a manual calibration's wait step 42002 brings the display back but leaves the calibration's flag set, and nothing at the panel shows it (protocol findings §18.4, design §13.1 #83). ESC on the wait step, at the panel, cancels a manual calibration.
Raises:
| Type | Description |
|---|---|
FujiConfirmationRequiredError
|
|
FujiAnalyzerStateError
|
a calibration, automatic or manual, is under way; nothing was sent. |
FujiVerificationError
|
acknowledged, but the panel does not show the measurement screen; or it shows it with a calibration flag set, which a calibration started at the panel meanwhile would do. |
FujiWriteOutcomeUnknownError
|
its reply was lost and the status cannot be read. |
FujiError
|
a transaction failed. |
Source code in src/fujilib/devices/analyzer.py
set_calibration_gas
async
¶
Set the zero or span calibration gas of a measured channel's range. DANGEROUS.
It takes effect at the next calibration, manual or automatic, and a
wrong value miscalibrates the analyzer then. unit must be the
range's own; span gas is limited to 1-105 % and zero gas to 0-100 % of
the range's full scale.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiError
|
as :meth: |
Source code in src/fujilib/devices/analyzer.py
set_hold_mode
async
¶
What the outputs hold during calibration: last_value or setting.
Raises:
| Type | Description |
|---|---|
FujiError
|
as :meth: |
Source code in src/fujilib/devices/analyzer.py
set_hold_value
async
¶
The value a measured channel holds in setting mode, 0-100 % of full scale.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiError
|
as :meth: |
Source code in src/fujilib/devices/analyzer.py
set_output_hold
async
¶
Hold the outputs, and the Modbus concentrations, during calibration, or not.
Raises:
| Type | Description |
|---|---|
FujiError
|
as :meth: |
Source code in src/fujilib/devices/analyzer.py
set_range
async
¶
Select range 1 or 2 of a measured channel, whose range method must be manual.
It returns once the channel measures on the range: the analyzer switches some tens of milliseconds after the setting reads back, so the channel's current range is read until it follows, within the read-back budget.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiVerificationError
|
the setting reads back otherwise, or it reads back as written but the channel did not switch to it. |
FujiError
|
as :meth: |
Source code in src/fujilib/devices/analyzer.py
set_range_method
async
¶
How a measured channel changes range: manual or auto (remote is refused).
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiError
|
as :meth: |
Source code in src/fujilib/devices/analyzer.py
set_response_time
async
¶
Set a response time, 0-60 s, by channel or by slot ("o2", "ndir1".."ndir4").
There are four NDIR-component slots and one O2 slot, not one per
channel (design §2.6). A channel is mapped to its slot only through
asserted gases: O2 to the O2 slot, the n-th NDIR channel to ndirn,
which needs every channel before it asserted. Otherwise name the slot.
0 switches the analyzer's filter off (protocol findings §20).
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
the channel cannot be mapped to a slot; as
:meth: |
FujiError
|
as :meth: |
Source code in src/fujilib/devices/analyzer.py
snapshot
async
¶
Identity and health from cached state, with no I/O (unified API §H).
name defaults to the model, or "analyzer" before identify().
Source code in src/fujilib/devices/analyzer.py
start_auto_calibration
async
¶
Run auto calibration once (42003). DANGEROUS: it overwrites the calibration.
It zeroes and spans every channel enabled for it against the
calibration gases the analyzer's own valves let in, so it is right only
where those gases are plumbed. It needs the auto-calibration option,
which the type code lists or open_device(options=...) asserts. It is
refused while a calibration runs, the panel is in a menu, or the
analyzer reports an instrument error. The plan is read and returned
with the result; no register stops a calibration once started.
Raises:
| Type | Description |
|---|---|
FujiConfirmationRequiredError
|
|
FujiCapabilityError
|
the option is not fitted; nothing was sent. |
FujiAnalyzerStateError
|
the analyzer's state forbids it; nothing was sent. |
FujiWriteOutcomeUnknownError
|
its reply was lost and the status does not show it running. |
FujiError
|
a transaction failed. |
Source code in src/fujilib/devices/analyzer.py
start_auto_zero_calibration
async
¶
Run auto zero calibration once (42004). DANGEROUS; as :meth:start_auto_calibration.
It zeroes every channel enabled for auto calibration, and needs the auto-zero option.
Source code in src/fujilib/devices/analyzer.py
start_blowback
async
¶
Run blowback once (42005). STATEFUL; the blowback option, which no ZPA has.
No register shows blowback running, so the outcome is only sent.
Raises:
| Type | Description |
|---|---|
FujiCapabilityError
|
the analyzer has no blowback; nothing was sent. |
FujiError
|
as :meth: |
Source code in src/fujilib/devices/analyzer.py
status
async
¶
The analyzer's status: errors, alarms, auto calibration, display. Two transactions.
Source code in src/fujilib/devices/analyzer.py
wait_for_calibration
async
¶
Wait until nothing is calibrating, reading the status every interval seconds.
The port is free between reads, so a recording goes on meanwhile.
timeout bounds the whole wait. The result says whether a
calibration was seen running at all (if not, it may have ended
already), whether any error 4-9 is active at the end, and which
appeared since since: pass the before of the command's result,
or the wait's own first read is the baseline.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiTimeoutError
|
something was still calibrating at |
FujiError
|
a status read failed. |
Source code in src/fujilib/devices/analyzer.py
wait_for_manual_calibration
async
¶
Wait for a manual zero or span made at the front panel to end, and describe it.
Polls every interval seconds (two transactions, and the A/D block
with adc) until a pass through the calibration steps ends, one
already under way included. The event gives the channels, how it
ended and why, the readings before and after, the calibration gases
and, with adc, the raw A/D values when it ran (design §6.5). The
port is free between polls, so a recording goes on meanwhile.
A calibration runs for a second or two, so a long interval can
miss it; the undocumented result register usually settles the outcome
then, and otherwise the event says ambiguous.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiCapabilityError
|
|
FujiTimeoutError
|
no pass ended within |
FujiError
|
a read failed. |
Source code in src/fujilib/devices/analyzer.py
write_parameter
async
¶
Write one setting by its name in the register map, then read it back (design §6.3).
Only the reviewed subset is writable (docs/registers.md, design
§5.4). value is True/False for a flag, an enum member or its
name for an enumerated setting, a whole number for a time or count, and
for a calibration gas a number in unit, which is then required and
must be the unit of the gas's (channel, range).
Before the write the analyzer's status is read, and the write refused while a calibration runs or the front panel is in a menu. The write is sent once, never retried, and read back whatever happens to its reply. About six transactions.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
an unknown or read-only name, or a value that does not fit; nothing was written. |
FujiConfirmationRequiredError
|
|
FujiAnalyzerStateError
|
a calibration is running or the panel is in a menu; nothing was written. |
FujiModbusError
|
the analyzer refused the write; nothing was applied. |
FujiVerificationError
|
the setting reads back as something else. |
FujiWriteOutcomeUnknownError
|
neither the write's reply nor the read-back arrived; the write may or may not have been applied. |
Source code in src/fujilib/devices/analyzer.py
fujilib.devices.session ¶
The session: the only path from the facade to the wire (design §6).
Every I/O call of an :class:~fujilib.devices.analyzer.Analyzer goes through
:meth:Session.run, which walks the gates (:meth:Session.gate) before
anything is sent (design §6.1):
- State. The session is open, and no connection failure has broken it.
- Safety tier. Anything above
READ_ONLYneedsconfirm=True. - Capability. An operation that needs a probed capability is refused
while that capability is known to be
UNSUPPORTED(design §6.6), and one that needs an option is refused unless the type code lists it or the caller asserted it (:attr:Session.options).
The facade resolves names and checks access before the gates, and checks
values after them, still before any I/O. run then starts the operation
deadline, which also covers the wait for the port's operation lock (design
§6.4), and holds the lock for the whole operation, so its transactions are
never interleaved with other traffic on the port. A failure is kept as
:attr:Session.last_error. A connection failure, or a write whose outcome is
unknown because the port failed, also breaks the session: every later call
is refused until the analyzer is opened again.
A setting write (:meth:Session.write_setting) then reads the analyzer's
status and refuses to write while a calibration runs or the front panel is in
a menu, or while this session drives a manual calibration at the panel
(:meth:Session.claim_panel); reads the setting and what it depends on; for a range-scaled value,
reads the range it is scaled by; encodes the value; writes it once and reads
it back (:mod:fujilib.devices.writes); and, for a channel's selected range,
waits until the channel measures on it.
The session keeps what it has learned about the station (design §6.7):
| Cache | Filled by | Changed by |
|---|---|---|
identity (DeviceInfo) |
identify() |
identify(); what the rows below learn |
| established channels | the caller's assertion; a non-zero reading | only ever grows |
| ranges | identify(), read_ranges() |
re-read when a poll sees a current range change |
| availability per capability | the probes | reprobe(); every read of the capability |
| last frame | poll() |
the next poll() |
The front panel stays live, so none of this stays authoritative for long: an operator can change a range or a setting at any time (design §1).
A session whose port was opened by name can be reopened after a connection
failure (:meth:Session.reopen): the port is opened again under the same
settings, the analyzer is identified again and must be the same one, and
what the session had learned is kept. When the reopen follows a connection
failure, readings that would be ok are settling for a while
(:attr:Session.settling_until, design §8): a pulled cable and a power cut
look alike from here, and an analyzer that was switched off reports nothing
of its own while it warms up.
Reopener ¶
Opens the session's port again, as it was first opened.
Session ¶
Session(
port,
*,
address,
profile,
channel_map=None,
reopener=None,
options=Capability.NONE,
verify_timeout=None,
write_warn_per_minute=DEFAULTS.write_warn_per_minute,
settle_after_reopen_s=DEFAULTS.settle_after_reopen_s,
)
One station's session: gates, the operation lock, deadlines and caches.
Created by :func:~fujilib.devices.factory.open_device; reached as
:attr:Analyzer.session <fujilib.devices.analyzer.Analyzer.session>.
Bind to station address on port, which the session then owns.
reopener opens the port again for :meth:reopen; without one the
session cannot be reopened. options are options the caller asserts
are fitted, whatever the type code says. verify_timeout bounds the
read after a write or command; by default it is what the port's timing
allows two block reads to take, each with its retries and a late-reply
window. write_warn_per_minute is where the write-rate warning
starts (0 for never). settle_after_reopen_s is how long readings
are settling after a reopen that follows a connection failure (0
for not at all).
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
Source code in src/fujilib/devices/session.py
asserted_options
property
¶
The options the caller asserted when opening the analyzer.
counters
property
¶
The station's traffic counters: requests, retries and failures by kind.
Live, and counted over the whole session: after :meth:reopen the same
object goes on counting.
current_ranges
property
¶
The range each of channels 1-5 was last seen measuring on, or None.
options
property
¶
The options taken as fitted: asserted, or listed by the type code.
An option the model's manual does not describe at all is never taken as fitted, even when asserted.
panel_claimed
property
¶
Whether this session is driving a manual calibration at the front panel.
recoverable_error_count
property
¶
Failed read attempts that a retry of the same read recovered (unified API §J).
Counted over the whole session, across :meth:reopen.
reopenable
property
¶
Whether :meth:reopen can open the port again: it was opened by name.
settling_until
property
¶
When readings stop being marked settling (UTC), or None when they are not.
The period starts when :meth:reopen brings back a session that a
connection failure had broken, and lasts the settle_after_reopen_s
the session was opened with (design §8).
verify_timeout
property
¶
Seconds the read after a write or command may take, whatever the deadline.
Design §6.4.
check_not_calibrating
async
¶
Read the status, and refuse return to measurement while a calibration is under way.
A menu or a manual calibration's channel selection does not refuse it: closing them is what it is for. On a manual calibration's wait step 42002 brings the display back but leaves the channel's flag set (protocol findings §18.4), and during a running calibration what it does is not known, so it is refused whenever a calibration flag is set (design §13.1 #83).
Raises:
| Type | Description |
|---|---|
FujiAnalyzerStateError
|
a calibration, automatic or manual, is under way, or this session drives one; nothing was written. |
Source code in src/fujilib/devices/session.py
check_quiet
async
¶
Read the status, and refuse a write or command it forbids (design §6.1).
Raises:
| Type | Description |
|---|---|
FujiAnalyzerStateError
|
a calibration is running, the front panel is in a menu, or this session drives a manual calibration; nothing was written. |
Source code in src/fujilib/devices/session.py
claim_panel ¶
Take the front panel for operation, a remote manual calibration (design §6.5).
One at a time: the keys of two would interleave. While it is held, the session refuses setting writes and commands, which would change what the calibration was planned from or take the panel from it.
Raises:
| Type | Description |
|---|---|
FujiAnalyzerStateError
|
another remote manual calibration holds it. |
Source code in src/fujilib/devices/session.py
close
async
¶
Close the session and its port. Idempotent.
Waits for an operation in progress to finish, and completes even when the caller is cancelled (the port closes shielded). A transport the caller supplied is left open.
Source code in src/fujilib/devices/session.py
ensure_ranges
async
¶
The range tables, read again first when none are cached or they are stale.
Source code in src/fujilib/devices/session.py
gate ¶
gate(
operation,
*,
tier=SafetyTier.READ_ONLY,
confirm=False,
requires=Capability.NONE,
subject=None,
)
Walk the gates of the module docstring; nothing is sent either way.
subject names what the operation acts on, for the refusal's message.
Raises:
| Type | Description |
|---|---|
FujiConnectionError
|
the session is closed or broken. |
FujiConfirmationRequiredError
|
|
FujiCapabilityError
|
a capability in |
Source code in src/fujilib/devices/session.py
learn_current_ranges ¶
Take in the current range of channels 1-5; a change makes the range tables stale.
Source code in src/fujilib/devices/session.py
learn_identity ¶
Take in an identity read; channel_map replaces the asserted map when given.
Raises:
| Type | Description |
|---|---|
FujiProtocolUnsupportedError
|
the type code does not name a ZP model. |
Source code in src/fujilib/devices/session.py
learn_poll ¶
Decode a poll, first establishing any channel it shows alive (design §2.9).
Source code in src/fujilib/devices/session.py
learn_ranges ¶
Take in a fresh read of the range tables.
note_port_failure ¶
Take in a failure reported in a result rather than raised.
A connection failure breaks the session, as a raised one does.
read_capability
async
¶
Run read of a probed capability, keeping its availability current.
Exception 02 to a well-formed read marks the capability UNSUPPORTED
and raises :class:FujiCapabilityError; words that do not validate mark
it INVALID_DATA; a result marks it SUPPORTED. Anything else, such
as a timeout, says nothing about it.
Source code in src/fujilib/devices/session.py
release_panel ¶
reopen
async
¶
Open the port again and identify the analyzer, usually after a connection failure.
The old port is closed first (after the operation in progress), then
opened again with the settings it was first opened with. The station
must identify as the analyzer that was open: the same serial number and
type code. The asserted channel map, the established channels and the
traffic counters are kept. If a connection failure had broken the
session, the settling period starts (:attr:settling_until). Calls
made meanwhile are refused. Reopens
are taken one at a time, and a call that finds the analyzer reopened
by another while it waited returns at once. :meth:close waits for a
reopen in progress, so no port is left open once it returns.
Raises:
| Type | Description |
|---|---|
FujiConfigurationError
|
the analyzer was closed; its port came from the caller, so it cannot be reopened; or another analyzer answers on the station. None of these changes by trying again. |
FujiConnectionError
|
the port cannot be opened. |
FujiValidationError
|
|
FujiError
|
identification failed. The session stays broken. |
Source code in src/fujilib/devices/session.py
993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 | |
run
async
¶
run(
operation,
body,
*,
timeout=None,
requires=Capability.NONE,
tier=SafetyTier.READ_ONLY,
confirm=False,
)
Run body as one operation, after the gates (see the module docstring).
body receives the station's client and the operation's deadline,
which it passes to every read so the budget is shared.
Raises:
| Type | Description |
|---|---|
FujiConnectionError
|
the session is closed or broken; nothing was sent. |
FujiConfirmationRequiredError
|
|
FujiCapabilityError
|
a capability in |
FujiValidationError
|
|
FujiError
|
whatever |
Source code in src/fujilib/devices/session.py
set_availability ¶
Record what is now known about capability.
snapshot ¶
Identity and health from what is cached; no I/O (unified API §H).
name defaults to the model, or "analyzer" before identify().
Source code in src/fujilib/devices/session.py
write_setting
async
¶
Write one prepared setting and read it back, under the operation lock.
In order: the status (refusing while a calibration runs or the panel is
in a menu), the range tables for a range-scaled value, the setting and
what it depends on, the encoding against that range, then the write and
its read-back (:func:~fujilib.devices.writes.write_setting). A
selected range that reads back as written is then waited for until the
channel measures on it. The range tables are read again next time they
are needed after a range or range-scaled write (design §6.7).
Raises:
| Type | Description |
|---|---|
FujiAnalyzerStateError
|
the status forbids a write now. |
FujiValidationError
|
the value does not fit the range, or a setting it depends on does not allow it; nothing was written. |
FujiVerificationError
|
a selected range reads back as written, but the channel did not measure on it within the read-back budget. |
FujiError
|
as :func: |
Source code in src/fujilib/devices/session.py
SessionState ¶
Bases: StrEnum
Whether a session can talk to its analyzer.
BROKEN
class-attribute
instance-attribute
¶
A connection failure: the analyzer has to be opened again.
describe_identity ¶
describe_identity(
identity,
*,
address,
serial_settings,
asserted=None,
established=(),
availability=None,
)
The :class:DeviceInfo of an identity read (design §2.9, §6.6).
established adds channels seen earlier; availability overrides
what the identity's own probes found.
Raises:
| Type | Description |
|---|---|
FujiProtocolUnsupportedError
|
the type code does not name a ZP model. |
Source code in src/fujilib/devices/session.py
fujilib.devices.discovery ¶
Finding analyzers on serial ports (design §7.5, unified API §B).
Discovery is read-only. Each station gets one FC04 read of type-code digits
1-3, which on a ZP analyzer name its model (ZPA, ZPB, ...):
- a reply naming a model is an analyzer, identified in full unless asked not to;
- an exception reply, or characters that name no model, is a Modbus device that is not a ZP analyzer;
- silence is an empty address (or one whose station number differs).
The baud rate is fixed at 38400, so a port is one sweep of station numbers. Ports are scanned concurrently; the stations of one port one at a time, on one bus. An absent station costs the probe timeout plus the 0.1 s quiet window after it, so a full sweep of 31 stations takes about 12 s per port.
Discovery never raises for a failed probe or a port that will not open; each
becomes a row with ok=False. It raises only for invalid arguments.
DiscoveryResult
dataclass
¶
DiscoveryResult(
ok,
port,
address,
baudrate,
protocol,
device_info,
error,
elapsed_s,
model=None,
)
One probed station (unified API §B).
Attributes:
| Name | Type | Description |
|---|---|---|
ok |
bool
|
Whether the station answered as an analyzer of a known family. |
port |
str
|
The canonical port name. |
address |
int | None
|
The station number probed. |
baudrate |
int | None
|
The baud rate used, or |
protocol |
ProtocolKind | None
|
The wire protocol when |
device_info |
DeviceInfo | None
|
The full identity when |
error |
FujiError | None
|
Why the station is not |
elapsed_s |
float
|
Seconds spent on the station. |
model |
str | None
|
The model the probe named ( |
DiscoverySummary
dataclass
¶
What discovery found on one port.
Attributes:
| Name | Type | Description |
|---|---|---|
port |
str
|
The canonical port name. |
ok |
bool
|
Whether any analyzer answered. |
addresses |
tuple[int, ...]
|
The stations that answered as analyzers. |
probed |
int
|
How many stations were probed. |
error |
FujiError | None
|
When nothing answered: why the port did not open, or else the
first probe's error. |
elapsed_s |
float
|
Seconds spent on the port's stations, summed. |
find_devices
async
¶
find_devices(
*,
ports=None,
addresses=(1,),
profiles=DEVICE_PROFILES,
per_probe_timeout_s=0.3,
identify=True,
max_concurrency=8,
)
Probe addresses on each of ports for analyzers; read-only.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ports
|
Sequence[str] | None
|
Port names; every serial port of the host when |
None
|
addresses
|
Sequence[int]
|
Station numbers to probe, 1-31. |
(1,)
|
profiles
|
Sequence[DeviceProfile]
|
Analyzer families to try, in order. |
DEVICE_PROFILES
|
per_probe_timeout_s
|
float
|
Seconds to wait for each station's reply. Probes are not retried. |
0.3
|
identify
|
bool
|
Identify each analyzer found (six more transactions each). |
True
|
max_concurrency
|
int
|
How many ports are scanned at once. |
8
|
Returns:
| Type | Description |
|---|---|
list[DiscoveryResult]
|
One result per port and station, in the order given. |
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
an argument is invalid; nothing was sent. |
FujiConnectionError
|
|
Source code in src/fujilib/devices/discovery.py
summarize_discovery ¶
One summary per port, in the order the ports first appear.
Source code in src/fujilib/devices/discovery.py
fujilib.devices.profile ¶
Device profiles: what one analyzer family brings (design §5.3).
A :class:DeviceProfile names a family and carries what differs between
families: its register map, its protocol and serial framing, how a station is
identified, and how discovery recognizes one. Profiles are shared and frozen;
what one station turns out to be (its channels, what its probes found) lives
in the session, never in the profile.
One profile exists: :data:ZP_PROFILE, for the ZPA and the ZPB, ZPG, ZPAJ and
ZPG3E models that share its MODBUS map (TN5A1190).
DeviceProfile
dataclass
¶
DeviceProfile(
name,
registry,
default_protocol,
default_serial,
identify,
discovery_probe,
recognize,
)
One analyzer family.
default_serial
instance-attribute
¶
The family's serial framing. Its port is a placeholder the caller's port replaces.
recognize
instance-attribute
¶
The model the probe's words name, or None when they are not this family's.
registry
instance-attribute
¶
The family's register map; parameter names resolve against it.
IdentifyStrategy ¶
Bases: Protocol
Reads what identify() establishes about one station.
__call__
async
¶
Read the station's identity, ranges and readings, and probe its capabilities.
recognize_zp ¶
The model when type-code digits 1-3 read Z, P and a model letter (design §7.5).
Returns "ZPA", "ZPB", "ZPG"… or None for anything else.
Source code in src/fujilib/devices/profile.py
Models¶
fujilib.devices.models ¶
Frozen data models (design §8).
Every model is a @dataclass(frozen=True, slots=True) and constructible by
keyword without hardware, so consumers can build them in simulators.
Validity. Each :class:Reading carries a :class:ReadingState, one token
from a closed vocabulary that says whether the value is live and, if not, why.
Reading.valid derives from it: True for ok, None when the status
was not read, False otherwise. Raw values are always kept; validity flags
them, it does not hide them.
Row columns. The columns a reading and the analyzer status contribute to a
row are defined once, here (:data:READING_COLUMNS, :data:ANALYZER_COLUMNS).
:meth:Reading.as_dict, :meth:Frame.as_long_rows and
:func:fujilib.sinks.base.sample_to_row all use these definitions, so they
cannot disagree. Every column value is a scalar (float, int, str,
bool or None); sets and tuples are encoded as strings.
AdcValues
dataclass
¶
AdcValues(
inputs,
temperatures,
resistances,
pressure,
reference_voltage,
ground,
raw,
received_at,
t_mono_ns,
)
The undocumented A/D block: raw service counts, not a gas measurement (design §6.6).
The groups follow the service manual's table order; that interpretation is inferred.
AnalyzerMetadata
dataclass
¶
AnalyzerMetadata(
serial_number,
ranges,
current_range,
response_time_s,
response_time_ndir_s,
response_time_o2_s,
moving_average,
calibration_gas,
calibration_scope,
hold_mode,
output_hold,
auto_calibration,
auto_zero,
clock,
clock_read_at,
captured_at,
)
The read-only settings snapshot a consumer records with its data (design §2.11).
Response times are per NDIR component slot and a separate O2 slot;
response_time_s maps them onto channels only where the channel labels
say which is which.
calibration_gas
instance-attribute
¶
(channel, range) → (zero, span), scaled by the range; None if unscalable.
AnalyzerStatus
dataclass
¶
AnalyzerStatus(
instrument_error,
calibration_error,
errors,
alarms,
peak_count,
peak_alarm,
auto_calibration_running,
display,
)
Analyzer-level status from a full poll.
AutoCalibrationSchedule
dataclass
¶
Auto calibration: the schedule, which channels and ranges, and the gas flow times.
AutoZeroSchedule
dataclass
¶
Auto zero calibration: the schedule and the gas flow time.
AveragePeriod
dataclass
¶
One moving-average period (holding 40085-40092).
CalibrationLogEntry
dataclass
¶
One calibration-log record (firmware 2.24 or later).
CalibrationScope
dataclass
¶
How far a calibration started on one channel reaches (design §2.6).
ChannelInfo
dataclass
¶
What is known about one established channel's label.
derived_from
class-attribute
instance-attribute
¶
For an O2-corrected value or average, the channel of the corrected component.
ChannelStatus
dataclass
¶
ChannelStatus(
range,
zero_calibrating,
span_calibrating,
auto_zero_running,
auto_span_running,
hold,
errors,
)
Per-channel status of a measured channel (1-5).
calibrating
property
¶
Whether any zero or span calibration, manual or automatic, is running.
ColumnDef
dataclass
¶
A row column: its name, scalar type and how to extract it from a model.
DeviceHealth ¶
Bases: StrEnum
How completely identify() succeeded.
DeviceInfo
dataclass
¶
DeviceInfo(
model,
type_code,
serial_number,
channels,
ranges,
capabilities,
availability,
protocol,
address,
serial_settings,
health,
firmware=None,
)
What identify() established about an analyzer.
DisplayState
dataclass
¶
DisplayState(
screen,
calibration_step,
top_channel,
cursor_channel,
calibration_result=None,
key=None,
)
What the front panel shows (input 30181-30183, 30189; 30186 and 30190 observed).
calibration_result
class-attribute
instance-attribute
¶
The last manual calibration (30186, observed; protocol findings §14.2).
calibration_step
instance-attribute
¶
The manual-calibration step (30182) on the measurement screen; on a menu, the raw page number the word shows there (protocol findings §15).
key
class-attribute
instance-attribute
¶
The key being pressed at the panel (30190, observed); KeyCode(0) for none.
ErrorLogEntry
dataclass
¶
One error-log entry (newest first in the log).
Frame
dataclass
¶
Every established channel and the analyzer status from one poll.
readings_timing
instance-attribute
¶
Timing of the block that holds every concentration.
as_long_rows ¶
One row per channel, with the analyzer status repeated on each.
For SQL unions with long-format siblings; the recorder's rows are wide.
Source code in src/fujilib/devices/models.py
channel ¶
The reading of channel.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
the channel is unknown or not in this frame. |
Source code in src/fujilib/devices/models.py
PartialTimestamp
dataclass
¶
A log time without a year (and, in the error log, without a month).
resolve ¶
The unique matching time at most max_age before clock.
clock is a reference in the same (naive local) time as the log,
normally the analyzer's own clock. Returns None when no candidate
or more than one candidate falls in the window.
Source code in src/fujilib/devices/models.py
RangeInfo
dataclass
¶
A measured channel's ranges, from the fixed-setting registers.
count
instance-attribute
¶
How many ranges the channel has, 1 or 2. Unused channels also report ranges.
of ¶
(unit, full_scale, decimals) of range rng (1 or 2).
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
Source code in src/fujilib/devices/models.py
Reading
dataclass
¶
Reading(
channel,
gas,
suggested_gas,
label_source,
role,
value,
unit,
raw_value,
decimals,
status,
state,
protocol,
)
One channel's concentration from a poll, with its label, validity and provenance.
decimals
instance-attribute
¶
The decimal-point register, 0-3, so the exact decimal can be rebuilt.
gas
instance-attribute
¶
UNKNOWN unless asserted by the caller (or, where so labelled, decoded).
status
instance-attribute
¶
None for derived channels and when the status block was not read.
as_decimal ¶
The exact decimal value, or None when the decimal point does not decode.
as_dict ¶
ReadingState ¶
Bases: StrEnum
Whether a reading is live and, if not, the most important reason why.
When several reasons apply, the first in this order wins: analyzer error, channel error, calibrating, auto calibration, hold, source invalid, settling.
ANALYZER_ERROR
class-attribute
instance-attribute
¶
The analyzer reports error 1, 2, 3 or 10.
AUTO_CALIBRATION
class-attribute
instance-attribute
¶
An auto calibration or auto zero calibration is running.
CALIBRATING
class-attribute
instance-attribute
¶
The channel is being zero- or span-calibrated.
CHANNEL_ERROR
class-attribute
instance-attribute
¶
The channel reports an error 4-9.
HOLD
class-attribute
instance-attribute
¶
The channel's output is held; the value is frozen, not live.
SETTLING
class-attribute
instance-attribute
¶
Read soon after the connection came back: the analyzer may have been switched off, and reports nothing of its own while it warms up (design §8).
SOURCE_INVALID
class-attribute
instance-attribute
¶
A derived channel whose source channel or O2 channel is not valid.
UNKNOWN
class-attribute
instance-attribute
¶
The status was not read, or the value did not decode.
Schedule
dataclass
¶
An automatic function's schedule.
The start hour and minute are kept raw: the manual says BCD, but the bench unit contradicts it (design §2.6).
TransferTiming
dataclass
¶
Host timing of one Modbus transaction.
midpoint_mono_ns
property
¶
Monotonic midpoint of request and reply: the best estimate of the reading's time.
received_at
instance-attribute
¶
Wall clock (UTC, tz-aware) when the reply had been read.
requested_at
instance-attribute
¶
Wall clock (UTC, tz-aware) when the request had been sent: after the inter-frame gap, the write and the drain, so no wait for the line is included.
encode_codes ¶
Encode a set of error codes for a row: sorted and comma-joined, "" if none.
encode_enum ¶
Encode an enum value for a row: its lower-case name, or the raw number.
Source code in src/fujilib/devices/models.py
fujilib.devices.capability ¶
Safety tiers, capability flags and probe availability (design §6.2, §6.6).
- :class:
SafetyTier— how dangerous an operation is; anything aboveREAD_ONLYneedsconfirm=True. - :class:
Capability— firmware, option and model features. - :class:
Availability— what a probe found for one capability.
This module is a leaf: it imports nothing from :mod:fujilib, so the
registry can use these enums without an import cycle. Note that anymodbus
also exports a Capability; never import that one bare beside this one.
Availability ¶
Bases: StrEnum
What a probe found for one capability (design §6.6).
Only UNSUPPORTED short-circuits later calls; UNKNOWN is retried on
next use.
INVALID_DATA
class-attribute
instance-attribute
¶
Readable, but the content does not validate (e.g. a clock that is not a date).
SUPPORTED
class-attribute
instance-attribute
¶
The probe returned data that validates.
UNKNOWN
class-attribute
instance-attribute
¶
Not probed yet, or the probe timed out or failed framing.
UNSUPPORTED
class-attribute
instance-attribute
¶
A well-formed probe inside one known block was answered with exception 02.
Capability ¶
Bases: Flag
Features that depend on firmware, options or model.
The first group is probed by identify() and never assumed
(design §6.6). The rest are options and model features
(:data:OPTION_CAPABILITIES): the type code suggests them and the caller
may assert them. They gate the operation commands that need them, never
reads, because an option's registers read even when it is not fitted.
ADC_VALUES
class-attribute
instance-attribute
¶
Undocumented A/D values at FC04 03EFh (observed on the bench unit).
AUTO_CALIBRATION
class-attribute
instance-attribute
¶
Auto calibration (option).
CALIBRATION_LOG
class-attribute
instance-attribute
¶
Calibration log at FC04 1000h; firmware 2.24 or later.
CLOCK
class-attribute
instance-attribute
¶
Undocumented real-time clock at FC04 03E8h (observed on the bench unit).
MEASUREMENT_POINT
class-attribute
instance-attribute
¶
Measurement-point switching (option).
O2_CORRECTION
class-attribute
instance-attribute
¶
O2-corrected outputs (type-code digit 21).
REFERENCE_GAS
class-attribute
instance-attribute
¶
Reference-gas switching and averaging: ZPB and ZPG only.
TYPE_CODE_EXT
class-attribute
instance-attribute
¶
Type-code digits 27-29 at FC04 047Ah; firmware 2.24 or later.
SafetyTier ¶
Bases: IntEnum
How dangerous an operation is. The tier follows its effect (design §6.2).
DANGEROUS
class-attribute
instance-attribute
¶
Calibration, calibration schedules, calibration gases and scope.
Requires confirm=True, and the CLI also requires
--i-understand-this-is-destructive.
PERSISTENT
class-attribute
instance-attribute
¶
A settings write that changes only configuration. Requires confirm=True.
STATEFUL
class-attribute
instance-attribute
¶
A transient state change: return to measurement, blowback. Requires confirm=True.
fujilib.devices.snapshot ¶
Identity and health snapshots (unified API §H).
snapshot() builds these from cached state and never performs I/O. The base
:class:DeviceSnapshot has the same fields in every sibling library, so a
consumer can render every device's snapshot uniformly; :class:FujiDeviceSnapshot
adds what is specific to the analyzer.
DeviceSnapshot
dataclass
¶
DeviceSnapshot(
name,
model,
firmware,
serial,
connected,
last_error,
recoverable_error_count,
captured_at,
)
Cross-library identity and health snapshot.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The device's name (a manager name, or the model when unmanaged). |
model |
str | None
|
Cached model, e.g. |
firmware |
str | None
|
Always |
serial |
str | None
|
Cached serial number, or |
connected |
bool
|
Whether the session is operational. |
last_error |
ErrorContext | None
|
The context of the last failure, or |
recoverable_error_count |
int
|
Read retries that later succeeded, since open. |
captured_at |
datetime
|
When the snapshot was taken (UTC, tz-aware). |
FujiDeviceSnapshot
dataclass
¶
FujiDeviceSnapshot(
name,
model,
firmware,
serial,
connected,
last_error,
recoverable_error_count,
captured_at,
address,
protocol,
type_code,
capabilities,
availability,
channels,
)
Bases: DeviceSnapshot
Analyzer-specific snapshot extras.
Attributes:
| Name | Type | Description |
|---|---|---|
address |
int
|
The station number, 1-31. |
protocol |
ProtocolKind
|
The session's wire protocol. |
type_code |
str | None
|
The raw type code, or |
capabilities |
Capability
|
Capabilities the session believes the analyzer has. |
availability |
Mapping[Capability, Availability]
|
What each probe found, per capability. |
channels |
tuple[ChannelId, ...]
|
The established channels, in order. |
Decoding and reading¶
fujilib.devices.decode ¶
Pure decoders: register banks to models (design §2.5-§2.9, §8).
A bank maps addresses of one register table to the words read there
(BlockRead.to_bank builds one from a reply; a register dump is one). Every
function here is pure: no I/O, no clock reads, no caches. The session supplies
what it has cached (ranges, labels, the channels established so far) and the
timing of each transaction.
Decoding is total where the analyzer may legitimately surprise: an
undocumented enum value is kept as an int, and a concentration whose
decimal point does not decode becomes a reading with value=None and state
unknown instead of failing the poll. Only structural problems, such as a
missing word, raise :class:~fujilib.errors.FujiDecodeError.
RegisterValue
dataclass
¶
decode_adc ¶
The 21 A/D counts (42 words, long words low word first), grouped in table order.
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
not exactly 42 words. |
Source code in src/fujilib/devices/decode.py
decode_analyzer_status ¶
The analyzer-level status, from both poll blocks.
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
a status word was not read. |
Source code in src/fujilib/devices/decode.py
decode_calibration_log ¶
One channel's calibration log, newest first, without empty records.
A record is empty when its channel field reads -1 (FFFFh, or 00FFh:
the manual writes "FF(-1)").
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
|
Source code in src/fujilib/devices/decode.py
decode_channel_status ¶
The status of measured channel (1-5), from both poll blocks.
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
|
Source code in src/fujilib/devices/decode.py
decode_clock ¶
The analyzer's clock: seven BCD words, year (two digits) to second.
The result is naive local time. The weekday register must agree with the date (0 = Sunday).
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
the words are not seven BCD values forming a valid date. |
Source code in src/fujilib/devices/decode.py
decode_current_ranges ¶
The range each of channels 1-5 is measuring on, 1 or 2 (input 30038-30042).
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
the current-range registers were not read. |
Source code in src/fujilib/devices/decode.py
decode_error_log ¶
The error log, newest first, without empty entries.
The log has no year and no month; each time is a :class:PartialTimestamp.
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
a log word was not read. |
Source code in src/fujilib/devices/decode.py
decode_frame ¶
decode_frame(
bank,
channels,
*,
readings_timing,
status_timing=None,
raw=b"",
protocol=ProtocolKind.MODBUS_RTU,
)
Decode a poll of the established channels.
With status_timing the bank must hold both poll blocks and every
reading gets a state; without it only the readings block was read, and
every state is UNKNOWN (poll(detail=False)).
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
a needed word was not read. |
Source code in src/fujilib/devices/decode.py
decode_identity ¶
The type code (with digits 27-29 when they were read) and the serial number.
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
the identity registers were not read, or are not characters. |
Source code in src/fujilib/devices/decode.py
decode_metadata ¶
decode_metadata(
bank,
*,
serial_number,
ranges,
channels,
current_range,
captured_at,
clock=None,
clock_read_at=None,
)
The settings snapshot, from the holding bank and what the session has cached.
Response times are mapped onto channels only where the labels say which channel is O2 and which are NDIR components (in channel order).
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
a holding word was not read. |
Source code in src/fujilib/devices/decode.py
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 | |
decode_ranges ¶
The range tables of channels 1-5.
A range whose decimal point does not decode has a nan full scale.
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
the range registers were not read. |
Source code in src/fujilib/devices/decode.py
decode_register ¶
Decode spec from its words.
scaling gives the decimals and unit of a range-scaled register; an
inline-scaled concentration is decoded with :func:decode_frame instead
and comes back raw here.
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
the words do not decode as the spec's type. |
Source code in src/fujilib/devices/decode.py
derive_states ¶
Apply the validity rule of design §8 to every established channel.
- Without the status block every state is
UNKNOWN. - A channel whose value did not decode is
UNKNOWN. - A channel is invalid when the analyzer reports an instrument error, the channel reports an error 4-9, it is calibrating, an auto calibration is running, or it is held.
- A derived channel (O2-corrected, an average, or an unlabelled channel
above 5) is also
SOURCE_INVALIDwhen its source channel or the O2 channel is invalid. When its source is not known, any invalid measured channel makes it invalid.
Source code in src/fujilib/devices/decode.py
label_channels ¶
Label the established channels (design §2.9).
The established channels are present plus every asserted one. An
asserted label wins and is the only source fit for calculation; otherwise
gas is UNKNOWN and the type code (or the layout rule) only
suggests one.
Source code in src/fujilib/devices/decode.py
nonzero_channels ¶
Channels whose reading triple is not all zero in bank.
An all-zero triple is a legal zero reading (0, dp 0, vol%), not proof of absence, so this only ever adds channels (design §2.9, step 2).
Source code in src/fujilib/devices/decode.py
range_scaling ¶
The decimals and unit that scale spec, or None if they cannot be known.
BY_RANGE uses the spec's own channel and range. BY_ALARM_TARGET
needs the alarm's target channel, which only alarm_targets can supply
because the target register's encoding is contested.
Source code in src/fujilib/devices/decode.py
words_of ¶
The count words from address.
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
a word is missing from |
Source code in src/fujilib/devices/decode.py
fujilib.devices.reads ¶
Read procedures: a precomputed plan, a client and a pure decoder (design §4.3, §6.6).
Each function reads one of the precomputed plans of
:mod:fujilib.protocol.modbus.read_plan through a
:class:~fujilib.protocol.base.ProtocolClient and decodes the words with
:mod:fujilib.devices.decode. They hold no state: whatever a decoder needs
beyond the words (the established channels, the ranges, the current range)
is passed in by the caller, which caches it (design §6.7). Gates, caches and
availability bookkeeping belong to the session; these functions only read.
A read plan runs under the port's operation lock, so the blocks of one procedure are never interleaved with other traffic on the port. They are not simultaneous: the analyzer updates between transactions, and the front panel stays live (design §1).
Transactions per procedure (FC04 unless noted), tested as exact lists:
| Procedure | Transactions |
|---|---|
read_poll, read_frame |
0000h+61, 0083h+60; the first only without detail |
read_status |
the same two blocks |
read_ranges |
0425h+35 |
read_identity |
0425h+35, 0448h+34, 0000h+42, then the probes |
probe_capabilities |
03E8h+49 (03E8h+7 and 03EFh+42 if it fails), 047Ah+3, 1000h+9 |
read_metadata |
FC03 0000h+64, 0040h+64, 0080h+36; 0025h+5; 03E8h+7 with the clock |
read_settings |
FC03 0000h+64, 0040h+64, 0080h+44 |
read_error_log |
003Dh+60, 0079h+10, then 003Dh+5 to check |
read_calibration_log |
five blocks of 63 words and one of 45, then the first record |
read_clock |
03E8h+7 |
read_adc |
03EFh+42 |
ClockReading
dataclass
¶
The analyzer's clock and when it was read.
The clock is naive local time with a two-digit year, and it drifts: on the bench it ran minutes behind the host. It never replaces host timestamps (design §6.6).
Identity
dataclass
¶
What identify() reads: identity, ranges, presence and capabilities.
nonzero
instance-attribute
¶
Channels whose reading triple was not all zero (design §2.9 step 2).
PollRead
dataclass
¶
The words of one poll, before they are decoded.
current_ranges
property
¶
The range each of channels 1-5 is measuring on; in the readings block.
timings
instance-attribute
¶
The timing of each block: the readings block, then the status block.
decode ¶
The frame of the established channels.
Source code in src/fujilib/devices/reads.py
status ¶
The analyzer's and channels 1-5's status in a poll read with detail.
Source code in src/fujilib/devices/reads.py
ProbeResult
dataclass
¶
StatusRead
dataclass
¶
The analyzer's status and every measured channel's, from one poll's blocks.
probe_capabilities
async
¶
Probe each of capabilities (design §6.6).
The clock and the A/D values are adjacent, so when both are asked for they are probed with one read of both, and separately only if that fails.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
a capability is not one that is probed. |
FujiTimeoutError
|
|
FujiConnectionError
|
the port failed. |
Source code in src/fujilib/devices/reads.py
probe_capability
async
¶
Probe one capability (design §6.6).
SUPPORTED: the probe read data that validates.UNSUPPORTED: exception 02. Every probe is a well-formed read inside one documented or observed block, so 02 means the block is absent.UNKNOWN: no reply, a damaged reply, or another exception.INVALID_DATA: readable, but the words do not validate.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiTimeoutError
|
|
FujiConnectionError
|
the port failed. |
Source code in src/fujilib/devices/reads.py
read_adc
async
¶
The 21 A/D counts of the service manual's table (undocumented; design §6.6).
A service diagnostic, not a calibrated or higher-resolution gas measurement.
Source code in src/fujilib/devices/reads.py
read_calibration_log
async
¶
One channel's calibration log, newest first; firmware 2.24 or later (design §4.3).
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
Source code in src/fujilib/devices/reads.py
read_clock
async
¶
The analyzer's clock (undocumented; design §6.6).
Raises:
| Type | Description |
|---|---|
FujiDecodeError
|
the words are not a date. |
Source code in src/fujilib/devices/reads.py
read_error_log
async
¶
The error log, newest first (design §4.3).
A new entry shifts the whole log, so the newest record is read again after the scan; if it changed, the log is read once more.
Source code in src/fujilib/devices/reads.py
read_frame
async
¶
Poll the established channels: two transactions, or one without detail.
Without detail only the concentrations are read, and every reading's
state is unknown rather than a manufactured "ok" (design §4.3).
Raises:
| Type | Description |
|---|---|
FujiError
|
as :func: |
Source code in src/fujilib/devices/reads.py
read_identity
async
¶
Read the type code, serial number, ranges and readings, then probe the capabilities.
With probe=False no capability is probed and :attr:Identity.probes
is empty.
Raises:
| Type | Description |
|---|---|
FujiError
|
a transaction of the identity plan failed. A failed probe
does not raise; it is reported in :attr: |
FujiDecodeError
|
the type code or serial number is not characters. |
Source code in src/fujilib/devices/reads.py
read_metadata
async
¶
The settings snapshot consumers such as capa carry (design §7.2).
The current range of each channel is read with the settings, so the
snapshot does not depend on an earlier poll. clock reads the analyzer's
clock as well; pass it only when :attr:Capability.CLOCK is supported. A
clock that does not decode is reported as None. captured_at is when
the last settings block arrived.
Raises:
| Type | Description |
|---|---|
FujiError
|
a transaction failed. |
Source code in src/fujilib/devices/reads.py
read_poll
async
¶
Read a poll's words: two transactions, or one without detail.
The words are decoded separately (:meth:PollRead.decode), so a caller
can first learn from them which channels have come to life (design §2.9).
Raises:
| Type | Description |
|---|---|
FujiError
|
a transaction failed. When the status block fails after the
concentrations were read, the error's |
Source code in src/fujilib/devices/reads.py
read_ranges
async
¶
The range tables of channels 1-5.
Source code in src/fujilib/devices/reads.py
read_registers
async
¶
The registers called names, read in the fewest blocks, decoded, in the order given.
A concentration (inline scaling) comes back raw; :func:read_frame scales it.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
a name is not in the registry. |
Source code in src/fujilib/devices/reads.py
read_settings
async
¶
Every holding register, decoded, by name.
Range-scaled values use ranges; alarm limits need alarm_targets
(alarm number to channel), because the target register's encoding is
contested (design §5.2). Without a scale a value is None; its raw word
is always kept.
Source code in src/fujilib/devices/reads.py
read_status
async
¶
The analyzer's status and channels 1-5's, from the two poll blocks.
Source code in src/fujilib/devices/reads.py
Writing and commands¶
fujilib.devices.encode ¶
The word a setting write sends, from the caller's value (design §6.1, §6.3).
The inverse of :func:fujilib.devices.decode.decode_register, in two steps,
because a range-scaled value can only be finished once its range has been
read, immediately before the write:
- :func:
prepare_valuechecks everything that needs no I/O: that the register may be written, the value's type, an enum member, a finite number, and the unit a scaled value is given in. An unscaled value is encoded here and its limits checked. - :func:
encode_preparedfinishes a range-scaled value against that range: the unit must be the range's, the value must fit the range's decimals exactly, and the raw and percent-of-full-scale limits must hold.
Nothing here does I/O, and every refusal is a
:class:~fujilib.errors.FujiValidationError: nothing was sent.
PreparedValue
dataclass
¶
encode_prepared ¶
The word to write for prepared.
range_info is the (unit, full scale, decimals) of a range-scaled
setting's (channel, range), read just before the write; it is ignored for
an unscaled one.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
the range is unknown or in another unit, the value has more decimals than the range, or it is outside the limits. |
Source code in src/fujilib/devices/encode.py
prepare_value ¶
Check value for spec without I/O, and encode it unless it is range-scaled.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
RegisterSpec
|
The register, which must be writable. |
required |
value
|
object
|
|
required |
unit
|
Unit | str | None
|
The unit of a range-scaled value; required there, and refused
unless it is the range's own. For an unscaled value it may be given,
and must then be the register's ( |
None
|
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
the register is read-only, or the value or unit does not fit it. |
Source code in src/fujilib/devices/encode.py
fujilib.devices.writes ¶
Write procedures: one setting, written once and read back (design §6.3, §6.4).
Like :mod:fujilib.devices.reads, these functions hold no state; the gates,
the caches and the write-rate warning belong to the session.
One write, never retried. :func:write_setting writes one word with FC06
and reads it back. The read-back runs in a scope shielded from cancellation,
with its own deadline (verify_timeout), so it happens even when the write
used up the operation's deadline or its reply was lost. A caller that cancels
the call itself (rather than a deadline running out) cancels it without a
read-back; the port's late-reply window still protects the next request. What
the read-back finds decides the outcome, a :class:WriteState:
verified: the register reads back as written, whether or not the write's own reply arrived;mismatch: it reads back otherwise (the analyzer refused it silently, the front panel changed it, or a write whose reply was lost never arrived);unknown: the read-back failed too, so nothing is established.
An exception reply to the write is a definite refusal and is raised as it is: nothing was applied. A port that fails while the write waits for its reply is raised as an unknown outcome at once; nothing more can be read.
When not to write. :func:busy_reasons says why the analyzer's status
forbids a write or a command now: a calibration running, or the front panel
in a menu or in a manual calibration. The operator may be changing the very
setting, and a setting changed during a calibration can change its scope
mid-run (design §6.1). :func:calibrating_reasons is the calibration part
alone, which also stops return to measurement (design §13.1 #83).
WriteResult
dataclass
¶
WriteResult(
name,
requested,
previous,
observed,
state,
acknowledged,
timing,
read_back_error=None,
)
One setting write and what its read-back found.
read_back_error
class-attribute
instance-attribute
¶
Why the read-back failed, when it did.
WriteState ¶
Bases: StrEnum
What the read-back after a write established.
busy_reasons ¶
Why the analyzer's status forbids a write or a command now; empty when it does not.
Source code in src/fujilib/devices/writes.py
calibrating_reasons ¶
Which calibrations the status shows under way, automatic or manual; empty when none.
A manual calibration's channel flags are set from its wait step to its end (protocol findings §14.3).
Source code in src/fujilib/devices/writes.py
describe ¶
A register value for a message: 15 s, 20.95 vol%, manual.
Source code in src/fujilib/devices/writes.py
outcome_error ¶
The error for a write that is not verified, or None for one that is.
Source code in src/fujilib/devices/writes.py
write_setting
async
¶
Write raw to spec once with FC06, then read it back (see the module docstring).
previous is the register as read just before; scaling decodes a
range-scaled value. The result is returned whatever it says;
:func:outcome_error turns one that is not verified into its error.
Raises:
| Type | Description |
|---|---|
FujiModbusError
|
the analyzer refused the write with an exception reply; nothing was applied. |
FujiWriteOutcomeUnknownError
|
the port failed while the write waited for its reply. |
FujiError
|
the write could not be sent (a closed port, an expired deadline): nothing was written. |
Source code in src/fujilib/devices/writes.py
fujilib.devices.settings ¶
The analyzer's settings as a document: compared and applied (design §6.3, §7.7).
A settings document is what fuji-configure dump writes (format
fujilib-settings/1): the analyzer's identity, then each holding register
by name with its value and unit (and raw, access, safety
and evidence, which are informative). A document written by hand may give
only the settings to change, each as {"value": ..., "unit": ...} or as a
bare value.
Comparing (:func:diff_settings) decides, for every setting the document
names, one :class:ChangeAction:
unchanged: the analyzer has that value already;write: it differs, the register is writable, and the value passes every check that can be made before writing, against the range tables read just before;refused: an unknown name, an operation, an input register, a read-only register whose value differs, a value that does not fit, a unit other than the range's, or a range selection whose method would not be manual.
A document from another analyzer (another serial number) is refused as a whole unless the caller says any analyzer will do.
Applying (:meth:~fujilib.devices.analyzer.Analyzer.apply_settings)
preflights the whole document first: if anything is refused, nothing is
written. The writes then go in a fixed order, each a setting write of its own
(read back, never retried): output hold before the hold settings when it is
switched on, after them when it is switched off; a range method before its
range; then the response times, the calibration scope and the calibration
gases. The first write that fails stops the rest. The report says which
writes completed, which failed and which were not attempted. Nothing is
rolled back or switched back on.
ApplyReport
dataclass
¶
ApplyStatus ¶
Bases: StrEnum
How applying a document ended.
FAILED
class-attribute
instance-attribute
¶
The first write failed, or was refused by the analyzer; nothing was changed.
OK
class-attribute
instance-attribute
¶
Every write verified (or there was nothing to write).
PARTIAL
class-attribute
instance-attribute
¶
A write failed after others had completed; the rest were not attempted.
UNKNOWN
class-attribute
instance-attribute
¶
A write's outcome could not be established.
VERIFY_FAILED
class-attribute
instance-attribute
¶
A write read back as something else.
ChangeAction ¶
Bases: StrEnum
What applying a document would do with one of its settings.
DesiredSetting
dataclass
¶
One setting as a document gives it.
raw
class-attribute
instance-attribute
¶
The raw word, compared only when value is None (an alarm limit with no scale).
SettingChange
dataclass
¶
One setting of a document, against the analyzer.
SettingsDiff
dataclass
¶
Every setting of a document against the analyzer, writes in the order they would go.
identity_mismatch
class-attribute
instance-attribute
¶
Why the document is not this analyzer's, unless any analyzer was allowed.
refusal ¶
The error for a document that may not be applied, naming every reason.
Source code in src/fujilib/devices/settings.py
SettingsDocument
dataclass
¶
A settings document: the analyzer it describes, and its settings by name.
from_json
classmethod
¶
Read a document, as parsed from JSON.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
it is not a |
Source code in src/fujilib/devices/settings.py
diff_settings ¶
Compare document with the analyzer's current holding registers.
ranges are the range tables read with them; serial_number is the
analyzer's. See the module docstring for what is refused.
Source code in src/fujilib/devices/settings.py
fujilib.devices.operations ¶
Operation commands: auto calibration, auto zero, blowback, return to measurement.
The four documented commands (design §6.2-§6.4) are FC06 writes of 1 to 42002-42005
(:data:~fujilib.registry.write_policy.OPERATIONS). This module plans them,
sends them and reports what they did; the gates belong to the session.
What a calibration touches (:func:plan_calibration, ZPA manual p.45-47,
p.53-60). Auto calibration and auto zero calibration act on the channels
enabled for auto calibration (40021-40025), each on its auto-calibration
range (40116-40120), and on both ranges where the calibration range is
"both" (40031-40035). The "at once" setting (40026-40030) does not widen
them: their zero is always done together. Auto calibration then spans the
channels one at a time from Ch1. The plan also gives the calibration gases
each range will be calibrated against, the flow times, whether the outputs
are held, and a duration that is inferred from the flow times, which the
manuals do not state as such.
What a command did (:class:CommandResult). The analyzer's reply means
it accepted the command, not that it finished (TN5A1190a p.11, p.17), and a
command is never retried. So the status is read after it, shielded from
cancellation and within its own deadline:
- a calibration is
startedwhen the status shows it running, andambiguouswhen the analyzer acknowledged it but nothing runs: it never started, or already ended (design §6.4); - return to measurement is
donewhen the panel shows the measurement screen and no calibration flag is set. It is refused while a flag is set before it (design §13.1 #83): on a manual calibration's wait step 42002 brings the display back but leaves the flag set (protocol findings §18.4); - blowback is only
sent: no register shows it running.
A command whose reply was lost is established from the status where the status can say (a calibration running, the measurement screen shown), and is otherwise an unknown outcome.
No register stops a running calibration. The front panel can force-stop one, but not while key lock is on (ZPA manual p.55-57, p.62).
CalibrationPlan
dataclass
¶
What an automatic calibration will do, read from the settings (see the module docstring).
estimated_duration_s
instance-attribute
¶
The sum of the phases' flow times; an inference, not a documented figure.
hold
instance-attribute
¶
Whether output hold is on: the outputs and Modbus concentrations are held throughout.
phases
instance-attribute
¶
(phase, flow time in s) in order; which flow time is which is inferred.
CalibrationRun ¶
CalibrationStatus
dataclass
¶
What is calibrating, held or failed, from one read of the status blocks.
calibration_error
instance-attribute
¶
The calibration-error contact: an error 4-9 is active on some channel.
channels
instance-attribute
¶
Channels 1-5: calibration flags, hold, errors 4-9, current range.
running
instance-attribute
¶
Input 30049: an auto calibration or auto zero calibration is running.
CalibrationTarget
dataclass
¶
One channel an automatic calibration will calibrate.
CalibrationWait
dataclass
¶
How :meth:~fujilib.devices.analyzer.Analyzer.wait_for_calibration ended.
failed
property
¶
Whether any calibration error is active at the end.
An error left by an earlier calibration counts too: the analyzer shows
no difference. :attr:new_errors says which appeared since the baseline.
new_errors
instance-attribute
¶
Errors 4-9 active at the end that were not in the baseline.
saw_running
instance-attribute
¶
Whether any read showed a calibration running; if not, it may have ended already.
CommandOutcome ¶
CommandResult
dataclass
¶
CommandResult(
operation,
outcome,
acknowledged,
status,
plan,
timing,
status_error=None,
before=None,
)
An operation command sent once, and what the status after it showed.
status_error
class-attribute
instance-attribute
¶
Why the status read after the command failed, when it did.
calibration_status ¶
The calibration view of a status read.
Source code in src/fujilib/devices/operations.py
check_healthy ¶
Refuse a calibration while the analyzer reports an analyzer-level error.
A calibration now would compute its coefficients from a faulty measurement.
Raises:
| Type | Description |
|---|---|
FujiAnalyzerStateError
|
an instrument error or error 1, 2, 3 or 10 is active. |
Source code in src/fujilib/devices/operations.py
plan_calibration ¶
What run will do, from the :data:PLAN_SETTINGS values and the range tables.
Source code in src/fujilib/devices/operations.py
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | |
read_calibration_plan
async
¶
Read the :data:PLAN_SETTINGS and make the plan of run (two transactions).
Source code in src/fujilib/devices/operations.py
send_command
async
¶
Send operation once with FC06 and read the status after it (see the module docstring).
Raises:
| Type | Description |
|---|---|
FujiModbusError
|
the analyzer refused the command with an exception reply. |
FujiWriteOutcomeUnknownError
|
its reply was lost and the status cannot say whether it acted, or the port failed. |
FujiVerificationError
|
return to measurement was acknowledged, but the panel does not show the measurement screen; or the panel shows it with a calibration flag set. |
FujiError
|
the command could not be sent; nothing was. |
Source code in src/fujilib/devices/operations.py
The front panel¶
fujilib.devices.panel ¶
Manual calibration at the front panel: plans, watching and events (design §6.5).
A manual zero or span exists only at the front panel: ZERO or SPAN, the
cursor to the channel, ENT to select it (the wait step, while the gas
settles), and ENT again to calibrate. This module watches one from the status
registers; it sends no key. :mod:fujilib.devices.keys drives one from the
host, and records it with the same tracker.
What the registers show (protocol findings §14, observed on the bench analyzer):
- the step (30182): 4 or 7 while a channel is selected, 5 or 8 while the gas settles, 6 or 9 while it runs, 10 on the error display, then 0. The screen (30181) stays on measurement throughout. On any other screen 30182 numbers the menu's pages instead, 4-10 among them (protocol findings §15), so it is read as a step only on the measurement screen;
- the per-channel zero and span flags (30050-30059), from the wait step to the end;
- 30186 (
display.calibration_result, undocumented): 0 once a channel is selected, 4 while it runs, 6 when it has finished, kept until the next; - 30190 (
display.key, undocumented): the key being pressed.
What a zero or span touches (:func:plan_manual_calibration, ZPA manual
p.43-45, p.75-77). A zero of a channel set to "at once" zeroes every channel
so set; a span, or a zero of a channel set to "each", calibrates that channel
only. A channel set to "both" is calibrated on both its ranges; otherwise on
the range it measures on or, when its range method is auto, on its
auto-calibration range.
How an event is decided (:class:ManualCalibrationTracker). A pass
starts when the step leaves 0 and ends when it is 0 again, or when a read
shows a screen other than measurement. A poll reads the
readings and the zero and span flags before the step, so when the first read
back on measurement still shows the pass's flags, the readings after it are
taken from the next read.
- It ran if a read showed it running or on the error display, or if the result register read 0 on the wait step and 6 at the end: a calibration takes a second or two, which can fall between two reads.
- It failed if the error display was shown, or an error 4-8 appeared on its channels.
- It was cancelled if it never reached the wait step, or if it did and the result register still reads 0 at the end.
- Anything else is ambiguous: the reads do not say whether it ran.
The result register is undocumented, so it decides nothing that the step contradicts (design §13.1 #73). Every event carries the evidence for its outcome.
Records. :func:calibration_record writes an event as a
fujilib-calibration/1 document, the same for a calibration watched at the
panel and one driven from the host (design §13.1 #75, #90).
ManualCalibrationEvent
dataclass
¶
ManualCalibrationEvent(
kind,
outcome,
channels,
ranges,
started_at,
selected_at,
ran_after,
ran_before,
ended_at,
before,
after,
adc_before,
new_errors,
evidence,
observations,
gases=(lambda: MappingProxyType({}))(),
)
A manual zero or span made at the front panel, and how it ended (design §8).
adc_before
instance-attribute
¶
The raw A/D values read with before: the detectors' counts when it ran.
calibrated_at
property
¶
When it ran, at the latest; None when it did not run or may not have.
channels
instance-attribute
¶
The channels calibrated: those whose zero or span flag was set, else the cursor's.
deviations
property
¶
Each channel's reading before it ran, less its calibration gas.
Empty unless it ran; only channels whose reading and gas are both known; in the reading's own unit, which a calibration gas shares (design §13.1 #62).
ended_at
instance-attribute
¶
The first read that showed no step: back on the measurement screen, or on a menu.
evidence
instance-attribute
¶
Why the outcome is what it is, and anything odd the reads showed.
gases
class-attribute
instance-attribute
¶
The calibration gas of each channel's range, in its unit, when it was read.
new_errors
instance-attribute
¶
Errors 4-9 on its channels at the end that were not active before it.
ran_after
instance-attribute
¶
It ran after this read (the last on the wait step), when it ran.
ran_before
instance-attribute
¶
It ran before this read (the first that showed it running or done), when it ran.
selected_at
instance-attribute
¶
The first read on the wait step: the channel was selected and the gas supplied.
ManualCalibrationKind ¶
Bases: StrEnum
A manual zero or a manual span.
ManualCalibrationOutcome ¶
Bases: StrEnum
How a manual calibration ended, as far as the reads can tell.
ManualCalibrationPlan
dataclass
¶
ManualCalibrationTracker ¶
Turns successive panel observations into manual calibration events.
Feed it every observation in order, from polls, status reads or recorded frames; the closer together they are, the less is left ambiguous. It does no I/O.
Start with no pass under way.
Source code in src/fujilib/devices/panel.py
feed ¶
Take in observation; return the event of a pass it ends, if it ends one.
Source code in src/fujilib/devices/panel.py
PanelObservation
dataclass
¶
PanelObservation(
at,
display,
channels,
calibration_error,
readings=(lambda: MappingProxyType({}))(),
adc=None,
)
One read of the panel and calibration status, with the host time it was read.
readings
class-attribute
instance-attribute
¶
The readings of the same poll, when it was a poll.
step
property
¶
The manual-calibration step shown; NONE on any screen but measurement.
A manual calibration keeps the measurement screen. In the menus 30182 numbers the pages instead, 4-10 among them (protocol findings §15), so it is no step there.
errors ¶
from_frame
classmethod
¶
The observation in a poll's frame; None for a poll without the status block.
Source code in src/fujilib/devices/panel.py
from_status
classmethod
¶
The observation in a calibration status; None when it has no display state.
Source code in src/fujilib/devices/panel.py
calibration_record ¶
event as a fujilib-calibration/1 document of JSON values.
source says who pressed the keys: "panel" for a calibration made at
the front panel and watched, "remote" for one driven from the host.
calibrated_at is when it ran, at the latest, or None when it did
not run; it is what a zero or span time is taken from. channel_gases
gives each channel's gas label, as asserted.
Source code in src/fujilib/devices/panel.py
calibration_record_header ¶
The part of a calibration record that names the document and the analyzer.
Source code in src/fujilib/devices/panel.py
plan_manual_calibration ¶
What a manual kind of channel would calibrate (see the module docstring).
settings holds the :data:MANUAL_PLAN_SETTINGS.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
FujiDecodeError
|
a range setting reads a value that is not a range. |
Source code in src/fujilib/devices/panel.py
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 | |
fujilib.devices.keys ¶
Front-panel keys over Modbus, and a manual zero or span driven with them (design §6.5).
A manual zero or span exists only as front-panel keys: ZERO or SPAN, the cursor to the channel, ENT to select it (the wait step, while the gas settles) and ENT again to calibrate. 42001 presses a key as the panel would (protocol findings §18). This module is the only one that writes it.
Which keys, and where. Only UP, DOWN, ESC, ENT, ZERO and SPAN, never MODE
or SIDE, which open the menus and enter their passwords; the write envelope
checks the value written as well as the address (design §5.4). Each key only
on the steps where it belongs (:func:key_refusal):
| Step | Keys |
|---|---|
| measurement, no calibration flag set | ZERO, SPAN |
| channel selection | UP, DOWN, ESC; ENT with the cursor on the planned channel |
| wait | ESC; ENT, which calibrates, only through :meth:RemoteCalibration.calibrate |
| running | none |
| error display | ESC; never ENT, which forces the calibration on errors 5 and 7 |
| any other screen | none |
One key is one locked operation: read the panel and refuse unless the step allows the key; write it once, never retried; read until the step, the cursor and the flags show it taken. 30190 cannot confirm a key written over Modbus: it shows only keys pressed at the panel (findings §18.1). A key the panel does not take (key lock, the backlight, a key pressed at the panel meanwhile) stops the run.
The cursor wraps round, and channels zeroed "at once" share a position that reads as its first channel reached going down (findings §18.2). So the cursor is moved with DOWN only, one key at a time, each confirmed by the cursor moving, until it reads the planned channel, or the first of the "at once" channels. ZERO may open it anywhere, so it is read, never predicted.
A remote calibration (:class:RemoteCalibration, from
:meth:Analyzer.manual_calibration <fujilib.devices.analyzer.Analyzer.manual_calibration>)
is an async context manager. Entering it refuses (nothing sent) unless the
panel is on the measurement screen with no calibration flag set, key lock
and output hold are off (design §13.1 #85, #86), there is no instrument
error, the plan reads the same again and calibrates no channel on both
ranges (#88), and the operator's gas for every channel is its calibration-gas
setting (#89). It then presses ZERO or SPAN, moves the cursor and selects the
channel. Inside, :meth:~RemoteCalibration.wait_steady watches the readings
until the steadiness rule holds (design §13.1 #78), and
:meth:~RemoteCalibration.calibrate checks it all again and sends the ENT
that calibrates, which is DANGEROUS. The other keys are STATEFUL.
Cleanup. However the block is left, the panel is returned to measurement for the step it is on, shielded from cancellation and within its own time:
| Step | Cleanup |
|---|---|
| channel selection | ESC |
| wait | ESC, which clears the flags; never 42002, which leaves them set (findings §18.4) |
| running | wait for the analyzer to finish |
| error display | ESC |
| any other screen | 42002 |
Each cleanup key is sent once. A flag still set on the measurement screen
afterwards raises :class:~fujilib.errors.FujiAnalyzerStateError naming the
channel and the recovery: enter its wait step at the panel and press ESC.
fujilib does not try that itself (design §13.1 #84).
The run is recorded as the watcher records one made at the panel: the
:class:~fujilib.devices.panel.ManualCalibrationTracker sees every read, and
:class:RemoteCalibrationResult adds the plan, the gases named, the
steadiness, the readings on the wait step, the keys and the cleanup.
CalibrationGas
dataclass
¶
The gas the operator says is at the inlet, for one channel (design §13.1 #89).
value is in unit, which must be the channel's unit on the range
calibrated; for a zero gas of 0 the unit may be left out. A unit given as
text is kept as a :class:~fujilib.registry.units.Unit. label is
kept in the record, e.g. "N2, cylinder 1234" or "20.95 % O2 in N2".
__post_init__ ¶
Refuse a value that is not a finite number.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
the value is not a finite number, or a gas other than 0 has no unit. |
Source code in src/fujilib/devices/keys.py
CleanupReport
dataclass
¶
How the panel was returned to measurement.
clean
instance-attribute
¶
The measurement screen, no step and no calibration flag at the end.
error
class-attribute
instance-attribute
¶
Why the panel could not be read or keyed, when it could not.
flags_left
class-attribute
instance-attribute
¶
Channels whose calibration flag was still set at the end.
KeyPress
dataclass
¶
One key written to 42001, and what the reads after it showed.
RemoteCalibration ¶
RemoteCalibration(
session,
plan,
*,
gas,
confirm,
rule=None,
adc=False,
interval=0.5,
key_timeout=2.0,
run_timeout=30.0,
cleanup_timeout=30.0,
)
A manual zero or span driven from the host (see the module docstring).
Made by :meth:Analyzer.manual_calibration
<fujilib.devices.analyzer.Analyzer.manual_calibration>; use it as an async
context manager, once.
Prepare the run; nothing is sent until the block is entered.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
a gas is missing or malformed, or a time is not a positive number of seconds. |
Source code in src/fujilib/devices/keys.py
__aenter__
async
¶
Check everything, then press ZERO or SPAN, move the cursor and select the channel.
Raises:
| Type | Description |
|---|---|
FujiConfirmationRequiredError
|
|
FujiAnalyzerStateError
|
the panel, a setting or the plan forbids it, or a key was not taken; the panel was then returned to measurement. |
FujiCapabilityError
|
A/D values were asked for and the analyzer has none. |
FujiError
|
a read or a key failed; the panel was then returned to measurement. |
Source code in src/fujilib/devices/keys.py
__aexit__
async
¶
Return the panel to measurement for the step it is on (see the module docstring).
Raises:
| Type | Description |
|---|---|
FujiAnalyzerStateError
|
a calibration flag is still set on the measurement screen, or the panel could not be returned to it. |
Source code in src/fujilib/devices/keys.py
calibrate
async
¶
Send the ENT that calibrates, and follow the calibration to its end. DANGEROUS.
Everything is checked again first, in the same operation as the key: the panel on the wait step with the plan's flags, the gas still steady on every channel with this read, the plan, key lock, output hold and the instrument errors. The calibration is then followed until the panel is back on measurement; on the error display it sends ESC, never ENT.
Raises:
| Type | Description |
|---|---|
FujiConfirmationRequiredError
|
|
FujiAnalyzerStateError
|
not on the wait step, not steady, or a check failed; nothing was sent. Or the key was not taken. |
FujiError
|
a read failed. |
Source code in src/fujilib/devices/keys.py
1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 | |
cancel
async
¶
Leave the wait step with ESC, which clears the flags; the pass's event.
Raises:
| Type | Description |
|---|---|
FujiAnalyzerStateError
|
the run is not on the wait step, or ESC was not taken. |
Source code in src/fujilib/devices/keys.py
read
async
¶
Read the panel once on the wait step, and judge the gas.
Raises:
| Type | Description |
|---|---|
FujiAnalyzerStateError
|
the run is not on the wait step, or the panel has left it. |
FujiError
|
the read failed. |
Source code in src/fujilib/devices/keys.py
wait_steady
async
¶
Read every interval until the gas is steady on every channel; the verdict.
The port is free between reads. timeout defaults to the rule's
timeout_s. progress is called with each verdict, on the event
loop.
Raises:
| Type | Description |
|---|---|
FujiTimeoutError
|
not steady within |
FujiAnalyzerStateError
|
the run is not on the wait step, or the panel left it. |
FujiValidationError
|
|
FujiError
|
a read failed. |
Source code in src/fujilib/devices/keys.py
RemoteCalibrationResult
dataclass
¶
RemoteCalibrationResult(
plan,
gases,
rule,
event,
steadiness,
calibrating_key_sent,
keys,
cleanup,
samples,
started_at,
ended_at,
error=None,
)
A manual zero or span driven from the host, and how it ended.
calibrating_key_sent
instance-attribute
¶
Whether the ENT that calibrates was sent.
as_record ¶
The run as a fujilib-calibration/1 document (design §13.1 #75, #90).
Source code in src/fujilib/devices/keys.py
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 | |
RunState ¶
Bases: StrEnum
Where a remote calibration is.
WaitSample
dataclass
¶
One read on the wait step: what the steadiness rule saw.
calibration_filename ¶
A file name for a run's record: serial, channel, kind and start time.
Source code in src/fujilib/devices/keys.py
key_refusal ¶
Why key may not be sent on the panel observation shows; None if it may.
cursor is where ENT on channel selection needs the cursor. ENT on a
wait step starts the calibration, so it is allowed only with
calibrate. ENT on the error display is never allowed.
Source code in src/fujilib/devices/keys.py
fujilib.devices.steadiness ¶
Whether a calibration gas has settled at the inlet: the steadiness rule (design §13.1 #78).
A manual zero or span is right only if it is computed from the gas it names,
after the reading has stopped moving. The operator switches the valves and
names the gas; the analyzer's own guard is coarse (error 8: more than 100
counts of change within 60 s during the calibration, service manual p.33).
So before the key that calibrates, fujilib watches each channel's reading on
the wait step and calls it steady when, over a window of the last
:meth:SteadinessRule.window_for seconds:
- it has moved by no more than :attr:
SteadinessRule.band_percent_fsof its range's full scale (highest less lowest reading), and - its mean lies within :attr:
SteadinessRule.tolerance_percent_fsof the gas the operator named, which catches the wrong cylinder or a valve left shut; - and no two reads in it are more than :attr:
SteadinessRule.max_gap_sapart, so that a verdict never rests on a couple of reads either side of a pause.
The window is the longer of :attr:SteadinessRule.window_s and
:attr:SteadinessRule.response_factor times the channel's response time.
On the bench, O2 settled within 0.01 vol% about 36 s after the gas changed,
with the response time at 15 s (protocol findings §14.4).
Every channel a calibration touches must be steady at once. The readings are the Modbus concentrations, which are live only while output hold is off; a remote calibration is refused while it is on (design §13.1 #86).
:class:SteadinessJudge is pure: it takes each read's readings with a
monotonic time and gives a :class:SteadinessVerdict. It does no I/O.
ChannelSteadiness
dataclass
¶
ChannelSteadiness(
channel,
steady,
window_s,
covered_s,
samples,
gas,
unit,
last,
mean,
spread_percent_fs,
offset_percent_fs,
reason,
)
SteadinessJudge ¶
Applies a :class:SteadinessRule to successive reads (see the module docstring).
Watch targets under rule.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
no channel to watch, or a full scale that is not positive. |
Source code in src/fujilib/devices/steadiness.py
feed ¶
Take in one read at monotonic time at_s; the verdict with it.
A channel missing from readings counts as a reading that could not
be decoded.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
|
Source code in src/fujilib/devices/steadiness.py
reset ¶
SteadinessRule
dataclass
¶
SteadinessRule(
window_s=30.0,
response_factor=2.0,
band_percent_fs=0.5,
tolerance_percent_fs=10.0,
timeout_s=600.0,
max_gap_s=5.0,
)
When a channel's reading counts as steady on the named gas (design §13.1 #78).
The defaults are the design's starting values, to be tuned on the bench.
band_percent_fs
class-attribute
instance-attribute
¶
How far the reading may move over the window, in % of full scale.
max_gap_s
class-attribute
instance-attribute
¶
The longest time between two reads in the window, in seconds.
response_factor
class-attribute
instance-attribute
¶
The window is at least this many times the channel's response time.
timeout_s
class-attribute
instance-attribute
¶
How long to wait for the gas to settle before giving up, in seconds.
tolerance_percent_fs
class-attribute
instance-attribute
¶
How far the window's mean may lie from the named gas, in % of full scale.
__post_init__ ¶
Refuse a rule that could never, or always, be met.
Raises:
| Type | Description |
|---|---|
FujiValidationError
|
a value is not a positive finite number, or the response factor is negative. |
Source code in src/fujilib/devices/steadiness.py
window_for ¶
The window for a channel with response_time_s (None: not known).
Source code in src/fujilib/devices/steadiness.py
SteadinessTarget
dataclass
¶
SteadinessVerdict
dataclass
¶
What the rule says after a read, for every channel watched.