Compare commits
5 Commits
nightly-0.
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7f851799 | ||
| 1d5ac8dc4e | |||
| 64c8bc32d7 | |||
| 0ca7618c85 | |||
| 6d6df59ff2 |
@@ -174,3 +174,20 @@ jobs:
|
||||
--data-binary @/tmp/release_body.json \
|
||||
"https://gitea.it-drui.de/api/v1/repos/viewit/KX-Bridge-Release/releases"
|
||||
rm -f "$BODY_FILE" /tmp/release_body.json
|
||||
|
||||
- name: Reset NIGHTLY_CHANGELOG.md for the next build
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
. /tmp/nightly_version.env
|
||||
# The changelog just consumed above must not carry over into the
|
||||
# next nightly - otherwise every build re-lists all prior entries
|
||||
# since the last manual reset instead of just what's new. Not in
|
||||
# nightly.yml's own push-trigger paths, so this commit does not
|
||||
# re-trigger this workflow.
|
||||
printf '## Changes in this build\n\n' > NIGHTLY_CHANGELOG.md
|
||||
git config user.name "gitea-actions"
|
||||
git config user.email "actions@gitea.it-drui.de"
|
||||
git add NIGHTLY_CHANGELOG.md
|
||||
git commit -m "chore: reset NIGHTLY_CHANGELOG.md after nightly-${VERSION} release" || exit 0
|
||||
git push https://gitea-actions:${GITEA_TOKEN}@gitea.it-drui.de/viewit/KX-Bridge-Release.git HEAD:nightly
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
## Changes in this build
|
||||
|
||||
- Fix: on printers with multiple daisy-chained ACE units and no toolhead buffer (e.g. Kobra S1 with 2 ACE Pro), only the first unit's 4 slots were ever shown on the dashboard or synced to OrcaSlicer — the bridge silently discarded every ACE unit after the first. All units now show up correctly (Issue #95, thanks @hoovercl for the detailed report and logs)
|
||||
- Feat: the GCode browser now supports multi-select and bulk delete — click any file's checkbox to enter select mode, use "Select All" to grab everything currently visible (respecting your active search/filter), then delete the selection with one confirmation instead of one-by-one (Issue #94, thanks @Blaim)
|
||||
- Fix: **printing via OrcaSlicer "Upload and print" failed (or fed the wrong spool) when a slot below the used filament was empty** — e.g. printing with Filament 4 while slot 3 was empty. The generated AMS slot mapping inserted a placeholder pointing at the physically empty tray, which the printer rejects. Placeholders now point at a loaded tray instead. Printing with all slots full already worked and is unchanged.
|
||||
- Fix: manually assigning a filament profile to an ACE slot could crash the MQTT callback (`'list' object has no attribute 'get'`) when the printer rejected the assignment, e.g. for custom-RFID/third-party filament. The bridge now handles the printer's failure response cleanly instead of crashing, and the log now correlates a rejected assignment with the request that triggered it (slot/type/color) so a rejection reason can be diagnosed from bridge logs alone (Issue #100, thanks @Blaim)
|
||||
- Fix: **the camera stream could hang forever after a printer reboot** — the printer rotates its stream token on reboot, but the running ffmpeg processes kept reading from the stale, now-silent connection and never noticed. The bridge now detects the URL change and automatically restarts the camera pipeline, ffmpeg itself now times out on a stalled read as a second line of defense, and `/api/camera/stream` now returns a proper 503 instead of hanging if no frame arrives within 5s (Issue #99, thanks @fmontagna for the excellent root-cause analysis and reproduction)
|
||||
- Fix: shrinking the Progress dashboard tile below its default height clipped the lower content (time grid, filename, buttons) out of view instead of scaling with it; dashboard tiles in general could show a scrollbar even when there was nothing to scroll. Both fixed (Issue #97, thanks @Blaim)
|
||||
|
||||
@@ -29,9 +29,7 @@ Feedback willkommen.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
> [!CAUTION]
|
||||
> **Laufende Wartungsarbeiten** — Wir strukturieren das Repository um (Branch-Modell, CI-Workflows, Beitragsprozess). Es kann zu Änderungen bei Branch-Namen, PR-Templates und der Art der Veröffentlichungen kommen. Wir entschuldigen uns für etwaige Unannehmlichkeiten. Handhabung, Workflow und langfristige Wartbarkeit werden dadurch deutlich verbessert.
|
||||
>
|
||||
|
||||
> 👉 Möchtest du beitragen? Bitte zuerst [CONTRIBUTING.md](CONTRIBUTING.md) lesen.
|
||||
|
||||
---
|
||||
|
||||
@@ -1394,12 +1394,12 @@ class KobraXBridge:
|
||||
self._spoolman_notify_end()
|
||||
self._current_job_id = ""
|
||||
|
||||
# Hide the upload banner after the print finishes (Issue #29): the
|
||||
# printer reports "finished" after a successful print - file_ready used to be
|
||||
# cleared only on stoped/canceled, which brought the banner back.
|
||||
if kobra_state == "finished":
|
||||
self._state["file_ready"] = ""
|
||||
if kobra_state in ("stoped", "canceled"):
|
||||
# Terminal states (successful finish AND stop/cancel) must leave the
|
||||
# same clean end state - a "finished" print used to only clear
|
||||
# file_ready (Issue #29), leaving progress/filename/duration/layer
|
||||
# fields stuck at the last job's values until the *next* print
|
||||
# happened to overwrite them (Issue #102).
|
||||
if kobra_state in ("finished", "stoped", "canceled"):
|
||||
self._state["progress"] = 0.0
|
||||
self._state["filename"] = ""
|
||||
self._state["file_ready"] = ""
|
||||
@@ -1409,9 +1409,20 @@ class KobraXBridge:
|
||||
self._state["layer_height"] = 0.0
|
||||
self._state["first_layer_height"] = 0.0
|
||||
self._state["supplies_usage"] = 0
|
||||
self._state["curr_layer"] = 0
|
||||
self._state["total_layers"] = 0
|
||||
self._thumbnail_b64 = ""
|
||||
self._state["filename"] = d.get("filename", self._state["filename"])
|
||||
if "progress" in d:
|
||||
else:
|
||||
# Only adopt the payload's filename outside terminal states - the
|
||||
# printer often still reports the just-finished job's filename in
|
||||
# the same "finished"/"stoped"/"canceled" message that triggered
|
||||
# the reset above, which would otherwise immediately undo it.
|
||||
self._state["filename"] = d.get("filename", self._state["filename"])
|
||||
# Pre-print phases (leveling/preheating/checking) report their own
|
||||
# "progress" - passing it through would make display_status.progress/
|
||||
# virtual_sdcard.progress jump non-monotonically once real printing
|
||||
# starts and the value resets (Issue #102).
|
||||
if "progress" in d and kobra_state not in ("preheating", "auto_leveling", "checking", "updated", "init"):
|
||||
self._state["progress"] = float(d["progress"]) / 100.0
|
||||
if "print_time" in d:
|
||||
self._state["print_duration"] = int(d["print_time"]) * 60
|
||||
@@ -1447,8 +1458,13 @@ class KobraXBridge:
|
||||
self._state["kobra_state"] = kobra_state
|
||||
# Hide the upload banner after the print ends (Issue #29) - the state also
|
||||
# arrives via info/report (project.state) depending on the printer, not only print/report.
|
||||
# Layer fields must reset here too (Issue #102) - info/report is the
|
||||
# only source for curr_layer/total_layers on some printers, and they
|
||||
# otherwise stay stuck at the last job's values indefinitely.
|
||||
if kobra_state in ("finished", "stoped", "canceled"):
|
||||
self._state["file_ready"] = ""
|
||||
self._state["curr_layer"] = 0
|
||||
self._state["total_layers"] = 0
|
||||
# Camera auto-start here as well (OrcaSlicer often reports the start via info/report).
|
||||
# The _camera_autostarted guard prevents a double start with _on_print.
|
||||
if kobra_state == "printing":
|
||||
@@ -1467,7 +1483,8 @@ class KobraXBridge:
|
||||
if project:
|
||||
if "filename" in project:
|
||||
self._state["filename"] = project["filename"]
|
||||
if "progress" in project:
|
||||
# Same non-monotonic-progress guard as _on_print (Issue #102).
|
||||
if "progress" in project and kobra_state not in ("preheating", "auto_leveling", "checking", "updated", "init"):
|
||||
self._state["progress"] = float(project["progress"]) / 100.0
|
||||
if "print_time" in project:
|
||||
self._state["print_duration"] = int(project["print_time"]) * 60
|
||||
@@ -2123,6 +2140,51 @@ class KobraXBridge:
|
||||
return fam
|
||||
return m
|
||||
|
||||
def _parse_combined_rfid_type(self, raw_type: str) -> tuple[str, str]:
|
||||
"""Split a combined ACE-RFID "VENDOR TYPE SERIAL" string (e.g.
|
||||
"GEEETECH PLA Bas", written via third-party RFID tools) into
|
||||
(vendor, material_family).
|
||||
|
||||
Anycubic's ACE RFID system concatenates vendor + material + a
|
||||
truncated serial/variant into one `type` string for custom tags -
|
||||
unlike a normal spool report where `type` is just "PLA"/"PETG"/etc.
|
||||
Returns ("", "") when the first token isn't a known vendor (from the
|
||||
merged system+user filament library), which leaves plain type
|
||||
strings like "PLA" completely unaffected (Issue #101).
|
||||
"""
|
||||
tokens = raw_type.split()
|
||||
if len(tokens) < 2:
|
||||
return "", ""
|
||||
first = tokens[0].strip().lower()
|
||||
vendors = {p.get("vendor", "").lower(): p.get("vendor", "") for p in self._load_orca_filaments()}
|
||||
vendor = vendors.get(first)
|
||||
if not vendor:
|
||||
return "", ""
|
||||
family = self._material_family(" ".join(tokens[1:]))
|
||||
if not family:
|
||||
return "", ""
|
||||
return vendor, family
|
||||
|
||||
def _match_profile_by_vendor_family(self, vendor: str, family: str) -> dict:
|
||||
"""Find an imported/system filament profile by (vendor, material
|
||||
family) - used to auto-resolve a combined ACE-RFID type string to
|
||||
the user's already-imported OrcaSlicer profile (Issue #101), since
|
||||
the exact profile `name` never appears verbatim in the truncated
|
||||
RFID string."""
|
||||
matches = [
|
||||
p for p in self._load_orca_filaments()
|
||||
if p.get("vendor", "").lower() == vendor.lower()
|
||||
and self._material_family(p.get("type", "")) == family
|
||||
]
|
||||
if not matches:
|
||||
return {}
|
||||
if len(matches) > 1:
|
||||
log.debug(
|
||||
f"_match_profile_by_vendor_family: {len(matches)} profiles match "
|
||||
f"vendor={vendor!r} family={family!r}, using first: {matches[0].get('name')}"
|
||||
)
|
||||
return matches[0]
|
||||
|
||||
def _profile_material(self, profile: dict) -> str:
|
||||
"""Material type (e.g. "PETG") of a saved slot profile, resolved by
|
||||
(vendor, name) from the Orca filament library. Returns "" when the
|
||||
@@ -2206,6 +2268,21 @@ class KobraXBridge:
|
||||
# family still matches the loaded filament (PETG profile + PLA
|
||||
# loaded -> dropped).
|
||||
user_profile = self._effective_slot_profile(slot_index, material)
|
||||
if not user_profile.get("name"):
|
||||
# Third layer: auto-resolve a combined ACE-RFID "VENDOR TYPE
|
||||
# SERIAL" string (e.g. "GEEETECH PLA Bas", from third-party
|
||||
# RFID tools) against the user's already-imported profile
|
||||
# library, instead of falling through to the neutral Generic
|
||||
# fallback (Issue #101). Not persisted to config.ini - this
|
||||
# re-derives on every _build_lane_data() call, so a
|
||||
# differently-tagged spool loaded later isn't stuck with a
|
||||
# stale match.
|
||||
vendor_guess, family_guess = self._parse_combined_rfid_type(slot.get("type", ""))
|
||||
if vendor_guess:
|
||||
auto_profile = self._match_profile_by_vendor_family(vendor_guess, family_guess)
|
||||
if auto_profile.get("name"):
|
||||
user_profile = auto_profile
|
||||
material = family_guess
|
||||
if user_profile.get("name"):
|
||||
vendor = user_profile.get("vendor", "")
|
||||
fila_name = user_profile.get("name", "")
|
||||
@@ -3345,10 +3422,16 @@ class KobraXBridge:
|
||||
`modified` are non-nullable in GCodeFile; `print_start_time` and the
|
||||
Slicer-Felder optional."""
|
||||
s = self._state
|
||||
layer_h = float(s.get("layer_height") or 0.0)
|
||||
first_h = float(s.get("first_layer_height") or 0.0)
|
||||
total_layers = int(s.get("total_layers") or 0)
|
||||
est_time = int(s.get("slicer_time") or 0)
|
||||
# Live _state values are only relevant for the currently/last tracked
|
||||
# job's own file - using them as a starting point for a DIFFERENT
|
||||
# filename leaked the tracked job's layer count/time into unrelated
|
||||
# metadata queries (Issue #102). For any other filename, rely solely
|
||||
# on that file's own GCodeStore row.
|
||||
is_tracked_file = bool(filename) and filename == s.get("filename")
|
||||
layer_h = float(s.get("layer_height") or 0.0) if is_tracked_file else 0.0
|
||||
first_h = float(s.get("first_layer_height") or 0.0) if is_tracked_file else 0.0
|
||||
total_layers = int(s.get("total_layers") or 0) if is_tracked_file else 0
|
||||
est_time = int(s.get("slicer_time") or 0) if is_tracked_file else 0
|
||||
size_bytes = 0
|
||||
try:
|
||||
gf = self._store.get_file_by_name(filename) or {}
|
||||
|
||||
111
tests/test_ace_rfid_vendor_matching.py
Normal file
111
tests/test_ace_rfid_vendor_matching.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Auto-matching for custom ACE-RFID filament tags (Issue #101).
|
||||
|
||||
Anycubic's ACE RFID system concatenates vendor + material + a truncated
|
||||
serial into one `type` string for custom (third-party) tags, e.g.
|
||||
"GEEETECH PLA Bas" (vendor "Geeetech", material "PLA", serial "Bas" for
|
||||
"Basic"). Previously the bridge treated this whole string as an unknown
|
||||
material and fell back to a neutral "Generic <type>" profile, even though
|
||||
the user had already imported a matching OrcaSlicer profile via the ZIP
|
||||
import feature (Issue #41) - requiring a manual per-slot reassignment every
|
||||
time that spool was loaded.
|
||||
|
||||
_parse_combined_rfid_type() + _match_profile_by_vendor_family() resolve this
|
||||
automatically against the merged system+user filament library.
|
||||
"""
|
||||
import argparse
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from kobrax_moonraker_bridge import KobraXBridge
|
||||
|
||||
USER_PROFILES = [
|
||||
{"id": "OGFL99", "name": "Generic PLA", "vendor": "Generic", "type": "PLA", "color": ""},
|
||||
{"id": "GTPLA01", "name": "Geeetech PLA Basic", "vendor": "Geeetech", "type": "PLA", "color": "", "is_user": True},
|
||||
]
|
||||
|
||||
|
||||
def _bridge(profiles=USER_PROFILES):
|
||||
c = MagicMock(); c.callbacks = {}; c.connected = False
|
||||
args = argparse.Namespace(
|
||||
printer_ip="", mqtt_port=9883, username="", password="",
|
||||
mode_id="20030", device_id="", host="127.0.0.1", port=7125,
|
||||
data_dir=tempfile.mkdtemp(prefix="kxrfid-"),
|
||||
)
|
||||
b = KobraXBridge(c, args=args)
|
||||
b._orca_filaments_cache = profiles
|
||||
return b
|
||||
|
||||
|
||||
def test_combined_rfid_type_parses_known_vendor_and_family():
|
||||
b = _bridge()
|
||||
vendor, family = b._parse_combined_rfid_type("GEEETECH PLA Bas")
|
||||
assert vendor == "Geeetech"
|
||||
assert family == "PLA"
|
||||
|
||||
|
||||
def test_plain_type_string_is_unaffected():
|
||||
"""Regression guard: a normal type="PLA" report (no vendor prefix) must
|
||||
not be mistaken for a combined RFID string."""
|
||||
b = _bridge()
|
||||
vendor, family = b._parse_combined_rfid_type("PLA")
|
||||
assert vendor == ""
|
||||
assert family == ""
|
||||
|
||||
|
||||
def test_unknown_vendor_prefix_returns_no_match():
|
||||
b = _bridge()
|
||||
vendor, family = b._parse_combined_rfid_type("TOTALLYUNKNOWNBRAND PLA Bas")
|
||||
assert vendor == ""
|
||||
assert family == ""
|
||||
|
||||
|
||||
def test_match_profile_by_vendor_family_finds_imported_profile():
|
||||
b = _bridge()
|
||||
profile = b._match_profile_by_vendor_family("Geeetech", "PLA")
|
||||
assert profile.get("name") == "Geeetech PLA Basic"
|
||||
|
||||
|
||||
def test_match_profile_by_vendor_family_no_match_returns_empty():
|
||||
b = _bridge()
|
||||
profile = b._match_profile_by_vendor_family("Geeetech", "PETG")
|
||||
assert profile == {}
|
||||
|
||||
|
||||
def test_match_profile_by_vendor_family_ambiguous_picks_first_without_crashing():
|
||||
profiles = USER_PROFILES + [
|
||||
{"id": "GTPLA02", "name": "Geeetech PLA Silk", "vendor": "Geeetech", "type": "PLA SILK", "color": "", "is_user": True},
|
||||
]
|
||||
b = _bridge(profiles)
|
||||
profile = b._match_profile_by_vendor_family("Geeetech", "PLA")
|
||||
assert profile.get("name") in ("Geeetech PLA Basic", "Geeetech PLA Silk")
|
||||
|
||||
|
||||
def test_build_lane_data_auto_resolves_combined_rfid_slot():
|
||||
"""End-to-end: a slot reporting the combined RFID string should surface
|
||||
the imported Geeetech profile in lane_data instead of the Generic
|
||||
fallback."""
|
||||
b = _bridge()
|
||||
b._filament_profiles = {} # no manual per-slot override - isolate the auto-match path
|
||||
b._filament_mode = "ace_hub"
|
||||
b._ams_slots = [
|
||||
{"global_index": 0, "box_id": 0, "status": 5, "type": "GEEETECH PLA Bas", "color": [238, 190, 152]},
|
||||
]
|
||||
lane = b._build_lane_data()
|
||||
tray = lane["ams"][0]["tray"][0]
|
||||
assert tray["vendor_name"] == "Geeetech"
|
||||
assert tray["name"] == "Geeetech PLA Basic"
|
||||
|
||||
|
||||
def test_build_lane_data_plain_type_still_uses_generic_fallback():
|
||||
"""Regression guard: everyday type="PLA" slots must keep using the
|
||||
existing Generic-library fallback, unaffected by the new matching path."""
|
||||
b = _bridge()
|
||||
b._filament_profiles = {} # no manual per-slot override - isolate the fallback path
|
||||
b._filament_mode = "ace_hub"
|
||||
b._ams_slots = [
|
||||
{"global_index": 0, "box_id": 0, "status": 5, "type": "PLA", "color": [255, 255, 255]},
|
||||
]
|
||||
lane = b._build_lane_data()
|
||||
tray = lane["ams"][0]["tray"][0]
|
||||
assert tray["name"] == "Generic PLA"
|
||||
assert tray["vendor_name"] == "Generic"
|
||||
151
tests/test_metadata_and_print_state_reset.py
Normal file
151
tests/test_metadata_and_print_state_reset.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""server/files/metadata state-leak + terminal-state reset gaps (Issue #102).
|
||||
|
||||
Reported by @fmontagna via moonraker-obico:
|
||||
1. Querying metadata for a file OTHER than the currently/last tracked job
|
||||
leaked that job's live layer count / estimated time into the response,
|
||||
because _build_file_metadata() read from self._state first and only fell
|
||||
back to the file's own GCodeStore row when the state value was falsy.
|
||||
2. curr_layer/total_layers (and, for a successful "finished" print, every
|
||||
other per-job field) were never reset at print end - they stayed at the
|
||||
last job's values until the next print happened to overwrite them.
|
||||
3. The printer reports its own "progress" during pre-print phases
|
||||
(preheating/auto_leveling/checking/...), which used to pass straight
|
||||
through to display_status.progress/virtual_sdcard.progress and then jump
|
||||
non-monotonically once real printing started and progress reset.
|
||||
"""
|
||||
import argparse
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from kobrax_moonraker_bridge import KobraXBridge
|
||||
|
||||
|
||||
def _bridge():
|
||||
c = MagicMock(); c.callbacks = {}; c.connected = False
|
||||
args = argparse.Namespace(
|
||||
printer_ip="", mqtt_port=9883, username="", password="",
|
||||
mode_id="20030", device_id="", host="127.0.0.1", port=7125,
|
||||
data_dir=tempfile.mkdtemp(prefix="kxmeta-"),
|
||||
)
|
||||
return KobraXBridge(c, args=args)
|
||||
|
||||
|
||||
def _insert_store_row(b, filename, layer_count=None, est_time=None, size_bytes=0):
|
||||
with b._store._lock:
|
||||
b._store._conn.execute(
|
||||
"INSERT OR REPLACE INTO gcode_files (id, filename, path, size_bytes, uploaded_at, layer_count, est_print_time_sec) "
|
||||
"VALUES (?,?,?,?,?,?,?)",
|
||||
(filename, filename, "/tmp/" + filename, size_bytes, "2026-01-01T00:00:00Z", layer_count, est_time),
|
||||
)
|
||||
b._store._conn.commit()
|
||||
|
||||
|
||||
# --- Fix 1: metadata state-leak -------------------------------------------
|
||||
|
||||
def test_metadata_for_untracked_file_does_not_leak_running_job_state():
|
||||
b = _bridge()
|
||||
# A job is "running": live state has its own layer/time values.
|
||||
b._state["filename"] = "running.gcode"
|
||||
b._state["total_layers"] = 999
|
||||
b._state["slicer_time"] = 12345
|
||||
b._state["layer_height"] = 0.3
|
||||
|
||||
# Querying a DIFFERENT, unrelated file must use ITS OWN store row, not
|
||||
# the running job's live state.
|
||||
_insert_store_row(b, "other.gcode", layer_count=42, est_time=600, size_bytes=1000)
|
||||
meta = b._build_file_metadata("other.gcode")
|
||||
assert meta["layer_count"] == 42
|
||||
assert meta["estimated_time"] == 600
|
||||
assert meta["size"] == 1000
|
||||
|
||||
|
||||
def test_metadata_for_tracked_file_still_uses_live_state():
|
||||
"""The currently-tracked file's OWN metadata query should still prefer
|
||||
live state (fresher than what was known at upload time)."""
|
||||
b = _bridge()
|
||||
b._state["filename"] = "running.gcode"
|
||||
b._state["total_layers"] = 55
|
||||
b._state["slicer_time"] = 999
|
||||
_insert_store_row(b, "running.gcode", layer_count=1, est_time=1)
|
||||
meta = b._build_file_metadata("running.gcode")
|
||||
assert meta["layer_count"] == 55
|
||||
assert meta["estimated_time"] == 999
|
||||
|
||||
|
||||
def test_metadata_for_nonexistent_file_falls_back_cleanly():
|
||||
b = _bridge()
|
||||
b._state["filename"] = "running.gcode"
|
||||
b._state["total_layers"] = 999
|
||||
meta = b._build_file_metadata("DOES_NOT_EXIST.gcode")
|
||||
assert meta["layer_count"] is None
|
||||
assert meta["size"] == 1 # documented fallback, unrelated to this fix
|
||||
|
||||
|
||||
# --- Fix 2: terminal-state reset -------------------------------------------
|
||||
|
||||
def _print_payload(state, **extra):
|
||||
"""print/report envelope: `state` is top-level, everything else is under
|
||||
`data` (see _on_print: kobra_state = payload.get("state", "")). """
|
||||
d = {"filename": "job.gcode"}
|
||||
d.update(extra)
|
||||
return {"state": state, "data": d}
|
||||
|
||||
|
||||
def test_finished_resets_layer_fields_like_stoped_canceled():
|
||||
b = _bridge()
|
||||
b._state["curr_layer"] = 10
|
||||
b._state["total_layers"] = 20
|
||||
b._state["progress"] = 0.5
|
||||
b._state["filename"] = "job.gcode"
|
||||
b._on_print(_print_payload("finished"))
|
||||
assert b._state["curr_layer"] == 0
|
||||
assert b._state["total_layers"] == 0
|
||||
assert b._state["progress"] == 0.0
|
||||
assert b._state["filename"] == ""
|
||||
|
||||
|
||||
def test_canceled_resets_layer_fields():
|
||||
b = _bridge()
|
||||
b._state["curr_layer"] = 7
|
||||
b._state["total_layers"] = 20
|
||||
b._on_print(_print_payload("canceled"))
|
||||
assert b._state["curr_layer"] == 0
|
||||
assert b._state["total_layers"] == 0
|
||||
|
||||
|
||||
def test_on_info_resets_layer_fields_on_terminal_state():
|
||||
b = _bridge()
|
||||
b._state["curr_layer"] = 7
|
||||
b._state["total_layers"] = 20
|
||||
b._on_info({"data": {"project": {"state": "finished"}}})
|
||||
assert b._state["curr_layer"] == 0
|
||||
assert b._state["total_layers"] == 0
|
||||
|
||||
|
||||
# --- Fix 3: progress clamping during pre-print phases ----------------------
|
||||
|
||||
def test_progress_not_updated_during_auto_leveling():
|
||||
b = _bridge()
|
||||
b._state["progress"] = 0.0
|
||||
b._on_print(_print_payload("auto_leveling", progress=87))
|
||||
assert b._state["progress"] == 0.0
|
||||
|
||||
|
||||
def test_progress_not_updated_during_preheating():
|
||||
b = _bridge()
|
||||
b._state["progress"] = 0.0
|
||||
b._on_print(_print_payload("preheating", progress=42))
|
||||
assert b._state["progress"] == 0.0
|
||||
|
||||
|
||||
def test_progress_updates_normally_once_printing():
|
||||
b = _bridge()
|
||||
b._on_print(_print_payload("printing", progress=33))
|
||||
assert b._state["progress"] == 0.33
|
||||
|
||||
|
||||
def test_on_info_progress_clamped_during_checking():
|
||||
b = _bridge()
|
||||
b._state["progress"] = 0.0
|
||||
b._on_info({"data": {"project": {"state": "checking", "progress": 55}}})
|
||||
assert b._state["progress"] == 0.0
|
||||
Reference in New Issue
Block a user