0.1.30
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled

This commit is contained in:
paul2212
2026-03-16 19:01:14 +01:00
parent 4c1bedf166
commit a23c33e293
3 changed files with 91 additions and 11 deletions

View File

@@ -4,6 +4,15 @@ 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. The format is based on Keep a Changelog and this project uses Semantic Versioning.
## [0.1.30] - 2026-03-16
### Fixed
- **BLE Connection**: Restored fallback to raw address string when BLE scan fails to find the specific device. This fixes connectivity for devices that are reachable but not advertising (e.g. during rapid reconnection or BlueZ cache issues), resolving "BLE device not found during scan" errors.
### Added
- **Web UI**: Restored support for the modern, responsive web interface. If the build artifacts are present in `fichero/web`, they will be served by default.
- **Web UI**: Added a `?legacy=true` query parameter to the root URL to force the simple server-side rendered UI, which includes the new debug scan tool.
## [0.1.29] - 2026-03-16 ## [0.1.29] - 2026-03-16
### Fixed ### Fixed

View File

@@ -27,8 +27,10 @@ from typing import Annotated
from fastapi import FastAPI, File, Form, HTTPException, UploadFile from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.docs import get_swagger_ui_html from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from PIL import Image from PIL import Image
from bleak import BleakScanner
from fichero.cli import DOTS_PER_MM, do_print from fichero.cli import DOTS_PER_MM, do_print
from fichero.imaging import text_to_image from fichero.imaging import text_to_image
@@ -75,7 +77,7 @@ async def lifespan(app: FastAPI): # noqa: ARG001
app = FastAPI( app = FastAPI(
title="Fichero Printer API", title="Fichero Printer API",
description="REST API for the Fichero D11s (AiYin) thermal label printer.", description="REST API for the Fichero D11s (AiYin) thermal label printer.",
version = "0.1.29", version = "0.1.30",
lifespan=lifespan, lifespan=lifespan,
docs_url=None, docs_url=None,
redoc_url=None, redoc_url=None,
@@ -88,6 +90,13 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
# Serve static files for the modern web UI (if built and present in 'web' dir)
_WEB_ROOT = Path(__file__).parent / "web"
if _WEB_ROOT.exists():
# Typical SPA assets folder
if (_WEB_ROOT / "assets").exists():
app.mount("/assets", StaticFiles(directory=_WEB_ROOT / "assets"), name="assets")
def _address(address: str | None) -> str | None: def _address(address: str | None) -> str | None:
"""Return the effective BLE address (request value overrides env default).""" """Return the effective BLE address (request value overrides env default)."""
@@ -105,21 +114,67 @@ def _ui_html() -> str:
return "<h1>Error: index.html not found</h1>" return "<h1>Error: index.html not found</h1>"
# Simple substitution for initial values # Simple substitution for initial values
return ( template = (
template.replace("{default_address}", default_address) template.replace("{default_address}", default_address)
.replace("{ble_selected}", " selected" if default_transport == "ble" else "") .replace("{ble_selected}", " selected" if default_transport == "ble" else "")
.replace("{classic_selected}", " selected" if default_transport == "classic" else "") .replace("{classic_selected}", " selected" if default_transport == "classic" else "")
.replace("{default_channel}", str(_DEFAULT_CHANNEL)) .replace("{default_channel}", str(_DEFAULT_CHANNEL))
) )
# Inject debug scan section and script
scan_html = """
<div class="section">
<h2>Debug Scan</h2>
<p>Scans for all nearby BLE devices to help with debugging connection issues.</p>
<button type="button" onclick="scanForDevices()">Scan for BLE Devices (10s)</button>
<pre id="scan-results" style="background-color: #f0f0f0; border: 1px solid #ccc; padding: 10px; margin-top: 10px; white-space: pre-wrap; word-wrap: break-word;"></pre>
</div>
"""
scan_script = r'''
<script>
async function scanForDevices() {
const resultsEl = document.getElementById('scan-results');
resultsEl.textContent = 'Scanning for 10 seconds...';
try {
const response = await fetch('/scan');
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || `HTTP error! status: ${response.status}`);
}
const devices = await response.json();
if (devices.length === 0) {
resultsEl.textContent = 'No BLE devices found.';
} else {
resultsEl.textContent = 'Found devices:\n\n' +
devices.map(d => ` ${d.address} | RSSI: ${d.rssi} | ${d.name}`).join('\n');
}
} catch (e) {
resultsEl.textContent = 'Error during scan: ' + e.message;
}
}
</script>
'''
# Inject before the closing </body> tag
if "</body>" in template:
parts = template.split("</body>", 1)
template = parts[0] + scan_html + scan_script + "</body>" + parts[1]
else:
# Fallback if no body tag
template += scan_html + scan_script
return template
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Endpoints # Endpoints
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@app.get("/", include_in_schema=False, response_class=HTMLResponse) @app.get("/", include_in_schema=False, response_class=HTMLResponse)
async def root(): async def root(legacy: bool = False):
"""Serve a compact printer UI for Home Assistant.""" """Serve a compact printer UI for Home Assistant."""
# Prefer the modern SPA if available, unless ?legacy=true is used
if not legacy and (_WEB_ROOT / "index.html").exists():
return HTMLResponse((_WEB_ROOT / "index.html").read_text(encoding="utf-8"))
return HTMLResponse(_ui_html()) return HTMLResponse(_ui_html())
@@ -190,6 +245,26 @@ async def get_info(
return info return info
@app.get(
"/scan",
summary="Scan for BLE devices",
response_description="List of discovered BLE devices",
)
async def scan_devices():
"""Scan for nearby BLE devices for 10 seconds for debugging."""
try:
devices = await BleakScanner.discover(timeout=10.0)
return [
{"address": d.address, "name": d.name or "N/A", "rssi": d.rssi}
for d in devices
]
except Exception as exc:
# This provides more debug info to the user if scanning fails
raise HTTPException(
status_code=500, detail=f"An error occurred during BLE scanning: {exc}"
)
@app.post( @app.post(
"/pair", "/pair",
summary="Pair and trust a Bluetooth device", summary="Pair and trust a Bluetooth device",

View File

@@ -101,17 +101,13 @@ async def resolve_ble_target(address: str | None = None):
device = await BleakScanner.find_device_by_address(address, timeout=8.0) device = await BleakScanner.find_device_by_address(address, timeout=8.0)
if device is not None: if device is not None:
return device return device
# Fallback to active scan/match before giving up; do not fall back to # Fallback to active scan/match before giving up.
# raw address because BlueZ may then attempt BR/EDR and fail with
# br-connection-not-supported.
devices = await BleakScanner.discover(timeout=8) devices = await BleakScanner.discover(timeout=8)
for d in devices: for d in devices:
if d.address and d.address.lower() == address.lower(): if d.address and d.address.lower() == address.lower():
return d return d
raise PrinterNotFound( print(f" Warning: BLE device {address} not found in scan. Falling back to direct address connection.")
f"BLE device {address} not found during scan. " return address
"Ensure printer is on, awake, and in range."
)
devices = await BleakScanner.discover(timeout=8) devices = await BleakScanner.discover(timeout=8)
for d in devices: for d in devices:
if d.name and any(d.name.startswith(p) for p in PRINTER_NAME_PREFIXES): if d.name and any(d.name.startswith(p) for p in PRINTER_NAME_PREFIXES):