Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cae322f312 | ||
|
|
7eeb18af94 |
No files matched your search
@@ -544,6 +544,30 @@ export const ip20TestMachine: MachineProperties = {
|
||||
],
|
||||
};
|
||||
|
||||
export const vacuum: MachineProperties = {
|
||||
name: "Vacuum",
|
||||
version: "V1",
|
||||
slug: "vacuum",
|
||||
icon: "lu:ToggleLeft",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x0037,
|
||||
},
|
||||
device_roles: [
|
||||
{
|
||||
role: 0,
|
||||
role_label: "IP20-EC-DI8-DO8",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 0x741,
|
||||
product_id: 0x117b6722,
|
||||
revision: 0x1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const TestMotor: MachineProperties = {
|
||||
name: "TestMotor",
|
||||
version: "V1",
|
||||
@@ -618,6 +642,7 @@ export const machineProperties: MachineProperties[] = [
|
||||
digitalInputTestMachine,
|
||||
ip20TestMachine,
|
||||
TestMotor,
|
||||
vacuum
|
||||
];
|
||||
|
||||
export const getMachineProperties = (
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { Label } from "@/control/Label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useVacuum } from "./use";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { roundToDecimals } from "@/lib/decimal";
|
||||
|
||||
export function VacuumConfigPage() {
|
||||
const { state, liveValues, setIntervalTimeOff, setIntervalTimeOn } = useVacuum();
|
||||
|
||||
const safeState = state ?? { mode: "Standby", interval_time_off: 0, interval_time_on: 0 };
|
||||
|
||||
const safeLiveValues = liveValues ?? {};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
|
||||
<ControlCard className="bg-red" title="Interval Settings">
|
||||
<Label label="Interval (OFF) duration">
|
||||
<EditValue
|
||||
value={safeState.interval_time_off}
|
||||
defaultValue={30}
|
||||
unit="s"
|
||||
title="Interval (OFF) duration"
|
||||
min={0}
|
||||
max={60}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={setIntervalTimeOff}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Interval (ON) duration">
|
||||
<EditValue
|
||||
value={safeState.interval_time_on}
|
||||
defaultValue={30}
|
||||
unit="s"
|
||||
title="Interval (ON) duration"
|
||||
min={0}
|
||||
max={60}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={setIntervalTimeOn}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { Label } from "@/control/Label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useVacuum } from "./use";
|
||||
import { Spool } from "../winder/Spool";
|
||||
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
|
||||
import { roundToDecimals } from "@/lib/decimal";
|
||||
|
||||
export function VacuumControlPage() {
|
||||
const { state, liveValues, remaining_time, spin_shitter, setMode } = useVacuum();
|
||||
|
||||
const safeState = state ?? { mode: "Idle", interval_time_off: 0, interval_time_on: 0, running: false };
|
||||
|
||||
const safeLiveValues = liveValues ?? { remaining_time: 0 };
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
|
||||
<ControlCard className="bg-red" title="Mode">
|
||||
<SelectionGroup<"Idle" | "On" | "Auto" | "Interval">
|
||||
value={ safeState.mode }
|
||||
onChange={setMode}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Idle: {
|
||||
children: "Idle",
|
||||
icon: "lu:Power",
|
||||
isActiveClassName: "bg-red-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
Auto: {
|
||||
children: "Auto",
|
||||
icon: "lu:BrainCog",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
Interval: {
|
||||
children: "Interval",
|
||||
icon: "lu:Activity",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Motor">
|
||||
<Badge className={safeState.running ? "bg-green-500" : "bg-red-500"}>
|
||||
{safeState.running ? "Running" : "Idle" }
|
||||
</Badge>
|
||||
<Spool rpm={spin_shitter.current?.value} />
|
||||
<TimeSeriesValueNumeric
|
||||
label="Remaining Time"
|
||||
unit="s"
|
||||
timeseries={remaining_time}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
/>
|
||||
</ControlCard>
|
||||
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import { StoreApi } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
Event,
|
||||
handleUnhandledEventError,
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
ThrottledStoreUpdater,
|
||||
} from "@/client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { createTimeSeries, TimeSeries, TimeSeriesValue } from "@/lib/timeseries";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
// ========== Event Schema ==========
|
||||
|
||||
/**
|
||||
* Machine operation mode enum
|
||||
*/
|
||||
export const modeSchema = z.enum(["Idle", "On", "Auto", "Interval"]);
|
||||
export type Mode = z.infer<typeof modeSchema>;
|
||||
|
||||
export const stateEventDataSchema = z.object({
|
||||
mode: modeSchema,
|
||||
|
||||
interval_time_off: z.number(),
|
||||
interval_time_on: z.number(),
|
||||
|
||||
running: z.boolean(),
|
||||
});
|
||||
|
||||
export const liveValuesEventDataSchema = z.object({
|
||||
remaining_time: z.number(),
|
||||
});
|
||||
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventDataSchema>;
|
||||
export type LiveValuesEvent = z.infer<typeof liveValuesEventDataSchema>;
|
||||
|
||||
// ========== Store ==========
|
||||
export type VacuumNamespaceStore = {
|
||||
state: StateEvent | null;
|
||||
liveValues: LiveValuesEvent | null;
|
||||
|
||||
// Timer series data for live values
|
||||
remaining_time: TimeSeries,
|
||||
|
||||
spin_shitter: TimeSeries,
|
||||
};
|
||||
|
||||
const { initialTimeSeries: remaining_time, insert: addRemainingTime } =
|
||||
createTimeSeries({ sampleIntervalLong: 1000 });
|
||||
|
||||
const { initialTimeSeries: spin_shitter, insert: addSpinShitter } =
|
||||
createTimeSeries({ sampleIntervalLong: 1000 });
|
||||
|
||||
export const createVacuumNamespaceStore =
|
||||
(): StoreApi<VacuumNamespaceStore> =>
|
||||
create<VacuumNamespaceStore>(() => ({
|
||||
state: null,
|
||||
liveValues: null,
|
||||
remaining_time,
|
||||
spin_shitter,
|
||||
}));
|
||||
|
||||
// ========== Message Handler ==========
|
||||
export function vacuumTestMachineMessageHandler(
|
||||
store: StoreApi<VacuumNamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<VacuumNamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const updateStore = (
|
||||
updater: (
|
||||
state: VacuumNamespaceStore,
|
||||
) => VacuumNamespaceStore,
|
||||
) => throttledUpdater.updateWith(updater);
|
||||
|
||||
try {
|
||||
if (event.name === "StateEvent")
|
||||
{
|
||||
console.log("STATE EVENT");
|
||||
|
||||
const stateEvent = stateEventSchema.parse(event);
|
||||
|
||||
const rpm = Math.floor(Math.random() * (125 - 115 + 1)) + 115;
|
||||
|
||||
const value: TimeSeriesValue = {
|
||||
value: stateEvent.data.running ? rpm : 0,
|
||||
timestamp: event.ts,
|
||||
};
|
||||
|
||||
updateStore((state) => ({
|
||||
...state,
|
||||
state: stateEvent.data,
|
||||
spin_shitter: addSpinShitter(state.spin_shitter, value)
|
||||
}));
|
||||
}
|
||||
|
||||
else if (event.name === "LiveValuesEvent")
|
||||
{
|
||||
console.log("LIVE VALUES EVENT");
|
||||
|
||||
const rpm = Math.floor(Math.random() * (125 - 115 + 1)) + 115;
|
||||
|
||||
const parsed = liveValuesEventSchema.parse(event);
|
||||
|
||||
|
||||
const rpm_value: TimeSeriesValue = {
|
||||
value: rpm,
|
||||
timestamp: event.ts,
|
||||
};
|
||||
|
||||
|
||||
const rpm_value_0: TimeSeriesValue = {
|
||||
value: 0,
|
||||
timestamp: event.ts,
|
||||
};
|
||||
|
||||
updateStore((liveValuesEvent) => ({
|
||||
...liveValuesEvent,
|
||||
liveValues: parsed.data,
|
||||
spin_shitter: addSpinShitter(liveValuesEvent.spin_shitter, liveValuesEvent.state?.running ? rpm_value : rpm_value_0),
|
||||
}));
|
||||
|
||||
if (parsed.data.remaining_time !== null)
|
||||
{
|
||||
const value: TimeSeriesValue = {
|
||||
value: parsed.data.remaining_time,
|
||||
timestamp: event.ts,
|
||||
};
|
||||
updateStore((state) => ({
|
||||
...state,
|
||||
remaining_time: addRemainingTime(state.remaining_time, value),
|
||||
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
handleUnhandledEventError(event.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${event.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Namespace Hook ==========
|
||||
const useVacuumNamespaceImplementation =
|
||||
createNamespaceHookImplementation<VacuumNamespaceStore>({
|
||||
createStore: createVacuumNamespaceStore,
|
||||
createEventHandler: vacuumTestMachineMessageHandler,
|
||||
});
|
||||
|
||||
export function useVacuumNamespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): VacuumNamespaceStore {
|
||||
const namespaceId: NamespaceId = {
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
};
|
||||
|
||||
return useVacuumNamespaceImplementation(namespaceId);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { vacuumSerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function VacuumPage(): React.JSX.Element {
|
||||
const { serial } = vacuumSerialRoute.useParams();
|
||||
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/vacuum/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:ToggleLeft",
|
||||
},
|
||||
{
|
||||
link: "config",
|
||||
activeLink: "config",
|
||||
title: "Config",
|
||||
icon: "lu:Settings2",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { vacuumSerialRoute } from "@/routes/routes";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import {
|
||||
useVacuumNamespace,
|
||||
StateEvent,
|
||||
LiveValuesEvent,
|
||||
Mode,
|
||||
} from "./MachineNamespace";
|
||||
import { useMachineMutate } from "@/client/useClient";
|
||||
import { produce } from "immer";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { vacuum } from "@/machines/properties";
|
||||
import { z } from "zod";
|
||||
|
||||
export function useVacuum() {
|
||||
const { serial: serialString } = vacuumSerialRoute.useParams();
|
||||
|
||||
// Memoize machine identification
|
||||
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 {
|
||||
machine_identification: vacuum.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
// Namespace state from backend
|
||||
const { state, liveValues, remaining_time, spin_shitter } = useVacuumNamespace(
|
||||
machineIdentification,
|
||||
);
|
||||
|
||||
// Optimistic state
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
useEffect(() => {
|
||||
if (state) stateOptimistic.setReal(state);
|
||||
}, [state, stateOptimistic]);
|
||||
|
||||
// Generic mutation sender
|
||||
const { request: sendMutation } = useMachineMutate(
|
||||
z.object({
|
||||
action: z.string(),
|
||||
value: z.any(),
|
||||
}),
|
||||
);
|
||||
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
serverRequest?: () => void,
|
||||
) => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState)
|
||||
stateOptimistic.setOptimistic(produce(currentState, producer));
|
||||
serverRequest?.();
|
||||
};
|
||||
|
||||
const setMode = (mode: Mode) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.mode = mode;
|
||||
},
|
||||
() =>
|
||||
sendMutation({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { action: "SetMode", value: mode },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setIntervalTimeOff = (value: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.interval_time_off = value;
|
||||
},
|
||||
() =>
|
||||
sendMutation({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { action: "SetIntervalTimeOff", value: value },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setIntervalTimeOn = (value: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.interval_time_on = value;
|
||||
},
|
||||
() =>
|
||||
sendMutation({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { action: "SetIntervalTimeOn", value: value },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
state: stateOptimistic.value,
|
||||
liveValues,
|
||||
remaining_time,
|
||||
spin_shitter,
|
||||
setMode,
|
||||
setIntervalTimeOff,
|
||||
setIntervalTimeOn
|
||||
};
|
||||
}
|
||||
@@ -70,11 +70,18 @@ import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestM
|
||||
import { TestMotorPage } from "@/machines/motor_test_machine/TestMotorPage";
|
||||
import { TestMotorControlPage } from "@/machines/motor_test_machine/TestMotorControlPage";
|
||||
|
||||
// vacuum
|
||||
import { VacuumPage } from "@/machines/vacuum/Page";
|
||||
import { VacuumControlPage } from "@/machines/vacuum/ControlPage";
|
||||
|
||||
|
||||
// ---
|
||||
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 { VacuumConfigPage } from "@/machines/vacuum/ConfigPage";
|
||||
|
||||
// make a route tree like this
|
||||
// _mainNavigation/machines/winder2/$serial/control
|
||||
@@ -144,6 +151,26 @@ export const ip20TestMachineControlRoute = createRoute({
|
||||
component: () => <IP20TestMachineControlPage />,
|
||||
});
|
||||
|
||||
// vacuum
|
||||
export const vacuumSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "vacuum/$serial",
|
||||
component: () => <VacuumPage />,
|
||||
});
|
||||
|
||||
export const vacuumControlRoute = createRoute({
|
||||
getParentRoute: () => vacuumSerialRoute,
|
||||
path: "control",
|
||||
component: () => <VacuumControlPage />,
|
||||
});
|
||||
|
||||
export const vacuumConfigRoute = createRoute({
|
||||
getParentRoute: () => vacuumSerialRoute,
|
||||
path: "config",
|
||||
component: () => <VacuumConfigPage />,
|
||||
});
|
||||
|
||||
// ---
|
||||
export const testMotorSerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "testmotor/$serial",
|
||||
@@ -499,6 +526,11 @@ export const rootTree = RootRoute.addChildren([
|
||||
|
||||
ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]),
|
||||
|
||||
vacuumSerialRoute.addChildren([
|
||||
vacuumControlRoute,
|
||||
vacuumConfigRoute,
|
||||
]),
|
||||
|
||||
testMotorSerialRoute.addChildren([testMotorControlRoute]),
|
||||
|
||||
aquapath1SerialRoute.addChildren([
|
||||
|
||||
@@ -35,6 +35,7 @@ pub mod test_machine;
|
||||
pub mod wago_ai_test_machine;
|
||||
pub mod wago_power;
|
||||
pub mod winder2;
|
||||
pub mod vacuum;
|
||||
|
||||
pub const VENDOR_QITECH: u16 = 0x0001;
|
||||
pub const MACHINE_WINDER_V1: u16 = 0x0002;
|
||||
@@ -50,6 +51,7 @@ pub const IP20_TEST_MACHINE: u16 = 0x0034;
|
||||
pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035;
|
||||
pub const WAGO_AI_TEST_MACHINE: u16 = 0x0036;
|
||||
pub const DIGITAL_INPUT_TEST_MACHINE: u16 = 0x0040;
|
||||
pub const MACHINE_VACUUM: u16 = 0x0037;
|
||||
|
||||
pub const MOTOR_TEST_MACHINE: u16 = 0x0011;
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ impl MachineIdentification {
|
||||
x if x == IP20_TEST_MACHINE => "ip20_test_machine".to_string(),
|
||||
x if x == ANALOG_INPUT_TEST_MACHINE => "analog_input_test_machine".to_string(),
|
||||
x if x == WAGO_AI_TEST_MACHINE => "wago_ai_test_machine".to_string(),
|
||||
x if x == MACHINE_VACUUM => "vacuum".to_string(),
|
||||
_ => unreachable!("Unknown machine id"),
|
||||
}
|
||||
}
|
||||
@@ -182,6 +183,7 @@ use crate::MACHINE_EXTRUDER_V1;
|
||||
use crate::MACHINE_EXTRUDER_V2;
|
||||
use crate::MACHINE_LASER_V1;
|
||||
use crate::MACHINE_MOCK;
|
||||
use crate::MACHINE_VACUUM;
|
||||
use crate::MACHINE_WAGO_POWER_V1;
|
||||
use crate::MACHINE_WINDER_V1;
|
||||
use crate::TEST_MACHINE;
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::{
|
||||
winder2::Winder2,
|
||||
};
|
||||
|
||||
use crate::vacuum::VacuumMachine;
|
||||
use crate::test_machine::TestMachine;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
@@ -137,6 +138,11 @@ lazy_static! {
|
||||
|
||||
mc.register::<DigitalInputTestMachine>(DigitalInputTestMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
mc.register::<VacuumMachine>(VacuumMachine::MACHINE_IDENTIFICATION);
|
||||
|
||||
|
||||
mc
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use super::VacuumMachine;
|
||||
use crate::{MachineAct, MachineMessage, MachineValues};
|
||||
use std::{ops::Add, time::{Duration, Instant}};
|
||||
|
||||
impl MachineAct for VacuumMachine
|
||||
{
|
||||
fn act(&mut self, now: Instant)
|
||||
{
|
||||
if let Ok(msg) = self.api_receiver.try_recv()
|
||||
{
|
||||
self.act_machine_message(msg);
|
||||
}
|
||||
|
||||
// Emit state at 30 Hz
|
||||
if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0)
|
||||
{
|
||||
|
||||
match self.mode
|
||||
{
|
||||
super::Mode::Idle => {},
|
||||
super::Mode::On => {},
|
||||
super::Mode::Auto => {},
|
||||
super::Mode::Interval =>
|
||||
{
|
||||
|
||||
|
||||
match self.interval_state
|
||||
{
|
||||
true =>
|
||||
{
|
||||
if Instant::now() >= self.interval_expiry
|
||||
{
|
||||
self.interval_expiry = Instant::now().add(Duration::from_secs_f64(self.interval_time_off));
|
||||
self.interval_state = false;
|
||||
self.set_running(false);
|
||||
}
|
||||
},
|
||||
false =>
|
||||
{
|
||||
if Instant::now() >= self.interval_expiry
|
||||
{
|
||||
self.interval_expiry = Instant::now().add(Duration::from_secs_f64(self.interval_time_on));
|
||||
self.interval_state = true;
|
||||
self.set_running(true);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
self.last_state_emit = now;
|
||||
}
|
||||
|
||||
// Emit live values at 10 Hz
|
||||
if now.duration_since(self.last_live_values_emit) > Duration::from_secs_f64(1.0 / 10.0)
|
||||
{
|
||||
self.emit_live_values();
|
||||
self.last_live_values_emit = now;
|
||||
}
|
||||
}
|
||||
|
||||
fn act_machine_message(&mut self, msg: MachineMessage)
|
||||
{
|
||||
match msg
|
||||
{
|
||||
MachineMessage::SubscribeNamespace(namespace) =>
|
||||
{
|
||||
self.namespace.namespace = Some(namespace);
|
||||
self.emit_state();
|
||||
self.emit_live_values();
|
||||
}
|
||||
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
|
||||
MachineMessage::HttpApiJsonRequest(value) =>
|
||||
{
|
||||
use crate::MachineApi;
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) =>
|
||||
{
|
||||
// Does not connect to any Machine; do nothing
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) =>
|
||||
{
|
||||
// Does not connect to any Machine; do nothing
|
||||
}
|
||||
MachineMessage::RequestValues(sender) =>
|
||||
{
|
||||
sender
|
||||
.send_blocking(MachineValues
|
||||
{
|
||||
state: serde_json::to_value(self.get_state())
|
||||
.expect("Failed to serialize state"),
|
||||
live_values: serde_json::to_value(self.get_live_values())
|
||||
.expect("Failed to serialize live values"),
|
||||
})
|
||||
.expect("Failed to send values");
|
||||
|
||||
sender.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
use super::VacuumMachine;
|
||||
use crate::{MachineApi, MachineMessage, vacuum::Mode};
|
||||
use anyhow::anyhow;
|
||||
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 std::sync::Arc;
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct StateEvent
|
||||
{
|
||||
pub mode: Mode,
|
||||
pub interval_time_off: f64,
|
||||
pub interval_time_on: f64,
|
||||
|
||||
pub running: bool,
|
||||
}
|
||||
|
||||
impl StateEvent
|
||||
{
|
||||
pub fn build(&self) -> Event<Self>
|
||||
{
|
||||
Event::new("StateEvent", self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct LiveValuesEvent
|
||||
{
|
||||
pub remaining_time: f64,
|
||||
}
|
||||
|
||||
impl LiveValuesEvent
|
||||
{
|
||||
pub fn build(&self) -> Event<Self>
|
||||
{
|
||||
Event::new("LiveValuesEvent", self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum VacuumMachineEvents
|
||||
{
|
||||
State(Event<StateEvent>),
|
||||
LiveValues(Event<LiveValuesEvent>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(tag = "action", content = "value")]
|
||||
pub enum Mutation
|
||||
{
|
||||
SetMode(Mode),
|
||||
SetIntervalTimeOff(f64),
|
||||
SetIntervalTimeOn(f64),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VacuumMachineNamespace
|
||||
{
|
||||
pub namespace: Option<Namespace>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<VacuumMachineEvents> for VacuumMachineNamespace
|
||||
{
|
||||
fn emit(&mut self, events: VacuumMachineEvents)
|
||||
{
|
||||
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<VacuumMachineEvents> for VacuumMachineEvents
|
||||
{
|
||||
fn event_value(&self) -> GenericEvent
|
||||
{
|
||||
match self
|
||||
{
|
||||
VacuumMachineEvents::State(event) => event.clone().into(),
|
||||
VacuumMachineEvents::LiveValues(event) => event.clone().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn
|
||||
{
|
||||
cache_first_and_last_event()
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineApi for VacuumMachine
|
||||
{
|
||||
fn api_get_sender(&self) -> smol::channel::Sender<MachineMessage>
|
||||
{
|
||||
self.api_sender.clone()
|
||||
}
|
||||
|
||||
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error>
|
||||
{
|
||||
let mutation: Mutation = serde_json::from_value(request_body)?;
|
||||
|
||||
match mutation
|
||||
{
|
||||
Mutation::SetMode(value) => self.set_mode(value),
|
||||
Mutation::SetIntervalTimeOff(value) => self.set_interval_time_off(value),
|
||||
Mutation::SetIntervalTimeOn(value) => self.set_interval_time_on(value),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Option<Namespace>
|
||||
{
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
use crate::vacuum::api::{VacuumMachineEvents, LiveValuesEvent, StateEvent};
|
||||
use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique};
|
||||
use crate::{AsyncThreadMessage, MACHINE_VACUUM, Machine, MachineMessage};
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use std::ops::Add;
|
||||
use std::time::{Duration, Instant};
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod new;
|
||||
use crate::vacuum::api::VacuumMachineNamespace;
|
||||
use crate::{VENDOR_QITECH};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VacuumMachine
|
||||
{
|
||||
pub api_receiver: Receiver<MachineMessage>,
|
||||
pub api_sender: Sender<MachineMessage>,
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub namespace: VacuumMachineNamespace,
|
||||
pub last_state_emit: Instant,
|
||||
pub last_live_values_emit: Instant,
|
||||
pub outputs: [bool; 8],
|
||||
pub inputs: [bool; 8],
|
||||
pub main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
pub douts: [DigitalOutput; 8],
|
||||
pub dins: [DigitalInput; 8],
|
||||
|
||||
pub mode: Mode,
|
||||
pub interval_time_off: f64,
|
||||
pub interval_time_on: f64,
|
||||
|
||||
// machine state:
|
||||
pub interval_state: bool,
|
||||
pub interval_expiry: Instant,
|
||||
|
||||
pub running: bool,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
#[repr(u8)]
|
||||
pub enum Mode
|
||||
{
|
||||
Idle,
|
||||
On,
|
||||
Auto,
|
||||
Interval,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Mode
|
||||
{
|
||||
type Error = ();
|
||||
|
||||
fn try_from(v: u8) -> Result<Self, Self::Error>
|
||||
{
|
||||
match v
|
||||
{
|
||||
0 => Ok(Mode::Idle),
|
||||
1 => Ok(Mode::On),
|
||||
2 => Ok(Mode::Auto),
|
||||
3 => Ok(Mode::Interval),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine for VacuumMachine
|
||||
{
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique
|
||||
{
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
|
||||
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>>
|
||||
{
|
||||
self.main_sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl VacuumMachine
|
||||
{
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_VACUUM,
|
||||
};
|
||||
}
|
||||
|
||||
impl VacuumMachine
|
||||
{
|
||||
// default functions
|
||||
pub fn get_state(&self) -> StateEvent
|
||||
{
|
||||
StateEvent {
|
||||
mode: self.mode.clone(),
|
||||
interval_time_off: self.interval_time_off,
|
||||
interval_time_on: self.interval_time_on,
|
||||
running: self.running,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_state(&mut self)
|
||||
{
|
||||
let event = self.get_state().build();
|
||||
self.namespace.emit(VacuumMachineEvents::State(event));
|
||||
}
|
||||
|
||||
pub fn get_live_values(&self) -> LiveValuesEvent
|
||||
{
|
||||
LiveValuesEvent
|
||||
{
|
||||
remaining_time: match self.mode
|
||||
{
|
||||
Mode::Idle => 0.0,
|
||||
Mode::On => 0.0,
|
||||
Mode::Auto => 999.0,
|
||||
Mode::Interval => (self.interval_expiry - Instant::now()).as_secs_f64(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_live_values(&mut self)
|
||||
{
|
||||
let event = self.get_live_values().build();
|
||||
self.namespace
|
||||
.emit(VacuumMachineEvents::LiveValues(event));
|
||||
}
|
||||
|
||||
fn set_running(&mut self, value: bool)
|
||||
{
|
||||
if self.running == value { return; }
|
||||
self.douts[0].set(value);
|
||||
self.running = value;
|
||||
|
||||
tracing::warn!("set running: {}", value);
|
||||
}
|
||||
|
||||
// getter/setter for api
|
||||
pub fn set_mode(&mut self, value: Mode)
|
||||
{
|
||||
if self.mode == value { return; }
|
||||
|
||||
self.mode = value;
|
||||
|
||||
match self.mode
|
||||
{
|
||||
Mode::Idle =>
|
||||
{
|
||||
self.set_running(false);
|
||||
},
|
||||
Mode::On =>
|
||||
{
|
||||
self.set_running(true);
|
||||
},
|
||||
Mode::Auto =>
|
||||
{
|
||||
//TODO: implement auto mode
|
||||
// if self.running { self.douts[0].set(false); }
|
||||
},
|
||||
Mode::Interval =>
|
||||
{
|
||||
self.interval_state = true;
|
||||
self.interval_expiry = Instant::now().add(Duration::from_secs_f64(self.interval_time_on));
|
||||
self.set_running(true);
|
||||
},
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn set_interval_time_off(&mut self, value: f64)
|
||||
{
|
||||
if self.interval_time_off == value { return; }
|
||||
|
||||
self.interval_time_off = value.clamp(1.0, 120.0);
|
||||
|
||||
let new_expiry = Instant::now().add(Duration::from_secs_f64(value));
|
||||
|
||||
if self.mode == Mode::Interval &&
|
||||
!self.interval_state &&
|
||||
new_expiry < self.interval_expiry
|
||||
{
|
||||
self.interval_expiry = new_expiry;
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn set_interval_time_on(&mut self, value: f64)
|
||||
{
|
||||
if self.interval_time_on == value { return; }
|
||||
|
||||
self.interval_time_on = value.clamp(1.0, 120.0);
|
||||
|
||||
let new_expiry = Instant::now().add(Duration::from_secs_f64(value));
|
||||
|
||||
if self.mode == Mode::Interval &&
|
||||
self.interval_state &&
|
||||
new_expiry < self.interval_expiry
|
||||
{
|
||||
self.interval_expiry = new_expiry;
|
||||
}
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use crate::vacuum::{Mode, VacuumMachine};
|
||||
use crate::vacuum::api::VacuumMachineNamespace;
|
||||
use smol::block_on;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
|
||||
validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
|
||||
use anyhow::Error;
|
||||
use ethercat_hal::devices::wago_modules::ip20_ec_di8_do8::{
|
||||
IP20_EC_DI8_DO8_IDENTITY, IP20EcDi8Do8, IP20EcDi8Do8InputPort, IP20EcDi8Do8OutputPort,
|
||||
};
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
use ethercat_hal::io::digital_output::DigitalOutput;
|
||||
|
||||
impl MachineNewTrait for VacuumMachine {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
.iter()
|
||||
.map(|device_identification| device_identification.clone())
|
||||
.collect::<Vec<_>>();
|
||||
validate_same_machine_identification_unique(&device_identification)?;
|
||||
validate_no_role_dublicates(&device_identification)?;
|
||||
|
||||
let hardware = match ¶ms.hardware
|
||||
{
|
||||
MachineNewHardware::Ethercat(x) => x, _ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::VacuumMachine::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
block_on(async
|
||||
{
|
||||
let ip20_device = get_ethercat_device::<IP20EcDi8Do8>(
|
||||
hardware,
|
||||
params,
|
||||
0,
|
||||
[IP20_EC_DI8_DO8_IDENTITY].to_vec(),
|
||||
).await?.0;
|
||||
|
||||
// Create digital outputs
|
||||
let do1 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO1);
|
||||
let do2 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO2);
|
||||
let do3 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO3);
|
||||
let do4 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO4);
|
||||
let do5 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO5);
|
||||
let do6 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO6);
|
||||
let do7 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO7);
|
||||
let do8 = DigitalOutput::new(ip20_device.clone(), IP20EcDi8Do8OutputPort::DO8);
|
||||
|
||||
// Create digital inputs
|
||||
let di1 = DigitalInput::new(ip20_device.clone(), IP20EcDi8Do8InputPort::DI1);
|
||||
let di2 = DigitalInput::new(ip20_device.clone(), IP20EcDi8Do8InputPort::DI2);
|
||||
let di3 = DigitalInput::new(ip20_device.clone(), IP20EcDi8Do8InputPort::DI3);
|
||||
let di4 = DigitalInput::new(ip20_device.clone(), IP20EcDi8Do8InputPort::DI4);
|
||||
let di5 = DigitalInput::new(ip20_device.clone(), IP20EcDi8Do8InputPort::DI5);
|
||||
let di6 = DigitalInput::new(ip20_device.clone(), IP20EcDi8Do8InputPort::DI6);
|
||||
let di7 = DigitalInput::new(ip20_device.clone(), IP20EcDi8Do8InputPort::DI7);
|
||||
let di8 = DigitalInput::new(ip20_device.clone(), IP20EcDi8Do8InputPort::DI8);
|
||||
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut machine = Self
|
||||
{
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: VacuumMachineNamespace
|
||||
{
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_state_emit: Instant::now(),
|
||||
last_live_values_emit: Instant::now(),
|
||||
outputs: [false; 8],
|
||||
inputs: [false; 8],
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
douts: [do1, do2, do3, do4, do5, do6, do7, do8],
|
||||
dins: [di1, di2, di3, di4, di5, di6, di7, di8],
|
||||
|
||||
running: false,
|
||||
|
||||
mode: Mode::Idle,
|
||||
interval_time_off: 10.0,
|
||||
interval_time_on: 10.0,
|
||||
interval_state: false,
|
||||
interval_expiry: Instant::now(),
|
||||
};
|
||||
|
||||
machine.emit_state();
|
||||
machine.emit_live_values();
|
||||
|
||||
Ok(machine)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user