Compare commits

..
Author SHA1 Message Date
Snacj 252fb31d65 Fixed merge issues 2025-10-31 13:24:56 +01:00
snacj bf12a25af4 Fixed merge issues 2025-10-31 12:32:15 +01:00
snacj c66f046a9b Fixed Formatting 2025-10-31 12:31:40 +01:00
snacj a7b1158f8d Fixed merge issues. 2025-10-31 12:30:53 +01:00
snacj be87b377ba Changed frontend Formatting 2025-10-31 12:29:23 +01:00
snacj 111664185e Fixed Formatting 2025-10-31 12:29:23 +01:00
snacj 29ccaea7f2 fixed formatting 2025-10-31 12:29:23 +01:00
snacj 8402199601 Fixed Merge issues 2025-10-31 12:29:23 +01:00
snacj b7399c8d82 Fixed Formula 2025-10-31 12:29:23 +01:00
snacj db71805912 Changed P Controller to Pi controller with dead time 2025-10-31 12:29:23 +01:00
snacj 3fd1b09589 Added Pi and Ei to the update logic. 2025-10-31 12:29:23 +01:00
snacj c806f59df5 Changed P Controller to be Pi Controller. 2025-10-31 12:29:23 +01:00
snacj a5c0d75f33 Fixed Clippy and Formatting 2025-10-31 12:29:23 +01:00
snacj 9629652d4f fixed merge issue 2025-10-31 12:29:23 +01:00
snacj 5ee0b7e61b Fixed Clippy 2025-10-31 12:29:23 +01:00
snacj bb975081e8 Testing fix for frontend freeze 2025-10-31 12:29:23 +01:00
snacj 6629055f9f Fixed clippy 2025-10-31 12:29:23 +01:00
snacj 3b10790146 Removed unwrap and fixed clippy 2025-10-31 12:29:23 +01:00
snacj cb6a954e3b Removed inline import 2025-10-31 12:29:23 +01:00
snacj bbf58e6b18 Fixed Error Pid to P 2025-10-31 12:29:23 +01:00
snacj 9af2364528 Fixed Merge Issue 2025-10-31 12:29:23 +01:00
snacj 7d774b0025 Fixed Merge issue 2025-10-31 12:29:23 +01:00
snacj 94655093cc Added Algorithm Only speed calculation for Diameter Regulation 2025-10-31 12:29:23 +01:00
snacj 5b57273153 Removed logs for testing 2025-10-31 12:29:23 +01:00
snacj 02951794f1 Fixed issues with kp scaling factor 2025-10-31 12:29:23 +01:00
snacj d1f96cc816 changed PID controller to P controller 2025-10-31 12:29:23 +01:00
snacj 673b85420c Implemented deadtime_p_controller to regulate puller speed by filament
diameter.
2025-10-31 12:29:23 +01:00
snacj 493cb39396 Moved PID settings impl from Laser to Winder 2025-10-31 12:29:22 +01:00
snacj 34d6e85ec3 fixed PID impl 2025-10-31 12:27:09 +01:00
snacj dd79586b13 Adjusted default PID setting and replaced info! with trace! 2025-10-31 12:27:09 +01:00
snacj fab801e62f Fixed Formatting 2025-10-31 12:27:09 +01:00
snacj 37a1641450 Fixed formatting. 2025-10-31 12:27:09 +01:00
snacj 6e4baaa356 Fixed merge issue 2025-10-31 12:27:09 +01:00
snacj 3292c62212 Added Debug messages 2025-10-31 12:27:09 +01:00
snacj 3e1d83a0dd changed maximum velocity by diameter for testing. 2025-10-31 12:27:09 +01:00
snacj 47dcd34364 Implemented setting of measured diameter 2025-10-31 12:27:09 +01:00
snacj 2c134c4479 Added working PID settings. 2025-10-31 12:27:09 +01:00
snacj 8613e0c5a8 Fixed Laser Winder connection 2025-10-31 12:27:09 +01:00
snacj 02daf33e55 Testing laser winder connection. 2025-10-31 12:27:09 +01:00
snacj 7b7eafee3b Added debug messages to test the connection 2025-10-31 12:27:07 +01:00
snacj 400fc016ae fixed naming 2025-10-31 11:46:20 +01:00
snacj 8b66ee415a Maybe fix laser Machine is Serial 2025-10-31 11:46:20 +01:00
snacj 89fef65347 Fixed Comments 2025-10-31 11:46:20 +01:00
snacj 5fbf5b7b97 Fix PID settings through laser. 2025-10-31 11:46:12 +01:00
snacj 8278bf579c Implemented Diameter Regulation Mode logic 2025-10-31 11:29:59 +01:00
snacj 6f7611ada5 Added Connect Machine selector to laser frontend 2025-10-31 11:29:59 +01:00
snacj a3ff479941 Fixed Machine Connection 2025-10-31 11:29:59 +01:00
snacj d14e1183d5 Fixed Laser Settings Page 2025-10-31 11:29:59 +01:00
snacj c7495a7d92 Added Pid Settings Logic 2025-10-31 11:29:59 +01:00
snacj 5428e950b0 Added Winder Laser Connection Frontend & Backend 2025-10-31 11:29:59 +01:00
Robin Krämer 13117a5d94 Merge pull request #845 from qitechgmbh/844-add-winder-mock-machine
added Winder mock
2025-10-28 17:58:25 +01:00
kraemr 6b74c7ec7a removed default mock-machine 2025-10-28 17:53:04 +01:00
kraemr e8b561f722 added Winder mock 2025-10-28 17:53:04 +01:00
Robin Krämer 75ee5af180 Merge pull request #852 from qitechgmbh/851-improve-caching
851 improve caching
2025-10-28 17:46:21 +01:00
kraemr 29f924f6cc removed socketioManager 2025-10-27 16:42:03 +01:00
kraemr 6e058c8e95 Keep namespaces alive 2025-10-27 15:11:30 +01:00
Robin Krämer 6822da80cf Merge pull request #838 from qitechgmbh/dependabot/npm_and_yarn/electron/multi-a50d7f32cf
Bump playwright and @playwright/test in /electron
2025-10-27 11:24:43 +01:00
dependabot[bot] 37e3302f61 Bump playwright and @playwright/test in /electron
Bumps [playwright](https://github.com/microsoft/playwright) to 1.56.1 and updates ancestor dependency [@playwright/test](https://github.com/microsoft/playwright). These dependencies need to be updated together.


Updates `playwright` from 1.54.2 to 1.56.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.2...v1.56.1)

Updates `@playwright/test` from 1.54.2 to 1.56.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.54.2...v1.56.1)

---
updated-dependencies:
- dependency-name: playwright
  dependency-version: 1.56.1
  dependency-type: indirect
- dependency-name: "@playwright/test"
  dependency-version: 1.56.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 11:21:25 +01:00
Robin Krämer 9b369d71ee Merge pull request #839 from qitechgmbh/dependabot/npm_and_yarn/electron/vite-6.4.1
Bump vite from 6.3.6 to 6.4.1 in /electron
2025-10-27 11:21:10 +01:00
dependabot[bot] cf58f0db6f Bump vite from 6.3.6 to 6.4.1 in /electron
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 11:17:46 +01:00
Robin Krämer 299f23c796 Merge pull request #847 from qitechgmbh/846-refactor-initialization-to-be-based-around-futures
started refactoring initialization to use futures instead
2025-10-27 11:14:18 +01:00
kraemr b58e4333a0 removed traces 2025-10-27 11:08:21 +01:00
kraemr f76cb9c381 Fixed Loop async code 2025-10-27 11:01:04 +01:00
kraemr e93e10d14d fixed all warnings and restored original ethercat discovery behaviour 2025-10-23 20:13:48 +02:00
kraemr 025464ebff all init functions look sane now
fixed compilation errors

fixed compile errors

refactored serial device detection successfully

added serial_detection functions

fixed  Killed error, and improved serial_detection

fixed all remaining issues

fix

fix
2025-10-23 20:13:33 +02:00
kraemr ae8f77f070 started refactoring initialization to use futures instead 2025-10-22 17:56:52 +02:00
63 changed files with 2065 additions and 1049 deletions

No files matched your search

