Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(spoolman): multi-tool support #1324

Merged
merged 15 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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