Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
658f524eff | ||
|
|
88b0c87a51 | ||
|
|
4178c90769 | ||
|
|
2c99231f1f | ||
|
|
26b417c0ec | ||
|
|
7df303bdf4 | ||
|
|
93c5e8d599 | ||
|
|
c5fbdd0a0f | ||
|
|
45e03b9c93 | ||
|
|
3a24c24836 | ||
|
|
26e4c6ab6c | ||
|
|
60f95ec9b6 | ||
|
|
2c7f65dc78 | ||
|
|
d3d2ab4828 | ||
|
|
a839a374a3 | ||
|
|
6049737955 | ||
|
|
0c4a1eec39 |
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user