Compare commits

...
2 Commits
Author SHA1 Message Date
jsentity c8673c2113 create frontend components 2026-02-20 04:11:18 +01:00
jsentity ba91f2266d Create backend code for ff01 mock machine 2026-02-20 03:02:59 +01:00
22 changed files with 1370 additions and 1 deletions

No files matched your search

@@ -0,0 +1,113 @@
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 "./use";
import { SelectionGroup } from "@/control/SelectionGroup";
import { Mode } from "./namespace";
import { TimeSeries } from "@/lib/timeseries";
type SineWaveCardProps = {
title: string;
timeseries: TimeSeries;
};
function SineWaveCard({ title, timeseries }: SineWaveCardProps) {
return (
<ControlCard title={title}>
<TimeSeriesValueNumeric
label="Current Value"
timeseries={timeseries}
renderValue={(value) => value.toFixed(3)}
/>
</ControlCard>
);
}
export function Mock1ControlPage() {
const {
state,
defaultState,
sineWave1,
sineWave2,
sineWave3,
sineWaveSum,
setFrequency1,
setFrequency2,
setFrequency3,
setMode,
isDisabled,
} = useMock1();
// Controlled local states synced with consolidated state
const frequency1 = state?.frequency1 ?? 1.0;
const frequency2 = state?.frequency2 ?? 2.0;
const frequency3 = state?.frequency3 ?? 5.0;
const mode = state?.mode_state?.mode ?? "Standby";
return (
<Page>
<ControlGrid columns={2}>
<SineWaveCard title="Sine Wave Sum" timeseries={sineWaveSum} />
<SineWaveCard title="Sine Wave 1" timeseries={sineWave1} />
<SineWaveCard title="Sine Wave 2" timeseries={sineWave2} />
<SineWaveCard title="Sine Wave 3" timeseries={sineWave3} />
<ControlCard title="Frequency">
<div className="flex flex-row gap-2">
<EditValue
title="Frequency 1"
unit="mHz"
value={frequency1}
defaultValue={defaultState?.frequency1}
min={0.0}
max={1000}
step={0.1}
renderValue={(value) => value.toFixed(0)}
onChange={setFrequency1}
/>
<EditValue
title="Frequency 2"
unit="mHz"
value={frequency2}
defaultValue={defaultState?.frequency2}
min={0.0}
max={1000}
step={0.1}
renderValue={(value) => value.toFixed(0)}
onChange={setFrequency2}
/>
<EditValue
title="Frequency 3"
unit="mHz"
value={frequency3}
defaultValue={defaultState?.frequency3}
min={0.0}
max={1000}
step={0.1}
renderValue={(value) => value.toFixed(0)}
onChange={setFrequency3}
/>
</div>
</ControlCard>
<ControlCard title="Mode">
<div className="flex flex-col gap-2">
<div className="text-sm font-medium">Mode</div>
<SelectionGroup
value={mode}
onChange={(newMode: Mode) => setMode(newMode)}
disabled={isDisabled}
options={{
Standby: { children: "Standby" },
Running: { children: "Running" },
}}
/>
</div>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,38 @@
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",
},
{
link: "graph",
activeLink: "graph",
title: "Graph",
icon: "lu:CirclePlay",
},
{
link: "manual",
activeLink: "manual",
title: "Manual",
icon: "lu:BookOpen",
},
{
link: "presets",
activeLink: "presets",
title: "Presets",
icon: "lu:Save",
},
]}
/>
);
}
@@ -0,0 +1,71 @@
import React from "react";
import { useMock1 } from "./use";
import { mock1 } from "@/machines/properties";
import { PresetsPage } from "@/components/preset/PresetsPage";
import { Preset } from "@/lib/preset/preset";
import { PresetPreviewEntries } from "@/components/preset/PresetPreviewTable";
import { z } from "zod";
const mock1PresetDataSchema = z
.object({
frequency1: z.number(),
frequency2: z.number(),
frequency3: z.number(),
})
.partial();
type Mock1 = z.infer<typeof mock1PresetDataSchema>;
const schemas = new Map([[1, mock1PresetDataSchema]]);
const previewEntries: PresetPreviewEntries<Mock1> = [
{
name: "Frequency 1",
unit: "mHz",
renderValue: (data: Mock1) => data?.frequency1?.toFixed(3),
},
{
name: "Frequency 2",
unit: "mHz",
renderValue: (data: Mock1) => data?.frequency2?.toFixed(3),
},
{
name: "Frequency 3",
unit: "mHz",
renderValue: (data: Mock1) => data?.frequency3?.toFixed(3),
},
];
export function Mock1PresetsPage() {
const { setFrequency1, setFrequency2, setFrequency3, defaultState, state } =
useMock1();
const applyPreset = (preset: Preset<Mock1>) => {
const frequency1 = preset.data?.frequency1 ?? 100;
const frequency2 = preset.data?.frequency2 ?? 200;
const frequency3 = preset.data?.frequency3 ?? 500;
setFrequency1(frequency1);
setFrequency2(frequency2);
setFrequency3(frequency3);
};
const toPresetData = (s: typeof state): Mock1 => ({
frequency1: s?.frequency1 ?? defaultState?.frequency1,
frequency2: s?.frequency2 ?? defaultState?.frequency2,
frequency3: s?.frequency3 ?? defaultState?.frequency3,
});
return (
<PresetsPage
machine_identification={mock1.machine_identification}
currentState={toPresetData(state)}
schemas={schemas}
schemaVersion={1}
applyPreset={applyPreset}
previewEntries={previewEntries}
defaultState={toPresetData(defaultState)}
/>
);
}
@@ -0,0 +1,190 @@
/**
* @file mock1Namespace.ts
* @description TypeScript implementation of Mock1 namespace with Zod schema validation.
*/
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
handleUnhandledEventError,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "../../../../client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import {
createTimeSeries,
TimeSeries,
TimeSeriesValue,
} from "@/lib/timeseries";
// ========== Event Schema Definitions ==========
/**
* Mode enum for Mock Machine
*/
export const modeSchema = z.enum(["Standby", "Running"]);
/**
* Mode state schema
*/
export const modeStateSchema = z.object({
mode: modeSchema,
});
/**
* Live values event schema (time-series data)
*/
export const liveValuesEventDataSchema = z.object({
amplitude_sum: z.number(),
amplitude1: z.number(),
amplitude2: z.number(),
amplitude3: z.number(),
});
/**
* State event schema (consolidated state)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
frequency1: z.number(),
frequency2: z.number(),
frequency3: z.number(),
mode_state: modeStateSchema,
});
// ========== Event Schemas with Wrappers ==========
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export const stateEventSchema = eventSchema(stateEventDataSchema);
// ========== Type Inferences ==========
export type Mode = z.infer<typeof modeSchema>;
export type ModeState = z.infer<typeof modeStateSchema>;
export type LiveValuesEvent = z.infer<typeof liveValuesEventSchema>;
export type StateEvent = z.infer<typeof stateEventSchema>;
export type Mock1NamespaceStore = {
// Single state event from server
state: StateEvent | null;
defaultState: StateEvent | null;
// Time series data for live values
sineWaveSum: TimeSeries;
sineWave1: TimeSeries;
sineWave2: TimeSeries;
sineWave3: TimeSeries;
};
const { initialTimeSeries: sineWave, insert: addSineWave } = createTimeSeries();
/**
* Factory function to create a new Mock1 namespace store
* @returns A new Zustand store instance for Mock1 namespace
*/
export const createMock1NamespaceStore = (): StoreApi<Mock1NamespaceStore> => {
return create<Mock1NamespaceStore>(() => {
return {
state: null,
defaultState: null,
sineWaveSum: sineWave,
sineWave1: sineWave,
sineWave2: sineWave,
sineWave3: 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
* @param throttledUpdater Throttled updater for batching updates at 30 FPS
* @returns A message handler function
*/
export function mock1MessageHandler(
store: StoreApi<Mock1NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Mock1NamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const eventName = event.name;
// Helper function to update store through buffer
const updateStore = (
updater: (state: Mock1NamespaceStore) => Mock1NamespaceStore,
) => {
throttledUpdater.updateWith(updater);
};
try {
// State events (latest only)
if (eventName === "StateEvent") {
const stateEvent = stateEventSchema.parse(event);
console.log("StateEvent", stateEvent);
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
}
// Live values events (time-series data)
else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
const wave1Value: TimeSeriesValue = {
value: liveValuesEvent.data.amplitude1 ?? 0,
timestamp: liveValuesEvent.ts,
};
const wave2Value: TimeSeriesValue = {
value: liveValuesEvent.data.amplitude2 ?? 0,
timestamp: liveValuesEvent.ts,
};
const wave3Value: TimeSeriesValue = {
value: liveValuesEvent.data.amplitude3 ?? 0,
timestamp: liveValuesEvent.ts,
};
const waveSumValue: TimeSeriesValue = {
value: liveValuesEvent.data.amplitude_sum ?? 0,
timestamp: liveValuesEvent.ts,
};
updateStore((state) => ({
...state,
sineWaveSum: addSineWave(state.sineWaveSum, waveSumValue),
sineWave1: addSineWave(state.sineWave1, wave1Value),
sineWave2: addSineWave(state.sineWave2, wave2Value),
sineWave3: addSineWave(state.sineWave3, wave3Value),
}));
} else {
handleUnhandledEventError(eventName);
}
} catch (error) {
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: NamespaceId = {
type: "machine",
machine_identification_unique,
};
// Use the implementation with validated namespace ID
return useMock1NamespaceImplementation(namespaceId);
}
@@ -0,0 +1,172 @@
import { toastError } from "@/components/Toast";
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
import { MachineIdentificationUnique } from "@/machines/types";
import { mock1 } from "@/machines/properties";
import { mock1SerialRoute } from "@/routes/routes";
import { z } from "zod";
import { useMock1Namespace, Mode, StateEvent } from "./namespace";
import { useEffect, useMemo } from "react";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { produce } from "immer";
function useMock(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, sineWaveSum, sineWave1, sineWave2, sineWave3 } =
useMock1Namespace(machine_identification_unique);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) {
stateOptimistic.setReal(state);
}
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest: () => Promise<any>,
) => {
const currentState = stateOptimistic.value;
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest()
.then((response) => {
if (!response.success) stateOptimistic.resetToReal();
})
.catch(() => stateOptimistic.resetToReal());
};
// Action functions with verb-first names
const setSchemaFrequency1 = z.object({ SetFrequency1: z.number() });
const { request: requestSetFrequency1 } =
useMachineMutation(setSchemaFrequency1);
const setFrequency1 = (frequency: number) => {
updateStateOptimistically(
(current) => {
current.data.frequency1 = frequency;
},
() =>
requestSetFrequency1({
machine_identification_unique,
data: {
SetFrequency1: frequency,
},
}),
);
};
const setSchemaFrequency2 = z.object({ SetFrequency2: z.number() });
const { request: requestSetFrequency2 } =
useMachineMutation(setSchemaFrequency2);
const setFrequency2 = (frequency: number) => {
updateStateOptimistically(
(current) => {
current.data.frequency2 = frequency;
},
() =>
requestSetFrequency2({
machine_identification_unique,
data: {
SetFrequency2: frequency,
},
}),
);
};
const setSchemaFrequency3 = z.object({ SetFrequency3: z.number() });
const { request: requestSetFrequency3 } =
useMachineMutation(setSchemaFrequency3);
const setFrequency3 = (frequency: number) => {
updateStateOptimistically(
(current) => {
current.data.frequency3 = frequency;
},
() =>
requestSetFrequency3({
machine_identification_unique,
data: {
SetFrequency3: frequency,
},
}),
);
};
const schemaSetMode = z.object({ SetMode: z.enum(["Standby", "Running"]) });
const { request: requestSetMode } = useMachineMutation(schemaSetMode);
const setMode = (mode: Mode) => {
updateStateOptimistically(
(current) => {
current.data.mode_state.mode = mode;
},
() =>
requestSetMode({
machine_identification_unique,
data: {
SetMode: mode,
},
}),
);
};
return {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
// Individual live values (TimeSeries)
sineWave1,
sineWave2,
sineWave3,
sineWaveSum,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
// Action functions (verb-first)
setFrequency1,
setFrequency2,
setFrequency3,
setMode,
};
}
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,
};
}
@@ -0,0 +1,20 @@
import { VENDOR_QITECH } from "../../machines/properties";
import { MachineProperties } from "../../machines/types";
export const ff01_mock: MachineProperties =
{
name: "FF01",
version: "V1",
slug: "stahlwerk_ff01",
icon: "lu:FlaskConical",
machine_identification:
{
vendor: VENDOR_QITECH,
machine: 0x0007,
},
device_roles: [],
};
export const machineProperties: MachineProperties[] = [
ff01_mock
];
@@ -0,0 +1,34 @@
import { createRoute, Outlet } from "@tanstack/react-router";
import React from "react";
import { z } from "zod";
import { Laser1ControlPage } from "@/extensions/stahlwerk/machines/ff01_mock/Page";
import { Laser1GraphsPage } from "@/machines/laser/laser1/Laser1Graph";
import { Laser1Page } from "@/machines/laser/laser1/Laser1Page";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
import { machinesRoute } from "@/routes/routes";
export const mock1SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "mock1/$serial",
component: () => <Mock1Page />,
});
export const mock1ControlRoute = createRoute({
getParentRoute: () => mock1SerialRoute,
path: "control",
component: () => <Mock1ControlPage />,
});
export const mock1GraphRoute = createRoute({
getParentRoute: () => mock1SerialRoute,
path: "graph",
component: () => <Mock1GraphPage />,
});
export const mock1ManualRoute = createRoute({
getParentRoute: () => mock1SerialRoute,
path: "manual",
component: () => <Mock1ManualPage />,
});
+5
View File
@@ -5,6 +5,10 @@ import {
machineIdentificationEquals,
} from "./types";
import {
machineProperties as stahlwerkMachineProperties
} from "../extensions/stahlwerk/properties";
export const VENDOR_QITECH = 0x0001;
export type VendorProperties = {
@@ -708,6 +712,7 @@ export const machineProperties: MachineProperties[] = [
wagoDoTestMachine,
TestMotor,
testmachinestepper,
...stahlwerkMachineProperties
];
export const getMachineProperties = (
+2
View File
@@ -39,6 +39,8 @@ pub mod wago_do_test_machine;
pub mod wago_power;
pub mod winder2;
pub mod stahlwerk;
pub const VENDOR_QITECH: u16 = 0x0001;
pub const MACHINE_WINDER_V1: u16 = 0x0002;
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
+8 -1
View File
@@ -49,7 +49,13 @@ impl MachineIdentification {
}
}
pub fn slug(&self) -> String {
pub fn slug(&self) -> String
{
if let Some(slug) = stahlwerk::get_slug(self.machine)
{
return slug.to_string();
}
match self.machine {
x if x == MACHINE_WINDER_V1 => "winder_v1".to_string(),
x if x == MACHINE_EXTRUDER_V1 => "extruder_v1".to_string(),
@@ -190,6 +196,7 @@ use crate::TEST_MACHINE;
use crate::VENDOR_QITECH;
use crate::WAGO_AI_TEST_MACHINE;
use crate::WAGO_DO_TEST_MACHINE;
use crate::stahlwerk;
#[derive(Debug)]
pub struct MachineIdentificationAddresses {
+5
View File
@@ -1,6 +1,7 @@
use crate::analog_input_test_machine::AnalogInputTestMachine;
use crate::digital_input_test_machine::DigitalInputTestMachine;
use crate::ip20_test_machine::IP20TestMachine;
use crate::stahlwerk::{self, register_machines};
use crate::wago_8ch_dio_test_machine::Wago8chDigitalIOTestMachine;
use crate::wago_ai_test_machine::WagoAiTestMachine;
use crate::wago_do_test_machine::WagoDOTestMachine;
@@ -145,6 +146,10 @@ lazy_static! {
);
mc.register::<TestMachineStepper>(TestMachineStepper::MACHINE_IDENTIFICATION);
// register all machines from stahlwerk
stahlwerk::register_machines(&mut mc);
mc
};
}
+60
View File
@@ -0,0 +1,60 @@
use std::{fmt::Debug, sync::Arc, time::Instant};
use crate::{MachineMessage, machine_identification::MachineIdentificationUnique};
use smol::channel::{Receiver, Sender};
use tracing::instrument;
use crate::AsyncThreadMessage;
use control_core::socketio::namespace::{
CacheableEvents,
Namespace,
NamespaceCacheingLogic
};
#[derive(Debug)]
pub struct BaseMachine
{
pub machine_uid: MachineIdentificationUnique,
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub namespace: NamespaceImpl,
// socketio
pub last_measurement_emit: Instant,
// api
pub api_sender: Sender<MachineMessage>,
pub api_receiver: Receiver<MachineMessage>,
pub emitted_default_state: bool,
}
#[derive(Debug)]
pub struct NamespaceImpl
{
pub namespace: Option<Namespace>,
}
impl NamespaceImpl
{
pub fn new(namespace: Option<Namespace>) -> Self
{
Self { namespace }
}
}
impl<E: CacheableEvents<E>> NamespaceCacheingLogic<E> for NamespaceImpl
{
#[instrument(skip_all)]
fn emit(&mut self, events: E)
{
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
match &mut self.namespace
{
Some(ns) => ns.emit(event, &buffer_fn),
None => (),
}
}
}
+12
View File
@@ -0,0 +1,12 @@
use std::time::Instant;
use super::DerivedMachine;
impl DerivedMachine
{
pub fn act_ext(&mut self, now: Instant)
{
_ = now;
// So quiet in here
}
}
+43
View File
@@ -0,0 +1,43 @@
use control_core_derive::BuildEvent;
use serde::{ Deserialize, Serialize };
use super::DerivedMachine;
#[derive(Serialize, Debug, Clone, Default)]
pub struct LiveValuesEvent
{
pub counter: u64,
}
#[derive(Serialize, Debug, Clone, PartialEq, BuildEvent)]
pub struct StateEvent
{
pub is_default_state: bool,
}
#[derive(Deserialize, Serialize)]
pub enum Mutation
{
Increment,
}
impl DerivedMachine
{
pub fn get_live_values(&self) -> LiveValuesEvent
{
LiveValuesEvent { counter: self.counter }
}
pub fn get_state(&self) -> StateEvent
{
StateEvent { is_default_state: true }
}
pub fn mutate(&mut self, mutation: Mutation)
{
match mutation
{
Mutation::Increment => self.increment(),
}
}
}
+217
View File
@@ -0,0 +1,217 @@
use std::time::{Duration, Instant};
// external deps
use anyhow::{Error};
use serde_json::Value;
use smol::channel::Sender;
// local deps
use control_core::socketio::{
event::{
BuildEvent,
Event,
GenericEvent
},
namespace::{
CacheFn,
CacheableEvents,
Namespace,
NamespaceCacheingLogic,
cache_first_and_last_event
}
};
use crate::{
AsyncThreadMessage,
Machine,
MachineAct,
MachineApi,
MachineMessage,
MachineNewParams,
MachineNewTrait,
MachineValues,
machine_identification::MachineIdentificationUnique,
};
use crate::stahlwerk::{
r#abstract::{
BaseMachine,
NamespaceImpl
}
};
use super::{
DerivedMachine,
api::{
LiveValuesEvent,
StateEvent,
Mutation,
}
};
impl DerivedMachine
{
/// Emit live values data event with the current sine wave amplitude
pub fn emit_live_values(&mut self)
{
let event = self.get_live_values().build();
self.base.namespace.emit(Events::LiveValues(event));
}
/// Emit the current state of the mock machine only if values have changed
pub fn emit_state(&mut self)
{
let state = self.get_state();
let event = state.build();
self.base.namespace.emit(Events::State(event));
self.base.emitted_default_state = true;
}
}
impl MachineNewTrait for DerivedMachine
{
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
where
Self: Sized
{
let (api_sender, api_receiver) = smol::channel::unbounded();
let base = BaseMachine {
machine_uid: params.get_machine_identification_unique(),
main_sender: params.main_thread_channel.clone(),
namespace: NamespaceImpl::new(params.namespace.clone()),
last_measurement_emit: Instant::now(),
api_sender,
api_receiver,
emitted_default_state: false,
};
let mut instance = Self::new(base);
instance.emit_state();
Ok(instance)
}
}
impl Machine for DerivedMachine
{
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique
{
self.base.machine_uid.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>>
{
self.base.main_sender.clone()
}
}
impl MachineApi for DerivedMachine
{
fn api_get_sender(&self) -> Sender<MachineMessage>
{
self.base.api_sender.clone()
}
fn api_event_namespace(&mut self) -> Option<Namespace>
{
self.base.namespace.namespace.clone()
}
fn api_mutate(&mut self, value: Value) -> Result<(), Error>
{
let mutation: Mutation = serde_json::from_value(value)?;
self.mutate(mutation);
Ok(())
}
}
impl MachineAct for DerivedMachine
{
fn act_machine_message(&mut self, msg: MachineMessage)
{
use MachineMessage::*;
use crate::MachineApi;
match msg
{
SubscribeNamespace(namespace) =>
{
self.base.namespace.namespace = Some(namespace);
self.emit_state();
}
UnsubscribeNamespace => self.base.namespace.namespace = None,
HttpApiJsonRequest(value) => _ = self.api_mutate(value),
ConnectToMachine(_) => {}
DisconnectMachine(_) => {}
RequestValues(sender) =>
{
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
fn act(&mut self, now: Instant)
{
let target_duration: Duration = Duration::from_secs_f64(1.0 / 30.0);
match self.base.api_receiver.try_recv()
{
Ok(msg) => _ = self.act_machine_message(msg),
Err(_) => (),
};
self.act_ext(now);
let now = Instant::now();
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.base.last_measurement_emit) > target_duration
{
self.emit_state();
self.emit_live_values();
self.base.last_measurement_emit = now;
}
}
}
impl LiveValuesEvent
{
pub fn build(&self) -> Event<Self>
{
Event::new("LiveValuesEvent", self.clone())
}
}
pub enum Events
{
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
}
impl CacheableEvents<Self> for Events
{
fn event_value(&self) -> GenericEvent
{
match self
{
Self::LiveValues(event) => event.into(),
Self::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn
{
cache_first_and_last_event()
}
}
+37
View File
@@ -0,0 +1,37 @@
use crate::stahlwerk::{ r#abstract::BaseMachine };
pub mod common;
pub mod api;
pub mod act;
pub(super) use FF01MachineMock as DerivedMachine;
#[derive(Debug)]
pub struct FF01MachineMock
{
base: BaseMachine,
counter: u64,
// todo: current processed entry HERE
}
impl FF01MachineMock
{
pub fn new(base: BaseMachine) -> Self
{
Self { base, counter: 0 }
}
pub fn increment(&mut self)
{
self.counter += 1;
self.emit_state();
}
pub fn finalize(&mut self)
{
todo!("Submit backflush")
// self.emit_state();
}
}
+20
View File
@@ -0,0 +1,20 @@
#[derive(Debug, Clone)]
pub struct MachineId
{
pub category_id: u8,
pub index: u8,
pub slug: &'static str,
}
impl MachineId
{
pub const fn new(category_id: u8, index: u8, slug: &'static str) -> Self
{
Self { category_id, index, slug }
}
pub const fn to_u16(&self) -> u16
{
((self.category_id as u16) << 8) | (self.index as u16)
}
}
+39
View File
@@ -0,0 +1,39 @@
use crate::{
VENDOR_QITECH,
machine_identification::MachineIdentification,
registry::MachineRegistry,
stahlwerk::{ machine_id::MachineId }
};
pub mod r#abstract;
pub mod template;
pub mod machine_id;
pub mod ff01_mock;
pub const CATEGORY_ID: u8 = 0x02; // stahlwerk category
pub const FF01_MOCK: MachineId = MachineId::new(CATEGORY_ID, 0, "stahlwerk_ff01");
pub const fn get_slug(id: u16) -> Option<&'static str>
{
match id
{
id if id == FF01_MOCK.to_u16() => Some(FF01_MOCK.slug),
_ => None,
}
}
pub fn register_machines(registry: &mut MachineRegistry)
{
#[cfg(feature = "mock-machine")]
registry.register::<FF01MachineMock>(new_identification(FF01_MOCK));
}
fn new_identification(id: MachineId) -> MachineIdentification
{
MachineIdentification {
vendor: VENDOR_QITECH,
machine: id.to_u16(),
}
}
+12
View File
@@ -0,0 +1,12 @@
use std::time::Instant;
use super::DerivedMachine;
impl DerivedMachine
{
pub fn act_ext(&mut self, now: Instant)
{
_ = now;
// So quiet in here
}
}
+34
View File
@@ -0,0 +1,34 @@
use control_core_derive::BuildEvent;
use serde::{ Deserialize, Serialize };
use super::DerivedMachine;
#[derive(Serialize, Debug, Clone, Default)]
pub struct LiveValuesEvent {}
#[derive(Serialize, Debug, Clone, PartialEq, BuildEvent)]
pub struct StateEvent
{
pub is_default_state: bool,
}
#[derive(Deserialize, Serialize)]
pub enum Mutation {}
impl DerivedMachine
{
pub fn get_live_values(&self) -> LiveValuesEvent
{
LiveValuesEvent {}
}
pub fn get_state(&self) -> StateEvent
{
StateEvent { is_default_state: true }
}
pub fn mutate(&mut self, mutation: Mutation)
{
match mutation {}
}
}
+217
View File
@@ -0,0 +1,217 @@
use std::time::{Duration, Instant};
// external deps
use anyhow::{Error};
use serde_json::Value;
use smol::channel::Sender;
// local deps
use control_core::socketio::{
event::{
BuildEvent,
Event,
GenericEvent
},
namespace::{
CacheFn,
CacheableEvents,
Namespace,
NamespaceCacheingLogic,
cache_first_and_last_event
}
};
use crate::{
AsyncThreadMessage,
Machine,
MachineAct,
MachineApi,
MachineMessage,
MachineNewParams,
MachineNewTrait,
MachineValues,
machine_identification::MachineIdentificationUnique,
};
use crate::stahlwerk::{
r#abstract::{
BaseMachine,
NamespaceImpl
}
};
use super::{
DerivedMachine,
api::{
LiveValuesEvent,
StateEvent,
Mutation,
}
};
impl DerivedMachine
{
/// Emit live values data event with the current sine wave amplitude
pub fn emit_live_values(&mut self)
{
let event = self.get_live_values().build();
self.base.namespace.emit(Events::LiveValues(event));
}
/// Emit the current state of the mock machine only if values have changed
pub fn emit_state(&mut self)
{
let state = self.get_state();
let event = state.build();
self.base.namespace.emit(Events::State(event));
self.base.emitted_default_state = true;
}
}
impl MachineNewTrait for DerivedMachine
{
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
where
Self: Sized
{
let (api_sender, api_receiver) = smol::channel::unbounded();
let base = BaseMachine {
machine_uid: params.get_machine_identification_unique(),
main_sender: params.main_thread_channel.clone(),
namespace: NamespaceImpl::new(params.namespace.clone()),
last_measurement_emit: Instant::now(),
api_sender,
api_receiver,
emitted_default_state: false,
};
let mut instance = Self::new(base);
instance.emit_state();
Ok(instance)
}
}
impl Machine for DerivedMachine
{
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique
{
self.base.machine_uid.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>>
{
self.base.main_sender.clone()
}
}
impl MachineApi for DerivedMachine
{
fn api_get_sender(&self) -> Sender<MachineMessage>
{
self.base.api_sender.clone()
}
fn api_event_namespace(&mut self) -> Option<Namespace>
{
self.base.namespace.namespace.clone()
}
fn api_mutate(&mut self, value: Value) -> Result<(), Error>
{
let mutation: Mutation = serde_json::from_value(value)?;
self.mutate(mutation);
Ok(())
}
}
impl MachineAct for DerivedMachine
{
fn act_machine_message(&mut self, msg: MachineMessage)
{
use MachineMessage::*;
use crate::MachineApi;
match msg
{
SubscribeNamespace(namespace) =>
{
self.base.namespace.namespace = Some(namespace);
self.emit_state();
}
UnsubscribeNamespace => self.base.namespace.namespace = None,
HttpApiJsonRequest(value) => _ = self.api_mutate(value),
ConnectToMachine(_) => {}
DisconnectMachine(_) => {}
RequestValues(sender) =>
{
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::to_value(self.get_live_values())
.expect("Failed to serialize live values"),
})
.expect("Failed to send values");
sender.close();
}
}
}
fn act(&mut self, now: Instant)
{
let target_duration: Duration = Duration::from_secs_f64(1.0 / 30.0);
match self.base.api_receiver.try_recv()
{
Ok(msg) => _ = self.act_machine_message(msg),
Err(_) => (),
};
self.act_ext(now);
let now = Instant::now();
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.base.last_measurement_emit) > target_duration
{
self.emit_state();
self.emit_live_values();
self.base.last_measurement_emit = now;
}
}
}
impl LiveValuesEvent
{
pub fn build(&self) -> Event<Self>
{
Event::new("LiveValuesEvent", self.clone())
}
}
pub enum Events
{
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
}
impl CacheableEvents<Self> for Events
{
fn event_value(&self) -> GenericEvent
{
match self
{
Self::LiveValues(event) => event.into(),
Self::State(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn
{
cache_first_and_last_event()
}
}
+21
View File
@@ -0,0 +1,21 @@
use crate::stahlwerk::{ r#abstract::BaseMachine };
pub mod common;
pub mod api;
pub mod act;
pub(super) use TemplateMachine as DerivedMachine;
#[derive(Debug)]
pub struct TemplateMachine
{
base: BaseMachine,
}
impl TemplateMachine
{
pub fn new(base: BaseMachine) -> Self
{
Self { base }
}
}