Skip to content

Commit

Permalink
feat: spoolman support (#1119)
Browse files Browse the repository at this point in the history
Signed-off-by: Mathis Mensing <[email protected]>
  • Loading branch information
matmen authored Aug 6, 2023
1 parent e28a958 commit 7e7c8dc
Show file tree
Hide file tree
Showing 41 changed files with 1,308 additions and 9 deletions.
Binary file added docs/assets/images/spoolman-dashboard-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/spoolman-scan-spool.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions docs/features/spoolman.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
layout: default
title: Spool Management
parent: Features
nav_order: 17
permalink: /features/spoolman
---

# Spool Management
{: .no_toc }

## Table of contents
{: .no_toc .text-delta }

1. TOC
{:toc}

---

Fluidd offers support for the [Spoolman](https://github.com/Donkie/Spoolman) filament spool manager.

### Print start
On print start, Fluidd will show a modal asking you to select the spool you want to use for printing.
The modal shows all available (i.e. not archived) spools.
<!-- TODO uncomment when QR scanning is available
A spool can either be selected by selecting it directly, or by scanning an associated QR code using an attached webcam.
![screenshot](/assets/images/spoolman-scan-spool.png)
-->

Automatically opening the spool selection modal can be disabled from the Fluidd settings.

### Dashboard card
The currently selected spool and its metadata is shown in the Spoolman dashboard card.

#### Selecting a different spool
If you need to select another spool during your print (e.g. when your current spool has run out, or you have a multicolor print),
you can do so through the "Change Spool" button in the dashboard card.

![screenshot](/assets/images/spoolman-dashboard-card.png)

### Sanity checks
When starting a print or changing spools, Fluidd will automatically perform these sanity checks and warn you if they fail:
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
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ firmware.
- [Print history](/features/print_history)
- [Version management](/updates/automated) and upgrades
- Utilization graphs
- Filament [spool management](/features/spoolman)

## Supporting Fluidd

Expand Down
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"monaco-textmate": "^3.0.1",
"onigasm": "^2.2.5",
"panzoom": "^9.4.3",
"qr-scanner": "^1.4.2",
"qrcode.vue": "^1.7.0",
"semver": "^7.5.4",
"shlex": "^2.1.2",
Expand Down
3 changes: 3 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@

<file-system-download-dialog />
<updating-dialog />
<spool-selection-dialog />
</v-main>

<app-footer />
Expand All @@ -90,6 +91,7 @@ import FilesMixin from '@/mixins/files'
import BrowserMixin from '@/mixins/browser'
import { LinkPropertyHref } from 'vue-meta'
import FileSystemDownloadDialog from '@/components/widgets/filesystem/FileSystemDownloadDialog.vue'
import SpoolSelectionDialog from '@/components/widgets/spoolman/SpoolSelectionDialog.vue'
@Component<App>({
metaInfo () {
Expand All @@ -105,6 +107,7 @@ import FileSystemDownloadDialog from '@/components/widgets/filesystem/FileSystem
}
},
components: {
SpoolSelectionDialog,
FileSystemDownloadDialog
}
})
Expand Down
27 changes: 27 additions & 0 deletions src/api/socketActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,5 +702,32 @@ export const SocketActions = {
dispatch: 'webcams/onWebcamsList'
}
)
},

async spoolmanState () {
baseEmit(
'server.spoolman.get_spool_id', {
dispatch: 'spoolman/onActiveSpool'
}
)

baseEmit(
'server.spoolman.proxy', {
params: {
request_method: 'GET',
path: '/v1/spool'
},
dispatch: 'spoolman/onAvailableSpools'
}
)
},

async spoolmanSetSpool (spoolId: number | undefined) {
baseEmit(
'server.spoolman.post_spool_id', {
params: { spool_id: spoolId },
dispatch: 'spoolman/onActiveSpool'
}
)
}
}
5 changes: 5 additions & 0 deletions src/components/layout/AppSettingsNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default class AppSettingsNav extends Vue {
{ name: this.$t('app.setting.title.thermal_presets'), hash: '#presets', visible: true },
{ name: this.$t('app.setting.title.gcode_preview'), icon: '$cubeScan', hash: '#gcodePreview', visible: true },
{ name: this.$t('app.general.title.timelapse'), hash: '#timelapse', visible: this.supportsTimelapse },
{ name: this.$t('app.spoolman.title.spoolman'), hash: '#spoolman', visible: this.supportsSpoolman },
{ name: this.$t('app.version.title'), hash: '#versions', visible: this.supportsVersions }
]
}
Expand All @@ -56,5 +57,9 @@ export default class AppSettingsNav extends Vue {
get supportsTimelapse () {
return this.$store.getters['server/componentSupport']('timelapse')
}
get supportsSpoolman () {
return this.$store.getters['spoolman/getSupported']
}
}
</script>
102 changes: 102 additions & 0 deletions src/components/settings/SpoolmanSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<template>
<div>
<v-subheader id="spoolman">
{{ $t('app.spoolman.title.spoolman') }}
</v-subheader>
<v-card
:elevation="5"
dense
class="mb-4"
>
<app-setting
:title="$t('app.spoolman.setting.show_spool_selection_dialog_on_print_start')"
>
<v-switch
v-model="autoSpoolSelectionDialog"
hide-details
class="mt-0 mb-4"
/>
</app-setting>

