From 644e75035381d1808f6842d544a80b439dfbe0a0 Mon Sep 17 00:00:00 2001 From: snacj <0xsnacj@proton.me> Date: Thu, 2 Oct 2025 16:02:47 +0200 Subject: [PATCH 01/15] Added global Laser Alert --- .../src/components/ui/checkbox-selection.tsx | 30 ++++++++++++++ .../src/machines/laser/LaserToastManager.tsx | 34 ++++++++++++++++ .../laser/laser1/Laser1ControlPage.tsx | 26 ++++++++++++- .../src/machines/laser/laser1/Laser1Page.tsx | 4 ++ .../machines/laser/laser1/Laser1Settings.tsx | 28 +++++++++++++ .../machines/laser/laser1/laser1Namespace.ts | 3 +- machines/src/laser/api.rs | 2 + machines/src/laser/mod.rs | 39 ++++++++++++++++--- machines/src/laser/new.rs | 3 ++ 9 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 electron/src/components/ui/checkbox-selection.tsx create mode 100644 electron/src/machines/laser/LaserToastManager.tsx create mode 100644 electron/src/machines/laser/laser1/Laser1Settings.tsx diff --git a/electron/src/components/ui/checkbox-selection.tsx b/electron/src/components/ui/checkbox-selection.tsx new file mode 100644 index 00000000..1d385c2f --- /dev/null +++ b/electron/src/components/ui/checkbox-selection.tsx @@ -0,0 +1,30 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +interface CheckboxProps extends React.ComponentProps<"input"> { + label?: string; // optional label next to the checkbox +} + +const Checkbox = React.forwardRef( + ({ className, label, ...props }, ref) => { + return ( + + ); + } +); + +Checkbox.displayName = "Checkbox"; + +export { Checkbox }; + diff --git a/electron/src/machines/laser/LaserToastManager.tsx b/electron/src/machines/laser/LaserToastManager.tsx new file mode 100644 index 00000000..69f9d4cb --- /dev/null +++ b/electron/src/machines/laser/LaserToastManager.tsx @@ -0,0 +1,34 @@ +import React, { useEffect, useRef } from "react"; +import { useLaser1 } from "./laser1/useLaser1"; +import { toast } from "sonner"; + +export function LaserToastManager() { + const { state } = useLaser1(); + const toastRef = useRef(null); + + useEffect(() => { + if (!state?.laser_state) return; + + if (state.laser_state.in_tolerance === false && !toastRef.current) { + toastRef.current = toast( +
+ Warning! + Diameter out of tolerance! + +
, + { duration: Infinity, position: "top-center", style: { background: "transparent", padding: 0, boxShadow: "none", border: "none" } } + ); + } + }, [state?.laser_state?.in_tolerance]); + + return null; +} + diff --git a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx index b3590ab4..60269f4a 100644 --- a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx +++ b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx @@ -1,6 +1,6 @@ import { ControlCard } from "@/control/ControlCard"; import { Page } from "@/components/Page"; -import React from "react"; +import React, { useEffect } from "react"; import { ControlGrid } from "@/control/ControlGrid"; import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue"; import { MinMaxValue, TIMEFRAME_OPTIONS } from "@/control/MinMaxValue"; @@ -11,6 +11,7 @@ import { Label } from "@/control/Label"; import { useLaser1 } from "./useLaser1"; import { DiameterVisualisation } from "../DiameterVisualisation"; +import { toast } from "sonner"; export function Laser1ControlPage() { const { @@ -35,6 +36,29 @@ export function Laser1ControlPage() { // Shared timeframe state (default 5 minutes) const [timeframe, setTimeframe] = React.useState(5 * 60 * 1000); + // Global Warning for out of tolerance + useEffect(() => { + if (state?.laser_state?.in_tolerance === false) { + const toastId = toast( +
+ Warning! + Diameter out of tolerance! + +
, + { + duration: Infinity, + position: "top-center", + style: { background: "transparent", padding: 0, boxShadow: "none" }, + } + ); + } + }, [state?.laser_state?.in_tolerance]); + return ( diff --git a/electron/src/machines/laser/laser1/Laser1Page.tsx b/electron/src/machines/laser/laser1/Laser1Page.tsx index 6d54c98d..9ae64578 100644 --- a/electron/src/machines/laser/laser1/Laser1Page.tsx +++ b/electron/src/machines/laser/laser1/Laser1Page.tsx @@ -1,10 +1,13 @@ import { Topbar } from "@/components/Topbar"; import { laser1SerialRoute } from "@/routes/routes"; import React from "react"; +import { LaserToastManager } from "../LaserToastManager"; export function Laser1Page() { const { serial } = laser1SerialRoute.useParams(); return ( + <> + + ); } diff --git a/electron/src/machines/laser/laser1/Laser1Settings.tsx b/electron/src/machines/laser/laser1/Laser1Settings.tsx new file mode 100644 index 00000000..e1663eb8 --- /dev/null +++ b/electron/src/machines/laser/laser1/Laser1Settings.tsx @@ -0,0 +1,28 @@ +import { Page } from "@/components/Page"; +import { ControlGrid } from "@/control/ControlGrid"; +import React, { useState } from "react"; +import { useLaser1 } from "./useLaser1"; +import { ControlCard } from "@/control/ControlCard"; +import { Checkbox } from "@/components/ui/checkbox-selection"; + +export function Buffer1SettingsPage() { + const { + state, + } = useLaser1(); + + const [ checked, setChecked ] = useState(false); + + return ( + + + + setChecked(e.target.checked)} + /> + + + + ); +} diff --git a/electron/src/machines/laser/laser1/laser1Namespace.ts b/electron/src/machines/laser/laser1/laser1Namespace.ts index af757d9b..54d194bf 100644 --- a/electron/src/machines/laser/laser1/laser1Namespace.ts +++ b/electron/src/machines/laser/laser1/laser1Namespace.ts @@ -42,6 +42,7 @@ export const stateEventDataSchema = z.object({ higher_tolerance: z.number(), lower_tolerance: z.number(), target_diameter: z.number(), + in_tolerance: z.boolean(), }), }); @@ -102,7 +103,7 @@ export const createLaser1NamespaceStore = (): StoreApi => diameter: diameter, x_diameter, y_diameter, - roundness: roundness, + roundness, }; }); diff --git a/machines/src/laser/api.rs b/machines/src/laser/api.rs index 2cc9abc1..923c8b32 100644 --- a/machines/src/laser/api.rs +++ b/machines/src/laser/api.rs @@ -49,6 +49,8 @@ pub struct LaserState { pub lower_tolerance: f64, /// target diameter in mm pub target_diameter: f64, + /// tolerance bool + pub in_tolerance: bool, } pub enum LaserEvents { diff --git a/machines/src/laser/mod.rs b/machines/src/laser/mod.rs index a7c27a5b..501000e3 100644 --- a/machines/src/laser/mod.rs +++ b/machines/src/laser/mod.rs @@ -14,7 +14,7 @@ use socketioxide::extract::SocketRef; use crate::AsyncThreadMessage; use std::{sync::Arc, time::Instant}; -use units::f64::*; +use units::{ConstZero, f64::*}; use units::length::millimeter; pub mod act; @@ -41,6 +41,10 @@ pub struct LaserMachine { y_diameter: Option, roundness: Option, + higher_tolerance: Length, + lower_tolerance: Length, + in_tolerance: bool, + //laser target configuration laser_target: LaserTarget, @@ -111,9 +115,10 @@ impl LaserMachine { pub fn build_state_event(&self) -> StateEvent { let laser = LaserState { - higher_tolerance: self.laser_target.higher_tolerance.get::(), - lower_tolerance: self.laser_target.lower_tolerance.get::(), + higher_tolerance: self.higher_tolerance.get::(), + lower_tolerance: self.lower_tolerance.get::(), target_diameter: self.laser_target.diameter.get::(), + in_tolerance: self.in_tolerance, }; StateEvent { @@ -129,6 +134,7 @@ impl LaserMachine { higher_tolerance: self.laser_target.higher_tolerance.get::(), lower_tolerance: self.laser_target.lower_tolerance.get::(), target_diameter: self.laser_target.diameter.get::(), + in_tolerance: self.in_tolerance }, }; @@ -136,12 +142,14 @@ impl LaserMachine { } pub fn set_higher_tolerance(&mut self, higher_tolerance: f64) { - self.laser_target.higher_tolerance = Length::new::(higher_tolerance); + self.higher_tolerance = Length::new::(higher_tolerance); + self.laser_target.higher_tolerance = self.higher_tolerance; self.emit_state(); } pub fn set_lower_tolerance(&mut self, lower_tolerance: f64) { - self.laser_target.lower_tolerance = Length::new::(lower_tolerance); + self.lower_tolerance = Length::new::(lower_tolerance); + self.laser_target.lower_tolerance = self.lower_tolerance; self.emit_state(); } @@ -172,6 +180,25 @@ impl LaserMachine { } } + /// + /// Calculates if the current diameter is inside of the tolerance + /// + fn calculate_in_tolerance(&mut self) -> bool { + // return true if the diameter is 0 to prevent warning happening before start + if self.diameter == Length::ZERO { + return true; + } + + let top = self.diameter + self.higher_tolerance; + let bottom = self.diameter - self.lower_tolerance; + + if self.diameter > top || self.diameter < bottom { + return false; + } else { + return true; + } + } + pub fn update(&mut self) { let laser_data = smol::block_on(async { self.laser.read().await.get_data().await }); self.diameter = Length::new::( @@ -192,6 +219,8 @@ impl LaserMachine { .cloned(); self.roundness = self.calculate_roundness(); + + self.in_tolerance = self.calculate_in_tolerance(); } } diff --git a/machines/src/laser/new.rs b/machines/src/laser/new.rs index 7dc94765..c6b1c252 100644 --- a/machines/src/laser/new.rs +++ b/machines/src/laser/new.rs @@ -50,6 +50,9 @@ impl MachineNewTrait for LaserMachine { x_diameter: None, y_diameter: None, roundness: None, + lower_tolerance: Length::new::(0.05), + higher_tolerance: Length::new::(0.05), + in_tolerance: true, }; Ok(laser_machine) From 80cf98b11d65f97c9048270d17a03b77ee2afec9 Mon Sep 17 00:00:00 2001 From: snacj <0xsnacj@proton.me> Date: Fri, 17 Oct 2025 14:24:50 +0200 Subject: [PATCH 02/15] Laser Warning works across all laser pages --- .../src/machines/laser/LaserToastManager.tsx | 1 - .../laser/laser1/Laser1ControlPage.tsx | 3 +- machines/src/laser/mod.rs | 22 +++++++++---- machines/src/laser/new.rs | 2 ++ server/src/machines/laser/act.rs | 33 +++++++++++++++++++ 5 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 server/src/machines/laser/act.rs diff --git a/electron/src/machines/laser/LaserToastManager.tsx b/electron/src/machines/laser/LaserToastManager.tsx index 69f9d4cb..633e3206 100644 --- a/electron/src/machines/laser/LaserToastManager.tsx +++ b/electron/src/machines/laser/LaserToastManager.tsx @@ -31,4 +31,3 @@ export function LaserToastManager() { return null; } - diff --git a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx index 60269f4a..d49e79b7 100644 --- a/electron/src/machines/laser/laser1/Laser1ControlPage.tsx +++ b/electron/src/machines/laser/laser1/Laser1ControlPage.tsx @@ -1,6 +1,6 @@ import { ControlCard } from "@/control/ControlCard"; import { Page } from "@/components/Page"; -import React, { useEffect } from "react"; +import React from "react"; import { ControlGrid } from "@/control/ControlGrid"; import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue"; import { MinMaxValue, TIMEFRAME_OPTIONS } from "@/control/MinMaxValue"; @@ -11,7 +11,6 @@ import { Label } from "@/control/Label"; import { useLaser1 } from "./useLaser1"; import { DiameterVisualisation } from "../DiameterVisualisation"; -import { toast } from "sonner"; export function Laser1ControlPage() { const { diff --git a/machines/src/laser/mod.rs b/machines/src/laser/mod.rs index 501000e3..35e622d1 100644 --- a/machines/src/laser/mod.rs +++ b/machines/src/laser/mod.rs @@ -41,6 +41,7 @@ pub struct LaserMachine { y_diameter: Option, roundness: Option, + target_diameter: Length, higher_tolerance: Length, lower_tolerance: Length, in_tolerance: bool, @@ -51,6 +52,7 @@ pub struct LaserMachine { /// Will be initialized 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, + did_change_state: bool, } impl Machine for LaserMachine { @@ -134,11 +136,12 @@ impl LaserMachine { higher_tolerance: self.laser_target.higher_tolerance.get::(), lower_tolerance: self.laser_target.lower_tolerance.get::(), target_diameter: self.laser_target.diameter.get::(), - in_tolerance: self.in_tolerance + in_tolerance: self.in_tolerance, }, }; self.namespace.emit(LaserEvents::State(state.build())); + self.did_change_state = false; } pub fn set_higher_tolerance(&mut self, higher_tolerance: f64) { @@ -154,6 +157,7 @@ impl LaserMachine { } pub fn set_target_diameter(&mut self, target_diameter: f64) { + self.target_diameter = Length::new::(target_diameter); self.laser_target.diameter = Length::new::(target_diameter); self.emit_state(); } @@ -186,17 +190,19 @@ impl LaserMachine { fn calculate_in_tolerance(&mut self) -> bool { // return true if the diameter is 0 to prevent warning happening before start if self.diameter == Length::ZERO { - return true; + self.in_tolerance = true; } - let top = self.diameter + self.higher_tolerance; - let bottom = self.diameter - self.lower_tolerance; + let top = self.target_diameter + self.higher_tolerance; + let bottom = self.target_diameter - self.lower_tolerance; if self.diameter > top || self.diameter < bottom { - return false; + self.in_tolerance = false; } else { - return true; + self.in_tolerance = true; } + + self.in_tolerance } pub fn update(&mut self) { @@ -220,7 +226,9 @@ impl LaserMachine { self.roundness = self.calculate_roundness(); - self.in_tolerance = self.calculate_in_tolerance(); + if self.in_tolerance != self.calculate_in_tolerance() { + self.did_change_state = true; + } } } diff --git a/machines/src/laser/new.rs b/machines/src/laser/new.rs index c6b1c252..f6ee3add 100644 --- a/machines/src/laser/new.rs +++ b/machines/src/laser/new.rs @@ -47,12 +47,14 @@ impl MachineNewTrait for LaserMachine { laser_target, emitted_default_state: false, diameter: Length::ZERO, + target_diameter: Length::ZERO, x_diameter: None, y_diameter: None, roundness: None, lower_tolerance: Length::new::(0.05), higher_tolerance: Length::new::(0.05), in_tolerance: true, + did_change_state: true, }; Ok(laser_machine) diff --git a/server/src/machines/laser/act.rs b/server/src/machines/laser/act.rs new file mode 100644 index 00000000..1de8565e --- /dev/null +++ b/server/src/machines/laser/act.rs @@ -0,0 +1,33 @@ +use super::LaserMachine; +use control_core::machines::new::MachineAct; +use std::time::{Duration, Instant}; + +/// Implements the `MachineAct` trait for the `LaserMachine`. +/// +/// # Parameters +/// - `_now_ts`: The current timestamp of type `Instant`. +/// +/// # Returns +/// A pinned `Future` that resolves to `()` and is `Send`-safe. The future encapsulates the asynchronous behavior of the `act` method. +/// +/// # Description +/// This method is called to perform periodic actions for the `LaserMachine`. Specifically: +/// - It checks if the time elapsed since the last measurement emission exceeds 20 milliseconds. +/// - If the condition is met, it asynchronously emits live values at 60 FPS. +/// +/// The method ensures that the diameter value is updated approximately 60 times per second. +/// +impl MachineAct for LaserMachine { + fn act(&mut self, now: Instant) { + self.update(); + if self.did_change_state { + tracing::info!("State changed: {}", self.did_change_state); + self.emit_state(); + } + // 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) { + self.emit_live_values(); + self.last_measurement_emit = now; + } + } +} From 09022c9c864bc9607a08696b696a7e64000a723f Mon Sep 17 00:00:00 2001 From: snacj <0xsnacj@proton.me> Date: Tue, 21 Oct 2025 12:47:36 +0200 Subject: [PATCH 03/15] Global Laser Warning addded --- electron/src/App.tsx | 8 +- .../src/machines/laser/LaserToastManager.tsx | 33 ----- .../src/machines/laser/laser1/Laser1Page.tsx | 4 - .../src/setup/GlobalLaserToastManager.tsx | 134 ++++++++++++++++++ 4 files changed, 141 insertions(+), 38 deletions(-) delete mode 100644 electron/src/machines/laser/LaserToastManager.tsx create mode 100644 electron/src/setup/GlobalLaserToastManager.tsx diff --git a/electron/src/App.tsx b/electron/src/App.tsx index 1786dd3e..5a5eae5f 100644 --- a/electron/src/App.tsx +++ b/electron/src/App.tsx @@ -9,6 +9,7 @@ import { RouterProvider } from "@tanstack/react-router"; import { Toaster } from "./components/ui/sonner"; import { enableMapSet } from "immer"; import { useGlobalLogStreaming } from "./hooks/useGlobalLogStreaming"; +import { GlobalLaserToastManager } from "./setup/GlobalLaserToastManager"; export default function App() { const { i18n } = useTranslation(); @@ -21,7 +22,12 @@ export default function App() { updateAppLanguage(i18n); }, [i18n]); - return ; + return ( + <> + + ; + + ) } // enable immer MapSet plugin diff --git a/electron/src/machines/laser/LaserToastManager.tsx b/electron/src/machines/laser/LaserToastManager.tsx deleted file mode 100644 index 633e3206..00000000 --- a/electron/src/machines/laser/LaserToastManager.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useEffect, useRef } from "react"; -import { useLaser1 } from "./laser1/useLaser1"; -import { toast } from "sonner"; - -export function LaserToastManager() { - const { state } = useLaser1(); - const toastRef = useRef(null); - - useEffect(() => { - if (!state?.laser_state) return; - - if (state.laser_state.in_tolerance === false && !toastRef.current) { - toastRef.current = toast( -
- Warning! - Diameter out of tolerance! - -
, - { duration: Infinity, position: "top-center", style: { background: "transparent", padding: 0, boxShadow: "none", border: "none" } } - ); - } - }, [state?.laser_state?.in_tolerance]); - - return null; -} diff --git a/electron/src/machines/laser/laser1/Laser1Page.tsx b/electron/src/machines/laser/laser1/Laser1Page.tsx index 9ae64578..6d54c98d 100644 --- a/electron/src/machines/laser/laser1/Laser1Page.tsx +++ b/electron/src/machines/laser/laser1/Laser1Page.tsx @@ -1,13 +1,10 @@ import { Topbar } from "@/components/Topbar"; import { laser1SerialRoute } from "@/routes/routes"; import React from "react"; -import { LaserToastManager } from "../LaserToastManager"; export function Laser1Page() { const { serial } = laser1SerialRoute.useParams(); return ( - <> - - ); } diff --git a/electron/src/setup/GlobalLaserToastManager.tsx b/electron/src/setup/GlobalLaserToastManager.tsx new file mode 100644 index 00000000..5d3252c4 --- /dev/null +++ b/electron/src/setup/GlobalLaserToastManager.tsx @@ -0,0 +1,134 @@ +import React, { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import { useMainNamespace } from "@/client/mainNamespace"; +import { useLaser1Namespace } from "@/machines/laser/laser1/laser1Namespace"; +import { laser1 } from "@/machines/properties"; +import type { MachineIdentificationUnique } from "@/machines/types"; + +/** + * Global manager that toasts laser StateEvent changes. + * If machineIdentification is omitted it will attempt to discover the laser via the main namespace. + * + */ +export function GlobalLaserToastManager({ + machineIdentification, +}: { + machineIdentification?: MachineIdentificationUnique; +}) { + const main = useMainNamespace(); + const [discovered, setDiscovered] = + useState(null); + + // Prefer explicit prop, otherwise use discovered id + const effectiveId = machineIdentification ?? discovered; + + // Attempt to discover the single laser machine from the main namespace + useEffect(() => { + if (machineIdentification) return; + + const machinesEvent = (main as any)?.machines; + if (!machinesEvent?.data?.machines) { + setDiscovered(null); + return; + } + + const machines: any[] = machinesEvent.data.machines; + + const match = machines.find((entry) => { + const mi = entry?.machine_identification; + return ( + mi?.vendor === laser1.machine_identification.vendor && + mi?.machine === laser1.machine_identification.machine + ); + }); + + if (!match) { + setDiscovered(null); + return; + } + + const serial = Number(match?.serial); + if (!Number.isNaN(serial)) { + setDiscovered({ + machine_identification: laser1.machine_identification, + serial, + }); + } else { + setDiscovered(null); + } + }, [main, machineIdentification]); + + return effectiveId ? ( + + ) : null; +} + +/** + * Watches StateEvent for the laser namespace and shows toasts for non-default states. + * Component is non-visual. + */ +function LaserToastWatcher({ + machineIdentification, +}: { + machineIdentification: MachineIdentificationUnique; +}) { + const { state } = useLaser1Namespace(machineIdentification); + + // Deduplicate toasts by event timestamp + const lastToastTs = useRef(null); + + useEffect(() => { + if (!state) return; + + const eventTs = (state as any)?.ts ?? null; + const isDefault = !!(state as any)?.data?.is_default_state; + const inTolereance = (state as any)?.data?.laser_state.in_tolerance; + + // skip the default snapshot emitted on connect + if (isDefault) return; + + // dedupe identical events + if (eventTs != null && lastToastTs.current === eventTs) return; + lastToastTs.current = eventTs; + + // skip if diameter is inside Tolerance + if (inTolereance) return; + + const toastId = eventTs?.toString() ?? String(Date.now()); + + try { + // Sonner toast call + toast( +
+ Warning + Laser Diameter out of Tolerance! + +
, + { + id: toastId, + duration: Infinity, + position: "top-center", + style: { + background: "transparent", + padding: 0, + boxShadow: "none", + border: "none", + }, + }, + ); + } catch (err) { + console.error("GlobalLaserToastManager: failed to build toast", err); + toast("Laser state changed"); + } + }, [state]); + + return null; +} From 4dcedb537d81778f58946283c71d75f68dc58ed1 Mon Sep 17 00:00:00 2001 From: snacj <0xsnacj@proton.me> Date: Tue, 21 Oct 2025 13:01:00 +0200 Subject: [PATCH 04/15] Changed Styling of Warning. --- .../src/setup/GlobalLaserToastManager.tsx | 102 ++++++++++-------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/electron/src/setup/GlobalLaserToastManager.tsx b/electron/src/setup/GlobalLaserToastManager.tsx index 5d3252c4..5cf4cfd1 100644 --- a/electron/src/setup/GlobalLaserToastManager.tsx +++ b/electron/src/setup/GlobalLaserToastManager.tsx @@ -4,6 +4,8 @@ import { useMainNamespace } from "@/client/mainNamespace"; import { useLaser1Namespace } from "@/machines/laser/laser1/laser1Namespace"; import { laser1 } from "@/machines/properties"; import type { MachineIdentificationUnique } from "@/machines/types"; +import { AlertTriangle } from "lucide-react"; +import { Icon } from "@/components/Icon"; /** * Global manager that toasts laser StateEvent changes. @@ -16,8 +18,7 @@ export function GlobalLaserToastManager({ machineIdentification?: MachineIdentificationUnique; }) { const main = useMainNamespace(); - const [discovered, setDiscovered] = - useState(null); + const [discovered, setDiscovered] = useState(null); // Prefer explicit prop, otherwise use discovered id const effectiveId = machineIdentification ?? discovered; @@ -25,53 +26,59 @@ export function GlobalLaserToastManager({ // Attempt to discover the single laser machine from the main namespace useEffect(() => { if (machineIdentification) return; - const machinesEvent = (main as any)?.machines; - if (!machinesEvent?.data?.machines) { + if (!machinesEvent) { setDiscovered(null); return; } - const machines: any[] = machinesEvent.data.machines; + // Defensive shapes: try common structures + let list: any[] = []; + if (Array.isArray(machinesEvent?.data?.machines)) list = machinesEvent.data.machines; + else if (Array.isArray(machinesEvent?.data)) list = machinesEvent.data; + else if (Array.isArray(machinesEvent?.machines)) list = machinesEvent.machines; + else if (machinesEvent && typeof machinesEvent === "object") { + list = Object.values(machinesEvent).filter((v) => v && typeof v === "object"); + } - const match = machines.find((entry) => { - const mi = entry?.machine_identification; + const match = list.find((entry) => { + const mi = + entry?.machine_identification || + entry?.machine_identification_unique?.machine_identification || + entry?.device_machine_identification?.machine_identification; + if (!mi) return false; return ( - mi?.vendor === laser1.machine_identification.vendor && - mi?.machine === laser1.machine_identification.machine + mi.vendor === (laser1.machine_identification as any).vendor && + mi.machine === (laser1.machine_identification as any).machine ); }); - if (!match) { - setDiscovered(null); - return; + if (match) { + const serial = + match?.serial ?? + match?.machine_identification_unique?.serial ?? + match?.device_machine_identification?.machine_identification_unique?.serial ?? + null; + if (serial != null && !Number.isNaN(Number(serial))) { + setDiscovered({ + machine_identification: laser1.machine_identification, + serial: Number(serial), + }); + return; + } } - const serial = Number(match?.serial); - if (!Number.isNaN(serial)) { - setDiscovered({ - machine_identification: laser1.machine_identification, - serial, - }); - } else { - setDiscovered(null); - } + setDiscovered(null); }, [main, machineIdentification]); - return effectiveId ? ( - - ) : null; + return effectiveId ? : null; } /** * Watches StateEvent for the laser namespace and shows toasts for non-default states. * Component is non-visual. */ -function LaserToastWatcher({ - machineIdentification, -}: { - machineIdentification: MachineIdentificationUnique; -}) { +function LaserToastWatcher({ machineIdentification }: { machineIdentification: MachineIdentificationUnique }) { const { state } = useLaser1Namespace(machineIdentification); // Deduplicate toasts by event timestamp @@ -91,7 +98,6 @@ function LaserToastWatcher({ if (eventTs != null && lastToastTs.current === eventTs) return; lastToastTs.current = eventTs; - // skip if diameter is inside Tolerance if (inTolereance) return; const toastId = eventTs?.toString() ?? String(Date.now()); @@ -99,18 +105,28 @@ function LaserToastWatcher({ try { // Sonner toast call toast( -
- Warning - Laser Diameter out of Tolerance! - +
+
+
+ + Warning +
+ +
+ +

+ Laser diameter is out of tolerance.
+ Please check the Filament immediately. +

, { id: toastId, @@ -122,7 +138,7 @@ function LaserToastWatcher({ boxShadow: "none", border: "none", }, - }, + } ); } catch (err) { console.error("GlobalLaserToastManager: failed to build toast", err); From 116c76d2185e0f1c70b70c4fbc60cb0162ec526c Mon Sep 17 00:00:00 2001 From: snacj <0xsnacj@proton.me> Date: Tue, 21 Oct 2025 13:31:30 +0200 Subject: [PATCH 05/15] Added AutoStopWinderOnOutOfTolerance --- machines/src/laser/api.rs | 6 ++++++ machines/src/laser/mod.rs | 13 +++++++++++++ machines/src/laser/new.rs | 1 + 3 files changed, 20 insertions(+) diff --git a/machines/src/laser/api.rs b/machines/src/laser/api.rs index 923c8b32..0e45ebb5 100644 --- a/machines/src/laser/api.rs +++ b/machines/src/laser/api.rs @@ -51,6 +51,8 @@ pub struct LaserState { pub target_diameter: f64, /// tolerance bool pub in_tolerance: bool, + /// auto stop on out of tolerance bool + pub auto_stop_on_out_of_tolerance: bool, } pub enum LaserEvents { @@ -89,6 +91,7 @@ enum Mutation { SetTargetDiameter(f64), SetLowerTolerance(f64), SetHigherTolerance(f64), + SetAutoStopOnOutOfTolerance(bool), } impl NamespaceCacheingLogic for LaserMachineNamespace { @@ -117,6 +120,9 @@ impl MachineApi for LaserMachine { Mutation::SetTargetDiameter(target_diameter) => { self.set_target_diameter(target_diameter); } + Mutation::SetAutoStopOnOutOfTolerance(auto_stop_on_out_of_tolerance) => { + self.set_auto_stop_on_out_of_tolerance(auto_stop_on_out_of_tolerance); + } } Ok(()) } diff --git a/machines/src/laser/mod.rs b/machines/src/laser/mod.rs index 35e622d1..1b1c19d1 100644 --- a/machines/src/laser/mod.rs +++ b/machines/src/laser/mod.rs @@ -46,6 +46,8 @@ pub struct LaserMachine { lower_tolerance: Length, in_tolerance: bool, + auto_stop_on_out_of_tolerance: bool, + //laser target configuration laser_target: LaserTarget, @@ -121,6 +123,7 @@ impl LaserMachine { lower_tolerance: self.lower_tolerance.get::(), target_diameter: self.laser_target.diameter.get::(), in_tolerance: self.in_tolerance, + auto_stop_on_out_of_tolerance: self.auto_stop_on_out_of_tolerance, }; StateEvent { @@ -137,6 +140,7 @@ impl LaserMachine { lower_tolerance: self.laser_target.lower_tolerance.get::(), target_diameter: self.laser_target.diameter.get::(), in_tolerance: self.in_tolerance, + auto_stop_on_out_of_tolerance: self.auto_stop_on_out_of_tolerance, }, }; @@ -162,6 +166,11 @@ impl LaserMachine { self.emit_state(); } + pub fn set_auto_stop_on_out_of_tolerance(&mut self, auto_stop_on_out_of_tolerance: bool) { + self.auto_stop_on_out_of_tolerance = auto_stop_on_out_of_tolerance; + self.emit_state(); + } + /// /// Roundness = min(x, y) / max(x, y) /// @@ -229,6 +238,10 @@ impl LaserMachine { if self.in_tolerance != self.calculate_in_tolerance() { self.did_change_state = true; } + + if !self.in_tolerance && self.auto_stop_on_out_of_tolerance && self.did_change_state { + tracing::info!("Disabling Winder..."); + } } } diff --git a/machines/src/laser/new.rs b/machines/src/laser/new.rs index f6ee3add..78bed666 100644 --- a/machines/src/laser/new.rs +++ b/machines/src/laser/new.rs @@ -54,6 +54,7 @@ impl MachineNewTrait for LaserMachine { lower_tolerance: Length::new::(0.05), higher_tolerance: Length::new::(0.05), in_tolerance: true, + auto_stop_on_out_of_tolerance: false, did_change_state: true, }; From 11bc62f40e914971032a7c2f491d5543912f8558 Mon Sep 17 00:00:00 2001 From: snacj <0xsnacj@proton.me> Date: Tue, 21 Oct 2025 13:31:40 +0200 Subject: [PATCH 06/15] formatting --- electron/src/App.tsx | 10 ++--- .../src/components/ui/checkbox-selection.tsx | 13 +++--- .../src/machines/laser/laser1/Laser1Page.tsx | 6 +++ .../machines/laser/laser1/Laser1Settings.tsx | 16 +++----- .../machines/laser/laser1/laser1Namespace.ts | 1 + .../src/machines/laser/laser1/useLaser1.ts | 26 ++++++++++++ electron/src/routes/routes.tsx | 8 ++++ .../src/setup/GlobalLaserToastManager.tsx | 41 +++++++++++++------ 8 files changed, 88 insertions(+), 33 deletions(-) diff --git a/electron/src/App.tsx b/electron/src/App.tsx index 5a5eae5f..e4c397f6 100644 --- a/electron/src/App.tsx +++ b/electron/src/App.tsx @@ -23,11 +23,11 @@ export default function App() { }, [i18n]); return ( - <> - - ; - - ) + <> + + ; + + ); } // enable immer MapSet plugin diff --git a/electron/src/components/ui/checkbox-selection.tsx b/electron/src/components/ui/checkbox-selection.tsx index 1d385c2f..9189fbcd 100644 --- a/electron/src/components/ui/checkbox-selection.tsx +++ b/electron/src/components/ui/checkbox-selection.tsx @@ -8,23 +8,24 @@ interface CheckboxProps extends React.ComponentProps<"input"> { const Checkbox = React.forwardRef( ({ className, label, ...props }, ref) => { return ( -