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.
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 theasynccargo feature. UnlocksAsyncTmp108::continuous, which has no blocking equivalent (see README's "Gotchas").AlertTmp108<I2C, ALERT>— wrapsAsyncTmp108with anembedded-hal/embedded-hal-asyncGPIO pin so it can implementembedded-sensors-hal-async::TemperatureThresholdWait. Async-only. Gated bycfg(feature = "embedded-sensors-hal-async"). Access the inner sensor viasensor()/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::*.
- The TMP108 datasheet — at minimum the register-map and ALERT-pin sections. The driver's behavior is faithful to the chip's, not invented.
docs/superpowers/specs/2026-06-03-tmp108-agent-usage-docs-design.mdif you're touching docs/examples — it captures the doctest + snippet-marker invariants.docs/superpowers/specs/2026-06-03-tmp108-reliability-fixes-design.mdfor the rationale behind the currentError<E, P>shape, theset_*_limitrejection contract, thecontinuous()cleanup contract, and the hysteresis snapping band.docs/superpowers/specs/2026-06-04-tmp108-type-split-design.mdfor the rationale behind theTmp108/AsyncTmp108split and themod opsshared codec.CONTRIBUTING.mdfor the commit-message convention (Conventional Commits v1.0.0) and the "each commit builds clean" rule.
.
├── 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 | 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:
AsyncTmp108only exists undercfg(feature = "async"). The blockingTmp108is always available.AlertTmp108and everything that takes anALERTpin only exist undercfg(feature = "embedded-sensors-hal-async")(which forcesasync).AsyncTmp108::continuousonly exists undercfg(feature = "async"). There is no blocking equivalent. The README's "Gotchas" section documents the workaround.cargo hack --feature-powerset check --lockedenumerates every legal combination (6 since 0.7.0, down from 8 becauseembedded-sensors-hal-asyncnow impliesasync). All combos must stay green. Use it before pushing.
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-releaseLive-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 + coolrustfmt.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 pushsrc/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.
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.
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.
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.
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.
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:
- Edit the example file's source between the markers.
- 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.
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 tosupply-chain/audits.tomlmatching the existing style (who,criteria,versionordelta,noteswith "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 vetinteractively (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.).
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).
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.
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, SDA → 0x4a, SCL → 0x4b.
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:
- Wrong
pico-de-gallo-halversion. Use 0.5.0 or newer. Older releases hang on USB I/O against current firmware. - The board isn't enumerated. Check
lsusb | grep 045e:067d. - You don't have permission to the USB node. Check
ls -la /dev/bus/usb/*/$(lsusb | awk '/045e:067d/ {print $4}' | tr -d :). - Another process has the board open. Check
lsofon the USB node.
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.
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.
- Plan before touching code. For any non-trivial change, write a
short design (the
superpowers
brainstormingskill workflow is what this repo's prior agent-driven work followed). Save specs underdocs/superpowers/specs/and plans underdocs/superpowers/plans/. - TDD where it applies. Write the failing test first, confirm it fails for the right reason, then implement.
- 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.
- 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. - Bisectable history. Each commit should compile, pass tests at its level of feature coverage, and pass clippy.
- PRs start as drafts. Per
CONTRIBUTING.md— make sure all CI workflows pass on the draft before requesting reviewers. - Bump the version when public API changes. Don't leave it for a
follow-up PR;
cargo-semver-checkswill fail and block merge.
| 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 |
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.