Metadata-Version: 2.5
Name: fujilib
Version: 0.1.0
Summary: Async Python driver for Fuji Electric ZP-series NDIR gas analyzers (Modbus RTU).
Project-URL: Homepage, https://github.com/GraysonBellamy/fujilib
Project-URL: Repository, https://github.com/GraysonBellamy/fujilib
Project-URL: Documentation, https://fujilib.graysonbellamy.dev/
Project-URL: Issues, https://github.com/GraysonBellamy/fujilib/issues
Project-URL: Changelog, https://github.com/GraysonBellamy/fujilib/blob/main/CHANGELOG.md
Author-email: Grayson Bellamy <gbellamy@umd.edu>
License-Expression: MIT
License-File: LICENSE
Keywords: fuji,fuji electric,gas analyzer,instrument,modbus,ndir,rs-485,serial,zpa
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AnyIO
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: System :: Hardware
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: anyio>=4.14
Requires-Dist: anymodbus<0.4,>=0.3
Requires-Dist: anyserial<0.3,>=0.2.0
Provides-Extra: docs
Requires-Dist: mkdocstrings-python>=2.0.4; extra == 'docs'
Requires-Dist: zensical>=0.0.45; extra == 'docs'
Provides-Extra: parquet
Requires-Dist: pyarrow>=22; extra == 'parquet'
Description-Content-Type: text/markdown

# 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`](https://pypi.org/project/anyserial/) and
> [`anymodbus`](https://pypi.org/project/anymodbus/).

`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.1.0 is the first 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`](docs/design.md)
for the architecture and the phased plan, and [`CHANGELOG.md`](CHANGELOG.md)
for what has landed.

```python
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]`):

```bash
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.

## 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+.

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

From source:

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

## Development

```bash
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`](CONTRIBUTING.md).

## License

MIT. See [`LICENSE`](LICENSE).