<!-- TODO uncomment when QR scanning is available
<v-divider />
<app-setting
:title="$tc('app.spoolman.setting.auto_open_qr_camera')"
>
<v-select
v-model="autoOpenQRDetectionCameraId"
filled
dense
single-line
hide-details="auto"
:items="supportedCameras"
/>
</app-setting>
-->

<v-divider />
<app-setting :title="$t('app.setting.label.reset')">
<app-btn
outlined
small
color="primary"
@click="handleReset"
>
{{ $t('app.setting.btn.reset') }}
</app-btn>
</app-setting>
</v-card>
</div>
</template>

<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import { defaultState } from '@/store/config/state'
import StateMixin from '@/mixins/state'
import { CameraConfig } from '@/store/cameras/types'
@Component({
components: {}
})
export default class SpoolmanSettings extends Mixins(StateMixin) {
get autoSpoolSelectionDialog (): boolean {
return this.$store.state.config.uiSettings.spoolman.autoSpoolSelectionDialog
}
set autoSpoolSelectionDialog (value: boolean) {
this.$store.dispatch('config/saveByPath', {
path: 'uiSettings.spoolman.autoSpoolSelectionDialog',
value,
server: true
})
}
get supportedCameras () {
return [
{ text: this.$tc('app.setting.label.none', 0), value: null },
...this.$store.getters['cameras/getEnabledCameras']
.map((camera: CameraConfig) => ({ text: camera.name, value: camera.id, disabled: !camera.enabled || camera.service === 'iframe' }))
]
}
get autoOpenQRDetectionCameraId (): string {
return this.$store.state.config.uiSettings.spoolman.autoOpenQRDetectionCamera
}
set autoOpenQRDetectionCameraId (value: string) {
this.$store.dispatch('config/saveByPath', {
path: 'uiSettings.spoolman.autoOpenQRDetectionCamera',
value,
server: true
})
}
handleReset () {
this.$store.dispatch('config/saveByPath', {
path: 'uiSettings.spoolman',
value: defaultState().uiSettings.spoolman,
server: true
})
}
}
</script>
4 changes: 4 additions & 0 deletions src/components/settings/ToolheadSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,10 @@ export default class ToolHeadSettings extends Mixins(ToolheadMixin) {
return this.$store.getters['printer/getPrinterSettings']('force_move.enable_force_move') ?? false
}
get printerSupportsSpoolman () {
return this.$store.getters['spoolman/getSupported']
}
get showManualProbeDialogAutomatically () {
return this.$store.state.config.uiSettings.general.showManualProbeDialogAutomatically
}
Expand Down
28 changes: 27 additions & 1 deletion src/components/widgets/camera/CameraItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<template v-if="cameraComponent">
<component
:is="cameraComponent"
ref="component-instance"
:camera="camera"
class="camera-image"
@raw-camera-url="rawCameraUrl = $event"
Expand Down Expand Up @@ -53,10 +54,11 @@
</template>

<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator'
import { Component, Vue, Prop, Watch, Ref } from 'vue-property-decorator'
import { CameraConfig } from '@/store/cameras/types'
import { CameraFullscreenAction } from '@/store/config/types'
import { CameraComponents } from '@/dynamicImports'
import CameraMixin from '@/mixins/camera'
@Component({})
export default class CameraItem extends Vue {
Expand All @@ -66,9 +68,33 @@ export default class CameraItem extends Vue {
@Prop({ type: Boolean, required: false, default: false })
readonly fullscreen!: boolean
@Ref('component-instance')
readonly componentInstance!: CameraMixin
rawCameraUrl: string | null = null
framesPerSecond : string | null = null
mounted () {
if (this.$listeners?.frame) {
if (this.componentInstance.streamingElement instanceof HTMLImageElement) {
this.componentInstance.streamingElement.addEventListener('load', () => this.handleFrame())
} else if (this.componentInstance.streamingElement instanceof HTMLVideoElement) {
this.handleFrame(true)
}
}
}
handleFrame (animate = false) {
const element = this.componentInstance?.streamingElement as HTMLImageElement | HTMLVideoElement
if (element) {
this.$emit('frame', element)
}
if (animate) {
requestAnimationFrame(() => this.handleFrame(this.componentInstance.animating))
}
}
@Watch('camera')
onCamera () {
this.rawCameraUrl = ''
Expand Down
1 change: 1 addition & 0 deletions src/components/widgets/camera/services/HlsstreamCamera.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ref="streamingElement"
autoplay
muted
crossorigin="anonymous"
:style="cameraStyle"
/>
</template>
Expand Down
1 change: 1 addition & 0 deletions src/components/widgets/camera/services/IpstreamCamera.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
:src="cameraVideoSource"
autoplay
muted
crossorigin="anonymous"
:style="cameraStyle"
/>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ref="streamingElement"
:src="cameraImageSource"
:style="cameraStyle"
crossorigin="anonymous"
@load="handleImageLoad"
>
</template>
Expand Down
Loading

0 comments on commit 7e7c8dc

Please sign in to comment.