Printer 'User Settings' Proposal

The User Problem

Currently a klipper printer lacks Gnosis - the self awareness of how the user is currently using the printer. Other printers generally know and remember:

  • If filament is loaded or not
  • The type of filament loaded
  • The filaments temperature and bed temperature
  • The diameter of the nozzle installed
  • The removable build sheet installed
  • The build sheets “squish” - additional z-offset required for a good first layer

All of these things are needed to implement functionality common in a modern printer:

  • Basic print safety checks:
    • Filament is installed, the filament matched the print file filament type
    • Print file nozzle size matches the installed nozzle
    • Build sheet installed matches the print file
  • Pre-heating / heat soaking the extruder and heater bed
  • Automating filament loading and unloading
  • Automating babystepping when build sheets are swapped
  • Automating safe purge lines for different nozzle widths

For the sake of needing a term I’ll calling these things ‘User Settings’. Klipper currently lacks a place for klippy modules to store user settings that are not true Config. User settings differ from Config in 3 ways:

  • A printer restart is not required to make the change, users don’t expect a workflow interruption.
  • The user expects the printer to remember the setting between restarts
  • Users don’t expect to have to confirm or save the setting for it to be remembered

Currently all front ends for klipper get around this limitation by asking the user to supply this information repeatedly every time it is required (e.g. filament temperatures). Or they simply omit basic functionality (build sheets, nozzles, print safety checks).

Over the past years I’ve implemented all of these ideas in macros and extensions (filaments, build sheets, nozzles). They all store their state in the save_variables file. This works, but only if save_variables is configured and set up to be stored in a separate file. Programming against save_variables has downsides. If the user doesn’t set things up as required is causes a lot of confusion. This is not a good solution for extensions.

Proposal

TLDR: Extend the config object with a user_settings property that implements the ConfigWrapper interface. Automatically save user settings in a separate file when user_settings.set(key, value) is used.

File Storage

The user settings file will be stored in a file named <config-name>.user_settings.cfg in the same location as the printers config file. e.g. if the printers config file is:
/usr/pi/printer-data/printer.cfg
The settings file will be:
/usr/pi/printer-data/printer.user_settings.cfg

If the file does not exist it is automatically created when something is saved. Whenever user settings are updated the file is silently written.

Programming Interface

User settings are accessible to a klippy module via the config object:

class MyModule:
    def __init__(self, config)
        user_settings = config.user_settings

The user settings interface matches the existing ConfigWrapper interface, this makes retrieving data from user_settings use the same patterns as retrieving data from config. This is already familiar to klipper developers:

user_settings.getint()
user_settings.getfloat()
# etc.

Default Values & Read Fall-through

User settings have fall-through on read behavior. If the setting is not available in the user settings file, it will be looked up in the config file in the same section as the module. This allows for authoring config to specify default values that override the default in the get* method calls. Here is a real world example: the most popular nozzle size is 0.4, so we might default the nozzle size in code to this value:

nozzle_diameter = user_settings.getfloat('installed_nozzle_diameter', default=0.4)

If you wanted to create a clay/chocolate printer with a default nozzle diameter of 2.0mm, you can specify that in the printer.cfg. This means printer designers never have to write to the settings file to change default printer behavior.

Saving Settings

Settings are saved via a set method on the settings object:

   user_settings.set('key', python_string_value)

When this happens the user settings file is written immediately.

Macro Access

The user setting’s current value is exposed via the printer model object. This means the storage value and the exposed model don’t have to match exactly if that is not desirable (storage format is an abstraction for the module). The values can be placed wherever is most appropriate for the object model.

If a macro author wants to read the settings directly, they can do this via the configfile.user_settings.

FAQ

user_settings is too long!
I agree :sob:. I’m not married to the name, please propose a better one. config_file currently exposes a settings field so that name was already taken. Maybe props?

Why not store settings in save_variables?

  • save_variables is accessible to macros meaning the data is vulnerable to being overwritten by macro authors.
  • save_variables lacks a namespacing scheme to stop multiple modules from overwriting each others settings. Config solves this problem.
  • Its optional, meaning access can fail
  • If its stored in the default location, inside printer config, it requires user action to save settings.

Why not use a structured database like SQLite?

  • It’s not clear there is a benefit based on the small number of things we want to actually store.
  • Moonraker already has a such a structured data store. For large amounts of data maybe that’s a better place to do it?
  • If your idea has specific storage requirements you can implement your own storage ala Spoolman.
  • We can re-use what we already have in config_file.py to implement this quickly.
  • Module and extensions authors wont have to learn a new API
4 Likes

In fact, I fully support this suggestion.

Just recently, I was thinking about a way to automate heat soaking in a smart way.
My typical workflow is:

  1. Turn on the printer and start heating the bed/chamber to the later intended temperature.
  2. Prepare the model, e.g. some CAD, slicing etc.
  3. Level / bed-mesh the printer at temperature
  4. Start printing

I would like to have a conditional “I’m already heat-soaked” flag (depending on temperature AND time) that could be used in the START_PRINT macro to determine if the printer is properly heat-soaked or not.
If not, continue soaking until the defined conditions, i.e. N minutes at M temperature, are met.

I have not found a satisfactory solution yet. If I understand your suggestion, this would be a good step towards facilitating such ideas.

Thanks for putting the work on such a detailed proposal.

Personally, I see a lot of advantages on having something like this available, so it has my :+1: all the way!

1 Like

