Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff13baf257 | ||
|
|
8f5df07c19 | ||
|
|
299b38515a | ||
|
|
2b073aeb76 | ||
|
|
7a6f92f8a4 | ||
|
|
2e283ea95f | ||
|
|
829bb5ea1c | ||
|
|
0be18aaed0 | ||
|
|
ffe0f09061 | ||
|
|
730d61a18d | ||
|
|
fc07525976 | ||
|
|
c48953aa6b | ||
|
|
0c948b436d | ||
|
|
1b3f92dd1c | ||
|
|
c29c549a1c | ||
|
|
17622cacde | ||
|
|
0b423454ec | ||
|
|
81ec98f670 | ||
|
|
1aed425544 | ||
|
|
da080ace68 | ||
|
|
ded8dd2d2d | ||
|
|
edd83ba559 | ||
|
|
7d1d8559a5 | ||
|
|
f0bb9bfed1 | ||
|
|
c6ea057736 | ||
|
|
90a00198a7 | ||
|
|
f8241547fe | ||
|
|
1cb5d51c13 | ||
|
|
c42cdf8db1 | ||
|
|
69014ef8c6 | ||
|
|
5faab29a2b | ||
|
|
9a8b16ca69 | ||
|
|
53f3c66214 | ||
|
|
432bf0d6ba | ||
|
|
f3990f35f1 | ||
|
|
6f17412363 | ||
|
|
68907b9750 | ||
|
|
e7007e1980 | ||
|
|
3bcd96d73c | ||
|
|
8a77c62a5e | ||
|
|
93add82ab5 | ||
|
|
690b1f7ab3 | ||
|
|
3c07b94e3a | ||
|
|
3b63b55859 | ||
|
|
f87982c465 | ||
|
|
b03cedb55c | ||
|
|
f5934efa6c | ||
|
|
186619cd48 | ||
|
|
583d4c1a4d | ||
|
|
63bee54948 | ||
|
|
1523d64ed7 |
No files matched your search
+30
-2
@@ -1,3 +1,31 @@
|
||||
# `2.9.0`
|
||||
_06.10.2025_
|
||||
|
||||
## Winder V2
|
||||
- [#778](https://github.com/qitechgmbh/control/pull/778) Increased Winder Traverse Step size limit
|
||||
|
||||
## General
|
||||
- [#725](https://github.com/qitechgmbh/control/pull/725) Improved troubleshooting and update page
|
||||
- [#625](https://github.com/qitechgmbh/control/pull/625) Improved stability of Machine-Connections
|
||||
|
||||
|
||||
# `2.8.0`
|
||||
_30.09.2025_
|
||||
|
||||
|
||||
## Laser V1
|
||||
- [#741](https://github.com/qitechgmbh/control/pull/741) Added roundness measurement to two-axis Laser
|
||||
|
||||
|
||||
## Winder V2
|
||||
- [#745](https://github.com/qitechgmbh/control/pull/745), [#756](https://github.com/qitechgmbh/control/pull/756) improved Winder Frontend
|
||||
|
||||
## General
|
||||
- [#447](https://github.com/qitechgmbh/control/pull/447) added AquaPath Machine
|
||||
|
||||
|
||||
|
||||
|
||||
# `2.7.0`
|
||||
|
||||
_17.09.2025_
|
||||
@@ -10,9 +38,9 @@ _17.09.2025_
|
||||
|
||||
- [#727](https://github.com/qitechgmbh/control/pull/727) Removed estimated diameter
|
||||
|
||||
## Laser V1/V2
|
||||
## Laser V1
|
||||
|
||||
- [#731](https://github.com/qitechgmbh/control/pull/731) Added LaserV2 with two-axis measurements
|
||||
- [#731](https://github.com/qitechgmbh/control/pull/731) Added Laser with two-axis measurements
|
||||
|
||||
## General
|
||||
|
||||
|
||||
Generated
+31
-21
@@ -99,9 +99,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.99"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -284,9 +284,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
|
||||
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
@@ -304,8 +304,7 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
@@ -319,9 +318,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.2"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
|
||||
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -330,7 +329,6 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -955,11 +953,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7"
|
||||
checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
@@ -1020,6 +1019,7 @@ dependencies = [
|
||||
"futures",
|
||||
"rand",
|
||||
"smol",
|
||||
"tracing",
|
||||
"uom",
|
||||
]
|
||||
|
||||
@@ -1728,9 +1728,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
version = "0.2.176"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -2380,9 +2380,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.2"
|
||||
version = "1.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
||||
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2392,9 +2392,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2540,18 +2540,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -64,12 +64,12 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
|
||||
| Machine Type | Version | Release Date | Description | Change to Previous Version | Vendor ID | Machine ID | Implemented | Docs |
|
||||
| ------------ | ------- | ------------ | --------------------------- | ------------------------------------------------------ | -------------------------- | ---------- | ----------- | ------------------------------- |
|
||||
| Winder | V1 | ??? | Winding Filaments & Similar | - | 1 (Qitech Industries GmbH) | 1 | Reserved | - |
|
||||
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | In Progress | [](./docs/machines/winder-1.md) |
|
||||
| Winder | V2 | 2025 | Winding Filaments & Similar | Reengineered Traverse | 1 (Qitech Industries GmbH) | 2 | Yes | [](./docs/machines/winder-1.md) |
|
||||
| Extruder | V1 | ??? | Single Screw Extruder | - | 1 (Qitech Industries GmbH) | 3 | Reserved | - |
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Not Yet | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | Not Yet | |
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Not Yet | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Not Yet | - |
|
||||
| Extruder | V2 | 2025 | Single Screw Extruder | PT100 Thermometers, Optional Additional Heating Zone 4 | 1 (Qitech Industries GmbH) | 4 | Yes | |
|
||||
| Waterway | V1 | 2025 | Filament Water Cooling | - | 1 (Qitech Industries GmbH) | 5 | In Progress | |
|
||||
| Laser | V1 | ??? | Diameter Measuring Laser | - | 1 (Qitech Industries GmbH) | 6 | Yes | |
|
||||
| Mock | - | ??? | Mock Machine for Testing | - | 1 (Qitech Industries GmbH) | 7 | Yes | - |
|
||||
|
||||
# More Docs
|
||||
|
||||
|
||||
@@ -40,10 +40,15 @@ fn machine_derive2(item: TokenStream2) -> Result<TokenStream2, Error> {
|
||||
let ast: DeriveInput = syn::parse2(item)?;
|
||||
|
||||
let ident = &ast.ident;
|
||||
|
||||
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
|
||||
|
||||
let expanded = quote! {
|
||||
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {}
|
||||
impl #impl_generics control_core::machines::Machine for #ident #ty_generics #where_clause {
|
||||
fn get_machine_identification_unique(&self) -> control_core::machines::identification::MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics control_core::machines::AnyGetters for #ident #ty_generics #where_clause {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
|
||||
@@ -7,9 +7,9 @@ edition = "2024"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.99"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
axum = "0.8.4"
|
||||
axum = "0.8.6"
|
||||
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
ethercrab = "0.6"
|
||||
@@ -24,14 +24,14 @@ bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
crc = "3.3.0"
|
||||
serial = "0.4.0"
|
||||
tracing = "0.1.41"
|
||||
erased-serde = "0.4.6"
|
||||
erased-serde = "0.4.8"
|
||||
core_affinity = "0.8.3"
|
||||
control_core_derive = { version = "0.1.0", path = "../control-core-derive" }
|
||||
tokio = { version = "1.45.1", features = ["sync"], optional = true }
|
||||
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.175"
|
||||
libc = "0.2.176"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::anyhow;
|
||||
use smol::lock::Mutex;
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait Downcast<T> {
|
||||
fn downcast(&self) -> Result<T, anyhow::Error>;
|
||||
}
|
||||
|
||||
pub trait DowncastRef<T> {
|
||||
fn downcast_ref(&self) -> Result<&T, anyhow::Error>;
|
||||
}
|
||||
|
||||
impl<T: 'static, U: Any + 'static> Downcast<Arc<Mutex<T>>> for Arc<Mutex<U>> {
|
||||
fn downcast(&self) -> Result<Arc<Mutex<T>>, anyhow::Error> {
|
||||
// Acquire a read lock on the RwLock
|
||||
let lock = self.lock_blocking();
|
||||
|
||||
// Check if the inner type can be downcasted to T
|
||||
let any: &dyn Any = &*lock;
|
||||
|
||||
if any.is::<T>() {
|
||||
// Clone the Arc and return it as the desired type
|
||||
let cloned: Arc<Mutex<dyn Any>> = self.clone();
|
||||
|
||||
// Transmute the Arc to the desired type
|
||||
unsafe {
|
||||
let arc = Arc::from_raw(Arc::into_raw(cloned) as *const Mutex<T>);
|
||||
|
||||
Ok(arc)
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("[{}::downcast] Downcast failed", module_path!()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ use ethercat_hal::devices::el3062_0030::EL3062_0030_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3204::EL3204_IDENTITY_A;
|
||||
use ethercat_hal::devices::el3204::EL3204_IDENTITY_B;
|
||||
use ethercat_hal::devices::el4002::EL4002_IDENTITY_A;
|
||||
use ethercat_hal::devices::el5152::EL5152_IDENTITY_A;
|
||||
use ethercat_hal::devices::el6021::{
|
||||
EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D,
|
||||
};
|
||||
@@ -239,23 +240,25 @@ pub fn get_identification_addresses(
|
||||
EL1008_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL2002_IDENTITY_A | EL2002_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL2004_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3204_IDENTITY_A | EL3204_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL2008_IDENTITY_A | EL2008_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL3001_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL2521_IDENTITY_0000_A | EL2521_IDENTITY_0000_B | EL2521_IDENTITY_0024_A => {
|
||||
MachineIdentificationAddresses::default()
|
||||
}
|
||||
EL2522_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3001_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3021_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3024_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3062_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL3204_IDENTITY_A | EL3204_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL4002_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL7031_IDENTITY_A | EL7031_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL7031_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL7041_0052_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL5152_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL6021_IDENTITY_A | EL6021_IDENTITY_B | EL6021_IDENTITY_C | EL6021_IDENTITY_D => {
|
||||
MachineIdentificationAddresses::default()
|
||||
}
|
||||
EL7031_IDENTITY_A | EL7031_IDENTITY_B => MachineIdentificationAddresses::default(),
|
||||
EL7031_0030_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
EL7041_0052_IDENTITY_A => MachineIdentificationAddresses::default(),
|
||||
|
||||
_ => {
|
||||
// block_on(u16dump(&subdevice, maindevice, 0x00, 0xff))?;
|
||||
Err(anyhow!(
|
||||
|
||||
@@ -144,7 +144,7 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: ethercrab::RetryBehaviour::None,
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(5),
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod controllers;
|
||||
pub mod converters;
|
||||
pub mod downcast;
|
||||
pub mod ethercat;
|
||||
pub mod helpers;
|
||||
pub mod irq_handling;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use serde_json::Value;
|
||||
use smol::lock::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::socketio::namespace::Namespace;
|
||||
|
||||
pub trait MachineApi {
|
||||
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace;
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>>;
|
||||
|
||||
/// Returns a list of available video stream identifiers for this machine
|
||||
#[cfg(feature = "video-streaming")]
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
use crate::downcast::Downcast;
|
||||
use crate::machines::identification::MachineIdentificationUnique;
|
||||
use crate::machines::manager::MachineManager;
|
||||
use crate::socketio::{event::GenericEvent, namespace::Namespace};
|
||||
use serde::Serialize;
|
||||
use smol::block_on;
|
||||
use smol::lock::RwLock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Weak;
|
||||
|
||||
use super::Machine;
|
||||
use anyhow::anyhow;
|
||||
use smol::{channel::Sender, lock::Mutex};
|
||||
use socketioxide::extract::SocketRef;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MachineConnection<M: Machine + ?Sized>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Error(anyhow::Error),
|
||||
Disconnected,
|
||||
Connected(Arc<Mutex<M>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineSlot<M: Machine + ?Sized>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
pub machine_connection: MachineConnection<M>,
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
pub type MachineConnectionGeneric = MachineConnection<dyn Machine>;
|
||||
|
||||
pub type MachineSlotGeneric = MachineSlot<dyn Machine>;
|
||||
|
||||
impl<M: Machine> Clone for MachineConnection<M> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Connected(m) => Self::Connected(m.clone()),
|
||||
Self::Error(e) => Self::Error(anyhow!(e.to_string())),
|
||||
Self::Disconnected => Self::Disconnected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Machine + ?Sized> MachineConnection<M> {
|
||||
pub fn to_error(&self) -> Option<anyhow::Error> {
|
||||
match self {
|
||||
MachineConnection::Error(err) => Some(anyhow!(err.to_string())),
|
||||
Self::Disconnected => Some(anyhow!("Machine is disconnected")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_machine(&self) -> Option<Arc<Mutex<M>>> {
|
||||
match self {
|
||||
Self::Connected(machine) => Some(machine.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Machine + ?Sized> MachineSlot<M> {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
let namespace = Namespace::new(socket_queue_tx);
|
||||
Self {
|
||||
machine_connection: MachineConnection::Disconnected,
|
||||
namespace: Arc::new(Mutex::new(namespace)),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_connected(&self) -> bool {
|
||||
matches!(self.machine_connection, MachineConnection::Connected(_))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CrossConnectableMachine<
|
||||
F: CrossConnectableMachine<F, T>,
|
||||
T: CrossConnectableMachine<T, F>,
|
||||
>: Machine
|
||||
{
|
||||
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<F, T>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineCrossConnection<
|
||||
F: CrossConnectableMachine<F, T>,
|
||||
T: CrossConnectableMachine<T, F>,
|
||||
> {
|
||||
machine_manager: Weak<RwLock<MachineManager>>,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
connected_machine: Weak<Mutex<MachineSlot<T>>>,
|
||||
_make_compiler_happy: std::marker::PhantomData<F>, // This marks tells the typechecker, that we
|
||||
// need F in our type arguemnts. We are ok with
|
||||
// it being used only in a fake field. Without
|
||||
// F being fixed, we would not be anble to implement
|
||||
// reverse connecting and disconnecting properly,
|
||||
// as the type on the other side would decay
|
||||
// to an unknown type. See also https://github.com/qitechgmbh/control/pull/625#discussion_r2379566315
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct MachineCrossConnectionState {
|
||||
/// Connected Machine
|
||||
pub machine_identification_unique: Option<MachineIdentificationUnique>,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
impl<F: CrossConnectableMachine<F, T>, T: CrossConnectableMachine<T, F>>
|
||||
MachineCrossConnection<F, T>
|
||||
{
|
||||
pub fn new(
|
||||
manager: Weak<RwLock<MachineManager>>,
|
||||
machine_identification_unique: &MachineIdentificationUnique,
|
||||
) -> Self {
|
||||
Self {
|
||||
machine_manager: manager,
|
||||
connected_machine: Weak::new(),
|
||||
machine_identification_unique: machine_identification_unique.clone(),
|
||||
_make_compiler_happy: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_connected_machine(
|
||||
&mut self,
|
||||
machine_identification_unique: &MachineIdentificationUnique,
|
||||
) {
|
||||
if let Some(machine_manager) = self.machine_manager.upgrade() {
|
||||
let machine_manager = block_on(machine_manager.read());
|
||||
|
||||
let slot: Arc<Mutex<MachineSlotGeneric>> =
|
||||
match machine_manager.get(&machine_identification_unique) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let slot: Arc<Mutex<MachineSlot<T>>> = match Downcast::downcast(&slot) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
self.connected_machine = Arc::downgrade(&slot);
|
||||
} else {
|
||||
panic!("The machine manager has died, cannot do anything at this point");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse_connect(&self) {
|
||||
if let Some(slot) = self.connected_machine.upgrade() {
|
||||
let slot = &mut slot.lock_blocking();
|
||||
|
||||
if let MachineConnection::Connected(machine) = &slot.machine_connection {
|
||||
let mut machine = machine.lock_blocking();
|
||||
let cross_connection = machine.get_cross_connection();
|
||||
|
||||
if !cross_connection.is_connected() {
|
||||
cross_connection.set_connected_machine(&self.machine_identification_unique);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect(&mut self) {
|
||||
self.connected_machine = Weak::new();
|
||||
}
|
||||
|
||||
pub fn reverse_disconnect(&mut self) {
|
||||
if let Some(slot) = self.connected_machine.upgrade() {
|
||||
let slot = slot.lock_blocking();
|
||||
|
||||
if let MachineConnection::Connected(machine) = &slot.machine_connection {
|
||||
let mut machine = machine.lock_blocking();
|
||||
let cross_connection = machine.get_cross_connection();
|
||||
cross_connection.disconnect();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
let arc = self.connected_machine.upgrade();
|
||||
arc.is_some()
|
||||
}
|
||||
|
||||
pub fn to_state(&self) -> MachineCrossConnectionState {
|
||||
if let Some(slot) = self.connected_machine.upgrade() {
|
||||
let connection = &slot.lock_blocking().machine_connection;
|
||||
|
||||
if let MachineConnection::Connected(machine) = connection {
|
||||
let machine = machine.lock_blocking();
|
||||
|
||||
return MachineCrossConnectionState {
|
||||
machine_identification_unique: Some(
|
||||
machine.get_machine_identification_unique(),
|
||||
),
|
||||
is_available: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
MachineCrossConnectionState {
|
||||
machine_identification_unique: None,
|
||||
is_available: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
use super::{
|
||||
Machine,
|
||||
identification::{DeviceIdentification, DeviceIdentificationIdentified},
|
||||
new::MachineNewHardwareSerial,
|
||||
registry::MachineRegistry,
|
||||
};
|
||||
use smol::{
|
||||
channel::Sender,
|
||||
lock::{Mutex, RwLock},
|
||||
@@ -6,6 +12,9 @@ use socketioxide::extract::SocketRef;
|
||||
|
||||
use crate::{
|
||||
machines::{
|
||||
connection::{
|
||||
MachineConnection, MachineConnectionGeneric, MachineSlot, MachineSlotGeneric,
|
||||
},
|
||||
identification::MachineIdentificationUnique,
|
||||
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
|
||||
},
|
||||
@@ -17,18 +26,10 @@ use std::{
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use super::{
|
||||
Machine,
|
||||
identification::{DeviceIdentification, DeviceIdentificationIdentified},
|
||||
new::MachineNewHardwareSerial,
|
||||
registry::MachineRegistry,
|
||||
};
|
||||
#[derive(Debug)]
|
||||
pub struct MachineManager {
|
||||
pub ethercat_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
pub serial_machines:
|
||||
HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
pub ethercat_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
pub serial_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
}
|
||||
|
||||
impl Default for MachineManager {
|
||||
@@ -53,9 +54,6 @@ impl MachineManager {
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_manager: Weak<RwLock<Self>>,
|
||||
) {
|
||||
// empty ethercat machines
|
||||
self.ethercat_machines.clear();
|
||||
|
||||
// group devices by machine device identification
|
||||
let device_grouping_result = group_devices_by_identification(device_identifications);
|
||||
|
||||
@@ -69,10 +67,18 @@ impl MachineManager {
|
||||
// iterate over all identified device groups but ignore unaffected machines
|
||||
for device_group in device_grouping_result.device_groups.iter() {
|
||||
// get the machine identification
|
||||
let machine_identification = match device_group.first() {
|
||||
Some(device_identification) => &device_identification.device_machine_identification,
|
||||
None => continue, // Skip this group if empty
|
||||
};
|
||||
let machine_identification_unique: MachineIdentificationUnique =
|
||||
match device_group.first() {
|
||||
Some(device_identification) => device_identification
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone(),
|
||||
None => continue, // Skip this group if empty
|
||||
};
|
||||
|
||||
let slot =
|
||||
self.get_or_create_slot(socket_queue_tx.clone(), machine_identification_unique);
|
||||
let mut slot = slot.lock_blocking();
|
||||
|
||||
// create the machine
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
@@ -80,23 +86,40 @@ impl MachineManager {
|
||||
hardware: &machine_new_hardware,
|
||||
socket_queue_tx: socket_queue_tx.clone(),
|
||||
machine_manager: machine_manager.clone(),
|
||||
namespace: slot.namespace.clone(),
|
||||
});
|
||||
|
||||
// insert the machine into the ethercat machines map
|
||||
self.ethercat_machines.insert(
|
||||
machine_identification.machine_identification_unique.clone(),
|
||||
new_machine,
|
||||
);
|
||||
slot.machine_connection = match new_machine {
|
||||
Err(err) => MachineConnectionGeneric::Error(err),
|
||||
Ok(machine) => MachineConnectionGeneric::Connected(machine),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_create_slot(
|
||||
&mut self,
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_identification: MachineIdentificationUnique,
|
||||
) -> Arc<Mutex<MachineSlotGeneric>> {
|
||||
if let Some(slot) = self.get(&machine_identification) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
let slot = Arc::new(Mutex::new(MachineSlot::new(socket_queue_tx)));
|
||||
self.ethercat_machines
|
||||
.insert(machine_identification, slot.clone());
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<&Result<Arc<Mutex<dyn Machine>>, anyhow::Error>> {
|
||||
) -> Option<Arc<Mutex<MachineSlotGeneric>>> {
|
||||
self.ethercat_machines
|
||||
.get(machine_identification)
|
||||
.or_else(|| self.serial_machines.get(machine_identification))
|
||||
.map(|x| x.clone())
|
||||
}
|
||||
|
||||
pub fn add_serial_device(
|
||||
@@ -115,21 +138,28 @@ impl MachineManager {
|
||||
.try_into()
|
||||
.expect("Serial devices always have machine identification");
|
||||
|
||||
let machine_identification: MachineIdentificationUnique = device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone();
|
||||
|
||||
let slot = self.get_or_create_slot(socket_queue_tx.clone(), machine_identification);
|
||||
let mut slot = slot.lock_blocking();
|
||||
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
device_group: &vec![device_identification_identified.clone()],
|
||||
hardware: &MachineNewHardware::Serial(&hardware),
|
||||
socket_queue_tx,
|
||||
machine_manager,
|
||||
machine_manager: machine_manager.clone(),
|
||||
namespace: slot.namespace.clone(),
|
||||
});
|
||||
|
||||
tracing::info!("Adding serial machine {:?}", new_machine);
|
||||
slot.machine_connection = match new_machine {
|
||||
Err(err) => MachineConnectionGeneric::Error(err),
|
||||
Ok(machine) => MachineConnectionGeneric::Connected(machine),
|
||||
};
|
||||
|
||||
self.serial_machines.insert(
|
||||
device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique,
|
||||
new_machine,
|
||||
);
|
||||
tracing::info!("Adding serial machine {:?}", slot);
|
||||
}
|
||||
|
||||
pub fn remove_serial_device(&mut self, device_identification: &DeviceIdentification) {
|
||||
@@ -144,53 +174,45 @@ impl MachineManager {
|
||||
device_identification_identified
|
||||
);
|
||||
|
||||
self.serial_machines.remove(
|
||||
&device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique,
|
||||
);
|
||||
let machine_identification_unique = &device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique;
|
||||
|
||||
if let Some(slot) = self.serial_machines.get(machine_identification_unique) {
|
||||
let mut slot = slot.lock_blocking();
|
||||
slot.machine_connection = MachineConnection::Disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_machine_weak(
|
||||
pub fn get_ethercat_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let machine = self.ethercat_machines.get(machine_identification);
|
||||
let machine = match machine {
|
||||
Some(machine) => machine,
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine = match machine {
|
||||
Ok(machine) => machine,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine_weak = Arc::downgrade(machine);
|
||||
Some(machine_weak)
|
||||
self.get_weak(&self.ethercat_machines, machine_identification)
|
||||
}
|
||||
|
||||
pub fn get_serial_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let machine = self.serial_machines.get(machine_identification);
|
||||
let machine = match machine {
|
||||
Some(machine) => machine,
|
||||
None => {
|
||||
return None;
|
||||
self.get_weak(&self.serial_machines, machine_identification)
|
||||
}
|
||||
|
||||
fn get_weak(
|
||||
&self,
|
||||
machines: &HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let slot = machines.get(machine_identification).clone();
|
||||
|
||||
return slot.and_then(|slot| {
|
||||
let connection = &slot.lock_blocking().machine_connection;
|
||||
match connection {
|
||||
MachineConnectionGeneric::Error(_) => None,
|
||||
MachineConnectionGeneric::Disconnected => None,
|
||||
MachineConnectionGeneric::Connected(machine) => Some(Arc::downgrade(&machine)),
|
||||
}
|
||||
};
|
||||
let machine = match machine {
|
||||
Ok(machine) => machine,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let machine_weak = Arc::downgrade(machine);
|
||||
Some(machine_weak)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use smol::lock::Mutex;
|
||||
|
||||
use super::{Machine, identification::MachineIdentificationUnique, manager::MachineManager};
|
||||
use crate::machines::connection::MachineSlotGeneric;
|
||||
|
||||
use super::{identification::MachineIdentificationUnique, manager::MachineManager};
|
||||
|
||||
pub enum MachineManagerIter<'a> {
|
||||
EthercatMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
Arc<Mutex<MachineSlotGeneric>>,
|
||||
>,
|
||||
},
|
||||
SerialMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
Arc<Mutex<MachineSlotGeneric>>,
|
||||
>,
|
||||
},
|
||||
Done,
|
||||
@@ -24,14 +27,13 @@ pub enum MachineManagerIter<'a> {
|
||||
|
||||
pub struct MachineManagerIterator<'a> {
|
||||
iter: MachineManagerIter<'a>,
|
||||
serial_machines:
|
||||
&'a HashMap<MachineIdentificationUnique, Result<Arc<Mutex<dyn Machine>>, anyhow::Error>>,
|
||||
serial_machines: &'a HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MachineManagerIterator<'a> {
|
||||
type Item = (
|
||||
&'a MachineIdentificationUnique,
|
||||
&'a Result<Arc<Mutex<dyn Machine>>, anyhow::Error>,
|
||||
&'a Arc<Mutex<MachineSlotGeneric>>,
|
||||
);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
@@ -71,7 +73,7 @@ impl<'a> Iterator for MachineManagerIterator<'a> {
|
||||
|
||||
impl MachineManager {
|
||||
// Returns an iterator over all machines (both ethercat and serial)
|
||||
pub fn iter(&'_ self) -> MachineManagerIterator<'_> {
|
||||
pub fn iter(&self) -> MachineManagerIterator<'_> {
|
||||
MachineManagerIterator {
|
||||
iter: MachineManagerIter::EthercatMachines {
|
||||
iter: self.ethercat_machines.iter(),
|
||||
|
||||
@@ -1,83 +1,23 @@
|
||||
use anyhow::anyhow;
|
||||
use api::MachineApi;
|
||||
use new::MachineNewTrait;
|
||||
use serde::Serialize;
|
||||
use smol::lock::Mutex;
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use crate::machines::identification::MachineIdentificationUnique;
|
||||
use crate::machines::new::MachineAct;
|
||||
use crate::machines::{
|
||||
api::MachineApi, identification::MachineIdentificationUnique, new::MachineAct,
|
||||
};
|
||||
pub mod api;
|
||||
pub mod connection;
|
||||
pub mod identification;
|
||||
pub mod manager;
|
||||
pub mod manager_iter;
|
||||
pub mod new;
|
||||
pub mod registry;
|
||||
|
||||
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + AnyGetters + Debug + Send {}
|
||||
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send + Sync {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique;
|
||||
}
|
||||
|
||||
pub trait AnyGetters: Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
|
||||
/// Casts a `dyn Machine` to a specific machine type
|
||||
pub async fn downcast_machine<T: Machine>(
|
||||
machine: Arc<Mutex<dyn Machine>>,
|
||||
) -> Result<Arc<Mutex<T>>, anyhow::Error> {
|
||||
// Acquire a read lock on the RwLock
|
||||
let read_lock = machine.lock().await;
|
||||
|
||||
// Check if the inner type can be downcasted to T
|
||||
if read_lock.as_any().is::<T>() {
|
||||
// Clone the Arc and return it as the desired type
|
||||
let cloned_machine = Arc::clone(&machine);
|
||||
// Transmute the Arc to the desired type
|
||||
unsafe {
|
||||
Ok(Arc::from_raw(
|
||||
Arc::into_raw(cloned_machine) as *const Mutex<T>
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"[{}::downcast_machine] Downcast failed",
|
||||
module_path!()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to get strong count to see if Weak has reference
|
||||
pub trait GetStrongCount {
|
||||
fn get_strong_count(&self) -> usize;
|
||||
}
|
||||
|
||||
impl<T> GetStrongCount for Weak<Mutex<T>> {
|
||||
fn get_strong_count(&self) -> usize {
|
||||
self.strong_count()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConnectedMachine<T: GetStrongCount> {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub machine: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ConnectedMachineData {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
impl<T> From<&ConnectedMachine<T>> for ConnectedMachineData
|
||||
where
|
||||
T: GetStrongCount,
|
||||
{
|
||||
fn from(value: &ConnectedMachine<T>) -> Self {
|
||||
Self {
|
||||
machine_identification_unique: value.machine_identification_unique.clone(),
|
||||
is_available: value.machine.get_strong_count() != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
use crate::{machines::manager::MachineManager, serial::SerialDevice};
|
||||
use crate::{
|
||||
machines::{identification::MachineIdentificationUnique, manager::MachineManager},
|
||||
serial::SerialDevice,
|
||||
socketio::namespace::Namespace,
|
||||
};
|
||||
|
||||
use super::identification::DeviceIdentificationIdentified;
|
||||
use anyhow::Error;
|
||||
@@ -6,7 +10,10 @@ use ethercat_hal::{
|
||||
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
|
||||
};
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
use smol::{channel::Sender, lock::RwLock};
|
||||
use smol::{
|
||||
channel::Sender,
|
||||
lock::{Mutex, RwLock},
|
||||
};
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{
|
||||
sync::{Arc, Weak},
|
||||
@@ -49,6 +56,19 @@ pub struct MachineNewParams<
|
||||
>,
|
||||
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
pub machine_manager: Weak<RwLock<MachineManager>>,
|
||||
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl MachineNewParams<'_, '_, '_, '_, '_, '_, '_> {
|
||||
pub fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.device_group
|
||||
.first()
|
||||
.expect("device group must have at least one device")
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MachineNewHardware<
|
||||
|
||||
Generated
+144
-24
@@ -15,7 +15,7 @@
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.3",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
"@tanstack/react-router": "^1.105.0",
|
||||
"@tanstack/react-router": "^1.130.17",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/router-devtools": "^1.105.0",
|
||||
"@tanstack/zod-adapter": "^1.116.0",
|
||||
@@ -43,6 +43,7 @@
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
@@ -77,8 +78,9 @@
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"electron": "^37.3.1",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.20.1",
|
||||
@@ -2655,18 +2657,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.13.tgz",
|
||||
"integrity": "sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==",
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
|
||||
"integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.10",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
@@ -2689,6 +2692,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
|
||||
@@ -3964,13 +4024,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-router": {
|
||||
"version": "1.130.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.12.tgz",
|
||||
"integrity": "sha512-7BYgOpGc1vK8MH1LIFLLBudGpH46GQy+hewnP7dNQJ4KHmkwPHv958L1IMA9jU/rs5g1ZH5n1f33BAMOBXUMYQ==",
|
||||
"version": "1.130.17",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.17.tgz",
|
||||
"integrity": "sha512-F1tyMEbX7PJclnA25hAFXhLKq4jqeODvvaogsd0/NX02Kmz3QoT9GV+rgpv2TxamU6RwjRgihSH06Dn5hM6SOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.130.12",
|
||||
"@tanstack/react-store": "^0.7.0",
|
||||
"@tanstack/router-core": "1.130.12",
|
||||
"@tanstack/router-core": "1.130.17",
|
||||
"isbot": "^5.1.22",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"tiny-warning": "^1.0.3"
|
||||
@@ -4044,9 +4105,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/router-core": {
|
||||
"version": "1.130.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.12.tgz",
|
||||
"integrity": "sha512-emq3cRU9Na1hnIToojzkfJcOZm/MG2bv9M+Kr/elUxEf83enGEwQXC1EKezTuwNgeJrOv8vPJdEhWM7IQodnHQ==",
|
||||
"version": "1.130.17",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.17.tgz",
|
||||
"integrity": "sha512-0AH4s/52X2shYLifr9hR+6R6NpGsBp1M3LyjBTZm6Jlkud+nOhkkRxKxYS7l+JErRlffdxVHxMdoTnKcQ+duiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.130.12",
|
||||
"@tanstack/store": "^0.7.0",
|
||||
@@ -4442,10 +4504,18 @@
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"version": "19.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
|
||||
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4459,6 +4529,17 @@
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-virtualized": {
|
||||
"version": "9.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.22.2.tgz",
|
||||
"integrity": "sha512-0Eg/ME3OHYWGxs+/n4VelfYrhXssireZaa1Uqj5SEkTpSaBu5ctFGOCVxcOqpGXRiEdrk/7uho9tlZaryCIjHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
|
||||
@@ -6295,6 +6376,16 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -9917,7 +10008,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
@@ -11301,7 +11391,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -12087,7 +12176,6 @@
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -12097,8 +12185,7 @@
|
||||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "7.1.0",
|
||||
@@ -12226,6 +12313,12 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||
@@ -12326,6 +12419,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized": {
|
||||
"version": "9.22.6",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz",
|
||||
"integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"clsx": "^1.0.4",
|
||||
"dom-helpers": "^5.1.3",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/read-binary-file-arch": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
||||
|
||||
@@ -42,8 +42,9 @@
|
||||
"@types/electron-squirrel-startup": "^1.0.2",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"electron": "^37.3.1",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.20.1",
|
||||
@@ -71,7 +72,7 @@
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.7",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.3",
|
||||
@@ -80,7 +81,7 @@
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.84.2",
|
||||
"@tanstack/react-router": "^1.105.0",
|
||||
"@tanstack/react-router": "^1.130.17",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/router-devtools": "^1.105.0",
|
||||
"@tanstack/zod-adapter": "^1.116.0",
|
||||
@@ -99,6 +100,7 @@
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Icon } from "./Icon";
|
||||
import {
|
||||
List,
|
||||
CellMeasurer,
|
||||
CellMeasurerCache,
|
||||
ListRowRenderer,
|
||||
AutoSizer,
|
||||
} from "react-virtualized";
|
||||
|
||||
// Create cache (outside the component so it persists across renders)
|
||||
const cache = new CellMeasurerCache({
|
||||
defaultHeight: 20,
|
||||
fixedWidth: true,
|
||||
});
|
||||
|
||||
type Props = {
|
||||
lines: string[];
|
||||
autoScroll?: boolean; // Optional prop to control if terminal should auto-scroll
|
||||
className?: string; // Optional prop to control terminal height
|
||||
autoScroll?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
exportPrefix?: string; // Optional prefix for exported log files
|
||||
exportPrefix?: string;
|
||||
};
|
||||
|
||||
const terminalStyle = cva([
|
||||
"flex flex-col overflow-hidden rounded-md border border-neutral-700 font-mono text-sm",
|
||||
]);
|
||||
|
||||
// Color mapping for ANSI color codes
|
||||
const colorMap: Record<string, string> = {
|
||||
"30": "text-gray-900",
|
||||
"31": "text-red-600",
|
||||
@@ -32,7 +44,6 @@ const colorMap: Record<string, string> = {
|
||||
"95": "text-purple-400",
|
||||
"96": "text-cyan-400",
|
||||
"97": "text-white",
|
||||
// Background colors
|
||||
"40": "bg-gray-900",
|
||||
"41": "bg-red-600",
|
||||
"42": "bg-green-600",
|
||||
@@ -43,36 +54,26 @@ const colorMap: Record<string, string> = {
|
||||
"47": "bg-gray-200",
|
||||
};
|
||||
|
||||
// Parse ANSI color codes in text
|
||||
const parseColorCodes = (text: string) => {
|
||||
// Split by ANSI escape sequences
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/* eslint-disable-next-line no-control-regex */
|
||||
const parts = text.split(/(\x1b\[\d+m)/g);
|
||||
|
||||
let currentClass = "";
|
||||
const result: { text: string; className: string }[] = [];
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
|
||||
if (part.startsWith("\x1b[")) {
|
||||
// This is a color code
|
||||
const code = part.slice(2, -1); // Extract the number from \x1b[XXm
|
||||
const code = part.slice(2, -1);
|
||||
currentClass = colorMap[code] || "";
|
||||
} else if (part) {
|
||||
// This is text content
|
||||
result.push({ text: part, className: currentClass });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Function to strip ANSI color codes for plain text copy
|
||||
const stripColorCodes = (text: string): string => {
|
||||
const stripColorCodes = (text: string): string =>
|
||||
// eslint-disable-next-line no-control-regex
|
||||
return text.replace(/\x1b\[\d+m/g, "");
|
||||
};
|
||||
text.replace(/\x1b\[\d+m/g, "");
|
||||
|
||||
export function Terminal({
|
||||
lines,
|
||||
@@ -81,99 +82,88 @@ export function Terminal({
|
||||
title = "Terminal",
|
||||
exportPrefix,
|
||||
}: Props) {
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
|
||||
const listRef = useRef<List>(null);
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const [exportSuccess, setExportSuccess] = useState(false);
|
||||
|
||||
// Handle scrolling
|
||||
// Auto-scroll to bottom when new lines arrive
|
||||
useEffect(() => {
|
||||
const terminal = terminalRef.current;
|
||||
if (!terminal) return;
|
||||
|
||||
// If auto-scroll is enabled and user was at bottom, scroll to bottom when lines change
|
||||
if (autoScroll && isScrolledToBottom) {
|
||||
terminal.scrollTop = terminal.scrollHeight;
|
||||
if (autoScroll && listRef.current) {
|
||||
listRef.current.scrollToRow(lines.length - 1);
|
||||
}
|
||||
}, [lines, autoScroll, isScrolledToBottom]);
|
||||
}, [lines, autoScroll]);
|
||||
|
||||
// Handle scroll events to detect if user is at bottom
|
||||
const handleScroll = () => {
|
||||
const terminal = terminalRef.current;
|
||||
if (!terminal) return;
|
||||
|
||||
const isAtBottom =
|
||||
Math.abs(
|
||||
terminal.scrollHeight - terminal.clientHeight - terminal.scrollTop,
|
||||
) < 10; // Small threshold to account for rounding errors
|
||||
|
||||
setIsScrolledToBottom(isAtBottom);
|
||||
};
|
||||
|
||||
// Handle copy to clipboard
|
||||
const handleCopy = async () => {
|
||||
// Strip ANSI color codes and join lines
|
||||
const plainText = lines.map((line) => stripColorCodes(line)).join("\n");
|
||||
|
||||
const plainText = lines.map(stripColorCodes).join("\n");
|
||||
try {
|
||||
await navigator.clipboard.writeText(plainText);
|
||||
setCopySuccess(true);
|
||||
|
||||
// Reset copy success message after 2 seconds
|
||||
setTimeout(() => {
|
||||
setCopySuccess(false);
|
||||
}, 2000);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle export to file
|
||||
const handleExport = () => {
|
||||
if (!exportPrefix) return;
|
||||
|
||||
// Strip ANSI color codes and join lines
|
||||
const plainText = lines.map((line) => stripColorCodes(line)).join("\n");
|
||||
|
||||
// Create timestamp for filename
|
||||
const plainText = lines.map(stripColorCodes).join("\n");
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[:.]/g, "-")
|
||||
.slice(0, -5);
|
||||
const filename = `${exportPrefix}_${timestamp}.log`;
|
||||
|
||||
// Create blob and download
|
||||
const blob = new Blob([plainText], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create temporary link element and trigger download
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// Clean up
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Show success feedback
|
||||
setExportSuccess(true);
|
||||
setTimeout(() => {
|
||||
setExportSuccess(false);
|
||||
}, 2000);
|
||||
setTimeout(() => setExportSuccess(false), 2000);
|
||||
};
|
||||
|
||||
const rowRenderer: ListRowRenderer = ({ index, key, parent, style }) => {
|
||||
const colorParts = parseColorCodes(lines[index] || "");
|
||||
|
||||
return (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
cache={cache}
|
||||
parent={parent}
|
||||
columnIndex={0}
|
||||
rowIndex={index}
|
||||
>
|
||||
<div
|
||||
style={style}
|
||||
className="bg-black px-2 py-1 whitespace-pre-wrap text-white"
|
||||
>
|
||||
{colorParts.length > 0
|
||||
? colorParts.map((part, partIndex) => (
|
||||
<span
|
||||
key={partIndex}
|
||||
className={part.className || "text-white"}
|
||||
>
|
||||
{part.text || " "}
|
||||
</span>
|
||||
))
|
||||
: lines[index] || " "}
|
||||
</div>
|
||||
</CellMeasurer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={terminalStyle({ className })}>
|
||||
{/* Terminal header */}
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-neutral-700 bg-neutral-800 px-4 py-2">
|
||||
<div className="text-xs text-neutral-400">{title}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center text-xs text-neutral-400 transition-colors hover:text-neutral-200"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copySuccess ? (
|
||||
<>
|
||||
@@ -191,7 +181,6 @@ export function Terminal({
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="flex items-center text-xs text-neutral-400 transition-colors hover:text-neutral-200"
|
||||
title="Export logs to file"
|
||||
>
|
||||
{exportSuccess ? (
|
||||
<>
|
||||
@@ -209,62 +198,28 @@ export function Terminal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terminal content with custom scrollbar styling */}
|
||||
<div
|
||||
ref={terminalRef}
|
||||
onScroll={handleScroll}
|
||||
className={`scrollbar-thin scrollbar-thumb-neutral-600 scrollbar-track-transparent flex-grow overflow-y-auto bg-neutral-900 p-4 text-neutral-300`}
|
||||
style={{
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "rgb(82 82 91) transparent",
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
Add commentMore actions
|
||||
/* For Webkit browsers (Chrome, Safari) */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(82 82 91);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* For Firefox */
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgb(82 82 91) transparent;
|
||||
}
|
||||
`}</style>
|
||||
{lines.map((line, index) => {
|
||||
const colorParts = parseColorCodes(line);
|
||||
|
||||
return (
|
||||
<div key={index} className="whitespace-pre-wrap">
|
||||
{colorParts.length > 0
|
||||
? colorParts.map((part, partIndex) => (
|
||||
<span key={partIndex} className={part.className}>
|
||||
{part.text || " "}
|
||||
</span>
|
||||
))
|
||||
: line || " "}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Virtualized List */}
|
||||
<div className="flex-grow bg-black text-white">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<List
|
||||
ref={listRef}
|
||||
width={width}
|
||||
height={height}
|
||||
deferredMeasurementCache={cache}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowCount={lines.length}
|
||||
rowRenderer={rowRenderer}
|
||||
overscanRowCount={5}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
|
||||
{/* Status bar */}
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between bg-neutral-800 px-4 py-1 text-xs text-neutral-400">
|
||||
<div>{lines.length} lines</div>
|
||||
<div>
|
||||
{isScrolledToBottom ? "At bottom" : "Scrolled up"} |
|
||||
{autoScroll ? " Auto-scroll enabled" : " Auto-scroll disabled"}
|
||||
</div>
|
||||
<div>{autoScroll ? "Auto-scroll enabled" : "Auto-scroll disabled"}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -33,6 +33,8 @@ export function getUnitIcon(unit: Unit): IconName {
|
||||
return "lu:Zap";
|
||||
case "kWh":
|
||||
return "lu:BatteryFull";
|
||||
case "l/min":
|
||||
return "lu:Waves";
|
||||
default:
|
||||
return "lu:ChartNoAxesColumn";
|
||||
}
|
||||
@@ -68,6 +70,8 @@ export function renderUnitSymbol(unit: Unit | undefined): string {
|
||||
return "A";
|
||||
case "kWh":
|
||||
return "kWh";
|
||||
case "l/min":
|
||||
return "l/min";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -125,6 +129,8 @@ export function renderUnitSymbolLong(unit: Unit): string {
|
||||
return "amperes";
|
||||
case "kWh":
|
||||
return "kilowatt-hours";
|
||||
case "l/min":
|
||||
return "liters/minute";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -145,6 +151,7 @@ export const units = [
|
||||
"V",
|
||||
"A",
|
||||
"kWh",
|
||||
"l/min",
|
||||
] as const;
|
||||
|
||||
export type Unit = (typeof units)[number];
|
||||
|
||||
@@ -18,17 +18,39 @@ type UpdateExecuteInvokeParams = {
|
||||
|
||||
export function exposeUpdateContext() {
|
||||
const { contextBridge, ipcRenderer } = window.require("electron");
|
||||
|
||||
let currentLogListener: ((event: any, log: string) => void) | null = null;
|
||||
let currentEndListener: ((event: any, params: any) => void) | null = null;
|
||||
|
||||
contextBridge.exposeInMainWorld("update", {
|
||||
execute: (params: UpdateExecuteInvokeParams) =>
|
||||
ipcRenderer.invoke(UPDATE_EXECUTE, params),
|
||||
cancel: () => ipcRenderer.invoke(UPDATE_CANCEL),
|
||||
onLog: (callback: (log: string) => void) =>
|
||||
ipcRenderer.on(UPDATE_LOG, (_event, log: string) => {
|
||||
|
||||
onLog: (callback: (log: string) => void) => {
|
||||
if (currentLogListener) {
|
||||
ipcRenderer.removeListener(UPDATE_LOG, currentLogListener);
|
||||
}
|
||||
|
||||
currentLogListener = (_event, log: string) => {
|
||||
callback(log);
|
||||
}),
|
||||
onEnd: (callback: (params: { success: boolean; error?: string }) => void) =>
|
||||
ipcRenderer.on(UPDATE_END, (_event, params) => {
|
||||
};
|
||||
|
||||
ipcRenderer.on(UPDATE_LOG, currentLogListener);
|
||||
},
|
||||
|
||||
onEnd: (
|
||||
callback: (params: { success: boolean; error?: string }) => void,
|
||||
) => {
|
||||
if (currentEndListener) {
|
||||
ipcRenderer.removeListener(UPDATE_END, currentEndListener);
|
||||
}
|
||||
|
||||
currentEndListener = (_event, params) => {
|
||||
callback(params);
|
||||
}),
|
||||
};
|
||||
|
||||
ipcRenderer.on(UPDATE_END, currentEndListener);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,16 +1,28 @@
|
||||
import type { UpdateInfo } from "@/stores/updateStore";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
|
||||
let currentLogListener: ((line: string) => void) | null = null;
|
||||
|
||||
export async function updateExecute(
|
||||
source: UpdateInfo,
|
||||
onLog: (log: string) => void,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
window.update.onLog(onLog);
|
||||
// Remove previous listener if exists
|
||||
if (currentLogListener) {
|
||||
window.update.onLog(() => {}); // clear previous
|
||||
currentLogListener = null;
|
||||
}
|
||||
|
||||
currentLogListener = onLog;
|
||||
window.update.onLog(currentLogListener);
|
||||
|
||||
window.update.execute(source);
|
||||
|
||||
window.update.onEnd((params) => {
|
||||
window.update.onLog(() => {});
|
||||
window.update.onLog(() => {}); // remove listener
|
||||
window.update.onEnd(() => {});
|
||||
currentLogListener = null;
|
||||
resolve(params);
|
||||
});
|
||||
});
|
||||
@@ -40,7 +52,10 @@ export async function updateCancel(): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
return window.update.cancel();
|
||||
const result = await window.update.cancel();
|
||||
window.update.onLog(() => {}); // remove listener
|
||||
window.update.onEnd(() => {}); // remove listener
|
||||
return result;
|
||||
}
|
||||
|
||||
// Enhanced helper that automatically manages store state
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
|
||||
import { useAquapath1 } from "./useAquapath";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { Label } from "@/control/Label";
|
||||
|
||||
export function Aquapath1ControlPage() {
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
front_flow,
|
||||
back_flow,
|
||||
front_temperature,
|
||||
back_temperature,
|
||||
front_temp_reservoir,
|
||||
back_temp_reservoir,
|
||||
setAquapathMode,
|
||||
setFrontTemperature,
|
||||
setBackTemperature,
|
||||
setFrontFlow,
|
||||
setBackFlow,
|
||||
} = useAquapath1();
|
||||
const frontTargetTemperature =
|
||||
state?.temperature_states?.front.target_temperature ?? 0;
|
||||
const backTargetTemperature =
|
||||
state?.temperature_states?.back.target_temperature ?? 0;
|
||||
|
||||
const frontTargetFlow = state?.flow_states.front.should_flow ?? false;
|
||||
const backTargetFlow = state?.flow_states.back.should_flow ?? false;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid columns={2}>
|
||||
<ControlCard title="Flow Measurements">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Front Flow"
|
||||
unit="l/min"
|
||||
timeseries={front_flow}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Back Flow"
|
||||
unit="l/min"
|
||||
timeseries={back_flow}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Label label="Front Pump">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={frontTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (value == "On") {
|
||||
setFrontFlow(true);
|
||||
} else {
|
||||
setFrontFlow(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Back Pump">
|
||||
<SelectionGroup<"On" | "Off">
|
||||
value={backTargetFlow ? "On" : "Off"}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Off: {
|
||||
children: "Off",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
On: {
|
||||
children: "On",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => {
|
||||
if (value == "On") {
|
||||
setBackFlow(true);
|
||||
} else {
|
||||
setBackFlow(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Temperature Measurements">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Front in"
|
||||
unit="C"
|
||||
timeseries={front_temperature}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<TimeSeriesValueNumeric
|
||||
label="Front Reservoir"
|
||||
unit="C"
|
||||
timeseries={front_temp_reservoir}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<Label label="Set Front Target Temperature">
|
||||
<EditValue
|
||||
title="Set Front Target Temperature"
|
||||
value={frontTargetTemperature}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setFrontTemperature(val);
|
||||
}}
|
||||
defaultValue={
|
||||
defaultState?.temperature_states.front.target_temperature
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-8">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Back in"
|
||||
unit="C"
|
||||
timeseries={back_temperature}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<TimeSeriesValueNumeric
|
||||
label="Back Reservoir"
|
||||
unit="C"
|
||||
timeseries={back_temp_reservoir}
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
/>
|
||||
<Label label="Set Back Target Temperature">
|
||||
<EditValue
|
||||
title="Set Back Target Temperature"
|
||||
value={backTargetTemperature}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
onChange={(val) => {
|
||||
setBackTemperature(val);
|
||||
}}
|
||||
defaultValue={
|
||||
defaultState?.temperature_states.back.target_temperature
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard className="bg-red" title="Mode">
|
||||
<SelectionGroup<"Standby" | "Auto">
|
||||
value={state?.mode_state.mode}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Standby: {
|
||||
children: "Standby",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
Auto: {
|
||||
children: "Auto",
|
||||
icon: "lu:CirclePlay",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
onChange={(value) => setAquapathMode(value)}
|
||||
/>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import {
|
||||
AutoSyncedBigGraph,
|
||||
SyncedFloatingControlPanel,
|
||||
useGraphSync,
|
||||
type GraphConfig,
|
||||
} from "@/components/graph";
|
||||
|
||||
import React from "react";
|
||||
import { useAquapath1 } from "./useAquapath";
|
||||
import { TimeSeries } from "@/lib/timeseries";
|
||||
|
||||
export function Aquapath1GraphPage() {
|
||||
const {
|
||||
state,
|
||||
front_flow,
|
||||
back_flow,
|
||||
front_temperature,
|
||||
back_temperature,
|
||||
front_temp_reservoir,
|
||||
back_temp_reservoir,
|
||||
} = useAquapath1();
|
||||
|
||||
const syncHook = useGraphSync("aquapath-group");
|
||||
|
||||
const front_temp_target =
|
||||
state?.temperature_states?.front.target_temperature ?? 0;
|
||||
const back_temp_target =
|
||||
state?.temperature_states?.back.target_temperature ?? 0;
|
||||
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
<FlowGraph
|
||||
syncHook={syncHook}
|
||||
flow={front_flow}
|
||||
name={"Front Flow"}
|
||||
id={"front_flow"}
|
||||
/>
|
||||
<FlowGraph
|
||||
syncHook={syncHook}
|
||||
flow={back_flow}
|
||||
name={"Back Flow"}
|
||||
id={"back_flow"}
|
||||
/>
|
||||
<TemperatureGraph
|
||||
syncHook={syncHook}
|
||||
temp_in={front_temperature}
|
||||
temp_out={front_temp_reservoir}
|
||||
targetTemp={front_temp_target}
|
||||
name={"Front Temperature"}
|
||||
id={"front_temp"}
|
||||
/>
|
||||
<TemperatureGraph
|
||||
syncHook={syncHook}
|
||||
temp_in={back_temperature}
|
||||
temp_out={back_temp_reservoir}
|
||||
targetTemp={back_temp_target}
|
||||
name={"Back Temperature"}
|
||||
id={"back_temp"}
|
||||
/>
|
||||
</div>
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export function FlowGraph({
|
||||
syncHook,
|
||||
flow,
|
||||
name,
|
||||
id,
|
||||
}: {
|
||||
syncHook: ReturnType<typeof useGraphSync>;
|
||||
flow: TimeSeries | null;
|
||||
name: string;
|
||||
id: string;
|
||||
}) {
|
||||
const config: GraphConfig = {
|
||||
title: name,
|
||||
icon: "lu:RotateCcw",
|
||||
colors: {
|
||||
primary: "#10b981",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "flow_data",
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData: flow,
|
||||
color: "#3b82f6",
|
||||
}}
|
||||
unit="l/min"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
config={config}
|
||||
graphId={id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export function TemperatureGraph({
|
||||
syncHook,
|
||||
temp_in,
|
||||
temp_out,
|
||||
targetTemp,
|
||||
name,
|
||||
id,
|
||||
}: {
|
||||
syncHook: ReturnType<typeof useGraphSync>;
|
||||
temp_in: TimeSeries | null;
|
||||
temp_out: TimeSeries | null;
|
||||
targetTemp: number;
|
||||
name: string;
|
||||
id: string;
|
||||
}) {
|
||||
const config: GraphConfig = {
|
||||
title: name,
|
||||
icon: "lu:RotateCcw",
|
||||
colors: {
|
||||
primary: "#10b981",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "temperature_data",
|
||||
showLegend: true,
|
||||
lines: [],
|
||||
};
|
||||
|
||||
const combinedData = [
|
||||
...(temp_in
|
||||
? [
|
||||
{
|
||||
newData: temp_in,
|
||||
title: "Temperature In",
|
||||
color: "#3b82f6",
|
||||
lines: [
|
||||
{
|
||||
type: "threshold" as const,
|
||||
value: targetTemp,
|
||||
label: "Target Temperature",
|
||||
color: "#3b82f6",
|
||||
show: true,
|
||||
width: 2,
|
||||
dash: [5, 5],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(temp_out
|
||||
? [
|
||||
{
|
||||
newData: temp_out,
|
||||
title: "Temperature Out",
|
||||
color: "#f87171",
|
||||
lines: [
|
||||
{
|
||||
type: "threshold" as const,
|
||||
value: targetTemp,
|
||||
label: "Target Temperature",
|
||||
color: "#f87171",
|
||||
show: true,
|
||||
width: 2,
|
||||
dash: [5, 5],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={combinedData}
|
||||
unit="C"
|
||||
renderValue={(value) => value.toFixed(1)}
|
||||
config={config}
|
||||
graphId={id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { aquapath1SerialRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function Aquapath1Page() {
|
||||
const { serial } = aquapath1SerialRoute.useParams();
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/aquapath1/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
{
|
||||
link: "graph",
|
||||
title: "Graph",
|
||||
icon: "lu:ChartSpline",
|
||||
activeLink: "graphs",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* @file Aquapath1Namespace.ts
|
||||
* @description TypeScript implementation of Aquapath1 namespace with Zod schema validation.
|
||||
*/
|
||||
|
||||
import { StoreApi } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
Event,
|
||||
handleUnhandledEventError,
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
ThrottledStoreUpdater,
|
||||
} from "../../../client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { createTimeSeries, TimeSeries } from "@/lib/timeseries";
|
||||
|
||||
// ========== Event Schema Definitions ==========
|
||||
/**
|
||||
* Mode enum for Aquapath Machine
|
||||
*/
|
||||
export const modeSchema = z.enum(["Standby", "Auto"]);
|
||||
|
||||
/**
|
||||
* Mode state schema
|
||||
*/
|
||||
export const modeStateSchema = z.object({
|
||||
mode: modeSchema,
|
||||
});
|
||||
export const tempStateSchema = z.object({
|
||||
temperature: z.number(),
|
||||
target_temperature: z.number(),
|
||||
});
|
||||
/**
|
||||
* Cooling states schema
|
||||
*/
|
||||
export const tempStatesSchema = z.object({
|
||||
front: tempStateSchema,
|
||||
back: tempStateSchema,
|
||||
});
|
||||
|
||||
export const flowStateSchema = z.object({
|
||||
flow: z.number(),
|
||||
should_flow: z.boolean(),
|
||||
});
|
||||
export const flowStatesSchema = z.object({
|
||||
front: flowStateSchema,
|
||||
back: flowStateSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Live values event schema (time-series data)
|
||||
*/
|
||||
export const liveValuesEventDataSchema = z.object({
|
||||
front_flow: z.number(),
|
||||
back_flow: z.number(),
|
||||
front_temperature: z.number(),
|
||||
back_temperature: z.number(),
|
||||
front_temp_reservoir: z.number(),
|
||||
back_temp_reservoir: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* State event schema (consolidated state)
|
||||
*/
|
||||
export const stateEventDataSchema = z.object({
|
||||
is_default_state: z.boolean(),
|
||||
mode_state: modeStateSchema,
|
||||
flow_states: flowStatesSchema,
|
||||
temperature_states: tempStatesSchema,
|
||||
});
|
||||
|
||||
// ========== Event Schemas with Wrappers ==========
|
||||
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
|
||||
// ========== Type Inferences ==========
|
||||
export type Mode = z.infer<typeof modeSchema>;
|
||||
export type ModeState = z.infer<typeof modeStateSchema>;
|
||||
export type LiveValuesEvent = z.infer<typeof liveValuesEventSchema>;
|
||||
export type StateEvent = z.infer<typeof stateEventSchema>;
|
||||
|
||||
export type Aquapath1NamespaceStore = {
|
||||
// Single state event from server
|
||||
state: StateEvent | null;
|
||||
defaultState: StateEvent | null;
|
||||
|
||||
front_flow: TimeSeries;
|
||||
back_flow: TimeSeries;
|
||||
|
||||
front_temperature: TimeSeries;
|
||||
back_temperature: TimeSeries;
|
||||
|
||||
front_temp_reservoir: TimeSeries;
|
||||
back_temp_reservoir: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
const { initialTimeSeries: front_temperature, insert: addTemperature1 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: back_temperature, insert: addTemperature2 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: front_temp_reservoir, insert: addTempReserv1 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: back_temp_reservoir, insert: addTempReserv2 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: front_flow, insert: addFlow1 } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
|
||||
const { initialTimeSeries: back_flow, insert: addFlow2 } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
/**
|
||||
* Factory function to create a new Aquapath namespace store
|
||||
* @returns A new Zustand store instance for Aquapath namespace
|
||||
*/
|
||||
export const createAquapath1NamespaceStore =
|
||||
(): StoreApi<Aquapath1NamespaceStore> => {
|
||||
return create<Aquapath1NamespaceStore>(() => {
|
||||
return {
|
||||
state: null,
|
||||
defaultState: null,
|
||||
front_temperature: front_temperature,
|
||||
back_temperature: back_temperature,
|
||||
front_flow: front_flow,
|
||||
back_flow: back_flow,
|
||||
front_temp_reservoir: front_temp_reservoir,
|
||||
back_temp_reservoir: back_temp_reservoir,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a message handler for Mock1 namespace events with validation and appropriate caching strategies
|
||||
* @param store The store to update when messages are received
|
||||
* @param throttledUpdater Throttled updater for batching updates at 60 FPS
|
||||
* @returns A message handler function
|
||||
*/
|
||||
export function aquapath1MessageHandler(
|
||||
store: StoreApi<Aquapath1NamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<Aquapath1NamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const eventName = event.name;
|
||||
|
||||
// Helper function to update store through buffer
|
||||
const updateStore = (
|
||||
updater: (state: Aquapath1NamespaceStore) => Aquapath1NamespaceStore,
|
||||
) => {
|
||||
throttledUpdater.updateWith(updater);
|
||||
};
|
||||
|
||||
try {
|
||||
// State events (latest only)
|
||||
if (eventName === "StateEvent") {
|
||||
const stateEvent = stateEventSchema.parse(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,
|
||||
}));
|
||||
}
|
||||
// Live values events (time-series data)
|
||||
else if (eventName === "LiveValuesEvent") {
|
||||
const liveValuesEvent = liveValuesEventSchema.parse(event);
|
||||
|
||||
updateStore((state) => ({
|
||||
...state,
|
||||
front_temperature: addTemperature1(state.front_temperature, {
|
||||
value: liveValuesEvent.data.front_temperature,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
back_temperature: addTemperature2(state.back_temperature, {
|
||||
value: liveValuesEvent.data.back_temperature,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
front_flow: addFlow1(state.front_flow, {
|
||||
value: liveValuesEvent.data.front_flow,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
back_flow: addFlow2(state.back_flow, {
|
||||
value: liveValuesEvent.data.back_flow,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
front_temp_reservoir: addTempReserv1(state.front_temp_reservoir, {
|
||||
value: liveValuesEvent.data.front_temp_reservoir,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
back_temp_reservoir: addTempReserv2(state.back_temp_reservoir, {
|
||||
value: liveValuesEvent.data.back_temp_reservoir,
|
||||
timestamp: event.ts,
|
||||
}),
|
||||
}));
|
||||
} else {
|
||||
handleUnhandledEventError(eventName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Unexpected error processing ${eventName} event:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the Aquapath1 namespace implementation
|
||||
*/
|
||||
const useAquapath1NamespaceImplementation =
|
||||
createNamespaceHookImplementation<Aquapath1NamespaceStore>({
|
||||
createStore: createAquapath1NamespaceStore,
|
||||
createEventHandler: aquapath1MessageHandler,
|
||||
});
|
||||
|
||||
export function useAquapath1Namespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): Aquapath1NamespaceStore {
|
||||
// Generate namespace ID from validated machine ID
|
||||
const namespaceId: NamespaceId = {
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
};
|
||||
|
||||
// Use the implementation with validated namespace ID
|
||||
return useAquapath1NamespaceImplementation(namespaceId);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { toastError } from "@/components/Toast";
|
||||
import { useStateOptimistic } from "@/lib/useStateOptimistic";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { aquapath1 } from "@/machines/properties";
|
||||
import { aquapath1SerialRoute } from "@/routes/routes";
|
||||
import { Mode, StateEvent, useAquapath1Namespace } from "./aquapath1Namespace";
|
||||
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { produce } from "immer";
|
||||
import { z } from "zod";
|
||||
|
||||
export function useAquapath1() {
|
||||
const { serial: serialString } = aquapath1SerialRoute.useParams();
|
||||
|
||||
// Memoize the machine identification to keep it stable between renders
|
||||
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
|
||||
const serial = parseInt(serialString);
|
||||
|
||||
if (isNaN(serial)) {
|
||||
toastError(
|
||||
"Invalid Serial Number",
|
||||
`"${serialString}" is not a valid serial number.`,
|
||||
);
|
||||
|
||||
return {
|
||||
machine_identification: {
|
||||
vendor: 0,
|
||||
machine: 0,
|
||||
},
|
||||
serial: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
machine_identification: aquapath1.machine_identification,
|
||||
serial,
|
||||
};
|
||||
}, [serialString]);
|
||||
|
||||
// Get consolidated state and live values from namespace
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
front_temperature: front_temperature,
|
||||
back_temperature: back_temperature,
|
||||
front_flow: front_flow,
|
||||
back_flow: back_flow,
|
||||
front_temp_reservoir: front_temp_reservoir,
|
||||
back_temp_reservoir: back_temp_reservoir,
|
||||
} = useAquapath1Namespace(machineIdentification);
|
||||
|
||||
// Single optimistic state for all state management
|
||||
const stateOptimistic = useStateOptimistic<StateEvent>();
|
||||
|
||||
// Update optimistic state when real state changes
|
||||
useEffect(() => {
|
||||
if (state) {
|
||||
stateOptimistic.setReal(state);
|
||||
}
|
||||
}, [state, stateOptimistic]);
|
||||
|
||||
const setAquapathMode = (mode: Mode) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.mode_state.mode = mode;
|
||||
},
|
||||
() =>
|
||||
requestAquapathMode({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetAquaPathMode: mode },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setFrontTemperature = (temperature: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.temperature_states.front.target_temperature = temperature;
|
||||
},
|
||||
() =>
|
||||
requestFrontTemperature({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetFrontTemperature: temperature },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setBackTemperature = (temperature: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.temperature_states.back.target_temperature = temperature;
|
||||
},
|
||||
() =>
|
||||
requestBackTemperature({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetBackTemperature: temperature },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setFrontFlow = (flow: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.flow_states.front.should_flow = flow;
|
||||
},
|
||||
() =>
|
||||
requestFrontFlow({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetFrontFlow: flow },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setBackFlow = (flow: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.flow_states.back.should_flow = flow;
|
||||
},
|
||||
() =>
|
||||
requestBackFlow({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetBackFlow: flow },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Mutation hooks
|
||||
const { request: requestAquapathMode } = useMachineMutation(
|
||||
z.object({ SetAquaPathMode: z.enum(["Standby", "Auto"]) }),
|
||||
);
|
||||
const { request: requestFrontTemperature } = useMachineMutation(
|
||||
z.object({ SetFrontTemperature: z.number() }),
|
||||
);
|
||||
const { request: requestBackTemperature } = useMachineMutation(
|
||||
z.object({ SetBackTemperature: z.number() }),
|
||||
);
|
||||
const { request: requestFrontFlow } = useMachineMutation(
|
||||
z.object({ SetFrontFlow: z.boolean() }),
|
||||
);
|
||||
const { request: requestBackFlow } = useMachineMutation(
|
||||
z.object({ SetBackFlow: z.boolean() }),
|
||||
);
|
||||
// Helper function for optimistic updates using produce
|
||||
const updateStateOptimistically = (
|
||||
producer: (current: StateEvent) => void,
|
||||
serverRequest: () => void,
|
||||
) => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState) {
|
||||
stateOptimistic.setOptimistic(produce(currentState, producer));
|
||||
}
|
||||
serverRequest();
|
||||
};
|
||||
|
||||
return {
|
||||
// Consolidated state
|
||||
state: stateOptimistic.value?.data,
|
||||
|
||||
// Default state for initial values
|
||||
defaultState: defaultState?.data,
|
||||
front_flow,
|
||||
back_flow,
|
||||
front_temperature,
|
||||
back_temperature,
|
||||
front_temp_reservoir,
|
||||
back_temp_reservoir,
|
||||
|
||||
setAquapathMode,
|
||||
setFrontTemperature,
|
||||
setBackTemperature,
|
||||
setFrontFlow,
|
||||
setBackFlow,
|
||||
};
|
||||
}
|
||||
@@ -271,12 +271,82 @@ export const buffer1: MachineProperties = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const aquapath1: MachineProperties = {
|
||||
name: "Aquapath",
|
||||
version: "V1",
|
||||
slug: "aquapath1",
|
||||
icon: "lu:Waves",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x0009,
|
||||
},
|
||||
device_roles: [
|
||||
{
|
||||
role: 0,
|
||||
role_label: "Bus Coupler",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x44c2c52,
|
||||
revision: 0x120000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 1,
|
||||
role_label: "EL2008",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x7d83052,
|
||||
revision: 0x110000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 2,
|
||||
role_label: "EL4002",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0xfa23052,
|
||||
revision: 0x140000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 3,
|
||||
role_label: "EL3204",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0xc843052,
|
||||
revision: 0x160000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 4,
|
||||
role_label: "EL5152",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x14203052,
|
||||
revision: 0x140000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const machineProperties: MachineProperties[] = [
|
||||
winder2,
|
||||
extruder2,
|
||||
laser1,
|
||||
mock1,
|
||||
buffer1,
|
||||
aquapath1,
|
||||
];
|
||||
|
||||
export const getMachineProperties = (
|
||||
|
||||
@@ -44,7 +44,7 @@ export function Winder2SettingPage() {
|
||||
unit="mm"
|
||||
step={0.05}
|
||||
min={0.1}
|
||||
max={10}
|
||||
max={75}
|
||||
defaultValue={defaultState?.traverse_state?.step_size}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={(value) => setTraverseStepSize(value)}
|
||||
|
||||
@@ -35,6 +35,10 @@ import { Mock1PresetsPage } from "@/machines/mock/mock1/Mock1PresetsPage";
|
||||
import { TroubleshootPage } from "@/setup/Trobleshoot";
|
||||
import { UpdateExecutePage } from "@/setup/UpdateExecutePage";
|
||||
import { Winder2PresetsPage } from "@/machines/winder/winder2/Winder2PresetsPage";
|
||||
import { Aquapath1ControlPage } from "@/machines/aquapath/aquapath1/Aquapath1ControlPage";
|
||||
import { Aquapath1Page } from "@/machines/aquapath/aquapath1/Aquapath1Page";
|
||||
import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph";
|
||||
|
||||
import { z } from "zod";
|
||||
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
|
||||
// make a route tree like this
|
||||
@@ -194,6 +198,24 @@ export const mock1PresetsRoute = createRoute({
|
||||
component: () => <Mock1PresetsPage />,
|
||||
});
|
||||
|
||||
export const aquapath1SerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "aquapath1/$serial",
|
||||
component: () => <Aquapath1Page />,
|
||||
});
|
||||
|
||||
export const aquapath1GraphRoute = createRoute({
|
||||
getParentRoute: () => aquapath1SerialRoute,
|
||||
path: "graph",
|
||||
component: () => <Aquapath1GraphPage />,
|
||||
});
|
||||
|
||||
export const aquapath1ControlRoute = createRoute({
|
||||
getParentRoute: () => aquapath1SerialRoute,
|
||||
path: "control",
|
||||
component: () => <Aquapath1ControlPage />,
|
||||
});
|
||||
|
||||
export const buffer1SettingsRoute = createRoute({
|
||||
getParentRoute: () => buffer1SerialRoute,
|
||||
path: "settings",
|
||||
@@ -292,6 +314,11 @@ export const rootTree = RootRoute.addChildren([
|
||||
laser1PresetsRoute,
|
||||
]),
|
||||
|
||||
aquapath1SerialRoute.addChildren([
|
||||
aquapath1ControlRoute,
|
||||
aquapath1GraphRoute,
|
||||
]),
|
||||
|
||||
winder2SerialRoute.addChildren([
|
||||
winder2ControlRoute,
|
||||
winder2ManualRoute,
|
||||
|
||||
@@ -11,15 +11,16 @@ import { toast } from "sonner";
|
||||
export function TroubleshootPage() {
|
||||
const [isRebootLoading, setIsRebootLoading] = useState(false);
|
||||
const [isRestartLoading, setIsRestartLoading] = useState(false);
|
||||
const { getLogsBySource, clearLogs } = useLogsStore();
|
||||
|
||||
const { getLogsBySource } = useLogsStore();
|
||||
|
||||
// Perhaps we just need to clear the logs ?
|
||||
// Get backend logs for display
|
||||
const backendLogs = getLogsBySource("qitech-control-server");
|
||||
const logLines = backendLogs.map((log) => log.raw);
|
||||
|
||||
console.log(backendLogs);
|
||||
const handleRebootHmi = async () => {
|
||||
setIsRebootLoading(true);
|
||||
|
||||
try {
|
||||
const result = await rebootHmi();
|
||||
if (result.success) {
|
||||
|
||||
@@ -22,6 +22,8 @@ export function UpdateExecutePage() {
|
||||
startUpdate,
|
||||
stopUpdate,
|
||||
addTerminalLine,
|
||||
clearTerminalLines,
|
||||
resetUpdateState,
|
||||
} = useUpdateStore();
|
||||
|
||||
// Set update info from search params when component mounts or search changes
|
||||
@@ -49,6 +51,7 @@ export function UpdateExecutePage() {
|
||||
};
|
||||
|
||||
startUpdate();
|
||||
// Perhaps we just need to clear the logs ?
|
||||
const res = await updateExecute(updateInfo, addTerminalLine);
|
||||
stopUpdate();
|
||||
|
||||
@@ -66,12 +69,14 @@ export function UpdateExecutePage() {
|
||||
const res = await updateCancelWithStore();
|
||||
if (res.success) {
|
||||
toast.info("Update cancelled successfully");
|
||||
clearTerminalLines();
|
||||
} else {
|
||||
toast.error("Failed to cancel update: " + res.error);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error("Failed to cancel update: " + error.message);
|
||||
}
|
||||
resetUpdateState();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -33,7 +33,6 @@ const initialState: LogsState = {
|
||||
|
||||
export const useLogsStore = create<LogsStore>((set, get) => ({
|
||||
...initialState,
|
||||
|
||||
addLogEntry: (entry) =>
|
||||
set(
|
||||
produce((state: LogsState) => {
|
||||
@@ -69,6 +68,7 @@ export const useLogsStore = create<LogsStore>((set, get) => ({
|
||||
),
|
||||
|
||||
getLogsBySource: (source) => {
|
||||
console.log(source);
|
||||
const state = get();
|
||||
return state.entries.filter((entry) => entry.source === source);
|
||||
},
|
||||
|
||||
@@ -78,11 +78,16 @@ export const useUpdateStore = create<UpdateStore>((set) => ({
|
||||
addTerminalLine: (line) =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
state.terminalLines.push(line);
|
||||
console.log(state.terminalLines);
|
||||
|
||||
// Keep only last 10000 lines to prevent memory issues
|
||||
if (state.terminalLines.length > 10000) {
|
||||
state.terminalLines.splice(0, state.terminalLines.length - 10000);
|
||||
const lastLine = state.terminalLines[state.terminalLines.length - 1];
|
||||
if (line !== lastLine) {
|
||||
state.terminalLines.push(line);
|
||||
|
||||
// Keep only last 10000 lines to prevent memory issues
|
||||
if (state.terminalLines.length > 10000) {
|
||||
state.terminalLines.splice(0, state.terminalLines.length - 10000);
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -14,7 +14,7 @@ tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
control_core = { path = "../control-core" }
|
||||
futures = "0.3.31"
|
||||
anyhow = "1.0.99"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ edition = "2024"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.99"
|
||||
anyhow = "1.0.100"
|
||||
ethercrab = "0.6"
|
||||
ethercat_hal_derive = { version = "0.1.0", path = "../ethercat-hal-derive" }
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
@@ -15,6 +15,7 @@ smol = "2.0.2"
|
||||
uom = "0.36.0"
|
||||
rand = "0.9.2"
|
||||
futures = "0.3.31"
|
||||
tracing = "0.1.41"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
helpers::ethercrab_types::EthercrabSubDevicePreoperational,
|
||||
io::analog_input::{AnalogInputDevice, AnalogInputInput},
|
||||
};
|
||||
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
|
||||
use ethercat_hal_derive::EthercatDevice;
|
||||
use uom::si::{electric_potential::volt, f64::ElectricPotential};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -98,7 +98,9 @@ impl AnalogInputDevice<EL3062_0030Port> for EL3062_0030 {
|
||||
EL30XXPresentation::Signed => raw_value.as_signed(),
|
||||
EL30XXPresentation::SignedMagnitude => raw_value.as_signed_magnitude(),
|
||||
};
|
||||
|
||||
let normalized = f32::from(value) / f32::from(i16::MAX);
|
||||
|
||||
AnalogInputInput {
|
||||
normalized,
|
||||
wiring_error: false,
|
||||
@@ -138,22 +140,97 @@ pub enum EL3062_0030Port {
|
||||
AI2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TxPdo)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EL3062_0030TxPdo {
|
||||
#[pdo_object_index(0x1A00)]
|
||||
pub ai_standard_channel1: Option<AiStandard>,
|
||||
#[pdo_object_index(0x1A01)]
|
||||
pub ai_compact_channel1: Option<AiCompact>,
|
||||
|
||||
#[pdo_object_index(0x1A02)]
|
||||
pub ai_standard_channel2: Option<AiStandard>,
|
||||
#[pdo_object_index(0x1A03)]
|
||||
pub ai_compact_channel2: Option<AiCompact>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, RxPdo)]
|
||||
pub struct EL3062_0030RxPdo {}
|
||||
impl crate::coe::Configuration for EL3062_0030TxPdo {
|
||||
///Implemented by the ethercat_hal_derive::TxPdo derive macro
|
||||
async fn write_config<'a>(
|
||||
&self,
|
||||
device: &EthercrabSubDevicePreoperational<'a>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
device.sdo_write(0x1C13, 0, 0u8).await?;
|
||||
let mut len = 0;
|
||||
if let Some(_) = &self.ai_standard_channel1 {
|
||||
len += 1;
|
||||
device.sdo_write(0x1C13, len, 0x1A00u16).await?;
|
||||
}
|
||||
if let Some(_) = &self.ai_compact_channel1 {
|
||||
len += 1;
|
||||
device.sdo_write(0x1C13, len, 0x1A01u16).await?;
|
||||
}
|
||||
if let Some(_) = &self.ai_standard_channel2 {
|
||||
len += 1;
|
||||
device.sdo_write(0x1C13, len, 0x1A02u16).await?;
|
||||
}
|
||||
if let Some(_) = &self.ai_compact_channel2 {
|
||||
len += 1;
|
||||
device.sdo_write(0x1C13, len, 0x1A03u16).await?;
|
||||
}
|
||||
device.sdo_write(0x1C13, 0, len).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl crate::pdo::TxPdo for EL3062_0030TxPdo {
|
||||
///Implemented by the ethercat_hal_derive::TxPdo derive macro
|
||||
fn get_objects(&self) -> Box<[Option<&dyn crate::pdo::TxPdoObject>]> {
|
||||
Box::new([
|
||||
self.ai_standard_channel1
|
||||
.as_ref()
|
||||
.map(|o| o as &dyn crate::pdo::TxPdoObject),
|
||||
self.ai_compact_channel1
|
||||
.as_ref()
|
||||
.map(|o| o as &dyn crate::pdo::TxPdoObject),
|
||||
self.ai_standard_channel2
|
||||
.as_ref()
|
||||
.map(|o| o as &dyn crate::pdo::TxPdoObject),
|
||||
self.ai_compact_channel2
|
||||
.as_ref()
|
||||
.map(|o| o as &dyn crate::pdo::TxPdoObject),
|
||||
])
|
||||
}
|
||||
///Implemented by the ethercat_hal_derive::TxPdo derive macro
|
||||
fn get_objects_mut(&mut self) -> Box<[Option<&mut dyn crate::pdo::TxPdoObject>]> {
|
||||
Box::new([
|
||||
self.ai_standard_channel1
|
||||
.as_mut()
|
||||
.map(|o| o as &mut dyn crate::pdo::TxPdoObject),
|
||||
self.ai_compact_channel1
|
||||
.as_mut()
|
||||
.map(|o| o as &mut dyn crate::pdo::TxPdoObject),
|
||||
self.ai_standard_channel2
|
||||
.as_mut()
|
||||
.map(|o| o as &mut dyn crate::pdo::TxPdoObject),
|
||||
self.ai_compact_channel2
|
||||
.as_mut()
|
||||
.map(|o| o as &mut dyn crate::pdo::TxPdoObject),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EL3062_0030RxPdo {}
|
||||
impl crate::coe::Configuration for EL3062_0030RxPdo {
|
||||
///Implemented by the ethercat_hal_derive::RxPdo derive macro
|
||||
async fn write_config<'a>(
|
||||
&self,
|
||||
_device: &EthercrabSubDevicePreoperational<'a>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl crate::pdo::RxPdo for EL3062_0030RxPdo {
|
||||
///Implemented by the ethercat_hal_derive::RxPdo derive macro
|
||||
fn get_objects(&self) -> Box<[Option<&dyn crate::pdo::RxPdoObject>]> {
|
||||
Box::new([])
|
||||
}
|
||||
}
|
||||
impl Configuration for EL3062_0030Configuration {
|
||||
async fn write_config<'a>(
|
||||
&self,
|
||||
|
||||
@@ -102,6 +102,7 @@ impl EL4002 {
|
||||
&mut self,
|
||||
subdevice: &EthercrabSubDevicePreoperational<'a>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("el4002");
|
||||
self.configuration
|
||||
.channel1
|
||||
.write_channel_config(subdevice, 0x8000)
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple};
|
||||
use crate::coe::{ConfigurableDevice, Configuration};
|
||||
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
|
||||
use crate::io::encoder_input::{
|
||||
EncoderInputCounter, EncoderInputDevice, EncoderInputFrequency, EncoderInputPeriod,
|
||||
};
|
||||
use crate::pdo::PredefinedPdoAssignment;
|
||||
use crate::pdo::RxPdo;
|
||||
use crate::pdo::TxPdo;
|
||||
use crate::pdo::el5152::{
|
||||
El5152EncoderControl, El5152EncoderFrequency, El5152EncoderPeriod, El5152EncoderStatus,
|
||||
};
|
||||
|
||||
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
|
||||
|
||||
/// EL5152 2-channel incremental encoder interface
|
||||
///
|
||||
/// 24V HTL, 100 kHz, dual channel
|
||||
|
||||
#[derive(EthercatDevice)]
|
||||
pub struct EL5152 {
|
||||
pub configuration: EL5152Configuration,
|
||||
pub rxpdo: EL5152RxPdo,
|
||||
pub txpdo: EL5152TxPdo,
|
||||
is_used: bool,
|
||||
}
|
||||
|
||||
impl EthercatDeviceProcessing for EL5152 {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EL5152Configuration {
|
||||
pub pdo_assignment: EL5152PredefinedPdoAssignment,
|
||||
pub channel1: EL5152ChannelConfiguration,
|
||||
pub channel2: EL5152ChannelConfiguration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EL5152PredefinedPdoAssignment {
|
||||
Period,
|
||||
Frequency,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EL5152 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "EL5152")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum EL5152Port {
|
||||
ENC1,
|
||||
ENC2,
|
||||
}
|
||||
|
||||
impl Default for EL5152Configuration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pdo_assignment: EL5152PredefinedPdoAssignment::Period,
|
||||
channel1: EL5152ChannelConfiguration::default(),
|
||||
channel2: EL5152ChannelConfiguration::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NewEthercatDevice for EL5152 {
|
||||
fn new() -> Self {
|
||||
let configuration: EL5152Configuration = EL5152Configuration::default();
|
||||
Self {
|
||||
configuration: configuration.clone(),
|
||||
rxpdo: configuration.pdo_assignment.rxpdo_assignment(),
|
||||
txpdo: configuration.pdo_assignment.txpdo_assignment(),
|
||||
is_used: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurableDevice<EL5152Configuration> for EL5152 {
|
||||
async fn write_config<'maindevice>(
|
||||
&mut self,
|
||||
device: &EthercrabSubDevicePreoperational<'maindevice>,
|
||||
config: &EL5152Configuration,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
config.write_config(device).await?;
|
||||
self.configuration = config.clone();
|
||||
self.txpdo = config.pdo_assignment.txpdo_assignment();
|
||||
self.rxpdo = config.pdo_assignment.rxpdo_assignment();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_config(&self) -> EL5152Configuration {
|
||||
self.configuration.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderInputDevice<EL5152Port> for EL5152 {
|
||||
fn get_counter_value(&self, port: EL5152Port) -> Result<EncoderInputCounter, anyhow::Error> {
|
||||
let value = match port {
|
||||
EL5152Port::ENC1 => self
|
||||
.txpdo
|
||||
.status_channel1
|
||||
.as_ref()
|
||||
.map_or(0, |status| status.counter_value),
|
||||
EL5152Port::ENC2 => self
|
||||
.txpdo
|
||||
.status_channel2
|
||||
.as_ref()
|
||||
.map_or(0, |status| status.counter_value),
|
||||
};
|
||||
Ok(EncoderInputCounter { value })
|
||||
}
|
||||
|
||||
fn get_frequency(
|
||||
&self,
|
||||
port: EL5152Port,
|
||||
) -> Result<Option<EncoderInputFrequency>, anyhow::Error> {
|
||||
let frequency = match port {
|
||||
EL5152Port::ENC1 => {
|
||||
self.txpdo
|
||||
.frequency_channel1
|
||||
.as_ref()
|
||||
.map(|f| EncoderInputFrequency {
|
||||
value: f.frequency_value,
|
||||
})
|
||||
}
|
||||
EL5152Port::ENC2 => {
|
||||
self.txpdo
|
||||
.frequency_channel2
|
||||
.as_ref()
|
||||
.map(|f| EncoderInputFrequency {
|
||||
value: f.frequency_value,
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(frequency)
|
||||
}
|
||||
|
||||
fn get_period(&self, port: EL5152Port) -> Result<Option<EncoderInputPeriod>, anyhow::Error> {
|
||||
let period = match port {
|
||||
EL5152Port::ENC1 => self
|
||||
.txpdo
|
||||
.period_channel1
|
||||
.as_ref()
|
||||
.map(|p| EncoderInputPeriod {
|
||||
value: p.period_value,
|
||||
}),
|
||||
EL5152Port::ENC2 => self
|
||||
.txpdo
|
||||
.period_channel2
|
||||
.as_ref()
|
||||
.map(|p| EncoderInputPeriod {
|
||||
value: p.period_value,
|
||||
}),
|
||||
};
|
||||
Ok(period)
|
||||
}
|
||||
|
||||
fn set_counter(&mut self, port: EL5152Port, value: u32) -> Result<(), anyhow::Error> {
|
||||
match port {
|
||||
EL5152Port::ENC1 => {
|
||||
if let Some(control) = self.rxpdo.control_channel1.as_mut() {
|
||||
control.set_counter_value = value;
|
||||
control.set_counter = true;
|
||||
}
|
||||
}
|
||||
EL5152Port::ENC2 => {
|
||||
if let Some(control) = self.rxpdo.control_channel2.as_mut() {
|
||||
control.set_counter_value = value;
|
||||
control.set_counter = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Configuration for EL5152Configuration {
|
||||
async fn write_config<'a>(
|
||||
&self,
|
||||
device: &EthercrabSubDevicePreoperational<'a>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
// Configure channel 1
|
||||
self.channel1.write_channel_config(device, 0x8000).await?;
|
||||
|
||||
// Configure channel 2
|
||||
self.channel2.write_channel_config(device, 0x8010).await?;
|
||||
// Write PDO assignments
|
||||
self.pdo_assignment
|
||||
.txpdo_assignment()
|
||||
.write_config(device)
|
||||
.await?;
|
||||
self.pdo_assignment
|
||||
.rxpdo_assignment()
|
||||
.write_config(device)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, RxPdo)]
|
||||
pub struct EL5152RxPdo {
|
||||
#[pdo_object_index(0x1600)]
|
||||
pub control_channel1: Option<El5152EncoderControl>,
|
||||
#[pdo_object_index(0x1602)]
|
||||
pub control_channel2: Option<El5152EncoderControl>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TxPdo)]
|
||||
pub struct EL5152TxPdo {
|
||||
#[pdo_object_index(0x1A00)]
|
||||
pub status_channel1: Option<El5152EncoderStatus>,
|
||||
#[pdo_object_index(0x1A02)]
|
||||
pub period_channel1: Option<El5152EncoderPeriod>,
|
||||
#[pdo_object_index(0x1A03)]
|
||||
pub frequency_channel1: Option<El5152EncoderFrequency>,
|
||||
|
||||
#[pdo_object_index(0x1A04)]
|
||||
pub status_channel2: Option<El5152EncoderStatus>,
|
||||
#[pdo_object_index(0x1A06)]
|
||||
pub period_channel2: Option<El5152EncoderPeriod>,
|
||||
#[pdo_object_index(0x1A07)]
|
||||
pub frequency_channel2: Option<El5152EncoderFrequency>,
|
||||
}
|
||||
|
||||
impl PredefinedPdoAssignment<EL5152TxPdo, EL5152RxPdo> for EL5152PredefinedPdoAssignment {
|
||||
fn rxpdo_assignment(&self) -> EL5152RxPdo {
|
||||
EL5152RxPdo {
|
||||
control_channel1: Some(El5152EncoderControl::default()),
|
||||
control_channel2: Some(El5152EncoderControl::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn txpdo_assignment(&self) -> EL5152TxPdo {
|
||||
match self {
|
||||
Self::Period => EL5152TxPdo {
|
||||
status_channel1: Some(El5152EncoderStatus::default()),
|
||||
period_channel1: Some(El5152EncoderPeriod::default()),
|
||||
frequency_channel1: None,
|
||||
|
||||
status_channel2: Some(El5152EncoderStatus::default()),
|
||||
period_channel2: Some(El5152EncoderPeriod::default()),
|
||||
frequency_channel2: None,
|
||||
},
|
||||
Self::Frequency => EL5152TxPdo {
|
||||
status_channel1: Some(El5152EncoderStatus::default()),
|
||||
period_channel1: None,
|
||||
frequency_channel1: Some(El5152EncoderFrequency::default()),
|
||||
|
||||
status_channel2: Some(El5152EncoderStatus::default()),
|
||||
period_channel2: None,
|
||||
frequency_channel2: Some(El5152EncoderFrequency::default()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EL5152ChannelConfiguration {
|
||||
// 80n0:3
|
||||
pub enable_counter: u8,
|
||||
// 80n0:8
|
||||
pub disable_filter: u8,
|
||||
// 80n0:A
|
||||
pub enable_micro_increment: u8,
|
||||
// 80n0:E
|
||||
pub reversion_rotation: u8,
|
||||
// 80n0:F 0-µs, 1-ms
|
||||
pub frequency_based_window: u8,
|
||||
// 80n0:11
|
||||
pub frequency_window: u16,
|
||||
// 80n0:13
|
||||
pub frequency_scaling: u16,
|
||||
// 80n0:14
|
||||
pub period_scaling: u16,
|
||||
// 80n0:15 100: "0.01 Hz"
|
||||
pub frequency_resolution: u16,
|
||||
// 80n0:16 100: "100 ns"
|
||||
pub period_resolution: u16,
|
||||
// 80n0:17
|
||||
pub frequency_wait_time: u16,
|
||||
}
|
||||
|
||||
impl EL5152ChannelConfiguration {
|
||||
pub async fn write_channel_config<'a>(
|
||||
&self,
|
||||
device: &EthercrabSubDevicePreoperational<'a>,
|
||||
base_index: u16,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
device
|
||||
.sdo_write(base_index, 0x03, self.enable_counter)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x08, self.disable_filter)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x0A, self.enable_micro_increment)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x0E, self.reversion_rotation)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x0F, self.frequency_based_window)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x11, self.frequency_window)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x13, self.frequency_scaling)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x14, self.period_scaling)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x15, self.frequency_resolution)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x16, self.period_resolution)
|
||||
.await?;
|
||||
device
|
||||
.sdo_write(base_index, 0x17, self.frequency_wait_time)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EL5152ChannelConfiguration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable_counter: 0x01u8,
|
||||
disable_filter: 0x00u8,
|
||||
enable_micro_increment: 0x00u8,
|
||||
reversion_rotation: 0x00u8,
|
||||
frequency_based_window: 0x00u8,
|
||||
frequency_window: 0x2710u16,
|
||||
frequency_scaling: 0x0064u16,
|
||||
period_scaling: 0x0064u16,
|
||||
frequency_resolution: 0x0064u16,
|
||||
period_resolution: 0x0064u16,
|
||||
frequency_wait_time: 0x0640u16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const EL5152_VENDOR_ID: u32 = 0x2;
|
||||
pub const EL5152_PRODUCT_ID: u32 = 0x14203052;
|
||||
pub const EL5152_REVISION_A: u32 = 0x140000;
|
||||
pub const EL5152_IDENTITY_A: SubDeviceIdentityTuple =
|
||||
(EL5152_VENDOR_ID, EL5152_PRODUCT_ID, EL5152_REVISION_A);
|
||||
@@ -15,6 +15,7 @@ pub mod el3024;
|
||||
pub mod el3062_0030;
|
||||
pub mod el3204;
|
||||
pub mod el4002;
|
||||
pub mod el5152;
|
||||
pub mod el6021;
|
||||
pub mod el7031;
|
||||
pub mod el7031_0030;
|
||||
@@ -22,7 +23,7 @@ pub mod el7041_0052;
|
||||
// pub mod el4008;
|
||||
use super::devices::el1008::EL1008;
|
||||
use crate::{
|
||||
devices::{el2521::EL2521, el3062_0030::EL3062_0030_IDENTITY_A},
|
||||
devices::{el2521::EL2521, el4002::EL4002},
|
||||
helpers::ethercrab_types::EthercrabSubDeviceGroupPreoperational,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -38,8 +39,10 @@ use el2522::{EL2522, EL2522_IDENTITY_A};
|
||||
use el3001::EL3001_IDENTITY_A;
|
||||
use el3021::EL3021_IDENTITY_A;
|
||||
use el3024::EL3024_IDENTITY_A;
|
||||
use el3062_0030::EL3062_0030_IDENTITY_A;
|
||||
use el3204::EL3204_IDENTITY_A;
|
||||
use el4002::EL4002_IDENTITY_A;
|
||||
use el5152::{EL5152, EL5152_IDENTITY_A};
|
||||
use el6021::{EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D};
|
||||
|
||||
use el3204::EL3204_IDENTITY_B;
|
||||
@@ -191,9 +194,6 @@ pub fn device_from_subdevice_identity_tuple(
|
||||
EL2008_IDENTITY_A | EL2008_IDENTITY_B => Ok(Arc::new(RwLock::new(EL2008::new()))),
|
||||
// TODO: implement EL2024 identity
|
||||
// EL2024 => Ok(Arc::new(RwLock::new(EL2024::new()))),
|
||||
EL2521_IDENTITY_0000_A | EL2521_IDENTITY_0000_B | EL2521_IDENTITY_0024_A => {
|
||||
Ok(Arc::new(RwLock::new(EL2521::new())))
|
||||
}
|
||||
EL2522_IDENTITY_A => Ok(Arc::new(RwLock::new(EL2522::new()))),
|
||||
// TODO: implement EL2634 identity
|
||||
// "EL2634" => Ok(Arc::new(RwLock::new(EL2634::new()))),
|
||||
@@ -203,7 +203,8 @@ pub fn device_from_subdevice_identity_tuple(
|
||||
EL3021_IDENTITY_A => Ok(Arc::new(RwLock::new(el3021::EL3021::new()))),
|
||||
EL3024_IDENTITY_A => Ok(Arc::new(RwLock::new(el3024::EL3024::new()))),
|
||||
EL3062_0030_IDENTITY_A => Ok(Arc::new(RwLock::new(el3062_0030::EL3062_0030::new()))),
|
||||
EL4002_IDENTITY_A => Ok(Arc::new(RwLock::new(el4002::EL4002::new()))),
|
||||
EL4002_IDENTITY_A => Ok(Arc::new(RwLock::new(EL4002::new()))),
|
||||
EL5152_IDENTITY_A => Ok(Arc::new(RwLock::new(EL5152::new()))),
|
||||
EL6021_IDENTITY_A | EL6021_IDENTITY_B | EL6021_IDENTITY_C | EL6021_IDENTITY_D => {
|
||||
Ok(Arc::new(RwLock::new(el6021::EL6021::new())))
|
||||
}
|
||||
@@ -214,6 +215,9 @@ pub fn device_from_subdevice_identity_tuple(
|
||||
EL7031_IDENTITY_A | EL7031_IDENTITY_B => Ok(Arc::new(RwLock::new(el7031::EL7031::new()))),
|
||||
EL7031_0030_IDENTITY_A => Ok(Arc::new(RwLock::new(el7031_0030::EL7031_0030::new()))),
|
||||
EL7041_0052_IDENTITY_A => Ok(Arc::new(RwLock::new(el7041_0052::EL7041_0052::new()))),
|
||||
EL2521_IDENTITY_0000_A | EL2521_IDENTITY_0000_B | EL2521_IDENTITY_0024_A => {
|
||||
Ok(Arc::new(RwLock::new(EL2521::new())))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::device_from_subdevice] No Driver: vendor_id: 0x{:x}, product_id: 0x{:x}, revision: 0x{:x}",
|
||||
module_path!(),
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use futures::executor::block_on;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
/// Encoder Input device
|
||||
///
|
||||
/// Reads encoder values (counter, frequency, period) from the device.
|
||||
pub struct EncoderInput {
|
||||
/// Read the counter value from the encoder
|
||||
get_counter: Box<dyn Fn() -> Result<EncoderInputCounter, anyhow::Error> + Send + Sync>,
|
||||
/// Read the frequency value from the encoder (if available)
|
||||
get_frequency:
|
||||
Box<dyn Fn() -> Result<Option<EncoderInputFrequency>, anyhow::Error> + Send + Sync>,
|
||||
/// Read the period value from the encoder (if available)
|
||||
get_period: Box<dyn Fn() -> Result<Option<EncoderInputPeriod>, anyhow::Error> + Send + Sync>,
|
||||
/// Set the counter value
|
||||
set_counter: Box<dyn Fn(u32) -> Result<(), anyhow::Error> + Send + Sync>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for EncoderInput {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "EncoderInput")
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement on device that have encoder inputs
|
||||
impl EncoderInput {
|
||||
pub fn new<PORT>(device: Arc<RwLock<dyn EncoderInputDevice<PORT>>>, port: PORT) -> Self
|
||||
where
|
||||
PORT: Clone + Send + Sync + 'static,
|
||||
{
|
||||
// build sync get counter closure
|
||||
let port_counter = port.clone();
|
||||
let device_counter = device.clone();
|
||||
let get_counter = Box::new(move || {
|
||||
let device = block_on(device_counter.read());
|
||||
device.get_counter_value(port_counter.clone())
|
||||
});
|
||||
|
||||
// build sync get frequency closure
|
||||
let port_frequency = port.clone();
|
||||
let device_frequency = device.clone();
|
||||
let get_frequency = Box::new(move || {
|
||||
let device = block_on(device_frequency.read());
|
||||
device.get_frequency(port_frequency.clone())
|
||||
});
|
||||
|
||||
// build sync get period closure
|
||||
let port_period = port.clone();
|
||||
let device_period = device.clone();
|
||||
let get_period = Box::new(move || {
|
||||
let device = block_on(device_period.read());
|
||||
device.get_period(port_period.clone())
|
||||
});
|
||||
|
||||
// build sync set counter closure
|
||||
let port_set = port;
|
||||
let device_set = device;
|
||||
let set_counter = Box::new(move |value: u32| {
|
||||
let mut device = block_on(device_set.write());
|
||||
device.set_counter(port_set.clone(), value)
|
||||
});
|
||||
|
||||
Self {
|
||||
get_counter,
|
||||
get_frequency,
|
||||
get_period,
|
||||
set_counter,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current counter value of the encoder
|
||||
pub fn get_counter_value(&self) -> Result<u32, anyhow::Error> {
|
||||
let counter = (self.get_counter)()?;
|
||||
Ok(counter.value)
|
||||
}
|
||||
|
||||
/// Get the current frequency value of the encoder (if available)
|
||||
pub fn get_frequency_value(&self) -> Result<Option<u32>, anyhow::Error> {
|
||||
let frequency = (self.get_frequency)()?;
|
||||
Ok(frequency.map(|f| f.value))
|
||||
}
|
||||
|
||||
/// Get the current period value of the encoder (if available)
|
||||
pub fn get_period_value(&self) -> Result<Option<u32>, anyhow::Error> {
|
||||
let period = (self.get_period)()?;
|
||||
Ok(period.map(|p| p.value))
|
||||
}
|
||||
|
||||
/// Set the counter value
|
||||
pub fn set_counter_value(&self, value: u32) -> Result<(), anyhow::Error> {
|
||||
(self.set_counter)(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncoderInputCounter {
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncoderInputFrequency {
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncoderInputPeriod {
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
pub trait EncoderInputDevice<PORTS>: Send + Sync
|
||||
where
|
||||
PORTS: Clone,
|
||||
{
|
||||
fn get_counter_value(&self, port: PORTS) -> Result<EncoderInputCounter, anyhow::Error>;
|
||||
fn get_frequency(&self, port: PORTS) -> Result<Option<EncoderInputFrequency>, anyhow::Error>;
|
||||
fn get_period(&self, port: PORTS) -> Result<Option<EncoderInputPeriod>, anyhow::Error>;
|
||||
fn set_counter(&mut self, port: PORTS, value: u32) -> Result<(), anyhow::Error>;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pub mod analog_input_dummy;
|
||||
pub mod analog_output;
|
||||
pub mod digital_input;
|
||||
pub mod digital_output;
|
||||
pub mod encoder_input;
|
||||
pub mod pulse_train_output;
|
||||
pub mod serial_interface;
|
||||
pub mod stepper_velocity_el70x1;
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
use super::RxPdoObject;
|
||||
use bitvec::prelude::*;
|
||||
use ethercat_hal_derive::PdoObject;
|
||||
|
||||
use super::RxPdoObject;
|
||||
|
||||
/// PDO Object for EL40xx (analog output) devices
|
||||
///
|
||||
/// The "Analog Output" holds the output value and status information.
|
||||
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
|
||||
#[pdo_object(bits = 16)]
|
||||
pub struct AnalogOutput {
|
||||
/// Output value (-32768-32767 typically corresponds to -10 -10V or 0mA-20mA (from 4-20mA for EL402x))
|
||||
/// Output value (-32768-32767 typically corresponds to -10V to +10V)
|
||||
pub value: i16,
|
||||
}
|
||||
|
||||
impl RxPdoObject for AnalogOutput {
|
||||
fn write(&self, bits: &mut BitSlice<u8, Lsb0>) {
|
||||
// Write the output value to bits 0-15
|
||||
bits[0..16].store_le(self.value);
|
||||
bits[0..16].store_le(self.value as u16);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
use super::{RxPdoObject, TxPdoObject};
|
||||
use bitvec::prelude::*;
|
||||
use ethercat_hal_derive::PdoObject;
|
||||
|
||||
/// PDO Object for EL5152 encoder control (RxPDO)
|
||||
/// Based on 1600/1602 mapping: Set counter (1 bit) + alignment + Set counter value (32/16 bit)
|
||||
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
|
||||
#[pdo_object(bits = 48)] // Total 48 bits for full mapping
|
||||
pub struct El5152EncoderControl {
|
||||
/// Set counter - execute counter setting (1 bit) - 0x7000:03
|
||||
pub set_counter: bool,
|
||||
/// Set counter value (32-bit) - 0x7000:11
|
||||
pub set_counter_value: u32,
|
||||
}
|
||||
|
||||
impl RxPdoObject for El5152EncoderControl {
|
||||
fn write(&self, bits: &mut BitSlice<u8, Lsb0>) {
|
||||
// bit 2: Set counter (0x7000:03)
|
||||
bits.set(2, self.set_counter);
|
||||
// Set counter value (bits 16-47, 32-bit value) - 0x7000:11
|
||||
bits[16..48].store_le(self.set_counter_value);
|
||||
}
|
||||
}
|
||||
|
||||
/// PDO Object for EL5152 encoder status (TxPDO)
|
||||
/// Based on 1A00/1A04 mapping: Status bits + Counter value (32-bit)
|
||||
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
|
||||
#[pdo_object(bits = 48)] // 16 bits status + 32 bits counter
|
||||
pub struct El5152EncoderStatus {
|
||||
/// Set counter done - counter was set (1 bit) - 0x6000:03
|
||||
pub set_counter_done: bool,
|
||||
/// Extrapolation stall - extrapolated counter invalid (1 bit) - 0x6000:08
|
||||
pub extrapolation_stall: bool,
|
||||
/// Status of input A (1 bit) - 0x6000:09
|
||||
pub status_input_a: bool,
|
||||
/// Status of input B (1 bit) - 0x6000:0A
|
||||
pub status_input_b: bool,
|
||||
/// Sync Error - synchronization error occurred (1 bit) - 0x1C32:20
|
||||
pub sync_error: bool,
|
||||
/// TxPDO Toggle - toggled when TxPDO data is updated (1 bit) - 0x1800:09
|
||||
pub txpdo_toggle: bool,
|
||||
/// Counter value (32-bit) - 0x6000:11
|
||||
pub counter_value: u32,
|
||||
}
|
||||
|
||||
impl TxPdoObject for El5152EncoderStatus {
|
||||
fn read(&mut self, bits: &BitSlice<u8, Lsb0>) {
|
||||
// bit 2: Set counter done (0x6000:03)
|
||||
self.set_counter_done = bits[2];
|
||||
// bit 7: Extrapolation stall (0x6000:08)
|
||||
self.extrapolation_stall = bits[7];
|
||||
// bit 8: Status of input A (0x6000:09)
|
||||
self.status_input_a = bits[8];
|
||||
// bit 9: Status of input B (0x6000:0A)
|
||||
self.status_input_b = bits[9];
|
||||
// bit 13: Sync error (0x1C32:20)
|
||||
self.sync_error = bits[13];
|
||||
// bit 15: TxPDO Toggle (0x1800:09)
|
||||
self.txpdo_toggle = bits[15];
|
||||
|
||||
// Counter value (bits 16-47, 32-bit value) - 0x6000:11
|
||||
self.counter_value = bits[16..48].load_le::<u32>();
|
||||
}
|
||||
}
|
||||
|
||||
/// PDO Object for EL5152 encoder frequency measurement (TxPDO)
|
||||
/// Based on 1A03/1A07 mapping: 32-bit frequency value only
|
||||
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
|
||||
#[pdo_object(bits = 32)]
|
||||
pub struct El5152EncoderFrequency {
|
||||
/// Frequency value (32-bit) - 0x6000:13 / 0x6010:13
|
||||
pub frequency_value: u32,
|
||||
}
|
||||
|
||||
impl TxPdoObject for El5152EncoderFrequency {
|
||||
fn read(&mut self, bits: &BitSlice<u8, Lsb0>) {
|
||||
// Frequency value (bits 0-31) - 0x6000:13 / 0x6010:13
|
||||
self.frequency_value = bits[0..32].load_le::<u32>();
|
||||
}
|
||||
}
|
||||
|
||||
/// PDO Object for EL5152 encoder period measurement (TxPDO)
|
||||
/// Based on 1A02/1A06 mapping: 32-bit period value only
|
||||
#[derive(Debug, Clone, Default, PdoObject, PartialEq, Eq)]
|
||||
#[pdo_object(bits = 32)]
|
||||
pub struct El5152EncoderPeriod {
|
||||
/// Period value (32-bit) - 0x6000:14 / 0x6010:14
|
||||
pub period_value: u32,
|
||||
}
|
||||
|
||||
impl TxPdoObject for El5152EncoderPeriod {
|
||||
fn read(&mut self, bits: &BitSlice<u8, Lsb0>) {
|
||||
// Period value (bits 0-31) - 0x6000:14 / 0x6010:14
|
||||
self.period_value = bits[0..32].load_le::<u32>();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ pub mod basic;
|
||||
pub mod el252x;
|
||||
pub mod el32xx;
|
||||
pub mod el40xx;
|
||||
pub mod el5152;
|
||||
pub mod el70x1;
|
||||
use crate::coe::Configuration;
|
||||
use bitvec::prelude::*;
|
||||
|
||||
@@ -113,6 +113,8 @@ impl EL40XXChannelConfiguration {
|
||||
device: &EthercrabSubDevicePreoperational<'a>,
|
||||
base_index: u16,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("write_channel_config");
|
||||
|
||||
// Write all configuration parameters according to the documentation table
|
||||
device
|
||||
.sdo_write(base_index, 0x01, self.enable_user_scale as u8)
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ control_core_derive = { path = "../control-core-derive" }
|
||||
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
|
||||
serde = "1.0.217"
|
||||
anyhow = "1.0.99"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
bitvec = "1.0.1"
|
||||
lazy_static = "1.5.0"
|
||||
@@ -26,14 +26,14 @@ euclid = "0.22.11"
|
||||
# concurrency
|
||||
smol = "2.0.2"
|
||||
tokio = { version = "1.45.1", features = ["rt-multi-thread"] }
|
||||
regex = "1.11.2"
|
||||
regex = "1.11.3"
|
||||
futures = "0.3.31"
|
||||
|
||||
# web
|
||||
serde_json = "1.0.143"
|
||||
socketioxide = { version = "0.17.2", features = ["msgpack"] }
|
||||
tower-http = { version = "0.6.6", features = ["cors", "trace", "fs"] }
|
||||
axum = { version = "0.8.4", features = ["macros"] }
|
||||
axum = { version = "0.8.6", features = ["macros"] }
|
||||
|
||||
# serial
|
||||
serialport = "4.7.3"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::ethercat::config::{MAX_SUBDEVICES, PDI_LEN};
|
||||
use crate::performance_metrics::EthercatPerformanceMetrics;
|
||||
use crate::serial::registry::SERIAL_DEVICE_REGISTRY;
|
||||
use crate::socketio::main_namespace::machines_event::MachineObj;
|
||||
use crate::socketio::namespaces::Namespaces;
|
||||
use control_core::machines::Machine;
|
||||
use control_core::machines::identification::{DeviceIdentification, MachineIdentificationUnique};
|
||||
@@ -90,4 +91,22 @@ impl AppState {
|
||||
performance_metrics: Arc::new(RwLock::new(EthercatPerformanceMetrics::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_machine_objs(&self) -> Vec<MachineObj> {
|
||||
let machines = self.machines.read_blocking();
|
||||
machines
|
||||
.iter()
|
||||
.map(|machine| {
|
||||
let error = {
|
||||
let slot = machine.1.lock_blocking();
|
||||
slot.machine_connection.to_error().map(|e| e.to_string())
|
||||
};
|
||||
|
||||
MachineObj {
|
||||
machine_identification_unique: machine.0.clone(),
|
||||
error,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeou
|
||||
use smol::channel::Sender;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
const SM_OUTPUT: u16 = 0x1C32;
|
||||
const SM_INPUT: u16 = 0x1C33;
|
||||
|
||||
pub async fn setup_loop(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
interface: &str,
|
||||
@@ -90,13 +93,13 @@ pub async fn setup_loop(
|
||||
// Default 30_000us
|
||||
pdu: Duration::from_micros(30_000),
|
||||
// Default 10ms
|
||||
eeprom: Duration::from_millis(10),
|
||||
eeprom: Duration::from_millis(100),
|
||||
// Default 0ms
|
||||
wait_loop_delay: Duration::from_millis(0),
|
||||
// Default 100ms
|
||||
mailbox_echo: Duration::from_millis(100),
|
||||
mailbox_echo: Duration::from_millis(1000),
|
||||
// Default 1000ms
|
||||
mailbox_response: Duration::from_millis(1000),
|
||||
mailbox_response: Duration::from_millis(10000),
|
||||
},
|
||||
MainDeviceConfig {
|
||||
// Default RetryBehaviour::None
|
||||
@@ -157,14 +160,12 @@ pub async fn setup_loop(
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let devices = device_identifications
|
||||
.into_iter()
|
||||
.zip(devices)
|
||||
.zip(&subdevices)
|
||||
.map(|((a, b), c)| (a, b, c))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// filter devices and if Option<DeviceMachineIdentification> is Some
|
||||
// return identified_devices, identified_device_identifications, identified_subdevices
|
||||
let (identified_device_identifications, identified_devices, identified_subdevices): (
|
||||
@@ -198,7 +199,6 @@ pub async fn setup_loop(
|
||||
acc
|
||||
},
|
||||
);
|
||||
|
||||
// construct machines
|
||||
{
|
||||
let mut machines_guard = app_state.machines.write().await;
|
||||
@@ -220,6 +220,12 @@ pub async fn setup_loop(
|
||||
.map(|(device_identification, device, _)| (device_identification.clone(), device.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for subdevice in subdevices.iter() {
|
||||
if subdevice.name() == "EL5152" {
|
||||
subdevice.sdo_write(SM_OUTPUT, 0x1, 0x00u16).await?; //set sync mode (1) for free run (0)
|
||||
subdevice.sdo_write(SM_INPUT, 0x1, 0x00u16).await?; //set sync mode (1) for free run (0)
|
||||
}
|
||||
}
|
||||
// Notify client via socketio
|
||||
let app_state_clone = app_state.clone();
|
||||
smol::block_on(async move {
|
||||
@@ -229,7 +235,7 @@ pub async fn setup_loop(
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event = MachinesEventBuilder().build(app_state_clone.clone()).await;
|
||||
let event = MachinesEventBuilder().build(app_state_clone.clone());
|
||||
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -1,6 +1,7 @@
|
||||
use crate::app_state::AppState;
|
||||
use crate::panic::{PanicDetails, send_panic};
|
||||
use bitvec::prelude::*;
|
||||
use control_core::machines::connection::MachineConnection;
|
||||
use control_core::realtime::{set_core_affinity, set_realtime_priority};
|
||||
use smol::channel::Sender;
|
||||
use std::sync::Arc;
|
||||
@@ -144,7 +145,8 @@ pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyh
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
for machine in machine_guard.iter() {
|
||||
if let Ok(machine) = machine.1 {
|
||||
let connection = &machine.1.lock_blocking().machine_connection;
|
||||
if let MachineConnection::Connected(machine) = connection {
|
||||
// if the machine is currenlty locked (likely processing API call)
|
||||
// we skip the machine
|
||||
if let Some(mut machine_guard) = machine.try_lock() {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
use super::{AquaPathV1, AquaPathV1Mode};
|
||||
use control_core::machines::new::MachineAct;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
impl MachineAct for AquaPathV1 {
|
||||
fn act(&mut self, now_ts: Instant) {
|
||||
match self.mode {
|
||||
AquaPathV1Mode::Standby => {
|
||||
self.switch_to_standby();
|
||||
}
|
||||
AquaPathV1Mode::Auto => {
|
||||
self.switch_to_auto();
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
self.front_controller.update(now_ts);
|
||||
self.back_controller.update(now_ts);
|
||||
|
||||
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
|
||||
self.emit_live_values();
|
||||
self.last_measurement_emit = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use super::{AquaPathV1, AquaPathV1Mode};
|
||||
use control_core::{
|
||||
machines::api::MachineApi,
|
||||
socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
|
||||
cache_first_and_last_event,
|
||||
},
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::lock::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
pub struct LiveValuesEvent {
|
||||
pub front_flow: f64,
|
||||
pub back_flow: f64,
|
||||
pub front_temperature: f64,
|
||||
pub back_temperature: f64,
|
||||
pub front_temp_reservoir: f64,
|
||||
pub back_temp_reservoir: f64,
|
||||
}
|
||||
|
||||
impl LiveValuesEvent {
|
||||
pub fn build(&self) -> Event<Self> {
|
||||
Event::new("LiveValuesEvent", self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct StateEvent {
|
||||
pub is_default_state: bool,
|
||||
/// mode state
|
||||
pub mode_state: ModeState,
|
||||
pub flow_states: FlowStates,
|
||||
pub temperature_states: TempStates,
|
||||
}
|
||||
|
||||
impl StateEvent {
|
||||
pub fn build(&self) -> Event<Self> {
|
||||
Event::new("StateEvent", self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct TempStates {
|
||||
pub front: TempState,
|
||||
pub back: TempState,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct TempState {
|
||||
pub temperature: f64,
|
||||
pub target_temperature: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ModeState {
|
||||
pub mode: AquaPathV1Mode,
|
||||
}
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct FlowStates {
|
||||
pub front: FlowState,
|
||||
pub back: FlowState,
|
||||
}
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct FlowState {
|
||||
pub flow: f64,
|
||||
pub should_flow: bool,
|
||||
}
|
||||
|
||||
pub enum AquaPathV1Events {
|
||||
LiveValues(Event<LiveValuesEvent>),
|
||||
State(Event<StateEvent>),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
enum Mutation {
|
||||
//Mode
|
||||
SetAquaPathMode(AquaPathV1Mode),
|
||||
|
||||
SetFrontTemperature(f64),
|
||||
SetBackTemperature(f64),
|
||||
|
||||
SetFrontFlow(bool),
|
||||
SetBackFlow(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AquaPathV1Namespace {
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<AquaPathV1Events> for AquaPathV1Namespace {
|
||||
#[instrument(skip_all)]
|
||||
fn emit(&mut self, events: AquaPathV1Events) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
|
||||
let mut namespace = self.namespace.lock_blocking();
|
||||
namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheableEvents<AquaPathV1Events> for AquaPathV1Events {
|
||||
fn event_value(&self) -> GenericEvent {
|
||||
match self {
|
||||
AquaPathV1Events::LiveValues(event) => event.into(),
|
||||
AquaPathV1Events::State(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();
|
||||
|
||||
match self {
|
||||
AquaPathV1Events::LiveValues(_) => cache_one_hour,
|
||||
AquaPathV1Events::State(_) => cache_first_and_last,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineApi for AquaPathV1 {
|
||||
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
|
||||
let control: Mutation = serde_json::from_value(request_body)?;
|
||||
match control {
|
||||
Mutation::SetAquaPathMode(mode) => self.set_mode_state(mode),
|
||||
Mutation::SetBackTemperature(temperature) => {
|
||||
self.set_target_temperature(temperature, super::AquaPathSideType::Back)
|
||||
}
|
||||
|
||||
Mutation::SetFrontTemperature(temperature) => {
|
||||
self.set_target_temperature(temperature, super::AquaPathSideType::Front)
|
||||
}
|
||||
|
||||
Mutation::SetBackFlow(should_pump) => {
|
||||
self.set_should_pump(should_pump, super::AquaPathSideType::Back)
|
||||
}
|
||||
Mutation::SetFrontFlow(should_pump) => {
|
||||
self.set_should_pump(should_pump, super::AquaPathSideType::Front)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
use crate::machines::aquapath1::VolumeRate;
|
||||
use crate::machines::aquapath1::{Flow, Temperature};
|
||||
use control_core::controllers::pid::PidController;
|
||||
use ethercat_hal::io::encoder_input::EncoderInput;
|
||||
use ethercat_hal::io::{
|
||||
analog_output::AnalogOutput, digital_output::DigitalOutput, temperature_input::TemperatureInput,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use uom::si::f64::ThermodynamicTemperature;
|
||||
use uom::si::thermodynamic_temperature::degree_celsius;
|
||||
use uom::si::volume_rate::liter_per_minute;
|
||||
#[derive(Debug)]
|
||||
|
||||
pub struct Controller {
|
||||
pub pid: PidController,
|
||||
temperature_pid_output: f64,
|
||||
window_start: Instant,
|
||||
pwm_period: Duration,
|
||||
|
||||
pub temperature: Temperature,
|
||||
pub target_temperature: ThermodynamicTemperature,
|
||||
pub current_temperature: ThermodynamicTemperature,
|
||||
pub temp_reservoir: ThermodynamicTemperature,
|
||||
pub min_temperature: ThermodynamicTemperature,
|
||||
pub max_temperature: ThermodynamicTemperature,
|
||||
|
||||
pub cooling_controller: AnalogOutput,
|
||||
pub cooling_relais: DigitalOutput,
|
||||
|
||||
pub heating_relais_1: DigitalOutput,
|
||||
pub heating_relais_2: DigitalOutput,
|
||||
pub temperature_sensor_in: TemperatureInput,
|
||||
pub temperature_sensor_out: TemperatureInput,
|
||||
|
||||
pub cooling_allowed: bool,
|
||||
pub heating_allowed: bool,
|
||||
|
||||
pub flow: Flow,
|
||||
pump_relais: DigitalOutput,
|
||||
pub should_pump: bool,
|
||||
pub flow_sensor: EncoderInput,
|
||||
pub pump_allowed: bool,
|
||||
pub current_flow: VolumeRate,
|
||||
pub max_flow: VolumeRate,
|
||||
}
|
||||
|
||||
impl Controller {
|
||||
pub fn new(
|
||||
kp: f64,
|
||||
ki: f64,
|
||||
kd: f64,
|
||||
pwm_duration: Duration,
|
||||
temp: Temperature,
|
||||
target_tempetature: ThermodynamicTemperature,
|
||||
cooling_controller: AnalogOutput,
|
||||
cooling_relais: DigitalOutput,
|
||||
heating_relais_1: DigitalOutput,
|
||||
heating_relais_2: DigitalOutput,
|
||||
temp_sensor_in: TemperatureInput,
|
||||
temp_sensor_out: TemperatureInput,
|
||||
|
||||
flow: Flow,
|
||||
pump_relais: DigitalOutput,
|
||||
flow_sensor: EncoderInput,
|
||||
) -> Self {
|
||||
Self {
|
||||
pid: PidController::new(kp, ki, kd),
|
||||
temperature_pid_output: 0.0,
|
||||
window_start: Instant::now(),
|
||||
pwm_period: pwm_duration,
|
||||
target_temperature: target_tempetature,
|
||||
current_temperature: ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
temp_reservoir: ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
min_temperature: ThermodynamicTemperature::new::<degree_celsius>(10.0),
|
||||
max_temperature: ThermodynamicTemperature::new::<degree_celsius>(50.0),
|
||||
|
||||
temperature: temp,
|
||||
cooling_controller: cooling_controller,
|
||||
cooling_relais: cooling_relais,
|
||||
heating_relais_1: heating_relais_1,
|
||||
heating_relais_2: heating_relais_2,
|
||||
cooling_allowed: false,
|
||||
heating_allowed: false,
|
||||
temperature_sensor_in: temp_sensor_in,
|
||||
temperature_sensor_out: temp_sensor_out,
|
||||
|
||||
flow: flow,
|
||||
pump_relais: pump_relais,
|
||||
flow_sensor: flow_sensor,
|
||||
should_pump: false,
|
||||
current_flow: VolumeRate::new::<liter_per_minute>(0.0),
|
||||
pump_allowed: false,
|
||||
max_flow: VolumeRate::new::<liter_per_minute>(10.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn turn_pump_off(&mut self) {
|
||||
tracing::info!("turn_pump_off");
|
||||
|
||||
self.flow.pump = false;
|
||||
self.pump_relais.set(false);
|
||||
}
|
||||
|
||||
pub fn turn_pump_on(&mut self) {
|
||||
tracing::info!("turn_pump_on");
|
||||
|
||||
self.flow.pump = true;
|
||||
self.pump_relais.set(true);
|
||||
}
|
||||
|
||||
pub fn disallow_pump(&mut self) {
|
||||
self.pump_allowed = false;
|
||||
}
|
||||
|
||||
pub fn allow_pump(&mut self) {
|
||||
self.pump_allowed = true;
|
||||
}
|
||||
|
||||
pub fn get_pump(&mut self) -> bool {
|
||||
self.pump_allowed
|
||||
}
|
||||
|
||||
pub fn disable_cooling(&mut self) {
|
||||
self.turn_cooling_off();
|
||||
self.disallow_cooling();
|
||||
}
|
||||
|
||||
pub fn disable_heating(&mut self) {
|
||||
self.turn_heating_off();
|
||||
self.disallow_heating();
|
||||
}
|
||||
|
||||
pub fn enable_cooling(&mut self) {
|
||||
self.turn_cooling_on();
|
||||
self.allow_cooling();
|
||||
}
|
||||
|
||||
pub fn enable_heating(&mut self) {
|
||||
self.turn_heating_on();
|
||||
self.allow_heating();
|
||||
}
|
||||
pub fn reset_pid(&mut self) {
|
||||
self.pid.reset()
|
||||
}
|
||||
pub fn set_target_temperature(&mut self, temperature: ThermodynamicTemperature) {
|
||||
self.reset_pid();
|
||||
self.target_temperature = temperature;
|
||||
}
|
||||
|
||||
pub fn get_temp_in(&mut self) -> ThermodynamicTemperature {
|
||||
let temp = self.temperature_sensor_in.get_temperature();
|
||||
match temp {
|
||||
Ok(value) => ThermodynamicTemperature::new::<degree_celsius>(value),
|
||||
Err(_) => ThermodynamicTemperature::new::<degree_celsius>(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_temp_out(&mut self) -> ThermodynamicTemperature {
|
||||
let temp = self.temperature_sensor_out.get_temperature();
|
||||
match temp {
|
||||
Ok(value) => ThermodynamicTemperature::new::<degree_celsius>(value),
|
||||
Err(_) => ThermodynamicTemperature::new::<degree_celsius>(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disallow_cooling(&mut self) {
|
||||
self.cooling_allowed = false;
|
||||
}
|
||||
|
||||
pub fn allow_cooling(&mut self) {
|
||||
self.cooling_allowed = true;
|
||||
}
|
||||
|
||||
pub fn disallow_heating(&mut self) {
|
||||
self.heating_allowed = false;
|
||||
}
|
||||
|
||||
pub fn allow_heating(&mut self) {
|
||||
self.heating_allowed = true;
|
||||
}
|
||||
|
||||
pub fn turn_cooling_on(&mut self) {
|
||||
tracing::info!("turn_cooling_on");
|
||||
self.cooling_relais.set(true);
|
||||
self.cooling_controller.set(10.0);
|
||||
self.temperature.cooling = true;
|
||||
}
|
||||
|
||||
pub fn turn_cooling_off(&mut self) {
|
||||
tracing::info!("turn_cooling_off");
|
||||
|
||||
self.cooling_relais.set(false);
|
||||
self.temperature.cooling = false;
|
||||
}
|
||||
|
||||
pub fn turn_heating_on(&mut self) {
|
||||
tracing::info!("turn_heating_on");
|
||||
|
||||
self.heating_relais_1.set(true);
|
||||
self.heating_relais_2.set(true);
|
||||
self.temperature.heating = true;
|
||||
}
|
||||
|
||||
pub fn turn_heating_off(&mut self) {
|
||||
tracing::info!("turn_heating_off");
|
||||
|
||||
self.heating_relais_1.set(false);
|
||||
self.heating_relais_2.set(false);
|
||||
self.temperature.heating = false;
|
||||
}
|
||||
|
||||
pub fn set_should_pump(&mut self, should_pump: bool) {
|
||||
self.should_pump = should_pump;
|
||||
}
|
||||
|
||||
pub fn get_should_pump(&mut self) -> bool {
|
||||
self.should_pump
|
||||
}
|
||||
|
||||
pub fn get_flow(&mut self) -> VolumeRate {
|
||||
let value = match self.flow_sensor.get_frequency_value() {
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
tracing::debug!("Error calculating frequency: {}", e);
|
||||
return VolumeRate::new::<liter_per_minute>(0.0);
|
||||
}
|
||||
};
|
||||
|
||||
match value {
|
||||
Some(val) => {
|
||||
if val == 0 {
|
||||
return VolumeRate::new::<liter_per_minute>(0.0);
|
||||
}
|
||||
// Formula: f = 8.1*q - 3, so q = (f + 3) / 8.1
|
||||
let actual_flow = ((val / 100) as f32 + 3.0) / 8.1;
|
||||
VolumeRate::new::<liter_per_minute>(actual_flow.into())
|
||||
}
|
||||
None => {
|
||||
return VolumeRate::new::<liter_per_minute>(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, now: Instant) -> () {
|
||||
let current_flow = self.get_flow();
|
||||
self.current_flow = current_flow;
|
||||
self.flow.flow = current_flow;
|
||||
|
||||
let should_flow = self.get_should_pump();
|
||||
self.flow.should_pump = should_flow;
|
||||
|
||||
if !self.flow.pump && self.get_pump() && should_flow {
|
||||
self.turn_pump_on();
|
||||
} else if self.flow.pump && (!self.get_pump() || !should_flow) {
|
||||
self.turn_pump_off();
|
||||
}
|
||||
|
||||
self.current_temperature = self.get_temp_in();
|
||||
self.temp_reservoir = self.get_temp_out();
|
||||
|
||||
if self.current_temperature < self.min_temperature && self.temperature.cooling {
|
||||
self.turn_cooling_off();
|
||||
} else if self.current_temperature > self.max_temperature && self.temperature.heating {
|
||||
self.turn_heating_off();
|
||||
}
|
||||
|
||||
// Calculate PID error once
|
||||
let error = self.target_temperature.get::<degree_celsius>()
|
||||
- self.current_temperature.get::<degree_celsius>();
|
||||
let control = self.pid.update(error, now);
|
||||
self.temperature_pid_output = control;
|
||||
|
||||
let elapsed = now.duration_since(self.window_start);
|
||||
if elapsed >= self.pwm_period {
|
||||
self.window_start = now;
|
||||
}
|
||||
|
||||
// Decide whether to heat or cool based on error
|
||||
if error > 0.0 {
|
||||
// Need heating (current < target)
|
||||
if self.temperature.cooling {
|
||||
self.turn_cooling_off();
|
||||
}
|
||||
if self.heating_allowed && current_flow > VolumeRate::new::<liter_per_minute>(0.0) {
|
||||
// Only start heating if pump is on
|
||||
let duty = control.clamp(0.0, 1.0);
|
||||
let on_time = self.pwm_period.mul_f64(duty);
|
||||
let on = elapsed < on_time;
|
||||
if on && !self.temperature.heating {
|
||||
self.turn_heating_on();
|
||||
} else if !on && self.temperature.heating {
|
||||
self.turn_heating_off();
|
||||
}
|
||||
} else {
|
||||
// Pump is off or heating not allowed - don't heat
|
||||
if self.temperature.heating {
|
||||
self.turn_heating_off();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Need cooling (current > target)
|
||||
if self.temperature.heating {
|
||||
self.turn_heating_off();
|
||||
}
|
||||
if self.cooling_allowed && !self.temperature.cooling {
|
||||
self.turn_cooling_on();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
use control_core::{
|
||||
machines::identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
|
||||
use control_core_derive::Machine;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
use uom::si::{
|
||||
f64::{ThermodynamicTemperature, VolumeRate},
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
volume_rate::liter_per_minute,
|
||||
};
|
||||
|
||||
use crate::machines::{
|
||||
MACHINE_AQUAPATH_V1, VENDOR_QITECH,
|
||||
aquapath1::{
|
||||
api::{
|
||||
AquaPathV1Events, AquaPathV1Namespace, FlowState, FlowStates, LiveValuesEvent,
|
||||
ModeState, StateEvent, TempState, TempStates,
|
||||
},
|
||||
controller::Controller,
|
||||
// flow_controller::FlowController,
|
||||
// temperature_controller::TemperatureController,
|
||||
},
|
||||
};
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod controller;
|
||||
// pub mod flow_controller;
|
||||
pub mod new;
|
||||
// pub mod temperature_controller;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
||||
pub enum AquaPathV1Mode {
|
||||
Standby,
|
||||
Auto,
|
||||
}
|
||||
|
||||
pub enum AquaPathSideType {
|
||||
Front,
|
||||
Back,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Temperature {
|
||||
pub temperature: ThermodynamicTemperature,
|
||||
pub cooling: bool,
|
||||
pub heating: bool,
|
||||
pub target_temperature: ThermodynamicTemperature,
|
||||
}
|
||||
|
||||
impl Default for Temperature {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
temperature: ThermodynamicTemperature::new::<degree_celsius>(0.0),
|
||||
cooling: false,
|
||||
heating: false,
|
||||
target_temperature: ThermodynamicTemperature::new::<degree_celsius>(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Flow {
|
||||
pub flow: VolumeRate,
|
||||
pub pump: bool,
|
||||
pub should_pump: bool,
|
||||
}
|
||||
|
||||
impl Default for Flow {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
flow: VolumeRate::new::<liter_per_minute>(0.0),
|
||||
pump: false,
|
||||
should_pump: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Machine)]
|
||||
pub struct AquaPathV1 {
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
namespace: AquaPathV1Namespace,
|
||||
mode: AquaPathV1Mode,
|
||||
last_measurement_emit: Instant,
|
||||
front_controller: Controller,
|
||||
back_controller: Controller,
|
||||
}
|
||||
|
||||
impl AquaPathV1 {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_AQUAPATH_V1,
|
||||
};
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AquaPathV1 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Aquapath")
|
||||
}
|
||||
}
|
||||
|
||||
impl AquaPathV1 {
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let live_values = LiveValuesEvent {
|
||||
front_temperature: self
|
||||
.front_controller
|
||||
.current_temperature
|
||||
.get::<degree_celsius>(),
|
||||
back_temperature: self
|
||||
.back_controller
|
||||
.current_temperature
|
||||
.get::<degree_celsius>(),
|
||||
front_flow: self.front_controller.current_flow.get::<liter_per_minute>(),
|
||||
back_flow: self.back_controller.current_flow.get::<liter_per_minute>(),
|
||||
front_temp_reservoir: self.front_controller.temp_reservoir.get::<degree_celsius>(),
|
||||
back_temp_reservoir: self.back_controller.temp_reservoir.get::<degree_celsius>(),
|
||||
};
|
||||
let event = live_values.build();
|
||||
self.namespace.emit(AquaPathV1Events::LiveValues(event));
|
||||
}
|
||||
|
||||
pub fn emit_state(&mut self) {
|
||||
let state = StateEvent {
|
||||
is_default_state: false,
|
||||
mode_state: ModeState {
|
||||
mode: self.mode.clone(),
|
||||
},
|
||||
temperature_states: TempStates {
|
||||
front: TempState {
|
||||
temperature: self
|
||||
.front_controller
|
||||
.current_temperature
|
||||
.get::<degree_celsius>(),
|
||||
|
||||
target_temperature: self
|
||||
.front_controller
|
||||
.target_temperature
|
||||
.get::<degree_celsius>(),
|
||||
},
|
||||
back: TempState {
|
||||
temperature: self
|
||||
.back_controller
|
||||
.current_temperature
|
||||
.get::<degree_celsius>(),
|
||||
target_temperature: self
|
||||
.back_controller
|
||||
.target_temperature
|
||||
.get::<degree_celsius>(),
|
||||
},
|
||||
},
|
||||
flow_states: FlowStates {
|
||||
front: FlowState {
|
||||
flow: self.front_controller.current_flow.get::<liter_per_minute>(),
|
||||
should_flow: self.front_controller.should_pump,
|
||||
},
|
||||
back: FlowState {
|
||||
flow: self.back_controller.current_flow.get::<liter_per_minute>(),
|
||||
should_flow: self.back_controller.should_pump,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let event = state.build();
|
||||
self.namespace.emit(AquaPathV1Events::State(event));
|
||||
}
|
||||
}
|
||||
impl AquaPathV1 {
|
||||
fn turn_cooling_off(&mut self) {
|
||||
self.front_controller.disable_cooling();
|
||||
self.back_controller.disable_cooling();
|
||||
}
|
||||
|
||||
fn turn_cooling_on(&mut self) {
|
||||
self.front_controller.allow_cooling();
|
||||
self.back_controller.allow_cooling();
|
||||
}
|
||||
|
||||
fn turn_heating_off(&mut self) {
|
||||
self.front_controller.disallow_heating();
|
||||
self.back_controller.disallow_heating();
|
||||
}
|
||||
|
||||
fn turn_heating_on(&mut self) {
|
||||
self.front_controller.allow_heating();
|
||||
self.back_controller.allow_heating();
|
||||
}
|
||||
|
||||
fn turn_pump_on(&mut self) {
|
||||
self.front_controller.allow_pump();
|
||||
self.back_controller.allow_pump();
|
||||
}
|
||||
|
||||
fn turn_pump_off(&mut self) {
|
||||
self.front_controller.disallow_pump();
|
||||
self.back_controller.disallow_pump();
|
||||
}
|
||||
|
||||
fn turn_off_all(&mut self) {
|
||||
self.turn_cooling_off();
|
||||
self.turn_heating_off();
|
||||
self.turn_pump_off();
|
||||
}
|
||||
|
||||
fn turn_on_all(&mut self) {
|
||||
self.turn_cooling_on();
|
||||
self.turn_heating_on();
|
||||
self.turn_pump_on();
|
||||
}
|
||||
// Turn all OFF and do nothing
|
||||
fn switch_to_standby(&mut self) {
|
||||
match self.mode {
|
||||
AquaPathV1Mode::Standby => (),
|
||||
AquaPathV1Mode::Auto => self.turn_off_all(),
|
||||
};
|
||||
self.mode = AquaPathV1Mode::Standby;
|
||||
}
|
||||
|
||||
fn switch_to_auto(&mut self) {
|
||||
match self.mode {
|
||||
AquaPathV1Mode::Auto => (),
|
||||
AquaPathV1Mode::Standby => self.turn_on_all(),
|
||||
}
|
||||
self.mode = AquaPathV1Mode::Auto;
|
||||
}
|
||||
|
||||
fn switch_mode(&mut self, mode: AquaPathV1Mode) {
|
||||
if self.mode == mode {
|
||||
return;
|
||||
}
|
||||
match mode {
|
||||
AquaPathV1Mode::Standby => self.switch_to_standby(),
|
||||
AquaPathV1Mode::Auto => self.switch_to_auto(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AquaPathV1 {
|
||||
fn set_mode_state(&mut self, mode: AquaPathV1Mode) {
|
||||
self.switch_mode(mode.clone());
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
impl AquaPathV1 {
|
||||
fn set_target_temperature(&mut self, temperature: f64, cooling_type: AquaPathSideType) {
|
||||
let target_temp = ThermodynamicTemperature::new::<degree_celsius>(temperature);
|
||||
|
||||
match cooling_type {
|
||||
AquaPathSideType::Back => self.back_controller.set_target_temperature(target_temp),
|
||||
AquaPathSideType::Front => self.front_controller.set_target_temperature(target_temp),
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
fn set_should_pump(&mut self, should_pump: bool, cooling_type: AquaPathSideType) {
|
||||
match cooling_type {
|
||||
AquaPathSideType::Back => self.back_controller.set_should_pump(should_pump),
|
||||
AquaPathSideType::Front => self.front_controller.set_should_pump(should_pump),
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
use super::{AquaPathV1, AquaPathV1Mode};
|
||||
use crate::machines::aquapath1::{
|
||||
Flow, Temperature, api::AquaPathV1Namespace, controller::Controller,
|
||||
};
|
||||
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 ethercat_hal::{
|
||||
coe::ConfigurableDevice,
|
||||
devices::{
|
||||
EthercatDeviceUsed, downcast_device,
|
||||
ek1100::{EK1100, EK1100_IDENTITY_A},
|
||||
el2008::{EL2008, EL2008_IDENTITY_A, EL2008Port},
|
||||
el3204::{EL3204, EL3204_IDENTITY_A, EL3204_IDENTITY_B, EL3204Port},
|
||||
el4002::{EL4002, EL4002_IDENTITY_A, EL4002Port},
|
||||
el5152::{
|
||||
EL5152, EL5152_IDENTITY_A, EL5152Configuration, EL5152Port,
|
||||
EL5152PredefinedPdoAssignment,
|
||||
},
|
||||
subdevice_identity_to_tuple,
|
||||
},
|
||||
io::{
|
||||
analog_output::AnalogOutput, digital_output::DigitalOutput, encoder_input::EncoderInput,
|
||||
temperature_input::TemperatureInput,
|
||||
},
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use uom::si::{f64::ThermodynamicTemperature, thermodynamic_temperature::degree_celsius};
|
||||
|
||||
impl MachineNewTrait for AquaPathV1 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params
|
||||
.device_group
|
||||
.iter()
|
||||
.map(|device_identification| device_identification.clone())
|
||||
.collect::<Vec<_>>();
|
||||
validate_same_machine_identification_unique(&device_identification)?;
|
||||
validate_no_role_dublicates(&device_identification)?;
|
||||
|
||||
let hardware = match ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
smol::block_on(async {
|
||||
// Role 0 - Buscoupler EK1100
|
||||
{
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 0)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 0 is not Ethercat",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EK1100_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
&hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EK1100>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 0 is not an EK1100",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Role 1 - EL2008 Digital Output Module
|
||||
let el2008 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 1)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 1 is not Ethercat",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL2008_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
&hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL2008>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 1 is not an EL2008",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
};
|
||||
|
||||
// Role 2 - EL4002 Analog Output Module
|
||||
let el4002 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 2)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 2 is not Ethercat",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
|
||||
let subdevice = get_subdevice_by_index(
|
||||
hardware.subdevices,
|
||||
device_hardware_identification_ethercat.subdevice_index,
|
||||
)?;
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL4002_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
&hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL4002>(ethercat_device).await?
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 2 is not an EL4002",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
};
|
||||
let el3204 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 3)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 3 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL3204_IDENTITY_A | EL3204_IDENTITY_B => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
&hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL3204>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 3 is not an EL3204",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
};
|
||||
|
||||
let el5152 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 4)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 4 is not Ethercat",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL5152_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
&hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL5152>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/AquaPath1::new] Device with role 4 is not an EL5152",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let config = EL5152Configuration {
|
||||
pdo_assignment: EL5152PredefinedPdoAssignment::Frequency,
|
||||
..Default::default()
|
||||
};
|
||||
device
|
||||
.write()
|
||||
.await
|
||||
.write_config(&subdevice, &config)
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
};
|
||||
let enc1 = EncoderInput::new(el5152.clone(), EL5152Port::ENC1);
|
||||
|
||||
let enc2 = EncoderInput::new(el5152.clone(), EL5152Port::ENC2);
|
||||
//after heating
|
||||
let t1 = TemperatureInput::new(el3204.clone(), EL3204Port::T1);
|
||||
//in reservoir
|
||||
let t2 = TemperatureInput::new(el3204.clone(), EL3204Port::T2);
|
||||
//after heating
|
||||
let t3 = TemperatureInput::new(el3204.clone(), EL3204Port::T3);
|
||||
//in reservoir
|
||||
let t4 = TemperatureInput::new(el3204.clone(), EL3204Port::T4);
|
||||
//pump flow control
|
||||
//phys 1
|
||||
let do1 = DigitalOutput::new(el2008.clone(), EL2008Port::DO1);
|
||||
//phys 5
|
||||
let do2 = DigitalOutput::new(el2008.clone(), EL2008Port::DO2);
|
||||
//heating
|
||||
//phys 2
|
||||
let do3 = DigitalOutput::new(el2008.clone(), EL2008Port::DO3);
|
||||
//phys 6
|
||||
let do4 = DigitalOutput::new(el2008.clone(), EL2008Port::DO4);
|
||||
//phys 3
|
||||
let do5 = DigitalOutput::new(el2008.clone(), EL2008Port::DO5);
|
||||
//phys 7
|
||||
let do6 = DigitalOutput::new(el2008.clone(), EL2008Port::DO6);
|
||||
//cooling power cut
|
||||
//phys 4
|
||||
let do7 = DigitalOutput::new(el2008.clone(), EL2008Port::DO7);
|
||||
//phys 8
|
||||
let do8 = DigitalOutput::new(el2008.clone(), EL2008Port::DO8);
|
||||
|
||||
let ao1 = AnalogOutput::new(el4002.clone(), EL4002Port::AO1);
|
||||
let ao2 = AnalogOutput::new(el4002.clone(), EL4002Port::AO2);
|
||||
|
||||
let front_controller = Controller::new(
|
||||
0.10,
|
||||
0.0,
|
||||
0.015,
|
||||
Duration::from_millis(500),
|
||||
Temperature::default(),
|
||||
ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
ao1,
|
||||
do7,
|
||||
do3,
|
||||
do5,
|
||||
t1,
|
||||
t2,
|
||||
Flow::default(),
|
||||
do1,
|
||||
enc1,
|
||||
);
|
||||
|
||||
let back_controller = Controller::new(
|
||||
0.10,
|
||||
0.0,
|
||||
0.015,
|
||||
Duration::from_millis(500),
|
||||
Temperature::default(),
|
||||
ThermodynamicTemperature::new::<degree_celsius>(25.0),
|
||||
ao2,
|
||||
do8,
|
||||
do4,
|
||||
do6,
|
||||
t3,
|
||||
t4,
|
||||
Flow::default(),
|
||||
do2,
|
||||
enc2,
|
||||
);
|
||||
|
||||
let mut water_cooling = Self {
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: AquaPathV1Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
mode: AquaPathV1Mode::Standby,
|
||||
last_measurement_emit: Instant::now(),
|
||||
front_controller,
|
||||
back_controller,
|
||||
};
|
||||
water_cooling.emit_state();
|
||||
|
||||
Ok(water_cooling)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use super::BufferV1;
|
||||
use control_core::machines::new::MachineAct;
|
||||
|
||||
impl MachineAct for BufferV1 {
|
||||
fn act(&mut self, now: Instant) {
|
||||
// if last measurement is older than 1 second, emit a new measurement
|
||||
|
||||
@@ -2,7 +2,10 @@ use std::{sync::Arc, time::Duration};
|
||||
|
||||
use super::{BufferV1, BufferV1Mode};
|
||||
use control_core::{
|
||||
machines::{api::MachineApi, identification::MachineIdentificationUnique},
|
||||
machines::{
|
||||
api::MachineApi, connection::MachineCrossConnectionState,
|
||||
identification::MachineIdentificationUnique,
|
||||
},
|
||||
socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
@@ -13,8 +16,7 @@ use control_core::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use smol::lock::Mutex;
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
@@ -31,7 +33,7 @@ pub struct StateEvent {
|
||||
/// mode state
|
||||
pub mode_state: ModeState,
|
||||
/// connected machine state
|
||||
pub connected_machine_state: ConnectedMachineState,
|
||||
pub connected_machine_state: MachineCrossConnectionState,
|
||||
}
|
||||
|
||||
impl StateEvent {
|
||||
@@ -78,7 +80,7 @@ enum Mutation {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Buffer1Namespace {
|
||||
pub namespace: Namespace,
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<BufferV1Events> for Buffer1Namespace {
|
||||
@@ -86,15 +88,9 @@ impl NamespaceCacheingLogic<BufferV1Events> for Buffer1Namespace {
|
||||
fn emit(&mut self, events: BufferV1Events) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
self.namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer1Namespace {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
Self {
|
||||
namespace: Namespace::new(socket_queue_tx),
|
||||
}
|
||||
let mut namespace = self.namespace.lock_blocking();
|
||||
namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +128,7 @@ impl MachineApi for BufferV1 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace {
|
||||
&mut self.namespace.namespace
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -5,26 +5,20 @@ pub mod new;
|
||||
|
||||
use api::{Buffer1Namespace, BufferV1Events, LiveValuesEvent, ModeState, StateEvent};
|
||||
use buffer_tower_controller::BufferTowerController;
|
||||
use control_core::machines::connection::{CrossConnectableMachine, MachineCrossConnection};
|
||||
use control_core::{
|
||||
machines::{
|
||||
ConnectedMachine, ConnectedMachineData, downcast_machine,
|
||||
identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
manager::MachineManager,
|
||||
},
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
use control_core_derive::Machine;
|
||||
use futures::executor::block_on;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::lock::{Mutex, RwLock};
|
||||
use std::{
|
||||
sync::{Arc, Weak},
|
||||
time::Instant,
|
||||
};
|
||||
use smol::lock::RwLock;
|
||||
use std::{sync::Weak, time::Instant};
|
||||
|
||||
use crate::machines::{
|
||||
MACHINE_BUFFER_V1, VENDOR_QITECH, buffer1::api::ConnectedMachineState, winder2::Winder2,
|
||||
};
|
||||
use crate::machines::{MACHINE_BUFFER_V1, VENDOR_QITECH, winder2::Winder2};
|
||||
|
||||
#[derive(Debug, Machine)]
|
||||
pub struct BufferV1 {
|
||||
@@ -40,12 +34,18 @@ pub struct BufferV1 {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
|
||||
// connected machines
|
||||
pub connected_winder: Option<ConnectedMachine<Weak<Mutex<Winder2>>>>,
|
||||
pub connected_winder: MachineCrossConnection<BufferV1, Winder2>,
|
||||
|
||||
// mode
|
||||
mode: BufferV1Mode,
|
||||
}
|
||||
|
||||
impl CrossConnectableMachine<BufferV1, Winder2> for BufferV1 {
|
||||
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<BufferV1, Winder2> {
|
||||
&mut self.connected_winder
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BufferV1 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "BufferV1")
|
||||
@@ -57,9 +57,6 @@ impl BufferV1 {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_BUFFER_V1,
|
||||
};
|
||||
}
|
||||
|
||||
impl BufferV1 {
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let live_values = LiveValuesEvent {};
|
||||
|
||||
@@ -72,28 +69,13 @@ impl BufferV1 {
|
||||
mode_state: ModeState {
|
||||
mode: self.mode.clone(),
|
||||
},
|
||||
connected_machine_state: ConnectedMachineState {
|
||||
machine_identification_unique: self.connected_winder.as_ref().map(
|
||||
|connected_machine| {
|
||||
ConnectedMachineData::from(connected_machine).machine_identification_unique
|
||||
},
|
||||
),
|
||||
is_available: self
|
||||
.connected_winder
|
||||
.as_ref()
|
||||
.map(|connected_machine| {
|
||||
ConnectedMachineData::from(connected_machine).is_available
|
||||
})
|
||||
.unwrap_or(false),
|
||||
},
|
||||
connected_machine_state: self.connected_winder.to_state(),
|
||||
};
|
||||
|
||||
let event = state.build();
|
||||
self.namespace.emit(BufferV1Events::State(event));
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferV1 {
|
||||
// To be implemented
|
||||
fn fill_buffer(&mut self) {
|
||||
todo!();
|
||||
@@ -146,17 +128,13 @@ impl BufferV1 {
|
||||
BufferV1Mode::EmptyingBuffer => self.switch_to_emptying(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferV1 {
|
||||
fn set_mode_state(&mut self, mode: BufferV1Mode) {
|
||||
self.switch_mode(mode);
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
/// Connecting/Disconnecting machine
|
||||
impl BufferV1 {
|
||||
/// Connecting/Disconnecting machine
|
||||
/// set connected winder
|
||||
pub fn set_connected_winder(
|
||||
&mut self,
|
||||
@@ -168,34 +146,13 @@ impl BufferV1 {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let machine_manager_arc = match self.machine_manager.upgrade() {
|
||||
Some(machine_manager_arc) => machine_manager_arc,
|
||||
None => return,
|
||||
};
|
||||
let machine_manager_guard = block_on(machine_manager_arc.read());
|
||||
let winder2_weak = machine_manager_guard.get_machine_weak(&machine_identification_unique);
|
||||
let winder2_weak = match winder2_weak {
|
||||
Some(winder2_weak) => winder2_weak,
|
||||
None => return,
|
||||
};
|
||||
let winder2_strong = match winder2_weak.upgrade() {
|
||||
Some(winder2_strong) => winder2_strong,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let winder2: Arc<Mutex<Winder2>> = block_on(downcast_machine::<Winder2>(winder2_strong))
|
||||
.expect("failed downcasting machine");
|
||||
|
||||
let machine = Arc::downgrade(&winder2);
|
||||
|
||||
self.connected_winder = Some(ConnectedMachine {
|
||||
machine_identification_unique,
|
||||
machine,
|
||||
});
|
||||
self.connected_winder
|
||||
.set_connected_machine(&machine_identification_unique);
|
||||
|
||||
self.emit_state();
|
||||
|
||||
self.reverse_connect();
|
||||
self.connected_winder.reverse_connect();
|
||||
}
|
||||
|
||||
/// disconnect winder
|
||||
@@ -209,36 +166,9 @@ impl BufferV1 {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if let Some(connected) = &self.connected_winder {
|
||||
if let Some(winder2_arc) = connected.machine.upgrade() {
|
||||
let future = async move {
|
||||
let mut winder2 = winder2_arc.lock().await;
|
||||
if winder2.connected_buffer.is_some() {
|
||||
winder2.connected_buffer = None;
|
||||
winder2.emit_state();
|
||||
}
|
||||
};
|
||||
smol::spawn(future).detach();
|
||||
}
|
||||
}
|
||||
self.connected_winder = None;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// initiate connection from winder to buffer
|
||||
pub fn reverse_connect(&mut self) {
|
||||
let machine_identification_unique = self.machine_identification_unique.clone();
|
||||
if let Some(connected) = &self.connected_winder {
|
||||
if let Some(winder2_arc) = connected.machine.upgrade() {
|
||||
let future = async move {
|
||||
let mut winder2 = winder2_arc.lock().await;
|
||||
if winder2.connected_buffer.is_none() {
|
||||
winder2.set_connected_buffer(machine_identification_unique);
|
||||
}
|
||||
};
|
||||
smol::spawn(future).detach();
|
||||
}
|
||||
}
|
||||
self.connected_winder.disconnect();
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+106
-190
@@ -1,208 +1,125 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Error;
|
||||
use control_core::machines::identification::DeviceHardwareIdentification;
|
||||
use control_core::machines::connection::MachineCrossConnection;
|
||||
use control_core::machines::new::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_device_identification_by_role,
|
||||
get_ethercat_device_by_index, get_subdevice_by_index, validate_no_role_dublicates,
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
|
||||
validate_same_machine_identification_unique,
|
||||
};
|
||||
use ethercat_hal::coe::ConfigurableDevice;
|
||||
use ethercat_hal::devices::ek1100::EK1100;
|
||||
use ethercat_hal::devices::ek1100::EK1100_IDENTITY_A;
|
||||
use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
|
||||
use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
|
||||
use ethercat_hal::devices::el7031_0030::{self, EL7031_0030, EL7031_0030_IDENTITY_A};
|
||||
use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration;
|
||||
use ethercat_hal::devices::el7041_0052::{EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port};
|
||||
use ethercat_hal::devices::{EthercatDeviceUsed, downcast_device, subdevice_identity_to_tuple};
|
||||
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
|
||||
use ethercat_hal::shared_config;
|
||||
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
|
||||
use ethercat_hal::{
|
||||
coe::ConfigurableDevice,
|
||||
devices::el7031_0030::coe::EL7031_0030Configuration,
|
||||
devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment,
|
||||
devices::el7041_0052::coe::EL7041_0052Configuration,
|
||||
devices::{
|
||||
EthercatDeviceUsed,
|
||||
ek1100::{EK1100, EK1100_IDENTITY_A},
|
||||
el7031_0030::{EL7031_0030, EL7031_0030_IDENTITY_A},
|
||||
el7041_0052::{EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port},
|
||||
},
|
||||
io::stepper_velocity_el70x1::StepperVelocityEL70x1,
|
||||
shared_config,
|
||||
shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration},
|
||||
};
|
||||
|
||||
use crate::machines::buffer1::BufferV1Mode;
|
||||
use crate::machines::buffer1::buffer_tower_controller::BufferTowerController;
|
||||
use crate::machines::get_ethercat_device;
|
||||
|
||||
use super::{BufferV1, api::Buffer1Namespace};
|
||||
|
||||
impl MachineNewTrait for BufferV1 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
fn new<'maindevice>(
|
||||
params: &MachineNewParams<'maindevice, '_, '_, '_, '_, '_, '_>,
|
||||
) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
let device_identification = params.device_group.to_vec();
|
||||
|
||||
validate_same_machine_identification_unique(&device_identification)?;
|
||||
validate_no_role_dublicates(&device_identification)?;
|
||||
|
||||
let hardware = match ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
let hardware: &&control_core::machines::new::MachineNewHardwareEthercat<'_, '_, '_> =
|
||||
match ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
smol::block_on(async {
|
||||
// Role 0
|
||||
// Buscoupler
|
||||
// EK1100
|
||||
// Role 0 - Buscoupler EK1100
|
||||
let _ek1100 =
|
||||
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
// Role 1 - Stepper Buffer EL7041-0052
|
||||
let (el7041, subdevice) = get_ethercat_device::<EL7041_0052>(
|
||||
hardware,
|
||||
params,
|
||||
1,
|
||||
[EL7041_0052_IDENTITY_A].to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let el7041_config = EL7041_0052Configuration {
|
||||
stm_features: shared_config::el70x1::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
..Default::default()
|
||||
},
|
||||
stm_motor: StmMotorConfiguration {
|
||||
max_current: 6000,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
el7041
|
||||
.write()
|
||||
.await
|
||||
.write_config(&subdevice, &el7041_config)
|
||||
.await?;
|
||||
{
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 0)?;
|
||||
let device_hardware_identification_ethercat =
|
||||
match &device_identification.device_hardware_identification {
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] Device with role 0 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EK1100_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EK1100>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] Device with role 0 is not an EK1100",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
let mut device_guard = el7041.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
|
||||
// Role 1
|
||||
// 1x Stepper Buffer
|
||||
// EL7041-0052
|
||||
let (el7041, _el7041_config) = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 1)?;
|
||||
let device_hardware_identification_ethercat =
|
||||
match &device_identification.device_hardware_identification {
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] Device with role 1 is not Ethercat",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let subdevice = get_subdevice_by_index(
|
||||
hardware.subdevices,
|
||||
device_hardware_identification_ethercat.subdevice_index,
|
||||
)?;
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL7041_0052_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL7041_0052>(ethercat_device).await?
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] Device with role 1 is not an EL7041-0052",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let config = EL7041_0052Configuration {
|
||||
stm_features: shared_config::el70x1::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
..Default::default()
|
||||
},
|
||||
stm_motor: StmMotorConfiguration {
|
||||
max_current: 6000,
|
||||
..Default::default()
|
||||
},
|
||||
// Role 2 - Stepper Puller EL7031-0030
|
||||
let (el7031, subdevice) = get_ethercat_device::<EL7031_0030>(
|
||||
hardware,
|
||||
params,
|
||||
2,
|
||||
[EL7031_0030_IDENTITY_A].to_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let el7031_config = EL7031_0030Configuration {
|
||||
stm_features: ethercat_hal::devices::el7031_0030::coe::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
// Max Speed of 1000 steps/s
|
||||
// Max @ 8cm diameter = approx 75 m/min
|
||||
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
|
||||
..Default::default()
|
||||
};
|
||||
device
|
||||
.write()
|
||||
.await
|
||||
.write_config(subdevice, &config)
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
(device, config)
|
||||
},
|
||||
stm_motor: StmMotorConfiguration {
|
||||
max_current: 1500,
|
||||
..Default::default()
|
||||
},
|
||||
pdo_assignment: EL7031_0030PredefinedPdoAssignment::VelocityControlCompact,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Role 2
|
||||
// 1x Stepper Puller
|
||||
// EL7031
|
||||
let (_el7031_0030, _el7031_0030_config) = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 2)?;
|
||||
let device_hardware_identification_ethercat =
|
||||
match &device_identification.device_hardware_identification {
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer::new] Device with role 2 is not Ethercat",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let subdevice = get_subdevice_by_index(
|
||||
hardware.subdevices,
|
||||
device_hardware_identification_ethercat.subdevice_index,
|
||||
)?;
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL7031_0030_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL7031_0030>(ethercat_device).await?
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Buffer1::new] Device with role 2 is not an EL7031-0030",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let config = EL7031_0030Configuration {
|
||||
stm_features: el7031_0030::coe::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
// Max Speed of 1000 steps/s
|
||||
// Max @ 8cm diameter = approx 75 m/min
|
||||
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
|
||||
..Default::default()
|
||||
},
|
||||
stm_motor: StmMotorConfiguration {
|
||||
max_current: 1500,
|
||||
..Default::default()
|
||||
},
|
||||
pdo_assignment: EL7031_0030PredefinedPdoAssignment::VelocityControlCompact,
|
||||
..Default::default()
|
||||
};
|
||||
device
|
||||
.write()
|
||||
.await
|
||||
.write_config(subdevice, &config)
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
(device, config)
|
||||
};
|
||||
el7031
|
||||
.write()
|
||||
.await
|
||||
.write_config(&subdevice, &el7031_config)
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = el7031.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
|
||||
// Controller
|
||||
let buffer_tower_controller = BufferTowerController::new(StepperVelocityEL70x1::new(
|
||||
@@ -210,23 +127,22 @@ impl MachineNewTrait for BufferV1 {
|
||||
EL7041_0052Port::STM1,
|
||||
));
|
||||
|
||||
let machine_id = params
|
||||
.device_group
|
||||
.first()
|
||||
.expect("device group must have at least one device")
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone();
|
||||
let machine_identification_unique = params.get_machine_identification_unique();
|
||||
|
||||
// create buffer instance
|
||||
let mut buffer: Self = Self {
|
||||
namespace: Buffer1Namespace::new(params.socket_queue_tx.clone()),
|
||||
let mut buffer: BufferV1 = Self {
|
||||
namespace: Buffer1Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_measurement_emit: Instant::now(),
|
||||
mode: BufferV1Mode::Standby,
|
||||
buffer_tower_controller,
|
||||
machine_manager: params.machine_manager.clone(),
|
||||
machine_identification_unique: machine_id,
|
||||
connected_winder: None,
|
||||
machine_identification_unique: machine_identification_unique.clone(),
|
||||
connected_winder: MachineCrossConnection::new(
|
||||
params.machine_manager.clone(),
|
||||
&machine_identification_unique,
|
||||
),
|
||||
};
|
||||
buffer.emit_state();
|
||||
Ok(buffer)
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
use super::{ExtruderV2Mode, HeatingType, mitsubishi_cs80::MotorStatus};
|
||||
use super::{ExtruderV2Mode, mitsubishi_cs80::MotorStatus};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use super::ExtruderV2;
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use super::mock::ExtruderV2;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::machines::extruder1::HeatingType;
|
||||
|
||||
use control_core::{
|
||||
machines::api::MachineApi,
|
||||
socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
|
||||
cache_first_and_last_event,
|
||||
},
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use control_core::machines::api::MachineApi;
|
||||
use control_core::socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
|
||||
cache_first_and_last_event,
|
||||
},
|
||||
};
|
||||
use control_core_derive::BuildEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use serde_json::Value;
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use smol::lock::Mutex;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tracing::instrument;
|
||||
use uom::si::{
|
||||
@@ -225,7 +224,7 @@ pub enum Mutation {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExtruderV2Namespace {
|
||||
pub namespace: Namespace,
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<ExtruderV2Events> for ExtruderV2Namespace {
|
||||
@@ -233,15 +232,9 @@ impl NamespaceCacheingLogic<ExtruderV2Events> for ExtruderV2Namespace {
|
||||
fn emit(&mut self, events: ExtruderV2Events) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
self.namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtruderV2Namespace {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
Self {
|
||||
namespace: Namespace::new(socket_queue_tx),
|
||||
}
|
||||
let mut namespace = self.namespace.lock_blocking();
|
||||
namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +296,7 @@ impl MachineApi for ExtruderV2 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace {
|
||||
&mut self.namespace.namespace
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,21 @@ use crate::machines::extruder1::{
|
||||
RotationState, ScrewState, StateEvent,
|
||||
},
|
||||
};
|
||||
use control_core::{
|
||||
helpers::hasher_serializer::hash_with_serde_model,
|
||||
socketio::{event::BuildEvent, namespace::NamespaceCacheingLogic},
|
||||
};
|
||||
use uom::si::{
|
||||
angular_velocity::revolution_per_minute,
|
||||
f64::{AngularVelocity, Pressure, ThermodynamicTemperature},
|
||||
pressure::bar,
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use control_core::helpers::hasher_serializer::hash_with_serde_model;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use control_core::socketio::event::BuildEvent;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::{angular_velocity::revolution_per_minute, thermodynamic_temperature::degree_celsius};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::angular_velocity::AngularVelocity;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::pressure::{Pressure, bar};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::thermodynamic_temperature::ThermodynamicTemperature;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl ExtruderV2 {
|
||||
@@ -199,7 +204,6 @@ impl ExtruderV2 {
|
||||
}
|
||||
|
||||
pub fn set_nozzle_pressure_limit(&mut self, pressure: f64) {
|
||||
use uom::si::pressure::bar;
|
||||
self.screw_speed_controller
|
||||
.set_nozzle_pressure_limit(Pressure::new::<bar>(pressure));
|
||||
self.emit_state();
|
||||
@@ -244,7 +248,6 @@ impl ExtruderV2 {
|
||||
}
|
||||
|
||||
pub fn set_target_rpm(&mut self, rpm: f64) {
|
||||
use uom::si::angular_velocity::revolution_per_minute;
|
||||
self.screw_speed_controller
|
||||
.set_target_screw_rpm(AngularVelocity::new::<revolution_per_minute>(rpm));
|
||||
self.emit_state();
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::machines::extruder1::{HeatingType, api::Mutation, mock::ExtruderV2};
|
||||
use control_core::machines::api::MachineApi;
|
||||
use control_core::socketio::namespace::Namespace;
|
||||
use smol::lock::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
impl MachineApi for ExtruderV2 {
|
||||
fn api_mutate(&mut self, request_body: serde_json::Value) -> Result<(), anyhow::Error> {
|
||||
@@ -38,7 +41,7 @@ impl MachineApi for ExtruderV2 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> &mut control_core::socketio::namespace::Namespace {
|
||||
&mut self.namespace.namespace
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use control_core::machines::identification::MachineIdentification;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use control_core::machines::identification::MachineIdentificationUnique;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use control_core_derive::Machine;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
use std::time::Instant;
|
||||
@@ -31,6 +33,7 @@ pub mod new;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
#[derive(Debug, Machine)]
|
||||
pub struct ExtruderV2 {
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
namespace: ExtruderV2Namespace,
|
||||
last_measurement_emit: Instant,
|
||||
last_status_hash: Option<u64>,
|
||||
|
||||
@@ -32,7 +32,10 @@ impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let mut extruder_mock_machine = Self {
|
||||
namespace: ExtruderV2Namespace::new(params.socket_queue_tx.clone()),
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: ExtruderV2Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_measurement_emit: now,
|
||||
|
||||
mode: ExtruderV2Mode::Standby, // Start in standby mode
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
use crate::machines::{MACHINE_EXTRUDER_V1, VENDOR_QITECH};
|
||||
use api::ExtruderV2Namespace;
|
||||
use control_core::machines::identification::MachineIdentification;
|
||||
use control_core_derive::Machine;
|
||||
use screw_speed_controller::ScrewSpeedController;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use std::time::Instant;
|
||||
use temperature_controller::TemperatureController;
|
||||
use uom::si::{
|
||||
electric_current::ampere, electric_potential::volt, f64::ThermodynamicTemperature,
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use control_core::machines::identification::{MachineIdentification, MachineIdentificationUnique};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use control_core_derive::Machine;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::electric_current::ampere;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::electric_potential::volt;
|
||||
use uom::si::{f64::ThermodynamicTemperature, thermodynamic_temperature::degree_celsius};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::machines::{
|
||||
MACHINE_EXTRUDER_V1, VENDOR_QITECH,
|
||||
extruder1::{
|
||||
api::ExtruderV2Namespace, screw_speed_controller::ScrewSpeedController,
|
||||
temperature_controller::TemperatureController,
|
||||
},
|
||||
};
|
||||
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod emit;
|
||||
@@ -52,9 +63,10 @@ pub enum HeatingType {
|
||||
Middle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Machine)]
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
#[derive(Debug, Machine)]
|
||||
pub struct ExtruderV2 {
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
namespace: ExtruderV2Namespace,
|
||||
last_measurement_emit: Instant,
|
||||
last_status_hash: Option<u64>,
|
||||
@@ -126,6 +138,9 @@ impl ExtruderV2 {
|
||||
}
|
||||
|
||||
// Funktionen ohne emit_state bleiben hier
|
||||
|
||||
// Set all relais to ZERO
|
||||
// We dont need a function to enable again though, as the act Loop will detect the mode
|
||||
fn turn_heating_off(&mut self) {
|
||||
self.temperature_controller_back.disable();
|
||||
self.temperature_controller_front.disable();
|
||||
|
||||
@@ -1,64 +1,87 @@
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use super::{
|
||||
ExtruderV2, ExtruderV2Mode, Heating, api::ExtruderV2Namespace, mitsubishi_cs80::MitsubishiCS80,
|
||||
screw_speed_controller::ScrewSpeedController,
|
||||
};
|
||||
use crate::machines::extruder1::temperature_controller::TemperatureController;
|
||||
use crate::machines::get_ethercat_device;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
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,
|
||||
},
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use control_core::machines::new::MachineNewHardware;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use control_core::machines::new::{MachineNewParams, MachineNewTrait};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use control_core::machines::new::{
|
||||
validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use ethercat_hal::coe::ConfigurableDevice;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use ethercat_hal::devices::{
|
||||
EthercatDeviceUsed,
|
||||
ek1100::EK1100,
|
||||
ek1100::EK1100_IDENTITY_A,
|
||||
el1002::EL1002,
|
||||
el1002::EL1002_IDENTITY_A,
|
||||
el6021::EL6021,
|
||||
el6021::EL6021Configuration,
|
||||
el6021::EL6021Port,
|
||||
el6021::{EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C, EL6021_IDENTITY_D},
|
||||
};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use std::time::Duration;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use std::time::Instant;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::angular_velocity::AngularVelocity;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::angular_velocity::revolution_per_minute;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::pressure::Pressure;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::pressure::bar;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use ethercat_hal::{
|
||||
coe::ConfigurableDevice,
|
||||
devices::{
|
||||
EthercatDeviceUsed, downcast_device,
|
||||
ek1100::{EK1100, EK1100_IDENTITY_A},
|
||||
el1002::{EL1002, EL1002_IDENTITY_A},
|
||||
el2004::{EL2004, EL2004_IDENTITY_A, EL2004Port},
|
||||
el3021::{EL3021, EL3021_IDENTITY_A, EL3021Port},
|
||||
el3204::{EL3204, EL3204_IDENTITY_A, EL3204_IDENTITY_B, EL3204Port},
|
||||
el6021::{
|
||||
self, EL6021, EL6021_IDENTITY_A, EL6021_IDENTITY_B, EL6021_IDENTITY_C,
|
||||
EL6021_IDENTITY_D, EL6021Configuration,
|
||||
},
|
||||
subdevice_identity_to_tuple,
|
||||
},
|
||||
io::{
|
||||
analog_input::AnalogInput, digital_output::DigitalOutput,
|
||||
serial_interface::SerialInterface, temperature_input::TemperatureInput,
|
||||
},
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use uom::si::{
|
||||
angular_velocity::revolution_per_minute,
|
||||
f64::{AngularVelocity, Pressure, ThermodynamicTemperature},
|
||||
pressure::bar,
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use uom::si::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::machines::extruder1::temperature_controller::TemperatureController;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use super::{
|
||||
ExtruderV2, ExtruderV2Mode, Heating, api::ExtruderV2Namespace, mitsubishi_cs80::MitsubishiCS80,
|
||||
screw_speed_controller::ScrewSpeedController,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl MachineNewTrait for ExtruderV2 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
// validate general stuff
|
||||
|
||||
let device_identification = params.device_group.to_vec();
|
||||
|
||||
validate_same_machine_identification_unique(&device_identification)?;
|
||||
validate_no_role_dublicates(&device_identification)?;
|
||||
|
||||
let hardware = match ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Extruder2::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
let hardware: &&control_core::machines::new::MachineNewHardwareEthercat<'_, '_, '_> =
|
||||
match ¶ms.hardware {
|
||||
MachineNewHardware::Ethercat(x) => x,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Extruder2::new] MachineNewHardware is not Ethercat",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// using block_on because making this funciton async creates a lifetime issue
|
||||
// if its async the compiler thinks &subdevices is persisted in the future which might never execute
|
||||
// so we can't drop subdevices unless this machine is dropped, which is bad
|
||||
@@ -66,245 +89,55 @@ impl MachineNewTrait for ExtruderV2 {
|
||||
// Role 0
|
||||
// Buscoupler
|
||||
// EK1100
|
||||
{
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 0)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 0 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EK1100_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EK1100>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 0 is not an EK1100",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
}
|
||||
let _ek1100 =
|
||||
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
|
||||
|
||||
// What is its use ?
|
||||
let _el1002 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 1)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 1 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL1002_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL1002>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 1 is not an EL1002",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
};
|
||||
let _el1002 =
|
||||
get_ethercat_device::<EL1002>(hardware, params, 1, [EL1002_IDENTITY_A].to_vec())
|
||||
.await?;
|
||||
|
||||
let el6021 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 2)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 2 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL6021_IDENTITY_A | EL6021_IDENTITY_B | EL6021_IDENTITY_C
|
||||
| EL6021_IDENTITY_D => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL6021>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 2 is not an EL6021",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
let identities = [
|
||||
EL6021_IDENTITY_A,
|
||||
EL6021_IDENTITY_B,
|
||||
EL6021_IDENTITY_C,
|
||||
EL6021_IDENTITY_D,
|
||||
]
|
||||
.to_vec();
|
||||
let device = get_ethercat_device::<EL6021>(hardware, params, 2, identities).await?;
|
||||
|
||||
device
|
||||
.0
|
||||
.write()
|
||||
.await
|
||||
.write_config(subdevice, &EL6021Configuration::default())
|
||||
.write_config(&device.1, &EL6021Configuration::default())
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
let mut device_guard = device.0.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
device.0
|
||||
};
|
||||
|
||||
let el2004 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 3)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 3 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL2004_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL2004>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 3 is not an EL2004",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
};
|
||||
let el2004 =
|
||||
get_ethercat_device::<EL2004>(hardware, params, 3, [EL2004_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
let el3021 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 4)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 4 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL3021_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL3021>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 4 is not an EL3021",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
};
|
||||
let el3021 =
|
||||
get_ethercat_device::<EL3021>(hardware, params, 4, [EL3021_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
let el3204 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 5)?;
|
||||
let device_hardware_identification_ethercat = match &device_identification
|
||||
.device_hardware_identification
|
||||
{
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 5 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL3204_IDENTITY_A | EL3204_IDENTITY_B => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL3204>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role 5 is not an EL3204",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
};
|
||||
let el3204 = get_ethercat_device::<EL3204>(
|
||||
hardware,
|
||||
params,
|
||||
5,
|
||||
[EL3204_IDENTITY_A, EL3204_IDENTITY_B].to_vec(),
|
||||
)
|
||||
.await?
|
||||
.0;
|
||||
|
||||
let t1 = TemperatureInput::new(el3204.clone(), EL3204Port::T1);
|
||||
let t2 = TemperatureInput::new(el3204.clone(), EL3204Port::T2);
|
||||
@@ -380,8 +213,7 @@ impl MachineNewTrait for ExtruderV2 {
|
||||
0.95,
|
||||
);
|
||||
|
||||
let inverter =
|
||||
MitsubishiCS80::new(SerialInterface::new(el6021, el6021::EL6021Port::SI1));
|
||||
let inverter = MitsubishiCS80::new(SerialInterface::new(el6021, EL6021Port::SI1));
|
||||
|
||||
let target_pressure = Pressure::new::<bar>(0.0);
|
||||
let target_rpm = AngularVelocity::new::<revolution_per_minute>(0.0);
|
||||
@@ -389,8 +221,11 @@ impl MachineNewTrait for ExtruderV2 {
|
||||
let screw_speed_controller =
|
||||
ScrewSpeedController::new(inverter, target_pressure, target_rpm, pressure_sensor);
|
||||
|
||||
let mut extruder: Self = Self {
|
||||
namespace: ExtruderV2Namespace::new(params.socket_queue_tx.clone()),
|
||||
let mut extruder: ExtruderV2 = Self {
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: ExtruderV2Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_measurement_emit: Instant::now(),
|
||||
mode: ExtruderV2Mode::Standby,
|
||||
total_energy_kwh: 0.0,
|
||||
|
||||
@@ -12,8 +12,7 @@ use control_core::{
|
||||
use control_core_derive::BuildEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use smol::lock::Mutex;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -62,15 +61,7 @@ pub enum LaserEvents {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LaserMachineNamespace {
|
||||
pub namespace: Namespace,
|
||||
}
|
||||
|
||||
impl LaserMachineNamespace {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
Self {
|
||||
namespace: Namespace::new(socket_queue_tx),
|
||||
}
|
||||
}
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl CacheableEvents<Self> for LaserEvents {
|
||||
@@ -107,7 +98,9 @@ impl NamespaceCacheingLogic<LaserEvents> for LaserMachineNamespace {
|
||||
fn emit(&mut self, events: LaserEvents) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
self.namespace.emit(event, &buffer_fn);
|
||||
|
||||
let mut namespace = self.namespace.lock_blocking();
|
||||
namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +121,7 @@ impl MachineApi for LaserMachine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace {
|
||||
&mut self.namespace.namespace
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ use crate::{
|
||||
};
|
||||
use api::{LaserEvents, LaserMachineNamespace, LaserState, LiveValuesEvent, StateEvent};
|
||||
use control_core::{
|
||||
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
|
||||
machines::identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
use control_core_derive::Machine;
|
||||
use smol::lock::RwLock;
|
||||
@@ -17,6 +18,8 @@ pub mod new;
|
||||
|
||||
#[derive(Debug, Machine)]
|
||||
pub struct LaserMachine {
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
|
||||
// drivers
|
||||
laser: Arc<RwLock<Laser>>,
|
||||
|
||||
@@ -43,9 +46,7 @@ impl LaserMachine {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_LASER_V1,
|
||||
};
|
||||
}
|
||||
|
||||
impl LaserMachine {
|
||||
///diameter in mm
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let diameter = self.diameter.get::<millimeter>();
|
||||
|
||||
@@ -4,11 +4,9 @@ use crate::serial::{devices::laser::Laser, registry::SERIAL_DEVICE_REGISTRY};
|
||||
|
||||
use super::{LaserMachine, LaserTarget, api::LaserMachineNamespace};
|
||||
use anyhow::Error;
|
||||
use control_core::machines::new::MachineNewTrait;
|
||||
use uom::{
|
||||
ConstZero,
|
||||
si::{f64::Length, length::millimeter},
|
||||
};
|
||||
use control_core::machines::new::{MachineNewHardware, MachineNewTrait};
|
||||
use uom::ConstZero;
|
||||
use uom::si::{f64::Length, length::millimeter};
|
||||
|
||||
impl MachineNewTrait for LaserMachine {
|
||||
fn new(
|
||||
@@ -18,7 +16,7 @@ impl MachineNewTrait for LaserMachine {
|
||||
Self: Sized,
|
||||
{
|
||||
let hardware_serial = match params.hardware {
|
||||
control_core::machines::new::MachineNewHardware::Serial(serial) => *serial,
|
||||
MachineNewHardware::Serial(serial) => *serial,
|
||||
_ => return Err(Error::msg("Invalid hardware type for LaserMachine")),
|
||||
};
|
||||
|
||||
@@ -37,8 +35,11 @@ impl MachineNewTrait for LaserMachine {
|
||||
diameter: Length::new::<millimeter>(1.75),
|
||||
};
|
||||
let mut laser_machine = Self {
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
laser,
|
||||
namespace: LaserMachineNamespace::new(params.socket_queue_tx.clone()),
|
||||
namespace: LaserMachineNamespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_measurement_emit: Instant::now(),
|
||||
laser_target,
|
||||
emitted_default_state: false,
|
||||
|
||||
@@ -12,8 +12,7 @@ use control_core::{
|
||||
use control_core_derive::BuildEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use smol::lock::Mutex;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -61,15 +60,7 @@ pub enum MockEvents {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockMachineNamespace {
|
||||
pub namespace: Namespace,
|
||||
}
|
||||
|
||||
impl MockMachineNamespace {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
Self {
|
||||
namespace: Namespace::new(socket_queue_tx),
|
||||
}
|
||||
}
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl CacheableEvents<Self> for MockEvents {
|
||||
@@ -106,7 +97,9 @@ impl NamespaceCacheingLogic<MockEvents> for MockMachineNamespace {
|
||||
fn emit(&mut self, events: MockEvents) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
self.namespace.emit(event, &buffer_fn);
|
||||
|
||||
let mut namespace = self.namespace.lock_blocking();
|
||||
namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +123,7 @@ impl MachineApi for MockMachine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace {
|
||||
&mut self.namespace.namespace
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::machines::{MACHINE_MOCK, VENDOR_QITECH};
|
||||
use api::{LiveValuesEvent, MockEvents, MockMachineNamespace, Mode, ModeState, StateEvent};
|
||||
use control_core::machines::identification::MachineIdentificationUnique;
|
||||
use control_core::socketio::event::BuildEvent;
|
||||
use control_core::{
|
||||
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
|
||||
@@ -19,6 +20,8 @@ pub mod new;
|
||||
|
||||
#[derive(Debug, Machine)]
|
||||
pub struct MockMachine {
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
|
||||
// socketio
|
||||
namespace: MockMachineNamespace,
|
||||
last_measurement_emit: Instant,
|
||||
@@ -40,12 +43,10 @@ pub struct MockMachine {
|
||||
|
||||
impl MockMachine {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_MOCK,
|
||||
vendor: VENDOR_QITECH,
|
||||
};
|
||||
}
|
||||
|
||||
impl MockMachine {
|
||||
/// Emit live values data event with the current sine wave amplitude
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let now = Instant::now();
|
||||
|
||||
@@ -5,12 +5,12 @@ use super::{
|
||||
api::{MockMachineNamespace, Mode},
|
||||
};
|
||||
use anyhow::Error;
|
||||
use control_core::machines::new::{MachineNewHardware, MachineNewTrait};
|
||||
use control_core::machines::new::{MachineNewHardware, MachineNewParams, MachineNewTrait};
|
||||
use uom::si::{f64::Frequency, frequency::hertz};
|
||||
|
||||
impl MachineNewTrait for MockMachine {
|
||||
fn new(
|
||||
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
|
||||
fn new<'maindevice, 'subdevices>(
|
||||
params: &MachineNewParams<'maindevice, 'subdevices, '_, '_, '_, '_, '_>,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -32,7 +32,10 @@ impl MachineNewTrait for MockMachine {
|
||||
let now = Instant::now();
|
||||
|
||||
let mut mock_machine = Self {
|
||||
namespace: MockMachineNamespace::new(params.socket_queue_tx.clone()),
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: MockMachineNamespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
last_measurement_emit: now,
|
||||
t_0: now, // Initialize start time to current time
|
||||
frequency1: Frequency::new::<hertz>(0.1), // Default frequency1 of 100 mHz
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::machines::{
|
||||
identification::{DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat},
|
||||
new::{
|
||||
MachineNewParams, get_device_identification_by_role, get_ethercat_device_by_index,
|
||||
get_subdevice_by_index,
|
||||
},
|
||||
};
|
||||
use ethercat_hal::devices::{
|
||||
EthercatDevice, SubDeviceIdentityTuple, downcast_device, subdevice_identity_to_tuple,
|
||||
};
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
use smol::lock::RwLock;
|
||||
|
||||
pub mod aquapath1;
|
||||
pub mod buffer1;
|
||||
pub mod extruder1;
|
||||
pub mod laser;
|
||||
@@ -11,3 +27,112 @@ pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
|
||||
pub const MACHINE_LASER_V1: u16 = 0x0006;
|
||||
pub const MACHINE_MOCK: u16 = 0x0007;
|
||||
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
|
||||
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
|
||||
|
||||
async fn get_device_ident<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware_serial,
|
||||
'machine_new_hardware,
|
||||
>(
|
||||
params: &MachineNewParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware_serial,
|
||||
'machine_new_hardware,
|
||||
>,
|
||||
role: u16,
|
||||
) -> Result<DeviceHardwareIdentificationEthercat, anyhow::Error> {
|
||||
let device_identification = get_device_identification_by_role(params.device_group, role)?;
|
||||
let device_hardware_identification_ethercat =
|
||||
match &device_identification.device_hardware_identification {
|
||||
DeviceHardwareIdentification::Ethercat(device_hardware_identification_ethercat) => {
|
||||
device_hardware_identification_ethercat
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device with role {} is not Ethercat",
|
||||
module_path!(),
|
||||
role
|
||||
));
|
||||
}
|
||||
};
|
||||
return Ok(device_hardware_identification_ethercat.clone());
|
||||
}
|
||||
|
||||
async fn get_ethercat_device<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware_serial,
|
||||
'machine_new_hardware,
|
||||
T,
|
||||
>(
|
||||
hardware: &&control_core::machines::new::MachineNewHardwareEthercat<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'ethercat_devices,
|
||||
>,
|
||||
params: &MachineNewParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware_serial,
|
||||
'machine_new_hardware,
|
||||
>,
|
||||
role: u16,
|
||||
expected_identities: Vec<SubDeviceIdentityTuple>,
|
||||
) -> Result<
|
||||
(
|
||||
Arc<RwLock<T>>,
|
||||
&'subdevices SubDeviceRef<'subdevices, &'subdevices SubDevice>,
|
||||
),
|
||||
anyhow::Error,
|
||||
>
|
||||
where
|
||||
T: 'static + Send + Sync + EthercatDevice,
|
||||
{
|
||||
let device_hardware_identification_ethercat = get_device_ident(params, role).await?;
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
|
||||
let actual_identity = subdevice_identity_to_tuple(&subdevice_identity);
|
||||
|
||||
let mut matched_any_identity = false;
|
||||
for identity in expected_identities.clone() {
|
||||
if actual_identity == identity {
|
||||
matched_any_identity = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !matched_any_identity {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/ExtruderV2::new] Device identity mismatch: expected {:?}",
|
||||
module_path!(),
|
||||
expected_identities
|
||||
));
|
||||
}
|
||||
|
||||
let ethercat_device =
|
||||
get_ethercat_device_by_index(&hardware.ethercat_devices, subdevice_index)?;
|
||||
let device = downcast_device::<T>(ethercat_device).await?;
|
||||
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
|
||||
Ok((device, subdevice))
|
||||
}
|
||||
@@ -4,8 +4,9 @@ use crate::machines::{extruder1::mock::ExtruderV2, mock::MockMachine};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::machines::extruder1::ExtruderV2;
|
||||
|
||||
use crate::machines::{buffer1::BufferV1, laser::LaserMachine, winder2::Winder2};
|
||||
|
||||
use crate::machines::{
|
||||
aquapath1::AquaPathV1, buffer1::BufferV1, laser::LaserMachine, winder2::Winder2,
|
||||
};
|
||||
use control_core::machines::registry::MachineRegistry;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
@@ -18,6 +19,7 @@ lazy_static! {
|
||||
#[cfg(feature = "mock-machine")]
|
||||
mc.register::<MockMachine>(MockMachine::MACHINE_IDENTIFICATION);
|
||||
mc.register::<BufferV1>(BufferV1::MACHINE_IDENTIFICATION);
|
||||
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
|
||||
mc
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::{Winder2, Winder2Mode, puller_speed_controller::PullerRegulationMode};
|
||||
use control_core::{
|
||||
machines::{api::MachineApi, identification::MachineIdentificationUnique},
|
||||
machines::{
|
||||
api::MachineApi, connection::MachineCrossConnectionState,
|
||||
identification::MachineIdentificationUnique,
|
||||
},
|
||||
socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
@@ -13,8 +16,7 @@ use control_core::{
|
||||
use control_core_derive::BuildEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
use smol::lock::Mutex;
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -141,7 +143,7 @@ pub struct StateEvent {
|
||||
/// spool speed controller state
|
||||
pub spool_speed_controller_state: SpoolSpeedControllerState,
|
||||
/// connected machine state
|
||||
pub connected_machine_state: ConnectedMachineState,
|
||||
pub connected_machine_state: MachineCrossConnectionState,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
@@ -237,13 +239,6 @@ pub struct SpoolSpeedControllerState {
|
||||
pub adaptive_deacceleration_urgency_multiplier: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct ConnectedMachineState {
|
||||
/// Connected Machine
|
||||
pub machine_identification_unique: Option<MachineIdentificationUnique>,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
pub enum Winder2Events {
|
||||
LiveValues(Event<LiveValuesEvent>),
|
||||
State(Event<StateEvent>),
|
||||
@@ -251,7 +246,7 @@ pub enum Winder2Events {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Winder2Namespace {
|
||||
pub namespace: Namespace,
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<Winder2Events> for Winder2Namespace {
|
||||
@@ -259,15 +254,9 @@ impl NamespaceCacheingLogic<Winder2Events> for Winder2Namespace {
|
||||
fn emit(&mut self, events: Winder2Events) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
self.namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
impl Winder2Namespace {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
Self {
|
||||
namespace: Namespace::new(socket_queue_tx),
|
||||
}
|
||||
let mut namespace = self.namespace.lock_blocking();
|
||||
namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +330,7 @@ impl MachineApi for Winder2 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> &mut Namespace {
|
||||
&mut self.namespace.namespace
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
use crate::machines::buffer1::BufferV1;
|
||||
use crate::machines::winder2::api::{
|
||||
ConnectedMachineState, LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode,
|
||||
SpoolAutomaticActionState, SpoolSpeedControllerState, StateEvent, TensionArmState,
|
||||
TraverseState, Winder2Events,
|
||||
};
|
||||
use crate::machines::winder2::puller_speed_controller::PullerRegulationMode;
|
||||
use crate::machines::winder2::{Winder2, Winder2Mode, spool_speed_controller};
|
||||
use control_core::machines::identification::MachineIdentificationUnique;
|
||||
use control_core::machines::{ConnectedMachine, ConnectedMachineData, downcast_machine};
|
||||
use control_core::socketio::event::BuildEvent;
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use control_core::uom_extensions::velocity::meter_per_minute;
|
||||
use smol::block_on;
|
||||
use smol::lock::Mutex;
|
||||
use std::sync::Arc;
|
||||
use uom::si::angle::degree;
|
||||
use uom::si::angular_velocity::revolution_per_minute;
|
||||
use uom::si::f64::{Length, Velocity};
|
||||
use uom::si::length::{meter, millimeter};
|
||||
|
||||
impl Winder2 {
|
||||
// --- Traverse functions ---
|
||||
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
|
||||
let new_inner = Length::new::<meter>(limit);
|
||||
let current_outer = self.traverse_controller.get_limit_outer();
|
||||
if !Self::validate_traverse_limits(new_inner, current_outer) {
|
||||
return;
|
||||
}
|
||||
self.traverse_controller.set_limit_inner(new_inner);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
|
||||
let new_outer = Length::new::<meter>(limit);
|
||||
let current_inner = self.traverse_controller.get_limit_inner();
|
||||
if !Self::validate_traverse_limits(current_inner, new_outer) {
|
||||
return;
|
||||
}
|
||||
self.traverse_controller.set_limit_outer(new_outer);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_step_size(&mut self, step_size: f64) {
|
||||
self.traverse_controller
|
||||
.set_step_size(Length::new::<meter>(step_size));
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_padding(&mut self, padding: f64) {
|
||||
self.traverse_controller
|
||||
.set_padding(Length::new::<meter>(padding));
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_inner(&mut self) {
|
||||
if self.can_go_in() {
|
||||
self.traverse_controller.goto_limit_inner();
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_outer(&mut self) {
|
||||
if self.can_go_out() {
|
||||
self.traverse_controller.goto_limit_outer();
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_home(&mut self) {
|
||||
if self.can_go_home() {
|
||||
self.traverse_controller.goto_home();
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
// --- Mode / Puller / Spool functions ---
|
||||
pub fn set_mode(&mut self, mode: &Winder2Mode) {
|
||||
let should_update = *mode != Winder2Mode::Wind || self.can_wind();
|
||||
if should_update {
|
||||
self.mode = mode.clone();
|
||||
self.set_spool_mode(mode);
|
||||
self.set_puller_mode(mode);
|
||||
self.set_traverse_mode(mode);
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
|
||||
self.puller_speed_controller
|
||||
.set_regulation_mode(puller_regulation_mode);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
|
||||
let target_speed = Velocity::new::<uom::si::velocity::meter_per_second>(target_speed);
|
||||
self.puller_speed_controller.set_target_speed(target_speed);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
|
||||
let target_diameter = Length::new::<meter>(target_diameter);
|
||||
self.puller_speed_controller
|
||||
.set_target_diameter(target_diameter);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn puller_set_forward(&mut self, forward: bool) {
|
||||
self.puller_speed_controller.set_forward(forward);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_set_regulation_mode(
|
||||
&mut self,
|
||||
regulation_mode: spool_speed_controller::SpoolSpeedControllerType,
|
||||
) {
|
||||
self.spool_speed_controller.set_type(regulation_mode);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64) {
|
||||
let min_speed = uom::si::f64::AngularVelocity::new::<
|
||||
uom::si::angular_velocity::revolution_per_minute,
|
||||
>(min_speed_rpm);
|
||||
if let Err(e) = self.spool_speed_controller.set_minmax_min_speed(min_speed) {
|
||||
tracing::error!("Failed to set spool min speed: {:?}", e);
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
|
||||
let max_speed = uom::si::f64::AngularVelocity::new::<
|
||||
uom::si::angular_velocity::revolution_per_minute,
|
||||
>(max_speed_rpm);
|
||||
if let Err(e) = self.spool_speed_controller.set_minmax_max_speed(max_speed) {
|
||||
tracing::error!("Failed to set spool max speed: {:?}", e);
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_tension_target(tension_target);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_radius_learning_rate(radius_learning_rate);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_max_speed_multiplier(max_speed_multiplier);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_acceleration_factor(acceleration_factor);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
|
||||
&mut self,
|
||||
deacceleration_urgency_multiplier: f64,
|
||||
) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
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 set_connected_buffer(
|
||||
&mut self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
if !matches!(
|
||||
machine_identification_unique.machine_identification,
|
||||
BufferV1::MACHINE_IDENTIFICATION
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let machine_manager_arc = match self.machine_manager.upgrade() {
|
||||
Some(machine_manager_arc) => machine_manager_arc,
|
||||
None => return,
|
||||
};
|
||||
let machine_manager_guard = block_on(machine_manager_arc.read());
|
||||
let buffer_weak = machine_manager_guard.get_machine_weak(&machine_identification_unique);
|
||||
let buffer_weak = match buffer_weak {
|
||||
Some(buffer_weak) => buffer_weak,
|
||||
None => return,
|
||||
};
|
||||
let buffer_strong = match buffer_weak.upgrade() {
|
||||
Some(buffer_strong) => buffer_strong,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let buffer: Arc<Mutex<BufferV1>> = block_on(downcast_machine::<BufferV1>(buffer_strong))
|
||||
.expect("failed downcasting machine");
|
||||
|
||||
let machine = Arc::downgrade(&buffer);
|
||||
|
||||
self.connected_buffer = Some(ConnectedMachine {
|
||||
machine_identification_unique,
|
||||
machine,
|
||||
});
|
||||
self.emit_state();
|
||||
|
||||
self.reverse_connect();
|
||||
}
|
||||
|
||||
pub fn disconnect_buffer(
|
||||
&mut self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
if !matches!(
|
||||
machine_identification_unique.machine_identification,
|
||||
BufferV1::MACHINE_IDENTIFICATION
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if let Some(connected) = &self.connected_buffer {
|
||||
if let Some(buffer_arc) = connected.machine.upgrade() {
|
||||
let future = async move {
|
||||
let mut buffer = buffer_arc.lock().await;
|
||||
if buffer.connected_winder.is_some() {
|
||||
buffer.connected_winder = None;
|
||||
buffer.emit_state();
|
||||
}
|
||||
};
|
||||
smol::spawn(future).detach();
|
||||
}
|
||||
}
|
||||
self.connected_buffer = None;
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn tension_arm_zero(&mut self) {
|
||||
self.tension_arm.zero();
|
||||
self.emit_live_values();
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let angle_deg = self.tension_arm.get_angle().get::<degree>();
|
||||
|
||||
// Wrap [270;<360] to [-90; 0]
|
||||
// This is done to reduce flicker in the graphs around the zero point
|
||||
let angle_deg = if angle_deg >= 270.0 {
|
||||
angle_deg - 360.0
|
||||
} else {
|
||||
angle_deg
|
||||
};
|
||||
|
||||
// Calculate puller speed from current motor steps
|
||||
let steps_per_second = self.puller.get_speed();
|
||||
let angular_velocity = self
|
||||
.puller_speed_controller
|
||||
.converter
|
||||
.steps_to_angular_velocity(steps_per_second as f64);
|
||||
let puller_speed = self
|
||||
.puller_speed_controller
|
||||
.angular_velocity_to_speed(angular_velocity);
|
||||
|
||||
// Calculate spool RPM from current motor steps
|
||||
let spool_rpm = self
|
||||
.spool_step_converter
|
||||
.steps_to_angular_velocity(self.spool.get_speed() as f64)
|
||||
.get::<revolution_per_minute>();
|
||||
|
||||
let live_values = LiveValuesEvent {
|
||||
traverse_position: self
|
||||
.traverse_controller
|
||||
.get_current_position()
|
||||
.map(|x| x.get::<millimeter>()),
|
||||
puller_speed: puller_speed.get::<meter_per_minute>(),
|
||||
spool_rpm,
|
||||
tension_arm_angle: angle_deg,
|
||||
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
|
||||
};
|
||||
|
||||
let event = live_values.build();
|
||||
self.namespace.emit(Winder2Events::LiveValues(event));
|
||||
}
|
||||
|
||||
pub fn build_state_event(&mut self) -> StateEvent {
|
||||
StateEvent {
|
||||
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
|
||||
traverse_state: TraverseState {
|
||||
limit_inner: self
|
||||
.traverse_controller
|
||||
.get_limit_inner()
|
||||
.get::<millimeter>(),
|
||||
limit_outer: self
|
||||
.traverse_controller
|
||||
.get_limit_outer()
|
||||
.get::<millimeter>(),
|
||||
position_in: self
|
||||
.traverse_controller
|
||||
.get_limit_inner()
|
||||
.get::<millimeter>(),
|
||||
position_out: self
|
||||
.traverse_controller
|
||||
.get_limit_outer()
|
||||
.get::<millimeter>(),
|
||||
is_going_in: self.traverse_controller.is_going_in(),
|
||||
is_going_out: self.traverse_controller.is_going_out(),
|
||||
is_homed: self.traverse_controller.is_homed(),
|
||||
is_going_home: self.traverse_controller.is_going_home(),
|
||||
is_traversing: self.traverse_controller.is_traversing(),
|
||||
laserpointer: self.laser.get(),
|
||||
step_size: self.traverse_controller.get_step_size().get::<millimeter>(),
|
||||
padding: self.traverse_controller.get_padding().get::<millimeter>(),
|
||||
can_go_in: self.can_go_in(),
|
||||
can_go_out: self.can_go_out(),
|
||||
can_go_home: self.can_go_home(),
|
||||
},
|
||||
puller_state: PullerState {
|
||||
regulation: self.puller_speed_controller.regulation_mode.clone(),
|
||||
target_speed: self
|
||||
.puller_speed_controller
|
||||
.target_speed
|
||||
.get::<meter_per_minute>(),
|
||||
target_diameter: self
|
||||
.puller_speed_controller
|
||||
.target_diameter
|
||||
.get::<millimeter>(),
|
||||
forward: self.puller_speed_controller.forward,
|
||||
},
|
||||
mode_state: ModeState {
|
||||
mode: self.mode.clone().into(),
|
||||
can_wind: self.can_wind(),
|
||||
},
|
||||
tension_arm_state: TensionArmState {
|
||||
zeroed: self.tension_arm.zeroed,
|
||||
},
|
||||
spool_speed_controller_state: SpoolSpeedControllerState {
|
||||
regulation_mode: self.spool_speed_controller.get_type().clone(),
|
||||
minmax_min_speed: self
|
||||
.spool_speed_controller
|
||||
.get_minmax_min_speed()
|
||||
.get::<revolution_per_minute>(),
|
||||
minmax_max_speed: self
|
||||
.spool_speed_controller
|
||||
.get_minmax_max_speed()
|
||||
.get::<revolution_per_minute>(),
|
||||
adaptive_tension_target: self.spool_speed_controller.get_adaptive_tension_target(),
|
||||
adaptive_radius_learning_rate: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_radius_learning_rate(),
|
||||
adaptive_max_speed_multiplier: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_max_speed_multiplier(),
|
||||
adaptive_acceleration_factor: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_acceleration_factor(),
|
||||
adaptive_deacceleration_urgency_multiplier: self
|
||||
.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(),
|
||||
},
|
||||
connected_machine_state: ConnectedMachineState {
|
||||
machine_identification_unique: self.connected_buffer.as_ref().map(
|
||||
|connected_machine| {
|
||||
ConnectedMachineData::from(connected_machine).machine_identification_unique
|
||||
},
|
||||
),
|
||||
is_available: self
|
||||
.connected_buffer
|
||||
.as_ref()
|
||||
.map(|connected_machine| {
|
||||
ConnectedMachineData::from(connected_machine).is_available
|
||||
})
|
||||
.unwrap_or(false),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn emit_state(&mut self) {
|
||||
let state_event = self.build_state_event();
|
||||
let event = state_event.build();
|
||||
self.namespace.emit(Winder2Events::State(event));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ pub mod act;
|
||||
pub mod adaptive_spool_speed_controller;
|
||||
pub mod api;
|
||||
pub mod clamp_revolution;
|
||||
pub mod emit;
|
||||
pub mod filament_tension;
|
||||
pub mod minmax_spool_speed_controller;
|
||||
pub mod new;
|
||||
@@ -11,31 +10,40 @@ pub mod spool_speed_controller;
|
||||
pub mod tension_arm;
|
||||
pub mod traverse_controller;
|
||||
|
||||
use api::{SpoolAutomaticActionMode, Winder2Namespace};
|
||||
use std::{fmt::Debug, sync::Weak, time::Instant};
|
||||
|
||||
use api::{
|
||||
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode, SpoolAutomaticActionState,
|
||||
SpoolSpeedControllerState, StateEvent, TensionArmState, TraverseState, Winder2Events,
|
||||
Winder2Namespace,
|
||||
};
|
||||
use control_core::socketio::event::BuildEvent;
|
||||
use control_core::{
|
||||
converters::angular_step_converter::AngularStepConverter,
|
||||
machines::{
|
||||
ConnectedMachine,
|
||||
connection::{CrossConnectableMachine, MachineCrossConnection},
|
||||
identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
manager::MachineManager,
|
||||
},
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
uom_extensions::velocity::meter_per_minute,
|
||||
};
|
||||
|
||||
use control_core_derive::Machine;
|
||||
use ethercat_hal::io::{
|
||||
digital_input::DigitalInput, digital_output::DigitalOutput,
|
||||
stepper_velocity_el70x1::StepperVelocityEL70x1,
|
||||
};
|
||||
use puller_speed_controller::PullerSpeedController;
|
||||
use smol::lock::{Mutex, RwLock};
|
||||
use puller_speed_controller::{PullerRegulationMode, PullerSpeedController};
|
||||
use smol::lock::RwLock;
|
||||
use spool_speed_controller::SpoolSpeedController;
|
||||
use std::{fmt::Debug, sync::Weak, time::Instant};
|
||||
use tension_arm::TensionArm;
|
||||
use traverse_controller::TraverseController;
|
||||
use uom::{
|
||||
ConstZero,
|
||||
si::{
|
||||
f64::Length,
|
||||
angle::degree,
|
||||
angular_velocity::revolution_per_minute,
|
||||
f64::{Length, Velocity},
|
||||
length::{meter, millimeter},
|
||||
velocity::meter_per_second,
|
||||
},
|
||||
@@ -73,7 +81,7 @@ pub struct Winder2 {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
|
||||
// connected machines
|
||||
pub connected_buffer: Option<ConnectedMachine<Weak<Mutex<BufferV1>>>>,
|
||||
pub connected_buffer: MachineCrossConnection<Winder2, BufferV1>,
|
||||
|
||||
// mode
|
||||
pub mode: Winder2Mode,
|
||||
@@ -96,23 +104,222 @@ pub struct Winder2 {
|
||||
emitted_default_state: bool,
|
||||
}
|
||||
|
||||
impl CrossConnectableMachine<Winder2, BufferV1> for Winder2 {
|
||||
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<Winder2, BufferV1> {
|
||||
&mut self.connected_buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl Winder2 {
|
||||
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: MACHINE_WINDER_V1,
|
||||
};
|
||||
}
|
||||
|
||||
/// Implement Traverse helpers
|
||||
impl Winder2 {
|
||||
/// Implement Traverse
|
||||
fn set_laser(&mut self, value: bool) {
|
||||
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
|
||||
fn validate_traverse_limits(inner: Length, outer: Length) -> bool {
|
||||
outer > inner + Length::new::<millimeter>(0.9)
|
||||
}
|
||||
|
||||
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
|
||||
let new_inner = Length::new::<millimeter>(limit);
|
||||
let current_outer = self.traverse_controller.get_limit_outer();
|
||||
|
||||
// Validate the new inner limit against current outer limit
|
||||
if !Self::validate_traverse_limits(new_inner, current_outer) {
|
||||
// Don't update if validation fails - keep the current value
|
||||
return;
|
||||
}
|
||||
self.traverse_controller.set_limit_inner(new_inner);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
|
||||
let new_outer = Length::new::<millimeter>(limit);
|
||||
let current_inner = self.traverse_controller.get_limit_inner();
|
||||
|
||||
// Validate the new outer limit against current inner limit
|
||||
if !Self::validate_traverse_limits(current_inner, new_outer) {
|
||||
// Don't update if validation fails - keep the current value
|
||||
return;
|
||||
}
|
||||
|
||||
self.traverse_controller.set_limit_outer(new_outer);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_step_size(&mut self, step_size: f64) {
|
||||
let step_size = Length::new::<millimeter>(step_size);
|
||||
self.traverse_controller.set_step_size(step_size);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_set_padding(&mut self, padding: f64) {
|
||||
let padding = Length::new::<millimeter>(padding);
|
||||
self.traverse_controller.set_padding(padding);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_inner(&mut self) {
|
||||
if self.can_go_in() {
|
||||
self.traverse_controller.goto_limit_inner();
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_limit_outer(&mut self) {
|
||||
if self.can_go_out() {
|
||||
self.traverse_controller.goto_limit_outer();
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn traverse_goto_home(&mut self) {
|
||||
if self.can_go_home() {
|
||||
self.traverse_controller.goto_home();
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn emit_live_values(&mut self) {
|
||||
let angle_deg = self.tension_arm.get_angle().get::<degree>();
|
||||
|
||||
// Wrap [270;<360] to [-90; 0]
|
||||
// This is done to reduce flicker in the graphs around the zero point
|
||||
let angle_deg = if angle_deg >= 270.0 {
|
||||
angle_deg - 360.0
|
||||
} else {
|
||||
angle_deg
|
||||
};
|
||||
|
||||
// Calculate puller speed from current motor steps
|
||||
let steps_per_second = self.puller.get_speed();
|
||||
let angular_velocity = self
|
||||
.puller_speed_controller
|
||||
.converter
|
||||
.steps_to_angular_velocity(steps_per_second as f64);
|
||||
let puller_speed = self
|
||||
.puller_speed_controller
|
||||
.angular_velocity_to_speed(angular_velocity);
|
||||
|
||||
// Calculate spool RPM from current motor steps
|
||||
let spool_rpm = self
|
||||
.spool_step_converter
|
||||
.steps_to_angular_velocity(self.spool.get_speed() as f64)
|
||||
.get::<revolution_per_minute>();
|
||||
|
||||
let live_values = LiveValuesEvent {
|
||||
traverse_position: self
|
||||
.traverse_controller
|
||||
.get_current_position()
|
||||
.map(|x| x.get::<millimeter>()),
|
||||
puller_speed: puller_speed.get::<meter_per_minute>(),
|
||||
spool_rpm,
|
||||
tension_arm_angle: angle_deg,
|
||||
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
|
||||
};
|
||||
|
||||
let event = live_values.build();
|
||||
self.namespace.emit(Winder2Events::LiveValues(event));
|
||||
}
|
||||
|
||||
pub fn build_state_event(&mut self) -> StateEvent {
|
||||
StateEvent {
|
||||
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
|
||||
traverse_state: TraverseState {
|
||||
limit_inner: self
|
||||
.traverse_controller
|
||||
.get_limit_inner()
|
||||
.get::<millimeter>(),
|
||||
limit_outer: self
|
||||
.traverse_controller
|
||||
.get_limit_outer()
|
||||
.get::<millimeter>(),
|
||||
position_in: self
|
||||
.traverse_controller
|
||||
.get_limit_inner()
|
||||
.get::<millimeter>(),
|
||||
position_out: self
|
||||
.traverse_controller
|
||||
.get_limit_outer()
|
||||
.get::<millimeter>(),
|
||||
is_going_in: self.traverse_controller.is_going_in(),
|
||||
is_going_out: self.traverse_controller.is_going_out(),
|
||||
is_homed: self.traverse_controller.is_homed(),
|
||||
is_going_home: self.traverse_controller.is_going_home(),
|
||||
is_traversing: self.traverse_controller.is_traversing(),
|
||||
laserpointer: self.laser.get(),
|
||||
step_size: self.traverse_controller.get_step_size().get::<millimeter>(),
|
||||
padding: self.traverse_controller.get_padding().get::<millimeter>(),
|
||||
can_go_in: self.can_go_in(),
|
||||
can_go_out: self.can_go_out(),
|
||||
can_go_home: self.can_go_home(),
|
||||
},
|
||||
puller_state: PullerState {
|
||||
regulation: self.puller_speed_controller.regulation_mode.clone(),
|
||||
target_speed: self
|
||||
.puller_speed_controller
|
||||
.target_speed
|
||||
.get::<meter_per_minute>(),
|
||||
target_diameter: self
|
||||
.puller_speed_controller
|
||||
.target_diameter
|
||||
.get::<millimeter>(),
|
||||
forward: self.puller_speed_controller.forward,
|
||||
},
|
||||
mode_state: ModeState {
|
||||
mode: self.mode.clone().into(),
|
||||
can_wind: self.can_wind(),
|
||||
},
|
||||
tension_arm_state: TensionArmState {
|
||||
zeroed: self.tension_arm.zeroed,
|
||||
},
|
||||
spool_speed_controller_state: SpoolSpeedControllerState {
|
||||
regulation_mode: self.spool_speed_controller.get_type().clone(),
|
||||
minmax_min_speed: self
|
||||
.spool_speed_controller
|
||||
.get_minmax_min_speed()
|
||||
.get::<revolution_per_minute>(),
|
||||
minmax_max_speed: self
|
||||
.spool_speed_controller
|
||||
.get_minmax_max_speed()
|
||||
.get::<revolution_per_minute>(),
|
||||
adaptive_tension_target: self.spool_speed_controller.get_adaptive_tension_target(),
|
||||
adaptive_radius_learning_rate: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_radius_learning_rate(),
|
||||
adaptive_max_speed_multiplier: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_max_speed_multiplier(),
|
||||
adaptive_acceleration_factor: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_acceleration_factor(),
|
||||
adaptive_deacceleration_urgency_multiplier: self
|
||||
.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(),
|
||||
},
|
||||
connected_machine_state: self.connected_buffer.to_state(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_state(&mut self) {
|
||||
let state_event = self.build_state_event();
|
||||
let event = state_event.build();
|
||||
self.namespace.emit(Winder2Events::State(event));
|
||||
}
|
||||
|
||||
pub fn sync_traverse_speed(&mut self) {
|
||||
self.traverse_controller.update_speed(
|
||||
&mut self.traverse,
|
||||
@@ -121,13 +328,19 @@ impl Winder2 {
|
||||
)
|
||||
}
|
||||
|
||||
/// Can wind capability check
|
||||
pub const fn can_wind(&self) -> bool {
|
||||
// Check if tension arm is zeroed and traverse is homed
|
||||
self.tension_arm.zeroed
|
||||
&& self.traverse_controller.is_homed()
|
||||
&& !self.traverse_controller.is_going_home()
|
||||
}
|
||||
|
||||
/// Can go to inner limit capability check
|
||||
pub fn can_go_in(&self) -> bool {
|
||||
// Check if traverse is homed, not in standby, not traversing
|
||||
// Allow changing direction (even when going out)
|
||||
// Disallow when homing is in progress
|
||||
self.traverse_controller.is_homed()
|
||||
&& self.traverse_mode != TraverseMode::Standby
|
||||
&& !self.traverse_controller.is_going_in()
|
||||
@@ -136,7 +349,11 @@ impl Winder2 {
|
||||
&& self.mode != Winder2Mode::Wind
|
||||
}
|
||||
|
||||
/// Can go to outer limit capability check
|
||||
pub fn can_go_out(&self) -> bool {
|
||||
// Check if traverse is homed, not in standby, not traversing
|
||||
// Allow changing direction (even when going in)
|
||||
// Disallow when homing is in progress
|
||||
self.traverse_controller.is_homed()
|
||||
&& self.traverse_mode != TraverseMode::Standby
|
||||
&& !self.traverse_controller.is_going_out()
|
||||
@@ -145,106 +362,214 @@ impl Winder2 {
|
||||
&& self.mode != Winder2Mode::Wind
|
||||
}
|
||||
|
||||
/// Can go home capability check
|
||||
pub fn can_go_home(&self) -> bool {
|
||||
// Check if not in standby, not traversing
|
||||
// Allow going home even when going in or out
|
||||
self.traverse_mode != TraverseMode::Standby
|
||||
&& !self.traverse_controller.is_going_home()
|
||||
&& !self.traverse_controller.is_traversing()
|
||||
&& self.mode != Winder2Mode::Wind
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement Mode transitions (without emit)
|
||||
impl Winder2 {
|
||||
/// Implement Mode
|
||||
fn set_mode(&mut self, mode: &Winder2Mode) {
|
||||
let should_update = *mode != Winder2Mode::Wind || self.can_wind();
|
||||
|
||||
if should_update {
|
||||
// all transitions are allowed
|
||||
self.mode = mode.clone();
|
||||
|
||||
// Apply the mode changes to the spool and puller
|
||||
self.set_spool_mode(mode);
|
||||
self.set_puller_mode(mode);
|
||||
self.set_traverse_mode(mode);
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Apply the mode changes to the spool
|
||||
///
|
||||
/// It contains a transition matrix for atomic changes.
|
||||
/// It will set [`Self::spool_mode`]
|
||||
fn set_spool_mode(&mut self, mode: &Winder2Mode) {
|
||||
// Convert to `Winder2Mode` to `SpoolMode`
|
||||
let mode: SpoolMode = mode.clone().into();
|
||||
|
||||
// Transition matrix
|
||||
match self.spool_mode {
|
||||
SpoolMode::Standby => match mode {
|
||||
SpoolMode::Hold => self.spool.set_enabled(true),
|
||||
SpoolMode::Standby => {}
|
||||
SpoolMode::Hold => {
|
||||
// From [`SpoolMode::Standby`] to [`SpoolMode::Hold`]
|
||||
self.spool.set_enabled(true);
|
||||
}
|
||||
SpoolMode::Wind => {
|
||||
self.spool.set_enabled(true);
|
||||
// self.spool_speed_controller.reset();
|
||||
self.spool_speed_controller.set_enabled(true);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
SpoolMode::Hold => match mode {
|
||||
SpoolMode::Standby => self.spool.set_enabled(false),
|
||||
SpoolMode::Wind => self.spool_speed_controller.set_enabled(true),
|
||||
_ => {}
|
||||
SpoolMode::Standby => {
|
||||
// From [`SpoolMode::Hold`] to [`SpoolMode::Standby`]
|
||||
self.spool.set_enabled(false);
|
||||
}
|
||||
SpoolMode::Hold => {}
|
||||
SpoolMode::Wind => {
|
||||
// From [`SpoolMode::Hold`] to [`SpoolMode::Wind`]
|
||||
// self.spool_speed_controller.reset();
|
||||
self.spool_speed_controller.set_enabled(true);
|
||||
}
|
||||
},
|
||||
SpoolMode::Wind => match mode {
|
||||
SpoolMode::Standby => {
|
||||
// From [`SpoolMode::Wind`] to [`SpoolMode::Standby`]
|
||||
self.spool.set_enabled(false);
|
||||
self.spool_speed_controller.set_enabled(false);
|
||||
}
|
||||
SpoolMode::Hold => self.spool_speed_controller.set_enabled(false),
|
||||
_ => {}
|
||||
SpoolMode::Hold => {
|
||||
// From [`SpoolMode::Wind`] to [`SpoolMode::Hold`]
|
||||
self.spool_speed_controller.set_enabled(false);
|
||||
}
|
||||
SpoolMode::Wind => {}
|
||||
},
|
||||
}
|
||||
|
||||
// Update the internal state
|
||||
self.spool_mode = mode;
|
||||
}
|
||||
|
||||
/// Apply the mode changes to the spool
|
||||
///
|
||||
/// It contains a transition matrix for atomic changes.
|
||||
/// It will set [`Self::spool_mode`]
|
||||
fn set_traverse_mode(&mut self, mode: &Winder2Mode) {
|
||||
// Convert to `Winder2Mode` to `TraverseMode`
|
||||
let mode: TraverseMode = mode.clone().into();
|
||||
|
||||
// If coming out of standby
|
||||
if self.traverse_mode == TraverseMode::Standby && mode != TraverseMode::Standby {
|
||||
self.traverse.set_enabled(true);
|
||||
self.traverse_controller.set_enabled(true);
|
||||
}
|
||||
|
||||
// If going into standby
|
||||
if mode == TraverseMode::Standby && self.traverse_mode != TraverseMode::Standby {
|
||||
// If we are going into standby, we need to stop the traverse
|
||||
self.traverse.set_enabled(false);
|
||||
self.traverse_controller.set_enabled(false);
|
||||
}
|
||||
|
||||
// Transition matrix
|
||||
match self.traverse_mode {
|
||||
TraverseMode::Standby => match mode {
|
||||
TraverseMode::Hold => self.traverse_controller.goto_home(),
|
||||
TraverseMode::Traverse => self.traverse_controller.start_traversing(),
|
||||
_ => {}
|
||||
TraverseMode::Standby => {}
|
||||
TraverseMode::Hold => {
|
||||
// From [`TraverseMode::Standby`] to [`TraverseMode::Hold`]
|
||||
self.traverse.set_enabled(true);
|
||||
self.traverse_controller.set_enabled(true);
|
||||
self.traverse_controller.goto_home();
|
||||
}
|
||||
TraverseMode::Traverse => {
|
||||
// From [`TraverseMode::Standby`] to [`TraverseMode::Wind`]
|
||||
self.traverse.set_enabled(true);
|
||||
self.traverse_controller.set_enabled(true);
|
||||
self.traverse_controller.start_traversing();
|
||||
}
|
||||
},
|
||||
TraverseMode::Hold => match mode {
|
||||
TraverseMode::Traverse => self.traverse_controller.start_traversing(),
|
||||
_ => {}
|
||||
TraverseMode::Standby => {
|
||||
// From [`TraverseMode::Hold`] to [`TraverseMode::Standby`]
|
||||
self.traverse.set_enabled(false);
|
||||
self.traverse_controller.set_enabled(false);
|
||||
}
|
||||
TraverseMode::Hold => {}
|
||||
TraverseMode::Traverse => {
|
||||
// From [`TraverseMode::Hold`] to [`TraverseMode::Wind`]
|
||||
self.traverse_controller.start_traversing();
|
||||
}
|
||||
},
|
||||
TraverseMode::Traverse => match mode {
|
||||
TraverseMode::Hold => self.traverse_controller.goto_home(),
|
||||
_ => {}
|
||||
TraverseMode::Standby => {
|
||||
// From [`TraverseMode::Wind`] to [`TraverseMode::Standby`]
|
||||
self.traverse.set_enabled(false);
|
||||
self.traverse_controller.set_enabled(false);
|
||||
}
|
||||
TraverseMode::Hold => {
|
||||
// From [`TraverseMode::Wind`] to [`TraverseMode::Hold`]
|
||||
self.traverse_controller.goto_home();
|
||||
}
|
||||
TraverseMode::Traverse => {}
|
||||
},
|
||||
}
|
||||
|
||||
// Update the internal state
|
||||
self.traverse_mode = mode;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Apply the mode changes to the puller
|
||||
///
|
||||
/// It contains a transition matrix for atomic changes.
|
||||
/// It will set [`Self::puller_mode`]
|
||||
fn set_puller_mode(&mut self, mode: &Winder2Mode) {
|
||||
// Convert to `Winder2Mode` to `PullerMode`
|
||||
let mode: PullerMode = mode.clone().into();
|
||||
|
||||
// Transition matrix
|
||||
match self.puller_mode {
|
||||
PullerMode::Standby => match mode {
|
||||
PullerMode::Hold => self.puller.set_enabled(true),
|
||||
PullerMode::Standby => {}
|
||||
PullerMode::Hold => {
|
||||
// From [`PullerMode::Standby`] to [`PullerMode::Hold`]
|
||||
self.puller.set_enabled(true);
|
||||
}
|
||||
PullerMode::Pull => {
|
||||
// From [`PullerMode::Standby`] to [`PullerMode::Pull`]
|
||||
self.puller.set_enabled(true);
|
||||
self.puller_speed_controller.set_enabled(true);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
PullerMode::Hold => match mode {
|
||||
PullerMode::Standby => self.puller.set_enabled(false),
|
||||
PullerMode::Pull => self.puller_speed_controller.set_enabled(true),
|
||||
_ => {}
|
||||
PullerMode::Standby => {
|
||||
// From [`PullerMode::Hold`] to [`PullerMode::Standby`]
|
||||
self.puller.set_enabled(false);
|
||||
}
|
||||
PullerMode::Hold => {}
|
||||
PullerMode::Pull => {
|
||||
// From [`PullerMode::Hold`] to [`PullerMode::Pull`]
|
||||
self.puller_speed_controller.set_enabled(true);
|
||||
}
|
||||
},
|
||||
PullerMode::Pull => match mode {
|
||||
PullerMode::Standby => {
|
||||
// From [`PullerMode::Pull`] to [`PullerMode::Standby`]
|
||||
self.puller.set_enabled(false);
|
||||
self.puller_speed_controller.set_enabled(false);
|
||||
}
|
||||
PullerMode::Hold => self.puller_speed_controller.set_enabled(false),
|
||||
_ => {}
|
||||
PullerMode::Hold => {
|
||||
// From [`PullerMode::Pull`] to [`PullerMode::Hold`]
|
||||
self.puller_speed_controller.set_enabled(false);
|
||||
}
|
||||
PullerMode::Pull => {}
|
||||
},
|
||||
}
|
||||
|
||||
// Update the internal state
|
||||
self.puller_mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement Spool calculations
|
||||
impl Winder2 {
|
||||
/// Implement Tension Arm
|
||||
fn tension_arm_zero(&mut self) {
|
||||
self.tension_arm.zero();
|
||||
self.emit_live_values(); // For angle update
|
||||
// For state update
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Implement Spool
|
||||
/// called by `act`
|
||||
pub fn sync_spool_speed(&mut self, t: Instant) {
|
||||
let angular_velocity = self.spool_speed_controller.update_speed(
|
||||
t,
|
||||
@@ -257,6 +582,16 @@ impl Winder2 {
|
||||
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 matches!(
|
||||
self.spool_automatic_action.mode,
|
||||
@@ -267,7 +602,8 @@ impl Winder2 {
|
||||
}
|
||||
|
||||
match self.mode {
|
||||
Winder2Mode::Pull | Winder2Mode::Wind => self.calculate_spool_auto_progress_(now),
|
||||
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;
|
||||
@@ -276,15 +612,15 @@ impl Winder2 {
|
||||
|
||||
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.mode = Winder2Mode::Pull;
|
||||
self.set_mode(&Winder2Mode::Pull);
|
||||
}
|
||||
SpoolAutomaticActionMode::Hold => {
|
||||
self.stop_or_pull_spool_reset(now);
|
||||
self.mode = Winder2Mode::Hold;
|
||||
self.set_mode(&Winder2Mode::Hold);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,10 +631,12 @@ impl Winder2 {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -306,13 +644,13 @@ impl Winder2 {
|
||||
* dt,
|
||||
);
|
||||
|
||||
// Update total meters pulled
|
||||
self.spool_automatic_action.progress += meters_pulled_this_interval;
|
||||
self.spool_automatic_action.progress_last_check = now;
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement Puller calculations
|
||||
impl Winder2 {
|
||||
/// Implement Puller
|
||||
/// called by `act`
|
||||
pub fn sync_puller_speed(&mut self, t: Instant) {
|
||||
let angular_velocity = self.puller_speed_controller.calc_angular_velocity(t);
|
||||
let steps_per_second = self
|
||||
@@ -321,27 +659,140 @@ impl Winder2 {
|
||||
.angular_velocity_to_steps(angular_velocity);
|
||||
let _ = self.puller.set_speed(steps_per_second);
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement machine connection helpers (without emit)
|
||||
impl Winder2 {
|
||||
pub fn reverse_connect(&mut self) {
|
||||
let machine_identification_unique = self.machine_identification_unique.clone();
|
||||
if let Some(connected) = &self.connected_buffer {
|
||||
if let Some(buffer_arc) = connected.machine.upgrade() {
|
||||
let future = async move {
|
||||
let mut buffer = buffer_arc.lock().await;
|
||||
if buffer.connected_winder.is_none() {
|
||||
buffer.set_connected_winder(machine_identification_unique);
|
||||
}
|
||||
};
|
||||
smol::spawn(future).detach();
|
||||
}
|
||||
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
|
||||
self.puller_speed_controller
|
||||
.set_regulation_mode(puller_regulation_mode);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set target speed in m/min
|
||||
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
|
||||
// Convert m/min to velocity
|
||||
let target_speed = Velocity::new::<meter_per_minute>(target_speed);
|
||||
self.puller_speed_controller.set_target_speed(target_speed);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set target diameter in mm
|
||||
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
|
||||
// Convert m/min to velocity
|
||||
let target_diameter = Length::new::<millimeter>(target_diameter);
|
||||
self.puller_speed_controller
|
||||
.set_target_diameter(target_diameter);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set forward direction
|
||||
pub fn puller_set_forward(&mut self, forward: bool) {
|
||||
self.puller_speed_controller.set_forward(forward);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
// Spool Speed Controller API methods
|
||||
pub fn spool_set_regulation_mode(
|
||||
&mut self,
|
||||
regulation_mode: spool_speed_controller::SpoolSpeedControllerType,
|
||||
) {
|
||||
self.spool_speed_controller.set_type(regulation_mode);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set minimum speed for minmax mode in RPM
|
||||
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64) {
|
||||
let min_speed = uom::si::f64::AngularVelocity::new::<revolution_per_minute>(min_speed_rpm);
|
||||
if let Err(e) = self.spool_speed_controller.set_minmax_min_speed(min_speed) {
|
||||
tracing::error!("Failed to set spool min speed: {:?}", e);
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set maximum speed for minmax mode in RPM
|
||||
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
|
||||
let max_speed = uom::si::f64::AngularVelocity::new::<revolution_per_minute>(max_speed_rpm);
|
||||
if let Err(e) = self.spool_speed_controller.set_minmax_max_speed(max_speed) {
|
||||
tracing::error!("Failed to set spool max speed: {:?}", e);
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set tension target for adaptive mode (0.0-1.0)
|
||||
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_tension_target(tension_target);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set radius learning rate for adaptive mode
|
||||
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_radius_learning_rate(radius_learning_rate);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set max speed multiplier for adaptive mode
|
||||
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_max_speed_multiplier(max_speed_multiplier);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set acceleration factor for adaptive mode
|
||||
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_acceleration_factor(acceleration_factor);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set deacceleration urgency multiplier for adaptive mode
|
||||
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
|
||||
&mut self,
|
||||
deacceleration_urgency_multiplier: f64,
|
||||
) {
|
||||
self.spool_speed_controller
|
||||
.set_adaptive_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// implement machine connection
|
||||
/// set connected buffer
|
||||
pub fn set_connected_buffer(
|
||||
&mut self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
if !matches!(
|
||||
machine_identification_unique.machine_identification,
|
||||
BufferV1::MACHINE_IDENTIFICATION
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.connected_buffer
|
||||
.set_connected_machine(&machine_identification_unique);
|
||||
self.connected_buffer.reverse_connect();
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// disconnect buffer
|
||||
pub fn disconnect_buffer(
|
||||
&mut self,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
) {
|
||||
if !matches!(
|
||||
machine_identification_unique.machine_identification,
|
||||
BufferV1::MACHINE_IDENTIFICATION
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.connected_buffer.reverse_disconnect();
|
||||
self.connected_buffer.disconnect();
|
||||
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
|
||||
// Enums
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Winder2Mode {
|
||||
Standby,
|
||||
@@ -409,3 +860,52 @@ impl std::fmt::Display for Winder2 {
|
||||
write!(f, "Winder2")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use uom::si::{f64::Length, length::millimeter};
|
||||
|
||||
#[test]
|
||||
fn test_validate_traverse_limits() {
|
||||
// Test case 1: Valid limits with exactly 1.0mm difference (should pass)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(16.0);
|
||||
assert!(Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 2: Invalid limits with exactly 0.9mm difference (should fail)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(15.9);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 3: Invalid limits with less than 0.9mm difference (should fail)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(15.5);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 4: Invalid limits where inner equals outer (should fail)
|
||||
let inner = Length::new::<millimeter>(20.0);
|
||||
let outer = Length::new::<millimeter>(20.0);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 5: Invalid limits where inner is greater than outer (should fail)
|
||||
let inner = Length::new::<millimeter>(25.0);
|
||||
let outer = Length::new::<millimeter>(20.0);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 6: Valid limits with large difference (should pass)
|
||||
let inner = Length::new::<millimeter>(10.0);
|
||||
let outer = Length::new::<millimeter>(80.0);
|
||||
assert!(Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 7: Edge case - exactly 0.91mm difference (should pass)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(15.91);
|
||||
assert!(Winder2::validate_traverse_limits(inner, outer));
|
||||
|
||||
// Test case 8: Edge case - exactly 0.89mm difference (should fail)
|
||||
let inner = Length::new::<millimeter>(15.0);
|
||||
let outer = Length::new::<millimeter>(15.89);
|
||||
assert!(!Winder2::validate_traverse_limits(inner, outer));
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,16 @@ use std::time::Instant;
|
||||
use super::api::Winder2Namespace;
|
||||
use super::tension_arm::TensionArm;
|
||||
use super::{Winder2, Winder2Mode};
|
||||
use crate::machines::get_ethercat_device;
|
||||
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::converters::angular_step_converter::AngularStepConverter;
|
||||
use control_core::converters::linear_step_converter::LinearStepConverter;
|
||||
use control_core::machines::identification::DeviceHardwareIdentification;
|
||||
use control_core::machines::connection::MachineCrossConnection;
|
||||
use control_core::machines::new::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_device_identification_by_role,
|
||||
get_ethercat_device_by_index, get_subdevice_by_index, validate_no_role_dublicates,
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
|
||||
validate_same_machine_identification_unique,
|
||||
};
|
||||
use control_core::uom_extensions::velocity::meter_per_minute;
|
||||
@@ -31,7 +31,6 @@ use ethercat_hal::devices::el7031_0030::{
|
||||
};
|
||||
use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration;
|
||||
use ethercat_hal::devices::el7041_0052::{EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port};
|
||||
use ethercat_hal::devices::{EthercatDeviceUsed, downcast_device, subdevice_identity_to_tuple};
|
||||
use ethercat_hal::devices::{ek1100::EK1100_IDENTITY_A, el2002::EL2002_IDENTITY_A};
|
||||
use ethercat_hal::io::analog_input::AnalogInput;
|
||||
use ethercat_hal::io::digital_input::DigitalInput;
|
||||
@@ -65,121 +64,31 @@ impl MachineNewTrait for Winder2 {
|
||||
// if its async the compiler thinks &subdevices is persisted in the future which might never execute
|
||||
// so we can't drop subdevices unless this machine is dropped, which is bad
|
||||
smol::block_on(async {
|
||||
// Role 0
|
||||
// Buscoupler
|
||||
// EK1100
|
||||
{
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 0)?;
|
||||
let device_hardware_identification_ethercat =
|
||||
match &device_identification.device_hardware_identification {
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 0 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EK1100_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EK1100>(ethercat_device).await?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 0 is not an EK1100",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
}
|
||||
// Role 0: Buscoupler EK1100
|
||||
let _ek1100 =
|
||||
get_ethercat_device::<EK1100>(hardware, params, 0, vec![EK1100_IDENTITY_A]).await?;
|
||||
|
||||
// Role 1
|
||||
// 2x Digitalausgang
|
||||
// EL2002
|
||||
let el2002 = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 1)?;
|
||||
let device_hardware_identification_ethercat =
|
||||
match &device_identification.device_hardware_identification {
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 1 is not Ethercat",
|
||||
module_path!()
|
||||
))?, //uncommented
|
||||
};
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL2002_IDENTITY_A | EL2002_IDENTITY_B => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL2002>(ethercat_device).await?
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 1 is not an EL2002",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
device
|
||||
};
|
||||
// Role 1: 2x Digital outputs EL2002
|
||||
let el2002 = get_ethercat_device::<EL2002>(
|
||||
hardware,
|
||||
params,
|
||||
1,
|
||||
vec![EL2002_IDENTITY_A, EL2002_IDENTITY_B],
|
||||
)
|
||||
.await?
|
||||
.0;
|
||||
|
||||
// Role 2
|
||||
// 1x Stepper Spool
|
||||
// EL7041-0052
|
||||
let (el7041, _el7041_config) = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 2)?;
|
||||
let device_hardware_identification_ethercat =
|
||||
match &device_identification.device_hardware_identification {
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 2 is not Ethercat",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let subdevice = get_subdevice_by_index(
|
||||
hardware.subdevices,
|
||||
device_hardware_identification_ethercat.subdevice_index,
|
||||
)?;
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL7041_0052_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL7041_0052>(ethercat_device).await?
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 3 is not an EL7041-0052",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let config = EL7041_0052Configuration {
|
||||
// Role 2: Stepper Spool EL7041-0052
|
||||
let el7041 = {
|
||||
let device = get_ethercat_device::<EL7041_0052>(
|
||||
hardware,
|
||||
params,
|
||||
2,
|
||||
vec![EL7041_0052_IDENTITY_A],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let el7041_config = EL7041_0052Configuration {
|
||||
stm_features: shared_config::el70x1::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
..Default::default()
|
||||
@@ -190,59 +99,29 @@ impl MachineNewTrait for Winder2 {
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
device
|
||||
.0
|
||||
.write()
|
||||
.await
|
||||
.write_config(subdevice, &config)
|
||||
.write_config(&device.1, &el7041_config)
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
(device, config)
|
||||
device.0
|
||||
};
|
||||
|
||||
// Role 3
|
||||
// 1x Stepper Traverse
|
||||
// EL7031
|
||||
let (el7031, _el7031_config) = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 3)?;
|
||||
let device_hardware_identification_ethercat =
|
||||
match &device_identification.device_hardware_identification {
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 3 is not Ethercat",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let subdevice = get_subdevice_by_index(
|
||||
hardware.subdevices,
|
||||
device_hardware_identification_ethercat.subdevice_index,
|
||||
)?;
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL7031_IDENTITY_A | EL7031_IDENTITY_B => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL7031>(ethercat_device).await?
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 4 is not an EL7031",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let config = EL7031Configuration {
|
||||
// Role 3: Stepper Traverse EL7031
|
||||
let el7031 = {
|
||||
let device = get_ethercat_device::<EL7031>(
|
||||
hardware,
|
||||
params,
|
||||
3,
|
||||
vec![EL7031_IDENTITY_A, EL7031_IDENTITY_B],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let el7031_config = EL7031Configuration {
|
||||
stm_features: shared_config::el70x1::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
// Max Speed of 1000 steps/s
|
||||
// Max @ 9cm diameter = approx 85 m/min
|
||||
// Max @ 20cm diameter = approx 185 m/min
|
||||
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -253,58 +132,30 @@ impl MachineNewTrait for Winder2 {
|
||||
pdo_assignment: EL7031PredefinedPdoAssignment::VelocityControlCompact,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
device
|
||||
.0
|
||||
.write()
|
||||
.await
|
||||
.write_config(subdevice, &config)
|
||||
.write_config(&device.1, &el7031_config)
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
(device, config)
|
||||
|
||||
device.0
|
||||
};
|
||||
|
||||
// Role 4
|
||||
// 1x Stepper Puller
|
||||
// EL7031
|
||||
let (el7031_0030, _el7031_0030_config) = {
|
||||
let device_identification =
|
||||
get_device_identification_by_role(params.device_group, 4)?;
|
||||
let device_hardware_identification_ethercat =
|
||||
match &device_identification.device_hardware_identification {
|
||||
DeviceHardwareIdentification::Ethercat(
|
||||
device_hardware_identification_ethercat,
|
||||
) => device_hardware_identification_ethercat,
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 4 is not Ethercat",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let subdevice = get_subdevice_by_index(
|
||||
hardware.subdevices,
|
||||
device_hardware_identification_ethercat.subdevice_index,
|
||||
)?;
|
||||
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
|
||||
let subdevice_identity = subdevice.identity();
|
||||
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
|
||||
EL7031_0030_IDENTITY_A => {
|
||||
let ethercat_device = get_ethercat_device_by_index(
|
||||
hardware.ethercat_devices,
|
||||
subdevice_index,
|
||||
)?;
|
||||
downcast_device::<EL7031_0030>(ethercat_device).await?
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"[{}::MachineNewTrait/Winder2::new] Device with role 5 is not an EL7031-0030",
|
||||
module_path!()
|
||||
))?,
|
||||
};
|
||||
let config = EL7031_0030Configuration {
|
||||
// Role 4: Stepper Puller EL7031-0030
|
||||
let el7031_0030 = {
|
||||
let device = get_ethercat_device::<EL7031_0030>(
|
||||
hardware,
|
||||
params,
|
||||
4,
|
||||
vec![EL7031_0030_IDENTITY_A],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let el7031_0030_config = EL7031_0030Configuration {
|
||||
stm_features: el7031_0030::coe::StmFeatures {
|
||||
operation_mode: EL70x1OperationMode::DirectVelocity,
|
||||
// Max Speed of 1000 steps/s
|
||||
// Max @ 8cm diameter = approx 75 m/min
|
||||
speed_range: shared_config::el70x1::EL70x1SpeedRange::Steps1000,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -316,15 +167,12 @@ impl MachineNewTrait for Winder2 {
|
||||
..Default::default()
|
||||
};
|
||||
device
|
||||
.0
|
||||
.write()
|
||||
.await
|
||||
.write_config(subdevice, &config)
|
||||
.write_config(&device.1, &el7031_0030_config)
|
||||
.await?;
|
||||
{
|
||||
let mut device_guard = device.write().await;
|
||||
device_guard.set_used(true);
|
||||
}
|
||||
(device, config)
|
||||
device.0
|
||||
};
|
||||
|
||||
let mode = Winder2Mode::Standby;
|
||||
@@ -350,7 +198,9 @@ impl MachineNewTrait for Winder2 {
|
||||
EL7031_0030AnalogInputPort::AI1,
|
||||
)),
|
||||
laser: DigitalOutput::new(el2002, EL2002Port::DO1),
|
||||
namespace: Winder2Namespace::new(params.socket_queue_tx.clone()),
|
||||
namespace: Winder2Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
mode: mode.clone(),
|
||||
spool_step_converter: AngularStepConverter::new(200),
|
||||
spool_speed_controller: SpoolSpeedController::new(),
|
||||
@@ -380,7 +230,10 @@ impl MachineNewTrait for Winder2 {
|
||||
},
|
||||
machine_manager: params.machine_manager.clone(),
|
||||
machine_identification_unique: machine_id,
|
||||
connected_buffer: None,
|
||||
connected_buffer: MachineCrossConnection::new(
|
||||
params.machine_manager.clone(),
|
||||
¶ms.get_machine_identification_unique(),
|
||||
),
|
||||
};
|
||||
|
||||
// initalize events
|
||||
|
||||
+2
-1
@@ -14,7 +14,6 @@ use app_state::AppState;
|
||||
use mock::init::init_mock;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use ethercat::init::init_ethercat;
|
||||
use r#loop::init_loop;
|
||||
use rest::init::init_api;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
@@ -23,6 +22,8 @@ use serial::init::init_serial;
|
||||
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
|
||||
use jemalloc_stats::init_jemalloc_stats;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::ethercat::init::init_ethercat;
|
||||
use crate::panic::init_panic;
|
||||
use crate::socketio::queue::init_socketio_queue;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn init_mock(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
};
|
||||
|
||||
// Create the mock serial device
|
||||
match MockSerialDevice::new_serial(&serial_params) {
|
||||
let _ = match MockSerialDevice::new_serial(&serial_params) {
|
||||
Ok((device_identification, mock_serial_device)) => {
|
||||
// Add the mock device to the machine manager
|
||||
{
|
||||
@@ -47,7 +47,7 @@ pub fn init_mock(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event = MachinesEventBuilder().build(app_state_event.clone()).await;
|
||||
let event = MachinesEventBuilder().build(app_state_event.clone());
|
||||
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}
|
||||
@@ -79,7 +79,7 @@ pub fn init_mock(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event = MachinesEventBuilder().build(app_state_event.clone()).await;
|
||||
let event = MachinesEventBuilder().build(app_state_event.clone());
|
||||
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ use crate::{
|
||||
rest::util::{ResponseUtil, ResponseUtilError},
|
||||
};
|
||||
use axum::{Json, body::Body, extract::State, http::Response};
|
||||
use control_core::rest::mutation::{MachineMutationBody, MutationResponse};
|
||||
use control_core::{
|
||||
machines::connection::MachineConnection,
|
||||
rest::mutation::{MachineMutationBody, MutationResponse},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -27,7 +30,7 @@ async fn _post_machine_mutate(
|
||||
let machines_guard = app_state.machines.read().await;
|
||||
|
||||
// find machine with given identification in hashmap
|
||||
let machine = machines_guard
|
||||
let slot = machines_guard
|
||||
.get(&body.machine_identification_unique)
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"[{}::_post_machine_mutate] Machine not found {:?}",
|
||||
@@ -35,14 +38,21 @@ async fn _post_machine_mutate(
|
||||
body.machine_identification_unique,
|
||||
))?;
|
||||
|
||||
// check machine for error
|
||||
let machine = match machine {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
// check machine for valid connection
|
||||
let connection = &slot.lock_blocking().machine_connection;
|
||||
let machine = match connection {
|
||||
MachineConnection::Connected(m) => m,
|
||||
MachineConnection::Error(error) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::_post_machine_mutate] Machine has error: {}",
|
||||
module_path!(),
|
||||
e
|
||||
error
|
||||
));
|
||||
}
|
||||
MachineConnection::Disconnected => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::_post_machine_mutate] Machine is disconnected",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,8 +68,7 @@ pub fn init_serial(
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event =
|
||||
MachinesEventBuilder().build(app_state_event.clone()).await;
|
||||
let event = MachinesEventBuilder().build(app_state_event.clone());
|
||||
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,20 +20,8 @@ pub struct MachinesEventBuilder();
|
||||
impl MachinesEventBuilder {
|
||||
const NAME: &'static str = "MachinesEvent";
|
||||
|
||||
pub async fn build(&self, app_state: Arc<AppState>) -> Event<MachinesEvent> {
|
||||
let mut machine_objs: Vec<_> = vec![];
|
||||
// add machines
|
||||
let machines_guard = app_state.machines.read().await;
|
||||
for machine in machines_guard.iter() {
|
||||
machine_objs.push(MachineObj {
|
||||
machine_identification_unique: machine.0.clone(),
|
||||
error: match machine.1 {
|
||||
Ok(_) => None,
|
||||
Err(e) => Some(e.to_string()),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn build(&self, app_state: Arc<AppState>) -> Event<MachinesEvent> {
|
||||
let machine_objs = app_state.get_machine_objs();
|
||||
Event::new(
|
||||
Self::NAME,
|
||||
MachinesEvent {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::socketio::{
|
||||
event::GenericEvent, namespace::Namespace, namespace_id::NamespaceId,
|
||||
use control_core::{
|
||||
machines::connection::MachineConnection,
|
||||
socketio::{event::GenericEvent, namespace::Namespace, namespace_id::NamespaceId},
|
||||
};
|
||||
use smol::channel::Sender;
|
||||
use socketioxide::extract::SocketRef;
|
||||
@@ -34,8 +35,8 @@ impl Namespaces {
|
||||
let machines_guard = app_state.machines.read().await;
|
||||
|
||||
// get machine
|
||||
let machine = match machines_guard.get(&machine_identification_unique) {
|
||||
Some(machine) => machine,
|
||||
let slot = match machines_guard.get(&machine_identification_unique) {
|
||||
Some(slot) => slot,
|
||||
None => {
|
||||
callback(Err(anyhow::anyhow!(
|
||||
"[{}::Namespaces::appply_mut] Machine {:?} not found",
|
||||
@@ -45,24 +46,32 @@ impl Namespaces {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let slot = slot.lock_blocking();
|
||||
|
||||
// check if machine has error
|
||||
let machine = match machine {
|
||||
Ok(machine) => machine,
|
||||
Err(err) => {
|
||||
let machine = match &slot.machine_connection {
|
||||
MachineConnection::Error(error) => {
|
||||
callback(Err(anyhow::anyhow!(
|
||||
"[{}::Namespaces::appply_mut] Machine {:?} has error: {}",
|
||||
module_path!(),
|
||||
machine_identification_unique,
|
||||
err
|
||||
error
|
||||
)));
|
||||
return;
|
||||
}
|
||||
MachineConnection::Disconnected => {
|
||||
callback(Err(anyhow::anyhow!(
|
||||
"[{}::Namespaces::appply_mut] Machine {:?} has disconnected",
|
||||
module_path!(),
|
||||
machine_identification_unique
|
||||
)));
|
||||
return;
|
||||
}
|
||||
MachineConnection::Connected(mutex) => mutex,
|
||||
};
|
||||
|
||||
let mut machine_guard = machine.lock().await;
|
||||
let namespace = machine_guard.api_event_namespace();
|
||||
callback(Ok(namespace));
|
||||
callback(Ok(&mut namespace.lock_blocking()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user