Compare commits
107
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
252fb31d65 | ||
|
|
bf12a25af4 | ||
|
|
c66f046a9b | ||
|
|
a7b1158f8d | ||
|
|
be87b377ba | ||
|
|
111664185e | ||
|
|
29ccaea7f2 | ||
|
|
8402199601 | ||
|
|
b7399c8d82 | ||
|
|
db71805912 | ||
|
|
3fd1b09589 | ||
|
|
c806f59df5 | ||
|
|
a5c0d75f33 | ||
|
|
9629652d4f | ||
|
|
5ee0b7e61b | ||
|
|
bb975081e8 | ||
|
|
6629055f9f | ||
|
|
3b10790146 | ||
|
|
cb6a954e3b | ||
|
|
bbf58e6b18 | ||
|
|
9af2364528 | ||
|
|
7d774b0025 | ||
|
|
94655093cc | ||
|
|
5b57273153 | ||
|
|
02951794f1 | ||
|
|
d1f96cc816 | ||
|
|
673b85420c | ||
|
|
493cb39396 | ||
|
|
34d6e85ec3 | ||
|
|
dd79586b13 | ||
|
|
fab801e62f | ||
|
|
37a1641450 | ||
|
|
6e4baaa356 | ||
|
|
3292c62212 | ||
|
|
3e1d83a0dd | ||
|
|
47dcd34364 | ||
|
|
2c134c4479 | ||
|
|
8613e0c5a8 | ||
|
|
02daf33e55 | ||
|
|
7b7eafee3b | ||
|
|
400fc016ae | ||
|
|
8b66ee415a | ||
|
|
89fef65347 | ||
|
|
5fbf5b7b97 | ||
|
|
8278bf579c | ||
|
|
6f7611ada5 | ||
|
|
a3ff479941 | ||
|
|
d14e1183d5 | ||
|
|
c7495a7d92 | ||
|
|
5428e950b0 | ||
|
|
13117a5d94 | ||
|
|
6b74c7ec7a | ||
|
|
e8b561f722 | ||
|
|
75ee5af180 | ||
|
|
29f924f6cc | ||
|
|
6e058c8e95 | ||
|
|
6822da80cf | ||
|
|
37e3302f61 | ||
|
|
9b369d71ee | ||
|
|
cf58f0db6f | ||
|
|
299f23c796 | ||
|
|
b58e4333a0 | ||
|
|
f76cb9c381 | ||
|
|
e93e10d14d | ||
|
|
025464ebff | ||
|
|
ae8f77f070 | ||
|
|
97811a12d1 | ||
|
|
1f7e01820a | ||
|
|
2f73771bbd | ||
|
|
503ac18e7b | ||
|
|
6a99b21c15 | ||
|
|
170667f2b4 | ||
|
|
314b7f2484 | ||
|
|
9353b12707 | ||
|
|
063810f906 | ||
|
|
ac7ce126e6 | ||
|
|
a920830dcf | ||
|
|
15d74fcee0 | ||
|
|
c598e3fc9f | ||
|
|
0dba0f2774 | ||
|
|
e2cdb6a94c | ||
|
|
5dd2011e78 | ||
|
|
7bc142dc83 | ||
|
|
1931f00806 | ||
|
|
253336b02c | ||
|
|
b0c3a97f7f | ||
|
|
22887d4f7a | ||
|
|
00dbc90c85 | ||
|
|
9ade1d8993 | ||
|
|
b2fb803033 | ||
|
|
988ed5b959 | ||
|
|
2e62f99724 | ||
|
|
530327b1e8 | ||
|
|
2f75b31fa9 | ||
|
|
ce8f0a8c4b | ||
|
|
321aae5b86 | ||
|
|
7712ea2d1f | ||
|
|
175f64f784 | ||
|
|
9357960463 | ||
|
|
4d043906bc | ||
|
|
b99b7f796b | ||
|
|
a7b0414613 | ||
|
|
13c608b11a | ||
|
|
d14fb91736 | ||
|
|
6beaa444af | ||
|
|
3b552558d4 | ||
|
|
9cc63bf8fb |
No files matched your search
@@ -3,3 +3,5 @@ dist/
|
||||
.DS_Store
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
build.sh
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
# `2.11.0`
|
||||
_21.10.2025_
|
||||
|
||||
## General
|
||||
- [#829](https://github.com/qitechgmbh/control/pull/829), [#834](https://github.com/qitechgmbh/control/pull/834), Improved overall system stability
|
||||
|
||||
## Winder V2
|
||||
- [#840](https://github.com/qitechgmbh/control/pull/840) Added XL Winder option
|
||||
- [#827](https://github.com/qitechgmbh/control/pull/827) Spool can now be run in reverse
|
||||
|
||||
## Extruder V2
|
||||
- [#826](https://github.com/qitechgmbh/control/pull/826) Fine grained Temperature-PID configuration now possible
|
||||
|
||||
|
||||
# `2.10.0`
|
||||
_16.10.2025_
|
||||
|
||||
## Winder V2
|
||||
- [#776](https://github.com/qitechgmbh/control/pull/776) Added explanation for padding in winder manual
|
||||
- [#804](https://github.com/qitechgmbh/control/pull/804) Added gearbox conversion
|
||||
|
||||
## General
|
||||
- [#717](https://github.com/qitechgmbh/control/pull/717) Show from option for graphs added
|
||||
- [#805](https://github.com/qitechgmbh/control/pull/805) Graphs run better now
|
||||
|
||||
|
||||
|
||||
# `2.9.1`
|
||||
_10.10.2025_
|
||||
|
||||
|
||||
+6
-4
@@ -15,16 +15,18 @@ 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
|
||||
if [ "$1" != "mock-machine" ]; then
|
||||
echo "Setting capabilities for server executable"
|
||||
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
|
||||
sudo setcap 'cap_dac_override,cap_net_raw,cap_sys_nice,cap_ipc_lock=eip' ./target/debug/server
|
||||
|
||||
echo "Setting permissions for serial ports"
|
||||
# sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
|
||||
if compgen -G "/dev/ttyUSB*" > /dev/null; then
|
||||
echo "Setting permissions for serial ports"
|
||||
sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
|
||||
fi
|
||||
fi
|
||||
|
||||
# run
|
||||
|
||||
@@ -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,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;
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::{process::Command, sync::Arc, time::Duration};
|
||||
|
||||
use ethercrab::{
|
||||
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
|
||||
@@ -12,14 +12,46 @@ const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
|
||||
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
|
||||
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
|
||||
|
||||
/// Finds an ethernet interface that is suitable for EtherCAT communication.
|
||||
///
|
||||
/// ```ignore
|
||||
/// match discover_ethercat_interface().await {
|
||||
/// Ok(interface) => println!("Found working interface: {}", interface),
|
||||
/// Err(_) => println!("No working interface found"),
|
||||
/// }
|
||||
/// ```
|
||||
/// Sets a network interface to unmanaged by NetworkManager.
|
||||
/// Returns true if the command succeeded.
|
||||
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
|
||||
let managed_str = match managed {
|
||||
true => "yes",
|
||||
false => "no",
|
||||
};
|
||||
tracing::info!(
|
||||
"set_interface_managed for {} managed was set to: {}",
|
||||
interface,
|
||||
managed_str
|
||||
);
|
||||
let status = Command::new("nmcli")
|
||||
.args(["dev", "set", interface, "managed", managed_str])
|
||||
.status();
|
||||
|
||||
matches!(status, Ok(s) if s.success())
|
||||
}
|
||||
|
||||
/// Returns true if the given network interface is Ethernet.
|
||||
/// Prevents testing wlan,loopback and other non ethernet devices
|
||||
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let base_path = format!("/sys/class/net/{}", interface);
|
||||
let type_path = std::path::Path::new(&base_path).join("type");
|
||||
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
|
||||
// 1 means Ethernet
|
||||
let reports_as_ethernet = iface_type == "1";
|
||||
// Double-check that it's not a wireless interface
|
||||
let uevent_path = std::path::Path::new(&base_path).join("uevent");
|
||||
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
|
||||
// If "DEVTYPE=wlan" appears, it's lying
|
||||
let actually_wifi = uevent.contains("DEVTYPE=wlan");
|
||||
return Ok(reports_as_ethernet && !actually_wifi);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
tracing::info!("Discovering EtherCAT interface...");
|
||||
|
||||
@@ -37,7 +69,6 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
} else {
|
||||
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
|
||||
}
|
||||
// Don't call the default hook, which would print the backtrace
|
||||
}));
|
||||
|
||||
// Get eligible interfaces
|
||||
@@ -48,6 +79,7 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
iface.is_up()
|
||||
&& iface.is_running()
|
||||
&& !iface.is_loopback()
|
||||
&& is_ethernet(&iface.name).unwrap_or(false)
|
||||
&& !iface.name.starts_with("bridge")
|
||||
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
|
||||
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
|
||||
@@ -56,57 +88,42 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort interfaces by name
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
let mut interface: Option<&str> = None;
|
||||
|
||||
// Try all interfaces concurrently
|
||||
let tasks = interfaces
|
||||
.iter()
|
||||
.map(|iface| {
|
||||
let name = iface.name.clone();
|
||||
std::thread::Builder::new()
|
||||
.name(format!("ethercat-test-{}", name))
|
||||
.spawn(move || {
|
||||
std::panic::catch_unwind(|| {
|
||||
smol::block_on(async {
|
||||
match test_interface(&name) {
|
||||
Ok(_) => {
|
||||
tracing::info!("Found working EtherCAT interface: {}", name);
|
||||
Some(name.clone())
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::debug!("Interface {} failed", name);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(None)
|
||||
})
|
||||
.expect("Failed to spawn thread")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for i in 0..interfaces.len() {
|
||||
// Avoid NetworkManager hangups
|
||||
set_interface_managed(&interfaces[i].name, false);
|
||||
|
||||
// Wait for all tasks to complete and collect successful results
|
||||
let successful_interfaces: Vec<String> = tasks
|
||||
.into_iter()
|
||||
.filter_map(|task| task.join().expect("Should join thread"))
|
||||
.collect();
|
||||
match test_interface(&interfaces[i].name) {
|
||||
Ok(_) => {
|
||||
// if interface found with ethercat Exit early, we expect only one interface with ethercat
|
||||
interface = Some(&interfaces[i].name);
|
||||
break;
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Return the first successful interface (they're sorted by name)
|
||||
let result = successful_interfaces.into_iter().next();
|
||||
for i in 0..interfaces.len() {
|
||||
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
|
||||
continue;
|
||||
} else {
|
||||
set_interface_managed(&interfaces[i].name, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the default panic hook
|
||||
std::panic::set_hook(default_hook);
|
||||
|
||||
match result {
|
||||
Some(name) => Ok(name),
|
||||
None => Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
match interface {
|
||||
Some(interface) => return Ok(interface.to_string()),
|
||||
None => return Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
tracing::trace!("Testing interface: {}", interface);
|
||||
tracing::info!("Testing interface: {}", interface);
|
||||
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<
|
||||
IFACE_DISCOVERY_MAX_FRAMES,
|
||||
@@ -144,7 +161,7 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(5),
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Generated
+19
-26
@@ -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"
|
||||
}
|
||||
},
|
||||
@@ -2278,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"
|
||||
@@ -7014,16 +7015,6 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/encoding": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
@@ -11877,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"
|
||||
@@ -11895,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"
|
||||
},
|
||||
@@ -14672,9 +14665,9 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SelectionGroupBoolean } from "@/control/SelectionGroup";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { roundToDecimals } from "@/lib/decimal";
|
||||
import { useExtruder2 } from "./useExtruder";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
|
||||
export function Extruder2SettingsPage() {
|
||||
const {
|
||||
@@ -18,6 +19,7 @@ export function Extruder2SettingsPage() {
|
||||
setPressurePidKp,
|
||||
setPressurePidKi,
|
||||
setPressurePidKd,
|
||||
setTemperaturePidValue,
|
||||
} = useExtruder2();
|
||||
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
@@ -116,6 +118,128 @@ export function Extruder2SettingsPage() {
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
<ControlGrid>
|
||||
<ControlCard title="Temperature PID Settings (Front) ">
|
||||
<Label label="Kp">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.front.kp}
|
||||
defaultValue={defaultState?.pid_settings.temperature.front.kp}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("front", "kp", v)}
|
||||
title="Temperature PID KP"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Ki">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.front.ki}
|
||||
defaultValue={defaultState?.pid_settings.temperature.front.ki}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("front", "ki", v)}
|
||||
title="Temperature PID KI"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Kd">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.front.kd}
|
||||
defaultValue={defaultState?.pid_settings.temperature.front.kd}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("front", "kd", v)}
|
||||
title="Temperature PID KD"
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
<ControlCard title="Temperature PID Settings (Middle) ">
|
||||
<Label label="Kp">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.middle.kp}
|
||||
defaultValue={
|
||||
defaultState?.pid_settings.temperature.middle.kp
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("middle", "kp", v)}
|
||||
title="Temperature PID KP"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Ki">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.middle.ki}
|
||||
defaultValue={
|
||||
defaultState?.pid_settings.temperature.middle.ki
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("middle", "ki", v)}
|
||||
title="Temperature PID KI"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Kd">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.middle.kd}
|
||||
defaultValue={
|
||||
defaultState?.pid_settings.temperature.middle.kd
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("middle", "kd", v)}
|
||||
title="Temperature PID KD"
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
<ControlCard title="Temperature PID Settings (Back) ">
|
||||
<Label label="Kp">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.back.kp}
|
||||
defaultValue={defaultState?.pid_settings.temperature.back.kp}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("back", "kp", v)}
|
||||
title="Temperature PID KP"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Ki">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.back.ki}
|
||||
defaultValue={defaultState?.pid_settings.temperature.back.ki}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("back", "ki", v)}
|
||||
title="Temperature PID KI"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Kd">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.back.kd}
|
||||
defaultValue={defaultState?.pid_settings.temperature.back.kd}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("back", "kd", v)}
|
||||
title="Temperature PID KD"
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
|
||||
@@ -131,9 +131,24 @@ export const inverterStatusStateSchema = z.object({
|
||||
*/
|
||||
export const pidSettingsSchema = z.object({
|
||||
temperature: z.object({
|
||||
ki: z.number(),
|
||||
kp: z.number(),
|
||||
kd: z.number(),
|
||||
front: z.object({
|
||||
ki: z.number(),
|
||||
kp: z.number(),
|
||||
kd: z.number(),
|
||||
zone: z.string(),
|
||||
}),
|
||||
middle: z.object({
|
||||
ki: z.number(),
|
||||
kp: z.number(),
|
||||
kd: z.number(),
|
||||
zone: z.string(),
|
||||
}),
|
||||
back: z.object({
|
||||
ki: z.number(),
|
||||
kp: z.number(),
|
||||
kd: z.number(),
|
||||
zone: z.string(),
|
||||
}),
|
||||
}),
|
||||
pressure: z.object({
|
||||
ki: z.number(),
|
||||
|
||||
@@ -293,6 +293,33 @@ export function useExtruder2() {
|
||||
);
|
||||
};
|
||||
|
||||
const setTemperaturePidValue = (
|
||||
zone: "front" | "middle" | "back",
|
||||
key: "kp" | "ki" | "kd",
|
||||
value: number,
|
||||
) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.pid_settings.temperature[zone][key] = value;
|
||||
},
|
||||
() => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState) {
|
||||
const settings = produce(
|
||||
currentState.data.pid_settings.temperature[zone],
|
||||
(draft) => {
|
||||
draft[key] = value;
|
||||
},
|
||||
);
|
||||
requestTemperaturePidSettings({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetTemperaturePidSettings: settings },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const resetInverter = () => {
|
||||
// No optimistic update needed for reset
|
||||
requestResetInverter({
|
||||
@@ -356,6 +383,17 @@ export function useExtruder2() {
|
||||
}),
|
||||
);
|
||||
|
||||
const { request: requestTemperaturePidSettings } = useMachineMutation(
|
||||
z.object({
|
||||
SetTemperaturePidSettings: z.object({
|
||||
ki: z.number(),
|
||||
kp: z.number(),
|
||||
kd: z.number(),
|
||||
zone: z.string(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const { request: requestResetInverter } = useMachineMutation(
|
||||
z.object({ ResetInverter: z.boolean() }),
|
||||
);
|
||||
@@ -404,6 +442,7 @@ export function useExtruder2() {
|
||||
setPressurePidKp,
|
||||
setPressurePidKi,
|
||||
setPressurePidKd,
|
||||
setTemperaturePidValue,
|
||||
resetInverter,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ import { Label } from "@/control/Label";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { StatusBadge } from "@/control/StatusBadge";
|
||||
import { useWinder2 } from "./useWinder";
|
||||
import { Mode, SpoolAutomaticActionMode } from "./winder2Namespace";
|
||||
import {
|
||||
Mode,
|
||||
SpoolAutomaticActionMode,
|
||||
getGearRatioMultiplier,
|
||||
} from "./winder2Namespace";
|
||||
import { TensionArm } from "../TensionArm";
|
||||
import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal";
|
||||
import { Spool } from "../Spool";
|
||||
@@ -24,9 +28,11 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { getWinder2TraverseMax } from "./winder2Config";
|
||||
|
||||
export function Winder2ControlPage() {
|
||||
const [showResetConfirmDialog, setShowResetConfirmDialog] = useState(false);
|
||||
const traverseMax = getWinder2TraverseMax();
|
||||
|
||||
// use optimistic state
|
||||
const {
|
||||
@@ -54,6 +60,13 @@ export function Winder2ControlPage() {
|
||||
isDisabled,
|
||||
} = useWinder2();
|
||||
|
||||
// Calculate max speed based on gear ratio
|
||||
const gearRatioMultiplier = getGearRatioMultiplier(
|
||||
state?.puller_state?.gear_ratio,
|
||||
);
|
||||
const maxMotorSpeed = 75; // Maximum motor speed in m/min
|
||||
const maxTargetSpeed = maxMotorSpeed / gearRatioMultiplier;
|
||||
|
||||
const handleResetProgress = () => {
|
||||
// Check if the machine is currently in Wind mode
|
||||
if (state?.mode_state?.mode === "Wind") {
|
||||
@@ -91,7 +104,7 @@ export function Winder2ControlPage() {
|
||||
{state?.traverse_state && (
|
||||
<TraverseBar
|
||||
inside={0}
|
||||
outside={180}
|
||||
outside={traverseMax}
|
||||
min={state?.traverse_state.limit_inner}
|
||||
max={state?.traverse_state.limit_outer}
|
||||
current={traversePosition.current?.value ?? 0}
|
||||
@@ -110,7 +123,7 @@ export function Winder2ControlPage() {
|
||||
min={Math.max(0, (state?.traverse_state?.limit_inner ?? 0) + 1)}
|
||||
minLabel="IN"
|
||||
maxLabel="OUT"
|
||||
max={180}
|
||||
max={traverseMax}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
onChange={setTraverseLimitOuter}
|
||||
/>
|
||||
@@ -134,8 +147,8 @@ export function Winder2ControlPage() {
|
||||
// We use 1mm buffer to ensure the backend validation (which requires outer > inner + 0.9mm) will pass
|
||||
// Formula: max_inner = outer_limit - 1mm
|
||||
max={Math.min(
|
||||
180,
|
||||
(state?.traverse_state?.limit_outer ?? 180) - 1,
|
||||
traverseMax,
|
||||
(state?.traverse_state?.limit_outer ?? traverseMax) - 1,
|
||||
)}
|
||||
defaultValue={defaultState?.traverse_state?.limit_inner}
|
||||
minLabel="IN"
|
||||
@@ -259,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
|
||||
@@ -274,7 +292,7 @@ export function Winder2ControlPage() {
|
||||
title="Target Speed"
|
||||
defaultValue={defaultState?.puller_state?.target_speed}
|
||||
min={0}
|
||||
max={75}
|
||||
max={maxTargetSpeed}
|
||||
step={0.1}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={setPullerTargetSpeed}
|
||||
|
||||
@@ -67,6 +67,16 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
data.puller_state?.forward ? "Forward" : "Backward",
|
||||
},
|
||||
{
|
||||
name: "Puller Gear Ratio",
|
||||
renderValue: (data: PresetData<Winder2>) => {
|
||||
const ratio = data.puller_state?.gear_ratio;
|
||||
if (ratio === "OneToOne") return "1:1";
|
||||
if (ratio === "OneToFive") return "1:5";
|
||||
if (ratio === "OneToTen") return "1:10";
|
||||
return "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Puller Target Speed",
|
||||
unit: "m/min",
|
||||
@@ -91,6 +101,11 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
data.spool_speed_controller_state?.regulation_mode,
|
||||
},
|
||||
{
|
||||
name: "Spool Direction",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
data.spool_speed_controller_state?.forward ? "Forward" : "Reverse",
|
||||
},
|
||||
{
|
||||
name: "Spool Min Speed",
|
||||
unit: "rpm",
|
||||
@@ -153,8 +168,10 @@ export function Winder2PresetsPage() {
|
||||
setPullerTargetDiameter,
|
||||
setPullerForward,
|
||||
setPullerTargetSpeed,
|
||||
setPullerGearRatio,
|
||||
|
||||
setSpoolRegulationMode,
|
||||
setSpoolForward,
|
||||
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
@@ -177,11 +194,13 @@ export function Winder2PresetsPage() {
|
||||
setPullerRegulationMode(preset.data?.puller_state?.regulation ?? "Speed");
|
||||
setPullerForward(preset.data?.puller_state?.forward ?? true);
|
||||
setPullerTargetSpeed(preset.data?.puller_state?.target_speed ?? 1.0);
|
||||
setPullerGearRatio(preset.data?.puller_state?.gear_ratio ?? "OneToOne");
|
||||
// setPullerTargetDiameter(preset.data?.puller_state?.target_diameter ?? 1.75);
|
||||
|
||||
setSpoolRegulationMode(
|
||||
preset.data?.spool_speed_controller_state?.regulation_mode ?? "MinMax",
|
||||
);
|
||||
setSpoolForward(preset.data?.spool_speed_controller_state?.forward ?? true);
|
||||
setSpoolMinMaxMinSpeed(
|
||||
preset.data?.spool_speed_controller_state?.minmax_min_speed ?? 0,
|
||||
);
|
||||
|
||||
@@ -2,24 +2,37 @@ import { Page } from "@/components/Page";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useWinder2 } from "./useWinder";
|
||||
import { roundToDecimals } from "@/lib/decimal";
|
||||
import { Label } from "@/control/Label";
|
||||
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { MachineSelector } from "@/components/MachineConnectionDropdown";
|
||||
import {
|
||||
getWinder2XLMode,
|
||||
setWinder2XLMode,
|
||||
WINDER2_TRAVERSE_MAX_STANDARD,
|
||||
WINDER2_TRAVERSE_MAX_XL,
|
||||
} from "./winder2Config";
|
||||
|
||||
export function Winder2SettingPage() {
|
||||
const [xlMode, setXlMode] = useState(getWinder2XLMode());
|
||||
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
setTraverseStepSize,
|
||||
setTraversePadding,
|
||||
setTraverseLimitInner,
|
||||
setTraverseLimitOuter,
|
||||
gotoTraverseHome,
|
||||
setPullerForward,
|
||||
setPullerGearRatio,
|
||||
setSpoolRegulationMode,
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
setSpoolForward,
|
||||
setSpoolAdaptiveTensionTarget,
|
||||
setSpoolAdaptiveRadiusLearningRate,
|
||||
setSpoolAdaptiveMaxSpeedMultiplier,
|
||||
@@ -27,16 +40,67 @@ export function Winder2SettingPage() {
|
||||
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
setSpeedPiKp,
|
||||
setSpeedPiKi,
|
||||
selectedMachine,
|
||||
filteredMachines,
|
||||
setConnectedLaser,
|
||||
disconnectLaser,
|
||||
setConnectedMachine,
|
||||
disconnectMachine,
|
||||
} = useWinder2();
|
||||
|
||||
const handleXlModeChange = (enabled: boolean) => {
|
||||
setWinder2XLMode(enabled);
|
||||
setXlMode(enabled);
|
||||
|
||||
// When switching from XL to normal mode, reset traverse limits to default values
|
||||
if (!enabled && defaultState) {
|
||||
// Only reset if current values exceed the standard max
|
||||
const currentOuter = state?.traverse_state?.limit_outer ?? 0;
|
||||
const currentInner = state?.traverse_state?.limit_inner ?? 0;
|
||||
const defaultOuter = defaultState.traverse_state?.limit_outer;
|
||||
const defaultInner = defaultState.traverse_state?.limit_inner;
|
||||
|
||||
if (
|
||||
currentOuter > WINDER2_TRAVERSE_MAX_STANDARD &&
|
||||
defaultOuter !== undefined
|
||||
) {
|
||||
setTraverseLimitOuter(defaultOuter);
|
||||
setTraverseLimitInner(defaultInner);
|
||||
}
|
||||
if (
|
||||
currentInner > WINDER2_TRAVERSE_MAX_STANDARD &&
|
||||
defaultInner !== undefined
|
||||
) {
|
||||
setTraverseLimitOuter(defaultOuter);
|
||||
setTraverseLimitInner(defaultInner);
|
||||
}
|
||||
|
||||
// Home the traverse when switching from XL to normal mode
|
||||
gotoTraverseHome();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid>
|
||||
<ControlCard title="Traverse">
|
||||
<Label label="Traverse Size">
|
||||
<SelectionGroupBoolean
|
||||
value={xlMode}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
optionFalse={{
|
||||
children: `Standard (${WINDER2_TRAVERSE_MAX_STANDARD}mm)`,
|
||||
icon: "lu:Settings",
|
||||
}}
|
||||
optionTrue={{
|
||||
children: `XL (${WINDER2_TRAVERSE_MAX_XL}mm)`,
|
||||
icon: "lu:Maximize2",
|
||||
}}
|
||||
onChange={handleXlModeChange}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Step Size">
|
||||
<EditValue
|
||||
value={state?.traverse_state?.step_size}
|
||||
@@ -87,6 +151,23 @@ export function Winder2SettingPage() {
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Rotation Direction">
|
||||
<SelectionGroupBoolean
|
||||
value={state?.spool_speed_controller_state?.forward}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
optionFalse={{
|
||||
children: "Reverse",
|
||||
icon: "lu:RotateCcw",
|
||||
}}
|
||||
optionTrue={{
|
||||
children: "Forward",
|
||||
icon: "lu:RotateCw",
|
||||
}}
|
||||
onChange={(value) => setSpoolForward(value)}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{state?.spool_speed_controller_state?.regulation_mode ===
|
||||
"MinMax" && (
|
||||
<>
|
||||
@@ -249,21 +330,75 @@ export function Winder2SettingPage() {
|
||||
onChange={(value) => setPullerForward(value)}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Gear Ratio">
|
||||
<SelectionGroup
|
||||
value={state?.puller_state?.gear_ratio}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
options={{
|
||||
OneToOne: {
|
||||
children: "1:1",
|
||||
icon: "lu:Circle",
|
||||
},
|
||||
OneToFive: {
|
||||
children: "1:5",
|
||||
icon: "lu:Cog",
|
||||
},
|
||||
OneToTen: {
|
||||
children: "1:10",
|
||||
icon: "lu:Cog",
|
||||
},
|
||||
}}
|
||||
onChange={(value) =>
|
||||
setPullerGearRatio(
|
||||
value as "OneToOne" | "OneToFive" | "OneToTen",
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
<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>
|
||||
<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);
|
||||
}}
|
||||
/>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
PullerRegulation,
|
||||
SpoolAutomaticActionMode,
|
||||
spoolAutomaticActionModeSchema,
|
||||
gearRatioSchema,
|
||||
GearRatio,
|
||||
} from "./winder2Namespace";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { produce } from "immer";
|
||||
@@ -120,6 +122,9 @@ export function useWinder2() {
|
||||
const { request: requestPullerSetForward } = useMachineMutation(
|
||||
z.object({ SetPullerForward: z.boolean() }),
|
||||
);
|
||||
const { request: requestPullerSetGearRatio } = useMachineMutation(
|
||||
z.object({ SetPullerGearRatio: gearRatioSchema }),
|
||||
);
|
||||
const { request: requestSpoolSetRegulationMode } = useMachineMutation(
|
||||
z.object({ SetSpoolRegulationMode: spoolRegulationModeSchema }),
|
||||
);
|
||||
@@ -129,6 +134,9 @@ export function useWinder2() {
|
||||
const { request: requestSpoolSetMinMaxMaxSpeed } = useMachineMutation(
|
||||
z.object({ SetSpoolMinMaxMaxSpeed: z.number() }),
|
||||
);
|
||||
const { request: requestSpoolSetForward } = useMachineMutation(
|
||||
z.object({ SetSpoolForward: z.boolean() }),
|
||||
);
|
||||
const { request: requestSpoolSetAdaptiveTensionTarget } = useMachineMutation(
|
||||
z.object({ SetSpoolAdaptiveTensionTarget: z.number() }),
|
||||
);
|
||||
@@ -171,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,
|
||||
@@ -366,6 +395,28 @@ export function useWinder2() {
|
||||
);
|
||||
};
|
||||
|
||||
const setPullerGearRatio = (gearRatio: GearRatio) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.puller_state.gear_ratio = gearRatio;
|
||||
// Reset target speed to 0 to prevent sudden speed changes
|
||||
current.data.puller_state.target_speed = 0;
|
||||
},
|
||||
async () => {
|
||||
// First set the target speed to 0
|
||||
await requestPullerSetTargetSpeed({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetPullerTargetSpeed: 0 },
|
||||
});
|
||||
// Then set the gear ratio
|
||||
await requestPullerSetGearRatio({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetPullerGearRatio: gearRatio },
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const setSpoolAutomaticRequiredMeters = (meters: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
@@ -440,6 +491,19 @@ export function useWinder2() {
|
||||
);
|
||||
};
|
||||
|
||||
const setSpoolForward = (forward: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.spool_speed_controller_state.forward = forward;
|
||||
},
|
||||
() =>
|
||||
requestSpoolSetForward({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetSpoolForward: forward },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setSpoolAdaptiveTensionTarget = (value: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
@@ -550,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;
|
||||
@@ -564,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],
|
||||
);
|
||||
@@ -572,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(
|
||||
@@ -619,17 +761,23 @@ export function useWinder2() {
|
||||
setPullerTargetDiameter,
|
||||
setPullerRegulationMode,
|
||||
setPullerForward,
|
||||
setPullerGearRatio,
|
||||
setSpoolAutomaticRequiredMeters,
|
||||
setSpoolAutomaticAction,
|
||||
setSpoolRegulationMode,
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
setSpoolForward,
|
||||
setSpoolAdaptiveTensionTarget,
|
||||
setSpoolAdaptiveRadiusLearningRate,
|
||||
setSpoolAdaptiveMaxSpeedMultiplier,
|
||||
setSpoolAdaptiveAccelerationFactor,
|
||||
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
|
||||
setSpeedPiKp,
|
||||
setSpeedPiKi,
|
||||
setConnectedMachine,
|
||||
disconnectMachine,
|
||||
setConnectedLaser,
|
||||
disconnectLaser,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Configuration helper for Winder2 machine settings
|
||||
*/
|
||||
|
||||
const WINDER2_XL_MODE_KEY = "winder2_xl_mode";
|
||||
|
||||
export const WINDER2_TRAVERSE_MAX_STANDARD = 180; // mm
|
||||
export const WINDER2_TRAVERSE_MAX_XL = 385; // mm
|
||||
|
||||
/**
|
||||
* Get the XL mode setting from localStorage
|
||||
* @returns true if XL mode is enabled, false otherwise
|
||||
*/
|
||||
export function getWinder2XLMode(): boolean {
|
||||
const stored = localStorage.getItem(WINDER2_XL_MODE_KEY);
|
||||
return stored === "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XL mode setting in localStorage
|
||||
* @param enabled - true to enable XL mode, false to disable
|
||||
*/
|
||||
export function setWinder2XLMode(enabled: boolean): void {
|
||||
localStorage.setItem(WINDER2_XL_MODE_KEY, enabled.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum traverse limit based on XL mode
|
||||
* @returns the maximum traverse limit in mm
|
||||
*/
|
||||
export function getWinder2TraverseMax(): number {
|
||||
return getWinder2XLMode()
|
||||
? WINDER2_TRAVERSE_MAX_XL
|
||||
: WINDER2_TRAVERSE_MAX_STANDARD;
|
||||
}
|
||||
@@ -39,9 +39,36 @@ 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>;
|
||||
|
||||
/**
|
||||
* Gear ratio enum for winding speed
|
||||
*/
|
||||
export const gearRatioSchema = z.enum(["OneToOne", "OneToFive", "OneToTen"]);
|
||||
export type GearRatio = z.infer<typeof gearRatioSchema>;
|
||||
|
||||
/**
|
||||
* Get the multiplier for a gear ratio
|
||||
*/
|
||||
export function getGearRatioMultiplier(
|
||||
gearRatio: GearRatio | undefined,
|
||||
): number {
|
||||
switch (gearRatio) {
|
||||
case "OneToOne":
|
||||
return 1.0;
|
||||
case "OneToFive":
|
||||
return 5.0;
|
||||
case "OneToTen":
|
||||
return 10.0;
|
||||
default:
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Machine operation mode enum
|
||||
*/
|
||||
@@ -105,6 +132,7 @@ export const pullerStateSchema = z.object({
|
||||
target_speed: z.number(),
|
||||
target_diameter: z.number(),
|
||||
forward: z.boolean(),
|
||||
gear_ratio: gearRatioSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -116,7 +144,7 @@ export const modeStateSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* Connected machine state scheme
|
||||
* Connected machine state schema
|
||||
*/
|
||||
export const machineIdentificationSchema = z.object({
|
||||
vendor: z.number(),
|
||||
@@ -133,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
|
||||
*/
|
||||
@@ -152,6 +188,7 @@ export const spoolSpeedControllerStateSchema = z.object({
|
||||
adaptive_max_speed_multiplier: z.number(),
|
||||
adaptive_acceleration_factor: z.number(),
|
||||
adaptive_deacceleration_urgency_multiplier: z.number(),
|
||||
forward: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -166,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 ==========
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
libpcap
|
||||
nodejs_22
|
||||
nodePackages.npm
|
||||
lldb
|
||||
];
|
||||
|
||||
hardeningDisable = [ "fortify" ];
|
||||
|
||||
@@ -15,12 +15,15 @@ in
|
||||
consoleMode = "max"; # Use the highest available resolution
|
||||
};
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages-rt_latest;
|
||||
boot.kernelPackages = pkgs.linuxPackages_6_13;
|
||||
boot.kernelModules = [ "i915" ];
|
||||
|
||||
|
||||
|
||||
boot.kernelParams = [
|
||||
# Realtime Preemption
|
||||
"preempt=full"
|
||||
|
||||
# Graphical
|
||||
"logo.nologo" # Remove kernel logo during boot
|
||||
|
||||
|
||||
+1
-4
@@ -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"
|
||||
@@ -77,6 +73,7 @@ tracing-otel = [
|
||||
"dep:tonic",
|
||||
]
|
||||
mock-machine = []
|
||||
memory-locking = []
|
||||
io-uring = []
|
||||
development-build = []
|
||||
|
||||
|
||||
@@ -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
-78
@@ -1,82 +1,23 @@
|
||||
use super::setup::setup_loop;
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
panic::{PanicDetails, send_panic},
|
||||
socketio::main_namespace::{
|
||||
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
|
||||
},
|
||||
};
|
||||
use control_core::{
|
||||
ethercat::interface_discovery::discover_ethercat_interface,
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
use smol::channel::Sender;
|
||||
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(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let thread_panic_tx_clone = thread_panic_tx;
|
||||
let app_state_clone = app_state;
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("init_ethercat".to_string())
|
||||
.spawn(move || {
|
||||
send_panic(thread_panic_tx_clone.clone());
|
||||
|
||||
// Notify client via socketio
|
||||
let app_state_socketio = app_state_clone.clone();
|
||||
smol::block_on(async move {
|
||||
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(
|
||||
thread_panic_tx_clone,
|
||||
&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,3 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod ethercat_discovery_info;
|
||||
pub mod init;
|
||||
pub mod setup;
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::app_state::EthercatSetup;
|
||||
use crate::machines::registry::MACHINE_REGISTRY;
|
||||
use crate::panic::{PanicDetails, send_panic};
|
||||
use crate::socketio::main_namespace::MainNamespaceEvents;
|
||||
use crate::socketio::main_namespace::ethercat_devices_event::EthercatDevicesEventBuilder;
|
||||
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
|
||||
@@ -18,19 +17,14 @@ 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 smol::channel::Sender;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
const SM_OUTPUT: u16 = 0x1C32;
|
||||
const SM_INPUT: u16 = 0x1C33;
|
||||
|
||||
pub async fn setup_loop(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
interface: &str,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
pub async fn setup_loop(interface: &str, app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
tracing::info!("Starting Ethercat PDU loop");
|
||||
|
||||
// Erase all all setup data from `app_state`
|
||||
@@ -44,12 +38,10 @@ pub async fn setup_loop(
|
||||
let pdu_storage = Box::leak(Box::new(PduStorage::<MAX_FRAMES, MAX_PDU_DATA>::new()));
|
||||
let (tx, rx, pdu) = pdu_storage.try_split().expect("can only split once");
|
||||
let interface = interface.to_string();
|
||||
let thread_panic_tx_clone = thread_panic_tx.clone();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("EthercatTxRxThread".to_owned())
|
||||
.spawn(move || {
|
||||
send_panic(thread_panic_tx_clone);
|
||||
|
||||
#[cfg(all(target_os = "linux", not(feature = "development-build")))]
|
||||
match set_irq_affinity(&interface, 3) {
|
||||
Ok(_) => tracing::info!("ethernet interrupt handler now runs on cpu:{}", 3),
|
||||
@@ -60,27 +52,14 @@ pub async fn setup_loop(
|
||||
let _ = set_core_affinity(3);
|
||||
|
||||
// Set the thread to real-time priority
|
||||
#[cfg(not(feature = "development-build"))]
|
||||
let _ = set_realtime_priority();
|
||||
|
||||
#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
|
||||
{
|
||||
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)");
|
||||
}
|
||||
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");
|
||||
|
||||
@@ -93,13 +72,13 @@ pub async fn setup_loop(
|
||||
// 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
-40
@@ -1,61 +1,81 @@
|
||||
use crate::app_state::AppState;
|
||||
use crate::panic::{PanicDetails, send_panic};
|
||||
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 smol::channel::Sender;
|
||||
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::{Duration, Instant};
|
||||
use std::time::Instant;
|
||||
use tracing::{instrument, trace_span};
|
||||
|
||||
pub fn init_loop(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
pub fn start_loop_thread(
|
||||
interface: &str,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
) -> 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 || {
|
||||
send_panic(thread_panic_tx.clone());
|
||||
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))]
|
||||
@@ -128,12 +148,6 @@ pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyh
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "development-build")]
|
||||
smol::Timer::after(Duration::from_micros(2000)).await;
|
||||
|
||||
#[cfg(not(feature = "development-build"))]
|
||||
smol::Timer::after(Duration::from_micros(500)).await;
|
||||
}
|
||||
|
||||
// execute machines
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -183,9 +183,24 @@ pub struct PidSettings {
|
||||
pub kd: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
||||
pub struct TemperaturePidStates {
|
||||
pub front: TemperaturePid,
|
||||
pub middle: TemperaturePid,
|
||||
pub back: TemperaturePid,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
||||
pub struct TemperaturePid {
|
||||
pub ki: f64,
|
||||
pub kp: f64,
|
||||
pub kd: f64,
|
||||
pub zone: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
pub struct PidSettingsStates {
|
||||
pub temperature: PidSettings,
|
||||
pub temperature: TemperaturePidStates,
|
||||
pub pressure: PidSettings,
|
||||
}
|
||||
|
||||
@@ -217,6 +232,7 @@ pub enum Mutation {
|
||||
|
||||
// Pid Configure
|
||||
SetPressurePidSettings(PidSettings),
|
||||
SetTemperaturePidSettings(TemperaturePid),
|
||||
|
||||
// Reset
|
||||
ResetInverter(bool),
|
||||
@@ -292,6 +308,10 @@ impl MachineApi for ExtruderV2 {
|
||||
Mutation::SetPressurePidSettings(settings) => {
|
||||
self.configure_pressure_pid(settings);
|
||||
}
|
||||
|
||||
Mutation::SetTemperaturePidSettings(settings) => {
|
||||
self.configure_temperature_pid(settings);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::machines::extruder1::{
|
||||
api::{
|
||||
ExtruderSettingsState, ExtruderV2Events, HeatingState, HeatingStates, InverterStatusState,
|
||||
LiveValuesEvent, ModeState, PidSettings, PidSettingsStates, PressureState, RegulationState,
|
||||
RotationState, ScrewState, StateEvent,
|
||||
RotationState, ScrewState, StateEvent, TemperaturePid,
|
||||
},
|
||||
};
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
@@ -27,6 +27,8 @@ use uom::si::thermodynamic_temperature::ThermodynamicTemperature;
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
impl ExtruderV2 {
|
||||
pub fn build_state_event(&mut self) -> StateEvent {
|
||||
use crate::machines::extruder1::api::{TemperaturePid, TemperaturePidStates};
|
||||
|
||||
StateEvent {
|
||||
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
|
||||
rotation_state: RotationState {
|
||||
@@ -106,10 +108,25 @@ impl ExtruderV2 {
|
||||
fault_occurence: self.screw_speed_controller.inverter.status.fault_occurence,
|
||||
},
|
||||
pid_settings: PidSettingsStates {
|
||||
temperature: PidSettings {
|
||||
ki: 0.0,
|
||||
kp: 0.0,
|
||||
kd: 0.0,
|
||||
temperature: TemperaturePidStates {
|
||||
front: TemperaturePid {
|
||||
ki: self.temperature_controller_front.pid.get_ki(),
|
||||
kp: self.temperature_controller_front.pid.get_kp(),
|
||||
kd: self.temperature_controller_front.pid.get_kd(),
|
||||
zone: String::from("front"),
|
||||
},
|
||||
middle: TemperaturePid {
|
||||
ki: self.temperature_controller_middle.pid.get_ki(),
|
||||
kp: self.temperature_controller_middle.pid.get_kp(),
|
||||
kd: self.temperature_controller_middle.pid.get_kd(),
|
||||
zone: String::from("middle"),
|
||||
},
|
||||
back: TemperaturePid {
|
||||
ki: self.temperature_controller_back.pid.get_ki(),
|
||||
kp: self.temperature_controller_back.pid.get_kp(),
|
||||
kd: self.temperature_controller_back.pid.get_kd(),
|
||||
zone: String::from("back"),
|
||||
},
|
||||
},
|
||||
pressure: PidSettings {
|
||||
ki: self.screw_speed_controller.pid.get_ki(),
|
||||
@@ -279,4 +296,32 @@ impl ExtruderV2 {
|
||||
.configure(settings.ki, settings.kp, settings.kd);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn configure_temperature_pid(&mut self, settings: TemperaturePid) {
|
||||
match settings.zone.as_str() {
|
||||
"front" => {
|
||||
self.temperature_controller_front.pid.configure(
|
||||
settings.ki,
|
||||
settings.kp,
|
||||
settings.kd,
|
||||
);
|
||||
}
|
||||
"middle" => {
|
||||
self.temperature_controller_middle.pid.configure(
|
||||
settings.ki,
|
||||
settings.kp,
|
||||
settings.kd,
|
||||
);
|
||||
}
|
||||
"back" => {
|
||||
self.temperature_controller_back.pid.configure(
|
||||
settings.ki,
|
||||
settings.kp,
|
||||
settings.kd,
|
||||
);
|
||||
}
|
||||
_ => tracing::warn!("Unknown zone: {}", settings.zone),
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,10 @@ impl MachineApi for ExtruderV2 {
|
||||
Mutation::SetPressurePidSettings(settings) => {
|
||||
self.configure_pressure_pid(settings);
|
||||
}
|
||||
|
||||
Mutation::SetTemperaturePidSettings(settings) => {
|
||||
self.configure_temperature_pid(settings);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::machines::extruder1::{
|
||||
ExtruderV2Mode, HeatingType,
|
||||
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent},
|
||||
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent, TemperaturePid},
|
||||
mock::ExtruderV2,
|
||||
};
|
||||
use control_core::{
|
||||
@@ -133,4 +133,26 @@ impl ExtruderV2 {
|
||||
self.pid_settings.pressure.kd = settings.kd;
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
pub fn configure_temperature_pid(&mut self, settings: TemperaturePid) {
|
||||
match settings.zone.as_str() {
|
||||
"front" => {
|
||||
self.pid_settings.temperature.front.ki = settings.ki;
|
||||
self.pid_settings.temperature.front.kp = settings.kp;
|
||||
self.pid_settings.temperature.front.kd = settings.kd;
|
||||
}
|
||||
"middle" => {
|
||||
self.pid_settings.temperature.middle.ki = settings.ki;
|
||||
self.pid_settings.temperature.middle.kp = settings.kp;
|
||||
self.pid_settings.temperature.middle.kd = settings.kd;
|
||||
}
|
||||
"back" => {
|
||||
self.pid_settings.temperature.back.ki = settings.ki;
|
||||
self.pid_settings.temperature.back.kp = settings.kp;
|
||||
self.pid_settings.temperature.back.kd = settings.kd;
|
||||
}
|
||||
_ => tracing::warn!("Unknown zone: {}", settings.zone),
|
||||
}
|
||||
self.emit_state();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ use crate::machines::extruder1::{
|
||||
api::{
|
||||
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
|
||||
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
|
||||
PressureState, RegulationState, RotationState, ScrewState,
|
||||
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
|
||||
TemperaturePidStates,
|
||||
},
|
||||
mock::ExtruderV2,
|
||||
};
|
||||
@@ -88,10 +89,25 @@ impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
|
||||
fault_occurence: false,
|
||||
},
|
||||
pid_settings: PidSettingsStates {
|
||||
temperature: PidSettings {
|
||||
ki: 0.0,
|
||||
kp: 0.0,
|
||||
kd: 0.0,
|
||||
temperature: TemperaturePidStates {
|
||||
front: TemperaturePid {
|
||||
ki: 0.0,
|
||||
kp: 0.0,
|
||||
kd: 0.0,
|
||||
zone: String::from("front"),
|
||||
},
|
||||
middle: TemperaturePid {
|
||||
ki: 0.0,
|
||||
kp: 0.0,
|
||||
kd: 0.0,
|
||||
zone: String::from("middle"),
|
||||
},
|
||||
back: TemperaturePid {
|
||||
ki: 0.0,
|
||||
kp: 0.0,
|
||||
kd: 0.0,
|
||||
zone: String::from("back"),
|
||||
},
|
||||
},
|
||||
pressure: PidSettings {
|
||||
ki: 0.0,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
¶ms.get_machine_identification_unique(),
|
||||
),
|
||||
diameter: Length::ZERO,
|
||||
x_diameter: None,
|
||||
y_diameter: None,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,30 +1,63 @@
|
||||
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,
|
||||
@@ -54,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),
|
||||
@@ -76,11 +109,13 @@ enum Mutation {
|
||||
SetPullerTargetSpeed(f64),
|
||||
SetPullerTargetDiameter(f64),
|
||||
SetPullerForward(bool),
|
||||
SetPullerGearRatio(GearRatio),
|
||||
|
||||
// Spool Speed Controller
|
||||
SetSpoolRegulationMode(super::spool_speed_controller::SpoolSpeedControllerType),
|
||||
SetSpoolMinMaxMinSpeed(f64),
|
||||
SetSpoolMinMaxMaxSpeed(f64),
|
||||
SetSpoolForward(bool),
|
||||
|
||||
// Adaptive Spool Speed Controller Parameters
|
||||
SetSpoolAdaptiveTensionTarget(f64),
|
||||
@@ -100,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)]
|
||||
@@ -142,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,
|
||||
@@ -180,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,
|
||||
@@ -190,22 +234,25 @@ pub struct PullerState {
|
||||
pub target_diameter: f64,
|
||||
/// forward rotation direction
|
||||
pub forward: bool,
|
||||
/// gear ratio for winding speed
|
||||
pub gear_ratio: GearRatio,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[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,
|
||||
@@ -219,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,
|
||||
@@ -237,6 +284,27 @@ pub struct SpoolSpeedControllerState {
|
||||
pub adaptive_acceleration_factor: f64,
|
||||
/// deacceleration urgency multiplier for adaptive mode
|
||||
pub adaptive_deacceleration_urgency_multiplier: f64,
|
||||
/// forward rotation direction
|
||||
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 {
|
||||
@@ -279,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)?;
|
||||
@@ -296,9 +365,11 @@ impl MachineApi for Winder2 {
|
||||
Mutation::SetPullerTargetSpeed(value) => self.puller_set_target_speed(value),
|
||||
Mutation::SetPullerTargetDiameter(_) => todo!(),
|
||||
Mutation::SetPullerForward(value) => self.puller_set_forward(value),
|
||||
Mutation::SetPullerGearRatio(gear_ratio) => self.puller_set_gear_ratio(gear_ratio),
|
||||
Mutation::SetSpoolRegulationMode(mode) => self.spool_set_regulation_mode(mode),
|
||||
Mutation::SetSpoolMinMaxMinSpeed(speed) => self.spool_set_minmax_min_speed(speed),
|
||||
Mutation::SetSpoolMinMaxMaxSpeed(speed) => self.spool_set_minmax_max_speed(speed),
|
||||
Mutation::SetSpoolForward(value) => self.spool_set_forward(value),
|
||||
Mutation::SetSpoolAdaptiveTensionTarget(value) => {
|
||||
self.spool_set_adaptive_tension_target(value)
|
||||
}
|
||||
@@ -326,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(())
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
@@ -30,9 +39,17 @@ impl Winder2 {
|
||||
&self.tension_arm,
|
||||
&self.puller_speed_controller,
|
||||
);
|
||||
|
||||
// Apply direction based on forward setting
|
||||
let directed_angular_velocity = if self.spool_speed_controller.get_forward() {
|
||||
angular_velocity
|
||||
} else {
|
||||
-angular_velocity
|
||||
};
|
||||
|
||||
let steps_per_second = self
|
||||
.spool_step_converter
|
||||
.angular_velocity_to_steps(angular_velocity);
|
||||
.angular_velocity_to_steps(directed_angular_velocity);
|
||||
let _ = self.spool.set_speed(steps_per_second);
|
||||
}
|
||||
|
||||
@@ -167,15 +184,19 @@ impl Winder2 {
|
||||
.puller_speed_controller
|
||||
.converter
|
||||
.steps_to_angular_velocity(steps_per_second as f64);
|
||||
let puller_speed = self
|
||||
let motor_speed = self
|
||||
.puller_speed_controller
|
||||
.angular_velocity_to_speed(angular_velocity);
|
||||
|
||||
// Calculate spool RPM from current motor steps
|
||||
// Divide by gear ratio to get actual puller/material speed
|
||||
let puller_speed = motor_speed / self.puller_speed_controller.get_gear_ratio().multiplier();
|
||||
|
||||
// Calculate spool RPM from current motor steps (always positive regardless of direction)
|
||||
let spool_rpm = self
|
||||
.spool_step_converter
|
||||
.steps_to_angular_velocity(self.spool.get_speed() as f64)
|
||||
.get::<revolution_per_minute>();
|
||||
.get::<revolution_per_minute>()
|
||||
.abs();
|
||||
|
||||
let live_values = LiveValuesEvent {
|
||||
traverse_position: self
|
||||
@@ -235,6 +256,7 @@ impl Winder2 {
|
||||
.target_diameter
|
||||
.get::<millimeter>(),
|
||||
forward: self.puller_speed_controller.forward,
|
||||
gear_ratio: self.puller_speed_controller.gear_ratio,
|
||||
},
|
||||
mode_state: ModeState {
|
||||
mode: self.mode.clone().into(),
|
||||
@@ -266,12 +288,18 @@ impl Winder2 {
|
||||
adaptive_deacceleration_urgency_multiplier: self
|
||||
.spool_speed_controller
|
||||
.get_adaptive_deacceleration_urgency_multiplier(),
|
||||
forward: self.spool_speed_controller.get_forward(),
|
||||
},
|
||||
spool_automatic_action_state: SpoolAutomaticActionState {
|
||||
spool_required_meters: self.spool_automatic_action.target_length.get::<meter>(),
|
||||
spool_automatic_action_mode: self.spool_automatic_action.mode.clone(),
|
||||
},
|
||||
connected_machine_state: self.connected_buffer.to_state(),
|
||||
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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +425,12 @@ impl Winder2 {
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set gear ratio for winding speed
|
||||
pub fn puller_set_gear_ratio(&mut self, gear_ratio: super::puller_speed_controller::GearRatio) {
|
||||
self.puller_speed_controller.set_gear_ratio(gear_ratio);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
// Spool Speed Controller API methods
|
||||
pub fn spool_set_regulation_mode(
|
||||
&mut self,
|
||||
@@ -462,6 +496,12 @@ impl Winder2 {
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// Set forward rotation direction
|
||||
pub fn spool_set_forward(&mut self, forward: bool) {
|
||||
self.spool_speed_controller.set_forward(forward);
|
||||
self.emit_state();
|
||||
}
|
||||
|
||||
/// implement machine connection
|
||||
/// set connected buffer
|
||||
pub fn set_connected_buffer(
|
||||
@@ -497,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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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(),
|
||||
¶ms.get_machine_identification_unique(),
|
||||
),
|
||||
connected_laser: MachineCrossConnection::new(
|
||||
params.machine_manager.clone(),
|
||||
¶ms.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,22 +16,54 @@ 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)]
|
||||
pub enum GearRatio {
|
||||
OneToOne,
|
||||
OneToFive,
|
||||
OneToTen,
|
||||
}
|
||||
|
||||
impl GearRatio {
|
||||
/// Get the speed multiplier for this gear ratio
|
||||
pub fn multiplier(&self) -> f64 {
|
||||
match self {
|
||||
GearRatio::OneToOne => 1.0,
|
||||
GearRatio::OneToFive => 5.0,
|
||||
GearRatio::OneToTen => 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GearRatio {
|
||||
fn default() -> Self {
|
||||
GearRatio::OneToOne
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PullerSpeedController {
|
||||
enabled: bool,
|
||||
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,
|
||||
/// Gear ratio for winding speed (1:5 or 1:10)
|
||||
pub gear_ratio: GearRatio,
|
||||
/// Linear acceleration controller to dampen speed change
|
||||
acceleration_controller: LinearJerkSpeedController,
|
||||
/// Converter for linear to angular transformations
|
||||
pub converter: LinearStepConverter,
|
||||
pub last_speed: Velocity,
|
||||
pub p_dead_controller: TimeAgnosticDeadTimePiController,
|
||||
}
|
||||
|
||||
impl PullerSpeedController {
|
||||
@@ -43,8 +80,10 @@ impl PullerSpeedController {
|
||||
enabled: false,
|
||||
target_speed,
|
||||
target_diameter,
|
||||
measured_diameter: Length::ZERO,
|
||||
regulation_mode: PullerRegulationMode::Speed,
|
||||
forward: true,
|
||||
gear_ratio: GearRatio::default(),
|
||||
acceleration_controller: LinearJerkSpeedController::new_simple(
|
||||
Some(speed),
|
||||
acceleration,
|
||||
@@ -52,6 +91,11 @@ impl PullerSpeedController {
|
||||
),
|
||||
converter,
|
||||
last_speed: Velocity::ZERO,
|
||||
p_dead_controller: TimeAgnosticDeadTimePiController::new(
|
||||
0.1 / 10000.0,
|
||||
0.0,
|
||||
Duration::ZERO,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,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;
|
||||
}
|
||||
@@ -75,15 +123,41 @@ impl PullerSpeedController {
|
||||
self.forward = forward;
|
||||
}
|
||||
|
||||
pub const fn set_gear_ratio(&mut self, gear_ratio: GearRatio) {
|
||||
self.gear_ratio = gear_ratio;
|
||||
}
|
||||
|
||||
pub const fn get_gear_ratio(&self) -> GearRatio {
|
||||
self.gear_ratio
|
||||
}
|
||||
|
||||
fn update_speed(&mut self, t: Instant) -> Velocity {
|
||||
let speed = match self.enabled {
|
||||
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
|
||||
let speed = base_speed * self.gear_ratio.multiplier();
|
||||
|
||||
let speed = if self.forward { speed } else { -speed };
|
||||
|
||||
let speed = self.acceleration_controller.update(speed, t);
|
||||
@@ -92,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)
|
||||
@@ -112,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,
|
||||
}
|
||||
@@ -21,6 +22,7 @@ pub struct SpoolSpeedController {
|
||||
adaptive_controller: AdaptiveSpoolSpeedController,
|
||||
minmax_controller: MinMaxSpoolSpeedController,
|
||||
r#type: SpoolSpeedControllerType,
|
||||
forward: bool,
|
||||
}
|
||||
|
||||
impl Default for SpoolSpeedController {
|
||||
@@ -35,6 +37,7 @@ impl SpoolSpeedController {
|
||||
adaptive_controller: AdaptiveSpoolSpeedController::new(),
|
||||
minmax_controller: MinMaxSpoolSpeedController::new(),
|
||||
r#type: SpoolSpeedControllerType::Adaptive,
|
||||
forward: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,4 +183,12 @@ impl SpoolSpeedController {
|
||||
self.adaptive_controller
|
||||
.set_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
|
||||
}
|
||||
|
||||
pub const fn get_forward(&self) -> bool {
|
||||
self.forward
|
||||
}
|
||||
|
||||
pub const fn set_forward(&mut self, forward: bool) {
|
||||
self.forward = forward;
|
||||
}
|
||||
}
|
||||
+44
-83
@@ -1,110 +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;
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
pub mod mock;
|
||||
|
||||
use crate::panic::init_panic_handling;
|
||||
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;
|
||||
|
||||
use crate::panic::init_panic;
|
||||
use crate::socketio::queue::init_socketio_queue;
|
||||
|
||||
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() {
|
||||
// Initialize panic handling
|
||||
let thread_panic_tx = init_panic();
|
||||
logging::init_tracing();
|
||||
tracing::info!("Tracing initialized successfully");
|
||||
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!());
|
||||
}
|
||||
init_panic_handling();
|
||||
|
||||
let app_state = Arc::new(AppState::new());
|
||||
|
||||
// Spawn init thread
|
||||
let init_thread = std::thread::Builder::new()
|
||||
.name("init".to_string())
|
||||
.spawn({
|
||||
let thread_panic_tx = thread_panic_tx;
|
||||
let app_state = app_state;
|
||||
move || {
|
||||
#[cfg(feature = "dhat-heap")]
|
||||
init_dhat_heap_profiling();
|
||||
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(thread_panic_tx.clone(), app_state.clone());
|
||||
init_api(thread_panic_tx.clone(), app_state.clone())
|
||||
.expect("Failed to initialize API");
|
||||
init_loop(thread_panic_tx.clone(), app_state.clone())
|
||||
.expect("Failed to initialize loop");
|
||||
|
||||
#[cfg(feature = "mock-machine")]
|
||||
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
init_serial(thread_panic_tx.clone(), app_state.clone())
|
||||
.expect("Failed to initialize Serial");
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
init_ethercat(thread_panic_tx.clone(), 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
@@ -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
|
||||
{
|
||||
|
||||
+32
-162
@@ -1,173 +1,43 @@
|
||||
use smol::channel::{Sender, unbounded};
|
||||
use std::process::exit;
|
||||
use tracing::error;
|
||||
use std::{backtrace::Backtrace, panic::PanicHookInfo, thread};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PanicDetails {
|
||||
pub thread_name: Option<String>,
|
||||
pub message: Option<String>,
|
||||
pub location: Option<PanicDetailsLocation>,
|
||||
pub backtrace: String,
|
||||
}
|
||||
fn panic_hook(panic_info: &PanicHookInfo) {
|
||||
let backtrace = Backtrace::capture().to_string();
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PanicDetailsLocation {
|
||||
pub file: String,
|
||||
pub line: u32,
|
||||
pub column: u32,
|
||||
}
|
||||
let thread = thread::current();
|
||||
let thread = thread.name().unwrap_or("<unknown>");
|
||||
let locataion = panic_info
|
||||
.location()
|
||||
.map_or_else(|| "<unknown>".to_string(), ToString::to_string);
|
||||
|
||||
impl std::fmt::Display for PanicDetailsLocation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{}:{}", self.file, self.line, self.column)
|
||||
}
|
||||
}
|
||||
let message = panic_info
|
||||
.payload()
|
||||
.downcast_ref::<String>()
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
panic_info
|
||||
.payload()
|
||||
.downcast_ref::<&str>()
|
||||
.map(ToString::to_string)
|
||||
})
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
|
||||
impl From<&std::panic::Location<'_>> for PanicDetailsLocation {
|
||||
fn from(location: &std::panic::Location<'_>) -> Self {
|
||||
Self {
|
||||
file: location.file().to_string(),
|
||||
line: location.line(),
|
||||
column: location.column(),
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::error!("thread '{}' panicked at {}:", thread, locataion);
|
||||
eprintln!("{}\n", message);
|
||||
eprintln!("Backtrace:\n{}", backtrace);
|
||||
|
||||
impl std::fmt::Display for PanicDetails {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let fmt_thread_name = match &self.thread_name {
|
||||
Some(name) => format!("'{}'", name),
|
||||
None => "<unknown>".to_string(),
|
||||
};
|
||||
|
||||
let fmt_message = match &self.message {
|
||||
Some(message) => format!("'{}'", message),
|
||||
None => "<no message>".to_string(),
|
||||
};
|
||||
|
||||
let fmt_location = match &self.location {
|
||||
Some(location) => location.to_string(),
|
||||
None => "<no location>".to_string(),
|
||||
};
|
||||
|
||||
// Apply syntax highlighting to backtrace
|
||||
let highlighted_backtrace = self.highlight_backtrace();
|
||||
|
||||
write!(
|
||||
f,
|
||||
"thread {} panicked at {}, {}\n{}",
|
||||
fmt_thread_name, fmt_message, fmt_location, highlighted_backtrace
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PanicDetails {
|
||||
/// Highlights backtrace lines that contain server or control-core crate references
|
||||
/// and the line immediately following them
|
||||
fn highlight_backtrace(&self) -> String {
|
||||
use regex::Regex;
|
||||
|
||||
// Regex to match lines starting with digits followed by colon
|
||||
let line_regex = Regex::new(r"^\s*(\d+):\s*(.*)$").unwrap();
|
||||
|
||||
let lines: Vec<&str> = self.backtrace.lines().collect();
|
||||
let mut result = Vec::new();
|
||||
let mut highlight_next = false;
|
||||
|
||||
for line in lines.iter() {
|
||||
if let Some(captures) = line_regex.captures(line) {
|
||||
let line_num = &captures[1];
|
||||
let content = &captures[2];
|
||||
|
||||
// Check if the line contains server or control-core
|
||||
if content.contains("server") || content.contains("control-core") {
|
||||
// Red highlighting for relevant lines
|
||||
result.push(format!("\x1b[91m{}: {}\x1b[0m", line_num, content));
|
||||
highlight_next = true;
|
||||
} else {
|
||||
// Dim gray for other numbered lines
|
||||
result.push(format!("\x1b[90m{}: {}\x1b[0m", line_num, content));
|
||||
}
|
||||
} else {
|
||||
// Non-numbered lines (headers, location info, etc.)
|
||||
if highlight_next {
|
||||
// Highlight the line after a matched regex line in yellow
|
||||
result.push(format!("\x1b[93m{}\x1b[0m", line));
|
||||
highlight_next = false;
|
||||
} else {
|
||||
// Default color for other non-numbered lines
|
||||
result.push(line.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-level panic handler for general thread crashes
|
||||
/// Sends detailed panic information including backtrace
|
||||
pub fn send_panic(thread_panic_tx: Sender<PanicDetails>) {
|
||||
// Ensure backtrace is enabled for panics
|
||||
if std::env::var("RUST_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
}
|
||||
}
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Capture backtrace at panic site
|
||||
let backtrace = std::backtrace::Backtrace::force_capture();
|
||||
|
||||
let thread_name = std::thread::current().name().map(|name| name.to_string());
|
||||
|
||||
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
|
||||
Some(s.to_string())
|
||||
} else {
|
||||
panic_info.payload().downcast_ref::<String>().cloned()
|
||||
};
|
||||
|
||||
let panic_details = PanicDetails {
|
||||
thread_name,
|
||||
message,
|
||||
location: panic_info.location().map(|loc| loc.into()),
|
||||
backtrace: backtrace.to_string(),
|
||||
};
|
||||
|
||||
// Send detailed panic info through channel
|
||||
let _ = smol::block_on(thread_panic_tx.send(panic_details));
|
||||
}));
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
/// Initialize panic handling system
|
||||
/// Sets up panic handler and starts dedicated panic monitoring thread
|
||||
pub fn init_panic() -> Sender<PanicDetails> {
|
||||
let (thread_panic_tx, thread_panic_rx) = unbounded::<PanicDetails>();
|
||||
send_panic(thread_panic_tx.clone());
|
||||
pub fn init_panic_handling() {
|
||||
// Ensure backtrace is enabled for panics
|
||||
if std::env::var("RUST_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
std::env::set_var("RUST_LIB_BACKTRACE", "1");
|
||||
}
|
||||
}
|
||||
|
||||
// Start panic monitoring thread
|
||||
let thread_panic_rx_clone = thread_panic_rx;
|
||||
std::thread::Builder::new()
|
||||
.name("panic".to_string())
|
||||
.spawn(move || {
|
||||
// Create an executor for this thread
|
||||
let rt = smol::Executor::new();
|
||||
smol::block_on(rt.run(async {
|
||||
loop {
|
||||
match thread_panic_rx_clone.recv().await {
|
||||
Ok(panic_details) => {
|
||||
error!("{}", panic_details);
|
||||
exit(1);
|
||||
}
|
||||
Err(_) => {
|
||||
// Channel closed, exit gracefully
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
.expect("Failed to spawn panic monitoring thread");
|
||||
|
||||
thread_panic_tx
|
||||
std::panic::set_hook(Box::new(panic_hook));
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+43
-58
@@ -1,72 +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::panic::{PanicDetails, send_panic};
|
||||
use crate::socketio::init::init_socketio;
|
||||
use anyhow::anyhow;
|
||||
use anyhow::Result;
|
||||
use axum::routing::post;
|
||||
use smol::channel::Sender;
|
||||
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(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<JoinHandle<()>, anyhow::Error> {
|
||||
std::thread::Builder::new()
|
||||
.name("api".to_string())
|
||||
.spawn(|| {
|
||||
send_panic(thread_panic_tx);
|
||||
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;
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
.expect("Failed to create runtime");
|
||||
async fn init_api(app_state: Arc<AppState>) -> Result<()> {
|
||||
let cors = CorsLayer::permissive();
|
||||
let socketio_layer = init_socketio(&app_state).await;
|
||||
|
||||
rt.block_on(async {
|
||||
// allow all CORS requests
|
||||
let cors = CorsLayer::permissive();
|
||||
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));
|
||||
|
||||
// creat socketio layer
|
||||
let socketio_layer = init_socketio(&app_state).await;
|
||||
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);
|
||||
|
||||
// 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 listener = tokio::net::TcpListener::bind("0.0.0.0:3001")
|
||||
.await
|
||||
.expect("Failed to bind to port 3001");
|
||||
|
||||
// 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);
|
||||
tracing::info!("HTTP server running on 0.0.0.0:3001");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001")
|
||||
.await
|
||||
.expect("Failed to bind to port 3001");
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Server error: {}", e))
|
||||
}
|
||||
|
||||
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))
|
||||
/// 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:?}");
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
})?;
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
+128
-72
@@ -1,83 +1,139 @@
|
||||
use crate::panic::{PanicDetails, send_panic};
|
||||
use crate::socketio::main_namespace::MainNamespaceEvents;
|
||||
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
|
||||
use crate::{app_state::AppState, machines::registry::MACHINE_REGISTRY};
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use smol::channel::Sender;
|
||||
use std::{sync::Arc, thread, time::Duration};
|
||||
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(
|
||||
thread_panic_tx: Sender<PanicDetails>,
|
||||
app_state: Arc<AppState>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let thread_panic_tx_clone = thread_panic_tx;
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
machines::{laser::LaserMachine, registry::MACHINE_REGISTRY},
|
||||
socketio::main_namespace::{MainNamespaceEvents, machines_event::MachinesEventBuilder},
|
||||
};
|
||||
|
||||
let app_state_clone = app_state.clone();
|
||||
use super::devices::laser::Laser;
|
||||
|
||||
thread::Builder::new()
|
||||
.name("SerialDetectionThread".to_owned())
|
||||
.spawn(move || {
|
||||
let app_state_clone = app_state_clone;
|
||||
smol::block_on(async move {
|
||||
send_panic(thread_panic_tx_clone);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
let rt = smol::LocalExecutor::new();
|
||||
rt.run(async {
|
||||
loop {
|
||||
let result = {
|
||||
let mut serial_setup_guard = app_state_clone.serial_setup.write().await;
|
||||
// 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())
|
||||
}
|
||||
|
||||
let mut port_result =
|
||||
serial_setup_guard.serial_detection.check_ports().await;
|
||||
// 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 mut removed_signals = serial_setup_guard
|
||||
.serial_detection
|
||||
.check_remove_signals()
|
||||
.await;
|
||||
let laser = match map {
|
||||
Some(map) => SerialDetection::get_path_by_id(laser_ident, map),
|
||||
None => None,
|
||||
};
|
||||
|
||||
port_result.removed.append(&mut removed_signals);
|
||||
let mut unique_ident: Option<MachineIdentificationUnique> = None;
|
||||
|
||||
port_result
|
||||
};
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
// 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),
|
||||
);
|
||||
}
|
||||
|
||||
// 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(())
|
||||
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));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
panic::{PanicDetails, send_panic},
|
||||
};
|
||||
use smol::channel::Sender;
|
||||
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)]
|
||||
@@ -62,56 +59,36 @@ async fn send_event_with_retry(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_socketio_queue(thread_panic_tx: Sender<PanicDetails>, app_state: Arc<AppState>) {
|
||||
std::thread::Builder::new()
|
||||
.name("socketio-queue".to_string())
|
||||
.spawn(move || {
|
||||
send_panic(thread_panic_tx);
|
||||
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))
|
||||
}
|
||||
Reference in new issue
Block a user