Merge pull request #849 from qitechgmbh/769-add-an-alert-or-warning-in-the-frontend-for-filament-out-of-spec
769 add an alert or warning in the frontend for filament out of spec
This commit is contained in:
10 files changed
+278
-11
No files matched your search
Generated
+11
@@ -7878,6 +7878,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/encoding": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
|
||||
@@ -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 <RouterProvider router={router} />;
|
||||
return (
|
||||
<>
|
||||
<GlobalLaserToastManager />
|
||||
<RouterProvider router={router} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// enable immer MapSet plugin
|
||||
|
||||
@@ -21,6 +21,12 @@ export function Laser1Page() {
|
||||
icon: "lu:ChartSpline",
|
||||
},
|
||||
// {
|
||||
// link: "settings",
|
||||
// activeLink: "settings",
|
||||
// title: "Config",
|
||||
// icon: "lu:Settings",
|
||||
// },
|
||||
// {
|
||||
// link: "manual",
|
||||
// activeLink: "manual",
|
||||
// title: "Manual",
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -68,24 +69,25 @@ export type Laser1NamespaceStore = {
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const HALF_SECOND = 500;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
HALF_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
const { initialTimeSeries: x_diameter, insert: addXDiameter } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries(TWENTY_MILLISECOND, HALF_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: y_diameter, insert: addYDiameter } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
createTimeSeries(TWENTY_MILLISECOND, HALF_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: roundness, insert: addRoundness } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
HALF_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
@@ -102,7 +104,7 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
|
||||
diameter: diameter,
|
||||
x_diameter,
|
||||
y_diameter,
|
||||
roundness: roundness,
|
||||
roundness,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
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";
|
||||
import { Icon } from "@/components/Icon";
|
||||
|
||||
/**
|
||||
* 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<MachineIdentificationUnique | null>(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) {
|
||||
setDiscovered(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 = 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 as any).vendor &&
|
||||
mi.machine === (laser1.machine_identification as any).machine
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
setDiscovered(null);
|
||||
}, [main, machineIdentification]);
|
||||
|
||||
return effectiveId ? (
|
||||
<LaserToastWatcher machineIdentification={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<number | string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!state) return;
|
||||
|
||||
const eventTs = (state as any)?.ts ?? null;
|
||||
const inTolerance = (state as any)?.data?.laser_state.in_tolerance;
|
||||
const isDefault = !!(state as any)?.data?.is_default_state;
|
||||
const toastId = eventTs?.toString() ?? String(Date.now());
|
||||
|
||||
if (isDefault) return;
|
||||
|
||||
// Only show toast if laser is out of tolerance and not already shown for this timestamp
|
||||
if (!inTolerance && lastToastTs.current !== eventTs) {
|
||||
lastToastTs.current = eventTs;
|
||||
// Sonner toast call
|
||||
toast(
|
||||
<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">
|
||||
Warning
|
||||
</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={() => {
|
||||
toast.dismiss(toastId);
|
||||
lastToastTs.current = null;
|
||||
}}
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-base leading-snug text-red-50">
|
||||
Laser diameter is <strong>out of tolerance</strong>.<br />
|
||||
Please check the filament immediately.
|
||||
</p>
|
||||
</div>,
|
||||
{
|
||||
id: toastId,
|
||||
duration: Infinity,
|
||||
position: "top-center",
|
||||
style: {
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
boxShadow: "none",
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
if (inTolerance) {
|
||||
lastToastTs.current = null;
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -29,6 +29,10 @@ impl MachineAct for LaserMachine {
|
||||
};
|
||||
self.update();
|
||||
|
||||
if 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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -11,10 +11,10 @@ use smol::{
|
||||
lock::RwLock,
|
||||
};
|
||||
use socketioxide::extract::SocketRef;
|
||||
use units::Length;
|
||||
|
||||
use crate::AsyncThreadMessage;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use units::f64::*;
|
||||
use units::length::millimeter;
|
||||
|
||||
pub mod act;
|
||||
@@ -41,12 +41,18 @@ pub struct LaserMachine {
|
||||
y_diameter: Option<Length>,
|
||||
roundness: Option<f64>,
|
||||
|
||||
target_diameter: Length,
|
||||
higher_tolerance: Length,
|
||||
lower_tolerance: Length,
|
||||
in_tolerance: bool,
|
||||
|
||||
//laser target configuration
|
||||
laser_target: LaserTarget,
|
||||
|
||||
/// 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 {
|
||||
@@ -111,9 +117,10 @@ impl LaserMachine {
|
||||
|
||||
pub fn build_state_event(&self) -> StateEvent {
|
||||
let laser = LaserState {
|
||||
higher_tolerance: self.laser_target.higher_tolerance.get::<millimeter>(),
|
||||
lower_tolerance: self.laser_target.lower_tolerance.get::<millimeter>(),
|
||||
higher_tolerance: self.higher_tolerance.get::<millimeter>(),
|
||||
lower_tolerance: self.lower_tolerance.get::<millimeter>(),
|
||||
target_diameter: self.laser_target.diameter.get::<millimeter>(),
|
||||
in_tolerance: self.in_tolerance,
|
||||
};
|
||||
|
||||
StateEvent {
|
||||
@@ -129,23 +136,28 @@ impl LaserMachine {
|
||||
higher_tolerance: self.laser_target.higher_tolerance.get::<millimeter>(),
|
||||
lower_tolerance: self.laser_target.lower_tolerance.get::<millimeter>(),
|
||||
target_diameter: self.laser_target.diameter.get::<millimeter>(),
|
||||
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) {
|
||||
self.laser_target.higher_tolerance = Length::new::<millimeter>(higher_tolerance);
|
||||
self.higher_tolerance = Length::new::<millimeter>(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::<millimeter>(lower_tolerance);
|
||||
self.lower_tolerance = Length::new::<millimeter>(lower_tolerance);
|
||||
self.laser_target.lower_tolerance = self.lower_tolerance;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn set_target_diameter(&mut self, target_diameter: f64) {
|
||||
self.target_diameter = Length::new::<millimeter>(target_diameter);
|
||||
self.laser_target.diameter = Length::new::<millimeter>(target_diameter);
|
||||
self.emit_state();
|
||||
}
|
||||
@@ -172,6 +184,29 @@ impl LaserMachine {
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Calculates if the current diameter is inside of the tolerance
|
||||
///
|
||||
fn calculate_in_tolerance(&mut self) -> bool {
|
||||
let diameter_epsilon: f64 = 0.0001; // 0.0001 mm
|
||||
// early return true if the diameter is 0 to prevent warning happening before start
|
||||
if self.diameter.get::<millimeter>() < diameter_epsilon {
|
||||
self.in_tolerance = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
let top = self.target_diameter + self.higher_tolerance;
|
||||
let bottom = self.target_diameter - self.lower_tolerance;
|
||||
|
||||
if self.diameter > top || self.diameter < bottom {
|
||||
self.in_tolerance = false;
|
||||
} else {
|
||||
self.in_tolerance = true;
|
||||
}
|
||||
|
||||
self.in_tolerance
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
let laser_data = smol::block_on(async { self.laser.read().await.get_data().await });
|
||||
self.diameter = Length::new::<millimeter>(
|
||||
@@ -192,6 +227,10 @@ impl LaserMachine {
|
||||
.cloned();
|
||||
|
||||
self.roundness = self.calculate_roundness();
|
||||
|
||||
if self.in_tolerance != self.calculate_in_tolerance() {
|
||||
self.did_change_state = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,9 +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::<millimeter>(0.05),
|
||||
higher_tolerance: Length::new::<millimeter>(0.05),
|
||||
in_tolerance: true,
|
||||
did_change_state: true,
|
||||
};
|
||||
|
||||
Ok(laser_machine)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user