Skip to content

Add read_second_capacitance + write_capacitance / write_second_capacitance#2

Merged
bonev-st merged 1 commit into
mainfrom
feat/write-capacitance
May 19, 2026
Merged

Add read_second_capacitance + write_capacitance / write_second_capacitance#2
bonev-st merged 1 commit into
mainfrom
feat/write-capacitance

Conversation

@bonev-st
Copy link
Copy Markdown
Contributor

Summary

The firmware exposes two tank-capacitor pairs as adjacent (value + exponent) holding registers, but the client only had a read accessor for the first pair and nothing on the write side. This rounds out the convenience API:

  • read_second_capacitance() -> float — mirror of read_capacitance against HOLD_REG_SECOND_CAP_VAL / HOLD_REG_SECOND_CAP_EXP (0x3012 / 0x3013).
  • write_capacitance(value: float) -> None — Farads in, atomic 2-register write out.
  • write_second_capacitance(value: float) -> None — same against the second pair.

Both writes encode the Farads value into a (val, exp) pair that maximises uint16 precision (mantissa lands in [6554, 65535] whenever possible) and emit one FC 0x10 (Write Multiple Registers) PDU. Reads of the second pair use FC 0x03 with count=2, same atomicity guarantee as the first pair.

Internal refactor: extracted _read_cap_pair / _write_cap_pair so the four public methods share one implementation, plus a module-level _encode_capacitance helper that's independently testable. read_capacitance keeps its existing semantics — the body just moves into _read_cap_pair.

Validation: negative, nan, inf raise InvalidValueError; exponent outside [-30, 6] raises (matches the read-side bound); bool inputs are explicitly rejected (int-subclass guard) so write_capacitance(True) can't sneak through as 1.0 F.

Test plan

  • CI: tests workflow passes on Python 3.10, 3.11, and 3.12
  • CI: ruff + mypy stay clean
  • CI: pytest --cov=smartpower_modbus reports 214 passing (179 pre-existing + 35 new in tests/test_capacitance.py)
  • Sanity (no hardware): python -c "from smartpower_modbus.client import _encode_capacitance; print(_encode_capacitance(100e-6))"(10000, -8)
  • Sanity (no hardware): the parametrised round-trip test exercises 1 pF → 1 F and stays within rel=1e-4
  • Hardware-loop check (optional): write a known capacitance to a real device and read it back through read_capacitance / read_second_capacitance

Local verification (already run)

  • pytest -q: 214 passed (179 pre-existing + 35 new)
  • ruff check .: clean
  • mypy smartpower_modbus: clean

🤖 Generated with Claude Code

…tance

The firmware exposes two tank-capacitor pairs as adjacent (value + exponent)
holding registers; the client only had a read accessor for the first pair
and nothing on the write side. Round out the API:

- read_second_capacitance() -> float (Farads, mirrors read_capacitance
  against HOLD_REG_SECOND_CAP_VAL / HOLD_REG_SECOND_CAP_EXP at 0x3012/13)
- write_capacitance(value: float) -> None
- write_second_capacitance(value: float) -> None

Both writes encode the Farads value into a (val, exp) pair that maximises
uint16 precision (mantissa lands in [6554, 65535] when possible) and emit
one atomic FC 0x10 (Write Multiple Registers) PDU. Reads of the second
pair use FC 0x03 with count=2 — same atomicity guarantee as the first
pair.

Internal refactor: extract _read_cap_pair and _write_cap_pair so the four
public methods share one implementation, plus a module-level
_encode_capacitance helper that's independently testable. read_capacitance
keeps its existing semantics; the body just moves into _read_cap_pair.

Validation:
- Negative, nan, inf inputs raise InvalidValueError.
- Exponent outside [-30, 6] raises (matches read_capacitance's bound,
  catches uninitialised firmware memory in either direction).
- bool inputs are explicitly rejected (int subclass guard) so
  write_capacitance(True) can't sneak through as 1.0 F.

Tests: 35 new in tests/test_capacitance.py covering the encoder round-
trip across pF→F, the single-call atomicity of both pairs in both
directions, and every documented rejection path.

Verification: pytest 214 passed (179 pre-existing + 35 new), ruff clean,
mypy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1398e35f7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

assert exp_reg.addr == val_reg.addr + 1, (
f"{val_reg.name} and {exp_reg.name} must be at adjacent addresses"
)
val, exp = _encode_capacitance(float(value))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject bools before casting capacitance values

When the public write_capacitance(True) or write_second_capacitance(False) path is used, this float(value) cast converts the bool to 1.0/0.0 before _encode_capacitance() can run its explicit bool guard, so the new API silently writes a plausible capacitor setting instead of raising InvalidValueError. This contradicts the intended int-subclass protection and can misconfigure hardware when a caller accidentally passes a boolean.

Useful? React with 👍 / 👎.

@bonev-st bonev-st merged commit 5ea0b89 into main May 19, 2026
3 checks passed
@bonev-st bonev-st deleted the feat/write-capacitance branch May 19, 2026 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant