Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
> **Read `AGENTS.md` first.** This file is kept minimal on purpose;
> `AGENTS.md` (at the repo root) is the canonical source of project
> conventions, build commands, and agent guidance for any AI coding
> assistant working on this repository.
321 changes: 321 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
# AGENTS.md — bq25723

Authoritative guidance for AI coding assistants (and humans) working on this
repository. Keep this file in sync with reality: when CI, layout, or
conventions change, update this file in the same commit.

## What this crate is

`bq25723` is a `#[no_std]`, platform‑agnostic Rust driver for the Texas
Instruments [BQ25723] buck‑boost battery charge controller (2–5 cell
batteries). The low‑level register interface is generated from
[`bq25723.yaml`](bq25723.yaml) by [`device-driver-cli`][device-driver-cli]
and committed as [`src/device.rs`](src/device.rs). A thin async wrapper in
[`src/lib.rs`](src/lib.rs) implements the
[`embedded-batteries-async`][embedded-batteries] `Charger` trait on top of
the `embedded-hal-async` I²C trait.

Key facts (verify before editing):

- Edition: `2024` (`Cargo.toml:12`).
- MSRV: **Rust 1.88** (`README.md:10`, `.github/workflows/check.yml:145`).
- I²C address: `0x6B` (`src/lib.rs:31`, constant `BQ_ADDR`).
- Largest register width: 2 bytes (`src/lib.rs:32`).
- Async only: the driver uses `embedded-hal-async` and exposes `*_async`
register accessors generated by `device-driver`.
- Optional `defmt-03` feature pulls in `defmt` and the matching features in
`device-driver` and `embedded-batteries-async` (`Cargo.toml:37-41`).
- License: MIT (`LICENSE`, `Cargo.toml:5`).

[BQ25723]: https://www.ti.com/lit/ds/symlink/bq25723.pdf
[device-driver-cli]: https://crates.io/crates/device-driver-cli
[embedded-batteries]: https://github.com/OpenDevicePartnership/embedded-batteries

## Repository layout

```
.
├── AGENTS.md # this file
├── Cargo.toml # package + lints + features
├── Cargo.lock # committed; CI uses --locked
├── README.md # short crate description, MSRV, license
├── CONTRIBUTING.md # commit / PR etiquette
├── CODE_OF_CONDUCT.md
├── CODEOWNERS
├── SECURITY.md
├── LICENSE # MIT
├── rust-toolchain.toml # pins rustfmt + clippy components only
├── rustfmt.toml # max_width=120 + nightly-only options
├── deny.toml # cargo-deny config (licenses/advisories/bans)
├── bq25723.yaml # device-driver manifest (single source of truth)
├── supply-chain/ # cargo-vet data
├── src/
│ ├── lib.rs # public API, interface, Bq25723, tests
│ └── device.rs # GENERATED from bq25723.yaml (do not hand-edit)
├── .github/
│ ├── workflows/
│ │ ├── check.yml # fmt, doc, hack-clippy, deny, test, msrv, machete
│ │ ├── nostd.yml # cargo check on thumbv8m.main-none-eabihf
│ │ ├── device-driver.yml # verifies device.rs matches regenerated output
│ │ ├── cargo-vet.yml
│ │ └── cargo-vet-pr-comment.yml
│ └── (no copilot-instructions.md as of writing)
└── .vscode/settings.json # rust-analyzer cfg (thumbv8m target, clippy on save)
```

There is no `examples/`, no `tests/` directory, and no `build.rs`. All unit
tests live in `src/lib.rs` under `mod tests`.

## Building and testing

The toolchain file (`rust-toolchain.toml`) does **not** pin a Rust version;
it only requests `rustfmt` + `clippy`. Use the host‑installed stable
toolchain, except where CI uses nightly (`doc`) or the MSRV (`check`).

All commands below are run from the repository root and are mirrored from
`.github/workflows/*.yml`. They are verified on Windows with stable Rust.

| Purpose | Command | CI job (`.github/workflows/check.yml`) |
|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------|
| Format check | `cargo fmt --check` | `fmt` |
| Build (host, locked) | `cargo check --locked` | `msrv` (with `+1.88`) |
| Build (no‑std target) | `cargo check --target thumbv8m.main-none-eabihf --locked` | `nostd.yml` |
| Tests across all feature combos | `cargo hack --feature-powerset test --locked` | `test` |
| Quick tests (host only) | `cargo test --locked` | (subset of `test`) |
| Clippy across feature combos & targets | `cargo hack --feature-powerset --target <T> clippy --locked -- -Dwarnings -D clippy::suspicious -D clippy::correctness -D clippy::perf -D clippy::style` | `hack-clippy` (targets: `x86_64-unknown-linux-gnu`, `thumbv8m.main-none-eabihf`) |
| Clippy on tests | `cargo hack --feature-powerset clippy --tests --locked -- -Dwarnings -D clippy::suspicious -D clippy::correctness -D clippy::perf -D clippy::style` | `test` |
| Docs (nightly recommended; stable works) | `RUSTDOCFLAGS=--cfg docsrs cargo doc --no-deps --all-features` | `doc` (nightly) |
| Dependency hygiene | `cargo deny --all-features --locked check` | `deny` |
| Unused dependencies | `cargo machete` | `machete` |
| Regenerate `src/device.rs` from manifest | `device-driver-cli --manifest bq25723.yaml --device-name Device -o src/device.rs && rustfmt --edition 2024 src/device.rs` | `device-driver.yml` (compares against committed) |

Tooling not in `cargo` itself:

- `cargo install cargo-hack` — required for feature‑powerset commands.
- `cargo install cargo-deny` — required for the `deny` job.
- `cargo install cargo-machete` — required for the `machete` job.
- `cargo install device-driver-cli` — required only when regenerating
`src/device.rs`.

Notes:

- `rustfmt.toml` enables nightly‑only options (`group_imports`,
`imports_granularity`); on stable, `cargo fmt --check` prints warnings
about them but still exits 0. Do not remove these options — running
`rustfmt` under nightly enforces them.
Comment on lines +102 to +105
- CI always passes `--locked`; do not commit changes that require
regenerating `Cargo.lock` without intending to update dependencies.
- The `device-driver.yml` workflow regenerates `src/device.rs` into a
temporary file and fails if it differs from the committed file. After
editing `bq25723.yaml`, regenerate and commit `src/device.rs` in the
same change.

## Code conventions

Enforced by `Cargo.toml` lints and `rustfmt.toml`:

- `unsafe_code = "deny"` and `missing_docs = "deny"` at crate level
(`Cargo.toml:24-26`). The single allowed pocket of `unsafe`/missing‑docs
is the generated `mod device`, which is gated with
`#[allow(unsafe_code)]` and `#![allow(missing_docs)]`
(`src/lib.rs:13`, `src/lib.rs:17-20`). Do not add `unsafe` elsewhere and
do not add `#![allow(missing_docs)]` to new modules.
Comment on lines +117 to +122
- Clippy: `correctness`, `suspicious`, `perf`, `style` are `forbid`;
`pedantic` is `deny` (`Cargo.toml:29-34`). `forbid` cannot be overridden
with `#[allow(...)]` — code must comply.
- Formatting: `max_width = 120`, `imports_granularity = "Module"`,
`group_imports = "StdExternalCrate"` (`rustfmt.toml`). Run `cargo fmt`
before committing; ideally on nightly to apply the import rules.
- Edition `2024` everywhere, including when regenerating `src/device.rs`
(`rustfmt --edition 2024`; see `.github/workflows/device-driver.yml:33`).
- Error type: wrap underlying I²C errors in `BQ25723Error::Bus(_)`
(`src/lib.rs:27-29`). Map the kind via `charger::Error` to
`charger::ErrorKind::CommError` (`src/lib.rs:79-85`).
- `defmt` support is feature‑gated: derive `defmt::Format` behind
`#[cfg_attr(feature = "defmt-03", derive(defmt::Format))]`
(see `src/lib.rs:24-29`). Never add an unconditional `defmt` dependency.
- Unit conversions use the helpers `ma_to_reg_value`, `reg_value_to_ma`,
`mv_to_reg_value`, `reg_value_to_mv` on `Bq25723`
(`src/lib.rs:97-115`). Use these rather than open‑coding the scaling
factors (64 mA/LSB for current, 8 mV/LSB for voltage).

## Driver / HAL specifics

- The I²C protocol is implemented in
`impl AsyncRegisterInterface for DeviceInterface<I2c>`
(`src/lib.rs:40-77`). Writes build a `[addr, byte0, byte1?]` buffer of
the exact register width — never write past `data.len()`. Reads use
`i2c.write_read`. Both map errors with `BQ25723Error::Bus`.
- The `Bq25723<I2c>` wrapper exposes the generated `device` field publicly
(`src/lib.rs:87-89`) so callers can reach register accessors directly,
in addition to the `Charger` trait implementation
(`src/lib.rs:121-143`).
- The `Charger` trait methods write a setpoint and then read it back,
returning the value actually programmed into the register (see
`charging_current`/`charging_voltage`). Preserve this contract — it is
what tests assert against in `tests::charging_current_trait_test`
(`src/lib.rs:188-206`).
- `src/device.rs` is **generated**. To change registers, fields, or
conversions, edit `bq25723.yaml` and regenerate (see Building section).
CI in `.github/workflows/device-driver.yml` will fail any PR where the
committed `src/device.rs` does not match the regenerated output.
- The crate targets bare‑metal `thumbv8m.main-none-eabihf` in CI. New code
must remain `no_std`‑compatible: only the `tests` module may use `std`
(the `#![cfg_attr(not(test), no_std)]` attribute, `src/lib.rs:12`).
- Tests use `embedded-hal-mock`'s `eh1::i2c::Mock` and the `tokio`
`current_thread` runtime via `#[tokio::test]` (`Cargo.toml:20-22`,
`src/lib.rs:145-207`). Drive every mock to `done()` to ensure all
expected transactions occurred.

## Commit and PR conventions

From `CONTRIBUTING.md` and observed history
(`git log --pretty=%s`: `Bump device-driver to 1.0.9 (#10)`,
`Sync CI workflows with embedded-rust-template (#8)`,
`Pregenerate manifest file (#7)`, `Update dependencies and CODEOWNERS (#6)`,
`add initial driver commit`):

- Subjects are short, imperative, capitalized, no trailing period; PR
numbers are appended in parentheses on merge.
- Squash‑merge is disabled. Maintain a clean, bisectable history: every
commit must build without warnings, and typo / formatting fix‑ups must
be squashed into the commit they belong to (`CONTRIBUTING.md:26-31`).
- Open PRs as **draft** first; only mark ready for review once all CI
checks are green on the draft (`CONTRIBUTING.md:22-24`).
- License of contributions: MIT, matching the repository
(`CONTRIBUTING.md:6-10`). Flag non‑MIT contributions explicitly in the
PR description.
- When reporting regressions, `git bisect` and include the first bad
commit (`CONTRIBUTING.md:33-35`).

## What not to do

- Do not hand‑edit `src/device.rs`. Edit `bq25723.yaml` and regenerate.
- Do not add `unsafe` code outside the generated module; the crate denies
`unsafe_code` globally.
- Do not add `#![allow(missing_docs)]` to new modules; document all public
items.
- Do not introduce blocking I²C or `std`‑only dependencies in the library
crate; everything must build for `thumbv8m.main-none-eabihf`.
- Do not pull in `defmt` unconditionally — gate it behind the `defmt-03`
feature exactly as existing items do.
- Do not loosen the clippy lint configuration in `Cargo.toml`.
- Do not force‑push to shared branches; do not enable squash merging on PRs.
- Do not commit changes that require `Cargo.lock` updates unless the
dependency bump is the intent of the change.

## How to find more context

- Datasheet: <https://www.ti.com/lit/ds/symlink/bq25723.pdf> — register
semantics and electrical behavior.
- `device-driver` crate docs:
<https://docs.rs/device-driver> — explains the macros and the
`AsyncRegisterInterface` trait implemented in `src/lib.rs`.
- `embedded-batteries-async`:
<https://github.com/OpenDevicePartnership/embedded-batteries> — defines
the `Charger` trait this crate implements.
- `embedded-hal-async`:
<https://docs.rs/embedded-hal-async> — defines the async I²C trait the
driver consumes.
- CI sources of truth: `.github/workflows/check.yml`, `nostd.yml`,
`device-driver.yml`. When in doubt about the “official” command to run,
read the workflow.

## Flags (state of this repo at AGENTS.md creation time)

- No `copilot-instructions.md` exists anywhere in the tree; this AGENTS.md
is the first authoritative agent‑guidance file. A pointer file is being
added under `.github/copilot-instructions.md` referring back to here.
Comment on lines +226 to +228
- No existing `AGENTS.md` was overwritten.
- `rustfmt.toml` references nightly‑only options; CI runs `cargo fmt
--check` on stable, which warns about (but does not fail on) those
options.

## Model selection & cost discipline

Premium models (Opus, GPT-5 family, "high"/"xhigh" reasoning variants)
cost an order of magnitude more than standard models (Sonnet, Haiku,
mini). Most steps in a typical task do not need premium reasoning,
and over-using premium models wastes credits without improving
outcomes. The rules below apply to *all* model selection: your own
session, sub-agents launched via the `task` tool, and parallel work
launched via `/fleet`.

### Default posture

- **Default to the cheapest model that can do the job.** Reach for a
premium model only when one of the escalation triggers below is hit.
- **Plan with premium, execute with cheap.** Spend at most one or two
premium turns on design / planning, then downshift to a cheaper
model for mechanical execution of the plan.
- **Never bump the model "just in case."** If you cannot articulate
*why* a cheaper model would fail, use the cheaper model.

### Escalation triggers (use a premium model)

Reach for a premium model when *any* of these are true:

- Cross-module refactor, architectural design, or API design from
scratch.
- Subtle correctness reasoning: concurrency, lifetimes, `unsafe`,
FFI ABI, cryptography, safety-critical control paths.
- Debugging a failure that survived one prior cheap-model attempt.
- Reviewing code on a safety-, security-, or money-critical path.
- The diff cannot be predicted in advance — i.e. there is genuine
creative or design work to do, not just typing.

### De-escalation triggers (use a cheap model)

Use the cheapest available model when *any* of these are true:

- Searching, reading, summarising files or docs.
- Single-file mechanical edits: rename, format, lint fix, dependency
bump, boilerplate, scaffolding from a known template.
- Generating tests for code that already works.
- Running builds, tests, linters, or other commands where the model
only needs to report success/failure.
- Routine commits, PR descriptions, changelog entries.
- The diff is essentially predictable before generation.

### Sub-agent routing (the `task` tool)

When delegating with the `task` tool, set `model:` explicitly. Do not
let sub-agents inherit a premium default for cheap work.

| Sub-agent type | Default model | Override to |
|-------------------|---------------------------|-------------------------------------------------|
| `explore` | cheap | keep cheap (`claude-haiku-4.5` or `gpt-5-mini`) |
| `task` (run cmd) | cheap | keep cheap |
| `research` | cheap for breadth | premium only for the final synthesis |
| `general-purpose` | match task | cheap for mechanical work; premium for design |
| `rubber-duck` | premium | keep premium — this is where reasoning pays off |
| `code-review` | premium on critical paths | cheap on cosmetic / mechanical diffs |

### `/fleet` (parallel sub-agents) rules

- Fleet mode multiplies cost by the fleet width. Apply the rules
above *per worker*, not in aggregate.
- Split a fleet job along complexity lines: route the cheap,
parallelisable workers (file edits, test runs, doc updates) to a
cheap model; reserve premium models for the small number of
workers that need real reasoning.
- If every worker in a fleet would need a premium model, the work is
probably not a good fit for fleet mode — reconsider the
decomposition before paying N× premium.

### Session hygiene

- Keep sessions short and focused. Long premium sessions are the
single largest source of waste because every turn re-processes the
full history.
- Use `/compact` when the conversation grows long, and `/new` for
unrelated work.
- Prefer `/ask` for one-off side questions so they don't extend the
main session.

### When in doubt

Ask: *"If a cheaper model produced the wrong answer here, would I
catch it in seconds (compiler, tests, my own review) or in
weeks (production incident)?"* If the former, use the cheap model
and let the feedback loop do its job.
Loading