Skip to content

Commit

Permalink
feat(spoolman): multi-tool support (#1324)
Browse files Browse the repository at this point in the history
Signed-off-by: Mathis Mensing <github@matmen.dev>
Co-authored-by: Pedro Lamas <pedrolamas@gmail.com>
  • Loading branch information
matmen and pedrolamas authored Feb 3, 2024
1 parent 0c94cbf commit a7f7622
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 21 deletions.
Binary file added docs/assets/images/spoolman-multitool.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions docs/features/spoolman.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<MACRO_NAME>__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 %}
43 changes: 38 additions & 5 deletions src/components/widgets/spoolman/SpoolSelectionDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
title-shadow
>
<template #title>
<span class="focus--text">{{ $t('app.spoolman.title.spool_selection') }}</span>
<span class="focus--text">$tc('app.spoolman.title.spool_selection', targetMacro ? 2 : 1, { macro: targetMacro })</span>

<v-spacer />

Expand Down Expand Up @@ -122,8 +122,8 @@
<div class="d-flex">
<v-icon
:color="`#${item.filament.color_hex ?? ($vuetify.theme.dark ? 'fff' : '000')}`"
x-large
class="mr-4 flex-column"
size="42px"
class="mr-4 flex-column spool-icon"
>
{{ item.id === selectedSpool ? '$markedCircle' : '$filament' }}
</v-icon>
Expand Down Expand Up @@ -182,7 +182,7 @@
<v-icon class="mr-2">
{{ filename ? '$printer' : '$send' }}
</v-icon>
{{ filename ? $t('app.general.btn.print') : $t('app.spoolman.btn.select') }}
{{ filename ? $t('app.general.btn.print') : $tc('app.spoolman.btn.select', targetMacro ? 2 : 1, { macro: targetMacro }) }}
</app-btn>
</template>

Expand All @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand All @@ -452,6 +484,7 @@ export default class SpoolSelectionDialog extends Mixins(StateMixin, BrowserMixi
this.$router.push({ path: '/' })
}
}
this.open = false
}
Expand Down
109 changes: 104 additions & 5 deletions src/components/widgets/spoolman/SpoolmanCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,87 @@
>
<template #menu>
<app-btn
v-if="!targetableMacros.length"
small
class="ms-1 my-1"
:disabled="!isConnected"
@click="handleSelectSpool"
@click="() => handleSelectSpool()"
>
{{ $t('app.spoolman.label.change_spool') }}
</app-btn>

<v-menu
v-else
bottom
left
offset-y
transition="slide-y-transition"
min-width="150"
>
<template #activator="{ on, attrs, value }">
<app-btn
v-bind="attrs"
small
class="ms-1 my-1"
:disabled="!isConnected"
v-on="on"
>
{{ $t('app.spoolman.label.change_spool') }}
<v-icon
small
class="ml-1"
:class="{ 'rotate-180': value }"
>
$chevronDown
</v-icon>
</app-btn>
</template>
<v-list dense>
<v-list-item @click="() => handleSelectSpool()">
<v-list-item-content>
<v-list-item-title>
{{ $t('app.spoolman.label.active_spool') }}
</v-list-item-title>
</v-list-item-content>

<v-list-item-icon
v-if="activeSpool"
>
<v-icon
:color="getSpoolColor(activeSpool)"
class="spool-icon"
>
$filament
</v-icon>
</v-list-item-icon>
</v-list-item>

<template v-for="macro of targetableMacros">
<v-list-item
:key="macro.name"
:class="{primary: macro.variables?.active}"
@click="() => handleSelectSpool(macro)"
>
<v-list-item-content>
<v-list-item-title>
{{ macro.name }}
</v-list-item-title>
</v-list-item-content>

<v-list-item-icon
v-if="macro.variables.spool_id"
>
<v-icon
:color="getSpoolColor(getSpoolById(macro.variables.spool_id))"
class="spool-icon"
>
$filament
</v-icon>
</v-list-item-icon>
</v-list-item>
</template>
</v-list>
</v-menu>
</template>

