Compare commits

...
Author SHA1 Message Date
JalleliAchraf 658f524eff glitch fix attempt 2026-02-18 10:07:43 +01:00
JalleliAchraf 88b0c87a51 Merge branch '1004-target-line-with-current-target' of github.com:qitechgmbh/control into 1004-target-line-with-current-target 2026-02-18 10:02:43 +01:00
JalleliAchraf 4178c90769 formatting 2026-02-17 20:49:28 +01:00
JalleliAchraf 2c99231f1f target line jittering replaced with a natural flow motion 2026-02-17 20:49:28 +01:00
Saurabh Bagade 26b417c0ec refactored to reduce slanted/vertical wiggle and remove horizontal wiggle 2026-02-17 20:49:28 +01:00
Saurabh Bagade 7df303bdf4 code formatting fixed for animation.ts 2026-02-17 20:49:28 +01:00
Saurabh Bagade 93c5e8d599 refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-02-17 20:49:28 +01:00
Saurabh Bagade c5fbdd0a0f refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-02-17 20:49:28 +01:00
Saurabh Bagade 45e03b9c93 fixed the issue with the target line 2026-02-17 20:49:28 +01:00
Achraf Jalleli 3a24c24836 Merge branch 'master' into 1004-target-line-with-current-target 2026-02-13 20:04:49 +01:00
JalleliAchraf 26e4c6ab6c formatting 2026-02-13 19:58:25 +01:00
JalleliAchraf 60f95ec9b6 target line jittering replaced with a natural flow motion 2026-02-13 19:53:25 +01:00
Saurabh Bagade 2c7f65dc78 refactored to reduce slanted/vertical wiggle and remove horizontal wiggle 2026-01-31 13:54:14 +01:00
Saurabh Bagade d3d2ab4828 code formatting fixed for animation.ts 2026-01-31 13:54:14 +01:00
Saurabh Bagade a839a374a3 refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-01-31 13:54:14 +01:00
Saurabh Bagade 6049737955 refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-01-31 13:54:14 +01:00
Saurabh Bagade 0c4a1eec39 fixed the issue with the target line 2026-01-31 13:54:14 +01:00
18 changed files with 569 additions and 30 deletions

No files matched your search

