Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5c5dfeccb |
No files matched your search
+3
-16
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, ¶m);
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
```
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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!(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¤t_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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in new issue
Block a user