I had that problem as well. I wrote a really complicated macro to solve this:

  • It uses a second temp sensor to see when the temperature on top of the bed stabilizes. If its already hot & stable the wait is quickly bypassed.
  • It pauses the print while it waits which leaves the machine interactive. If you hit the resume button it will continue with the print. Everything runs in a background task to make this work.

I think you are more describing “state” vs “settings”. One of the difficulties with using a saved variable as you describe for this is that the state degrades over time. If I shut the printer off over night, or don’t print for an hour, its no longer hot. Saved state is only good if I have some way of validating that state is still accurate. And if I can do that, I can probably measure the state directly.

Lastly, but maybe most importantly, why did you restart the printer? If we could reduce the number of restarts maybe this is less of an issue. My most frequent restart cause is editing a macro. I keep meaning to give DynamicMacros a try.

My thought is to keep it simple like a [instance_on] binary of “on” equals a 10 minute heatsoak when the printer is turned on. Or, inclusion of heatsoak=on as pre-print condition.

I would not use a condition where the print bed temp is constantly met, though.

Interesting, thanks.

I agree that Klipper could do a better job of handling “filament/print type settings” from “physical printer description”. Currently it all seems mixed together in a printer.cfg file.

For what it is worth, though, I’m not sure how these types of “filament/print settings” should make its way into Klipper. As a general rule, the software can’t auto-detect them (eg, the software can’t know which build plate is on the printer). So, it would seem there would need to be some “workflow” for users to enter the information into the computer for it to be stored. It seems to me that the technical challenge of storing that information is small relative to the challenge of implementing that workflow. In particular, with such a wide variety of printer types, I’m not sure what items need storing, and I’m not sure how tolerant users will be if they are prompted to provide additional information.

On the technical side, we could certainly make save_variables an automatically loaded module (or replace it with a similar module that is auto-loaded). We could also attempt to define certain core variables that are expected to be common (as is done, for example, with the printer.print_status.info information).

Cheers,
-Kevin

1 Like

I don’t think you, or I, have to solve that. That’s what the extension system will solve. Its up to extension/extra authors to decide which things are user settings vs printer config and to provide the ‘workflow’ (gcode commands, GUI elements) to enter these values.

I see this as an enabler, a missing part of the extensions/extras API.

If the core klipper system doesn’t provide this storage capability in a consistent way, you’ll get a hodgepodge of alternate solutions. Save variables, saving to custom per-extension files etc. And then there will be no hope of any kind of consistent access to the settings.

3 Likes

I’ve let your proposal “marinate” for a while and here are my thoughts.

The idea of “pre-flight checks” is okay, I guess, as long as it deals with printer operations only and nothing concerning the print (materials, configs) itself. There are many variables that are discovered/stored in the slicer and communicated through GCODE that it does not need to be repeated in Klipper.

So something like “soak” is more of an operation of the printer and not necessarily pertaining to any print job. I can manually set bed temp through a UI to the machine, via Moonraker/Klipper. And right now there is a time-out so the bed doesn’t stay heated forever. Having soak as a startup command is interesting, but I would also have a time-out paired with it. Another idea is that I would like to have the option to keep the bed and nozzle temp at settings for 5-10 minutes for serial printing. That is, I don’t want the bed and nozzle to be depowered while I swap or remove the print from the bed and start up a new print job. Right now I manually reset the bed temp after a print has finished, go to the printers, swap beds, and then get back to my interface to start the new job. I guess I could set my workspace right next to the printer but I won’t because I don’t have proper venting hoods for printing polystyrene filaments. I could use my phone but I don’t have access to my slicers or CAD on it.

Another thought - Perhaps I could perform a batch print job action which has a 5-10 grace period to perform the manual actions of prepping the bed for the next print. This I think could be in Klipper because the operation is not tied to a specific print or series of prints. It doesn’t care what is being printed; it concerns itself only to the mechanical operations of the assigned printer. If there is no consecutive print job, the machine depowers the assigned components.

But something like nozzle size, filament type comes from the slicer’s printer configuration and not something that would need to be stored in Klipper’s interface or backend. What a filament prints at 195 on my machine may print at 200 for anyone else. I’ve built up a catalog of filaments in Orca and Cura. I don’t need or want another database that I have to worry about being synced with my slicers.

As for build sheets, this is currently a (user) manual process. Klipper won’t know if you swapped them or not. The only thing it knows, like when you forget to put a bed back in, is that the bed mesh has changed. Even then Klipper really doesn’t know it is different because it doesn’t compare the previous mesh to the new one.

I am not trying to harsh on your idea, I just wonder if it is really necessary to add a “this won’t directly wreck Klipper but more of a other” folder to Klipper. Because I think there’s too much “other” right now with companies selling “klipperized” versions dedicated solely to their own models - and these “others” have to do with the printer’s mechanical properties like bed mesh, filament swaps, etc.

I guess my last thought is when will we see a slicer with built-in Klipper? Or for that matter, a CAD program with a slicer and Klipper installed as modules? Basically you could then bypass GCODE all together for a binary that gets decoded to signal at the printer. And to me that would be the end of tinkering because the process could be locked down without a chance of open source.

I suggested something like “dynamic variables” a while ago, and am working with a friend on it (I don’t know enough python). To query moonraker’s API and pull in the metadata properties and make them available for use in macro’s (to prevent errors, and having to change start gcode in the slicer to take advantage of data already in the sliced file.)

1 Like