feat: add LED / Neopixel support (#1050)

This commit is contained in:
Stefan Dej 2022-10-18 23:44:32 +02:00 committed by GitHub
parent 128baea88c
commit a88c9ba083
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 2074 additions and 34 deletions

43
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.1.0",
"@codemirror/view": "^6.0.3",
"@jaames/iro": "^5.5.2",
"@lezer/highlight": "^1.0.0",
"@sindarius/gcodeviewer": "^3.1.4",
"@types/node": "^18.0.0",
@ -2419,6 +2420,20 @@
"node": ">= 14"
}
},
"node_modules/@irojs/iro-core": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@irojs/iro-core/-/iro-core-1.2.1.tgz",
"integrity": "sha512-p2OvsBSSmidsDsTSkID6jEyXDF7lcyxPrkh3qBzasBZFpjkYd6kZ3yMWai3MlAaQ3F7li/Et7rSJVV09Fpei+A=="
},
"node_modules/@jaames/iro": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/@jaames/iro/-/iro-5.5.2.tgz",
"integrity": "sha512-Fbi5U4Vdkw6UsF+R3oMlPONqkvUDMkwzh+mX718gQsDFt3+1r1jvGsrfCbedmXAAy0WsjDHOrefK0BkDk99TQg==",
"dependencies": {
"@irojs/iro-core": "^1.2.1",
"preact": "^10.0.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
@ -7617,6 +7632,15 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/preact": {
"version": "10.10.6",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.10.6.tgz",
"integrity": "sha512-w0mCL5vICUAZrh1DuHEdOWBjxdO62lvcO++jbzr8UhhYcTbFkpegLH9XX+7MadjTl/y0feoqwQ/zAnzkc/EGog==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -11667,6 +11691,20 @@
"@intlify/shared": "9.2.2"
}
},
"@irojs/iro-core": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@irojs/iro-core/-/iro-core-1.2.1.tgz",
"integrity": "sha512-p2OvsBSSmidsDsTSkID6jEyXDF7lcyxPrkh3qBzasBZFpjkYd6kZ3yMWai3MlAaQ3F7li/Et7rSJVV09Fpei+A=="
},
"@jaames/iro": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/@jaames/iro/-/iro-5.5.2.tgz",
"integrity": "sha512-Fbi5U4Vdkw6UsF+R3oMlPONqkvUDMkwzh+mX718gQsDFt3+1r1jvGsrfCbedmXAAy0WsjDHOrefK0BkDk99TQg==",
"requires": {
"@irojs/iro-core": "^1.2.1",
"preact": "^10.0.0"
}
},
"@jridgewell/gen-mapping": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
@ -15422,6 +15460,11 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"preact": {
"version": "10.10.6",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.10.6.tgz",
"integrity": "sha512-w0mCL5vICUAZrh1DuHEdOWBjxdO62lvcO++jbzr8UhhYcTbFkpegLH9XX+7MadjTl/y0feoqwQ/zAnzkc/EGog=="
},
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View File

@ -31,6 +31,7 @@
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.1.0",
"@codemirror/view": "^6.0.3",
"@jaames/iro": "^5.5.2",
"@lezer/highlight": "^1.0.0",
"@sindarius/gcodeviewer": "^3.1.4",
"@types/node": "^18.0.0",

View File

