Add Mock Machine 0x7
This commit is contained in:
1 parent
2241c7f996
commit
e7617d2ac2
22 files changed
+946
-7
No files matched your search
@@ -64,6 +64,7 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Not Yet | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | Not Yet | |
|
||||
| DRE | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
|
||||
|
||||
|
||||
# More Docs
|
||||
|
||||
@@ -21,6 +21,8 @@ export function getUnitIcon(unit: Unit): IconName {
|
||||
return "lu:TriangleRight";
|
||||
case "m/min":
|
||||
return "lu:Gauge";
|
||||
case "mHz":
|
||||
return "lu:AudioWaveform";
|
||||
default:
|
||||
return "lu:ChartNoAxesColumn";
|
||||
}
|
||||
@@ -44,6 +46,8 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
|
||||
return "deg";
|
||||
case "m/min":
|
||||
return "m/min";
|
||||
case "mHz":
|
||||
return "mHz";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -89,6 +93,8 @@ export function renderUnitSymbolLong(unit: Unit): string {
|
||||
return "degrees";
|
||||
case "m/min":
|
||||
return "meters/minute";
|
||||
case "mHz":
|
||||
return "millihertz";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -103,6 +109,7 @@ export const units = [
|
||||
"C",
|
||||
"bar",
|
||||
"m/min",
|
||||
"mHz",
|
||||
] as const;
|
||||
|
||||
export type Unit = (typeof units)[number];
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { useMock1 } from "./useMock";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Icon } from "@/components/Icon";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { Mode } from "./mock1Namespace";
|
||||
|
||||
export function Mock1ControlPage() {
|
||||
const {
|
||||
sineWave,
|
||||
mockState,
|
||||
modeState,
|
||||
mockSetFrequency,
|
||||
mockSetMode,
|
||||
mockStateIsLoading,
|
||||
mockStateIsDisabled,
|
||||
modeStateIsLoading,
|
||||
modeStateIsDisabled,
|
||||
} = useMock1();
|
||||
|
||||
// Controlled local states synced with mockState and modeState
|
||||
const frequency = mockState?.data?.frequency ?? 1.0;
|
||||
const mode = modeState?.data?.mode ?? "Standby";
|
||||
const isRunning = mode === "Running";
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
<ControlCard title="Sine Wave Generator">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Current Value"
|
||||
timeseries={sineWave}
|
||||
renderValue={(value) => value.toFixed(3)}
|
||||
/>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Settings">
|
||||
<div className="flex flex-col gap-4">
|
||||
<EditValue
|
||||
title="Frequency (Hz)"
|
||||
unit="mHz"
|
||||
value={frequency}
|
||||
defaultValue={500}
|
||||
min={1}
|
||||
max={1000}
|
||||
step={10}
|
||||
renderValue={(value) => value.toFixed(0)}
|
||||
onChange={mockSetFrequency}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm font-medium">Mode</div>
|
||||
<SelectionGroup
|
||||
value={mode}
|
||||
onChange={(newMode: Mode) => mockSetMode(newMode)}
|
||||
disabled={modeStateIsDisabled}
|
||||
options={{
|
||||
Standby: { children: "Standby" },
|
||||
Running: { children: "Running" },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { mock1SerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function Mock1Page() {
|
||||
const { serial } = mock1SerialRoute.useParams();
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/mock1/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @file mock1Namespace.ts
|
||||
* @description TypeScript implementation of Mock1 namespace with Zod schema validation.
|
||||
*/
|
||||
|
||||
import { StoreApi } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { produce } from "immer";
|
||||
import { number, z } from "zod";
|
||||
import {
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
Event,
|
||||
handleEventValidationError,
|
||||
handleUnhandledEventError,
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
} from "../../../client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
createTimeSeries,
|
||||
TimeSeries,
|
||||
TimeSeriesValue,
|
||||
} from "@/lib/timeseries";
|
||||
|
||||
// ========== Event Schema Definitions ==========
|
||||
/**
|
||||
* Mode enum for Mock Machine
|
||||
*/
|
||||
export const modeSchema = z.enum(["Standby", "Running"]);
|
||||
|
||||
/**
|
||||
* Sine wave data from Mock Machine (only amplitude)
|
||||
*/
|
||||
export const sineWaveEventDataSchema = z.object({
|
||||
amplitude: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Sine wave state event (frequency only)
|
||||
*/
|
||||
export const mockStateEventDataSchema = z.object({
|
||||
frequency: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Mode state event schema
|
||||
*/
|
||||
export const modeStateEventDataSchema = z.object({
|
||||
mode: modeSchema,
|
||||
});
|
||||
|
||||
// ========== Event Schemas with Wrappers ==========
|
||||
export const sineWaveEventSchema = eventSchema(sineWaveEventDataSchema);
|
||||
export const mockStateEventSchema = eventSchema(mockStateEventDataSchema);
|
||||
export const modeStateEventSchema = eventSchema(modeStateEventDataSchema);
|
||||
|
||||
// ========== Type Inferences ==========
|
||||
export type Mode = z.infer<typeof modeSchema>;
|
||||
export type SineWaveEvent = z.infer<typeof sineWaveEventSchema>;
|
||||
export type MockStateEvent = z.infer<typeof mockStateEventSchema>;
|
||||
export type ModeStateEvent = z.infer<typeof modeStateEventSchema>;
|
||||
|
||||
export type Mock1NamespaceStore = {
|
||||
// State events (latest only)
|
||||
mockState: MockStateEvent | null;
|
||||
modeState: ModeStateEvent | null;
|
||||
// Metric events (cached for 1 hour)
|
||||
sineWave: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const ONE_SECOND = 1000;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
const { initialTimeSeries: sineWave, insert: addSineWave } = createTimeSeries(
|
||||
ONE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
/**
|
||||
* Factory function to create a new Mock1 namespace store
|
||||
* @returns A new Zustand store instance for Mock1 namespace
|
||||
*/
|
||||
export const createMock1NamespaceStore = (): StoreApi<Mock1NamespaceStore> =>
|
||||
create<Mock1NamespaceStore>((set) => {
|
||||
return {
|
||||
mockState: null,
|
||||
modeState: null,
|
||||
sineWave: sineWave,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a message handler for Mock1 namespace events with validation and appropriate caching strategies
|
||||
* @param store The store to update when messages are received
|
||||
* @returns A message handler function
|
||||
*/
|
||||
export function mock1MessageHandler(
|
||||
store: StoreApi<Mock1NamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const eventName = event.name;
|
||||
|
||||
try {
|
||||
// Apply appropriate caching strategy based on event type
|
||||
if (eventName === "MockStateEvent") {
|
||||
store.setState(
|
||||
produce(store.getState(), (state) => {
|
||||
state.mockState = {
|
||||
name: event.name,
|
||||
data: mockStateEventDataSchema.parse(event.data),
|
||||
ts: event.ts,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Mode state events (latest only)
|
||||
else if (eventName === "ModeStateEvent") {
|
||||
store.setState(
|
||||
produce(store.getState(), (state) => {
|
||||
state.modeState = {
|
||||
name: event.name,
|
||||
data: modeStateEventDataSchema.parse(event.data),
|
||||
ts: event.ts,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Metric events (keep for 1 hour)
|
||||
else if (eventName === "SineWaveEvent") {
|
||||
let parsed = sineWaveEventSchema.parse(event);
|
||||
let timeseriesValue: TimeSeriesValue = {
|
||||
value: parsed.data.amplitude,
|
||||
timestamp: event.ts,
|
||||
};
|
||||
store.setState(
|
||||
produce(store.getState(), (state) => {
|
||||
state.sineWave = addSineWave(state.sineWave, timeseriesValue);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
handleUnhandledEventError(eventName);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
handleEventValidationError(error, eventName);
|
||||
} else {
|
||||
console.error(`Unexpected error processing ${eventName} event:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the Mock1 namespace implementation
|
||||
*/
|
||||
const useMock1NamespaceImplementation =
|
||||
createNamespaceHookImplementation<Mock1NamespaceStore>({
|
||||
createStore: createMock1NamespaceStore,
|
||||
createEventHandler: mock1MessageHandler,
|
||||
});
|
||||
|
||||
export function useMock1Namespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): Mock1NamespaceStore {
|
||||
// Generate namespace ID from validated machine ID
|
||||
const namespaceId = useRef<NamespaceId>({
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
});
|
||||
|
||||
// Use the implementation with validated namespace ID
|
||||
return useMock1NamespaceImplementation(namespaceId.current);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
|
||||
import { mock1, MachineIdentificationUnique } from "@/machines/types";
|
||||
import { mock1SerialRoute } from "@/routes/routes";
|
||||
import { z } from "zod";
|
||||
import { useMock1Namespace, Mode } from "./mock1Namespace";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
|
||||
function useMock(machine_identification_unique: MachineIdentificationUnique) {
|
||||
// Write Path
|
||||
const mockStateOptimistic = useStateOptimistic<{
|
||||
frequency: number;
|
||||
}>();
|
||||
|
||||
const modeStateOptimistic = useStateOptimistic<{
|
||||
mode: Mode;
|
||||
}>();
|
||||
|
||||
const schemaSetFrequency = z.object({ SetFrequency: z.number() });
|
||||
const { request: requestSetFrequency } =
|
||||
useMachineMutation(schemaSetFrequency);
|
||||
const mockSetFrequency = async (frequency: number) => {
|
||||
if (mockStateOptimistic.value) {
|
||||
mockStateOptimistic.setOptimistic({
|
||||
...mockStateOptimistic.value,
|
||||
frequency: frequency,
|
||||
});
|
||||
}
|
||||
requestSetFrequency({
|
||||
machine_identification_unique,
|
||||
data: {
|
||||
SetFrequency: frequency,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.success) mockStateOptimistic.resetToReal();
|
||||
})
|
||||
.catch(() => mockStateOptimistic.resetToReal());
|
||||
};
|
||||
|
||||
const schemaSetMode = z.object({ SetMode: z.enum(["Standby", "Running"]) });
|
||||
const { request: requestSetMode } = useMachineMutation(schemaSetMode);
|
||||
const mockSetMode = async (mode: Mode) => {
|
||||
if (modeStateOptimistic.value) {
|
||||
modeStateOptimistic.setOptimistic({
|
||||
...modeStateOptimistic.value,
|
||||
mode: mode,
|
||||
});
|
||||
}
|
||||
requestSetMode({
|
||||
machine_identification_unique,
|
||||
data: {
|
||||
SetMode: mode,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.success) modeStateOptimistic.resetToReal();
|
||||
})
|
||||
.catch(() => modeStateOptimistic.resetToReal());
|
||||
};
|
||||
|
||||
// Read Path
|
||||
const { sineWave, mockState, modeState } = useMock1Namespace(
|
||||
machine_identification_unique,
|
||||
);
|
||||
|
||||
// Update real values from server
|
||||
useEffect(() => {
|
||||
if (mockState?.data) {
|
||||
mockStateOptimistic.setReal(mockState.data);
|
||||
}
|
||||
}, [mockState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modeState?.data) {
|
||||
modeStateOptimistic.setReal(modeState.data);
|
||||
}
|
||||
}, [modeState]);
|
||||
|
||||
return {
|
||||
sineWave,
|
||||
mockState,
|
||||
modeState,
|
||||
mockSetFrequency,
|
||||
mockSetMode,
|
||||
mockStateIsLoading:
|
||||
mockStateOptimistic.isOptimistic || !mockStateOptimistic.isInitialized,
|
||||
mockStateIsDisabled:
|
||||
mockStateOptimistic.isOptimistic || !mockStateOptimistic.isInitialized,
|
||||
modeStateIsLoading:
|
||||
modeStateOptimistic.isOptimistic || !modeStateOptimistic.isInitialized,
|
||||
modeStateIsDisabled:
|
||||
modeStateOptimistic.isOptimistic || !modeStateOptimistic.isInitialized,
|
||||
};
|
||||
}
|
||||
|
||||
export function useMock1() {
|
||||
const { serial: serialString } = mock1SerialRoute.useParams();
|
||||
|
||||
// Memoize the machine identification to keep it stable between renders
|
||||
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
|
||||
const serial = parseInt(serialString); // Use 0 as fallback if NaN
|
||||
|
||||
if (isNaN(serial)) {
|
||||
toastError(
|
||||
"Invalid Serial Number",
|
||||
`"${serialString}" is not a valid serial number.`,
|
||||
);
|
||||
|
||||
return {
|
||||
machine_identification: {
|
||||
vendor: 0,
|
||||
machine: 0,
|
||||
},
|
||||
serial: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
machine_identification: mock1.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]); // Only recreate when serialString changes
|
||||
|
||||
const mock = useMock(machineIdentification);
|
||||
|
||||
return {
|
||||
...mock,
|
||||
};
|
||||
}
|
||||
@@ -280,7 +280,25 @@ export const dre1: MachinePreset = {
|
||||
},
|
||||
device_roles: [],
|
||||
};
|
||||
export const machinePresets: MachinePreset[] = [winder2, extruder2, dre1];
|
||||
|
||||
export const mock1: MachinePreset = {
|
||||
name: "Mock",
|
||||
version: "V1",
|
||||
slug: "mock1",
|
||||
icon: "lu:FlaskConical",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x0007,
|
||||
},
|
||||
device_roles: [],
|
||||
};
|
||||
|
||||
export const machinePresets: MachinePreset[] = [
|
||||
winder2,
|
||||
extruder2,
|
||||
dre1,
|
||||
mock1,
|
||||
];
|
||||
|
||||
export const getMachinePreset = (
|
||||
machine_identification: MachineIdentification,
|
||||
|
||||
@@ -28,6 +28,8 @@ import { ChangelogPage } from "@/setup/ChangelogPage";
|
||||
import { UpdateExecutePage } from "@/setup/UpdateExecutePage";
|
||||
import { Dre1Page } from "@/machines/dre/dre1/Dre1Page";
|
||||
import { Dre1ControlPage } from "@/machines/dre/dre1/Dre1ControlPage";
|
||||
import { Mock1Page } from "@/machines/mock/mock1/Mock1Page";
|
||||
import { Mock1ControlPage } from "@/machines/mock/mock1/Mock1ControlPage";
|
||||
import { Dre1GraphsPage } from "@/machines/dre/dre1/Dre1Graph";
|
||||
|
||||
// make a route tree like this
|
||||
@@ -121,6 +123,18 @@ export const dre1GraphsRoute = createRoute({
|
||||
component: () => <Dre1GraphsPage />,
|
||||
});
|
||||
|
||||
export const mock1SerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "mock1/$serial",
|
||||
component: () => <Mock1Page />,
|
||||
});
|
||||
|
||||
export const mock1ControlRoute = createRoute({
|
||||
getParentRoute: () => mock1SerialRoute,
|
||||
path: "control",
|
||||
component: () => <Mock1ControlPage />,
|
||||
});
|
||||
|
||||
export const setupRoute = createRoute({
|
||||
getParentRoute: () => sidebarRoute,
|
||||
path: "setup",
|
||||
@@ -213,6 +227,8 @@ export const rootTree = RootRoute.addChildren([
|
||||
extruder2SettingsRoute,
|
||||
extruder2ManualRoute,
|
||||
]),
|
||||
|
||||
mock1SerialRoute.addChildren([mock1ControlRoute]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
@@ -9,11 +9,11 @@ use restore::restore_eeoprom;
|
||||
/// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1.
|
||||
const MAX_SUBDEVICES: usize = 16;
|
||||
/// Maximum PDU data payload size - set this to the max PDI size or higher.
|
||||
const MAX_PDU_DATA: usize = PduStorage::element_size(128);
|
||||
const MAX_PDU_DATA: usize = PduStorage::element_size(256);
|
||||
/// Maximum number of EtherCAT frames that can be in flight at any one time.
|
||||
const MAX_FRAMES: usize = 16;
|
||||
/// Maximum total PDI length.
|
||||
const PDI_LEN: usize = 128;
|
||||
const PDI_LEN: usize = 256;
|
||||
|
||||
static PDU_STORAGE: PduStorage<MAX_FRAMES, MAX_PDU_DATA> = PduStorage::new();
|
||||
|
||||
|
||||
+2
-1
@@ -21,7 +21,7 @@ tower-http = { version = "0.6.2", features = ["cors", "trace", "fs"] }
|
||||
axum = { version = "0.8.4", features = ["macros"] }
|
||||
|
||||
# utility
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64"]}
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
|
||||
serde = "1.0.217"
|
||||
anyhow = "1.0.98"
|
||||
serde_json = "1.0.137"
|
||||
@@ -46,6 +46,7 @@ approx = "0.5.1"
|
||||
[features]
|
||||
never_type = []
|
||||
dhat-heap = ["dep:dhat"]
|
||||
mock-machine = []
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
use super::{MockMachine, api};
|
||||
use control_core::actors::Actor;
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Implements the `Actor` trait for the `MockMachine`.
|
||||
///
|
||||
/// # 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 `MockMachine`. Specifically:
|
||||
/// - It checks if the time elapsed since the last measurement emission exceeds 16 milliseconds.
|
||||
/// - If the condition is met and the machine is in Running mode, it emits a sine wave data event.
|
||||
/// - State events (frequency, mode) are only emitted when values change, not continuously.
|
||||
///
|
||||
/// The method ensures that the sine wave value is updated approximately 60 times per second (16ms intervals) when running.
|
||||
///
|
||||
impl Actor for MockMachine {
|
||||
fn act(&mut self, _now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
let now = Instant::now();
|
||||
|
||||
// Emit initial state if this is the first call
|
||||
if self.last_emitted_frequency.is_none() || self.last_emitted_mode.is_none() {
|
||||
self.emit_sine_wave_state();
|
||||
self.emit_mode_state();
|
||||
}
|
||||
|
||||
// Only emit sine wave if machine is in Running mode
|
||||
// The sine wave value is updated approximately 60 times per second
|
||||
if now.duration_since(self.last_measurement_emit) > Duration::from_millis(16)
|
||||
{
|
||||
self.emit_sine_wave();
|
||||
self.last_measurement_emit = now;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
use super::MockMachine;
|
||||
use control_core::{
|
||||
machines::api::MachineApi,
|
||||
socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, NamespaceInterface,
|
||||
cache_duration, cache_one_event,
|
||||
},
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub enum Mode {
|
||||
Standby,
|
||||
Running,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct SineWaveEvent {
|
||||
pub amplitude: f64,
|
||||
}
|
||||
|
||||
impl SineWaveEvent {
|
||||
pub fn build(&self) -> Event<Self> {
|
||||
Event::new("SineWaveEvent", self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct SineWaveStateEvent {
|
||||
pub frequency: f64,
|
||||
}
|
||||
|
||||
impl SineWaveStateEvent {
|
||||
pub fn build(&self) -> Event<Self> {
|
||||
Event::new("MockStateEvent", self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ModeStateEvent {
|
||||
pub mode: Mode,
|
||||
}
|
||||
|
||||
impl ModeStateEvent {
|
||||
pub fn build(&self) -> Event<Self> {
|
||||
Event::new("ModeStateEvent", self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MockEvents {
|
||||
SineWave(Event<SineWaveEvent>),
|
||||
SineWaveState(Event<SineWaveStateEvent>),
|
||||
ModeState(Event<ModeStateEvent>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockMachineNamespace(Namespace);
|
||||
|
||||
impl MockMachineNamespace {
|
||||
pub fn new() -> Self {
|
||||
Self(Namespace::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheableEvents<MockEvents> for MockEvents {
|
||||
fn event_value(&self) -> Result<GenericEvent, serde_json::Error> {
|
||||
match self {
|
||||
MockEvents::SineWave(event) => event.try_into(),
|
||||
MockEvents::SineWaveState(event) => event.try_into(),
|
||||
MockEvents::ModeState(event) => event.try_into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
let cache_ten_secs = cache_duration(Duration::from_secs(10), Duration::from_secs(1));
|
||||
let cache_one = cache_one_event();
|
||||
|
||||
match self {
|
||||
MockEvents::SineWave(_) => cache_ten_secs,
|
||||
MockEvents::SineWaveState(_) => cache_one,
|
||||
MockEvents::ModeState(_) => cache_one,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
/// Mutation for controlling the mock machine
|
||||
enum Mutation {
|
||||
/// Set the frequency of the sine wave in millihertz
|
||||
SetFrequency(f64),
|
||||
SetMode(Mode),
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<MockEvents> for MockMachineNamespace {
|
||||
fn emit_cached(&mut self, events: MockEvents) {
|
||||
let event = match events.event_value() {
|
||||
Ok(event) => event,
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"[{}::emit_cached] Failed to event.event_value(): {:?}",
|
||||
module_path!(),
|
||||
err
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
self.0.emit_cached(&event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineApi for MockMachine {
|
||||
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
|
||||
let mutation: Mutation = serde_json::from_value(request_body)?;
|
||||
match mutation {
|
||||
Mutation::SetFrequency(frequency) => {
|
||||
self.set_frequency(frequency);
|
||||
}
|
||||
Mutation::SetMode(mode) => {
|
||||
self.set_mode(mode);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> &mut dyn NamespaceInterface {
|
||||
&mut self.namespace.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use api::{
|
||||
MockEvents, MockMachineNamespace, Mode, ModeStateEvent, SineWaveEvent, SineWaveStateEvent,
|
||||
};
|
||||
use control_core::{machines::Machine, socketio::namespace::NamespaceCacheingLogic};
|
||||
use std::time::Instant;
|
||||
use uom::si::{
|
||||
f64::Frequency,
|
||||
frequency::{hertz, millihertz},
|
||||
};
|
||||
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod new;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockMachine {
|
||||
// socketio
|
||||
namespace: MockMachineNamespace,
|
||||
last_measurement_emit: Instant,
|
||||
|
||||
// mock machine specific fields
|
||||
t_0: Instant,
|
||||
frequency: Frequency,
|
||||
mode: Mode,
|
||||
|
||||
// State tracking to only emit when values change
|
||||
last_emitted_frequency: Option<f64>,
|
||||
last_emitted_mode: Option<Mode>,
|
||||
}
|
||||
|
||||
impl Machine for MockMachine {}
|
||||
|
||||
impl MockMachine {
|
||||
/// Emit a sine wave data event with the current time and frequency
|
||||
pub fn emit_sine_wave(&mut self) {
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(self.t_0).as_secs_f64();
|
||||
let freq_hz = self.frequency.get::<hertz>();
|
||||
|
||||
// Calculate sine wave: sin(2π * frequency * time)
|
||||
let y = match self.mode {
|
||||
Mode::Standby => 0.0,
|
||||
Mode::Running => (2.0 * std::f64::consts::PI * freq_hz * elapsed).sin(),
|
||||
};
|
||||
|
||||
let sine_wave_event = SineWaveEvent { amplitude: y };
|
||||
|
||||
self.namespace
|
||||
.emit_cached(MockEvents::SineWave(sine_wave_event.build()));
|
||||
}
|
||||
|
||||
/// Emit the current state of the mock machine only if values have changed
|
||||
pub fn emit_sine_wave_state(&mut self) {
|
||||
let current_frequency_mhz = self.frequency.get::<millihertz>();
|
||||
|
||||
// Only emit if values have changed or this is the first emission
|
||||
let should_emit = self.last_emitted_frequency != Some(current_frequency_mhz);
|
||||
|
||||
if should_emit {
|
||||
let mock_state_event = SineWaveStateEvent {
|
||||
frequency: current_frequency_mhz,
|
||||
};
|
||||
|
||||
self.namespace
|
||||
.emit_cached(MockEvents::SineWaveState(mock_state_event.build()));
|
||||
|
||||
// Update last emitted values
|
||||
self.last_emitted_frequency = Some(current_frequency_mhz);
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the current mode state only if values have changed
|
||||
pub fn emit_mode_state(&mut self) {
|
||||
let current_mode = self.mode.clone();
|
||||
|
||||
// Only emit if values have changed or this is the first emission
|
||||
let should_emit = self.last_emitted_mode != Some(current_mode.clone());
|
||||
|
||||
if should_emit {
|
||||
let mode_state_event = ModeStateEvent {
|
||||
mode: current_mode.clone(),
|
||||
};
|
||||
|
||||
self.namespace
|
||||
.emit_cached(MockEvents::ModeState(mode_state_event.build()));
|
||||
|
||||
// Update last emitted values
|
||||
self.last_emitted_mode = Some(current_mode);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the frequency of the sine wave
|
||||
pub fn set_frequency(&mut self, frequency_mhz: f64) {
|
||||
self.frequency = Frequency::new::<millihertz>(frequency_mhz);
|
||||
// Emit state change immediately
|
||||
self.emit_sine_wave_state();
|
||||
}
|
||||
|
||||
/// Set the mode of the mock machine
|
||||
pub fn set_mode(&mut self, mode: Mode) {
|
||||
self.mode = mode;
|
||||
// Emit state change immediately
|
||||
self.emit_mode_state();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use super::{
|
||||
MockMachine,
|
||||
api::{MockMachineNamespace, Mode},
|
||||
};
|
||||
use anyhow::Error;
|
||||
use control_core::machines::new::{MachineNewHardware, MachineNewTrait};
|
||||
use uom::si::{f64::Frequency, frequency::hertz};
|
||||
|
||||
impl MachineNewTrait for MockMachine {
|
||||
fn new<'maindevice, 'subdevices>(
|
||||
params: &control_core::machines::new::MachineNewParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'_,
|
||||
'_,
|
||||
'_,
|
||||
'_,
|
||||
'_,
|
||||
>,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// Mock machine can work with either Serial or Ethercat hardware
|
||||
// For the mock machine, we don't need to actually use the hardware
|
||||
// We just validate that we have the expected hardware type
|
||||
match params.hardware {
|
||||
MachineNewHardware::Serial(_) => {
|
||||
// For serial mode, we could potentially use the serial device if needed
|
||||
// but for a mock machine, we'll just note it and proceed
|
||||
}
|
||||
MachineNewHardware::Ethercat(_) => {
|
||||
// For ethercat mode, we could potentially use the ethercat devices
|
||||
// but for a mock machine, we'll just note it and proceed
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
Ok(Self {
|
||||
namespace: MockMachineNamespace::new(),
|
||||
last_measurement_emit: now,
|
||||
t_0: now, // Initialize start time to current time
|
||||
frequency: Frequency::new::<hertz>(0.5), // Default frequency of 500 mHz
|
||||
mode: Mode::Standby, // Start in standby mode
|
||||
last_emitted_frequency: None, // No previous emissions
|
||||
last_emitted_mode: None, // No previous emissions
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod dre;
|
||||
pub mod extruder1;
|
||||
pub mod mock;
|
||||
pub mod registry;
|
||||
pub mod winder2;
|
||||
|
||||
@@ -7,3 +8,4 @@ pub const VENDOR_QITECH: u16 = 0x0001;
|
||||
pub const MACHINE_WINDER_V1: u16 = 0x0002;
|
||||
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
|
||||
pub const MACHINE_DRE: u16 = 0x0006;
|
||||
pub const MACHINE_MOCK: u16 = 0x0007;
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::machines::{
|
||||
MACHINE_DRE, MACHINE_EXTRUDER_V1, MACHINE_WINDER_V1, VENDOR_QITECH, dre::DreMachine,
|
||||
extruder1::ExtruderV2, winder2::Winder2,
|
||||
MACHINE_DRE, MACHINE_EXTRUDER_V1, MACHINE_MOCK, MACHINE_WINDER_V1, VENDOR_QITECH,
|
||||
dre::DreMachine, extruder1::ExtruderV2, mock::MockMachine, winder2::Winder2,
|
||||
};
|
||||
use control_core::machines::{identification::MachineIdentification, registry::MachineRegistry};
|
||||
use lazy_static::lazy_static;
|
||||
@@ -20,6 +20,10 @@ lazy_static! {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_EXTRUDER_V1,
|
||||
});
|
||||
mc.register::<MockMachine>(MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_MOCK,
|
||||
});
|
||||
mc
|
||||
};
|
||||
}
|
||||
+9
-1
@@ -3,6 +3,8 @@
|
||||
static ALLOC: dhat::Alloc = dhat::Alloc;
|
||||
|
||||
use app_state::AppState;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use mock::init::init_mock;
|
||||
use std::{panic::catch_unwind, process::exit, sync::Arc};
|
||||
|
||||
use env_logger::Env;
|
||||
@@ -16,6 +18,8 @@ pub mod app_state;
|
||||
pub mod ethercat;
|
||||
pub mod r#loop;
|
||||
pub mod machines;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
pub mod mock;
|
||||
pub mod panic;
|
||||
pub mod rest;
|
||||
pub mod serial;
|
||||
@@ -44,10 +48,14 @@ fn main2() {
|
||||
|
||||
let (thread_panic_tx, thread_panic_rx) = unbounded::<&'static str>();
|
||||
|
||||
init_serial(thread_panic_tx.clone(), app_state.clone()).expect("Failed to initialize Serial");
|
||||
init_api(thread_panic_tx.clone(), app_state.clone()).expect("Failed to initialize API");
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
init_serial(thread_panic_tx.clone(), app_state.clone()).expect("Failed to initialize Serial");
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
init_ethercat(thread_panic_tx.clone(), app_state.clone())
|
||||
.expect("Failed to initialize EtherCAT");
|
||||
#[cfg(feature = "mock-machine")]
|
||||
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
|
||||
init_loop(thread_panic_tx, app_state).expect("Failed to initialize loop");
|
||||
|
||||
smol::block_on(async {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use {
|
||||
crate::app_state::AppState,
|
||||
crate::machines::registry::MACHINE_REGISTRY,
|
||||
crate::serial::devices::mock::MockSerialDevice,
|
||||
crate::socketio::main_namespace::{MainNamespaceEvents, machines_event::MachinesEventBuilder},
|
||||
control_core::{
|
||||
serial::{SerialDeviceNew, SerialDeviceNewParams},
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
},
|
||||
std::sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
pub fn init_mock(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
log::info!("[{}::init_mock] Initializing mock machines", module_path!());
|
||||
|
||||
// For mock devices, we need to manually create and add them to the machine manager
|
||||
// since they won't be detected by the serial detection loop
|
||||
smol::block_on(async {
|
||||
// Create a mock serial device manually
|
||||
let (device_thread_panic_tx, _device_thread_panic_rx) = smol::channel::unbounded();
|
||||
let serial_params = SerialDeviceNewParams {
|
||||
path: "/dev/mock-serial".to_string(),
|
||||
device_thread_panix_tx: device_thread_panic_tx,
|
||||
};
|
||||
|
||||
// Create the mock serial device
|
||||
match MockSerialDevice::new_serial(&serial_params) {
|
||||
Ok((device_identification, mock_serial_device)) => {
|
||||
// Add the mock device to the machine manager
|
||||
{
|
||||
let mut machine_guard = app_state.machines.write().await;
|
||||
machine_guard.add_serial_device(
|
||||
&device_identification,
|
||||
mock_serial_device,
|
||||
&MACHINE_REGISTRY,
|
||||
);
|
||||
}
|
||||
|
||||
// Notify clients via socketio about the new machine
|
||||
let app_state_event = app_state.clone();
|
||||
let main_namespace = &mut app_state_event
|
||||
.socketio_setup
|
||||
.namespaces
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event = MachinesEventBuilder().build(app_state_event.clone()).await;
|
||||
main_namespace.emit_cached(MainNamespaceEvents::MachinesEvent(event));
|
||||
|
||||
log::info!(
|
||||
"[{}::init_mock] Mock machine added successfully",
|
||||
module_path!()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"[{}::init_mock] Failed to create mock serial device: {}",
|
||||
module_path!(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log::info!(
|
||||
"[{}::init_mock] Mock machines initialized successfully",
|
||||
module_path!()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod init;
|
||||
@@ -0,0 +1,59 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::{
|
||||
helpers::hashing::{hashing, xor_u128_to_u16},
|
||||
machines::identification::{
|
||||
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
|
||||
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
|
||||
},
|
||||
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
|
||||
};
|
||||
use smol::lock::RwLock;
|
||||
|
||||
use crate::machines::{MACHINE_MOCK, VENDOR_QITECH};
|
||||
|
||||
/// Mock serial device for testing MockMachine
|
||||
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
|
||||
#[derive(Debug)]
|
||||
pub struct MockSerialDevice {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl SerialDevice for MockSerialDevice {}
|
||||
|
||||
impl SerialDeviceNew for MockSerialDevice {
|
||||
fn new_serial(
|
||||
params: &SerialDeviceNewParams,
|
||||
) -> Result<(DeviceIdentification, Arc<RwLock<Self>>), anyhow::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// Generate a unique serial number based on the path
|
||||
let hash = hashing(¶ms.path);
|
||||
let serial = xor_u128_to_u16(hash);
|
||||
|
||||
let device_identification = DeviceIdentification {
|
||||
device_machine_identification: Some(DeviceMachineIdentification {
|
||||
machine_identification_unique: MachineIdentificationUnique {
|
||||
machine_identification: MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_MOCK,
|
||||
},
|
||||
serial: serial,
|
||||
},
|
||||
role: 0,
|
||||
}),
|
||||
device_hardware_identification: DeviceHardwareIdentification::Serial(
|
||||
DeviceHardwareIdentificationSerial {
|
||||
path: params.path.clone(),
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
let mock_serial_device = Arc::new(RwLock::new(MockSerialDevice {
|
||||
path: params.path.clone(),
|
||||
}));
|
||||
|
||||
Ok((device_identification, mock_serial_device))
|
||||
}
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
pub mod dre;
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
pub mod mock;
|
||||
@@ -3,6 +3,9 @@ use lazy_static::lazy_static;
|
||||
|
||||
use crate::serial::devices::dre::Dre;
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use crate::serial::devices::mock::MockSerialDevice;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SERIAL_DEVICE_REGISTRY: SerialDeviceRegistry = {
|
||||
let mut sdr = SerialDeviceRegistry::new();
|
||||
@@ -10,6 +13,14 @@ lazy_static! {
|
||||
vendor_id: 0x0403,
|
||||
product_id: 0x6001,
|
||||
});
|
||||
|
||||
// Register MockSerialDevice when mock-machine feature is enabled
|
||||
#[cfg(feature = "mock-machine")]
|
||||
sdr.register::<MockSerialDevice>(SerialDeviceIdentification {
|
||||
vendor_id: 0x0001, // VENDOR_QITECH
|
||||
product_id: 0x0007, // MACHINE_MOCK
|
||||
});
|
||||
|
||||
sdr
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user