0.1.30
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user