<v-progress-linear
Expand Down Expand Up @@ -107,8 +181,9 @@
>
<v-icon
v-if="activeSpool"
:color="activeSpool.filament.color_hex ? `#${activeSpool.filament.color_hex}` : undefined"
:color="getSpoolColor(activeSpool)"
size="110px"
class="spool-icon"
>
$filament
</v-icon>
Expand All @@ -134,17 +209,21 @@
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import StateMixin from '@/mixins/state'
import type { Spool } from '@/store/spoolman/types'
import type { MacroWithSpoolId, Spool } from '@/store/spoolman/types'
import StatusLabel from '@/components/widgets/status/StatusLabel.vue'
import type { Macro } from '@/store/macros/types'
@Component({
components: { StatusLabel }
})
export default class SpoolmanCard extends Mixins(StateMixin) {
labelWidth = '86px'
handleSelectSpool () {
this.$store.commit('spoolman/setDialogState', { show: true })
handleSelectSpool (targetMacro?: Macro) {
this.$store.commit('spoolman/setDialogState', {
show: true,
targetMacro: targetMacro?.name
})
}
get activeSpool (): Spool | null {
Expand All @@ -155,5 +234,25 @@ export default class SpoolmanCard extends Mixins(StateMixin) {
get isConnected (): boolean {
return this.$store.getters['spoolman/getConnected']
}
get targetableMacros (): MacroWithSpoolId[] {
const macros = this.$store.getters['macros/getMacros'] as Macro[]
return macros
.filter((macro): macro is MacroWithSpoolId => macro.variables != null && 'spool_id' in macro.variables)
.map(macro => ({
...macro,
name: macro.name.toUpperCase()
}))
.sort((a, b) => a.name.localeCompare(b.name))
}
getSpoolById (id: number): Spool | undefined {
return this.$store.getters['spoolman/getSpoolById'](id)
}
getSpoolColor (spool?: Spool) {
return `#${spool?.filament.color_hex ?? (this.$vuetify.theme.dark ? 'fff' : '000')}`
}
}
</script>
24 changes: 21 additions & 3 deletions src/components/widgets/toolhead/ToolChangeCommands.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@
v-on="on"
@click="sendGcode(macro.name)"
>
<v-icon
v-if="macro.spoolId && getSpoolById(macro.spoolId)"
class="mr-1 spool-icon"
:color="getSpoolColor(getSpoolById(macro.spoolId))"
>
$filament
</v-icon>
<span
v-if="macro.color"
v-else-if="macro.color"
class="extruder-color mr-1"
:class="{
active: macro.active
Expand All @@ -47,12 +54,14 @@ import { Component, Mixins } from 'vue-property-decorator'
import StateMixin from '@/mixins/state'
import type { GcodeCommands } from '@/store/printer/types'
import type { TranslateResult } from 'vue-i18n'
import type { Spool } from '@/store/spoolman/types'
type ToolChangeCommand = {
name: string,
description: string | TranslateResult,
color?: string,
active?: boolean
active?: boolean,
spoolId?: number
}
@Component({})
Expand All @@ -78,7 +87,8 @@ export default class ToolChangeCommands extends Mixins(StateMixin) {
name: command,
description,
color: macro?.variables?.color ? `#${macro.variables.color}` : undefined,
active: macro?.variables?.active ?? false
active: macro?.variables?.active ?? false,
spoolId: macro?.variables?.spool_id
} satisfies ToolChangeCommand
})
.sort((a, b) => {
Expand All @@ -88,6 +98,14 @@ export default class ToolChangeCommands extends Mixins(StateMixin) {
return numberA - numberB
})
}
getSpoolById (id: number): Spool | undefined {
return this.$store.getters['spoolman/getSpoolById'](id)
}
getSpoolColor (spool: Spool | undefined) {
return `#${spool?.filament.color_hex ?? (this.$vuetify.theme.dark ? 'fff' : '000')}`
}
}
</script>

Expand Down
5 changes: 3 additions & 2 deletions src/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -841,12 +841,13 @@ app:
btn:
manage_spools: Spulen verwalten
scan_code: Code scannen
select: Auswählen
select: 'Auswählen | Für {macro} auswählen'
title:
spool_selection: Spulenauswahl
spool_selection: 'Spulenauswahl | Spulenauswahl für {macro}'
scan_spool: Spule scannen
spoolman: Spoolman
label:
active_spool: Aktive Spule
change_spool: Spule wechseln
comment: Kommentar
device_camera: Gerät
Expand Down
5 changes: 3 additions & 2 deletions src/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -813,12 +813,13 @@ app:
btn:
manage_spools: Manage Spools
scan_code: Scan Code
select: Select
select: 'Select | Select for {macro}'
title:
spoolman: Spoolman
spool_selection: Spool Selection
spool_selection: 'Spool Selection | Spool Selection for macro {macro}'
scan_spool: Scan Spool
label:
active_spool: Active Spool
change_spool: Change Spool
comment: Comment
device_camera: Device
Expand Down
9 changes: 9 additions & 0 deletions src/scss/misc.scss
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,12 @@ input[type=number] {
.constrained-width.quad {
max-width: map-get($container-max-widths, 'xl') * 2;
}

.spool-icon {
stroke: rgba(0,0,0, 0.25);
stroke-width: .025rem;
}

.theme--dark .spool-icon {
stroke: rgba(0,0,0, 0.5);
}
Loading

0 comments on commit a7f7622

Please sign in to comment.