diff --git a/docs/assets/images/spoolman-multitool.png b/docs/assets/images/spoolman-multitool.png new file mode 100644 index 0000000000..ac85254f6b Binary files /dev/null and b/docs/assets/images/spoolman-multitool.png differ diff --git a/docs/features/spoolman.md b/docs/features/spoolman.md index e8627dd741..7c0e3b948b 100644 --- a/docs/features/spoolman.md +++ b/docs/features/spoolman.md @@ -42,3 +42,46 @@ When starting a print or changing spools, Fluidd will automatically perform thes 1) a spool is selected 2) the selected spool has enough filament left on it to finish the print job 3) the selected spool's filament type matches the one selected in the slicer + +### Toolchanger Support +Fluidd supports selecting spools for individual toolchange macros. +For toolchange macros to show up in the "Change Spool" dropdown, simply add a `spool_id` +variable to your toolchange `gcode_macro`s with a default value of `None`. +You will also need to call the [`SET_ACTIVE_SPOOL`](https://moonraker.readthedocs.io/en/latest/configuration#setting-the-active-spool-from-klipper) +macro in an appropriate place in your toolchange macro. + +```yaml +[gcode_macro T0] +variable_spool_id: None +gcode: + ... + SET_ACTIVE_SPOOL ID={ printer['gcode_macro T0'].spool_id } + ... +``` + +![screenshot](/assets/images/spoolman-multitool.png) + +#### Remembering associated spools across restarts +By default, Klipper does not keep track of Gcode macro variables across restarts. +If Fluidd detects a [`[save_variables]`](https://www.klipper3d.org/Config_Reference.html#save_variables) +section in your configuration, it will automatically emit a `SAVE_VARIABLE` command +on spool selection, saving the selected spool to the `__SPOOL_ID` variable. + +You can use the following macro to restore the previous selection after a restart: +{% raw %} +```sh +[delayed_gcode RESTORE_SELECTED_SPOOLS] +initial_duration: 0.1 +gcode: + {% set svv = printer.save_variables.variables %} + {% for object in printer %} + {% if object.startswith('gcode_macro ') and printer[object].spool_id is defined %} + {% set macro = object.replace('gcode_macro ', '') %} + {% set var = (macro + '__SPOOL_ID')|lower %} + {% if svv[var] is defined %} + SET_GCODE_VARIABLE MACRO={macro} VARIABLE=spool_id VALUE={svv[var]} + {% endif %} + {% endif %} + {% endfor %} +``` +{% endraw %} diff --git a/src/components/widgets/spoolman/SpoolSelectionDialog.vue b/src/components/widgets/spoolman/SpoolSelectionDialog.vue index 1f857c42dd..7b58c58104 100644 --- a/src/components/widgets/spoolman/SpoolSelectionDialog.vue +++ b/src/components/widgets/spoolman/SpoolSelectionDialog.vue @@ -6,7 +6,7 @@ title-shadow > @@ -198,7 +198,7 @@ import { Component, Mixins, Watch } from 'vue-property-decorator' import StateMixin from '@/mixins/state' import { SocketActions } from '@/api/socketActions' -import type { Spool } from '@/store/spoolman/types' +import type { MacroWithSpoolId, Spool } from '@/store/spoolman/types' import BrowserMixin from '@/mixins/browser' import QRReader from '@/components/widgets/spoolman/QRReader.vue' import type { CameraConfig } from '@/store/cameras/types' @@ -224,6 +224,10 @@ export default class SpoolSelectionDialog extends Mixins(StateMixin, BrowserMixi onOpen () { if (this.open) { this.selectedSpoolId = this.$store.state.spoolman.activeSpool ?? null + if (this.targetMacro) { + const macro: MacroWithSpoolId | undefined = this.$store.getters['macros/getMacroByName'](this.targetMacro.toLowerCase()) + this.selectedSpoolId = macro?.variables.spool_id ?? null + } if (this.currentFileName) { // prefetch file metadata @@ -327,6 +331,10 @@ export default class SpoolSelectionDialog extends Mixins(StateMixin, BrowserMixi return this.$store.getters['files/getFile'](filepath ? `gcodes/${filepath}` : 'gcodes', filename) } + get targetMacro (): string | undefined { + return this.$store.state.spoolman.dialog.targetMacro + } + get cameras () { const cameras = this.$store.getters['cameras/getEnabledCameras'] .filter((camera: CameraConfig) => camera.service !== 'iframe') @@ -444,6 +452,30 @@ export default class SpoolSelectionDialog extends Mixins(StateMixin, BrowserMixi } } + if (this.targetMacro) { + // set spool_id via SET_GCODE_VARIABLE + const commands = [ + `SET_GCODE_VARIABLE MACRO=${this.targetMacro} VARIABLE=spool_id VALUE=${this.selectedSpool ?? 'None'}` + ] + + const supportsSaveVariables = this.$store.getters['printer/getPrinterConfig']('save_variables') + if (supportsSaveVariables) { + // persist selected spool across restarts + commands.push(`SAVE_VARIABLE VARIABLE=${this.targetMacro.toUpperCase()}__SPOOL_ID VALUE=${this.selectedSpool ?? 'None'}`) + } + + await SocketActions.printerGcodeScript(commands.join('\n')) + + const macro: MacroWithSpoolId | undefined = this.$store.getters['macros/getMacroByName'](this.targetMacro.toLowerCase()) + if (macro?.variables.active) { + // selected tool is active, update active spool + await SocketActions.serverSpoolmanPostSpoolId(this.selectedSpool ?? undefined) + } + + this.open = false + return + } + await SocketActions.serverSpoolmanPostSpoolId(this.selectedSpool ?? undefined) if (this.filename) { await SocketActions.printerPrintStart(this.filename) @@ -452,6 +484,7 @@ export default class SpoolSelectionDialog extends Mixins(StateMixin, BrowserMixi this.$router.push({ path: '/' }) } } + this.open = false } diff --git a/src/components/widgets/spoolman/SpoolmanCard.vue b/src/components/widgets/spoolman/SpoolmanCard.vue index fbeba57a47..fc1b394dc1 100644 --- a/src/components/widgets/spoolman/SpoolmanCard.vue +++ b/src/components/widgets/spoolman/SpoolmanCard.vue @@ -7,13 +7,87 @@ > $filament @@ -134,8 +209,9 @@ diff --git a/src/components/widgets/toolhead/ToolChangeCommands.vue b/src/components/widgets/toolhead/ToolChangeCommands.vue index 748b3b3668..48175394c3 100644 --- a/src/components/widgets/toolhead/ToolChangeCommands.vue +++ b/src/components/widgets/toolhead/ToolChangeCommands.vue @@ -22,8 +22,15 @@ v-on="on" @click="sendGcode(macro.name)" > + + $filament +