Skip to content
Merged
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
17 changes: 15 additions & 2 deletions src/opendisplay/ota.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@

from .exceptions import OTAError


def _increment_mac(address: str) -> str:
"""Return the BLE MAC address incremented by 1, carrying across octets.

The nRF DFU bootloader advertises at ``original + 1``. Incrementing only the
last octet (``+1 & 0xFF``) fails to carry when it is 0xFF (e.g. ...:FF -> ...:00
instead of rolling into the previous octet), causing a silent 30 s scan miss.
"""
parts = address.upper().split(":")
mac_int = int("".join(parts), 16)
mac_int = (mac_int + 1) & 0xFFFFFFFFFFFF
return ":".join(f"{(mac_int >> (8 * (5 - i))) & 0xFF:02X}" for i in range(6))


if TYPE_CHECKING:
from bleak.backends.device import BLEDevice

Expand Down Expand Up @@ -180,8 +194,7 @@ async def find_nrf_dfu_device(original_address: str) -> BLEDevice | None:
"""
from bleak import BleakScanner

parts = original_address.upper().split(":")
mac_plus1 = ":".join(parts[:-1] + [f"{(int(parts[-1], 16) + 1) & 0xFF:02X}"])
mac_plus1 = _increment_mac(original_address)

for attempt in range(15): # 2 s × 15 = 30 s
await asyncio.sleep(2.0)
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_ota.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ async def test_find_nrf_dfu_device_not_found_returns_none() -> None:

@pytest.mark.asyncio
async def test_find_nrf_dfu_device_mac_plus1_wraps_ff() -> None:
"""MAC+1 wraps correctly: EE:FF → EE:00."""
dfu_dev = _make_scanner_device("AA:BB:CC:DD:EE:00")
"""MAC+1 carries across octets: EE:FF → EF:00 (not EE:00)."""
dfu_dev = _make_scanner_device("AA:BB:CC:DD:EF:00")

with (
patch("opendisplay.ota.asyncio.sleep", new=AsyncMock()),
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/test_ota_mac_increment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Test nRF DFU MAC increment carries across octets (§4)."""

from __future__ import annotations

import pytest

from opendisplay.ota import _increment_mac


@pytest.mark.parametrize(
("addr", "expected"),
[
("AA:BB:CC:DD:EE:01", "AA:BB:CC:DD:EE:02"),
("AA:BB:CC:DD:EE:FF", "AA:BB:CC:DD:EF:00"), # carry into previous octet
("AA:BB:CC:DD:FF:FF", "AA:BB:CC:DE:00:00"), # double carry
("FF:FF:FF:FF:FF:FF", "00:00:00:00:00:00"), # wraps around
],
)
def test_increment_mac_carries(addr: str, expected: str) -> None:
assert _increment_mac(addr) == expected