Browse Source

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 four 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
  4. The heartbeat response delivering the tare command still had
     pre-tare values, immediately overwriting the new offset to zero

  Added set-tare endpoint + API client method, and fixed heartbeat
  loop to execute tare, persist the result, propagate calibration
  changes to the ScaleReader, and skip calibration sync on the
  heartbeat cycle that delivers a tare command.

  Also replaced the calibration weight input with a touch-friendly
  numpad since the RPi kiosk has no physical keyboard.
maziggy 2 months ago
parent
commit
49ddb570cc

+ 1 - 1
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.
 - **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.
 - **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 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 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 `<input type="number">`, 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.
 - **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
 ### Improved

+ 33 - 11
frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx

@@ -22,9 +22,19 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [calibrating, setCalibrating] = useState(false);
   const [calibrating, setCalibrating] = useState(false);
   const [calStep, setCalStep] = useState<'idle' | 'tare' | 'weight'>('idle');
   const [calStep, setCalStep] = useState<'idle' | 'tare' | 'weight'>('idle');
-  const [knownWeight, setKnownWeight] = useState(500);
+  const [knownWeight, setKnownWeight] = useState('500');
   const [taring, setTaring] = useState(false);
   const [taring, setTaring] = useState(false);
 
 
+  const numpadPress = (key: string) => {
+    if (key === 'backspace') {
+      setKnownWeight((v) => v.slice(0, -1) || '');
+    } else if (key === '.' && !knownWeight.includes('.')) {
+      setKnownWeight((v) => v + '.');
+    } else if (key >= '0' && key <= '9') {
+      setKnownWeight((v) => (v === '0' ? key : v + key));
+    }
+  };
+
   const handleTare = async () => {
   const handleTare = async () => {
     setTaring(true);
     setTaring(true);
     try {
     try {
@@ -52,10 +62,11 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: {
         setCalibrating(false);
         setCalibrating(false);
       }
       }
     } else if (calStep === 'weight') {
     } else if (calStep === 'weight') {
-      if (rawAdc === null) return;
+      const weightNum = parseFloat(knownWeight);
+      if (rawAdc === null || !weightNum || weightNum <= 0) return;
       setCalibrating(true);
       setCalibrating(true);
       try {
       try {
-        await spoolbuddyApi.setCalibrationFactor(device.device_id, knownWeight, rawAdc);
+        await spoolbuddyApi.setCalibrationFactor(device.device_id, weightNum, rawAdc);
         setCalStep('idle');
         setCalStep('idle');
       } catch (e) {
       } catch (e) {
         console.error('Failed to calibrate:', e);
         console.error('Failed to calibrate:', e);
@@ -120,15 +131,26 @@ function ScaleCalibration({ device, weight, weightStable, rawAdc }: {
           </div>
           </div>
 
 
           {calStep === 'weight' && (
           {calStep === 'weight' && (
-            <div className="flex items-center gap-2">
+            <div className="space-y-2">
               <label className="text-xs text-zinc-400">{t('spoolbuddy.settings.knownWeight', 'Known weight (g)')}</label>
               <label className="text-xs text-zinc-400">{t('spoolbuddy.settings.knownWeight', 'Known weight (g)')}</label>
-              <input
-                type="number"
-                value={knownWeight}
-                onChange={(e) => setKnownWeight(Number(e.target.value))}
-                className="w-24 px-2 py-2 bg-zinc-900 border border-zinc-600 rounded text-sm text-zinc-100 focus:outline-none focus:border-green-500 min-h-[44px]"
-                min={1}
-              />
+              <div className="bg-zinc-900 border border-zinc-600 rounded px-3 py-2 text-right text-lg font-mono text-zinc-100 min-h-[44px]">
+                {knownWeight || '0'}<span className="text-zinc-500 ml-1">g</span>
+              </div>
+              <div className="grid grid-cols-4 gap-1.5">
+                {['7','8','9','backspace','4','5','6','.','1','2','3','0'].map((key) => (
+                  <button
+                    key={key}
+                    onClick={() => numpadPress(key)}
+                    className={`py-3 rounded-lg text-base font-medium transition-colors min-h-[48px] ${
+                      key === 'backspace'
+                        ? 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600'
+                        : 'bg-zinc-800 text-zinc-100 hover:bg-zinc-700 border border-zinc-700'
+                    }`}
+                  >
+                    {key === 'backspace' ? '⌫' : key}
+                  </button>
+                ))}
+              </div>
             </div>
             </div>
           )}
           )}
 
 

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-C-1HSQvy.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-CREQMlgt.js"></script>
+    <script type="module" crossorigin src="/assets/index-C-1HSQvy.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-BnyNVaG5.css">
     <link rel="stylesheet" crossorigin href="/assets/index-BnyNVaG5.css">
   </head>
   </head>
   <body>
   <body>

Some files were not shown because too many files changed in this diff