@ -97,7 +97,9 @@ import {
mdiTune,
mdiVideo3d,
mdiWebcam,
mdiDipSwitch,
} from '@mdi/js'
import SettingsMiscellaneousTab from '@/components/settings/SettingsMiscellaneousTab.vue'
@Component({
components: {
Panel,
@ -113,6 +115,7 @@ import {
SettingsGCodeViewerTab,
SettingsEditorTab,
SettingsTimelapseTab,
SettingsMiscellaneousTab,
},
})
export default class TheSettingsMenu extends Mixins(BaseMixin) {
@ -186,6 +189,11 @@ export default class TheSettingsMenu extends Mixins(BaseMixin) {
name: 'editor',
title: this.$t('Settings.EditorTab.Editor'),
},
{
icon: mdiDipSwitch,
name: 'miscellaneous',
title: this.$t('Settings.MiscellaneousTab.Miscellaneous'),
},
]
if (this.moonrakerComponents.includes('timelapse')) {

View File

@ -0,0 +1,63 @@
<template>
<div>
<div ref="picker"></div>
</div>
</template>
<script lang="ts">
import { Component, Mixins, Prop, Ref, Watch } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import iro from '@jaames/iro'
import { IroColor } from '@irojs/iro-core'
import { ColorPickerProps, IroColorPicker as IroCP } from '@jaames/iro/dist/ColorPicker.d'
@Component
export default class ColorPicker extends Mixins(BaseMixin) {
colorPicker: IroCP | null = null
@Ref('picker')
readonly picker!: HTMLElement
@Prop({ type: [Object, String], default: '#ffffff' })
readonly color!: IroColor
@Prop({ type: Object, default: () => ({}) })
readonly options!: ColorPickerProps
@Watch('color', { deep: true })
colorChanged(value: string) {
if (this.colorPicker && this.colorPicker.color.rgbString !== value) {
this.colorPicker.color.rgbString = value
}
}
get internalOptions(): ColorPickerProps {
return {
...this.options,
color: this.color,
borderWidth: 2,
sliderSize: 16,
}
}
emitColorChange(color: IroColor) {
this.$emit('change', color)
this.$emit('update:color', color)
}
onColorChange(color: IroColor) {
this.emitColorChange(color)
}
mounted() {
this.colorPicker = iro.ColorPicker(this.picker, this.internalOptions)
this.colorPicker.on('color:change', this.onColorChange)
}
beforeDestroy() {
this.colorPicker?.off('color:change', this.onColorChange)
}
}
</script>
<style scoped></style>

View File

@ -0,0 +1,519 @@
<template>
<v-container :class="containerClass">
<v-row>
<v-col class="pb-3">
<v-subheader class="_light-subheader">
<v-icon v-if="(!root || groups.length === 0) && isOn" small left @click="off">
{{ mdiLightbulbOnOutline }}
</v-icon>
<v-icon v-else-if="!root || groups.length === 0" small left @click="on">
{{ mdiLightbulbOutline }}
</v-icon>
<span>{{ name }}</span>
<v-spacer></v-spacer>
<span
v-if="!root || groups.length === 0"
class="_currentState"
:style="currentStateStyle"
@click="boolDialog = true"></span>
</v-subheader>
</v-col>
</v-row>
<template v-if="root && groups.length">
<miscellaneous-light v-for="group in groups" :key="group.id" :object="object" :group="group" />
</template>
<v-dialog v-model="boolDialog" persistent :width="400">
<panel
:title="name"
:icon="mdiLightbulbOutline"
card-class="temperature-edit-heater-dialog"
:margin-bottom="false">
<template #buttons>
<v-btn icon tile @click="boolDialog = false">
<v-icon>{{ mdiCloseThick }}</v-icon>
</v-btn>
</template>
<v-card-text class="pt-6">
<template v-if="presets.length">
<v-row>
<v-col class="light-presets-container pt-0 d-flex flex-wrap flex-row justify-center">
<v-tooltip v-for="preset in presets" :key="preset.id" top>
<template #activator="{ on, attrs }">
<div
:style="presetStyle(preset)"
v-bind="attrs"
v-on="on"
@click="usePreset(preset)"></div>
</template>
<span>{{ preset.name }}</span>
</v-tooltip>
</v-col>
</v-row>
<v-divider class="my-3"></v-divider>
</template>
<v-row>
<v-col class="text-center">
<color-picker
:color="colorRGB"
:options="colorPickerOptions"
@update:color="onColorRGBChanged" />
<color-picker
v-if="existWhite"
:color="colorRGBW"
:options="colorPickerWhiteOptions"
class="mt-3"
@update:color="onColorWhiteChanged" />
</v-col>
<v-col>
<v-row v-if="existRed">
<v-col>
<number-input
:label="$t('Panels.MiscellaneousPanel.Light.Red')"
param="red"
:target="redInt"
:default-value="Math.round(object.initialRed * 255)"
:min="0"
:max="255"
:dec="1"
:step="1"
:output-error-msg="true"
:has-spinner="true"
@submit="onColorInput" />
</v-col>
</v-row>
<v-row v-if="existGreen">
<v-col>
<number-input
:label="$t('Panels.MiscellaneousPanel.Light.Green')"
param="green"
:target="greenInt"
:default-value="Math.round(object.initialGreen * 255)"
:min="0"
:max="255"
:dec="1"
:step="1"
:has-spinner="true"
@submit="onColorInput" />
</v-col>
</v-row>
<v-row v-if="existBlue">
<v-col>
<number-input
:label="$t('Panels.MiscellaneousPanel.Light.Blue')"
param="blue"
:target="blueInt"
:default-value="Math.round(object.initialBlue * 255)"
:min="0"
:max="255"
:dec="1"
:step="1"
:has-spinner="true"
@submit="onColorInput" />
</v-col>
</v-row>
<v-row v-if="existWhite">
<v-col>
<number-input
:label="$t('Panels.MiscellaneousPanel.Light.White')"
param="white"
:target="whiteInt"
:default-value="Math.round(object.initialWhite * 255)"
:min="0"
:max="255"
:dec="1"
:step="1"
:has-spinner="true"
@submit="onColorInput" />
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
</panel>
</v-dialog>
</v-container>
</template>
<script lang="ts">
import { convertName } from '@/plugins/helpers'
import { Component, Mixins, Prop } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import { mdiCloseThick, mdiLightbulbOutline, mdiLightbulbOnOutline } from '@mdi/js'
import { PrinterStateLight } from '@/store/printer/types'
import ColorPicker from '@/components/inputs/ColorPicker.vue'
import { ColorPickerProps } from '@jaames/iro/dist/ColorPicker.d'
import { Debounce } from 'vue-debounce-decorator'
import iro from '@jaames/iro'
import { IroColor } from '@irojs/iro-core'
import { GuiMiscellaneousStateEntryLightgroup, GuiMiscellaneousStateEntryPreset } from '@/store/gui/miscellaneous/types'
interface ColorData {
red: number | null
green: number | null
blue: number | null
white: number | null
}
@Component({
components: {
ColorPicker,
},
})
export default class MiscellaneousLight extends Mixins(BaseMixin) {
mdiCloseThick = mdiCloseThick
mdiLightbulbOutline = mdiLightbulbOutline
mdiLightbulbOnOutline = mdiLightbulbOnOutline
@Prop({ type: Object, required: true })
declare object: PrinterStateLight
@Prop({ type: Boolean, default: false }) readonly root!: boolean
@Prop(Object) readonly group!: GuiMiscellaneousStateEntryLightgroup | undefined
private boolDialog = false
private inputValue = 0
get name() {
if (this.group) return convertName(this.group.name)
return convertName(this.object.name)
}
get colorPickerOptions() {
let options: ColorPickerProps = {
width: 200,
margin: 15,
layout: [],
}
if (this.existRed) {
// @ts-ignore
options?.layout.push({
component: iro.ui.Slider,
options: {
sliderType: 'red',
},
})
}
if (this.existGreen) {
// @ts-ignore
options?.layout.push({
component: iro.ui.Slider,
options: {
sliderType: 'green',
},
})
}
if (this.existBlue) {
// @ts-ignore
options?.layout.push({
component: iro.ui.Slider,
options: {
sliderType: 'blue',
},
})
}
if (this.existRed && this.existGreen && this.existBlue) {
options.layout = [
{
component: iro.ui.Wheel,
},
{
component: iro.ui.Slider,
options: {
sliderType: 'value',
},
},
]
}
return options
}
get colorPickerWhiteOptions() {
let options: ColorPickerProps = {
width: 200,
margin: 15,
layout: [
{
component: iro.ui.Slider,
options: {
sliderType: 'alpha',
},
},
],
}
return options
}
get optionsColors() {
let output: string[] = []
this.presets.forEach((preset: GuiMiscellaneousStateEntryPreset) => {
output.push(`rgb(${preset.red}%, ${preset.green}%, ${preset.blue}%)`)
})
return output
}
get current() {
const color: ColorData = {
red: 0,
green: 0,
blue: 0,
white: null,
}
if (this.existWhite) color.white = 0
if (this.object.colorData.length === 0) return color
const firstColorData = this.object.colorData[(this.group?.start ?? 1) - 1]
color.red = firstColorData[0] * 255
color.green = firstColorData[1] * 255
color.blue = firstColorData[2] * 255
if (this.object.colorOrder.indexOf('W') !== -1) color.white = firstColorData[3] * 255
return color
}
get isOn() {
return (
(this.current.red ?? 0) +
(this.current?.green ?? 0) +
(this.current.blue ?? 0) +
(this.current.white ?? 0) >
0
)
}
get existRed() {
return this.object.colorOrder.indexOf('R') !== -1
}
get existGreen() {
return this.object.colorOrder.indexOf('G') !== -1
}
get existBlue() {
return this.object.colorOrder.indexOf('B') !== -1
}
get existWhite() {
return this.object.colorOrder.indexOf('W') !== -1
}
get currentStateStyle() {
let output = this.colorRGB
if (this.current.white !== null && this.current.red == 0 && this.current.green == 0 && this.current.blue == 0)
output = `rgb(${this.current.white * 255}, ${this.current.white * 255}, ${this.current.white * 255})`
return {
'background-color': output,
}
}
get colorRGB() {
return `rgb(${Math.round(this.current.red ?? 0)}, ${Math.round(this.current.green ?? 0)}, ${Math.round(
this.current.blue ?? 0
)})`
}
get colorRGBW() {
return `rgba(255, 255, 255, ${(this.current.white ?? 0) / 255})`
}
get redInt() {
return Math.round(this.current.red ?? 0)
}
get greenInt() {
return Math.round(this.current.green ?? 0)
}
get blueInt() {
return Math.round(this.current.blue ?? 0)
}
get whiteInt() {
return Math.round(this.current.white ?? 0)
}
get groups() {
return (
this.$store.getters['gui/miscellaneous/getEntryLightgroups']({
type: this.object.type,
name: this.object.name,
}) ?? []
)
}
get presets() {
return (
this.$store.getters['gui/miscellaneous/getEntryPresets']({
type: this.object.type,
name: this.object.name,
}) ?? []
)
}
get containerClass() {
let output = ['px-0']
output.push(this.root ? 'py-2' : 'pt-2 pb-0')
return output
}
colorChanged(color: ColorData) {
if (
Math.round(color.red ?? 0) === Math.round(this.current.red ?? 0) &&
Math.round(color.green ?? 0) === Math.round(this.current.green ?? 0) &&
Math.round(color.blue ?? 0) === Math.round(this.current.blue ?? 0) &&
Math.round(color.white ?? 0) === Math.round(this.current.white ?? 0)
)
return
const red = Math.round(((color.red ?? 0) / 255) * 10000) / 10000
const green = Math.round(((color.green ?? 0) / 255) * 10000) / 10000
const blue = Math.round(((color.blue ?? 0) / 255) * 10000) / 10000
const white = Math.round(((color.white ?? 0) / 255) * 10000) / 10000
let gcode = `SET_LED LED="${this.object.name}" RED=${red} GREEN=${green} BLUE=${blue}`
if (this.existWhite) gcode += ` WHITE=${white}`
gcode += ` SYNC=0`
if (this.group) {
const tmp = gcode
for (let i = this.group.start; i <= this.group.end; i++) {
if (i === this.group.start) {
gcode += ` INDEX=${i}`
continue
}
gcode += `\n${tmp} INDEX=${i}`
}
}
gcode += ` TRANSMIT=1`
this.$store.dispatch('server/addEvent', {
message: gcode,
type: 'command',
})
this.$socket.emit('printer.gcode.script', { script: gcode })
}
@Debounce({ time: 500 })
onColorRGBChanged(payload: IroColor) {
const color: ColorData = {
red: payload.red,
green: payload.green,
blue: payload.blue,
white: this.current.white,
}
this.colorChanged(color)
}
@Debounce({ time: 500 })
onColorWhiteChanged(payload: IroColor) {
const color: ColorData = {
red: this.current.red,
green: this.current.green,
blue: this.current.blue,
white: this.current.white,
}
// @ts-ignore
color.white = payload.alpha * 255
this.colorChanged(color)
}
@Debounce({ time: 500 })
onColorInput(payload: { name: string; value: number }) {
const color: ColorData = {
red: this.current.red,
green: this.current.green,
blue: this.current.blue,
white: this.current.white,
}
// @ts-ignore
color[payload.name] = payload.value
this.colorChanged(color)
}
off() {
const color: ColorData = {
red: 0,
green: 0,
blue: 0,
white: 0,
}
this.colorChanged(color)
}
on() {
const color: ColorData = {
red: 255,
green: 255,
blue: 255,
white: 255,
}
this.colorChanged(color)
}
presetStyle(preset: GuiMiscellaneousStateEntryPreset) {
if ((preset?.red ?? 0) + (preset?.green ?? 0) + (preset?.blue ?? 0) === 0 && (preset?.white ?? 0) > 0) {
return {
backgroundColor: `rgb(${preset.white}%, ${preset.white}%, ${preset.white}%)`,
}
}
return {
backgroundColor: `rgb(${preset.red}%, ${preset.green}%, ${preset.blue}%)`,
}
}
usePreset(preset: GuiMiscellaneousStateEntryPreset) {
const color: ColorData = { ...preset }
this.colorChanged(color)
}
}
</script>
<style scoped>
._light-subheader {
height: auto;
}
._currentState {
width: 15px;
height: 15px;
border-radius: 50%;
border: 1px solid lightgray;
cursor: pointer;
}
.light-presets-container {
gap: 6px;
}
.light-presets-container > div {
width: 28px;
height: 28px;
border-radius: 4px;
cursor: pointer;
}
</style>

View File

@ -3,7 +3,18 @@
<v-row>
<v-col :class="pwm ? 'pb-1' : 'pb-3'">
<v-subheader class="_fan-slider-subheader">
<v-icon v-if="type !== 'output_pin'" small :class="fanClasses">{{ mdiFan }}</v-icon>
<v-icon
v-if="type === 'led' && target > 0"
class="mr-2"
small
:retain-focus-on-click="true"
@click="ledOff">
{{ mdiLightbulbOnOutline }}
</v-icon>
<v-icon v-else-if="type === 'led'" class="mr-2" small :retain-focus-on-click="true" @click="ledOn">
{{ mdiLightbulbOutline }}
</v-icon>
<v-icon v-else-if="type !== 'output_pin'" small :class="fanClasses">{{ mdiFan }}</v-icon>
<span>{{ convertName(name) }}</span>
<v-spacer></v-spacer>
<small v-if="rpm !== null" :class="rpmClasses">{{ Math.round(rpm ?? 0) }} RPM</small>
@ -87,6 +98,8 @@ import {
mdiPlus,
mdiToggleSwitch,
mdiToggleSwitchOffOutline,
mdiLightbulbOutline,
mdiLightbulbOnOutline,
} from '@mdi/js'
@Component
@ -98,6 +111,8 @@ export default class MiscellaneousSlider extends Mixins(BaseMixin) {
mdiLockOpenVariantOutline = mdiLockOpenVariantOutline
mdiMinus = mdiMinus
mdiPlus = mdiPlus
mdiLightbulbOutline = mdiLightbulbOutline
mdiLightbulbOnOutline = mdiLightbulbOnOutline
convertName = convertName
private declare timeout: ReturnType<typeof setTimeout>
@ -135,6 +150,9 @@ export default class MiscellaneousSlider extends Mixins(BaseMixin) {
@Prop({ type: Number, default: 0 })
declare off_below: number
@Prop({ type: String, default: '' })
declare colorOrder: string
get value(): number {
return Math.round((this.target / this.max) * 100) / 100
}
@ -187,6 +205,8 @@ export default class MiscellaneousSlider extends Mixins(BaseMixin) {
if (this.type === 'fan') gcode = `M106 S${newVal.toFixed(0)}`
if (this.type === 'fan_generic') gcode = `SET_FAN_SPEED FAN=${this.name} SPEED=${newVal}`
if (this.type === 'output_pin') gcode = `SET_PIN PIN=${this.name} VALUE=${newVal.toFixed(2)}`
if (this.type === 'led')
gcode = `SET_LED LED=${this.name} ${this.ledChannelName}=${newVal.toFixed(2)} SYNC=0 TRANSMIT=1`
if (gcode !== '') {
this.$store.dispatch('server/addEvent', { message: gcode, type: 'command' })
@ -196,6 +216,14 @@ export default class MiscellaneousSlider extends Mixins(BaseMixin) {
this.startLockTimer()
}
ledOff() {
this.sendCmd(0)
}
ledOn() {
this.sendCmd(1)
}
switchOutputPin(): void {
const newVal = this.value ? 0 : 1
const gcode = `SET_PIN PIN=${this.name} VALUE=${(newVal * this.multi).toFixed(2)}`
@ -266,6 +294,14 @@ export default class MiscellaneousSlider extends Mixins(BaseMixin) {
return output
}
get ledChannelName() {
if (this.colorOrder === 'R') return 'RED'
if (this.colorOrder === 'G') return 'GREEN'
if (this.colorOrder === 'B') return 'BLUE'
return 'WHITE'
}
submitInput(): void {
if (this.errors.length > 0) return

View File

@ -2,7 +2,7 @@
<template>
<panel
v-if="klipperReadyForGui && (miscellaneous.length || filamentSensors.length)"
v-if="showMiscellaneousPanel"
:icon="mdiDipSwitch"
:title="$t('Panels.MiscellaneousPanel.Headline')"
:collapsible="true"
@ -20,8 +20,21 @@
:max="object.max_power"
:multi="parseInt(object.scale)"></miscellaneous-slider>
</div>
<div v-for="(sensor, index) of filamentSensors" :key="'sensor_' + index">
<div v-for="(light, index) of lights" :key="'light_' + light.name">
<v-divider v-if="index || miscellaneous.length"></v-divider>
<miscellaneous-slider
v-if="light.type === 'led' && light.colorOrder.length === 1"
:name="light.name"
type="led"
:rpm="null"
:controllable="true"
:pwm="true"
:target="light.singleChannelTarget"
:color-order="light.colorOrder" />
<miscellaneous-light v-else :object="light" :root="true" />
</div>
<div v-for="(sensor, index) of filamentSensors" :key="'sensor_' + index">
<v-divider v-if="index || miscellaneous.length || lights.length"></v-divider>
<filament-sensor
:name="sensor.name"
:enabled="sensor.enabled"
@ -34,21 +47,32 @@
import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '@/components/mixins/base'
import MiscellaneousSlider from '@/components/inputs/MiscellaneousSlider.vue'
import MiscellaneousLight from '@/components/inputs/MiscellaneousLight.vue'
import FilamentSensor from '@/components/inputs/FilamentSensor.vue'
import Panel from '@/components/ui/Panel.vue'
import { mdiDipSwitch } from '@mdi/js'
@Component({
components: { Panel, FilamentSensor, MiscellaneousSlider },
components: { Panel, FilamentSensor, MiscellaneousSlider, MiscellaneousLight },
})
export default class MiscellaneousPanel extends Mixins(BaseMixin) {
mdiDipSwitch = mdiDipSwitch
get filamentSensors() {
return this.$store.getters['printer/getFilamentSensors'] ?? []
}
get miscellaneous() {
return this.$store.getters['printer/getMiscellaneous'] ?? []
}
get filamentSensors() {
return this.$store.getters['printer/getFilamentSensors'] ?? []
get lights() {
return this.$store.getters['printer/getLights'] ?? []
}
get showMiscellaneousPanel() {
return (
this.klipperReadyForGui && (this.miscellaneous.length || this.filamentSensors.length || this.lights.length)
)
}
}
</script>

View File

@ -0,0 +1,81 @@
<template>
<div>
<settings-miscellaneous-tab-light-groups
v-if="editLightGroupObject"
:light="editLightGroupObject"
@close="editLightGroupObject = null" />
<settings-miscellaneous-tab-light-presets
v-else-if="editLightPresetObject"
:light="editLightPresetObject"
@close="editLightPresetObject = null" />
<v-card-text v-else>
<h3 class="text-h5 mb-3">{{ $t('Settings.MiscellaneousTab.Miscellaneous') }}</h3>
<template v-if="filteredLights.length">
<div v-for="(light, index) in filteredLights" :key="index">
<v-divider v-if="index" class="my-2"></v-divider>
<settings-row :title="convertName(light.name)" :dynamic-slot-width="true">
<v-btn
v-if="light.chainCount > 1"
small
outlined
class="ml-3"
@click="editLightGroupObject = light">
<v-icon left small>{{ mdiPencil }}</v-icon>
{{ $t('Settings.MiscellaneousTab.Groups') }}
</v-btn>
<v-btn small outlined class="ml-3" @click="editLightPresetObject = light">
<v-icon left small>{{ mdiPalette }}</v-icon>
{{ $t('Settings.MiscellaneousTab.Presets') }}
</v-btn>
</settings-row>
</div>
</template>
<template v-else>
<v-row>
<v-col>
<p class="mb-0 text-center font-italic">{{ $t('Settings.MiscellaneousTab.NoDevicesFound') }}</p>
</v-col>
</v-row>
</template>
</v-card-text>
</div>
</template>
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import BaseMixin from '../mixins/base'
import SettingsRow from '@/components/settings/SettingsRow.vue'
import { mdiDelete, mdiPalette, mdiPencil } from '@mdi/js'
import { convertName } from '@/plugins/helpers'
import SettingsMiscellaneousTabLightGroups from '@/components/settings/SettingsMiscellaneousTabLightGroups.vue'
import SettingsMiscellaneousTabLightPresets from '@/components/settings/SettingsMiscellaneousTabLightPresets.vue'
import { PrinterStateLight } from '@/store/printer/types'
@Component({
components: {
SettingsRow,
SettingsMiscellaneousTabLightGroups,
SettingsMiscellaneousTabLightPresets,
},
})
export default class SettingsMiscellaneousTab extends Mixins(BaseMixin) {
mdiDelete = mdiDelete
mdiPalette = mdiPalette
mdiPencil = mdiPencil
convertName = convertName
editLightGroupObject: PrinterStateLight | null = null
editLightPresetObject: PrinterStateLight | null = null
get lights() {
return this.$store.getters['printer/getLights'] ?? []
}
get filteredLights() {
return this.lights.filter((light: PrinterStateLight) => light.colorOrder.length > 1)
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,236 @@
<template>
<div>
<template v-if="boolForm">
<v-card-text>
<h3 class="text-h5 mb-3">{{ $t('Settings.MiscellaneousTab.CreateGroup') }}</h3>
<settings-row :title="$t('Settings.MiscellaneousTab.Name').toString()">
<v-text-field
v-model="form.name"
hide-details="auto"
:rules="[rules.required, rules.groupUnique]"
dense
outlined></v-text-field>
</settings-row>
<v-divider class="my-2"></v-divider>
<settings-row
:title="$t('Settings.MiscellaneousTab.Start').toString()"
:sub-title="$t('Settings.MiscellaneousTab.StartDescription').toString()">
<v-text-field
v-model="form.start"
hide-details="auto"
type="number"
step="1"
:rules="[rules.minStart, rules.max]"
dense
outlined></v-text-field>
</settings-row>
<v-divider class="my-2"></v-divider>
<settings-row
:title="$t('Settings.MiscellaneousTab.End').toString()"
:sub-title="$t('Settings.MiscellaneousTab.EndDescription').toString()">
<v-text-field
v-model="form.end"
hide-details="auto"
type="number"
step="1"
:rules="[rules.minEnd, rules.max]"
dense
outlined></v-text-field>
</settings-row>
</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn text @click="closeForm">{{ $t('Settings.Cancel') }}</v-btn>
<v-btn v-if="form.id !== null" text color="primary" @click="updateGroup">
{{ $t('Settings.Update') }}
</v-btn>
<v-btn v-else text color="primary" @click="storeGroup">{{ $t('Settings.Store') }}</v-btn>
</v-card-actions>
</template>
<template v-else>
<v-card-text>
<h3 class="text-h5 mb-3">{{ $t('Settings.MiscellaneousTab.LightGroups', { name: light.name }) }}</h3>
<template v-if="light">
<template v-if="groups.length">
<div v-for="(group, index) in groups" :key="group.id">
<v-divider v-if="index" class="my-2"></v-divider>
<settings-row
:title="group.name"
:sub-title="
$t('Settings.MiscellaneousTab.GroupSubTitle', {
start: group.start,
end: group.end,
}).toString()
"
:dynamic-slot-width="true">
<v-btn small outlined class="ml-3" @click="editGroup(group)">
<v-icon left small>{{ mdiPencil }}</v-icon>
{{ $t('Settings.Edit') }}
</v-btn>
<v-btn
small
outlined
class="ml-3 minwidth-0 px-2"
color="error"
@click="deleteGroup(group.id)">
<v-icon small>{{ mdiDelete }}</v-icon>
</v-btn>
</settings-row>
</div>
</template>
<template v-else>
<v-row>
<v-col>
<p class="mb-0 text-center font-italic">
{{ $t('Settings.MiscellaneousTab.NoGroupFound') }}
</p>
</v-col>
</v-row>
</template>
</template>
<template v-else>
<v-row>
<v-col>
<p class="mb-0 text-center font-italic">
{{ $t('Settings.MiscellaneousTab.UnableToLoadLight') }}
</p>
</v-col>
</v-row>
</template>
</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn text @click="$emit('close')">{{ $t('Settings.Close') }}</v-btn>
<v-btn text color="primary" @click="createGroup">
{{ $t('Settings.MiscellaneousTab.AddGroup') }}
</v-btn>
</v-card-actions>
</template>
</div>
</template>
<script lang="ts">
import { Component, Mixins, Prop } from 'vue-property-decorator'
import BaseMixin from '../mixins/base'
import SettingsRow from '@/components/settings/SettingsRow.vue'
import { mdiDelete, mdiPalette, mdiPencil } from '@mdi/js'
import { caseInsensitiveSort, convertName } from '@/plugins/helpers'
import { PrinterStateLight } from '@/store/printer/types'
import { GuiMacrosStateMacrogroup } from '@/store/gui/macros/types'
import { GuiMiscellaneousStateEntry, GuiMiscellaneousStateEntryLightgroup } from '@/store/gui/miscellaneous/types'
@Component({
components: {
SettingsRow,
},
})
export default class SettingsMiscellaneousTabLightGroups extends Mixins(BaseMixin) {
mdiDelete = mdiDelete
mdiPalette = mdiPalette
mdiPencil = mdiPencil
convertName = convertName
private boolForm = false
private form: {
id: string | null
name: string
start: number
end: number
} = {
id: null,
name: '',
start: 1,
end: 1,
}
private rules = {
required: (value: string) => value !== '' || 'required',
groupUnique: (value: string) => !this.existsGroupName(value) || 'Name already exists',
minStart: (value: number) => value > 0 || 'smaller than 1',
minEnd: (value: number) => value >= this.form.start || 'smaller than start value',
max: (value: number) => value <= (this.light?.chainCount ?? 1) || 'higher than chain_count',
}
@Prop({ type: Object, default: null })
declare light: PrinterStateLight | null
get entry() {
return this.$store.getters['gui/miscellaneous/getEntry']({
type: this.light?.type,
name: this.light?.name,
}) as GuiMiscellaneousStateEntry | null
}
get groups() {
if (!this.entry) return []
const groups: GuiMiscellaneousStateEntryLightgroup[] = []
Object.entries(this.entry.lightgroups).forEach(([key, lightgroup]) => {
groups.push({
name: lightgroup.name,
start: lightgroup.start,
end: lightgroup.end,
id: key,
})
})
window.console.log('getEntryLightgroups', groups)
return caseInsensitiveSort(groups, 'name')
}
createGroup() {
this.form.id = null
this.form.name = ''
this.form.start = 1
this.form.end = this.light?.chainCount ?? 1
this.boolForm = true
}
editGroup(group: GuiMiscellaneousStateEntryLightgroup) {
this.form.id = group.id ?? null
this.form.name = group.name
this.form.start = group.start
this.form.end = group.end
this.boolForm = true
}
closeForm() {
this.boolForm = false
}
storeGroup() {
this.$store.dispatch('gui/miscellaneous/storeLightgroup', {
entry: this.light,
lightgroup: this.form,
})
this.boolForm = false
}
updateGroup() {
this.$store.dispatch('gui/miscellaneous/updateLightgroup', {
entry: this.light,
lightgroup: this.form,
})
this.boolForm = false
}
deleteGroup(groupId: string) {
this.$store.dispatch('gui/miscellaneous/deleteLightgroup', {
entry: this.light,
lightgroupId: groupId,
})
}
existsGroupName(name: string) {
return (
this.groups.findIndex(
(group: GuiMacrosStateMacrogroup) => group.name === name && group.id != this.form.id
) >= 0
)
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,467 @@
<template>
<div>
<template v-if="boolForm">
<v-card-text>
<h3 class="text-h5 mb-3">{{ $t('Settings.MiscellaneousTab.CreatePreset') }}</h3>
<settings-row :title="$t('Settings.MiscellaneousTab.Name').toString()">
<v-text-field
v-model="form.name"
hide-details="auto"
:rules="[rules.required, rules.presetUnique]"
dense
outlined></v-text-field>
</settings-row>
<v-divider class="my-2"></v-divider>
<settings-row :title="$t('Settings.MiscellaneousTab.Color').toString()">
<v-row>
<v-col class="text-center">
<color-picker
:color="colorRGB"
:options="colorPickerOptions"
@update:color="onColorRGBChanged" />
<color-picker
v-if="existWhite"
:color="colorRGBW"
:options="colorPickerWhiteOptions"
class="mt-3"
@update:color="onColorWhiteChanged" />
</v-col>
<v-col>
<v-row v-if="existRed">
<v-col>
<number-input
:label="$t('Panels.MiscellaneousPanel.Light.Red')"
param="red"
:target="redInt"
:min="0"
:max="255"
:dec="1"
:step="1"
:output-error-msg="true"
:has-spinner="true"
@submit="onColorInput" />
</v-col>
</v-row>
<v-row v-if="existGreen">
<v-col>
<number-input
:label="$t('Panels.MiscellaneousPanel.Light.Green')"
param="green"
:target="greenInt"
:min="0"
:max="255"
:dec="1"
:step="1"
:has-spinner="true"
@submit="onColorInput" />
</v-col>
</v-row>
<v-row v-if="existBlue">
<v-col>
<number-input
:label="$t('Panels.MiscellaneousPanel.Light.Blue')"
param="blue"
:target="blueInt"
:min="0"
:max="255"
:dec="1"
:step="1"
:has-spinner="true"
@submit="onColorInput" />
</v-col>
</v-row>
<v-row v-if="existWhite">
<v-col>
<number-input
:label="$t('Panels.MiscellaneousPanel.Light.White')"
param="white"
:target="whiteInt"
:min="0"
:max="255"
:dec="1"
:step="1"
:has-spinner="true"
@submit="onColorInput" />
</v-col>
</v-row>
</v-col>
</v-row>
</settings-row>
</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn text @click="closeForm">{{ $t('Settings.Cancel') }}</v-btn>
<v-btn v-if="form.id !== null" text color="primary" @click="updatePreset">
{{ $t('Settings.Update') }}
</v-btn>
<v-btn v-else text color="primary" @click="storePreset">{{ $t('Settings.Store') }}</v-btn>
</v-card-actions>
</template>
<template v-else>
<v-card-text>
<h3 class="text-h5 mb-3">{{ $t('Settings.MiscellaneousTab.LightPresets', { name: light.name }) }}</h3>
<template v-if="light">
<template v-if="presets.length">
<div v-for="(preset, index) in presets" :key="preset.id">
<v-divider v-if="index" class="my-2"></v-divider>
<settings-row
:title="preset.name"
:sub-title="entryDescriptionText(preset)"
:dynamic-slot-width="true">
<v-btn small outlined class="ml-3" @click="editPreset(preset)">
<v-icon left small>{{ mdiPencil }}</v-icon>
{{ $t('Settings.Edit') }}
</v-btn>
<v-btn
small
outlined
class="ml-3 minwidth-0 px-2"
color="error"
@click="deletePreset(preset.id)">
<v-icon small>{{ mdiDelete }}</v-icon>
</v-btn>
</settings-row>
</div>
</template>
<template v-else>
<v-row>
<v-col>
<p class="mb-0 text-center font-italic">
{{ $t('Settings.MiscellaneousTab.NoPresetFound') }}
</p>
</v-col>
</v-row>
</template>
</template>
<template v-else>
<v-row>
<v-col>
<p class="mb-0 text-center font-italic">
{{ $t('Settings.MiscellaneousTab.UnableToLoadPreset') }}
</p>
</v-col>
</v-row>
</template>
</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn text @click="$emit('close')">{{ $t('Settings.Close') }}</v-btn>
<v-btn text color="primary" @click="createPreset">
{{ $t('Settings.MiscellaneousTab.AddPreset') }}
</v-btn>
</v-card-actions>
</template>
</div>
</template>
<script lang="ts">
import { Component, Mixins, Prop } from 'vue-property-decorator'
import BaseMixin from '../mixins/base'
import SettingsRow from '@/components/settings/SettingsRow.vue'
import { mdiDelete, mdiPencil } from '@mdi/js'
import { caseInsensitiveSort, convertName } from '@/plugins/helpers'
import { PrinterStateLight } from '@/store/printer/types'
import { GuiMiscellaneousStateEntry, GuiMiscellaneousStateEntryPreset } from '@/store/gui/miscellaneous/types'
import { ColorPickerProps } from '@jaames/iro/dist/ColorPicker.d'
import iro from '@jaames/iro'
import { Debounce } from 'vue-debounce-decorator'
import { IroColor } from '@irojs/iro-core'
interface ColorData {
red: number | null
green: number | null
blue: number | null
white: number | null
}
@Component({
components: {
SettingsRow,
},
})
export default class SettingsMiscellaneousTabLightPresets extends Mixins(BaseMixin) {
mdiDelete = mdiDelete
mdiPencil = mdiPencil
convertName = convertName
private boolForm = false
private form: {
id: string | null
name: string
red: number | null
green: number | null
blue: number | null
white: number | null
} = {
id: null,
name: '',
red: null,
green: null,
blue: null,
white: null,
}
private rules = {
required: (value: string) => value !== '' || 'required',
presetUnique: (value: string) => !this.existsPresetName(value) || 'Name already exists',
min: (value: number) => value >= 0 || 'Must be minimum 0',
max: (value: number) => value <= 255 || 'Must be smaller then 256',
}
@Prop({ type: Object, default: null })
declare light: PrinterStateLight | null
get entry() {
return this.$store.getters['gui/miscellaneous/getEntry']({
type: this.light?.type,
name: this.light?.name,
}) as GuiMiscellaneousStateEntry | null
}
get presets() {
if (!this.entry) return []
const presets: GuiMiscellaneousStateEntryPreset[] = []
Object.entries(this.entry.presets).forEach(([key, preset]) => {
presets.push({
...preset,
id: key,
})
})
window.console.log('getEntryPresets', presets)
return caseInsensitiveSort(presets, 'name')
}
get existRed() {
return this.light?.colorOrder.indexOf('R') !== -1
}
get existGreen() {
return this.light?.colorOrder.indexOf('G') !== -1
}
get existBlue() {
return this.light?.colorOrder.indexOf('B') !== -1
}
get existWhite() {
return this.light?.colorOrder.indexOf('W') !== -1
}
get colorRGB() {
return `rgb(${Math.round(this.form.red ?? 0)}, ${Math.round(this.form.green ?? 0)}, ${Math.round(
this.form.blue ?? 0
)})`
}
get colorRGBW() {
return `rgba(255, 255, 255, ${(this.form.white ?? 0) / 255})`
}
get redInt() {
return Math.round(this.form.red ?? 0)
}
get greenInt() {
return Math.round(this.form.green ?? 0)
}
get blueInt() {
return Math.round(this.form.blue ?? 0)
}
get whiteInt() {
return Math.round(this.form.white ?? 0)
}
get colorPickerOptions() {
let options: ColorPickerProps = {
width: 200,
margin: 15,
layout: [],
}
if (this.existRed) {
// @ts-ignore
options?.layout.push({
component: iro.ui.Slider,
options: {
sliderType: 'red',
},
})
}
if (this.existGreen) {
// @ts-ignore
options?.layout.push({
component: iro.ui.Slider,
options: {
sliderType: 'green',
},
})
}
if (this.existBlue) {
// @ts-ignore
options?.layout.push({
component: iro.ui.Slider,
options: {
sliderType: 'blue',
},
})
}
if (this.existRed && this.existGreen && this.existBlue) {
options.layout = [
{
component: iro.ui.Wheel,
},
{
component: iro.ui.Slider,
options: {
sliderType: 'value',
},
},
]
}
return options
}
get colorPickerWhiteOptions() {
let options: ColorPickerProps = {
width: 200,
margin: 15,
layout: [
{
component: iro.ui.Slider,
options: {
sliderType: 'alpha',
},
},
],
}
return options
}
entryDescriptionText(preset: GuiMiscellaneousStateEntryPreset) {
let output: string[] = []
if (this.light?.colorOrder.includes('R')) output.push(`R: ${preset.red}`)
if (this.light?.colorOrder.includes('G')) output.push(`G: ${preset.green}`)
if (this.light?.colorOrder.includes('B')) output.push(`B: ${preset.blue}`)
if (this.light?.colorOrder.includes('W')) output.push(`W: ${preset.white}`)
return output.join(', ')
}
createPreset() {
this.form.id = null
this.form.name = ''
this.form.red = this.light?.colorOrder.indexOf('R') != -1 ? 0 : null
this.form.green = this.light?.colorOrder.indexOf('G') != -1 ? 0 : null
this.form.blue = this.light?.colorOrder.indexOf('B') != -1 ? 0 : null
this.form.white = this.light?.colorOrder.indexOf('W') != -1 ? 0 : null
this.boolForm = true
}
editPreset(preset: GuiMiscellaneousStateEntryPreset) {
this.form.id = preset.id ?? null
this.form.name = preset.name
this.form.red = this.light?.colorOrder.indexOf('R') != -1 ? preset.red : null
this.form.green = this.light?.colorOrder.indexOf('G') != -1 ? preset.green : null
this.form.blue = this.light?.colorOrder.indexOf('B') != -1 ? preset.blue : null
this.form.white = this.light?.colorOrder.indexOf('W') != -1 ? preset.white : null
this.boolForm = true
}
closeForm() {
this.boolForm = false
}
storePreset() {
this.$store.dispatch('gui/miscellaneous/storePreset', {
entry: this.light,
preset: this.form,
})
this.boolForm = false
}
updatePreset() {
this.$store.dispatch('gui/miscellaneous/updatePreset', {
entry: this.light,
preset: this.form,
})
this.boolForm = false
}
deletePreset(presetId: string) {
this.$store.dispatch('gui/miscellaneous/deletePreset', {
entry: this.light,
presetId: presetId,
})
}
existsPresetName(name: string) {
return (
this.presets.findIndex(
(group: GuiMiscellaneousStateEntryPreset) => group.name === name && group.id != this.form.id
) >= 0
)
}
@Debounce({ time: 250 })
onColorRGBChanged(payload: IroColor) {
const color: ColorData = {
red: payload.red,
green: payload.green,
blue: payload.blue,
white: this.form.white,
}
this.colorChanged(color)
}
@Debounce({ time: 250 })
onColorWhiteChanged(payload: IroColor) {
const color: ColorData = {
red: this.form.red,
green: this.form.green,
blue: this.form.blue,
white: this.form.white,
}
// @ts-ignore
color.white = payload.alpha * 255
this.colorChanged(color)
}
onColorInput(payload: { name: string; value: number }) {
const color: ColorData = {
red: this.form.red,
green: this.form.green,
blue: this.form.blue,
white: this.form.white,
}
// @ts-ignore
color[payload.name] = payload.value
this.colorChanged(color)
}
colorChanged(color: ColorData) {
this.form.red = color.red
this.form.green = color.green
this.form.blue = color.blue
this.form.white = color.white
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -948,7 +948,6 @@
"EditWebcam": "Rediger Webcam",
"FlipWebcam": "Vend webcam-billedet:",
"Horizontally": "horisontalt",
"Vertically": "vertikalt",
"IconBed": "Bed",
"IconCam": "Kamera",
"IconDoor": "Dør",
@ -971,6 +970,7 @@
"UrlSnapshot": "URL Snapshot",
"UrlStream": "URL Stream",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "vertikalt",
"Webcams": "Webcams"
}
},

View File

@ -530,6 +530,12 @@
},
"MiscellaneousPanel": {
"Headline": "Sonstiges",
"Light": {
"Blue": "blau",
"Green": "grün",
"Red": "rot",
"White": "weiß"
},
"RunoutSensor": {
"Detected": "erkannt",
"Disabled": "deaktiviert",
@ -833,6 +839,30 @@
"UnknownGroup": "Unbekannte Gruppe",
"Warning": "Warnung"
},
"MiscellaneousTab": {
"AddGroup": "Gruppe hinzufügen",
"AddPreset": "Voreinstellung hinzufügen",
"Color": "Farbe",
"CreateGroup": "Gruppe erstellen",
"CreatePreset": "Voreinstellung erstellen",
"End": "Ende",
"EndDescription": "Letzte LED von dieser Gruppe.",
"Groups": "Gruppen",
"GroupSubTitle": "Start: {start}, Ende: {end}",
"LightGroups": "{name} - Gruppen",
"LightPresets": "{name} - Voreinstellungen",
"Miscellaneous": "Sonstiges",
"Name": "Name",
"NoDevicesFound": "Keine Komponente gefunden",
"NoGroupFound": "Keine Gruppe gefunden",
"NoPresetFound": "Keine Voreinstellungen gefunden",
"Presets": "Voreinstellungen",
"PresetSubTitle": "R: {red}, G: {green}, B: {blue}, W: {white}",
"Start": "Start",
"StartDescription": "Erste LED von dieser Gruppe.",
"UnableToLoadLight": "Licht konnte nicht geladen werden",
"UnableToLoadPreset": "Voreinstellung konnte nicht geladen werden"
},
"PresetsTab": {
"AddPreset": "Preset hinzufügen",
"Cooldown": "Abkühlen",
@ -860,6 +890,7 @@
"UpdatePrinter": "Drucker aktualisieren",
"UseConfigJson": "InstanceDB = JSON erkannt. Bitte bearbeite die config.json um die Druckerliste zu modifizieren."
},
"Store": "anlegen",
"TimelapseTab": {
"Autorender": "Autorender",
"AutorenderDescription": "Wenn diese Option aktiviert ist, wird das Zeitraffervideo am Ende des Druckvorgangs automatisch gerendert",
@ -954,6 +985,7 @@
"ShowWebcamInNavigation": "Zeige Webcam in der Navigation",
"UiSettings": "UI-Einstellungen"
},
"Update": "speichern",
"WebcamsTab": {
"AddWebcam": "Webcam hinzufügen",
"CreateWebcam": "Erstelle Webcam",
@ -961,7 +993,6 @@
"EditWebcam": "Webcam bearbeiten",
"FlipWebcam": "Webcam-Bild spiegeln:",
"Horizontally": "horizontal",
"Vertically": "vertikal",
"IconBed": "Bett",
"IconCam": "Kamera",
"IconDoor": "Tür",
@ -984,6 +1015,7 @@
"UrlSnapshot": "Schnappschuss URL",
"UrlStream": "Stream URL",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "vertikal",
"Webcams": "Webcams"
}
},

View File

@ -530,6 +530,12 @@
},
"MiscellaneousPanel": {
"Headline": "Miscellaneous",
"Light": {
"Blue": "blue",
"Green": "green",
"Red": "red",
"White": "white"
},
"RunoutSensor": {
"Detected": "detected",
"Disabled": "disabled",
@ -837,6 +843,30 @@
"UnknownGroup": "Unknown Group",
"Warning": "warning"
},
"MiscellaneousTab": {
"AddGroup": "add group",
"AddPreset": "add preset",
"Color": "Color",
"CreateGroup": "Create group",
"CreatePreset": "Create preset",
"End": "End",
"EndDescription": "Last LED of this group.",
"Groups": "Groups",
"GroupSubTitle": "Start: {start}, End: {end}",
"LightGroups": "{name} - Groups",
"LightPresets": "{name} - Presets",
"Miscellaneous": "Miscellaneous",
"Name": "Name",
"NoDevicesFound": "No devices found",
"NoGroupFound": "No group found",
"NoPresetFound": "No preset found",
"Presets": "Presets",
"PresetSubTitle": "R: {red}, G: {green}, B: {blue}, W: {white}",
"Start": "Start",
"StartDescription": "First LED of this group.",
"UnableToLoadLight": "Unable to load light",
"UnableToLoadPreset": "Unable to load preset"
},
"PresetsTab": {
"AddPreset": "add preset",
"Cooldown": "Cooldown",
@ -864,6 +894,7 @@
"UpdatePrinter": "Update Printer",
"UseConfigJson": "InstanceDB = JSON detected. Please use the config.json to modify the printers list."
},
"Store": "store",
"TimelapseTab": {
"Autorender": "Autorender",
"AutorenderDescription": "If enabled, the timelapse video will automatically render at the end of the print",
@ -958,6 +989,7 @@
"ShowWebcamInNavigation": "Show Webcam in navigation",
"UiSettings": "UI-Settings"
},
"Update": "update",
"WebcamsTab": {
"AddWebcam": "add webcam",
"CreateWebcam": "Create Webcam",
@ -965,7 +997,6 @@
"EditWebcam": "Edit Webcam",
"FlipWebcam": "Flip webcam image:",
"Horizontally": "horizontally",
"Vertically": "vertically",
"IconBed": "Bed",
"IconCam": "Cam",
"IconDoor": "Door",
@ -988,6 +1019,7 @@
"UrlSnapshot": "URL Snapshot",
"UrlStream": "URL Stream",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "vertically",
"Webcams": "Webcams"
}
},

View File

@ -929,7 +929,6 @@
"EditWebcam": "Editar cámara web",
"FlipWebcam": "Voltear la imagen de la cámara web:",
"Horizontally": "horizontalmente",
"Vertically": "verticalmente",
"IconBed": "Cama",
"IconCam": "Cámara",
"IconDoor": "Puerta",
@ -951,6 +950,7 @@
"UrlSnapshot": "URL Snapshot",
"UrlStream": "URL Stream",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "verticalmente",
"Webcams": "Cámaras web"
}
},

View File

@ -953,7 +953,6 @@
"EditWebcam": "Editer caméra",
"FlipWebcam": "Miroir l'image de la webcam:",
"Horizontally": "horizontal",
"Vertically": "vertical",
"IconBed": "Plateau",
"IconCam": "Caméra",
"IconDoor": "Porte",
@ -976,6 +975,7 @@
"UrlSnapshot": "URL des instantanés",
"UrlStream": "URL du flux",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "vertical",
"Webcams": "Caméras"
}
},

View File

@ -929,7 +929,6 @@
"EditWebcam": "Webkamera szerkesztése",
"FlipWebcam": "Webkamera tükrözése:",
"Horizontally": "vízszintes",
"Vertically": "függőleges",
"IconBed": "Asztal",
"IconCam": "Kamera",
"IconDoor": "Ajtó",
@ -951,6 +950,7 @@
"UrlSnapshot": "Snapshot URL-je ",
"UrlStream": "Stream URL-je ",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "függőleges",
"Webcams": "Webkamerák"
}
},

View File

@ -796,7 +796,6 @@
"EditWebcam": "Modifica Webcam",
"FlipWebcam": "Specchio dell'immagine della webcam:",
"Horizontally": "orizzontalmente",
"Vertically": "verticalmente",
"IconBed": "Letto",
"IconCam": "Cam",
"IconDoor": "Porta",
@ -818,6 +817,7 @@
"UrlSnapshot": "URL Snaphot",
"UrlStream": "URL Stream",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "verticalmente",
"Webcams": "Webcam"
}
},

View File

@ -952,7 +952,6 @@
"EditWebcam": "Bewerk Webcam",
"FlipWebcam": "Flip webcam beeld:",
"Horizontally": "horizontaal",
"Vertically": "verticaal",
"IconBed": "Bed",
"IconCam": "Camera",
"IconDoor": "Deur",
@ -975,6 +974,7 @@
"UrlSnapshot": "URL Snapshot",
"UrlStream": "URL Stream",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "verticaal",
"Webcams": "Webcams"
}
},

View File

@ -930,7 +930,6 @@
"EditWebcam": "Edytuj kamerę",
"FlipWebcam": "Odwróć widok kamery:",
"Horizontally": "poziomo",
"Vertically": "pionowo",
"IconBed": "Stół",
"IconCam": "Kamera",
"IconDoor": "Drzwi",
@ -952,6 +951,7 @@
"UrlSnapshot": "Migawka URL",
"UrlStream": "Transmisja po URL",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "pionowo",
"Webcams": "Kamery"
}
},

