Add addon motor support with state management and control functions, remove cutting unit

This commit is contained in:
Christian committed 2026-02-16 16:01:17 +01:00
1 parent 315ed1b31c
commit eb085fa80c
9 files changed
+530 -72

No files matched your search

@@ -25,9 +25,8 @@ export function GluetexAddonsPage() {
defaultState,
isLoading,
isDisabled,
setStepper2Mode,
setStepper34Mode,
setCuttingUnitMode,
setStepper3Mode,
setStepper4Mode,
setHeatingMode,
temperature1,
temperature2,
@@ -53,12 +52,12 @@ export function GluetexAddonsPage() {
<Page>
<ControlGrid>
<ControlCard className="bg-red" title="Motors">
<Label label="Stepper 2">
<Label label="Stepper 3">
<SelectionGroup<StepperMode>
value={state?.stepper_state?.stepper2_mode}
value={state?.stepper_state?.stepper3_mode}
disabled={isDisabled}
loading={isLoading}
onChange={setStepper2Mode}
onChange={setStepper3Mode}
orientation="horizontal"
className="grid grid-cols-2 gap-2"
options={{
@@ -75,34 +74,12 @@ export function GluetexAddonsPage() {
}}
/>
</Label>
<Label label="Stepper 3&4">
<Label label="Stepper 4">
<SelectionGroup<StepperMode>
value={state?.stepper_state?.stepper34_mode}
value={state?.stepper_state?.stepper4_mode}
disabled={isDisabled}
loading={isLoading}
onChange={setStepper34Mode}
orientation="horizontal"
className="grid grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:Power",
isActiveClassName: "bg-green-600",
},
Run: {
children: "Run",
icon: "lu:Play",
isActiveClassName: "bg-green-600",
},
}}
/>
</Label>
<Label label="Cutting Unit">
<SelectionGroup<StepperMode>
value={state?.stepper_state?.cutting_unit_mode}
disabled={isDisabled}
loading={isLoading}
onChange={setCuttingUnitMode}
onChange={setStepper4Mode}
orientation="horizontal"
className="grid grid-cols-2 gap-2"
options={{
@@ -213,6 +213,15 @@ export const heatingStatesSchema = z.object({
zone_6: heatingStateSchema,
});
/**
* Addon motor state schema (from backend)
*/
export const addonMotorStateSchema = z.object({
enabled: z.boolean(),
master_ratio: z.number(),
slave_ratio: z.number(),
});
/**
* Consolidated state event schema (state changes only) - from backend
*/
@@ -226,6 +235,8 @@ export const stateEventDataSchema = z.object({
spool_automatic_action_state: spoolAutomaticActionStateSchema,
heating_states: heatingStatesSchema,
connected_machine_state: connectedMachineStateSchema,
addon_motor_3_state: addonMotorStateSchema,
addon_motor_4_state: addonMotorStateSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -253,11 +264,34 @@ export type MotorRatiosState = {
};
export type StepperState = {
stepper2_mode: StepperMode;
stepper34_mode: StepperMode;
cutting_unit_mode: StepperMode;
stepper3_mode: StepperMode;
stepper4_mode: StepperMode;
};
/**
* Helper to convert backend addon motor state to frontend MotorRatiosState
*/
function getMotorRatiosFromBackend(
motor3: z.infer<typeof addonMotorStateSchema>,
motor4: z.infer<typeof addonMotorStateSchema>,
): MotorRatiosState {
return {
stepper3_master: motor3.master_ratio,
stepper3_slave: motor3.slave_ratio,
stepper4_master: motor4.master_ratio,
stepper4_slave: motor4.slave_ratio,
};
}
/**
* Helper to determine stepper4 mode from backend enabled state
*/
function getStepper4ModeFromBackend(
motor4: z.infer<typeof addonMotorStateSchema>,
): StepperMode {
return motor4.enabled ? "Run" : "Standby";
}
export type HeatingState = {
heating_mode: HeatingMode;
};
@@ -388,9 +422,8 @@ const DEFAULT_ADDON_STATE = {
stepper4_slave: 1.0,
},
stepper_state: {
stepper2_mode: "Standby" as StepperMode,
stepper34_mode: "Standby" as StepperMode,
cutting_unit_mode: "Standby" as StepperMode,
stepper3_mode: "Standby" as StepperMode,
stepper4_mode: "Standby" as StepperMode,
},
heating_state: {
heating_mode: "Standby" as HeatingMode,
@@ -472,19 +505,32 @@ export function gluetexMessageHandler(
const stateEvent = stateEventSchema.parse(event);
updateStore((state) => {
// Extend backend state with addon state
// Derive motor ratios from backend addon motor state
const motorRatiosState = getMotorRatiosFromBackend(
stateEvent.data.addon_motor_3_state,
stateEvent.data.addon_motor_4_state,
);
// Derive stepper4 mode from backend enabled state
const stepper4Mode = getStepper4ModeFromBackend(
stateEvent.data.addon_motor_4_state,
);
// Extend backend state with addon state (some local, some derived from backend)
const extendedData: ExtendedStateEventData = {
...stateEvent.data,
// Preserve existing addon state or use defaults
// Preserve existing local-only addon state
slave_puller_state:
state.state?.data.slave_puller_state ||
DEFAULT_ADDON_STATE.slave_puller_state,
motor_ratios_state:
state.state?.data.motor_ratios_state ||
DEFAULT_ADDON_STATE.motor_ratios_state,
stepper_state:
state.state?.data.stepper_state ||
DEFAULT_ADDON_STATE.stepper_state,
// Derive from backend addon motor state
motor_ratios_state: motorRatiosState,
stepper_state: {
stepper3_mode:
state.state?.data.stepper_state.stepper3_mode ||
DEFAULT_ADDON_STATE.stepper_state.stepper3_mode,
stepper4_mode: stepper4Mode,
},
heating_state:
state.state?.data.heating_state ||
DEFAULT_ADDON_STATE.heating_state,
@@ -178,6 +178,28 @@ export function useGluetex() {
}),
);
// Addon Motor 3 mutations
const { request: requestAddonMotor3SetEnabled } = useMachineMutation(
z.object({ SetAddonMotor3Enabled: z.boolean() }),
);
const { request: requestAddonMotor3SetMasterRatio } = useMachineMutation(
z.object({ SetAddonMotor3MasterRatio: z.number() }),
);
const { request: requestAddonMotor3SetSlaveRatio } = useMachineMutation(
z.object({ SetAddonMotor3SlaveRatio: z.number() }),
);
// Addon Motor 4 mutations
const { request: requestAddonMotor4SetEnabled } = useMachineMutation(
z.object({ SetAddonMotor4Enabled: z.boolean() }),
);
const { request: requestAddonMotor4SetMasterRatio } = useMachineMutation(
z.object({ SetAddonMotor4MasterRatio: z.number() }),
);
const { request: requestAddonMotor4SetSlaveRatio } = useMachineMutation(
z.object({ SetAddonMotor4SlaveRatio: z.number() }),
);
// ========== Helper Functions ==========
// Helper function for optimistic updates using produce
@@ -596,22 +618,25 @@ export function useGluetex() {
// ========== Action Functions (Addon Features - Local Only) ==========
const setStepper2Mode = (mode: StepperMode) => {
const setStepper3Mode = (mode: StepperMode) => {
updateStateLocally((current) => {
current.data.stepper_state.stepper2_mode = mode;
current.data.stepper_state.stepper3_mode = mode;
});
};
const setStepper34Mode = (mode: StepperMode) => {
updateStateLocally((current) => {
current.data.stepper_state.stepper34_mode = mode;
});
};
const setCuttingUnitMode = (mode: StepperMode) => {
updateStateLocally((current) => {
current.data.stepper_state.cutting_unit_mode = mode;
});
const setStepper4Mode = (mode: StepperMode) => {
const enabled = mode === "Run";
updateStateOptimistically(
(current) => {
current.data.stepper_state.stepper4_mode = mode;
},
() => {
requestAddonMotor4SetEnabled({
machine_identification_unique: machineIdentification,
data: { SetAddonMotor4Enabled: enabled },
});
},
);
};
const setHeatingMode = (mode: HeatingMode) => {
@@ -645,27 +670,55 @@ export function useGluetex() {
};
const setStepper3Master = (value: number) => {
updateStateLocally((current) => {
current.data.motor_ratios_state.stepper3_master = value;
});
updateStateOptimistically(
(current) => {
current.data.motor_ratios_state.stepper3_master = value;
},
() =>
requestAddonMotor3SetMasterRatio({
machine_identification_unique: machineIdentification,
data: { SetAddonMotor3MasterRatio: value },
}),
);
};
const setStepper3Slave = (value: number) => {
updateStateLocally((current) => {
current.data.motor_ratios_state.stepper3_slave = value;
});
updateStateOptimistically(
(current) => {
current.data.motor_ratios_state.stepper3_slave = value;
},
() =>
requestAddonMotor3SetSlaveRatio({
machine_identification_unique: machineIdentification,
data: { SetAddonMotor3SlaveRatio: value },
}),
);
};
const setStepper4Master = (value: number) => {
updateStateLocally((current) => {
current.data.motor_ratios_state.stepper4_master = value;
});
updateStateOptimistically(
(current) => {
current.data.motor_ratios_state.stepper4_master = value;
},
() =>
requestAddonMotor4SetMasterRatio({
machine_identification_unique: machineIdentification,
data: { SetAddonMotor4MasterRatio: value },
}),
);
};
const setStepper4Slave = (value: number) => {
updateStateLocally((current) => {
current.data.motor_ratios_state.stepper4_slave = value;
});
updateStateOptimistically(
(current) => {
current.data.motor_ratios_state.stepper4_slave = value;
},
() =>
requestAddonMotor4SetSlaveRatio({
machine_identification_unique: machineIdentification,
data: { SetAddonMotor4SlaveRatio: value },
}),
);
};
// ========== Machine Filtering ==========
@@ -767,9 +820,8 @@ export function useGluetex() {
disconnectMachine,
// Addon action functions (local only)
setStepper2Mode,
setStepper34Mode,
setCuttingUnitMode,
setStepper3Mode,
setStepper4Mode,
setHeatingMode,
setTemperature1Min,
setTemperature1Max,
+4
View File
@@ -15,6 +15,10 @@ impl MachineAct for Gluetex {
// sync the puller speed
self.sync_puller_speed(now);
// sync addon motor speeds
self.sync_addon_motor_3_speed(now);
self.sync_addon_motor_4_speed(now);
// sync the traverse speed
self.sync_traverse_speed();
@@ -0,0 +1,221 @@
use control_core::converters::angular_step_converter::AngularStepConverter;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use units::angular_velocity::revolution_per_second;
use units::f64::AngularVelocity;
/// Controller for addon motors that follow the puller speed with a configurable ratio
#[derive(Debug)]
pub struct AddonMotorController {
/// Whether the motor is enabled (Run mode)
enabled: bool,
/// Master ratio value (e.g., 2 in "2:1")
master_ratio: f64,
/// Slave ratio value (e.g., 1 in "2:1")
slave_ratio: f64,
/// Converter for angular velocity to steps
converter: AngularStepConverter,
}
impl AddonMotorController {
/// Create a new addon motor controller
///
/// # Arguments
/// * `steps_per_revolution` - Number of steps per revolution for the stepper motor
pub fn new(steps_per_revolution: i16) -> Self {
Self {
enabled: false,
master_ratio: 1.0,
slave_ratio: 1.0,
converter: AngularStepConverter::new(steps_per_revolution),
}
}
/// Set whether the motor is enabled (Run mode)
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
/// Get whether the motor is enabled
pub const fn is_enabled(&self) -> bool {
self.enabled
}
/// Set the master ratio value
///
/// The master ratio represents the puller rotations in the ratio.
/// For example, in a 2:1 ratio, the master is 2.
pub fn set_master_ratio(&mut self, master: f64) {
self.master_ratio = master.max(0.1); // Prevent division issues
}
/// Set the slave ratio value
///
/// The slave ratio represents the motor rotations in the ratio.
/// For example, in a 2:1 ratio, the slave is 1.
pub fn set_slave_ratio(&mut self, slave: f64) {
self.slave_ratio = slave.max(0.1); // Prevent division issues
}
/// Get the master ratio value
pub const fn get_master_ratio(&self) -> f64 {
self.master_ratio
}
/// Get the slave ratio value
pub const fn get_slave_ratio(&self) -> f64 {
self.slave_ratio
}
/// Calculate the motor angular velocity based on puller angular velocity and ratio
///
/// # Arguments
/// * `puller_angular_velocity` - The current angular velocity of the puller
///
/// # Returns
/// The angular velocity this motor should run at
///
/// # Example
/// If puller is rotating at 10 rev/s and ratio is 2:1 (master=2, slave=1),
/// then motor should rotate at 5 rev/s (10 * 1/2)
fn calculate_motor_velocity(
&self,
puller_angular_velocity: AngularVelocity,
) -> AngularVelocity {
if !self.enabled {
return AngularVelocity::new::<revolution_per_second>(0.0);
}
// Motor velocity = puller velocity * (slave_ratio / master_ratio)
// For 2:1, motor rotates at half the puller speed (1/2)
// For 1:2, motor rotates at twice the puller speed (2/1)
let ratio = self.slave_ratio / self.master_ratio;
puller_angular_velocity * ratio
}
/// Update the motor speed based on the puller angular velocity
///
/// # Arguments
/// * `motor` - The stepper motor to control
/// * `puller_angular_velocity` - The current angular velocity of the puller
pub fn sync_motor_speed(
&self,
motor: &mut StepperVelocityEL70x1,
puller_angular_velocity: AngularVelocity,
) {
if !self.enabled {
// If disabled, ensure motor is not enabled
if motor.is_enabled() {
motor.set_enabled(false);
}
return;
}
// Enable motor if not already enabled
if !motor.is_enabled() {
motor.set_enabled(true);
}
// Calculate target velocity
let target_velocity = self.calculate_motor_velocity(puller_angular_velocity);
// Convert to steps per second and set motor speed
let steps_per_second = self.converter.angular_velocity_to_steps(target_velocity);
let _ = motor.set_speed(steps_per_second);
}
}
#[cfg(test)]
mod tests {
use super::*;
use units::angular_velocity::revolution_per_second;
#[test]
fn test_addon_motor_controller_disabled() {
let controller = AddonMotorController::new(200);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(10.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
0.0,
"Motor should be stopped when disabled"
);
}
#[test]
fn test_addon_motor_controller_ratio_2_to_1() {
let mut controller = AddonMotorController::new(200);
controller.set_enabled(true);
controller.set_master_ratio(2.0);
controller.set_slave_ratio(1.0);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(10.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
// For every 2 puller rotations, motor should rotate 1 time
// So motor speed should be half of puller speed
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
5.0,
"Motor should run at half speed with 2:1 ratio"
);
}
#[test]
fn test_addon_motor_controller_ratio_1_to_2() {
let mut controller = AddonMotorController::new(200);
controller.set_enabled(true);
controller.set_master_ratio(1.0);
controller.set_slave_ratio(2.0);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(10.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
// For every 1 puller rotation, motor should rotate 2 times
// So motor speed should be twice the puller speed
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
20.0,
"Motor should run at twice speed with 1:2 ratio"
);
}
#[test]
fn test_addon_motor_controller_ratio_3_to_2() {
let mut controller = AddonMotorController::new(200);
controller.set_enabled(true);
controller.set_master_ratio(3.0);
controller.set_slave_ratio(2.0);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(9.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
// For every 3 puller rotations, motor should rotate 2 times
// So motor speed should be 2/3 of puller speed
// 9 * (2/3) = 6.0
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
6.0,
"Motor should run at 2/3 speed with 3:2 ratio"
);
}
#[test]
fn test_addon_motor_controller_ratio_1_to_1() {
let mut controller = AddonMotorController::new(200);
controller.set_enabled(true);
controller.set_master_ratio(1.0);
controller.set_slave_ratio(1.0);
let puller_velocity = AngularVelocity::new::<revolution_per_second>(10.0);
let motor_velocity = controller.calculate_motor_velocity(puller_velocity);
// 1:1 ratio means motor runs at same speed as puller
assert_eq!(
motor_velocity.get::<revolution_per_second>(),
10.0,
"Motor should run at same speed with 1:1 ratio"
);
}
}
+46
View File
@@ -133,6 +133,14 @@ pub enum Mutation {
// Disconnect Machine
DisconnectMachine(MachineIdentificationUnique),
// Addon Motors
SetAddonMotor3Enabled(bool),
SetAddonMotor4Enabled(bool),
SetAddonMotor3MasterRatio(f64),
SetAddonMotor3SlaveRatio(f64),
SetAddonMotor4MasterRatio(f64),
SetAddonMotor4SlaveRatio(f64),
}
#[derive(Serialize, Debug, Clone, Default)]
@@ -198,6 +206,10 @@ pub struct StateEvent {
pub heating_states: HeatingStates,
/// Is a Machine Connected?
pub connected_machine_state: MachineCrossConnectionState,
/// addon motor 3 state
pub addon_motor_3_state: AddonMotorState,
/// addon motor 4 state
pub addon_motor_4_state: AddonMotorState,
}
#[derive(Serialize, Debug, Clone, Default)]
@@ -298,6 +310,16 @@ pub struct SpoolSpeedControllerState {
pub forward: bool,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct AddonMotorState {
/// is motor enabled (running)
pub enabled: bool,
/// master ratio value (e.g., 2 in "2:1")
pub master_ratio: f64,
/// slave ratio value (e.g., 1 in "2:1")
pub slave_ratio: f64,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct HeatingStates {
pub zone_1: HeatingState,
@@ -435,6 +457,30 @@ impl MachineApi for Gluetex {
//main_sender.try_send(crate::AsyncThreadMessage::ConnectOneWayRequest(crate::CrossConnection { src: self.get_machine_identification_unique(), dest: machine_identification_unique }))?;
self.emit_state();
}
Mutation::SetAddonMotor3Enabled(enabled) => {
self.addon_motor_3_controller.set_enabled(enabled);
self.emit_state();
}
Mutation::SetAddonMotor4Enabled(enabled) => {
self.addon_motor_4_controller.set_enabled(enabled);
self.emit_state();
}
Mutation::SetAddonMotor3MasterRatio(ratio) => {
self.addon_motor_3_controller.set_master_ratio(ratio);
self.emit_state();
}
Mutation::SetAddonMotor3SlaveRatio(ratio) => {
self.addon_motor_3_controller.set_slave_ratio(ratio);
self.emit_state();
}
Mutation::SetAddonMotor4MasterRatio(ratio) => {
self.addon_motor_4_controller.set_master_ratio(ratio);
self.emit_state();
}
Mutation::SetAddonMotor4SlaveRatio(ratio) => {
self.addon_motor_4_controller.set_slave_ratio(ratio);
self.emit_state();
}
}
Ok(())
}
+10
View File
@@ -378,6 +378,16 @@ impl Gluetex {
},
},
connected_machine_state: cross_conn,
addon_motor_3_state: api::AddonMotorState {
enabled: self.addon_motor_3_controller.is_enabled(),
master_ratio: self.addon_motor_3_controller.get_master_ratio(),
slave_ratio: self.addon_motor_3_controller.get_slave_ratio(),
},
addon_motor_4_state: api::AddonMotorState {
enabled: self.addon_motor_4_controller.is_enabled(),
master_ratio: self.addon_motor_4_controller.get_master_ratio(),
slave_ratio: self.addon_motor_4_controller.get_slave_ratio(),
},
}
}
+26
View File
@@ -1,5 +1,6 @@
pub mod act;
pub mod adaptive_spool_speed_controller;
pub mod addon_motor_controller;
pub mod api;
pub mod clamp_revolution;
pub mod emit;
@@ -17,6 +18,7 @@ use units::f64::ThermodynamicTemperature;
use units::thermodynamic_temperature::degree_celsius;
mod gluetex_imports {
pub use super::addon_motor_controller::AddonMotorController;
pub use super::api::GluetexNamespace;
pub use super::api::SpoolAutomaticActionMode;
pub use super::puller_speed_controller::PullerSpeedController;
@@ -118,10 +120,18 @@ pub struct Gluetex {
pub tension_arm: TensionArm,
pub laser: DigitalOutput,
// addon motors
pub addon_motor_3: StepperVelocityEL70x1,
pub addon_motor_4: StepperVelocityEL70x1,
// controllers
pub traverse_controller: TraverseController,
pub traverse_end_stop: DigitalInput,
// addon motor controllers
pub addon_motor_3_controller: AddonMotorController,
pub addon_motor_4_controller: AddonMotorController,
// temperature controllers (PID-controlled heaters with temperature sensors)
pub temperature_controller_1: TemperatureController,
pub temperature_controller_2: TemperatureController,
@@ -359,6 +369,22 @@ impl Gluetex {
.angular_velocity_to_steps(angular_velocity);
let _ = self.puller.set_speed(steps_per_second);
}
/// Sync addon motor 3 speed based on puller angular velocity and ratio
/// called by `act`
pub fn sync_addon_motor_3_speed(&mut self, t: Instant) {
let puller_angular_velocity = self.puller_speed_controller.calc_angular_velocity(t);
self.addon_motor_3_controller
.sync_motor_speed(&mut self.addon_motor_3, puller_angular_velocity);
}
/// Sync addon motor 4 speed based on puller angular velocity and ratio
/// called by `act`
pub fn sync_addon_motor_4_speed(&mut self, t: Instant) {
let puller_angular_velocity = self.puller_speed_controller.calc_angular_velocity(t);
self.addon_motor_4_controller
.sync_motor_speed(&mut self.addon_motor_4, puller_angular_velocity);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
+76
View File
@@ -230,6 +230,74 @@ impl MachineNewTrait for Gluetex {
.await?
.0;
// Role 9: Addon Motor 3 EL7031
let el7031_addon3 = {
let device = get_ethercat_device::<EL7031>(
hardware,
params,
9,
vec![EL7031_IDENTITY_A, EL7031_IDENTITY_B],
)
.await?;
let el7031_config = EL7031Configuration {
stm_features: shared_config::el70x1::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 1500,
..Default::default()
},
pdo_assignment: EL7031PredefinedPdoAssignment::VelocityControlCompact,
..Default::default()
};
device
.0
.write()
.await
.write_config(&device.1, &el7031_config)
.await?;
device.0
};
// Role 10: Addon Motor 4 EL7031
let el7031_addon4 = {
let device = get_ethercat_device::<EL7031>(
hardware,
params,
10,
vec![EL7031_IDENTITY_A, EL7031_IDENTITY_B],
)
.await?;
let el7031_config = EL7031Configuration {
stm_features: shared_config::el70x1::StmFeatures {
operation_mode: EL70x1OperationMode::DirectVelocity,
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
..Default::default()
},
stm_motor: StmMotorConfiguration {
max_current: 1500,
..Default::default()
},
pdo_assignment: EL7031PredefinedPdoAssignment::VelocityControlCompact,
..Default::default()
};
device
.0
.write()
.await
.write_config(&device.1, &el7031_config)
.await?;
device.0
};
// Digital outputs for SSR control (24V to external SSRs for 60W heaters)
let heater_ssr_1 = DigitalOutput::new(el2004_1.clone(), EL2004Port::DO1);
let heater_ssr_2 = DigitalOutput::new(el2004_1.clone(), EL2004Port::DO2);
@@ -355,6 +423,14 @@ impl MachineNewTrait for Gluetex {
EL7031_0030StepperPort::STM1,
),
spool: StepperVelocityEL70x1::new(el7041, EL7041_0052Port::STM1),
addon_motor_3: StepperVelocityEL70x1::new(el7031_addon3, EL7031StepperPort::STM1),
addon_motor_4: StepperVelocityEL70x1::new(el7031_addon4, EL7031StepperPort::STM1),
addon_motor_3_controller: super::addon_motor_controller::AddonMotorController::new(
200,
),
addon_motor_4_controller: super::addon_motor_controller::AddonMotorController::new(
200,
),
tension_arm: TensionArm::new(AnalogInput::new(
el7031_0030,
EL7031_0030AnalogInputPort::AI1,