Skip to content

Latest commit

 

History

History
444 lines (346 loc) · 19.3 KB

File metadata and controls

444 lines (346 loc) · 19.3 KB

AGENTS.md

Authoritative guidance for AI coding agents working in this repository. Humans should also read this — it captures real invariants and footguns that aren't obvious from Cargo.toml alone.

If you are GitHub Copilot, you may have been pointed here from .github/copilot-instructions.md — the information you need is on this page.


What this crate is

tmp108 is a #![no_std] platform-agnostic driver for the Texas Instruments TMP108 I²C temperature sensor, built on embedded-hal 1.0 and embedded-hal-async 1.0. The register layout is generated from a TOML device description (tmp108.toml) by the device-driver crate.

Public driver types:

  • Tmp108<I2C> — the blocking driver, built on [embedded_hal::i2c::I2c]. Always available.
  • AsyncTmp108<I2C> — the async driver, built on [embedded_hal_async::i2c::I2c]. Gated by the async cargo feature. Unlocks AsyncTmp108::continuous, which has no blocking equivalent (see README's "Gotchas").
  • AlertTmp108<I2C, ALERT> — wraps AsyncTmp108 with an embedded-hal / embedded-hal-async GPIO pin so it can implement embedded-sensors-hal-async::TemperatureThresholdWait. Async-only. Gated by cfg(feature = "embedded-sensors-hal-async"). Access the inner sensor via sensor() / sensor_mut() / into_inner().

Since 0.7.0 both Tmp108 and AsyncTmp108 are available simultaneously when both features are enabled. The shared sync/async-agnostic register codec lives in a private mod ops; the two driver types are thin shells that perform I²C and delegate the meaningful work to ops::*.


Required reading before you change anything

  1. The TMP108 datasheet — at minimum the register-map and ALERT-pin sections. The driver's behavior is faithful to the chip's, not invented.
  2. docs/superpowers/specs/2026-06-03-tmp108-agent-usage-docs-design.md if you're touching docs/examples — it captures the doctest + snippet-marker invariants.
  3. docs/superpowers/specs/2026-06-03-tmp108-reliability-fixes-design.md for the rationale behind the current Error<E, P> shape, the set_*_limit rejection contract, the continuous() cleanup contract, and the hysteresis snapping band.
  4. docs/superpowers/specs/2026-06-04-tmp108-type-split-design.md for the rationale behind the Tmp108 / AsyncTmp108 split and the mod ops shared codec.
  5. CONTRIBUTING.md for the commit-message convention (Conventional Commits v1.0.0) and the "each commit builds clean" rule.

Repository layout

.
├── Cargo.toml              # MSRV 1.90; edition 2024; deny-list lints
├── tmp108.toml             # device-driver register description
├── CHANGELOG.md            # Keep-a-Changelog v1.1.0 format
├── src/
│   ├── lib.rs              # Tmp108 + AsyncTmp108 + AlertTmp108 +
│   │                       # Config + Error + private mod ops + traits
│   └── inner.rs            # GENERATED by device-driver from tmp108.toml — do not hand-edit
├── examples/               # All five examples run on Pico de Gallo (see below)
│   ├── oneshot.rs              # blocking OR async, depending on features
│   ├── continuous.rs           # async-only (AsyncTmp108)
│   ├── alert_interrupt.rs      # async + embedded-sensors-hal-async
│   ├── alert_comparator.rs     # async + embedded-sensors-hal-async
│   └── sensor_trait.rs         # blocking OR async, depending on features
├── tests/
│   └── reexports.rs        # compile-only: pins Tmp108 + AsyncTmp108
├── scripts/
│   └── check-readme-snippets.sh    # CI gate: README in sync with examples/
├── supply-chain/           # cargo-vet audits
├── .github/
│   ├── copilot-instructions.md # short stub → this file
│   └── workflows/          # fmt, clippy, test, doc, hack, deny, semver, vet, no-std
└── docs/superpowers/       # specs + plans for non-trivial work

Feature-flag matrix (memorize this)

Feature Effect Implies
(none) blocking Tmp108 over embedded-hal
async async AsyncTmp108 over embedded-hal-async; unlocks AsyncTmp108::continuous
embedded-sensors-hal blocking TemperatureSensor trait impl on Tmp108
embedded-sensors-hal-async async TemperatureSensor, TemperatureThresholdSet, TemperatureHysteresis on AsyncTmp108; AlertTmp108 wrapper async (forced in Cargo.toml since 0.7.0)

Things that follow from the matrix and trip up new contributors:

  • AsyncTmp108 only exists under cfg(feature = "async"). The blocking Tmp108 is always available.
  • AlertTmp108 and everything that takes an ALERT pin only exist under cfg(feature = "embedded-sensors-hal-async") (which forces async).
  • AsyncTmp108::continuous only exists under cfg(feature = "async"). There is no blocking equivalent. The README's "Gotchas" section documents the workaround.
  • cargo hack --feature-powerset check --locked enumerates every legal combination (6 since 0.7.0, down from 8 because embedded-sensors-hal-async now implies async). All combos must stay green. Use it before pushing.

Building, testing, linting — the canonical local matrix

Run all of these before declaring a change complete. CI runs the same things; failing locally now is faster than failing in CI later.

# format (nightly! see Gotcha #1 below)
cargo +nightly fmt --check

# pedantic clippy across every feature combination
cargo clippy --all-features --all-targets -- \
    -W clippy::suspicious -W clippy::correctness \
    -W clippy::perf -W clippy::style

# docs (catches broken intra-doc links and bad code-block attributes)
cargo doc --no-deps --all-features --locked

# unit tests (mock-based, no hardware needed)
cargo test --locked
cargo test --locked -F async
cargo test --locked -F async,embedded-sensors-hal-async

# doctests (the `# Examples` blocks on every public method)
cargo test --doc --locked
cargo test --doc --locked -F async
cargo test --doc --locked -F async,embedded-sensors-hal-async

# examples build in every supported feature combination
cargo build --examples --locked
cargo build --examples --locked -F async
cargo build --examples --locked -F embedded-sensors-hal
cargo build --examples --locked -F async,embedded-sensors-hal-async

# feature-powerset
cargo hack --feature-powerset check --locked

# README snippets match the examples they were lifted from
./scripts/check-readme-snippets.sh

# supply-chain (every new dependency needs an audit or trust entry)
cargo vet --locked

# semver (catches accidental breaking changes)
cargo semver-checks check-release

Live-hardware verification (Pico de Gallo + TMP108 — see "Hardware setup" below) is optional for most edits but mandatory for any change that touches alert-pin behavior:

cargo run --example oneshot
cargo run --example oneshot -F async
cargo run --example continuous -F async
cargo run --example sensor_trait -F embedded-sensors-hal
cargo run --example sensor_trait -F async,embedded-sensors-hal-async
cargo run --example alert_interrupt -F async,embedded-sensors-hal-async   # needs human warming
cargo run --example alert_comparator -F async,embedded-sensors-hal-async  # needs human warm + cool

Gotchas — read these before editing

1. cargo fmt must be run on nightly

rustfmt.toml sets imports_granularity = Module and group_imports = StdExternalCrate. Both are unstable. Stable rustfmt silently ignores them with a Warning: can't set … line; nightly enforces them. CI runs nightly fmt. If you only run stable fmt locally, you can push broken code that fails CI.

rustup toolchain install nightly --component rustfmt   # one-time
cargo +nightly fmt --check                             # before every push

2. src/inner.rs is generated — do not hand-edit

src/inner.rs is regenerated from tmp108.toml by the device-driver crate at build time (actually committed for now, but it's machine output). Modify tmp108.toml for register-layout changes; everything else flows from there.

3. Two parallel driver types share a private mod ops

Since 0.7.0 the driver is implemented as two named types:

  • pub struct Tmp108<I2C: embedded_hal::i2c::I2c> — always available.
  • pub struct AsyncTmp108<I2C: embedded_hal_async::i2c::I2c>#[cfg(feature = "async")].

Both delegate all chip-specific logic (Celsius↔raw conversion, limit-range validation, hysteresis snapping, Config ↔ register-byte codec) to a private mod ops of pure functions. The two driver shells are otherwise mirrors of each other; when adding a new method on one, add the matching one on the other and route the shared logic through ops::*.

Doctests are independent per type: Tmp108 doctests use the plain blocking shape, AsyncTmp108 doctests wrap their body in tokio::runtime::Runtime::new().unwrap().block_on(async { ... }). Don't try to share doctest bodies — the macro shim from earlier versions is gone, and the architect's review (see docs/superpowers/specs/2026-06-04-tmp108-type-split-design.md) explicitly chose source-duplication-with-shared-codec over maybe-async-cfg precisely because of how cleanly this separates.

4. AsyncTmp108::continuous requires async |t| { ... }

The signature is F: AsyncFnOnce(&mut Self) -> Result<(), I2C::Error> — Rust's native async-closure syntax (async |t| { ... }, stable since 1.85). The naive |t| async { ... } shape does not satisfy the bound and produces opaque "lifetime may not live long enough" or "implementation of FnOnce is not general enough" errors. If you're debugging such an error inside a tmp.continuous(...) call, the fix is the closure syntax, not your call site.

The doctest on continuous and examples/continuous.rs are both correct templates. Copy from them, not from the old README.

5. Config field-type re-exports are public surface

tmp108::{Mode, Thermostat, ConversionRate, Hysteresis, Polarity} are pub use'd at the crate root. Without them, downstream consumers cannot construct a non-default Config. There is a compile-only test in tests/reexports.rs that pins all five names plus Tmp108 and (under -F async) AsyncTmp108 — removing or renaming any of them breaks that test and is a breaking change.

6. Each commit must build clean and pass clippy/fmt independently

Per CONTRIBUTING.md ("Each commit builds successfully without warning"). When fixing up a commit series, use git rebase -i with edit to amend the originating commit, not a "style: cargo fmt" follow-up commit. Squash typo/format-only fixes back into the source commit.

7. README snippets are not hand-written

The three usage snippets in README.md are extracted by scripts/check-readme-snippets.sh from marker regions in examples/oneshot.rs, examples/continuous.rs, and examples/alert_interrupt.rs. To update a snippet:

  1. Edit the example file's source between the markers.
  2. Copy the new body into the README's matching <!-- snippet: NAME --> block (the script tells you what to copy if there's drift).

The fences in README.md are deliberately rust,ignore so rustdoc doesn't try to compile them — they lack imports and are display-only. Don't change the fence syntax without also updating the script.

8. New dependencies need cargo-vet entries

This crate uses cargo-vet for supply-chain review. Adding any new dependency (including dev-deps and transitive ones that aren't already covered) will fail the CI vet-dependencies job. Either:

  • Add an [[audits.<crate>]] entry to supply-chain/audits.toml matching the existing style (who, criteria, version or delta, notes with "Assisted-by:" if AI-generated), OR
  • Add a [[trusted.<crate>]] entry if the publisher already has a trust window with this project, OR
  • Use cargo vet interactively (cargo vet certify <crate> <version>).

Most dev-deps use criteria = "safe-to-run". Use criteria = "safe-to-deploy" only for things that end up in the released binary or library (build-script dependencies for proc macros called from src/, etc.).

9. Semver: bumping the crate version is part of the change

If you change a public signature, add or remove a pub item, or modify a trait bound on a pub fn, cargo semver-checks check-release will fail CI until you bump the version in Cargo.toml. Bump in the same PR, not later. Major bumps need a BREAKING CHANGE: footer in the commit message (per Conventional Commits).

10. The device-driver crate is a runtime dependency

device-driver is a [dependencies] entry (not [build-dependencies]) because the generated code lives in src/inner.rs and uses runtime types from device-driver. Don't move it.


Hardware setup (for running examples)

All five examples run on a Pico de Gallo USB-attached host adapter wired to a TMP108:

Signal Wire Notes
SDA Pico de Gallo default I²C SDA
SCL Pico de Gallo default I²C SCL
A0 GND Yields I²C address 0x48
V+ 3.3 V (or 5 V, datasheet range)
GND GND
ALERT Pico de Gallo GPIO0 External 2 kΩ pull-up to V+; used only by alert examples

Other valid A0 strappings: V+0x49, SDA0x4a, SCL0x4b. The driver has a new_with_a0_* constructor for each.

The Pico de Gallo enumerates as USB VID 0x045e / PID 0x067d. On Linux you need write access to the USB device node — typically via a udev rule that grants the plugdev group (or your user) access, or a posix ACL.

If cargo run --example oneshot hangs without printing, the most common causes are:

  1. Wrong pico-de-gallo-hal version. Use 0.5.0 or newer. Older releases hang on USB I/O against current firmware.
  2. The board isn't enumerated. Check lsusb | grep 045e:067d.
  3. You don't have permission to the USB node. Check ls -la /dev/bus/usb/*/$(lsusb | awk '/045e:067d/ {print $4}' | tr -d :).
  4. Another process has the board open. Check lsof on the USB node.

Commit-message convention

Conventional Commits v1.0.0. See CONTRIBUTING.md for the full rules. Short version:

<type>[optional scope]: <description>

[optional body, wrapped at 72 cols]

[optional footers including BREAKING CHANGE: and trailers]

Assisted-by: <agent>:<model> [tool1] [tool2]    # if AI-assisted

Allowed <type> values: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.

AI-attribution trailer — mandatory for AI-generated commits

Every commit that includes AI-generated or AI-assisted work must end with an Assisted-by trailer:

Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]
  • AGENT_NAME — the framework or product (opencode, github-copilot, claude-code, etc.).
  • MODEL_VERSION — the specific model identifier the agent reports for itself (claude-opus-4.7-1m-internal, claude-opus-4.6, gpt-5, etc.). Verify your own identity — do not hard-code a model name from a previous session.
  • Optional analysis tools in brackets (coccinelle, clang-tidy, etc.). Basic dev tools (git, cargo, editors) should not be listed.

AI agents MUST NOT add a Signed-off-by: trailer. Only humans certify the Developer Certificate of Origin.


Workflow expectations for agents

  1. Plan before touching code. For any non-trivial change, write a short design (the superpowers brainstorming skill workflow is what this repo's prior agent-driven work followed). Save specs under docs/superpowers/specs/ and plans under docs/superpowers/plans/.
  2. TDD where it applies. Write the failing test first, confirm it fails for the right reason, then implement.
  3. Run the full local matrix before pushing. Listed above. Pay special attention to nightly fmt and cargo-vet — they're the most common CI surprises.
  4. One concern per commit. Don't mix a feature change with a formatting cleanup. If you discover formatting issues in an already-committed commit, amend that commit via git rebase -i edit, don't tack on a "style: fmt" follow-up.
  5. Bisectable history. Each commit should compile, pass tests at its level of feature coverage, and pass clippy.
  6. PRs start as drafts. Per CONTRIBUTING.md — make sure all CI workflows pass on the draft before requesting reviewers.
  7. Bump the version when public API changes. Don't leave it for a follow-up PR; cargo-semver-checks will fail and block merge.

Where to put new things

You're adding… Put it here
A new public method that should exist on both driver flavors Add it twice: once on Tmp108 (blocking impl), once on AsyncTmp108 (async impl). Put any sync/async-agnostic helper logic in mod ops; each method body should be a thin shell that performs I²C and calls into ops::*. Both methods need their own # Examples doctest in the right shape (blocking vs tokio::block_on(async {…})).
A new public method on AlertTmp108 src/lib.rs, in the AlertTmp108 impl block, with a tokio::block_on-wrapped doctest. Use self.sensor() / self.sensor_mut() to reach the inner AsyncTmp108; do not access the private tmp108 field.
A new register field tmp108.toml, regenerate src/inner.rs if needed
A new example examples/<name>.rs, with //! block stating hardware + features + register interactions; add to the CI matrix in .github/workflows/check.yml
A new doctest pattern If it's reusable, document it in this file under "Gotchas"
A new test (unit) src/lib.rs mod tests (pure-function tests go in mod tests::ops_tests; blocking-driver tests go in mod tests::blocking; async-driver tests go in mod tests::asynchronous)
A new test (integration) tests/<topic>.rs
A new dev-dep Cargo.toml [dev-dependencies] + a [[audits.<crate>]] entry
A README usage snippet Marker region in an example file + matching <!-- snippet: NAME --> block in README.md; register in scripts/check-readme-snippets.sh
A design document docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md
An implementation plan docs/superpowers/plans/YYYY-MM-DD-<topic>.md
A user-visible change An entry in CHANGELOG.md under the next-unreleased version's appropriate section

When in doubt

Read the source. src/lib.rs is ~2,850 lines (most of it # Examples doctests and an mod tests that shows every register interaction the driver performs at the wire level). The doctests show how each public method is meant to be used. The examples show end-to-end workflows on real hardware.

If you're stuck and the answer isn't in the source, ask the human maintainer. Don't guess at chip behavior — the datasheet is authoritative and free.