Compare commits

...
Author SHA1 Message Date
EinsPommes 4e233ae718 refactor(extruder): improve code readability and structure in API and mock implementations 2026-02-24 22:55:07 +01:00
EinsPommes f35ba7862e refactor(extruder): streamline heating fault management and enhance UI interactions 2026-02-24 22:47:50 +01:00
ᴏxᴏ.ᴍɪᴋᴀ 80048c0bbb Merge branch 'master' into 972-extruder-v3-heat-problem 2026-02-24 22:07:11 +01:00
EinsPommes b8bb8053c7 refactor(extruder): remove temperature target enabled state from Extruder3Settings 2026-02-21 13:39:19 +01:00
EinsPommes 0305867394 feat(extruder): introduce temperature target enable/disable functionality for nozzle control 2026-02-21 13:37:51 +01:00
EinsPommes 09f56ed990 feat(extruder): add target temperature control feature 2026-02-21 13:37:39 +01:00
Christian 8beb24cef0 Update electron/src/machines/extruder/extruder3/Extruder3ControlPage.tsx 2026-02-21 13:37:16 +01:00
EinsPommes ed84982c8e refactor(extruder): enhance heating fault handling and watchdog timeout 2026-02-21 13:37:13 +01:00
EinsPommes e792ed1b4d feat(extruder): implement heating fault acknowledgment for v3 2026-02-21 13:37:06 +01:00
EinsPommes 132ee383ee Update comments 2026-02-21 13:36:59 +01:00
EinsPommes 34a8c54098 Add heating fault handling and safeguard features to Extruder v3
- Introduced heating fault state management with UI notifications in HeatingZone component.
- Implemented heating safeguard feature to monitor temperature increases and automatically set to standby if conditions are not met.
- Added retry functionality for heating faults, allowing users to attempt to reheat after a fault.
- Updated state schemas and API to accommodate new heating fault and safeguard states.
- Enhanced Extruder v3 control page to integrate new features and improve user experience.
2026-02-21 13:36:48 +01:00
22 changed files with 720 additions and 65 deletions

No files matched your search

