From 8520a8819712400159410e2c05de1f90ac26a9f4 Mon Sep 17 00:00:00 2001 From: paul2212 Date: Mon, 16 Mar 2026 10:15:22 +0100 Subject: [PATCH] refactor: Externalize web UI to index.html Refactors the embedded web UI in the API server to be loaded from a separate index.html file instead of a large inline string. This improves maintainability by separating the presentation layer (HTML/CSS/JS) from the backend Python logic. --- CHANGELOG.md | 30 +++ fichero/api.py | 394 +++++++++-------------------------- fichero/index.html | 307 +++++++++++++++++++++++++++ fichero_printer/CHANGELOG.md | 16 ++ 4 files changed, 453 insertions(+), 294 deletions(-) create mode 100644 fichero/index.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 38336eb..3990f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ 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.20] - 2026-03-08 + +### Changed + +- Refactored the embedded web UI in the API server to be loaded from a separate `index.html` file instead of a large inline string, improving maintainability. + +## [0.1.19] - 2026-03-08 + +### Added + +- Added `POST /unpair` endpoint and "Unpair Device" button in the web UI to remove a Bluetooth device from the host's paired devices. + +## [0.1.18] - 2026-03-08 + +### Added + +- Added `POST /pair` endpoint and "Pair Device" button in the web UI to easily pair/trust the printer via `bluetoothctl` for Classic Bluetooth connections. + +## [0.1.17] - 2026-03-08 + +### Added + +- Added automatic fallback to BLE connection if Classic Bluetooth (RFCOMM) fails with `[Errno 12] Out of memory`, a common issue on Linux with stale device states. + +## [0.1.16] - 2026-03-08 + +### Fixed + +- Corrected typos in the Code128B bit pattern table for characters '$' (ASCII 36) and ')' (ASCII 41), which caused incorrect barcodes to be generated. + ## [0.1.15] - 2026-03-07 ### Fixed diff --git a/fichero/api.py b/fichero/api.py index 75aee00..93c27ab 100644 --- a/fichero/api.py +++ b/fichero/api.py @@ -5,6 +5,7 @@ Start with: or: python -m fichero.api + Endpoints: GET /status – Printer status GET /info – Printer info (model, firmware, battery, …) @@ -17,8 +18,10 @@ from __future__ import annotations import argparse import asyncio import io +import re import os from contextlib import asynccontextmanager +from pathlib import Path from typing import Annotated from fastapi import FastAPI, File, Form, HTTPException, UploadFile @@ -72,7 +75,7 @@ async def lifespan(app: FastAPI): # noqa: ARG001 app = FastAPI( title="Fichero Printer API", description="REST API for the Fichero D11s (AiYin) thermal label printer.", - version="0.1.13", + version="0.1.20", lifespan=lifespan, docs_url=None, redoc_url=None, @@ -94,301 +97,20 @@ def _address(address: str | None) -> str | None: def _ui_html() -> str: default_address = _DEFAULT_ADDRESS or "" default_transport = "classic" if _DEFAULT_CLASSIC else "ble" - return f""" - - - - - Fichero Printer - - - -
-
-

Fichero Printer

-

Home Assistant print console for status, text labels, and image uploads.

-

API docs remain available at /docs.

-
-
-
-

Connection

- - + try: + template_path = Path(__file__).parent / "index.html" + template = template_path.read_text(encoding="utf-8") + except FileNotFoundError: + return "

Error: index.html not found

" -
-
- - -
-
- - -
-
- -
- - -
-
- -
-

Output

-
Ready.
-
- -
-

Print Text

- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
- -
-
- -
-

Print Image

- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
- -
-
-
-
- - - -""" + # Simple substitution for initial values + return ( + template.replace("{default_address}", default_address) + .replace("{ble_selected}", " selected" if default_transport == "ble" else "") + .replace("{classic_selected}", " selected" if default_transport == "classic" else "") + .replace("{default_channel}", str(_DEFAULT_CHANNEL)) + ) # --------------------------------------------------------------------------- @@ -468,6 +190,90 @@ async def get_info( return info +@app.post( + "/pair", + summary="Pair and trust a Bluetooth device", + status_code=200, +) +async def pair_device( + address: Annotated[str | None, Form(description="Device address (optional, overrides FICHERO_ADDR)")] = None, +): + """ + Attempt to pair and trust the device using `bluetoothctl`. + This is intended for setting up Classic Bluetooth connections. + """ + addr = _address(address) + if not addr: + raise HTTPException(status_code=422, detail="Address is required to pair.") + + # Basic validation for MAC address format to mitigate injection risk. + if not re.match(r"^[0-9A-F]{2}(:[0-9A-F]{2}){5}$", addr, re.IGNORECASE): + raise HTTPException(status_code=422, detail=f"Invalid address format: {addr}") + + cmd = f'echo -e "pair {addr}\\ntrust {addr}\\nquit" | bluetoothctl' + + try: + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15.0) + except FileNotFoundError: + raise HTTPException(status_code=500, detail="`bluetoothctl` command not found. Is BlueZ installed and in PATH?") + except asyncio.TimeoutError: + raise HTTPException(status_code=504, detail="`bluetoothctl` command timed out after 15 seconds.") + + output = stdout.decode(errors="ignore") + error = stderr.decode(errors="ignore") + + if "Failed to pair" in output or "not available" in output.lower(): + raise HTTPException(status_code=502, detail=f"Pairing failed. Output: {output}. Error: {error}") + + return {"ok": True, "message": "Pair/trust command sent. Check output for details.", "output": output, "error": error} + + +@app.post( + "/unpair", + summary="Unpair a Bluetooth device", + status_code=200, +) +async def unpair_device( + address: Annotated[str | None, Form(description="Device address (optional, overrides FICHERO_ADDR)")] = None, +): + """ + Attempt to unpair the device using `bluetoothctl`. + """ + addr = _address(address) + if not addr: + raise HTTPException(status_code=422, detail="Address is required to unpair.") + + # Basic validation for MAC address format to mitigate injection risk. + if not re.match(r"^[0-9A-F]{2}(:[0-9A-F]{2}){5}$", addr, re.IGNORECASE): + raise HTTPException(status_code=422, detail=f"Invalid address format: {addr}") + + cmd = f'echo -e "remove {addr}\\nquit" | bluetoothctl' + + try: + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15.0) + except FileNotFoundError: + raise HTTPException(status_code=500, detail="`bluetoothctl` command not found. Is BlueZ installed and in PATH?") + except asyncio.TimeoutError: + raise HTTPException(status_code=504, detail="`bluetoothctl` command timed out after 15 seconds.") + + output = stdout.decode(errors="ignore") + error = stderr.decode(errors="ignore") + + if "Failed to remove" in output or "not available" in output.lower(): + raise HTTPException(status_code=502, detail=f"Unpairing failed. Output: {output}. Error: {error}") + + return {"ok": True, "message": "Unpair command sent. Check output for details.", "output": output, "error": error} + @app.post( "/print/text", summary="Print a text label", diff --git a/fichero/index.html b/fichero/index.html new file mode 100644 index 0000000..a0f927e --- /dev/null +++ b/fichero/index.html @@ -0,0 +1,307 @@ + + + + + + Fichero Printer + + + +
+
+

Fichero Printer

+

Home Assistant print console for status, text labels, and image uploads.

+

API docs remain available at /docs.

+
+ +
+
+

Connection

+ + + +
+
+ + +
+
+ + +
+
+ +
+ + + + +
+
+ +
+

Output

+
Ready.
+
+ +
+

Print Text

+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

Print Image

+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fichero_printer/CHANGELOG.md b/fichero_printer/CHANGELOG.md index 6dd5bad..9b9aa1a 100644 --- a/fichero_printer/CHANGELOG.md +++ b/fichero_printer/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.1.20 + +- Refactored the embedded web UI to be loaded from an external `index.html` file. + +## 0.1.19 + +- Added "Unpair Device" button to the web UI. + +## 0.1.18 + +- Added "Pair Device" button to the web UI. + +## 0.1.16 + +- Added automatic fallback to BLE if Classic Bluetooth fails with `[Errno 12] Out of memory`. + ## 0.1.15 - Added a BLE recovery retry for `br-connection-not-supported` that forces fresh LE target resolution from scan results before failing.