Add Mock Machine 0x7

This commit is contained in:
Icedsoda committed 2025-05-28 16:11:38 +02:00
1 parent 2241c7f996
commit e7617d2ac2
22 files changed
+946 -7

No files matched your search

+1
View File
@@ -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
+7
View File
@@ -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);
}
+131
View File
@@ -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,
};
}
+19 -1
View File
@@ -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,
+16
View File
@@ -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]),
]),
]),
]);
+2 -2
View File
@@ -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
View File
@@ -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
+45
View File
@@ -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;
}
})
}
}
+134
View File
@@ -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
}
}
+105
View File
@@ -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();
}
}
+51
View File
@@ -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
})
}
}
+2
View File
@@ -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;
+6 -2
View File
@@ -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
View File
@@ -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 {
+72
View File
@@ -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(())
}
+1
View File
@@ -0,0 +1 @@
pub mod init;
+59
View File
@@ -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(&params.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))
}
}
+3
View File
@@ -1 +1,4 @@
pub mod dre;
#[cfg(feature = "mock-machine")]
pub mod mock;
+11
View File
@@ -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
};
}