# atm90e32-async

[![CI](https://github.com/jethub-iot/atm90e32-async/actions/workflows/ci.yml/badge.svg)](https://github.com/jethub-iot/atm90e32-async/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/atm90e32-async.svg)](https://crates.io/crates/atm90e32-async)
[![Documentation](https://docs.rs/atm90e32-async/badge.svg)](https://docs.rs/atm90e32-async)
[![License: GPL-2.0-or-later OR Apache-2.0](https://img.shields.io/badge/license-GPL--2.0--or--later%20OR%20Apache--2.0-blue.svg)](#license)
[![MSRV](https://img.shields.io/badge/MSRV-1.75-red.svg)](#msrv)

Async `no_std` driver for the Microchip / Atmel **ATM90E32** 3-phase
SPI power metering IC.

Built on top of [`embedded-hal-async`](https://crates.io/crates/embedded-hal-async),
with no hard dependency on any specific async runtime — works with
Embassy, RTIC2, or any executor that provides `SpiDevice` and `DelayNs`.

## Architecture

The crate is split into a **sans-I/O core** and an **async transport** layer:

* `proto` — pure functions: byte-frame building, response parsing,
  raw → engineering-unit conversions, and the post-reset init sequence
  materialised as data (`[InitStep; 22]`). 100% host-testable.
* `driver` — the `Atm90e32<SPI, D>` struct: wraps SPI reads/writes,
  drives the init sequence, issues delays through a generic `DelayNs`.

The practical effect is that the tricky parts — wire format, 32-bit
power-register assembly, signed power-factor decoding, line-frequency
dependent MMode0 bit flip — are covered by 18 host unit tests and
don't need a mock SPI bus.

## Features

What the current version does:

* Chip presence probe via `SysStatus0`
* Full init sequence (soft reset, unlock, enable, calibration gains,
  startup thresholds, freq thresholds, PGA, MMode0/1, lock) driven by
  a single `Config` struct
* Bulk 3-phase readout: RMS voltage, RMS current, active power,
  reactive power, power factor, mains frequency, **phase angle**
* Per-phase reads for each of the above
* **Phase status** decoding: per-phase overcurrent, overvoltage,
  voltage sag, phase loss, frequency warnings — from `EMMState0`/`EMMState1`
* **Chip temperature** readout
* Raw register read/write escape hatch (`read_register` / `write_register`)
* Typed errors: `Error::Spi(E)`, `Error::NotPresent`,
  `Error::InitFailed(InitStage)` with a per-step breakdown
* Optional `defmt::Format` derives behind the `defmt` feature
* No heap, no global state, no hard runtime dependency

What is **not** yet implemented (PRs welcome):

* Energy accumulation (`EPosA`/`EPosT`/…)
* Apparent power (S) registers
* Harmonic analysis registers
* Zero-crossing interrupts
* Calibration assist helpers (auto-gain)
* ATM90E36 family support (planned — the code layout anticipates it)
* ATM90E26 (8-bit addressing, out of scope)

## Quick start

```rust,no_run
use atm90e32_async::{Atm90e32, Config, LineFreq, PgaGain};

async fn run<SPI, D>(spi: SPI, delay: D) -> Result<(), atm90e32_async::Error<SPI::Error>>
where
    SPI: embedded_hal_async::spi::SpiDevice,
    D:   embedded_hal_async::delay::DelayNs,
{
    let mut meter = Atm90e32::new(spi, delay);
    meter.probe().await?;

    let cfg = Config::default()
        .with_voltage_gain([39470, 39470, 39470])
        .with_current_gain([65327, 65327, 65327])
        .with_line_freq(LineFreq::Hz50)
        .with_pga_gain(PgaGain::X2);
    meter.init(&cfg).await?;

    let r = meter.read_all_phases().await?;
    // Raw values — no f32 conversion until you need it:
    // r.voltage[0] is hundredths of a volt (u16)
    // r.power[0] is signed combined register word (i32)

    // Convert on demand:
    use atm90e32_async::proto;
    let volts = proto::voltage_raw_to_volts(r.voltage[0]);

    let status = meter.read_status().await?;
    // status.overcurrent, status.overvoltage, status.voltage_sag, status.phase_loss
    // status.is_ok()

    let temp_raw = meter.read_chip_temperature().await?;
    let temp_c = proto::temperature_raw_to_celsius(temp_raw);
    Ok(())
}
```

For a real integration on an ESP32 + Embassy + esp-hal, see the sketch
in [`examples/basic.rs`](examples/basic.rs).

## Hardware requirements

* SPI mode 3 (CPOL=1, CPHA=1), MSB-first
* SPI clock ≤ 16 MHz (datasheet limit)
* One GPIO for chip select — exposed through the caller's
  `SpiDevice` implementation (e.g. `embedded-hal-bus::spi::ExclusiveDevice`)

The driver itself is transport-agnostic and works with any
`embedded-hal-async::spi::SpiDevice` implementation.

## Datasheet

[ATM90E32AS datasheet (Microchip)](https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-46002-SE-M90E32AS-Datasheet.pdf)

## MSRV

Minimum supported Rust version is **1.75**, required by
`embedded-hal-async` 1.0 (async-fn-in-traits). MSRV is part of the
semver contract and will only be bumped with a minor version release.

## License

Copyright (c) Viacheslav Bocharov \<v@baodeep.com\> and JetHome (r).

Dual-licensed under either of:

* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
  <http://www.apache.org/licenses/LICENSE-2.0>)
* GNU General Public License, Version 2.0 or later
  ([LICENSE-GPL](LICENSE-GPL) or
  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>)

at your option.

### Contribution

Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in this project by you shall be dual-licensed
as above, without any additional terms or conditions.