+77 -4
View File
@@ -5,8 +5,23 @@ import {
AnimationRefs,
AnimationState,
BigGraphProps,
GraphLine,
SeriesData,
} from "./types";
import { alignTargetSeriesToTimestamps } from "@/lib/timeseries";
function hasHistoricalDashedTargets(
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>,
): boolean {
return (
lines?.some(
(line) =>
line.show !== false &&
!!line.targetSeries &&
(line.dash?.length ?? 0) > 0,
) ?? false
);
}
export function useAnimationRefs(): AnimationRefs {
const animationFrameRef = useRef<number | null>(null);
@@ -24,12 +39,14 @@ export function useAnimationRefs(): AnimationRefs {
values: number[];
}>({ timestamps: [], values: [] });
const realPointsCountRef = useRef(0);
const targetLineCacheRef = useRef<Map<number, number[]>>(new Map());
return {
animationFrame: animationFrameRef,
animationState: animationStateRef,
lastRenderedData: lastRenderedDataRef,
realPointsCount: realPointsCountRef,
targetLineCache: targetLineCacheRef,
};
}
@@ -50,8 +67,12 @@ export function buildUPlotData(
values: number[],
realPointsCount: number | undefined,
realPointsCountRef: React.RefObject<number>,
config: { lines?: Array<{ show?: boolean; value: number }> },
config: {
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
},
allSeriesData?: number[][],
targetLineCache?: React.RefObject<Map<number, number[]>>,
freezeTargetTail: boolean = false,
): uPlot.AlignedData {
const uData: uPlot.AlignedData = [timestamps];
@@ -70,9 +91,55 @@ export function buildUPlotData(
}
// Add config lines
let lineIndex = 0;
config.lines?.forEach((line) => {
if (line.show !== false) {
uData.push(timestamps.map(() => line.value));
let lineData: number[];
if (line.targetSeries && targetLineCache) {
// Target line with history - use cache to prevent wiggle
const cached = targetLineCache.current.get(lineIndex);
if (cached && cached.length <= timestamps.length) {
// Reuse cached data for existing points, extend with current value
const extension = timestamps.length - cached.length;
if (extension > 0) {
const stableTailValue = cached[cached.length - 1] ?? line.value;
lineData = [
...cached,
...new Array(extension).fill(
freezeTargetTail ? stableTailValue : line.value,
),
];
} else {
lineData = [...cached];
}
if (!freezeTargetTail) {
targetLineCache.current.set(lineIndex, lineData);
}
} else {
// Full recalculation (first time or chart was rebuilt)
lineData = alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
);
targetLineCache.current.set(lineIndex, lineData);
}
} else if (line.targetSeries) {
// targetSeries but no cache available - full calculation
lineData = alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
);
} else {
// Constant value line (original behavior)
lineData = timestamps.map(() => line.value);
}
uData.push(lineData);
lineIndex++;
}
});
@@ -88,8 +155,10 @@ export function animateNewPoint(
viewMode: string,
selectedTimeWindow: number | "all",
startTimeRef: React.RefObject<number | null>,
config: { lines?: Array<{ show?: boolean; value: number }> },
updateYAxisScale: (xMin?: number, xMax?: number) => void, // Updated signature
config: {
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
},
updateYAxisScale: (xMin?: number, xMax?: number) => void,
getAllSeriesData?: () => number[][],
): void {
if (targetData.timestamps.length <= currentData.timestamps.length) {
@@ -123,6 +192,8 @@ export function animateNewPoint(
targetIndex: newIndex,
};
const freezeTargetTail = hasHistoricalDashedTargets(config.lines);
const animate = (currentTime: number) => {
if (
!uplotRef.current ||
@@ -173,6 +244,8 @@ export function animateNewPoint(
refs.realPointsCount,
config,
allSeriesData,
refs.targetLineCache,
freezeTargetTail,
);
uplotRef.current.setData(animatedUData);
+130 -5
View File
@@ -1,5 +1,9 @@
import uPlot from "uplot";
import { seriesToUPlotData, TimeSeries } from "@/lib/timeseries";
import {
seriesToUPlotData,
TimeSeries,
alignTargetSeriesToTimestamps,
} from "@/lib/timeseries";
import {
createEventHandlers,
attachEventHandlers,
@@ -8,6 +12,13 @@ import {
import { BigGraphProps, CreateChartParams } from "./types";
import { normalizeDataSeries } from "./animation";
type CustomDashedTargetLine = {
dataIndex: number;
dash: number[];
color: string;
width: number;
};
export function createChart({
containerRef,
uplotRef,
@@ -56,11 +67,28 @@ export function createChart({
uPlotData.push(values as any);
});
// Add config lines data
// Add config lines data and populate target line cache
animationRefs.targetLineCache.current.clear();
let lineIndex = 0;
config.lines?.forEach((line) => {
if (line.show !== false) {
const lineData = new Array(timestamps.length).fill(line.value);
let lineData: number[];
if (line.targetSeries) {
// Use historical target values aligned with data timestamps
lineData = alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
);
// Cache the computed target line data so subsequent updates
// can extend it instead of recalculating (prevents wiggle)
animationRefs.targetLineCache.current.set(lineIndex, [...lineData]);
} else {
// Use constant value (original behavior)
lineData = new Array(timestamps.length).fill(line.value);
}
uPlotData.push(lineData as any);
lineIndex++;
}
});
@@ -142,6 +170,7 @@ export function createChart({
// Build series configuration for ALL series (but control visibility)
const seriesConfig: uPlot.Series[] = [{ label: "Time" }];
const customDashedTargetLines: CustomDashedTargetLine[] = [];
// Add ALL data series with visibility control
const defaultColors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6"];
@@ -167,16 +196,35 @@ export function createChart({
});
// Add config lines
let visibleLineIndex = 0;
const firstConfigLineDataIndex = 1 + allOriginalSeries.length;
config.lines?.forEach((line) => {
if (line.show !== false) {
const dash =
line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
const lineDataIndex = firstConfigLineDataIndex + visibleLineIndex;
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
if (isHistoricalDashedTarget) {
customDashedTargetLines.push({
dataIndex: lineDataIndex,
dash: dash!,
color: line.color,
width: line.width ?? 1,
});
}
seriesConfig.push({
label: line.label,
stroke: line.color,
// Historical dashed targets are custom rendered in draw hook to avoid
// canvas dash shimmer during smooth live updates.
stroke: isHistoricalDashedTarget ? "rgba(0,0,0,0)" : line.color,
width: line.width ?? 1,
dash: line.dash ?? (line.type === "threshold" ? [5, 5] : undefined),
dash: isHistoricalDashedTarget ? undefined : dash,
show: true,
points: { show: false },
});
visibleLineIndex++;
}
});
@@ -306,6 +354,83 @@ export function createChart({
}
},
],
draw: [
(u) => {
if (customDashedTargetLines.length === 0) return;
const xData = u.data[0] as number[] | undefined;
if (!xData || xData.length === 0) return;
const xMin = u.scales.x?.min;
const xMax = u.scales.x?.max;
if (xMin === undefined || xMax === undefined) return;
const pxPerX = u.bbox.width / Math.max(xMax - xMin, 1e-9);
const ctx = u.ctx;
const left = u.bbox.left;
const top = u.bbox.top;
const right = left + u.bbox.width;
const bottom = top + u.bbox.height;
ctx.save();
ctx.beginPath();
ctx.rect(left, top, u.bbox.width, u.bbox.height);
ctx.clip();
ctx.lineCap = "butt";
customDashedTargetLines.forEach(
({ dataIndex, dash, color, width }) => {
const yData = u.data[dataIndex] as
| Array<number | null>
| undefined;
if (!yData || yData.length === 0) return;
const dashPeriod = dash.reduce((acc, curr) => acc + curr, 0);
ctx.setLineDash(dash);
// Anchor dash phase to world-X so dashes remain stable while axis slides.
ctx.lineDashOffset =
dashPeriod > 0 ? -((xMin * pxPerX) % dashPeriod) : 0;
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.beginPath();
let started = false;
let prevX = 0;
let prevY = 0;
for (let i = 0; i < xData.length; i++) {
const value = yData[i];
if (value === null || value === undefined) continue;
const x = u.valToPos(xData[i], "x", true);
const y = u.valToPos(value, "y", true);
if (x < left || x > right || y < top || y > bottom) continue;
if (!started) {
ctx.moveTo(x, y);
started = true;
prevX = x;
prevY = y;
continue;
}
if (x !== prevX) {
ctx.lineTo(x, prevY);
}
if (y !== prevY) {
ctx.lineTo(x, y);
}
prevX = x;
prevY = y;
}
if (started) {
ctx.stroke();
}
},
);
ctx.restore();
},
],
},
scales: {
x: {
+25 -3
View File
@@ -14,6 +14,17 @@ import {
AnimationRefs,
} from "./types";
function hasHistoricalDashedTargets(config: GraphConfig): boolean {
return (
config.lines?.some(
(line) =>
line.show !== false &&
!!line.targetSeries &&
(line.dash?.length ?? 0) > 0,
) ?? false
);
}
// Retrieve all series data as an array of number arrays
function getAllSeriesData(data: BigGraphProps["newData"]): number[][] {
const normalized = normalizeDataSeries(data);
@@ -77,10 +88,16 @@ export function useLiveMode({
return;
try {
const hasDashedHistoricalTargets = hasHistoricalDashedTargets(config);
const [timestamps, values] = seriesToUPlotData(primaryData.long);
const cur = primaryData.current;
const liveTimestamps = [...timestamps, cur.timestamp];
const liveValues = [...values, cur.value];
const lastHistoricalTimestamp =
timestamps[timestamps.length - 1] ?? cur.timestamp;
const shouldAppendCurrent = cur.timestamp > lastHistoricalTimestamp;
const liveTimestamps = shouldAppendCurrent
? [...timestamps, cur.timestamp]
: timestamps;
const liveValues = shouldAppendCurrent ? [...values, cur.value] : values;
const allSeriesValues = getAllSeriesData(newData);
const normalized = normalizeDataSeries(newData);
@@ -96,7 +113,9 @@ export function useLiveMode({
(seriesValues.length > 0
? seriesValues[seriesValues.length - 1]
: 0);
return [...seriesValues, currentValue];
return shouldAppendCurrent
? [...seriesValues, currentValue]
: seriesValues;
})
: [];
@@ -107,6 +126,8 @@ export function useLiveMode({
animationRefs.realPointsCount,
config,
additionalSeriesValues,
animationRefs.targetLineCache,
hasDashedHistoricalTargets,
);
uplotRef.current.setData(liveData);
@@ -283,6 +304,7 @@ export function useLiveMode({
animationRefs.realPointsCount,
config,
additionalSeriesValues,
animationRefs.targetLineCache,
);
uplotRef.current.setData(uData);
+4
View File
@@ -26,7 +26,10 @@ export type PropGraphSync = {
// Configuration types for additional lines
export type GraphLine = {
type: "threshold" | "target";
/** Current value (used as fallback and for Y-axis scaling) */
value: number;
/** Optional TimeSeries for historical target values. When provided, the line shows the target history over time instead of a constant horizontal line */
targetSeries?: TimeSeries;
color: string;
label?: string;
width?: number;
@@ -158,6 +161,7 @@ export interface AnimationRefs {
values: number[];
}>;
realPointsCount: React.RefObject<number>;
targetLineCache: React.RefObject<Map<number, number[]>>;
}
export interface HandlerRefs {
@@ -4,7 +4,11 @@ import uPlot from "uplot";
import { seriesToUPlotData } from "@/lib/timeseries";
import { BigGraphProps, SeriesData, AnimationRefs, HandlerRefs } from "./types";
import { GraphExportData } from "./excelExport";
import { getPrimarySeries, stopAnimations } from "./animation";
import {
getPrimarySeries,
stopAnimations,
normalizeDataSeries,
} from "./animation";
import { createChart } from "./createChart";
import { LiveModeHandlers } from "./liveMode";
import { HistoricalModeHandlers } from "./historicalMode";
@@ -106,6 +110,10 @@ export function useBigGraphEffects({
handleTimeWindowChangeInternal,
}: UseBigGraphEffectsProps) {
const [isChartCreated, setIsChartCreated] = useState(false);
const setChartCreatedState = (created: boolean) => {
setIsChartCreated(created);
chartCreatedRef.current = created;
};
// Track the last synchronized state for comparison
const lastSyncStateRef = useRef({
@@ -239,18 +247,25 @@ export function useBigGraphEffects({
updateYAxisScale,
]);
// Create and initialize the chart when data becomes available
// Compute stable structural keys for chart creation.
// These only change when series count or visible line count changes,
// NOT on every data update (which would destroy/recreate the chart every ~1s).
const seriesWithDataCount = normalizeDataSeries(newData).filter(
(s) => s.newData !== null,
).length;
const visibleLineCount =
config.lines?.filter((l) => l.show !== false)?.length ?? 0;
// Create and initialize the chart when data becomes available or structure changes
useEffect(() => {
if (!containerRef.current || !primarySeries?.newData?.long) {
setIsChartCreated(false);
chartCreatedRef.current = false;
setChartCreatedState(false);
return;
}
const [timestamps] = seriesToUPlotData(primarySeries.newData.long);
if (timestamps.length === 0) {
setIsChartCreated(false);
chartCreatedRef.current = false;
setChartCreatedState(false);
return;
}
@@ -280,8 +295,7 @@ export function useBigGraphEffects({
showFromTimestamp,
});
setIsChartCreated(true);
chartCreatedRef.current = true;
setChartCreatedState(true);
if (isLiveMode) {
lastProcessedCountRef.current = timestamps.length;
@@ -292,10 +306,9 @@ export function useBigGraphEffects({
uplotRef.current?.destroy();
uplotRef.current = null;
stopAnimations(animationRefs);
setIsChartCreated(false);
chartCreatedRef.current = false;
setChartCreatedState(false);
};
}, [primarySeries?.newData?.long, containerRef.current]);
}, [seriesWithDataCount, visibleLineCount, containerRef.current]);
// Process new historical data in live mode
useEffect(() => {
+53
View File
@@ -304,3 +304,56 @@ export function resetSeries(series: Series): void {
series.lastTimestamp = 0;
series.validCount = 0;
}
/**
* Aligns target series values with a given set of timestamps.
* For each timestamp in dataTimestamps, finds the most recent target value
* that was set before or at that timestamp (step function interpolation).
*
* @param targetSeries - The TimeSeries containing historical target values
* @param dataTimestamps - Array of timestamps to align the target values to
* @param fallbackValue - Value to use when no target was set before a timestamp
* @returns Array of values aligned with dataTimestamps
*/
export function alignTargetSeriesToTimestamps(
targetSeries: TimeSeries,
dataTimestamps: number[],
fallbackValue: number,
): number[] {
if (dataTimestamps.length === 0) {
return [];
}
// Extract target series data in chronological order
const [targetTimestamps, targetValues] = extractDataFromSeries(
targetSeries.long,
);
if (targetTimestamps.length === 0) {
// No target history, use fallback for all timestamps
return new Array(dataTimestamps.length).fill(fallbackValue);
}
const result: number[] = [];
let targetIndex = 0;
for (const dataTs of dataTimestamps) {
// Find the most recent target value at or before this timestamp
// Advance targetIndex while the next target timestamp is still <= dataTs
while (
targetIndex < targetTimestamps.length - 1 &&
targetTimestamps[targetIndex + 1] <= dataTs
) {
targetIndex++;
}
// If the current target timestamp is > dataTs, we haven't reached the first target yet
if (targetTimestamps[targetIndex] > dataTs) {
result.push(fallbackValue);
} else {
result.push(targetValues[targetIndex]);
}
}
return result;
}
@@ -24,6 +24,13 @@ export function Extruder2GraphsPage() {
motorCurrent,
motorPower,
combinedPower,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder2();
const syncHook = useGraphSync("extruder-graphs");
@@ -49,6 +56,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
targetSeries: targetNozzleTemperature,
color: "#ef4444",
show: true,
},
@@ -69,6 +77,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
targetSeries: targetFrontTemperature,
color: "#f59e0b",
show: true,
},
@@ -89,6 +98,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
targetSeries: targetMiddleTemperature,
color: "#8b5cf6",
show: true,
},
@@ -109,6 +119,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
targetSeries: targetBackTemperature,
color: "#3b82f6",
show: true,
},
@@ -253,6 +264,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.pressure_state.target_bar,
targetSeries: targetPressure,
color: "#3b82f6",
show: true,
},
@@ -306,6 +318,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.screw_state.target_rpm,
targetSeries: targetScrewRpm,
color: "#8b5cf6",
show: true,
},
@@ -221,6 +221,14 @@ export type Extruder2NamespaceStore = {
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
// Target value history (for graph target lines)
targetPressure: TimeSeries;
targetScrewRpm: TimeSeries;
targetNozzleTemperature: TimeSeries;
targetFrontTemperature: TimeSeries;
targetMiddleTemperature: TimeSeries;
targetBackTemperature: TimeSeries;
};
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
@@ -253,6 +261,28 @@ const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries();
// Target value time series (for historical target lines on graphs)
const { initialTimeSeries: targetPressure, insert: addTargetPressure } =
createTimeSeries();
const { initialTimeSeries: targetScrewRpm, insert: addTargetScrewRpm } =
createTimeSeries();
const {
initialTimeSeries: targetNozzleTemperature,
insert: addTargetNozzleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetFrontTemperature,
insert: addTargetFrontTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetMiddleTemperature,
insert: addTargetMiddleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetBackTemperature,
insert: addTargetBackTemperature,
} = createTimeSeries();
export function extruder2MessageHandler(
store: StoreApi<Extruder2NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder2NamespaceStore>,
@@ -271,6 +301,7 @@ export function extruder2MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -278,6 +309,43 @@ export function extruder2MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure: addTargetPressure(state.targetPressure, {
value: stateEvent.data.pressure_state.target_bar,
timestamp,
}),
targetScrewRpm: addTargetScrewRpm(state.targetScrewRpm, {
value: stateEvent.data.screw_state.target_rpm,
timestamp,
}),
targetNozzleTemperature: addTargetNozzleTemperature(
state.targetNozzleTemperature,
{
value: stateEvent.data.heating_states.nozzle.target_temperature,
timestamp,
},
),
targetFrontTemperature: addTargetFrontTemperature(
state.targetFrontTemperature,
{
value: stateEvent.data.heating_states.front.target_temperature,
timestamp,
},
),
targetMiddleTemperature: addTargetMiddleTemperature(
state.targetMiddleTemperature,
{
value: stateEvent.data.heating_states.middle.target_temperature,
timestamp,
},
),
targetBackTemperature: addTargetBackTemperature(
state.targetBackTemperature,
{
value: stateEvent.data.heating_states.back.target_temperature,
timestamp,
},
),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -378,6 +446,14 @@ export const createExtruder2NamespaceStore =
middlePower,
combinedPower,
totalEnergyKWh,
// Target value history
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
};
});
@@ -57,6 +57,14 @@ export function useExtruder2() {
backPower,
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder2Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -423,6 +431,14 @@ export function useExtruder2() {
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
@@ -24,6 +24,13 @@ export function Extruder3GraphsPage() {
motorCurrent,
motorPower,
combinedPower,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder3();
const syncHook = useGraphSync("extruder-graphs");
@@ -49,6 +56,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
targetSeries: targetNozzleTemperature,
color: "#ef4444",
show: true,
},
@@ -69,6 +77,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
targetSeries: targetFrontTemperature,
color: "#f59e0b",
show: true,
},
@@ -89,6 +98,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
targetSeries: targetMiddleTemperature,
color: "#8b5cf6",
show: true,
},
@@ -109,6 +119,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
targetSeries: targetBackTemperature,
color: "#3b82f6",
show: true,
},
@@ -253,6 +264,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.pressure_state.target_bar,
targetSeries: targetPressure,
color: "#3b82f6",
show: true,
},
@@ -306,6 +318,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.screw_state.target_rpm,
targetSeries: targetScrewRpm,
color: "#8b5cf6",
show: true,
},
@@ -221,6 +221,14 @@ export type Extruder3NamespaceStore = {
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
// Target value history (for graph target lines)
targetPressure: TimeSeries;
targetScrewRpm: TimeSeries;
targetNozzleTemperature: TimeSeries;
targetFrontTemperature: TimeSeries;
targetMiddleTemperature: TimeSeries;
targetBackTemperature: TimeSeries;
};
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
@@ -253,6 +261,28 @@ const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries();
// Target value time series (for historical target lines on graphs)
const { initialTimeSeries: targetPressure, insert: addTargetPressure } =
createTimeSeries();
const { initialTimeSeries: targetScrewRpm, insert: addTargetScrewRpm } =
createTimeSeries();
const {
initialTimeSeries: targetNozzleTemperature,
insert: addTargetNozzleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetFrontTemperature,
insert: addTargetFrontTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetMiddleTemperature,
insert: addTargetMiddleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetBackTemperature,
insert: addTargetBackTemperature,
} = createTimeSeries();
export function extruder3MessageHandler(
store: StoreApi<Extruder3NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder3NamespaceStore>,
@@ -271,6 +301,7 @@ export function extruder3MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -278,6 +309,43 @@ export function extruder3MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure: addTargetPressure(state.targetPressure, {
value: stateEvent.data.pressure_state.target_bar,
timestamp,
}),
targetScrewRpm: addTargetScrewRpm(state.targetScrewRpm, {
value: stateEvent.data.screw_state.target_rpm,
timestamp,
}),
targetNozzleTemperature: addTargetNozzleTemperature(
state.targetNozzleTemperature,
{
value: stateEvent.data.heating_states.nozzle.target_temperature,
timestamp,
},
),
targetFrontTemperature: addTargetFrontTemperature(
state.targetFrontTemperature,
{
value: stateEvent.data.heating_states.front.target_temperature,
timestamp,
},
),
targetMiddleTemperature: addTargetMiddleTemperature(
state.targetMiddleTemperature,
{
value: stateEvent.data.heating_states.middle.target_temperature,
timestamp,
},
),
targetBackTemperature: addTargetBackTemperature(
state.targetBackTemperature,
{
value: stateEvent.data.heating_states.back.target_temperature,
timestamp,
},
),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -378,6 +446,14 @@ export const createExtruder3NamespaceStore =
middlePower,
combinedPower,
totalEnergyKWh,
// Target value history
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
};
});
@@ -57,6 +57,14 @@ export function useExtruder3() {
backPower,
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder3Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -423,6 +431,14 @@ export function useExtruder3() {
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
@@ -10,10 +10,17 @@ import React from "react";
import { useLaser1 } from "./useLaser1";
export function Laser1GraphsPage() {
const { diameter, x_diameter, y_diameter, roundness, state } = useLaser1();
const {
diameter,
x_diameter,
y_diameter,
roundness,
state,
targetDiameter: targetDiameterSeries,
} = useLaser1();
const syncHook = useGraphSync("diameter-roundness-group");
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const targetDiameterValue = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
@@ -71,21 +78,22 @@ export function Laser1GraphsPage() {
// Tolerance for diameter
{
type: "threshold" as const,
value: targetDiameter + higherTolerance,
value: targetDiameterValue + higherTolerance,
label: "Upper Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "threshold" as const,
value: targetDiameter - lowerTolerance,
value: targetDiameterValue - lowerTolerance,
label: "Lower Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "target" as const,
value: targetDiameter,
value: targetDiameterValue,
targetSeries: targetDiameterSeries,
label: "Target",
color: diameterColor,
},
@@ -64,6 +64,7 @@ export type Laser1NamespaceStore = {
x_diameter: TimeSeries;
y_diameter: TimeSeries;
roundness: TimeSeries;
targetDiameter: TimeSeries;
};
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries();
@@ -73,6 +74,8 @@ const { initialTimeSeries: y_diameter, insert: addYDiameter } =
createTimeSeries();
const { initialTimeSeries: roundness, insert: addRoundness } =
createTimeSeries();
const { initialTimeSeries: targetDiameter, insert: addTargetDiameter } =
createTimeSeries();
/**
* Factory function to create a new Laser1 namespace store
@@ -87,6 +90,7 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
x_diameter,
y_diameter,
roundness,
targetDiameter,
};
});
@@ -117,6 +121,10 @@ export function laser1MessageHandler(
updateStore((state) => ({
...state,
state: stateEvent,
targetDiameter: addTargetDiameter(state.targetDiameter, {
value: stateEvent.data.laser_state.target_diameter,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
@@ -11,8 +11,15 @@ import { produce } from "immer";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, diameter, x_diameter, y_diameter, roundness } =
useLaser1Namespace(machine_identification_unique);
const {
state,
defaultState,
diameter,
x_diameter,
y_diameter,
roundness,
targetDiameter,
} = useLaser1Namespace(machine_identification_unique);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
@@ -113,6 +120,7 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
x_diameter,
y_diameter,
roundness,
targetDiameter,
// Loading states
isLoading: stateOptimistic.isOptimistic,
@@ -19,6 +19,7 @@ export function Winder2GraphsPage() {
traversePosition,
tensionArmAngle,
pullerSpeed,
targetPullerSpeed,
spoolProgress,
} = useWinder2();
@@ -31,6 +32,7 @@ export function Winder2GraphsPage() {
<PullerSpeedGraph
syncHook={syncHook}
newData={pullerSpeed}
targetSeries={targetPullerSpeed}
targetSpeed={state?.puller_state?.target_speed}
unit="m/min"
renderValue={(value) => roundToDecimals(value, 0)}
@@ -243,12 +245,14 @@ export function SpoolProgressGraph({
export function PullerSpeedGraph({
syncHook,
newData,
targetSeries,
targetSpeed,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
targetSeries?: TimeSeries;
targetSpeed?: number;
unit?: Unit;
renderValue?: (value: number) => string;
@@ -259,6 +263,7 @@ export function PullerSpeedGraph({
lines.push({
type: "target",
value: targetSpeed,
targetSeries,
label: "Target Speed",
color: "#06b6d4", // Match series color
});
@@ -62,6 +62,7 @@ export function useWinder2() {
defaultState,
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -638,6 +639,7 @@ export function useWinder2() {
// Individual live values (TimeSeries)
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -211,6 +211,7 @@ export type Winder2NamespaceStore = {
spoolRpm: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
targetPullerSpeed: TimeSeries;
};
//Store Factory and Message Handler -> no param, so default values
@@ -220,6 +221,8 @@ const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
createTimeSeries();
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
createTimeSeries();
const { initialTimeSeries: targetPullerSpeed, insert: addTargetPullerSpeed } =
createTimeSeries();
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries();
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
createTimeSeries();
@@ -239,6 +242,7 @@ export const createWinder2NamespaceStore =
// Time series data for live values
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -277,6 +281,10 @@ export function winder2MessageHandler(
updateStore((state) => ({
...state,
state: stateEvent,
targetPullerSpeed: addTargetPullerSpeed(state.targetPullerSpeed, {
value: stateEvent.data.puller_state.target_speed,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent