Skip to content

Add I2C bus frequency readback property#278

Open
voldemort9999 wants to merge 2 commits intofossasia:mainfrom
voldemort9999:fix/i2c-baudrate-readback
Open

Add I2C bus frequency readback property#278
voldemort9999 wants to merge 2 commits intofossasia:mainfrom
voldemort9999:fix/i2c-baudrate-readback

Conversation

@voldemort9999
Copy link

@voldemort9999 voldemort9999 commented Mar 1, 2026

Summary

While going through the codebase I noticed that I2CMaster.configure() sends the frequency to the hardware but doesn't keep track of what was set. This makes it impossible for sensor drivers to verify whether the bus speed is within their supported range — for example, the MLX90614 only supports up to 100 kHz SMBus, but there's currently no way to check this programmatically.

This PR adds a frequency property to I2CMaster that reflects the actual configured bus speed.

Closes #195

Changes

  • Added _frequency attribute to _I2CPrimitive.__init__ (initialized as None)
  • _configure() now stores the actual hardware frequency after a successful configuration. The stored value is computed from the rounded brgval register value (via _get_i2c_frequency), so it reflects what the hardware is actually running at rather than what was requested
  • Added a read-only frequency property on I2CMaster

Usage

from pslab.bus.i2c import I2CMaster

bus = I2CMaster()
print(bus.frequency)      # default ~125 kHz

bus.configure(4e5)
print(bus.frequency)      # actual hardware frequency after rounding

Tests

Added tests/test_i2c_frequency.py with 18 unit tests covering default frequency, reconfigurations, brgval rounding, read-only enforcement, boundary conditions, and error handling. All tests use unittest.mock and run without hardware.

============================= 18 passed in 0.05s =============================

Summary by Sourcery

Add an I2C bus frequency readback on I2CMaster so code can query the actual configured bus speed.

New Features:

  • Expose a read-only frequency property on I2CMaster that reports the current I2C bus speed in Hz.

Enhancements:

  • Track the effective hardware I2C frequency inside the primitive I2C implementation after successful configuration so it can be reused by higher-level components.

Tests:

  • Add comprehensive unit tests for I2C frequency tracking, including defaults, reconfiguration behavior, error cases, boundary conditions, and hardware interaction with a mocked device.

Store the actual I2C bus frequency after configuration so that
sensors can check if the configured baudrate is compatible.

Fixes fossasia#195
@sourcery-ai
Copy link

sourcery-ai bot commented Mar 1, 2026

Reviewer's Guide

Adds a stored I2C bus frequency to the core I2C primitive and exposes it as a read-only property on I2CMaster, ensuring the value reflects the actual hardware frequency derived from the configured BRG register, with comprehensive unit tests validating behavior and boundaries.

Sequence diagram for I2CMaster configuration and frequency readback

sequenceDiagram
    actor Driver
    participant I2CMaster
    participant _I2CPrimitive
    participant ConnectionHandler as ConnectionHandler

    Driver->>I2CMaster: __init__()
    I2CMaster->>_I2CPrimitive: __init__(device)
    _I2CPrimitive->>ConnectionHandler: autoconnect() (if device is None)
    _I2CPrimitive-->>I2CMaster: _device, _running=False, _mode=None, _frequency=None
    I2CMaster->>_I2CPrimitive: _init()
    _I2CPrimitive->>ConnectionHandler: send_byte(CP.I2C_HEADER)
    I2CMaster->>I2CMaster: configure(125e3)
    I2CMaster->>_I2CPrimitive: _configure(125e3)
    _I2CPrimitive->>_I2CPrimitive: compute brgval from frequency
    _I2CPrimitive->>ConnectionHandler: send_byte(CP.I2C_CONFIG)
    _I2CPrimitive->>ConnectionHandler: send_int(brgval)
    _I2CPrimitive->>ConnectionHandler: get_ack()
    _I2CPrimitive->>_I2CPrimitive: _frequency = _get_i2c_frequency(brgval)

    Driver->>I2CMaster: configure(400e3)
    I2CMaster->>_I2CPrimitive: _configure(400e3)
    _I2CPrimitive->>_I2CPrimitive: compute brgval, update _frequency

    Driver->>I2CMaster: frequency
    I2CMaster-->>Driver: _frequency (actual hardware frequency)
Loading

Updated class diagram for I2C primitives and I2CMaster frequency property

classDiagram
    class ConnectionHandler

    class _I2CPrimitive {
        - ConnectionHandler _device
        - bool _running
        - any _mode
        - float _frequency
        + _I2CPrimitive(device ConnectionHandler)
        + _init() void
        + _configure(frequency float) void
        + _get_i2c_frequency(brgval int) float
    }

    class I2CMaster {
        + I2CMaster(device ConnectionHandler)
        + frequency float
        + configure(frequency float) void
    }

    _I2CPrimitive --> ConnectionHandler : uses
    I2CMaster --> _I2CPrimitive : composes/extends (implementation detail)
Loading

File-Level Changes

