diff --git a/CHANGELOG.md b/CHANGELOG.md index b404860..38c03e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project are documented in this file. The format is based on Keep a Changelog and this project uses Semantic Versioning. +## [0.1.10] - 2026-03-07 + +### Changed +- Added automatic BLE reconnect retry with linear backoff for transient timeout errors (including `br-connection-timeout`) before returning a failure. + ## [0.1.9] - 2026-03-07 ### Added diff --git a/fichero/printer.py b/fichero/printer.py index 2f6644f..0773645 100644 --- a/fichero/printer.py +++ b/fichero/printer.py @@ -53,6 +53,8 @@ DELAY_CHUNK_GAP = 0.02 # inter-chunk pacing for BLE throughput DELAY_RASTER_SETTLE = 0.50 # wait for printhead after raster transfer DELAY_AFTER_FEED = 0.30 # wait after form feed before stop command DELAY_NOTIFY_EXTRA = 0.05 # extra wait for trailing BLE notification fragments +BLE_CONNECT_RETRIES = 3 # retry transient BLE connect failures +BLE_CONNECT_BACKOFF = 0.7 # base backoff in seconds (linear: n * base) # --- Exceptions --- @@ -405,17 +407,38 @@ async def connect( raise PrinterError(f"Classic Bluetooth connection failed for '{address}'.") else: addr = address or await find_printer() - try: - async with BleakClient(addr) as client: - pc = PrinterClient(client) - await pc.start() - yield pc - except BleakDBusError as exc: - if "br-connection-not-supported" in str(exc).lower(): - raise PrinterError( - "BLE connection failed (br-connection-not-supported). " - "Try Classic Bluetooth with classic=true and channel=1." - ) from exc - raise PrinterError(f"BLE connection failed: {exc}") from exc - except BleakError as exc: - raise PrinterError(f"BLE error: {exc}") from exc + def _is_retryable_ble_error(exc: Exception) -> bool: + msg = str(exc).lower() + return any(token in msg for token in ("timeout", "timed out", "br-connection-timeout")) + + last_exc: Exception | None = None + for attempt in range(1, BLE_CONNECT_RETRIES + 1): + try: + async with BleakClient(addr) as client: + pc = PrinterClient(client) + await pc.start() + yield pc + return + except BleakDBusError as exc: + msg = str(exc).lower() + if "br-connection-not-supported" in msg: + raise PrinterError( + "BLE connection failed (br-connection-not-supported). " + "Try Classic Bluetooth with classic=true and channel=1." + ) from exc + last_exc = exc + if _is_retryable_ble_error(exc) and attempt < BLE_CONNECT_RETRIES: + await asyncio.sleep(BLE_CONNECT_BACKOFF * attempt) + continue + raise PrinterError(f"BLE connection failed: {exc}") from exc + except BleakError as exc: + last_exc = exc + if _is_retryable_ble_error(exc) and attempt < BLE_CONNECT_RETRIES: + await asyncio.sleep(BLE_CONNECT_BACKOFF * attempt) + continue + raise PrinterError(f"BLE error: {exc}") from exc + if last_exc is not None: + raise PrinterError( + f"BLE connection failed after {BLE_CONNECT_RETRIES} attempts: {last_exc}" + ) from last_exc + raise PrinterError("BLE connection failed for unknown reason.") diff --git a/fichero_printer/CHANGELOG.md b/fichero_printer/CHANGELOG.md index 8438071..39d2acb 100644 --- a/fichero_printer/CHANGELOG.md +++ b/fichero_printer/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.1.10 + +- Added automatic BLE reconnect retry with backoff for transient timeout errors (`br-connection-timeout`). + ## 0.1.9 - Added add-on local changelog file so Home Assistant can display release notes. @@ -29,4 +33,3 @@ ## 0.1.3 - Added ingress/webui metadata updates. - diff --git a/fichero_printer/config.yaml b/fichero_printer/config.yaml index b8e3879..f9774b2 100644 --- a/fichero_printer/config.yaml +++ b/fichero_printer/config.yaml @@ -1,5 +1,5 @@ name: "Fichero Printer" -version: "0.1.9" +version: "0.1.10" slug: "fichero_printer" description: "REST API for the Fichero D11s (AiYin) thermal label printer over Bluetooth" url: "https://git.leuschner.dev/Tobias/Fichero" diff --git a/fichero_printer/fichero/printer.py b/fichero_printer/fichero/printer.py index 2f6644f..0773645 100644 --- a/fichero_printer/fichero/printer.py +++ b/fichero_printer/fichero/printer.py @@ -53,6 +53,8 @@ DELAY_CHUNK_GAP = 0.02 # inter-chunk pacing for BLE throughput DELAY_RASTER_SETTLE = 0.50 # wait for printhead after raster transfer DELAY_AFTER_FEED = 0.30 # wait after form feed before stop command DELAY_NOTIFY_EXTRA = 0.05 # extra wait for trailing BLE notification fragments +BLE_CONNECT_RETRIES = 3 # retry transient BLE connect failures +BLE_CONNECT_BACKOFF = 0.7 # base backoff in seconds (linear: n * base) # --- Exceptions --- @@ -405,17 +407,38 @@ async def connect( raise PrinterError(f"Classic Bluetooth connection failed for '{address}'.") else: addr = address or await find_printer() - try: - async with BleakClient(addr) as client: - pc = PrinterClient(client) - await pc.start() - yield pc - except BleakDBusError as exc: - if "br-connection-not-supported" in str(exc).lower(): - raise PrinterError( - "BLE connection failed (br-connection-not-supported). " - "Try Classic Bluetooth with classic=true and channel=1." - ) from exc - raise PrinterError(f"BLE connection failed: {exc}") from exc - except BleakError as exc: - raise PrinterError(f"BLE error: {exc}") from exc + def _is_retryable_ble_error(exc: Exception) -> bool: + msg = str(exc).lower() + return any(token in msg for token in ("timeout", "timed out", "br-connection-timeout")) + + last_exc: Exception | None = None + for attempt in range(1, BLE_CONNECT_RETRIES + 1): + try: + async with BleakClient(addr) as client: + pc = PrinterClient(client) + await pc.start() + yield pc + return + except BleakDBusError as exc: + msg = str(exc).lower() + if "br-connection-not-supported" in msg: + raise PrinterError( + "BLE connection failed (br-connection-not-supported). " + "Try Classic Bluetooth with classic=true and channel=1." + ) from exc + last_exc = exc + if _is_retryable_ble_error(exc) and attempt < BLE_CONNECT_RETRIES: + await asyncio.sleep(BLE_CONNECT_BACKOFF * attempt) + continue + raise PrinterError(f"BLE connection failed: {exc}") from exc + except BleakError as exc: + last_exc = exc + if _is_retryable_ble_error(exc) and attempt < BLE_CONNECT_RETRIES: + await asyncio.sleep(BLE_CONNECT_BACKOFF * attempt) + continue + raise PrinterError(f"BLE error: {exc}") from exc + if last_exc is not None: + raise PrinterError( + f"BLE connection failed after {BLE_CONNECT_RETRIES} attempts: {last_exc}" + ) from last_exc + raise PrinterError("BLE connection failed for unknown reason.") diff --git a/pyproject.toml b/pyproject.toml index bf4ef25..7ae7e1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fichero-printer" -version = "0.1.9" +version = "0.1.10" description = "Fichero D11s thermal label printer - BLE CLI tool" requires-python = ">=3.10" dependencies = [