Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2fb803033 | ||
|
|
988ed5b959 | ||
|
|
2e62f99724 | ||
|
|
530327b1e8 | ||
|
|
2f75b31fa9 | ||
|
|
ce8f0a8c4b | ||
|
|
321aae5b86 | ||
|
|
7712ea2d1f | ||
|
|
175f64f784 | ||
|
|
9357960463 | ||
|
|
4d043906bc | ||
|
|
b99b7f796b | ||
|
|
a7b0414613 | ||
|
|
13c608b11a | ||
|
|
d14fb91736 | ||
|
|
6beaa444af | ||
|
|
3b552558d4 | ||
|
|
9cc63bf8fb | ||
|
|
70ce7625b2 | ||
|
|
9936764125 | ||
|
|
a0f09e1671 | ||
|
|
1100b880f9 | ||
|
|
6224f33ba5 | ||
|
|
b0a9b5a6d2 | ||
|
|
ac7247c87a | ||
|
|
2ea459f3be | ||
|
|
25250f640f | ||
|
|
57a0cc8af4 | ||
|
|
5ebb978eae | ||
|
|
d647ee6ebe | ||
|
|
fea203ae7a | ||
|
|
b5eb413d37 | ||
|
|
aa1e5c4f39 | ||
|
|
ee91934bf7 | ||
|
|
1fa04eae9a | ||
|
|
98e1269f6f | ||
|
|
8f640fcb21 | ||
|
|
6c120b7fe9 | ||
|
|
033b93b9da | ||
|
|
6092358e87 | ||
|
|
71de39e871 | ||
|
|
54ffa193c3 | ||
|
|
03164f1ceb | ||
|
|
f67a46ab4c | ||
|
|
610790a778 | ||
|
|
6eaf451ad4 | ||
|
|
022e0adae3 | ||
|
|
6e6d4b31e7 | ||
|
|
0dafad3fb5 | ||
|
|
534de55239 | ||
|
|
d9f92bcdf2 | ||
|
|
1570299e38 | ||
|
|
c95f6f3bd9 | ||
|
|
9fcde2a234 | ||
|
|
a2ffc41e05 | ||
|
|
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 |
No files matched your search
@@ -1,3 +1,37 @@
|
||||
# `2.10.0`
|
||||
_16.10.2025_
|
||||
|
||||
## Winder V2
|
||||
- [#776](https://github.com/qitechgmbh/control/pull/776) Added explanation for padding in winder manual
|
||||
- [#804](https://github.com/qitechgmbh/control/pull/804) Added gearbox conversion
|
||||
|
||||
## General
|
||||
- [#717](https://github.com/qitechgmbh/control/pull/717) Show from option for graphs added
|
||||
- [#805](https://github.com/qitechgmbh/control/pull/805) Graphs run better now
|
||||
|
||||
|
||||
|
||||
# `2.9.1`
|
||||
_10.10.2025_
|
||||
|
||||
## Winder V2
|
||||
- [#796](https://github.com/qitechgmbh/control/pull/796) Winding in adaptive mode, while puller is in reverse mode is now possible
|
||||
|
||||
## General
|
||||
- [#780](https://github.com/qitechgmbh/control/pull/780) Updates are faster now
|
||||
- [#746](https://github.com/qitechgmbh/control/pull/746) Improved scrollbar styling for manuals
|
||||
|
||||
# `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_
|
||||
|
||||
|
||||
Generated
+30
-54
@@ -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",
|
||||
]
|
||||
|
||||
@@ -1729,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"
|
||||
@@ -2381,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",
|
||||
@@ -2393,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",
|
||||
@@ -2541,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",
|
||||
@@ -2680,8 +2689,6 @@ dependencies = [
|
||||
"smol",
|
||||
"socketioxide",
|
||||
"textplots",
|
||||
"tikv-jemalloc-ctl",
|
||||
"tikv-jemallocator",
|
||||
"tokio",
|
||||
"tonic",
|
||||
"tower-http",
|
||||
@@ -3004,37 +3011,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-ctl"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21f216790c8df74ce3ab25b534e0718da5a1916719771d3fec23315c99e468b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"paste",
|
||||
"tikv-jemalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-sys"
|
||||
version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemallocator"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tikv-jemalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "timerfd"
|
||||
version = "1.6.0"
|
||||
|
||||
@@ -7,6 +7,9 @@ QiTech Control is an industrial control panel software for the next generation o
|
||||
[](https://www.youtube.com/watch?v=KI3YeBwfV-s)
|
||||
*Click here to watch a video demo of our software.*
|
||||
|
||||
[](https://youtu.be/55egCAkQgyM)
|
||||
*Click here to watch a full explainer Video of our Software.*
|
||||
|
||||
# Repo Structure
|
||||
|
||||
Frontend
|
||||
@@ -32,7 +35,7 @@ Other
|
||||
|
||||
## Backend
|
||||
|
||||
To interface with Beckhoff and other EtherCAT devices we need an EtherCAY master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
|
||||
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
|
||||
|
||||
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
|
||||
|
||||
@@ -64,12 +67,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
|
||||
|
||||
|
||||
+5
-3
@@ -21,10 +21,12 @@ fi
|
||||
# Only setcap if not building with mock-machine
|
||||
if [ "$1" != "mock-machine" ]; then
|
||||
echo "Setting capabilities for server executable"
|
||||
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
|
||||
sudo setcap 'cap_dac_override,cap_net_raw,cap_sys_nice,cap_ipc_lock=eip' ./target/debug/server
|
||||
|
||||
echo "Setting permissions for serial ports"
|
||||
# sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
|
||||
if compgen -G "/dev/ttyUSB*" > /dev/null; then
|
||||
echo "Setting permissions for serial ports"
|
||||
sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
|
||||
fi
|
||||
fi
|
||||
|
||||
# run
|
||||
|
||||
@@ -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"
|
||||
@@ -19,19 +19,19 @@ serde_json = "1.0.143"
|
||||
serialport = "4.7.3"
|
||||
smol = "2.0.2"
|
||||
socketioxide = "0.17.2"
|
||||
uom = "0.36.0"
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
|
||||
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"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use uom::si::angle::radian;
|
||||
use uom::si::{angle::radian, f64::Angle};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AngleConverter {
|
||||
@@ -63,7 +63,7 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Convert angle from mathematical system to this system (f32)
|
||||
pub fn degrees_encode(&self, math_angle_degrees: f32) -> f32 {
|
||||
pub fn degrees_encode(&self, math_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle(math_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
@@ -85,7 +85,7 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Convert angle from this system to mathematical system (f32)
|
||||
pub fn degrees_decode(&self, system_angle_degrees: f32) -> f32 {
|
||||
pub fn degrees_decode(&self, system_angle_degrees: f64) -> f64 {
|
||||
let normalized_input = self.normalize_angle(system_angle_degrees);
|
||||
|
||||
let mut result = normalized_input;
|
||||
@@ -107,14 +107,14 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Convert angle in radians from mathematical system to this system (f32)
|
||||
pub fn radians_encode(&self, math_angle_radians: f32) -> f32 {
|
||||
pub fn radians_encode(&self, math_angle_radians: f64) -> f64 {
|
||||
let degrees = math_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_encode(degrees);
|
||||
converted_degrees.to_radians()
|
||||
}
|
||||
|
||||
/// Convert angle in radians from this system to mathematical system (f32)
|
||||
pub fn radians_decode(&self, system_angle_radians: f32) -> f32 {
|
||||
pub fn radians_decode(&self, system_angle_radians: f64) -> f64 {
|
||||
let degrees = system_angle_radians.to_degrees();
|
||||
let converted_degrees = self.degrees_decode(degrees);
|
||||
converted_degrees.to_radians()
|
||||
@@ -179,7 +179,7 @@ impl AngleConverter {
|
||||
}
|
||||
|
||||
/// Normalize angle to [0, 360) range (f32)
|
||||
fn normalize_angle(&self, angle: f32) -> f32 {
|
||||
fn normalize_angle(&self, angle: f64) -> f64 {
|
||||
let mut normalized = angle % 360.0;
|
||||
if normalized < 0.0 {
|
||||
normalized += 360.0;
|
||||
@@ -207,30 +207,12 @@ impl AngleConverterUom {
|
||||
Self { angle_converter }
|
||||
}
|
||||
|
||||
pub fn encode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
|
||||
uom::si::f32::Angle::new::<radian>(
|
||||
self.angle_converter.radians_encode(angle.get::<radian>()),
|
||||
)
|
||||
pub fn encode(&self, angle: Angle) -> Angle {
|
||||
Angle::new::<radian>(self.angle_converter.radians_encode(angle.get::<radian>()))
|
||||
}
|
||||
|
||||
pub fn decode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
|
||||
uom::si::f32::Angle::new::<radian>(
|
||||
self.angle_converter.radians_decode(angle.get::<radian>()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn encode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
|
||||
uom::si::f64::Angle::new::<radian>(
|
||||
self.angle_converter
|
||||
.radians_encode_f64(angle.get::<radian>()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn decode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
|
||||
uom::si::f64::Angle::new::<radian>(
|
||||
self.angle_converter
|
||||
.radians_decode_f64(angle.get::<radian>()),
|
||||
)
|
||||
pub fn decode(&self, angle: Angle) -> Angle {
|
||||
Angle::new::<radian>(self.angle_converter.radians_decode(angle.get::<radian>()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,8 +279,8 @@ mod tests {
|
||||
let converter = AngleConverter::screen();
|
||||
|
||||
// Test radians_encode
|
||||
let result = converter.radians_encode(PI / 2.0); // 90° in radians
|
||||
let expected = 270.0 * PI / 180.0; // 270° in radians
|
||||
let result = converter.radians_encode(PI as f64 / 2.0) as f64; // 90° in radians
|
||||
let expected: f64 = 270.0f64 * PI as f64 / 180.0f64; // 270° in radians
|
||||
assert!((result - expected).abs() < 1e-6);
|
||||
}
|
||||
|
||||
@@ -306,10 +288,10 @@ mod tests {
|
||||
fn test_roundtrip_conversion() {
|
||||
let converter = AngleConverter::new(true, false, true); // X-flip + CW
|
||||
|
||||
let original = 75.0_f32;
|
||||
let original = 75.0_f64;
|
||||
let converted = converter.degrees_encode(original);
|
||||
let roundtrip = converter.degrees_decode(converted);
|
||||
|
||||
assert!((original - roundtrip).abs() < 1e-6);
|
||||
assert!((original - roundtrip as f64).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
@@ -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!()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::{process::Command, sync::Arc, time::Duration};
|
||||
|
||||
use ethercrab::{
|
||||
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
|
||||
@@ -12,14 +12,46 @@ const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
|
||||
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
|
||||
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
|
||||
|
||||
/// Finds an ethernet interface that is suitable for EtherCAT communication.
|
||||
///
|
||||
/// ```ignore
|
||||
/// match discover_ethercat_interface().await {
|
||||
/// Ok(interface) => println!("Found working interface: {}", interface),
|
||||
/// Err(_) => println!("No working interface found"),
|
||||
/// }
|
||||
/// ```
|
||||
/// Sets a network interface to unmanaged by NetworkManager.
|
||||
/// Returns true if the command succeeded.
|
||||
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
|
||||
let managed_str = match managed {
|
||||
true => "yes",
|
||||
false => "no",
|
||||
};
|
||||
tracing::info!(
|
||||
"set_interface_managed for {} managed was set to: {}",
|
||||
interface,
|
||||
managed_str
|
||||
);
|
||||
let status = Command::new("nmcli")
|
||||
.args(["dev", "set", interface, "managed", managed_str])
|
||||
.status();
|
||||
|
||||
matches!(status, Ok(s) if s.success())
|
||||
}
|
||||
|
||||
/// Returns true if the given network interface is Ethernet.
|
||||
/// Prevents testing wlan,loopback and other non ethernet devices
|
||||
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let base_path = format!("/sys/class/net/{}", interface);
|
||||
let type_path = std::path::Path::new(&base_path).join("type");
|
||||
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
|
||||
// 1 means Ethernet
|
||||
let reports_as_ethernet = iface_type == "1";
|
||||
// Double-check that it's not a wireless interface
|
||||
let uevent_path = std::path::Path::new(&base_path).join("uevent");
|
||||
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
|
||||
// If "DEVTYPE=wlan" appears, it's lying
|
||||
let actually_wifi = uevent.contains("DEVTYPE=wlan");
|
||||
return Ok(reports_as_ethernet && !actually_wifi);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
@@ -37,7 +69,6 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
} else {
|
||||
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
}
|
||||
// Don't call the default hook, which would print the backtrace
|
||||
}));
|
||||
|
||||
// Get eligible interfaces
|
||||
@@ -48,6 +79,7 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
iface.is_up()
|
||||
&& iface.is_running()
|
||||
&& !iface.is_loopback()
|
||||
&& is_ethernet(&iface.name).unwrap_or(false)
|
||||
&& !iface.name.starts_with("bridge")
|
||||
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
|
||||
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
|
||||
@@ -56,57 +88,42 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort interfaces by name
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let mut interface: Option<&str> = None;
|
||||
|
||||
// Try all interfaces concurrently
|
||||
let tasks = interfaces
|
||||
.iter()
|
||||
.map(|iface| {
|
||||
let name = iface.name.clone();
|
||||
std::thread::Builder::new()
|
||||
.name(format!("ethercat-test-{}", name))
|
||||
.spawn(move || {
|
||||
std::panic::catch_unwind(|| {
|
||||
smol::block_on(async {
|
||||
match test_interface(&name) {
|
||||
Ok(_) => {
|
||||
tracing::info!("Found working EtherCAT interface: {}", name);
|
||||
Some(name.clone())
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::debug!("Interface {} failed", name);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(None)
|
||||
})
|
||||
.expect("Failed to spawn thread")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for i in 0..interfaces.len() {
|
||||
// Avoid NetworkManager hangups
|
||||
set_interface_managed(&interfaces[i].name, false);
|
||||
|
||||
// Wait for all tasks to complete and collect successful results
|
||||
let successful_interfaces: Vec<String> = tasks
|
||||
.into_iter()
|
||||
.filter_map(|task| task.join().expect("Should join thread"))
|
||||
.collect();
|
||||
match test_interface(&interfaces[i].name) {
|
||||
Ok(_) => {
|
||||
// if interface found with ethercat Exit early, we expect only one interface with ethercat
|
||||
interface = Some(&interfaces[i].name);
|
||||
break;
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Return the first successful interface (they're sorted by name)
|
||||
let result = successful_interfaces.into_iter().next();
|
||||
for i in 0..interfaces.len() {
|
||||
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
|
||||
continue;
|
||||
} else {
|
||||
set_interface_managed(&interfaces[i].name, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the default panic hook
|
||||
std::panic::set_hook(default_hook);
|
||||
|
||||
match result {
|
||||
Some(name) => Ok(name),
|
||||
None => Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
match interface {
|
||||
Some(interface) => return Ok(interface.to_string()),
|
||||
None => return Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
tracing::trace!("Testing interface: {}", interface);
|
||||
tracing::info!("Testing interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
@@ -144,7 +161,7 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(5),
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
@@ -416,6 +416,29 @@ Then start the machine as follows:
|
||||
|
||||
<br>
|
||||
|
||||
**Understanding Padding**
|
||||
|
||||
The **padding** parameter creates a safety margin that prevents the filament from winding too close to the spool edges. When you set the inner and outer limits during calibration, the padding value is automatically subtracted from the actual travel distance.
|
||||
|
||||
**How padding works:**
|
||||
|
||||
- When you have a 1.75mm diameter filament and the traverse moves to a flank position, the **center** of the filament aligns with that flank
|
||||
- If you set 1mm padding, the system automatically maintains at least 1mm distance from the flanks
|
||||
- This prevents the filament from overlapping or winding beyond the spool edges
|
||||
|
||||
**Example:**
|
||||
|
||||
- Outer limit: 90mm, Inner limit: 22mm
|
||||
- Padding: 1mm
|
||||
- **Actual winding area:** 89mm to 23mm (reduced by 1mm on each side)
|
||||
- This ensures the filament stays within the spool boundaries with a safe margin
|
||||
|
||||
The default padding is 0.88mm, but you can adjust this value in the Control settings based on your filament diameter and winding requirements. Larger filaments may require more padding, while smaller filaments can use less padding for maximum spool utilization.
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
**4. Start pulling**
|
||||
|
||||
- Select “Control” in the function bar
|
||||
|
||||
@@ -58,15 +58,10 @@ export function MarkdownWithToc({
|
||||
className={`sticky ${stickyTop}`}
|
||||
style={{ maxHeight: "calc(100vh - 6rem)" }}
|
||||
>
|
||||
{/* Hide scrollbar for the TOC container across browsers */}
|
||||
<style>{`
|
||||
[data-toc-scroll-container] { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
[data-toc-scroll-container]::-webkit-scrollbar { display: none; }
|
||||
`}</style>
|
||||
{/* Table of Contents with hidden scrollbar */}
|
||||
<div
|
||||
className="overflow-y-auto"
|
||||
className="scrollbar-hidden overflow-y-auto"
|
||||
style={{ maxHeight: "calc(100vh - 8rem)" }}
|
||||
data-toc-scroll-container
|
||||
>
|
||||
<MarkdownToc
|
||||
markdownContent={markdownContent}
|
||||
|
||||
@@ -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 overflow-y-auto bg-neutral-900 p-4 text-neutral-300">
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -474,6 +474,7 @@ export function BigGraph({
|
||||
isLiveMode,
|
||||
selectedTimeWindow,
|
||||
visibleSeries, // Keep for initial creation
|
||||
showFromTimestamp: syncGraph?.showFromTimestamp,
|
||||
setSelectedTimeWindow,
|
||||
setViewMode,
|
||||
setIsLiveMode,
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { Icon } from "@/components/Icon";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { TimeInput } from "@/components/time/TimeInput";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -21,8 +22,14 @@ export function GraphControls({
|
||||
onSwitchToHistorical,
|
||||
onExport,
|
||||
timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS,
|
||||
showFromTimestamp,
|
||||
onShowFromChange,
|
||||
}: ControlProps) {
|
||||
const getSelectedTimeWindowLabel = () => {
|
||||
// If Show from Time is set, display "Show All" for the time window
|
||||
if (showFromTimestamp) {
|
||||
return "Show All";
|
||||
}
|
||||
const option = timeWindowOptions.find((opt) => opt.value === timeWindow);
|
||||
return option ? option.label : "1m";
|
||||
};
|
||||
@@ -35,7 +42,10 @@ export function GraphControls({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
|
||||
className={`h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50 ${
|
||||
showFromTimestamp ? "cursor-not-allowed opacity-50" : ""
|
||||
}`}
|
||||
disabled={!!showFromTimestamp}
|
||||
>
|
||||
{getSelectedTimeWindowLabel()}
|
||||
<Icon name="lu:ChevronDown" className="ml-2 size-4" />
|
||||
@@ -60,6 +70,15 @@ export function GraphControls({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Show From Time Input - only visible in historical mode */}
|
||||
{!isLiveMode && onShowFromChange && (
|
||||
<TimeInput
|
||||
timestamp={showFromTimestamp}
|
||||
onTimeChange={onShowFromChange}
|
||||
onClear={() => onShowFromChange(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TouchButton
|
||||
onClick={onSwitchToHistorical}
|
||||
variant="outline"
|
||||
@@ -109,10 +128,16 @@ export function FloatingControlPanel({
|
||||
onSwitchToHistorical,
|
||||
onExport,
|
||||
timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS,
|
||||
showFromTimestamp,
|
||||
onShowFromChange,
|
||||
}: ControlProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const getSelectedTimeWindowLabel = () => {
|
||||
// If Show from Time is set, display "Show All" for the time window
|
||||
if (showFromTimestamp) {
|
||||
return "Show All";
|
||||
}
|
||||
const option = timeWindowOptions.find((opt) => opt.value === timeWindow);
|
||||
return option ? option.label : "1m";
|
||||
};
|
||||
@@ -134,7 +159,10 @@ export function FloatingControlPanel({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
|
||||
className={`h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50 ${
|
||||
showFromTimestamp ? "cursor-not-allowed opacity-50" : ""
|
||||
}`}
|
||||
disabled={!!showFromTimestamp}
|
||||
>
|
||||
{getSelectedTimeWindowLabel()}
|
||||
<Icon name="lu:ChevronDown" className="ml-2 size-4" />
|
||||
@@ -159,6 +187,15 @@ export function FloatingControlPanel({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Show From Time Input - only visible in historical mode */}
|
||||
{!isLiveMode && onShowFromChange && (
|
||||
<TimeInput
|
||||
timestamp={showFromTimestamp}
|
||||
onTimeChange={onShowFromChange}
|
||||
onClear={() => onShowFromChange(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TouchButton
|
||||
onClick={onSwitchToHistorical}
|
||||
variant="outline"
|
||||
|
||||
@@ -31,10 +31,8 @@ export function createChart({
|
||||
setCursorValue,
|
||||
setCursorValues,
|
||||
visibleSeries,
|
||||
}: CreateChartParams & {
|
||||
setCursorValues?: React.Dispatch<React.SetStateAction<(number | null)[]>>;
|
||||
visibleSeries?: boolean[];
|
||||
}): (() => void) | undefined {
|
||||
showFromTimestamp,
|
||||
}: CreateChartParams): (() => void) | undefined {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Get ALL series from original data
|
||||
@@ -73,7 +71,7 @@ export function createChart({
|
||||
initialMin = manualScaleRef.current.x.min;
|
||||
initialMax = manualScaleRef.current.x.max;
|
||||
} else if (selectedTimeWindow === "all") {
|
||||
initialMin = fullStart;
|
||||
initialMin = showFromTimestamp ?? fullStart;
|
||||
initialMax = getHistoricalEndTimestamp();
|
||||
} else {
|
||||
const endTimestamp = getHistoricalEndTimestamp();
|
||||
@@ -83,9 +81,10 @@ export function createChart({
|
||||
endTimestamp - (selectedTimeWindow as number),
|
||||
fullStart,
|
||||
);
|
||||
initialMin = defaultViewStart;
|
||||
initialMin = showFromTimestamp ?? defaultViewStart;
|
||||
} else {
|
||||
initialMin = endTimestamp - (selectedTimeWindow as number);
|
||||
initialMin =
|
||||
showFromTimestamp ?? endTimestamp - (selectedTimeWindow as number);
|
||||
}
|
||||
|
||||
initialMax = endTimestamp;
|
||||
|
||||
@@ -10,6 +10,7 @@ export type PropGraphSync = {
|
||||
isLiveMode: boolean;
|
||||
xRange?: { min: number; max: number };
|
||||
historicalFreezeTimestamp?: number | null;
|
||||
showFromTimestamp?: number | null;
|
||||
onTimeWindowChange?: (graphId: string, timeWindow: number | "all") => void;
|
||||
onViewModeChange?: (
|
||||
graphId: string,
|
||||
@@ -81,6 +82,8 @@ export type ControlProps = {
|
||||
onSwitchToHistorical: () => void;
|
||||
onExport?: () => void;
|
||||
timeWindowOptions?: TimeWindowOption[];
|
||||
showFromTimestamp?: number | null;
|
||||
onShowFromChange?: (timestamp: number | null) => void;
|
||||
};
|
||||
|
||||
export interface LiveModeHandlers {
|
||||
@@ -128,6 +131,9 @@ export interface CreateChartParams {
|
||||
>;
|
||||
setIsLiveMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setCursorValue: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
setCursorValues: React.Dispatch<React.SetStateAction<(number | null)[]>>;
|
||||
visibleSeries: boolean[];
|
||||
showFromTimestamp?: number | null;
|
||||
}
|
||||
|
||||
export interface AnimationState {
|
||||
|
||||
@@ -41,6 +41,7 @@ interface UseBigGraphEffectsProps {
|
||||
isLiveMode: boolean;
|
||||
selectedTimeWindow: number | "all";
|
||||
visibleSeries: boolean[];
|
||||
showFromTimestamp?: number | null;
|
||||
|
||||
// State setters
|
||||
setSelectedTimeWindow: (value: number | "all") => void;
|
||||
@@ -90,6 +91,7 @@ export function useBigGraphEffects({
|
||||
isLiveMode,
|
||||
selectedTimeWindow,
|
||||
visibleSeries,
|
||||
showFromTimestamp,
|
||||
|
||||
setSelectedTimeWindow,
|
||||
setViewMode,
|
||||
@@ -275,6 +277,7 @@ export function useBigGraphEffects({
|
||||
setCursorValue,
|
||||
setCursorValues,
|
||||
visibleSeries,
|
||||
showFromTimestamp,
|
||||
});
|
||||
|
||||
setIsChartCreated(true);
|
||||
|
||||
@@ -15,6 +15,9 @@ export function useGraphSync(exportGroupId?: string) {
|
||||
const [xRange, setXRange] = useState<
|
||||
{ min: number; max: number } | undefined
|
||||
>();
|
||||
const [showFromTimestamp, setShowFromTimestamp] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const historicalFreezeTimestampRef = useRef<number | null>(null);
|
||||
const graphDataRef = useRef<Map<string, () => GraphExportData | null>>(
|
||||
@@ -59,6 +62,7 @@ export function useGraphSync(exportGroupId?: string) {
|
||||
viewMode?: "default" | "all" | "manual";
|
||||
isLiveMode?: boolean;
|
||||
xRange?: { min: number; max: number } | undefined;
|
||||
showFromTimestamp?: number | null;
|
||||
clearHistoricalFreeze?: boolean;
|
||||
setHistoricalFreeze?: boolean;
|
||||
},
|
||||
@@ -90,6 +94,8 @@ export function useGraphSync(exportGroupId?: string) {
|
||||
if (updates.viewMode !== undefined) setViewMode(updates.viewMode);
|
||||
if (updates.isLiveMode !== undefined) setIsLiveMode(updates.isLiveMode);
|
||||
if (updates.xRange !== undefined) setXRange(updates.xRange);
|
||||
if (updates.showFromTimestamp !== undefined)
|
||||
setShowFromTimestamp(updates.showFromTimestamp);
|
||||
|
||||
// Handle historical freeze timestamp
|
||||
if (updates.clearHistoricalFreeze) {
|
||||
@@ -195,6 +201,7 @@ export function useGraphSync(exportGroupId?: string) {
|
||||
viewMode: timeWindow === "all" ? "all" : "default",
|
||||
xRange: undefined,
|
||||
clearHistoricalFreeze: true,
|
||||
showFromTimestamp: null, // Clear the show from timestamp when switching to live
|
||||
});
|
||||
}, [updateSyncState, timeWindow]);
|
||||
|
||||
@@ -206,6 +213,15 @@ export function useGraphSync(exportGroupId?: string) {
|
||||
});
|
||||
}, [updateSyncState]);
|
||||
|
||||
const handleShowFromChange = useCallback(
|
||||
(timestamp: number | null) => {
|
||||
updateSyncState("control", {
|
||||
showFromTimestamp: timestamp,
|
||||
});
|
||||
},
|
||||
[updateSyncState],
|
||||
);
|
||||
|
||||
const handleControlTimeWindowChange = useCallback(
|
||||
(newTimeWindow: number | "all") => {
|
||||
updateSyncState("control", {
|
||||
@@ -225,6 +241,7 @@ export function useGraphSync(exportGroupId?: string) {
|
||||
isLiveMode,
|
||||
xRange,
|
||||
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
|
||||
showFromTimestamp,
|
||||
onTimeWindowChange: handleTimeWindowChange,
|
||||
onViewModeChange: handleViewModeChange,
|
||||
onZoomChange: handleZoomChangeThrottled,
|
||||
@@ -237,6 +254,8 @@ export function useGraphSync(exportGroupId?: string) {
|
||||
onSwitchToLive: handleSwitchToLive,
|
||||
onSwitchToHistorical: handleSwitchToHistorical,
|
||||
onExport: handleExport,
|
||||
showFromTimestamp,
|
||||
onShowFromChange: handleShowFromChange,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -250,5 +269,6 @@ export function useGraphSync(exportGroupId?: string) {
|
||||
isLiveMode,
|
||||
xRange,
|
||||
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
|
||||
showFromTimestamp,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { Icon } from "@/components/Icon";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
type TimeInputProps = {
|
||||
timestamp?: number | null;
|
||||
onTimeChange?: (timestamp: number | null) => void;
|
||||
onClear?: () => void;
|
||||
};
|
||||
|
||||
const timeSchema = z.object({
|
||||
hours: z.number().min(0).max(23),
|
||||
minutes: z.number().min(0).max(59),
|
||||
seconds: z.number().min(0).max(59),
|
||||
});
|
||||
|
||||
type TimeFormData = z.infer<typeof timeSchema>;
|
||||
|
||||
export function TimeInput({
|
||||
timestamp,
|
||||
onTimeChange,
|
||||
onClear,
|
||||
}: TimeInputProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
// Get current time for initial values
|
||||
const getCurrentTime = useCallback(() => {
|
||||
const now = new Date();
|
||||
return {
|
||||
hours: now.getHours(),
|
||||
minutes: now.getMinutes(),
|
||||
seconds: now.getSeconds(),
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Get initial values based on timestamp or current time
|
||||
const getInitialValues = useCallback(() => {
|
||||
if (timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return {
|
||||
hours: date.getHours(),
|
||||
minutes: date.getMinutes(),
|
||||
seconds: date.getSeconds(),
|
||||
};
|
||||
}
|
||||
return getCurrentTime();
|
||||
}, [timestamp, getCurrentTime]);
|
||||
|
||||
const form = useForm<TimeFormData>({
|
||||
resolver: zodResolver(timeSchema),
|
||||
defaultValues: getInitialValues(),
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
// Convert timestamp to time components
|
||||
const timestampToTime = useCallback((ts: number) => {
|
||||
const date = new Date(ts);
|
||||
return {
|
||||
hours: date.getHours(),
|
||||
minutes: date.getMinutes(),
|
||||
seconds: date.getSeconds(),
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Check if the selected time would be in the future (today)
|
||||
const isTimeInFuture = useCallback((time: TimeFormData) => {
|
||||
const now = new Date();
|
||||
const date = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
time.hours,
|
||||
time.minutes,
|
||||
time.seconds,
|
||||
);
|
||||
return date.getTime() > now.getTime();
|
||||
}, []);
|
||||
|
||||
// Convert time components to timestamp (today's date only)
|
||||
const timeToTimestamp = useCallback((time: TimeFormData) => {
|
||||
const now = new Date();
|
||||
const date = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
time.hours,
|
||||
time.minutes,
|
||||
time.seconds,
|
||||
);
|
||||
|
||||
return date.getTime();
|
||||
}, []);
|
||||
|
||||
// Initialize form with timestamp or current time when dialog opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const initialValues = getInitialValues();
|
||||
form.reset(initialValues);
|
||||
}
|
||||
}, [isOpen, getInitialValues, form]);
|
||||
|
||||
const formatTimeDisplay = useCallback(() => {
|
||||
if (!timestamp) return "Show from Time";
|
||||
const time = timestampToTime(timestamp);
|
||||
return `${time.hours.toString().padStart(2, "0")}:${time.minutes.toString().padStart(2, "0")}:${time.seconds.toString().padStart(2, "0")}`;
|
||||
}, [timestamp, timestampToTime]);
|
||||
|
||||
// Increment/decrement functions with bounds checking and cascading
|
||||
const adjustTimeValue = useCallback(
|
||||
(field: "hours" | "minutes" | "seconds", increment: boolean) => {
|
||||
// Define the order of fields for cascading
|
||||
const fields: ("hours" | "minutes" | "seconds")[] = [
|
||||
"seconds",
|
||||
"minutes",
|
||||
"hours",
|
||||
];
|
||||
const startIndex = fields.indexOf(field);
|
||||
let carry = increment ? 1 : -1;
|
||||
|
||||
for (let i = startIndex; i < fields.length; i++) {
|
||||
const currentField = fields[i];
|
||||
const currentValue = form.getValues(currentField);
|
||||
const max = currentField === "hours" ? 23 : 59;
|
||||
let newValue = currentValue + carry;
|
||||
carry = 0;
|
||||
|
||||
if (increment) {
|
||||
if (newValue > max) {
|
||||
newValue = 0;
|
||||
carry = 1;
|
||||
}
|
||||
} else {
|
||||
if (newValue < 0) {
|
||||
newValue = max;
|
||||
carry = -1;
|
||||
}
|
||||
}
|
||||
|
||||
form.setValue(currentField, newValue, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
|
||||
// If no carry, stop cascading
|
||||
if (carry === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
form.handleSubmit((data) => {
|
||||
// Prevent submission if time is in the future
|
||||
if (isTimeInFuture(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTimestamp = timeToTimestamp(data);
|
||||
onTimeChange?.(newTimestamp);
|
||||
setIsOpen(false);
|
||||
})();
|
||||
};
|
||||
|
||||
const handleAbort = () => {
|
||||
if (timestamp) {
|
||||
const time = timestampToTime(timestamp);
|
||||
form.reset(time);
|
||||
} else {
|
||||
const currentTime = getCurrentTime();
|
||||
form.reset(currentTime);
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
onClear?.();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
// Watch individual form fields for real-time updates
|
||||
const hours = useWatch({
|
||||
control: form.control,
|
||||
name: "hours",
|
||||
defaultValue: getInitialValues().hours,
|
||||
});
|
||||
|
||||
const minutes = useWatch({
|
||||
control: form.control,
|
||||
name: "minutes",
|
||||
defaultValue: getInitialValues().minutes,
|
||||
});
|
||||
|
||||
const seconds = useWatch({
|
||||
control: form.control,
|
||||
name: "seconds",
|
||||
defaultValue: getInitialValues().seconds,
|
||||
});
|
||||
|
||||
// Get current form values with fallback
|
||||
const currentValues = {
|
||||
hours: hours ?? 0,
|
||||
minutes: minutes ?? 0,
|
||||
seconds: seconds ?? 0,
|
||||
};
|
||||
|
||||
// TimeValueInput component for individual time fields
|
||||
const TimeValueInput = ({
|
||||
field,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
field: "hours" | "minutes" | "seconds";
|
||||
label: string;
|
||||
value: number;
|
||||
}) => (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<label className="text-sm font-medium text-gray-700">{label}</label>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-10 w-16 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
adjustTimeValue(field, true);
|
||||
}}
|
||||
>
|
||||
<Icon name="lu:ChevronUp" className="size-4" />
|
||||
</TouchButton>
|
||||
<div className="flex h-16 w-20 items-center justify-center rounded border-2 border-gray-300 bg-white font-mono text-3xl">
|
||||
{value.toString().padStart(2, "0")}
|
||||
</div>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-10 w-16 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
adjustTimeValue(field, false);
|
||||
}}
|
||||
>
|
||||
<Icon name="lu:ChevronDown" className="size-4" />
|
||||
</TouchButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
|
||||
>
|
||||
<Icon name="lu:Clock" className="mr-2 size-4" />
|
||||
{formatTimeDisplay()}
|
||||
</TouchButton>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-2xl p-8">
|
||||
<div className="flex flex-col gap-8">
|
||||
{/* Header */}
|
||||
<div className="flex min-h-[2.5rem] items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon name="lu:Clock" className="size-6" />
|
||||
<h3 className="text-xl font-semibold">Show from Time</h3>
|
||||
</div>
|
||||
{timestamp && (
|
||||
<TouchButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClear}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Icon name="lu:X" className="mr-2 size-4" />
|
||||
Clear
|
||||
</TouchButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="-mt-4 text-base text-gray-600">
|
||||
Set a specific time to display data from that point forward in the
|
||||
graph.
|
||||
</div>
|
||||
|
||||
{/* Error for future time */}
|
||||
{isTimeInFuture(currentValues) && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Icon name="lu:X" className="mt-0.5 size-5 text-red-600" />
|
||||
<div>
|
||||
<div className="font-medium text-red-800">
|
||||
Cannot set future time
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-red-700">
|
||||
The selected time is in the future. Please choose a time
|
||||
that has already occurred today.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Time inputs */}
|
||||
<div className="flex items-center justify-center gap-8">
|
||||
<TimeValueInput
|
||||
field="hours"
|
||||
label="Hours"
|
||||
value={currentValues.hours}
|
||||
/>
|
||||
<div className="mt-8 text-4xl font-bold">:</div>
|
||||
<TimeValueInput
|
||||
field="minutes"
|
||||
label="Minutes"
|
||||
value={currentValues.minutes}
|
||||
/>
|
||||
<div className="mt-8 text-4xl font-bold">:</div>
|
||||
<TimeValueInput
|
||||
field="seconds"
|
||||
label="Seconds"
|
||||
value={currentValues.seconds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-4">
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
className="h-14 flex-1"
|
||||
onClick={handleAbort}
|
||||
>
|
||||
<Icon name="lu:X" className="mr-2 size-4" />
|
||||
Cancel
|
||||
</TouchButton>
|
||||
<TouchButton
|
||||
className={`h-14 flex-1 ${
|
||||
isTimeInFuture(currentValues)
|
||||
? "cursor-not-allowed bg-gray-300 text-gray-500"
|
||||
: "bg-blue-600 text-white hover:bg-blue-700"
|
||||
}`}
|
||||
onClick={handleSubmit}
|
||||
disabled={isTimeInFuture(currentValues)}
|
||||
>
|
||||
<Icon name="lu:Check" className="mr-2 size-4" />
|
||||
Apply
|
||||
</TouchButton>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -13,7 +13,11 @@ import { Label } from "@/control/Label";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { StatusBadge } from "@/control/StatusBadge";
|
||||
import { useWinder2 } from "./useWinder";
|
||||
import { Mode, SpoolAutomaticActionMode } from "./winder2Namespace";
|
||||
import {
|
||||
Mode,
|
||||
SpoolAutomaticActionMode,
|
||||
getGearRatioMultiplier,
|
||||
} from "./winder2Namespace";
|
||||
import { TensionArm } from "../TensionArm";
|
||||
import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal";
|
||||
import { Spool } from "../Spool";
|
||||
@@ -54,6 +58,13 @@ export function Winder2ControlPage() {
|
||||
isDisabled,
|
||||
} = useWinder2();
|
||||
|
||||
// Calculate max speed based on gear ratio
|
||||
const gearRatioMultiplier = getGearRatioMultiplier(
|
||||
state?.puller_state?.gear_ratio,
|
||||
);
|
||||
const maxMotorSpeed = 75; // Maximum motor speed in m/min
|
||||
const maxTargetSpeed = maxMotorSpeed / gearRatioMultiplier;
|
||||
|
||||
const handleResetProgress = () => {
|
||||
// Check if the machine is currently in Wind mode
|
||||
if (state?.mode_state?.mode === "Wind") {
|
||||
@@ -274,7 +285,7 @@ export function Winder2ControlPage() {
|
||||
title="Target Speed"
|
||||
defaultValue={defaultState?.puller_state?.target_speed}
|
||||
min={0}
|
||||
max={75}
|
||||
max={maxTargetSpeed}
|
||||
step={0.1}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={setPullerTargetSpeed}
|
||||
|
||||
@@ -67,6 +67,16 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
data.puller_state?.forward ? "Forward" : "Backward",
|
||||
},
|
||||
{
|
||||
name: "Puller Gear Ratio",
|
||||
renderValue: (data: PresetData<Winder2>) => {
|
||||
const ratio = data.puller_state?.gear_ratio;
|
||||
if (ratio === "OneToOne") return "1:1";
|
||||
if (ratio === "OneToFive") return "1:5";
|
||||
if (ratio === "OneToTen") return "1:10";
|
||||
return "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Puller Target Speed",
|
||||
unit: "m/min",
|
||||
@@ -153,6 +163,7 @@ export function Winder2PresetsPage() {
|
||||
setPullerTargetDiameter,
|
||||
setPullerForward,
|
||||
setPullerTargetSpeed,
|
||||
setPullerGearRatio,
|
||||
|
||||
setSpoolRegulationMode,
|
||||
|
||||
@@ -177,6 +188,7 @@ export function Winder2PresetsPage() {
|
||||
setPullerRegulationMode(preset.data?.puller_state?.regulation ?? "Speed");
|
||||
setPullerForward(preset.data?.puller_state?.forward ?? true);
|
||||
setPullerTargetSpeed(preset.data?.puller_state?.target_speed ?? 1.0);
|
||||
setPullerGearRatio(preset.data?.puller_state?.gear_ratio ?? "OneToOne");
|
||||
// setPullerTargetDiameter(preset.data?.puller_state?.target_diameter ?? 1.75);
|
||||
|
||||
setSpoolRegulationMode(
|
||||
|
||||
@@ -17,6 +17,7 @@ export function Winder2SettingPage() {
|
||||
setTraverseStepSize,
|
||||
setTraversePadding,
|
||||
setPullerForward,
|
||||
setPullerGearRatio,
|
||||
setSpoolRegulationMode,
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
@@ -44,7 +45,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)}
|
||||
@@ -249,6 +250,32 @@ export function Winder2SettingPage() {
|
||||
onChange={(value) => setPullerForward(value)}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Gear Ratio">
|
||||
<SelectionGroup
|
||||
value={state?.puller_state?.gear_ratio}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
options={{
|
||||
OneToOne: {
|
||||
children: "1:1",
|
||||
icon: "lu:Circle",
|
||||
},
|
||||
OneToFive: {
|
||||
children: "1:5",
|
||||
icon: "lu:Cog",
|
||||
},
|
||||
OneToTen: {
|
||||
children: "1:10",
|
||||
icon: "lu:Cog",
|
||||
},
|
||||
}}
|
||||
onChange={(value) =>
|
||||
setPullerGearRatio(
|
||||
value as "OneToOne" | "OneToFive" | "OneToTen",
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
<MachineSelector
|
||||
machines={filteredMachines}
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
PullerRegulation,
|
||||
SpoolAutomaticActionMode,
|
||||
spoolAutomaticActionModeSchema,
|
||||
gearRatioSchema,
|
||||
GearRatio,
|
||||
} from "./winder2Namespace";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { produce } from "immer";
|
||||
@@ -120,6 +122,9 @@ export function useWinder2() {
|
||||
const { request: requestPullerSetForward } = useMachineMutation(
|
||||
z.object({ SetPullerForward: z.boolean() }),
|
||||
);
|
||||
const { request: requestPullerSetGearRatio } = useMachineMutation(
|
||||
z.object({ SetPullerGearRatio: gearRatioSchema }),
|
||||
);
|
||||
const { request: requestSpoolSetRegulationMode } = useMachineMutation(
|
||||
z.object({ SetSpoolRegulationMode: spoolRegulationModeSchema }),
|
||||
);
|
||||
@@ -366,6 +371,28 @@ export function useWinder2() {
|
||||
);
|
||||
};
|
||||
|
||||
const setPullerGearRatio = (gearRatio: GearRatio) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.puller_state.gear_ratio = gearRatio;
|
||||
// Reset target speed to 0 to prevent sudden speed changes
|
||||
current.data.puller_state.target_speed = 0;
|
||||
},
|
||||
async () => {
|
||||
// First set the target speed to 0
|
||||
await requestPullerSetTargetSpeed({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetPullerTargetSpeed: 0 },
|
||||
});
|
||||
// Then set the gear ratio
|
||||
await requestPullerSetGearRatio({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetPullerGearRatio: gearRatio },
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setSpoolAutomaticRequiredMeters = (meters: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
@@ -619,6 +646,7 @@ export function useWinder2() {
|
||||
setPullerTargetDiameter,
|
||||
setPullerRegulationMode,
|
||||
setPullerForward,
|
||||
setPullerGearRatio,
|
||||
setSpoolAutomaticRequiredMeters,
|
||||
setSpoolAutomaticAction,
|
||||
setSpoolRegulationMode,
|
||||
|
||||
@@ -42,6 +42,30 @@ export const liveValuesEventDataSchema = z.object({
|
||||
export const pullerRegulationSchema = z.enum(["Speed", "Diameter"]);
|
||||
export type PullerRegulation = z.infer<typeof pullerRegulationSchema>;
|
||||
|
||||
/**
|
||||
* Gear ratio enum for winding speed
|
||||
*/
|
||||
export const gearRatioSchema = z.enum(["OneToOne", "OneToFive", "OneToTen"]);
|
||||
export type GearRatio = z.infer<typeof gearRatioSchema>;
|
||||
|
||||
/**
|
||||
* Get the multiplier for a gear ratio
|
||||
*/
|
||||
export function getGearRatioMultiplier(
|
||||
gearRatio: GearRatio | undefined,
|
||||
): number {
|
||||
switch (gearRatio) {
|
||||
case "OneToOne":
|
||||
return 1.0;
|
||||
case "OneToFive":
|
||||
return 5.0;
|
||||
case "OneToTen":
|
||||
return 10.0;
|
||||
default:
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Machine operation mode enum
|
||||
*/
|
||||
@@ -105,6 +129,7 @@ export const pullerStateSchema = z.object({
|
||||
target_speed: z.number(),
|
||||
target_diameter: z.number(),
|
||||
forward: z.boolean(),
|
||||
gear_ratio: gearRatioSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,11 +3,7 @@ import { Page } from "@/components/Page";
|
||||
import { SectionTitle } from "@/components/SectionTitle";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
defaultGithubSource,
|
||||
GithubSource,
|
||||
GithubSourceDialog,
|
||||
} from "./GithubSourceDialog";
|
||||
import { GithubSourceDialog } from "./GithubSourceDialog";
|
||||
import { LoadingSpinner } from "@/components/LoadingSpinner";
|
||||
import { Alert } from "@/components/Alert";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -21,6 +17,7 @@ import {
|
||||
isNixOSAvailable,
|
||||
} from "@/helpers/nixos_helpers";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import { useGithubSourceStore } from "@/stores/githubSourceStore";
|
||||
|
||||
export function ChooseVersionPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -54,8 +51,7 @@ export function ChooseVersionPage() {
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const [githubSource, setGithubSource] =
|
||||
useState<GithubSource>(defaultGithubSource);
|
||||
const { githubSource, setGithubSource } = useGithubSourceStore();
|
||||
|
||||
const githubApiUrl = `https://api.github.com/repos/${githubSource.githubRepoOwner}/${githubSource.githubRepoName}`;
|
||||
|
||||
|
||||
@@ -114,11 +114,13 @@ export function GithubSourceDialogContent({
|
||||
name="githubRepoOwner"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Serial</FormLabel>
|
||||
<FormLabel>GitHub Repository Owner</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="qitechgmbh" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Serial number of the machine.</FormDescription>
|
||||
<FormDescription>
|
||||
Name of the GitHub repository owner.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { GithubSource, defaultGithubSource } from "@/setup/GithubSourceDialog";
|
||||
|
||||
export type GithubSourceState = {
|
||||
githubSource: GithubSource;
|
||||
};
|
||||
|
||||
export type GithubSourceActions = {
|
||||
setGithubSource: (source: GithubSource) => void;
|
||||
resetGithubSource: () => void;
|
||||
};
|
||||
|
||||
export type GithubSourceStore = GithubSourceState & GithubSourceActions;
|
||||
|
||||
export const useGithubSourceStore = create<GithubSourceStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
githubSource: defaultGithubSource,
|
||||
|
||||
setGithubSource: (source: GithubSource) => set({ githubSource: source }),
|
||||
|
||||
resetGithubSource: () => set({ githubSource: defaultGithubSource }),
|
||||
}),
|
||||
{
|
||||
name: "github-source-storage",
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -177,3 +177,107 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* macOS-style scrollbars */
|
||||
@layer base {
|
||||
/* WebKit browsers (Safari, Chrome, newer Edge) */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:active {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
html,
|
||||
body {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
}
|
||||
|
||||
.dark html,
|
||||
.dark body {
|
||||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* Auto-hide behavior for overlay scrollbars */
|
||||
@media (pointer: fine) {
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:hover {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* Container hover shows scrollbars */
|
||||
.scrollbar-visible:hover::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Utility classes for scrollbar customization */
|
||||
.scrollbar-hidden {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hidden::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-always-visible::-webkit-scrollbar {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
}
|
||||
|
||||
@media (pointer: fine) {
|
||||
.scrollbar-always-visible::-webkit-scrollbar {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -7,12 +7,12 @@ 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"] }
|
||||
smol = "2.0.2"
|
||||
uom = "0.36.0"
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64","si"] }
|
||||
rand = "0.9.2"
|
||||
futures = "0.3.31"
|
||||
tracing = "0.1.41"
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
libpcap
|
||||
nodejs_22
|
||||
nodePackages.npm
|
||||
lldb
|
||||
];
|
||||
|
||||
hardeningDisable = [ "fortify" ];
|
||||
|
||||
@@ -78,18 +78,20 @@ serviceConfig = {
|
||||
Restart = "always";
|
||||
RestartSec = "10s";
|
||||
|
||||
# Capabilities (unchanged, as you wanted)
|
||||
CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
|
||||
AmbientCapabilities = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
|
||||
# Capabilities
|
||||
CapabilityBoundingSet = "CAP_NET_RAW CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
|
||||
AmbientCapabilities = "CAP_NET_RAW CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
|
||||
|
||||
# Hardening options
|
||||
NoNewPrivileges = true;
|
||||
ProtectSystem = "strict";
|
||||
|
||||
# Open only /proc/irq explicitly
|
||||
ReadWritePaths = [ "/proc/irq" ];
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = false;
|
||||
|
||||
# Must disable this to allow /proc/irq writes
|
||||
ProtectKernelTunables = false;
|
||||
ProtectControlGroups = true;
|
||||
|
||||
@@ -15,12 +15,15 @@ in
|
||||
consoleMode = "max"; # Use the highest available resolution
|
||||
};
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages-rt_latest;
|
||||
boot.kernelPackages = pkgs.linuxPackages_6_13;
|
||||
boot.kernelModules = [ "i915" ];
|
||||
|
||||
|
||||
|
||||
boot.kernelParams = [
|
||||
# Realtime Preemption
|
||||
"preempt=full"
|
||||
|
||||
# Graphical
|
||||
"logo.nologo" # Remove kernel logo during boot
|
||||
|
||||
|
||||
@@ -102,11 +102,10 @@ installPhase = ''
|
||||
--name="QiTech Control" \
|
||||
--single-instance \
|
||||
--winrm-class="de.qitech.control-electron" \
|
||||
--enable-gpu \
|
||||
--enable-gpu \
|
||||
--ignore-gpu-blocklist \
|
||||
--enable-gpu-rasterization \
|
||||
--disable-software-rasterizer \
|
||||
--use-gl=desktop \
|
||||
--disable-software-rasterizer \
|
||||
"\$@"
|
||||
EOF
|
||||
chmod +x $out/bin/qitech-control-electron
|
||||
|
||||
+7
-6
@@ -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"
|
||||
@@ -55,9 +55,9 @@ opentelemetry-otlp = { version = "0.30.0", features = [
|
||||
], optional = true }
|
||||
tonic = { version = "0.13.1", optional = true }
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator = { version = "0.6", features = ["profiling", "stats"] }
|
||||
tikv-jemalloc-ctl = "0.6"
|
||||
#[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
#tikv-jemallocator = { version = "0.6", features = ["profiling", "stats"] }
|
||||
#tikv-jemalloc-ctl = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
@@ -77,6 +77,7 @@ tracing-otel = [
|
||||
"dep:tonic",
|
||||
]
|
||||
mock-machine = []
|
||||
memory-locking = []
|
||||
io-uring = []
|
||||
development-build = []
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::setup::setup_loop;
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
panic::{PanicDetails, send_panic},
|
||||
socketio::main_namespace::{
|
||||
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
|
||||
},
|
||||
@@ -10,21 +9,14 @@ use control_core::{
|
||||
ethercat::interface_discovery::discover_ethercat_interface,
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
use smol::channel::Sender;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn init_ethercat(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let thread_panic_tx_clone = thread_panic_tx;
|
||||
pub fn init_ethercat(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
let app_state_clone = app_state;
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("init_ethercat".to_string())
|
||||
.spawn(move || {
|
||||
send_panic(thread_panic_tx_clone.clone());
|
||||
|
||||
// Notify client via socketio
|
||||
let app_state_socketio = app_state_clone.clone();
|
||||
smol::block_on(async move {
|
||||
@@ -69,11 +61,7 @@ pub fn init_ethercat(
|
||||
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
|
||||
});
|
||||
|
||||
if let Err(e) = smol::block_on(setup_loop(
|
||||
thread_panic_tx_clone,
|
||||
&interface,
|
||||
app_state_clone,
|
||||
)) {
|
||||
if let Err(e) = smol::block_on(setup_loop(&interface, app_state_clone)) {
|
||||
tracing::error!("EtherCAT setup failed: {:?}", e);
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::app_state::EthercatSetup;
|
||||
use crate::machines::registry::MACHINE_REGISTRY;
|
||||
use crate::panic::{PanicDetails, send_panic};
|
||||
use crate::socketio::main_namespace::MainNamespaceEvents;
|
||||
use crate::socketio::main_namespace::ethercat_devices_event::EthercatDevicesEventBuilder;
|
||||
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
|
||||
@@ -20,17 +19,12 @@ use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use ethercat_hal::devices::devices_from_subdevices;
|
||||
use ethercrab::std::ethercat_now;
|
||||
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
|
||||
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,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
pub async fn setup_loop(interface: &str, app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("Starting Ethercat PDU loop");
|
||||
|
||||
// Erase all all setup data from `app_state`
|
||||
@@ -44,12 +38,10 @@ pub async fn setup_loop(
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<MAX_FRAMES, MAX_PDU_DATA>::new()));
|
||||
let (tx, rx, pdu) = pdu_storage.try_split().expect("can only split once");
|
||||
let interface = interface.to_string();
|
||||
let thread_panic_tx_clone = thread_panic_tx.clone();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("EthercatTxRxThread".to_owned())
|
||||
.spawn(move || {
|
||||
send_panic(thread_panic_tx_clone);
|
||||
|
||||
#[cfg(all(target_os = "linux", not(feature = "development-build")))]
|
||||
match set_irq_affinity(&interface, 3) {
|
||||
Ok(_) => tracing::info!("ethernet interrupt handler now runs on cpu:{}", 3),
|
||||
@@ -235,7 +227,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));
|
||||
});
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
pub fn init_jemalloc_stats() {
|
||||
std::thread::Builder::new()
|
||||
.name("jemalloc-stats".to_string())
|
||||
.spawn(|| {
|
||||
let mut last_allocated = 0usize;
|
||||
let mut last_time = std::time::Instant::now();
|
||||
|
||||
loop {
|
||||
smol::block_on(async {
|
||||
sleep(Duration::from_secs(10));
|
||||
// Advance epoch and get allocated bytes
|
||||
let _ = tikv_jemalloc_ctl::epoch::advance();
|
||||
|
||||
if let Ok(allocated_bytes) =
|
||||
unsafe { tikv_jemalloc_ctl::raw::read::<usize>(b"stats.allocated\0") }
|
||||
{
|
||||
let current_time = std::time::Instant::now();
|
||||
let time_diff = current_time.duration_since(last_time).as_secs_f64();
|
||||
|
||||
let change = allocated_bytes as i64 - last_allocated as i64;
|
||||
let change_per_sec = if time_diff > 0.0 {
|
||||
(change as f64 / time_diff) as i64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let change_str = if change_per_sec >= 0 {
|
||||
format!("+{} KB/s", format_bytes(change_per_sec))
|
||||
} else {
|
||||
format!("-{} KB/s", format_bytes(-change_per_sec))
|
||||
};
|
||||
|
||||
let formatted = format_bytes(allocated_bytes as i64);
|
||||
|
||||
tracing::debug!("Memory: {} KB ({})", formatted, change_str);
|
||||
last_allocated = allocated_bytes;
|
||||
last_time = current_time;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.expect("Failed to spawn jemalloc-stats thread");
|
||||
}
|
||||
|
||||
fn format_bytes(bytes: i64) -> String {
|
||||
let kb = bytes / 1024;
|
||||
format!("{}", kb)
|
||||
.chars()
|
||||
.rev()
|
||||
.enumerate()
|
||||
.map(|(i, c)| {
|
||||
if i > 0 && i % 3 == 0 {
|
||||
format!("_{}", c)
|
||||
} else {
|
||||
c.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<String>()
|
||||
.chars()
|
||||
.rev()
|
||||
.collect::<String>()
|
||||
}
|
||||
+4
-8
@@ -1,21 +1,16 @@
|
||||
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;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{instrument, trace_span};
|
||||
|
||||
pub fn init_loop(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
pub fn init_loop(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
// Start control loop
|
||||
std::thread::Builder::new()
|
||||
.name("loop".to_owned())
|
||||
.spawn(move || {
|
||||
send_panic(thread_panic_tx.clone());
|
||||
let rt = smol::LocalExecutor::new();
|
||||
|
||||
// Set core affinity to third core
|
||||
@@ -144,7 +139,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() {
|
||||
|
||||
@@ -13,8 +13,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)]
|
||||
@@ -94,7 +93,7 @@ enum Mutation {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AquaPathV1Namespace {
|
||||
pub namespace: Namespace,
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<AquaPathV1Events> for AquaPathV1Namespace {
|
||||
@@ -102,15 +101,9 @@ impl NamespaceCacheingLogic<AquaPathV1Events> for AquaPathV1Namespace {
|
||||
fn emit(&mut self, events: AquaPathV1Events) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
self.namespace.emit(event, &buffer_fn);
|
||||
}
|
||||
}
|
||||
|
||||
impl AquaPathV1Namespace {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +149,7 @@ impl MachineApi for AquaPathV1 {
|
||||
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,8 +1,9 @@
|
||||
use control_core::{
|
||||
machines::identification::MachineIdentification, socketio::namespace::NamespaceCacheingLogic,
|
||||
machines::identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
use control_core_derive::Machine;
|
||||
|
||||
use control_core_derive::Machine;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
use uom::si::{
|
||||
@@ -79,12 +80,14 @@ impl Default for Flow {
|
||||
|
||||
#[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,
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use super::{AquaPathV1, AquaPathV1Mode};
|
||||
use crate::machines::aquapath1::{
|
||||
Flow, Temperature, api::AquaPathV1Namespace, controller::Controller,
|
||||
use crate::machines::{
|
||||
aquapath1::{Flow, Temperature, api::AquaPathV1Namespace, controller::Controller},
|
||||
get_ethercat_device,
|
||||
};
|
||||
use anyhow::Error;
|
||||
use control_core::machines::{
|
||||
identification::DeviceHardwareIdentification,
|
||||
new::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_device_identification_by_role,
|
||||
get_ethercat_device_by_index, get_subdevice_by_index, validate_no_role_dublicates,
|
||||
validate_same_machine_identification_unique,
|
||||
},
|
||||
use control_core::machines::new::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, 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},
|
||||
@@ -23,7 +19,6 @@ use ethercat_hal::{
|
||||
EL5152, EL5152_IDENTITY_A, EL5152Configuration, EL5152Port,
|
||||
EL5152PredefinedPdoAssignment,
|
||||
},
|
||||
subdevice_identity_to_tuple,
|
||||
},
|
||||
io::{
|
||||
analog_output::AnalogOutput, digital_output::DigitalOutput, encoder_input::EncoderInput,
|
||||
@@ -56,217 +51,46 @@ impl MachineNewTrait for AquaPathV1 {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
let _ek1100 =
|
||||
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
|
||||
|
||||
// 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
|
||||
};
|
||||
let el2008 =
|
||||
get_ethercat_device::<EL2008>(hardware, params, 1, [EL2008_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
// 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 el4002 =
|
||||
get_ethercat_device::<EL4002>(hardware, params, 2, [EL4002_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
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 el3204 = get_ethercat_device::<EL3204>(
|
||||
hardware,
|
||||
params,
|
||||
3,
|
||||
[EL3204_IDENTITY_A, EL3204_IDENTITY_B].to_vec(),
|
||||
)
|
||||
.await?
|
||||
.0;
|
||||
|
||||
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 el5152 =
|
||||
get_ethercat_device::<EL5152>(hardware, params, 4, [EL5152_IDENTITY_A].to_vec())
|
||||
.await?
|
||||
.0;
|
||||
|
||||
{
|
||||
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 config = EL5152Configuration {
|
||||
pdo_assignment: EL5152PredefinedPdoAssignment::Frequency,
|
||||
..Default::default()
|
||||
};
|
||||
let subdevice = get_subdevice_by_index(hardware.subdevices, 4)?;
|
||||
el5152
|
||||
.write()
|
||||
.await
|
||||
.write_config(&subdevice, &config)
|
||||
.await?;
|
||||
|
||||
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);
|
||||
@@ -338,7 +162,10 @@ impl MachineNewTrait for AquaPathV1 {
|
||||
);
|
||||
|
||||
let mut water_cooling = Self {
|
||||
namespace: AquaPathV1Namespace::new(params.socket_queue_tx.clone()),
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: AquaPathV1Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
},
|
||||
mode: AquaPathV1Mode::Standby,
|
||||
last_measurement_emit: Instant::now(),
|
||||
front_controller,
|
||||
|
||||
@@ -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,310 +1,141 @@
|
||||
#[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
|
||||
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/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);
|
||||
}
|
||||
}
|
||||
// Role 0 - Buscoupler EK1100
|
||||
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 +211,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 +219,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,18 @@
|
||||
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;
|
||||
@@ -13,3 +28,111 @@ 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))
|
||||
}
|
||||
@@ -186,7 +186,7 @@ impl AdaptiveSpoolSpeedController {
|
||||
puller_speed_controller: &PullerSpeedController,
|
||||
) -> AngularVelocity {
|
||||
let min_speed = AngularVelocity::ZERO;
|
||||
let max_speed = self.get_max_speed(puller_speed_controller);
|
||||
let max_speed = self.get_max_speed(puller_speed_controller).abs();
|
||||
|
||||
// Calculate filament tension from arm angle
|
||||
let tension_arm_angle = tension_arm.get_angle();
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use super::puller_speed_controller::GearRatio;
|
||||
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 +17,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},
|
||||
@@ -74,6 +77,7 @@ enum Mutation {
|
||||
SetPullerTargetSpeed(f64),
|
||||
SetPullerTargetDiameter(f64),
|
||||
SetPullerForward(bool),
|
||||
SetPullerGearRatio(GearRatio),
|
||||
|
||||
// Spool Speed Controller
|
||||
SetSpoolRegulationMode(super::spool_speed_controller::SpoolSpeedControllerType),
|
||||
@@ -141,7 +145,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)]
|
||||
@@ -188,6 +192,8 @@ pub struct PullerState {
|
||||
pub target_diameter: f64,
|
||||
/// forward rotation direction
|
||||
pub forward: bool,
|
||||
/// gear ratio for winding speed
|
||||
pub gear_ratio: GearRatio,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
@@ -237,13 +243,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 +250,7 @@ pub enum Winder2Events {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Winder2Namespace {
|
||||
pub namespace: Namespace,
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<Winder2Events> for Winder2Namespace {
|
||||
@@ -259,15 +258,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +300,7 @@ impl MachineApi for Winder2 {
|
||||
Mutation::SetPullerTargetSpeed(value) => self.puller_set_target_speed(value),
|
||||
Mutation::SetPullerTargetDiameter(_) => todo!(),
|
||||
Mutation::SetPullerForward(value) => self.puller_set_forward(value),
|
||||
Mutation::SetPullerGearRatio(gear_ratio) => self.puller_set_gear_ratio(gear_ratio),
|
||||
Mutation::SetSpoolRegulationMode(mode) => self.spool_set_regulation_mode(mode),
|
||||
Mutation::SetSpoolMinMaxMinSpeed(speed) => self.spool_set_minmax_min_speed(speed),
|
||||
Mutation::SetSpoolMinMaxMaxSpeed(speed) => self.spool_set_minmax_max_speed(speed),
|
||||
@@ -341,7 +335,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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::machines::buffer1::BufferV1;
|
||||
|
||||
use super::puller_speed_controller::PullerRegulationMode;
|
||||
use super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
|
||||
use api::{
|
||||
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode, SpoolAutomaticActionState,
|
||||
SpoolSpeedControllerState, StateEvent, TensionArmState, TraverseState, Winder2Events,
|
||||
};
|
||||
use control_core::socketio::event::BuildEvent;
|
||||
use control_core::{
|
||||
machines::identification::MachineIdentificationUnique,
|
||||
socketio::namespace::NamespaceCacheingLogic, uom_extensions::velocity::meter_per_minute,
|
||||
};
|
||||
|
||||
use uom::si::{
|
||||
angle::degree,
|
||||
angular_velocity::revolution_per_minute,
|
||||
f64::{Length, Velocity},
|
||||
length::{meter, millimeter},
|
||||
};
|
||||
|
||||
impl Winder2 {
|
||||
/// Implement Spool
|
||||
/// called by `act`
|
||||
pub fn sync_spool_speed(&mut self, t: Instant) {
|
||||
let angular_velocity = self.spool_speed_controller.update_speed(
|
||||
t,
|
||||
&self.tension_arm,
|
||||
&self.puller_speed_controller,
|
||||
);
|
||||
let steps_per_second = self
|
||||
.spool_step_converter
|
||||
.angular_velocity_to_steps(angular_velocity);
|
||||
let _ = self.spool.set_speed(steps_per_second);
|
||||
}
|
||||
|
||||
pub fn stop_or_pull_spool(&mut self, now: Instant) {
|
||||
if matches!(
|
||||
self.spool_automatic_action.mode,
|
||||
SpoolAutomaticActionMode::NoAction
|
||||
) {
|
||||
self.calculate_spool_auto_progress_(now);
|
||||
return;
|
||||
}
|
||||
|
||||
match self.mode {
|
||||
Winder2Mode::Pull => self.calculate_spool_auto_progress_(now),
|
||||
Winder2Mode::Wind => self.calculate_spool_auto_progress_(now),
|
||||
_ => {
|
||||
self.spool_automatic_action.progress_last_check = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.spool_automatic_action.progress >= self.spool_automatic_action.target_length {
|
||||
match self.spool_automatic_action.mode {
|
||||
SpoolAutomaticActionMode::NoAction => (),
|
||||
SpoolAutomaticActionMode::Pull => {
|
||||
self.stop_or_pull_spool_reset(now);
|
||||
self.set_mode(&Winder2Mode::Pull);
|
||||
}
|
||||
SpoolAutomaticActionMode::Hold => {
|
||||
self.stop_or_pull_spool_reset(now);
|
||||
self.set_mode(&Winder2Mode::Hold);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Implement Mode
|
||||
pub 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();
|
||||
}
|
||||
|
||||
/// Implement Traverse
|
||||
pub fn set_laser(&mut self, value: bool) {
|
||||
self.laser.set(value);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
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 motor_speed = self
|
||||
.puller_speed_controller
|
||||
.angular_velocity_to_speed(angular_velocity);
|
||||
|
||||
// Divide by gear ratio to get actual puller/material speed
|
||||
let puller_speed = motor_speed / self.puller_speed_controller.get_gear_ratio().multiplier();
|
||||
|
||||
// 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>().abs(),
|
||||
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,
|
||||
gear_ratio: self.puller_speed_controller.gear_ratio,
|
||||
},
|
||||
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));
|
||||
}
|
||||
|
||||
/// 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::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::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::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();
|
||||
}
|
||||
|
||||
/// Implement Tension Arm
|
||||
pub fn tension_arm_zero(&mut self) {
|
||||
self.tension_arm.zero();
|
||||
self.emit_live_values(); // For angle update
|
||||
// For state update
|
||||
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 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();
|
||||
}
|
||||
|
||||
/// Set gear ratio for winding speed
|
||||
pub fn puller_set_gear_ratio(&mut self, gear_ratio: super::puller_speed_controller::GearRatio) {
|
||||
self.puller_speed_controller.set_gear_ratio(gear_ratio);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ impl FilamentTensionCalculator {
|
||||
pub fn calc_filament_length(&self, tension_arm_angle: Angle) -> Length {
|
||||
let tension_arm_angle_rad = self
|
||||
.angle_converter
|
||||
.decode_f64(tension_arm_angle)
|
||||
.decode(tension_arm_angle)
|
||||
.get::<radian>();
|
||||
|
||||
// Calculate tension arm tip position (flipped Y-axis: 0° = down, 90° = left)
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -9,52 +10,38 @@ pub mod puller_speed_controller;
|
||||
pub mod spool_speed_controller;
|
||||
pub mod tension_arm;
|
||||
pub mod traverse_controller;
|
||||
use api::{
|
||||
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode, SpoolAutomaticActionState,
|
||||
SpoolSpeedControllerState, StateEvent, TensionArmState, TraverseState, Winder2Events,
|
||||
Winder2Namespace,
|
||||
};
|
||||
use control_core::socketio::event::BuildEvent;
|
||||
|
||||
use std::{fmt::Debug, sync::Weak, time::Instant};
|
||||
|
||||
use api::{SpoolAutomaticActionMode, Winder2Namespace};
|
||||
use control_core::{
|
||||
converters::angular_step_converter::AngularStepConverter,
|
||||
machines::{
|
||||
ConnectedMachine, ConnectedMachineData, downcast_machine,
|
||||
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 futures::executor::block_on;
|
||||
use puller_speed_controller::{PullerRegulationMode, PullerSpeedController};
|
||||
use smol::lock::{Mutex, RwLock};
|
||||
use puller_speed_controller::PullerSpeedController;
|
||||
use smol::lock::RwLock;
|
||||
use spool_speed_controller::SpoolSpeedController;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
sync::{Arc, Weak},
|
||||
time::Instant,
|
||||
};
|
||||
use tension_arm::TensionArm;
|
||||
use traverse_controller::TraverseController;
|
||||
use uom::{
|
||||
ConstZero,
|
||||
si::{
|
||||
angle::degree,
|
||||
angular_velocity::revolution_per_minute,
|
||||
f64::{Length, Velocity},
|
||||
f64::Length,
|
||||
length::{meter, millimeter},
|
||||
velocity::meter_per_second,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::machines::{
|
||||
MACHINE_WINDER_V1, VENDOR_QITECH, buffer1::BufferV1, winder2::api::ConnectedMachineState,
|
||||
};
|
||||
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH, buffer1::BufferV1};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SpoolAutomaticAction {
|
||||
@@ -86,7 +73,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,
|
||||
@@ -109,19 +96,17 @@ 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
|
||||
impl Winder2 {
|
||||
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
|
||||
@@ -130,210 +115,6 @@ impl Winder2 {
|
||||
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: 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));
|
||||
}
|
||||
|
||||
pub fn sync_traverse_speed(&mut self) {
|
||||
self.traverse_controller.update_speed(
|
||||
&mut self.traverse,
|
||||
@@ -385,26 +166,6 @@ impl Winder2 {
|
||||
&& !self.traverse_controller.is_traversing()
|
||||
&& self.mode != Winder2Mode::Wind
|
||||
}
|
||||
}
|
||||
|
||||
impl Winder2 {}
|
||||
|
||||
/// Implement Mode
|
||||
impl Winder2 {
|
||||
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
|
||||
///
|
||||
@@ -458,75 +219,6 @@ impl Winder2 {
|
||||
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::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::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::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.
|
||||
@@ -577,75 +269,6 @@ impl Winder2 {
|
||||
// Update the internal state
|
||||
self.puller_mode = mode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement Tension Arm
|
||||
impl Winder2 {
|
||||
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
|
||||
impl Winder2 {
|
||||
/// called by `act`
|
||||
pub fn sync_spool_speed(&mut self, t: Instant) {
|
||||
let angular_velocity = self.spool_speed_controller.update_speed(
|
||||
t,
|
||||
&self.tension_arm,
|
||||
&self.puller_speed_controller,
|
||||
);
|
||||
let steps_per_second = self
|
||||
.spool_step_converter
|
||||
.angular_velocity_to_steps(angular_velocity);
|
||||
let _ = self.spool.set_speed(steps_per_second);
|
||||
}
|
||||
|
||||
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
|
||||
self.spool_automatic_action.target_length = Length::new::<meter>(meters);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
|
||||
self.spool_automatic_action.mode = mode;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn stop_or_pull_spool(&mut self, now: Instant) {
|
||||
if matches!(
|
||||
self.spool_automatic_action.mode,
|
||||
SpoolAutomaticActionMode::NoAction
|
||||
) {
|
||||
self.calculate_spool_auto_progress_(now);
|
||||
return;
|
||||
}
|
||||
|
||||
match self.mode {
|
||||
Winder2Mode::Pull => self.calculate_spool_auto_progress_(now),
|
||||
Winder2Mode::Wind => self.calculate_spool_auto_progress_(now),
|
||||
_ => {
|
||||
self.spool_automatic_action.progress_last_check = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.spool_automatic_action.progress >= self.spool_automatic_action.target_length {
|
||||
match self.spool_automatic_action.mode {
|
||||
SpoolAutomaticActionMode::NoAction => (),
|
||||
SpoolAutomaticActionMode::Pull => {
|
||||
self.stop_or_pull_spool_reset(now);
|
||||
self.set_mode(&Winder2Mode::Pull);
|
||||
}
|
||||
SpoolAutomaticActionMode::Hold => {
|
||||
self.stop_or_pull_spool_reset(now);
|
||||
self.set_mode(&Winder2Mode::Hold);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn stop_or_pull_spool_reset(&mut self, now: Instant) {
|
||||
self.spool_automatic_action.progress = Length::ZERO;
|
||||
@@ -667,13 +290,11 @@ impl Winder2 {
|
||||
);
|
||||
|
||||
// Update total meters pulled
|
||||
self.spool_automatic_action.progress += meters_pulled_this_interval;
|
||||
self.spool_automatic_action.progress += meters_pulled_this_interval.abs();
|
||||
self.spool_automatic_action.progress_last_check = now;
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement Puller
|
||||
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);
|
||||
@@ -683,186 +304,6 @@ impl Winder2 {
|
||||
.angular_velocity_to_steps(angular_velocity);
|
||||
let _ = self.puller.set_speed(steps_per_second);
|
||||
}
|
||||
|
||||
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
|
||||
impl Winder2 {
|
||||
/// 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;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
/// disconnect buffer
|
||||
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();
|
||||
}
|
||||
|
||||
/// initiate connection from buffer to winder
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,6 +14,30 @@ use uom::{
|
||||
si::f64::{Acceleration, AngularVelocity, Jerk, Length, Velocity},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum GearRatio {
|
||||
OneToOne,
|
||||
OneToFive,
|
||||
OneToTen,
|
||||
}
|
||||
|
||||
impl GearRatio {
|
||||
/// Get the speed multiplier for this gear ratio
|
||||
pub fn multiplier(&self) -> f64 {
|
||||
match self {
|
||||
GearRatio::OneToOne => 1.0,
|
||||
GearRatio::OneToFive => 5.0,
|
||||
GearRatio::OneToTen => 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GearRatio {
|
||||
fn default() -> Self {
|
||||
GearRatio::OneToOne
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PullerSpeedController {
|
||||
enabled: bool,
|
||||
@@ -22,6 +46,8 @@ pub struct PullerSpeedController {
|
||||
pub regulation_mode: PullerRegulationMode,
|
||||
/// Forward rotation direction. If false, applies negative sign to speed
|
||||
pub forward: bool,
|
||||
/// Gear ratio for winding speed (1:5 or 1:10)
|
||||
pub gear_ratio: GearRatio,
|
||||
/// Linear acceleration controller to dampen speed change
|
||||
acceleration_controller: LinearJerkSpeedController,
|
||||
/// Converter for linear to angular transformations
|
||||
@@ -45,6 +71,7 @@ impl PullerSpeedController {
|
||||
target_diameter,
|
||||
regulation_mode: PullerRegulationMode::Speed,
|
||||
forward: true,
|
||||
gear_ratio: GearRatio::default(),
|
||||
acceleration_controller: LinearJerkSpeedController::new_simple(
|
||||
Some(speed),
|
||||
acceleration,
|
||||
@@ -75,8 +102,16 @@ impl PullerSpeedController {
|
||||
self.forward = forward;
|
||||
}
|
||||
|
||||
pub const fn set_gear_ratio(&mut self, gear_ratio: GearRatio) {
|
||||
self.gear_ratio = gear_ratio;
|
||||
}
|
||||
|
||||
pub const fn get_gear_ratio(&self) -> GearRatio {
|
||||
self.gear_ratio
|
||||
}
|
||||
|
||||
fn update_speed(&mut self, t: Instant) -> Velocity {
|
||||
let speed = match self.enabled {
|
||||
let base_speed = match self.enabled {
|
||||
true => match self.regulation_mode {
|
||||
PullerRegulationMode::Speed => self.target_speed,
|
||||
PullerRegulationMode::Diameter => unimplemented!(),
|
||||
@@ -84,6 +119,9 @@ impl PullerSpeedController {
|
||||
false => Velocity::ZERO,
|
||||
};
|
||||
|
||||
// Apply gear ratio multiplier
|
||||
let speed = base_speed * self.gear_ratio.multiplier();
|
||||
|
||||
let speed = if self.forward { speed } else { -speed };
|
||||
|
||||
let speed = self.acceleration_controller.update(speed, t);
|
||||
|
||||
+18
-40
@@ -1,29 +1,17 @@
|
||||
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
|
||||
use tikv_jemallocator::Jemalloc;
|
||||
|
||||
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: Jemalloc = Jemalloc;
|
||||
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
#[global_allocator]
|
||||
static ALLOC: dhat::Alloc = dhat::Alloc;
|
||||
|
||||
use app_state::AppState;
|
||||
use r#loop::init_loop;
|
||||
#[cfg(feature = "mock-machine")]
|
||||
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"))]
|
||||
use serial::init::init_serial;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
|
||||
use jemalloc_stats::init_jemalloc_stats;
|
||||
|
||||
use crate::panic::init_panic;
|
||||
use crate::panic::init_panic_handling;
|
||||
use crate::socketio::queue::init_socketio_queue;
|
||||
|
||||
pub mod app_state;
|
||||
@@ -39,53 +27,42 @@ pub mod rest;
|
||||
pub mod serial;
|
||||
pub mod socketio;
|
||||
|
||||
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
|
||||
pub mod jemalloc_stats;
|
||||
use crate::ethercat::init::init_ethercat;
|
||||
|
||||
fn main() {
|
||||
// Initialize panic handling
|
||||
let thread_panic_tx = init_panic();
|
||||
logging::init_tracing();
|
||||
tracing::info!("Tracing initialized successfully");
|
||||
|
||||
// lock memory (not working thus commented out)
|
||||
// if let Err(e) = lock_memory() {
|
||||
// tracing::error!("[{}::main] Failed to lock memory: {:?}", module_path!(), e);
|
||||
// } else {
|
||||
// tracing::info!("[{}::main] Memory locked successfully", module_path!());
|
||||
// }
|
||||
|
||||
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
|
||||
init_jemalloc_stats();
|
||||
init_panic_handling();
|
||||
|
||||
#[cfg(feature = "memory-locking")]
|
||||
if let Err(e) = control_core::realtime::lock_memory() {
|
||||
tracing::error!("[{}::main] Failed to lock memory: {:?}", module_path!(), e);
|
||||
} else {
|
||||
tracing::info!("[{}::main] Memory locked successfully", module_path!());
|
||||
}
|
||||
let app_state = Arc::new(AppState::new());
|
||||
|
||||
// Spawn init thread
|
||||
let init_thread = std::thread::Builder::new()
|
||||
.name("init".to_string())
|
||||
.spawn({
|
||||
let thread_panic_tx = thread_panic_tx;
|
||||
let app_state = app_state;
|
||||
move || {
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
init_dhat_heap_profiling();
|
||||
|
||||
init_socketio_queue(thread_panic_tx.clone(), app_state.clone());
|
||||
init_api(thread_panic_tx.clone(), app_state.clone())
|
||||
.expect("Failed to initialize API");
|
||||
init_loop(thread_panic_tx.clone(), app_state.clone())
|
||||
.expect("Failed to initialize loop");
|
||||
init_socketio_queue(app_state.clone());
|
||||
init_api(app_state.clone()).expect("Failed to initialize API");
|
||||
init_loop(app_state.clone()).expect("Failed to initialize loop");
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
init_serial(thread_panic_tx.clone(), app_state.clone())
|
||||
.expect("Failed to initialize Serial");
|
||||
init_serial(app_state.clone()).expect("Failed to initialize Serial");
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
init_ethercat(thread_panic_tx.clone(), app_state)
|
||||
.expect("Failed to initialize EtherCAT");
|
||||
init_ethercat(app_state).expect("Failed to initialize EtherCAT");
|
||||
}
|
||||
})
|
||||
.expect("Failed to spawn init thread");
|
||||
@@ -96,7 +73,8 @@ fn main() {
|
||||
// Keep the main thread alive indefinitely
|
||||
// The program should only exit via panic handling or external signals
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_secs(u64::MAX));
|
||||
// better way to "sleep", uses basically
|
||||
std::thread::park();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
+32
-162
@@ -1,173 +1,43 @@
|
||||
use smol::channel::{Sender, unbounded};
|
||||
use std::process::exit;
|
||||
use tracing::error;
|
||||
use std::{backtrace::Backtrace, panic::PanicHookInfo, thread};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PanicDetails {
|
||||
pub thread_name: Option<String>,
|
||||
pub message: Option<String>,
|
||||
pub location: Option<PanicDetailsLocation>,
|
||||
pub backtrace: String,
|
||||
}
|
||||
fn panic_hook(panic_info: &PanicHookInfo) {
|
||||
let backtrace = Backtrace::capture().to_string();
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PanicDetailsLocation {
|
||||
pub file: String,
|
||||
pub line: u32,
|
||||
pub column: u32,
|
||||
}
|
||||
let thread = thread::current();
|
||||
let thread = thread.name().unwrap_or("<unknown>");
|
||||
let locataion = panic_info
|
||||
.location()
|
||||
.map_or_else(|| "<unknown>".to_string(), ToString::to_string);
|
||||
|
||||
impl std::fmt::Display for PanicDetailsLocation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{}:{}", self.file, self.line, self.column)
|
||||
}
|
||||
}
|
||||
let message = panic_info
|
||||
.payload()
|
||||
.downcast_ref::<String>()
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
panic_info
|
||||
.payload()
|
||||
.downcast_ref::<&str>()
|
||||
.map(ToString::to_string)
|
||||
})
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
|
||||
impl From<&std::panic::Location<'_>> for PanicDetailsLocation {
|
||||
fn from(location: &std::panic::Location<'_>) -> Self {
|
||||
Self {
|
||||
file: location.file().to_string(),
|
||||
line: location.line(),
|
||||
column: location.column(),
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::error!("thread '{}' panicked at {}:", thread, locataion);
|
||||
eprintln!("{}\n", message);
|
||||
eprintln!("Backtrace:\n{}", backtrace);
|
||||
|
||||
impl std::fmt::Display for PanicDetails {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let fmt_thread_name = match &self.thread_name {
|
||||
Some(name) => format!("'{}'", name),
|
||||
None => "<unknown>".to_string(),
|
||||
};
|
||||
|
||||
let fmt_message = match &self.message {
|
||||
Some(message) => format!("'{}'", message),
|
||||
None => "<no message>".to_string(),
|
||||
};
|
||||
|
||||
let fmt_location = match &self.location {
|
||||
Some(location) => location.to_string(),
|
||||
None => "<no location>".to_string(),
|
||||
};
|
||||
|
||||
// Apply syntax highlighting to backtrace
|
||||
let highlighted_backtrace = self.highlight_backtrace();
|
||||
|
||||
write!(
|
||||
f,
|
||||
"thread {} panicked at {}, {}\n{}",
|
||||
fmt_thread_name, fmt_message, fmt_location, highlighted_backtrace
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PanicDetails {
|
||||
/// Highlights backtrace lines that contain server or control-core crate references
|
||||
/// and the line immediately following them
|
||||
fn highlight_backtrace(&self) -> String {
|
||||
use regex::Regex;
|
||||
|
||||
// Regex to match lines starting with digits followed by colon
|
||||
let line_regex = Regex::new(r"^\s*(\d+):\s*(.*)$").unwrap();
|
||||
|
||||
let lines: Vec<&str> = self.backtrace.lines().collect();
|
||||
let mut result = Vec::new();
|
||||
let mut highlight_next = false;
|
||||
|
||||
for line in lines.iter() {
|
||||
if let Some(captures) = line_regex.captures(line) {
|
||||
let line_num = &captures[1];
|
||||
let content = &captures[2];
|
||||
|
||||
// Check if the line contains server or control-core
|
||||
if content.contains("server") || content.contains("control-core") {
|
||||
// Red highlighting for relevant lines
|
||||
result.push(format!("\x1b[91m{}: {}\x1b[0m", line_num, content));
|
||||
highlight_next = true;
|
||||
} else {
|
||||
// Dim gray for other numbered lines
|
||||
result.push(format!("\x1b[90m{}: {}\x1b[0m", line_num, content));
|
||||
}
|
||||
} else {
|
||||
// Non-numbered lines (headers, location info, etc.)
|
||||
if highlight_next {
|
||||
// Highlight the line after a matched regex line in yellow
|
||||
result.push(format!("\x1b[93m{}\x1b[0m", line));
|
||||
highlight_next = false;
|
||||
} else {
|
||||
// Default color for other non-numbered lines
|
||||
result.push(line.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-level panic handler for general thread crashes
|
||||
/// Sends detailed panic information including backtrace
|
||||
pub fn send_panic(thread_panic_tx: Sender<PanicDetails>) {
|
||||
// Ensure backtrace is enabled for panics
|
||||
if std::env::var("RUST_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
}
|
||||
}
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Capture backtrace at panic site
|
||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||
|
||||
let thread_name = std::thread::current().name().map(|name| name.to_string());
|
||||
|
||||
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
|
||||
Some(s.to_string())
|
||||
} else {
|
||||
panic_info.payload().downcast_ref::<String>().cloned()
|
||||
};
|
||||
|
||||
let panic_details = PanicDetails {
|
||||
thread_name,
|
||||
message,
|
||||
location: panic_info.location().map(|loc| loc.into()),
|
||||
backtrace: backtrace.to_string(),
|
||||
};
|
||||
|
||||
// Send detailed panic info through channel
|
||||
let _ = smol::block_on(thread_panic_tx.send(panic_details));
|
||||
}));
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
/// Initialize panic handling system
|
||||
/// Sets up panic handler and starts dedicated panic monitoring thread
|
||||
pub fn init_panic() -> Sender<PanicDetails> {
|
||||
let (thread_panic_tx, thread_panic_rx) = unbounded::<PanicDetails>();
|
||||
send_panic(thread_panic_tx.clone());
|
||||
pub fn init_panic_handling() {
|
||||
// Ensure backtrace is enabled for panics
|
||||
if std::env::var("RUST_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
}
|
||||
|
||||
// Start panic monitoring thread
|
||||
let thread_panic_rx_clone = thread_panic_rx;
|
||||
std::thread::Builder::new()
|
||||
.name("panic".to_string())
|
||||
.spawn(move || {
|
||||
// Create an executor for this thread
|
||||
let rt = smol::Executor::new();
|
||||
smol::block_on(rt.run(async {
|
||||
loop {
|
||||
match thread_panic_rx_clone.recv().await {
|
||||
Ok(panic_details) => {
|
||||
error!("{}", panic_details);
|
||||
exit(1);
|
||||
}
|
||||
Err(_) => {
|
||||
// Channel closed, exit gracefully
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
.expect("Failed to spawn panic monitoring thread");
|
||||
|
||||
thread_panic_tx
|
||||
std::panic::set_hook(Box::new(panic_hook));
|
||||
}
|
||||
@@ -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!()
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
use super::handlers::machine_mutation::post_machine_mutate;
|
||||
use super::handlers::write_machine_device_identification::post_write_machine_device_identification;
|
||||
use crate::app_state::AppState;
|
||||
use crate::panic::{PanicDetails, send_panic};
|
||||
use crate::socketio::init::init_socketio;
|
||||
use anyhow::anyhow;
|
||||
use axum::routing::post;
|
||||
use smol::channel::Sender;
|
||||
use std::sync::Arc;
|
||||
use std::thread::JoinHandle;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_http::trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer};
|
||||
use tracing::Level;
|
||||
|
||||
pub fn init_api(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<JoinHandle<()>, anyhow::Error> {
|
||||
pub fn init_api(app_state: Arc<AppState>) -> Result<JoinHandle<()>, anyhow::Error> {
|
||||
std::thread::Builder::new()
|
||||
.name("api".to_string())
|
||||
.spawn(|| {
|
||||
send_panic(thread_panic_tx);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
use crate::panic::{PanicDetails, send_panic};
|
||||
use crate::socketio::main_namespace::MainNamespaceEvents;
|
||||
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
|
||||
use crate::{app_state::AppState, machines::registry::MACHINE_REGISTRY};
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use smol::channel::Sender;
|
||||
use std::{sync::Arc, thread, time::Duration};
|
||||
|
||||
pub fn init_serial(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let thread_panic_tx_clone = thread_panic_tx;
|
||||
|
||||
pub fn init_serial(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
let app_state_clone = app_state.clone();
|
||||
|
||||
thread::Builder::new()
|
||||
@@ -19,8 +12,6 @@ pub fn init_serial(
|
||||
.spawn(move || {
|
||||
let app_state_clone = app_state_clone;
|
||||
smol::block_on(async move {
|
||||
send_panic(thread_panic_tx_clone);
|
||||
|
||||
let rt = smol::LocalExecutor::new();
|
||||
rt.run(async {
|
||||
loop {
|
||||
@@ -68,8 +59,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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
panic::{PanicDetails, send_panic},
|
||||
};
|
||||
use smol::channel::Sender;
|
||||
use crate::app_state::AppState;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use tracing::{debug, error, info, instrument, trace};
|
||||
|
||||
@@ -62,11 +58,10 @@ async fn send_event_with_retry(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_socketio_queue(thread_panic_tx: Sender<PanicDetails>, app_state: Arc<AppState>) {
|
||||
pub fn init_socketio_queue(app_state: Arc<AppState>) {
|
||||
std::thread::Builder::new()
|
||||
.name("socketio-queue".to_string())
|
||||
.spawn(move || {
|
||||
send_panic(thread_panic_tx);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
|
||||
Reference in new issue
Block a user