View File

@ -930,7 +930,6 @@
"EditWebcam": "Редактирование веб-камеры",
"FlipWebcam": "Зеркальное отображение веб-камеры:",
"Horizontally": "горизонтально",
"Vertically": "вертикально",
"IconBed": "Кровать",
"IconCam": "Камера",
"IconDoor": "Дверь",
@ -952,6 +951,7 @@
"UrlSnapshot": "URL моментального снимка",
"UrlStream": "URL потока",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "вертикально",
"Webcams": "Веб-камеры"
}
},

View File

@ -824,7 +824,6 @@
"EditWebcam": "Redigera webbkamera",
"FlipWebcam": "Vänd webbkamerabilden",
"Horizontally": "horisontellt",
"Vertically": "vertikalt",
"IconBed": "Bädd",
"IconCam": "Kamera",
"IconDoor": "Dörr",
@ -846,6 +845,7 @@
"UrlSnapshot": "Ögonblicksbild av webbadressen",
"UrlStream": "Webbadressström",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "vertikalt",
"Webcams": "Webbkameror"
}
},

View File

@ -951,7 +951,6 @@
"EditWebcam": "Web Kamerası Düzenle",
"FlipWebcam": "Web kamerasını çevirin:",
"Horizontally": "yatay olarak",
"Vertically": "dikey olarak",
"IconBed": "Yatak",
"IconCam": "Kamera",
"IconDoor": "Kapı",
@ -974,6 +973,7 @@
"UrlSnapshot": "URL Anlık Görüntüsü",
"UrlStream": "URL Akışı",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "dikey olarak",
"Webcams": "Web kameraları"
}
},

