Compare commits

..
Author SHA1 Message Date
Janhegerq f5c5dfeccb converted old PDF to MD 2025-07-09 16:53:17 +02:00
102 changed files with 3927 additions and 2884 deletions

No files matched your search

+3 -16
View File
@@ -1,25 +1,12 @@
# `2.2.0`
_15.07.2025_
# `Unreleased`
## Extruder V2
- [#455](https://github.com/qitechgmbh/control/pull/458), Greatly improved stability and precision of pressure-based regulation for the motor
- [#467](https://github.com/qitechgmbh/control/pull/468), [#448](https://github.com/qitechgmbh/control/pull/449), [#485](https://github.com/qitechgmbh/control/pull/486) Improved the Responsiveness for motor control and monitoring
- [#138](https://github.com/qitechgmbh/control/pull/495), Added a Warning in case the Pressure sensor is disconnected or incorrectly wired
- [#169](https://github.com/qitechgmbh/control/pull/492), Added a Confirmation Dialog when extruder has not reached 90% of target temperature
## Winder V2
- [#471](https://github.com/qitechgmbh/control/pull/480), Added a live value to track the pulled distance and the option of switching to a different Mode after a given distance.
## General
- [#121](https://github.com/qitechgmbh/control/pull/121), Use Linux realtime threads for EtherCAT and control loop (PREEMPT_RT)
- [#138](https://github.com/qitechgmbh/control/pull/495), Updated Quick Start Guide
- [#455](https://github.com/qitechgmbh/control/pull/458) Greatly improved stability and precision of pressure-based regulation for the motor
# `2.1.0`
_30.06.2025_
_30.07.2025_
## Extruder V2
Generated
-25
View File
@@ -581,13 +581,11 @@ dependencies = [
"approx",
"axum",
"bitvec",
"core_affinity",
"crc",
"erased-serde",
"ethercat_hal",
"ethercrab",
"interfaces",
"libc",
"serde",
"serde_json",
"serial",
@@ -615,17 +613,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core_affinity"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342"
dependencies = [
"libc",
"num_cpus",
"winapi",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -1006,7 +993,6 @@ dependencies = [
"bitvec",
"ethercat_hal_derive",
"ethercrab",
"futures",
"rand",
"smol",
"uom",
@@ -1960,16 +1946,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
"hermit-abi 0.5.0",
"libc",
]
[[package]]
name = "object"
version = "0.36.7"
@@ -2714,7 +2690,6 @@ dependencies = [
"ethercat_hal",
"ethercrab",
"euclid",
"futures",
"lazy_static",
"open",
"opentelemetry",
+3 -5
View File
@@ -41,8 +41,6 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
# Dev Setup
[Developer Documentation](./docs/developer-docs/)
## Backend
- Rust stable 1.86^ toolchain (install via [rustup](https://rustup.rs/))
@@ -68,6 +66,7 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
# More Docs
- [x] [Architecture & Data Flow](./docs/architecture-overview.md)
@@ -91,9 +90,8 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
- Write Device Identification
- Server
- [x] [Threading](./docs/control-loop.md)
- [x] [Logging](./docs/logging.md)
- [X] [Threading](./docs/control-loop.md)
- [X] [Logging](./docs/logging.md)
- [ ] Control Loop Setup
- Control Loop Thread
- [ ] realtime
+8 -18
View File
@@ -1,23 +1,13 @@
#!/bin/bash
FEATURE=""
# Check if "mock-machine" was passed as an argument
if [ "$1" == "mock-machine" ]; then
echo "Building Debug Code with mock-machine feature"
FEATURE="--features mock-machine"
if [ $1 == "release" ]; then
echo "building Release Code"
cargo build --release
else
echo "Building Debug Code"
echo "building Debug Code"
cargo build
fi
# Build the project with or without features
cargo build $FEATURE
# Only setcap if not building with mock-machine
if [ "$1" != "mock-machine" ]; then
echo "Setting capabilities for server executable"
sudo setcap cap_net_raw=eip ./target/debug/server
fi
# Run the server
# set cap for server executable
sudo setcap cap_net_raw=eip ./target/debug/server
# run
./target/debug/server
-5
View File
@@ -20,11 +20,6 @@ crc = "3.3.0"
serial = "0.4.0"
tracing = "0.1.41"
erased-serde = "0.4.6"
core_affinity = "0.8.3"
[target.'cfg(unix)'.dependencies]
libc = "0.2.172"
[dev-dependencies]
approx = "0.5.1"
textplots = "0.8"
@@ -0,0 +1,91 @@
use super::Actor;
use ethercat_hal::io::analog_output::AnalogOutput;
use std::{
f64::consts::PI,
future::Future,
pin::Pin,
time::{Duration, Instant},
};
use uom::si::{angle::radian, f64::Angle, ratio::ratio};
pub type AnalogFunction = Box<dyn Fn(Duration) -> f64 + Send + Sync>;
/// Can module analog output with a function
pub struct AnalogFunctionGenerator {
output: AnalogOutput,
function: AnalogFunction,
start_ts: Instant,
}
impl AnalogFunctionGenerator {
pub fn new(output: AnalogOutput, function: AnalogFunction) -> Self {
Self {
output,
function,
start_ts: Instant::now(),
}
}
}
impl Actor for AnalogFunctionGenerator {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let diff = now - self.start_ts;
let value = (self.function)(diff);
(self.output.write)((value as f32).into()).await;
})
}
}
pub fn analog_sine(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
// Convert to seconds as f64 for calculation
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let angle = Angle::new::<radian>(2.0 * PI * phase_secs / period_secs);
let value = angle.sin().get::<ratio>();
value * (amplitude / 2.0) + normal + (amplitude / 2.0)
})
}
pub fn analog_multiply<const N: usize>(functions: [AnalogFunction; N]) -> AnalogFunction {
Box::new(move |dt: Duration| functions.iter().fold(1.0, |acc, func| acc * func(dt)))
}
pub fn analog_sawtooth(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let value = phase_secs / period_secs;
value * amplitude + normal
})
}
pub fn analog_square(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let value = if phase_secs < period_secs / 2.0 {
1.0
} else {
-1.0
};
value * amplitude + normal
})
}
pub fn analog_triangle(amplitude: f64, normal: f64, period: Duration) -> AnalogFunction {
Box::new(move |dt: Duration| {
let dt_secs = dt.as_secs_f64();
let period_secs = period.as_secs_f64();
let phase_secs = dt_secs % period_secs;
let value = if phase_secs < period_secs / 2.0 {
phase_secs / period_secs * 2.0
} else {
2.0 - phase_secs / period_secs * 2.0
};
value * amplitude + normal
})
}
@@ -0,0 +1,40 @@
use super::Actor;
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a analog input
#[derive(Debug)]
pub struct AnalogInputGetter {
input: AnalogInput,
normalized: Option<f32>,
}
impl AnalogInputGetter {
pub fn new(input: AnalogInput) -> Self {
Self {
input,
normalized: None,
}
}
/// Value from -1.0 to 1.0
pub fn get_normalized(&self) -> Option<f32> {
self.normalized
}
pub fn get_physical(&self) -> Option<AnalogInputValue> {
match self.get_normalized() {
Some(normalized) => Some(self.input.range.normalized_to_physical(normalized)),
None => return None,
}
}
}
impl Actor for AnalogInputGetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)().await;
self.normalized = Some(state.input.normalized);
})
}
}
@@ -0,0 +1,35 @@
use super::Actor;
use ethercat_hal::io::digital_input::DigitalInput;
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a digital input
#[derive(Debug)]
pub struct DigitalInputGetter {
input: DigitalInput,
value: bool,
}
impl DigitalInputGetter {
pub fn new(input: DigitalInput) -> Self {
Self {
input,
value: false,
}
}
/// Get the current value of the digital input
pub fn value(&self) -> bool {
self.value
}
}
impl Actor for DigitalInputGetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)()
.await
.expect("Failed to get digital input state");
self.value = state.input.value;
})
}
}
@@ -0,0 +1,25 @@
use super::Actor;
use ethercat_hal::io::digital_input::DigitalInput;
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a digital input
pub struct DigitalInputLogger {
input: DigitalInput,
}
impl DigitalInputLogger {
pub fn new(input: DigitalInput) -> Self {
Self { input }
}
}
impl Actor for DigitalInputLogger {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)()
.await
.expect("Failed to get digital input state");
println!("DigitalInputLogger: {}", state.input.value);
})
}
}
@@ -0,0 +1,51 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{
future::Future,
pin::Pin,
time::{Duration, Instant},
};
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct DigitalOutputBlinker {
last_toggle: Instant,
output: DigitalOutput,
interval: Duration,
enabled: bool,
}
impl DigitalOutputBlinker {
pub fn new(output: DigitalOutput, interval: Duration) -> Self {
Self {
last_toggle: Instant::now(),
output,
interval,
enabled: true,
}
}
pub fn set_interval(&mut self, interval: Duration) {
self.interval = interval;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
impl Actor for DigitalOutputBlinker {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
{
let state = (self.output.state)().await;
if now - self.last_toggle > self.interval {
match state.output.into() {
true => (self.output.write)(false.into()).await,
false => (self.output.write)(true.into()).await,
}
self.last_toggle = now;
}
}
})
}
}
@@ -0,0 +1,57 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{
future::Future,
pin::Pin,
time::{Duration, Instant},
};
/// Set a series of digital outputs high in sequence with a given interval
pub struct DigitalOutputBlinkers {
last_toggle: Instant,
outputs: Vec<Option<DigitalOutput>>,
index: usize,
interval: Duration,
enabled: bool,
amount: usize,
}
impl DigitalOutputBlinkers {
pub fn new(outputs: Vec<Option<DigitalOutput>>, interval: Duration, amount: usize) -> Self {
Self {
last_toggle: Instant::now(),
outputs,
interval,
enabled: true,
index: 0,
amount,
}
}
pub fn set_interval(&mut self, interval: Duration) {
self.interval = interval;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
impl Actor for DigitalOutputBlinkers {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
if now - self.last_toggle > self.interval {
if let Some(output) = &self.outputs[self.index] {
(output.write)(true.into()).await;
}
let index_end =
(self.index + self.outputs.len() - self.amount) % self.outputs.len();
if let Some(output) = &self.outputs[index_end] {
(output.write)(false.into()).await;
}
self.last_toggle = now;
self.index = (self.index + 1) % self.outputs.len();
}
})
}
}
@@ -0,0 +1,38 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{future::Future, pin::Pin, time::Instant};
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct DigitalOutputSetter {
output: DigitalOutput,
enabled: bool,
}
impl DigitalOutputSetter {
pub fn new(output: DigitalOutput) -> Self {
Self {
output,
enabled: false,
}
}
pub fn set(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn get(&self) -> bool {
self.enabled
}
pub fn toggle(&mut self) {
self.enabled = !self.enabled;
}
}
impl Actor for DigitalOutputSetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
{
(self.output.write)(self.enabled.into()).await;
}
})
}
}
+19
View File
@@ -0,0 +1,19 @@
pub mod analog_function_generator;
pub mod analog_input_getter;
pub mod digital_input_getter;
pub mod digital_input_logger;
pub mod digital_output_blinker;
pub mod digital_output_blinkers;
pub mod digital_output_setter;
pub mod serial_interface_actor;
pub mod stepper_driver_el70x1;
pub mod stepper_driver_max_speed;
pub mod stepper_driver_pulse_train;
pub mod temperature_input_getter;
pub mod temperature_input_logger;
use std::{pin::Pin, time::Instant};
pub trait Actor: Send + Sync {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}
@@ -0,0 +1,337 @@
use crate::modbus::{ModbusRequest, ModbusResponse};
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
use std::{
collections::HashMap,
pin::Pin,
time::{Duration, Instant},
};
#[derive(Debug, Clone)]
struct RequestMetaData {
pub priority: u32,
pub ignored_times: u32,
/// This is used when a machine for example takes 4ms to process the request
/// and is added ontop of the standard waiting time for a serial transfer
pub extra_delay: Option<u32>,
pub no_response_expected: bool,
}
#[derive(Debug)]
pub enum State {
/// WaitingForResponse is set after sending a request through the serial_interface
WaitingForResponse,
/// After Sending a Resuest we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has transmit accepted toggled
/// Then we can set state = ReadyToSend
WaitingForRequestAccept,
/// After Receiving a Response we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has received accepted toggled
WaitingForReceiveAccept,
/// ReadyToSend is set after receiving the response from the serial_interface
ReadyToSend,
/// Initial State
Uninitialized,
}
#[derive(Debug)]
pub struct SerialInterfaceActor {
pub serial_interface: SerialInterface,
pub baudrate: Option<u32>,
pub encoding: Option<SerialEncoding>,
pub response: Option<ModbusResponse>,
pub state: State,
pub last_message_size: usize,
pub last_message_delay: u32,
pub last_message_id: u32,
pub no_response_expected: bool,
request_map: HashMap<u32, ModbusRequest>,
request_metadata_map: HashMap<u32, RequestMetaData>,
last_ts: Instant,
}
impl SerialInterfaceActor {
pub fn new(serial_interface: SerialInterface) -> Self {
Self {
serial_interface,
baudrate: None,
encoding: None,
request_map: HashMap::new(),
request_metadata_map: HashMap::new(),
response: None,
last_message_size: 0,
last_message_delay: 0,
state: State::Uninitialized,
last_ts: Instant::now(),
last_message_id: 0,
no_response_expected: false,
}
}
pub fn is_initialized(&mut self) -> bool {
if let State::Uninitialized = self.state {
return false;
} else {
return true;
}
}
pub fn add_request(
&mut self,
request_id: u32,
priority: u32,
request: ModbusRequest,
no_response_expected: bool,
delay: Option<u32>,
) {
self.request_map.insert(request_id, request);
if !self.request_metadata_map.contains_key(&request_id) {
let meta_data = RequestMetaData {
priority,
ignored_times: 0,
extra_delay: delay,
no_response_expected: no_response_expected,
};
self.request_metadata_map.insert(request_id, meta_data);
}
}
/// This is used internally to read the receive buffer of the el6021
fn read_modbus_response(
&mut self,
) -> Pin<Box<dyn Future<Output = Result<ModbusResponse, anyhow::Error>> + Send + '_>> {
Box::pin(async move {
let res: Option<Vec<u8>> = (self.serial_interface.read_message)().await;
let raw_response = match res {
Some(res) => res,
None => {
vec![]
}
};
let response: Result<ModbusResponse, _> =
ModbusResponse::try_from(raw_response.clone());
match response {
Ok(result) => {
self.last_message_size = result.clone().data.len() + 4;
self.state = State::WaitingForReceiveAccept;
Ok(result)
}
Err(err) => {
self.last_message_size = 22;
Err(anyhow::anyhow!(err))
}
}
})
}
/// This is used internally to fill the write buffer of the el6021 with the modbus request
/// Decides what requests to send first by finding the one with the highest priority
/// For example Highest Priority requests: ResetInverter StopMotor
fn send_modbus_request(&mut self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
if self.request_map.len() == 0 {
return;
};
let mut highest_prio_request: Option<&ModbusRequest> = None;
let mut highest_priority: u32 = 0;
let mut highest_id: &u32 = &0;
let mut delay: u32 = 0;
let mut no_response_expected: bool = false;
for (key, value) in self.request_metadata_map.iter_mut() {
// borrowchecker complaining
let priority = value.priority as u32;
let ignored_times = value.ignored_times;
let effective_priority: u32 = priority as u32 + ignored_times;
if effective_priority > highest_priority {
highest_prio_request = self.request_map.get(key);
highest_priority = effective_priority;
highest_id = key;
delay = match value.extra_delay {
Some(delay) => delay,
None => 0,
};
no_response_expected = value.no_response_expected;
}
}
let request = match highest_prio_request {
Some(request) => request,
None => return,
};
let modbus_request: Vec<u8> = request.clone().into();
let res = (self.serial_interface.write_message)(modbus_request.clone()).await;
match res {
Ok(_) => {
self.last_message_delay = delay;
self.last_message_id = *highest_id;
self.no_response_expected = no_response_expected;
}
Err(_) => tracing::error!("ERROR: serial_interface.write_message has failed"),
}
self.state = State::WaitingForRequestAccept;
self.last_message_size = modbus_request.len();
})
}
pub async fn initialize_communication_settings(&mut self) -> bool {
let baudrate = (self.serial_interface.get_baudrate)().await;
let encoding = (self.serial_interface.get_serial_encoding)().await;
match encoding {
None => return false,
Some(_) => (),
};
match baudrate {
None => return false,
Some(_) => (),
};
self.baudrate = baudrate;
self.encoding = encoding;
return true;
}
pub async fn initialize(&mut self) -> bool {
if let State::Uninitialized = self.state {
let res = (self.serial_interface.initialize)().await;
if res == true {
self.state = State::ReadyToSend;
self.initialize_communication_settings().await;
return res;
}
return res;
}
return false;
}
fn set_ignored_times_modbus_requests(&mut self) {
for (_, value) in self.request_metadata_map.iter_mut() {
value.ignored_times += 1;
}
}
/// Modbus RTU has silent time between frames that needs to be adhered to, if you send before silent_time is over between frames, then there will be lost frames
/// This silent time is needed to identify the start and end of messages
/// This function also takes into account the time that the slave we are talking to needs to process our request
/// bits: amount of bits sent per byte -> for a 8n1 coding: 8 data bits, 0 parity, 1 stop bit (1 start,1 stop) -> 10 bits
/// machine_operation_delay_nano: Delay for the given operation in nanoseconds as specified by the slaves datasheet (example: mitsubishi csfr84 has 12ms for read write in RAM)
/// baudrate: bits per second
/// message_size: size of original message in bytes
pub fn calculate_modbus_rtu_timeout(
&self,
machine_operation_delay_nano: Duration,
message_size: usize,
) -> Option<Duration> {
let baudrate = match self.baudrate {
Some(baudrate) => baudrate,
None => return None,
};
let bits = match self.encoding {
Some(encoding) => encoding.total_bits(),
None => return None,
};
let nanoseconds_per_bit: u64 = (1000000 / baudrate) as u64;
let nanoseconds_per_byte: u64 = bits as u64 * nanoseconds_per_bit as u64;
let transmission_timeout: u64 = nanoseconds_per_byte * message_size as u64;
let silent_time: u64 = (nanoseconds_per_byte * (35)) / 10 as u64; // silent_time is 3.5x of character length,which is 11 bit for 8E1
let mut full_timeout: u64 = transmission_timeout;
full_timeout += machine_operation_delay_nano.as_nanos() as u64;
full_timeout += silent_time;
return Some(Duration::from_nanos(full_timeout));
}
pub fn get_response(&self) -> Option<ModbusResponse> {
return self.response.clone();
}
pub fn act(&mut self, now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let elapsed: Duration = now_ts.duration_since(self.last_ts);
let timeout = self.calculate_modbus_rtu_timeout(
Duration::from_nanos(self.last_message_delay as u64),
self.last_message_size,
);
let timeout = match timeout {
Some(timeout) => timeout,
None => return,
};
match self.state {
State::Uninitialized => (),
State::WaitingForReceiveAccept => (),
State::WaitingForRequestAccept => (),
_ => {
if elapsed < timeout {
return;
}
}
}
self.last_ts = now_ts;
self.response = None;
match self.state {
State::WaitingForResponse => {
let ret = self.read_modbus_response().await;
match ret {
Ok(ret) => {
self.response = Some(ret);
}
Err(_) => {
self.response = None;
if self.no_response_expected {
self.state = State::ReadyToSend;
return;
}
}
}
}
State::ReadyToSend => {
self.last_message_id = 0;
self.send_modbus_request().await;
self.request_map.remove(&self.last_message_id);
self.request_metadata_map.remove(&self.last_message_id);
self.set_ignored_times_modbus_requests();
}
State::WaitingForReceiveAccept => {
// Waste atleast one Ethercat Cycle here to ensure that request/response stay in sync
self.state = State::ReadyToSend;
}
State::WaitingForRequestAccept => {
// An empty vec is used to check if we are finished with writing the message
// This is to keep the Serialinterface more simple
let res = (self.serial_interface.write_message)(vec![]).await;
let finished = match res {
Ok(res) => res,
Err(_) => {
self.state = State::ReadyToSend;
return;
}
};
if finished == true {
self.state = State::WaitingForResponse;
}
}
_ => (),
}
})
}
}
@@ -0,0 +1,91 @@
use super::Actor;
use core::panic;
use ethercat_hal::{
helpers::el70xx_velocity_converter::EL70x1VelocityConverter,
io::stepper_velocity_el70x1::StepperVelocityEL70x1, shared_config::el70x1::EL70x1SpeedRange,
};
use std::{future::Future, pin::Pin, time::Instant};
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct StepperDriverEL70x1 {
stepper: StepperVelocityEL70x1,
enabled: bool,
velocity: i16,
position: i128,
set_position: Option<i128>,
pub converter: EL70x1VelocityConverter,
}
impl StepperDriverEL70x1 {
pub fn new(stepper: StepperVelocityEL70x1, speed_range: &EL70x1SpeedRange) -> Self {
Self {
stepper,
enabled: false,
velocity: 0,
converter: EL70x1VelocityConverter::new(speed_range),
position: 0,
set_position: None,
}
}
/// Set the speed in steps per second
pub fn set_speed(&mut self, steps_per_second: f64) {
self.velocity = self.converter.steps_to_velocity(steps_per_second, true)
}
/// Get the speed in steps per second
pub fn get_speed(&self) -> i32 {
self.converter.velocity_to_steps(self.velocity, true) as i32
}
/// Enable or disable the stepper
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
/// Get the enabled state of the stepper
pub fn is_enabled(&self) -> bool {
self.enabled
}
/// Get the current position of the stepper
pub fn get_position(&self) -> i128 {
self.position
}
/// Set the position of the stepper
pub fn set_position(&mut self, position: i128) {
self.set_position = Some(position);
}
}
impl Actor for StepperDriverEL70x1 {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = match (self.stepper.state)().await {
Ok(state) => state,
Err(e) => {
panic!("Error while reading StepperVelocity {:?}", e);
}
};
// set the input
self.position = state.input.counter_value;
let mut output = state.output.clone();
// set the output
output.enable = self.enabled;
output.velocity = self.velocity;
output.set_counter = self.set_position.take();
// write the output
match (self.stepper.write)(output).await {
Ok(_) => {}
Err(e) => {
panic!("Error while writing StepperVelocity {:?}", e);
}
}
})
}
}
@@ -0,0 +1,37 @@
use super::Actor;
use ethercat_hal::io::digital_output::DigitalOutput;
use std::{
future::Future,
pin::Pin,
sync::{Arc, RwLock},
time::Instant,
};
/// Set a digital output high and low with a given interval
pub struct StepperDriverMaxSpeed {
pulse: DigitalOutput,
}
impl StepperDriverMaxSpeed {
pub fn new(output: DigitalOutput) -> Self {
Self { pulse: output }
}
}
impl Actor for StepperDriverMaxSpeed {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.pulse.state)().await;
match state.output.into() {
true => (self.pulse.write)(false.into()).await,
false => (self.pulse.write)(true.into()).await,
}
})
}
}
impl From<StepperDriverMaxSpeed> for Arc<RwLock<StepperDriverMaxSpeed>> {
fn from(actor: StepperDriverMaxSpeed) -> Self {
Arc::new(RwLock::new(actor))
}
}
@@ -0,0 +1,222 @@
/// Translates the encoder value to a larger space and handle underflow/overflow situations
#[derive(Debug)]
pub struct Encoder {
pub position: i64,
pub last_encoder_position: u32,
}
impl Encoder {
pub fn new() -> Self {
Self {
position: 0,
last_encoder_position: 0,
}
}
pub fn update(&mut self, counter_value: u32, counter_overflow: bool, counter_underflow: bool) {
// Handle overflow case
if counter_overflow {
// When overflow occurs, the counter wraps from MAX to 0
// Calculate how much it moved past MAX
let delta = counter_value as i64;
// Add the remaining distance from last position to MAX, plus the overflow amount
self.position += (u32::MAX - self.last_encoder_position) as i64 + 1 + delta;
}
// Handle underflow case
else if counter_underflow {
// When underflow occurs, the counter wraps from 0 to MAX
// Calculate how much it moved below 0
let delta = (u32::MAX - counter_value) as i64;
// Subtract the distance from last position to 0, plus the underflow amount
self.position -= self.last_encoder_position as i64 + 1 + delta;
}
// Normal case - no overflow/underflow
else {
// Calculate the signed difference between current and last position
let delta = counter_value as i64 - self.last_encoder_position as i64;
// Update the position
self.position += delta;
}
// Update the last encoder position for next time
self.last_encoder_position = counter_value;
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Tests that the position tracker correctly handles normal forward movement
/// without any overflow conditions.
///
/// This test verifies:
/// 1. Initial state is zeroed
/// 2. Moving forward updates both position and last_encoder_position
/// 3. Consecutive forward movements accumulate correctly
#[test]
fn test_normal_forward_movement() {
let mut tracker = Encoder::new();
// Initial state
assert_eq!(tracker.position, 0);
assert_eq!(tracker.last_encoder_position, 0);
// Move forward by 100
tracker.update(100, false, false);
assert_eq!(tracker.position, 100);
assert_eq!(tracker.last_encoder_position, 100);
// Move forward by another 50
tracker.update(150, false, false);
assert_eq!(tracker.position, 150);
assert_eq!(tracker.last_encoder_position, 150);
}
/// Tests that the position tracker correctly handles normal backward movement
/// without any underflow conditions.
///
/// This test verifies:
/// 1. Starting from a non-zero position
/// 2. Moving backward updates both position and last_encoder_position
/// 3. Consecutive backward movements accumulate correctly
#[test]
fn test_normal_backward_movement() {
let mut tracker = Encoder::new();
// Start at position 1000
tracker.position = 1000;
tracker.last_encoder_position = 1000;
// Move backward by 100
tracker.update(900, false, false);
assert_eq!(tracker.position, 900);
assert_eq!(tracker.last_encoder_position, 900);
// Move backward by another 50
tracker.update(850, false, false);
assert_eq!(tracker.position, 850);
assert_eq!(tracker.last_encoder_position, 850);
}
/// Tests that the position tracker correctly handles overflow conditions
/// where the encoder counter wraps from near its maximum value to a small value.
///
/// This test verifies:
/// 1. When overflow flag is set, the tracker correctly calculates the true movement
/// 2. Position is updated to account for the full distance traveled
/// 3. last_encoder_position is updated to the new counter value
#[test]
fn test_overflow() {
let mut tracker = Encoder::new();
// Set position near the maximum u32 value
tracker.position = 100;
tracker.last_encoder_position = u32::MAX - 10;
// Overflow: counter goes from (MAX-10) to 5
// This means we moved forward by 10 steps to MAX, then 1 step to overflow,
// then 5 more steps to reach the current position
tracker.update(5, true, false);
// Expected: position += (10 + 1 + 5) = position += 16
assert_eq!(tracker.position, 116);
assert_eq!(tracker.last_encoder_position, 5);
}
/// Tests that the position tracker correctly handles underflow conditions
/// where the encoder counter wraps from near zero to a value near its maximum.
///
/// This test verifies:
/// 1. When underflow flag is set, the tracker correctly calculates the true movement
/// 2. Position is updated to account for the full distance traveled (in negative direction)
/// 3. last_encoder_position is updated to the new counter value
#[test]
fn test_underflow() {
let mut tracker = Encoder::new();
// Set position near zero
tracker.position = 100;
tracker.last_encoder_position = 10;
// Underflow: counter goes from 10 to (MAX-5)
// This means we moved backward by 10 steps to 0, then 1 step to underflow,
// then 5 more steps to reach the current position
tracker.update(u32::MAX - 5, false, true);
// Expected: position -= (10 + 1 + 5) = position -= 16
assert_eq!(tracker.position, 84);
assert_eq!(tracker.last_encoder_position, u32::MAX - 5);
}
/// Tests that the position tracker maintains correct behavior across
/// a sequence of different operations including normal movement,
/// overflow, and underflow.
///
/// This test verifies:
/// 1. The tracker maintains state correctly between operations
/// 2. Different types of movements can be handled in sequence
/// 3. Position calculations remain accurate throughout various scenarios
#[test]
fn test_multiple_operations() {
let mut tracker = Encoder::new();
// Normal forward
tracker.update(100, false, false);
assert_eq!(tracker.position, 100);
// Normal backward
tracker.update(50, false, false);
assert_eq!(tracker.position, 50);
// Overflow
tracker.last_encoder_position = u32::MAX - 5;
tracker.position = 50; // Reset for clarity
tracker.update(10, true, false);
assert_eq!(tracker.position, 66); // 50 + (5 + 1 + 10)
// Underflow
tracker.last_encoder_position = 5;
tracker.position = 100; // Reset for clarity
tracker.update(u32::MAX - 10, false, true);
assert_eq!(tracker.position, 84); // 100 - (5 + 1 + 10)
}
/// Tests that the position tracker can handle multiple overflow and underflow
/// events, maintaining an accurate position count that exceeds the u32 range.
///
/// This test verifies:
/// 1. Multiple overflow events accumulate correctly in the i64 position
/// 2. Multiple underflow events correctly subtract from the position
/// 3. The i64 position field properly extends the range beyond u32 limits
#[test]
fn test_large_movements() {
let mut tracker = Encoder::new();
// Test tracking beyond u32 limits with multiple overflows
for _ in 0..5 {
// Simulate overflow: move from (MAX-100) to 100
tracker.last_encoder_position = u32::MAX - 100;
tracker.update(100, true, false);
// Each iteration adds (100 + 1 + 100) = 201 to position
// We move 100 steps to MAX, 1 step to overflow, and 100 more steps to reach 100
}
// After 5 iterations, position should be 5 * 201 = 1005
assert_eq!(tracker.position, 1005);
// Test tracking with multiple underflows
for _ in 0..3 {
// Simulate underflow: move from 100 to (MAX-100)
tracker.last_encoder_position = 100;
tracker.update(u32::MAX - 100, false, true);
// Each iteration subtracts (100 + 1 + 100) = 201 from position
// We move 100 steps to 0, 1 step to underflow, and 100 more steps to reach MAX-100
}
// Final position: 1005 - (3 * 201) = 1005 - 603 = 402
assert_eq!(tracker.position, 402);
}
}
@@ -0,0 +1,51 @@
use super::Actor;
use encoder::Encoder;
use ethercat_hal::io::pulse_train_output::PulseTrainOutput;
use std::{future::Future, pin::Pin, time::Instant};
pub mod encoder;
/// Set a digital output high and low with a given interval
#[derive(Debug)]
pub struct StepperDriverPulseTrain {
pulse: PulseTrainOutput,
encoder: Encoder,
frequency: i32,
}
impl<'ptodevice> StepperDriverPulseTrain {
pub fn new(output: PulseTrainOutput) -> Self {
Self {
pulse: output,
encoder: Encoder::new(),
frequency: 0,
}
}
}
impl<'ptodevice> Actor for StepperDriverPulseTrain {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.pulse.state)().await;
let mut output = state.output.clone();
// sync the counter to the encoder
self.encoder.update(
state.input.counter_value,
state.input.counter_overflow,
state.input.counter_underflow,
);
// write frequency
output.frequency_value = self.frequency;
(self.pulse.write)(output).await;
})
}
}
impl<'ptodevice> StepperDriverPulseTrain {
pub fn set_frequency(&mut self, frequency: i32) {
self.frequency = frequency;
}
}
@@ -0,0 +1,43 @@
use std::{pin::Pin, time::Instant};
use ethercat_hal::io::temperature_input::TemperatureInput;
use super::Actor;
#[derive(Debug)]
pub struct TemperatureInputGetter {
input: TemperatureInput,
temperature: f64,
wiring_error: bool,
}
impl TemperatureInputGetter {
pub fn new(input: TemperatureInput) -> Self {
Self {
input,
temperature: 0.0,
wiring_error: false,
}
}
pub fn get_temperature(&self) -> f64 {
self.temperature
}
pub fn get_wiring_error(&self) -> bool {
self.wiring_error
}
}
impl Actor for TemperatureInputGetter {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let input = (self.input.state)().await.input;
self.temperature = input.temperature as f64;
self.wiring_error = input.overvoltage | input.undervoltage;
if self.wiring_error {
self.temperature = 0.0;
}
})
}
}
@@ -0,0 +1,23 @@
use super::Actor;
use ethercat_hal::io::temperature_input::TemperatureInput;
use std::{future::Future, pin::Pin, time::Instant};
/// Log the state of a temperature input
pub struct TemperatureInputLogger {
input: TemperatureInput,
}
impl TemperatureInputLogger {
pub fn new(input: TemperatureInput) -> Self {
Self { input }
}
}
impl Actor for TemperatureInputLogger {
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let state = (self.input.state)().await;
println!("TemperatureInputLogger: {:?}C", state.input.temperature);
})
}
}
+1 -1
View File
@@ -1,10 +1,10 @@
pub mod actors;
pub mod controllers;
pub mod converters;
pub mod ethercat;
pub mod helpers;
pub mod machines;
pub mod modbus;
pub mod realtime;
pub mod rest;
pub mod serial;
pub mod socketio;
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::machines::new::MachineAct;
use crate::actors::Actor;
use api::MachineApi;
use new::MachineNewTrait;
use std::any::Any;
@@ -10,4 +10,4 @@ pub mod manager_iter;
pub mod new;
pub mod registry;
pub trait Machine: MachineNewTrait + MachineApi + MachineAct + Any + Send + Sync + Debug {}
pub trait Machine: MachineNewTrait + MachineApi + Actor + Any + Debug {}
+1 -5
View File
@@ -8,7 +8,7 @@ use ethercat_hal::{
use ethercrab::{SubDevice, SubDeviceRef};
use smol::{channel::Sender, lock::RwLock};
use socketioxide::extract::SocketRef;
use std::{pin::Pin, sync::Arc, time::Instant};
use std::sync::Arc;
use crate::socketio::event::GenericEvent;
@@ -20,10 +20,6 @@ pub trait MachineNewTrait {
Self: Sized;
}
pub trait MachineAct {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
}
pub struct MachineNewParams<
'maindevice,
'subdevices,
-2
View File
@@ -1,5 +1,3 @@
pub mod modbus_serial_interface;
use anyhow::Error;
use crc::{CRC_16_MODBUS, Crc};
use serial;
@@ -1,268 +0,0 @@
use crate::modbus::{ModbusRequest, ModbusResponse};
use ethercat_hal::io::serial_interface::{SerialEncoding, SerialInterface};
use std::{
collections::HashMap,
time::{Duration, Instant},
};
#[derive(Debug, Clone)]
struct RequestMetaData {
priority: u32,
ignored_times: u32,
/// This is used when a machine for example takes 4ms to process the request
/// and is added ontop of the standard waiting time for a serial transfer
extra_delay: Option<u32>,
no_response_expected: bool,
}
#[derive(Debug)]
pub enum State {
/// WaitingForResponse is set after sending a request through the serial_interface
WaitingForResponse,
/// After Sending a Request we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has transmit accepted toggled
/// Then we can set state = ReadyToSend
WaitingForRequestAccept,
/// After Receiving a Response we need to wait atleast one ethercat cycle
/// After one Cycle we check if el6021 status has received accepted toggled
WaitingForReceiveAccept,
/// ReadyToSend is set after receiving the response from the serial_interface
ReadyToSend,
/// Initial State
Uninitialized,
}
#[derive(Debug)]
pub struct ModbusSerialInterface {
serial_interface: SerialInterface,
baudrate: Option<u32>,
encoding: Option<SerialEncoding>,
response: Option<ModbusResponse>,
state: State,
last_message_size: usize,
last_message_delay: u32,
pub last_message_id: u32,
no_response_expected: bool,
request_map: HashMap<u32, ModbusRequest>,
request_metadata_map: HashMap<u32, RequestMetaData>,
last_ts: Instant,
}
impl ModbusSerialInterface {
pub fn new(serial_interface: SerialInterface) -> Self {
Self {
serial_interface,
baudrate: None,
encoding: None,
request_map: HashMap::new(),
request_metadata_map: HashMap::new(),
response: None,
last_message_size: 0,
last_message_delay: 0,
state: State::Uninitialized,
last_ts: Instant::now(),
last_message_id: 0,
no_response_expected: false,
}
}
pub fn is_initialized(&self) -> bool {
!matches!(self.state, State::Uninitialized)
}
pub fn add_request(
&mut self,
request_id: u32,
priority: u32,
request: ModbusRequest,
no_response_expected: bool,
delay: Option<u32>,
) {
self.request_map.insert(request_id, request);
self.request_metadata_map
.entry(request_id)
.or_insert_with(|| RequestMetaData {
priority,
ignored_times: 0,
extra_delay: delay,
no_response_expected,
});
}
/// This is used internally to read the receive buffer of the el6021
async fn read_modbus_response(&mut self) -> Result<ModbusResponse, anyhow::Error> {
let raw_response = (self.serial_interface.read_message)()
.await
.unwrap_or_default();
let response = ModbusResponse::try_from(raw_response)?;
self.last_message_size = response.data.len() + 4;
self.state = State::WaitingForReceiveAccept;
Ok(response)
}
/// This is used internally to fill the write buffer of the el6021 with the modbus request
/// Decides what requests to send first by finding the one with the highest priority
/// For example Highest Priority requests: ResetInverter StopMotor
async fn send_modbus_request(&mut self) {
if self.request_map.is_empty() {
return;
}
let (highest_id, delay, no_response_expected) = self
.request_metadata_map
.iter_mut()
.map(|(key, value)| {
let effective_priority = value.priority + value.ignored_times;
(
*key,
effective_priority,
value.extra_delay.unwrap_or(0),
value.no_response_expected,
)
})
.max_by_key(|(_, priority, _, _)| *priority)
.map(|(key, _, delay, no_response)| (key, delay, no_response))
.unwrap(); // Safe because we checked is_empty() above
let request = &self.request_map[&highest_id];
let modbus_request: Vec<u8> = request.clone().into();
if let Err(_) = (self.serial_interface.write_message)(modbus_request.clone()).await {
tracing::error!("ERROR: serial_interface.write_message has failed");
} else {
self.last_message_delay = delay;
self.last_message_id = highest_id;
self.no_response_expected = no_response_expected;
}
self.state = State::WaitingForRequestAccept;
self.last_message_size = modbus_request.len();
}
pub async fn initialize_communication_settings(&mut self) -> bool {
let baudrate = (self.serial_interface.get_baudrate)().await;
let encoding = (self.serial_interface.get_serial_encoding)().await;
match (baudrate, encoding) {
(Some(b), Some(e)) => {
self.baudrate = Some(b);
self.encoding = Some(e);
true
}
_ => false,
}
}
pub async fn initialize(&mut self) -> bool {
if matches!(self.state, State::Uninitialized) {
let success = (self.serial_interface.initialize)().await;
if success {
self.state = State::ReadyToSend;
self.initialize_communication_settings().await;
}
success
} else {
false
}
}
fn increment_ignored_times(&mut self) {
for metadata in self.request_metadata_map.values_mut() {
metadata.ignored_times += 1;
}
}
/// Modbus RTU has silent time between frames that needs to be adhered to, if you send before silent_time is over between frames, then there will be lost frames
/// This silent time is needed to identify the start and end of messages
/// This function also takes into account the time that the slave we are talking to needs to process our request
/// bits: amount of bits sent per byte -> for a 8n1 coding: 8 data bits, 0 parity, 1 stop bit (1 start,1 stop) -> 10 bits
/// machine_operation_delay_nano: Delay for the given operation in nanoseconds as specified by the slaves datasheet (example: mitsubishi csfr84 has 12ms for read write in RAM)
/// baudrate: bits per second
/// message_size: size of original message in bytes
pub fn calculate_modbus_rtu_timeout(
&self,
machine_operation_delay: Duration,
message_size: usize,
) -> Option<Duration> {
let baudrate = self.baudrate?;
let encoding = self.encoding?;
let nanoseconds_per_bit = 1_000_000_000 / baudrate as u64;
let nanoseconds_per_byte = encoding.total_bits() as u64 * nanoseconds_per_bit;
let transmission_timeout = nanoseconds_per_byte * message_size as u64;
let silent_time = (nanoseconds_per_byte * 35) / 10; // silent_time is 3.5x of character length
let total_timeout =
transmission_timeout + machine_operation_delay.as_nanos() as u64 + silent_time;
Some(Duration::from_nanos(total_timeout))
}
pub fn get_response(&self) -> Option<&ModbusResponse> {
self.response.as_ref()
}
pub async fn act(&mut self, now_ts: Instant) {
let elapsed = now_ts.duration_since(self.last_ts);
let timeout = self.calculate_modbus_rtu_timeout(
Duration::from_nanos(self.last_message_delay as u64),
self.last_message_size,
);
let Some(timeout) = timeout else { return };
// Check if we need to wait for timeout (except for specific states)
if !matches!(
self.state,
State::Uninitialized | State::WaitingForReceiveAccept | State::WaitingForRequestAccept
) && elapsed < timeout
{
return;
}
self.last_ts = now_ts;
self.response = None;
match self.state {
State::WaitingForResponse => match self.read_modbus_response().await {
Ok(response) => {
self.response = Some(response);
}
Err(_) => {
self.response = None;
if self.no_response_expected {
self.state = State::ReadyToSend;
}
}
},
State::ReadyToSend => {
self.send_modbus_request().await;
// Remove the sent request
self.request_map.remove(&self.last_message_id);
self.request_metadata_map.remove(&self.last_message_id);
self.increment_ignored_times();
}
State::WaitingForReceiveAccept => {
// Waste at least one Ethercat Cycle here to ensure that request/response stay in sync
self.state = State::ReadyToSend;
}
State::WaitingForRequestAccept => {
// An empty vec is used to check if we are finished with writing the message
// This is to keep the Serialinterface more simple
match (self.serial_interface.write_message)(vec![]).await {
Ok(finished) if finished => {
self.state = State::WaitingForResponse;
}
Err(_) => {
self.state = State::ReadyToSend;
}
_ => {} // Still waiting
}
}
State::Uninitialized => {}
}
}
}
-178
View File
@@ -1,178 +0,0 @@
use tracing::debug;
use tracing::error;
#[cfg(unix)]
/// Makes the current thread a real-time thread using the PREEMPT_RT capabilities of Linux.
///
/// This function configures the current thread with real-time scheduling properties by:
///
/// 1. Setting the scheduling policy to SCHED_FIFO (First-In-First-Out), which is a real-time
/// scheduling policy that allows the thread to run until it voluntarily yields, blocks on I/O,
/// or is preempted by a higher-priority real-time thread.
///
/// 2. Setting the real-time priority to 80 (on a scale of 1-99, where higher values indicate
/// higher priority). This priority level is chosen to be higher than most IRQ handlers
/// (typically priority 50) but lower than critical kernel tasks (priority 99).
///
/// When running on a PREEMPT_RT patched Linux kernel, this combination provides deterministic
/// scheduling with minimal latency, which is essential for real-time applications.
///
/// # Requirements
///
/// - The program must run with appropriate permissions to set real-time priorities.
/// This typically means either:
/// - Running as root
/// - Configuring `/etc/security/limits.conf` to allow the user to set real-time priorities
///
/// - The system should be running a Linux kernel with PREEMPT_RT patch applied for
/// optimal real-time performance.
///
/// # Example
///
/// ```ignore
/// fn main() {
/// // Lock memory to prevent page faults
/// lock_memory();
///
/// // Create a thread and make it real-time
/// std::thread::spawn(|| {
/// set_realtime_priority();
///
/// // Real-time code here
/// });
/// }
/// ```
///
/// # Returns
///
/// - `Ok(())` if the real-time priority was successfully set
/// - `Err` with the error message if setting the priority failed
#[cfg(unix)]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
use anyhow::anyhow;
use libc::{SCHED_FIFO, pthread_setschedparam, sched_param};
use std::mem;
unsafe {
// Get the pthread_t of the current thread using pthread_self()
let pthread_self = libc::pthread_self();
// Set up the scheduling parameters
let mut param: sched_param = mem::zeroed();
param.sched_priority = 95;
// Set the thread to use SCHED_FIFO scheduling policy with our priority
let result = pthread_setschedparam(pthread_self, SCHED_FIFO, &param);
if result != 0 {
error!(
"Failed to set real-time scheduling policy for thread \"{:?}\": {}",
std::thread::current().name().unwrap_or("unknown"),
std::io::Error::last_os_error()
);
return Err(anyhow!(
"Failed to set real-time priority: {}",
std::io::Error::last_os_error()
));
} else {
debug!(
"Successfully set real-time priority for the thread \"{:?}\"",
std::thread::current().name().unwrap_or("unknown")
);
}
}
Ok(())
}
#[cfg(not(unix))]
pub fn set_realtime_priority() -> Result<(), anyhow::Error> {
log::error!("This platform does not support realtime threads");
Ok(())
}
/// Locks all current and future memory pages of the process into RAM to prevent page faults.
///
/// # When to call this function
///
/// This function should be called from the main thread, as early as possible in your program's
/// execution, ideally before creating any threads and before allocating large amounts of memory.
///
/// Important: You only need to call this function ONCE per process, not in each thread.
/// The memory locking applies to the entire process, including:
/// - The main thread
/// - All threads created after the call
///
/// This happens because all threads in a process share the same virtual memory space, and
/// `mlockall()` operates on the entire process's memory space, not just the calling thread.
///
/// # Example: Correct usage
///
/// ```ignore
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Call early in main, before creating threads
/// lock_memory()?;
///
/// // Now create real-time threads...
/// std::thread::spawn(|| {
/// set_realtime_priority();
/// // Real-time code here
/// });
///
/// Ok(())
/// }
/// ```
///
/// # Example: Incorrect usage
///
/// ```ignore
/// fn main() {
/// // Don't do this - calling in each thread is unnecessary
/// std::thread::spawn(|| {
/// lock_memory().unwrap(); // Redundant if already called in main
/// set_realtime_priority();
/// });
/// }
/// ```
///
/// # How it works
///
/// [Rest of documentation as before...]
#[cfg(unix)]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
use libc::{MCL_FUTURE, mlockall};
unsafe {
if mlockall(MCL_FUTURE) != 0 {
error!("Failed to lock memory: {}", std::io::Error::last_os_error());
return Err(
format!("Failed to lock memory: {}", std::io::Error::last_os_error()).into(),
);
} else {
debug!("Successfully locked memory for the process");
}
}
Ok(())
}
#[cfg(not(unix))]
pub fn lock_memory() -> Result<(), Box<dyn std::error::Error>> {
log::error!("This platform does not support memory locking");
Ok(())
}
pub fn set_core_affinity_first_core(core_id: usize) -> Result<(), anyhow::Error> {
let cores = core_affinity::get_core_ids().unwrap_or_default();
let core = cores.get(core_id);
if let Some(core) = core {
core_affinity::set_for_current(*core);
debug!(
"Set core affinity of thread \"{:?}\" to core {:?}",
std::thread::current().name().unwrap_or("unknown"),
core
);
Ok(())
} else {
error!("No cores available to set affinity");
Err(anyhow::anyhow!("No cores available to set affinity"))
}
}
+3 -23
View File
@@ -193,7 +193,7 @@ pub trait CacheableEvents<Events> {
pub type CacheFn = Box<dyn Fn(&mut Vec<Arc<GenericEvent>>, &Arc<GenericEvent>) -> ()>;
/// [`BufferFn`] that stores the last n events
/// [BufferFn] that stores the last n events
pub fn cache_n_events(n: usize) -> CacheFn {
Box::new(move |events, event| {
if events.len() >= n {
@@ -203,32 +203,12 @@ pub fn cache_n_events(n: usize) -> CacheFn {
})
}
/// [`BufferFn`] that stores only one event
/// [BufferFn] that stores only one event
pub fn cache_one_event() -> CacheFn {
cache_n_events(1)
}
/// [`BufferFn`] that stores first and last event
///
/// The primary use case of this function is to cache both the default state of a machine, which should be emitted first,
/// and the last event, which is the most recent state of the machine.
pub fn cache_first_and_last_event() -> CacheFn {
Box::new(move |events, event| {
// if the events length 0 or 1, we just push the event
if events.is_empty() || events.len() == 1 {
events.push(event.clone());
return;
}
// if the event length is 2 we remove the last event and append a new one
if events.len() == 2 {
events.remove(1);
events.push(event.clone());
return;
}
})
}
/// [`BufferFn`] that stores events for a certain duration
/// [BufferFn] that stores events for a certain duration
pub fn cache_duration(duration: Duration, bucket_size: Duration) -> CacheFn {
Box::new(move |events, event| {
// Use event.ts instead of system time
-84
View File
@@ -1,84 +0,0 @@
# Environment Setup
Working on this repository requires you to use cargo and npm. In the following document the installation process will be shown
## Ubuntu/Debian Installation
```bash
# Just press enter when prompted
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
apt install npm
apt install nodejs
# assuming you dont have git already, Git setup not included in here
apt install git
git clone git@github.com:qitechgmbh/control.git
cd control
cd electron
npm install
```
# Running Backend and Frontend Linux
## Backend
To Compile backend code and run it on Linux:
```bash
./cargo_run_linux.sh
```
The script sets capabilities on the compiled binary like raw socket access.
Here it is required that you are connected to atleast one machine that is communicated with over ethercat or usb/serial.
The Most minimal working setup would be to connect over ethernet to an ek1100 beckhoff terminal.
## Mock-Machine
The Mock-Machine can be used to test code that does not require an actual machine connection.
Like Frontend Code for example.
Mock machines can be used if the compile feature mock-machine is enabled.
```bash
# in root git folder
./cargo_run_linux.sh mock-machine
```
## Frontend
To run the Frontend Code:
```bash
# need to be in the electron folder and an additional terminal
cd electron
npm run start
```
# Contributing
Generally you contribute to the codebase by:
1. Opening or Choosing an existing issue (Bug,Feature,Task etc)
2. Work on it locally
3. Commit your changes locally
4. Before pushing changes, if there were changes on the master branch, rebase your branch like so:
```bash
git fetch
git rebase origin/master
```
5. Push changes
```bash
# IF you needed to rebase
git push --force-with-lease
# ELSE you just push
git push
```
6. Open a Pull Request on Github and link the issue by writing fix #issue_number, after the pull request is merged into master the branch is closed automatically
7. Request a Review and hope for the best :)
# Recommended Editor Setup
We recommend you to use an Editor with rust-analyzer support like VSCode to speed up development and detect errors before compiling.
+75 -144
View File
@@ -11,8 +11,6 @@ A functionality can be virtually wired to any device that matches the IO type. E
Functionalities of different machines should not be wired to the same devices because of possibly conflicting configuration and machine identification issues (which devices belong to what machine).
The IO types provide direct synchronous methods for interacting with hardware, eliminating the need for actor patterns while maintaining clean abstractions.
# Implemented IO types
- Digital Output (DO)
@@ -37,13 +35,13 @@ pub enum EL2002Port {
Then the device must implement the respective device trait like `DigitalOutputDevice`.
The `set_output` method is used to set the desired level of the output of a given port.
The `digital_output_write` is used to set the desired level of the output of a given port.
The `get_output` method is used to read the current state of the output of a given port.
The `digital_output_state` is used to read the current state of the output of a given port.
```rust
impl DigitalOutputDevice<EL2002Port> for EL2002 {
fn set_output(&mut self, port: EL2002Port, value: DigitalOutputOutput) {
fn digital_output_write(&mut self, port: EL2002Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2002Port::DO1 => self.rxpdo.channel1.as_mut().expect(&expect_text).value = value.into(),
@@ -51,95 +49,84 @@ impl DigitalOutputDevice<EL2002Port> for EL2002 {
}
}
fn get_output(&self, port: EL2002Port) -> DigitalOutputOutput {
fn digital_output_state(&self, port: EL2002Port) -> DigitalOutputState {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2002Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2002Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
})
DigitalOutputState {
output: DigitalOutputOutput(match port {
EL2002Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2002Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
}),
}
}
}
```
## Using IO types in machine logic
## Implementing for an `Actor`
IO types now provide direct synchronous methods for reading and writing values, making them easy to use in machine logic without async/await patterns.
The `act` function of an `Actor` will be called after the inputs are read from the device and before the outputs are written for the next EtherCAT frame.
The actor needs to be constructed with the `DigitalOutput` and hold it in its struct.
```rust
#[derive(Debug)]
pub struct DigitalOutputToggler {
output: DigitalOutput,
}
impl DigitalOutputToggler {
pub fn new(output: DigitalOutput) -> Self {
Self { output }
}
pub fn toggle(&mut self) {
let current_state = self.output.get();
self.output.set(!current_state);
}
pub fn turn_on(&mut self) {
self.output.set(true);
}
pub fn turn_off(&mut self) {
self.output.set(false);
}
```
Implementing the `act` function which will toggle the output every cycle.
The `state` and `write` values are callbacks and must be wrapped with parentheses to be called.
```rust
fn act(&mut self, _now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
{
let state = (self.output.state)().await;
match state.output.into() {
true => (self.output.write)(false.into()).await,
false => (self.output.write)(true.into()).await,
}
}
})
}
```
## Getting `DigitalOutput` from a `Device`
With the `new` function of the `DigitalOutput` struct, you can create a new `DigitalOutput` instance from a device and its port. The IO type provides direct synchronous methods for interaction.
With the `new` function of the `DigitalOutput` struct, you can create a new `DigitalOutput` instance from a device and its port. We can then provide this `DigitalOutput` instance to the `Actor` struct.
```rust
let el2002 = Arc::new(RwLock::new(EL2002::new()));
let el2002 = EL2002::new();
let digital_output = DigitalOutput::new(el2002, EL2002Port::DO1);
let mut toggler = DigitalOutputToggler::new(digital_output);
// Direct usage
toggler.turn_on();
toggler.toggle();
let actor = DigitalOutputToggler::new(digital_output);
```
# Creating a new IO type
To create a new IO type, we must know if it's writable (output) or readable (input), or combined (input/output) type.
For output types, we define the struct with sync closures for setting and getting values:
We first define the `DigitalOutput` struct with the `write` and `state` fields. Every IO needs a `state` field but only writable types need a `write` field.
The `state` field is an async callback which returns the `DigitalOutputState` struct.
The `write` field is an async callback which takes the value.
```rust
pub struct DigitalOutput {
set_output: Box<dyn Fn(DigitalOutputOutput) -> () + Send + Sync>,
get_output: Box<dyn Fn() -> DigitalOutputOutput + Send + Sync>,
pub write: Box<dyn Fn(DigitalOutputOutput) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>,
pub state:
Box<dyn Fn() -> Pin<Box<dyn Future<Output = DigitalOutputState> + Send>> + Send + Sync>,
}
```
For input types, we only need the getter:
Next, we need to implement the `DigitalOutputState` and `DigitalOutputOutput` structs.
The `DigitalOutputState` contains the timestamps of the output (and inputs if applicable) and the `DigitalOutputOutput` struct (and the input struct if applicable). This is the return type of `DigitalOutput::state`.
The `DigitalOutputOutput` is both contained in the `DigitalOutputState` and is the input type of the `DigitalOutput::write` function. It implements the `From` trait in both directions to `bool` to allow easy access to the inner value on this simple type (as seen in the `act` example above).
```rust
pub struct DigitalInput {
get_input: Box<dyn Fn() -> Result<DigitalInputInput, anyhow::Error> + Send + Sync>,
#[derive(Debug, Clone)]
pub struct DigitalOutputState {
pub output: DigitalOutputOutput,
}
```
For combined input/output types, we have separate getters for input and output:
```rust
pub struct StepperVelocityEL70x1 {
set_output: Box<dyn Fn(StepperVelocityEL70x1Output) -> Result<(), Error> + Send + Sync>,
get_output: Box<dyn Fn() -> Result<StepperVelocityEL70x1Output, Error> + Send + Sync>,
get_input: Box<dyn Fn() -> Result<StepperVelocityEL70x1Input, Error> + Send + Sync>,
// ... additional closures as needed
}
```
Next, we need to implement the input and output structs separately.
For a digital output, we only need the output struct:
```rust
/// Output value
/// true: high
/// false: low
@@ -159,35 +146,8 @@ impl From<DigitalOutputOutput> for bool {
}
```
For input types, we define input structs:
```rust
#[derive(Debug, Clone)]
pub struct DigitalInputInput {
pub value: bool,
}
```
For combined input/output types, we define both structs separately, avoiding the need for state containers.
```rust
pub struct DigitalOutputOutput(pub bool);
impl From<bool> for DigitalOutputOutput {
fn from(value: bool) -> Self {
DigitalOutputOutput(value)
}
}
impl From<DigitalOutputOutput> for bool {
fn from(value: DigitalOutputOutput) -> Self {
value.0
}
}
```
Now we implement the constructor for the IO type. The `new` function takes a device and port and returns the IO instance with sync closures:
Now we need to implement the constructor for a `DigitalOutput` instance. The `new` function takes a device and a port and returns a `DigitalOutput` instance.
We need to construct the async closures which is a bit of type madness since we also need to move the device `Arc` into them.
```rust
impl DigitalOutput {
pub fn new<PORT>(
@@ -197,75 +157,46 @@ impl DigitalOutput {
where
PORT: Clone + Send + Sync + 'static,
{
// build sync write closure
// build async write closure
let port1 = port.clone();
let device1 = device.clone();
let set_output = Box::new(move |value: DigitalOutputOutput| {
let mut device = block_on(device1.write());
device.set_output(port1.clone(), value);
});
let write = Box::new(
move |value: DigitalOutputOutput| -> Pin<Box<dyn Future<Output = ()> + Send>> {
let device_clone = device1.clone();
let port_clone = port1.clone();
Box::pin(async move {
let mut device = device_clone.write().await;
device.digital_output_write(port_clone, value);
})
},
);
// build sync get closure
// build async get closure
let port2 = port.clone();
let device2 = device.clone();
let get_output = Box::new(move || -> DigitalOutputOutput {
let device = block_on(device2.read());
device.get_output(port2.clone())
});
DigitalOutput {
set_output,
get_output,
}
}
/// Set the digital output value
pub fn set(&self, enabled: bool) {
(self.set_output)(enabled.into());
}
/// Get the current output value
pub fn get(&self) -> bool {
let output = (self.get_output)();
output.into()
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = DigitalOutputState> + Send>> {
let device2 = device2.clone();
let port_clone = port2.clone();
Box::pin(async move {
let device = device2.read().await;
device.digital_output_state(port_clone)
})
},
);
DigitalOutput { write, state }
}
}
```
Lastly, we create the device trait that needs to be implemented by devices. The trait methods are simplified and separated by input/output:
Lastly, we can create the `DigitalOutputDevice` trait which needs to be implemented by the device.
```rust
pub trait DigitalOutputDevice<PORT>: Send + Sync
where
PORT: Clone,
{
fn set_output(&mut self, port: PORT, value: DigitalOutputOutput);
fn get_output(&self, port: PORT) -> DigitalOutputOutput;
fn digital_output_write(&mut self, port: PORT, value: DigitalOutputOutput);
fn digital_output_state(&self, port: PORT) -> DigitalOutputState;
}
```
For input devices:
```rust
pub trait DigitalInputDevice<PORT>: Send + Sync
where
PORT: Clone,
{
fn get_input(&self, port: PORT) -> Result<DigitalInputInput, anyhow::Error>;
}
```
For combined input/output devices:
```rust
pub trait StepperVelocityEL70x1Device<PORT>: Send + Sync
where
PORT: Clone,
{
fn set_output(&mut self, port: PORT, value: StepperVelocityEL70x1Output) -> Result<(), Error>;
fn get_output(&self, port: PORT) -> Result<StepperVelocityEL70x1Output, Error>;
fn get_input(&self, port: PORT) -> Result<StepperVelocityEL70x1Input, Error>;
// Additional methods as needed for specific device functionality
fn get_speed_range(&self, port: PORT) -> SpeedRange;
}
```
+980
View File
@@ -0,0 +1,980 @@
# Operating Instructions
Original operating instructions
QiTech Industries
Weichselstraße 9, 67574 Osthofen,
Germany
---
# JARVIS Pro Winder
# EC Declaration of Conformity
according to 2006/42/EC, Annex II, No. 1 A
The content of the EC Declaration of Conformity corresponds to Annex II, No. 1 A, of Directive
2006 / 42 /EC.
Manufacturer of the machine:
```
QiTech Industries GmbH
Weichselstraße 9
67574 Osthofen Authorised
```
representative of the manufacturer:
Simon Kolb
Salzgasse 6
67574 Osthofen Machine
under consideration:
We hereby declare that the above mentioned machine is in conformity with all relevant provisions
of the EC Machinery Directive 2006/42/EC. The machine is also in compliance with all relevant
provisions of the following EC Directives:
- 2014 / 30 /EU Electromagnetic Compatibility / European EMC Directive
- 2014/35/EU Low Voltage Directive (protection objectives only)
- 2011/65/EU ROHS Directives (ROHS 2 )
The following harmonised standards (or parts of these standards) have been applied
- DIN EN ISO 12100
```
General designation Filament winding machine
function Winding and rewinding of filaments
Model JARVIS Pro Winder
type Filament winding machine
Serial number PB- 5 W HTL- 02
Trade name JARVIS Pro Winder
Year of manufacture 2024
```
Darmstadt,
15 / 03 / 2023
_Note: References of the EC directives, harmonised standards and standards and specifications can be found in the
standards search_
```
Darmstadt, 26/10/
```
Mr Kolb
# Operating instructions based on DIN
# EN ISO 20607
```
Designation JARVIS Pro Winder
Year of manufacture 2024
Manufacturer: QiTech QiTech Industries GmbH
Administrator Simon Kolb
Salzgasse 6
67574 Osthofen
```
```
Date of issue 11 April 2023
Publication version V1.0 - German
```
Read before commissioning!
These operating instructions are intended to enable all persons working on the machine to carry
out their work safely and correctly.
Every person who operates, maintains or repairs the machine must have read the operating
instructions, in particular the safety regulations, before commissioning.
These operating instructions must be available on the machine at all times. The operator
must ensure that all information necessary for the safe operation of the machine is made
available to the operating personnel. If the operating instructions are available in electronic
form, the operator must ensure that the operating personnel are able to view the operating
instructions.
The original version of these operating instructions is written in German.
We therefore ask you to read through the relevant instructions carefully and to observe them
thoroughly.
After unpacking the machine, check for any damage that may have occurred during
transport.
ATTENTION: Before using the machine for the first time, please inform yourself about the national
regulations (occupational safety, environment).
Reproduction and reprinting in any form, even in extracts, requires the written authorisation of
QiTech Industries.
## Table of contents
- 1 Machine description
- 1.1 Noise emissions
- 1.2 General, important safety instructions
- 1.3 Description of the pictograms, symbols and warnings used
- 1.4 Intended use.............................................................................................................................................
- 1.5 Reasonably foreseeable misuse................................................................................................................
- 1.6 Significant change / other use
- 1.7 Description of the safety devices
- 1.8 Instruction and training............................................................................................................................
- 1.9 Noise emissions from the machine during activities
- 2 Life phases of the machine
- 2.1 Transport and assembly
- 2.2 Assembly, installation and commissioning
- 2.3 Operation and control (work sequence)
- 2.4 Maintenance, inspection, testing
- 2.5 Troubleshooting and fault rectification....................................................................................................
- 2.6 Dismantling, decommissioning and disposal
- 3 Hazard warning and information on residual risks
- 4 Glossary and definitions
## 1 Machine description
```
Item number Item number Description
```
```
1 Spool axis
```
```
2 Coil quick-release fastener
```
```
3 Grooved plate
```
```
4 Traverse
```
```
5 Installer
```
```
6 Laser diode
```
```
7 Load arm
```
```
8 Weight disc
```
```
9 Puller rollers
```
```
10 Filament guide
```
```
11 Emergency stop switch
```
```
12 24V 10A power supply unit
```
```
13 USB-A to USB-C cable
```
```
14 Driver (mandrel)
```
```
15 Underframe (optional)
```
## 1.1 Noise emissions
Sound power level (LWAC)
```
Specification Value Unit
Idle mode <70 dB
```
Workplace-related emission value (LPAC)
```
Specification Value Unit
Idle mode <70 dB
```
Noise emission specification according to DIN 45649 Part 1
Noise measurement method and operating condition acc: DIN 45635 Part 16 and Part 1605
Accuracy class: 2
## 1.2 General, important safety instructions
1. Only professionally trained persons may operate the machine. The guarantee and warranty will
be invalidated if damage is caused by improper operation.
2. We would like to point out that no liability is accepted for damage caused by failure to observe these
operating instructions.
3. The operator of the machine must ensure that at least one copy of the operating instructions is kept in
the immediate vicinity of the machine and is accessible to persons working with the machine.
4. The operator must ensure that the safety and danger notices on the machine are observed and that the
signs are clearly legible.
5. Never work without the prescribed personal protective equipment (e.g. safety footwear).
6. Wear close-fitting clothing and a hairnet if you have long hair. Do not wear loose or baggy clothing
(ties, shirt sleeves, jewellery, etc.).
7. Activities with a risk of entrapment may only be carried out without gloves. If gloves are required for
individual activities, the machine must first be brought into a safe condition.
8. If the emission sound pressure level at the workplace exceeds 80 dB (A), hearing protection must be worn.
9. Never leave the machine in operation without supervision.
10. Secure your machine so that it cannot be switched on by children. Persons who have not been
instructed must not operate the machine.
11. Before using the machine, make sure that it is in perfect condition (carry out a visual inspection before
starting work). Pay particular attention to any damage to the earthing contact plug or the electrical
connection. Never use the machine with defective, crushed or bare cables.
12. Insert the earthing contact plug into an earthing contact socket suitable for the machine. The supply cable
for the machine may only be connected to a socket outlet with earthing contact or a connection box. Have
the socket outlet or connection box checked by a qualified electrician beforehand.
13. During maintenance and cleaning work, the machine must be switched off and the earthing contact
plug disconnected. Work on the electrical equipment of the machine may only be carried out by
qualified electricians. The safety regulations for electrical engineering must be observed:
- Switch off the machine
- Disconnect the USB connection
- Secure the machine against being switched on again
14. Have the machine de-energised by a qualified electrician
15. Earth and short-circuit the machine
16. Cover or isolate neighbouring live parts
17. Do not reach into moving parts of the machine.
18. Keep a safe distance from rotating parts of the machine. Only change the bobbin when the bobbin axis is stationary.
19. Keep the work area clean. Do not place any objects on the machine.
20. Always switch off the machine when you are not using it.
21. Stay with the machine until it has come to a standstill.
22. Repairs may only be carried out by qualified specialist personnel! Repair work may only be carried out by persons who are qualified for the repair work in question and who are familiar with the relevant occupational safety regulations.
23. Protect the machine from moisture.
24. Check the machine regularly for damage. Only replace damaged parts with original parts and have them
replaced by qualified specialists. The guarantee and warranty will be invalidated if accessories and spare
parts are used that are not authorised for the machine.
1. To avoid inadequate lighting, we recommend installing light sources in accordance with national
health and safety regulations (e.g. workplace directive).
1. All persons working on the machine must be trained on the basis of these operating instructions before
starting work. In particular, persons working on the machine must be familiarised with the safety
markings described in chapter 1.3.
```
Danger!
Danger to life due to technically faulty machine
The machine may only be used when it is in perfect technical condition and in accordance with
its intended use, in a safety-conscious and hazard-conscious manner and in compliance with
these operating instructions. All faults, especially those that could impair safety, must be
rectified immediately before starting the winder.
Everyone who works on or with the JARVIS Pro Winder is responsible for observing these safety
aspects!
```
## 1.3 Description of the pictograms, symbols and warnings used
```
Warning signs Mandatory sign
Pictogram Description of pictogram Pictogram Pictogram Description
```
```
W001 General warning sign M001 General warning sign
Mandatory sign
The combination of the
symbol W001 and the
signal word " Danger "
indicates an
indicates an imminently
hazardous situation which, if
not avoided
death or serious injury if it is
not avoided.
```
```
This combination of symbol
and signal word
indicates a potentially
hazardous situation which, if
not avoided, may result in
damage to property or the
environment.
not avoided.
The combination of the
symbol W001 and the signal
word " Warning " indicates an
imminently hazardous
situation which, if not avoided,
will result in death or serious
injury.
death or serious injury if it is
not avoided.
```
```
The combination of the
symbol W001 and the signal
word " Caution " indicates an
indicates an imminently
hazardous situation which, if
not avoided
death or serious injury if not
avoided.
```
W008 Warning of risk of falling M003 Use hearing protection
W011 Warning of
danger of slipping
M004 Use eye protection
W004 Warning of laser beam M008 Use foot protection
W012 Warning of electrical
voltage
M009 Use hand protection
W017 Warning of hot surface M010 Use protective clothing
W018 Warning
against automatic
start-up
M012 Use handrail
W019 Warning of crushing
hazard
```
M021 Switch off before
maintenance or repair
```
W024 Warning against hand
injuries
M014 Use head protection
W025 Warning
against counter-
rotating rollers
W016 Warning of toxic
substances
**Prohibition signs International pictograms / other**
**Pictogram Pictogram description Pictogram Pictogram Description**
```
P001 General
prohibition sign
```
F001 Fire extinguisher
P002 No smoking ANSI Z535.3 - DANGER
```
P003 No naked flames; fire,
open source of ignition and
smoking prohibited
```
ANSI Z535.3 - WARNING
```
P006 Prohibited for industrial
trucks
```
ANSI Z535.3 - CAUTION
```
P009 Climbing prohibited -
In the sense of climbing
prohibited for unauthorised
persons
```
ANSI Z535.3 - NOTICE
```
P022 Eating and drinking
prohibited
```
```
ANSI Z535.3 - SAFETY
INSTRUCTIONS
```
```
P024 Entering the area
prohibited
```
```
Symbol for separate collection
of waste electrical and
electronic equipment
```
P031 Switching prohibited
```
D-P006 Access prohibited for
unauthorised persons
```
## 1.4 Intended use.............................................................................................................................................
The JARVIS Pro Winder is designed for winding and rewinding filaments between 1 mm and 3 mm.
The winder must be placed on a stable, level surface or a braked trolley and secured if necessary. The test and
work sequence is described in chapter 3.4 Work sequence.
When using the winder, it is essential that the work steps described in the chapter "Work sequence" or in the
operating instructions are followed.
In the event of significant changes or deviations, it is necessary to carry out a new risk assessment in
accordance with the Machinery Directive and to document the changes in the corresponding documents.
The machine is designed exclusively for use inside buildings. The machine must not be
operated in potentially explosive atmospheres.
Use in the area of food production is prohibited.
Use of the machine for purposes other than those specified is prohibited.
Other intended uses include cleaning and maintenance work as well as troubleshooting, fault rectification and
repair.
## 1.5 Reasonably foreseeable misuse................................................................................................................
**Reasonably foreseeable misuse** exists in particular when winding unsuitable filaments:
- Filaments that are difficult to bend
- Easily tearable filaments
- Filaments outside the permitted diameter range of 1mm - 3mm
- Filaments with a surface temperature of> 50°C
- Particularly flat, sharp-edged or pointed filaments
In addition, the following procedures must be regarded as misuse:
- Operating the winder on an uneven surface or in a position other than the intended horizontal position
or on an unbraked trolley.
- The use of unauthorised coil sizes.
- Use of spare parts and components not authorised by the manufacturer.
- Operating the winder without supervision or in ghost shifts.
Figure 1: Authorised coil sizes
Furthermore, there is always the possibility that existing/provided safety guards are dismantled or the
existing sensors are manipulated or triggered by means other than the intended components.
The **misuse** mentioned must be explicitly prohibited with reference to these operating instructions and by written
operating instructions, and the use of the winder must also be explicitly restricted in the operating instructions to
the intended use mentioned above.
Maintenance and repair of the machine may only be carried out by the manufacturer's employees, trained
skilled workers or semi-skilled and trained personnel.
## 1.6 Significant change / other use
Any use of the machine other than the intended use is prohibited and is considered improper use. Claims of
any kind against the manufacturer or its authorised representatives due to damage resulting from improper
use of the machine are excluded. The operator alone is liable for all damage caused by improper use.
## 1.7 Description of the safety devices
**Emergency stop devices or emergency stop devices**
```
Figure 2: Emergency stop or emergency stop command device
```
The emergency stop switch is used to quickly stop the machine in the event of danger. It can be reached from
all positions in front of the machine. When an emergency stop device is actuated, the entire machine is
stopped by disconnecting the mains supply.
Hazardous movements are brought to a controlled standstill as quickly as possible. The machine can only be
restarted by physically resetting the emergency stop switch and then starting one of the operating modes in
the software.
**Overheating protection**
If one of the motor drivers becomes too hot, it switches off automatically and sends an error message to the
control system. This then stops all other components of the machine.
**Spool stop in the event of filament breakage**
If the filament tears during winding, the spool stops automatically after a short time.
**Separating protective devices:**
The housing clad with aluminium Dibond plates prevents contact with hot motors, interference with the drive
spindle of the traverse and manipulation of the electronics. It also prevents electrostatic charges from jumping
from the filament to the electronics.
If guards are opened or removed for maintenance work, they must be closed or refitted before the machine is
switched on again. It is not permitted to operate the machine without all properly installed and operational
protective devices.
## 1.8 Instruction and training............................................................................................................................
As the contractor/operator, you are obliged to inform and instruct the operating personnel about existing legal
and accident prevention regulations as well as about existing safety equipment on the winder. This obligation
also extends to safety equipment installed around the winder. The various technical qualifications of the
employees must be taken into account.
The operating personnel must have understood the instruction and it must be ensured that the instruction is
observed.
This is the only way to ensure that your personnel work in a safety and hazard-conscious manner. Compliance with
the contents of the instruction should be checked regularly.
As the owner/operator, you must therefore obtain written confirmation from each employee that they have
attended an instruction session.
## 1.9 Noise emissions from the machine during activities
The noise emissions of the machine were measured on the same machine types. Under normal operating
conditions, the maximum noise emission of the extruder is below the maximum permitted values.
Depending on the activity or the duration of the activity, the operator must determine whether suitable hearing
protection is required as part of a risk assessment.
## 2 Life phases of the machine
## 2.1 Transport and assembly
**Requirements for the installation site**
- Installation only in industrial and commercial indoor areas.
- The surface must be level and solid.
- The location must be as fire-free as possible and equipped with a fire extinguisher.
## 2.2 Assembly, installation and commissioning
**Basic installation**
If the winder is supplied without a base, it must be screwed to the surface with M8 screws at each of the four
corners.
If the Winder is supplied with a mobile container, the parking brakes must be applied.
In both cases, the cables must be routed in such a way that they cannot be tripped over.
**Installing the JARVIS Control Software**
The machine is controlled using the specially designed JARVIS Control software. This can either be obtained
from the manufacturer's website or installed on a Windows or MacOS system from the enclosed USB stick.
Follow the instructions in the software to install the required drivers and establish a connection with the
winder via the USB cable.
```
Caution!
Danger to life due to incorrect installation and commissioning!
Errors during installation can lead to life-threatening situations or cause considerable damage to
property.
Installation and commissioning may only be carried out by the manufacturer's employees.
```
```
Danger!
Risk of injury due to improper transport.
Be careful when loading the machine, do not place any limbs under the parts when setting them
down, do not stand under suspended loads or between moving loads and walls or machine parts.
Wear personal protective equipment: safety shoes, protective work clothing, protective gloves
```
Some important safety measures must be observed to ensure safe operation of the extruder:
- It is important to only use original fuses with the prescribed current ratings to avoid overloading.
- If inspection, maintenance and repair work is carried out on electrical components, these must be de-
energised beforehand. Equipment that has been disconnected must be secured against unintentional
or automatic reconnection. First check that there is no voltage and insulate neighbouring live
components.
- Care must be taken to ensure that design features are not modified in such a way as to reduce safety.
- If it is necessary to work on live components, an additional person should be called in to operate
the emergency stop button or the main switch in an emergency.
- Only voltage-insulated tools and appropriate clothing may be used.
- Proper earthing of the electrical system must be ensured by protective conductor systems.
- Cables must be checked regularly for damage and replaced if necessary.
## 2.3 Operation and control (work sequence)
A visual inspection must be carried out every working day before the machine is started
up. Then start the machine as follows:
1. Switch on the machine by plugging in the mains plug
- Connect the machine to the control software
- Connect the USB cable to the PC
- Follow the instructions in JARVIS Control
- Winder then appears in the "Machines" tab of the software
2. Mount the bobbin on the bobbin axle
- Remove the quick-release fastener
- Move the driver in the groove to the height of the hole in the coil flank
- Slide the bobbin over the axle and the driver
- Tighten the quick-release fastener again
3. Calibrating the bobbin sides
- Select "Calibrate" in the function bar of the software
- Move the "Endstops" slider to the approximate position of the inside of the bobbin
- Click on "Calibrate" again
- using the laser dot, repeat the previous steps and fine-tune until the dot is exactly on the edge
of the inner bobbin flank
- Repeat the same process for the other outer coil flank
4. Adjust the filament diameter
- Adjust the diameter of the filament in the Settings tab (1.75 mm by default)
- Select "Pull in" in the function bar
- Thread the filament through the puller rollers (be careful, counter-rotating rollers!)
- For extrusion: Use the "Puller Speed" slider to manually adjust the speed until the filament has
the desired diameter
5. Start winding
- Depending on the winding speed and the desired winding pressure, load the load arm with the
appropriate number of weighted discs (slow winding with few discs, fast winding with more
discs)
- Rotate the load arm upwards and place it on the puller
- Press the "Wind" button
- Thread the filament coming out of the puller through the feeder into the spool and fix it there
- Place the load arm on the filament strand from above
6. Change the spool
- Press the "Pull in" button
- Remove the spool as described above and insert a new spool
- Continue winding by clicking on "Wind"
7. Stopping the winding process
- "Standby" mode - motors energised but not in motion or
- Select "Off" mode - no current on the motors
- If the filament gets caught, the spool spins or there is a danger to persons, press the emergency
stop button
8. Switch off the machine by disconnecting the mains connection and the USB cable
**Unwinding filament**
- Mount the filament roll to be unwound onto the spool axle
- Select the "Pull out" function
- Feed the filament through the puller rollers in the opposite direction to winding (caution,
counter-rotating rollers!)
- Omit the traverseer and fold the load arm upwards
**Changing process variables during operation**
All settings such as the position of the spool sides, the winding speed or the filament diameter can also be
adjusted during the winding process without interrupting it. To do this, change the value in the relevant field
and click the "Wind" button again to apply it. As soon as a setting has been changed, it is marked with an
orange bar.
**Rewinding filament - not all winder versions**
- Mount the full spool on the holder provided for this purpose
- Mount the empty bobbin on the bobbin axle
- Calibrate the winding sides and set the filament width
- Slightly increase the pressure on the axle with the full spool using the rotary wheel on the top of the
housing
- Guide the filament from the full spool over the feeder and fix it to the spool
- Click on "Rewind" in the Jarvis Control function bar
- Adjust the rewinding speed using the slider
**Automatically tune the winding speed to the desired diameter**
- Ensure that both a DRE and the winder are connected to Jarvis Control
- Ensure that the extruder has a constant output of +-1% fluctuation
- Enter the distance between the DRE measuring window and extruder nozzle in the "Settings" tab
- Start the winding process as usual via "Wind"
- Activate the "Sync to DRE" button next to the "Puller Speed" slider
- Wait approx. 10 minutes until the filament diameter is within the range of the set value
**Stop the winding process automatically after a certain number of metres**
- Activate the "Auto stop winding or pulling" option
- Select target length, target mode and alarm settings
- Click on "Wind" or "Pull" in the function bar
- As soon as the blue progress bar has reached the right-hand edge of the screen, the desired
number of metres has been wound and the winder automatically switches to the specified mode
Figure 3: View of the winder control in Jarvis Control
## 2.4 Maintenance, inspection, testing
Maintenance and servicing work on the machine may only be carried out by
only be carried out by reliable, trained personnel. The minimum age required by law in the country of use must be
observed!
Personnel to be trained, instructed or undergoing general training may only work on the Winder under the
constant supervision of an experienced person!
**Updating the Winder software**
There are regular updates for the "JARVIS Control" software, which may contain bug fixes as well as new
functions or security updates. It is therefore recommended that you regularly check the manufacturer's
website for new versions of the control software. Updates for the machine software itself and installation
instructions may also be provided there.
```
Danger!
Danger to life due to operation of the machine in faulty condition
If safety-relevant changes or defects occur on the
If safety-relevant changes or defects occur on the machine, shut down immediately, secure against
restarting and report the process to the responsible office/person!
```
**Lubricating the traverse spindle**
The threaded spindle must be re-oiled after approx. 20 operating hours, but at the latest when the traverser no
longer moves to the desired position because the motor is spinning.
To do this, loosen the plate on the top of the housing with an M5 Allen key and drizzle commercially available
lubricating oil or grease, e.g. WD40, over the entire length of the threaded spindle.
**Retightening screw connections**
After approx. 100 operating hours, the rear panel of the winder should be removed using an M5 Allen key so that
the screw connections inside the housing can be retightened. The following should be checked:
- Grub screws of the motor couplings (puller, traverse, coil)
- Fastening screws of the motors (puller, traverse, coil)
- Fastening screws of the angle sensor of the load arm All
should be tightened at least hand-tight.
**Replacing the puller rollers**
The polyurethane coating of the puller rollers will inevitably wear out over time. As soon as you notice the
filament slipping through the rollers, they should be replaced. Suitable spare parts are available from the dealer.
To replace them, the rear panel must be removed and the screws of the puller panelling loosened from the
rear. The connection between the motor axle and rollers must also be disconnected. By loosening a few screws
on the front panelling on the side of the puller, it is possible to remove the panelling by bending it slightly. The
rollers can now be pulled off the axles and replaced.
## 2.5 Troubleshooting and fault rectification....................................................................................................
**Machine is not recognised by JARVIS Control**
- Is the USB cable connected?
- Are the drivers listed in the software installed correctly?
- Is the latest version of JARVIS Control installed?
- Is the device displayed in the "Device Manager" (Windows)?
**Filament tears off frequently**
- Was the filament sufficiently cooled before winding?
- Have the weights been removed from the load arm?
- Is the filament within the permitted diameter range of 1mm - 3mm?
**Puller rollers are scored**
It is normal for the puller rollers to wear out over time. However, some options can be checked
**Caution!**
```
Observe the instructions for maintenance work when servicing the machine!
```
can be checked:
- Is the surface temperature< 50°C when the filament runs through the rollers?
- Is the filament slipping through the rollers instead of being conveyed by them, causing more
abrasion?
- Is the tension of the filament too strong? Adjust with weights if necessary.
**Filament is not wound perfectly parallel**
- How large is the inner diameter of the spool on which the filament is wound? The closer the winding is
to the traverse, the better the winding pattern will be.
- Have the spool sides been calibrated as described?
- Has the correct filament diameter been set?
- Have you tried to fine-tune the two settings independently of the procedure described here?
- Does the problem already occur in the first layer, or only afterwards? If the first layer is not perfectly
wound (e.g. due to skewed threading), the filament in the subsequent layers always slips into the
gaps in the winding below and "perfect winding" can no longer take place.
**Winder suddenly switches off**
One of the motor drivers has probably overheated. To prevent uncontrolled winding behaviour, the controller
therefore switches off the entire winder. It is advisable to wait approx. 5 minutes and try winding again. If the
error occurs frequently, a lower ambient temperature (optionally housing temperature) must be ensured.
**Load adjustment does not work correctly at high or low speeds**
The sensor inside the housing has probably twisted and the load adjustment range has therefore shifted
upwards or downwards. To rectify this, turn the angle sensor back and tighten its screws as described in
"Tightening screw connections".
## 2.6 Dismantling, decommissioning and disposal
When the machine has reached the end of its planned service life, it is recommended that the manufacturer be
contacted for the purpose of taking it back free of charge. If this is not possible, the machine must be taken out
of service permanently and then disposed of or scrapped properly.
To do this, first disconnect the entire power supply. All components must then be separated by material class to
enable subsequent recycling or differentiated scrapping.
The machine consists largely of aluminium and plastic and can be easily separated by loosening the screw
connections. Electrical scrap, plastic, rubber seals, lubricants and other auxiliary materials must be
disposed of separately by an authorised specialist company.
## 3 Hazard warning and information on residual risks
```
Danger!
Automatic speed adjustment of puller, reel and traverse
The bobbin automatically adjusts its speed to keep the winding pressure constant.
```
```
Danger!
Electrostatic charges on the puller and spool
Friction of the filament on the puller rollers and on the spool can lead to electrostatic charges.
There is a risk of minor electric shocks! Do not touch the affected areas, but keep a sufficient
distance!
```
```
Danger!
Electric
shock
Before carrying out any repair, set-up or maintenance work, the winder must be de-energised
by disconnecting the mains plug!
Pull out the mains plug!
```
```
Danger!
Danger of cutting tensioned filament
The stretched filament must not be touched between the puller and the spool. Danger of
cutting! Do not wind any pointed, sharp-edged or particularly thin materials!
```
```
Danger!
Operation only permitted with protective devices fitted!
The winder may only be operated with properly fitted and functioning safety guards. The operator
is responsible for
operator is responsible for ensuring that all protective devices are properly fitted.
```
```
Danger!
Danger of getting caught, pulled in or pulled along by moving parts of the
machine
Do not wear loose long hair, loose clothing or jewellery! There is always a risk of getting caught,
pulled in or pulled along by moving parts!
```
```
Appliances with this logo on the packaging or on the appliance must be
disposed of separately. These devices must not be disposed of
disposed of with normal household waste. You are responsible for
ensuring that any electrical or electronic waste is disposed of at the
appropriate locations, e.g. recycling centres.
```
**Danger!
Risk of crushing between the traversing unit and machine housing**
Exercise extreme caution when moving the traverser towards the housing! Do not place any limbs
in the gap!
**Danger!
Risk of crushing when threading the filament**
Exercise extreme caution when threading the filament through the puller rollers! There is a risk of
crushing between the upper and lower pair of rollers.
**Danger!
Feeding of body parts**
Extreme caution is required in the area of the puller and spool axle! There is an acute danger of
getting caught or pulled in!
Make sure you wear close-fitting clothing!
Always keep the emergency stop switch within easy reach so that it can be activated immediately
in an emergency!
The traverse automatically adjusts its speed to the reel in order to maintain the same travel
distance per revolution.
In "Sync to DRE" mode, the puller automatically adjusts its speed to achieve the correct
filament diameter.
## 4 Glossary and definitions
**Specialised personnel**
Due to their specialist training, knowledge and experience as well as knowledge of the relevant regulations,
skilled personnel are able to carry out the work assigned to them and to recognise and avoid potential
hazards independently.
It is assumed that specialised personnel have also been instructed (see definition of instructed person).
**Instructed person**
The instructed person has been instructed by the operator on the tasks assigned to them and the potential
hazards associated with improper behaviour. The correct execution of the activities is monitored by specialised
personnel.
**Qualified electrician******
A qualified electrician is someone who is able to assess the work assigned to them and recognise potential
hazards based on their technical training, knowledge and experience as well as knowledge of the relevant
regulations.
**Operator**
The operator is the person who uses, adjusts and monitors the machine in day-to-day operation. He is
characterised by experience in handling the machine as well as in-depth knowledge of its functions
and how to proceed in the event of a fault.
**Employment ban**
The following persons may not work on the machine
- Persons with skin diseases
- Persons with chronic respiratory problems or a tendency to allergic reactions
- Pregnant women and nursing mothers
```
Danger!
Risk of injury if insufficiently qualified!
Improper handling can lead to considerable personal injury and damage to property.
```
- Special activities may only be carried out by the persons specified in the relevant sections of
these instructions
**Life phases of the machine / system**
```
Life phase
of the machine/system
```
```
User/qualification
```
```
Construction - Commissioning
A Construction Trained and instructed specialised personnel of the manufacturer
B Transport Trained and instructed specialist personnel of the manufacturer or forwarding
agent/transporter
C Installation
Commissioning
```
```
Trained and instructed specialist personnel of the manufacturer or service
technician of the manufacturer
Deployment/use
D Automatic operation
E Semi-automatic operation
F Setting/adjusting/restarting the
system
G Testing
```
```****
Instructed personnel of the operator
```
```
H Eliminating faults in the work
process
```
```
Trained and instructed specialised personnel of the operator or service
technicians of the manufacturer
I Observation of production processes
J Troubleshooting and fault rectification
K Cleaning, maintenance
```
```
Instructed personnel of the operator
```
```
L Maintenance Trained and instructed specialised personnel of the operator or service
technicians of the manufacturer
M Decommissioning Trained and instructed specialised personnel of the operator
```
+15 -16
View File
@@ -6,32 +6,31 @@ This guide will help you set up a NixOS system with QiTech Control and manage up
## Initial Setup
### 1. Install Git
### 1. Add Home Manager Channel
```bash
nixos-env -i git
sudo nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager
sudo nix-channel --update
```
### 2. Clone the Control Repo
### 2. Configure System Files
Create the three main configuration files:
- `flake.nix` - Defines inputs, overlays, and system configuration
- `configuration.nix` - Main NixOS system configuration
- `home.nix` - Home Manager configuration for the qitech user
### 3. Build and Apply Configuration
```bash
git clone https://github.com/qitechgmbh/control
cd control
# Build and switch to the new configuration
sudo nixos-rebuild switch --flake .#nixos
```
### 3. Run Install Script
```bash
# run as admin either with sudo or as root!
sudo ./nixos-install.sh
```
After the Installation script is finished the Computer should have rebooted and automatically started the control software.
### 4. Manual configuration
Disable any power management features or display sleep settings in the GNOME settings.
Check that the on-screen keyboard is enabled under accessibility settings.
Disable any power management features or display sleep settings in the GNOME settings. Check that the on-screen keyboard is enabled under accessibility settings.
## System Configuration Overview
+6 -20
View File
@@ -28,7 +28,7 @@ type Props = {
title: string;
description?: string;
icon?: IconName;
defaultValue?: number;
defaultValue: number;
min?: number;
minSlider?: number; // Override the slider min value
minLabel?: string;
@@ -38,7 +38,6 @@ type Props = {
step?: number;
valueSchema?: z.ZodType<number>;
inverted?: boolean;
confirmation?: string;
renderValue: (value: number) => string;
onChange?: (value: number) => void;
};
@@ -97,11 +96,8 @@ export function EditValue({
maxLabel,
minSlider,
maxSlider,
confirmation,
onChange,
}: Props) {
const defaultOrZero = defaultValue ?? 0;
// Form setup
const formSchema = z.object({
value: schema ?? z.number(),
@@ -110,7 +106,7 @@ export function EditValue({
const form = useForm<FormSchema>({
resolver: zodResolver<FormSchema>(formSchema),
values: { value: value ?? defaultOrZero },
values: { value: value ?? defaultValue },
defaultValues: { value: defaultValue },
mode: "all",
});
@@ -254,20 +250,10 @@ export function EditValue({
const handleSubmit = () => {
form.handleSubmit((data) => {
if (confirmation) {
if (window.confirm(confirmation)) {
onChange?.(data.value);
setOpen(false);
setNumpadExtended(false);
resetInput();
}
// else do nothing if user cancels
} else {
onChange?.(data.value);
setOpen(false);
setNumpadExtended(false);
resetInput();
}
onChange?.(data.value);
setOpen(false);
setNumpadExtended(false);
resetInput();
})();
};
@@ -17,7 +17,6 @@ import { StatusBadge } from "@/control/StatusBadge";
export function Extruder2ControlPage() {
const {
state,
defaultState,
nozzleTemperature,
nozzlePower,
frontTemperature,
@@ -42,35 +41,6 @@ export function Extruder2ControlPage() {
isDisabled,
} = useExtruder2();
function isZoneReadyForExtrusion(
temperature: number,
targetTemperature: number,
) {
// if temperature is 90% of the target temperature, then we are ready for extrusion
return temperature >= 0.9 * targetTemperature && targetTemperature > 0.0;
}
const allZonesReadyForExtrude = () => {
const frontReady = isZoneReadyForExtrusion(
frontTemperature.current?.value ?? 0,
state?.heating_states.front.target_temperature ?? 1,
);
const middleReady = isZoneReadyForExtrusion(
middleTemperature.current?.value ?? 0,
state?.heating_states.middle.target_temperature ?? 1,
);
const backReady = isZoneReadyForExtrusion(
backTemperature.current?.value ?? 0,
state?.heating_states.back.target_temperature ?? 1,
);
const nozzleReady = isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
return frontReady && middleReady && backReady && nozzleReady;
};
return (
<Page>
<ControlGrid>
@@ -139,7 +109,7 @@ export function Extruder2ControlPage() {
<Label label="Target Output RPM">
<EditValue
value={state?.screw_state.target_rpm}
defaultValue={defaultState?.screw_state.target_rpm}
defaultValue={0}
unit="rpm"
title="Target Output RPM"
min={0}
@@ -151,11 +121,11 @@ export function Extruder2ControlPage() {
<Label label="Target Pressure">
<EditValue
value={state?.pressure_state.target_bar}
defaultValue={defaultState?.pressure_state.target_bar}
defaultValue={0}
unit="bar"
title="Target Pressure"
min={0.0}
max={270.0}
max={150.0}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetPressure}
/>
@@ -169,11 +139,6 @@ export function Extruder2ControlPage() {
timeseries={screwRpm}
/>
{state?.pressure_state?.wiring_error && (
<StatusBadge variant="error">
Cant Measure Pressure! Check Pressure Sensor Wiring!
</StatusBadge>
)}
<TimeSeriesValueNumeric
label="Pressure"
unit="bar"
@@ -206,9 +171,6 @@ export function Extruder2ControlPage() {
icon: "lu:ArrowBigLeftDash",
isActiveClassName: "bg-green-600",
className: "h-full",
confirmation: allZonesReadyForExtrude()
? undefined
: "Temperature is too low. Are you sure you want to extrude?",
},
}}
onChange={setExtruderMode}
@@ -10,7 +10,6 @@ import { useExtruder2 } from "./useExtruder";
export function Extruder2SettingsPage() {
const {
state,
defaultState,
setInverterRotationDirection,
resetInverter,
setExtruderPressureLimit,
@@ -49,7 +48,7 @@ export function Extruder2SettingsPage() {
<Label label="Nozzle Pressure Limit">
<EditValue
value={state?.extruder_settings_state.pressure_limit}
defaultValue={defaultState?.extruder_settings_state.pressure_limit}
defaultValue={0}
unit="bar"
title="Nozzle Pressure Limit"
min={0}
@@ -82,7 +81,7 @@ export function Extruder2SettingsPage() {
<Label label="Kp">
<EditValue
value={state?.pid_settings.pressure.kp}
defaultValue={defaultState?.pid_settings.pressure.kp}
defaultValue={0}
min={0}
max={100}
step={0.01}
@@ -94,7 +93,7 @@ export function Extruder2SettingsPage() {
<Label label="Ki">
<EditValue
value={state?.pid_settings.pressure.ki}
defaultValue={defaultState?.pid_settings.pressure.ki}
defaultValue={0}
min={0}
max={100}
step={0.01}
@@ -106,7 +105,7 @@ export function Extruder2SettingsPage() {
<Label label="Kd">
<EditValue
value={state?.pid_settings.pressure.kd}
defaultValue={defaultState?.pid_settings.pressure.kd}
defaultValue={0}
min={0}
max={100}
step={0.01}
@@ -65,7 +65,6 @@ export const regulationStateSchema = z.object({
export const pressureStateSchema = z.object({
bar: z.number(),
target_bar: z.number(),
wiring_error: z.boolean(),
});
/**
@@ -138,7 +137,6 @@ export const pidSettingsSchema = z.object({
* Consolidated state event schema (state changes only)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
rotation_state: rotationStateSchema,
mode_state: modeStateSchema,
regulation_state: regulationStateSchema,
@@ -171,7 +169,6 @@ export const mode = z.object({
export type Extruder2NamespaceStore = {
// Single state event from server
state: StateEvent | null;
defaultState: StateEvent | null;
// Time series data for live values
screwRpm: TimeSeries;
@@ -254,10 +251,6 @@ export function extruder2MessageHandler(
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -320,7 +313,6 @@ export const createExtruder2NamespaceStore =
create<Extruder2NamespaceStore>(() => {
return {
state: null,
defaultState: null,
screwRpm,
pressure,
nozzleTemperature,
@@ -40,10 +40,8 @@ export function useExtruder2() {
// Get consolidated state and live values from namespace
const {
state,
defaultState,
screwRpm,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
@@ -360,9 +358,6 @@ export function useExtruder2() {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
// Individual live values (TimeSeries)
screwRpm,
pressure,
@@ -44,6 +44,7 @@ export function DiameterVisualisation({
const pixelMin = 100;
const pixelMax = 200;
const lerp = (
value: number,
inputMin: number,
@@ -59,9 +60,6 @@ export function DiameterVisualisation({
const pixelMid = lerp(midDia, minDia, maxDia, pixelMin, pixelMax);
let dynamicDiameterPx: number | null = null;
const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
if (actualDiameter > 0) {
if (actualDiameter < minDia) {
dynamicDiameterPx = lerp(actualDiameter, 0, minDia, 0, pixelMin);
@@ -74,17 +72,12 @@ export function DiameterVisualisation({
pixelMax,
);
} else {
const dynamicDiameterPxLerp = lerp(
dynamicDiameterPx = lerp(
actualDiameter,
maxDia,
maxDia * 1.5,
pixelMax,
pixelMax * 1.1,
);
dynamicDiameterPx = clamp(
dynamicDiameterPxLerp,
pixelMin,
pixelMax * 1.1,
pixelMax * 1.5,
);
}
}
@@ -93,6 +86,7 @@ export function DiameterVisualisation({
const smallRadius = pixelMin / 2;
const midRadius = pixelMid / 2;
const dynamicRadius = dynamicDiameterPx ? dynamicDiameterPx / 2 : 0;
const inTolerance = actualDiameter >= minDia && actualDiameter <= maxDia;
const centerStyle = (radius: number) => ({
@@ -15,7 +15,6 @@ export function Laser1ControlPage() {
const {
diameter,
state,
defaultState,
setTargetDiameter,
setLowerTolerance,
setHigherTolerance,
@@ -60,7 +59,7 @@ export function Laser1ControlPage() {
}
setTargetDiameter(val);
}}
defaultValue={defaultState?.laser_state.target_diameter}
defaultValue={0}
/>
</Label>
<Label label="Set Lower Tolerance">
@@ -73,7 +72,7 @@ export function Laser1ControlPage() {
max={Math.min(targetDiameter, 1)}
renderValue={(value) => value.toFixed(2)}
onChange={(val) => setLowerTolerance(val)}
defaultValue={defaultState?.laser_state.lower_tolerance}
defaultValue={0}
/>
</Label>
<Label label="Set Higher Tolerance">
@@ -86,7 +85,7 @@ export function Laser1ControlPage() {
max={1}
renderValue={(value) => value.toFixed(2)}
onChange={(val) => setHigherTolerance(val)}
defaultValue={defaultState?.laser_state.higher_tolerance}
defaultValue={0}
/>
</Label>
</ControlCard>
@@ -34,7 +34,6 @@ export const liveValuesEventDataSchema = z.object({
* State event from Laser (on state changes only)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
laser_state: z.object({
higher_tolerance: z.number(),
lower_tolerance: z.number(),
@@ -53,7 +52,6 @@ export type StateEvent = z.infer<typeof stateEventSchema>;
export type Laser1NamespaceStore = {
// Single state event from server
state: StateEvent | null;
defaultState: StateEvent | null;
// Time series data for live values
diameter: TimeSeries;
@@ -78,7 +76,6 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
create<Laser1NamespaceStore>(() => {
return {
state: null,
defaultState: null,
diameter: diameter,
};
});
@@ -110,10 +107,6 @@ export function laser1MessageHandler(
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
}
// Live values events (keep for 1 hour)
@@ -11,9 +11,7 @@ import { produce } from "immer";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, diameter } = useLaser1Namespace(
machine_identification_unique,
);
const { state, diameter } = useLaser1Namespace(machine_identification_unique);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
@@ -107,9 +105,6 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
// Live values (TimeSeries)
diameter,
@@ -9,12 +9,18 @@ import { SelectionGroup } from "@/control/SelectionGroup";
import { Mode } from "./mock1Namespace";
export function Mock1ControlPage() {
const { state, defaultState, sineWave, setFrequency, setMode, isDisabled } =
useMock1();
const {
sineWave,
mockState,
modeState,
mockSetFrequency,
mockSetMode,
modeStateIsDisabled,
} = useMock1();
// Controlled local states synced with consolidated state
const frequency = state?.sine_wave_state?.frequency ?? 1.0;
const mode = state?.mode_state?.mode ?? "Standby";
// Controlled local states synced with mockState and modeState
const frequency = mockState?.data?.frequency ?? 1.0;
const mode = modeState?.data?.mode ?? "Standby";
return (
<Page>
@@ -54,12 +60,12 @@ export function Mock1ControlPage() {
title="Frequency"
unit="mHz"
value={frequency}
defaultValue={defaultState?.sine_wave_state.frequency}
defaultValue={500}
min={0.0}
max={1000}
step={0.1}
renderValue={(value) => value.toFixed(0)}
onChange={setFrequency}
onChange={mockSetFrequency}
/>
</div>
</ControlCard>
@@ -69,8 +75,8 @@ export function Mock1ControlPage() {
<div className="text-sm font-medium">Mode</div>
<SelectionGroup
value={mode}
onChange={(newMode: Mode) => setMode(newMode)}
disabled={isDisabled}
onChange={(newMode: Mode) => mockSetMode(newMode)}
disabled={modeStateIsDisabled}
options={{
Standby: { children: "Standby" },
Running: { children: "Running" },
@@ -29,52 +29,42 @@ import {
export const modeSchema = z.enum(["Standby", "Running"]);
/**
* Sine wave state schema
* Sine wave data from Mock Machine (only amplitude)
*/
export const sineWaveStateSchema = z.object({
export const sineWaveEventDataSchema = z.object({
amplitude: z.number(),
});
/**
* Sine wave state event (frequency only)
*/
export const mockStateEventDataSchema = z.object({
frequency: z.number(),
});
/**
* Mode state schema
* Mode state event schema
*/
export const modeStateSchema = z.object({
export const modeStateEventDataSchema = z.object({
mode: modeSchema,
});
/**
* Live values event schema (time-series data)
*/
export const liveValuesEventDataSchema = z.object({
sine_wave_amplitude: z.number(),
});
/**
* State event schema (consolidated state)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
sine_wave_state: sineWaveStateSchema,
mode_state: modeStateSchema,
});
// ========== Event Schemas with Wrappers ==========
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export const stateEventSchema = eventSchema(stateEventDataSchema);
export const sineWaveEventSchema = eventSchema(sineWaveEventDataSchema);
export const mockStateEventSchema = eventSchema(mockStateEventDataSchema);
export const modeStateEventSchema = eventSchema(modeStateEventDataSchema);
// ========== Type Inferences ==========
export type Mode = z.infer<typeof modeSchema>;
export type SineWaveState = z.infer<typeof sineWaveStateSchema>;
export type ModeState = z.infer<typeof modeStateSchema>;
export type LiveValuesEvent = z.infer<typeof liveValuesEventSchema>;
export type StateEvent = z.infer<typeof stateEventSchema>;
export type SineWaveEvent = z.infer<typeof sineWaveEventSchema>;
export type MockStateEvent = z.infer<typeof mockStateEventSchema>;
export type ModeStateEvent = z.infer<typeof modeStateEventSchema>;
export type Mock1NamespaceStore = {
// Single state event from server
state: StateEvent | null;
defaultState: StateEvent | null;
// Time series data for live values
// State events (latest only)
mockState: MockStateEvent | null;
modeState: ModeStateEvent | null;
// Metric events (cached for 1 hour)
sineWave: TimeSeries;
};
@@ -98,8 +88,8 @@ const { initialTimeSeries: sineWave, insert: addSineWave } = createTimeSeries(
export const createMock1NamespaceStore = (): StoreApi<Mock1NamespaceStore> =>
create<Mock1NamespaceStore>(() => {
return {
state: null,
defaultState: null,
mockState: null,
modeState: null,
sineWave: sineWave,
};
});
@@ -125,25 +115,27 @@ export function mock1MessageHandler(
};
try {
// State events (latest only)
if (eventName === "StateEvent") {
const stateEvent = stateEventSchema.parse(event);
console.log("StateEvent", stateEvent);
// Apply appropriate caching strategy based on event type
if (eventName === "MockStateEvent") {
console.log("MockStateEvent", event);
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
mockState: event as MockStateEvent,
}));
}
// Live values events (time-series data)
else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
// Mode state events (latest only)
else if (eventName === "ModeStateEvent") {
console.log("ModeStateEvent", event);
updateStore((state) => ({
...state,
modeState: event as ModeStateEvent,
}));
}
// Metric events (keep for 1 hour)
else if (eventName === "SineWaveEvent") {
const timeseriesValue: TimeSeriesValue = {
value: liveValuesEvent.data.sine_wave_amplitude ?? 0,
timestamp: liveValuesEvent.ts,
value: event.data.amplitude ?? 0,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
+70 -71
View File
@@ -4,96 +4,95 @@ import { MachineIdentificationUnique } from "@/machines/types";
import { mock1 } from "@/machines/properties";
import { mock1SerialRoute } from "@/routes/routes";
import { z } from "zod";
import { useMock1Namespace, Mode, StateEvent } from "./mock1Namespace";
import { useMock1Namespace, Mode } from "./mock1Namespace";
import { useEffect, useMemo } from "react";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { produce } from "immer";
function useMock(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, sineWave } = useMock1Namespace(
machine_identification_unique,
);
// Write Path
const mockStateOptimistic = useStateOptimistic<{
frequency: number;
}>();
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
const modeStateOptimistic = useStateOptimistic<{
mode: Mode;
}>();
// Update optimistic state when real state changes
useEffect(() => {
if (state) {
stateOptimistic.setReal(state);
}
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest: () => Promise<any>,
) => {
const currentState = stateOptimistic.value;
if (currentState) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest()
.then((response) => {
if (!response.success) stateOptimistic.resetToReal();
})
.catch(() => stateOptimistic.resetToReal());
};
// Action functions with verb-first names
const schemaSetFrequency = z.object({ SetFrequency: z.number() });
const { request: requestSetFrequency } =
useMachineMutation(schemaSetFrequency);
const setFrequency = (frequency: number) => {
updateStateOptimistically(
(current) => {
current.data.sine_wave_state.frequency = frequency;
const mockSetFrequency = async (frequency: number) => {
if (mockStateOptimistic.value) {
mockStateOptimistic.setOptimistic({
...mockStateOptimistic.value,
frequency: frequency,
});
}
requestSetFrequency({
machine_identification_unique,
data: {
SetFrequency: frequency,
},
() =>
requestSetFrequency({
machine_identification_unique,
data: {
SetFrequency: frequency,
},
}),
);
})
.then((response) => {
if (!response.success) mockStateOptimistic.resetToReal();
})
.catch(() => mockStateOptimistic.resetToReal());
};
const schemaSetMode = z.object({ SetMode: z.enum(["Standby", "Running"]) });
const { request: requestSetMode } = useMachineMutation(schemaSetMode);
const setMode = (mode: Mode) => {
updateStateOptimistically(
(current) => {
current.data.mode_state.mode = mode;
const mockSetMode = async (mode: Mode) => {
if (modeStateOptimistic.value) {
modeStateOptimistic.setOptimistic({
...modeStateOptimistic.value,
mode: mode,
});
}
requestSetMode({
machine_identification_unique,
data: {
SetMode: mode,
},
() =>
requestSetMode({
machine_identification_unique,
data: {
SetMode: mode,
},
}),
);
})
.then((response) => {
if (!response.success) modeStateOptimistic.resetToReal();
})
.catch(() => modeStateOptimistic.resetToReal());
};
// Read Path
const { sineWave, mockState, modeState } = useMock1Namespace(
machine_identification_unique,
);
// Update real values from server
useEffect(() => {
if (mockState?.data) {
mockStateOptimistic.setReal(mockState.data);
}
}, [mockState]);
useEffect(() => {
if (modeState?.data) {
modeStateOptimistic.setReal(modeState.data);
}
}, [modeState]);
return {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
// Individual live values (TimeSeries)
sineWave,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
// Action functions (verb-first)
setFrequency,
setMode,
mockState,
modeState,
mockSetFrequency,
mockSetMode,
mockStateIsLoading:
mockStateOptimistic.isOptimistic || !mockStateOptimistic.isInitialized,
mockStateIsDisabled:
mockStateOptimistic.isOptimistic || !mockStateOptimistic.isInitialized,
modeStateIsLoading:
modeStateOptimistic.isOptimistic || !modeStateOptimistic.isInitialized,
modeStateIsDisabled:
modeStateOptimistic.isOptimistic || !modeStateOptimistic.isInitialized,
};
}
@@ -13,7 +13,7 @@ import { Label } from "@/control/Label";
import { TouchButton } from "@/components/touch/TouchButton";
import { StatusBadge } from "@/control/StatusBadge";
import { useWinder2 } from "./useWinder";
import { Mode, SpoolAutomaticActionMode } from "./winder2Namespace";
import { Mode } from "./winder2Namespace";
import { TensionArm } from "../TensionArm";
import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal";
import { Spool } from "../Spool";
@@ -22,7 +22,6 @@ export function Winder2ControlPage() {
// use optimistic state
const {
state,
defaultState,
enableTraverseLaserpointer,
tensionArmAngle,
zeroTensionArmAngle,
@@ -30,18 +29,14 @@ export function Winder2ControlPage() {
spoolDiameter,
setMode,
pullerSpeed,
spoolProgress,
setPullerRegulationMode,
setPullerTargetSpeed,
traversePosition,
resetSpoolProgress,
setTraverseLimitInner,
setTraverseLimitOuter,
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
setSpoolAutomaticRequiredMeters,
setSpoolAutomaticAction,
isLoading,
isDisabled,
} = useWinder2();
@@ -87,7 +82,7 @@ export function Winder2ControlPage() {
value={state?.traverse_state?.limit_outer}
unit="mm"
title="Outer Limit"
defaultValue={defaultState?.traverse_state?.limit_outer}
defaultValue={80}
// Traverse limit validation: Outer limit must be at least 0.9mm greater than inner limit
// We use 1mm buffer to ensure the backend validation (which requires >0.9mm) will pass
// Formula: min_outer = inner_limit + 1mm
@@ -122,7 +117,7 @@ export function Winder2ControlPage() {
180,
(state?.traverse_state?.limit_outer ?? 180) - 1,
)}
defaultValue={defaultState?.traverse_state?.limit_inner}
defaultValue={16}
minLabel="IN"
maxLabel="OUT"
renderValue={(value) => roundToDecimals(value, 0)}
@@ -257,7 +252,7 @@ export function Winder2ControlPage() {
value={state?.puller_state?.target_speed}
unit="m/min"
title="Target Speed"
defaultValue={defaultState?.puller_state?.target_speed}
defaultValue={1}
min={0}
max={75}
step={0.1}
@@ -266,68 +261,6 @@ export function Winder2ControlPage() {
/>
</Label>
</ControlCard>
<ControlCard className="bg-red" title="Spool Autostop">
<TimeSeriesValueNumeric
label="Pulled Distance"
renderValue={(value) => roundToDecimals(value, 2)}
unit="m"
timeseries={spoolProgress}
/>
<Label label="Target Length">
<EditValue
value={state?.spool_automatic_action_state.spool_required_meters}
unit="m"
title="Expected Meters"
defaultValue={250}
min={10}
max={10000}
step={10}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={setSpoolAutomaticRequiredMeters}
/>
</Label>
<TouchButton
variant="outline"
onClick={resetSpoolProgress}
disabled={isDisabled || !state?.traverse_state?.can_go_out}
isLoading={isLoading || state?.traverse_state?.is_going_out}
>
Reset Progress
</TouchButton>
<Label label="After Target Length Reached">
<SelectionGroup<SpoolAutomaticActionMode>
value={
state?.spool_automatic_action_state.spool_automatic_action_mode
}
disabled={isDisabled}
loading={isLoading}
onChange={setSpoolAutomaticAction}
orientation="vertical"
options={{
Hold: {
children: "Hold",
icon: "lu:CirclePause",
className: "h-full",
},
Pull: {
children: "Pull",
icon: "lu:ChevronsLeft",
className: "h-full",
},
NoAction: {
children: "No Action",
icon: "lu:RefreshCcw",
className: "h-full",
},
}}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
@@ -20,7 +20,6 @@ export function Winder2GraphsPage() {
traversePosition,
tensionArmAngle,
pullerSpeed,
spoolProgress,
} = useWinder2();
const syncHook = useGraphSync(30 * 60 * 1000, "winder2-group");
@@ -66,13 +65,6 @@ export function Winder2GraphsPage() {
unit="cm"
renderValue={(value) => roundToDecimals(value, 1)}
/>
<SpoolProgressGraph
syncHook={syncHook}
newData={spoolProgress}
unit="m"
renderValue={(value) => roundToDecimals(value, 2)}
/>
</div>
</div>
@@ -213,41 +205,6 @@ export function TensionArmAngleGraph({
);
}
export function SpoolProgressGraph({
syncHook,
newData,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
unit?: Unit;
renderValue?: (value: number) => string;
}) {
const config: GraphConfig = {
title: "Spool Progress",
icon: "lu:RotateCw",
colors: {
primary: "#f59e0b",
},
exportFilename: "spool_progress",
};
return (
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData,
color: "#f59e0b",
}}
unit={unit}
renderValue={renderValue}
config={config}
graphId="spool-progress"
/>
);
}
export function PullerSpeedGraph({
syncHook,
newData,
@@ -12,7 +12,6 @@ import { SelectionGroup } from "@/control/SelectionGroup";
export function Winder2SettingPage() {
const {
state,
defaultState,
setTraverseStepSize,
setTraversePadding,
setPullerForward,
@@ -40,7 +39,7 @@ export function Winder2SettingPage() {
step={0.05}
min={0.1}
max={10}
defaultValue={defaultState?.traverse_state?.step_size}
defaultValue={1.0}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) => setTraverseStepSize(value)}
/>
@@ -53,7 +52,7 @@ export function Winder2SettingPage() {
step={0.01}
min={0}
max={5}
defaultValue={defaultState?.traverse_state?.padding}
defaultValue={0.01}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) => setTraversePadding(value)}
/>
@@ -93,9 +92,7 @@ export function Winder2SettingPage() {
step={10}
min={0}
max={600}
defaultValue={
defaultState?.spool_speed_controller_state?.minmax_min_speed
}
defaultValue={50}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSpoolMinMaxMinSpeed(value)}
/>
@@ -108,9 +105,7 @@ export function Winder2SettingPage() {
step={10}
min={0}
max={600}
defaultValue={
defaultState?.spool_speed_controller_state?.minmax_max_speed
}
defaultValue={150}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={(value) => setSpoolMinMaxMaxSpeed(value)}
/>
@@ -131,10 +126,7 @@ export function Winder2SettingPage() {
step={0.01}
min={0}
max={1}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_tension_target
}
defaultValue={0.7}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) => setSpoolAdaptiveTensionTarget(value)}
/>
@@ -150,10 +142,7 @@ export function Winder2SettingPage() {
step={0.001}
min={0}
max={100}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_radius_learning_rate
}
defaultValue={0.5}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) =>
setSpoolAdaptiveRadiusLearningRate(value)
@@ -171,10 +160,7 @@ export function Winder2SettingPage() {
step={0.1}
min={0.1}
max={10}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_max_speed_multiplier
}
defaultValue={4.0}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) =>
setSpoolAdaptiveMaxSpeedMultiplier(value)
@@ -192,10 +178,7 @@ export function Winder2SettingPage() {
step={0.01}
min={0.01}
max={100}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_acceleration_factor
}
defaultValue={0.2}
renderValue={(value) => roundToDecimals(value, 2)}
onChange={(value) =>
setSpoolAdaptiveAccelerationFactor(value)
@@ -213,10 +196,7 @@ export function Winder2SettingPage() {
step={0.5}
min={1}
max={100}
defaultValue={
defaultState?.spool_speed_controller_state
?.adaptive_deacceleration_urgency_multiplier
}
defaultValue={15.0}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={(value) =>
setSpoolAdaptiveDeaccelerationUrgencyMultiplier(value)
@@ -14,8 +14,6 @@ import {
spoolRegulationModeSchema,
pullerRegulationSchema,
PullerRegulation,
SpoolAutomaticActionMode,
spoolAutomaticActionModeSchema,
} from "./winder2Namespace";
import { useEffect, useMemo } from "react";
import { produce } from "immer";
@@ -51,13 +49,11 @@ export function useWinder2() {
// Get consolidated state and live values from namespace
const {
state,
defaultState,
traversePosition,
pullerSpeed,
spoolRpm,
spoolDiameter,
tensionArmAngle,
spoolProgress,
} = useWinder2Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -143,16 +139,6 @@ export function useWinder2() {
useMachineMutation(
z.object({ SetSpoolAdaptiveDeaccelerationUrgencyMultiplier: z.number() }),
);
const { request: requestSpoolAutomaticRequiredMeters } = useMachineMutation(
z.object({ SetSpoolAutomaticRequiredMeters: z.number() }),
);
const { request: requestSpoolResetProgress } = useMachineMutation(
z.literal("ResetSpoolProgress"),
);
const { request: requestSpoolAutomaticAction } = useMachineMutation(
z.object({ SetSpoolAutomaticAction: spoolAutomaticActionModeSchema }),
);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
@@ -349,41 +335,6 @@ export function useWinder2() {
);
};
const setSpoolAutomaticRequiredMeters = (meters: number) => {
updateStateOptimistically(
(current) => {
current.data.spool_automatic_action_state.spool_required_meters =
meters;
},
() =>
requestSpoolAutomaticRequiredMeters({
machine_identification_unique: machineIdentification,
data: { SetSpoolAutomaticRequiredMeters: meters },
}),
);
};
const setSpoolAutomaticAction = (mode: SpoolAutomaticActionMode) => {
updateStateOptimistically(
(current) => {
current.data.spool_automatic_action_state.spool_automatic_action_mode =
mode;
},
() =>
requestSpoolAutomaticAction({
machine_identification_unique: machineIdentification,
data: { SetSpoolAutomaticAction: mode },
}),
);
};
const resetSpoolProgress = () => {
requestSpoolResetProgress({
machine_identification_unique: machineIdentification,
data: "ResetSpoolProgress",
});
};
const setSpoolRegulationMode = (mode: SpoolRegulationMode) => {
updateStateOptimistically(
(current) => {
@@ -501,16 +452,12 @@ export function useWinder2() {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
// Individual live values (TimeSeries)
traversePosition,
pullerSpeed,
spoolRpm,
spoolDiameter,
tensionArmAngle,
spoolProgress,
// Loading states
isLoading,
@@ -525,15 +472,12 @@ export function useWinder2() {
gotoTraverseLimitInner,
gotoTraverseLimitOuter,
gotoTraverseHome,
resetSpoolProgress,
setTraverseStepSize,
setTraversePadding,
setPullerTargetSpeed,
setPullerTargetDiameter,
setPullerRegulationMode,
setPullerForward,
setSpoolAutomaticRequiredMeters,
setSpoolAutomaticAction,
setSpoolRegulationMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
@@ -34,7 +34,6 @@ export const liveValuesEventDataSchema = z.object({
spool_rpm: z.number(),
spool_diameter: z.number(),
tension_arm_angle: z.number(),
spool_progress: z.number(),
});
/**
@@ -49,34 +48,12 @@ export type PullerRegulation = z.infer<typeof pullerRegulationSchema>;
export const modeSchema = z.enum(["Standby", "Hold", "Pull", "Wind"]);
export type Mode = z.infer<typeof modeSchema>;
/**
* Machine operation mode enum
*/
export const spoolAutomaticActionModeSchema = z.enum([
"NoAction",
"Pull",
"Hold",
]);
export type SpoolAutomaticActionMode = z.infer<
typeof spoolAutomaticActionModeSchema
>;
/**
* Spool speed controller regulation mode enum
*/
export const spoolRegulationModeSchema = z.enum(["Adaptive", "MinMax"]);
export type SpoolRegulationMode = z.infer<typeof spoolRegulationModeSchema>;
export const spoolAutomaticActionStateSchema = z.object({
spool_required_meters: z.number(),
spool_automatic_action_mode: spoolAutomaticActionModeSchema,
});
export type SpoolAutomaticActionState = z.infer<
typeof spoolAutomaticActionStateSchema
>;
/**
* Traverse state schema
*/
@@ -141,13 +118,11 @@ export const spoolSpeedControllerStateSchema = z.object({
* Consolidated state event schema (state changes only)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
traverse_state: traverseStateSchema,
puller_state: pullerStateSchema,
mode_state: modeStateSchema,
tension_arm_state: tensionArmStateSchema,
spool_speed_controller_state: spoolSpeedControllerStateSchema,
spool_automatic_action_state: spoolAutomaticActionStateSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -164,7 +139,6 @@ export type StateEvent = z.infer<typeof stateEventSchema>;
export type Winder2NamespaceStore = {
// State event from server
state: StateEvent | null;
defaultState: StateEvent | null;
// Time series data for live values
traversePosition: TimeSeries;
@@ -172,7 +146,6 @@ export type Winder2NamespaceStore = {
spoolRpm: TimeSeries;
spoolDiameter: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
};
// Constants for time durations
@@ -180,9 +153,6 @@ const TWENTY_MILLISECOND = 20;
const ONE_SECOND = 1000;
const FIVE_SECOND = 5 * ONE_SECOND;
const ONE_HOUR = 60 * 60 * ONE_SECOND;
const { initialTimeSeries: spoolProgress, insert: addSpoolProgress } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
@@ -208,7 +178,6 @@ export const createWinder2NamespaceStore =
return {
// State event from server
state: null,
defaultState: null,
// Time series data for live values
traversePosition,
@@ -216,7 +185,6 @@ export const createWinder2NamespaceStore =
spoolRpm,
spoolDiameter,
tensionArmAngle,
spoolProgress,
};
});
/**
@@ -247,14 +215,11 @@ export function winder2MessageHandler(
if (eventName === "StateEvent") {
// Parse and validate the state event
const stateEvent = stateEventSchema.parse(event);
console.log("StateEvent", stateEvent);
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
} else if (eventName === "LiveValuesEvent") {
// Parse and validate the live values event
@@ -268,12 +233,12 @@ export function winder2MessageHandler(
spool_rpm,
spool_diameter,
tension_arm_angle,
spool_progress,
} = liveValuesEvent.data;
const timestamp = liveValuesEvent.ts;
updateStore((state) => {
const newState = { ...state };
// Add traverse position if not null
if (traverse_position !== null) {
const timeseriesValue: TimeSeriesValue = {
@@ -286,17 +251,6 @@ export function winder2MessageHandler(
);
}
if (spoolProgress !== null) {
const timeseriesValue: TimeSeriesValue = {
value: spool_progress,
timestamp,
};
newState.spoolProgress = addSpoolProgress(
state.spoolProgress,
timeseriesValue,
);
}
// Add puller speed
const pullerSpeedValue: TimeSeriesValue = {
value: puller_speed,
-1
View File
@@ -11,7 +11,6 @@ bitvec = { version = "1.0.1", features = ["alloc"] }
smol = "2.0.2"
uom = "0.36.0"
rand = "0.9.1"
futures = "0.3.31"
[dev-dependencies]
approx = "0.5.1"
+8 -6
View File
@@ -1,7 +1,7 @@
use super::{NewEthercatDevice, SubDeviceIdentityTuple};
use crate::devices::EthercatDeviceProcessing;
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_input::{DigitalInputDevice, DigitalInputInput};
use crate::io::digital_input::{DigitalInputDevice, DigitalInputInput, DigitalInputState};
use crate::pdo::{PredefinedPdoAssignment, TxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, TxPdo};
/// EL1002 2-channel digital input device
@@ -30,16 +30,18 @@ impl NewEthercatDevice for EL1002 {
}
impl DigitalInputDevice<EL1002Port> for EL1002 {
fn get_input(&self, port: EL1002Port) -> Result<DigitalInputInput, anyhow::Error> {
fn digital_input_state(&self, port: EL1002Port) -> Result<DigitalInputState, anyhow::Error> {
let error = anyhow::anyhow!(
"[{}::Device::digital_input_state] Port {:?} is not available",
module_path!(),
port
);
Ok(DigitalInputInput {
value: match port {
EL1002Port::DI1 => self.txpdo.channel1.as_ref().ok_or(error)?.value,
EL1002Port::DI2 => self.txpdo.channel2.as_ref().ok_or(error)?.value,
Ok(DigitalInputState {
input: DigitalInputInput {
value: match port {
EL1002Port::DI1 => self.txpdo.channel1.as_ref().ok_or(error)?.value,
EL1002Port::DI2 => self.txpdo.channel2.as_ref().ok_or(error)?.value,
},
},
})
}
+14 -12
View File
@@ -1,6 +1,6 @@
use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple};
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_input::{DigitalInputDevice, DigitalInputInput};
use crate::io::digital_input::{DigitalInputDevice, DigitalInputInput, DigitalInputState};
use crate::pdo::{PredefinedPdoAssignment, TxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, TxPdo};
@@ -31,22 +31,24 @@ impl NewEthercatDevice for EL1008 {
}
impl DigitalInputDevice<EL1008Port> for EL1008 {
fn get_input(&self, port: EL1008Port) -> Result<DigitalInputInput, anyhow::Error> {
fn digital_input_state(&self, port: EL1008Port) -> Result<DigitalInputState, anyhow::Error> {
let error = anyhow::anyhow!(
"[{}::Device::digital_input_state] Port {:?} is not available",
module_path!(),
port
);
Ok(DigitalInputInput {
value: match port {
EL1008Port::DI1 => self.txpdo.channel1.as_ref().ok_or(error)?.value,
EL1008Port::DI2 => self.txpdo.channel2.as_ref().ok_or(error)?.value,
EL1008Port::DI3 => self.txpdo.channel3.as_ref().ok_or(error)?.value,
EL1008Port::DI4 => self.txpdo.channel4.as_ref().ok_or(error)?.value,
EL1008Port::DI5 => self.txpdo.channel5.as_ref().ok_or(error)?.value,
EL1008Port::DI6 => self.txpdo.channel6.as_ref().ok_or(error)?.value,
EL1008Port::DI7 => self.txpdo.channel7.as_ref().ok_or(error)?.value,
EL1008Port::DI8 => self.txpdo.channel8.as_ref().ok_or(error)?.value,
Ok(DigitalInputState {
input: DigitalInputInput {
value: match port {
EL1008Port::DI1 => self.txpdo.channel1.as_ref().ok_or(error)?.value,
EL1008Port::DI2 => self.txpdo.channel2.as_ref().ok_or(error)?.value,
EL1008Port::DI3 => self.txpdo.channel3.as_ref().ok_or(error)?.value,
EL1008Port::DI4 => self.txpdo.channel4.as_ref().ok_or(error)?.value,
EL1008Port::DI5 => self.txpdo.channel5.as_ref().ok_or(error)?.value,
EL1008Port::DI6 => self.txpdo.channel6.as_ref().ok_or(error)?.value,
EL1008Port::DI7 => self.txpdo.channel7.as_ref().ok_or(error)?.value,
EL1008Port::DI8 => self.txpdo.channel8.as_ref().ok_or(error)?.value,
},
},
})
}
+9 -7
View File
@@ -1,6 +1,6 @@
use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple};
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput, DigitalOutputState};
use crate::pdo::{RxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, RxPdo};
@@ -31,7 +31,7 @@ impl NewEthercatDevice for EL2002 {
}
impl DigitalOutputDevice<EL2002Port> for EL2002 {
fn set_output(&mut self, port: EL2002Port, value: DigitalOutputOutput) {
fn digital_output_write(&mut self, port: EL2002Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2002Port::DO1 => {
@@ -43,12 +43,14 @@ impl DigitalOutputDevice<EL2002Port> for EL2002 {
}
}
fn get_output(&self, port: EL2002Port) -> DigitalOutputOutput {
fn digital_output_state(&self, port: EL2002Port) -> DigitalOutputState {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2002Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2002Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
})
DigitalOutputState {
output: DigitalOutputOutput(match port {
EL2002Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2002Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
}),
}
}
}
+11 -9
View File
@@ -1,6 +1,6 @@
use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple};
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput, DigitalOutputState};
use crate::pdo::{RxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, RxPdo};
/// EL2004 4-channel digital output device
@@ -29,7 +29,7 @@ impl NewEthercatDevice for EL2004 {
}
impl DigitalOutputDevice<EL2004Port> for EL2004 {
fn set_output(&mut self, port: EL2004Port, value: DigitalOutputOutput) {
fn digital_output_write(&mut self, port: EL2004Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2004Port::DO1 => {
@@ -47,14 +47,16 @@ impl DigitalOutputDevice<EL2004Port> for EL2004 {
}
}
fn get_output(&self, port: EL2004Port) -> DigitalOutputOutput {
fn digital_output_state(&self, port: EL2004Port) -> DigitalOutputState {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2004Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2004Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2004Port::DO3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2004Port::DO4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
})
DigitalOutputState {
output: DigitalOutputOutput(match port {
EL2004Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2004Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2004Port::DO3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2004Port::DO4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
}),
}
}
}
+15 -13
View File
@@ -1,6 +1,6 @@
use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple};
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput, DigitalOutputState};
use crate::pdo::{RxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, RxPdo};
@@ -31,7 +31,7 @@ impl NewEthercatDevice for EL2008 {
}
impl DigitalOutputDevice<EL2008Port> for EL2008 {
fn set_output(&mut self, port: EL2008Port, value: DigitalOutputOutput) {
fn digital_output_write(&mut self, port: EL2008Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2008Port::DO1 => {
@@ -61,18 +61,20 @@ impl DigitalOutputDevice<EL2008Port> for EL2008 {
}
}
fn get_output(&self, port: EL2008Port) -> DigitalOutputOutput {
fn digital_output_state(&self, port: EL2008Port) -> DigitalOutputState {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2008Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2008Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2008Port::DO3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2008Port::DO4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
EL2008Port::DO5 => self.rxpdo.channel5.as_ref().expect(&expect_text).value,
EL2008Port::DO6 => self.rxpdo.channel6.as_ref().expect(&expect_text).value,
EL2008Port::DO7 => self.rxpdo.channel7.as_ref().expect(&expect_text).value,
EL2008Port::DO8 => self.rxpdo.channel8.as_ref().expect(&expect_text).value,
})
DigitalOutputState {
output: DigitalOutputOutput(match port {
EL2008Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2008Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2008Port::DO3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2008Port::DO4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
EL2008Port::DO5 => self.rxpdo.channel5.as_ref().expect(&expect_text).value,
EL2008Port::DO6 => self.rxpdo.channel6.as_ref().expect(&expect_text).value,
EL2008Port::DO7 => self.rxpdo.channel7.as_ref().expect(&expect_text).value,
EL2008Port::DO8 => self.rxpdo.channel8.as_ref().expect(&expect_text).value,
}),
}
}
}
+11 -9
View File
@@ -1,5 +1,5 @@
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput, DigitalOutputState};
use crate::pdo::{RxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, RxPdo};
@@ -32,7 +32,7 @@ impl NewEthercatDevice for EL2024 {
}
impl DigitalOutputDevice<EL2024Port> for EL2024 {
fn set_output(&mut self, port: EL2024Port, value: DigitalOutputOutput) {
fn digital_output_write(&mut self, port: EL2024Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2024Port::DO1 => {
@@ -50,14 +50,16 @@ impl DigitalOutputDevice<EL2024Port> for EL2024 {
}
}
fn get_output(&self, port: EL2024Port) -> DigitalOutputOutput {
fn digital_output_state(&self, port: EL2024Port) -> DigitalOutputState {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2024Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2024Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2024Port::DO3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2024Port::DO4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
})
DigitalOutputState {
output: DigitalOutputOutput(match port {
EL2024Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2024Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2024Port::DO3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2024Port::DO4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
}),
}
}
}
+23 -23
View File
@@ -4,6 +4,7 @@ use crate::{
helpers::ethercrab_types::EthercrabSubDevicePreoperational,
io::pulse_train_output::{
PulseTrainOutputDevice, PulseTrainOutputInput, PulseTrainOutputOutput,
PulseTrainOutputState,
},
pdo::{
PredefinedPdoAssignment, RxPdo, TxPdo,
@@ -46,7 +47,7 @@ impl NewEthercatDevice for EL2521 {
}
impl PulseTrainOutputDevice<EL2521Port> for EL2521 {
fn set_output(&mut self, _port: EL2521Port, value: PulseTrainOutputOutput) {
fn pulse_train_output_write(&mut self, _port: EL2521Port, value: PulseTrainOutputOutput) {
self.rxpdo.pto_control.as_mut().unwrap().disble_ramp = value.disble_ramp;
self.rxpdo.pto_control.as_mut().unwrap().frequency_value = value.frequency_value;
self.rxpdo.pto_target.as_mut().unwrap().target_counter_value = value.target_counter_value;
@@ -54,28 +55,27 @@ impl PulseTrainOutputDevice<EL2521Port> for EL2521 {
self.rxpdo.enc_control.as_mut().unwrap().set_counter_value = value.set_counter_value;
}
fn get_output(&self, _port: EL2521Port) -> PulseTrainOutputOutput {
PulseTrainOutputOutput {
disble_ramp: self.rxpdo.pto_control.as_ref().unwrap().disble_ramp,
frequency_value: self.rxpdo.pto_control.as_ref().unwrap().frequency_value,
target_counter_value: self.rxpdo.pto_target.as_ref().unwrap().target_counter_value,
set_counter: self.rxpdo.enc_control.as_ref().unwrap().set_counter,
set_counter_value: self.rxpdo.enc_control.as_ref().unwrap().set_counter_value,
}
}
fn get_input(&self, _port: EL2521Port) -> PulseTrainOutputInput {
PulseTrainOutputInput {
select_end_counter: self.txpdo.pto_status.as_ref().unwrap().select_end_counter,
ramp_active: self.txpdo.pto_status.as_ref().unwrap().ramp_active,
input_t: self.txpdo.pto_status.as_ref().unwrap().input_t,
input_z: self.txpdo.pto_status.as_ref().unwrap().input_z,
error: self.txpdo.pto_status.as_ref().unwrap().error,
sync_error: self.txpdo.pto_status.as_ref().unwrap().sync_error,
counter_underflow: self.txpdo.enc_status.as_ref().unwrap().counter_underflow,
counter_overflow: self.txpdo.enc_status.as_ref().unwrap().counter_overflow,
counter_value: self.txpdo.enc_status.as_ref().unwrap().counter_value,
set_counter_done: self.txpdo.enc_status.as_ref().unwrap().set_counter_done,
fn pulse_train_output_state(&self, _port: EL2521Port) -> PulseTrainOutputState {
PulseTrainOutputState {
input: PulseTrainOutputInput {
select_end_counter: self.txpdo.pto_status.as_ref().unwrap().select_end_counter,
ramp_active: self.txpdo.pto_status.as_ref().unwrap().ramp_active,
input_t: self.txpdo.pto_status.as_ref().unwrap().input_t,
input_z: self.txpdo.pto_status.as_ref().unwrap().input_z,
error: self.txpdo.pto_status.as_ref().unwrap().error,
sync_error: self.txpdo.pto_status.as_ref().unwrap().sync_error,
counter_underflow: self.txpdo.enc_status.as_ref().unwrap().counter_underflow,
counter_overflow: self.txpdo.enc_status.as_ref().unwrap().counter_overflow,
counter_value: self.txpdo.enc_status.as_ref().unwrap().counter_value,
set_counter_done: self.txpdo.enc_status.as_ref().unwrap().set_counter_done,
},
output: PulseTrainOutputOutput {
disble_ramp: self.rxpdo.pto_control.as_ref().unwrap().disble_ramp,
frequency_value: self.rxpdo.pto_control.as_ref().unwrap().frequency_value,
target_counter_value: self.rxpdo.pto_target.as_ref().unwrap().target_counter_value,
set_counter: self.rxpdo.enc_control.as_ref().unwrap().set_counter,
set_counter_value: self.rxpdo.enc_control.as_ref().unwrap().set_counter_value,
},
}
}
}
+24 -24
View File
@@ -4,6 +4,7 @@ use crate::{
helpers::ethercrab_types::EthercrabSubDevicePreoperational,
io::pulse_train_output::{
PulseTrainOutputDevice, PulseTrainOutputInput, PulseTrainOutputOutput,
PulseTrainOutputState,
},
pdo::{
PredefinedPdoAssignment, RxPdo, TxPdo,
@@ -64,7 +65,7 @@ impl ConfigurableDevice<EL2522Configuration> for EL2522 {
}
impl PulseTrainOutputDevice<EL2522Port> for EL2522 {
fn set_output(&mut self, port: EL2522Port, value: PulseTrainOutputOutput) {
fn pulse_train_output_write(&mut self, port: EL2522Port, value: PulseTrainOutputOutput) {
let (pto_control, pto_target, enc_control) = self.get_rxpdo_mut(port);
pto_control.disble_ramp = value.disble_ramp;
@@ -74,31 +75,30 @@ impl PulseTrainOutputDevice<EL2522Port> for EL2522 {
enc_control.set_counter_value = value.set_counter_value;
}
fn get_output(&self, port: EL2522Port) -> PulseTrainOutputOutput {
fn pulse_train_output_state(&self, port: EL2522Port) -> PulseTrainOutputState {
let (pto_status, enc_status) = self.get_txpdo(port);
let (pto_control, pto_target, enc_control) = self.get_rxpdo(port);
PulseTrainOutputOutput {
disble_ramp: pto_control.disble_ramp,
frequency_value: pto_control.frequency_value,
target_counter_value: pto_target.target_counter_value,
set_counter: enc_control.set_counter,
set_counter_value: enc_control.set_counter_value,
}
}
fn get_input(&self, port: EL2522Port) -> PulseTrainOutputInput {
let (pto_status, enc_status) = self.get_txpdo(port);
PulseTrainOutputInput {
select_end_counter: pto_status.select_end_counter,
ramp_active: pto_status.ramp_active,
input_t: pto_status.input_t,
input_z: pto_status.input_z,
error: pto_status.error,
sync_error: pto_status.sync_error,
counter_underflow: enc_status.counter_underflow,
counter_overflow: enc_status.counter_overflow,
counter_value: enc_status.counter_value,
set_counter_done: enc_status.set_counter_done,
PulseTrainOutputState {
input: PulseTrainOutputInput {
select_end_counter: pto_status.select_end_counter,
ramp_active: pto_status.ramp_active,
input_t: pto_status.input_t,
input_z: pto_status.input_z,
error: pto_status.error,
sync_error: pto_status.sync_error,
counter_underflow: enc_status.counter_underflow,
counter_overflow: enc_status.counter_overflow,
counter_value: enc_status.counter_value,
set_counter_done: enc_status.set_counter_done,
},
output: PulseTrainOutputOutput {
disble_ramp: pto_control.disble_ramp,
frequency_value: pto_control.frequency_value,
target_counter_value: pto_target.target_counter_value,
set_counter: enc_control.set_counter,
set_counter_value: enc_control.set_counter_value,
},
}
}
}
+11 -9
View File
@@ -1,5 +1,5 @@
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput, DigitalOutputState};
use crate::pdo::{RxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, RxPdo};
@@ -32,7 +32,7 @@ impl NewEthercatDevice for EL2634 {
}
impl DigitalOutputDevice<EL2634Port> for EL2634 {
fn set_output(&mut self, port: EL2634Port, value: DigitalOutputOutput) {
fn digital_output_write(&mut self, port: EL2634Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2634Port::R1 => {
@@ -50,14 +50,16 @@ impl DigitalOutputDevice<EL2634Port> for EL2634 {
}
}
fn get_output(&self, port: EL2634Port) -> DigitalOutputOutput {
fn digital_output_state(&self, port: EL2634Port) -> DigitalOutputState {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2634Port::R1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2634Port::R2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2634Port::R3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2634Port::R4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
})
DigitalOutputState {
output: DigitalOutputOutput(match port {
EL2634Port::R1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2634Port::R2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2634Port::R3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2634Port::R4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
}),
}
}
}
+23 -21
View File
@@ -1,5 +1,5 @@
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput, DigitalOutputState};
use crate::pdo::{RxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, RxPdo};
@@ -32,7 +32,7 @@ impl NewEthercatDevice for EL2809 {
}
impl DigitalOutputDevice<EL2809Port> for EL2809 {
fn set_output(&mut self, port: EL2809Port, value: DigitalOutputOutput) {
fn digital_output_write(&mut self, port: EL2809Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2809Port::DO1 => {
@@ -86,26 +86,28 @@ impl DigitalOutputDevice<EL2809Port> for EL2809 {
}
}
fn get_output(&self, port: EL2809Port) -> DigitalOutputOutput {
fn digital_output_state(&self, port: EL2809Port) -> DigitalOutputState {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2809Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2809Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2809Port::DO3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2809Port::DO4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
EL2809Port::DO5 => self.rxpdo.channel5.as_ref().expect(&expect_text).value,
EL2809Port::DO6 => self.rxpdo.channel6.as_ref().expect(&expect_text).value,
EL2809Port::DO7 => self.rxpdo.channel7.as_ref().expect(&expect_text).value,
EL2809Port::DO8 => self.rxpdo.channel8.as_ref().expect(&expect_text).value,
EL2809Port::DO9 => self.rxpdo.channel9.as_ref().expect(&expect_text).value,
EL2809Port::DO10 => self.rxpdo.channel10.as_ref().expect(&expect_text).value,
EL2809Port::DO11 => self.rxpdo.channel11.as_ref().expect(&expect_text).value,
EL2809Port::DO12 => self.rxpdo.channel12.as_ref().expect(&expect_text).value,
EL2809Port::DO13 => self.rxpdo.channel13.as_ref().expect(&expect_text).value,
EL2809Port::DO14 => self.rxpdo.channel14.as_ref().expect(&expect_text).value,
EL2809Port::DO15 => self.rxpdo.channel15.as_ref().expect(&expect_text).value,
EL2809Port::DO16 => self.rxpdo.channel16.as_ref().expect(&expect_text).value,
})
DigitalOutputState {
output: DigitalOutputOutput(match port {
EL2809Port::DO1 => self.rxpdo.channel1.as_ref().expect(&expect_text).value,
EL2809Port::DO2 => self.rxpdo.channel2.as_ref().expect(&expect_text).value,
EL2809Port::DO3 => self.rxpdo.channel3.as_ref().expect(&expect_text).value,
EL2809Port::DO4 => self.rxpdo.channel4.as_ref().expect(&expect_text).value,
EL2809Port::DO5 => self.rxpdo.channel5.as_ref().expect(&expect_text).value,
EL2809Port::DO6 => self.rxpdo.channel6.as_ref().expect(&expect_text).value,
EL2809Port::DO7 => self.rxpdo.channel7.as_ref().expect(&expect_text).value,
EL2809Port::DO8 => self.rxpdo.channel8.as_ref().expect(&expect_text).value,
EL2809Port::DO9 => self.rxpdo.channel9.as_ref().expect(&expect_text).value,
EL2809Port::DO10 => self.rxpdo.channel10.as_ref().expect(&expect_text).value,
EL2809Port::DO11 => self.rxpdo.channel11.as_ref().expect(&expect_text).value,
EL2809Port::DO12 => self.rxpdo.channel12.as_ref().expect(&expect_text).value,
EL2809Port::DO13 => self.rxpdo.channel13.as_ref().expect(&expect_text).value,
EL2809Port::DO14 => self.rxpdo.channel14.as_ref().expect(&expect_text).value,
EL2809Port::DO15 => self.rxpdo.channel15.as_ref().expect(&expect_text).value,
EL2809Port::DO16 => self.rxpdo.channel16.as_ref().expect(&expect_text).value,
}),
}
}
}
+4 -5
View File
@@ -11,7 +11,7 @@ use crate::{
};
use crate::{
helpers::ethercrab_types::EthercrabSubDevicePreoperational,
io::analog_input::{AnalogInputDevice, AnalogInputInput},
io::analog_input::{AnalogInputDevice, AnalogInputInput, AnalogInputState},
};
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
use uom::si::{electric_potential::volt, f64::ElectricPotential};
@@ -49,7 +49,7 @@ impl NewEthercatDevice for EL3001 {
}
impl AnalogInputDevice<EL3001Port> for EL3001 {
fn get_input(&self, port: EL3001Port) -> AnalogInputInput {
fn analog_output_state(&self, port: EL3001Port) -> AnalogInputState {
let raw_value = match port {
EL3001Port::AI1 => match &self.txpdo {
EL3001TxPdo {
@@ -73,9 +73,8 @@ impl AnalogInputDevice<EL3001Port> for EL3001 {
EL30XXPresentation::SignedMagnitude => raw_value.as_signed_magnitude(),
};
let normalized = f32::from(value) / f32::from(i16::MAX);
AnalogInputInput {
normalized,
wiring_error: false,
AnalogInputState {
input: AnalogInputInput { normalized },
}
}
+4 -5
View File
@@ -14,7 +14,7 @@ use crate::{
};
use crate::{
helpers::ethercrab_types::EthercrabSubDevicePreoperational,
io::analog_input::{AnalogInputDevice, AnalogInputInput},
io::analog_input::{AnalogInputDevice, AnalogInputInput, AnalogInputState},
};
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
use uom::si::electric_current::milliampere;
@@ -71,7 +71,7 @@ impl NewEthercatDevice for EL3021 {
}
impl AnalogInputDevice<EL3021Port> for EL3021 {
fn get_input(&self, port: EL3021Port) -> AnalogInputInput {
fn analog_output_state(&self, port: EL3021Port) -> AnalogInputState {
let raw_value = match port {
EL3021Port::AI1 => match &self.txpdo {
EL3021TxPdo {
@@ -98,9 +98,8 @@ impl AnalogInputDevice<EL3021Port> for EL3021 {
};
let normalized = f32::from(value) / f32::from(i16::MAX);
AnalogInputInput {
normalized,
wiring_error: false,
AnalogInputState {
input: AnalogInputInput { normalized },
}
}
+4 -5
View File
@@ -14,7 +14,7 @@ use crate::{
};
use crate::{
helpers::ethercrab_types::EthercrabSubDevicePreoperational,
io::analog_input::{AnalogInputDevice, AnalogInputInput},
io::analog_input::{AnalogInputDevice, AnalogInputInput, AnalogInputState},
};
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
use uom::si::electric_current::milliampere;
@@ -80,7 +80,7 @@ impl NewEthercatDevice for EL3024 {
}
impl AnalogInputDevice<EL3024Port> for EL3024 {
fn get_input(&self, port: EL3024Port) -> AnalogInputInput {
fn analog_output_state(&self, port: EL3024Port) -> AnalogInputState {
let raw_value = match port {
EL3024Port::AI1 => match &self.txpdo {
EL3024TxPdo {
@@ -143,9 +143,8 @@ impl AnalogInputDevice<EL3024Port> for EL3024 {
};
let normalized = f32::from(value) / f32::from(i16::MAX);
AnalogInputInput {
normalized,
wiring_error: false,
AnalogInputState {
input: AnalogInputInput { normalized },
}
}
+13 -11
View File
@@ -1,7 +1,7 @@
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::pdo::TxPdo;
use crate::{
io::temperature_input::{TemperatureInputDevice, TemperatureInputInput},
io::temperature_input::{TemperatureInputDevice, TemperatureInputInput, TemperatureInputState},
pdo::el32xx::RtdInput,
};
use ethercat_hal_derive::{EthercatDevice, TxPdo};
@@ -35,7 +35,7 @@ impl NewEthercatDevice for EL3204 {
}
impl TemperatureInputDevice<EL3204Port> for EL3204 {
fn get_input(&self, port: EL3204Port) -> TemperatureInputInput {
fn temperature_input_state(&self, port: EL3204Port) -> TemperatureInputState {
let expect_text = "All channels should be Some(_)";
let channel = match port {
EL3204Port::T1 => self.txpdo.channel1.as_ref().expect(&expect_text),
@@ -43,15 +43,17 @@ impl TemperatureInputDevice<EL3204Port> for EL3204 {
EL3204Port::T3 => self.txpdo.channel3.as_ref().expect(&expect_text),
EL3204Port::T4 => self.txpdo.channel4.as_ref().expect(&expect_text),
};
TemperatureInputInput {
temperature: channel.temperature,
undervoltage: channel.undervoltage,
overvoltage: channel.overvoltage,
limit1: channel.limit1,
limit2: channel.limit2,
error: channel.error,
txpdo_state: channel.txpdo_state,
txpdo_toggle: channel.txpdo_toggle,
TemperatureInputState {
input: TemperatureInputInput {
temperature: channel.temperature,
undervoltage: channel.undervoltage,
overvoltage: channel.overvoltage,
limit1: channel.limit1,
limit2: channel.limit2,
error: channel.error,
txpdo_state: channel.txpdo_state,
txpdo_toggle: channel.txpdo_toggle,
},
}
}
}
+45 -62
View File
@@ -9,9 +9,10 @@ use pdo::{EL7031RxPdo, EL7031TxPdo};
use crate::{
helpers::counter_wrapper_u16_i128::CounterWrapperU16U128,
io::{
digital_input::{DigitalInputDevice, DigitalInputInput},
digital_input::{DigitalInputDevice, DigitalInputInput, DigitalInputState},
stepper_velocity_el70x1::{
StepperVelocityEL70x1Device, StepperVelocityEL70x1Input, StepperVelocityEL70x1Output,
StepperVelocityEL70x1State,
},
},
pdo::{PredefinedPdoAssignment, RxPdo, TxPdo},
@@ -108,7 +109,7 @@ impl NewEthercatDevice for EL7031 {
}
impl StepperVelocityEL70x1Device<EL7031StepperPort> for EL7031 {
fn set_output(
fn stepper_velocity_write(
&mut self,
port: EL7031StepperPort,
value: StepperVelocityEL70x1Output,
@@ -151,10 +152,10 @@ impl StepperVelocityEL70x1Device<EL7031StepperPort> for EL7031 {
}
}
fn get_input(
fn stepper_velocity_state(
&self,
port: EL7031StepperPort,
) -> Result<StepperVelocityEL70x1Input, anyhow::Error> {
) -> Result<StepperVelocityEL70x1State, anyhow::Error> {
// check if operating mode is velocity
if self.configuration.stm_features.operation_mode != EL70x1OperationMode::DirectVelocity {
return Err(anyhow!(
@@ -170,41 +171,6 @@ impl StepperVelocityEL70x1Device<EL7031StepperPort> for EL7031 {
None => return Err(anyhow!("stm_status is None")),
};
Ok(StepperVelocityEL70x1Input {
counter_value: self.counter_wrapper.current(),
ready_to_enable: stm_status.ready_to_enable,
ready: stm_status.ready,
warning: stm_status.warning,
error: stm_status.error,
moving_positive: stm_status.moving_positive,
moving_negative: stm_status.moving_negative,
torque_reduced: stm_status.torque_reduced,
})
}
}
}
fn get_speed_range(
&self,
_port: EL7031StepperPort,
) -> crate::shared_config::el70x1::EL70x1SpeedRange {
self.configuration.stm_features.speed_range
}
fn get_output(
&self,
port: EL7031StepperPort,
) -> Result<StepperVelocityEL70x1Output, anyhow::Error> {
// check if operating mode is velocity
if self.configuration.stm_features.operation_mode != EL70x1OperationMode::DirectVelocity {
return Err(anyhow!(
"Operation mode is not velocity, but {:?}",
self.configuration.stm_features.operation_mode
));
}
match port {
EL7031StepperPort::STM1 => {
let stm_control = match &self.rxpdo.stm_control {
Some(value) => value,
None => return Err(anyhow!("stm_control is None")),
@@ -215,12 +181,24 @@ impl StepperVelocityEL70x1Device<EL7031StepperPort> for EL7031 {
None => return Err(anyhow!("stm_velocity is None")),
};
Ok(StepperVelocityEL70x1Output {
velocity: stm_velocity.velocity,
enable: stm_control.enable,
reduce_torque: stm_control.reduce_torque,
reset: stm_control.reset,
set_counter: self.counter_wrapper.get_override(),
Ok(StepperVelocityEL70x1State {
input: StepperVelocityEL70x1Input {
counter_value: self.counter_wrapper.current(),
ready_to_enable: stm_status.ready_to_enable,
ready: stm_status.ready,
warning: stm_status.warning,
error: stm_status.error,
moving_positive: stm_status.moving_positive,
moving_negative: stm_status.moving_negative,
torque_reduced: stm_status.torque_reduced,
},
output: StepperVelocityEL70x1Output {
velocity: stm_velocity.velocity,
enable: stm_control.enable,
reduce_torque: stm_control.reduce_torque,
reset: stm_control.reset,
set_counter: self.counter_wrapper.get_override(),
},
})
}
}
@@ -228,28 +206,33 @@ impl StepperVelocityEL70x1Device<EL7031StepperPort> for EL7031 {
}
impl DigitalInputDevice<EL7031DigitalInputPort> for EL7031 {
fn get_input(&self, port: EL7031DigitalInputPort) -> Result<DigitalInputInput, anyhow::Error> {
fn digital_input_state(
&self,
port: EL7031DigitalInputPort,
) -> Result<DigitalInputState, anyhow::Error> {
let error1 = anyhow::anyhow!(
"[{}::Device::digital_input_state] Port {:?} is not available",
module_path!(),
port
);
Ok(DigitalInputInput {
value: match port {
EL7031DigitalInputPort::DI1 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_1
}
EL7031DigitalInputPort::DI2 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_2
}
Ok(DigitalInputState {
input: DigitalInputInput {
value: match port {
EL7031DigitalInputPort::DI1 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_1
}
EL7031DigitalInputPort::DI2 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_2
}
},
},
})
}
+57 -76
View File
@@ -12,10 +12,13 @@ use crate::{
counter_wrapper_u16_i128::CounterWrapperU16U128, signing_converter_u16::U16SigningConverter,
},
io::{
analog_input::{AnalogInputDevice, AnalogInputInput, physical::AnalogInputRange},
digital_input::{DigitalInputDevice, DigitalInputInput},
analog_input::{
AnalogInputDevice, AnalogInputInput, AnalogInputState, physical::AnalogInputRange,
},
digital_input::{DigitalInputDevice, DigitalInputInput, DigitalInputState},
stepper_velocity_el70x1::{
StepperVelocityEL70x1Device, StepperVelocityEL70x1Input, StepperVelocityEL70x1Output,
StepperVelocityEL70x1State,
},
},
pdo::{PredefinedPdoAssignment, RxPdo, TxPdo},
@@ -112,7 +115,7 @@ impl NewEthercatDevice for EL7031_0030 {
}
impl StepperVelocityEL70x1Device<EL7031_0030StepperPort> for EL7031_0030 {
fn set_output(
fn stepper_velocity_write(
&mut self,
port: EL7031_0030StepperPort,
value: StepperVelocityEL70x1Output,
@@ -162,10 +165,10 @@ impl StepperVelocityEL70x1Device<EL7031_0030StepperPort> for EL7031_0030 {
}
}
fn get_input(
fn stepper_velocity_state(
&self,
port: EL7031_0030StepperPort,
) -> Result<StepperVelocityEL70x1Input, anyhow::Error> {
) -> Result<StepperVelocityEL70x1State, anyhow::Error> {
// check if operating mode is velocity
if self.configuration.stm_features.operation_mode != EL70x1OperationMode::DirectVelocity {
return Err(anyhow!(
@@ -187,35 +190,6 @@ impl StepperVelocityEL70x1Device<EL7031_0030StepperPort> for EL7031_0030 {
}
};
Ok(StepperVelocityEL70x1Input {
counter_value: self.counter_wrapper.current(),
ready_to_enable: stm_status.ready_to_enable,
ready: stm_status.ready,
warning: stm_status.warning,
error: stm_status.error,
moving_positive: stm_status.moving_positive,
moving_negative: stm_status.moving_negative,
torque_reduced: stm_status.torque_reduced,
})
}
}
}
fn get_output(
&self,
port: EL7031_0030StepperPort,
) -> Result<StepperVelocityEL70x1Output, anyhow::Error> {
// check if operating mode is velocity
if self.configuration.stm_features.operation_mode != EL70x1OperationMode::DirectVelocity {
return Err(anyhow!(
"[{}::StepperVelocityEL70x1Device::stepper_velocity_state] Operation mode is not velocity, but {:?}",
module_path!(),
self.configuration.stm_features.operation_mode
));
}
match port {
EL7031_0030StepperPort::STM1 => {
let stm_control = match &self.rxpdo.stm_control {
Some(value) => value,
None => {
@@ -236,88 +210,95 @@ impl StepperVelocityEL70x1Device<EL7031_0030StepperPort> for EL7031_0030 {
}
};
Ok(StepperVelocityEL70x1Output {
velocity: stm_velocity.velocity,
enable: stm_control.enable,
reduce_torque: stm_control.reduce_torque,
reset: stm_control.reset,
set_counter: self.counter_wrapper.get_override(),
Ok(StepperVelocityEL70x1State {
input: StepperVelocityEL70x1Input {
counter_value: self.counter_wrapper.current(),
ready_to_enable: stm_status.ready_to_enable,
ready: stm_status.ready,
warning: stm_status.warning,
error: stm_status.error,
moving_positive: stm_status.moving_positive,
moving_negative: stm_status.moving_negative,
torque_reduced: stm_status.torque_reduced,
},
output: StepperVelocityEL70x1Output {
velocity: stm_velocity.velocity,
enable: stm_control.enable,
reduce_torque: stm_control.reduce_torque,
reset: stm_control.reset,
set_counter: self.counter_wrapper.get_override(),
},
})
}
}
}
fn get_speed_range(
&self,
_port: EL7031_0030StepperPort,
) -> crate::shared_config::el70x1::EL70x1SpeedRange {
self.configuration.stm_features.speed_range
}
}
impl DigitalInputDevice<EL7031_0030DigitalInputPort> for EL7031_0030 {
fn get_input(
fn digital_input_state(
&self,
port: EL7031_0030DigitalInputPort,
) -> Result<DigitalInputInput, anyhow::Error> {
) -> Result<DigitalInputState, anyhow::Error> {
let error1 = anyhow::anyhow!(
"[{}::DigitalInputDevice::digital_input_state] StmStatus is None",
module_path!(),
);
Ok(DigitalInputInput {
value: match port {
EL7031_0030DigitalInputPort::DI1 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_1
}
EL7031_0030DigitalInputPort::DI2 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_2
}
Ok(DigitalInputState {
input: DigitalInputInput {
value: match port {
EL7031_0030DigitalInputPort::DI1 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_1
}
EL7031_0030DigitalInputPort::DI2 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_2
}
},
},
})
}
}
impl AnalogInputDevice<EL7031_0030AnalogInputPort> for EL7031_0030 {
fn get_input(&self, port: EL7031_0030AnalogInputPort) -> AnalogInputInput {
let (raw_value, wiring_error) = match port {
fn analog_output_state(&self, port: EL7031_0030AnalogInputPort) -> AnalogInputState {
let raw_value = match port {
EL7031_0030AnalogInputPort::AI1 => match &self.txpdo {
EL7031_0030TxPdo {
ai_standard_channel_1: Some(ai_standard_channel_1),
..
} => (ai_standard_channel_1.value, ai_standard_channel_1.error),
} => ai_standard_channel_1.value,
EL7031_0030TxPdo {
ai_compact_channel_1: Some(ai_compact_channel_1),
..
} => (ai_compact_channel_1.value, false),
} => ai_compact_channel_1.value,
_ => panic!("Invalid TxPdo assignment"),
},
EL7031_0030AnalogInputPort::AI2 => match &self.txpdo {
EL7031_0030TxPdo {
ai_standard_channel_2: Some(ai_standard_channel_2),
..
} => (ai_standard_channel_2.value, ai_standard_channel_2.error),
} => ai_standard_channel_2.value,
EL7031_0030TxPdo {
ai_compact_channel_2: Some(ai_compact_channel_2),
..
} => (ai_compact_channel_2.value, false),
} => ai_compact_channel_2.value,
_ => panic!("Invalid TxPdo assignment"),
},
};
let converted_raw_value = U16SigningConverter::load_raw(raw_value);
let value: i16 = converted_raw_value.as_signed();
let raw_value = U16SigningConverter::load_raw(raw_value);
let value: i16 = raw_value.as_signed();
let normalized = f32::from(value) / f32::from(i16::MAX);
AnalogInputInput {
normalized,
wiring_error,
AnalogInputState {
input: AnalogInputInput { normalized },
}
}
+52 -75
View File
@@ -5,9 +5,10 @@ use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple}
use crate::{
helpers::counter_wrapper_u16_i128::CounterWrapperU16U128,
io::{
digital_input::{DigitalInputDevice, DigitalInputInput},
digital_input::{DigitalInputDevice, DigitalInputInput, DigitalInputState},
stepper_velocity_el70x1::{
StepperVelocityEL70x1Device, StepperVelocityEL70x1Input, StepperVelocityEL70x1Output,
StepperVelocityEL70x1State,
},
},
pdo::{PredefinedPdoAssignment, RxPdo, TxPdo},
@@ -108,7 +109,7 @@ impl EthercatDeviceProcessing for EL7041_0052 {
}
impl StepperVelocityEL70x1Device<EL7041_0052Port> for EL7041_0052 {
fn set_output(
fn stepper_velocity_write(
&mut self,
port: EL7041_0052Port,
value: StepperVelocityEL70x1Output,
@@ -157,10 +158,10 @@ impl StepperVelocityEL70x1Device<EL7041_0052Port> for EL7041_0052 {
}
}
fn get_input(
fn stepper_velocity_state(
&self,
port: EL7041_0052Port,
) -> Result<StepperVelocityEL70x1Input, anyhow::Error> {
) -> Result<StepperVelocityEL70x1State, anyhow::Error> {
// check if operating mode is velocity
if self.configuration.stm_features.operation_mode != EL70x1OperationMode::DirectVelocity {
return Err(anyhow!(
@@ -176,41 +177,6 @@ impl StepperVelocityEL70x1Device<EL7041_0052Port> for EL7041_0052 {
None => return Err(anyhow!("stm_status is None")),
};
Ok(StepperVelocityEL70x1Input {
// Use the counter wrapper to get the current counter value
counter_value: self.counter_wrapper.current(),
ready_to_enable: stm_status.ready_to_enable,
ready: stm_status.ready,
warning: stm_status.warning,
error: stm_status.error,
moving_positive: stm_status.moving_positive,
moving_negative: stm_status.moving_negative,
torque_reduced: stm_status.torque_reduced,
})
}
_ => {
return Err(anyhow!(
"Port {:?} is not supported for stepper velocity",
port
));
}
}
}
fn get_output(
&self,
port: EL7041_0052Port,
) -> Result<StepperVelocityEL70x1Output, anyhow::Error> {
// check if operating mode is velocity
if self.configuration.stm_features.operation_mode != EL70x1OperationMode::DirectVelocity {
return Err(anyhow!(
"Operation mode is not velocity, but {:?}",
self.configuration.stm_features.operation_mode
));
}
match port {
EL7041_0052Port::STM1 => {
let stm_control = match &self.rxpdo.stm_control {
Some(value) => value,
None => return Err(anyhow!("stm_control is None")),
@@ -221,12 +187,25 @@ impl StepperVelocityEL70x1Device<EL7041_0052Port> for EL7041_0052 {
None => return Err(anyhow!("stm_velocity is None")),
};
Ok(StepperVelocityEL70x1Output {
velocity: stm_velocity.velocity,
enable: stm_control.enable,
reduce_torque: stm_control.reduce_torque,
reset: stm_control.reset,
set_counter: self.counter_wrapper.get_override(),
Ok(StepperVelocityEL70x1State {
input: StepperVelocityEL70x1Input {
// Use the counter wrapper to get the current counter value
counter_value: self.counter_wrapper.current(),
ready_to_enable: stm_status.ready_to_enable,
ready: stm_status.ready,
warning: stm_status.warning,
error: stm_status.error,
moving_positive: stm_status.moving_positive,
moving_negative: stm_status.moving_negative,
torque_reduced: stm_status.torque_reduced,
},
output: StepperVelocityEL70x1Output {
velocity: stm_velocity.velocity,
enable: stm_control.enable,
reduce_torque: stm_control.reduce_torque,
reset: stm_control.reset,
set_counter: self.counter_wrapper.get_override(),
},
})
}
_ => {
@@ -237,41 +216,39 @@ impl StepperVelocityEL70x1Device<EL7041_0052Port> for EL7041_0052 {
}
}
}
fn get_speed_range(
&self,
_port: EL7041_0052Port,
) -> crate::shared_config::el70x1::EL70x1SpeedRange {
self.configuration.stm_features.speed_range
}
}
impl DigitalInputDevice<EL7041_0052Port> for EL7041_0052 {
fn get_input(&self, port: EL7041_0052Port) -> Result<DigitalInputInput, anyhow::Error> {
fn digital_input_state(
&self,
port: EL7041_0052Port,
) -> Result<DigitalInputState, anyhow::Error> {
let error1 = anyhow::anyhow!("stm_status is None");
Ok(DigitalInputInput {
value: match port {
EL7041_0052Port::DI1 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_1
}
EL7041_0052Port::DI2 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_2
}
_ => {
return Err(anyhow!(
"Port {:?} is not supported for digital input",
port
));
}
Ok(DigitalInputState {
input: DigitalInputInput {
value: match port {
EL7041_0052Port::DI1 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_1
}
EL7041_0052Port::DI2 => {
self.txpdo
.stm_status
.as_ref()
.ok_or(error1)?
.digital_input_2
}
_ => {
return Err(anyhow!(
"Port {:?} is not supported for digital input",
port
));
}
},
},
})
}
+22 -29
View File
@@ -1,7 +1,8 @@
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use futures::executor::block_on;
use physical::{AnalogInputRange, AnalogInputValue};
use smol::lock::RwLock;
@@ -13,7 +14,8 @@ pub mod physical;
/// depending on the type of device and its range.
pub struct AnalogInput {
/// Read the state of the analog input
get_input: Box<dyn Fn() -> AnalogInputInput + Send + Sync>,
pub state:
Box<dyn Fn() -> Pin<Box<dyn Future<Output = AnalogInputState> + Send>> + Send + Sync>,
pub range: AnalogInputRange,
}
@@ -37,40 +39,31 @@ impl AnalogInput {
});
// build async get closure
let get_input = Box::new(move || -> AnalogInputInput {
let device2 = Arc::clone(&device);
let port_clone = port.clone();
block_on(async move {
let device = device2.read().await;
device.get_input(port_clone)
})
});
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = AnalogInputState> + Send>> {
let device2 = Arc::clone(&device);
let port_clone = port.clone();
Box::pin(async move {
let device = device2.read().await;
device.analog_output_state(port_clone)
})
},
);
AnalogInput { get_input, range }
}
/// Value from -1.0 to 1.0
pub fn get_normalized(&self) -> f32 {
let input = (self.get_input)();
input.normalized
}
pub fn get_physical(&self) -> AnalogInputValue {
let normalized = self.get_normalized();
self.range.normalized_to_physical(normalized)
}
pub fn get_wiring_error(&self) -> bool {
let input = (self.get_input)();
input.wiring_error
AnalogInput { state, range }
}
}
#[derive(Debug, Clone)]
pub struct AnalogInputState {
pub input: AnalogInputInput,
}
#[derive(Debug, Clone)]
pub struct AnalogInputInput {
/// from -1.0 to 1.0
/// Can be converted to voltage or mA knowning the type and range of the device
pub normalized: f32,
pub wiring_error: bool,
}
impl AnalogInputInput {
@@ -81,6 +74,6 @@ impl AnalogInputInput {
}
pub trait AnalogInputDevice<PORTS>: Send + Sync {
fn get_input(&self, port: PORTS) -> AnalogInputInput;
fn analog_output_state(&self, port: PORTS) -> AnalogInputState;
fn analog_input_range(&self) -> AnalogInputRange;
}
+36 -38
View File
@@ -1,57 +1,56 @@
use super::analog_input::physical::AnalogInputRange;
use super::analog_input::{AnalogInput, AnalogInputDevice, AnalogInputInput};
use smol::lock::RwLock;
use std::sync::Arc;
use super::analog_input::{AnalogInput, AnalogInputInput, AnalogInputState};
use std::{future::Future, pin::Pin, sync::Arc};
use std::sync::Mutex;
#[derive(Clone)]
pub struct AnalogInputDummyPort;
pub struct AnalogInputDummy {
state: Arc<Mutex<AnalogInputInput>>,
state: Arc<Mutex<AnalogInputState>>,
range: AnalogInputRange,
}
impl AnalogInputDevice<AnalogInputDummyPort> for AnalogInputDummy {
fn get_input(&self, _port: AnalogInputDummyPort) -> AnalogInputInput {
let guard = self.state.lock().unwrap();
guard.clone()
}
fn analog_input_range(&self) -> AnalogInputRange {
self.range.clone()
}
}
impl AnalogInputDummy {
pub fn new(range: AnalogInputRange) -> Self {
let state = Arc::new(Mutex::new(AnalogInputInput {
normalized: 0.0,
wiring_error: false,
let state = Arc::new(Mutex::new(AnalogInputState {
input: AnalogInputInput { normalized: 0.0 },
}));
Self { state, range }
}
pub fn analog_input(&self) -> AnalogInput {
let device: Arc<RwLock<dyn AnalogInputDevice<AnalogInputDummyPort>>> =
Arc::new(RwLock::new(AnalogInputDummy {
state: self.state.clone(),
range: self.range.clone(),
}));
AnalogInput::new(device, AnalogInputDummyPort)
pub fn analog_input(&mut self) -> AnalogInput {
let state_arc = self.state.clone();
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = AnalogInputState> + Send>> {
let state_arc = state_arc.clone();
Box::pin(async move { state_arc.lock().unwrap().clone() })
},
);
AnalogInput {
state,
range: self.range.clone(),
}
}
pub fn set_state(&mut self, state: AnalogInputState) {
let mut guard = self.state.lock().unwrap();
*guard = state;
}
pub fn set_input(&mut self, input: AnalogInputInput) {
{
let mut input_guard = self.state.lock().unwrap();
*input_guard = input;
let mut guard = self.state.lock().unwrap();
guard.input = input;
}
}
pub fn get_input(&self) -> AnalogInputInput {
let input_guard = self.state.lock().unwrap();
input_guard.clone()
let guard = self.state.lock().unwrap();
guard.input.clone()
}
pub fn get_state(&self) -> AnalogInputState {
let guard = self.state.lock().unwrap();
guard.clone()
}
}
@@ -69,14 +68,13 @@ mod tests {
min_raw: 0,
max_raw: i16::MAX,
});
let input = AnalogInputInput {
normalized: 0.5,
wiring_error: false,
let state = AnalogInputState {
input: AnalogInputInput { normalized: 0.5 },
};
dummy.set_input(input.clone());
dummy.set_input(state.input.clone());
let analog_input = dummy.analog_input();
let normalized = analog_input.get_normalized();
assert_eq!(normalized, 0.5);
let state = smol::block_on(async { (analog_input.state)().await });
assert_eq!(state.input.normalized, 0.5);
}
}
+39 -31
View File
@@ -1,18 +1,22 @@
use smol::future::block_on;
use smol::lock::RwLock;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use smol::lock::RwLock;
/// Analog Output (AO) device
///
/// We can write values in clip space (0 to 1) to the device. The voltage output depends on the
/// device and its range.
pub struct AnalogOutput {
/// Write a value to the analog output
pub set_output: Box<dyn Fn(AnalogOutputOutput) -> () + Send + Sync>,
pub write:
Box<dyn Fn(AnalogOutputOutput) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>,
/// Read the state of the analog output
pub get_output: Box<dyn Fn() -> AnalogOutputOutput + Send + Sync>,
pub state:
Box<dyn Fn() -> Pin<Box<dyn Future<Output = AnalogOutputState> + Send>> + Send + Sync>,
}
impl fmt::Debug for AnalogOutput {
@@ -27,38 +31,42 @@ impl AnalogOutput {
where
PORT: Clone + Send + Sync + 'static,
{
// build sync write closure
// build async write closure
let port1 = port.clone();
let device1 = device.clone();
let set_output = Box::new(move |value: AnalogOutputOutput| {
let mut device = block_on(device1.write());
device.set_output(port1.clone(), value);
});
let write = Box::new(
move |value: AnalogOutputOutput| -> Pin<Box<dyn Future<Output = ()> + Send>> {
let device_clone = device1.clone();
let port_clone = port1.clone();
Box::pin(async move {
let mut device = device_clone.write().await;
device.analog_output_write(port_clone, value);
})
},
);
// build sync get closure
// build async get closure
let port2 = port.clone();
let device2 = device.clone();
let get_output = Box::new(move || -> AnalogOutputOutput {
let device = block_on(device2.read());
device.get_output(port2.clone())
});
AnalogOutput {
set_output,
get_output,
}
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = AnalogOutputState> + Send>> {
let device2 = device2.clone();
let port_clone = port2.clone();
Box::pin(async move {
let device = device2.read().await;
device.analog_output_state(port_clone)
})
},
);
AnalogOutput { write, state }
}
}
/// Set the analog output value
pub fn set(&self, value: f32) {
(self.set_output)(value.into());
}
/// Get the current output value
pub fn get(&self) -> f32 {
let output = (self.get_output)();
output.into()
}
#[derive(Debug, Clone)]
pub struct AnalogOutputState {
/// Output value from 0.0 to 1.0
/// Voltage depends on the device
pub output: AnalogOutputOutput,
}
#[derive(Debug, Clone)]
@@ -77,6 +85,6 @@ impl From<AnalogOutputOutput> for f32 {
}
pub trait AnalogOutputDevice<PORTS>: Send + Sync {
fn set_output(&mut self, port: PORTS, value: AnalogOutputOutput);
fn get_output(&self, port: PORTS) -> AnalogOutputOutput;
fn analog_output_write(&mut self, port: PORTS, value: AnalogOutputOutput);
fn analog_output_state(&self, port: PORTS) -> AnalogOutputState;
}
+26 -15
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use std::{fmt, future::Future, pin::Pin, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Digital Input (DI) device
@@ -8,7 +7,11 @@ use smol::lock::RwLock;
/// Reads digital values (true or false) from the device.
pub struct DigitalInput {
/// Read the state of the digital input
get_input: Box<dyn Fn() -> Result<DigitalInputInput, anyhow::Error> + Send + Sync>,
pub state: Box<
dyn Fn() -> Pin<Box<dyn Future<Output = Result<DigitalInputState, anyhow::Error>> + Send>>
+ Send
+ Sync,
>,
}
impl fmt::Debug for DigitalInput {
@@ -23,22 +26,30 @@ impl DigitalInput {
where
PORT: Clone + Send + Sync + 'static,
{
// build sync get closure
// build async get closure
let port2 = port.clone();
let device2 = device.clone();
let get_input = Box::new(move || {
let device = block_on(device2.read());
device.get_input(port2.clone())
});
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = Result<DigitalInputState, anyhow::Error>> + Send>> {
let device2 = device2.clone();
let port_clone = port2.clone();
Box::pin(async move {
let device = device2.read().await;
device.digital_input_state(port_clone)
})
},
);
DigitalInput { get_input }
DigitalInput { state }
}
}
/// Get the current value of the digital input
pub fn get_value(&self) -> Result<bool, anyhow::Error> {
let input = (self.get_input)()?;
Ok(input.value)
}
#[derive(Debug, Clone)]
pub struct DigitalInputState {
/// Input value
/// true: high
/// false: low
pub input: DigitalInputInput,
}
#[derive(Debug, Clone)]
@@ -50,5 +61,5 @@ pub trait DigitalInputDevice<PORTS>: Send + Sync
where
PORTS: Clone,
{
fn get_input(&self, port: PORTS) -> Result<DigitalInputInput, anyhow::Error>;
fn digital_input_state(&self, port: PORTS) -> Result<DigitalInputState, anyhow::Error>;
}
+34 -31
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use std::{fmt, future::Future, pin::Pin, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Digital Output (DO) device
@@ -8,10 +7,12 @@ use smol::lock::RwLock;
/// Writes digital values (true or false) to the device.
pub struct DigitalOutput {
/// Write a value to the digital output
set_output: Box<dyn Fn(DigitalOutputOutput) -> () + Send + Sync>,
pub write:
Box<dyn Fn(DigitalOutputOutput) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>,
/// Read the state of the digital output
get_output: Box<dyn Fn() -> DigitalOutputOutput + Send + Sync>,
pub state:
Box<dyn Fn() -> Pin<Box<dyn Future<Output = DigitalOutputState> + Send>> + Send + Sync>,
}
impl fmt::Debug for DigitalOutput {
@@ -28,38 +29,40 @@ impl DigitalOutput {
where
PORT: Clone + Send + Sync + 'static,
{
// build sync write closure
// build async write closure
let port1 = port.clone();
let device1 = device.clone();
let set_output = Box::new(move |value: DigitalOutputOutput| {
let mut device = block_on(device1.write());
device.set_output(port1.clone(), value);
});
let write = Box::new(
move |value: DigitalOutputOutput| -> Pin<Box<dyn Future<Output = ()> + Send>> {
let device_clone = device1.clone();
let port_clone = port1.clone();
Box::pin(async move {
let mut device = device_clone.write().await;
device.digital_output_write(port_clone, value);
})
},
);
// build sync get closure
// build async get closure
let port2 = port.clone();
let device2 = device.clone();
let get_output = Box::new(move || -> DigitalOutputOutput {
let device = block_on(device2.read());
device.get_output(port2.clone())
});
DigitalOutput {
set_output,
get_output,
}
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = DigitalOutputState> + Send>> {
let device2 = device2.clone();
let port_clone = port2.clone();
Box::pin(async move {
let device = device2.read().await;
device.digital_output_state(port_clone)
})
},
);
DigitalOutput { write, state }
}
}
/// Set the digital output value
pub fn set(&self, enabled: bool) {
(self.set_output)(enabled.into());
}
/// Get the current output value
pub fn get(&self) -> bool {
let output = (self.get_output)();
output.into()
}
#[derive(Debug, Clone)]
pub struct DigitalOutputState {
pub output: DigitalOutputOutput,
}
/// Output value
@@ -84,6 +87,6 @@ pub trait DigitalOutputDevice<PORT>: Send + Sync
where
PORT: Clone,
{
fn set_output(&mut self, port: PORT, value: DigitalOutputOutput);
fn get_output(&self, port: PORT) -> DigitalOutputOutput;
fn digital_output_write(&mut self, port: PORT, value: DigitalOutputOutput);
fn digital_output_state(&self, port: PORT) -> DigitalOutputState;
}
+29 -49
View File
@@ -1,6 +1,5 @@
use std::{fmt, sync::Arc};
use std::{fmt, future::Future, pin::Pin, sync::Arc};
use futures::executor::block_on;
use smol::lock::RwLock;
/// Pulse Train Output (PTO) device
@@ -8,10 +7,12 @@ use smol::lock::RwLock;
/// Generates digital puleses with a given frequency (not PWM) and counts them.
pub struct PulseTrainOutput {
/// Write to the pulse train output
set_output: Box<dyn Fn(PulseTrainOutputOutput) -> () + Send + Sync>,
pub write: Box<
dyn Fn(PulseTrainOutputOutput) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
>,
/// Read the state of the pulse train output
get_output: Box<dyn Fn() -> PulseTrainOutputOutput + Send + Sync>,
get_input: Box<dyn Fn() -> PulseTrainOutputInput + Send + Sync>,
pub state:
Box<dyn Fn() -> Pin<Box<dyn Future<Output = PulseTrainOutputState> + Send>> + Send + Sync>,
}
impl fmt::Debug for PulseTrainOutput {
@@ -26,51 +27,31 @@ impl<'device> PulseTrainOutput {
PORT: Clone + Copy + Send + Sync + 'static,
DEVICE: PulseTrainOutputDevice<PORT> + Send + Sync + 'static,
{
// build sync write closure
// build async write closure
let device1 = device.clone();
let set_output = Box::new(move |value: PulseTrainOutputOutput| {
let mut device = block_on(device1.write());
device.set_output(port, value);
});
let write = Box::new(
move |value: PulseTrainOutputOutput| -> Pin<Box<dyn Future<Output = ()> + Send>> {
let device_clone = device1.clone();
Box::pin(async move {
let mut device = device_clone.write().await;
device.pulse_train_output_write(port, value);
})
},
);
// build sync get closure
// build async get closure
let device2 = device.clone();
let get_output = Box::new(move || -> PulseTrainOutputOutput {
let device = block_on(device2.read());
device.get_output(port)
});
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = PulseTrainOutputState> + Send>> {
let device2 = device2.clone();
Box::pin(async move {
let device = device2.read().await;
device.pulse_train_output_state(port)
})
},
);
// build sync get closure
let device2 = device.clone();
let get_input = Box::new(move || -> PulseTrainOutputInput {
let device = block_on(device2.read());
device.get_input(port)
});
PulseTrainOutput {
set_output,
get_output,
get_input,
}
}
/// Set the frequency value
pub fn set_frequency(&mut self, frequency: i32) {
let mut output = (self.get_output)();
output.frequency_value = frequency;
(self.set_output)(output);
}
/// Get the current frequency value
pub fn get_frequency(&self) -> i32 {
let output = (self.get_output)();
output.frequency_value
}
/// Get the current encoder position (counter value)
pub fn get_position(&self) -> u32 {
let input = (self.get_input)();
input.counter_value
PulseTrainOutput { write, state }
}
}
@@ -107,7 +88,6 @@ pub trait PulseTrainOutputDevice<PORT>: Send + Sync
where
PORT: Clone,
{
fn set_output(&mut self, port: PORT, value: PulseTrainOutputOutput);
fn get_output(&self, port: PORT) -> PulseTrainOutputOutput;
fn get_input(&self, port: PORT) -> PulseTrainOutputInput;
fn pulse_train_output_write(&mut self, port: PORT, value: PulseTrainOutputOutput);
fn pulse_train_output_state(&self, port: PORT) -> PulseTrainOutputState;
}
+99
View File
@@ -0,0 +1,99 @@
use std::{fmt, future::Future, pin::Pin, sync::Arc};
use anyhow::Error;
use smol::lock::RwLock;
use crate::pdo::el7031::{
EncControlCompact, EncStatusCompact, PosControl2, StmControl, StmStatus, StmVelocity,
};
/// Pulse Train Output (PTO) device
///
/// Generates digital puleses with a given frequency (not PWM) and counts them.
pub struct StepperVelocity {
/// Write to the pulse train output
pub write: Box<
dyn Fn(StepperVelocityOutput) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>
+ Send
+ Sync,
>,
/// Read the state of the pulse train output
pub state: Box<
dyn Fn() -> Pin<Box<dyn Future<Output = Result<StepperVelocityState, Error>> + Send>>
+ Send
+ Sync,
>,
}
impl fmt::Debug for StepperVelocity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "StepperVelocity")
}
}
impl<'device> StepperVelocity {
pub fn new<PORT, DEVICE>(device: Arc<RwLock<DEVICE>>, port: PORT) -> StepperVelocity
where
PORT: Clone + Copy + Send + Sync + 'static,
DEVICE: StepperVelocityDevice<PORT> + Send + Sync + 'static,
{
// build async write closure
let device1 = device.clone();
let write = Box::new(
move |value: StepperVelocityOutput| -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> {
let device_clone = device1.clone();
Box::pin(async move {
let mut device = device_clone.write().await;
device.stepper_velocity_write(port, value);
})
},
);
// build async get closure
let device2 = device.clone();
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = Result<StepperVelocityState, Error>> + Send>> {
let device2 = device2.clone();
Box::pin(async move {
let device = device2.read().await;
device.stepper_velocity_state(port)
})
},
);
StepperVelocity { write, state }
}
}
#[derive(Debug, Clone)]
pub struct StepperVelocityState {
pub input: StepperVelocityInput,
pub output: StepperVelocityOutput,
}
#[derive(Debug, Clone)]
pub struct StepperVelocityInput {
pub enc_status_compact: EncStatusCompact,
pub stm_status: StmStatus,
// pub stm_sychron_info_data: StmSynchronInfoData,
}
#[derive(Debug, Clone)]
pub struct StepperVelocityOutput {
pub enc_control_compact: EncControlCompact,
pub stm_control: StmControl,
pub stm_velocity: StmVelocity,
pub pos_control_2: PosControl2,
}
pub trait StepperVelocityDevice<PORT>: Send + Sync
where
PORT: Clone,
{
fn stepper_velocity_write(
&mut self,
port: PORT,
value: StepperVelocityOutput,
) -> Result<(), Error>;
fn stepper_velocity_state(&self, port: PORT) -> Result<StepperVelocityState, Error>;
}
+38 -107
View File
@@ -1,8 +1,6 @@
use std::{fmt, sync::Arc};
use std::{fmt, future::Future, pin::Pin, sync::Arc};
use crate::helpers::el70xx_velocity_converter::EL70x1VelocityConverter;
use anyhow::Error;
use futures::executor::block_on;
use smol::lock::RwLock;
/// Pulse Train Output (PTO) device
@@ -10,14 +8,18 @@ use smol::lock::RwLock;
/// Generates digital puleses with a given frequency (not PWM) and counts them.
pub struct StepperVelocityEL70x1 {
/// Write to the pulse train output
set_output: Box<dyn Fn(StepperVelocityEL70x1Output) -> Result<(), Error> + Send + Sync>,
pub write: Box<
dyn Fn(
StepperVelocityEL70x1Output,
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>
+ Send
+ Sync,
>,
/// Read the state of the pulse train output
get_output: Box<dyn Fn() -> Result<StepperVelocityEL70x1Output, Error> + Send + Sync>,
/// Read the state of the pulse train output
get_input: Box<dyn Fn() -> Result<StepperVelocityEL70x1Input, Error> + Send + Sync>,
/// Get the speed range configuration
get_speed_range: Box<
dyn Fn() -> Result<crate::shared_config::el70x1::EL70x1SpeedRange, Error> + Send + Sync,
pub state: Box<
dyn Fn() -> Pin<Box<dyn Future<Output = Result<StepperVelocityEL70x1State, Error>> + Send>>
+ Send
+ Sync,
>,
}
@@ -33,111 +35,38 @@ impl<'device> StepperVelocityEL70x1 {
PORT: Clone + Copy + Send + Sync + 'static,
DEVICE: StepperVelocityEL70x1Device<PORT> + Send + Sync + 'static,
{
// build sync write closure
// build async write closure
let device1 = device.clone();
let set_output = Box::new(
move |value: StepperVelocityEL70x1Output| -> Result<(), Error> {
smol::block_on(async {
let mut device = device1.write().await;
device.set_output(port, value)
let write = Box::new(
move |value: StepperVelocityEL70x1Output| -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> {
let device_clone = device1.clone();
Box::pin(async move {
let mut device = device_clone.write().await;
device.stepper_velocity_write(port, value)
})
},
);
// build sync get closure
// build async get closure
let device2 = device.clone();
let get_input = Box::new(move || -> Result<StepperVelocityEL70x1Input, Error> {
block_on(async {
let device = device2.read().await;
device.get_input(port)
})
});
let device3 = device.clone();
let get_output = Box::new(move || -> Result<StepperVelocityEL70x1Output, Error> {
block_on(async {
let device = device3.read().await;
device.get_output(port)
})
});
// build async get speed range closure
let device3 = device.clone();
let get_speed_range = Box::new(
move || -> Result<crate::shared_config::el70x1::EL70x1SpeedRange, Error> {
block_on(async {
let device = device3.read().await;
Ok(device.get_speed_range(port))
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = Result<StepperVelocityEL70x1State, Error>> + Send>> {
let device2 = device2.clone();
Box::pin(async move {
let device = device2.read().await;
device.stepper_velocity_state(port)
})
},
);
StepperVelocityEL70x1 {
set_output,
get_input,
get_output,
get_speed_range,
}
StepperVelocityEL70x1 { write, state }
}
}
/// Set the speed in steps per second
pub fn set_speed(&mut self, steps_per_second: f64) -> Result<(), Error> {
// Get current state to preserve other output values
let mut output = (self.get_output)().unwrap();
// Get speed range from device to convert steps to velocity
let speed_range = (self.get_speed_range)();
let converter = EL70x1VelocityConverter::new(&speed_range.unwrap());
let velocity = converter.steps_to_velocity(steps_per_second, true);
output.velocity = velocity;
// Write to device
(self.set_output)(output)
}
/// Get the speed in steps per second
pub fn get_speed(&self) -> i32 {
let output = (self.get_output)().unwrap();
let speed_range = (self.get_speed_range)();
let converter = EL70x1VelocityConverter::new(&speed_range.unwrap());
converter.velocity_to_steps(output.velocity, true) as i32
}
/// Enable or disable the stepper
pub fn set_enabled(&mut self, enabled: bool) {
// Get current state to preserve other output values
let mut output = (self.get_output)().unwrap();
output.enable = enabled;
// Write to device
(self.set_output)(output).unwrap();
}
/// Get the enabled state of the stepper
pub fn is_enabled(&self) -> bool {
let output = (self.get_output)().unwrap();
output.enable
}
/// Get the current position of the stepper
pub fn get_position(&self) -> i128 {
let input = (self.get_input)().unwrap();
input.counter_value
}
/// Set the position of the stepper
pub fn set_position(&mut self, position: i128) {
// Get current state to preserve other output values
let mut output = (self.get_output)().unwrap();
output.set_counter = Some(position);
// Write to device
(self.set_output)(output).unwrap();
}
#[derive(Debug, Clone)]
pub struct StepperVelocityEL70x1State {
pub input: StepperVelocityEL70x1Input,
pub output: StepperVelocityEL70x1Output,
}
#[derive(Debug, Clone)]
@@ -189,8 +118,10 @@ pub trait StepperVelocityEL70x1Device<PORT>: Send + Sync
where
PORT: Clone,
{
fn set_output(&mut self, port: PORT, value: StepperVelocityEL70x1Output) -> Result<(), Error>;
fn get_input(&self, port: PORT) -> Result<StepperVelocityEL70x1Input, Error>;
fn get_output(&self, port: PORT) -> Result<StepperVelocityEL70x1Output, Error>;
fn get_speed_range(&self, port: PORT) -> crate::shared_config::el70x1::EL70x1SpeedRange;
fn stepper_velocity_write(
&mut self,
port: PORT,
value: StepperVelocityEL70x1Output,
) -> Result<(), Error>;
fn stepper_velocity_state(&self, port: PORT) -> Result<StepperVelocityEL70x1State, Error>;
}
+16 -34
View File
@@ -1,15 +1,15 @@
use futures::executor::block_on;
use smol::lock::RwLock;
use crate::pdo::basic::Limit;
use std::{fmt, sync::Arc};
use std::{fmt, future::Future, pin::Pin, sync::Arc};
/// Temperature Input (TI) device
///
/// Reads temperature values from the device.
pub struct TemperatureInput {
/// Read the state of the temperature input
get_input: Box<dyn Fn() -> TemperatureInputInput + Send + Sync>,
pub state:
Box<dyn Fn() -> Pin<Box<dyn Future<Output = TemperatureInputState> + Send>> + Send + Sync>,
}
impl fmt::Debug for TemperatureInput {
@@ -26,39 +26,21 @@ impl TemperatureInput {
where
PORTS: Clone + Send + Sync + 'static,
{
// build sync get closure
// build async get closure
let port2 = port.clone();
let device2 = device.clone();
let get_input = Box::new(move || {
let device2 = device2.clone();
let port_clone = port2.clone();
block_on(async move {
let device = device2.read().await;
device.get_input(port_clone)
})
});
TemperatureInput { get_input }
let state = Box::new(
move || -> Pin<Box<dyn Future<Output = TemperatureInputState> + Send>> {
let device2 = device2.clone();
let port_clone = port2.clone();
Box::pin(async move {
let device = device2.read().await;
device.temperature_input_state(port_clone)
})
},
);
TemperatureInput { state }
}
/// Get the current temperature in degrees Celsius
pub fn get_temperature(&self) -> Result<f64, TemperatureInputError> {
let input = (self.get_input)();
if input.overvoltage {
Err(TemperatureInputError::OverVoltage)
} else if input.undervoltage {
Err(TemperatureInputError::UnderVoltage)
} else {
Ok(input.temperature as f64)
}
}
}
pub enum TemperatureInputError {
/// Over-voltage error
OverVoltage,
/// Under-voltage error
UnderVoltage,
}
#[derive(Debug, Clone)]
@@ -95,5 +77,5 @@ pub struct TemperatureInputInput {
}
pub trait TemperatureInputDevice<PORTS>: Send + Sync {
fn get_input(&self, port: PORTS) -> TemperatureInputInput;
fn temperature_input_state(&self, port: PORTS) -> TemperatureInputState;
}
-1
View File
@@ -24,7 +24,6 @@ smol = "2.0.2"
tokio = { version = "1.44.2", features = ["rt-multi-thread"] }
thread-priority = "1.2.0"
regex = "1.11.1"
futures = "0.3.31"
# web
serde_json = "1.0.140"
-8
View File
@@ -13,7 +13,6 @@ use control_core::machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat, DeviceIdentification,
};
use control_core::machines::new::MachineNewHardwareEthercat;
use control_core::realtime::{set_core_affinity_first_core, set_realtime_priority};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::devices::devices_from_subdevices;
use ethercrab::std::{ethercat_now, tx_rx_task};
@@ -44,13 +43,6 @@ pub async fn setup_loop(
.name("EthercatTxRxThread".to_owned())
.spawn(move || {
send_panic(thread_panic_tx_clone);
// Set core affinity to second core
let _ = set_core_affinity_first_core(1);
// Set the thread to real-time priority
let _ = set_realtime_priority();
let rt = smol::LocalExecutor::new();
let _ = smol::block_on(rt.run(async {
tx_rx_task(&interface, tx, rx)
+6 -21
View File
@@ -2,7 +2,6 @@ use crate::app_state::AppState;
use crate::panic::{PanicDetails, send_panic};
use bitvec::prelude::*;
use control_core::helpers::loop_trottle::LoopThrottle;
use control_core::realtime::{set_core_affinity_first_core, set_realtime_priority};
use smol::channel::Sender;
use std::sync::Arc;
use std::time::Duration;
@@ -19,24 +18,6 @@ pub fn init_loop(
send_panic(thread_panic_tx.clone());
let rt = smol::LocalExecutor::new();
let mut throttle = LoopThrottle::new(Duration::from_millis(1), 10, None);
// Set core affinity to first core
let _ = set_core_affinity_first_core(0);
// Set the thread to real-time priority
if let Err(e) = set_realtime_priority() {
tracing::error!(
"[{}::init_loop] Failed to set real-time priority \n{:?}",
module_path!(),
e
);
} else {
tracing::info!(
"[{}::init_loop] Real-time priority set successfully",
module_path!()
);
}
loop {
let res = smol::block_on(rt.run(async {
throttle.sleep().await;
@@ -48,8 +29,12 @@ pub fn init_loop(
break;
}
}
// Exit the entire program if the Loop fails (gets restarted by systemd if running on NixOS)
std::process::exit(1);
// loop should never exit, but if it does, we send a panic message
// this causes the server to exit (and restarted by systemd if running on NixOS)
panic!(
"[{}::init_loop] Loop thread exited unexpectedly",
module_path!()
);
})
.or_else(|e| {
Err(anyhow::anyhow!(
+8 -13
View File
@@ -1,23 +1,22 @@
use control_core::machines::new::MachineAct;
use super::{ExtruderV2, ExtruderV2Mode};
use control_core::actors::Actor;
use std::time::{Duration, Instant};
impl MachineAct for ExtruderV2 {
impl Actor for ExtruderV2 {
fn act(
&mut self,
now_ts: Instant,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + '_>> {
Box::pin(async move {
self.temperature_controller_back.update(now_ts);
self.temperature_controller_nozzle.update(now_ts);
self.temperature_controller_front.update(now_ts);
self.temperature_controller_middle.update(now_ts);
self.temperature_controller_back.update(now_ts).await;
self.temperature_controller_nozzle.update(now_ts).await;
self.temperature_controller_front.update(now_ts).await;
self.temperature_controller_middle.update(now_ts).await;
if self.mode == ExtruderV2Mode::Extrude {
self.screw_speed_controller.update(now_ts, true);
self.screw_speed_controller.update(now_ts, true).await;
} else {
self.screw_speed_controller.update(now_ts, false);
self.screw_speed_controller.update(now_ts, false).await;
}
if self.mode == ExtruderV2Mode::Standby {
@@ -30,10 +29,6 @@ impl MachineAct for ExtruderV2 {
self.switch_to_heat();
}
if self.screw_speed_controller.get_wiring_error() {
self.emit_state();
}
let now = Instant::now();
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 60.0)
+3 -5
View File
@@ -7,7 +7,7 @@ use control_core::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
cache_one_event,
},
},
};
@@ -49,7 +49,6 @@ impl LiveValuesEvent {
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub is_default_state: bool,
/// rotation state
pub rotation_state: RotationState,
/// mode state
@@ -95,7 +94,6 @@ pub struct RegulationState {
pub struct PressureState {
pub bar: f64,
pub target_bar: f64,
pub wiring_error: bool,
}
#[derive(Serialize, Debug, Clone)]
@@ -225,11 +223,11 @@ impl CacheableEvents<ExtruderV2Events> for ExtruderV2Events {
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
let cache_one = cache_one_event();
match self {
ExtruderV2Events::LiveValues(_) => cache_one_hour,
ExtruderV2Events::State(_) => cache_first_and_last,
ExtruderV2Events::State(_) => cache_one,
}
}
}
@@ -1,461 +0,0 @@
use bitvec::{order::Lsb0, slice::BitSlice};
use control_core::modbus::{
ModbusFunctionCode, ModbusRequest, ModbusResponse,
modbus_serial_interface::ModbusSerialInterface,
};
use ethercat_hal::io::serial_interface::SerialInterface;
use std::time::{Duration, Instant};
use uom::{
ConstZero,
si::{f64::Frequency, frequency::centihertz},
};
/// Specifies all System environment Variables
/// Register addresses are calculated as follows: Register-value 40002 -> address: 40002-40001 -> address:0x1
#[derive(Debug, Clone, Copy)]
enum MitsubishiCS80Register {
/// Register 40002
InverterReset,
/// Register 40003
//ParameterClear,
/// Register 40004
//AllParameterClear,
/// Register 40006
//ParamClearNonCommunication,
/// Register 40007
//AllParameterClearNonCommunication,
/// Register 40009
InverterStatusAndControl,
/// Register 40010
//OperationModeAndSetting,
/// Register 40014
RunningFrequencyRAM,
/// Register 40015
//RunningFrequencyEEPROM,
/// Register 40201
MotorFrequency,
}
impl MitsubishiCS80Register {
const fn address(self) -> u16 {
match self {
Self::InverterReset => 0x1,
Self::InverterStatusAndControl => 0x8,
Self::RunningFrequencyRAM => 0x0d,
Self::MotorFrequency => 0x00C8,
}
}
const fn address_be_bytes(self) -> [u8; 2] {
self.address().to_be_bytes()
}
}
/// These Requests Serve as Templates for controlling the inverter
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
pub enum MitsubishiCS80Requests {
None,
/// Register 40002, Reset/Restart the Inverter
ResetInverter,
/// Register 40004, Clear ALL parameters
ClearAllParameters,
/// Register 40006, Clear a non communication parameter
ClearNonCommunicationParameter,
/// Register 40007, Clear all Non Communication related Parameters
ClearNonCommunicationParameters,
/// Register 40009, Read Inverter Status
ReadInverterStatus,
/// Register 40009, Stops the Motor
StopMotor,
/// Register 40009, Starts the Motor in Forward Rotation
StartForwardRotation,
/// Register 40009, Starts the Motor in Reverse Rotation
StartReverseRotation,
/// Register 40014, Read the current frequency the motor runs at (RAM)
ReadRunningFrequency,
/// Register 40014, Write the frequency
WriteRunningFrequency,
/// Read Register 40201, This contains the actual output frequency
ReadMotorFrequency,
/// Write "Arbitrary" Parameters
WriteParameter,
}
impl From<MitsubishiCS80Requests> for u32 {
fn from(request: MitsubishiCS80Requests) -> Self {
request as u32
}
}
impl TryFrom<u32> for MitsubishiCS80Requests {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::ResetInverter),
2 => Ok(Self::ClearAllParameters),
3 => Ok(Self::ClearNonCommunicationParameter),
4 => Ok(Self::ClearNonCommunicationParameters),
5 => Ok(Self::ReadInverterStatus),
6 => Ok(Self::StopMotor),
7 => Ok(Self::StartForwardRotation),
8 => Ok(Self::StartReverseRotation),
9 => Ok(Self::ReadRunningFrequency),
10 => Ok(Self::WriteRunningFrequency),
11 => Ok(Self::ReadMotorFrequency),
12 => Ok(Self::WriteParameter),
_ => Err(()),
}
}
}
/*
MitsubishiModbusRequest get executed by their priority
Start and StopMotor are highest priority while writeRunningFrequency and readMotorFrequency are one lower
lets say we had StartMotor and readMotorFrequency the order of execution is:
1. StartMotor
2. readMotorFrequency
this is because StartMotor is higher priority
Since the events do not need to be pushed into a queue this makes the inverter operation more stable
Lets say one request "A" with priority 1 and one with 2 "B" are queued up, assume that request B is frequently used
1. Request "B" is executed due to higher priority
2. When B is added again request A has the same priority because it was ignored. B is executed once again
3. B is added again, now A has an effective priority of 3, which is higher then B
*/
impl From<MitsubishiCS80Requests> for MitsubishiCS80Request {
fn from(request: MitsubishiCS80Requests) -> Self {
match request {
MitsubishiCS80Requests::WriteRunningFrequency => {
let reg_bytes = MitsubishiCS80Register::RunningFrequencyRAM.address_be_bytes();
Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x0],
},
request,
RequestType::OperationCommand,
u16::MAX - 1,
)
}
MitsubishiCS80Requests::ReadInverterStatus => {
let reg_bytes = MitsubishiCS80Register::InverterStatusAndControl.address_be_bytes();
Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
},
request,
RequestType::OperationCommand,
u16::MAX - 6, // Priority is -6 because we do not want to know the status as frequently as the frequency
)
}
MitsubishiCS80Requests::StopMotor => {
let reg_bytes = MitsubishiCS80Register::InverterStatusAndControl.address_be_bytes();
Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Value 1 to stop
},
request,
RequestType::OperationCommand,
u16::MAX, // StopMotor should have highest priority
)
}
MitsubishiCS80Requests::StartForwardRotation => {
let reg_bytes = MitsubishiCS80Register::InverterStatusAndControl.address_be_bytes();
Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000010], // Value 2 for forward rotation
},
request,
RequestType::OperationCommand,
u16::MAX - 1,
)
}
MitsubishiCS80Requests::StartReverseRotation => {
let reg_bytes = MitsubishiCS80Register::InverterStatusAndControl.address_be_bytes();
Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000100], // Value 4 for reverse rotation
},
request,
RequestType::OperationCommand,
u16::MAX - 1,
)
}
MitsubishiCS80Requests::ReadRunningFrequency => {
let reg_bytes = MitsubishiCS80Register::RunningFrequencyRAM.address_be_bytes();
Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
},
request,
RequestType::OperationCommand,
u16::MAX - 4,
)
}
MitsubishiCS80Requests::ReadMotorFrequency => {
let reg_bytes = MitsubishiCS80Register::MotorFrequency.address_be_bytes();
Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x1],
},
request,
RequestType::OperationCommand,
u16::MAX - 2,
)
}
MitsubishiCS80Requests::ResetInverter => {
let reg_bytes = MitsubishiCS80Register::InverterReset.address_be_bytes();
Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0b00000001],
},
request,
RequestType::Reset,
u16::MAX,
)
}
MitsubishiCS80Requests::WriteParameter => Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![0x0, 0x0, 0x0, 0x0],
},
request,
RequestType::ReadWrite,
u16::MAX,
),
// For unimplemented variants, return a default request
_ => Self::new(
ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![0x0, 0x0, 0x0, 0x1],
},
request,
RequestType::None,
0,
),
}
}
}
#[derive(Debug, Default)]
pub struct MitsubishiCS80Status {
pub running: bool,
pub forward_running: bool,
pub reverse_running: bool,
pub su: bool,
pub ol: bool,
pub no_function: bool,
pub fu: bool,
pub abc_: bool,
pub fault_occurence: bool,
}
#[derive(Debug)]
pub struct MitsubishiCS80 {
// Communication
pub status: MitsubishiCS80Status,
pub modbus_serial_interface: ModbusSerialInterface,
pub last_ts: Instant,
pub frequency: Frequency,
}
#[derive(Debug, Clone, Copy)]
pub enum RequestType {
None,
/// Monitoring, Operation (start,stop etc) command, frequency setting (RAM), less than 12 milliseconds timeout for Response
OperationCommand,
/// Parameter Read/Write and Frequency (EEPROM), Less than 30 milliseconds timeout for Response
ReadWrite,
/// Less than 5 seconds timeout for Response
ParamClear,
/// Supposedly no waiting time, however inverter takes a while to start ~300ms should be more than enough
Reset,
}
impl RequestType {
const fn timeout_duration(self) -> Duration {
match self {
Self::OperationCommand => Duration::from_millis(12),
Self::ReadWrite => Duration::from_millis(30),
Self::ParamClear => Duration::from_millis(5000),
Self::Reset => Duration::from_millis(900),
Self::None => Duration::from_millis(12),
}
}
}
// We need to know from the request queue which events are of what operation type, so that the correct timeout can be used
#[derive(Debug, Clone)]
pub struct MitsubishiCS80Request {
request: ModbusRequest,
control_request_type: MitsubishiCS80Requests,
request_type: RequestType,
priority: u16,
}
impl MitsubishiCS80Request {
const fn new(
request: ModbusRequest,
control_request_type: MitsubishiCS80Requests,
request_type: RequestType,
priority: u16,
) -> Self {
Self {
request,
control_request_type,
request_type,
priority,
}
}
}
impl MitsubishiCS80 {
pub fn new(serial_interface: SerialInterface) -> Self {
Self {
modbus_serial_interface: ModbusSerialInterface::new(serial_interface),
last_ts: Instant::now(),
frequency: Frequency::ZERO,
status: MitsubishiCS80Status::default(),
}
}
fn handle_motor_frequency(&mut self, resp: &ModbusResponse) {
if resp.data.len() >= 3 {
let freq_bytes = &resp.data[1..3]; // bytes 1 and 2 are needed
let raw_frequency = u16::from_be_bytes([freq_bytes[0], freq_bytes[1]]) as f64;
self.frequency = Frequency::new::<centihertz>(raw_frequency);
}
}
fn handle_read_inverter_status(&mut self, resp: &ModbusResponse) {
if resp.data.len() < 3 {
return;
}
let status_bytes: [u8; 2] = match resp.data[1..3].try_into() {
Ok(bytes) => bytes,
Err(_) => return,
};
let bits: &BitSlice<u8, Lsb0> = BitSlice::<_, Lsb0>::from_slice(&status_bytes);
if bits.len() >= 16 {
self.status = MitsubishiCS80Status {
running: bits[8],
forward_running: bits[9],
reverse_running: bits[10],
su: bits[11],
ol: bits[12],
no_function: bits[13],
fu: bits[14],
abc_: bits[15],
fault_occurence: bits[7],
};
}
}
fn handle_response(&mut self, control_request_type: u32) {
let response_type = match MitsubishiCS80Requests::try_from(control_request_type) {
Ok(request_type) => request_type,
Err(_) => return,
};
let Some(response) = self.modbus_serial_interface.get_response().cloned() else {
return;
};
match response_type {
MitsubishiCS80Requests::ReadInverterStatus => {
self.handle_read_inverter_status(&response);
}
MitsubishiCS80Requests::ReadMotorFrequency => {
self.handle_motor_frequency(&response);
}
// Other request types don't need response handling
_ => {}
}
}
fn convert_frequency_to_word(&self, frequency: Frequency) -> u16 {
let scaled = frequency.get::<centihertz>(); // Convert Hz to 0.01 Hz units
scaled.round() as u16
}
fn add_request(&mut self, request: MitsubishiCS80Request) {
let no_response_expected = matches!(
request.control_request_type,
MitsubishiCS80Requests::None | MitsubishiCS80Requests::ResetInverter
);
self.modbus_serial_interface.add_request(
request.control_request_type.into(),
request.priority as u32,
request.request,
no_response_expected,
Some(request.request_type.timeout_duration().as_nanos() as u32),
);
}
pub fn stop_motor(&mut self) {
self.add_request(MitsubishiCS80Requests::StopMotor.into());
}
pub fn set_frequency_target(&mut self, frequency: Frequency) {
let mut request: MitsubishiCS80Request =
MitsubishiCS80Requests::WriteRunningFrequency.into();
let result = self.convert_frequency_to_word(frequency);
let bytes = result.to_le_bytes();
request.request.data[2] = bytes[1];
request.request.data[3] = bytes[0];
self.add_request(request);
}
pub fn set_rotation(&mut self, forward_rotation: bool) {
let request = if forward_rotation {
// Gearbox is inverted!
MitsubishiCS80Requests::StartReverseRotation
} else {
MitsubishiCS80Requests::StartForwardRotation
};
self.add_request(request.into());
}
pub fn reset_inverter(&mut self) {
self.add_request(MitsubishiCS80Requests::ResetInverter.into());
}
pub async fn act(&mut self, now: Instant) {
if !self.modbus_serial_interface.is_initialized() {
if self.modbus_serial_interface.initialize().await {
self.add_request(MitsubishiCS80Requests::ResetInverter.into());
}
return;
}
self.add_request(MitsubishiCS80Requests::ReadInverterStatus.into());
self.add_request(MitsubishiCS80Requests::ReadMotorFrequency.into());
self.modbus_serial_interface.act(now).await;
self.handle_response(self.modbus_serial_interface.last_message_id);
}
}
@@ -0,0 +1,489 @@
use bitvec::{order::Lsb0, slice::BitSlice};
use control_core::actors::serial_interface_actor::SerialInterfaceActor;
use control_core::modbus::{ModbusFunctionCode, ModbusRequest, ModbusResponse};
use ethercat_hal::io::serial_interface::SerialInterface;
use std::{
time::{Duration, Instant},
u16,
};
use uom::{
ConstZero,
si::{f64::Frequency, frequency::centihertz},
};
/// Specifies all System environmet Variables
/// Register addresses are calculated as follows: Register-value 40002 -> address: 40002-40001 -> address:0x1
enum MitsubishiSystemRegister {
/// Register 40002
InverterReset,
/// Register 40003
//ParameterClear,
/// Register 40004
//AllParameterClear,
/// Register 40006
//ParamClearNonCommunication,
/// Register 40007
//AllParameterClearNonCommunication,
/// Register 40009
InverterStatusAndControl,
/// Register 40010
//OperationModeAndSetting,
/// Register 40014
RunningFrequencyRAM,
/// Register 40015
//RunningFrequencyEEPROM,
/// Register 40201
MotorFrequency,
}
impl MitsubishiControlRequests {
fn get_system_register(value: MitsubishiSystemRegister) -> u16 {
match value {
MitsubishiSystemRegister::InverterReset => 0x1,
// MitsubishiSystemRegister::ParameterClear => 0x2,
// MitsubishiSystemRegister::AllParameterClear => 0x3,
// MitsubishiSystemRegister::ParamClearNonCommunication => 0x5,
// MitsubishiSystemRegister::AllParameterClearNonCommunication => 0x6,
MitsubishiSystemRegister::InverterStatusAndControl => 0x8,
// MitsubishiSystemRegister::OperationModeAndSetting => 0x9,
MitsubishiSystemRegister::RunningFrequencyRAM => 0x0d,
//MitsubishiSystemRegister::RunningFrequencyEEPROM => 0x0e,
MitsubishiSystemRegister::MotorFrequency => 0x00C8,
}
}
}
// So that the generic SerialInterfaceActor can identify the Requests
impl From<MitsubishiControlRequests> for u32 {
fn from(request: MitsubishiControlRequests) -> Self {
match request {
MitsubishiControlRequests::None => 0,
MitsubishiControlRequests::ResetInverter => 1,
MitsubishiControlRequests::ClearAllParameters => 2,
MitsubishiControlRequests::ClearNonCommunicationParameter => 3,
MitsubishiControlRequests::ClearNonCommunicationParameters => 4,
MitsubishiControlRequests::ReadInverterStatus => 5,
MitsubishiControlRequests::StopMotor => 6,
MitsubishiControlRequests::StartForwardRotation => 7,
MitsubishiControlRequests::StartReverseRotation => 8,
MitsubishiControlRequests::ReadRunningFrequency => 9,
MitsubishiControlRequests::WriteRunningFrequency => 10,
MitsubishiControlRequests::ReadMotorFrequency => 11,
MitsubishiControlRequests::WriteParameter => 12,
}
}
}
impl TryFrom<u32> for MitsubishiControlRequests {
type Error = ();
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(MitsubishiControlRequests::None),
1 => Ok(MitsubishiControlRequests::ResetInverter),
2 => Ok(MitsubishiControlRequests::ClearAllParameters),
3 => Ok(MitsubishiControlRequests::ClearNonCommunicationParameter),
4 => Ok(MitsubishiControlRequests::ClearNonCommunicationParameters),
5 => Ok(MitsubishiControlRequests::ReadInverterStatus),
6 => Ok(MitsubishiControlRequests::StopMotor),
7 => Ok(MitsubishiControlRequests::StartForwardRotation),
8 => Ok(MitsubishiControlRequests::StartReverseRotation),
9 => Ok(MitsubishiControlRequests::ReadRunningFrequency),
10 => Ok(MitsubishiControlRequests::WriteRunningFrequency),
11 => Ok(MitsubishiControlRequests::ReadMotorFrequency),
_ => Err(()),
}
}
}
/*
MitsubishiModbusRequest get executed by their priority
Start and StopMotor are highest priority while writeRunningFrequency and readMotorFrequency are one lower
lets say we had StartMotor and readMotorFrequency the order of execution is:
1. StartMotor
2. readMotorFrequency
this is because StartMotor is higher priority
Since the events do not need to be pushed into a queue this makes the inverter operation more stable
Lets say one request "A" with priority 1 and one with 2 "B" are queued up, assume that request B is frequently used
1. Request "B" is executed due to higher priority
2. When B is added again request A has the same priority because it was ignored. B is executed once again
3. B is added again, now A has an effective priority of 3, which is higher then B
*/
impl From<MitsubishiControlRequests> for MitsubishiModbusRequest {
fn from(request: MitsubishiControlRequests) -> Self {
match request {
MitsubishiControlRequests::WriteRunningFrequency => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::RunningFrequencyRAM,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x0],
},
request_type: RequestType::OperationCommand,
priority: u16::MAX - 1,
control_request_type: MitsubishiControlRequests::WriteRunningFrequency,
}
}
MitsubishiControlRequests::ReadInverterStatus => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
},
request_type: RequestType::OperationCommand,
// Priority is -6 because we do not want to know the status as frequently as the frequency, which is why its priority is lower
// In essence this means for every fifth ReadMotorFrequency request an InverterStatus request is sent
priority: u16::MAX - 6,
control_request_type: MitsubishiControlRequests::ReadInverterStatus,
}
}
MitsubishiControlRequests::StopMotor => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Value 1 to stop
},
request_type: RequestType::OperationCommand,
priority: u16::MAX, // StopMotor should have highest priority
control_request_type: MitsubishiControlRequests::StopMotor,
}
}
MitsubishiControlRequests::StartForwardRotation => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000010], // Value 2 for forward rotation
},
request_type: RequestType::OperationCommand,
priority: u16::MAX - 1,
control_request_type: MitsubishiControlRequests::StartForwardRotation,
}
}
MitsubishiControlRequests::StartReverseRotation => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterStatusAndControl,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0, 0b00000100], // Value 4 for reverse rotation
},
request_type: RequestType::OperationCommand,
priority: u16::MAX - 1,
control_request_type: MitsubishiControlRequests::StartReverseRotation,
}
}
MitsubishiControlRequests::ReadRunningFrequency => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::RunningFrequencyRAM,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x00, 0x01], // Read 1 register
},
request_type: RequestType::OperationCommand,
priority: u16::MAX - 4,
control_request_type: MitsubishiControlRequests::ReadRunningFrequency,
}
}
MitsubishiControlRequests::ReadMotorFrequency => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::MotorFrequency,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::ReadHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0x1],
},
request_type: RequestType::OperationCommand,
priority: u16::MAX - 2,
control_request_type: MitsubishiControlRequests::ReadMotorFrequency,
}
}
MitsubishiControlRequests::ResetInverter => {
let reg: u16 = MitsubishiControlRequests::get_system_register(
MitsubishiSystemRegister::InverterReset,
);
let reg_bytes = reg.to_be_bytes();
MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![reg_bytes[0], reg_bytes[1], 0x0, 0b00000001],
},
request_type: RequestType::Reset,
priority: u16::MAX,
control_request_type: MitsubishiControlRequests::ResetInverter,
}
}
MitsubishiControlRequests::ClearAllParameters => todo!(),
MitsubishiControlRequests::ClearNonCommunicationParameter => todo!(),
MitsubishiControlRequests::ClearNonCommunicationParameters => todo!(),
MitsubishiControlRequests::None => todo!(),
MitsubishiControlRequests::WriteParameter => MitsubishiModbusRequest {
request: ModbusRequest {
slave_id: 1,
function_code: ModbusFunctionCode::PresetHoldingRegister,
data: vec![0x0, 0x0, 0x0, 0x0],
},
request_type: RequestType::ReadWrite,
priority: u16::MAX,
control_request_type: MitsubishiControlRequests::WriteParameter,
},
}
}
}
/// These Requests Serve as Templates for controling the inverter
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub enum MitsubishiControlRequests {
None,
/// Register 40002, Reset/Restart the Inverter
ResetInverter,
/// Register 40004, Clear ALL parameters
ClearAllParameters,
/// Register 40006, Clear a non communication parameter
ClearNonCommunicationParameter,
/// Register 40007, Clear all Non Communication related Parameters
ClearNonCommunicationParameters,
/// Register 40009, Read Inverter Status
ReadInverterStatus,
/// Register 40009, Stops the Motor
StopMotor,
/// Register 40009, Starts the Motor in Forward Rotation
StartForwardRotation,
/// Register 40009, Starts the Motor in Reverse Rotation
StartReverseRotation,
/// Register 40014, Read the current frequency the motor runs at (RAM)
ReadRunningFrequency,
/// Register 40014, Write the frequency
WriteRunningFrequency,
/// Read Register 40201, This contains the actual output frequency
ReadMotorFrequency,
/// Write "Arbitrary" Parameters
WriteParameter,
}
// We need to know from the request queue which events are of what operation type, so that the correct timeout can be used
#[derive(Debug, Clone)]
pub struct MitsubishiModbusRequest {
request: ModbusRequest,
control_request_type: MitsubishiControlRequests,
request_type: RequestType,
priority: u16,
}
#[derive(Debug, Default)]
pub struct MitsubishiInverterStatus {
pub running: bool,
pub forward_running: bool,
pub reverse_running: bool,
pub su: bool,
pub ol: bool,
pub no_function: bool,
pub fu: bool,
pub abc_: bool,
pub fault_occurence: bool,
}
#[derive(Debug)]
pub struct MitsubishiInverterController {
// Communication
pub inverter_status: MitsubishiInverterStatus,
pub serial_actor: SerialInterfaceActor,
pub last_ts: Instant,
pub frequency: Frequency,
}
impl MitsubishiInverterController {
pub fn new(serial_interface: SerialInterface) -> Self {
Self {
serial_actor: SerialInterfaceActor::new(serial_interface),
last_ts: Instant::now(),
frequency: Frequency::ZERO,
inverter_status: MitsubishiInverterStatus::default(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum RequestType {
None,
/// Monitoring, Operation (start,stop etc) command, frequency setting (RAM), less than 12 milliseconds timeout for Response
OperationCommand,
/// Parameter Read/Write and Frequency (EEPROM), Less than 30 milliseconds timeout for Response
ReadWrite,
/// Less than 5 seconds timeout for Response
ParamClear,
/// Supposedly no waiting time, however inverter takes a while to start ~300ms should be more than enough
Reset,
}
impl RequestType {
fn timeout_duration(self) -> Duration {
match self {
RequestType::OperationCommand => Duration::from_millis(12),
RequestType::ReadWrite => Duration::from_millis(30),
RequestType::ParamClear => Duration::from_millis(5000),
RequestType::Reset => Duration::from_millis(900),
RequestType::None => Duration::from_millis(12),
}
}
}
impl MitsubishiInverterController {
fn handle_motor_frequency(&mut self, resp: ModbusResponse) {
let freq_bytes = &resp.data[1..3]; // bytes 1 and 2 are needed
let raw_frequency = u16::from_be_bytes([freq_bytes[0], freq_bytes[1]]) as f64;
self.frequency = Frequency::new::<centihertz>(raw_frequency);
}
fn handle_read_inverter_status(&mut self, resp: ModbusResponse) {
let status_bytes: [u8; 2] = match resp.data[1..3].try_into() {
Ok(res) => res,
Err(_) => return,
};
let bits: &BitSlice<u8, Lsb0> = BitSlice::<_, Lsb0>::from_slice(&status_bytes);
self.inverter_status = MitsubishiInverterStatus {
running: bits[8],
forward_running: bits[9],
reverse_running: bits[10],
su: bits[11],
ol: bits[12],
no_function: bits[13],
fu: bits[14],
abc_: bits[15],
fault_occurence: bits[7],
};
}
fn handle_response(&mut self, control_request_type: u32) {
let response_type: Result<MitsubishiControlRequests, ()> = control_request_type.try_into();
let result = match response_type {
Ok(response_type) => response_type,
Err(_) => {
return;
}
};
let response = match self.serial_actor.get_response() {
Some(response) => response,
None => {
return;
}
};
match result {
MitsubishiControlRequests::None => (),
MitsubishiControlRequests::ResetInverter => (),
MitsubishiControlRequests::ClearAllParameters => (),
MitsubishiControlRequests::ClearNonCommunicationParameter => (),
MitsubishiControlRequests::ClearNonCommunicationParameters => (),
MitsubishiControlRequests::ReadInverterStatus => {
self.handle_read_inverter_status(response.clone())
}
MitsubishiControlRequests::StopMotor => (),
MitsubishiControlRequests::StartForwardRotation => (),
MitsubishiControlRequests::StartReverseRotation => (),
MitsubishiControlRequests::ReadRunningFrequency => (),
MitsubishiControlRequests::WriteRunningFrequency => (),
MitsubishiControlRequests::ReadMotorFrequency => {
self.handle_motor_frequency(response.clone())
}
MitsubishiControlRequests::WriteParameter => (),
}
}
pub fn convert_hz_float_to_word(&mut self, value: Frequency) -> u16 {
let scaled = value.get::<centihertz>(); // Convert Hz to 0.01 Hz units
scaled.round() as u16
}
fn add_request(&mut self, request: MitsubishiModbusRequest) {
let mut no_response_expected = false;
match request.control_request_type {
MitsubishiControlRequests::None => no_response_expected = true,
MitsubishiControlRequests::ResetInverter => no_response_expected = true,
_ => (),
}
self.serial_actor.add_request(
request.control_request_type.into(),
request.priority as u32,
request.request,
no_response_expected,
Some(request.request_type.timeout_duration().as_nanos() as u32),
);
}
pub fn stop_motor(&mut self) {
self.add_request(MitsubishiControlRequests::StopMotor.into());
}
pub fn set_frequency_target(&mut self, frequency: Frequency) {
let mut request: MitsubishiModbusRequest =
MitsubishiControlRequests::WriteRunningFrequency.into();
let result: u16 = self.convert_hz_float_to_word(frequency); // convert hz float to short
request.request.data[2] = result.to_le_bytes()[1];
request.request.data[3] = result.to_le_bytes()[0];
self.add_request(request);
}
pub fn set_rotation(&mut self, forward_rotation: bool) {
if forward_rotation {
// Gearbox is inverted!
self.add_request(MitsubishiControlRequests::StartReverseRotation.into());
} else {
self.add_request(MitsubishiControlRequests::StartForwardRotation.into());
}
}
pub fn reset_inverter(&mut self) {
self.add_request(MitsubishiControlRequests::ResetInverter.into());
}
pub async fn act(&mut self, now: Instant) {
if self.serial_actor.is_initialized() == false {
let res = self.serial_actor.initialize().await;
if res {
self.add_request(MitsubishiControlRequests::ResetInverter.into());
}
return;
}
self.add_request(MitsubishiControlRequests::ReadInverterStatus.into());
self.add_request(MitsubishiControlRequests::ReadMotorFrequency.into());
self.serial_actor.act(now).await;
self.handle_response(self.serial_actor.last_message_id);
}
}
+26 -16
View File
@@ -16,7 +16,7 @@ use uom::si::{
};
pub mod act;
pub mod api;
pub mod mitsubishi_cs80;
pub mod mitsubishi_inverter_rs485;
pub mod new;
pub mod screw_speed_controller;
pub mod temperature_controller;
@@ -64,10 +64,6 @@ pub struct ExtruderV2 {
temperature_controller_middle: TemperatureController,
temperature_controller_back: TemperatureController,
temperature_controller_nozzle: TemperatureController,
/// will be initalized as false and set to true by `emit_state`
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
}
impl std::fmt::Display for ExtruderV2 {
@@ -125,7 +121,6 @@ impl ExtruderV2 {
pub fn emit_state(&mut self) {
let state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
rotation_state: RotationState {
forward: self.screw_speed_controller.get_rotation_direction(),
},
@@ -141,7 +136,6 @@ impl ExtruderV2 {
.screw_speed_controller
.get_target_pressure()
.get::<bar>(),
wiring_error: self.screw_speed_controller.get_wiring_error(),
},
screw_state: ScrewState {
rpm: self
@@ -217,15 +211,31 @@ impl ExtruderV2 {
.get_nozzle_pressure_limit_enabled(),
},
inverter_status_state: InverterStatusState {
running: self.screw_speed_controller.inverter.status.running,
forward_running: self.screw_speed_controller.inverter.status.forward_running,
reverse_running: self.screw_speed_controller.inverter.status.reverse_running,
up_to_frequency: self.screw_speed_controller.inverter.status.su,
overload_warning: self.screw_speed_controller.inverter.status.ol,
no_function: self.screw_speed_controller.inverter.status.no_function,
output_frequency_detection: self.screw_speed_controller.inverter.status.fu,
abc_fault: self.screw_speed_controller.inverter.status.abc_,
fault_occurence: self.screw_speed_controller.inverter.status.fault_occurence,
running: self.screw_speed_controller.inverter.inverter_status.running,
forward_running: self
.screw_speed_controller
.inverter
.inverter_status
.forward_running,
reverse_running: self
.screw_speed_controller
.inverter
.inverter_status
.reverse_running,
up_to_frequency: self.screw_speed_controller.inverter.inverter_status.su,
overload_warning: self.screw_speed_controller.inverter.inverter_status.ol,
no_function: self
.screw_speed_controller
.inverter
.inverter_status
.no_function,
output_frequency_detection: self.screw_speed_controller.inverter.inverter_status.fu,
abc_fault: self.screw_speed_controller.inverter.inverter_status.abc_,
fault_occurence: self
.screw_speed_controller
.inverter
.inverter_status
.fault_occurence,
},
pid_settings: PidSettingsStates {
temperature: PidSettings {
+32 -19
View File
@@ -1,15 +1,23 @@
use super::{
ExtruderV2, ExtruderV2Mode, Heating, api::ExtruderV2Namespace, mitsubishi_cs80::MitsubishiCS80,
ExtruderV2, ExtruderV2Mode, Heating, api::ExtruderV2Namespace,
mitsubishi_inverter_rs485::MitsubishiInverterController,
screw_speed_controller::ScrewSpeedController,
};
use crate::machines::extruder1::temperature_controller::TemperatureController;
use anyhow::Error;
use control_core::machines::{
identification::DeviceHardwareIdentification,
new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_device_identification_by_role,
get_ethercat_device_by_index, get_subdevice_by_index, validate_no_role_dublicates,
validate_same_machine_identification_unique,
use control_core::{
actors::{
analog_input_getter::AnalogInputGetter, digital_output_setter::DigitalOutputSetter,
temperature_input_getter::TemperatureInputGetter,
},
machines::{
identification::DeviceHardwareIdentification,
new::{
MachineNewHardware, MachineNewParams, MachineNewTrait,
get_device_identification_by_role, get_ethercat_device_by_index,
get_subdevice_by_index, validate_no_role_dublicates,
validate_same_machine_identification_unique,
},
},
};
use ethercat_hal::{
@@ -313,13 +321,17 @@ impl MachineNewTrait for ExtruderV2 {
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
let t4 = TemperatureInput::new(el3204.clone(), EL3204Port::T4);
let t1_getter = TemperatureInputGetter::new(t1);
let t2_getter = TemperatureInputGetter::new(t2);
let t3_getter = TemperatureInputGetter::new(t3);
let t4_getter = TemperatureInputGetter::new(t4);
// For the Relais
let digital_out_1 = DigitalOutput::new(el2004.clone(), EL2004Port::DO1);
let digital_out_2 = DigitalOutput::new(el2004.clone(), EL2004Port::DO2);
let digital_out_3 = DigitalOutput::new(el2004.clone(), EL2004Port::DO3);
let digital_out_4 = DigitalOutput::new(el2004.clone(), EL2004Port::DO4);
let pressure_sensor = AnalogInput::new(el3021, EL3021Port::AI1);
let pressure_sensor = AnalogInputGetter::new(AnalogInput::new(el3021, EL3021Port::AI1));
// The Extruders temparature Controllers should disable the relais when the max_temperature is reached
let extruder_max_temperature = ThermodynamicTemperature::new::<degree_celsius>(300.0);
// Only front heating on: These values work 0.08, 0.001, 0.007, Overshoot 0.5 undershoot ~0.7 (Problems when starting far away because of integral)
@@ -329,8 +341,8 @@ impl MachineNewTrait for ExtruderV2 {
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t1,
digital_out_1,
t1_getter,
DigitalOutputSetter::new(digital_out_1),
Heating::default(),
Duration::from_millis(500),
700.0,
@@ -344,8 +356,8 @@ impl MachineNewTrait for ExtruderV2 {
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t2,
digital_out_2,
t2_getter,
DigitalOutputSetter::new(digital_out_2),
Heating::default(),
Duration::from_millis(500),
700.0,
@@ -359,8 +371,8 @@ impl MachineNewTrait for ExtruderV2 {
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t3,
digital_out_3,
t3_getter,
DigitalOutputSetter::new(digital_out_3),
Heating::default(),
Duration::from_millis(500),
700.0,
@@ -374,16 +386,18 @@ impl MachineNewTrait for ExtruderV2 {
0.008,
ThermodynamicTemperature::new::<degree_celsius>(150.0),
extruder_max_temperature,
t4,
digital_out_4,
t4_getter,
DigitalOutputSetter::new(digital_out_4),
Heating::default(),
Duration::from_millis(500),
200.0,
0.95,
);
let inverter =
MitsubishiCS80::new(SerialInterface::new(el6021, el6021::EL6021Port::SI1));
let inverter = MitsubishiInverterController::new(SerialInterface::new(
el6021,
el6021::EL6021Port::SI1,
));
let target_pressure = Pressure::new::<bar>(0.0);
let target_rpm = AngularVelocity::new::<revolution_per_minute>(0.0);
@@ -400,7 +414,6 @@ impl MachineNewTrait for ExtruderV2 {
temperature_controller_back: temperature_controller_back,
temperature_controller_nozzle: temperature_controller_nozzle,
screw_speed_controller: screw_speed_controller,
emitted_default_state: false,
};
extruder.emit_state();
Ok(extruder)
@@ -1,11 +1,11 @@
use std::time::Instant;
use control_core::{
actors::{Actor, analog_input_getter::AnalogInputGetter},
controllers::clamping_timeagnostic_pid::ClampingTimeagnosticPidController,
converters::transmission_converter::TransmissionConverter, helpers::interpolation::normalize,
converters::transmission_converter::TransmissionConverter,
helpers::interpolation::normalize,
};
use ethercat_hal::io::analog_input::AnalogInput;
use futures::executor::block_on;
use uom::si::{
angular_velocity::revolution_per_minute,
electric_current::milliampere,
@@ -14,15 +14,15 @@ use uom::si::{
pressure::bar,
};
use super::mitsubishi_cs80::MitsubishiCS80;
use super::mitsubishi_inverter_rs485::MitsubishiInverterController;
#[derive(Debug)]
pub struct ScrewSpeedController {
pub pid: ClampingTimeagnosticPidController,
pub target_pressure: Pressure,
pub target_rpm: AngularVelocity,
pub inverter: MitsubishiCS80,
pressure_sensor: AnalogInput,
pub inverter: MitsubishiInverterController,
pressure_sensor: AnalogInputGetter,
last_update: Instant,
uses_rpm: bool,
forward_rotation: bool,
@@ -37,10 +37,10 @@ pub struct ScrewSpeedController {
impl ScrewSpeedController {
pub fn new(
inverter: MitsubishiCS80,
inverter: MitsubishiInverterController,
target_pressure: Pressure,
target_rpm: AngularVelocity,
pressure_sensor: AnalogInput,
pressure_sensor: AnalogInputGetter,
) -> Self {
let now = Instant::now();
Self {
@@ -160,13 +160,11 @@ impl ScrewSpeedController {
}
}
pub fn get_wiring_error(&self) -> bool {
self.pressure_sensor.get_wiring_error()
}
pub fn get_sensor_current(&self) -> Result<ElectricCurrent, anyhow::Error> {
let phys: ethercat_hal::io::analog_input::physical::AnalogInputValue =
self.pressure_sensor.get_physical();
let phys: ethercat_hal::io::analog_input::physical::AnalogInputValue = self
.pressure_sensor
.get_physical()
.ok_or_else(|| anyhow::anyhow!("no value"))?;
match phys {
ethercat_hal::io::analog_input::physical::AnalogInputValue::Potential(_) => {
@@ -197,14 +195,9 @@ impl ScrewSpeedController {
return Pressure::new::<bar>(actual_pressure);
}
pub fn update(&mut self, now: Instant, is_extruding: bool) {
// TODO: move this logic elsewhere or make non async
block_on(self.inverter.act(now));
let wiring_error = self.get_wiring_error();
if wiring_error {
// emit in act
}
pub async fn update(&mut self, now: Instant, is_extruding: bool) {
self.pressure_sensor.act(now).await;
self.inverter.act(now).await;
let measured_pressure = self.get_pressure();
@@ -1,6 +1,11 @@
use super::Heating;
use control_core::controllers::pid::PidController;
use ethercat_hal::io::{digital_output::DigitalOutput, temperature_input::TemperatureInput};
use control_core::{
actors::{
Actor, digital_output_setter::DigitalOutputSetter,
temperature_input_getter::TemperatureInputGetter,
},
controllers::pid::PidController,
};
use std::time::{Duration, Instant};
use uom::si::{f64::ThermodynamicTemperature, thermodynamic_temperature::degree_celsius};
@@ -8,8 +13,8 @@ use uom::si::{f64::ThermodynamicTemperature, thermodynamic_temperature::degree_c
pub struct TemperatureController {
pub pid: PidController,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
temperature_sensor: TemperatureInputGetter,
relais: DigitalOutputSetter,
pub heating: Heating,
pub target_temp: ThermodynamicTemperature,
window_start: Instant,
@@ -34,8 +39,8 @@ impl TemperatureController {
kd: f64,
target_temp: ThermodynamicTemperature,
max_temperature: ThermodynamicTemperature,
temperature_sensor: TemperatureInput,
relais: DigitalOutput,
temperature_sensor: TemperatureInputGetter,
relais: DigitalOutputSetter,
heating: Heating,
pwm_duration: Duration,
heating_element_wattage: f64,
@@ -73,22 +78,25 @@ impl TemperatureController {
return self.temperature_pid_output * self.heating_element_wattage;
}
pub fn update(&mut self, now: Instant) -> () {
pub async fn update(&mut self, now: Instant) -> () {
self.temperature_sensor.act(now).await;
self.temperature_pid_output = 0.0;
let temperature = self.temperature_sensor.get_temperature();
let temperature_celsius = ThermodynamicTemperature::new::<degree_celsius>(
temperature.as_ref().unwrap_or(&0.0).to_owned(),
let temperature = ThermodynamicTemperature::new::<degree_celsius>(
self.temperature_sensor.get_temperature(),
);
self.heating.wiring_error = temperature.is_err();
self.heating.temperature = temperature_celsius;
self.heating.temperature = temperature;
self.heating.wiring_error = self.temperature_sensor.get_wiring_error();
if self.heating.temperature > self.max_temperature {
// disable the relais and return
self.relais.set(false);
self.heating.heating = false;
self.relais.act(now).await;
return;
} else {
self.relais.act(now).await;
}
if self.heating_allowed {
+3 -3
View File
@@ -1,12 +1,12 @@
use super::LaserMachine;
use control_core::machines::new::MachineAct;
use control_core::actors::Actor;
use std::{
future::Future,
pin::Pin,
time::{Duration, Instant},
};
/// Implements the `MachineAct` trait for the `LaserMachine`.
/// Implements the `Actor` trait for the `LaserMachine`.
///
/// # Parameters
/// - `_now_ts`: The current timestamp of type `Instant`.
@@ -21,7 +21,7 @@ use std::{
///
/// The method ensures that the diameter value is updated approximately 60 times per second.
///
impl MachineAct for LaserMachine {
impl Actor for LaserMachine {
fn act(&mut self, now: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
// The live values are updated approximately 60 times per second
+3 -4
View File
@@ -5,7 +5,7 @@ use control_core::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
cache_one_event,
},
},
};
@@ -30,7 +30,6 @@ impl LiveValuesEvent {
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub is_default_state: bool,
/// laser state
pub laser_state: LaserState,
}
@@ -79,11 +78,11 @@ impl CacheableEvents<LaserEvents> for LaserEvents {
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
let cache_one = cache_one_event();
match self {
LaserEvents::LiveValues(_) => cache_one_hour,
LaserEvents::State(_) => cache_first_and_last,
LaserEvents::State(_) => cache_one,
}
}
}
-5
View File
@@ -20,10 +20,6 @@ pub struct LaserMachine {
//laser target configuration
laser_target: LaserTarget,
/// Will be initialized as false and set to true by emit_state
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
}
impl Machine for LaserMachine {}
@@ -48,7 +44,6 @@ impl LaserMachine {
pub fn emit_state(&mut self) {
let state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
laser_state: LaserState {
higher_tolerance: self.laser_target.higher_tolerance.get::<millimeter>(),
lower_tolerance: self.laser_target.lower_tolerance.get::<millimeter>(),
+2 -3
View File
@@ -46,12 +46,11 @@ impl MachineNewTrait for LaserMachine {
namespace: LaserMachineNamespace::new(params.socket_queue_tx.clone()),
last_measurement_emit: Instant::now(),
laser_target,
emitted_default_state: false,
};
// Emit initial state
laser_machine.emit_state();
Ok(laser_machine)
}
}
+9 -9
View File
@@ -1,13 +1,12 @@
use control_core::machines::new::MachineAct;
use super::MockMachine;
use control_core::actors::Actor;
use std::{
future::Future,
pin::Pin,
time::{Duration, Instant},
};
/// Implements the `MachineAct` trait for the `MockMachine`.
/// Implements the `Actor` trait for the `MockMachine`.
///
/// # Parameters
/// - `_now_ts`: The current timestamp of type `Instant`.
@@ -23,21 +22,22 @@ use std::{
///
/// The method ensures that the sine wave value is updated approximately 60 times per second (16ms intervals) when running.
///
impl MachineAct for MockMachine {
impl Actor for MockMachine {
fn act(&mut self, _now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let now = Instant::now();
// Emit initial state if this is the first call
if self.last_emitted_state.is_none() {
self.emit_state();
if self.last_emitted_frequency.is_none() || self.last_emitted_mode.is_none() {
self.emit_sine_wave_state();
self.emit_mode_state();
}
// Only emit live values if machine is in Running mode
// The live values are updated approximately 60 times per second
// Only emit sine wave if machine is in Running mode
// The sine wave value is updated approximately 60 times per second
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 60.0)
{
self.emit_live_values();
self.emit_sine_wave();
self.last_measurement_emit = now;
}
})
+32 -35
View File
@@ -5,7 +5,7 @@ use control_core::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
cache_one_event,
},
},
};
@@ -22,48 +22,43 @@ pub enum Mode {
Running,
}
#[derive(Serialize, Debug, Clone, Default)]
pub struct LiveValuesEvent {
/// sine wave amplitude value
pub sine_wave_amplitude: f64,
#[derive(Serialize, Debug, Clone)]
pub struct SineWaveEvent {
pub amplitude: f64,
}
impl LiveValuesEvent {
impl SineWaveEvent {
pub fn build(&self) -> Event<Self> {
Event::new("LiveValuesEvent", self.clone())
Event::new("SineWaveEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct StateEvent {
pub is_default_state: bool,
/// sine wave state
pub sine_wave_state: SineWaveState,
/// mode state
pub mode_state: ModeState,
}
impl StateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("StateEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct SineWaveState {
/// frequency in millihertz
#[derive(Serialize, Debug, Clone)]
pub struct SineWaveStateEvent {
pub frequency: f64,
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct ModeState {
/// current mode
impl SineWaveStateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("MockStateEvent", self.clone())
}
}
#[derive(Serialize, Debug, Clone)]
pub struct ModeStateEvent {
pub mode: Mode,
}
impl ModeStateEvent {
pub fn build(&self) -> Event<Self> {
Event::new("ModeStateEvent", self.clone())
}
}
pub enum MockEvents {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
SineWave(Event<SineWaveEvent>),
SineWaveState(Event<SineWaveStateEvent>),
ModeState(Event<ModeStateEvent>),
}
#[derive(Debug)]
@@ -82,18 +77,20 @@ impl MockMachineNamespace {
impl CacheableEvents<MockEvents> for MockEvents {
fn event_value(&self) -> GenericEvent {
match self {
MockEvents::LiveValues(event) => event.into(),
MockEvents::State(event) => event.into(),
MockEvents::SineWave(event) => event.into(),
MockEvents::SineWaveState(event) => event.into(),
MockEvents::ModeState(event) => event.into(),
}
}
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
let cache_one = cache_one_event();
match self {
MockEvents::LiveValues(_) => cache_one_hour,
MockEvents::State(_) => cache_first_and_last,
MockEvents::SineWave(_) => cache_one_hour,
MockEvents::SineWaveState(_) => cache_one,
MockEvents::ModeState(_) => cache_one,
}
}
}
+41 -36
View File
@@ -1,9 +1,8 @@
use api::{
LiveValuesEvent, MockEvents, MockMachineNamespace, Mode, ModeState, SineWaveState, StateEvent,
MockEvents, MockMachineNamespace, Mode, ModeStateEvent, SineWaveEvent, SineWaveStateEvent,
};
use control_core::{machines::Machine, socketio::namespace::NamespaceCacheingLogic};
use std::time::Instant;
use tracing::info;
use uom::si::{
f64::Frequency,
frequency::{hertz, millihertz},
@@ -25,62 +24,68 @@ pub struct MockMachine {
mode: Mode,
// State tracking to only emit when values change
last_emitted_state: Option<StateEvent>,
/// Will be initialized as false and set to true by emit_state
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
last_emitted_frequency: Option<f64>,
last_emitted_mode: Option<Mode>,
}
impl Machine for MockMachine {}
impl MockMachine {
/// Emit live values data event with the current sine wave amplitude
pub fn emit_live_values(&mut self) {
/// Emit a sine wave data event with the current time and frequency
pub fn emit_sine_wave(&mut self) {
let now = Instant::now();
let elapsed = now.duration_since(self.t_0).as_secs_f64();
let freq_hz = self.frequency.get::<hertz>();
// Calculate sine wave: sin(2π * frequency * time)
let amplitude = match self.mode {
let y = match self.mode {
Mode::Standby => 0.0,
Mode::Running => (2.0 * std::f64::consts::PI * freq_hz * elapsed).sin(),
};
let live_values = LiveValuesEvent {
sine_wave_amplitude: amplitude,
};
let sine_wave_event = SineWaveEvent { amplitude: y };
self.namespace
.emit(MockEvents::LiveValues(live_values.build()));
.emit(MockEvents::SineWave(sine_wave_event.build()));
}
/// Emit the current state of the mock machine only if values have changed
pub fn emit_state(&mut self) {
info!(
"Emitting state for MockMachine, is default state: {}",
!self.emitted_default_state
);
let current_state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
sine_wave_state: SineWaveState {
frequency: self.frequency.get::<millihertz>(),
},
mode_state: ModeState {
mode: self.mode.clone(),
},
};
pub fn emit_sine_wave_state(&mut self) {
let current_frequency_mhz = self.frequency.get::<millihertz>();
// Only emit if values have changed or this is the first emission
let should_emit = self.last_emitted_state.as_ref() != Some(&current_state);
let should_emit = self.last_emitted_frequency != Some(current_frequency_mhz);
if should_emit {
self.namespace
.emit(MockEvents::State(current_state.build()));
let mock_state_event = SineWaveStateEvent {
frequency: current_frequency_mhz,
};
// Update last emitted state
self.last_emitted_state = Some(current_state);
self.namespace
.emit(MockEvents::SineWaveState(mock_state_event.build()));
// Update last emitted values
self.last_emitted_frequency = Some(current_frequency_mhz);
}
}
/// Emit the current mode state only if values have changed
pub fn emit_mode_state(&mut self) {
let current_mode = self.mode.clone();
// Only emit if values have changed or this is the first emission
let should_emit = self.last_emitted_mode != Some(current_mode.clone());
if should_emit {
let mode_state_event = ModeStateEvent {
mode: current_mode.clone(),
};
self.namespace
.emit(MockEvents::ModeState(mode_state_event.build()));
// Update last emitted values
self.last_emitted_mode = Some(current_mode);
}
}
@@ -88,13 +93,13 @@ impl MockMachine {
pub fn set_frequency(&mut self, frequency_mhz: f64) {
self.frequency = Frequency::new::<millihertz>(frequency_mhz);
// Emit state change immediately
self.emit_state();
self.emit_sine_wave_state();
}
/// Set the mode of the mock machine
pub fn set_mode(&mut self, mode: Mode) {
self.mode = mode;
// Emit state change immediately
self.emit_state();
self.emit_mode_state();
}
}
+5 -9
View File
@@ -38,19 +38,15 @@ impl MachineNewTrait for MockMachine {
}
let now = Instant::now();
let mut mock_machine = Self {
Ok(Self {
namespace: MockMachineNamespace::new(params.socket_queue_tx.clone()),
last_measurement_emit: now,
t_0: now, // Initialize start time to current time
frequency: Frequency::new::<hertz>(0.5), // Default frequency of 500 mHz
mode: Mode::Standby, // Start in standby mode
last_emitted_state: None, // No previous state emissions
emitted_default_state: false,
};
mock_machine.emit_state();
Ok(mock_machine)
last_emitted_frequency: None, // No previous emissions
last_emitted_mode: None, // No previous emissions
})
}
}
+9 -5
View File
@@ -1,14 +1,21 @@
use std::time::{Duration, Instant};
use super::Winder2;
use control_core::machines::new::MachineAct;
use control_core::actors::Actor;
impl MachineAct for Winder2 {
impl Actor for Winder2 {
fn act(
&mut self,
now: Instant,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + '_>> {
Box::pin(async move {
self.traverse.act(now).await;
self.traverse_end_stop.act(now).await;
self.puller.act(now).await;
self.spool.act(now).await;
self.tension_arm.analog_input_getter.act(now).await;
self.laser.act(now).await;
// sync the spool speed
self.sync_spool_speed(now);
@@ -18,9 +25,6 @@ impl MachineAct for Winder2 {
// sync the traverse speed
self.sync_traverse_speed();
// automatically stops or pulls after N Meters if enabled
self.stop_or_pull_spool(now);
// check if traverse state changed
if self.traverse_controller.did_change_state() {
self.emit_state();
+5 -35
View File
@@ -1,3 +1,5 @@
use std::{sync::Arc, time::Duration};
use super::{Winder2, Winder2Mode, puller_speed_controller::PullerRegulationMode};
use control_core::{
machines::api::MachineApi,
@@ -5,7 +7,7 @@ use control_core::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
cache_one_event,
},
},
};
@@ -13,10 +15,6 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::channel::Sender;
use socketioxide::extract::SocketRef;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use tracing::instrument;
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -85,11 +83,6 @@ enum Mutation {
SetSpoolAdaptiveAccelerationFactor(f64),
SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(f64),
// Spool Auto Stop/Pull
SetSpoolAutomaticRequiredMeters(f64),
SetSpoolAutomaticAction(SpoolAutomaticActionMode),
ResetSpoolProgress,
// Tension Arm
ZeroTensionArmAngle,
@@ -109,8 +102,6 @@ pub struct LiveValuesEvent {
pub spool_diameter: f64,
/// tension arm angle in degrees
pub tension_arm_angle: f64,
// spool progress in meters (pulled distance of filament)
pub spool_progress: f64,
}
impl LiveValuesEvent {
@@ -121,13 +112,10 @@ impl LiveValuesEvent {
#[derive(Serialize, Debug, Clone)]
pub struct StateEvent {
pub is_default_state: bool,
/// traverse state
pub traverse_state: TraverseState,
/// puller state
pub puller_state: PullerState,
/// spool automatic action state and progress
pub spool_automatic_action_state: SpoolAutomaticActionState,
/// mode state
pub mode_state: ModeState,
/// tension arm state
@@ -188,19 +176,6 @@ pub struct PullerState {
pub forward: bool,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum SpoolAutomaticActionMode {
NoAction,
Pull,
Hold,
}
#[derive(Serialize, Debug, Clone)]
pub struct SpoolAutomaticActionState {
pub spool_required_meters: f64,
pub spool_automatic_action_mode: SpoolAutomaticActionMode,
}
#[derive(Serialize, Debug, Clone)]
pub struct ModeState {
/// mode
@@ -272,11 +247,11 @@ impl CacheableEvents<Winder2Events> for Winder2Events {
fn event_cache_fn(&self) -> CacheFn {
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
let cache_first_and_last = cache_first_and_last_event();
let cache_one = cache_one_event();
match self {
Winder2Events::LiveValues(_) => cache_one_hour,
Winder2Events::State(_) => cache_first_and_last,
Winder2Events::State(_) => cache_one,
}
}
}
@@ -316,11 +291,6 @@ impl MachineApi for Winder2 {
Mutation::SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(value) => {
self.spool_set_adaptive_deacceleration_urgency_multiplier(value)
}
Mutation::SetSpoolAutomaticRequiredMeters(meters) => {
self.set_spool_automatic_required_meters(meters)
}
Mutation::SetSpoolAutomaticAction(mode) => self.set_spool_automatic_mode(mode),
Mutation::ResetSpoolProgress => self.stop_or_pull_spool_reset(Instant::now()),
Mutation::ZeroTensionArmAngle => self.tension_arm_zero(),
}
Ok(())
+25 -116
View File
@@ -13,53 +13,42 @@ pub mod traverse_controller;
use std::{fmt::Debug, time::Instant};
use api::{
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode, SpoolAutomaticActionState,
SpoolSpeedControllerState, StateEvent, TensionArmState, TraverseState, Winder2Events,
Winder2Namespace,
LiveValuesEvent, ModeState, PullerState, SpoolSpeedControllerState, StateEvent,
TensionArmState, TraverseState, Winder2Events, Winder2Namespace,
};
use control_core::{
converters::angular_step_converter::AngularStepConverter, machines::Machine,
socketio::namespace::NamespaceCacheingLogic, uom_extensions::velocity::meter_per_minute,
};
use ethercat_hal::io::{
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1,
actors::{
digital_input_getter::DigitalInputGetter, digital_output_setter::DigitalOutputSetter,
stepper_driver_el70x1::StepperDriverEL70x1,
},
converters::angular_step_converter::AngularStepConverter,
machines::Machine,
socketio::namespace::NamespaceCacheingLogic,
uom_extensions::velocity::meter_per_minute,
};
use puller_speed_controller::{PullerRegulationMode, PullerSpeedController};
use spool_speed_controller::SpoolSpeedController;
use tension_arm::TensionArm;
use traverse_controller::TraverseController;
use uom::{
ConstZero,
si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
length::{meter, millimeter},
velocity::meter_per_second,
},
use uom::si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
length::millimeter,
};
#[derive(Debug)]
pub struct SpoolAutomaticAction {
pub progress: Length,
progress_last_check: Instant,
pub target_length: Length,
pub mode: SpoolAutomaticActionMode,
}
#[derive(Debug)]
pub struct Winder2 {
// drivers
pub traverse: StepperVelocityEL70x1,
pub puller: StepperVelocityEL70x1,
pub spool: StepperVelocityEL70x1,
pub traverse: StepperDriverEL70x1,
pub puller: StepperDriverEL70x1,
pub spool: StepperDriverEL70x1,
pub tension_arm: TensionArm,
pub laser: DigitalOutput,
pub laser: DigitalOutputSetter,
// controllers
pub traverse_controller: TraverseController,
pub traverse_end_stop: DigitalInput,
pub traverse_end_stop: DigitalInputGetter,
// socketio
namespace: Winder2Namespace,
@@ -75,15 +64,8 @@ pub struct Winder2 {
pub spool_speed_controller: SpoolSpeedController,
pub spool_step_converter: AngularStepConverter,
// spool automatic action state
pub spool_automatic_action: SpoolAutomaticAction,
// control circuit puller
// control cirguit puller
pub puller_speed_controller: PullerSpeedController,
/// Will be initialized as false and set to true by emit_state
/// This way we can signal to the client that the first state emission is a default state
emitted_default_state: bool,
}
impl Machine for Winder2 {}
@@ -94,7 +76,6 @@ impl Winder2 {
self.laser.set(value);
self.emit_state();
}
/// Validates that traverse limits maintain proper constraints:
/// - Inner limit must be smaller than outer limit
/// - At least 0.9mm difference between inner and outer limits
@@ -203,7 +184,6 @@ impl Winder2 {
.get::<millimeter>()
* 2.0,
tension_arm_angle: angle_deg,
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
};
let event = live_values.build();
@@ -212,7 +192,6 @@ impl Winder2 {
pub fn emit_state(&mut self) {
let state = StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
traverse_state: TraverseState {
limit_inner: self
.traverse_controller
@@ -285,10 +264,6 @@ impl Winder2 {
.spool_speed_controller
.get_adaptive_deacceleration_urgency_multiplier(),
},
spool_automatic_action_state: SpoolAutomaticActionState {
spool_required_meters: self.spool_automatic_action.target_length.get::<meter>(),
spool_automatic_action_mode: self.spool_automatic_action.mode.clone(),
},
};
let event = state.build();
@@ -298,9 +273,9 @@ impl Winder2 {
pub fn sync_traverse_speed(&mut self) {
self.traverse_controller.update_speed(
&mut self.traverse,
&self.traverse_end_stop,
&mut self.traverse_end_stop,
self.spool_speed_controller.get_speed(),
)
);
}
/// Can wind capability check
@@ -348,8 +323,6 @@ impl Winder2 {
}
}
impl Winder2 {}
/// Implement Mode
impl Winder2 {
fn set_mode(&mut self, mode: &Winder2Mode) {
@@ -561,71 +534,7 @@ impl Winder2 {
let steps_per_second = self
.spool_step_converter
.angular_velocity_to_steps(angular_velocity);
let _ = self.spool.set_speed(steps_per_second);
}
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
self.spool_automatic_action.target_length = Length::new::<meter>(meters);
self.emit_state();
}
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
self.spool_automatic_action.mode = mode;
self.emit_state();
}
pub fn stop_or_pull_spool(&mut self, now: Instant) {
if let SpoolAutomaticActionMode::NoAction = self.spool_automatic_action.mode {
self.calculate_spool_auto_progress_(now);
return;
}
match self.mode {
Winder2Mode::Pull => self.calculate_spool_auto_progress_(now),
Winder2Mode::Wind => self.calculate_spool_auto_progress_(now),
_ => {
self.spool_automatic_action.progress_last_check = now;
return;
}
}
if self.spool_automatic_action.progress >= self.spool_automatic_action.target_length {
match self.spool_automatic_action.mode {
SpoolAutomaticActionMode::NoAction => (),
SpoolAutomaticActionMode::Pull => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&Winder2Mode::Pull);
}
SpoolAutomaticActionMode::Hold => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&Winder2Mode::Hold);
}
}
}
}
pub fn stop_or_pull_spool_reset(&mut self, now: Instant) {
self.spool_automatic_action.progress = Length::ZERO;
self.spool_automatic_action.progress_last_check = now;
}
pub fn calculate_spool_auto_progress_(&mut self, now: Instant) {
// Calculate time elapsed since last progress check (in minutes)
let dt = now
.duration_since(self.spool_automatic_action.progress_last_check)
.as_secs_f64();
// Calculate distance pulled during this time interval
let meters_pulled_this_interval = Length::new::<meter>(
self.puller_speed_controller
.last_speed
.get::<meter_per_second>()
* dt,
);
// Update total meters pulled
self.spool_automatic_action.progress += meters_pulled_this_interval;
self.spool_automatic_action.progress_last_check = now;
self.spool.set_speed(steps_per_second);
}
}
@@ -638,7 +547,7 @@ impl Winder2 {
.puller_speed_controller
.converter
.angular_velocity_to_steps(angular_velocity);
let _ = self.puller.set_speed(steps_per_second);
self.puller.set_speed(steps_per_second);
}
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
+26 -21
View File
@@ -7,6 +7,10 @@ use crate::machines::winder2::puller_speed_controller::PullerSpeedController;
use crate::machines::winder2::spool_speed_controller::SpoolSpeedController;
use crate::machines::winder2::traverse_controller::TraverseController;
use anyhow::Error;
use control_core::actors::analog_input_getter::AnalogInputGetter;
use control_core::actors::digital_input_getter::DigitalInputGetter;
use control_core::actors::digital_output_setter::DigitalOutputSetter;
use control_core::actors::stepper_driver_el70x1::StepperDriverEL70x1;
use control_core::converters::angular_step_converter::AngularStepConverter;
use control_core::converters::linear_step_converter::LinearStepConverter;
use control_core::machines::identification::DeviceHardwareIdentification;
@@ -39,9 +43,8 @@ use ethercat_hal::io::digital_output::DigitalOutput;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use ethercat_hal::shared_config;
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
use uom::ConstZero;
use uom::si::f64::{Length, Velocity};
use uom::si::length::{centimeter, meter, millimeter};
use uom::si::length::{centimeter, millimeter};
impl MachineNewTrait for Winder2 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
@@ -151,7 +154,7 @@ impl MachineNewTrait for Winder2 {
// Role 2
// 1x Stepper Spool
// EL7041-0052
let (el7041, _el7041_config) = {
let (el7041, el7041_config) = {
let device_identification =
get_device_identification_by_role(params.device_group, 2)?;
let device_hardware_identification_ethercat =
@@ -209,7 +212,7 @@ impl MachineNewTrait for Winder2 {
// Role 3
// 1x Stepper Traverse
// EL7031
let (el7031, _el7031_config) = {
let (el7031, el7031_config) = {
let device_identification =
get_device_identification_by_role(params.device_group, 3)?;
let device_hardware_identification_ethercat =
@@ -272,7 +275,7 @@ impl MachineNewTrait for Winder2 {
// Role 4
// 1x Stepper Puller
// EL7031
let (el7031_0030, _el7031_0030_config) = {
let (el7031_0030, el7031_0030_config) = {
let device_identification =
get_device_identification_by_role(params.device_group, 4)?;
let device_hardware_identification_ethercat =
@@ -334,18 +337,27 @@ impl MachineNewTrait for Winder2 {
let mode = Winder2Mode::Standby;
let mut new = Self {
traverse: StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
traverse_end_stop: DigitalInput::new(el7031, EL7031DigitalInputPort::DI1),
puller: StepperVelocityEL70x1::new(
el7031_0030.clone(),
EL7031_0030StepperPort::STM1,
traverse: StepperDriverEL70x1::new(
StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
&el7031_config.stm_features.speed_range,
),
spool: StepperVelocityEL70x1::new(el7041, EL7041_0052Port::STM1),
tension_arm: TensionArm::new(AnalogInput::new(
traverse_end_stop: DigitalInputGetter::new(DigitalInput::new(
el7031,
EL7031DigitalInputPort::DI1,
)),
puller: StepperDriverEL70x1::new(
StepperVelocityEL70x1::new(el7031_0030.clone(), EL7031_0030StepperPort::STM1),
&el7031_0030_config.stm_features.speed_range,
),
spool: StepperDriverEL70x1::new(
StepperVelocityEL70x1::new(el7041, EL7041_0052Port::STM1),
&el7041_config.stm_features.speed_range,
),
tension_arm: TensionArm::new(AnalogInputGetter::new(AnalogInput::new(
el7031_0030,
EL7031_0030AnalogInputPort::AI1,
)),
laser: DigitalOutput::new(el2002, EL2002Port::DO1),
))),
laser: DigitalOutputSetter::new(DigitalOutput::new(el2002, EL2002Port::DO1)),
namespace: Winder2Namespace::new(params.socket_queue_tx.clone()),
mode: mode.clone(),
spool_step_converter: AngularStepConverter::new(200),
@@ -367,13 +379,6 @@ impl MachineNewTrait for Winder2 {
Length::new::<millimeter>(92.0), // Default outer limit
64, // Microsteps
),
emitted_default_state: false,
spool_automatic_action: super::SpoolAutomaticAction {
progress: Length::ZERO,
progress_last_check: Instant::now(),
target_length: Length::new::<meter>(250.0),
mode: super::api::SpoolAutomaticActionMode::NoAction,
},
};
// initalize events
+45 -23
View File
@@ -1,18 +1,23 @@
use ethercat_hal::io::analog_input::{AnalogInput, physical::AnalogInputValue};
use uom::si::{angle::revolution, electric_potential::volt, f64::Angle};
use control_core::actors::analog_input_getter::AnalogInputGetter;
use ethercat_hal::io::analog_input::physical::AnalogInputValue;
use uom::si::{
angle::revolution,
electric_potential::{ElectricPotential, volt},
f64::Angle,
};
#[derive(Debug)]
pub struct TensionArm {
pub analog_input: AnalogInput,
pub analog_input_getter: AnalogInputGetter,
pub zero: Angle,
/// was zeroed at least once
pub zeroed: bool,
}
impl TensionArm {
pub fn new(analog_input: AnalogInput) -> Self {
pub fn new(analog_input_getter: AnalogInputGetter) -> Self {
Self {
analog_input,
analog_input_getter,
zero: Angle::new::<revolution>(0.0),
zeroed: false,
}
@@ -26,7 +31,12 @@ impl TensionArm {
fn get_volts(&self) -> f64 {
// get the normalized value from the analog input
let value = self.analog_input.get_physical();
let value = self
.analog_input_getter
.get_physical()
.unwrap_or(AnalogInputValue::Potential(ElectricPotential::new::<volt>(
0.0,
)));
let volts = match value {
AnalogInputValue::Potential(v) => v.get::<volt>(),
@@ -68,17 +78,17 @@ impl TensionArm {
mod tests {
use super::*;
use approx::assert_relative_eq;
use control_core::actors::Actor;
use core::f64;
use ethercat_hal::io::{
analog_input::{AnalogInputInput, physical::AnalogInputRange},
analog_input_dummy::AnalogInputDummy,
};
use std::i16;
use uom::si::f64::ElectricPotential;
use std::{i16, time::Instant};
#[test]
fn volts_to_angle() {
let tension_arm = TensionArm::new(
let tension_arm = TensionArm::new(AnalogInputGetter::new(
AnalogInputDummy::new(AnalogInputRange::Potential {
min: ElectricPotential::new::<volt>(0.0),
max: ElectricPotential::new::<volt>(10.0),
@@ -86,7 +96,7 @@ mod tests {
max_raw: i16::MAX,
})
.analog_input(),
);
));
// 0V = 0deg
assert_relative_eq!(
@@ -118,8 +128,8 @@ mod tests {
min_raw: 0,
max_raw: i16::MAX,
});
let analog_input = analog_input_dummy.analog_input();
let tension_arm = TensionArm::new(analog_input);
let analog_input_getter = AnalogInputGetter::new(analog_input_dummy.analog_input());
let mut tension_arm = TensionArm::new(analog_input_getter);
// 0.0 normalized = 0V
let volts = tension_arm.get_volts();
@@ -127,13 +137,14 @@ mod tests {
// 0.5 normalized = 5V
analog_input_dummy.set_input(AnalogInputInput {
// 5V of 10V in positive range
normalized: (5.0 / 10.0),
wiring_error: false,
normalized: (5.0 / 10.0), // 5V of 10V in positive range
});
let physical = tension_arm.analog_input.get_physical();
smol::block_on(async {
tension_arm.analog_input_getter.act(Instant::now()).await;
});
let physical = tension_arm.analog_input_getter.get_physical();
match physical {
AnalogInputValue::Potential(v) => {
Some(AnalogInputValue::Potential(v)) => {
assert_relative_eq!(v.get::<volt>(), 5.0, epsilon = f64::EPSILON);
}
_ => panic!("Expected a potential value"),
@@ -149,7 +160,8 @@ mod tests {
max_raw: i16::MAX,
});
let analog_input = analog_input_dummy.analog_input();
let tension_arm = TensionArm::new(analog_input);
let analog_input_getter = AnalogInputGetter::new(analog_input);
let mut tension_arm = TensionArm::new(analog_input_getter);
// 0V = 0.25 = 0 revolution
let angle = tension_arm.get_angle();
@@ -158,7 +170,9 @@ mod tests {
// 1.25V = 0.25 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (1.25 / 10.0),
wiring_error: false,
});
smol::block_on(async {
tension_arm.analog_input_getter.act(Instant::now()).await;
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.25, epsilon = f64::EPSILON);
@@ -166,7 +180,9 @@ mod tests {
// 2.5V = 0.5 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (2.5 / 10.0),
wiring_error: false,
});
smol::block_on(async {
tension_arm.analog_input_getter.act(Instant::now()).await;
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.5, epsilon = f64::EPSILON);
@@ -174,7 +190,9 @@ mod tests {
// 3.75V = 0.75 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (3.75 / 10.0),
wiring_error: false,
});
smol::block_on(async {
tension_arm.analog_input_getter.act(Instant::now()).await;
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.75, epsilon = f64::EPSILON);
@@ -182,7 +200,9 @@ mod tests {
// 5V = 1 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (5.0 / 10.0),
wiring_error: false,
});
smol::block_on(async {
tension_arm.analog_input_getter.act(Instant::now()).await;
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.0, epsilon = f64::EPSILON);
@@ -190,7 +210,9 @@ mod tests {
// 6.25V = 0.25 revolution
analog_input_dummy.set_input(AnalogInputInput {
normalized: (6.25 / 10.0),
wiring_error: false,
});
smol::block_on(async {
tension_arm.analog_input_getter.act(Instant::now()).await;
});
let angle = tension_arm.raw_angle();
assert_relative_eq!(angle.get::<revolution>(), 0.25, epsilon = f64::EPSILON);
@@ -1,8 +1,10 @@
use std::time::Instant;
use control_core::converters::linear_step_converter::LinearStepConverter;
use ethercat_hal::io::{
digital_input::DigitalInput, stepper_velocity_el70x1::StepperVelocityEL70x1,
use control_core::{
actors::{
digital_input_getter::DigitalInputGetter, stepper_driver_el70x1::StepperDriverEL70x1,
},
converters::linear_step_converter::LinearStepConverter,
};
use uom::{
ConstZero,
@@ -260,7 +262,7 @@ impl TraverseController {
}
/// Gets the current traverse position as a [`Length`].
pub fn sync_position(&mut self, traverse: &StepperVelocityEL70x1) {
pub fn sync_position(&mut self, traverse: &StepperDriverEL70x1) {
let steps = traverse.get_position();
self.position = self.microstep_converter.steps_to_distance(steps as f64);
}
@@ -283,8 +285,8 @@ impl TraverseController {
/// Positive speed moved out, negative speed moves in.
fn get_speed(
&mut self,
traverse: &mut StepperVelocityEL70x1,
traverse_end_stop: &DigitalInput,
traverse: &mut StepperDriverEL70x1,
traverse_end_stop: &DigitalInputGetter,
spool_speed: AngularVelocity,
) -> Velocity {
// Don't move if not enabled or in a state that doesn't result in movement
@@ -318,7 +320,7 @@ impl TraverseController {
State::Homing(homing_state) => match homing_state {
HomingState::Initialize => {
// If endstop is triggered, escape the endstop
if traverse_end_stop.get_value().unwrap_or(false) == true {
if traverse_end_stop.value() == true {
self.state = State::Homing(HomingState::EscapeEndstop);
} else {
// If endstop is not triggered, move to the endstop
@@ -327,20 +329,20 @@ impl TraverseController {
}
HomingState::EscapeEndstop => {
// Move out until endstop is not triggered anymore
if traverse_end_stop.get_value().unwrap_or(false) == false {
if traverse_end_stop.value() == false {
self.state = State::Homing(HomingState::FindEnstopFineDistancing);
}
}
HomingState::FindEnstopFineDistancing => {
// Move out until endstop is not triggered anymore
if traverse_end_stop.get_value().unwrap_or(false) == false {
if traverse_end_stop.value() == false {
// Find endstop fine
self.state = State::Homing(HomingState::FindEndtopFine);
}
}
HomingState::FindEndtopFine => {
// If endstop is reached change to idle
if traverse_end_stop.get_value().unwrap_or(false) == true {
if traverse_end_stop.value() == true {
// Set poition of traverse to 0
traverse.set_position(0);
// Put Into Idle
@@ -349,7 +351,7 @@ impl TraverseController {
}
HomingState::FindEndstopCoarse => {
// Move to endstop
if traverse_end_stop.get_value().unwrap_or(false) == true {
if traverse_end_stop.value() == true {
// Move awaiy from endstop
self.state = State::Homing(HomingState::FindEnstopFineDistancing);
}
@@ -494,14 +496,13 @@ impl TraverseController {
pub fn update_speed(
&mut self,
traverse: &mut StepperVelocityEL70x1,
traverse_end_stop: &DigitalInput,
traverse: &mut StepperDriverEL70x1,
traverse_end_stop: &DigitalInputGetter,
spool_speed: AngularVelocity,
) {
let speed = self.get_speed(traverse, traverse_end_stop, spool_speed);
let steps_per_second = self.fullstep_converter.velocity_to_steps(speed);
// ignore if we can't set speed
let _ = traverse.set_speed(steps_per_second);
traverse.set_speed(steps_per_second);
}
}
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more