Просмотр исходного кода

Fix SpoolBuddy scale tare & calibration not being applied

  The tare and calibrate buttons on the Settings page queued commands
  but never executed them due to three broken links:

  1. Daemon received tare command via heartbeat but never called
     scale.tare() — the ScaleReader was available in shared dict
     but unused
  2. No API endpoint for the daemon to report the new tare offset
     back to the backend DB, so tare results were lost
  3. Heartbeat updated config but never called
     scale.update_calibration(), so ScaleReader kept initial values

  Added set-tare endpoint + API client method, and fixed heartbeat
  loop to execute tare, persist the result, and propagate calibration
  changes to the ScaleReader instance.
maziggy 6 месяцев назад
Родитель
Сommit
7bc549e985

+ 1 - 0
CHANGELOG.md

@@ -17,6 +17,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Windows Install Fails With "Syntax of the Command Is Incorrect"** ([#544](https://github.com/maziggy/bambuddy/issues/544)) — The `start_bambuddy.bat` launcher had Unix (LF) line endings instead of Windows (CRLF). When a user's git config has `core.autocrlf=false` or `input`, the file is checked out with LF endings and `cmd.exe` cannot parse it. Added a `.gitattributes` file that forces CRLF for all `.bat` files regardless of git config.
 - **Queue Badge Shows on Incompatible Printers** ([#486](https://github.com/maziggy/bambuddy/issues/486)) — The purple queue counter badge in the printer card header showed on all printers of the same model when a job was scheduled for "any [model]", even if the printer didn't have the matching filament color loaded. The `PrinterQueueWidget` (which shows "Clear Plate & Start") already filtered by filament type and color, but the badge count used the raw unfiltered queue length. Now applies the same filament compatibility filter to the badge count.
 - **SpoolBuddy Daemon Can't Find Hardware Drivers** — The daemon's `nfc_reader.py` and `scale_reader.py` import `read_tag` and `scale_diag` as bare modules, but these files live in `spoolbuddy/scripts/` which isn't on Python's module search path. The systemd service sets `WorkingDirectory` to `spoolbuddy/` and runs `python -m daemon.main`, so only the `spoolbuddy/` and `daemon/` directories are on `sys.path`. Added `scripts/` to `sys.path` at daemon startup, resolved relative to the module file so it works regardless of install path. Also moved the `read_tag` import inside `NFCReader.__init__`'s try/except block — it was previously outside, so a missing module crashed the entire daemon instead of gracefully skipping NFC polling. Demoted hardware-not-available log messages from ERROR to INFO since missing modules are expected when hardware isn't connected.
+- **SpoolBuddy Scale Tare & Calibration Not Applied** — The SpoolBuddy scale tare and calibrate buttons on the Settings page queued commands but never executed them. Three bugs in the chain: (1) the daemon received the `tare` command via heartbeat but never called `scale.tare()` — a comment said "need cross-task communication" but the ScaleReader was already available in the shared dict; (2) no API endpoint existed for the daemon to report the new tare offset back to the backend database, so tare results were lost; (3) when calibration values changed in heartbeat responses, the daemon updated its config object but never called `scale.update_calibration()`, so the ScaleReader kept using its initial values forever. Added a `POST /devices/{device_id}/calibration/set-tare` endpoint and `update_tare()` API client method. The heartbeat loop now executes `scale.tare()` when the tare command is received, persists the result via the new endpoint, and propagates calibration changes to the ScaleReader instance.
 - **SpoolBuddy NFC Reader Fails to Detect Tags** — The PN5180 NFC reader had two polling issues. First, each `activate_type_a()` call that returned `None` (no tag) corrupted the PN5180 transceive state — subsequent calls silently failed even when a tag was physically present, making it impossible to detect tags placed after startup (only tags already on the reader during init were detected). Fixed by performing a full hardware reset (RST pin toggle + RF re-init, ~240ms) before every idle poll, giving a ~1.8 Hz effective poll rate. Second, after a successful SELECT the card stayed in ACTIVE state and ignored subsequent WUPA/REQA, causing false "tag removed" events after ~1 second. Fixed with a light RF off/on cycle (13ms) before each poll when a tag is present, resetting the card to IDLE for re-selection. Also added error-based auto-recovery (full hardware reset after 10 consecutive poll exceptions), periodic status logging every 60 seconds, and accurate heartbeat reporting of NFC/scale health.
 
 ### Improved

+ 24 - 0
backend/app/api/routes/spoolbuddy.py

@@ -21,6 +21,7 @@ from backend.app.schemas.spoolbuddy import (
     HeartbeatResponse,
     ScaleReadingRequest,
     SetCalibrationFactorRequest,
+    SetTareRequest,
     TagRemovedRequest,
     TagScannedRequest,
     UpdateSpoolWeightRequest,
@@ -305,6 +306,29 @@ async def tare_scale(
     return {"status": "ok", "message": "Tare command queued"}
 
 
+@router.post("/devices/{device_id}/calibration/set-tare")
+async def set_tare_offset(
+    device_id: str,
+    req: SetTareRequest,
+    db: AsyncSession = Depends(get_db),
+    _: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
+):
+    """Store tare offset reported by the daemon after executing a tare."""
+    result = await db.execute(select(SpoolBuddyDevice).where(SpoolBuddyDevice.device_id == device_id))
+    device = result.scalar_one_or_none()
+    if not device:
+        raise HTTPException(status_code=404, detail="Device not registered")
+
+    device.tare_offset = req.tare_offset
+    await db.commit()
+
+    logger.info("SpoolBuddy %s tare offset set to %d", device_id, req.tare_offset)
+    return CalibrationResponse(
+        tare_offset=device.tare_offset,
+        calibration_factor=device.calibration_factor,
+    )
+
+
 @router.post("/devices/{device_id}/calibration/set-factor")
 async def set_calibration_factor(
     device_id: str,

+ 4 - 0
backend/app/schemas/spoolbuddy.py

@@ -92,6 +92,10 @@ class TareRequest(BaseModel):
     pass
 
 
+class SetTareRequest(BaseModel):
+    tare_offset: int
+
+
 class SetCalibrationFactorRequest(BaseModel):
     known_weight_grams: float = Field(..., gt=0)
     raw_adc: int

+ 6 - 0
spoolbuddy/daemon/api_client.py

@@ -133,6 +133,12 @@ class APIClient:
             },
         )
 
+    async def update_tare(self, device_id: str, tare_offset: int) -> dict | None:
+        return await self._post(
+            f"/devices/{device_id}/calibration/set-tare",
+            {"tare_offset": tare_offset},
+        )
+
     async def scale_reading(
         self, device_id: str, weight_grams: float, stable: bool, raw_adc: int | None = None
     ) -> dict | None:

+ 12 - 3
spoolbuddy/daemon/main.py

@@ -131,14 +131,23 @@ async def heartbeat_loop(config: Config, api: APIClient, start_time: float, shar
         if result:
             cmd = result.get("pending_command")
             if cmd == "tare":
-                logger.info("Tare command received from backend")
-                # Tare is handled by scale_reader — need cross-task communication
-                # For now, update calibration from backend response
+                scale = shared.get("scale")
+                if scale and scale.ok:
+                    new_offset = await asyncio.to_thread(scale.tare)
+                    logger.info("Tare executed: offset=%d", new_offset)
+                    await api.update_tare(config.device_id, new_offset)
+                    config.tare_offset = new_offset
+                else:
+                    logger.warning("Tare command received but scale not available")
+
             tare = result.get("tare_offset", config.tare_offset)
             cal = result.get("calibration_factor", config.calibration_factor)
             if tare != config.tare_offset or cal != config.calibration_factor:
                 config.tare_offset = tare
                 config.calibration_factor = cal
+                scale = shared.get("scale")
+                if scale:
+                    scale.update_calibration(tare, cal)
                 logger.info("Calibration updated from backend: tare=%d, factor=%.6f", tare, cal)