Compare commits

...
Author SHA1 Message Date
just-some-entity cae322f312 set correct page for config 2026-01-30 16:13:12 +00:00
just-some-entity 7eeb18af94 copy from broken branch 2026-01-30 16:00:09 +00:00
14 changed files with 1045 additions and 0 deletions

No files matched your search

+25
View File
@@ -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);
}
+27
View File
@@ -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",
},
]}
/>
);
}
+120
View File
@@ -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
};
}
+32
View File
@@ -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([
+2
View File
@@ -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;
+2
View File
@@ -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;
+6
View File
@@ -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
};
}
+103
View File
@@ -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();
}
}
}
}
+122
View File
@@ -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()
}
}
+209
View File
@@ -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();
}
}
+101
View File
@@ -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 &params.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)
})
}
}