diff --git a/CHANGELOG.md b/CHANGELOG.md index d10f91ce0..9f29c2c12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +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; (4) the heartbeat response that delivered the tare command still contained pre-tare calibration values, which immediately overwrote the new tare offset back to zero. 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, propagates calibration changes to the ScaleReader instance, and skips calibration sync on the heartbeat cycle that delivers a tare command. The calibration weight input now uses a touch-friendly numpad instead of a native ``, since the RPi kiosk has no physical keyboard. +- **SpoolBuddy Scale Tare & Calibration Not Applied** — The SpoolBuddy scale tare and calibrate buttons on the Settings page queued commands but never executed them. Five 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; (4) the heartbeat response that delivered the tare command still contained pre-tare calibration values, which immediately overwrote the new tare offset back to zero; (5) the `set-factor` endpoint computed `calibration_factor` using the DB `tare_offset`, which could be stale or zero if the tare hadn't persisted yet — producing a wildly wrong factor (e.g., 5000g displayed with empty scale). 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, propagates calibration changes to the ScaleReader instance, and skips calibration sync on the heartbeat cycle that delivers a tare command. The calibration flow now captures the raw ADC at tare time and sends it alongside the loaded-weight ADC in step 2, so the factor is computed from the actual tare reference rather than the DB value — making calibration self-contained and independent of the tare persistence round-trip. The calibration weight input uses a compact touch-friendly numpad since the RPi kiosk has no physical keyboard. - **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 diff --git a/backend/app/api/routes/spoolbuddy.py b/backend/app/api/routes/spoolbuddy.py index badfa0e7b..8d7d6679e 100644 --- a/backend/app/api/routes/spoolbuddy.py +++ b/backend/app/api/routes/spoolbuddy.py @@ -342,11 +342,14 @@ async def set_calibration_factor( if not device: raise HTTPException(status_code=404, detail="Device not registered") - raw_delta = req.raw_adc - device.tare_offset + tare = req.tare_raw_adc if req.tare_raw_adc is not None else device.tare_offset + raw_delta = req.raw_adc - tare if raw_delta == 0: raise HTTPException(status_code=400, detail="Raw ADC value equals tare offset — place weight on scale") device.calibration_factor = req.known_weight_grams / raw_delta + if req.tare_raw_adc is not None: + device.tare_offset = tare await db.commit() logger.info( @@ -355,7 +358,7 @@ async def set_calibration_factor( device.calibration_factor, req.known_weight_grams, req.raw_adc, - device.tare_offset, + tare, ) return CalibrationResponse( tare_offset=device.tare_offset, diff --git a/backend/app/schemas/spoolbuddy.py b/backend/app/schemas/spoolbuddy.py index 681a2ab04..67927da68 100644 --- a/backend/app/schemas/spoolbuddy.py +++ b/backend/app/schemas/spoolbuddy.py @@ -99,6 +99,7 @@ class SetTareRequest(BaseModel): class SetCalibrationFactorRequest(BaseModel): known_weight_grams: float = Field(..., gt=0) raw_adc: int + tare_raw_adc: int | None = None class CalibrationResponse(BaseModel): diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 5559650b8..e5fca13a9 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -4849,10 +4849,10 @@ export const spoolbuddyApi = { getCalibration: (deviceId: string) => request<{ tare_offset: number; calibration_factor: number }>(`/spoolbuddy/devices/${deviceId}/calibration`), - setCalibrationFactor: (deviceId: string, knownWeightGrams: number, rawAdc: number) => + setCalibrationFactor: (deviceId: string, knownWeightGrams: number, rawAdc: number, tareRawAdc?: number) => request<{ tare_offset: number; calibration_factor: number }>(`/spoolbuddy/devices/${deviceId}/calibration/set-factor`, { method: 'POST', - body: JSON.stringify({ known_weight_grams: knownWeightGrams, raw_adc: rawAdc }), + body: JSON.stringify({ known_weight_grams: knownWeightGrams, raw_adc: rawAdc, tare_raw_adc: tareRawAdc }), }), updateSpoolWeight: (spoolId: number, weightGrams: number) => diff --git a/frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx b/frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx index 6cfeca216..353c2d690 100644 --- a/frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx +++ b/frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx @@ -23,6 +23,7 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: { const [calibrating, setCalibrating] = useState(false); const [calStep, setCalStep] = useState<'idle' | 'tare' | 'weight'>('idle'); const [knownWeight, setKnownWeight] = useState('500'); + const [tareRawAdc, setTareRawAdc] = useState(null); const [taring, setTaring] = useState(false); const numpadPress = (key: string) => { @@ -54,6 +55,8 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: { if (calStep === 'tare') { setCalibrating(true); try { + // Capture raw ADC before taring — this is our zero reference + setTareRawAdc(rawAdc); await spoolbuddyApi.tare(device.device_id); setCalStep('weight'); } catch (e) { @@ -66,7 +69,7 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: { if (rawAdc === null || !weightNum || weightNum <= 0) return; setCalibrating(true); try { - await spoolbuddyApi.setCalibrationFactor(device.device_id, weightNum, rawAdc); + await spoolbuddyApi.setCalibrationFactor(device.device_id, weightNum, rawAdc, tareRawAdc ?? undefined); setCalStep('idle'); } catch (e) { console.error('Failed to calibrate:', e); @@ -123,7 +126,7 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: { ) : ( -
+
{calStep === 'tare' ? t('spoolbuddy.settings.calStep1', 'Step 1: Remove all items from the scale') @@ -131,17 +134,19 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: {
{calStep === 'weight' && ( -
- -
- {knownWeight || '0'}g +
+
+ {t('spoolbuddy.settings.knownWeight', 'Known weight (g)')} +
+ {knownWeight || '0'}g +
-
+
{['7','8','9','backspace','4','5','6','.','1','2','3','0'].map((key) => (