Change Details Files
Track the configured I2C bus frequency inside the low-level I2C primitive and update it on successful hardware configuration.
  • Introduce a _frequency instance attribute initialized to None in the I2C primitive constructor.
  • In the internal _configure method, after successfully sending configuration bytes and receiving ACK, compute the actual I2C frequency from the BRG value and store it in _frequency.
  • Ensure _frequency is only updated when configuration succeeds, leaving it unchanged when configure raises errors for out-of-range frequencies.
pslab/bus/i2c.py
Expose the stored I2C frequency as a read-only property on I2CMaster.
  • Add a frequency @Property on I2CMaster that returns the underlying _frequency value.
  • Rely on the existing I2CMaster.init call to configure(125e3) so that frequency is initialized to the default hardware frequency on construction.
  • Keep the property read-only by not defining a setter, so attempts to assign to frequency raise AttributeError.
pslab/bus/i2c.py
Add unit tests to verify I2C frequency tracking behavior, edge cases, and hardware interaction without requiring real hardware.
  • Create pytest fixtures that provide a MagicMock-based ConnectionHandler and use patching to inject it via autoconnect for I2CMaster, I2CSlave, and _I2CPrimitive.
  • Test initialization behavior, ensuring _frequency is None on bare primitives/slaves and set to a float on I2CMaster after its default configure call.
  • Validate that the stored frequency matches the BRG-based computation, not the requested frequency, across multiple configurations and boundary values.
  • Verify that frequency updates on valid reconfigurations, remains unchanged when configure raises ValueError for out-of-range frequencies, and is always a positive float.
  • Assert that the frequency property is read-only, and that _configure sends the correct protocol bytes and BRG value to the mocked hardware interface.
tests/test_i2c_frequency.py

Assessment against linked issues

Issue Objective Addressed Explanation
#195 Have I2CMaster store the configured I2C bus speed (baudrate) on the instance so that sensor drivers can read back the current bus speed and check compatibility.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The I2CMaster.frequency docstring advertises a float, but _frequency can be None before configure() runs in some code paths; consider either initializing _frequency to a sensible default or updating the documentation/typing to reflect the possible None value.
  • Since _frequency is managed in _I2CPrimitive, you might consider exposing a common frequency property on _I2CPrimitive rather than only on I2CMaster, so that consumers using I2CSlave (or other subclasses) can access the configured bus speed in a consistent way.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `I2CMaster.frequency` docstring advertises a `float`, but `_frequency` can be `None` before `configure()` runs in some code paths; consider either initializing `_frequency` to a sensible default or updating the documentation/typing to reflect the possible `None` value.
- Since `_frequency` is managed in `_I2CPrimitive`, you might consider exposing a common `frequency` property on `_I2CPrimitive` rather than only on `I2CMaster`, so that consumers using `I2CSlave` (or other subclasses) can access the configured bus speed in a consistent way.

## Individual Comments

### Comment 1
<location path="tests/test_i2c_frequency.py" line_range="218-226" />
<code_context>
+        master.configure(min_freq)
+        assert master.frequency == pytest.approx(min_freq)
+
+    def test_max_valid_frequency(self, master):
+        """Test with a high frequency whose brgval equals MIN_BRGVAL."""
+        # _get_i2c_frequency(MIN_BRGVAL) gives the theoretical max, but due
+        # to int() truncation in _get_i2c_brgval, that value may not
+        # round-trip. Instead, find a frequency that computes to MIN_BRGVAL.
+        max_brgval_freq = _I2CPrimitive._get_i2c_frequency(
+            _I2CPrimitive._MIN_BRGVAL + 1
+        )
+        master.configure(max_brgval_freq)
+        assert master.frequency is not None
+        assert master.frequency > 0
+
+    def test_just_below_min_frequency_raises(self, master):
</code_context>
<issue_to_address>
**suggestion (testing):** The max-frequency boundary test could assert a stronger condition than just `frequency > 0`.

In `test_max_valid_frequency`, you derive `max_brgval_freq` from `_MIN_BRGVAL + 1` but only check that `master.frequency` is positive. To better validate this boundary, consider also asserting that `_I2CPrimitive._get_i2c_brgval(max_brgval_freq)` returns the expected BRG value (e.g. `_MIN_BRGVAL + 1`), and that `master.frequency == pytest.approx(_I2CPrimitive._get_i2c_frequency(_I2CPrimitive._get_i2c_brgval(max_brgval_freq)))`, mirroring the minimum-frequency test and confirming the upper edge is handled correctly.

```suggestion
        # _get_i2c_frequency(MIN_BRGVAL) gives the theoretical max, but due
        # to int() truncation in _get_i2c_brgval, that value may not
        # round-trip. Instead, find a frequency that computes to MIN_BRGVAL.
        max_brgval_freq = _I2CPrimitive._get_i2c_frequency(
            _I2CPrimitive._MIN_BRGVAL + 1
        )

        # Configure with a frequency that should map to BRG = MIN_BRGVAL + 1
        master.configure(max_brgval_freq)

        # Verify that the BRG value computed from this frequency is as expected
        computed_brgval = _I2CPrimitive._get_i2c_brgval(max_brgval_freq)
        assert computed_brgval == _I2CPrimitive._MIN_BRGVAL + 1

        # And that the configured frequency matches the "round-tripped" frequency
        roundtrip_freq = _I2CPrimitive._get_i2c_frequency(computed_brgval)
        assert master.frequency == pytest.approx(roundtrip_freq)
```
</issue_to_address>

### Comment 2
<location path="tests/test_i2c_frequency.py" line_range="133-142" />
<code_context>
+# ============================================================
+# Test 5: frequency unchanged on invalid configure (ValueError)
+# ============================================================
+class TestFrequencyOnError:
+
+    def test_frequency_unchanged_on_invalid_frequency(self, master):
+        """If configure() raises ValueError, frequency should remain unchanged."""
+        master.configure(125e3)
+        original_freq = master.frequency
+
+        # Too low frequency should raise ValueError.
+        with pytest.raises(ValueError):
+            master.configure(1)  # Way too low
+
+        # Frequency should be unchanged.
+        assert master.frequency == original_freq
+
+    def test_frequency_unchanged_on_too_high_frequency(self, master):
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for how `_frequency` behaves if the underlying device NACKs the configuration.

Current tests only cover `ValueError` from invalid frequency values and verify `_frequency` is unchanged. There’s another failure mode where the device NACKs (e.g., `device.get_ack()` returns 0 or raises); in that case `_frequency` should also stay unchanged. Please add a test that uses a `mock_device` with a failing `get_ack` and asserts that `configure()` handles the failure and does not update `master.frequency`.

Suggested implementation:

```python
# Test 5: frequency unchanged on invalid configure (ValueError)
# ============================================================
class TestFrequencyOnError:

    def test_frequency_unchanged_on_invalid_frequency(self, master):
        """If configure() raises ValueError, frequency should remain unchanged."""
        master.configure(125e3)
        original_freq = master.frequency

        # Too low frequency should raise ValueError.
        with pytest.raises(ValueError):
            master.configure(1)  # Way too low

        # Frequency should be unchanged.
        assert master.frequency == original_freq

    def test_frequency_unchanged_on_too_high_frequency(self, master):
        """If configure() raises ValueError for too-high freq, frequency unchanged."""
        master.configure(125e3)
        original_freq = master.frequency

        # Too high frequency should raise ValueError.
        with pytest.raises(ValueError):
            master.configure(100e6)  # Way too high (100 MHz)

        assert master.frequency == original_freq
=======
# ============================================================
# Test 5: frequency unchanged on invalid configure (ValueError)
# ============================================================
class TestFrequencyOnError:

    def test_frequency_unchanged_on_invalid_frequency(self, master):
        """If configure() raises ValueError, frequency should remain unchanged."""
        master.configure(125e3)
        original_freq = master.frequency

        # Too low frequency should raise ValueError.
        with pytest.raises(ValueError):
            master.configure(1)  # Way too low

        # Frequency should be unchanged.
        assert master.frequency == original_freq

    def test_frequency_unchanged_on_too_high_frequency(self, master):
        """If configure() raises ValueError for too-high freq, frequency unchanged."""
        master.configure(125e3)
        original_freq = master.frequency

        # Too high frequency should raise ValueError.
        with pytest.raises(ValueError):
            master.configure(100e6)  # Way too high (100 MHz)

        assert master.frequency == original_freq

    def test_frequency_unchanged_on_device_nack(self, master, mock_device, monkeypatch):
        """
        If the underlying device NACKs during configure(), frequency should remain unchanged.

        This simulates a failure where device.get_ack() indicates a NACK (e.g., returns 0).
        """
        # Start from a known good configuration.
        master.configure(125e3)
        original_freq = master.frequency

        # Force the device to NACK any subsequent configuration attempt.
        monkeypatch.setattr(mock_device, "get_ack", lambda *args, **kwargs: 0)

        # configure() should handle the NACK as a failure and not update the frequency.
        with pytest.raises(Exception):
            master.configure(400e3)

        # Frequency must remain unchanged after the failed configure().
        assert master.frequency == original_freq

    def test_frequency_unchanged_when_get_ack_raises(self, master, mock_device, monkeypatch):
        """
        If device.get_ack() raises during configure(), frequency should remain unchanged.

        This covers the failure mode where the low-level I/O layer throws instead of
        returning a NACK code.
        """
        master.configure(125e3)
        original_freq = master.frequency

        def _failing_get_ack(*args, **kwargs):
            raise IOError("Simulated I2C NACK/IO failure")

        monkeypatch.setattr(mock_device, "get_ack", _failing_get_ack)

        with pytest.raises(IOError):
            master.configure(400e3)

        assert master.frequency == original_freq

```

If `configure()` currently does not surface NACKs as `Exception`/`IOError` in the ways assumed here (e.g., if it uses a custom exception type), adjust the `pytest.raises(...)` expectations to match the actual exception class(es) used in your driver. Also ensure the `mock_device` fixture is the same object used internally by `master`; if not, you may need to patch `master._device` (or equivalent) instead of `mock_device` directly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

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.

Add readback parameter for I2C baudrate

1 participant