Compare commits

...
98 changed files with 1310 additions and 92 deletions

No files matched your search

@@ -0,0 +1,107 @@
// ============================================================================
// MyMachineControlPage.tsx — Control UI for this machine
// ============================================================================
// This component is the main control panel rendered under the "Control" tab.
// It reads machine state from the hook and dispatches mutations on user action.
//
// Layout components:
// Page — full-height scrollable container
// ControlGrid — responsive column grid (columns prop)
// ControlCard — card with a title header
// Label — labelled wrapper for a single control
//
// Common control components (import as needed):
// SelectionGroup — radio-style button group
// Slider — numeric range input
// Toggle — on/off switch
// NumericInput — free-form number input
//
// FIND & REPLACE to adapt this template:
// MyMachine → YourMachineName
// myMachine → yourMachineName
// useMyMachine → useYourMachineName
// ============================================================================
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { ControlGrid } from "@/control/ControlGrid";
import React from "react";
import { useMyMachine } from "./useMyMachine";
export function MyMachineControlPage() {
// Pull state and mutation functions from the hook.
// Add your mutation functions to the destructuring as you implement them.
const { state } = useMyMachine();
// Provide a safe default so the UI renders before the first server event.
// TODO: replace with the actual shape of your StateEvent.
const safeState =
state ??
{
// e.g. outputs: [false, false, false, false],
// value: 0,
};
return (
<Page>
<ControlGrid columns={2}>
{/* ------------------------------------------------------------------ */}
{/* TODO: replace the example card below with your real controls. */}
{/* */}
{/* Example: four digital outputs */}
{/* ------------------------------------------------------------------ */}
<ControlCard title="Outputs">
<p className="text-muted-foreground text-sm">
{/* Replace this placeholder with real controls, e.g.: */}
</p>
{/* Example — toggle individual outputs:
<div className="grid grid-cols-2 gap-6">
{safeState.outputs.map((on, index) => (
<Label key={index} label={`Output ${index + 1}`}>
<SelectionGroup<"On" | "Off">
value={on ? "On" : "Off"}
orientation="vertical"
className="flex flex-col gap-3"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-red-600",
className: "flex-1",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "flex-1",
},
}}
onChange={(value) => setOutput(index, value === "On")}
/>
</Label>
))}
</div>
*/}
</ControlCard>
{/* ------------------------------------------------------------------ */}
{/* Example: master control card */}
{/* ------------------------------------------------------------------ */}
{/* <ControlCard title="Master Control">
<SelectionGroup<"On" | "Off">
value={safeState.outputs.every(Boolean) ? "On" : "Off"}
orientation="horizontal"
options={{
Off: { children: "Turn All Off" },
On: { children: "Turn All On" },
}}
onChange={(value) => setAllOutputs(value === "On")}
/>
</ControlCard> */}
{/* TODO: add more ControlCard sections for your machine */}
</ControlGrid>
</Page>
);
}
@@ -0,0 +1,125 @@
// ============================================================================
// MyMachineNamespace.ts — WebSocket event schema, store, and namespace hook
// ============================================================================
// This file is the glue between the backend socket.io events and the React
// component tree. It has four sections:
//
// 1. Event Schema — Zod schemas that mirror the Rust `StateEvent` struct
// 2. Store — Zustand store holding the latest parsed state
// 3. Message Handler — maps incoming event names to store updates
// 4. Namespace Hook — the public hook consumed by useMyMachine.ts
//
// FIND & REPLACE to adapt this template:
// MyMachine → YourMachineName (e.g. WagoDiMachine)
// myMachine → yourMachineName (camelCase, e.g. wagoDiMachine)
// myMachineSerialRoute → the route export from routes.tsx
// ============================================================================
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";
// ============================================================================
// 1. Event Schema
//
// Mirror every field from the Rust `StateEvent` struct in api.rs exactly.
// Field names must be snake_case to match the JSON Rust serialises.
//
// Examples:
// z.boolean() — bool
// z.number() — f32 / f64 / u16 / i32 …
// z.array(z.boolean()).length(4) — [bool; 4]
// z.string() — String
// z.tuple([z.number(), z.string()]) — (f64, String)
// ============================================================================
export const stateEventDataSchema = z.object({
// TODO: mirror your Rust StateEvent fields here, e.g.:
// outputs: z.array(z.boolean()).length(4),
// value: z.number(),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ============================================================================
// 2. Store
//
// One store per machine instance (keyed by namespace ID internally).
// `state: null` means "not yet received from backend".
// ============================================================================
export type MyMachineNamespaceStore = {
state: StateEvent | null;
};
export const createMyMachineNamespaceStore =
(): StoreApi<MyMachineNamespaceStore> =>
create<MyMachineNamespaceStore>(() => ({
state: null,
}));
// ============================================================================
// 3. Message Handler
//
// Called for every socket.io event on this machine's namespace.
// Add a branch for each event name your backend emits.
//
// The most common pattern (single "StateEvent") is shown below.
// For machines with multiple event types, add more `if` branches.
// ============================================================================
export function myMachineMessageHandler(
store: StoreApi<MyMachineNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<MyMachineNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (state: MyMachineNamespaceStore) => MyMachineNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
// TODO: if your Rust api.rs emits a different event name, change it here.
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ============================================================================
// 4. Namespace Hook
//
// Returns the store for a given machine instance.
// Used by useMyMachine.ts — do not call this directly from components.
// ============================================================================
const useMyMachineNamespaceImplementation =
createNamespaceHookImplementation<MyMachineNamespaceStore>({
createStore: createMyMachineNamespaceStore,
createEventHandler: myMachineMessageHandler,
});
export function useMyMachineNamespace(
machine_identification_unique: MachineIdentificationUnique,
): MyMachineNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useMyMachineNamespaceImplementation(namespaceId);
}
@@ -0,0 +1,44 @@
// ============================================================================
// MyMachinePage.tsx — Top navigation bar for this machine
// ============================================================================
// This component renders the tab bar at the top of the machine page.
// Each `items` entry creates one tab that links to a sub-route.
//
// FIND & REPLACE to adapt this template:
// MyMachine → YourMachineName
// myMachine → yourMachineName
// myMachineSerialRoute → the route export from routes.tsx (see step 8 in README)
// mymachine → the URL segment used in routes.tsx (e.g. "wagodotestmachine")
// ============================================================================
import { Topbar } from "@/components/Topbar";
// TODO: Import the actual serial route.
// import { myMachineSerialRoute } from "@/routes/routes";
import React from "react";
export function MyMachinePage() {
// TODO: Use the serial route.
// const { serial } = myMachineSerialRoute.useParams();
return (
<Topbar
// The pathname must match the route defined in routes.tsx. TODO: use actual serial
pathname={`/_sidebar/machines/mymachine/${"REPLACEME"}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
// TODO: add more tabs here if your machine needs multiple views, e.g.:
// {
// link: "diagnostics",
// activeLink: "diagnostics",
// title: "Diagnostics",
// icon: "lu:Activity",
// },
]}
/>
);
}
@@ -0,0 +1,195 @@
# Minimal Machine Frontend Boilerplate
This directory is a copy-paste template for creating the React/TypeScript frontend
for a new minimal machine. Follow the steps below in order. Each file has inline
`// TODO:` comments that mark every place you need to edit.
Pair this with the backend `BOILERPLATE_TEMPLATE` in
`machines/src/minimal_machines/BOILERPLATE_TEMPLATE/`.
---
## Quick-start checklist
### 1 — Copy this directory
```
cp -r BOILERPLATE_TEMPLATE MyNewMachine
```
### 2 — Rename throughout the template
Do a project-wide find & replace inside your new directory:
| Find | Replace with |
|---------------------------|-------------------------------------------|
| `MyMachine` | `YourMachineName` (PascalCase) |
| `myMachine` | `yourMachineName` (camelCase) |
| `myMachineSerialRoute` | the route export you add in step 8 |
| `mymachine` | the URL segment you choose in step 8 |
### 3 — Fill in `MyMachineNamespace.ts` — event schema
Open `MyMachineNamespace.ts` and add fields to `stateEventDataSchema` that
mirror your Rust `StateEvent` struct in `api.rs`:
```typescript
export const stateEventDataSchema = z.object({
outputs: z.array(z.boolean()).length(4),
value: z.number(),
});
```
### 4 — Fill in `useMyMachine.ts` — mutations
Open `useMyMachine.ts` and uncomment / add mutation functions for every
`Mutation` variant in your Rust `api.rs`. Each mutation should:
1. Call `updateStateOptimistically` with an immer producer to update local state instantly
2. Call `sendMutation` with `{ action: "VariantName", value: { ... } }` to inform the backend
```typescript
const setOutput = (index: number, on: boolean) => {
updateStateOptimistically(
(current) => { current.outputs[index] = on; },
() => sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetOutput", value: { index, on } },
}),
);
};
```
Then expose the function in the return object at the bottom of the hook.
### 5 — Fill in `MyMachineControlPage.tsx` — UI
Open `MyMachineControlPage.tsx` and:
- Update `safeState` default to match your `StateEvent` shape
- Replace the placeholder `ControlCard` with real controls
- Destructure your mutation functions from `useMyMachine()`
Common control components:
| Component | Use for |
|------------------|--------------------------------------|
| `SelectionGroup` | Radio-style on/off or multi-option |
| `Label` | Adds a text label to any control |
| `ControlCard` | Groups related controls in a card |
| `ControlGrid` | Arranges cards in a responsive grid |
| `Slider` | Numeric range input |
| `Toggle` | Simple boolean toggle switch |
| `NumericInput` | Free-form number input |
### 6 — Fill in `MyMachinePage.tsx` — tabs
Update the `pathname` and `items` array if you add more tabs beyond "Control".
Usually this file needs no changes for simple machines.
### 7 — Add to `properties.ts`
Open `electron/src/machines/properties.ts` and add an entry for your machine:
```typescript
export const myMachine: MachineProperties = {
machine_identification: {
vendor: VENDOR_QITECH,
machine: MY_MACHINE_ID, // must match the Rust constant
},
name: "My Machine",
slug: "mymachine",
device_roles: [
{ role: 1, description: "EL2004 — 4× digital output" },
],
};
```
### 8 — Add to `routes.tsx`
Open `electron/src/routes/routes.tsx`.
**a) Import your page components** (at the top of the file):
```typescript
import { MyMachinePage } from "@/machines/minimal_machines/MyNewMachine/MyMachinePage";
import { MyMachineControlPage } from "@/machines/minimal_machines/MyNewMachine/MyMachineControlPage";
```
**b) Create a serial route** (follow the existing pattern):
```typescript
export const myMachineSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "mymachine/$serial",
component: MyMachinePage,
});
```
**c) Create a control sub-route**:
```typescript
const myMachineControlRoute = createRoute({
getParentRoute: () => myMachineSerialRoute,
path: "control",
component: MyMachineControlPage,
});
```
**d) Wire up children**:
```typescript
const myMachineRoutes = myMachineSerialRoute.addChildren([myMachineControlRoute]);
```
**e) Add to `routeTree`** — include `myMachineRoutes` in the top-level `addChildren` call.
### 9 — Verify the build
```
npm run typecheck # or: tsc --noEmit
```
---
## File overview
| File | Responsibility |
|----------------------------|----------------------------------------------------------|
| `MyMachineNamespace.ts` | Zod schema, Zustand store, socket.io event handler, hook |
| `useMyMachine.ts` | Route params, machine ID, state subscription, mutations |
| `MyMachinePage.tsx` | Topbar navigation tabs |
| `MyMachineControlPage.tsx` | Control UI — cards, buttons, sliders |
## Data flow
```
Backend (Rust)
│ socket.io "StateEvent"
▼
MyMachineNamespace.ts
myMachineMessageHandler() ──► Zustand store
│
▼
useMyMachine.ts
useMyMachineNamespace() ──► real state
useStateOptimistic() ──► optimistic state (instant UI)
useMachineMutate() ──► sends JSON to backend api_mutate
│
▼
MyMachineControlPage.tsx
reads: state (optimistic)
writes: mutation functions
```
## Naming conventions
| Concept | Convention | Example |
|---------------------|---------------|---------------------------------|
| Namespace file | `PascalCase` | `MyMachineNamespace.ts` |
| Hook file | `camelCase` | `useMyMachine.ts` |
| Page component | `PascalCase` | `MyMachinePage.tsx` |
| Control page | `PascalCase` | `MyMachineControlPage.tsx` |
| Directory | `lowercase` | `mymachine/` |
| Route serial export | `camelCase` | `myMachineSerialRoute` |
| Properties export | `camelCase` | `myMachine` |
@@ -0,0 +1,150 @@
// ============================================================================
// useMyMachine.ts — The primary React hook for this machine
// ============================================================================
// This hook is the single point of contact for components. It:
// 1. Reads the serial number from the route params
// 2. Builds the MachineIdentificationUnique
// 3. Subscribes to backend state via the namespace hook
// 4. Applies optimistic updates so the UI feels instant
// 5. Sends mutations to the backend
//
// Components should only import from this file, not from MyMachineNamespace.ts
//
// FIND & REPLACE to adapt this template:
// MyMachine → YourMachineName
// myMachine → yourMachineName
// myMachineSerialRoute → the route export from routes.tsx (see step 8 in README)
// myMachine (properties)→ the export from properties.ts (see step 7 in README)
// ============================================================================
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
// TODO: Import the actual serial route.
//import { myMachineSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import { useMyMachineNamespace, StateEvent } from "./MyMachineNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
// TODO: Import the actual properties.
//import { myMachine } from "@/machines/properties";
import { z } from "zod";
export function useMyMachine() {
// --------------------------------------------------------------------------
// 1. Route params — serial comes from the URL (e.g. /machines/mymachine/42)
// --------------------------------------------------------------------------
// TODO: use myMachineSerialRoute.useParams();
const { serial: serialString } = { serial: "REPLACEME" };
// --------------------------------------------------------------------------
// 2. Build MachineIdentificationUnique
// vendor + machine ID uniquely identify the machine type;
// serial distinguishes multiple instances of the same type.
// --------------------------------------------------------------------------
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
//TODO: use the actual myMachine.machine_identification,
machine_identification: {
vendor: Number.NaN,
machine: Number.NaN,
},
serial,
};
}, [serialString]);
// --------------------------------------------------------------------------
// 3. Subscribe to backend state via WebSocket namespace
// --------------------------------------------------------------------------
const { state } = useMyMachineNamespace(machineIdentification);
// --------------------------------------------------------------------------
// 4. Optimistic state — mirrors real state but allows instant local updates.
// The UI reads `stateOptimistic.value` instead of raw `state`.
// --------------------------------------------------------------------------
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// --------------------------------------------------------------------------
// 5. Mutation sender — sends JSON to the backend api_mutate handler.
// The schema here validates what we send (use z.any() for flexibility).
// --------------------------------------------------------------------------
const { request: sendMutation } = useMachineMutate(
z.object({
action: z.string(),
value: z.any(),
}),
);
// --------------------------------------------------------------------------
// Helper: apply an optimistic update locally and optionally fire a request.
// --------------------------------------------------------------------------
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
// --------------------------------------------------------------------------
// 6. Mutation actions — one function per Rust `Mutation` variant.
//
// The `action` string and `value` shape must exactly match the
// `#[serde(tag = "action", content = "value")]` enum in api.rs.
//
// TODO: replace/extend these examples with your actual mutations.
// For read-only machines, delete all mutation functions and the
// sendMutation declaration above.
// --------------------------------------------------------------------------
// Example: toggle a single output
// const setOutput = (index: number, on: boolean) => {
// updateStateOptimistically(
// (current) => { current.outputs[index] = on; },
// () => sendMutation({
// machine_identification_unique: machineIdentification,
// data: { action: "SetOutput", value: { index, on } },
// }),
// );
// };
// Example: set all outputs at once
// const setAllOutputs = (on: boolean) => {
// updateStateOptimistically(
// (current) => { current.outputs = current.outputs.map(() => on); },
// () => sendMutation({
// machine_identification_unique: machineIdentification,
// data: { action: "SetAllOutputs", value: { on } },
// }),
// );
// };
// --------------------------------------------------------------------------
// Return everything the components need.
// --------------------------------------------------------------------------
return {
state: stateOptimistic.value,
// TODO: expose your mutation functions here, e.g.:
// setOutput,
// setAllOutputs,
};
}
@@ -12,9 +12,9 @@ import {
useAnalogInputTestMachineNamespace,
} from "./useAnalogInputTestMachineNamespace";
import { toastError } from "@/components/Toast";
import { MachineIdentificationUnique } from "../types";
import { MachineIdentificationUnique } from "../../types";
import { analogInputTestMachineSerialRoute } from "@/routes/routes";
import { analogInputTestMachine } from "../properties";
import { analogInputTestMachine } from "../../properties";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { produce } from "immer";
@@ -14,7 +14,7 @@ import {
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
} from "../../../client/socketioStore";
} from "../../../../client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import {
createTimeSeries,
@@ -12,7 +12,7 @@ import { wagoAiTestMachineSerialRoute } from "@/routes/routes";
import { produce } from "immer";
import { useEffect, useMemo, useState } from "react";
import z from "zod";
import { wagoAiTestMachine } from "../properties";
import { wagoAiTestMachine } from "../../properties";
import {
StateEvent,
useWagoAiTestMachineNamespace,
+23 -23
View File
@@ -43,44 +43,44 @@ import { Laser1GraphsPage } from "@/machines/laser/laser1/Laser1Graph";
import { Laser1Page } from "@/machines/laser/laser1/Laser1Page";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
import { Mock1ControlPage } from "@/machines/mock/mock1/Mock1ControlPage";
import { Mock1GraphPage } from "@/machines/mock/mock1/Mock1Graph";
import { Mock1ManualPage } from "@/machines/mock/mock1/Mock1Manual";
import { Mock1Page } from "@/machines/mock/mock1/Mock1Page";
import { Mock1PresetsPage } from "@/machines/mock/mock1/Mock1PresetsPage";
import { Mock1ControlPage } from "@/machines/minimal_machines/mock/mock1/Mock1ControlPage";
import { Mock1GraphPage } from "@/machines/minimal_machines/mock/mock1/Mock1Graph";
import { Mock1ManualPage } from "@/machines/minimal_machines/mock/mock1/Mock1Manual";
import { Mock1Page } from "@/machines/minimal_machines/mock/mock1/Mock1Page";
import { Mock1PresetsPage } from "@/machines/minimal_machines/mock/mock1/Mock1PresetsPage";
import { Aquapath1ControlPage } from "@/machines/aquapath/aquapath1/Aquapath1ControlPage";
import { Aquapath1Page } from "@/machines/aquapath/aquapath1/Aquapath1Page";
import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph";
import { Aquapath1SettingsPage } from "@/machines/aquapath/aquapath1/Aquapath1Settings";
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
import { TestMachinePage } from "@/machines/minimal_machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/minimal_machines/testmachine/TestMachineControlPage";
import { TestMachineStepperPage } from "@/machines/testmachinestepper/TestMachineStepperPage";
import { TestMachineStepperControlPage } from "@/machines/testmachinestepper/TestMachineStepperControlPage";
import { AnalogInputTestMachine } from "@/machines/analoginputtestmachine/AnalogInputTestMachinePage";
import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
import { WagoAiTestMachine } from "@/machines/wagoaitestmachine/WagoAiTestMachinePage";
import { WagoAiTestMachineControl } from "@/machines/wagoaitestmachine/WagoAiTestMachineControlPage";
import { TestMachineStepperPage } from "@/machines/minimal_machines/testmachinestepper/TestMachineStepperPage";
import { TestMachineStepperControlPage } from "@/machines/minimal_machines/testmachinestepper/TestMachineStepperControlPage";
import { AnalogInputTestMachine } from "@/machines/minimal_machines/analoginputtestmachine/AnalogInputTestMachinePage";
import { AnalogInputTestMachineControl } from "@/machines/minimal_machines/analoginputtestmachine/AnalogInputTestMachineControlPage";
import { WagoAiTestMachine } from "@/machines/minimal_machines/wagoaitestmachine/WagoAiTestMachinePage";
import { WagoAiTestMachineControl } from "@/machines/minimal_machines/wagoaitestmachine/WagoAiTestMachineControlPage";
import { DigitalInputTestMachinePage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachinePage";
import { DigitalInputTestMachineControlPage } from "@/machines/digitalinputtestmachine/DigitalInputTestMachineControlPage";
import { DigitalInputTestMachinePage } from "@/machines/minimal_machines/digitalinputtestmachine/DigitalInputTestMachinePage";
import { DigitalInputTestMachineControlPage } from "@/machines/minimal_machines/digitalinputtestmachine/DigitalInputTestMachineControlPage";
import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage";
import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage";
import { TestMotorPage } from "@/machines/motor_test_machine/TestMotorPage";
import { TestMotorControlPage } from "@/machines/motor_test_machine/TestMotorControlPage";
import { IP20TestMachinePage } from "@/machines/minimal_machines/ip20testmachine/IP20TestMachinePage";
import { IP20TestMachineControlPage } from "@/machines/minimal_machines/ip20testmachine/IP20TestMachineControlPage";
import { TestMotorPage } from "@/machines/minimal_machines/motor_test_machine/TestMotorPage";
import { TestMotorControlPage } from "@/machines/minimal_machines/motor_test_machine/TestMotorControlPage";
import { MetricsGraphsPage } from "@/metrics/MetricsGraphsPage";
import { MetricsControlPage } from "@/metrics/MetricsControlPage";
import { WagoPower1Page } from "@/machines/wago_power/wago_power1/WagoPower1Page";
import { WagoPower1ControlPage } from "@/machines/wago_power/wago_power1/WagoPower1ControlPage";
import { WagoDoTestMachinePage } from "@/machines/wagodotestmachine/WagoDoTestMachinePage";
import { WagoDoTestMachineControlPage } from "@/machines/wagodotestmachine/WagoDoTestMachineControlPage";
import { Wago8chDioTestMachinePage } from "@/machines/wago8chdiotestmachine/wago8chDioTestMachinePage";
import { Wago8chDioTestMachineControlRoute } from "@/machines/wago8chdiotestmachine/wago8chDioTestMachineControlPage";
import { WagoDoTestMachinePage } from "@/machines/minimal_machines/wagodotestmachine/WagoDoTestMachinePage";
import { WagoDoTestMachineControlPage } from "@/machines/minimal_machines/wagodotestmachine/WagoDoTestMachineControlPage";
import { Wago8chDioTestMachinePage } from "@/machines/minimal_machines/wago8chdiotestmachine/wago8chDioTestMachinePage";
import { Wago8chDioTestMachineControlRoute } from "@/machines/minimal_machines/wago8chdiotestmachine/wago8chDioTestMachineControlPage";
// make a route tree like this
// _mainNavigation/machines/winder2/$serial/control
+1 -10
View File
@@ -17,25 +17,16 @@ use std::any::Any;
use std::fmt::Debug;
use std::sync::Arc;
use std::time::Instant;
pub mod analog_input_test_machine;
pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))]
pub mod buffer1;
pub mod digital_input_test_machine;
pub mod extruder1;
pub mod extruder2;
pub mod ip20_test_machine;
pub mod laser;
pub mod machine_identification;
pub mod mock;
pub mod motor_test_machine;
pub mod minimal_machines;
pub mod registry;
pub mod serial;
pub mod test_machine;
pub mod test_machine_stepper;
pub mod wago_8ch_dio_test_machine;
pub mod wago_ai_test_machine;
pub mod wago_do_test_machine;
pub mod wago_power;
pub mod winder2;
@@ -0,0 +1,144 @@
# Minimal Machine Boilerplate
This directory is a copy-paste template for creating a new minimal machine.
Follow the steps below in order. Each file has inline `// TODO:` comments that
mark every place you need to edit.
---
## Quick-start checklist
### 1 — Copy this directory
```
cp -r BOILERPLATE_TEMPLATE my_new_machine
```
### 2 — Pick a unique machine ID
Open `machines/src/lib.rs` and look at the existing constants:
```rust
pub const TEST_MACHINE: u16 = 0x0033;
pub const IP20_TEST_MACHINE: u16 = 0x0034;
pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035;
// ...
```
Add a new constant with the next available hex value:
```rust
pub const MY_MACHINE_ID: u16 = 0x00XX; // replace XX
```
### 3 — Rename throughout the template
Do a project-wide find & replace inside your new directory:
| Find | Replace with |
|---------------|---------------------------|
| `MyMachine` | `YourMachineName` |
| `MY_MACHINE_ID` | the constant you added |
### 4 — Fill in `mod.rs`
- Add hardware fields to the struct (e.g. `douts: [DigitalOutput; 4]`)
- Add domain-specific helper methods (`set_output`, `read_inputs`, …)
### 5 — Fill in `new.rs`
- Choose Pattern A (Beckhoff terminal) or Pattern B (WAGO coupler + module)
- Uncomment and adapt the hardware imports and initialization code
- Add your hardware fields to the `Self { … }` constructor at the bottom
### 6 — Fill in `api.rs`
- Add fields to `StateEvent` that the UI needs to display
- Add `Mutation` variants for every action the UI can trigger
- Implement `api_mutate` to dispatch each mutation to your helper methods
### 7 — Fill in `act.rs`
- If your machine reads hardware on every cycle, add that call inside `act()`
before `emit_state()` is called
- Everything else is boilerplate and usually does not need changes
### 8 — Register the module in `minimal_machines/mod.rs`
```rust
pub mod my_new_machine;
```
### 9 — Register the machine in `machines/src/registry.rs`
```rust
// at the top of registry.rs — add the import:
use crate::minimal_machines::my_new_machine::YourMachineName;
// inside the lazy_static! MACHINE_REGISTRY block — add the registration:
mc.register::<YourMachineName>(YourMachineName::MACHINE_IDENTIFICATION);
```
### 10 — Verify it compiles
```
cargo check -p machines
```
---
## File overview
| File | Responsibility |
|-----------|-------------------------------------------------------------|
| `mod.rs` | Struct definition, `Machine` trait, business logic helpers |
| `new.rs` | `MachineNewTrait` — hardware init, called once at startup |
| `api.rs` | Events, mutations, `MachineApi` trait |
| `act.rs` | `MachineAct` trait — update loop called every EtherCAT cycle|
## Architecture diagram
```
EtherCAT cycle
│
▼
act() ──── drains message queue ──► act_machine_message()
│ │
│ SubscribeNamespace → emit_state()
│ HttpApiJsonRequest → api_mutate()
│ RequestValues → serialize state
│
└── every 33 ms ──► emit_state() ──► socket.io subscribers
```
## Hardware patterns
### Pattern A — Beckhoff EtherCAT terminal
```
EtherCAT bus: [Bus Coupler (role 0)] [EL2004 (role 1)]
```
The role index matches the `device_roles` array in `properties.ts`.
### Pattern B — WAGO 750 coupler + expansion modules
```
EtherCAT bus: [WAGO 750-354 (role 0)] ─── local backplane ───
[750-530 slot 0]
[750-402 slot 1]
...
```
The coupler is always role 0. Modules are found by slot index (0-based)
after calling `initialize_modules` + `init_slot_modules`.
## Naming conventions
| Concept | Convention | Example |
|--------------------|-------------------------------|-------------------------------|
| Machine struct | `PascalCase` | `WagoDiMachine` |
| Module directory | `snake_case` | `wago_di_machine` |
| Machine ID constant| `SCREAMING_SNAKE_CASE` | `WAGO_DI_MACHINE` |
| Event struct | `<Name>Events` enum | `WagoDiMachineEvents` |
| Namespace struct | `<Name>Namespace` | `WagoDiMachineNamespace` |
@@ -0,0 +1,73 @@
// ============================================================================
// act.rs — The machine's update loop
// ============================================================================
// This file implements `MachineAct` which is called on every control cycle.
//
// `act()` is called at the EtherCAT cycle rate (typically 1 kHz). Keep it
// fast: drain the message queue, then emit state at the UI refresh rate.
//
// The `act_machine_message()` handler is mandatory boilerplate — only the
// `SubscribeNamespace` arm needs customization (emit your initial state there).
// ============================================================================
use std::time::{Duration, Instant};
use crate::{MachineAct, MachineMessage, MachineValues};
use super::MyMachine;
impl MachineAct for MyMachine {
/// Called every EtherCAT cycle. Drain messages and update at 30 Hz.
fn act(&mut self, now: Instant) {
// Drain the inbound message queue (API calls, subscriptions, etc.)
if let Ok(msg) = self.api_receiver.try_recv() {
self.act_machine_message(msg);
}
// Emit state at ~30 Hz — change the divisor to adjust the rate.
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) {
// TODO: read hardware values into your struct fields here if needed,
// e.g.: self.read_inputs();
self.emit_state();
self.last_state_emit = now;
}
}
/// Handle a single inbound message. This is mandatory plumbing — the only
/// arm that typically needs editing is `SubscribeNamespace` (send initial
/// state to a new subscriber) and `RequestValues` (serialize state for
/// HTTP polling).
fn act_machine_message(&mut self, msg: MachineMessage) {
match msg {
MachineMessage::SubscribeNamespace(namespace) => {
self.namespace.namespace = Some(namespace);
// Send the current state immediately so the new subscriber
// doesn't wait up to 33 ms for the next cycle.
self.emit_state();
}
MachineMessage::UnsubscribeNamespace => {
self.namespace.namespace = None;
}
MachineMessage::HttpApiJsonRequest(value) => {
use crate::MachineApi;
let _res = self.api_mutate(value);
}
MachineMessage::ConnectToMachine(_machine_connection) => {
// Cross-machine connections — not used in minimal machines.
}
MachineMessage::DisconnectMachine(_machine_connection) => {
// Cross-machine connections — not used in minimal machines.
}
MachineMessage::RequestValues(sender) => {
// Called by the HTTP polling path. Serialize current state.
sender
.send_blocking(MachineValues {
state: serde_json::to_value(self.get_state())
.expect("Failed to serialize state"),
live_values: serde_json::Value::Null,
})
.expect("Failed to send values");
sender.close();
}
}
}
}
@@ -0,0 +1,134 @@
// ============================================================================
// api.rs — Events, mutations, and MachineApi implementation
// ============================================================================
// This file defines:
// • StateEvent — the data structure broadcast to frontend subscribers
// • MyMachineEvents — enum wrapping all event types for this machine
// • Mutation — the actions the frontend can send to this machine
// • MyMachineNamespace — the socket.io namespace handle
// • MachineApi impl — wires mutations and namespace access
// ============================================================================
use std::sync::Arc;
use control_core::socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{MachineApi, MachineMessage};
use super::MyMachine;
// ----------------------------------------------------------------------------
// Step 1 — Define the state that gets broadcast to the frontend.
//
// Add every field that the UI needs to display. Must be Serialize + Clone.
// ----------------------------------------------------------------------------
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
// TODO: add your state fields, e.g.:
// pub outputs: [bool; 4],
// pub inputs: [bool; 4],
// pub value: f64,
}
impl StateEvent {
/// Wrap state in a named socket.io event ready to be emitted.
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
// ----------------------------------------------------------------------------
// Step 2 — Declare all event variants your machine can emit.
//
// For most minimal machines a single `State` variant is enough.
// Add more variants (e.g. `LiveValues`, `Alert`) only if the frontend needs
// them distinguished.
// ----------------------------------------------------------------------------
pub enum MyMachineEvents {
State(Event<StateEvent>),
// TODO: add more variants if needed, e.g.:
// LiveValues(Event<LiveValuesEvent>),
}
// ----------------------------------------------------------------------------
// Step 3 — Define mutations the frontend can trigger.
//
// Use `#[serde(tag = "action", content = "value")]` so the wire format is:
// { "action": "SetOutput", "value": { "index": 0, "on": true } }
//
// If your machine is read-only (inputs only), keep the enum empty or with a
// single never-used variant, and return Ok(()) in api_mutate.
// ----------------------------------------------------------------------------
#[derive(Deserialize)]
#[serde(tag = "action", content = "value")]
pub enum Mutation {
// TODO: add your mutations, e.g.:
// SetOutput { index: usize, on: bool },
// SetAllOutputs { on: bool },
}
// ----------------------------------------------------------------------------
// Step 4 — The namespace handle (mandatory plumbing, do not change the struct).
// ----------------------------------------------------------------------------
#[derive(Debug, Clone)]
pub struct MyMachineNamespace {
pub namespace: Option<Namespace>,
}
impl NamespaceCacheingLogic<MyMachineEvents> for MyMachineNamespace {
fn emit(&mut self, events: MyMachineEvents) {
let event = Arc::new(events.event_value());
let buffer_fn = events.event_cache_fn();
if let Some(ns) = &mut self.namespace {
ns.emit(event, &buffer_fn);
}
}
}
impl CacheableEvents<MyMachineEvents> for MyMachineEvents {
fn event_value(&self) -> GenericEvent {
match self {
MyMachineEvents::State(event) => event.clone().into(),
// TODO: add arms for every new variant you add above
}
}
fn event_cache_fn(&self) -> CacheFn {
// `cache_first_and_last_event` keeps the very first and the most recent
// event in the cache so new subscribers always get a value immediately.
cache_first_and_last_event()
}
}
// ----------------------------------------------------------------------------
// Step 5 — Implement MachineApi (mandatory plumbing + your mutation logic).
// ----------------------------------------------------------------------------
impl MachineApi for MyMachine {
fn api_get_sender(&self) -> smol::channel::Sender<MachineMessage> {
self.api_sender.clone()
}
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
// TODO: deserialize and dispatch mutations:
//
// let mutation: Mutation = serde_json::from_value(request_body)?;
// match mutation {
// Mutation::SetOutput { index, on } => self.set_output(index, on),
// Mutation::SetAllOutputs { on } => { /* ... */ }
// }
// If this machine is read-only, just return Ok(()) without parsing:
let _ = request_body;
Ok(())
}
fn api_event_namespace(&mut self) -> Option<Namespace> {
self.namespace.namespace.clone()
}
}
@@ -0,0 +1,103 @@
// ============================================================================
// mod.rs — Core struct definition
// ============================================================================
// This file defines the machine struct and implements the `Machine` trait.
//
// FIND & REPLACE to adapt this template:
// MyMachine → YourMachineName (e.g. WagoDiMachine)
// MY_MACHINE_ID → your constant (e.g. WAGO_DI_MACHINE)
// ============================================================================
use std::time::Instant;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use smol::channel::{Receiver, Sender};
use crate::{
AsyncThreadMessage, Machine, MachineMessage, MY_MACHINE_ID, VENDOR_QITECH,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
use self::api::{MyMachineEvents, MyMachineNamespace, StateEvent};
pub mod act;
pub mod api;
pub mod new;
// ----------------------------------------------------------------------------
// Step 1 — Define your machine struct.
//
// Add the hardware handles your machine needs (DigitalOutput, DigitalInput,
// AnalogInput, etc.) as fields. The first five fields are mandatory plumbing
// for every machine — do not remove them.
// ----------------------------------------------------------------------------
#[derive(Debug)]
pub struct MyMachine {
// --- mandatory plumbing (keep as-is) ------------------------------------
pub api_receiver: Receiver<MachineMessage>,
pub api_sender: Sender<MachineMessage>,
pub machine_identification_unique: MachineIdentificationUnique,
pub main_sender: Option<Sender<AsyncThreadMessage>>,
pub namespace: MyMachineNamespace,
pub last_state_emit: Instant,
// --- TODO: add your hardware handles here --------------------------------
// Examples:
// pub douts: [DigitalOutput; 4],
// pub dins: [DigitalInput; 4],
// pub ain: AnalogInput,
}
// ----------------------------------------------------------------------------
// Step 2 — Implement the `Machine` trait (mandatory, do not change).
// ----------------------------------------------------------------------------
impl Machine for MyMachine {
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
self.machine_identification_unique.clone()
}
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
self.main_sender.clone()
}
}
// ----------------------------------------------------------------------------
// Step 3 — Set the unique machine ID constant.
//
// The constant MY_MACHINE_ID must be declared in machines/src/lib.rs and must
// be a unique u16 value not used by any other machine. See lib.rs for the
// existing values and pick the next available one.
// ----------------------------------------------------------------------------
impl MyMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MY_MACHINE_ID,
};
}
// ----------------------------------------------------------------------------
// Step 4 — Add your machine's business logic methods here.
//
// `get_state()` and `emit_state()` are called by act.rs and api.rs — keep
// their signatures stable. Add any other helpers your machine needs.
// ----------------------------------------------------------------------------
impl MyMachine {
/// Build the current state snapshot (used for serialization and events).
pub fn get_state(&self) -> StateEvent {
StateEvent {
// TODO: fill with your actual state fields
}
}
/// Serialize and broadcast the current state to all subscribers.
pub fn emit_state(&mut self) {
let event = self.get_state().build();
self.namespace.emit(MyMachineEvents::State(event));
}
// TODO: add domain-specific helpers, e.g.:
//
// pub fn set_output(&mut self, index: usize, on: bool) {
// self.douts[index].set(on);
// self.emit_state();
// }
}
@@ -0,0 +1,140 @@
// ============================================================================
// new.rs — Machine constructor (hardware initialization)
// ============================================================================
// This file implements `MachineNewTrait` for your machine, which is called
// once at startup to acquire hardware handles and build the machine struct.
//
// Two common hardware patterns are shown:
// A) Beckhoff EtherCAT terminal (e.g. EL2004 digital output)
// B) WAGO 750 coupler + expansion module (e.g. 750-530 digital output)
//
// Only keep the block that matches your hardware — delete the other.
// ============================================================================
use std::time::Instant;
use anyhow::Error;
use smol::block_on;
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use super::{MyMachine, api::MyMachineNamespace};
// --- Pattern A: Beckhoff terminal imports ------------------------------------
// Uncomment and adapt for the specific terminal you need.
//
// use ethercat_hal::devices::el2004::{EL2004, EL2004_IDENTITY_A, EL2004Port};
// use ethercat_hal::io::digital_output::DigitalOutput;
// --- Pattern B: WAGO coupler + module imports ---------------------------------
// Uncomment and adapt for the specific WAGO module you need.
//
// use std::sync::Arc;
// use smol::lock::RwLock;
// use ethercat_hal::devices::{EthercatDevice, downcast_device};
// use ethercat_hal::devices::wago_750_354::{WAGO_750_354_IDENTITY_A, Wago750_354};
// use ethercat_hal::devices::wago_modules::wago_750_530::{Wago750_530, Wago750_530Port};
// use ethercat_hal::io::digital_output::DigitalOutput;
impl MachineNewTrait for MyMachine {
fn new(params: &MachineNewParams) -> Result<Self, Error> {
// --- Validate the device group (mandatory, keep as-is) --------------
let device_identification = params
.device_group
.iter()
.map(|d| d.clone())
.collect::<Vec<_>>();
validate_same_machine_identification_unique(&device_identification)?;
validate_no_role_dublicates(&device_identification)?;
// --- Require EtherCAT hardware (mandatory for EtherCAT machines) ----
let hardware = match &params.hardware {
MachineNewHardware::Ethercat(x) => x,
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/MyMachine::new] MachineNewHardware is not Ethercat",
module_path!()
));
}
};
block_on(async {
// ----------------------------------------------------------------
// Pattern A — Beckhoff EtherCAT terminal
// ----------------------------------------------------------------
// `get_ethercat_device` arguments:
// hardware — from above
// params — from above
// role — device role index as configured in properties.ts
// identities — list of accepted (vendor_id, product_id, revision)
//
// Example: EL2004 (4× digital output), role 1
//
// let el2004 = get_ethercat_device::<EL2004>(
// hardware, params, 1, [EL2004_IDENTITY_A].to_vec(),
// ).await?.0;
//
// let do1 = DigitalOutput::new(el2004.clone(), EL2004Port::DO1);
// let do2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
// let do3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
// let do4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
// ----------------------------------------------------------------
// Pattern B — WAGO 750 bus coupler + expansion module
// ----------------------------------------------------------------
// The coupler is always role 0. Modules are discovered by reading
// the PDO slot map from the coupler subdevice.
//
// Example: WAGO 750-354 coupler with a 750-530 (8× DO) in slot 0
//
// let (coupler_dev, coupler_subdev) = get_ethercat_device::<Wago750_354>(
// hardware, params, 0, [WAGO_750_354_IDENTITY_A].to_vec(),
// ).await?;
//
// let modules = Wago750_354::initialize_modules(coupler_subdev).await?;
// let mut coupler = coupler_dev.write().await;
// for module in modules { coupler.set_module(module); }
// coupler.init_slot_modules(coupler_subdev);
//
// // Get the device at slot index 0 (first expansion module).
// let dev = coupler.slot_devices.get(0)
// .ok_or_else(|| anyhow::anyhow!(
// "[{}::MyMachine::new] slot 0 not configured", module_path!()
// ))?
// .clone()
// .ok_or_else(|| anyhow::anyhow!(
// "[{}::MyMachine::new] slot 0 is empty", module_path!()
// ))?;
// let wago750_530: Arc<RwLock<Wago750_530>> =
// downcast_device::<Wago750_530>(dev).await?;
//
// let do1 = DigitalOutput::new(wago750_530.clone(), Wago750_530Port::Port1);
// // ... repeat for do2..do8
// drop(coupler);
// ----------------------------------------------------------------
// Build the machine struct (mandatory plumbing, adapt fields)
// ----------------------------------------------------------------
let (sender, receiver) = smol::channel::unbounded();
let mut machine = Self {
api_receiver: receiver,
api_sender: sender,
machine_identification_unique: params.get_machine_identification_unique(),
main_sender: params.main_thread_channel.clone(),
namespace: MyMachineNamespace {
namespace: params.namespace.clone(),
},
last_state_emit: Instant::now(),
// TODO: add your hardware fields here, e.g.:
// douts: [do1, do2, do3, do4],
};
// Emit initial state so subscribers get values immediately.
machine.emit_state();
Ok(machine)
})
}
}
@@ -1,7 +1,8 @@
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::{
MachineAct, MachineMessage, MachineValues, analog_input_test_machine::AnalogInputTestMachine,
MachineAct, MachineMessage, MachineValues,
minimal_machines::analog_input_test_machine::AnalogInputTestMachine,
};
impl MachineAct for AnalogInputTestMachine {
@@ -8,7 +8,7 @@ use control_core::socketio::{
};
use serde::{Deserialize, Serialize};
use crate::{MachineApi, analog_input_test_machine::AnalogInputTestMachine};
use crate::{MachineApi, minimal_machines::analog_input_test_machine::AnalogInputTestMachine};
#[derive(Debug, Clone)]
pub struct AnalogInputTestMachineNamespace {
@@ -4,11 +4,9 @@ use control_core::socketio::{event::Event, namespace::NamespaceCacheingLogic};
use ethercat_hal::io::analog_input::AnalogInput;
use smol::channel::{Receiver, Sender};
use self::api::{AnalogInputTestMachineEvents, AnalogInputTestMachineNamespace, MeasurementEvent};
use crate::{
ANALOG_INPUT_TEST_MACHINE, AsyncThreadMessage, Machine, MachineMessage, VENDOR_QITECH,
analog_input_test_machine::api::{
AnalogInputTestMachineEvents, AnalogInputTestMachineNamespace, MeasurementEvent,
},
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
@@ -7,10 +7,10 @@ use ethercat_hal::{
};
use smol::{block_on, channel::unbounded};
use super::{AnalogInputTestMachine, api::AnalogInputTestMachineNamespace};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
analog_input_test_machine::{AnalogInputTestMachine, api::AnalogInputTestMachineNamespace},
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
impl MachineNewTrait for AnalogInputTestMachine {
@@ -8,7 +8,7 @@ use control_core::socketio::{
};
use serde::{Deserialize, Serialize};
use crate::{MachineApi, digital_input_test_machine::DigitalInputTestMachine};
use crate::{MachineApi, minimal_machines::digital_input_test_machine::DigitalInputTestMachine};
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
@@ -4,11 +4,9 @@ use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_input::DigitalInput;
use smol::channel::{Receiver, Sender};
use self::api::{DigitalInputTestMachineEvents, DigitalInputTestMachineNamespace, StateEvent};
use crate::{
AsyncThreadMessage, DIGITAL_INPUT_TEST_MACHINE, Machine, MachineMessage, VENDOR_QITECH,
digital_input_test_machine::api::{
DigitalInputTestMachineEvents, DigitalInputTestMachineNamespace, StateEvent,
},
machine_identification::{MachineIdentification, MachineIdentificationUnique},
};
@@ -11,10 +11,10 @@ use ethercat_hal::{
};
use smol::{block_on, lock::RwLock};
use super::{DigitalInputTestMachine, api::DigitalInputTestMachineNamespace};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
digital_input_test_machine::{DigitalInputTestMachine, api::DigitalInputTestMachineNamespace},
get_ethercat_device, validate_no_role_dublicates, validate_same_machine_identification_unique,
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
impl MachineNewTrait for DigitalInputTestMachine {
@@ -1,5 +1,7 @@
use crate::ip20_test_machine::api::{IP20TestMachineEvents, LiveValuesEvent, StateEvent};
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::minimal_machines::ip20_test_machine::api::{
IP20TestMachineEvents, LiveValuesEvent, StateEvent,
};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_input::DigitalInput;
@@ -9,7 +11,7 @@ use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::ip20_test_machine::api::IP20TestMachineNamespace;
use crate::minimal_machines::ip20_test_machine::api::IP20TestMachineNamespace;
use crate::{IP20_TEST_MACHINE, VENDOR_QITECH};
#[derive(Debug)]
@@ -1,5 +1,5 @@
use crate::ip20_test_machine::IP20TestMachine;
use crate::ip20_test_machine::api::IP20TestMachineNamespace;
use crate::minimal_machines::ip20_test_machine::IP20TestMachine;
use crate::minimal_machines::ip20_test_machine::api::IP20TestMachineNamespace;
use smol::block_on;
use std::time::Instant;
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+10
View File
@@ -0,0 +1,10 @@
pub mod analog_input_test_machine;
pub mod digital_input_test_machine;
pub mod ip20_test_machine;
pub mod mock;
pub mod motor_test_machine;
pub mod test_machine;
pub mod test_machine_stepper;
pub mod wago_8ch_dio_test_machine;
pub mod wago_ai_test_machine;
pub mod wago_do_test_machine;
@@ -1,5 +1,5 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::test_machine::api::{StateEvent, TestMachineEvents};
use crate::minimal_machines::test_machine::api::{StateEvent, TestMachineEvents};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_output::DigitalOutput;
@@ -8,7 +8,7 @@ use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::test_machine::api::TestMachineNamespace;
use crate::minimal_machines::test_machine::api::TestMachineNamespace;
use crate::{TEST_MACHINE, VENDOR_QITECH};
#[derive(Debug)]
@@ -1,5 +1,5 @@
use crate::test_machine::TestMachine;
use crate::test_machine::api::TestMachineNamespace;
use crate::minimal_machines::test_machine::TestMachine;
use crate::minimal_machines::test_machine::api::TestMachineNamespace;
use smol::block_on;
use std::time::Instant;
@@ -1,5 +1,5 @@
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::test_machine_stepper::api::{StateEvent, TestMachineStepperEvents};
use crate::minimal_machines::test_machine_stepper::api::{StateEvent, TestMachineStepperEvents};
use crate::{AsyncThreadMessage, Machine, MachineMessage};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::stepper_velocity_wago_750_672::StepperVelocityWago750672;
@@ -8,7 +8,7 @@ use std::time::Instant;
pub mod act;
pub mod api;
pub mod new;
use crate::test_machine_stepper::api::TestMachineStepperNamespace;
use crate::minimal_machines::test_machine_stepper::api::TestMachineStepperNamespace;
use crate::{TEST_MACHINE_STEPPER, VENDOR_QITECH};
#[derive(Debug)]
@@ -1,4 +1,6 @@
use crate::test_machine_stepper::{TestMachineStepper, api::TestMachineStepperNamespace};
use crate::minimal_machines::test_machine_stepper::{
TestMachineStepper, api::TestMachineStepperNamespace,
};
use ethercat_hal::{
devices::{
EthercatDevice, downcast_device,
@@ -8,7 +8,7 @@ use control_core::socketio::{
};
use serde::{Deserialize, Serialize};
use crate::{MachineApi, wago_8ch_dio_test_machine::Wago8chDigitalIOTestMachine};
use crate::{MachineApi, minimal_machines::wago_8ch_dio_test_machine::Wago8chDigitalIOTestMachine};
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
@@ -4,12 +4,12 @@ use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::{digital_input::DigitalInput, digital_output::DigitalOutput};
use smol::channel::{Receiver, Sender};
use self::api::{
StateEvent, Wago8chDigitalIOTestMachineEvents, Wago8chDigitalIOTestMachineNamespace,
};
use crate::{
AsyncThreadMessage, Machine, MachineMessage, VENDOR_QITECH, WAGO_8CH_IO_TEST_MACHINE,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
wago_8ch_dio_test_machine::api::{
StateEvent, Wago8chDigitalIOTestMachineEvents, Wago8chDigitalIOTestMachineNamespace,
},
};
pub mod act;
@@ -13,12 +13,10 @@ use ethercat_hal::{
};
use smol::{block_on, lock::RwLock};
use super::{Wago8chDigitalIOTestMachine, api::Wago8chDigitalIOTestMachineNamespace};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
wago_8ch_dio_test_machine::{
Wago8chDigitalIOTestMachine, api::Wago8chDigitalIOTestMachineNamespace,
},
};
impl MachineNewTrait for Wago8chDigitalIOTestMachine {
@@ -1,6 +1,9 @@
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::{MachineAct, MachineMessage, MachineValues, wago_ai_test_machine::WagoAiTestMachine};
use crate::{
MachineAct, MachineMessage, MachineValues,
minimal_machines::wago_ai_test_machine::WagoAiTestMachine,
};
impl MachineAct for WagoAiTestMachine {
fn act_machine_message(&mut self, msg: MachineMessage) {
@@ -8,7 +8,7 @@ use control_core::socketio::{
};
use serde::{Deserialize, Serialize};
use crate::{MachineApi, wago_ai_test_machine::WagoAiTestMachine};
use crate::{MachineApi, minimal_machines::wago_ai_test_machine::WagoAiTestMachine};
#[derive(Debug, Clone)]
pub struct WagoAiTestMachineNamespace {
@@ -4,12 +4,10 @@ use control_core::socketio::{event::Event, namespace::NamespaceCacheingLogic};
use ethercat_hal::io::analog_input::AnalogInput;
use smol::channel::{Receiver, Sender};
use self::api::{AnalogInputsEvent, WagoAiTestMachineEvents, WagoAiTestMachineNamespace};
use crate::{
AsyncThreadMessage, Machine, MachineMessage, VENDOR_QITECH, WAGO_AI_TEST_MACHINE,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
wago_ai_test_machine::api::{
AnalogInputsEvent, WagoAiTestMachineEvents, WagoAiTestMachineNamespace,
},
};
pub mod act;
@@ -12,10 +12,10 @@ use ethercat_hal::{
use smol::{block_on, channel::unbounded, lock::RwLock};
use std::sync::Arc;
use super::{WagoAiTestMachine, api::WagoAiTestMachineNamespace};
use crate::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
validate_no_role_dublicates, validate_same_machine_identification_unique,
wago_ai_test_machine::{WagoAiTestMachine, api::WagoAiTestMachineNamespace},
};
impl MachineNewTrait for WagoAiTestMachine {
@@ -4,10 +4,10 @@ use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::io::digital_output::DigitalOutput;
use smol::channel::{Receiver, Sender};
use self::api::{StateEvent, WagoDOTestMachineEvents, WagoDOTestMachineNamespace};
use crate::{
AsyncThreadMessage, Machine, MachineMessage, VENDOR_QITECH, WAGO_DO_TEST_MACHINE,
machine_identification::{MachineIdentification, MachineIdentificationUnique},
wago_do_test_machine::api::{StateEvent, WagoDOTestMachineEvents, WagoDOTestMachineNamespace},
};
pub mod act;
@@ -1,5 +1,5 @@
use crate::wago_do_test_machine::WagoDOTestMachine;
use crate::wago_do_test_machine::api::WagoDOTestMachineNamespace;
use crate::minimal_machines::wago_do_test_machine::WagoDOTestMachine;
use crate::minimal_machines::wago_do_test_machine::api::WagoDOTestMachineNamespace;
use smol::block_on;
use std::time::Instant;
+11 -9
View File
@@ -1,13 +1,13 @@
use crate::analog_input_test_machine::AnalogInputTestMachine;
use crate::digital_input_test_machine::DigitalInputTestMachine;
use crate::ip20_test_machine::IP20TestMachine;
use crate::wago_8ch_dio_test_machine::Wago8chDigitalIOTestMachine;
use crate::wago_ai_test_machine::WagoAiTestMachine;
use crate::wago_do_test_machine::WagoDOTestMachine;
use crate::minimal_machines::analog_input_test_machine::AnalogInputTestMachine;
use crate::minimal_machines::digital_input_test_machine::DigitalInputTestMachine;
use crate::minimal_machines::ip20_test_machine::IP20TestMachine;
use crate::minimal_machines::wago_8ch_dio_test_machine::Wago8chDigitalIOTestMachine;
use crate::minimal_machines::wago_ai_test_machine::WagoAiTestMachine;
use crate::minimal_machines::wago_do_test_machine::WagoDOTestMachine;
#[cfg(feature = "mock-machine")]
use crate::{
extruder1::mock::ExtruderV2 as ExtruderV2Mock1, extruder2::mock::ExtruderV2 as ExtruderV2Mock2,
mock::MockMachine, winder2::mock::Winder2,
minimal_machines::mock::MockMachine, winder2::mock::Winder2,
};
use crate::{
@@ -22,11 +22,13 @@ use crate::{
winder2::Winder2,
};
use crate::{test_machine::TestMachine, test_machine_stepper::TestMachineStepper};
use crate::minimal_machines::{
test_machine::TestMachine, test_machine_stepper::TestMachineStepper,
};
use lazy_static::lazy_static;
use crate::motor_test_machine::MotorTestMachine;
use crate::minimal_machines::motor_test_machine::MotorTestMachine;
use anyhow::Error;
use std::{any::TypeId, collections::HashMap};
+4 -4
View File
@@ -4,14 +4,14 @@ use axum::extract::{Path, State};
use axum::routing::{get, post};
use axum::{Extension, Json, Router, debug_handler};
use machines::MachineMessage;
use machines::analog_input_test_machine::AnalogInputTestMachine;
use machines::aquapath1::AquaPathV1;
use machines::extruder1::ExtruderV2;
use machines::ip20_test_machine::IP20TestMachine;
use machines::laser::LaserMachine;
use machines::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use machines::mock::MockMachine;
use machines::test_machine::TestMachine;
use machines::minimal_machines::analog_input_test_machine::AnalogInputTestMachine;
use machines::minimal_machines::ip20_test_machine::IP20TestMachine;
use machines::minimal_machines::mock::MockMachine;
use machines::minimal_machines::test_machine::TestMachine;
use machines::wago_power::WagoPower;
use machines::winder2::Winder2;
use serde::Serialize;