Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d20328f71 | ||
|
|
58cdfac8e5 | ||
|
|
38367a7267 | ||
|
|
8c40375c50 | ||
|
|
97c9eee8c2 | ||
|
|
8b7b91907b |
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,
|
||||
};
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -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";
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
ThrottledStoreUpdater,
|
||||
} from "../../../client/socketioStore";
|
||||
} from "../../../../client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import {
|
||||
createTimeSeries,
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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,
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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
@@ -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 ¶ms.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)
|
||||
})
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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 {
|
||||
+1
-1
@@ -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 {
|
||||
+1
-3
@@ -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},
|
||||
};
|
||||
|
||||
+3
-3
@@ -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 {
|
||||
File renamed without changes.
+1
-1
@@ -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 {
|
||||
+1
-3
@@ -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},
|
||||
};
|
||||
|
||||
+3
-3
@@ -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 {
|
||||
File renamed without changes.
File renamed without changes.
+4
-2
@@ -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)]
|
||||
+2
-2
@@ -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.
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -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)]
|
||||
+2
-2
@@ -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;
|
||||
|
||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -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)]
|
||||
+3
-1
@@ -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,
|
||||
File renamed without changes.
+1
-1
@@ -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 {
|
||||
+3
-3
@@ -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;
|
||||
+1
-3
@@ -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 {
|
||||
+4
-1
@@ -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) {
|
||||
+1
-1
@@ -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 {
|
||||
+1
-3
@@ -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;
|
||||
+1
-1
@@ -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 {
|
||||
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
Reference in new issue
Block a user