diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index 6b9806ec..15c71577 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -28,17 +28,18 @@ jobs: path: .venv key: ${{ runner.os }}-quality-py3.12-${{ hashFiles('poetry.lock') }} - - name: Install Linux test prerequisites - run: | - sudo apt-get update - sudo apt-get install -y xvfb libgl1 libgles2 libmtdev1 - - name: Install poetry and dependencies uses: ./.github/actions/bootstrap-poetry with: os: linux install-args: --only main,lint,test + - name: Install Linux prerequisites + run: poetry run ./scripts/install_linux_prereqs.sh + + - name: Install Linux test prerequisites + run: sudo apt-get install -y xvfb libgl1 libgles2 libmtdev1 + - name: Run quality hooks run: poetry run pre-commit run --all-files diff --git a/CHANGELOG.md b/CHANGELOG.md index 603ca7b9..a98fc3d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed: Sanitize missing or malformed spindle values before updating the WHB04 display - Enhancement: Add multi-select to the remote file browser - Enhancement: Display error message in halt popup. Requires halt errors to start with "ERROR: " in the firmware - Enhancement: Add popup notice when using stock firmware instead of the Community Firmware diff --git a/carveracontroller/addons/pendant/pendant.py b/carveracontroller/addons/pendant/pendant.py index 51f5c392..92de5f12 100644 --- a/carveracontroller/addons/pendant/pendant.py +++ b/carveracontroller/addons/pendant/pendant.py @@ -2,6 +2,7 @@ import json import logging +import math from collections.abc import Callable logger = logging.getLogger(__name__) @@ -181,7 +182,18 @@ def _handle_display_update(self, daemon: whb04.Daemon) -> None: except ValueError: pass # end of feedrate fix - daemon.set_display_spindle_speed(self._cnc.vars["curspindle"]) + spindle_value = self._cnc.vars.get("curspindle", 0) + try: + raw_spindle = float(spindle_value) + except OverflowError: + # A finite integer can exceed float's range; clamp it by sign. + raw_spindle = 65535.0 if spindle_value > 0 else 0.0 + except (TypeError, ValueError): + raw_spindle = 0.0 + if not math.isfinite(raw_spindle): + raw_spindle = 0.0 + safe_spindle = max(0.0, min(raw_spindle, 65535.0)) + daemon.set_display_spindle_speed(safe_spindle) # Update the step indicator to reflect current jog mode if self._controller.jog_mode == self._controller.JOG_MODE_CONTINUOUS: diff --git a/tests/unit/test_pendant_display.py b/tests/unit/test_pendant_display.py new file mode 100644 index 00000000..11de30e9 --- /dev/null +++ b/tests/unit/test_pendant_display.py @@ -0,0 +1,49 @@ +"""Tests for defensive pendant display updates.""" + +from types import SimpleNamespace + +import pytest + +from carveracontroller.addons.pendant import pendant as pendant_module +from carveracontroller.addons.pendant import whb04 + + +@pytest.mark.parametrize( + ("spindle_vars", "expected_speed"), + [ + ({}, 0), + ({"curspindle": None}, 0), + ({"curspindle": "invalid"}, 0), + ({"curspindle": -1}, 0), + ({"curspindle": 70000}, 65535), + ({"curspindle": "1234.5"}, 1234), + ({"curspindle": float("nan")}, 0), + ({"curspindle": float("inf")}, 0), + ({"curspindle": float("-inf")}, 0), + pytest.param({"curspindle": 10**10000}, 65535, id="huge-positive-integer"), + pytest.param({"curspindle": -(10**10000)}, 0, id="huge-negative-integer"), + ], +) +def test_whb04_display_sanitizes_spindle_speed(spindle_vars, expected_speed): + pendant = pendant_module.WHB04.__new__(pendant_module.WHB04) + pendant._cnc = SimpleNamespace( + vars={ + "wx": 0.0, + "wy": 0.0, + "wz": 0.0, + "wa": 0.0, + "curfeed": 0.0, + **spindle_vars, + } + ) + pendant._controller = SimpleNamespace( + jog_mode=0, + JOG_MODE_CONTINUOUS=1, + JOG_MODE_STEP=0, + ) + daemon = whb04.Daemon() + daemon.set_display_spindle_speed(4321) + + pendant._handle_display_update(daemon) + + assert daemon._display_spindle_speed == expected_speed