mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-19 03:49:50 -05:00
The first boot imports the factory calibration and keeps it
auto_import already loaded firmwareExe's per-unit numbers whenever no tool had a saved nozzle position -- but only live, so every boot re-imported, TOOL_OFFSET_STATUS nagged about an unsaved calibration, and the SAVE_CONFIG was homework for the user. Now the first successful import persists itself: ff_legacy schedules SAVE_CONFIG one reactor callback after klippy:ready, the restart that save causes lands seconds into the first boot -- before HelixScreen is anywhere near its setup wizard -- and the saved nozzle positions are exactly what stops the import from ever running again. Guarded on three sides: nothing is saved when something else was already staged before the import (not ours to commit), when the JSON put no tool on its feet (a restart with nothing to keep), or when auto_save is off in [ff_legacy], which restores the old live-but-unsaved contract. A failed save never restarted klippy, so there is no loop behind any of it. The extras finally get a klippy-free unit lane: test/integration/test_ff_legacy.py stubs the four objects ff_legacy touches and pins the once-ness, both guards, and the failure path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e36219cf40
commit
64ea5a84a9
7 files changed
+354
-47
No files matched your search
@@ -137,15 +137,12 @@ emergency stop within reach.
|
||||
## Before your first print: calibrate
|
||||
|
||||
**Prints refuse to start on an uncalibrated toolchanger — by design.** The
|
||||
sequence is short, and the first part of it may already have happened at boot;
|
||||
the status commands tell you.
|
||||
sequence is short, and the first part happens without you.
|
||||
|
||||
1. Import your unit's factory calibration once, then persist it:
|
||||
|
||||
```gcode
|
||||
FF_IMPORT_FIRMWARE_CONFIG
|
||||
SAVE_CONFIG
|
||||
```
|
||||
1. Your unit's factory calibration imports itself on the first boot after the
|
||||
flash and is saved automatically — Klipper restarts once, before the
|
||||
screen is up, and that is the last you hear of it. Check it took:
|
||||
`TOOL_OFFSET_STATUS` should show a nozzle triple for every tool.
|
||||
|
||||
2. **Take the PEI sheet off** — the calibration station sits below the bed
|
||||
plane — then home and calibrate:
|
||||
|
||||
@@ -220,8 +220,12 @@ print.
|
||||
|
||||
The `.cfg` includes are wired up **for** you: `printer.base.cfg` ends with the
|
||||
seven `[include ff-*.cfg]` lines, so a flash brings the mod up by itself. Your
|
||||
`printer.cfg` is not touched. Run `FF_IMPORT_FIRMWARE_CONFIG` once after the
|
||||
first flash to pull in the factory dock and nozzle numbers.
|
||||
`printer.cfg` is not touched. The factory dock and nozzle numbers are pulled
|
||||
in for you too: on the first boot after the flash, `[ff_legacy]` imports
|
||||
firmwareExe's per-unit JSON and persists it with its own `SAVE_CONFIG`, so
|
||||
Klipper restarts once, right after coming up — before the screen is on its
|
||||
feet. `TOOL_OFFSET_STATUS` afterwards should show a nozzle triple for every
|
||||
tool and no `NOT CALIBRATED`.
|
||||
|
||||
The included files are the mod's, and every update overwrites them without
|
||||
asking — `ff-*.cfg` and `printer.base.cfg` alike. Do not edit them. Put changes
|
||||
|
||||
+24
-19
@@ -27,10 +27,10 @@ documented in the file headers.
|
||||
| [`payload/klipper/extras/ff_print.py`](../payload/klipper/extras/ff_print.py) | `/usr/prog/klipper/klippy/extras/` | `[ff_print]` — takes over `SDCARD_PRINT_FILE` and `M23`, reads bed/nozzle/initial tool/first-layer height out of the file itself, and calls `FF_BEFORE_PRINT_START` before the file's first line and `FF_AFTER_PRINT_END` once the job leaves the printing state. Declared in `ff-print-macros.cfg`; holds no policy of its own |
|
||||
| [`payload/klipper/extras/ff_tool.py`](../payload/klipper/extras/ff_tool.py) | `/usr/prog/klipper/klippy/extras/` | `[ff_tool n]` — one section per tool; `dock_x/dock_y`, `nozzle_x/y/z` and `z_adjust` are all autosaved (import or calibration + `SAVE_CONFIG`) |
|
||||
| [`payload/klipper/extras/ff_tool_offset.py`](../payload/klipper/extras/ff_tool_offset.py) | `/usr/prog/klipper/klippy/extras/` | `TOOL_OFFSET_CALIBRATE` / `STATION_CALIBRATE` / `TOOL_OFFSET_STATUS` — the touchscreen's nozzle XY/Z offset calibration, recovered from the binary and reimplemented in Klipper |
|
||||
| [`payload/klipper/extras/ff_legacy.py`](../payload/klipper/extras/ff_legacy.py) | `/usr/prog/klipper/klippy/extras/` | `FF_IMPORT_FIRMWARE_CONFIG` — one-shot import of the factory/touchscreen JSON into Klipper config. Needed once, at install |
|
||||
| [`payload/klipper/extras/ff_legacy.py`](../payload/klipper/extras/ff_legacy.py) | `/usr/prog/klipper/klippy/extras/` | `FF_IMPORT_FIRMWARE_CONFIG` — one-shot import of the factory/touchscreen JSON into Klipper config. Runs (and saves) by itself on the first boot; the command remains for doing it by hand |
|
||||
| [`payload/klipper/config/ff-toolchange.cfg`](../payload/klipper/config/ff-toolchange.cfg) | `/usr/data/config/` | empty `[ff_tool 0..3]` sections (the per-unit dock/nozzle data is autosaved, nothing unit-specific ships), `[ff_toolchange]` feeds/geometry, the `G28` dock-first wrapper |
|
||||
| [`payload/klipper/config/ff-tool-offset.cfg`](../payload/klipper/config/ff-tool-offset.cfg) | `/usr/data/config/` | `[ff_tool_offset]` — probe geometry and guards for the calibration commands |
|
||||
| [`payload/klipper/config/ff-legacy.cfg`](../payload/klipper/config/ff-legacy.cfg) | `/usr/data/config/` | `[ff_legacy]` — stays included permanently; with `auto_import` it re-imports on every startup until a tool has a saved nozzle position, then does nothing |
|
||||
| [`payload/klipper/config/ff-legacy.cfg`](../payload/klipper/config/ff-legacy.cfg) | `/usr/data/config/` | `[ff_legacy]` — stays included permanently; with `auto_import` it imports at startup until a tool has a saved nozzle position, and with `auto_save` the first import `SAVE_CONFIG`s itself (one restart, before the UI is up), after which it does nothing |
|
||||
| [`payload/klipper/config/ff-print-macros.cfg`](../payload/klipper/config/ff-print-macros.cfg) | `/usr/data/config/` | `START_PRINT` / `END_PRINT` / `PAUSE` / `RESUME` / `CANCEL_PRINT`, reconstructed from the app's sequences, plus the `_FF_PREFLIGHT` calibration and tool-presence gate; declares `[ff_print]` and the `FF_BEFORE_PRINT_START` / `FF_AFTER_PRINT_END` entry points it calls |
|
||||
| [`payload/klipper/config/ff-filament.cfg`](../payload/klipper/config/ff-filament.cfg) | `/usr/data/config/` | `LOAD_FILAMENT` / `UNLOAD_FILAMENT` / `PURGE` — the touchscreen's filament-load sequence (grab tool, purge chute, feed) recovered from the binary; unload is a designed retract (the stock app has none) |
|
||||
| [`payload/klipper/config/ff-runout.cfg`](../payload/klipper/config/ff-runout.cfg) | `/usr/data/config/` | Runout / clog handling: gives the stock `fd_ex*` / `fm_ex*` sensors a `runout_gcode` that pauses a Mainsail print when the **mounted** tool runs out or clogs (the app's E0162 / E0163, reported here in plain words); `ff_toolchange` arms only the mounted tool's sensors |
|
||||
@@ -96,7 +96,8 @@ file. That is also why the includes live in `printer.base.cfg` and not there
|
||||
— and it makes the undo button work, since flashing the stock FlashForge
|
||||
package restores its own `printer.base.cfg` and the includes go with it.
|
||||
|
||||
Flash, then jump to step 5 below to import your unit's calibration.
|
||||
Flash, and your unit's calibration imports itself on the first boot — step 5
|
||||
below says how, and step 6 how to check it.
|
||||
|
||||
### By hand
|
||||
|
||||
@@ -127,7 +128,7 @@ scp payload/klipper/config/ff-*.cfg \
|
||||
[include ff-print-macros.cfg]
|
||||
[include ff-runout.cfg] ; after printer.base.cfg (overrides its sensor sections)
|
||||
[include ff-chamber.cfg] ; M141/M191; follows whatever printer.chamber.cfg declared
|
||||
[include ff-legacy.cfg] ; harmless to leave in — see step 8
|
||||
[include ff-legacy.cfg] ; harmless to leave in — see step 7
|
||||
```
|
||||
|
||||
> **If you put these in `printer.cfg` instead, they must go ABOVE the
|
||||
@@ -148,30 +149,34 @@ scp payload/klipper/config/ff-*.cfg \
|
||||
|
||||
4. `RESTART` (or reboot).
|
||||
|
||||
5. Import the factory/touchscreen calibration once:
|
||||
5. The factory/touchscreen calibration imports itself on that startup:
|
||||
`[ff_legacy]` reads `extruder.json` / `test.json` / `zoffset.json`, stages
|
||||
**your unit's** dock coordinates, nozzle and station values, station start
|
||||
point and any per-tool Z tune, and — with `auto_save`, the default —
|
||||
persists them with its own `SAVE_CONFIG` into `printer.cfg`'s `#*#` block.
|
||||
That save restarts Klipper once more, straight after ready, which on a
|
||||
flashed printer lands before HelixScreen is up: the wizard never meets an
|
||||
uncalibrated machine. The manual form, for inspecting or with
|
||||
`auto_save: False`:
|
||||
|
||||
```gcode
|
||||
FF_IMPORT_FIRMWARE_CONFIG
|
||||
FF_IMPORT_FIRMWARE_CONFIG ; APPLY=0 to only print, stage nothing
|
||||
SAVE_CONFIG
|
||||
```
|
||||
|
||||
This reads `extruder.json` / `test.json` / `zoffset.json` and stages
|
||||
**your unit's** dock coordinates, nozzle and station values, station
|
||||
start point and any per-tool Z tune for `SAVE_CONFIG`. Nothing to
|
||||
paste; it only prints an `[ff_toolchange]` snippet if a feed in the JSON
|
||||
differs from the running config (it does not on a stock unit).
|
||||
Nothing to paste either way; the import only prints an `[ff_toolchange]`
|
||||
snippet if a feed in the JSON differs from the running config (it does not
|
||||
on a stock unit).
|
||||
|
||||
6. `SAVE_CONFIG` — persists the imported values into `printer.cfg`'s `#*#`
|
||||
block and restarts.
|
||||
|
||||
7. `TOOLCHANGE_STATUS` and `TOOL_OFFSET_STATUS` — every tool should show a
|
||||
6. `TOOLCHANGE_STATUS` and `TOOL_OFFSET_STATUS` — every tool should show a
|
||||
nozzle triple and a dock position, the station `z` must be present;
|
||||
nothing should say `NOT CALIBRATED`, "no dock position" or "unsaved
|
||||
calibration pending".
|
||||
|
||||
8. Optionally remove the `[include ff-legacy.cfg]` line and `RESTART`. Not
|
||||
required: the import is one-shot, and `ff_legacy`'s startup auto-import
|
||||
returns early as soon as any tool has a nozzle position, so the section is
|
||||
inert once step 6 has run. A flashed printer keeps it included.
|
||||
7. Optionally remove the `[include ff-legacy.cfg]` line and `RESTART`. Not
|
||||
required: the import returns early as soon as any tool has a nozzle
|
||||
position, so the section is inert once the import has been saved. A
|
||||
flashed printer keeps it included.
|
||||
|
||||
## Verify
|
||||
|
||||
|
||||
@@ -21,11 +21,14 @@
|
||||
#
|
||||
# Keep it included (payload/klipper/extras/ff_legacy.py must be in
|
||||
# /usr/prog/klipper/klippy/extras/). With auto_import (default) the stock
|
||||
# firmware's nozzle/station/dock numbers are loaded at every startup for as
|
||||
# long as no [ff_tool n] has a saved nozzle position, so a fresh install
|
||||
# starts with the calibration firmwareExe already made and a setup wizard can
|
||||
# skip it. SAVE_CONFIG (after a recalibration, or by hand) persists the
|
||||
# values and ends the auto import. Manual form:
|
||||
# firmware's nozzle/station/dock numbers are loaded at startup for as long as
|
||||
# no [ff_tool n] has a saved nozzle position, so a fresh install starts with
|
||||
# the calibration firmwareExe already made and a setup wizard can skip it.
|
||||
# With auto_save (also default) that first import is persisted on the spot:
|
||||
# klipper runs SAVE_CONFIG itself and restarts once, right after the first
|
||||
# boot's ready and before the screen is up, and neither runs again. Set
|
||||
# auto_save off to keep the values live-but-unsaved until a SAVE_CONFIG of
|
||||
# your own. Manual form:
|
||||
#
|
||||
# FF_IMPORT_FIRMWARE_CONFIG ; reads /usr/data/firmwareRes/config
|
||||
# SAVE_CONFIG ; persists nozzle_*/station_*/z_adjust
|
||||
@@ -39,4 +42,5 @@
|
||||
|
||||
[ff_legacy]
|
||||
#auto_import: True
|
||||
#auto_save: True
|
||||
#firmware_config_dir: /usr/data/firmwareRes/config
|
||||
@@ -50,11 +50,11 @@
|
||||
# like pid_* do.
|
||||
#
|
||||
# First install on a machine that was calibrated from the touchscreen: nothing
|
||||
# to include -- printer.base.cfg already ships ff-legacy.cfg, and its
|
||||
# auto_import runs FF_IMPORT_FIRMWARE_CONFIG on every startup until a tool has
|
||||
# a saved nozzle position. Run SAVE_CONFIG to keep the staged factory
|
||||
# dock/nozzle/station numbers. Check with TOOLCHANGE_STATUS before moving
|
||||
# anything.
|
||||
# to include -- printer.base.cfg already ships ff-legacy.cfg, whose
|
||||
# auto_import runs FF_IMPORT_FIRMWARE_CONFIG at startup until a tool has a
|
||||
# saved nozzle position, and whose auto_save then runs SAVE_CONFIG itself on
|
||||
# the first boot (one restart) to keep the staged factory dock/nozzle/station
|
||||
# numbers. Check with TOOLCHANGE_STATUS before moving anything.
|
||||
#
|
||||
# Tool-to-tool offsets are DIFFERENCES of nozzle_* against offset_base (T0),
|
||||
# exactly as CommMgr::setGrabGcodeOffsetMgr computes them:
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
# [ff_legacy] is shipped permanently (printer.base.cfg includes ff-legacy.cfg).
|
||||
# With auto_import -- the default -- _handle_ready re-imports on EVERY
|
||||
# klippy:ready until some tool has a saved nozzle position, so a fresh install
|
||||
# comes up with the calibration firmwareExe already made. Run SAVE_CONFIG once
|
||||
# to persist it; after that the import finds a saved nozzle and does nothing.
|
||||
# comes up with the calibration firmwareExe already made. With auto_save --
|
||||
# also the default -- a successful first import runs SAVE_CONFIG by itself:
|
||||
# klipper restarts once, right after the first ready and before anyone has had
|
||||
# time to reach a screen, and comes back with the values persisted -- after
|
||||
# which the import finds a saved nozzle and neither runs again. Setting
|
||||
# auto_save off restores the old contract: values applied live each boot,
|
||||
# SAVE_CONFIG left to you.
|
||||
#
|
||||
# FF_IMPORT_FIRMWARE_CONFIG [DIR=/usr/data/firmwareRes/config] [APPLY=1]
|
||||
#
|
||||
@@ -124,10 +129,12 @@ class FFLegacy:
|
||||
# auto_import: while no [ff_tool n] carries a nozzle position (nothing
|
||||
# calibrated or saved yet), load the stock firmware's numbers at
|
||||
# startup so the printer -- and a UI's setup wizard -- sees the
|
||||
# calibration firmwareExe already made. Applied live and staged; the
|
||||
# next SAVE_CONFIG (e.g. after a recalibration) persists it, after
|
||||
# which this no longer triggers.
|
||||
# calibration firmwareExe already made. Applied live and staged; with
|
||||
# auto_save the staging is persisted immediately (see _handle_ready),
|
||||
# otherwise the next SAVE_CONFIG (e.g. after a recalibration) does it.
|
||||
# Either way, once saved this no longer triggers.
|
||||
self.auto_import = config.getboolean('auto_import', True)
|
||||
self.auto_save = config.getboolean('auto_save', True)
|
||||
self.auto_import_dir = config.get('firmware_config_dir',
|
||||
FIRMWARE_CONFIG_DIR)
|
||||
self.printer.register_event_handler('klippy:ready',
|
||||
@@ -143,15 +150,50 @@ class FFLegacy:
|
||||
if not os.path.exists(os.path.join(self.auto_import_dir,
|
||||
'extruder.json')):
|
||||
return
|
||||
# Read BEFORE the import stages anything: SAVE_CONFIG commits every
|
||||
# staged value, so if something else is already pending it is not ours
|
||||
# to commit and the automatic save stands down.
|
||||
reactor = self.printer.get_reactor()
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
pending_before = configfile.get_status(
|
||||
reactor.monotonic()).get('save_config_pending')
|
||||
try:
|
||||
out = self._import(self.auto_import_dir, apply=True)
|
||||
except Exception as e:
|
||||
logging.warning("%s: auto import failed: %s", self.name, e)
|
||||
return
|
||||
logging.info("%s: auto import\n%s", self.name, "\n".join(out))
|
||||
self.gcode.respond_info(
|
||||
"%s: imported the stock firmware's calibration (%s); run"
|
||||
" SAVE_CONFIG to keep it" % (self.name, self.auto_import_dir))
|
||||
# Only restart for values that actually landed: a JSON with no tool
|
||||
# data stages nothing worth a restart, and calibrated() turning true
|
||||
# is also what stops this from ever running a second time.
|
||||
landed = any(
|
||||
t is not None and t.calibrated()
|
||||
for t in (self.printer.lookup_object('ff_tool %d' % n, None)
|
||||
for n in range(EXTRUDER_COUNT)))
|
||||
if self.auto_save and landed and not pending_before:
|
||||
self.gcode.respond_info(
|
||||
"%s: imported the stock firmware's calibration (%s);"
|
||||
" persisting with SAVE_CONFIG -- klipper restarts once"
|
||||
% (self.name, self.auto_import_dir))
|
||||
# After the ready handlers, not during them: SAVE_CONFIG restarts
|
||||
# klippy, and the other handlers deserve to finish first. One
|
||||
# reactor callback later is still seconds before any UI is up.
|
||||
reactor.register_callback(self._auto_save)
|
||||
else:
|
||||
self.gcode.respond_info(
|
||||
"%s: imported the stock firmware's calibration (%s); run"
|
||||
" SAVE_CONFIG to keep it" % (self.name, self.auto_import_dir))
|
||||
|
||||
def _auto_save(self, eventtime):
|
||||
try:
|
||||
self.gcode.run_script("SAVE_CONFIG")
|
||||
except Exception as e:
|
||||
# A failed save does not restart, so there is no loop here: the
|
||||
# printer runs on the live-applied values and the next boot tries
|
||||
# again, exactly as a manual SAVE_CONFIG failure would leave it.
|
||||
logging.warning("%s: automatic SAVE_CONFIG failed (%s) -- the"
|
||||
" imported values are live but unsaved; run"
|
||||
" SAVE_CONFIG by hand", self.name, e)
|
||||
|
||||
cmd_FF_IMPORT_FIRMWARE_CONFIG_help = (
|
||||
"Import firmwareExe's extruder/test/zoffset.json into [ff_tool n] /"
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
"""ff_legacy's automatic import-and-save at first boot.
|
||||
|
||||
The contract under test: on a printer fresh from stock firmware -- no
|
||||
[ff_tool n] has a saved nozzle position -- the first klippy:ready imports
|
||||
firmwareExe's per-unit JSON and, with auto_save (the default), persists it
|
||||
with a SAVE_CONFIG that klipper issues itself. Once. The restart that save
|
||||
causes is exactly the point: it happens right after ready, before a UI is up,
|
||||
so the wizard never sees an uncalibrated machine.
|
||||
|
||||
These run against payload/klipper/extras/ff_legacy.py with the klippy objects
|
||||
stubbed -- the extra only touches gcode, configfile, reactor and the ff_tool
|
||||
objects, all narrow enough to fake honestly. The replica lane cannot cover
|
||||
this: klippy itself does not run there.
|
||||
"""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def _load_ff_legacy():
|
||||
path = os.path.join(ROOT, "payload", "klipper", "extras", "ff_legacy.py")
|
||||
spec = importlib.util.spec_from_file_location("ff_legacy", path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
ff_legacy = _load_ff_legacy()
|
||||
|
||||
|
||||
class FakeReactor:
|
||||
def __init__(self):
|
||||
self.callbacks = []
|
||||
|
||||
def monotonic(self):
|
||||
return 0.0
|
||||
|
||||
def register_callback(self, cb):
|
||||
self.callbacks.append(cb)
|
||||
|
||||
def run_pending(self):
|
||||
cbs, self.callbacks = self.callbacks, []
|
||||
for cb in cbs:
|
||||
cb(self.monotonic())
|
||||
|
||||
|
||||
class FakeConfigfile:
|
||||
def __init__(self):
|
||||
self.staged = {}
|
||||
self.pending = False
|
||||
|
||||
def set(self, section, option, value):
|
||||
self.staged[(section, option)] = value
|
||||
self.pending = True
|
||||
|
||||
def get_status(self, eventtime):
|
||||
return {"save_config_pending": self.pending}
|
||||
|
||||
|
||||
class FakeGcode:
|
||||
def __init__(self):
|
||||
self.commands = {}
|
||||
self.messages = []
|
||||
self.scripts = []
|
||||
self.fail_scripts = False
|
||||
|
||||
def register_command(self, name, fn, desc=None):
|
||||
self.commands[name] = fn
|
||||
|
||||
def respond_info(self, msg):
|
||||
self.messages.append(msg)
|
||||
|
||||
def run_script(self, script):
|
||||
if self.fail_scripts:
|
||||
raise RuntimeError("shutdown")
|
||||
self.scripts.append(script)
|
||||
|
||||
|
||||
class FakeTool:
|
||||
def __init__(self):
|
||||
self.nozzle = None
|
||||
self.dock_x = None
|
||||
self.dock_y = None
|
||||
self.z_adjust = 0.0
|
||||
|
||||
def calibrated(self):
|
||||
return self.nozzle is not None
|
||||
|
||||
|
||||
class FakePrinter:
|
||||
def __init__(self, objects, reactor):
|
||||
self.objects = objects
|
||||
self.reactor = reactor
|
||||
self.handlers = {}
|
||||
|
||||
def lookup_object(self, name, default="raise"):
|
||||
if name in self.objects:
|
||||
return self.objects[name]
|
||||
if default == "raise":
|
||||
raise KeyError(name)
|
||||
return default
|
||||
|
||||
def register_event_handler(self, event, fn):
|
||||
self.handlers[event] = fn
|
||||
|
||||
def get_reactor(self):
|
||||
return self.reactor
|
||||
|
||||
|
||||
class FakeConfig:
|
||||
def __init__(self, printer, values):
|
||||
self.printer = printer
|
||||
self.values = values
|
||||
|
||||
def get_printer(self):
|
||||
return self.printer
|
||||
|
||||
def get_name(self):
|
||||
return "ff_legacy"
|
||||
|
||||
def getboolean(self, name, default):
|
||||
return self.values.get(name, default)
|
||||
|
||||
def get(self, name, default=None):
|
||||
return self.values.get(name, default)
|
||||
|
||||
|
||||
def write_firmware_json(directory):
|
||||
"""The three files firmwareExe keeps, in their real not-quite-JSON shape:
|
||||
a JSON object followed by a trailing C comment."""
|
||||
ext = {"x_station_pos": 100.0, "y_station_pos": 200.0, "z_station_pos": -3.0}
|
||||
for n in range(4):
|
||||
sfx = "" if n == 0 else "%d" % n
|
||||
ext["x_check_pos" + sfx] = 10.0 + n
|
||||
ext["y_check_pos" + sfx] = 20.0 + n
|
||||
ext["t%d_offset_x" % n] = 1.0 + n
|
||||
ext["t%d_offset_y" % n] = 2.0 + n
|
||||
ext["t%d_offset_z" % n] = 3.0 + n
|
||||
for name, obj in (("extruder", ext), ("test", {}), ("zoffset", {})):
|
||||
with open(os.path.join(directory, name + ".json"), "w") as fh:
|
||||
fh.write(json.dumps(obj) + "\n/* firmwareExe trailer */\n")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rig(tmp_path):
|
||||
write_firmware_json(str(tmp_path))
|
||||
reactor = FakeReactor()
|
||||
gcode = FakeGcode()
|
||||
configfile = FakeConfigfile()
|
||||
objects = {"gcode": gcode, "configfile": configfile}
|
||||
for n in range(4):
|
||||
objects["ff_tool %d" % n] = FakeTool()
|
||||
printer = FakePrinter(objects, reactor)
|
||||
return printer, reactor, gcode, configfile, str(tmp_path)
|
||||
|
||||
|
||||
def make(printer, values):
|
||||
config = FakeConfig(printer, values)
|
||||
return ff_legacy.FFLegacy(config)
|
||||
|
||||
|
||||
def ready(printer):
|
||||
printer.handlers["klippy:ready"]()
|
||||
|
||||
|
||||
def test_first_boot_imports_and_saves(rig):
|
||||
printer, reactor, gcode, configfile, jdir = rig
|
||||
make(printer, {"firmware_config_dir": jdir})
|
||||
ready(printer)
|
||||
# applied live: every tool got its nozzle triple
|
||||
for n in range(4):
|
||||
assert printer.objects["ff_tool %d" % n].nozzle == (1.0 + n, 2.0 + n, 3.0 + n)
|
||||
# staged for the save that is about to happen
|
||||
assert ("ff_tool 0", "nozzle_x") in configfile.staged
|
||||
assert ("ff_tool_offset", "station_z") in configfile.staged
|
||||
# the save is deferred to after the ready handlers, then actually issued
|
||||
assert gcode.scripts == []
|
||||
assert len(reactor.callbacks) == 1
|
||||
reactor.run_pending()
|
||||
assert gcode.scripts == ["SAVE_CONFIG"]
|
||||
assert any("restarts once" in m for m in gcode.messages)
|
||||
|
||||
|
||||
def test_never_a_second_time(rig):
|
||||
printer, reactor, gcode, configfile, jdir = rig
|
||||
make(printer, {"firmware_config_dir": jdir})
|
||||
ready(printer)
|
||||
reactor.run_pending()
|
||||
# after the save-restart the tools come back calibrated; ready again must
|
||||
# neither re-import nor schedule another save -- this is the "once"
|
||||
ready(printer)
|
||||
assert reactor.callbacks == []
|
||||
assert gcode.scripts == ["SAVE_CONFIG"]
|
||||
|
||||
|
||||
def test_auto_save_off_keeps_the_old_contract(rig):
|
||||
printer, reactor, gcode, configfile, jdir = rig
|
||||
make(printer, {"firmware_config_dir": jdir, "auto_save": False})
|
||||
ready(printer)
|
||||
assert reactor.callbacks == []
|
||||
assert gcode.scripts == []
|
||||
assert any("run SAVE_CONFIG to keep it" in m for m in gcode.messages)
|
||||
# the values are still applied live, exactly as before
|
||||
assert printer.objects["ff_tool 0"].nozzle == (1.0, 2.0, 3.0)
|
||||
|
||||
|
||||
def test_someone_elses_pending_save_is_not_committed(rig):
|
||||
printer, reactor, gcode, configfile, jdir = rig
|
||||
configfile.pending = True # staged by something else before ready
|
||||
make(printer, {"firmware_config_dir": jdir})
|
||||
ready(printer)
|
||||
assert reactor.callbacks == []
|
||||
assert gcode.scripts == []
|
||||
assert any("run SAVE_CONFIG to keep it" in m for m in gcode.messages)
|
||||
|
||||
|
||||
def test_no_json_no_import_no_restart(rig, tmp_path):
|
||||
printer, reactor, gcode, configfile, _ = rig
|
||||
empty = tmp_path / "empty"
|
||||
empty.mkdir()
|
||||
make(printer, {"firmware_config_dir": str(empty)})
|
||||
ready(printer)
|
||||
assert reactor.callbacks == []
|
||||
assert configfile.staged == {}
|
||||
assert gcode.messages == []
|
||||
|
||||
|
||||
def test_nothing_landed_no_restart(rig, tmp_path):
|
||||
# extruder.json exists but carries no usable tool data: importing stages
|
||||
# nothing a restart would be worth, so no save is scheduled.
|
||||
printer, reactor, gcode, configfile, _ = rig
|
||||
bare = tmp_path / "bare"
|
||||
bare.mkdir()
|
||||
with open(os.path.join(str(bare), "extruder.json"), "w") as fh:
|
||||
fh.write('{"irrelevant": 1}\n/* trailer */\n')
|
||||
make(printer, {"firmware_config_dir": str(bare)})
|
||||
ready(printer)
|
||||
assert reactor.callbacks == []
|
||||
assert gcode.scripts == []
|
||||
|
||||
|
||||
def test_failed_save_does_not_raise(rig):
|
||||
# SAVE_CONFIG can refuse (read-only config, a conflict); the callback must
|
||||
# swallow it -- the values are live, the next boot retries, and a failed
|
||||
# save never restarted klippy so there is no loop to fear.
|
||||
printer, reactor, gcode, configfile, jdir = rig
|
||||
gcode.fail_scripts = True
|
||||
make(printer, {"firmware_config_dir": jdir})
|
||||
ready(printer)
|
||||
reactor.run_pending() # must not raise
|
||||
assert gcode.scripts == []
|
||||
Reference in new issue
Block a user