Generated
+7 -105
View File
@@ -367,26 +367,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.9.0",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.105",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -475,15 +455,6 @@ dependencies = [
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -535,17 +506,6 @@ dependencies = [
"half",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.40"
@@ -829,7 +789,7 @@ dependencies = [
"lazy_static",
"mintex",
"parking_lot",
"rustc-hash 1.1.0",
"rustc-hash",
"serde",
"serde_json",
"thousands",
@@ -1076,7 +1036,8 @@ dependencies = [
[[package]]
name = "ethercrab"
version = "0.6.0"
source = "git+https://github.com/ethercrab-rs/ethercrab?branch=main#5bd732c491172835b8d122545bee0c4f4dd79eb8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb13becfa15713adb09b27fdb74468b827cdc78aa99aea638a89c1b0db3ce94"
dependencies = [
"async-io",
"atomic-waker",
@@ -1100,13 +1061,13 @@ dependencies = [
"smlang",
"spin",
"timerfd",
"xsk-rs",
]
[[package]]
name = "ethercrab-wire"
version = "0.2.0"
source = "git+https://github.com/ethercrab-rs/ethercrab?branch=main#5bd732c491172835b8d122545bee0c4f4dd79eb8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d15828cb020fb44c92ca67e1ee3c122c9621c2e00d383d066e488f0619023ba2"
dependencies = [
"ethercrab-wire-derive",
"heapless",
@@ -1115,7 +1076,8 @@ dependencies = [
[[package]]
name = "ethercrab-wire-derive"
version = "0.2.0"
source = "git+https://github.com/ethercrab-rs/ethercrab?branch=main#5bd732c491172835b8d122545bee0c4f4dd79eb8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e790b8011211ed70babfcacbd4277b91ee21402ad5ba6a2c293db02e2f12919"
dependencies = [
"criterion",
"proc-macro2",
@@ -1310,12 +1272,6 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "h2"
version = "0.4.10"
@@ -1806,15 +1762,6 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "libxdp-sys"
version = "0.2.2+1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e07efa8681fd85d4a284b5c62025ea4c34d19ae8299e5c39bf9e8e0b72d9c8e3"
dependencies = [
"bindgen",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -1913,12 +1860,6 @@ dependencies = [
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.8"
@@ -1992,16 +1933,6 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
@@ -2340,16 +2271,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.105",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -2566,12 +2487,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "0.38.44"
@@ -3969,19 +3884,6 @@ dependencies = [
"tap",
]
[[package]]
name = "xsk-rs"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "964acfdda803c92ec548602e3e7e8e946d59513abf920e12a820f6a621193c06"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"libc",
"libxdp-sys",
"log",
]
[[package]]
name = "yoke"
version = "0.8.0"
+1 -1
View File
@@ -15,7 +15,7 @@ if [[ "$1" == "release" ]]; then
cargo build --release
else
echo "Building Debug Code"
cargo build $FEATURE
cargo build $FEATURE --features development-build
fi
# Only setcap if not building with mock-machine
+1 -1
View File
@@ -12,7 +12,7 @@ anyhow = "1.0.100"
axum = "0.8.6"
ethercat_hal = { path = "../ethercat-hal" }
ethercrab = { git = "https://github.com/ethercrab-rs/ethercrab", branch = "main", features = ["xdp"] }
ethercrab = "0.6"
interfaces = "0.0.9"
serde = "1.0.219"
serde_json = "1.0.143"
@@ -0,0 +1,124 @@
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct TimeAgnosticDeadTimePiController {
// Params
/// Proportional gain
kp: f64,
/// Integral gain
ki: f64,
// State
/// Proportional error
ep: f64,
/// Integral error
ei: f64,
/// Dead Time
dead: Duration,
last: Option<Instant>,
}
impl TimeAgnosticDeadTimePiController {
pub fn new(kp: f64, ki: f64, dead: Duration) -> Self {
Self {
kp,
ki,
dead,
ep: 0.0,
ei: 0.0,
last: None,
}
}
pub fn configure(&mut self, kp: f64, ki: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
}
pub fn get_kp(&self) -> f64 {
self.kp
}
pub fn get_ki(&self) -> f64 {
self.ki
}
pub fn get_dead(&self) -> Duration {
self.dead
}
pub fn set_kp(&mut self, kp: f64) {
self.kp = kp;
}
pub fn set_ki(&mut self, ki: f64) {
self.ki = ki;
}
pub fn set_dead(&mut self, dead: Duration) {
self.dead = dead;
}
pub fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
match (min, max) {
(Some(min), Some(max)) => value.clamp(min, max),
(Some(min), None) => value.max(min),
(None, Some(max)) => value.min(max),
(None, None) => value,
}
}
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
let kp = self.kp / 10000.0;
let ki = self.ki / 10000.0;
let signal = match self.last {
// First update
None => {
let ep = error;
// Initialize integral error
self.ei = 0.0;
// Proportional + Integral
let signal = kp * ep + ki * self.ei;
// Save state
self.ep = ep;
self.last = Some(t);
-signal
}
// Subsequent updates
Some(last) => {
// Calculate elapsed time
let dt = t.duration_since(last).as_secs_f64();
// Dead-time scaling factor
let dead_secs = self.dead.as_secs_f64();
let scale = (dt / dead_secs).min(1.0);
// Smoothed proportional error
let ep = self.ep + (error - self.ep) * scale;
// Integrate error over time
self.ei += error * dt;
// Proportional + Integral
let signal = kp * ep + ki * self.ei;
// Save state
self.ep = ep;
self.last = Some(t);
-signal
}
};
-signal
}
pub fn reset(&mut self) {
self.ep = 0.0;
//self.last = None;
}
}
+1
View File
@@ -1,4 +1,5 @@
pub mod clamping_timeagnostic_pid;
pub mod deadtime_pi_controller;
pub mod first_degree_motion;
pub mod pid;
pub mod second_degree_motion;
+2 -2
View File
@@ -92,7 +92,7 @@ pub struct MachineCrossConnection<
> {
machine_manager: Weak<RwLock<MachineManager>>,
machine_identification_unique: MachineIdentificationUnique,
connected_machine: Weak<Mutex<MachineSlot<T>>>,
pub 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
@@ -102,7 +102,7 @@ pub struct MachineCrossConnection<
// to an unknown type. See also https://github.com/qitechgmbh/control/pull/625#discussion_r2379566315
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct MachineCrossConnectionState {
/// Connected Machine
pub machine_identification_unique: Option<MachineIdentificationUnique>,
+3 -7
View File
@@ -1,13 +1,10 @@
use std::{any::Any, sync::Arc};
use smol::{channel::Sender, lock::RwLock};
use smol::lock::RwLock;
use std::fmt::Debug;
use crate::{
machines::identification::DeviceIdentification, serial::serial_detection::SerialDeviceRemoval,
};
use crate::machines::identification::DeviceIdentification;
pub mod panic;
pub mod registry;
pub mod serial_detection;
@@ -27,10 +24,9 @@ pub trait SerialDeviceThread {
pub struct SerialDeviceNewParams {
pub path: String,
pub device_thread_panic_tx: Sender<SerialDeviceRemoval<String>>,
}
#[derive(PartialEq, Eq, Clone, Debug)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct SerialDeviceIdentification {
pub vendor_id: u16,
pub product_id: u16,
-53
View File
@@ -1,53 +0,0 @@
use smol::channel::Sender;
use crate::serial::serial_detection::SerialDeviceRemoval;
/// Device-level panic handler for individual device crashes
/// Sends device identifier and error information for cleanup
/// Used by serial devices and other individual components that need to be removed on panic
pub fn send_serial_device_panic<T>(
device_identifier: T,
thread_panic_tx: Sender<SerialDeviceRemoval<T>>,
) where
T: Send + Sync + Clone + 'static,
{
// 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| {
let thread_name = std::thread::current()
.name()
.unwrap_or("unnamed")
.to_string();
let location = panic_info.location().map_or("unknown".to_string(), |loc| {
format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
});
let message = panic_info
.payload()
.downcast_ref::<&str>()
.unwrap_or(&"Box<dyn Any>")
.to_string();
let panic_message = format!(
"Device panicked in thread '{}' at {}: {}",
thread_name, location, message
);
// Send device panic info through channel
smol::block_on(async {
let _ = thread_panic_tx
.send(SerialDeviceRemoval::Error(
device_identifier.clone(),
anyhow::anyhow!("{}", panic_message),
))
.await;
});
// Note: We don't call old_hook to avoid duplicate logging
// All logging is handled in the main thread when receiving panic details
}));
}
+48 -172
View File
@@ -7,194 +7,70 @@
*/
use serialport::{SerialPortInfo, SerialPortType, UsbPortInfo};
use smol::{
channel::{Receiver, Sender, unbounded},
lock::RwLock,
};
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
use crate::{
helpers::compare_lists::compare_lists, machines::identification::DeviceIdentification,
};
use super::SerialDeviceIdentification;
/// Handles scanning and caching of connected USB serial devices.
pub struct SerialDetection {}
use super::{
SerialDevice, SerialDeviceIdentification, SerialDeviceNewParams, registry::SerialDeviceRegistry,
};
pub enum SerialDeviceRemoval<T> {
Disconnect(T),
Error(T, anyhow::Error),
}
pub struct SerialDetection<'serialdeviceregistry> {
pub serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry,
pub ports: HashMap<
String,
(
UsbPortInfo,
DeviceIdentification,
Arc<RwLock<dyn SerialDevice>>,
),
>,
pub device_removal_signal_rx: Receiver<SerialDeviceRemoval<String>>,
pub device_removal_signal_tx: Sender<SerialDeviceRemoval<String>>,
}
impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
pub fn new(serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry) -> Self {
let (device_removal_signal_tx, device_removal_signal_rx) = unbounded();
SerialDetection {
serial_device_registry,
ports: HashMap::new(),
device_removal_signal_rx,
device_removal_signal_tx,
impl SerialDetection {
/// Returns all serial ports on the system (minus macOS "cu" noise).
fn get_ports() -> Vec<SerialPortInfo> {
match serialport::available_ports() {
Ok(ports) => ports
.into_iter()
.filter(|port| !port.port_name.contains("cu"))
.collect(),
Err(_) => Vec::new(),
}
}
/*
*@return: Option::None if there are no available ports on device
*@return: Option::Some(Vec) with all available ports
*
*@description: detects all available ports and returns them as list
*/
fn get_ports() -> Vec<SerialPortInfo> {
let all = match serialport::available_ports() {
Ok(ports) => ports,
Err(_) => return Vec::new(),
};
// exclude ports that contai "cu" in their name
all.into_iter()
.filter(|port| !port.port_name.contains("cu"))
.collect::<Vec<_>>()
}
/* @param: port_list -> list of available ports
*
* @return: Vec<(String, UsbPortInfo)> -> list of ports with their names and usb information
*
* @description: This function extracts the USB serial devices from the given list of ports.
*/
fn extract_usb_serial_devices(
&self,
port_list: Vec<SerialPortInfo>,
) -> Vec<(String, UsbPortInfo)> {
let mut usb_ports: Vec<(String, UsbPortInfo)> = Vec::new();
/// Extracts only USB serial devices from the port list.
fn extract_usb_serial_devices(port_list: Vec<SerialPortInfo>) -> HashMap<String, UsbPortInfo> {
let mut map = HashMap::new();
for port in port_list {
if let SerialPortType::UsbPort(usb_info) = &port.port_type {
usb_ports.push((port.port_name.to_string(), usb_info.clone()));
};
if let SerialPortType::UsbPort(usb_info) = port.port_type {
map.insert(port.port_name, usb_info);
}
}
usb_ports
map
}
pub async fn check_ports(&mut self) -> CheckPortsResult {
// get available ports
/// Scans for connected USB serial devices and updates the internal map.
/// Returns a clone of the map for quick access.
pub fn detect_devices() -> HashMap<String, UsbPortInfo> {
let ports = Self::get_ports();
// extract all ports that are usb ports
let usb_ports = self.extract_usb_serial_devices(ports);
let last_usb_ports = self
.ports
.iter()
.map(|last_usb_port| (last_usb_port.0.clone(), last_usb_port.1.0.clone()))
.collect::<Vec<_>>();
let usb_ports_diff = compare_lists(&last_usb_ports, &usb_ports);
let mut result = CheckPortsResult {
added: Vec::new(),
removed: Vec::new(),
};
// reamove ports
for removed in usb_ports_diff.removed {
if let Some((_, device_identification, _)) = self.ports.remove(&removed.0) {
result.removed.push(device_identification);
}
tracing::trace!("Removed port: {}", removed.0);
}
// add new ports
for added in usb_ports_diff.added {
// add the port to the list
let serial_device_identification = SerialDeviceIdentification {
vendor_id: added.1.vid,
product_id: added.1.pid,
};
let device_result = self.serial_device_registry.new_serial_device(
&SerialDeviceNewParams {
path: added.0.clone(),
device_thread_panic_tx: self.device_removal_signal_tx.clone(),
},
&serial_device_identification,
);
// only if created device driver sucessfully
if let Ok((device_identification, device)) = device_result {
// add the device to the ports list
self.ports.insert(
added.0.clone(),
(
added.1.clone(),
device_identification.clone(),
device.clone(),
),
);
// add to result list
result.added.push((device_identification, device.clone()));
tracing::trace!("Added port: {}", added.0);
}
}
result
let usb_devices = Self::extract_usb_serial_devices(ports);
usb_devices
}
pub async fn check_remove_signals(&mut self) -> Vec<DeviceIdentification> {
let mut removed_signals: Vec<DeviceIdentification> = Vec::new();
pub fn device_port_exists(device_path: &str) -> bool {
let ports = Self::get_ports();
return ports.iter().any(|p| p.port_name == device_path);
}
let mut add_removal_signal = |path: &String| {
if let Some(info) = self.ports.get(path) {
removed_signals.push(info.1.clone());
};
self.ports.remove(path);
};
if self.device_removal_signal_rx.is_empty() {
return removed_signals;
}
let removal = self
.device_removal_signal_rx
.recv()
.await
.expect("Failed to receive removal signals");
match removal {
SerialDeviceRemoval::Disconnect(path) => {
add_removal_signal(&path);
tracing::trace!("Removed serial device after disconnect: {:?}", path);
}
SerialDeviceRemoval::Error(path, error) => {
add_removal_signal(&path);
tracing::error!("Removed erroring serial device: {:?}: {}", path, error);
pub fn device_id_exists(sdevid: SerialDeviceIdentification) -> bool {
let ports = Self::get_ports();
for port in ports {
if let SerialPortType::UsbPort(usb_info) = port.port_type {
if usb_info.vid == sdevid.vendor_id && usb_info.pid == sdevid.product_id {
return true;
}
}
}
return false;
}
removed_signals
pub fn get_path_by_id(
sdevid: SerialDeviceIdentification,
map: HashMap<String, UsbPortInfo>,
) -> Option<String> {
for (port, usbport) in map.iter() {
if usbport.vid == sdevid.vendor_id && usbport.pid == sdevid.product_id {
return Some(port.clone());
}
}
return None;
}
}
#[derive(Debug)]
pub struct CheckPortsResult {
pub added: Vec<(DeviceIdentification, Arc<RwLock<dyn SerialDevice>>)>,
pub removed: Vec<DeviceIdentification>,
}
-1
View File
@@ -118,7 +118,6 @@ impl Namespace {
) {
// cache the event
self.cache(event.clone(), buffer_fn);
// emit the event - inlined from emit function
// Send to global queue for each socket in the namespace
for socket in self.sockets.clone() {
+31 -52
View File
@@ -70,7 +70,7 @@
"@electron/fuses": "^2.0.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "^4.0.6",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -97,7 +97,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vitest": "^3.0.6"
}
},
@@ -175,7 +175,6 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -639,7 +638,6 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
},
@@ -662,7 +660,6 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1105,7 +1102,6 @@
"integrity": "sha512-lyb1zK3YHeWUjaz7yiK0GnxSPduwASKMyiDbCtbn3spP6EEt+UWtktggWehG0icFrXAk3GwvcJ4nCrJO0N9IhQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"electron-fuses": "dist/bin.js"
},
@@ -2282,12 +2278,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.54.2"
"playwright": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -4032,7 +4029,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.17.tgz",
"integrity": "sha512-F1tyMEbX7PJclnA25hAFXhLKq4jqeODvvaogsd0/NX02Kmz3QoT9GV+rgpv2TxamU6RwjRgihSH06Dn5hM6SOw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/react-store": "^0.7.0",
@@ -4114,7 +4110,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.17.tgz",
"integrity": "sha512-0AH4s/52X2shYLifr9hR+6R6NpGsBp1M3LyjBTZm6Jlkud+nOhkkRxKxYS7l+JErRlffdxVHxMdoTnKcQ+duiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.130.12",
"@tanstack/store": "^0.7.0",
@@ -4348,7 +4343,8 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -4505,7 +4501,6 @@
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.10.0"
}
@@ -4522,7 +4517,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4532,7 +4526,6 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
"devOptional": true,
"peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -4615,7 +4608,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.38.0",
"@typescript-eslint/types": "8.38.0",
@@ -4970,7 +4962,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5418,7 +5409,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
@@ -6111,8 +6101,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"peer": true
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/data-urls": {
"version": "5.0.0",
@@ -6385,7 +6374,8 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -7385,7 +7375,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7446,7 +7435,6 @@
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8753,7 +8741,6 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -8816,7 +8803,6 @@
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -9491,7 +9477,6 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true,
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -10057,6 +10042,7 @@
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -11882,12 +11868,13 @@
}
},
"node_modules/playwright": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.54.2"
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -11900,10 +11887,11 @@
}
},
"node_modules/playwright-core": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
@@ -11999,7 +11987,6 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12113,6 +12100,7 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -12127,6 +12115,7 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
},
@@ -12255,7 +12244,6 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12264,7 +12252,6 @@
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -12276,7 +12263,6 @@
"version": "7.62.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -12317,7 +12303,8 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
@@ -13068,7 +13055,6 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
"peer": true,
"engines": {
"node": ">=10"
}
@@ -13858,8 +13844,7 @@
"node_modules/tailwindcss": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"peer": true
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -13961,8 +13946,7 @@
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"peer": true
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
@@ -14013,7 +13997,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14325,7 +14308,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14683,11 +14665,10 @@
}
},
"node_modules/vite": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -14809,7 +14790,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15353,7 +15333,6 @@
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+2 -2
View File
@@ -34,7 +34,7 @@
"@electron/fuses": "^2.0.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "^4.0.6",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -61,7 +61,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vitest": "^3.0.6"
},
"dependencies": {
+2 -2
View File
@@ -386,7 +386,7 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
// check if the namespace exists
const namespace = get().namespaces[namespace_path];
if (namespace) {
/*if (namespace) {
set(
produce((state: SocketioStore) => {
// decrement the count
@@ -430,7 +430,7 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
}
}),
);
}
}*/
},
}));
@@ -10,7 +10,7 @@ import {
import React from "react";
import { Icon } from "@/components/Icon";
import { ControlCard } from "@/control/ControlCard";
import { TouchButton } from "./touch/TouchButton";
interface MachineIdentificationUnique {
machine_identification: {
@@ -49,67 +49,64 @@ export const MachineSelector: React.FC<MachineSelectorProps> = ({
connectedMachineState,
setConnectedMachine,
clearConnectedMachine,
title = "Machine Connection",
}) => {
return (
<ControlCard title={title}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 rounded border px-4 py-2 text-left">
<Icon name="lu:Settings" className="text-xl" />
<span>
{selectedMachine?.name ?? "Select a Machine"}{" "}
{selectedMachine?.machine_identification_unique.serial ?? ""}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>Available Machines</DropdownMenuLabel>
<DropdownMenuSeparator />
{machines.map((machine) => {
const isSelected =
connectedMachineState?.machine_identification_unique
?.machine_identification.machine ===
machine.machine_identification_unique.machine_identification
.machine;
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TouchButton className="text-md h-max px-6 py-6" variant="outline">
<div className="flex flex-row items-center gap-2 text-wrap">
<Icon name="lu:Link2" className="size-6" />
{selectedMachine?.name ?? "Select a Machine"}{" "}
{selectedMachine?.machine_identification_unique.serial ?? ""}
</div>
</TouchButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>Available Machines</DropdownMenuLabel>
<DropdownMenuSeparator />
{machines.map((machine) => {
const isSelected =
connectedMachineState?.machine_identification_unique
?.machine_identification.machine ===
machine.machine_identification_unique.machine_identification
.machine;
return (
<DropdownMenuItem
key={machine.machine_identification_unique.serial}
onClick={() =>
setConnectedMachine(machine.machine_identification_unique)
}
className={`flex min-h-[48px] cursor-pointer items-center gap-2 py-2${
isSelected ? "bg-blue-50" : ""
}`}
>
{connectedMachineState?.is_available ? (
<Icon name="lu:Power" className="text-lg text-green-600" />
) : (
<Icon name="lu:Power" className="text-lg text-gray-400" />
)}
<span>
{machine.name} – Serial:{" "}
{machine.machine_identification_unique.serial}
</span>
</DropdownMenuItem>
);
})}
return (
<DropdownMenuItem
key={machine.machine_identification_unique.serial}
onClick={() =>
setConnectedMachine(machine.machine_identification_unique)
}
className={`flex h-max cursor-pointer items-center gap-2 px-6 py-6${
isSelected ? "bg-blue-50" : ""
}`}
>
{connectedMachineState?.is_available ? (
<Icon name="lu:Link2" className="size-6 text-green-600" />
) : (
<Icon name="lu:Link2Off" className="size-6 text-black" />
)}
<span>
{machine.name} – Serial:{" "}
{machine.machine_identification_unique.serial}
</span>
</DropdownMenuItem>
);
})}
{clearConnectedMachine && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={clearConnectedMachine}
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
>
<Icon name="lu:Settings" className="text-lg" />
<span>Clear Selection</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</ControlCard>
{clearConnectedMachine && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={clearConnectedMachine}
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
>
<Icon name="lu:X" className="size-6 text-red-600" />
<span>Disconnect Machine</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
+10 -2
View File
@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority";
import React from "react";
type Props = {
variant: "error" | "success";
variant: "error" | "success" | "info";
children: React.ReactNode;
};
@@ -14,10 +14,18 @@ export function StatusBadge({ variant, children }: Props) {
variant: {
error: "bg-red-500",
success: "bg-green-600",
info: "bg-yellow-400 text-black",
},
},
});
const icon: IconName = variant === "error" ? "lu:TriangleAlert" : "lu:Check";
const icon: IconName =
variant === "error"
? "lu:TriangleAlert"
: variant === "success"
? "lu:Check"
: "lu:Info";
return (
<Badge
className={badgeStyle({
@@ -72,6 +72,7 @@ export function Laser1ControlPage() {
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Roundness"
icon="lu:Circle"
timeseries={roundness}
renderValue={(value) => value.toFixed(3)}
/>
@@ -20,6 +20,12 @@ export function Laser1Page() {
title: "Graphs",
icon: "lu:ChartSpline",
},
{
link: "settings",
activeLink: "settings",
title: "Config",
icon: "lu:Settings",
},
// {
// link: "manual",
// activeLink: "manual",
@@ -0,0 +1,42 @@
import React from "react";
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { Label } from "@/control/Label";
import { useLaser1 } from "./useLaser1";
import { MachineSelector } from "@/components/MachineConnectionDropdown";
import { ControlGrid } from "@/control/ControlGrid";
export function Laser1SettingsPage() {
const {
state,
selectedMachine,
filteredMachines,
setConnectedWinder,
disconnectWinder,
} = useLaser1();
return (
<Page>
<ControlGrid>
<ControlCard title="Speed by Diameter Settings">
<Label label="Associated Winder">
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_winder_state}
setConnectedMachine={setConnectedWinder}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedWinder({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectWinder(selectedMachine.machine_identification_unique);
}}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -23,6 +23,24 @@ import {
} from "@/lib/timeseries";
// ========== Event Schema Definitions ==========
/**
* Connected machine state schema
*/
export const machineIdentificationSchema = z.object({
vendor: z.number(),
machine: z.number(),
});
export const machineIdentificationUniqueSchema = z.object({
machine_identification: machineIdentificationSchema,
serial: z.number(),
});
export const connectedMachineStateSchema = z.object({
machine_identification_unique: machineIdentificationUniqueSchema.nullable(),
is_available: z.boolean(),
});
/**
* Live values from Laser (30 FPS)
*/
@@ -43,6 +61,7 @@ export const stateEventDataSchema = z.object({
lower_tolerance: z.number(),
target_diameter: z.number(),
}),
connected_winder_state: connectedMachineStateSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -1,13 +1,18 @@
import { toastError } from "@/components/Toast";
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
import { MachineIdentificationUnique } from "@/machines/types";
import { laser1 } from "@/machines/properties";
import {
machineIdentification,
machineIdentificationUnique,
MachineIdentificationUnique,
} from "@/machines/types";
import { laser1, VENDOR_QITECH } from "@/machines/properties";
import { laser1SerialRoute } from "@/routes/routes";
import { z } from "zod";
import { useLaser1Namespace, StateEvent } from "./laser1Namespace";
import { useEffect, useMemo } from "react";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { produce } from "immer";
import { useMachines } from "@/client/useMachines";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
@@ -54,6 +59,16 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
const { request: requestHigherTolerance } = useMachineMutation(
schemaHigherTolerance,
);
const { request: requestConnectedWinder } = useMachineMutation(
z.object({
SetConnectedWinder: machineIdentificationUnique,
}),
);
const { request: requestDisconnectWinder } = useMachineMutation(
z.object({
DisconnectWinder: machineIdentificationUnique,
}),
);
// Action functions with verb-first names
const setTargetDiameter = (target_diameter: number) => {
@@ -101,10 +116,80 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
);
};
const setConnectedWinder = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_winder_state.machine_identification_unique =
machineIdentificationUnique;
},
() =>
requestConnectedWinder({
machine_identification_unique,
data: { SetConnectedWinder: machineIdentificationUnique },
}),
);
};
const disconnectWinder = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_winder_state.machine_identification_unique =
null;
},
() =>
requestDisconnectWinder({
machine_identification_unique,
data: { DisconnectWinder: machineIdentificationUnique },
}),
);
};
// General Helper functions
const machines = useMachines();
// Filter machines for the correct type
const filteredMachines = useMemo(
() =>
machines.filter(
(m) =>
m.machine_identification_unique.machine_identification.vendor ===
VENDOR_QITECH &&
m.machine_identification_unique.machine_identification.machine ===
0x0002,
),
[machines, machineIdentification],
);
// Get selected machine by serial
const selectedMachine = useMemo(() => {
const serial =
state?.data.connected_winder_state?.machine_identification_unique?.serial;
return (
filteredMachines.find(
(m) => m.machine_identification_unique.serial === serial,
) ?? null
);
}, [filteredMachines, state]);
return {
// Consolidated state
state: stateOptimistic.value?.data,
filteredMachines,
selectedMachine,
// Default state for initial values
defaultState: defaultState?.data,
@@ -122,6 +207,8 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
setTargetDiameter,
setLowerTolerance,
setHigherTolerance,
setConnectedWinder,
disconnectWinder,
};
}
@@ -272,13 +272,18 @@ export function Winder2ControlPage() {
Diameter: {
children: "Diameter",
icon: "lu:Sun",
disabled: true,
disabled: !state?.connected_laser_state.is_available,
},
}}
onChange={setPullerRegulationMode}
disabled={isDisabled}
loading={isLoading}
/>
{state?.connected_laser_state?.is_available !== true ? (
<StatusBadge variant={"info"}>
{"No associated Laser found"}
</StatusBadge>
) : null}
</Label>
<Label label="Target Speed">
<EditValue
@@ -40,10 +40,13 @@ export function Winder2SettingPage() {
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
isLoading,
isDisabled,
setSpeedPiKp,
setSpeedPiKi,
selectedMachine,
filteredMachines,
setConnectedLaser,
disconnectLaser,
setConnectedMachine,
disconnectMachine,
} = useWinder2();
const handleXlModeChange = (enabled: boolean) => {
@@ -354,20 +357,48 @@ export function Winder2SettingPage() {
/>
</Label>
</ControlCard>
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_machine_state}
setConnectedMachine={setConnectedMachine}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedMachine({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectMachine(selectedMachine.machine_identification_unique);
}}
/>
<ControlCard title="Diameter-Based Winder Control">
<Label label="Associated Laser">
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_laser_state}
setConnectedMachine={setConnectedLaser}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedMachine({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectLaser(selectedMachine.machine_identification_unique);
}}
/>
</Label>
<Label label="PI Controller Kp Factor">
<EditValue
value={state?.pi_settings?.kp}
defaultValue={defaultState?.pi_settings?.kp}
min={0}
max={1000}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={setSpeedPiKp}
title="Speed P KP"
/>
</Label>
<Label label="PI Controller Ki Factor">
<EditValue
value={state?.pi_settings?.ki}
defaultValue={defaultState?.pi_settings?.ki}
min={0}
max={1000}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={setSpeedPiKi}
title="Speed P KI"
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
@@ -179,6 +179,27 @@ export function useWinder2() {
}),
);
const { request: requestConnectedLaser } = useMachineMutation(
z.object({
SetConnectedLaser: machineIdentificationUnique,
}),
);
const { request: requestDisconnectLaser } = useMachineMutation(
z.object({
DisconnectLaser: machineIdentificationUnique,
}),
);
const { request: requestSpeedPiSettings } = useMachineMutation(
z.object({
SetSpeedPiSettings: z.object({
kp: z.number(),
ki: z.number(),
}),
}),
);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
@@ -593,6 +614,85 @@ export function useWinder2() {
);
};
const setSpeedPiKp = (kp: number) => {
updateStateOptimistically(
(current) => {
current.data.pi_settings.kp = kp;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(currentState.data.pi_settings, (draft) => {
draft.kp = kp;
});
requestSpeedPiSettings({
machine_identification_unique: machineIdentification,
data: { SetSpeedPiSettings: settings },
});
}
},
);
};
const setSpeedPiKi = (ki: number) => {
updateStateOptimistically(
(current) => {
current.data.pi_settings.ki = ki;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(currentState.data.pi_settings, (draft) => {
draft.ki = ki;
});
requestSpeedPiSettings({
machine_identification_unique: machineIdentification,
data: { SetSpeedPiSettings: settings },
});
}
},
);
};
const setConnectedLaser = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_laser_state.machine_identification_unique =
machineIdentificationUnique;
},
() =>
requestConnectedLaser({
machine_identification_unique,
data: { SetConnectedLaser: machineIdentificationUnique },
}),
);
};
const disconnectLaser = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_laser_state.machine_identification_unique = null;
},
() =>
requestDisconnectLaser({
machine_identification_unique,
data: { DisconnectLaser: machineIdentificationUnique },
}),
);
};
// Calculate loading states
const isLoading = stateOptimistic.isOptimistic;
const isDisabled = !stateOptimistic.isInitialized;
@@ -607,7 +707,7 @@ export function useWinder2() {
m.machine_identification_unique.machine_identification.vendor ===
VENDOR_QITECH &&
m.machine_identification_unique.machine_identification.machine ===
0x0008,
0x0006,
),
[machines, machineIdentification],
);
@@ -615,8 +715,7 @@ export function useWinder2() {
// Get selected machine by serial
const selectedMachine = useMemo(() => {
const serial =
state?.data.connected_machine_state?.machine_identification_unique
?.serial;
state?.data.connected_laser_state?.machine_identification_unique?.serial;
return (
filteredMachines.find(
@@ -674,7 +773,11 @@ export function useWinder2() {
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
setSpeedPiKp,
setSpeedPiKi,
setConnectedMachine,
disconnectMachine,
setConnectedLaser,
disconnectLaser,
};
}
@@ -39,7 +39,10 @@ export const liveValuesEventDataSchema = z.object({
/**
* Puller regulation type enum
*/
export const pullerRegulationSchema = z.enum(["Speed", "Diameter"]);
export const pullerRegulationSchema = z.enum([
"Speed",
"Diameter",
]);
export type PullerRegulation = z.infer<typeof pullerRegulationSchema>;
/**
@@ -141,7 +144,7 @@ export const modeStateSchema = z.object({
});
/**
* Connected machine state scheme
* Connected machine state schema
*/
export const machineIdentificationSchema = z.object({
vendor: z.number(),
@@ -158,6 +161,14 @@ export const connectedMachineStateSchema = z.object({
is_available: z.boolean(),
});
/**
* PI Controller settings schema
*/
export const piSettingsSchema = z.object({
kp: z.number(),
ki: z.number(),
});
/**
* Tension arm state schema
*/
@@ -192,6 +203,8 @@ export const stateEventDataSchema = z.object({
spool_speed_controller_state: spoolSpeedControllerStateSchema,
spool_automatic_action_state: spoolAutomaticActionStateSchema,
connected_machine_state: connectedMachineStateSchema,
connected_laser_state: connectedMachineStateSchema,
pi_settings: piSettingsSchema,
});
// ========== Event Schemas with Wrappers ==========
+8
View File
@@ -41,6 +41,7 @@ import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph
import { z } from "zod";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
import { Laser1SettingsPage } from "@/machines/laser/laser1/Laser1Settings";
// make a route tree like this
// _mainNavigation/machines/winder2/$serial/control
// _mainNavigation/configuration/a
@@ -156,6 +157,12 @@ export const laser1PresetsRoute = createRoute({
component: () => <Laser1PresetsPage />,
});
export const laser1SettingsRoute = createRoute({
getParentRoute: () => laser1SerialRoute,
path: "settings",
component: () => <Laser1SettingsPage />,
});
export const mock1SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "mock1/$serial",
@@ -312,6 +319,7 @@ export const rootTree = RootRoute.addChildren([
laser1ControlRoute,
laser1GraphsRoute,
laser1PresetsRoute,
laser1SettingsRoute,
]),
aquapath1SerialRoute.addChildren([
+1 -1
View File
@@ -8,7 +8,7 @@ workspace = true
[dependencies]
clap = { version = "4.5.40", features = ["derive"] }
ethercrab = { git = "https://github.com/ethercrab-rs/ethercrab", branch = "main", features = ["xdp"] }
ethercrab = "0.6.0"
interfaces = "0.0.9"
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
ethercat_hal = { path = "../ethercat-hal" }
+1 -1
View File
@@ -8,7 +8,7 @@ workspace = true
[dependencies]
anyhow = "1.0.100"
ethercrab = { git = "https://github.com/ethercrab-rs/ethercrab", branch = "main", features = ["xdp"] }
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"
+1 -1
View File
@@ -34,5 +34,5 @@ craneLib.buildPackage {
inherit src strictDeps nativeBuildInputs buildInputs pname version;
};
cargoExtraArgs = "-p server --features tracing-journald,xdp --no-default-features";
cargoExtraArgs = "-p server --features tracing-journald,io-uring --no-default-features";
}
+1 -6
View File
@@ -9,7 +9,7 @@ workspace = true
[dependencies]
ethercrab = { git = "https://github.com/ethercrab-rs/ethercrab", branch = "main", features = ["xdp"] }
ethercrab = "0.6"
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
control_core_derive = { path = "../control-core-derive" }
@@ -55,10 +55,6 @@ 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"
[dev-dependencies]
approx = "0.5.1"
textplots = "0.8.7"
@@ -79,7 +75,6 @@ tracing-otel = [
mock-machine = []
memory-locking = []
io-uring = []
xdp = []
development-build = []
[profile.release]
+2 -3
View File
@@ -6,7 +6,6 @@ use crate::socketio::namespaces::Namespaces;
use control_core::machines::Machine;
use control_core::machines::identification::{DeviceIdentification, MachineIdentificationUnique};
use control_core::machines::manager::MachineManager;
use control_core::serial::serial_detection::SerialDetection;
use control_core::socketio::event::GenericEvent;
use ethercat_hal::devices::EthercatDevice;
use ethercrab::{MainDevice, SubDeviceGroup, subdevice_group::Op};
@@ -25,7 +24,7 @@ pub struct SocketioSetup {
}
pub struct SerialSetup {
pub serial_detection: SerialDetection<'static>,
pub serial_registry: &'static SERIAL_DEVICE_REGISTRY,
}
pub struct AppState {
@@ -85,7 +84,7 @@ impl AppState {
},
ethercat_setup: Arc::new(RwLock::new(None)),
serial_setup: Arc::new(RwLock::new(SerialSetup {
serial_detection: SerialDetection::new(&SERIAL_DEVICE_REGISTRY),
serial_registry: &SERIAL_DEVICE_REGISTRY,
})),
machines: Arc::new(RwLock::new(MachineManager::new())),
performance_metrics: Arc::new(RwLock::new(EthercatPerformanceMetrics::new())),
@@ -0,0 +1,38 @@
use std::sync::Arc;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use crate::{
app_state::AppState,
socketio::main_namespace::{
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
},
};
pub async fn send_ethercat_discovering(app_state: Arc<AppState>) {
{
let main_namespace = &mut app_state
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Discovering(true).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
}
}
pub async fn send_ethercat_found(app_state: Arc<AppState>, interface: &str) {
// Notify client via socketio
let interface = interface.to_string();
let app_state_socketio = app_state.clone();
smol::block_on(async move {
let main_namespace = &mut app_state_socketio
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Done(interface).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
});
}
+19 -66
View File
@@ -1,70 +1,23 @@
use super::setup::setup_loop;
use crate::{
app_state::AppState,
socketio::main_namespace::{
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
},
};
use control_core::{
ethercat::interface_discovery::discover_ethercat_interface,
socketio::namespace::NamespaceCacheingLogic,
};
use std::sync::Arc;
use control_core::ethercat::interface_discovery::discover_ethercat_interface;
use smol::{Task, Timer};
use std::time::Duration;
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 || {
// Notify client via socketio
let app_state_socketio = app_state_clone.clone();
smol::block_on(async move {
let main_namespace = &mut app_state_socketio
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Discovering(true).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
});
// tries to find a suitable interface in a loop
let interface = smol::block_on(async {
loop {
match discover_ethercat_interface().await {
Ok(interface) => {
tracing::info!("Found working interface: {}", interface);
break interface;
}
Err(_) => {
tracing::warn!("No working interface found, retrying...");
// wait 1 seconds before retrying
smol::Timer::after(std::time::Duration::from_secs(1)).await;
}
}
}
});
// Notify client via socketio
let interface_clone = interface.clone();
let app_state_socketio = app_state_clone.clone();
smol::block_on(async move {
let main_namespace = &mut app_state_socketio
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Done(interface_clone).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
});
if let Err(e) = smol::block_on(setup_loop(&interface, app_state_clone)) {
tracing::error!("EtherCAT setup failed: {:?}", e);
pub async fn find_ethercat_interface() -> Result<String, anyhow::Error> {
loop {
match discover_ethercat_interface().await {
Ok(interface) => {
tracing::info!("Found EtherCAT Interface at: {}", interface);
return Ok(interface);
}
})?;
Err(e) => {
tracing::warn!("No working interface found: {}. Retrying...", e);
Timer::after(Duration::from_secs(1)).await;
}
}
}
}
Ok(())
// Returns a Future to the interface
pub fn start_interface_discovery() -> Task<Result<String, anyhow::Error>> {
smol::spawn(find_ethercat_interface())
}
+1
View File
@@ -1,3 +1,4 @@
pub mod config;
pub mod ethercat_discovery_info;
pub mod init;
pub mod setup;
+11 -35
View File
@@ -17,7 +17,7 @@ use control_core::machines::new::MachineNewHardwareEthercat;
use control_core::realtime::{set_core_affinity, set_realtime_priority};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::devices::devices_from_subdevices;
use ethercrab::std::ethercat_now;
use ethercrab::std::{ethercat_now, tx_rx_task};
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
use std::{sync::Arc, time::Duration};
@@ -52,38 +52,14 @@ pub async fn setup_loop(interface: &str, app_state: Arc<AppState>) -> Result<(),
let _ = set_core_affinity(3);
// Set the thread to real-time priority
#[cfg(not(feature = "development-build"))]
let _ = set_realtime_priority();
#[cfg(not(any(
all(target_os = "linux", feature = "io-uring"),
all(target_os = "linux", feature = "xdp")
)))]
{
use ethercrab::std::tx_rx_task;
use futures::executor::block_on;
let rt = smol::LocalExecutor::new();
let _ = block_on(rt.run(async {
tx_rx_task(&interface, tx, rx)
.expect("spawn TX/RX task")
.await
}));
}
#[cfg(all(target_os = "linux", feature = "io-uring"))]
{
use ethercrab::std::tx_rx_task_io_uring;
let _ = tx_rx_task_io_uring(&interface, tx, rx)
.expect("Failed to spawn TX/RX task (io_uring)");
}
#[cfg(all(target_os = "linux", feature = "xdp"))]
{
use ethercrab::std::tx_rx_task_xdp;
let _ = tx_rx_task_xdp(&interface, tx, rx)
.expect("Failed to spawn TX/RX task (io_uring)");
}
let rt = smol::LocalExecutor::new();
let _ = smol::block_on(rt.run(async {
tx_rx_task(&interface, tx, rx)
.expect("spawn TX/RX task")
.await
}));
})
.expect("Building thread");
@@ -96,13 +72,13 @@ pub async fn setup_loop(interface: &str, app_state: Arc<AppState>) -> Result<(),
// Default 30_000us
pdu: Duration::from_micros(30_000),
// Default 10ms
eeprom: Duration::from_millis(100),
eeprom: Duration::from_millis(10),
// Default 0ms
wait_loop_delay: Duration::from_millis(0),
// Default 100ms
mailbox_echo: Duration::from_millis(1000),
mailbox_echo: Duration::from_millis(100),
// Default 1000ms
mailbox_response: Duration::from_millis(10000),
mailbox_response: Duration::from_millis(1000),
},
MainDeviceConfig {
// Default RetryBehaviour::None
+54 -28
View File
@@ -1,55 +1,81 @@
use crate::app_state::AppState;
use crate::{app_state::AppState, ethercat::setup::setup_loop};
use bitvec::prelude::*;
use control_core::machines::connection::MachineConnection;
use control_core::realtime::{set_core_affinity, set_realtime_priority};
use control_core::realtime::set_core_affinity;
#[cfg(not(feature = "development-build"))]
use control_core::realtime::set_realtime_priority;
use std::sync::Arc;
use std::time::Instant;
use tracing::{instrument, trace_span};
pub fn init_loop(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
pub fn start_loop_thread(
interface: &str,
app_state: Arc<AppState>,
) -> Result<std::thread::JoinHandle<()>, std::io::Error> {
let res = smol::block_on(async { setup_loop(interface, app_state.clone()).await });
match res {
Ok(_) => tracing::info!("Successfully initialized EtherCAT network"),
Err(e) => {
tracing::error!(
"[{}::init_loop] Failed to initialize EtherCAT network \n{:?}",
module_path!(),
e
);
}
}
// Start control loop
std::thread::Builder::new()
let res = std::thread::Builder::new()
.name("loop".to_owned())
.spawn(move || {
let rt = smol::LocalExecutor::new();
// Set core affinity to third core
let _ = set_core_affinity(2);
// Set the thread to real-time priority
#[cfg(not(feature = "development-build"))]
if let Err(e) = set_realtime_priority() {
tracing::error!(
"[{}::init_loop] Failed to set real-time priority \n{:?}",
"[{}::init_loop] Failed to set thread to real-time priority \n{:?}",
module_path!(),
e
);
} else {
tracing::info!(
"[{}::init_loop] Real-time priority set successfully",
"[{}::init_loop] Real-time priority set successfully for current thread",
module_path!()
);
}
loop {
let res = smol::block_on(rt.run(async { loop_once(app_state.clone()).await }));
let rt = smol::LocalExecutor::new();
// Instead of creating a NEW async task every iteration of our realtime txrx loop, create it ONCE
smol::block_on(async {
rt.run(async {
loop {
if let Err(e) = loop_once(app_state.clone()).await {
tracing::error!("Loop failed\n{:?}", e);
std::process::exit(1);
}
}
})
.await;
});
if let Err(err) = res {
tracing::error!("Loop failed\n{:?}", err);
break;
/*
// If timeouts keep happenning do a spin_sleep, better then thread_sleep and allows for more deterministic timing
let elapsed = start.elapsed();
if elapsed < target_cycle {
spin_sleep::sleep(target_cycle - elapsed);
}
}
// Exit the entire program if the Loop fails (gets restarted by systemd if running on NixOS)
std::process::exit(1);
})
.map_err(|e| {
anyhow::anyhow!(
"[{}::init_loop] Failed to spawn loop thread\n{:?}",
module_path!(),
e
)
})?;
*/
Ok(())
if let Some(last_loop_start) = app_state
.performance_metrics
.read_arc_blocking()
.last_loop_start
{
tracing::info!("Failing Loop Took {:?}", last_loop_start.elapsed());
}
// Exit the entire program if the Loop fails
// gets restarted by systemd if running on NixOS, or different distro wtih the same sysd service
std::process::exit(1);
});
return res;
}
#[instrument(skip(app_state))]
+2 -2
View File
@@ -3,6 +3,8 @@ pub mod api;
pub mod buffer_tower_controller;
pub mod new;
#[cfg(not(feature = "mock-machine"))]
use crate::machines::{MACHINE_BUFFER_V1, VENDOR_QITECH, winder2::Winder2};
use api::{Buffer1Namespace, BufferV1Events, LiveValuesEvent, ModeState, StateEvent};
use buffer_tower_controller::BufferTowerController;
use control_core::machines::connection::{CrossConnectableMachine, MachineCrossConnection};
@@ -18,8 +20,6 @@ use serde::{Deserialize, Serialize};
use smol::lock::RwLock;
use std::{sync::Weak, time::Instant};
use crate::machines::{MACHINE_BUFFER_V1, VENDOR_QITECH, winder2::Winder2};
#[derive(Debug, Machine)]
pub struct BufferV1 {
// controllers
+3 -1
View File
@@ -19,8 +19,10 @@ use std::time::{Duration, Instant};
///
impl MachineAct for LaserMachine {
fn act(&mut self, now: Instant) {
// sync the diameter
self.set_measured_diameter();
self.update();
// more than 33ms have passed since last emit (30 "fps" target)
// The live values are updated approximately 60 times per second
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
+24 -1
View File
@@ -1,6 +1,9 @@
use super::LaserMachine;
use control_core::{
machines::api::MachineApi,
machines::{
api::MachineApi, connection::MachineCrossConnectionState,
identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
@@ -36,6 +39,8 @@ pub struct StateEvent {
pub is_default_state: bool,
/// laser state
pub laser_state: LaserState,
/// connected winder state
pub connected_winder_state: MachineCrossConnectionState,
}
impl StateEvent {
@@ -54,6 +59,13 @@ pub struct LaserState {
pub target_diameter: f64,
}
#[derive(Serialize, Debug, Clone)]
pub struct ConnectedMachineState {
/// Connected Machine
pub machine_identification_unique: Option<MachineIdentificationUnique>,
pub is_available: bool,
}
pub enum LaserEvents {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
@@ -91,6 +103,11 @@ enum Mutation {
SetTargetDiameter(f64),
SetLowerTolerance(f64),
SetHigherTolerance(f64),
// Connect Machine
SetConnectedWinder(MachineIdentificationUnique),
// Disconnect Machine
DisconnectWinder(MachineIdentificationUnique),
}
impl NamespaceCacheingLogic<LaserEvents> for LaserMachineNamespace {
@@ -117,6 +134,12 @@ impl MachineApi for LaserMachine {
Mutation::SetTargetDiameter(target_diameter) => {
self.set_target_diameter(target_diameter);
}
Mutation::SetConnectedWinder(machine_identification_unique) => {
self.set_connected_winder(machine_identification_unique);
}
Mutation::DisconnectWinder(machine_identification_unique) => {
self.disconnect_winder(machine_identification_unique);
}
}
Ok(())
}
+89 -5
View File
@@ -1,15 +1,22 @@
use crate::{
machines::{MACHINE_LASER_V1, VENDOR_QITECH},
machines::{MACHINE_LASER_V1, VENDOR_QITECH, winder2::Winder2},
serial::devices::laser::Laser,
};
use api::{LaserEvents, LaserMachineNamespace, LaserState, LiveValuesEvent, StateEvent};
use control_core::{
machines::identification::{MachineIdentification, MachineIdentificationUnique},
machines::{
connection::{CrossConnectableMachine, MachineConnection, MachineCrossConnection},
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use smol::lock::RwLock;
use std::{sync::Arc, time::Instant};
use std::{
sync::{Arc, Weak},
time::Instant,
};
use uom::si::{f64::Length, length::millimeter};
pub mod act;
@@ -18,8 +25,6 @@ pub mod new;
#[derive(Debug, Machine)]
pub struct LaserMachine {
machine_identification_unique: MachineIdentificationUnique,
// drivers
laser: Arc<RwLock<Laser>>,
@@ -27,6 +32,13 @@ pub struct LaserMachine {
namespace: LaserMachineNamespace,
last_measurement_emit: Instant,
// machine connection
pub machine_manager: Weak<RwLock<MachineManager>>,
pub machine_identification_unique: MachineIdentificationUnique,
// connected machines
pub connected_winder: MachineCrossConnection<LaserMachine, Winder2>,
// laser values
diameter: Length,
x_diameter: Option<Length>,
@@ -41,6 +53,12 @@ pub struct LaserMachine {
emitted_default_state: bool,
}
impl CrossConnectableMachine<LaserMachine, Winder2> for LaserMachine {
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<LaserMachine, Winder2> {
&mut self.connected_winder
}
}
impl LaserMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
@@ -74,6 +92,7 @@ impl LaserMachine {
StateEvent {
is_default_state: false,
laser_state: laser,
connected_winder_state: self.connected_winder.to_state(),
}
}
@@ -85,6 +104,7 @@ impl LaserMachine {
lower_tolerance: self.laser_target.lower_tolerance.get::<millimeter>(),
target_diameter: self.laser_target.diameter.get::<millimeter>(),
},
connected_winder_state: self.connected_winder.to_state(),
};
self.namespace.emit(LaserEvents::State(state.build()));
@@ -150,6 +170,70 @@ impl LaserMachine {
}
}
/// implement machine connection
impl LaserMachine {
/// set connected winder
pub fn set_connected_winder(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
Winder2::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_winder
.set_connected_machine(&machine_identification_unique);
self.connected_winder.reverse_connect();
self.emit_state();
}
/// disconnect winder
pub fn disconnect_winder(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
Winder2::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_winder.reverse_disconnect();
self.connected_winder.disconnect();
self.emit_state();
}
}
impl LaserMachine {
fn set_measured_diameter(&mut self) {
let diameter = smol::block_on(async {
self.laser
.read()
.await
.get_data()
.await
.map(|laser_data| laser_data.diameter.get::<millimeter>())
});
if let Some(slot) = self.get_cross_connection().connected_machine.upgrade() {
let slot = slot.lock_blocking();
if let MachineConnection::Connected(machine) = &slot.machine_connection {
let mut winder = machine.lock_blocking();
winder
.puller_speed_controller
.set_measured_diameter(diameter.unwrap_or(0.0));
}
}
}
}
#[derive(Debug, Clone)]
pub struct LaserTarget {
diameter: Length,
+7
View File
@@ -4,6 +4,7 @@ use crate::serial::{devices::laser::Laser, registry::SERIAL_DEVICE_REGISTRY};
use super::{LaserMachine, LaserTarget, api::LaserMachineNamespace};
use anyhow::Error;
use control_core::machines::connection::MachineCrossConnection;
use control_core::machines::new::{MachineNewHardware, MachineNewTrait};
use uom::ConstZero;
use uom::si::{f64::Length, length::millimeter};
@@ -34,6 +35,7 @@ impl MachineNewTrait for LaserMachine {
lower_tolerance: Length::new::<millimeter>(0.05),
diameter: Length::new::<millimeter>(1.75),
};
let mut laser_machine = Self {
machine_identification_unique: params.get_machine_identification_unique(),
laser,
@@ -43,6 +45,11 @@ impl MachineNewTrait for LaserMachine {
last_measurement_emit: Instant::now(),
laser_target,
emitted_default_state: false,
machine_manager: params.machine_manager.clone(),
connected_winder: MachineCrossConnection::new(
params.machine_manager.clone(),
&params.get_machine_identification_unique(),
),
diameter: Length::ZERO,
x_diameter: None,
y_diameter: None,
+6 -1
View File
@@ -14,7 +14,10 @@ use ethercrab::{SubDevice, SubDeviceRef};
use smol::lock::RwLock;
pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))]
pub mod buffer1;
pub mod extruder1;
pub mod laser;
pub mod mock;
@@ -26,9 +29,11 @@ pub const MACHINE_WINDER_V1: u16 = 0x0002;
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
pub const MACHINE_LASER_V1: u16 = 0x0006;
pub const MACHINE_MOCK: u16 = 0x0007;
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
#[cfg(not(feature = "mock-machine"))]
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
async fn get_device_ident<
'maindevice,
'subdevices,
+12 -5
View File
@@ -1,12 +1,12 @@
#[cfg(feature = "mock-machine")]
use crate::machines::{extruder1::mock::ExtruderV2, mock::MockMachine};
use crate::machines::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
#[cfg(not(feature = "mock-machine"))]
use crate::machines::extruder1::ExtruderV2;
use crate::machines::{
aquapath1::AquaPathV1, buffer1::BufferV1, laser::LaserMachine, winder2::Winder2,
aquapath1::AquaPathV1, buffer1::BufferV1, extruder1::ExtruderV2, laser::LaserMachine,
winder2::Winder2,
};
use control_core::machines::registry::MachineRegistry;
use lazy_static::lazy_static;
@@ -14,11 +14,18 @@ lazy_static! {
pub static ref MACHINE_REGISTRY: MachineRegistry = {
let mut mc = MachineRegistry::new();
mc.register::<Winder2>(Winder2::MACHINE_IDENTIFICATION);
mc.register::<LaserMachine>(LaserMachine::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV2>(ExtruderV2::MACHINE_IDENTIFICATION);
#[cfg(feature = "mock-machine")]
mc.register::<MockMachine>(MockMachine::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<LaserMachine>(LaserMachine::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<BufferV1>(BufferV1::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
mc
};
+4
View File
@@ -1,7 +1,11 @@
#[cfg(not(feature = "mock-machine"))]
use super::Winder2;
#[cfg(not(feature = "mock-machine"))]
use control_core::machines::new::MachineAct;
#[cfg(not(feature = "mock-machine"))]
use std::time::{Duration, Instant};
#[cfg(not(feature = "mock-machine"))]
impl MachineAct for Winder2 {
fn act(&mut self, now: Instant) {
// sync the spool speed
+103 -32
View File
@@ -1,31 +1,63 @@
use super::puller_speed_controller::GearRatio;
use super::{Winder2, Winder2Mode, puller_speed_controller::PullerRegulationMode};
use control_core::{
machines::{
api::MachineApi, connection::MachineCrossConnectionState,
identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
#[cfg(feature = "mock-machine")]
mod winder2_imports {
pub use super::super::Winder2Mode;
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use serde::Deserialize;
pub use serde::Serialize;
pub use std::{sync::Arc, time::Duration};
pub use control_core::{
machines::{
connection::MachineCrossConnectionState, identification::MachineIdentificationUnique,
},
},
};
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
},
};
pub use control_core_derive::BuildEvent;
pub use smol::lock::Mutex;
pub use tracing::instrument;
}
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::lock::Mutex;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use tracing::instrument;
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use super::super::{Winder2, Winder2Mode};
pub use control_core::{
machines::{
api::MachineApi, connection::MachineCrossConnectionState,
identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
},
};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub use control_core_derive::BuildEvent;
pub use serde::{Deserialize, Serialize};
pub use serde_json::Value;
pub use smol::lock::Mutex;
pub use std::{
sync::Arc,
time::{Duration, Instant},
};
pub use tracing::instrument;
}
pub use winder2_imports::*;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum Mode {
#[default]
Standby,
Hold,
Pull,
@@ -55,7 +87,7 @@ impl From<Mode> for Winder2Mode {
}
#[derive(Deserialize, Serialize)]
enum Mutation {
pub enum Mutation {
// Traverse
/// Position in mm from home point
SetTraverseLimitOuter(f64),
@@ -103,11 +135,16 @@ enum Mutation {
// Mode
SetMode(Mode),
// Pid Configure
SetSpeedPiSettings(PiSettings),
// Connected Machine
SetConnectedMachine(MachineIdentificationUnique),
SetConnectedLaser(MachineIdentificationUnique),
// Disconnect Machine
DisconnectMachine(MachineIdentificationUnique),
DisconnectLaser(MachineIdentificationUnique),
}
#[derive(Serialize, Debug, Clone, Default)]
@@ -145,11 +182,15 @@ pub struct StateEvent {
pub tension_arm_state: TensionArmState,
/// spool speed controller state
pub spool_speed_controller_state: SpoolSpeedControllerState,
/// connected machine state
/// connected machine state (Buffer)
pub connected_machine_state: MachineCrossConnectionState,
/// connected laser state
pub connected_laser_state: MachineCrossConnectionState,
/// pi settings
pub pi_settings: PiSettingsStates,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct TraverseState {
/// min position in mm
pub limit_inner: f64,
@@ -183,7 +224,7 @@ pub struct TraverseState {
pub can_go_home: bool,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct PullerState {
/// regulation type
pub regulation: PullerRegulationMode,
@@ -197,20 +238,21 @@ pub struct PullerState {
pub gear_ratio: GearRatio,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub enum SpoolAutomaticActionMode {
#[default]
NoAction,
Pull,
Hold,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolAutomaticActionState {
pub spool_required_meters: f64,
pub spool_automatic_action_mode: SpoolAutomaticActionMode,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct ModeState {
/// mode
pub mode: Mode,
@@ -224,7 +266,7 @@ pub struct TensionArmState {
pub zeroed: bool,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolSpeedControllerState {
/// regulation mode
pub regulation_mode: super::spool_speed_controller::SpoolSpeedControllerType,
@@ -246,6 +288,25 @@ pub struct SpoolSpeedControllerState {
pub forward: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct ConnectedMachineState {
/// Connected Machine
pub machine_identification_unique: Option<MachineIdentificationUnique>,
pub is_available: bool,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct PiSettings {
pub kp: f64,
pub ki: f64,
}
#[derive(Serialize, Debug, Clone)]
pub struct PiSettingsStates {
pub kp: f64,
pub ki: f64,
}
pub enum Winder2Events {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
@@ -286,6 +347,7 @@ impl CacheableEvents<Self> for Winder2Events {
}
}
#[cfg(not(feature = "mock-machine"))]
impl MachineApi for Winder2 {
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
@@ -335,6 +397,15 @@ impl MachineApi for Winder2 {
Mutation::DisconnectMachine(machine_identification_unique) => {
self.disconnect_buffer(machine_identification_unique)
}
Mutation::SetConnectedLaser(machine_identification_unique) => {
self.set_connected_laser(machine_identification_unique)
}
Mutation::DisconnectLaser(machine_identification_unique) => {
self.disconnect_laser(machine_identification_unique)
}
Mutation::SetSpeedPiSettings(settings) => {
self.configure_pi_controller(settings);
}
}
Ok(())
}
+66 -19
View File
@@ -1,26 +1,35 @@
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::puller_speed_controller::PullerRegulationMode;
use crate::machines::buffer1::BufferV1;
pub use super::super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
pub use crate::machines::buffer1::BufferV1;
pub use crate::machines::laser::LaserMachine;
pub use crate::machines::winder2::api::{PiSettings, PiSettingsStates};
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,
};
pub use api::{
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode,
SpoolAutomaticActionState, SpoolSpeedControllerState, StateEvent, TensionArmState,
TraverseState, Winder2Events,
};
pub use control_core::socketio::event::BuildEvent;
pub use control_core::{
machines::identification::MachineIdentificationUnique,
socketio::namespace::NamespaceCacheingLogic, uom_extensions::velocity::meter_per_minute,
};
pub use std::time::Instant;
pub use uom::si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
length::{meter, millimeter},
};
}
use uom::si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
length::{meter, millimeter},
};
#[cfg(not(feature = "mock-machine"))]
pub use winder2_imports::*;
#[cfg(not(feature = "mock-machine"))]
impl Winder2 {
/// Implement Spool
/// called by `act`
@@ -286,6 +295,11 @@ impl Winder2 {
spool_automatic_action_mode: self.spool_automatic_action.mode.clone(),
},
connected_machine_state: self.connected_buffer.to_state(),
connected_laser_state: self.connected_laser.to_state(),
pi_settings: PiSettingsStates {
kp: self.puller_speed_controller.p_dead_controller.get_kp(),
ki: self.puller_speed_controller.p_dead_controller.get_ki(),
},
}
}
@@ -523,6 +537,39 @@ impl Winder2 {
self.connected_buffer.reverse_disconnect();
self.connected_buffer.disconnect();
self.emit_state();
}
/// set connected laser
pub fn set_connected_laser(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
LaserMachine::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_laser
.set_connected_machine(&machine_identification_unique);
self.connected_laser.reverse_connect();
self.emit_state();
}
/// disconnect laser
pub fn disconnect_laser(&mut self, machine_identification_unique: MachineIdentificationUnique) {
if !matches!(
machine_identification_unique.machine_identification,
LaserMachine::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_laser.reverse_disconnect();
self.connected_laser.disconnect();
self.emit_state();
}
}
+13
View File
@@ -0,0 +1,13 @@
use crate::machines::winder2::mock::Winder2;
use control_core::machines::new::MachineAct;
use std::time::{Duration, Instant};
impl MachineAct for Winder2 {
fn act(&mut self, now: Instant) {
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
}
}
}
+67
View File
@@ -0,0 +1,67 @@
use std::{sync::Arc, time::Instant};
use control_core::{machines::api::MachineApi, socketio::namespace::Namespace};
use serde_json::Value;
use smol::lock::Mutex;
use crate::machines::winder2::api::Mutation;
use super::Winder2;
impl MachineApi for Winder2 {
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::EnableTraverseLaserpointer(enable) => self.set_laser(enable),
Mutation::SetMode(mode) => self.set_mode(&mode.into()),
Mutation::SetTraverseLimitOuter(limit) => self.traverse_set_limit_outer(limit),
Mutation::SetTraverseLimitInner(limit) => self.traverse_set_limit_inner(limit),
Mutation::SetTraverseStepSize(size) => self.traverse_set_step_size(size),
Mutation::SetTraversePadding(padding) => self.traverse_set_padding(padding),
Mutation::GotoTraverseLimitOuter => self.traverse_goto_limit_outer(),
Mutation::GotoTraverseLimitInner => self.traverse_goto_limit_inner(),
Mutation::GotoTraverseHome => self.traverse_goto_home(),
Mutation::SetPullerRegulationMode(regulation) => self.puller_set_regulation(regulation),
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),
Mutation::SetSpoolForward(value) => self.spool_set_forward(value),
Mutation::SetSpoolAdaptiveTensionTarget(value) => {
self.spool_set_adaptive_tension_target(value)
}
Mutation::SetSpoolAdaptiveRadiusLearningRate(value) => {
self.spool_set_adaptive_radius_learning_rate(value)
}
Mutation::SetSpoolAdaptiveMaxSpeedMultiplier(value) => {
self.spool_set_adaptive_max_speed_multiplier(value)
}
Mutation::SetSpoolAdaptiveAccelerationFactor(value) => {
self.spool_set_adaptive_acceleration_factor(value)
}
Mutation::SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(value) => {
self.spool_set_adaptive_deacceleration_urgency_multiplier(value)
}
Mutation::SetSpoolAutomaticRequiredMeters(meters) => {
self.set_spool_automatic_required_meters(meters)
}
Mutation::SetSpoolAutomaticAction(mode) => self.set_spool_automatic_mode(mode),
Mutation::ResetSpoolProgress => self.stop_or_pull_spool_reset(Instant::now()),
Mutation::ZeroTensionArmAngle => self.tension_arm_zero(),
Mutation::SetConnectedMachine(machine_identification_unique) => {
self.set_connected_buffer(machine_identification_unique)
}
Mutation::DisconnectMachine(machine_identification_unique) => {
self.disconnect_buffer(machine_identification_unique)
}
}
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
self.namespace.namespace.clone()
}
}
@@ -0,0 +1,242 @@
use std::time::Instant;
use crate::machines::winder2::Winder2Mode;
use crate::machines::winder2::api::LiveValuesEvent;
use crate::machines::winder2::api::{
ModeState, SpoolAutomaticActionMode, StateEvent, Winder2Events,
};
use crate::machines::winder2::puller_speed_controller::{GearRatio, PullerRegulationMode};
use crate::machines::winder2::spool_speed_controller::SpoolSpeedControllerType;
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH};
use control_core::machines::identification::{MachineIdentification, MachineIdentificationUnique};
use control_core::socketio::event::BuildEvent;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use super::Winder2;
impl Winder2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_WINDER_V1,
};
pub fn stop_or_pull_spool_reset(&mut self, now: Instant) {}
/// Implement Mode
pub fn set_mode(&mut self, mode: &Winder2Mode) {
self.mode_state = ModeState {
mode: mode.clone().into(),
can_wind: true,
};
self.emit_state();
}
/// Implement Traverse
pub fn set_laser(&mut self, value: bool) {
self.traverse_state.laserpointer = value;
self.emit_state();
}
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
self.traverse_state.limit_inner = limit;
self.emit_state();
}
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
self.traverse_state.limit_outer = limit;
self.emit_state();
}
pub fn traverse_set_step_size(&mut self, step_size: f64) {
self.traverse_state.step_size = step_size;
self.emit_state();
}
pub fn traverse_set_padding(&mut self, padding: f64) {
self.traverse_state.padding = padding;
self.emit_state();
}
pub fn traverse_goto_limit_inner(&mut self) {
self.traverse_state.position_in = self.traverse_state.limit_inner;
self.emit_state();
}
pub fn traverse_goto_limit_outer(&mut self) {
self.traverse_state.position_in = self.traverse_state.limit_outer;
self.emit_state();
}
pub fn traverse_goto_home(&mut self) {
self.traverse_state.position_in = 0.0;
self.emit_state();
}
pub fn emit_live_values(&mut self) {
let event = LiveValuesEvent {
traverse_position: Some(0.0),
puller_speed: 0.0,
spool_rpm: 0.0,
tension_arm_angle: 0.0,
spool_progress: 0.0,
};
let event = event.build();
self.namespace.emit(Winder2Events::LiveValues(event));
}
pub fn build_state_event(&mut self) -> StateEvent {
StateEvent {
is_default_state: self.is_default_state,
traverse_state: self.traverse_state.clone(),
puller_state: self.puller_state.clone(),
spool_automatic_action_state: self.spool_automatic_action_state.clone(),
mode_state: self.mode_state.clone(),
tension_arm_state: self.tension_arm_state.clone(),
spool_speed_controller_state: self.spool_speed_controller_state.clone(),
connected_machine_state: self.connected_machine_state.clone(),
}
}
pub fn emit_state(&mut self) {
let state = self.build_state_event();
let event = state.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) {
self.mode_state = ModeState {
mode: crate::machines::winder2::api::Mode::Standby,
can_wind: true,
};
self.emit_state();
}
/// Implement Tension Arm
pub fn tension_arm_zero(&mut self) {
self.tension_arm_state.zeroed = true;
self.emit_state();
}
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
self.spool_automatic_action_state.spool_required_meters = meters;
self.emit_state();
}
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
self.spool_automatic_action_state
.spool_automatic_action_mode = mode;
self.emit_state();
}
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
self.puller_state.regulation = puller_regulation_mode;
self.emit_state();
}
/// Set target speed in m/min
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
self.puller_state.target_speed = target_speed;
self.emit_state();
}
/// Set target diameter in mm
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
self.puller_state.target_diameter = target_diameter;
self.emit_state();
}
/// Set forward direction
pub fn puller_set_forward(&mut self, forward: bool) {
self.puller_state.forward = forward;
self.emit_state();
}
/// Set gear ratio for winding speed
pub fn puller_set_gear_ratio(&mut self, gear_ratio: GearRatio) {
self.puller_state.gear_ratio = gear_ratio;
self.emit_state();
}
// Spool Speed Controller API methods
pub fn spool_set_regulation_mode(&mut self, regulation_mode: SpoolSpeedControllerType) {
self.spool_speed_controller_state.regulation_mode = 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) {
self.spool_speed_controller_state.minmax_min_speed = min_speed_rpm;
self.emit_state();
}
/// Set maximum speed for minmax mode in RPM
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
self.spool_speed_controller_state.minmax_max_speed = max_speed_rpm;
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_state.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_state
.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_state
.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_state
.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_state
.adaptive_deacceleration_urgency_multiplier = deacceleration_urgency_multiplier;
self.emit_state();
}
/// Set forward rotation direction
pub fn spool_set_forward(&mut self, forward: bool) {
self.spool_speed_controller_state.forward = forward;
self.emit_state();
}
/// implement machine connection
/// set connected buffer
pub fn set_connected_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
self.emit_state();
}
/// disconnect buffer
pub fn disconnect_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
self.emit_state();
}
}
+44
View File
@@ -0,0 +1,44 @@
use std::time::Instant;
use control_core::machines::{
connection::MachineCrossConnectionState, identification::MachineIdentificationUnique,
};
use control_core_derive::Machine;
pub mod act;
pub mod api;
pub mod mock_emit;
pub mod new;
use crate::machines::winder2::api::Winder2Namespace;
use super::api::{
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState, TensionArmState,
TraverseState,
};
#[derive(Debug, Machine)]
pub struct Winder2 {
machine_identification_unique: MachineIdentificationUnique,
namespace: Winder2Namespace,
last_measurement_emit: Instant,
pub is_default_state: bool,
/// traverse state
pub traverse_state: TraverseState,
/// puller state
pub puller_state: PullerState,
/// spool automatic action state and progress
pub spool_automatic_action_state: SpoolAutomaticActionState,
/// mode state
pub mode_state: ModeState,
/// tension arm state
pub tension_arm_state: TensionArmState,
/// spool speed controller state
pub spool_speed_controller_state: SpoolSpeedControllerState,
/// connected machine state
pub connected_machine_state: MachineCrossConnectionState,
}
impl std::fmt::Display for Winder2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Winder2")
}
}
+53
View File
@@ -0,0 +1,53 @@
use crate::machines::winder2::{
api::{
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState,
TensionArmState, TraverseState, Winder2Namespace,
},
mock::Winder2,
};
use control_core::machines::connection::MachineCrossConnectionState;
impl control_core::machines::new::MachineNewTrait for Winder2 {
fn new(
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
) -> Result<Self, anyhow::Error>
where
Self: Sized,
{
// Mock machine can work with either Serial or Ethercat hardware
// For the mock machine, we don't need to actually use the hardware
// We just validate that we have the expected hardware type
match params.hardware {
control_core::machines::new::MachineNewHardware::Serial(_) => {
// For serial mode, we could potentially use the serial device if needed
// but for a mock machine, we'll just note it and proceed
}
control_core::machines::new::MachineNewHardware::Ethercat(_) => {
// For ethercat mode, we could potentially use the ethercat devices
// but for a mock machine, we'll just note it and proceed
}
}
let now = std::time::Instant::now();
let mut extruder_mock_machine = Self {
machine_identification_unique: params.get_machine_identification_unique(),
namespace: Winder2Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: now,
is_default_state: true,
traverse_state: TraverseState::default(),
puller_state: PullerState::default(),
spool_automatic_action_state: SpoolAutomaticActionState::default(),
mode_state: ModeState::default(),
tension_arm_state: TensionArmState::default(),
spool_speed_controller_state: SpoolSpeedControllerState::default(),
connected_machine_state: MachineCrossConnectionState::default(),
};
extruder_mock_machine.emit_state();
Ok(extruder_mock_machine)
}
}
+74 -29
View File
@@ -11,37 +11,47 @@ pub mod spool_speed_controller;
pub mod tension_arm;
pub mod traverse_controller;
use std::{fmt::Debug, sync::Weak, time::Instant};
#[cfg(feature = "mock-machine")]
pub mod mock;
use api::{SpoolAutomaticActionMode, Winder2Namespace};
use control_core::{
converters::angular_step_converter::AngularStepConverter,
machines::{
connection::{CrossConnectableMachine, MachineCrossConnection},
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
};
use control_core_derive::Machine;
use ethercat_hal::io::{
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1,
};
use puller_speed_controller::PullerSpeedController;
use smol::lock::RwLock;
use spool_speed_controller::SpoolSpeedController;
use tension_arm::TensionArm;
use traverse_controller::TraverseController;
use uom::{
ConstZero,
si::{
f64::Length,
length::{meter, millimeter},
velocity::meter_per_second,
},
};
#[cfg(feature = "mock-machine")]
mod winder2_imports {
pub use super::api::SpoolAutomaticActionMode;
pub use std::time::Instant;
pub use uom::si::f64::Length;
}
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH, buffer1::BufferV1};
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::api::SpoolAutomaticActionMode;
pub use super::api::Winder2Namespace;
pub use super::puller_speed_controller::PullerSpeedController;
pub use super::spool_speed_controller::SpoolSpeedController;
pub use super::tension_arm::TensionArm;
pub use super::traverse_controller::TraverseController;
pub use control_core::{
converters::angular_step_converter::AngularStepConverter,
machines::{
connection::{CrossConnectableMachine, MachineCrossConnection},
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
};
pub use control_core_derive::Machine;
pub use ethercat_hal::io::{
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1,
};
pub use smol::lock::RwLock;
pub use std::{fmt::Debug, sync::Weak, time::Instant};
pub use uom::si::f64::Length;
pub use uom::ConstZero;
pub use uom::si::{length::meter, length::millimeter, velocity::meter_per_second};
pub use crate::machines::{buffer1::BufferV1, laser::LaserMachine, winder2::api::PiSettings, MACHINE_WINDER_V1, VENDOR_QITECH};
}
pub use winder2_imports::*;
#[derive(Debug)]
pub struct SpoolAutomaticAction {
@@ -51,6 +61,18 @@ pub struct SpoolAutomaticAction {
pub mode: SpoolAutomaticActionMode,
}
impl Default for SpoolAutomaticAction {
fn default() -> Self {
SpoolAutomaticAction {
progress: Length::new::<uom::si::length::meter>(0.0),
progress_last_check: Instant::now(),
target_length: Length::new::<uom::si::length::meter>(0.0),
mode: SpoolAutomaticActionMode::default(),
}
}
}
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug, Machine)]
pub struct Winder2 {
// drivers
@@ -74,6 +96,7 @@ pub struct Winder2 {
// connected machines
pub connected_buffer: MachineCrossConnection<Winder2, BufferV1>,
pub connected_laser: MachineCrossConnection<Winder2, LaserMachine>,
// mode
pub mode: Winder2Mode,
@@ -96,12 +119,20 @@ pub struct Winder2 {
emitted_default_state: bool,
}
#[cfg(not(feature = "mock-machine"))]
impl CrossConnectableMachine<Winder2, BufferV1> for Winder2 {
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<Winder2, BufferV1> {
&mut self.connected_buffer
}
}
#[cfg(not(feature = "mock-machine"))]
impl CrossConnectableMachine<Winder2, LaserMachine> for Winder2 {
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<Winder2, LaserMachine> {
&mut self.connected_laser
}
}
impl Winder2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
@@ -277,6 +308,7 @@ impl Winder2 {
pub fn calculate_spool_auto_progress_(&mut self, now: Instant) {
// Calculate time elapsed since last progress check (in minutes)
let dt = now
.duration_since(self.spool_automatic_action.progress_last_check)
.as_secs_f64();
@@ -306,6 +338,17 @@ impl Winder2 {
}
}
impl Winder2 {
pub fn configure_pi_controller(&mut self, settings: PiSettings) {
// Implement pid to controll speed of winder
self.puller_speed_controller
.p_dead_controller
.configure(settings.kp, settings.ki);
self.emit_state();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Winder2Mode {
Standby,
@@ -368,12 +411,14 @@ impl From<Winder2Mode> for PullerMode {
}
}
#[cfg(not(feature = "mock-machine"))]
impl std::fmt::Display for Winder2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Winder2")
}
}
#[cfg(not(feature = "mock-machine"))]
#[cfg(test)]
mod tests {
use super::*;
+59 -50
View File
@@ -1,50 +1,63 @@
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::api::Winder2Namespace;
pub use super::super::tension_arm::TensionArm;
pub use super::super::{Winder2, Winder2Mode};
pub use crate::machines::winder2::puller_speed_controller::PullerSpeedController;
pub use crate::machines::winder2::spool_speed_controller::SpoolSpeedController;
pub use crate::machines::winder2::traverse_controller::TraverseController;
pub use anyhow::Error;
pub use control_core::converters::angular_step_converter::AngularStepConverter;
pub use control_core::converters::linear_step_converter::LinearStepConverter;
pub use control_core::machines::connection::MachineCrossConnection;
pub use control_core::machines::new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
use super::api::Winder2Namespace;
use super::tension_arm::TensionArm;
use super::{Winder2, Winder2Mode};
pub use control_core::uom_extensions::velocity::meter_per_minute;
pub use ethercat_hal::coe::ConfigurableDevice;
pub use ethercat_hal::devices::ek1100::EK1100;
pub use ethercat_hal::devices::el2002::{EL2002, EL2002_IDENTITY_B, EL2002Port};
pub use ethercat_hal::devices::el7031::coe::EL7031Configuration;
pub use ethercat_hal::devices::el7031::pdo::EL7031PredefinedPdoAssignment;
pub use ethercat_hal::devices::el7031::{
EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B, EL7031DigitalInputPort, EL7031StepperPort,
};
pub use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
pub use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
pub use ethercat_hal::devices::el7031_0030::{
self, EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030AnalogInputPort,
EL7031_0030StepperPort,
};
pub use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration;
pub use ethercat_hal::devices::el7041_0052::{
EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port,
};
pub use ethercat_hal::devices::{ek1100::EK1100_IDENTITY_A, el2002::EL2002_IDENTITY_A};
pub use ethercat_hal::io::analog_input::AnalogInput;
pub use ethercat_hal::io::digital_input::DigitalInput;
pub use ethercat_hal::io::digital_output::DigitalOutput;
pub use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
pub use ethercat_hal::shared_config;
pub use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
pub use std::time::Instant;
pub use uom::ConstZero;
pub use uom::si::f64::{Length, Velocity};
pub use uom::si::length::{centimeter, meter, millimeter};
}
#[cfg(not(feature = "mock-machine"))]
pub use winder2_imports::*;
#[cfg(not(feature = "mock-machine"))]
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::connection::MachineCrossConnection;
use control_core::machines::new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
use control_core::uom_extensions::velocity::meter_per_minute;
use ethercat_hal::coe::ConfigurableDevice;
use ethercat_hal::devices::ek1100::EK1100;
use ethercat_hal::devices::el2002::{EL2002, EL2002_IDENTITY_B, EL2002Port};
use ethercat_hal::devices::el7031::coe::EL7031Configuration;
use ethercat_hal::devices::el7031::pdo::EL7031PredefinedPdoAssignment;
use ethercat_hal::devices::el7031::{
EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B, EL7031DigitalInputPort, EL7031StepperPort,
};
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, EL7031_0030AnalogInputPort, EL7031_0030StepperPort,
};
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::{ek1100::EK1100_IDENTITY_A, el2002::EL2002_IDENTITY_A};
use ethercat_hal::io::analog_input::AnalogInput;
use ethercat_hal::io::digital_input::DigitalInput;
use ethercat_hal::io::digital_output::DigitalOutput;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use ethercat_hal::shared_config;
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
use uom::ConstZero;
use uom::si::f64::{Length, Velocity};
use uom::si::length::{centimeter, meter, millimeter};
#[cfg(not(feature = "mock-machine"))]
impl MachineNewTrait for Winder2 {
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)?;
@@ -177,14 +190,6 @@ impl MachineNewTrait for Winder2 {
let mode = Winder2Mode::Standby;
let machine_id = params
.device_group
.first()
.expect("device group must have at least one device")
.device_machine_identification
.machine_identification_unique
.clone();
let mut new = Self {
traverse: StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
traverse_end_stop: DigitalInput::new(el7031, EL7031DigitalInputPort::DI1),
@@ -229,11 +234,15 @@ impl MachineNewTrait for Winder2 {
mode: super::api::SpoolAutomaticActionMode::NoAction,
},
machine_manager: params.machine_manager.clone(),
machine_identification_unique: machine_id,
machine_identification_unique: params.get_machine_identification_unique(),
connected_buffer: MachineCrossConnection::new(
params.machine_manager.clone(),
&params.get_machine_identification_unique(),
),
connected_laser: MachineCrossConnection::new(
params.machine_manager.clone(),
&params.get_machine_identification_unique(),
),
};
// initalize events
@@ -1,7 +1,12 @@
use std::time::Instant;
use std::{
time::{Duration, Instant},
};
use control_core::{
controllers::second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController,
controllers::{
deadtime_pi_controller::TimeAgnosticDeadTimePiController,
second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController,
},
converters::linear_step_converter::LinearStepConverter,
uom_extensions::{
acceleration::meter_per_minute_per_second, jerk::meter_per_minute_per_second_squared,
@@ -11,7 +16,11 @@ use control_core::{
use serde::{Deserialize, Serialize};
use uom::{
ConstZero,
si::f64::{Acceleration, AngularVelocity, Jerk, Length, Velocity},
si::{
f64::{Acceleration, AngularVelocity, Jerk, Length, Velocity},
length::{meter, millimeter},
time::second,
},
};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
@@ -43,6 +52,7 @@ pub struct PullerSpeedController {
enabled: bool,
pub target_speed: Velocity,
pub target_diameter: Length,
pub measured_diameter: Length,
pub regulation_mode: PullerRegulationMode,
/// Forward rotation direction. If false, applies negative sign to speed
pub forward: bool,
@@ -53,6 +63,7 @@ pub struct PullerSpeedController {
/// Converter for linear to angular transformations
pub converter: LinearStepConverter,
pub last_speed: Velocity,
pub p_dead_controller: TimeAgnosticDeadTimePiController,
}
impl PullerSpeedController {
@@ -69,6 +80,7 @@ impl PullerSpeedController {
enabled: false,
target_speed,
target_diameter,
measured_diameter: Length::ZERO,
regulation_mode: PullerRegulationMode::Speed,
forward: true,
gear_ratio: GearRatio::default(),
@@ -79,6 +91,11 @@ impl PullerSpeedController {
),
converter,
last_speed: Velocity::ZERO,
p_dead_controller: TimeAgnosticDeadTimePiController::new(
0.1 / 10000.0,
0.0,
Duration::ZERO,
),
}
}
@@ -94,6 +111,10 @@ impl PullerSpeedController {
self.target_diameter = target;
}
pub fn set_measured_diameter(&mut self, target: f64) {
self.measured_diameter = Length::new::<millimeter>(target);
}
pub const fn set_regulation_mode(&mut self, regulation: PullerRegulationMode) {
self.regulation_mode = regulation;
}
@@ -111,12 +132,27 @@ impl PullerSpeedController {
}
fn update_speed(&mut self, t: Instant) -> Velocity {
let base_speed = match self.enabled {
true => match self.regulation_mode {
PullerRegulationMode::Speed => self.target_speed,
PullerRegulationMode::Diameter => unimplemented!(),
},
false => Velocity::ZERO,
if !self.enabled {
return Velocity::ZERO;
}
let base_speed = match self.regulation_mode {
PullerRegulationMode::Speed => {
let directional_target = if self.forward {
self.target_speed
} else {
-self.target_speed
};
self.acceleration_controller.update(directional_target, t)
}
PullerRegulationMode::Diameter => {
let diameter_speed = self.speed_from_diameter(t);
if self.forward {
diameter_speed
} else {
-diameter_speed
}
}
};
// Apply gear ratio multiplier
@@ -130,6 +166,49 @@ impl PullerSpeedController {
speed
}
fn speed_from_diameter(&mut self, now: Instant) -> Velocity {
// get current error
let error =
self.measured_diameter.get::<millimeter>() - self.target_diameter.get::<millimeter>();
// calculate/set deadtime based of speed and distance
let deadtime = Self::calc_deadtime(self.last_speed, Length::new::<meter>(2.0));
self.p_dead_controller.set_dead(deadtime);
// get speed change from p controller
let speed_change = self.p_dead_controller.update(error, now);
// apply speed change to target speed
let next_speed = self.last_speed + Velocity::new::<meter_per_minute>(speed_change);
// clamp the speed to 0 - 50 for safety
Self::clamp_speed(
next_speed,
Velocity::new::<meter_per_minute>(0.0),
Velocity::new::<meter_per_minute>(50.0),
)
}
fn calc_deadtime(speed: Velocity, distance: Length) -> Duration {
// check if speed is 0 => This should not happen
if speed <= Velocity::ZERO {
return Duration::from_secs(180);
}
// get the duration based of distance and speed
let secs = (distance / speed).get::<second>().max(0.0);
Duration::from_secs_f64(secs)
}
fn clamp_speed(speed: Velocity, min: Velocity, max: Velocity) -> Velocity {
if speed < min {
min
} else if speed > max {
max
} else {
speed
}
}
pub fn speed_to_angular_velocity(&self, speed: Velocity) -> AngularVelocity {
// Use the converter to transform from linear velocity to angular velocity
self.converter.velocity_to_angular_velocity(speed)
@@ -150,8 +229,9 @@ impl PullerSpeedController {
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum PullerRegulationMode {
#[default]
Speed,
Diameter,
}
@@ -10,8 +10,9 @@ use serde::{Deserialize, Serialize};
use std::time::Instant;
use uom::si::f64::AngularVelocity;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum SpoolSpeedControllerType {
#[default]
Adaptive,
MinMax,
}
+41 -76
View File
@@ -1,106 +1,71 @@
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
use app_state::AppState;
use r#loop::init_loop;
use futures::{FutureExt, select};
#[cfg(feature = "mock-machine")]
use mock::init::init_mock;
use rest::init::init_api;
#[cfg(not(feature = "mock-machine"))]
use serial::init::init_serial;
use std::sync::Arc;
#[cfg(feature = "mock-machine")]
pub mod mock;
use crate::panic::init_panic_handling;
use crate::socketio::queue::init_socketio_queue;
use app_state::AppState;
use ethercat::{
ethercat_discovery_info::{send_ethercat_discovering, send_ethercat_found},
init::start_interface_discovery,
};
use r#loop::start_loop_thread;
use rest::init::start_api_thread;
use serial::init::{handle_serial_device_hotplug, start_serial_discovery};
use socketio::queue::start_socketio_queue;
use std::sync::Arc;
pub mod app_state;
pub mod ethercat;
pub mod logging;
pub mod r#loop;
pub mod machines;
#[cfg(feature = "mock-machine")]
pub mod mock;
pub mod panic;
pub mod performance_metrics;
pub mod rest;
pub mod serial;
pub mod socketio;
use crate::ethercat::init::init_ethercat;
fn main() {
logging::init_tracing();
tracing::info!("Tracing initialized successfully");
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({
move || {
#[cfg(feature = "dhat-heap")]
init_dhat_heap_profiling();
let _ = start_api_thread(app_state.clone());
let mut socketio_fut = start_socketio_queue(app_state.clone()).fuse();
let mut ethercat_fut = start_interface_discovery().fuse();
let mut serial_fut = start_serial_discovery().fuse();
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(app_state.clone()).expect("Failed to initialize Serial");
#[cfg(not(feature = "mock-machine"))]
init_ethercat(app_state).expect("Failed to initialize EtherCAT");
}
})
.expect("Failed to spawn init thread");
// Wait for init thread to complete
init_thread.join().expect("Init thread panicked");
// Keep the main thread alive indefinitely
// The program should only exit via panic handling or external signals
loop {
// better way to "sleep", uses basically
std::thread::park();
}
}
#[cfg(feature = "dhat-heap")]
fn init_dhat_heap_profiling() {
use std::{process::exit, time::Duration};
let profiler = dhat::Profiler::new_heap();
smol::block_on(async { send_ethercat_discovering(app_state.clone()).await });
smol::block_on(async {
let dhat_analysis_time = std::time::Duration::from_secs(60);
let dhat_analysis_start = std::time::Instant::now();
tracing::info!(
"Starting dhat heap profiler for {} seconds",
dhat_analysis_time.as_secs()
);
loop {
use std::time::Duration;
smol::Timer::after(Duration::from_secs(1)).await;
// lets the async runtime decide which future to run next
select! {
res = ethercat_fut => {
tracing::info!("EtherCAT task finished: {:?}", res);
match res {
Ok(interface) =>
{
send_ethercat_found(app_state.clone(), &interface).await;
let _ = start_loop_thread(&interface, app_state.clone());
},
Err(_) => (),
};
// if `dhat-heap` is enabled, we will analyze the heap for 60 seconds and then exit
if dhat_analysis_start.elapsed() > dhat_analysis_time {
drop(profiler);
exit(0)
},
res = serial_fut => {
let _ = handle_serial_device_hotplug(app_state.clone(),res).await;
serial_fut = start_serial_discovery().fuse();
},
res = socketio_fut => {
// In theory it should never finish
tracing::warn!("SocketIO task finished: {:?}", res);
},
}
}
})
});
}
+35 -3
View File
@@ -1,5 +1,6 @@
#[cfg(feature = "mock-machine")]
use crate::serial::devices::extruder_mock::ExtruderMockSerialDevice;
use crate::serial::devices::winder_mock::WinderMockSerialDevice;
#[cfg(feature = "mock-machine")]
use {
crate::app_state::AppState,
@@ -12,16 +13,15 @@ use {
},
std::sync::Arc,
};
#[cfg(feature = "mock-machine")]
pub fn init_mock(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
// For mock devices, we need to manually create and add them to the machine manager
// since they won't be detected by the serial detection loop
return smol::block_on(async {
// Create a mock serial device manually
let (device_thread_panic_tx, _device_thread_panic_rx) = smol::channel::unbounded();
let serial_params = SerialDeviceNewParams {
path: "/dev/mock-serial".to_string(),
device_thread_panic_tx,
};
// Create the mock serial device
@@ -57,7 +57,39 @@ pub fn init_mock(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
}
};
match ExtruderMockSerialDevice::new_serial(&serial_params) {
let _ = match ExtruderMockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
{
let mut machine_guard = app_state.machines.write().await;
machine_guard.add_serial_device(
&device_identification,
mock_serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
);
}
// Notify clients via socketio about the new machine
let app_state_event = app_state.clone();
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
Ok::<(), anyhow::Error>(())
}
Err(e) => {
tracing::error!("Failed to create extruder mock serial device: {}", e);
return Err(e);
}
};
match WinderMockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
{
+2 -2
View File
@@ -10,8 +10,8 @@ const METRICS_LOG_INTERVAL_SECS: u64 = 30; // Log every 30 seconds
pub struct EthercatPerformanceMetrics {
txrx_times: VecDeque<Duration>,
loop_times: VecDeque<Duration>,
last_loop_start: Option<Instant>,
last_log_time: Instant,
pub last_loop_start: Option<Instant>,
pub last_log_time: Instant,
}
impl Default for EthercatPerformanceMetrics {
+44 -52
View File
@@ -1,65 +1,57 @@
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::socketio::init::init_socketio;
use anyhow::anyhow;
use anyhow::Result;
use axum::routing::post;
use std::sync::Arc;
use std::thread::JoinHandle;
use std::thread;
use tower_http::cors::CorsLayer;
use tower_http::trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::Level;
pub fn init_api(app_state: Arc<AppState>) -> Result<JoinHandle<()>, anyhow::Error> {
std::thread::Builder::new()
.name("api".to_string())
.spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.expect("Failed to create runtime");
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::socketio::init::init_socketio;
rt.block_on(async {
// allow all CORS requests
let cors = CorsLayer::permissive();
async fn init_api(app_state: Arc<AppState>) -> Result<()> {
let cors = CorsLayer::permissive();
let socketio_layer = init_socketio(&app_state).await;
// creat socketio layer
let socketio_layer = init_socketio(&app_state).await;
let trace_layer = TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::DEBUG))
.on_request(DefaultOnRequest::new().level(Level::TRACE))
.on_response(DefaultOnResponse::new().level(Level::TRACE));
// Create a more detailed trace layer for HTTP requests
let trace_layer = TraceLayer::new_for_http()
.make_span_with(
DefaultMakeSpan::new()
.level(Level::DEBUG)
.include_headers(false),
)
.on_request(DefaultOnRequest::new().level(Level::TRACE))
.on_response(
DefaultOnResponse::new()
.level(Level::TRACE)
.include_headers(false),
);
let app = axum::Router::new()
.route(
"/api/v1/write_machine_device_identification",
post(post_write_machine_device_identification),
)
.route("/api/v1/machine/mutate", post(post_machine_mutate))
.layer(socketio_layer)
.layer(cors)
.layer(trace_layer)
.with_state(app_state);
// make axum server to serve the data on /ethercat
let app = axum::Router::new()
.route(
"/api/v1/write_machine_device_identification",
post(post_write_machine_device_identification),
)
.route("/api/v1/machine/mutate", post(post_machine_mutate))
.layer(socketio_layer)
.layer(cors)
.layer(trace_layer)
.with_state(app_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001")
.await
.expect("Failed to bind to port 3001");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001")
.await
.expect("Failed to bind to port 3001");
tracing::info!("HTTP server running on 0.0.0.0:3001");
tracing::info!("Starting HTTP server on 0.0.0.0:3001");
axum::serve(listener, app).await.expect("Failed to serve");
});
})
.map_err(|e| anyhow!("Failed to spawn API thread: {}", e))
axum::serve(listener, app)
.await
.map_err(|e| anyhow::anyhow!("Server error: {}", e))
}
/// Starts the API server in its own thread with a single-threaded Tokio runtime
pub fn start_api_thread(app_state: Arc<AppState>) -> std::thread::JoinHandle<()> {
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
if let Err(err) = rt.block_on(init_api(app_state)) {
eprintln!("API server exited with error: {err:?}");
}
})
}
+3 -20
View File
@@ -17,10 +17,7 @@ use control_core::{
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
modbus::{self, ModbusRequest, ModbusResponse},
serial::{
SerialDevice, SerialDeviceNew, SerialDeviceNewParams, panic::send_serial_device_panic,
serial_detection::SerialDeviceRemoval,
},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
};
use serialport::SerialPort;
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, StopBits};
@@ -125,27 +122,13 @@ impl SerialDeviceNew for Laser {
path: params.path.clone(),
}));
// Spawn the device thread
let device_thread_panic_tx = params.device_thread_panic_tx.clone();
//// Spawn the device thread
let _self_clone = _self.clone();
let path = params.path.clone();
thread::Builder::new()
.name("laser".to_owned())
.spawn(move || {
send_serial_device_panic(path.clone(), device_thread_panic_tx.clone());
smol::block_on(async {
let process_result = Self::process(_self_clone).await;
let removal = match process_result {
Ok(_) => SerialDeviceRemoval::Disconnect(path),
Err(e) => SerialDeviceRemoval::Error(path, e),
};
// if the task exists we want to remove the device
device_thread_panic_tx
.send(removal)
.await
.expect("Failed to send device removal signal");
let _ = Self::process(_self_clone).await;
});
})?;
+2
View File
@@ -3,3 +3,5 @@ pub mod extruder_mock;
pub mod laser;
#[cfg(feature = "mock-machine")]
pub mod mock;
#[cfg(feature = "mock-machine")]
pub mod winder_mock;
@@ -0,0 +1,59 @@
use std::sync::Arc;
use control_core::{
helpers::hashing::{byte_folding_u16, hash_djb2},
machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
};
use smol::lock::RwLock;
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH};
/// Mock serial device for testing MockMachine
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
#[derive(Debug)]
pub struct WinderMockSerialDevice {
pub path: String,
}
impl SerialDevice for WinderMockSerialDevice {}
impl SerialDeviceNew for WinderMockSerialDevice {
fn new_serial(
params: &SerialDeviceNewParams,
) -> Result<(DeviceIdentification, Arc<RwLock<Self>>), anyhow::Error>
where
Self: Sized,
{
// Generate a unique serial number based on the path
let hash = hash_djb2(params.path.as_bytes());
let serial = byte_folding_u16(&hash.to_le_bytes());
let device_identification = DeviceIdentification {
device_machine_identification: Some(DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_WINDER_V1,
},
serial,
},
role: 0,
}),
device_hardware_identification: DeviceHardwareIdentification::Serial(
DeviceHardwareIdentificationSerial {
path: params.path.clone(),
},
),
};
let mock_serial_device = Arc::new(RwLock::new(WinderMockSerialDevice {
path: params.path.clone(),
}));
Ok((device_identification, mock_serial_device))
}
}
+135 -70
View File
@@ -1,74 +1,139 @@
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 std::{sync::Arc, thread, time::Duration};
use control_core::{
machines::identification::MachineIdentificationUnique,
serial::{
SerialDeviceIdentification, SerialDeviceNew, SerialDeviceNewParams,
serial_detection::SerialDetection,
},
socketio::namespace::NamespaceCacheingLogic,
};
use serialport::UsbPortInfo;
use smol::Task;
use std::{collections::HashMap, sync::Arc, time::Duration};
pub fn init_serial(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
let app_state_clone = app_state.clone();
use crate::{
app_state::AppState,
machines::{laser::LaserMachine, registry::MACHINE_REGISTRY},
socketio::main_namespace::{MainNamespaceEvents, machines_event::MachinesEventBuilder},
};
thread::Builder::new()
.name("SerialDetectionThread".to_owned())
.spawn(move || {
let app_state_clone = app_state_clone;
smol::block_on(async move {
let rt = smol::LocalExecutor::new();
rt.run(async {
loop {
let result = {
let mut serial_setup_guard = app_state_clone.serial_setup.write().await;
use super::devices::laser::Laser;
let mut port_result =
serial_setup_guard.serial_detection.check_ports().await;
let mut removed_signals = serial_setup_guard
.serial_detection
.check_remove_signals()
.await;
port_result.removed.append(&mut removed_signals);
port_result
};
if !result.added.is_empty() || !result.removed.is_empty() {
// sync serial device discovery to machine manager
{
let mut machine_guard = app_state_clone.machines.write().await;
// turn added devices into machines
for (device_identifiaction, device) in result.added {
machine_guard.add_serial_device(
&device_identifiaction,
device.clone(),
&MACHINE_REGISTRY,
app_state_clone.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
)
}
for device_identification in result.removed {
machine_guard.remove_serial_device(&device_identification)
}
}
// Notify client via socketio
let app_state_event = app_state.clone();
smol::block_on(async {
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
});
}
smol::Timer::after(Duration::from_millis(300)).await;
}
})
.await;
});
})
.expect("Failed to spawn SerialTxRxThread");
Ok(())
// Async function that runs until at least one serial device is found.
pub async fn find_serial() -> Option<HashMap<String, UsbPortInfo>> {
smol::Timer::after(Duration::from_secs(1)).await;
let devices = SerialDetection::detect_devices();
if !devices.is_empty() {
return Some(devices);
} else {
return None;
}
}
// Returns a smol::Task that resolves when atleast one device is found.
pub fn start_serial_discovery() -> Task<Option<HashMap<String, UsbPortInfo>>> {
smol::spawn(find_serial())
}
// This function handles a disconnection and or reconnection of Laser
// You can easily rewrite it when we have more serialdevices
// TODO: Add A static list of known SeriadeviceIdents
pub async fn handle_serial_device_hotplug(
app_state: Arc<AppState>,
map: Option<HashMap<String, UsbPortInfo>>,
) {
let laser_ident = SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
};
let laser = match map {
Some(map) => SerialDetection::get_path_by_id(laser_ident, map),
None => None,
};
let mut unique_ident: Option<MachineIdentificationUnique> = None;
{
let machines = app_state.machines.read_arc_blocking();
for (id, _) in machines.iter() {
if id.machine_identification == LaserMachine::MACHINE_IDENTIFICATION {
unique_ident = Some(id.clone());
break;
}
}
}
// Machine isnt connected, so add it back
if laser.is_some() && unique_ident.is_none() {
let serial_params = SerialDeviceNewParams {
path: laser.unwrap(),
};
let _ = match Laser::new_serial(&serial_params) {
Ok((device_identification, serial_device)) => {
{
let mut machines = app_state.machines.write().await;
machines.add_serial_device(
&device_identification,
serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
);
}
let app_state_event = app_state.clone();
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
}
_ => (),
};
} else if laser.is_none() && unique_ident.is_some() {
let serial_params = SerialDeviceNewParams {
path: "".to_string(),
};
let _ = match Laser::new_serial(&serial_params) {
Ok((device_identification, _)) => {
app_state
.machines
.write()
.await
.remove_serial_device(&device_identification);
drop(app_state.machines.clone());
app_state
.machines
.write_arc()
.await
.serial_machines
.remove(&unique_ident.clone().unwrap());
app_state
.machines
.write_arc()
.await
.ethercat_machines
.remove(&unique_ident.clone().unwrap());
drop(app_state.machines.clone());
let main_namespace = &mut app_state
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
}
_ => (),
};
}
}
+29 -47
View File
@@ -1,6 +1,7 @@
use crate::app_state::AppState;
use smol::Task;
use std::{sync::Arc, time::Instant};
use tracing::{debug, error, info, instrument, trace};
use tracing::{debug, info, instrument, trace};
/// Send a single event with retry logic
#[instrument(skip_all)]
@@ -58,55 +59,36 @@ async fn send_event_with_retry(
}
}
pub fn init_socketio_queue(app_state: Arc<AppState>) {
std::thread::Builder::new()
.name("socketio-queue".to_string())
.spawn(move || {
async fn socketio_queue_worker(app_state: Arc<AppState>) {
tracing::info!("SocketIO global queue listener started");
let mut event_count = 0;
let mut batch_start = Instant::now();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.expect("Failed to create runtime");
while let Ok((socket, event)) = app_state.socketio_setup.socket_queue_rx.recv().await {
event_count += 1;
rt.block_on(async {
info!("SocketIO global queue listener started");
send_event_with_retry(&socket, &event).await;
let mut event_count = 0;
let mut batch_start = Instant::now();
if batch_start.elapsed().as_secs() >= 5 {
let elapsed = batch_start.elapsed();
if event_count > 0 {
debug!(
"[{}::socketio_queue_worker] Processed {} events in {:.2?} ({:.1} events/s)",
module_path!(),
event_count,
elapsed,
event_count as f64 / elapsed.as_secs_f64(),
);
}
event_count = 0;
batch_start = Instant::now();
}
}
loop {
match app_state.socketio_setup.socket_queue_rx.recv().await {
Ok((socket, event)) => {
event_count += 1;
info!("SocketIO global queue listener stopped");
}
// Handle the received message with retry logic
send_event_with_retry(&socket, &event).await;
// Log batch statistics every 5 seconds
if batch_start.elapsed().as_secs() >= 5 {
let elapsed = batch_start.elapsed();
if event_count > 0 {
debug!(
"[{}::init_socketio_queue] Processed {} events in {:.2?} ({:.1} events/s)",
module_path!(),
event_count,
elapsed,
event_count as f64 / elapsed.as_secs_f64(),
);
}
event_count = 0;
batch_start = Instant::now();
}
}
Err(e) => {
error!(error = %e, "Error receiving from global socketio queue");
info!("SocketIO global queue listener stopping");
break;
}
}
}
});
})
.expect("Failed to spawn socketio queue thread");
/// Returns a spawned async task handling the Socket.IO event queue
pub fn start_socketio_queue(app_state: Arc<AppState>) -> Task<()> {
smol::spawn(socketio_queue_worker(app_state))
}