+23 -2
View File
@@ -16,7 +16,9 @@ type Props = {
min: number;
max: number;
onChangeTargetTemp?: (temperature: number) => void;
targetTemperatureEnabled: boolean;
heatingFault?: boolean;
onRetryHeating?: () => void;
targetTemperatureEnabled?: boolean;
};
export function HeatingZone({
title,
@@ -26,7 +28,9 @@ export function HeatingZone({
heatingTimeSeries,
heatingPower,
onChangeTargetTemp,
targetTemperatureEnabled,
heatingFault,
onRetryHeating,
targetTemperatureEnabled = true,
}: Props) {
const targetTemperature = heatingState?.target_temperature ?? 150;
@@ -73,6 +77,23 @@ export function HeatingZone({
Cant Read Temperature! Check Temperature Sensor Wiring!
</StatusBadge>
)}
{heatingFault && (
<div className="flex flex-col gap-2">
<StatusBadge variant="error">
Heating Fault: Temperature not increasing. Extruder set to standby.
</StatusBadge>
{onRetryHeating && (
<button
onClick={onRetryHeating}
className="inline-block w-fit max-w-max rounded bg-blue-600 px-4 py-2 text-base whitespace-nowrap text-white hover:bg-blue-700"
style={{ minWidth: "auto", width: "fit-content" }}
>
Retry Heating
</button>
)}
</div>
)}
</ControlCard>
);
}
@@ -13,6 +13,7 @@ import { roundToDecimals } from "@/lib/decimal";
import { useExtruder3 } from "./useExtruder";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { StatusBadge } from "@/control/StatusBadge";
import { GlobalHeatingFaultToastManager } from "./GlobalHeatingFaultToastManager";
export function Extruder3ControlPage() {
const {
@@ -45,8 +46,17 @@ export function Extruder3ControlPage() {
isLoading,
isDisabled,
retryHeating,
acknowledgeHeatingFault,
} = useExtruder3();
// Helper to check if a zone has a heating fault
const getZoneFault = (zone: "front" | "middle" | "back" | "nozzle") => {
const faultZone = state?.heating_fault_state?.fault_zone;
const faultAcknowledged = state?.heating_fault_state?.fault_acknowledged;
return faultZone === zone && !faultAcknowledged;
};
function isZoneReadyForExtrusion(
temperature: number,
targetTemperature: number,
@@ -80,6 +90,10 @@ export function Extruder3ControlPage() {
return (
<Page>
<GlobalHeatingFaultToastManager
state={state}
onAcknowledgeHeatingFault={acknowledgeHeatingFault}
/>
<ControlGrid>
<HeatingZone
title={"Heating Front"}
@@ -89,7 +103,9 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setFrontHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
heatingFault={getZoneFault("front")}
onRetryHeating={retryHeating}
targetTemperatureEnabled
/>
<HeatingZone
title={"Heating Middle"}
@@ -99,7 +115,9 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setMiddleHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
heatingFault={getZoneFault("middle")}
onRetryHeating={retryHeating}
targetTemperatureEnabled
/>
<HeatingZone
title={"Heating Back"}
@@ -109,7 +127,9 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setBackHeatingTemperature}
min={0}
max={300}
targetTemperatureEnabled={true}
heatingFault={getZoneFault("back")}
onRetryHeating={retryHeating}
targetTemperatureEnabled
/>
<HeatingZone
title={"Heating Nozzle"}
@@ -119,6 +139,8 @@ export function Extruder3ControlPage() {
onChangeTargetTemp={setNozzleHeatingTemperature}
min={0}
max={300}
heatingFault={getZoneFault("nozzle")}
onRetryHeating={retryHeating}
targetTemperatureEnabled={
state?.extruder_settings_state.nozzle_temperature_target_enabled ??
true
@@ -16,11 +16,12 @@ export function Extruder3SettingsPage() {
resetInverter,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setHeatingSafeguardEnabled,
setTemperatureTargetEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
setTemperatureTargetEnabled,
} = useExtruder3();
const [showAdvanced, setShowAdvanced] = useState(false);
@@ -69,10 +70,26 @@ export function Extruder3SettingsPage() {
onChange={setExtruderPressureLimitEnabled}
/>
</Label>
<Label label="Heating Safeguard">
<SelectionGroupBoolean
value={
state?.extruder_settings_state.heating_safeguard_enabled ?? true
}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
onChange={setHeatingSafeguardEnabled}
/>
<p className="text-muted-foreground mt-1 text-sm">
When enabled, monitors heating progress and automatically sets to
standby if temperature doesn&apos;t increase by at least 5°C within
60 seconds.
</p>
</Label>
<Label label="Nozzle Temperature Target Enabled">
<SelectionGroupBoolean
value={
state?.extruder_settings_state.nozzle_temperature_target_enabled
state?.extruder_settings_state
.nozzle_temperature_target_enabled ?? true
}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
@@ -0,0 +1,157 @@
import React, { useEffect, useRef } from "react";
import { toast } from "sonner";
import { Icon } from "@/components/Icon";
type HeatingFaultState = {
heating_fault_state?: {
fault_zone?: string | null;
fault_acknowledged?: boolean;
};
is_default_state?: boolean;
};
type HeatingFaultToastProps = {
zoneName: string;
onAcknowledge: () => void;
children?: React.ReactNode;
};
function HeatingFaultToast({
zoneName,
onAcknowledge,
children,
}: HeatingFaultToastProps) {
return (
<div className="flex w-100 flex-col gap-3 rounded-xl border border-red-400 bg-red-600 p-4 text-white shadow-xl backdrop-blur-sm transition-all duration-300">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Icon name="lu:TriangleAlert" className="h-5 w-5 text-yellow-200" />
<strong className="text-lg font-semibold tracking-wide">
Heating Fault
</strong>
</div>
<button
className="rounded-md p-1 text-2xl font-bold text-white/80 hover:bg-red-500 hover:text-white focus:ring-2 focus:ring-white/30 focus:outline-none"
onClick={onAcknowledge}
aria-label="Close"
>
×
</button>
</div>
{children ?? (
<p className="text-base leading-snug text-red-50">
The <strong>{zoneName}</strong> heating zone did not increase in
temperature by at least 5°C within 60 seconds. The extruder has been
automatically set to standby mode.
</p>
)}
</div>
);
}
function toZoneName(faultZone: string | null): string {
switch (faultZone) {
case "front":
return "Front";
case "middle":
return "Middle";
case "back":
return "Back";
case "nozzle":
return "Nozzle";
default:
return "Unknown";
}
}
function useHeatingFaultToastEffect({
state,
onAcknowledgeHeatingFault,
}: {
state: HeatingFaultState | null | undefined;
onAcknowledgeHeatingFault: () => void;
}) {
// Deduplicate by active fault zone
const activeFaultZone = useRef<string | null>(null);
const currentToastId = useRef<string | null>(null);
useEffect(() => {
if (!state) return;
const faultZone = state.heating_fault_state?.fault_zone ?? null;
const faultAcknowledged =
state.heating_fault_state?.fault_acknowledged ?? false;
const isDefault = !!state.is_default_state;
if (isDefault) return;
const faultZoneStr = faultZone ?? "unknown";
const zoneName = toZoneName(faultZone);
if (
faultZone &&
!faultAcknowledged &&
activeFaultZone.current !== faultZone
) {
activeFaultZone.current = faultZone;
const toastId = `heating-fault-${faultZoneStr}`;
currentToastId.current = toastId;
toast(
<HeatingFaultToast
zoneName={zoneName}
onAcknowledge={() => {
onAcknowledgeHeatingFault();
toast.dismiss(toastId);
activeFaultZone.current = null;
currentToastId.current = null;
}}
/>,
{
id: toastId,
duration: Infinity,
position: "top-center",
style: {
background: "transparent",
padding: 0,
boxShadow: "none",
border: "none",
},
},
);
}
if (faultAcknowledged || !faultZone) {
if (currentToastId.current) {
toast.dismiss(currentToastId.current);
currentToastId.current = null;
}
activeFaultZone.current = null;
}
}, [state, onAcknowledgeHeatingFault]);
}
/**
* Watches StateEvent for heating faults and shows toasts.
* Component is non-visual.
*/
export function GlobalHeatingFaultToastManager({
state,
onAcknowledgeHeatingFault,
}: {
state:
| {
heating_fault_state?: {
fault_zone?: string | null;
fault_acknowledged?: boolean;
};
is_default_state?: boolean;
}
| null
| undefined;
onAcknowledgeHeatingFault: () => void;
}) {
useHeatingFaultToastEffect({ state, onAcknowledgeHeatingFault });
return null;
}
@@ -103,13 +103,22 @@ export const heatingStatesSchema = z.object({
middle: heatingStateSchema,
});
/**
* Heating fault state schema
*/
export const heatingFaultStateSchema = z.object({
fault_zone: z.string().nullable(),
fault_acknowledged: z.boolean(),
});
/**
* Extruder settings state schema
*/
export const extruderSettingsStateSchema = z.object({
pressure_limit: z.number(),
pressure_limit_enabled: z.boolean(),
nozzle_temperature_target_enabled: z.boolean(),
heating_safeguard_enabled: z.boolean(),
nozzle_temperature_target_enabled: z.boolean().optional(),
});
/**
@@ -178,6 +187,7 @@ export const stateEventDataSchema = z.object({
extruder_settings_state: extruderSettingsStateSchema,
inverter_status_state: inverterStatusStateSchema,
pid_settings: pidSettingsSchema,
heating_fault_state: heatingFaultStateSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -300,7 +310,6 @@ export function extruder3MessageHandler(
try {
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
const nextTargetPressure = stateEvent.data.pressure_state.target_bar;
@@ -232,18 +232,39 @@ export function useExtruder3() {
);
};
const { request: requestHeatingSafeguardEnabled } = useMachineMutation(
z.object({ SetHeatingSafeguardEnabled: z.boolean() }),
);
const setHeatingSafeguardEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.heating_safeguard_enabled =
enabled;
},
() =>
requestHeatingSafeguardEnabled({
machine_identification_unique: machineIdentification,
data: { SetHeatingSafeguardEnabled: enabled },
}),
);
};
const setTemperatureTargetEnabled = (enabled: boolean) => {
const res = enabled
? true
: confirm(
"This will disable the ability to set a temperature target for the nozzle heating element and will any wiring error message. Only proceed if you understand the risks.",
"This will disable the ability to set a temperature target for the nozzle heating element. Only proceed if you understand the risks.",
);
if (res) {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.nozzle_temperature_target_enabled =
enabled;
(
current.data.extruder_settings_state as {
nozzle_temperature_target_enabled?: boolean;
}
).nozzle_temperature_target_enabled = enabled;
},
() =>
requestNozzleTemperatureTargetEnabled({
@@ -432,6 +453,28 @@ export function useExtruder3() {
z.object({ ResetInverter: z.boolean() }),
);
const { request: requestRetryHeating } = useMachineMutation(
z.object({ RetryHeating: z.literal(true) }),
);
const retryHeating = () => {
requestRetryHeating({
machine_identification_unique: machineIdentification,
data: { RetryHeating: true },
});
};
const { request: requestAcknowledgeHeatingFault } = useMachineMutation(
z.object({ AcknowledgeHeatingFault: z.literal(true) }),
);
const acknowledgeHeatingFault = () => {
requestAcknowledgeHeatingFault({
machine_identification_unique: machineIdentification,
data: { AcknowledgeHeatingFault: true },
});
};
return {
// Consolidated state
state: stateOptimistic.value?.data,
@@ -481,11 +524,14 @@ export function useExtruder3() {
setMiddleHeatingTemperature,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setHeatingSafeguardEnabled,
setTemperatureTargetEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
resetInverter,
retryHeating,
acknowledgeHeatingFault,
};
}
+2
View File
@@ -152,6 +152,8 @@ pub struct HeatingState {
pub struct ExtruderSettingsState {
pub pressure_limit: f64,
pub pressure_limit_enabled: bool,
/// Heating safeguard watchdog enabled (default: true)
pub heating_safeguard_enabled: bool,
pub nozzle_temperature_target_enabled: bool,
}
+3 -1
View File
@@ -94,6 +94,8 @@ impl ExtruderV2 {
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
// For ExtruderV2 the heating safeguard is always enabled and not user-configurable
heating_safeguard_enabled: true,
nozzle_temperature_target_enabled: self
.temperature_controller_nozzle
.get_temperature_target_enabled(),
@@ -215,7 +217,7 @@ impl ExtruderV2 {
self.namespace.emit(ExtruderV2Events::LiveValues(event));
}
// === Steuerungsfunktionen mit emit_state ===
// === Control functions with emit_state ===
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.screw_speed_controller
+1
View File
@@ -81,6 +81,7 @@ impl MachineNewTrait for ExtruderV2 {
extruder_settings_state: ExtruderSettingsState {
pressure_limit: 200.0,
pressure_limit_enabled: false,
heating_safeguard_enabled: true,
nozzle_temperature_target_enabled: true,
},
inverter_status_state: InverterStatusState {
+3 -3
View File
@@ -166,10 +166,10 @@ impl ExtruderV2 {
self.last_energy_calculation_time = Some(now);
}
// Funktionen ohne emit_state bleiben hier
// Functions without emit_state remain here
// Set all relais to ZERO
// We dont need a function to enable again though, as the act Loop will detect the mode
// Set all relays to ZERO
// We don't need a function to enable again though, as the act loop will detect the mode
fn turn_heating_off(&mut self) {
self.temperature_controller_back.disable();
self.temperature_controller_front.disable();
+2 -2
View File
@@ -140,14 +140,14 @@ impl MachineNewTrait for ExtruderV2 {
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
let t4 = TemperatureInput::new(el3204, EL3204Port::T4);
// For the Relais
// For the Relays
let digital_out_1 = DigitalOutput::new(el2004.clone(), EL2004Port::DO1);
let digital_out_2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
let digital_out_3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
let digital_out_4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
let pressure_sensor = AnalogInput::new(el3021, EL3021Port::AI1);
// The Extruders temparature Controllers should disable the relais when the max_temperature is reached
// The Extruder's temperature controllers should disable the relays when the max_temperature is reached
let extruder_max_temperature = ThermodynamicTemperature::new::<degree_celsius>(300.0);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_front = TemperatureController::new(
@@ -20,7 +20,8 @@ pub struct TemperatureController {
temperature_pid_output: f64,
heating_element_wattage: f64,
max_clamp: f64,
target_temp_enabled: bool, // Sets whether the frontend should display a target temperature setter for this temp controller
/// When false, target temperature cannot be set (nozzle only; other zones always true)
temperature_target_enabled: bool,
}
impl TemperatureController {
@@ -56,22 +57,22 @@ impl TemperatureController {
temperature_pid_output: 0.0,
heating_element_wattage,
max_clamp,
target_temp_enabled: true,
temperature_target_enabled: true,
}
}
pub fn get_temperature_target_enabled(&self) -> bool {
self.temperature_target_enabled
}
pub fn set_temperature_target_enabled(&mut self, enabled: bool) {
self.temperature_target_enabled = enabled;
}
pub fn set_target_temperature(&mut self, temp: ThermodynamicTemperature) {
self.heating.target_temperature = temp;
}
pub fn set_temperature_target_enabled(&mut self, enabled: bool) {
self.target_temp_enabled = enabled;
}
pub fn get_temperature_target_enabled(&self) -> bool {
self.target_temp_enabled
}
pub const fn disallow_heating(&mut self) {
self.heating_allowed = false;
}
@@ -96,7 +97,7 @@ impl TemperatureController {
self.heating.temperature = temperature_celsius;
if self.heating.temperature > self.max_temperature {
// disable the relais and return
// Disable the relays and return
self.relais.set(false);
self.heating.heating = false;
return;
+4 -1
View File
@@ -41,7 +41,10 @@ impl MachineAct for ExtruderV3 {
self.switch_to_heat();
}
let now = Instant::now();
// Monitor heating watchdog if safeguard is enabled
if self.heating_safeguard_enabled {
self.update_heating_watchdog(now);
}
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
+35 -3
View File
@@ -110,6 +110,8 @@ pub struct StateEvent {
pub inverter_status_state: InverterStatusState,
/// pid settings
pub pid_settings: PidSettingsStates,
/// heating fault state
pub heating_fault_state: HeatingFaultState,
}
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
@@ -117,6 +119,15 @@ pub struct ModeState {
pub mode: ExtruderV3Mode,
}
/// Heating fault information
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
pub struct HeatingFaultState {
/// Zone that has a heating fault (None if no fault)
pub fault_zone: Option<String>,
/// Whether a fault has been detected and acknowledged
pub fault_acknowledged: bool,
}
pub enum ExtruderV3Events {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
@@ -143,6 +154,9 @@ pub enum Mutation {
SetExtruderPressureLimit(f64),
SetExtruderPressureLimitIsEnabled(bool),
// Heating Safeguard
SetHeatingSafeguardEnabled(bool),
// ToggleTemperatureTarget
SetNozzleTemperatureTargetEnabled(bool),
@@ -152,6 +166,10 @@ pub enum Mutation {
// Reset
ResetInverter(bool),
// Heating Fault Handling
RetryHeating,
AcknowledgeHeatingFault,
}
#[derive(Debug)]
@@ -216,7 +234,12 @@ impl MachineApi for ExtruderV3 {
self.set_target_temperature(temp, HeatingType::Back)
}
Mutation::SetNozzleHeatingTemperature(temp) => {
self.set_target_temperature(temp, HeatingType::Nozzle)
if self
.temperature_controller_nozzle
.get_temperature_target_enabled()
{
self.set_target_temperature(temp, HeatingType::Nozzle)
}
}
Mutation::SetExtruderPressureLimit(pressure_limit) => {
self.set_nozzle_pressure_limit(pressure_limit);
@@ -224,14 +247,23 @@ impl MachineApi for ExtruderV3 {
Mutation::SetExtruderPressureLimitIsEnabled(enabled) => {
self.set_nozzle_pressure_limit_is_enabled(enabled);
}
Mutation::SetHeatingSafeguardEnabled(enabled) => {
self.set_heating_safeguard_enabled(enabled);
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
}
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
Mutation::RetryHeating => {
self.retry_heating();
}
Mutation::AcknowledgeHeatingFault => {
self.acknowledge_heating_fault();
}
}
Ok(())
+35 -1
View File
@@ -102,6 +102,7 @@ impl ExtruderV3 {
pressure_limit_enabled: self
.screw_speed_controller
.get_nozzle_pressure_limit_enabled(),
heating_safeguard_enabled: self.heating_safeguard_enabled,
nozzle_temperature_target_enabled: self
.temperature_controller_nozzle
.get_temperature_target_enabled(),
@@ -150,6 +151,7 @@ impl ExtruderV3 {
kd: self.screw_speed_controller.pid.get_kd(),
},
},
heating_fault_state: self.heating_fault_state.clone(),
}
}
}
@@ -235,7 +237,7 @@ impl ExtruderV3 {
self.namespace.emit(ExtruderV3Events::LiveValues(event));
}
// === Steuerungsfunktionen mit emit_state ===
// === Control functions with emit_state ===
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
self.screw_speed_controller
@@ -243,9 +245,41 @@ impl ExtruderV3 {
self.emit_state();
}
pub fn set_heating_safeguard_enabled(&mut self, enabled: bool) {
self.heating_safeguard_enabled = enabled;
tracing::info!(
"Heating safeguard {}",
if enabled { "enabled" } else { "disabled" }
);
// Reset watchdog state when toggling
self.heating_watchdog = super::HeatingWatchdog::default();
self.emit_state();
}
pub fn set_nozzle_temperature_target_is_enabled(&mut self, enabled: bool) {
self.temperature_controller_nozzle
.set_temperature_target_enabled(enabled);
tracing::info!(
"Nozzle temperature target {}",
if enabled { "enabled" } else { "disabled" }
);
self.emit_state();
}
pub fn retry_heating(&mut self) {
tracing::info!(
"Retrying heating after fault - resetting watchdog and switching to Heat mode"
);
self.heating_fault_state.fault_zone = None;
self.heating_fault_state.fault_acknowledged = false;
self.heating_watchdog = super::HeatingWatchdog::default();
self.switch_to_heat();
self.emit_state();
}
pub fn acknowledge_heating_fault(&mut self) {
tracing::info!("Heating fault acknowledged by user");
self.heating_fault_state.fault_acknowledged = true;
self.emit_state();
}
+21 -2
View File
@@ -1,15 +1,25 @@
use super::ExtruderV2;
use crate::{
MachineApi,
extruder1::{HeatingType, api::Mutation},
extruder1::{ExtruderV2Mode, HeatingType},
extruder2::ExtruderV3Mode,
extruder2::api::Mutation,
};
fn map_v3_mode_to_v2(mode: ExtruderV3Mode) -> ExtruderV2Mode {
match mode {
ExtruderV3Mode::Standby => ExtruderV2Mode::Standby,
ExtruderV3Mode::Heat => ExtruderV2Mode::Heat,
ExtruderV3Mode::Extrude => ExtruderV2Mode::Extrude,
}
}
impl MachineApi for ExtruderV2 {
fn api_mutate(&mut self, request_body: serde_json::Value) -> Result<(), anyhow::Error> {
// there are multiple Modbus Frames that are "prebuilt"
let control: Mutation = serde_json::from_value(request_body)?;
match control {
Mutation::SetExtruderMode(mode) => self.set_mode_state(mode),
Mutation::SetExtruderMode(mode) => self.set_mode_state(map_v3_mode_to_v2(mode)),
Mutation::SetInverterRotationDirection(forward) => self.set_rotation_state(forward),
Mutation::SetInverterRegulation(uses_rpm) => self.set_regulation(uses_rpm),
Mutation::SetInverterTargetPressure(bar) => self.set_target_pressure(bar),
@@ -41,9 +51,18 @@ impl MachineApi for ExtruderV2 {
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
Mutation::SetHeatingSafeguardEnabled(_enabled) => {
// Mock implementation - just acknowledge
}
Mutation::SetNozzleTemperatureTargetEnabled(enabled) => {
self.set_nozzle_temperature_target_is_enabled(enabled);
}
Mutation::RetryHeating => {
// Mock implementation - just acknowledge
}
Mutation::AcknowledgeHeatingFault => {
// Mock implementation - just acknowledge
}
}
Ok(())
}
+32 -6
View File
@@ -1,6 +1,10 @@
use crate::extruder1::{
ExtruderV2Mode, HeatingType,
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent, TemperaturePid},
api::{ModeState as ModeStateV2, PidSettings, TemperaturePid},
};
use crate::extruder2::ExtruderV3Mode;
use crate::extruder2::api::{
ExtruderV3Events, HeatingFaultState, LiveValuesEvent, ModeState, MotorStatusValues, StateEvent,
};
use crate::extruder2::mock::ExtruderV2;
@@ -14,7 +18,13 @@ impl ExtruderV2 {
StateEvent {
is_default_state: !self.emitted_default_state,
rotation_state: self.rotation_state.clone(),
mode_state: self.mode_state.clone(),
mode_state: ModeState {
mode: match self.mode_state.mode {
ExtruderV2Mode::Standby => ExtruderV3Mode::Standby,
ExtruderV2Mode::Heat => ExtruderV3Mode::Heat,
ExtruderV2Mode::Extrude => ExtruderV3Mode::Extrude,
},
},
regulation_state: self.regulation_state.clone(),
pressure_state: self.pressure_state.clone(),
screw_state: self.screw_state.clone(),
@@ -22,6 +32,10 @@ impl ExtruderV2 {
extruder_settings_state: self.extruder_settings_state.clone(),
inverter_status_state: self.inverter_status_state.clone(),
pid_settings: self.pid_settings.clone(),
heating_fault_state: HeatingFaultState {
fault_zone: None,
fault_acknowledged: false,
},
}
}
@@ -31,7 +45,7 @@ impl ExtruderV2 {
self.last_status_hash = Some(hash);
self.emitted_default_state = true;
let event = state.build();
self.namespace.emit(ExtruderV2Events::State(event));
self.namespace.emit(ExtruderV3Events::State(event));
}
pub fn maybe_emit_state_event(&mut self) {
@@ -51,7 +65,13 @@ impl ExtruderV2 {
pub fn get_live_values(&self) -> LiveValuesEvent {
LiveValuesEvent {
motor_status: self.motor_status.clone(),
motor_status: MotorStatusValues {
screw_rpm: self.motor_status.screw_rpm,
frequency: self.motor_status.frequency,
voltage: self.motor_status.voltage,
current: self.motor_status.current,
power: self.motor_status.power,
},
pressure: self.pressure,
nozzle_temperature: self.nozzle_temperature,
front_temperature: self.front_temperature,
@@ -68,7 +88,7 @@ impl ExtruderV2 {
pub fn emit_live_values(&mut self) {
let event = self.get_live_values().build();
self.namespace.emit(ExtruderV2Events::LiveValues(event));
self.namespace.emit(ExtruderV3Events::LiveValues(event));
}
pub fn set_nozzle_pressure_limit_is_enabled(&mut self, enabled: bool) {
@@ -76,6 +96,12 @@ impl ExtruderV2 {
self.emit_state();
}
pub fn set_nozzle_temperature_target_enabled(&mut self, enabled: bool) {
self.extruder_settings_state
.nozzle_temperature_target_enabled = enabled;
self.emit_state();
}
pub fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
self.extruder_settings_state.pressure_limit = pressure;
self.emit_state();
@@ -95,7 +121,7 @@ impl ExtruderV2 {
}
pub fn set_mode_state(&mut self, mode: ExtruderV2Mode) {
self.mode_state = ModeState { mode };
self.mode_state = ModeStateV2 { mode };
self.emit_state();
}
+5 -4
View File
@@ -20,11 +20,12 @@ use crate::{
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingStates, InverterStatusState,
ModeState, MotorStatusValues, PidSettingsStates, PressureState, RegulationState,
RotationState, ScrewState,
ExtruderSettingsState, HeatingStates, InverterStatusState, ModeState,
MotorStatusValues, PidSettingsStates, PressureState, RegulationState, RotationState,
ScrewState,
},
},
extruder2::api::ExtruderV3Namespace,
};
// Just checking mock-machine feature here to exclude these modules from compilation entirely
@@ -57,7 +58,7 @@ pub struct ExtruderV2 {
machine_identification_unique: MachineIdentificationUnique,
namespace: ExtruderV2Namespace,
namespace: ExtruderV3Namespace,
last_measurement_emit: Instant,
last_status_hash: Option<u64>,
mode: ExtruderV2Mode,
+6 -6
View File
@@ -3,13 +3,12 @@ use crate::{
extruder1::{
ExtruderV2Mode,
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
TemperaturePidStates,
ExtruderSettingsState, HeatingState, HeatingStates, InverterStatusState, ModeState,
MotorStatusValues, PidSettings, PidSettingsStates, PressureState, RegulationState,
RotationState, ScrewState, TemperaturePid, TemperaturePidStates,
},
},
extruder2::mock::ExtruderV2,
extruder2::{api::ExtruderV3Namespace, mock::ExtruderV2},
};
impl MachineNewTrait for ExtruderV2 {
@@ -39,7 +38,7 @@ impl MachineNewTrait for ExtruderV2 {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
namespace: ExtruderV2Namespace {
namespace: ExtruderV3Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: now,
@@ -81,6 +80,7 @@ impl MachineNewTrait for ExtruderV2 {
extruder_settings_state: ExtruderSettingsState {
pressure_limit: 200.0,
pressure_limit_enabled: false,
heating_safeguard_enabled: true,
nozzle_temperature_target_enabled: true,
},
inverter_status_state: InverterStatusState {
+259 -3
View File
@@ -52,6 +52,151 @@ pub enum HeatingType {
Middle,
}
/// Watchdog state for monitoring heating progress
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug, Clone)]
pub struct HeatingWatchdogZone {
/// Temperature when heating started for this zone
start_temperature: Option<f64>,
/// Timestamp when heating started
heating_start_time: Option<Instant>,
/// Whether a fault has been detected
fault_detected: bool,
/// Last second that was logged
last_logged_second: Option<u64>,
}
#[cfg(not(feature = "mock-machine"))]
impl Default for HeatingWatchdogZone {
fn default() -> Self {
Self {
start_temperature: None,
heating_start_time: None,
fault_detected: false,
last_logged_second: None,
}
}
}
#[cfg(not(feature = "mock-machine"))]
impl HeatingWatchdogZone {
fn check(
&mut self,
controller: &TemperatureController,
zone_name: &str,
now: Instant,
min_temperature_delta_c: f64,
watchdog_timeout_secs: u64,
) -> bool {
use std::time::Duration;
use units::thermodynamic_temperature::degree_celsius;
let current_temp = controller.heating.temperature.get::<degree_celsius>();
let target_temp = controller
.heating
.target_temperature
.get::<degree_celsius>();
let is_heating_active = controller.heating.heating;
// Check if heating should be active
let should_be_heating = target_temp > current_temp;
// Start monitoring when heating becomes active
if should_be_heating && is_heating_active {
if self.start_temperature.is_none() {
self.start_temperature = Some(current_temp);
self.heating_start_time = Some(now);
self.fault_detected = false;
tracing::info!(
"Heating watchdog started for {}: start_temp={:.1}°C, target_temp={:.1}°C",
zone_name,
current_temp,
target_temp
);
}
} else {
// Reset if heating is no longer active
if self.start_temperature.is_some() {
tracing::info!(
"Heating watchdog reset for {}: heating no longer active",
zone_name
);
*self = HeatingWatchdogZone::default();
}
return false; // No fault when not monitoring
}
// Check for fault if we're monitoring
if let (Some(start_temp), Some(start_time)) =
(self.start_temperature, self.heating_start_time)
{
let elapsed = now.duration_since(start_time);
let temp_increase = current_temp - start_temp;
// Check if timeout exceeded without sufficient temperature increase
if elapsed >= Duration::from_secs(watchdog_timeout_secs)
&& temp_increase < min_temperature_delta_c
&& !self.fault_detected
{
self.fault_detected = true;
tracing::warn!(
"Heating watchdog fault detected for {}: elapsed={:.1}s, temp_increase={:.2}°C, start_temp={:.1}°C, current_temp={:.1}°C, target_temp={:.1}°C",
zone_name,
elapsed.as_secs_f64(),
temp_increase,
start_temp,
current_temp,
target_temp
);
return true; // Fault detected
}
// Log progress periodically
let current_second = elapsed.as_secs();
if current_second > 0 && current_second % 10 == 0 {
let should_log = match self.last_logged_second {
Some(last) => current_second > last,
None => true,
};
if should_log {
tracing::debug!(
"Heating watchdog progress for {}: elapsed={:.1}s, temp_increase={:.2}°C",
zone_name,
elapsed.as_secs_f64(),
temp_increase
);
self.last_logged_second = Some(current_second);
}
}
}
false // No fault
}
}
/// Watchdog state for all heating zones
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug, Clone)]
pub struct HeatingWatchdog {
pub front: HeatingWatchdogZone,
pub middle: HeatingWatchdogZone,
pub back: HeatingWatchdogZone,
pub nozzle: HeatingWatchdogZone,
}
#[cfg(not(feature = "mock-machine"))]
impl Default for HeatingWatchdog {
fn default() -> Self {
Self {
front: HeatingWatchdogZone::default(),
middle: HeatingWatchdogZone::default(),
back: HeatingWatchdogZone::default(),
nozzle: HeatingWatchdogZone::default(),
}
}
}
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug)]
pub struct ExtruderV3 {
@@ -79,6 +224,15 @@ pub struct ExtruderV3 {
/// will be initalized as false and set to true by `emit_state`
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
/// Heating safeguard configuration
heating_safeguard_enabled: bool,
/// Heating watchdog state for each zone
heating_watchdog: HeatingWatchdog,
/// Current heating fault state
heating_fault_state: crate::extruder2::api::HeatingFaultState,
}
#[cfg(not(feature = "mock-machine"))]
@@ -143,10 +297,10 @@ impl ExtruderV3 {
self.last_energy_calculation_time = Some(now);
}
// Funktionen ohne emit_state bleiben hier
// Functions without emit_state remain here
// Set all relais to ZERO
// We dont need a function to enable again though, as the act Loop will detect the mode
// Set all relays to ZERO
// We don't need a function to enable again though, as the act loop will detect the mode
fn turn_heating_off(&mut self) {
self.temperature_controller_back.disable();
self.temperature_controller_front.disable();
@@ -214,4 +368,106 @@ impl ExtruderV3 {
fn reset_inverter(&mut self) {
self.screw_speed_controller.inverter.reset_inverter();
}
/// Update heating watchdog for all zones
/// Monitors temperature increases and detects heating faults
fn update_heating_watchdog(&mut self, now: Instant) {
// Watchdog parameters
// These defaults are intentionally conservative to avoid false positives
// on older machines or in cold environments. If we do not see at least
// ~5°C increase within one minute, we treat this as a fault.
const MIN_TEMPERATURE_DELTA_C: f64 = 5.0; // Minimum temperature increase in °C
const WATCHDOG_TIMEOUT_SECS: u64 = 60; // Timeout in seconds
// Only monitor when in Heat or Extrude mode
if self.mode == ExtruderV3Mode::Standby {
// Reset all watchdog states when in standby
self.heating_watchdog.front = HeatingWatchdogZone::default();
self.heating_watchdog.middle = HeatingWatchdogZone::default();
self.heating_watchdog.back = HeatingWatchdogZone::default();
self.heating_watchdog.nozzle = HeatingWatchdogZone::default();
return;
}
// Check all zones
let mut fault_detected = false;
if self.heating_watchdog.front.check(
&self.temperature_controller_front,
"front",
now,
MIN_TEMPERATURE_DELTA_C,
WATCHDOG_TIMEOUT_SECS,
) {
fault_detected = true;
}
if self.heating_watchdog.middle.check(
&self.temperature_controller_middle,
"middle",
now,
MIN_TEMPERATURE_DELTA_C,
WATCHDOG_TIMEOUT_SECS,
) {
fault_detected = true;
}
if self.heating_watchdog.back.check(
&self.temperature_controller_back,
"back",
now,
MIN_TEMPERATURE_DELTA_C,
WATCHDOG_TIMEOUT_SECS,
) {
fault_detected = true;
}
if self.heating_watchdog.nozzle.check(
&self.temperature_controller_nozzle,
"nozzle",
now,
MIN_TEMPERATURE_DELTA_C,
WATCHDOG_TIMEOUT_SECS,
) {
fault_detected = true;
}
// If fault detected, automatically set to standby and record fault
if fault_detected {
// Determine which zone(s) have faults
let fault_zones: Vec<&str> = [
(self.heating_watchdog.front.fault_detected, "front"),
(self.heating_watchdog.middle.fault_detected, "middle"),
(self.heating_watchdog.back.fault_detected, "back"),
(self.heating_watchdog.nozzle.fault_detected, "nozzle"),
]
.iter()
.filter_map(|(has_fault, zone)| if *has_fault { Some(*zone) } else { None })
.collect();
// In theory fault_detected guarantees at least one zone here, but use a
// defensive default to avoid panicking on an empty list in case the logic
// above is changed in the future.
let first_fault_zone = fault_zones.first().copied().unwrap_or("unknown");
if first_fault_zone == "unknown" {
tracing::error!(
"Heating watchdog reported fault_detected=true but no individual zone was marked as faulty"
);
}
let fault_zone_str = if fault_zones.is_empty() {
"unknown".to_string()
} else {
fault_zones.join(", ")
};
tracing::error!(
"Heating fault detected in zone(s): {} - automatically setting extruder to standby",
fault_zone_str
);
// Set fault state
self.heating_fault_state.fault_zone = Some(first_fault_zone.to_string());
self.heating_fault_state.fault_acknowledged = false;
self.switch_to_standby();
self.emit_state();
}
}
}
+9 -3
View File
@@ -88,7 +88,7 @@ impl MachineNewTrait for ExtruderV3 {
Heating, mitsubishi_cs80::MitsubishiCS80,
screw_speed_controller::ScrewSpeedController,
},
extruder2::{ExtruderV3Mode, api::ExtruderV3Namespace},
extruder2::{ExtruderV3Mode, HeatingWatchdog, api::ExtruderV3Namespace},
};
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
@@ -139,14 +139,14 @@ impl MachineNewTrait for ExtruderV3 {
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
let t4 = TemperatureInput::new(el3204, EL3204Port::T4);
// For the Relais
// For the Relays
let digital_out_1 = DigitalOutput::new(el2004.clone(), EL2004Port::DO1);
let digital_out_2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
let digital_out_3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
let digital_out_4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
let pressure_sensor = AnalogInput::new(el3021, EL3021Port::AI1);
// The Extruders temparature Controllers should disable the relais when the max_temperature is reached
// The Extruder's temperature controllers should disable the relays when the max_temperature is reached
let extruder_max_temperature = ThermodynamicTemperature::new::<degree_celsius>(300.0);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
let temperature_controller_front = TemperatureController::new(
@@ -241,6 +241,12 @@ impl MachineNewTrait for ExtruderV3 {
screw_speed_controller,
emitted_default_state: false,
last_status_hash: None,
heating_safeguard_enabled: true,
heating_watchdog: HeatingWatchdog::default(),
heating_fault_state: crate::extruder2::api::HeatingFaultState {
fault_zone: None,
fault_acknowledged: false,
},
};
extruder.emit_state();
Ok(extruder)
@@ -10,7 +10,7 @@ use units::thermodynamic_temperature::degree_celsius;
pub struct TemperatureController {
pub pid: PidController,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
relay: DigitalOutput,
pub heating: Heating,
pub target_temp: ThermodynamicTemperature,
window_start: Instant,
@@ -24,7 +24,7 @@ pub struct TemperatureController {
impl TemperatureController {
pub fn disable(&mut self) {
self.relais.set(false);
self.relay.set(false);
self.heating.heating = false;
self.disallow_heating();
}
@@ -36,7 +36,7 @@ impl TemperatureController {
target_temp: ThermodynamicTemperature,
max_temperature: ThermodynamicTemperature,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
relay: DigitalOutput,
heating: Heating,
pwm_duration: Duration,
heating_element_wattage: f64,
@@ -47,7 +47,7 @@ impl TemperatureController {
target_temp,
window_start: Instant::now(),
temperature_sensor,
relais,
relay,
heating,
heating_allowed: false,
pwm_period: pwm_duration,
@@ -86,8 +86,8 @@ impl TemperatureController {
self.heating.temperature = temperature_celsius;
if self.heating.temperature > self.max_temperature {
// disable the relais and return
self.relais.set(false);
// Disable the relays and return
self.relay.set(false);
self.heating.heating = false;
return;
}
@@ -113,7 +113,7 @@ impl TemperatureController {
// Relay is ON if within duty cycle window
let on = elapsed < on_time;
self.relais.set(on);
self.relay.set(on);
self.heating.heating = on;
}
}