diff --git a/electron/src/machines/minimalbottlesorter/MinimalBottleSorterControlPage.tsx b/electron/src/machines/minimalbottlesorter/MinimalBottleSorterControlPage.tsx new file mode 100644 index 000000000..f2d5c75bf --- /dev/null +++ b/electron/src/machines/minimalbottlesorter/MinimalBottleSorterControlPage.tsx @@ -0,0 +1,142 @@ +import { Page } from "@/components/Page"; +import { ControlGrid } from "@/control/ControlGrid"; +import { ControlCard } from "@/control/ControlCard"; +import { Label } from "@/control/Label"; +import { SelectionGroup } from "@/control/SelectionGroup"; +import { Button } from "@/components/ui/button"; +import { EditValue } from "@/control/EditValue"; +import { Badge } from "@/components/ui/badge"; +import { useMinimalBottleSorter } from "./useMinimalBottleSorter"; +import React from "react"; + +export function MinimalBottleSorterControlPage() { + const { + state, + liveValues, + setStepperSpeed, + setStepperDirection, + setStepperEnabled, + pulseOutput, + } = useMinimalBottleSorter(); + + const safeState = state ?? { + stepper_enabled: false, + stepper_speed: 0, + stepper_direction: true, + outputs: [false, false, false, false, false, false, false, false], + }; + const safeLiveValues = liveValues ?? { + stepper_actual_speed: 0, + stepper_position: 0, + }; + + return ( + + + {/* Stepper Motor Control */} + +
+ {/* Enable/Disable */} + + + {/* Direction */} + + + {/* Speed Control */} + + + {/* Live Values */} +
+
+ Actual Speed: + + {safeLiveValues.stepper_actual_speed.toFixed(1)} steps/s + +
+
+ Position: + + {safeLiveValues.stepper_position.toLocaleString()} steps + +
+
+
+
+ + {/* Digital Output Pulses */} + +
+ {safeState.outputs.map((output, index) => ( +
+ +
+ ))} +
+
+ Click to pulse output for 100ms +
+
+
+
+ ); +} diff --git a/electron/src/machines/minimalbottlesorter/MinimalBottleSorterPage.tsx b/electron/src/machines/minimalbottlesorter/MinimalBottleSorterPage.tsx new file mode 100644 index 000000000..c3f94a08a --- /dev/null +++ b/electron/src/machines/minimalbottlesorter/MinimalBottleSorterPage.tsx @@ -0,0 +1,21 @@ +import { Topbar } from "@/components/Topbar"; +import { bottleSorterSerialRoute } from "@/routes/routes"; +import React from "react"; + +export function MinimalBottleSorterPage(): React.JSX.Element { + const { serial } = bottleSorterSerialRoute.useParams(); + + return ( + + ); +} diff --git a/electron/src/machines/minimalbottlesorter/minimalBottleSorterNamespace.ts b/electron/src/machines/minimalbottlesorter/minimalBottleSorterNamespace.ts new file mode 100644 index 000000000..6e0b4f4b0 --- /dev/null +++ b/electron/src/machines/minimalbottlesorter/minimalBottleSorterNamespace.ts @@ -0,0 +1,93 @@ +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"; + +// ========== Event Schema ========== + +export const stateEventDataSchema = z.object({ + stepper_enabled: z.boolean(), + stepper_speed: z.number(), + stepper_direction: z.boolean(), + outputs: z.array(z.boolean()).length(8), +}); + +export const liveValuesEventDataSchema = z.object({ + stepper_actual_speed: z.number(), + stepper_position: z.number(), +}); + +export const stateEventSchema = eventSchema(stateEventDataSchema); +export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema); + +export type StateEvent = z.infer; +export type LiveValuesEvent = z.infer; + +// ========== Store ========== +export type MinimalBottleSorterNamespaceStore = { + state: StateEvent | null; + liveValues: LiveValuesEvent | null; +}; + +export const createMinimalBottleSorterNamespaceStore = + (): StoreApi => + create(() => ({ + state: null, + liveValues: null, + })); + +// ========== Message Handler ========== +export function minimalBottleSorterMessageHandler( + store: StoreApi, + throttledUpdater: ThrottledStoreUpdater, +): EventHandler { + return (event: Event) => { + const updateStore = ( + updater: ( + state: MinimalBottleSorterNamespaceStore, + ) => MinimalBottleSorterNamespaceStore, + ) => throttledUpdater.updateWith(updater); + + try { + if (event.name === "StateEvent") { + const parsed = stateEventSchema.parse(event); + updateStore((current) => ({ ...current, state: parsed.data })); + } else if (event.name === "LiveValuesEvent") { + const parsed = liveValuesEventSchema.parse(event); + updateStore((current) => ({ ...current, liveValues: parsed.data })); + } else { + handleUnhandledEventError(event.name); + } + } catch (error) { + console.error(`Error processing ${event.name}:`, error); + throw error; + } + }; +} + +// ========== Namespace Hook ========== +const useMinimalBottleSorterNamespaceImplementation = + createNamespaceHookImplementation({ + createStore: createMinimalBottleSorterNamespaceStore, + createEventHandler: minimalBottleSorterMessageHandler, + }); + +export function useMinimalBottleSorterNamespace( + machine_identification_unique: MachineIdentificationUnique, +): MinimalBottleSorterNamespaceStore { + const namespaceId: NamespaceId = { + type: "machine", + machine_identification_unique, + }; + + return useMinimalBottleSorterNamespaceImplementation(namespaceId); +} diff --git a/electron/src/machines/minimalbottlesorter/useMinimalBottleSorter.ts b/electron/src/machines/minimalbottlesorter/useMinimalBottleSorter.ts new file mode 100644 index 000000000..08a21299e --- /dev/null +++ b/electron/src/machines/minimalbottlesorter/useMinimalBottleSorter.ts @@ -0,0 +1,126 @@ +import { toastError } from "@/components/Toast"; +import { useStateOptimistic } from "@/lib/useStateOptimistic"; +import { bottleSorterSerialRoute } from "@/routes/routes"; +import { MachineIdentificationUnique } from "@/machines/types"; +import { + useMinimalBottleSorterNamespace, + StateEvent, + LiveValuesEvent, +} from "./minimalBottleSorterNamespace"; +import { useMachineMutate } from "@/client/useClient"; +import { produce } from "immer"; +import { useEffect, useMemo } from "react"; +import { minimalBottleSorter } from "@/machines/properties"; +import { z } from "zod"; + +export function useMinimalBottleSorter() { + const { serial: serialString } = bottleSorterSerialRoute.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: minimalBottleSorter.machine_identification, + serial, + }; + }, [serialString]); + + // Namespace state from backend + const { state, liveValues } = useMinimalBottleSorterNamespace( + machineIdentification, + ); + + // Optimistic state + const stateOptimistic = useStateOptimistic(); + + 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 setStepperSpeed = (speed: number) => { + updateStateOptimistically( + (current) => { + current.stepper_speed = speed; + }, + () => + sendMutation({ + machine_identification_unique: machineIdentification, + data: { action: "SetStepperSpeed", value: { speed } }, + }), + ); + }; + + const setStepperDirection = (forward: boolean) => { + updateStateOptimistically( + (current) => { + current.stepper_direction = forward; + }, + () => + sendMutation({ + machine_identification_unique: machineIdentification, + data: { action: "SetStepperDirection", value: { forward } }, + }), + ); + }; + + const setStepperEnabled = (enabled: boolean) => { + updateStateOptimistically( + (current) => { + current.stepper_enabled = enabled; + }, + () => + sendMutation({ + machine_identification_unique: machineIdentification, + data: { action: "SetStepperEnabled", value: { enabled } }, + }), + ); + }; + + const pulseOutput = (index: number, duration_ms: number = 100) => { + // Don't use optimistic updates for pulses - they're brief and backend-controlled + sendMutation({ + machine_identification_unique: machineIdentification, + data: { action: "PulseOutput", value: { index, duration_ms } }, + }); + }; + + return { + state: stateOptimistic.value, + liveValues, + setStepperSpeed, + setStepperDirection, + setStepperEnabled, + pulseOutput, + }; +} diff --git a/electron/src/machines/properties.ts b/electron/src/machines/properties.ts index be0698f79..c5591ef19 100644 --- a/electron/src/machines/properties.ts +++ b/electron/src/machines/properties.ts @@ -522,6 +522,52 @@ export const ip20TestMachine: MachineProperties = { ], }; +export const minimalBottleSorter: MachineProperties = { + name: "Minimal Bottle Sorter", + version: "V1", + slug: "minimalbottlesorter", + icon: "lu:Package", + machine_identification: { + vendor: VENDOR_QITECH, + machine: 0x0036, + }, + device_roles: [ + { + role: 0, + role_label: "EK1100 Bus Coupler", + allowed_devices: [ + { + vendor_id: 2, + product_id: 0x44c2c52, + revision: 0x120000, + }, + ], + }, + { + role: 1, + role_label: "EL7041-0052 Stepper", + allowed_devices: [ + { + vendor_id: 2, + product_id: 461451346, + revision: 1048628, + }, + ], + }, + { + role: 2, + role_label: "IP20-EC-DI8-DO8", + allowed_devices: [ + { + vendor_id: 0x741, + product_id: 0x117b6722, + revision: 0x1, + }, + ], + }, + ], +}; + export const machineProperties: MachineProperties[] = [ winder2, extruder3, @@ -533,6 +579,7 @@ export const machineProperties: MachineProperties[] = [ testmachine, analogInputTestMachine, ip20TestMachine, + minimalBottleSorter, ]; export const getMachineProperties = ( diff --git a/electron/src/routes/routes.tsx b/electron/src/routes/routes.tsx index e3a332096..55589db00 100644 --- a/electron/src/routes/routes.tsx +++ b/electron/src/routes/routes.tsx @@ -61,6 +61,9 @@ import { AnalogInputTestMachineControl } from "@/machines/analoginputtestmachine import { IP20TestMachinePage } from "@/machines/ip20testmachine/IP20TestMachinePage"; import { IP20TestMachineControlPage } from "@/machines/ip20testmachine/IP20TestMachineControlPage"; +import { MinimalBottleSorterPage } from "@/machines/minimalbottlesorter/MinimalBottleSorterPage"; +import { MinimalBottleSorterControlPage } from "@/machines/minimalbottlesorter/MinimalBottleSorterControlPage"; + import { MetricsGraphsPage } from "@/metrics/MetricsGraphsPage"; import { MetricsControlPage } from "@/metrics/MetricsControlPage"; @@ -108,6 +111,18 @@ export const ip20TestMachineControlRoute = createRoute({ component: () => , }); +export const bottleSorterSerialRoute = createRoute({ + getParentRoute: () => machinesRoute, + path: "minimalbottlesorter/$serial", + component: () => , +}); + +export const bottleSorterControlRoute = createRoute({ + getParentRoute: () => bottleSorterSerialRoute, + path: "control", + component: () => , +}); + export const sidebarRoute = createRoute({ getParentRoute: () => RootRoute, path: "_sidebar", @@ -426,6 +441,8 @@ export const rootTree = RootRoute.addChildren([ ip20TestMachineSerialRoute.addChildren([ip20TestMachineControlRoute]), + bottleSorterSerialRoute.addChildren([bottleSorterControlRoute]), + aquapath1SerialRoute.addChildren([ aquapath1ControlRoute, aquapath1GraphRoute, diff --git a/machines/src/lib.rs b/machines/src/lib.rs index 47a2e62df..121e1f339 100644 --- a/machines/src/lib.rs +++ b/machines/src/lib.rs @@ -24,6 +24,7 @@ pub mod extruder2; pub mod ip20_test_machine; pub mod laser; pub mod machine_identification; +pub mod minimal_bottle_sorter; pub mod mock; pub mod registry; pub mod serial; @@ -42,6 +43,7 @@ pub const MACHINE_EXTRUDER_V2: u16 = 0x0016; pub const TEST_MACHINE: u16 = 0x0033; pub const IP20_TEST_MACHINE: u16 = 0x0034; pub const ANALOG_INPUT_TEST_MACHINE: u16 = 0x0035; +pub const MINIMAL_BOTTLE_SORTER: u16 = 0x0036; use serde_json::Value; use smol::lock::RwLock; diff --git a/machines/src/minimal_bottle_sorter/act.rs b/machines/src/minimal_bottle_sorter/act.rs new file mode 100644 index 000000000..cc288bc0e --- /dev/null +++ b/machines/src/minimal_bottle_sorter/act.rs @@ -0,0 +1,62 @@ +use super::MinimalBottleSorter; +use crate::{MachineAct, MachineMessage}; +use std::time::{Duration, Instant}; + +impl MachineAct for MinimalBottleSorter { + fn act(&mut self, now: Instant) { + if let Ok(msg) = self.api_receiver.try_recv() { + self.act_machine_message(msg); + } + + // Update output pulses (check every ~10ms) + static mut LAST_PULSE_UPDATE: Option = None; + unsafe { + let should_update = match LAST_PULSE_UPDATE { + Some(last) => now.duration_since(last) > Duration::from_millis(10), + None => true, + }; + + if should_update { + let delta_ms = match LAST_PULSE_UPDATE { + Some(last) => now.duration_since(last).as_millis() as u32, + None => 10, + }; + self.update_pulses(delta_ms); + LAST_PULSE_UPDATE = Some(now); + } + } + + // Emit state at 30 Hz + if now.duration_since(self.last_state_emit) > Duration::from_secs_f64(1.0 / 30.0) { + 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 + } + } + } +} diff --git a/machines/src/minimal_bottle_sorter/api.rs b/machines/src/minimal_bottle_sorter/api.rs new file mode 100644 index 000000000..7ddb98f28 --- /dev/null +++ b/machines/src/minimal_bottle_sorter/api.rs @@ -0,0 +1,101 @@ +use super::MinimalBottleSorter; +use crate::{MachineApi, MachineMessage}; +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 stepper_enabled: bool, + pub stepper_speed: f64, + pub stepper_direction: bool, + pub outputs: [bool; 8], +} + +impl StateEvent { + pub fn build(&self) -> Event { + Event::new("StateEvent", self.clone()) + } +} + +#[derive(Serialize, Debug, Clone)] +pub struct LiveValuesEvent { + pub stepper_actual_speed: f64, + pub stepper_position: i128, +} + +impl LiveValuesEvent { + pub fn build(&self) -> Event { + Event::new("LiveValuesEvent", self.clone()) + } +} + +pub enum MinimalBottleSorterEvents { + State(Event), + LiveValues(Event), +} + +#[derive(Deserialize)] +#[serde(tag = "action", content = "value")] +pub enum Mutation { + SetStepperSpeed { speed: f64 }, + SetStepperDirection { forward: bool }, + SetStepperEnabled { enabled: bool }, + PulseOutput { index: usize, duration_ms: u32 }, +} + +#[derive(Debug, Clone)] +pub struct MinimalBottleSorterNamespace { + pub namespace: Option, +} + +impl NamespaceCacheingLogic for MinimalBottleSorterNamespace { + fn emit(&mut self, events: MinimalBottleSorterEvents) { + 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 for MinimalBottleSorterEvents { + fn event_value(&self) -> GenericEvent { + match self { + MinimalBottleSorterEvents::State(event) => event.clone().into(), + MinimalBottleSorterEvents::LiveValues(event) => event.clone().into(), + } + } + + fn event_cache_fn(&self) -> CacheFn { + cache_first_and_last_event() + } +} + +impl MachineApi for MinimalBottleSorter { + fn api_get_sender(&self) -> smol::channel::Sender { + 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::SetStepperSpeed { speed } => self.set_stepper_speed(speed), + Mutation::SetStepperDirection { forward } => self.set_stepper_direction(forward), + Mutation::SetStepperEnabled { enabled } => self.set_stepper_enabled(enabled), + Mutation::PulseOutput { index, duration_ms } => self.pulse_output(index, duration_ms), + } + + Ok(()) + } + + fn api_event_namespace(&mut self) -> Option { + self.namespace.namespace.clone() + } +} diff --git a/machines/src/minimal_bottle_sorter/mod.rs b/machines/src/minimal_bottle_sorter/mod.rs new file mode 100644 index 000000000..b1fb930ec --- /dev/null +++ b/machines/src/minimal_bottle_sorter/mod.rs @@ -0,0 +1,135 @@ +use crate::machine_identification::{MachineIdentification, MachineIdentificationUnique}; +use crate::minimal_bottle_sorter::api::{LiveValuesEvent, MinimalBottleSorterEvents, StateEvent}; +use crate::{AsyncThreadMessage, Machine, MachineMessage}; +use control_core::socketio::namespace::NamespaceCacheingLogic; +use ethercat_hal::io::digital_output::DigitalOutput; +use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1; +use smol::channel::{Receiver, Sender}; +use std::time::Instant; +pub mod act; +pub mod api; +pub mod new; +use crate::minimal_bottle_sorter::api::MinimalBottleSorterNamespace; +use crate::{MINIMAL_BOTTLE_SORTER, VENDOR_QITECH}; + +#[derive(Debug)] +pub struct MinimalBottleSorter { + pub api_receiver: Receiver, + pub api_sender: Sender, + pub machine_identification_unique: MachineIdentificationUnique, + pub namespace: MinimalBottleSorterNamespace, + pub last_state_emit: Instant, + pub last_live_values_emit: Instant, + pub stepper_enabled: bool, + pub stepper_speed: f64, // mm/s + pub stepper_direction: bool, // true = forward, false = backward + pub outputs: [bool; 8], + pub pulse_remaining: [u32; 8], // remaining milliseconds for pulse + pub main_sender: Option>, + pub stepper: StepperVelocityEL70x1, + pub douts: [DigitalOutput; 8], +} + +impl Machine for MinimalBottleSorter { + fn get_machine_identification_unique(&self) -> MachineIdentificationUnique { + self.machine_identification_unique.clone() + } + + fn get_main_sender(&self) -> Option> { + self.main_sender.clone() + } +} + +impl MinimalBottleSorter { + pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification { + vendor: VENDOR_QITECH, + machine: MINIMAL_BOTTLE_SORTER, + }; +} + +impl MinimalBottleSorter { + pub fn emit_state(&mut self) { + let event = StateEvent { + stepper_enabled: self.stepper_enabled, + stepper_speed: self.stepper_speed, + stepper_direction: self.stepper_direction, + outputs: self.outputs, + } + .build(); + + self.namespace.emit(MinimalBottleSorterEvents::State(event)); + } + + pub fn emit_live_values(&mut self) { + let event = LiveValuesEvent { + stepper_actual_speed: self.stepper.get_speed() as f64, + stepper_position: self.stepper.get_position(), + } + .build(); + + self.namespace + .emit(MinimalBottleSorterEvents::LiveValues(event)); + } + + /// Set stepper speed in mm/s + pub fn set_stepper_speed(&mut self, speed_mm_s: f64) { + self.stepper_speed = speed_mm_s.abs().max(0.0).min(100.0); // Limit to 0-100 mm/s + self.update_stepper(); + self.emit_state(); + } + + /// Set stepper direction + pub fn set_stepper_direction(&mut self, forward: bool) { + self.stepper_direction = forward; + self.update_stepper(); + self.emit_state(); + } + + /// Enable/disable stepper motor + pub fn set_stepper_enabled(&mut self, enabled: bool) { + self.stepper_enabled = enabled; + self.stepper.set_enabled(enabled); + self.emit_state(); + } + + /// Update stepper motor speed based on current settings + fn update_stepper(&mut self) { + if self.stepper_enabled { + // Convert mm/s to steps/s (assuming 200 steps per revolution and some mechanical conversion) + // This is a placeholder - adjust based on your actual mechanical setup + let steps_per_mm = 100.0; // Adjust this value based on your setup + let speed_steps_s = self.stepper_speed * steps_per_mm; + let signed_speed = if self.stepper_direction { + speed_steps_s + } else { + -speed_steps_s + }; + let _ = self.stepper.set_speed(signed_speed); + } + } + + /// Trigger a pulse on a digital output (turn on briefly then off) + pub fn pulse_output(&mut self, index: usize, duration_ms: u32) { + if index < 8 { + self.outputs[index] = true; + self.douts[index].set(true); + self.pulse_remaining[index] = duration_ms; + self.emit_state(); + } + } + + /// Update pulse timers and turn off outputs when time expires + pub fn update_pulses(&mut self, delta_ms: u32) { + for i in 0..8 { + if self.pulse_remaining[i] > 0 { + if self.pulse_remaining[i] <= delta_ms { + self.pulse_remaining[i] = 0; + self.outputs[i] = false; + self.douts[i].set(false); + } else { + self.pulse_remaining[i] -= delta_ms; + } + } + } + } +} diff --git a/machines/src/minimal_bottle_sorter/new.rs b/machines/src/minimal_bottle_sorter/new.rs new file mode 100644 index 000000000..5f6328d54 --- /dev/null +++ b/machines/src/minimal_bottle_sorter/new.rs @@ -0,0 +1,136 @@ +use crate::minimal_bottle_sorter::MinimalBottleSorter; +use crate::minimal_bottle_sorter::api::MinimalBottleSorterNamespace; +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::coe::ConfigurableDevice; +use ethercat_hal::devices::EthercatDeviceUsed; +use ethercat_hal::devices::ek1100::{EK1100, EK1100_IDENTITY_A}; +use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration; +use ethercat_hal::devices::el7041_0052::pdo::EL7041_0052PredefinedPdoAssignment; +use ethercat_hal::devices::el7041_0052::{EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port}; +use ethercat_hal::devices::wago_modules::ip20_ec_di8_do8::{ + IP20_EC_DI8_DO8_IDENTITY, IP20EcDi8Do8, IP20EcDi8Do8OutputPort, +}; +use ethercat_hal::io::digital_output::DigitalOutput; +use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1; +use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration}; + +impl MachineNewTrait for MinimalBottleSorter { + fn new<'maindevice>(params: &MachineNewParams) -> Result { + // validate general stuff + let device_identification = params + .device_group + .iter() + .map(|device_identification| device_identification.clone()) + .collect::>(); + 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!( + "[{}::MachineNewTrait/MinimalBottleSorter::new] MachineNewHardware is not Ethercat", + module_path!() + )); + } + }; + + block_on(async { + // Role 0: EK1100 EtherCAT Coupler + get_ethercat_device::(hardware, params, 0, vec![EK1100_IDENTITY_A]).await?; + + // Role 1: EL7041-0052 Stepper Motor Terminal + let (el7041, subdevice) = get_ethercat_device::( + hardware, + params, + 1, + vec![EL7041_0052_IDENTITY_A], + ) + .await?; + + let el7041_config = EL7041_0052Configuration { + stm_features: ethercat_hal::shared_config::el70x1::StmFeatures { + operation_mode: EL70x1OperationMode::DirectVelocity, + speed_range: ethercat_hal::shared_config::el70x1::EL70x1SpeedRange::Steps1000, + ..Default::default() + }, + stm_motor: StmMotorConfiguration { + max_current: 1500, // 1.5A + ..Default::default() + }, + pdo_assignment: EL7041_0052PredefinedPdoAssignment::VelocityControlCompact, + ..Default::default() + }; + + el7041 + .write() + .await + .write_config(&subdevice, &el7041_config) + .await?; + { + let mut device_guard = el7041.write().await; + device_guard.set_used(true); + } + + let stepper = StepperVelocityEL70x1::new(el7041.clone(), EL7041_0052Port::STM1); + + // Role 2: IP20 DI8/DO8 Module + let ip20_device = get_ethercat_device::( + hardware, + params, + 2, + [IP20_EC_DI8_DO8_IDENTITY].to_vec(), + ) + .await? + .0; + + { + let mut device_guard = ip20_device.write().await; + device_guard.set_used(true); + } + + // 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); + + 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: MinimalBottleSorterNamespace { + namespace: params.namespace.clone(), + }, + last_state_emit: Instant::now(), + last_live_values_emit: Instant::now(), + stepper_enabled: false, + stepper_speed: 0.0, + stepper_direction: true, + outputs: [false; 8], + pulse_remaining: [0; 8], + main_sender: params.main_thread_channel.clone(), + stepper, + douts: [do1, do2, do3, do4, do5, do6, do7, do8], + }; + + machine.emit_state(); + machine.emit_live_values(); + + Ok(machine) + }) + } +} diff --git a/machines/src/registry.rs b/machines/src/registry.rs index 99d918084..5c2924040 100644 --- a/machines/src/registry.rs +++ b/machines/src/registry.rs @@ -7,6 +7,7 @@ use crate::{ use crate::{ Machine, MachineNewParams, analog_input_test_machine::AnalogInputTestMachine, ip20_test_machine::IP20TestMachine, machine_identification::MachineIdentification, + minimal_bottle_sorter::MinimalBottleSorter, }; #[cfg(not(feature = "mock-machine"))] @@ -124,6 +125,7 @@ lazy_static! { mc.register::(TestMachine::MACHINE_IDENTIFICATION); mc.register::(IP20TestMachine::MACHINE_IDENTIFICATION); mc.register::(AnalogInputTestMachine::MACHINE_IDENTIFICATION); + mc.register::(MinimalBottleSorter::MACHINE_IDENTIFICATION); mc };