Skip to main content

fujilib

Async-first Python driver for Fuji Electric ZP-series NDIR gas analyzers (ZPA, and the ZPB / ZPG / ZPAJ / ZPG3E models that share its MODBUS map), built on anyserial and anymodbus.

The ZPA is also sold as the CAI ZPA by California Analytical Instruments (CAI), now part of ENVEA. fujilib is developed and tested against a CAI-branded ZPA. See Also sold as.

fujilib is a member of the *lib instrument-driver family (alicatlib, sartoriuslib, watlowlib, servomexlib, …). It shares their entry point, frozen models, error hierarchy, streaming / sinks / sync / CLI conventions and the unified device-library API; its internals are shaped to this analyzer.

Status

Alpha. 0.2.0 is the current release. The read-only API works against the development analyzer: open_device(), identification, polls with validity, metadata, settings, logs, discovery, recording at a fixed rate to memory, CSV or Parquet, a blocking facade, and the fuji-read, fuji-discover, fuji-configure, fuji-decode, fuji-stream, fuji-capture and fuji-diag commands. A reviewed subset of settings writes, settings documents, return to measurement, and manual zeros and spans watched at the panel or driven from the host (fuji-calibrate) work on the development analyzer too; auto calibration and auto zero have run only on the simulated analyzer, since the development analyzer has no calibration valves. The documentation is at https://fujilib.graysonbellamy.dev/. See docs/design.md for the architecture and the phased plan, and CHANGELOG.md for what has landed.

import anyio

from fujilib import open_device


async def main() -> None:
    async with await open_device(
        "COM8", channel_map={"CH1": "co2", "CH2": "co", "CH3": "o2"}
    ) as anz:
        frame = await anz.poll()  # every channel and the analyzer status, 2 transactions
        o2 = frame.channel("CH3")
        print(o2.value, o2.unit, o2.state)  # 20.2 vol% ok


anyio.run(main)

Record to a file from the command line (Parquet needs fujilib[parquet]):

fuji-capture COM8 --gas CH1=co2 --gas CH2=co --gas CH3=o2 --out run.parquet

The first release, 0.1.0, covers:

  • monitoring, metadata and acquisition. poll() returns every channel with its hold, calibration and error state in two Modbus transactions, and the library streams and records to memory, CSV or Parquet;
  • a reviewed subset of settings writes and the documented operation commands (auto calibration, auto zero, blowback, return to measurement), behind confirm=True, pre-I/O validation and read-back verification;
  • a record of every manual zero and span made at the front panel while it is connected, since the analyzer keeps no calibration log on older firmware;
  • a manual zero or span driven from the host (fuji-calibrate): fujilib presses the calibration keys while the operator switches the gas valves, waits for the reading to settle on the gas named, and records the run.

0.2.0 adds what an application built on fujilib needs: the builders of fujilib.testing.frames for its own tests and simulator, and the state settling for readings taken in the 90 s after a reconnect, when the analyzer may have been switched off. It also takes a response time of 0 s, which switches the analyzer's filter off.

Also sold as

ZP-series analyzers are sold under other names too:

Sold as By Status
ZPA NDIR/O2 Multichannel Analyzer (CAI ZPA) California Analytical Instruments (CAI), now part of ENVEA (CAI ENVEA Group) The development analyzer is a CAI-branded ZPA.
IR202 Infrared Gas Analyzer Yokogawa Untested. Its MODBUS manual (IM 11G02Q02-51EN) documents the same register map and the same 38400 8-N-1 link.

fujilib is an independent project. It is not affiliated with or endorsed by Fuji Electric, California Analytical Instruments, ENVEA or Yokogawa; their names and product names belong to their owners.

Design points

  • Validity and provenance travel with every value. Hold, calibration and error state, and where each channel's gas label came from, are carried into every reading and row. Unknown validity stays unknown.
  • Gas labels that feed a calculation are asserted by the caller. The analyzer's type code is treated as a hint, not an authority.
  • Writes are hard to get wrong. Everything fujilib may ever write is a frozen envelope of documented user settings and operation commands. Factory parameters are out of scope, and the only front-panel keys fujilib presses are the six of a manual zero or span, never the keys into the menus.
  • No hardware needed to develop or test. A simulated analyzer runs the full stack in CI.

Oxygen measurements

Over Modbus the analyzer reports O2 with the display's resolution: 0.01 vol% (100 ppm) per step on the development unit. Modbus O2 is not validated for oxygen-consumption calorimetry. See design §2.11.

Installation

Requires Python 3.13+.

pip install fujilib
pip install "fujilib[parquet]"  # with the Parquet sink

From source:

git clone https://github.com/GraysonBellamy/fujilib
cd fujilib
uv sync --all-extras --dev

Development

uv run pre-commit install
uv run ruff format --check .
uv run ruff check .
uv run mypy
uv run pyright
uv run pytest

See CONTRIBUTING.md.

License

MIT. See LICENSE.

Metadata

Release files for fujilib 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fujilib 0.2.0
File Size Uploaded
fujilib-0.2.0.tar.gz 576.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fujilib 0.2.0
File Interpreter ABI Platform
fujilib-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 871.1 kB

Release files / fujilib-0.2.0.tar.gz

Download URL fujilib-0.2.0.tar.gz
Size 576.1 kB
Tags Source
SHA-256 checksum
How to use checksums
55652caf6bbbd1aa001de2d40e62622e536c4d4e651f4b96b5759a01f4dbda1e
BLAKE2b-256 checksum
How to use checksums
579052168fd07fecd44ee869646dbf15927d19645e45d01da39cb2bb190c074a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / fujilib-0.2.0-py3-none-any.whl

Download URL fujilib-0.2.0-py3-none-any.whl
Size 295.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
659709891efd2d9505a485dcd58b46538486cada5bd20aa1883edfff774291a1
BLAKE2b-256 checksum
How to use checksums
6fb154de3374daebbbe4282e3dcb9be9f2d5b02dfc7d9b2a8c9771d760eaa36f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page