View File

@ -952,7 +952,6 @@
"EditWebcam": "Редагувати веб-камеру",
"FlipWebcam": "Повернути веб-камеру:",
"Horizontally": "горизонтально",
"Vertically": "вертикально",
"IconBed": "Ліжко",
"IconCam": "Cam",
"IconDoor": "Двері",
@ -975,6 +974,7 @@
"UrlSnapshot": "URL-адреса Знімку",
"UrlStream": "URL-адреса Потоку",
"Uv4lMjpeg": "UV4L-MJPEG",
"Vertically": "вертикально",
"Webcams": "Веб-камери"
}
},

View File

@ -9,6 +9,7 @@ import { defaultLogoColor, defaultPrimaryColor } from '@/store/variables'
import { console } from '@/store/gui/console'
import { gcodehistory } from '@/store/gui/gcodehistory'
import { macros } from '@/store/gui/macros'
import { miscellaneous } from '@/store/gui/miscellaneous'
import { presets } from '@/store/gui/presets'
import { remoteprinters } from '@/store/gui/remoteprinters'
import { webcams } from '@/store/gui/webcams'
@ -255,6 +256,7 @@ export const gui: Module<GuiState, any> = {
console,
gcodehistory,
macros,
miscellaneous,
notifications,
presets,
remoteprinters,

View File

@ -0,0 +1,133 @@
import { ActionTree } from 'vuex'
import { RootState } from '@/store/types'
import { v4 as uuidv4 } from 'uuid'
import Vue from 'vue'
import {
GuiMiscellaneousState,
GuiMiscellaneousStateEntryLightgroup,
GuiMiscellaneousStateEntryPreset,
} from '@/store/gui/miscellaneous/types'
export const actions: ActionTree<GuiMiscellaneousState, RootState> = {
reset({ commit }) {
commit('reset')
},
upload({ state }, id) {
Vue.$socket.emit('server.database.post_item', {
namespace: 'mainsail',
key: 'miscellaneous.entries.' + id,
value: state.entries[id],
})
},
async store({ commit, dispatch }, payload: payloadStore) {
const id = uuidv4()
await commit('store', { id, values: payload })
await dispatch('upload', id)
return id
},
async storeLightgroup({ commit, dispatch, getters }, payload: payloadStoreLightgroup) {
let entryId = getters['getId'](payload.entry)
if (entryId === null) entryId = await dispatch('store', payload.entry)
const lightgroupId = uuidv4()
await commit('updateLightgroup', { entryId, lightgroupId, values: payload.lightgroup })
await dispatch('upload', entryId)
return lightgroupId
},
async updateLightgroup({ commit, dispatch, getters }, payload: payloadStoreLightgroup) {
const entryId = getters['getId'](payload.entry)
if (entryId === null) return
await commit('updateLightgroup', { entryId, lightgroupId: payload.lightgroup.id, values: payload.lightgroup })
await dispatch('upload', entryId)
return payload.lightgroup.id
},
async deleteLightgroup({ commit, dispatch, getters }, payload: payloadDeleteLightgroup) {
const entryId = getters['getId'](payload.entry)
if (entryId === null) return
await commit('destroyLightgroup', { entryId, lightgroupId: payload.lightgroupId })
await dispatch('upload', entryId)
},
async storePreset({ commit, dispatch, getters }, payload: payloadStorePreset) {
let entryId = getters['getId'](payload.entry)
if (entryId === null) entryId = await dispatch('store', payload.entry)
const presetId = uuidv4()
await commit('updatePreset', { entryId, presetId, values: payload.preset })
await dispatch('upload', entryId)
return presetId
},
async updatePreset({ commit, dispatch, getters }, payload: payloadStorePreset) {
const entryId = getters['getId'](payload.entry)
if (entryId === null) return
await commit('updatePreset', { entryId, presetId: payload.preset.id, values: payload.preset })
await dispatch('upload', entryId)
return payload.preset.id
},
async deletePreset({ commit, dispatch, getters }, payload: payloadDeletePreset) {
const entryId = getters['getId'](payload.entry)
if (entryId === null) return
await commit('destroyPreset', { entryId, presetId: payload.presetId })
await dispatch('upload', entryId)
},
}
interface payloadStore {
type: string
name: string
}
interface payloadStoreLightgroup {
entry: {
type: string
name: string
}
lightgroup: GuiMiscellaneousStateEntryLightgroup
}
interface payloadDeleteLightgroup {
entry: {
type: string
name: string
}
lightgroupId: string
}
interface payloadStorePreset {
entry: {
type: string
name: string
}
preset: GuiMiscellaneousStateEntryPreset
}
interface payloadDeletePreset {
entry: {
type: string
name: string
}
presetId: string
}

View File

@ -0,0 +1,75 @@
import { GetterTree } from 'vuex'
import {
GuiMiscellaneousState,
GuiMiscellaneousStateEntry,
GuiMiscellaneousStateEntryLightgroup,
GuiMiscellaneousStateEntryPreset,
} from '@/store/gui/miscellaneous/types'
import { caseInsensitiveSort } from '@/plugins/helpers'
// eslint-disable-next-line
export const getters: GetterTree<GuiMiscellaneousState, any> = {
getEntries: (state) => {
const output: GuiMiscellaneousStateEntry[] = []
Object.entries(state.entries).forEach(([key, values]) => {
output.push({
id: key,
name: values.name,
type: values.type,
lightgroups: { ...values.lightgroups },
presets: { ...values.presets },
})
})
return output
},
getEntry: (state, getters) => (payload: { type: string; name: string }) => {
return getters.getEntries.find(
(entry: GuiMiscellaneousStateEntry) => entry.name === payload.name && entry.type === payload.type
) as GuiMiscellaneousStateEntry
},
getId: (state, getters) => (payload: { type: string; name: string }) => {
return getters.getEntry(payload)?.id ?? null
},
getEntryLightgroups: (state, getters) => (payload: { type: string; name: string }) => {
const entry = getters.getEntry(payload) as GuiMiscellaneousStateEntry | null
if (!entry) return []
const groups: GuiMiscellaneousStateEntryLightgroup[] = []
Object.entries(entry.lightgroups).forEach(([key, lightgroup]) => {
groups.push({
name: lightgroup.name,
start: lightgroup.start,
end: lightgroup.end,
id: key,
})
})
return caseInsensitiveSort(groups, 'name')
},
getEntryPresets: (state, getters) => (payload: { type: string; name: string }) => {
const entry = getters.getEntry(payload) as GuiMiscellaneousStateEntry | null
if (!entry) return []
const presets: GuiMiscellaneousStateEntryPreset[] = []
Object.entries(entry.presets).forEach(([key, preset]) => {
presets.push({
name: preset.name,
red: preset.red,
green: preset.green,
blue: preset.blue,
white: preset.white,
id: key,
})
})
return caseInsensitiveSort(presets, 'name')
},
}

View File

@ -0,0 +1,23 @@
import { Module } from 'vuex'
import { actions } from '@/store/gui/miscellaneous/actions'
import { mutations } from '@/store/gui/miscellaneous/mutations'
import { getters } from '@/store/gui/miscellaneous/getters'
import { GuiMiscellaneousState } from '@/store/gui/miscellaneous/types'
export const getDefaultState = (): GuiMiscellaneousState => {
return {
entries: {},
}
}
// initial state
const state = getDefaultState()
// eslint-disable-next-line
export const miscellaneous: Module<GuiMiscellaneousState, any> = {
namespaced: true,
state,
getters,
actions,
mutations,
}

View File

@ -0,0 +1,92 @@
import { getDefaultState } from './index'
import { MutationTree } from 'vuex'
import Vue from 'vue'
import {
GuiMiscellaneousState,
GuiMiscellaneousStateEntry,
GuiMiscellaneousStateEntryLightgroup,
GuiMiscellaneousStateEntryPreset,
} from '@/store/gui/miscellaneous/types'
export const mutations: MutationTree<GuiMiscellaneousState> = {
reset(state) {
Object.assign(state, getDefaultState())
},
store(state, payload: payloadStore) {
const values: GuiMiscellaneousStateEntry = {
name: payload.values.name,
type: payload.values.type,
lightgroups: {},
presets: {},
}
Vue.set(state.entries, payload.id, values)
},
updateLightgroup(state, payload: payloadUpdateLightgroup) {
const lightgroup: GuiMiscellaneousStateEntryLightgroup = {
name: payload.values.name,
start: parseInt(payload.values.start.toString()),
end: parseInt(payload.values.end.toString()),
}
Vue.set(state.entries[payload.entryId].lightgroups, payload.lightgroupId, lightgroup)
},
destroyLightgroup(state, payload: payloadDestroyLightgroup) {
const entries = { ...state.entries }
delete entries[payload.entryId].lightgroups[payload.lightgroupId]
Vue.set(state, 'entries', entries)
},
updatePreset(state, payload: payloadUpdatePreset) {
const preset: GuiMiscellaneousStateEntryPreset = {
name: payload.values.name,
red: payload.values.red,
green: payload.values.green,
blue: payload.values.blue,
white: payload.values.white,
}
Vue.set(state.entries[payload.entryId].presets, payload.presetId, preset)
},
destroyPreset(state, payload: payloadDestroyPreset) {
const entries = { ...state.entries }
delete entries[payload.entryId].presets[payload.presetId]
Vue.set(state, 'entries', entries)
},
}
interface payloadStore {
id: string
values: {
type: string
name: string
}
}
interface payloadUpdateLightgroup {
entryId: string
lightgroupId: string
values: GuiMiscellaneousStateEntryLightgroup
}
interface payloadDestroyLightgroup {
entryId: string
lightgroupId: string
}
interface payloadUpdatePreset {
entryId: string
presetId: string
values: GuiMiscellaneousStateEntryPreset
}
interface payloadDestroyPreset {
entryId: string
presetId: string
}

View File

@ -0,0 +1,33 @@
export interface GuiMiscellaneousState {
entries: {
[key: string]: GuiMiscellaneousStateEntry
}
}
export interface GuiMiscellaneousStateEntry {
id?: string
type: string
name: string
lightgroups: {
[key: string]: GuiMiscellaneousStateEntryLightgroup
}
presets: {
[key: string]: GuiMiscellaneousStateEntryPreset
}
}
export interface GuiMiscellaneousStateEntryLightgroup {
id?: string
name: string
start: number
end: number
}
export interface GuiMiscellaneousStateEntryPreset {
id?: string
name: string
red: number | null
blue: number | null
green: number | null
white: number | null
}

View File

@ -21,6 +21,8 @@ import {
PrinterStateTemperatureSensor,
PrinterStateToolchangeMacro,
PrinterStateAdditionalSensor,
PrinterGetterObject,
PrinterStateLight,
} from '@/store/printer/types'
import { caseInsensitiveSort, formatFrequency, getMacroParams } from '@/plugins/helpers'
import { RootState } from '@/store/types'
@ -139,6 +141,29 @@ export const getters: GetterTree<PrinterState, RootState> = {
return 0
},
getPrinterObjects: (state) => (supportedObjects: string[]) => {
const outputObjects: PrinterGetterObject[] = []
for (const [key, value] of Object.entries(state)) {
let type = key.substring(0, key.indexOf(' ')).trimEnd()
let name = key.substring(key.indexOf(' ') + 1).trimStart()
if (key.indexOf(' ') === -1) type = name = key
if (supportedObjects.includes(type)) {
outputObjects.push({
name,
type,
state: { ...value },
config: state.configfile?.config[key] ?? {},
settings: state.configfile?.settings[key] ?? {},
})
}
}
return outputObjects
},
getMacros: (state) => {
const array: PrinterStateMacro[] = []
const config = state.configfile?.config ?? {}
@ -429,26 +454,21 @@ export const getters: GetterTree<PrinterState, RootState> = {
return 'fan' in state ? state.fan.speed : 0
},
getFans: (state) => {
getFans: (state, getters) => {
const fans: PrinterStateFan[] = []
const supportedFans = ['temperature_fan', 'controller_fan', 'heater_fan', 'fan_generic', 'fan']
const objects = getters.getPrinterObjects(supportedFans)
const controllableFans = ['fan_generic', 'fan']
for (const [key, value] of Object.entries(state)) {
const nameSplit = key.split(' ')
if (supportedFans.includes(nameSplit[0])) {
const name = nameSplit.length > 1 ? nameSplit[1] : nameSplit[0]
fans.push({
name: name,
type: nameSplit[0],
speed: 'speed' in value ? value.speed : 0,
controllable: controllableFans.includes(nameSplit[0]),
})
}
}
objects.foreach((object: PrinterGetterObject) => {
fans.push({
name: object.name,
type: object.type,
speed: object.state.speed ?? 0,
controllable: controllableFans.includes(object.type),
})
})
return fans.sort((a, b) => {
if (a.controllable < b.controllable) return 1
@ -464,6 +484,86 @@ export const getters: GetterTree<PrinterState, RootState> = {
})
},
getLights: (state, getters) => {
const lights: PrinterStateLight[] = []
const supportedObjects = ['dotstar', 'led', 'neopixel', 'pca9533', 'pca9632']
const objects = getters.getPrinterObjects(supportedObjects)
objects
.filter((object: PrinterGetterObject) => {
return !object.name.startsWith('_')
})
.forEach((object: PrinterGetterObject) => {
let colorOrder = 'RGB'
let singleChannelTarget = null
const colorData = object.state.color_data ?? []
if ('color_order' in object.settings) colorOrder = object.settings.color_order[0] ?? ''
if (object.type === 'led') {
colorOrder = ''
if ('red_pin' in object.config) colorOrder += 'R'
if ('green_pin' in object.config) colorOrder += 'G'
if ('blue_pin' in object.config) colorOrder += 'B'
if ('white_pin' in object.config) colorOrder += 'W'
}
let initialRed = object.settings.initial_red ?? null
if (!('initial_red' in object.config)) initialRed = null
let initialGreen = object.settings.initial_green ?? null
if (!('initial_green' in object.config)) initialGreen = null
let initialBlue = object.settings.initial_blue ?? null
if (!('initial_blue' in object.config)) initialBlue = null
let initialWhite = object.settings.initial_white ?? null
if (!('initial_white' in object.config)) initialWhite = null
if (object.type === 'led' && colorOrder.length === 1) {
const firstColorData = colorData[0] ?? []
switch (colorOrder) {
case 'R':
singleChannelTarget = firstColorData[0] ?? 0
break
case 'G':
singleChannelTarget = firstColorData[1] ?? 0
break
case 'B':
singleChannelTarget = firstColorData[2] ?? 0
break
case 'W':
singleChannelTarget = firstColorData[3] ?? 0
break
}
}
lights.push({
name: object.name,
type: object.type as PrinterStateLight['type'],
chainCount: object.settings.chain_count ?? 1,
colorOrder,
initialRed,
initialGreen,
initialBlue,
initialWhite,
colorData,
singleChannelTarget,
})
})
return lights.sort((a, b) => {
const nameA = a.name.toUpperCase()
const nameB = b.name.toUpperCase()
if (nameA < nameB) return -1
if (nameA > nameB) return 1
return 0
})
},
getMiscellaneous: (state) => {
const output: PrinterStateMiscellaneous[] = []
const supportedObjects = ['controller_fan', 'heater_fan', 'fan_generic', 'fan', 'output_pin']

View File

@ -108,6 +108,32 @@ export interface PrinterStateFan {
controllable: boolean
}
export interface PrinterStateLight {
name: string
type: 'led' | 'neopixel' | 'dotstar' | 'pca9533' | 'pca9632'
colorOrder: string
chainCount: number
initialRed: number | null
initialGreen: number | null
initialBlue: number | null
initialWhite: number | null
colorData: number[][]
singleChannelTarget: number | null
}
export interface PrinterStateLight {
name: string
type: 'led' | 'neopixel' | 'dotstar' | 'pca9533' | 'pca9632'
colorOrder: string
chainCount: number
initialRed: number | null
initialGreen: number | null
initialBlue: number | null
initialWhite: number | null
colorData: number[][]
singleChannelTarget: number | null
}
export interface PrinterStateMiscellaneous {
name: string
type: string
@ -227,3 +253,17 @@ export interface PrinterStateToolchangeMacro {
name: string
active: boolean
}
export interface PrinterGetterObject {
name: string
type: string
state: {
[key: string]: any
}
config: {
[key: string]: string
}
settings: {
[key: string]: any
}
}