Compare commits
140
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97dca1d56d | ||
|
|
6f1e34e8dc | ||
|
|
2d366d1ac7 | ||
|
|
bf5c584594 | ||
|
|
ca6f7185e3 | ||
|
|
f51b14a382 | ||
|
|
5469a4e993 | ||
|
|
dc56396c89 | ||
|
|
ca66187dd0 | ||
|
|
88f4ebbee9 | ||
|
|
59796e8970 | ||
|
|
17138e06ee | ||
|
|
9ab498cbd4 | ||
|
|
88890288cd | ||
|
|
10eb564c7f | ||
|
|
b8a4eda728 | ||
|
|
93b6f26344 | ||
|
|
051fbc7642 | ||
|
|
4ffbbc61e4 | ||
|
|
acbc9e3cf7 | ||
|
|
ad3d0b620f | ||
|
|
35f2ead11b | ||
|
|
a13e757a98 | ||
|
|
cb7fa3cff8 | ||
|
|
5e86988ac4 | ||
|
|
2d28c18b1a | ||
|
|
b19764281a | ||
|
|
c25d63f781 | ||
|
|
d79efc412f | ||
|
|
fde8a7f01b | ||
|
|
448e8c5d68 | ||
|
|
7ad74cf089 | ||
|
|
6073d37892 | ||
|
|
c04937553f | ||
|
|
e08bdfa3c7 | ||
|
|
d282678b19 | ||
|
|
9ff4243350 | ||
|
|
6c79b33752 | ||
|
|
861b5da7a3 | ||
|
|
771284c23d | ||
|
|
7d230defea | ||
|
|
caf4ae47a0 | ||
|
|
8513201080 | ||
|
|
b080414bf2 | ||
|
|
7dd3d50653 | ||
|
|
ba8e04c641 | ||
|
|
cece245a55 | ||
|
|
fab4d2810e | ||
|
|
5bff3ca929 | ||
|
|
afe1d3b80e | ||
|
|
6c8005dba9 | ||
|
|
9f399ca6f3 | ||
|
|
a7574dd348 | ||
|
|
b39f3597c8 | ||
|
|
bb9a86d52e | ||
|
|
e5329e40c6 | ||
|
|
319611f69d | ||
|
|
4b16253c8c | ||
|
|
b60423fde6 | ||
|
|
5d3399c72e | ||
|
|
d247bafcbb | ||
|
|
5227aad4c6 | ||
|
|
da4c54a99e | ||
|
|
8d42bd93f7 | ||
|
|
37e467272b | ||
|
|
ed0b9cd5ae | ||
|
|
8045f799f0 | ||
|
|
ca6ea16f89 | ||
|
|
452614abf1 | ||
|
|
f63c797cd1 | ||
|
|
de791f6ea8 | ||
|
|
024f751ef1 | ||
|
|
d4c82bfd7d | ||
|
|
a49f19133d | ||
|
|
a94009514c | ||
|
|
d1d3724f52 | ||
|
|
9f4177aff9 | ||
|
|
e323fd76f8 | ||
|
|
b0d24d8502 | ||
|
|
8ae20c2aae | ||
|
|
89843514b8 | ||
|
|
1a9e39a49e | ||
|
|
f4d5e74c59 | ||
|
|
eb1301df99 | ||
|
|
2885fa91cc | ||
|
|
d89e4681db | ||
|
|
dedcd76241 | ||
|
|
e987dd07f9 | ||
|
|
6747433100 | ||
|
|
3ffd67918c | ||
|
|
3fe6556037 | ||
|
|
5ccc65241d | ||
|
|
be92fd0665 | ||
|
|
8a6d4d1385 | ||
|
|
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 |
No files matched your search
@@ -3,3 +3,5 @@ dist/
|
||||
.DS_Store
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
build.sh
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
# `2.12.0`
|
||||
_17.11.2025_
|
||||
|
||||
## General
|
||||
- [#847](https://github.com/qitechgmbh/control/pull/847) Improved discovery and initialization
|
||||
- [#859](https://github.com/qitechgmbh/control/pull/859) Improved update speed
|
||||
- [#897](https://github.com/qitechgmbh/control/pull/897), [#899](https://github.com/qitechgmbh/control/pull/899), [#852](https://github.com/qitechgmbh/control/pull/852) Improved system stability
|
||||
- [#817](https://github.com/qitechgmbh/control/pull/817) Added an update bar
|
||||
|
||||
## Laser
|
||||
- [#857](https://github.com/qitechgmbh/control/pull/857) Fixed potential issues with 2 Axis Laser
|
||||
|
||||
## Extruder V2
|
||||
- [#881](https://github.com/qitechgmbh/control/pull/881) Extended Temperature PID Configuration
|
||||
|
||||
|
||||
# `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_
|
||||
|
||||
|
||||
Generated
+144
-715
File diff suppressed because it is too large.
Load diff
+4
-1
@@ -4,8 +4,11 @@ members = [
|
||||
"ethercat-hal",
|
||||
"ethercat-hal-derive",
|
||||
"control-core",
|
||||
"ethercat-eeprom-dump",
|
||||
"machines",
|
||||
#"ethercat-eeprom-dump",
|
||||
"control-core-derive",
|
||||
"units",
|
||||
"utils",
|
||||
]
|
||||
exclude = []
|
||||
resolver = "2"
|
||||
|
||||
+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
|
||||
|
||||
@@ -12,17 +12,15 @@ anyhow = "1.0.100"
|
||||
axum = "0.8.6"
|
||||
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
units = { path = "../units" }
|
||||
ethercrab = "0.6"
|
||||
interfaces = "0.0.9"
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.143"
|
||||
serialport = "4.7.3"
|
||||
smol = "2.0.2"
|
||||
socketioxide = "0.17.2"
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
crc = "3.3.0"
|
||||
serial = "0.4.0"
|
||||
tracing = "0.1.41"
|
||||
erased-serde = "0.4.8"
|
||||
core_affinity = "0.8.3"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::radian_per_second,
|
||||
f64::{AngularAcceleration, AngularVelocity},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
f64::{Acceleration, Velocity},
|
||||
velocity::meter_per_second,
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angle::radian,
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::radian_per_second,
|
||||
@@ -199,8 +199,8 @@ impl AngularAccelerationPositionController {
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use control_core::controllers::second_degree_motion::angular_acceleration_position_controller::AngularAccelerationPositionController;
|
||||
/// use uom::si::f64::Angle;
|
||||
/// use uom::si::angle::degree;
|
||||
/// use units::f64::Angle;
|
||||
/// use units::angle::degree;
|
||||
///
|
||||
/// let mut controller = AngularAccelerationPositionController::new_simple(
|
||||
/// Some(Angle::new::<degree>(180.0)),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_jerk::radian_per_second_cubed,
|
||||
angular_velocity::radian_per_second,
|
||||
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// angular_velocity::revolution_per_minute,
|
||||
/// angular_acceleration::revolution_per_minute_per_second,
|
||||
/// angular_jerk::revolution_per_minute_per_second_squared,
|
||||
@@ -65,7 +65,7 @@ impl AngularJerkSpeedController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// angular_velocity::revolution_per_minute,
|
||||
/// angular_acceleration::revolution_per_minute_per_second,
|
||||
/// angular_jerk::revolution_per_minute_per_second_squared,
|
||||
@@ -127,7 +127,7 @@ impl AngularJerkSpeedController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// angular_velocity::revolution_per_minute,
|
||||
/// angular_acceleration::revolution_per_minute_per_second,
|
||||
/// angular_jerk::revolution_per_minute_per_second_squared,
|
||||
@@ -185,7 +185,7 @@ impl AngularJerkSpeedController {
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use std::time::Instant;
|
||||
/// use uom::si::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
|
||||
/// use units::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
|
||||
///
|
||||
/// let mut controller = AngularJerkSpeedController::new_simple(/* ... */);
|
||||
/// let target = AngularVelocity::new::<revolution_per_minute>(1500.0);
|
||||
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
f64::{Acceleration, Length, Velocity},
|
||||
length::meter,
|
||||
@@ -22,8 +22,8 @@ use super::acceleration_position_controller::{
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
|
||||
/// use uom::si::f64::{Length, Velocity, Acceleration};
|
||||
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
|
||||
/// use units::f64::{Length, Velocity, Acceleration};
|
||||
///
|
||||
/// let position_limit = Length::new::<meter>(10.0);
|
||||
/// let max_speed = Velocity::new::<meter_per_second>(2.0);
|
||||
@@ -101,8 +101,8 @@ impl LinearAccelerationPositionController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
|
||||
/// use uom::si::f64::{Length, Velocity, Acceleration};
|
||||
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
|
||||
/// use units::f64::{Length, Velocity, Acceleration};
|
||||
///
|
||||
/// let position_limit = Length::new::<meter>(5.0); // Creates [-5.0, +5.0] meter range
|
||||
/// let max_speed = Velocity::new::<meter_per_second>(1.0); // Creates [-1.0, +1.0] m/s range
|
||||
@@ -150,7 +150,7 @@ impl LinearAccelerationPositionController {
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use std::time::Instant;
|
||||
/// use uom::si::{length::meter, f64::Length};
|
||||
/// use units::{length::meter, f64::Length};
|
||||
///
|
||||
/// let mut controller = LinearAccelerationPositionController::new_simple(
|
||||
/// Some(Length::new::<meter>(10.0)),
|
||||
@@ -375,7 +375,7 @@ impl LinearAccelerationPositionController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
|
||||
/// use units::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
|
||||
///
|
||||
/// let new_position = Length::new::<meter>(0.0);
|
||||
///
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
f64::{Acceleration, Jerk, Velocity},
|
||||
jerk::meter_per_second_cubed,
|
||||
@@ -23,7 +23,7 @@ use super::jerk_speed_controller::JerkSpeedController;
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// velocity::meter_per_second,
|
||||
/// acceleration::meter_per_second_squared,
|
||||
/// jerk::meter_per_second_cubed,
|
||||
@@ -65,7 +65,7 @@ impl LinearJerkSpeedController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// velocity::meter_per_second,
|
||||
/// acceleration::meter_per_second_squared,
|
||||
/// jerk::meter_per_second_cubed,
|
||||
@@ -127,7 +127,7 @@ impl LinearJerkSpeedController {
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// use uom::si::{
|
||||
/// use units::{
|
||||
/// velocity::meter_per_second,
|
||||
/// acceleration::meter_per_second_squared,
|
||||
/// jerk::meter_per_second_cubed,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use uom::si::{angle::radian, f64::Angle};
|
||||
use units::{angle::radian, f64::Angle};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AngleConverter {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angle::revolution,
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
angular_velocity::revolution_per_second,
|
||||
@@ -90,7 +90,7 @@ mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
use std::f64::EPSILON;
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angle::degree, angular_acceleration::degree_per_second_squared,
|
||||
angular_velocity::degree_per_second,
|
||||
};
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
//!
|
||||
//! ```rust
|
||||
//! use control_core::converters::circular_converter::CircularConverter;
|
||||
//! use uom::si::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
|
||||
//! use units::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
|
||||
//!
|
||||
//! let converter = CircularConverter::from_radius(Length::new::<meter>(0.1));
|
||||
//! let velocity = Velocity::new::<meter_per_second>(1.0);
|
||||
//! let angular_velocity = converter.linear_to_angular_velocity(velocity);
|
||||
//! ```
|
||||
|
||||
use uom::si::{
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared,
|
||||
angle::radian,
|
||||
angular_acceleration::radian_per_second_squared,
|
||||
@@ -199,7 +199,7 @@ impl CircularConverter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use uom::si::length::centimeter;
|
||||
use units::length::centimeter;
|
||||
|
||||
#[test]
|
||||
fn test_constructors() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angle::revolution,
|
||||
angular_velocity::revolution_per_second,
|
||||
f64::{Acceleration, Angle, AngularAcceleration, AngularVelocity, Length, Velocity},
|
||||
@@ -251,7 +251,7 @@ mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
use std::f64::EPSILON;
|
||||
use uom::si::{
|
||||
use units::{
|
||||
acceleration::meter_per_second_squared, angular_acceleration::radian_per_second_squared,
|
||||
length::meter, velocity::meter_per_second,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use ethercrab::{
|
||||
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
|
||||
std::{ethercat_now, tx_rx_task},
|
||||
};
|
||||
use interfaces::Interface;
|
||||
use std::{process::Command, sync::Arc, time::Duration};
|
||||
|
||||
// Constants for EtherCAT configuration
|
||||
const IFACE_DISCOVERY_MAX_SUBDEVICES: usize = 16; // Must be power of 2 > 1
|
||||
@@ -12,14 +11,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 +68,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 +78,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 +87,39 @@ 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() {
|
||||
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(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// 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 {
|
||||
set_interface_managed(&interfaces[i].name, false);
|
||||
} 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 +157,7 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
|
||||
// Default 10000
|
||||
dc_static_sync_iterations: 10000,
|
||||
// Default None
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(5),
|
||||
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
|
||||
},
|
||||
));
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
pub mod eeprom_identification;
|
||||
pub mod interface_discovery;
|
||||
@@ -4,17 +4,11 @@ pub mod downcast;
|
||||
pub mod ethercat;
|
||||
pub mod helpers;
|
||||
pub mod irq_handling;
|
||||
pub mod machines;
|
||||
pub mod modbus;
|
||||
pub mod realtime;
|
||||
pub mod rest;
|
||||
pub mod serial;
|
||||
pub mod socketio;
|
||||
pub mod transmission;
|
||||
pub mod uom_extensions;
|
||||
|
||||
#[cfg(feature = "video-streaming")]
|
||||
pub mod video_streaming;
|
||||
|
||||
#[macro_use]
|
||||
extern crate uom;
|
||||
@@ -1,27 +0,0 @@
|
||||
use serde_json::Value;
|
||||
use smol::lock::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::socketio::namespace::Namespace;
|
||||
|
||||
pub trait MachineApi {
|
||||
fn api_mutate(&mut self, value: Value) -> Result<(), anyhow::Error>;
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>>;
|
||||
|
||||
/// Returns a list of available video stream identifiers for this machine
|
||||
#[cfg(feature = "video-streaming")]
|
||||
fn api_video_streams(&self) -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Get a video stream receiver for the specified stream ID
|
||||
/// Returns None if the stream doesn't exist
|
||||
#[cfg(feature = "video-streaming")]
|
||||
fn api_get_video_stream(
|
||||
&mut self,
|
||||
stream_id: &str,
|
||||
) -> Option<tokio::sync::broadcast::Receiver<Vec<u8>>> {
|
||||
let _ = stream_id;
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
use crate::downcast::Downcast;
|
||||
use crate::machines::identification::MachineIdentificationUnique;
|
||||
use crate::machines::manager::MachineManager;
|
||||
use crate::socketio::{event::GenericEvent, namespace::Namespace};
|
||||
use serde::Serialize;
|
||||
use smol::block_on;
|
||||
use smol::lock::RwLock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Weak;
|
||||
|
||||
use super::Machine;
|
||||
use anyhow::anyhow;
|
||||
use smol::{channel::Sender, lock::Mutex};
|
||||
use socketioxide::extract::SocketRef;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MachineConnection<M: Machine + ?Sized>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Error(anyhow::Error),
|
||||
Disconnected,
|
||||
Connected(Arc<Mutex<M>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineSlot<M: Machine + ?Sized>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
pub machine_connection: MachineConnection<M>,
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
pub type MachineConnectionGeneric = MachineConnection<dyn Machine>;
|
||||
|
||||
pub type MachineSlotGeneric = MachineSlot<dyn Machine>;
|
||||
|
||||
impl<M: Machine> Clone for MachineConnection<M> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Connected(m) => Self::Connected(m.clone()),
|
||||
Self::Error(e) => Self::Error(anyhow!(e.to_string())),
|
||||
Self::Disconnected => Self::Disconnected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Machine + ?Sized> MachineConnection<M> {
|
||||
pub fn to_error(&self) -> Option<anyhow::Error> {
|
||||
match self {
|
||||
MachineConnection::Error(err) => Some(anyhow!(err.to_string())),
|
||||
Self::Disconnected => Some(anyhow!("Machine is disconnected")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_machine(&self) -> Option<Arc<Mutex<M>>> {
|
||||
match self {
|
||||
Self::Connected(machine) => Some(machine.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Machine + ?Sized> MachineSlot<M> {
|
||||
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
|
||||
let namespace = Namespace::new(socket_queue_tx);
|
||||
Self {
|
||||
machine_connection: MachineConnection::Disconnected,
|
||||
namespace: Arc::new(Mutex::new(namespace)),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_connected(&self) -> bool {
|
||||
matches!(self.machine_connection, MachineConnection::Connected(_))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CrossConnectableMachine<
|
||||
F: CrossConnectableMachine<F, T>,
|
||||
T: CrossConnectableMachine<T, F>,
|
||||
>: Machine
|
||||
{
|
||||
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<F, T>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineCrossConnection<
|
||||
F: CrossConnectableMachine<F, T>,
|
||||
T: CrossConnectableMachine<T, F>,
|
||||
> {
|
||||
machine_manager: Weak<RwLock<MachineManager>>,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
connected_machine: Weak<Mutex<MachineSlot<T>>>,
|
||||
_make_compiler_happy: std::marker::PhantomData<F>, // This marks tells the typechecker, that we
|
||||
// need F in our type arguemnts. We are ok with
|
||||
// it being used only in a fake field. Without
|
||||
// F being fixed, we would not be anble to implement
|
||||
// reverse connecting and disconnecting properly,
|
||||
// as the type on the other side would decay
|
||||
// to an unknown type. See also https://github.com/qitechgmbh/control/pull/625#discussion_r2379566315
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct MachineCrossConnectionState {
|
||||
/// Connected Machine
|
||||
pub machine_identification_unique: Option<MachineIdentificationUnique>,
|
||||
pub is_available: bool,
|
||||
}
|
||||
|
||||
impl<F: CrossConnectableMachine<F, T>, T: CrossConnectableMachine<T, F>>
|
||||
MachineCrossConnection<F, T>
|
||||
{
|
||||
pub fn new(
|
||||
manager: Weak<RwLock<MachineManager>>,
|
||||
machine_identification_unique: &MachineIdentificationUnique,
|
||||
) -> Self {
|
||||
Self {
|
||||
machine_manager: manager,
|
||||
connected_machine: Weak::new(),
|
||||
machine_identification_unique: machine_identification_unique.clone(),
|
||||
_make_compiler_happy: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_connected_machine(
|
||||
&mut self,
|
||||
machine_identification_unique: &MachineIdentificationUnique,
|
||||
) {
|
||||
if let Some(machine_manager) = self.machine_manager.upgrade() {
|
||||
let machine_manager = block_on(machine_manager.read());
|
||||
|
||||
let slot: Arc<Mutex<MachineSlotGeneric>> =
|
||||
match machine_manager.get(&machine_identification_unique) {
|
||||
Some(c) => c,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let slot: Arc<Mutex<MachineSlot<T>>> = match Downcast::downcast(&slot) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
self.connected_machine = Arc::downgrade(&slot);
|
||||
} else {
|
||||
panic!("The machine manager has died, cannot do anything at this point");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse_connect(&self) {
|
||||
if let Some(slot) = self.connected_machine.upgrade() {
|
||||
let slot = &mut slot.lock_blocking();
|
||||
|
||||
if let MachineConnection::Connected(machine) = &slot.machine_connection {
|
||||
let mut machine = machine.lock_blocking();
|
||||
let cross_connection = machine.get_cross_connection();
|
||||
|
||||
if !cross_connection.is_connected() {
|
||||
cross_connection.set_connected_machine(&self.machine_identification_unique);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect(&mut self) {
|
||||
self.connected_machine = Weak::new();
|
||||
}
|
||||
|
||||
pub fn reverse_disconnect(&mut self) {
|
||||
if let Some(slot) = self.connected_machine.upgrade() {
|
||||
let slot = slot.lock_blocking();
|
||||
|
||||
if let MachineConnection::Connected(machine) = &slot.machine_connection {
|
||||
let mut machine = machine.lock_blocking();
|
||||
let cross_connection = machine.get_cross_connection();
|
||||
cross_connection.disconnect();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
let arc = self.connected_machine.upgrade();
|
||||
arc.is_some()
|
||||
}
|
||||
|
||||
pub fn to_state(&self) -> MachineCrossConnectionState {
|
||||
if let Some(slot) = self.connected_machine.upgrade() {
|
||||
let connection = &slot.lock_blocking().machine_connection;
|
||||
|
||||
if let MachineConnection::Connected(machine) = connection {
|
||||
let machine = machine.lock_blocking();
|
||||
|
||||
return MachineCrossConnectionState {
|
||||
machine_identification_unique: Some(
|
||||
machine.get_machine_identification_unique(),
|
||||
),
|
||||
is_available: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
MachineCrossConnectionState {
|
||||
machine_identification_unique: None,
|
||||
is_available: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Identifies a spacifi machine
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct MachineIdentificationUnique {
|
||||
pub machine_identification: MachineIdentification,
|
||||
pub serial: u16,
|
||||
}
|
||||
|
||||
impl MachineIdentificationUnique {
|
||||
/// Check if values are non-zero
|
||||
pub const fn is_valid(&self) -> bool {
|
||||
self.machine_identification.is_valid() && self.serial != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MachineIdentificationUnique {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}/{}/{}",
|
||||
self.machine_identification.vendor, self.machine_identification.machine, self.serial
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifies a machine
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct MachineIdentification {
|
||||
pub vendor: u16,
|
||||
pub machine: u16,
|
||||
}
|
||||
|
||||
impl MachineIdentification {
|
||||
/// Check if values are non-zero
|
||||
pub const fn is_valid(&self) -> bool {
|
||||
self.vendor != 0 && self.machine != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DeviceMachineIdentification {
|
||||
pub machine_identification_unique: MachineIdentificationUnique,
|
||||
pub role: u16,
|
||||
}
|
||||
|
||||
impl DeviceMachineIdentification {
|
||||
/// Check if values are non-zero
|
||||
pub const fn is_valid(&self) -> bool {
|
||||
self.machine_identification_unique.is_valid()
|
||||
&& self
|
||||
.machine_identification_unique
|
||||
.machine_identification
|
||||
.machine
|
||||
!= 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DeviceIdentification {
|
||||
pub device_machine_identification: Option<DeviceMachineIdentification>,
|
||||
pub device_hardware_identification: DeviceHardwareIdentification,
|
||||
}
|
||||
|
||||
impl From<DeviceIdentificationIdentified> for DeviceIdentification {
|
||||
fn from(value: DeviceIdentificationIdentified) -> Self {
|
||||
Self {
|
||||
device_machine_identification: Some(value.device_machine_identification),
|
||||
device_hardware_identification: value.device_hardware_identification,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DeviceIdentificationIdentified {
|
||||
pub device_machine_identification: DeviceMachineIdentification,
|
||||
pub device_hardware_identification: DeviceHardwareIdentification,
|
||||
}
|
||||
|
||||
impl TryFrom<DeviceIdentification> for DeviceIdentificationIdentified {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: DeviceIdentification) -> Result<Self, Self::Error> {
|
||||
let device_machine_identification =
|
||||
value.device_machine_identification.ok_or(anyhow::anyhow!(
|
||||
"[{}::DeviceIdentificationIdentified::try_from] No device machine identification",
|
||||
module_path!()
|
||||
))?;
|
||||
|
||||
Ok(Self {
|
||||
device_machine_identification,
|
||||
device_hardware_identification: value.device_hardware_identification,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum DeviceHardwareIdentification {
|
||||
Ethercat(DeviceHardwareIdentificationEthercat),
|
||||
Serial(DeviceHardwareIdentificationSerial),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DeviceHardwareIdentificationEthercat {
|
||||
pub subdevice_index: usize,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct DeviceHardwareIdentificationSerial {
|
||||
pub path: String,
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
use super::{
|
||||
Machine,
|
||||
identification::{DeviceIdentification, DeviceIdentificationIdentified},
|
||||
new::MachineNewHardwareSerial,
|
||||
registry::MachineRegistry,
|
||||
};
|
||||
use smol::{
|
||||
channel::Sender,
|
||||
lock::{Mutex, RwLock},
|
||||
};
|
||||
use socketioxide::extract::SocketRef;
|
||||
|
||||
use crate::{
|
||||
machines::{
|
||||
connection::{
|
||||
MachineConnection, MachineConnectionGeneric, MachineSlot, MachineSlotGeneric,
|
||||
},
|
||||
identification::MachineIdentificationUnique,
|
||||
new::{MachineNewHardware, MachineNewHardwareEthercat, MachineNewParams},
|
||||
},
|
||||
serial::SerialDevice,
|
||||
socketio::event::GenericEvent,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MachineManager {
|
||||
pub ethercat_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
pub serial_machines: HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
}
|
||||
|
||||
impl Default for MachineManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ethercat_machines: HashMap::new(),
|
||||
serial_machines: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ethercat_devices<const MAX_SUBDEVICES: usize, const MAX_PDI: usize>(
|
||||
&mut self,
|
||||
device_identifications: &Vec<DeviceIdentification>,
|
||||
machine_registry: &MachineRegistry,
|
||||
hardware: &MachineNewHardwareEthercat,
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_manager: Weak<RwLock<Self>>,
|
||||
) {
|
||||
// group devices by machine device identification
|
||||
let device_grouping_result = group_devices_by_identification(device_identifications);
|
||||
|
||||
tracing::info!(
|
||||
"Device Groups {}",
|
||||
device_grouping_result.device_groups.len()
|
||||
);
|
||||
|
||||
let machine_new_hardware = MachineNewHardware::Ethercat(hardware);
|
||||
|
||||
// iterate over all identified device groups but ignore unaffected machines
|
||||
for device_group in device_grouping_result.device_groups.iter() {
|
||||
// get the machine identification
|
||||
let machine_identification_unique: MachineIdentificationUnique =
|
||||
match device_group.first() {
|
||||
Some(device_identification) => device_identification
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone(),
|
||||
None => continue, // Skip this group if empty
|
||||
};
|
||||
|
||||
let slot =
|
||||
self.get_or_create_slot(socket_queue_tx.clone(), machine_identification_unique);
|
||||
let mut slot = slot.lock_blocking();
|
||||
|
||||
// create the machine
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
device_group,
|
||||
hardware: &machine_new_hardware,
|
||||
socket_queue_tx: socket_queue_tx.clone(),
|
||||
machine_manager: machine_manager.clone(),
|
||||
namespace: slot.namespace.clone(),
|
||||
});
|
||||
|
||||
slot.machine_connection = match new_machine {
|
||||
Err(err) => MachineConnectionGeneric::Error(err),
|
||||
Ok(machine) => MachineConnectionGeneric::Connected(machine),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn get_or_create_slot(
|
||||
&mut self,
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_identification: MachineIdentificationUnique,
|
||||
) -> Arc<Mutex<MachineSlotGeneric>> {
|
||||
if let Some(slot) = self.get(&machine_identification) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
let slot = Arc::new(Mutex::new(MachineSlot::new(socket_queue_tx)));
|
||||
self.ethercat_machines
|
||||
.insert(machine_identification, slot.clone());
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Arc<Mutex<MachineSlotGeneric>>> {
|
||||
self.ethercat_machines
|
||||
.get(machine_identification)
|
||||
.or_else(|| self.serial_machines.get(machine_identification))
|
||||
.map(|x| x.clone())
|
||||
}
|
||||
|
||||
pub fn add_serial_device(
|
||||
&mut self,
|
||||
device_identification: &DeviceIdentification,
|
||||
device: Arc<RwLock<dyn SerialDevice>>,
|
||||
machine_registry: &MachineRegistry,
|
||||
socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
machine_manager: Weak<RwLock<Self>>,
|
||||
) {
|
||||
let hardware = MachineNewHardwareSerial { device };
|
||||
|
||||
let device_identification_identified: DeviceIdentificationIdentified =
|
||||
device_identification
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("Serial devices always have machine identification");
|
||||
|
||||
let machine_identification: MachineIdentificationUnique = device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone();
|
||||
|
||||
let slot = self.get_or_create_slot(socket_queue_tx.clone(), machine_identification);
|
||||
let mut slot = slot.lock_blocking();
|
||||
|
||||
let new_machine = machine_registry.new_machine(&MachineNewParams {
|
||||
device_group: &vec![device_identification_identified.clone()],
|
||||
hardware: &MachineNewHardware::Serial(&hardware),
|
||||
socket_queue_tx,
|
||||
machine_manager: machine_manager.clone(),
|
||||
namespace: slot.namespace.clone(),
|
||||
});
|
||||
|
||||
slot.machine_connection = match new_machine {
|
||||
Err(err) => MachineConnectionGeneric::Error(err),
|
||||
Ok(machine) => MachineConnectionGeneric::Connected(machine),
|
||||
};
|
||||
|
||||
tracing::info!("Adding serial machine {:?}", slot);
|
||||
}
|
||||
|
||||
pub fn remove_serial_device(&mut self, device_identification: &DeviceIdentification) {
|
||||
let device_identification_identified: DeviceIdentificationIdentified =
|
||||
device_identification
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("Serial devices always have machine identification");
|
||||
|
||||
tracing::info!(
|
||||
"Removing serial machine {:?}",
|
||||
device_identification_identified
|
||||
);
|
||||
|
||||
let machine_identification_unique = &device_identification_identified
|
||||
.device_machine_identification
|
||||
.machine_identification_unique;
|
||||
|
||||
if let Some(slot) = self.serial_machines.get(machine_identification_unique) {
|
||||
let mut slot = slot.lock_blocking();
|
||||
slot.machine_connection = MachineConnection::Disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ethercat_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
self.get_weak(&self.ethercat_machines, machine_identification)
|
||||
}
|
||||
|
||||
pub fn get_serial_weak(
|
||||
&self,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
self.get_weak(&self.serial_machines, machine_identification)
|
||||
}
|
||||
|
||||
fn get_weak(
|
||||
&self,
|
||||
machines: &HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
machine_identification: &MachineIdentificationUnique,
|
||||
) -> Option<Weak<Mutex<dyn Machine>>> {
|
||||
let slot = machines.get(machine_identification).clone();
|
||||
|
||||
return slot.and_then(|slot| {
|
||||
let connection = &slot.lock_blocking().machine_connection;
|
||||
match connection {
|
||||
MachineConnectionGeneric::Error(_) => None,
|
||||
MachineConnectionGeneric::Disconnected => None,
|
||||
MachineConnectionGeneric::Connected(machine) => Some(Arc::downgrade(&machine)),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Groups devices by machine identification
|
||||
///
|
||||
/// Returns a DeviceGroupingResult containing:
|
||||
/// - device_groups: a vector of devices grouped by machine identification
|
||||
/// - unidentified_devices: a vector of devices that could not be identified
|
||||
pub fn group_devices_by_identification(
|
||||
device_identifications: &Vec<DeviceIdentification>,
|
||||
) -> DeviceGroupingResult {
|
||||
let mut device_groups: Vec<Vec<DeviceIdentificationIdentified>> = Vec::new();
|
||||
let mut unidentified_devices: Vec<DeviceIdentification> = Vec::new();
|
||||
|
||||
for device_identification in device_identifications {
|
||||
// if vendor or serial or machine is 0, it is not a valid machine device
|
||||
if let Some(device_machine_identification) =
|
||||
device_identification.device_machine_identification.as_ref()
|
||||
{
|
||||
if !device_machine_identification.is_valid() {
|
||||
unidentified_devices.push(device_identification.clone());
|
||||
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
unidentified_devices.push(device_identification.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
// scan over all deice groups
|
||||
// get the first DeviceMachineIdentification
|
||||
// compare and append to the group
|
||||
let mut found = false;
|
||||
for check_group in device_groups.iter_mut() {
|
||||
// get first device in group
|
||||
let first_device = check_group.first().expect("group to not be empty");
|
||||
let first_device_machine_identification = &first_device
|
||||
.device_machine_identification
|
||||
.machine_identification_unique;
|
||||
|
||||
// chek if it has machine identification
|
||||
if let Some(device_machine_identification) =
|
||||
device_identification.device_machine_identification.as_ref()
|
||||
{
|
||||
// compare with the current device
|
||||
if first_device_machine_identification
|
||||
== &device_machine_identification.machine_identification_unique
|
||||
{
|
||||
let device_identification_identified = device_identification
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("should have Some(DeviceMachineIdentification)");
|
||||
check_group.push(device_identification_identified);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
let device_identification_identified = device_identification
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("should have Some(DeviceMachineIdentification)");
|
||||
device_groups.push(vec![device_identification_identified]);
|
||||
}
|
||||
}
|
||||
|
||||
DeviceGroupingResult {
|
||||
device_groups,
|
||||
unidentified_devices,
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure to hold the result of grouping devices by identification
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceGroupingResult {
|
||||
/// Devices grouped by machine identification
|
||||
pub device_groups: Vec<Vec<DeviceIdentificationIdentified>>,
|
||||
/// Devices that could not be identified
|
||||
pub unidentified_devices: Vec<DeviceIdentification>,
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use smol::lock::Mutex;
|
||||
|
||||
use crate::machines::connection::MachineSlotGeneric;
|
||||
|
||||
use super::{identification::MachineIdentificationUnique, manager::MachineManager};
|
||||
|
||||
pub enum MachineManagerIter<'a> {
|
||||
EthercatMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Arc<Mutex<MachineSlotGeneric>>,
|
||||
>,
|
||||
},
|
||||
SerialMachines {
|
||||
iter: std::collections::hash_map::Iter<
|
||||
'a,
|
||||
MachineIdentificationUnique,
|
||||
Arc<Mutex<MachineSlotGeneric>>,
|
||||
>,
|
||||
},
|
||||
Done,
|
||||
}
|
||||
|
||||
pub struct MachineManagerIterator<'a> {
|
||||
iter: MachineManagerIter<'a>,
|
||||
serial_machines: &'a HashMap<MachineIdentificationUnique, Arc<Mutex<MachineSlotGeneric>>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MachineManagerIterator<'a> {
|
||||
type Item = (
|
||||
&'a MachineIdentificationUnique,
|
||||
&'a Arc<Mutex<MachineSlotGeneric>>,
|
||||
);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
match &mut self.iter {
|
||||
MachineManagerIter::EthercatMachines { iter } => {
|
||||
// Try to get the next item from ethercat_machines
|
||||
if let Some(item) = iter.next() {
|
||||
return Some(item);
|
||||
} else {
|
||||
// If ethercat_machines is exhausted, move to serial_machines
|
||||
self.iter = MachineManagerIter::SerialMachines {
|
||||
iter: self.serial_machines.iter(),
|
||||
};
|
||||
// Continue to the next iteration of the loop
|
||||
continue;
|
||||
}
|
||||
}
|
||||
MachineManagerIter::SerialMachines { iter } => {
|
||||
// Return the next item from serial_machines or None if exhausted
|
||||
if let Some(item) = iter.next() {
|
||||
return Some(item);
|
||||
} else {
|
||||
// If serial_machines is also exhausted, we're done
|
||||
self.iter = MachineManagerIter::Done;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
MachineManagerIter::Done => {
|
||||
// We've exhausted both collections
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineManager {
|
||||
// Returns an iterator over all machines (both ethercat and serial)
|
||||
pub fn iter(&self) -> MachineManagerIterator<'_> {
|
||||
MachineManagerIterator {
|
||||
iter: MachineManagerIter::EthercatMachines {
|
||||
iter: self.ethercat_machines.iter(),
|
||||
},
|
||||
serial_machines: &self.serial_machines,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use new::MachineNewTrait;
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::machines::{
|
||||
api::MachineApi, identification::MachineIdentificationUnique, new::MachineAct,
|
||||
};
|
||||
pub mod api;
|
||||
pub mod connection;
|
||||
pub mod identification;
|
||||
pub mod manager;
|
||||
pub mod manager_iter;
|
||||
pub mod new;
|
||||
pub mod registry;
|
||||
|
||||
pub trait Machine: MachineAct + MachineNewTrait + MachineApi + Any + Debug + Send + Sync {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique;
|
||||
}
|
||||
|
||||
pub trait AnyGetters: Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
use crate::{
|
||||
machines::{identification::MachineIdentificationUnique, manager::MachineManager},
|
||||
serial::SerialDevice,
|
||||
socketio::namespace::Namespace,
|
||||
};
|
||||
|
||||
use super::identification::DeviceIdentificationIdentified;
|
||||
use anyhow::Error;
|
||||
use ethercat_hal::{
|
||||
devices::EthercatDevice, helpers::ethercrab_types::EthercrabSubDevicePreoperational,
|
||||
};
|
||||
use ethercrab::{SubDevice, SubDeviceRef};
|
||||
use smol::{
|
||||
channel::Sender,
|
||||
lock::{Mutex, RwLock},
|
||||
};
|
||||
use socketioxide::extract::SocketRef;
|
||||
use std::{
|
||||
sync::{Arc, Weak},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::socketio::event::GenericEvent;
|
||||
|
||||
pub trait MachineNewTrait {
|
||||
fn new(params: &MachineNewParams<'_, '_, '_, '_, '_, '_, '_>) -> Result<Self, Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
pub trait MachineAct {
|
||||
fn act(&mut self, now: Instant);
|
||||
}
|
||||
|
||||
pub struct MachineNewParams<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'device_identifications_identified,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware_serial,
|
||||
'machine_new_hardware,
|
||||
> where
|
||||
'maindevice: 'machine_new_hardware,
|
||||
'subdevices: 'machine_new_hardware,
|
||||
'ethercat_devices: 'machine_new_hardware,
|
||||
'machine_new_hardware_etehrcat: 'machine_new_hardware,
|
||||
{
|
||||
pub device_group: &'device_identifications_identified Vec<DeviceIdentificationIdentified>,
|
||||
pub hardware: &'machine_new_hardware MachineNewHardware<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware_serial,
|
||||
>,
|
||||
pub socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>,
|
||||
pub machine_manager: Weak<RwLock<MachineManager>>,
|
||||
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
}
|
||||
|
||||
impl MachineNewParams<'_, '_, '_, '_, '_, '_, '_> {
|
||||
pub fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.device_group
|
||||
.first()
|
||||
.expect("device group must have at least one device")
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MachineNewHardware<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'ethercat_devices,
|
||||
'machine_new_hardware_etehrcat,
|
||||
'machine_new_hardware_serial,
|
||||
> where
|
||||
'maindevice: 'machine_new_hardware_etehrcat,
|
||||
'subdevices: 'machine_new_hardware_etehrcat,
|
||||
'ethercat_devices: 'machine_new_hardware_etehrcat,
|
||||
{
|
||||
Ethercat(
|
||||
&'machine_new_hardware_etehrcat MachineNewHardwareEthercat<
|
||||
'maindevice,
|
||||
'subdevices,
|
||||
'ethercat_devices,
|
||||
>,
|
||||
),
|
||||
Serial(&'machine_new_hardware_serial MachineNewHardwareSerial),
|
||||
}
|
||||
|
||||
pub struct MachineNewHardwareEthercat<'maindevice, 'subdevices, 'ethercat_devices> {
|
||||
pub subdevices:
|
||||
&'subdevices Vec<&'subdevices SubDeviceRef<'maindevice, &'subdevices SubDevice>>,
|
||||
pub ethercat_devices: &'ethercat_devices Vec<Arc<RwLock<dyn EthercatDevice>>>,
|
||||
}
|
||||
|
||||
pub struct MachineNewHardwareSerial {
|
||||
pub device: Arc<RwLock<dyn SerialDevice>>,
|
||||
}
|
||||
|
||||
// validates that all devices in the group have the same machine identification
|
||||
pub fn validate_same_machine_identification_unique(
|
||||
identified_device_group: &Vec<DeviceIdentificationIdentified>,
|
||||
) -> Result<(), Error> {
|
||||
let machine_identification_unique = &identified_device_group
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"[{}::validate_same_machine_identification] No devices in group",
|
||||
module_path!()
|
||||
)
|
||||
})?
|
||||
.device_machine_identification
|
||||
.machine_identification_unique;
|
||||
for device in identified_device_group.iter() {
|
||||
if device
|
||||
.device_machine_identification
|
||||
.machine_identification_unique
|
||||
!= *machine_identification_unique
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::validate_same_machine_identification] Different machine identifications",
|
||||
module_path!()
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// validates that every role is unique
|
||||
pub fn validate_no_role_dublicates(
|
||||
identified_device_group: &Vec<DeviceIdentificationIdentified>,
|
||||
) -> Result<(), Error> {
|
||||
let mut roles = vec![];
|
||||
for device in identified_device_group.iter() {
|
||||
if roles.contains(&device.device_machine_identification.role) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"[{}::validate_no_role_dublicates] Role dublicate",
|
||||
module_path!(),
|
||||
));
|
||||
}
|
||||
roles.push(device.device_machine_identification.role);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Inside control_core::machines::new module:
|
||||
pub fn get_device_identification_by_role(
|
||||
identified_device_group: &Vec<DeviceIdentificationIdentified>,
|
||||
role: u16,
|
||||
) -> Result<&DeviceIdentificationIdentified, Error> {
|
||||
for device in identified_device_group.iter() {
|
||||
if device.device_machine_identification.role == role {
|
||||
return Ok(device);
|
||||
}
|
||||
}
|
||||
Err(anyhow::anyhow!(
|
||||
"[{}::get_device_identification_by_role] Role {} not found",
|
||||
module_path!(),
|
||||
role
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_device_by_index<'maindevice>(
|
||||
devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
|
||||
subdevice_index: usize,
|
||||
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
|
||||
Ok(devices
|
||||
.get(subdevice_index)
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"[{}::get_device_by_index] Index {} out of bounds for devices",
|
||||
module_path!(),
|
||||
subdevice_index
|
||||
))?
|
||||
.clone())
|
||||
}
|
||||
|
||||
pub fn get_subdevice_by_index<'subdevices, 'maindevice>(
|
||||
subdevices: &'subdevices Vec<&EthercrabSubDevicePreoperational<'maindevice>>,
|
||||
subdevice_index: usize,
|
||||
) -> Result<&'subdevices EthercrabSubDevicePreoperational<'maindevice>, Error> {
|
||||
Ok(subdevices.get(subdevice_index).ok_or(anyhow::anyhow!(
|
||||
"Index {} out of bounds for subdevices",
|
||||
subdevice_index
|
||||
))?)
|
||||
}
|
||||
|
||||
pub fn get_ethercat_device_by_index<'maindevice>(
|
||||
ethercat_devices: &Vec<Arc<RwLock<dyn EthercatDevice>>>,
|
||||
subdevice_index: usize,
|
||||
) -> Result<Arc<RwLock<dyn EthercatDevice>>, Error> {
|
||||
Ok(ethercat_devices
|
||||
.get(subdevice_index)
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"[{}::get_ethercat_device_by_index] Index {} out of bounds for ethercat devices",
|
||||
module_path!(),
|
||||
subdevice_index
|
||||
))?
|
||||
.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::machines::identification::{
|
||||
DeviceHardwareIdentification, DeviceHardwareIdentificationEthercat,
|
||||
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
|
||||
};
|
||||
|
||||
pub use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_device_identification_by_role() {
|
||||
let device_identifications = vec![
|
||||
// role 0
|
||||
DeviceIdentificationIdentified {
|
||||
device_machine_identification: DeviceMachineIdentification {
|
||||
machine_identification_unique: MachineIdentificationUnique {
|
||||
machine_identification: MachineIdentification {
|
||||
vendor: 1,
|
||||
machine: 1,
|
||||
},
|
||||
serial: 1,
|
||||
},
|
||||
role: 0,
|
||||
},
|
||||
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
|
||||
DeviceHardwareIdentificationEthercat { subdevice_index: 0 },
|
||||
),
|
||||
},
|
||||
// role 1
|
||||
DeviceIdentificationIdentified {
|
||||
device_machine_identification: DeviceMachineIdentification {
|
||||
machine_identification_unique: MachineIdentificationUnique {
|
||||
machine_identification: MachineIdentification {
|
||||
vendor: 2,
|
||||
machine: 2,
|
||||
},
|
||||
serial: 2,
|
||||
},
|
||||
role: 2,
|
||||
},
|
||||
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
|
||||
DeviceHardwareIdentificationEthercat { subdevice_index: 1 },
|
||||
),
|
||||
},
|
||||
// role 2
|
||||
DeviceIdentificationIdentified {
|
||||
device_machine_identification: DeviceMachineIdentification {
|
||||
machine_identification_unique: MachineIdentificationUnique {
|
||||
machine_identification: MachineIdentification {
|
||||
vendor: 3,
|
||||
machine: 3,
|
||||
},
|
||||
serial: 3,
|
||||
},
|
||||
role: 1,
|
||||
},
|
||||
device_hardware_identification: DeviceHardwareIdentification::Ethercat(
|
||||
DeviceHardwareIdentificationEthercat { subdevice_index: 2 },
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// search for role 0
|
||||
let result = get_device_identification_by_role(&device_identifications, 0);
|
||||
assert_eq!(result.unwrap().device_machine_identification.role, 0,);
|
||||
|
||||
// search for role 1
|
||||
let result = get_device_identification_by_role(&device_identifications, 1);
|
||||
assert_eq!(result.unwrap().device_machine_identification.role, 1,);
|
||||
// search for role 2
|
||||
let result = get_device_identification_by_role(&device_identifications, 2);
|
||||
assert_eq!(result.unwrap().device_machine_identification.role, 2,);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod mutation;
|
||||
@@ -1,14 +1,10 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
/*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;
|
||||
|
||||
pub trait SerialDevice: Any + Send + Sync + SerialDeviceNew + Debug {}
|
||||
@@ -27,11 +23,11 @@ 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>,
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod event;
|
||||
pub mod namespace;
|
||||
pub mod namespace_id;
|
||||
@@ -4,7 +4,7 @@ use socketioxide::extract::SocketRef;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Namespace {
|
||||
pub sockets: Vec<SocketRef>,
|
||||
pub events: HashMap<String, Vec<Arc<GenericEvent>>>,
|
||||
@@ -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() {
|
||||
@@ -168,7 +167,7 @@ impl Namespace {
|
||||
|
||||
impl Drop for Namespace {
|
||||
fn drop(&mut self) {
|
||||
self.disconnect_all();
|
||||
//self.disconnect_all();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use uom::si::{
|
||||
use units::{
|
||||
angle::radian,
|
||||
angular_velocity::radian_per_second,
|
||||
f64::{Angle, AngularVelocity, Length, Velocity},
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::acceleration;
|
||||
|
||||
@meter_per_minute_per_second: 0.016_666_666_666_666_67; "m/min/s", "meters per minute per second",
|
||||
"meters per minute per second";
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::angular_acceleration;
|
||||
|
||||
@revolution_per_minute_per_second: 0.104_719_755_119_659_77; "rev/min/s", "revolutions per minute per second",
|
||||
"revolutions per minute per second";
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::angular_jerk;
|
||||
|
||||
@revolution_per_minute_per_second_squared: 0.104_719_755_119_659_77; "rev/min/s²", "revolution per minute per second squared",
|
||||
"revolution per minute per second squared";
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::jerk;
|
||||
|
||||
@meter_per_minute_per_second_squared: 0.016_666_666_666_666_666_67; "m/min/s²", "meters per minute per second squared",
|
||||
"meters per minute per second squared";
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod acceleration;
|
||||
pub mod angular_acceleration;
|
||||
pub mod angular_jerk;
|
||||
pub mod jerk;
|
||||
pub mod velocity;
|
||||
@@ -1,7 +0,0 @@
|
||||
unit! {
|
||||
system: uom::si;
|
||||
quantity: uom::si::velocity;
|
||||
|
||||
@meter_per_minute: 0.016_666_666_666_666_67; "m/min", "meters per minute",
|
||||
"meters per minute";
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,69 @@
|
||||
# Laser DRE Device Documentation
|
||||
|
||||
## Overview
|
||||
The **Laser DRE** is a precision measurement device designed for real-time diameter measurement of rotating objects. It communicates over a **USB Serial interface** using the **Modbus RTU** protocol.
|
||||
|
||||
Key features include:
|
||||
|
||||
- High-speed diameter measurement
|
||||
- Dual-axis capability
|
||||
- USB Serial communication
|
||||
- Modbus RTU protocol support
|
||||
|
||||
---
|
||||
|
||||
## Technical Specifications
|
||||
|
||||
- **Communication Interface:** USB Serial
|
||||
- **Protocol:** Modbus RTU
|
||||
- **Axes:** 2-axis measurement
|
||||
- **Polling Rate:** Every 16 ms
|
||||
- **Measurement Rate:** 320 measurements per second (manufacturer specification)
|
||||
- **Measurement Type:** Diameter measurement | X/Y Axis width
|
||||
|
||||
---
|
||||
|
||||
## Operation
|
||||
|
||||
### Polling
|
||||
The device **polls the diameter every 16 milliseconds**, ensuring near real-time data acquisition for precise monitoring.
|
||||
|
||||
### Measurement
|
||||
According to the manufacturer, the Laser DRE can measure the diameter **320 times per second**, providing high-resolution data for process control and analysis.
|
||||
|
||||
### Axes
|
||||
The device is capable of measuring across **two axes**, allowing for comprehensive profiling of the object’s diameter and shape.
|
||||
|
||||
---
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
The device uses **Modbus RTU** over the USB Serial connection. Key points for communication:
|
||||
|
||||
- **Master/Slave Model:** The Laser DRE acts as a Modbus slave device.
|
||||
- **Function Codes:** Supports standard Modbus RTU function codes for reading measurements.
|
||||
- **Data Format:** Diameter measurements are transmitted in the format specified by the manufacturer (consult the device manual for exact register addresses and scaling).
|
||||
|
||||
---
|
||||
|
||||
## Settings
|
||||
|
||||
The device includes a small display that allows changing several operational settings:
|
||||
|
||||
- **Unit:**
|
||||
The laser can operate in **millimeters (mm)** or **inches (in)**.
|
||||
Our software uses **millimeters**.
|
||||
|
||||
- **Alarm:**
|
||||
The laser can emit an audible alarm if the measured diameter is out of tolerance.
|
||||
⚠️ **Note:** The tolerance settings on the laser may differ from the tolerance configured in our software.
|
||||
|
||||
- **Communication:**
|
||||
Several communication parameters can be configured:
|
||||
- **Set UART:** Configure **parity (8N1)** and **baud rate (38400)**.
|
||||
- **Set Protocol:** Choose the communication protocol. *(We use **Modbus RTU**.)*
|
||||
- **Set Slave Address:** The slave address of our laser is **1**.
|
||||
|
||||
- **Calibration:**
|
||||
The laser can be calibrated using **4 mm** and **8 mm** reference rods.
|
||||
|
||||
Generated
+98
-43
@@ -8,7 +8,7 @@
|
||||
"name": "qitech-control-electron",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^4.1.2",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@icons-pack/react-simple-icons": "^11.2.0",
|
||||
"@jub3i/tree-kill": "^0.0.15",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
@@ -64,13 +64,13 @@
|
||||
"@electron-forge/maker-squirrel": "^7.6.1",
|
||||
"@electron-forge/maker-zip": "^7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
|
||||
"@electron-forge/plugin-fuses": "^7.8.0",
|
||||
"@electron-forge/plugin-fuses": "^7.10.2",
|
||||
"@electron-forge/plugin-vite": "^7.6.1",
|
||||
"@electron-forge/shared-types": "^7.4.0",
|
||||
"@electron/fuses": "^2.0.0",
|
||||
"@electron/fuses": "^1.8.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"
|
||||
}
|
||||
},
|
||||
@@ -926,19 +926,62 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/plugin-fuses": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-fuses/-/plugin-fuses-7.8.2.tgz",
|
||||
"integrity": "sha512-BrN26y8xRuz43aJasktLvze2rh9XIxN6m4FR5NJvXbdcS0vUQvLSOgRm9koV5YL0/zr99OS5FXLiw04U+kKy1g==",
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-fuses/-/plugin-fuses-7.10.2.tgz",
|
||||
"integrity": "sha512-X8FaBL5pVvKCTBNaa9EjbH6vuaeIU7UcPSmP9501XF4zcKPCfTbQKz49LTMl7gd5YzUm82IlqRjte12LLpcSDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/plugin-base": "7.8.2",
|
||||
"@electron-forge/shared-types": "7.8.2"
|
||||
"@electron-forge/plugin-base": "7.10.2",
|
||||
"@electron-forge/shared-types": "7.10.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@electron/fuses": ">=1.0.0"
|
||||
"@electron/fuses": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/plugin-fuses/node_modules/@electron-forge/plugin-base": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-7.10.2.tgz",
|
||||
"integrity": "sha512-+4YLmkLZxvS6JFXYNI4dHt8Il8iIvwk2o6lCJGwNysOUq2KOZ3Wu1He4Ko8HhKcO1VWbFvslbh57oQn963Aryw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/shared-types": "7.10.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/plugin-fuses/node_modules/@electron-forge/shared-types": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.10.2.tgz",
|
||||
"integrity": "sha512-e2pd9RsdbKwsNf6UtKoolmJGy92Nc0/XO4SI91doV8cM954hM2XSYz3VHoqXebMFAF1JDfXoEUt6UCRbEDgMgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron-forge/tracer": "7.10.2",
|
||||
"@electron/packager": "^18.3.5",
|
||||
"@electron/rebuild": "^3.7.0",
|
||||
"listr2": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/plugin-fuses/node_modules/@electron-forge/tracer": {
|
||||
"version": "7.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.10.2.tgz",
|
||||
"integrity": "sha512-jhLLQbttfZViSOYn/3SJc8HML+jNZAytPVJwgGGd3coUiFysWJ2Xald99iqOiouPAhIigBfNPxQb/q/EbcDu4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chrome-trace-event": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-forge/plugin-vite": {
|
||||
@@ -1097,16 +1140,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/fuses": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-2.0.0.tgz",
|
||||
"integrity": "sha512-lyb1zK3YHeWUjaz7yiK0GnxSPduwASKMyiDbCtbn3spP6EEt+UWtktggWehG0icFrXAk3GwvcJ4nCrJO0N9IhQ==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz",
|
||||
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"electron-fuses": "dist/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/fuses/node_modules/fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"at-least-node": "^1.0.0",
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get": {
|
||||
@@ -2045,14 +2106,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz",
|
||||
"integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
|
||||
"integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.0.0"
|
||||
"react-hook-form": "^7.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
@@ -2278,12 +2340,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 +7077,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 +11930,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 +11949,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 +14727,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",
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
"@electron-forge/maker-squirrel": "^7.6.1",
|
||||
"@electron-forge/maker-zip": "^7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
|
||||
"@electron-forge/plugin-fuses": "^7.8.0",
|
||||
"@electron-forge/plugin-fuses": "^7.10.2",
|
||||
"@electron-forge/plugin-vite": "^7.6.1",
|
||||
"@electron-forge/shared-types": "^7.4.0",
|
||||
"@electron/fuses": "^2.0.0",
|
||||
"@electron/fuses": "^1.8.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,11 +61,11 @@
|
||||
"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": {
|
||||
"@hookform/resolvers": "^4.1.2",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@icons-pack/react-simple-icons": "^11.2.0",
|
||||
"@jub3i/tree-kill": "^0.0.15",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -720,3 +720,7 @@ The following persons must not work on the machine:
|
||||

|
||||
|
||||

|
||||
|
||||
## Mechanical Diagrams
|
||||
|
||||

|
||||
@@ -145,8 +145,17 @@ export function mainMessageHandler(
|
||||
};
|
||||
}
|
||||
|
||||
// Create the store instance once and export it
|
||||
export const mainNamespaceStore: StoreApi<MainNamespaceStore> =
|
||||
createMainNamespaceStore();
|
||||
|
||||
// Message handler stays the same
|
||||
export const mainMessageHandlerInstance: EventHandler =
|
||||
mainMessageHandler(mainNamespaceStore);
|
||||
|
||||
// Hook that uses the exported store
|
||||
const mainRoomImplementation = createNamespaceHookImplementation({
|
||||
createStore: createMainNamespaceStore,
|
||||
createStore: () => mainNamespaceStore, // use the exported store
|
||||
createEventHandler: mainMessageHandler,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { z } from "zod";
|
||||
import { toastError, toastZodError } from "@/components/Toast";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { FPS_30 } from "@/lib/constants";
|
||||
import { mainNamespaceStore, createMainNamespaceStore } from "./mainNamespace";
|
||||
import { softReloadMainNamespaceStore } from "./softReload";
|
||||
|
||||
/**
|
||||
* Simple buffer-based store updater to limit React re-renders to ~30 FPS
|
||||
@@ -305,27 +307,13 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
|
||||
// add handlers
|
||||
socket.on("connect", () => {
|
||||
console.log(`Connected to ${namespace_path}`);
|
||||
|
||||
// reset the store
|
||||
resetStore(set);
|
||||
});
|
||||
socket.on("disconnect", (reason) => {
|
||||
socket.disconnect();
|
||||
console.warn(`Disconnected from ${namespace_path}, reason: ${reason}`);
|
||||
|
||||
// reset the store
|
||||
resetStore(set);
|
||||
|
||||
// Attempt to reconnect after a short delay for any disconnect reason
|
||||
console.log(
|
||||
`Disconnected from ${namespace_path}, attempting reconnect in 1s...`,
|
||||
);
|
||||
setTimeout(() => {
|
||||
if (get().hasNamespace(namespaceId) && !socket.connected) {
|
||||
console.log(`Reconnecting to ${namespace_path}...`);
|
||||
socket.connect();
|
||||
}
|
||||
}, 1000);
|
||||
// Its hacky but i do not care, electron + react is annoying ...
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
socket.on("event", (event: unknown) => {
|
||||
@@ -334,11 +322,9 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
|
||||
if (!event_parsed.success) {
|
||||
toastZodError(event_parsed.error, "Invalid event");
|
||||
return;
|
||||
}
|
||||
// handle the event
|
||||
} // handle the event
|
||||
get().namespaces[namespace_path].handler(event_parsed.data);
|
||||
});
|
||||
|
||||
// store the namespace initally
|
||||
set(
|
||||
produce((state: SocketioStore) => {
|
||||
@@ -356,8 +342,25 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
|
||||
}),
|
||||
);
|
||||
|
||||
// finally connect
|
||||
socket.connect();
|
||||
if (namespace_path !== "/main") {
|
||||
const intervalId = setInterval(() => {
|
||||
const mainState = mainNamespaceStore.getState();
|
||||
const machineExists =
|
||||
namespaceId.type === "machine" &&
|
||||
mainState.machines?.data?.machines.some(
|
||||
(m) =>
|
||||
m.machine_identification_unique.serial ===
|
||||
namespaceId.machine_identification_unique.serial,
|
||||
);
|
||||
//console.log(machineExists);
|
||||
if (machineExists && !socket.connected) {
|
||||
socket.connect();
|
||||
clearInterval(intervalId); // stop polling
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
socket.connect();
|
||||
}
|
||||
},
|
||||
incrementNamespace: (namespaceId: NamespaceId) => {
|
||||
const namespace_path = serializeNamespaceId(namespaceId);
|
||||
@@ -386,7 +389,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 +433,7 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}*/
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { mainNamespaceStore, mainMessageHandler } from "./mainNamespace";
|
||||
|
||||
// Soft reset function
|
||||
export function softReloadMainNamespaceStore() {
|
||||
// 1️⃣ Reset the store to initial state
|
||||
mainNamespaceStore.setState({
|
||||
ethercatDevices: null,
|
||||
machines: null,
|
||||
ethercatInterfaceDiscovery: null,
|
||||
});
|
||||
|
||||
// 2️⃣ Re-attach the message handler if needed
|
||||
// This ensures future socket events still update the store
|
||||
mainMessageHandler(mainNamespaceStore);
|
||||
|
||||
console.log("MainNamespaceStore soft reloaded");
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import React from "react";
|
||||
import { Icon } from "./Icon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { UpdateStep } from "@/stores/updateStore";
|
||||
|
||||
interface UpdateProgressBarProps {
|
||||
steps: UpdateStep[];
|
||||
overallProgress: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function UpdateProgressBar({
|
||||
steps,
|
||||
overallProgress,
|
||||
className,
|
||||
}: UpdateProgressBarProps) {
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{/* Overall Progress Bar */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Overall Progress</span>
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
{overallProgress}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-2.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800">
|
||||
<div
|
||||
className="h-full rounded-full bg-gray-900 transition-all duration-500 ease-out dark:bg-gray-100"
|
||||
style={{ width: `${overallProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Steps List */}
|
||||
<div className="space-y-2">
|
||||
{steps.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded border px-3 py-2.5 transition-all duration-200",
|
||||
step.status === "in-progress" &&
|
||||
"border-gray-400 bg-gray-50 dark:border-gray-600 dark:bg-gray-900",
|
||||
step.status === "completed" &&
|
||||
"border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-950",
|
||||
step.status === "error" &&
|
||||
"border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950/30",
|
||||
step.status === "pending" &&
|
||||
"border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950",
|
||||
)}
|
||||
>
|
||||
{/* Status Icon */}
|
||||
<div className="flex-shrink-0">
|
||||
{step.status === "pending" && (
|
||||
<div className="size-5 rounded-full border-2 border-gray-300 dark:border-gray-700" />
|
||||
)}
|
||||
{step.status === "in-progress" && (
|
||||
<Icon
|
||||
name="lu:Loader"
|
||||
className="size-5 animate-spin text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
)}
|
||||
{step.status === "completed" && (
|
||||
<div className="flex size-5 items-center justify-center rounded-full bg-gray-700 dark:bg-gray-300">
|
||||
<Icon
|
||||
name="lu:Check"
|
||||
className="size-3 text-white dark:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{step.status === "error" && (
|
||||
<div className="flex size-5 items-center justify-center rounded-full bg-red-600 dark:bg-red-500">
|
||||
<Icon name="lu:X" className="size-3 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step Content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm",
|
||||
step.status === "completed" &&
|
||||
"text-gray-600 dark:text-gray-400",
|
||||
step.status === "error" &&
|
||||
"font-medium text-red-700 dark:text-red-400",
|
||||
step.status === "in-progress" &&
|
||||
"font-medium text-gray-900 dark:text-gray-100",
|
||||
step.status === "pending" &&
|
||||
"text-gray-500 dark:text-gray-500",
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
{/* Subsector Badge - minimal inline display */}
|
||||
<span
|
||||
className={cn("text-xs text-gray-400 dark:text-gray-600")}
|
||||
>
|
||||
(
|
||||
{step.id === "rust-build"
|
||||
? "rust & electron"
|
||||
: step.subsector}
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
{/* Progress percentage for in-progress steps */}
|
||||
{step.status === "in-progress" &&
|
||||
step.progress !== undefined &&
|
||||
step.progress > 0 && (
|
||||
<span className="text-xs font-medium text-gray-600 tabular-nums dark:text-gray-400">
|
||||
{Math.round(step.progress)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar for steps with detailed tracking */}
|
||||
{step.status === "in-progress" &&
|
||||
step.progress !== undefined &&
|
||||
step.progress > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
className="h-full rounded-full bg-gray-700 transition-all duration-500 dark:bg-gray-300"
|
||||
style={{ width: `${step.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { Icon } from "@/components/Icon";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
type RatioInputProps = {
|
||||
master?: number;
|
||||
slave?: number;
|
||||
onRatioChange?: (master: number, slave: number) => void;
|
||||
title?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
};
|
||||
|
||||
const ratioSchema = z.object({
|
||||
master: z.number().min(0.1).max(1000),
|
||||
slave: z.number().min(0.1).max(1000),
|
||||
});
|
||||
|
||||
type RatioFormData = z.infer<typeof ratioSchema>;
|
||||
|
||||
export function RatioInput({
|
||||
master = 1,
|
||||
slave = 1,
|
||||
onRatioChange,
|
||||
title = "Motor Ratio",
|
||||
min = 0.1,
|
||||
max = 1000,
|
||||
step = 0.1,
|
||||
}: RatioInputProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [activeField, setActiveField] = useState<"master" | "slave" | null>(
|
||||
null,
|
||||
);
|
||||
const [inputValue, setInputValue] = useState<string>("");
|
||||
|
||||
const getInitialValues = useCallback(() => {
|
||||
return {
|
||||
master: master ?? 1,
|
||||
slave: slave ?? 1,
|
||||
};
|
||||
}, [master, slave]);
|
||||
|
||||
const form = useForm<RatioFormData>({
|
||||
resolver: zodResolver(ratioSchema),
|
||||
defaultValues: getInitialValues(),
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
// Initialize form with current values when dialog opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const initialValues = getInitialValues();
|
||||
form.reset(initialValues);
|
||||
setActiveField(null);
|
||||
setInputValue("");
|
||||
}
|
||||
}, [isOpen, getInitialValues, form]);
|
||||
|
||||
const formatRatioDisplay = useCallback(() => {
|
||||
const m = master ?? 1;
|
||||
const s = slave ?? 1;
|
||||
return `${m.toFixed(1)} : ${s.toFixed(1)}`;
|
||||
}, [master, slave]);
|
||||
|
||||
// Increment/decrement functions with bounds checking
|
||||
const adjustRatioValue = useCallback(
|
||||
(field: "master" | "slave", increment: boolean) => {
|
||||
const currentValue = form.getValues(field);
|
||||
let newValue = increment ? currentValue + step : currentValue - step;
|
||||
|
||||
// Round to avoid floating point precision issues
|
||||
newValue = Math.round(newValue * 10) / 10;
|
||||
|
||||
// Clamp to min/max bounds
|
||||
if (newValue < min) {
|
||||
newValue = min;
|
||||
} else if (newValue > max) {
|
||||
newValue = max;
|
||||
}
|
||||
|
||||
form.setValue(field, newValue, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
},
|
||||
[form, step, min, max],
|
||||
);
|
||||
|
||||
const handleNumpadClick = (digit: string) => {
|
||||
if (activeField === null) return;
|
||||
|
||||
const newValue = inputValue + digit;
|
||||
setInputValue(newValue);
|
||||
};
|
||||
|
||||
const handleDecimal = () => {
|
||||
if (activeField === null) return;
|
||||
if (!inputValue.includes(".")) {
|
||||
setInputValue(inputValue === "" ? "0." : inputValue + ".");
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackspace = () => {
|
||||
setInputValue(inputValue.slice(0, -1));
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
const handleNumpadConfirm = () => {
|
||||
if (activeField === null || inputValue === "") return;
|
||||
|
||||
let numValue = parseFloat(inputValue);
|
||||
|
||||
// Clamp to min/max bounds
|
||||
if (numValue < min) {
|
||||
numValue = min;
|
||||
} else if (numValue > max) {
|
||||
numValue = max;
|
||||
}
|
||||
|
||||
form.setValue(activeField, numValue, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
|
||||
setInputValue("");
|
||||
setActiveField(null);
|
||||
};
|
||||
|
||||
const handleFieldClick = (field: "master" | "slave") => {
|
||||
if (activeField === field) {
|
||||
// If same field, submit and close
|
||||
handleNumpadConfirm();
|
||||
} else {
|
||||
// Switch to new field
|
||||
if (activeField !== null && inputValue !== "") {
|
||||
handleNumpadConfirm();
|
||||
}
|
||||
setActiveField(field);
|
||||
setInputValue("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (activeField !== null && inputValue !== "") {
|
||||
handleNumpadConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
form.handleSubmit((data) => {
|
||||
onRatioChange?.(data.master, data.slave);
|
||||
setIsOpen(false);
|
||||
})();
|
||||
};
|
||||
|
||||
const handleAbort = () => {
|
||||
const initialValues = getInitialValues();
|
||||
form.reset(initialValues);
|
||||
setActiveField(null);
|
||||
setInputValue("");
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.setValue("master", 1, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
form.setValue("slave", 1, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
});
|
||||
setActiveField(null);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
// Watch individual form fields for real-time updates
|
||||
const masterValue = useWatch({
|
||||
control: form.control,
|
||||
name: "master",
|
||||
defaultValue: getInitialValues().master,
|
||||
});
|
||||
|
||||
const slaveValue = useWatch({
|
||||
control: form.control,
|
||||
name: "slave",
|
||||
defaultValue: getInitialValues().slave,
|
||||
});
|
||||
|
||||
// Get current form values with fallback
|
||||
const currentValues = {
|
||||
master: masterValue ?? 1,
|
||||
slave: slaveValue ?? 1,
|
||||
};
|
||||
|
||||
// RatioValueInput component for individual ratio fields
|
||||
const RatioValueInput = ({
|
||||
field,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
field: "master" | "slave";
|
||||
label: string;
|
||||
value: number;
|
||||
}) => {
|
||||
const isActive = activeField === field;
|
||||
const displayValue =
|
||||
isActive && inputValue !== "" ? inputValue : value.toFixed(1);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<label className="text-sm font-medium text-gray-700">{label}</label>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-10 w-20 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
adjustRatioValue(field, true);
|
||||
}}
|
||||
disabled={value >= max}
|
||||
>
|
||||
<Icon name="lu:ChevronUp" className="size-4" />
|
||||
</TouchButton>
|
||||
<div
|
||||
onClick={() => handleFieldClick(field)}
|
||||
className={`flex h-16 w-24 cursor-pointer items-center justify-center rounded border-2 font-mono text-3xl transition-colors ${
|
||||
isActive
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-300 bg-white"
|
||||
}`}
|
||||
>
|
||||
{displayValue}
|
||||
</div>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-10 w-20 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
adjustRatioValue(field, false);
|
||||
}}
|
||||
disabled={value <= min}
|
||||
>
|
||||
<Icon name="lu:ChevronDown" className="size-4" />
|
||||
</TouchButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Numpad component
|
||||
const Numpad = () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Display showing input value */}
|
||||
<div className="flex h-12 items-center justify-center rounded border-2 border-gray-300 bg-gray-50 font-mono text-xl">
|
||||
{inputValue === "" ? "0" : inputValue}
|
||||
</div>
|
||||
|
||||
{/* Numpad grid */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
["1", "2", "3"],
|
||||
["4", "5", "6"],
|
||||
["7", "8", "9"],
|
||||
[".", "0", "DEL"],
|
||||
].map((row, i) => (
|
||||
<div key={i} className="contents">
|
||||
{row.map((key) => (
|
||||
<TouchButton
|
||||
key={key}
|
||||
variant="outline"
|
||||
className="h-12 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (key === "DEL") {
|
||||
handleBackspace();
|
||||
} else if (key === ".") {
|
||||
handleDecimal();
|
||||
} else {
|
||||
handleNumpadClick(key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{key === "DEL" ? (
|
||||
<Icon name="lu:Delete" className="size-4" />
|
||||
) : (
|
||||
key
|
||||
)}
|
||||
</TouchButton>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Control buttons */}
|
||||
<div className="flex gap-2">
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
className="h-10 flex-1"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleClear();
|
||||
}}
|
||||
>
|
||||
C
|
||||
</TouchButton>
|
||||
<TouchButton
|
||||
className="h-10 flex-1 bg-green-600 text-white hover:bg-green-700"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleNumpadConfirm();
|
||||
}}
|
||||
disabled={activeField === null || inputValue === ""}
|
||||
>
|
||||
<Icon name="lu:Check" className="size-4" />
|
||||
</TouchButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
|
||||
>
|
||||
<Icon name="lu:Settings" className="mr-2 size-4" />
|
||||
{formatRatioDisplay()}
|
||||
</TouchButton>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-4xl p-8">
|
||||
<div className="flex flex-col gap-8">
|
||||
{/* Header */}
|
||||
<div className="flex min-h-[2.5rem] items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon name="lu:Settings" className="size-6" />
|
||||
<h3 className="text-xl font-semibold">{title}</h3>
|
||||
</div>
|
||||
<TouchButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
className="text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
<Icon name="lu:RotateCcw" className="mr-2 size-4" />
|
||||
Reset to 1:1
|
||||
</TouchButton>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="-mt-4 text-base text-gray-600">
|
||||
Set the gear ratio. For every {currentValues.master.toFixed(1)}{" "}
|
||||
rotation(s) of the master puller, the motor will make{" "}
|
||||
{currentValues.slave.toFixed(1)} rotation(s).
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Main content: Ratio inputs and Numpad */}
|
||||
<div className="flex gap-8">
|
||||
{/* Ratio inputs */}
|
||||
<div className="flex flex-1 items-center justify-center gap-8">
|
||||
<RatioValueInput
|
||||
field="master"
|
||||
label="Master"
|
||||
value={currentValues.master}
|
||||
/>
|
||||
<div className="mt-8 text-4xl font-bold">:</div>
|
||||
<RatioValueInput
|
||||
field="slave"
|
||||
label="Slave"
|
||||
value={currentValues.slave}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Numpad */}
|
||||
<div className="w-40">
|
||||
<Numpad />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-4">
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
className="h-14 flex-1"
|
||||
onClick={handleAbort}
|
||||
>
|
||||
<Icon name="lu:X" className="mr-2 size-4" />
|
||||
Cancel
|
||||
</TouchButton>
|
||||
<TouchButton
|
||||
className="h-14 flex-1 bg-blue-600 text-white hover:bg-blue-700"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<Icon name="lu:Check" className="mr-2 size-4" />
|
||||
Apply
|
||||
</TouchButton>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -109,7 +109,7 @@ export function EditValue({
|
||||
type FormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormSchema>({
|
||||
resolver: zodResolver<FormSchema>(formSchema),
|
||||
resolver: zodResolver(formSchema),
|
||||
values: { value: value ?? defaultOrZero },
|
||||
defaultValues: { value: defaultValue },
|
||||
mode: "all",
|
||||
|
||||
@@ -2,3 +2,4 @@ export const UPDATE_EXECUTE = "update:execute";
|
||||
export const UPDATE_LOG = "update:log";
|
||||
export const UPDATE_END = "update:end";
|
||||
export const UPDATE_CANCEL = "update:cancel";
|
||||
export const UPDATE_STEP = "update:step";
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
UPDATE_END,
|
||||
UPDATE_EXECUTE,
|
||||
UPDATE_LOG,
|
||||
UPDATE_STEP,
|
||||
} from "./update-channels";
|
||||
|
||||
type UpdateExecuteInvokeParams = {
|
||||
@@ -16,11 +17,20 @@ type UpdateExecuteInvokeParams = {
|
||||
};
|
||||
};
|
||||
|
||||
type UpdateStepParams = {
|
||||
stepId: string;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
progress?: number;
|
||||
};
|
||||
|
||||
export function exposeUpdateContext() {
|
||||
const { contextBridge, ipcRenderer } = window.require("electron");
|
||||
|
||||
let currentLogListener: ((event: any, log: string) => void) | null = null;
|
||||
let currentEndListener: ((event: any, params: any) => void) | null = null;
|
||||
let currentStepListener:
|
||||
| ((event: any, params: UpdateStepParams) => void)
|
||||
| null = null;
|
||||
|
||||
contextBridge.exposeInMainWorld("update", {
|
||||
execute: (params: UpdateExecuteInvokeParams) =>
|
||||
@@ -52,5 +62,17 @@ export function exposeUpdateContext() {
|
||||
|
||||
ipcRenderer.on(UPDATE_END, currentEndListener);
|
||||
},
|
||||
|
||||
onStep: (callback: (params: UpdateStepParams) => void) => {
|
||||
if (currentStepListener) {
|
||||
ipcRenderer.removeListener(UPDATE_STEP, currentStepListener);
|
||||
}
|
||||
|
||||
currentStepListener = (_event, params) => {
|
||||
callback(params);
|
||||
};
|
||||
|
||||
ipcRenderer.on(UPDATE_STEP, currentStepListener);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
UPDATE_END,
|
||||
UPDATE_EXECUTE,
|
||||
UPDATE_LOG,
|
||||
UPDATE_STEP,
|
||||
} from "./update-channels";
|
||||
import { spawn, ChildProcess } from "child_process";
|
||||
import tkill from "@jub3i/tree-kill";
|
||||
@@ -116,6 +117,13 @@ async function update(
|
||||
commit,
|
||||
});
|
||||
|
||||
// Reset Rust build progress tracking
|
||||
rustBuildProgress = {
|
||||
totalDerivations: 0,
|
||||
builtDerivations: 0,
|
||||
maxPercent: 0,
|
||||
};
|
||||
|
||||
const qitechControlEnv = process.env.QITECH_CONTROL_ENV;
|
||||
const homeDir =
|
||||
qitechControlEnv === "control-os" ? "/home/qitech" : process.env.HOME;
|
||||
@@ -129,6 +137,8 @@ async function update(
|
||||
|
||||
// 1. first make sure the clone path is empty by deleting it if it containsa .git folder
|
||||
const repoDir = `${homeDir}/${githubRepoName}`;
|
||||
|
||||
// Clear repo (not tracked in progress UI)
|
||||
const clearResult = await clearRepoDirectory(
|
||||
`${homeDir}/${githubRepoName}`,
|
||||
event,
|
||||
@@ -139,6 +149,10 @@ async function update(
|
||||
}
|
||||
|
||||
// 2. clone the repository
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "in-progress",
|
||||
});
|
||||
const cloneResult = await cloneRepository(
|
||||
{
|
||||
githubRepoOwner,
|
||||
@@ -151,11 +165,19 @@ async function update(
|
||||
event,
|
||||
);
|
||||
if (!cloneResult.success) {
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "error",
|
||||
});
|
||||
event.sender.send(UPDATE_LOG, cloneResult.error);
|
||||
return;
|
||||
}
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "completed",
|
||||
});
|
||||
|
||||
// 3. make the nixos-install.sh script executable
|
||||
// 3. make the nixos-install.sh script executable (not tracked in progress UI)
|
||||
const chmodResult = await runCommand(
|
||||
"chmod",
|
||||
["+x", "nixos-install.sh"],
|
||||
@@ -168,17 +190,35 @@ async function update(
|
||||
}
|
||||
|
||||
// 4. run the nixos-install.sh script
|
||||
const installResult = await runCommand(
|
||||
// This script will handle rust-build, electron-build, and system-install
|
||||
// Start with rust-build (cargo builds)
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "in-progress",
|
||||
});
|
||||
|
||||
const installResult = await runCommandWithStepTracking(
|
||||
"./nixos-install.sh",
|
||||
[],
|
||||
repoDir,
|
||||
event,
|
||||
);
|
||||
|
||||
if (!installResult.success) {
|
||||
// Mark current and remaining steps as error
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "error",
|
||||
});
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "system-install",
|
||||
status: "error",
|
||||
});
|
||||
event.sender.send(UPDATE_LOG, installResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Success - steps are already marked as completed by runCommandWithStepTracking
|
||||
resolve();
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
@@ -244,7 +284,7 @@ async function cloneRepository(
|
||||
: `https://github.com/${githubRepoOwner}/${githubRepoName}.git`;
|
||||
|
||||
// Determine clone arguments based on whether tag, branch, or commit is specified
|
||||
const cloneArgs = ["clone", repoUrl];
|
||||
const cloneArgs = ["clone", "--progress", repoUrl];
|
||||
|
||||
if (tag) {
|
||||
// Clone a specific tag
|
||||
@@ -333,6 +373,11 @@ async function runCommand(
|
||||
const log = data.toString();
|
||||
console.error(log);
|
||||
event.sender.send(UPDATE_LOG, log);
|
||||
|
||||
// Git outputs progress to stderr
|
||||
if (cmd === "git" && args.includes("--progress")) {
|
||||
parseGitProgress(log, event);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle process completion
|
||||
@@ -386,6 +431,249 @@ async function runCommand(
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Git clone progress
|
||||
function parseGitProgress(
|
||||
log: string,
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
): void {
|
||||
// Git progress format: "Receiving objects: 45% (234/520)"
|
||||
// or "Resolving deltas: 100% (150/150)"
|
||||
const receivingMatch = log.match(/Receiving objects:\s*(\d+)%/);
|
||||
const resolvingMatch = log.match(/Resolving deltas:\s*(\d+)%/);
|
||||
|
||||
if (receivingMatch) {
|
||||
const percent = parseInt(receivingMatch[1], 10);
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "in-progress",
|
||||
progress: Math.floor(percent * 0.8), // Receiving is 80% of clone
|
||||
});
|
||||
} else if (resolvingMatch) {
|
||||
const percent = parseInt(resolvingMatch[1], 10);
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "clone-repo",
|
||||
status: "in-progress",
|
||||
progress: Math.floor(80 + percent * 0.2), // Resolving is last 20%
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Track Rust build progress
|
||||
let rustBuildProgress = {
|
||||
totalDerivations: 0,
|
||||
builtDerivations: 0,
|
||||
maxPercent: 0, // Track max to prevent backward movement
|
||||
};
|
||||
|
||||
// Parse Rust build output for progress
|
||||
function parseRustBuildOutput(
|
||||
log: string,
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
): void {
|
||||
// Track derivations to build
|
||||
const derivationsMatch = log.match(/these (\d+) derivations? will be built/i);
|
||||
if (derivationsMatch) {
|
||||
rustBuildProgress.totalDerivations = parseInt(derivationsMatch[1], 10);
|
||||
rustBuildProgress.builtDerivations = 0;
|
||||
rustBuildProgress.maxPercent = 0;
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "in-progress",
|
||||
progress: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Track building packages
|
||||
if (
|
||||
log.includes("building '/nix/store/") ||
|
||||
log.includes("building /nix/store/")
|
||||
) {
|
||||
rustBuildProgress.builtDerivations++;
|
||||
|
||||
// Check if this is the server-deps package (one of the last builds)
|
||||
const isServerDeps = log.includes("-server-deps");
|
||||
|
||||
let percent = 15;
|
||||
if (isServerDeps) {
|
||||
// server-deps indicates we're at 85%
|
||||
percent = 85;
|
||||
} else if (rustBuildProgress.totalDerivations > 0) {
|
||||
const derivationProgress =
|
||||
rustBuildProgress.builtDerivations / rustBuildProgress.totalDerivations;
|
||||
percent = 15 + Math.floor(derivationProgress * 70); // Map to 15-85%
|
||||
}
|
||||
|
||||
// Only move forward
|
||||
percent = Math.max(percent, rustBuildProgress.maxPercent);
|
||||
rustBuildProgress.maxPercent = percent;
|
||||
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "in-progress",
|
||||
progress: percent,
|
||||
});
|
||||
}
|
||||
|
||||
// Track installing phase - go up to 90%
|
||||
if (log.includes("installing") || log.includes("Installing")) {
|
||||
const percent = Math.max(90, rustBuildProgress.maxPercent);
|
||||
rustBuildProgress.maxPercent = percent;
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "in-progress",
|
||||
progress: percent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced version of runCommand that tracks build steps based on log output
|
||||
async function runCommandWithStepTracking(
|
||||
cmd: string,
|
||||
args: string[],
|
||||
workingDir: string,
|
||||
event: Electron.IpcMainInvokeEvent,
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const completeCommand = `${cmd} ${args.join(" ")}`;
|
||||
const workingDirText = terminalGray(workingDir);
|
||||
event.sender.send(
|
||||
UPDATE_LOG,
|
||||
`🚀 ${workingDirText} ${terminalColor("blue", completeCommand)}`,
|
||||
);
|
||||
|
||||
const childProcess = spawn(cmd, args, {
|
||||
cwd: workingDir,
|
||||
});
|
||||
|
||||
// Store reference to current process for cancellation
|
||||
currentUpdateProcess = childProcess;
|
||||
|
||||
// Track which steps have been marked as in-progress
|
||||
// Note: rust-build is already marked as in-progress before calling this function
|
||||
let systemInstallStarted = false;
|
||||
|
||||
// Function to process log output and update steps
|
||||
const processLogForSteps = (log: string) => {
|
||||
const logLower = log.toLowerCase();
|
||||
|
||||
// Parse Rust build progress
|
||||
parseRustBuildOutput(log, event);
|
||||
|
||||
// Check for system install indicators
|
||||
// System install happens after Rust build completes (at ~90%)
|
||||
// Look for bootloader/activation messages that indicate final system installation
|
||||
if (
|
||||
!systemInstallStarted &&
|
||||
rustBuildProgress.maxPercent >= 90 &&
|
||||
(logLower.includes("updating grub") ||
|
||||
logLower.includes("installing bootloader") ||
|
||||
logLower.includes("updating bootloader") ||
|
||||
logLower.includes("activating the configuration") ||
|
||||
logLower.includes("building the system configuration") ||
|
||||
logLower.includes("these 0 derivations"))
|
||||
) {
|
||||
console.log("🟢 Detected system install start:", log.substring(0, 100));
|
||||
// Mark rust as complete, start system install
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "completed",
|
||||
});
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "system-install",
|
||||
status: "in-progress",
|
||||
});
|
||||
systemInstallStarted = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Stream stdout logs back to renderer
|
||||
childProcess.stdout.on("data", (data) => {
|
||||
const log = data.toString();
|
||||
console.log(log);
|
||||
event.sender.send(UPDATE_LOG, log);
|
||||
processLogForSteps(log);
|
||||
});
|
||||
|
||||
// Stream stderr logs back to renderer
|
||||
childProcess.stderr.on("data", (data) => {
|
||||
const log = data.toString();
|
||||
console.error(log);
|
||||
event.sender.send(UPDATE_LOG, log);
|
||||
processLogForSteps(log);
|
||||
});
|
||||
|
||||
// Handle process completion
|
||||
return new Promise((resolve, reject) => {
|
||||
childProcess.on("close", (code, signal) => {
|
||||
// Clear process reference when completed
|
||||
if (currentUpdateProcess === childProcess) {
|
||||
currentUpdateProcess = null;
|
||||
}
|
||||
|
||||
if (signal === "SIGTERM" || signal === "SIGKILL") {
|
||||
event.sender.send(UPDATE_LOG, terminalInfo("Command was cancelled"));
|
||||
reject({
|
||||
success: false,
|
||||
error: "Command was cancelled",
|
||||
});
|
||||
} else if (code === 0) {
|
||||
// Mark all remaining steps as completed on success
|
||||
if (!systemInstallStarted) {
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "rust-build",
|
||||
status: "completed",
|
||||
});
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "system-install",
|
||||
status: "completed",
|
||||
});
|
||||
} else {
|
||||
event.sender.send(UPDATE_STEP, {
|
||||
stepId: "system-install",
|
||||
status: "completed",
|
||||
});
|
||||
}
|
||||
|
||||
event.sender.send(
|
||||
UPDATE_LOG,
|
||||
terminalSuccess("Command completed successfully"),
|
||||
);
|
||||
resolve({ success: true, error: undefined });
|
||||
} else {
|
||||
event.sender.send(
|
||||
UPDATE_LOG,
|
||||
terminalError(`Command failed with code ${code}`),
|
||||
);
|
||||
reject({
|
||||
success: false,
|
||||
error: terminalError(code?.toString() ?? "NO_CODE"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on("error", (err) => {
|
||||
// Clear process reference on error
|
||||
if (currentUpdateProcess === childProcess) {
|
||||
currentUpdateProcess = null;
|
||||
}
|
||||
|
||||
event.sender.send(
|
||||
UPDATE_LOG,
|
||||
terminalError(`Command error: ${err.message}`),
|
||||
);
|
||||
reject({
|
||||
success: false,
|
||||
error: terminalError(err.message),
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
event.sender.send(UPDATE_LOG, terminalError(`Error: ${error.toString()}`));
|
||||
return { success: false, error: error.toString() };
|
||||
}
|
||||
}
|
||||
|
||||
function terminalColor(
|
||||
color: "blue" | "green" | "red" | "cyan" | "gray",
|
||||
text: string,
|
||||
|
||||
@@ -2,6 +2,13 @@ import type { UpdateInfo } from "@/stores/updateStore";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
|
||||
let currentLogListener: ((line: string) => void) | null = null;
|
||||
let currentStepListener:
|
||||
| ((params: {
|
||||
stepId: string;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
progress?: number;
|
||||
}) => void)
|
||||
| null = null;
|
||||
|
||||
export async function updateExecute(
|
||||
source: UpdateInfo,
|
||||
@@ -14,15 +21,32 @@ export async function updateExecute(
|
||||
currentLogListener = null;
|
||||
}
|
||||
|
||||
if (currentStepListener) {
|
||||
window.update.onStep(() => {}); // clear previous
|
||||
currentStepListener = null;
|
||||
}
|
||||
|
||||
currentLogListener = onLog;
|
||||
window.update.onLog(currentLogListener);
|
||||
|
||||
// Set up step listener
|
||||
currentStepListener = (params) => {
|
||||
const { setStepStatus, setStepProgress } = useUpdateStore.getState();
|
||||
setStepStatus(params.stepId, params.status);
|
||||
if (params.progress !== undefined) {
|
||||
setStepProgress(params.stepId, params.progress);
|
||||
}
|
||||
};
|
||||
window.update.onStep(currentStepListener);
|
||||
|
||||
window.update.execute(source);
|
||||
|
||||
window.update.onEnd((params) => {
|
||||
window.update.onLog(() => {}); // remove listener
|
||||
window.update.onEnd(() => {});
|
||||
window.update.onStep(() => {}); // remove step listener
|
||||
currentLogListener = null;
|
||||
currentStepListener = null;
|
||||
resolve(params);
|
||||
});
|
||||
});
|
||||
@@ -55,6 +79,7 @@ export async function updateCancel(): Promise<{
|
||||
const result = await window.update.cancel();
|
||||
window.update.onLog(() => {}); // remove listener
|
||||
window.update.onEnd(() => {}); // remove listener
|
||||
window.update.onStep(() => {}); // remove step listener
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ export const connectedMachineStateSchema = z.object({
|
||||
/**
|
||||
* Consolidated state event schema (state changes only)
|
||||
*/
|
||||
|
||||
export const stateEventDataSchema = z.object({
|
||||
mode_state: modeStateSchema,
|
||||
connected_machine_state: connectedMachineStateSchema,
|
||||
@@ -111,8 +110,7 @@ export function buffer1MessageHandler(
|
||||
state: stateEvent,
|
||||
}));
|
||||
} else if (eventName === "LiveValuesEvent") {
|
||||
const liveValuesEvent = liveValuesEventSchema.parse(event);
|
||||
const timestamp = event.ts;
|
||||
liveValuesEventSchema.parse(event);
|
||||
updateStore((state) => ({
|
||||
...state,
|
||||
}));
|
||||
|
||||
@@ -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,172 @@ 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>
|
||||
<ControlCard title="Temperature PID Settings (Nozzle) ">
|
||||
<Label label="Kp">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.nozzle.kp}
|
||||
defaultValue={
|
||||
defaultState?.pid_settings.temperature.nozzle.kp
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("nozzle", "kp", v)}
|
||||
title="Temperature PID KP"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Ki">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.nozzle.ki}
|
||||
defaultValue={
|
||||
defaultState?.pid_settings.temperature.nozzle.ki
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("nozzle", "ki", v)}
|
||||
title="Temperature PID KI"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Kd">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.nozzle.kd}
|
||||
defaultValue={
|
||||
defaultState?.pid_settings.temperature.nozzle.kd
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("nozzle", "kd", v)}
|
||||
title="Temperature PID KD"
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
|
||||
@@ -131,9 +131,30 @@ 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(),
|
||||
}),
|
||||
nozzle: 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" | "nozzle",
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -90,13 +90,18 @@ export const winder2: MachineProperties = {
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 4,
|
||||
role_label: "1x Stepper Puller",
|
||||
role: 10,
|
||||
role_label: "Slave Puller",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x10001e,
|
||||
revision: 0x1a0000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x190000,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -340,6 +345,215 @@ export const aquapath1: MachineProperties = {
|
||||
],
|
||||
};
|
||||
|
||||
export const gluetex: MachineProperties = {
|
||||
name: "Gluetex",
|
||||
version: "V1",
|
||||
slug: "gluetex",
|
||||
icon: "lu:Disc3",
|
||||
machine_identification: {
|
||||
vendor: VENDOR_QITECH,
|
||||
machine: 0x000a,
|
||||
},
|
||||
device_roles: [
|
||||
{
|
||||
role: 0,
|
||||
role_label: "Bus Coupler",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x44c2c52,
|
||||
revision: 0x120000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 1,
|
||||
role_label: "2x Digital Output",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x7d23052,
|
||||
revision: 0x110000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x7d23052,
|
||||
revision: 0x120000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x7d83052,
|
||||
revision: 0x110000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x7d83052,
|
||||
revision: 0x120000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 2,
|
||||
role_label: "1x Stepper Spool EL7041-0052",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 461451346,
|
||||
revision: 0x100034,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 3,
|
||||
role_label: "1x Stepper Traverse",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x1a0000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x190000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 4,
|
||||
role_label: "1x Stepper Puller",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x10001e,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 5,
|
||||
role_label: "Temperature Sensors 1-4",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0xc843052,
|
||||
revision: 0x160000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0xc843052,
|
||||
revision: 0x150000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0xc843052,
|
||||
revision: 0x1a0000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 6,
|
||||
role_label: "Temperature Sensors 5-6",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0xc843052,
|
||||
revision: 0x160000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0xc843052,
|
||||
revision: 0x150000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0xc843052,
|
||||
revision: 0x1a0000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 7,
|
||||
role_label: "Digital Outputs for Heater SSRs",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x7d83052,
|
||||
revision: 0x110000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x7d83052,
|
||||
revision: 0x120000,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 8,
|
||||
role_label: "Addon Motor 3",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x1a0000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x190000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x10001e,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 9,
|
||||
role_label: "Addon Motor 4",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x1a0000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x190000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x10001e,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 10,
|
||||
role_label: "Slave Puller",
|
||||
allowed_devices: [
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x1a0000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x190000,
|
||||
},
|
||||
{
|
||||
vendor_id: 2,
|
||||
product_id: 0x1b773052,
|
||||
revision: 0x10001e,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const machineProperties: MachineProperties[] = [
|
||||
winder2,
|
||||
extruder2,
|
||||
@@ -347,6 +561,7 @@ export const machineProperties: MachineProperties[] = [
|
||||
mock1,
|
||||
buffer1,
|
||||
aquapath1,
|
||||
gluetex,
|
||||
];
|
||||
|
||||
export const getMachineProperties = (
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { cn } from "@/utils/tailwind";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
min: number;
|
||||
max: number;
|
||||
minLimit: number;
|
||||
maxLimit: number;
|
||||
current: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function TemperatureBar({
|
||||
min,
|
||||
max,
|
||||
minLimit,
|
||||
maxLimit,
|
||||
current,
|
||||
className,
|
||||
}: Props) {
|
||||
// Calculate percentages for positioning
|
||||
const range = max - min;
|
||||
const minLimitPercent = ((minLimit - min) / range) * 100;
|
||||
const maxLimitPercent = ((maxLimit - min) / range) * 100;
|
||||
const currentPercent = ((current - min) / range) * 100;
|
||||
|
||||
// Determine if current value is within limits
|
||||
const isInRange = current >= minLimit && current <= maxLimit;
|
||||
|
||||
return (
|
||||
<div className={cn("w-full space-y-1 pt-6", className)}>
|
||||
<div className="relative h-4 w-full">
|
||||
{/* Base bar with overflow hidden to clip inner bar */}
|
||||
<div className="absolute h-full w-full overflow-hidden rounded-full bg-gray-200">
|
||||
{/* Valid range (green section) */}
|
||||
<div
|
||||
className="absolute h-full bg-green-600"
|
||||
style={{
|
||||
left: `${minLimitPercent}%`,
|
||||
width: `${maxLimitPercent - minLimitPercent}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Current temperature indicator - outside the overflow container so it's not clipped */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -top-5 z-10 h-14 w-1.5 rounded-full",
|
||||
isInRange ? "bg-green-600" : "bg-red-600",
|
||||
)}
|
||||
style={{
|
||||
left: `${currentPercent}%`,
|
||||
transform: "translateX(-50%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>MIN</span>
|
||||
<span>MAX</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
|
||||
import { TemperatureBar } from "../TemperatureBar";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { Label } from "@/control/Label";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { StatusBadge } from "@/control/StatusBadge";
|
||||
import { useGluetex } from "./useGluetex";
|
||||
import {
|
||||
StepperMode,
|
||||
HeatingMode,
|
||||
getGearRatioMultiplier,
|
||||
} from "./gluetexNamespace";
|
||||
import { roundToDecimals, roundDegreesToDecimals } from "@/lib/decimal";
|
||||
import { TensionArm } from "../TensionArm";
|
||||
import { RatioInput } from "@/components/ratio/RatioInput";
|
||||
|
||||
export function GluetexAddonsPage() {
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
setStepper3Mode,
|
||||
setStepper4Mode,
|
||||
setHeatingMode,
|
||||
temperature1,
|
||||
temperature2,
|
||||
setTemperature1Min,
|
||||
setTemperature1Max,
|
||||
setTemperature2Min,
|
||||
setTemperature2Max,
|
||||
slavePullerSpeed,
|
||||
slaveTensionArmAngle,
|
||||
addonTensionArmAngle,
|
||||
zeroSlaveTensionArm,
|
||||
zeroAddonTensionArm,
|
||||
setStepper3Master,
|
||||
setStepper3Slave,
|
||||
setStepper4Master,
|
||||
setStepper4Slave,
|
||||
} = useGluetex();
|
||||
|
||||
// Calculate max speed based on gear ratio (same as main puller)
|
||||
const gearRatioMultiplier = getGearRatioMultiplier(
|
||||
state?.puller_state?.gear_ratio,
|
||||
);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid>
|
||||
<ControlCard className="bg-red" title="Motors">
|
||||
<Label label="Stepper 3">
|
||||
<SelectionGroup<StepperMode>
|
||||
value={state?.stepper_state?.stepper3_mode}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
onChange={setStepper3Mode}
|
||||
orientation="horizontal"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
options={{
|
||||
Standby: {
|
||||
children: "Standby",
|
||||
icon: "lu:Power",
|
||||
isActiveClassName: "bg-green-600",
|
||||
},
|
||||
Run: {
|
||||
children: "Run",
|
||||
icon: "lu:Play",
|
||||
isActiveClassName: "bg-green-600",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Stepper 4">
|
||||
<SelectionGroup<StepperMode>
|
||||
value={state?.stepper_state?.stepper4_mode}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
onChange={setStepper4Mode}
|
||||
orientation="horizontal"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
options={{
|
||||
Standby: {
|
||||
children: "Standby",
|
||||
icon: "lu:Power",
|
||||
isActiveClassName: "bg-green-600",
|
||||
},
|
||||
Run: {
|
||||
children: "Run",
|
||||
icon: "lu:Play",
|
||||
isActiveClassName: "bg-green-600",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard className="bg-red" title="Heating Mode">
|
||||
<SelectionGroup<HeatingMode>
|
||||
value={state?.heating_state?.heating_mode}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
onChange={setHeatingMode}
|
||||
orientation="horizontal"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
options={{
|
||||
Standby: {
|
||||
children: "Standby",
|
||||
icon: "lu:Power",
|
||||
isActiveClassName: "bg-green-600",
|
||||
},
|
||||
Heating: {
|
||||
children: "Heating",
|
||||
icon: "lu:Flame",
|
||||
isActiveClassName: "bg-green-600",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard className="bg-red" height={2} title="Quality Control">
|
||||
<div className="space-y-6">
|
||||
{/* Temperature 1 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Temperature 1"
|
||||
unit="C"
|
||||
timeseries={temperature1}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TemperatureBar
|
||||
min={0}
|
||||
max={150}
|
||||
minLimit={
|
||||
state?.quality_control_state?.temperature1.min_temperature ??
|
||||
0
|
||||
}
|
||||
maxLimit={
|
||||
state?.quality_control_state?.temperature1.max_temperature ??
|
||||
150
|
||||
}
|
||||
current={temperature1.current?.value ?? 0}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Min Temperature">
|
||||
<EditValue
|
||||
value={
|
||||
state?.quality_control_state?.temperature1.min_temperature
|
||||
}
|
||||
unit="C"
|
||||
title="Min Temperature 1"
|
||||
defaultValue={
|
||||
defaultState?.quality_control_state?.temperature1
|
||||
.min_temperature
|
||||
}
|
||||
min={0}
|
||||
max={Math.min(
|
||||
149,
|
||||
(state?.quality_control_state?.temperature1
|
||||
.max_temperature ?? 150) - 1,
|
||||
)}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={setTemperature1Min}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Max Temperature">
|
||||
<EditValue
|
||||
value={
|
||||
state?.quality_control_state?.temperature1.max_temperature
|
||||
}
|
||||
unit="C"
|
||||
title="Max Temperature 1"
|
||||
defaultValue={
|
||||
defaultState?.quality_control_state?.temperature1
|
||||
.max_temperature
|
||||
}
|
||||
min={Math.max(
|
||||
1,
|
||||
(state?.quality_control_state?.temperature1
|
||||
.min_temperature ?? 0) + 1,
|
||||
)}
|
||||
max={150}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={setTemperature1Max}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Temperature 2 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Temperature 2"
|
||||
unit="C"
|
||||
timeseries={temperature2}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TemperatureBar
|
||||
min={0}
|
||||
max={200}
|
||||
minLimit={
|
||||
state?.quality_control_state?.temperature2.min_temperature ??
|
||||
0
|
||||
}
|
||||
maxLimit={
|
||||
state?.quality_control_state?.temperature2.max_temperature ??
|
||||
200
|
||||
}
|
||||
current={temperature2.current?.value ?? 0}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Min Temperature">
|
||||
<EditValue
|
||||
value={
|
||||
state?.quality_control_state?.temperature2.min_temperature
|
||||
}
|
||||
unit="C"
|
||||
title="Min Temperature 2"
|
||||
defaultValue={
|
||||
defaultState?.quality_control_state?.temperature2
|
||||
.min_temperature
|
||||
}
|
||||
min={0}
|
||||
max={Math.min(
|
||||
199,
|
||||
(state?.quality_control_state?.temperature2
|
||||
.max_temperature ?? 200) - 1,
|
||||
)}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={setTemperature2Min}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Max Temperature">
|
||||
<EditValue
|
||||
value={
|
||||
state?.quality_control_state?.temperature2.max_temperature
|
||||
}
|
||||
unit="C"
|
||||
title="Max Temperature 2"
|
||||
defaultValue={
|
||||
defaultState?.quality_control_state?.temperature2
|
||||
.max_temperature
|
||||
}
|
||||
min={Math.max(
|
||||
1,
|
||||
(state?.quality_control_state?.temperature2
|
||||
.min_temperature ?? 0) + 1,
|
||||
)}
|
||||
max={200}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={setTemperature2Max}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Tension Arm">
|
||||
<TensionArm degrees={slaveTensionArmAngle.current?.value} />
|
||||
<TimeSeriesValueNumeric
|
||||
label="Tension Arm"
|
||||
unit="deg"
|
||||
timeseries={slaveTensionArmAngle}
|
||||
renderValue={(value) => roundDegreesToDecimals(value, 0)}
|
||||
/>
|
||||
<TimeSeriesValueNumeric
|
||||
label="Slave Puller Speed"
|
||||
unit="m/min"
|
||||
timeseries={slavePullerSpeed}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
/>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
icon="lu:House"
|
||||
onClick={zeroSlaveTensionArm}
|
||||
disabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Set Zero Point
|
||||
</TouchButton>
|
||||
{!state?.slave_puller_state?.tension_arm?.zeroed && (
|
||||
<StatusBadge variant="error">Not Zeroed</StatusBadge>
|
||||
)}
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard className="bg-red" title="Motor Ratios">
|
||||
<Label label="Stepper 3 Ratio">
|
||||
<RatioInput
|
||||
master={state?.motor_ratios_state?.stepper3_master}
|
||||
slave={state?.motor_ratios_state?.stepper3_slave}
|
||||
title="Stepper 3 Motor Ratio"
|
||||
onRatioChange={(master, slave) => {
|
||||
setStepper3Master(master);
|
||||
setStepper3Slave(slave);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Stepper 4 Ratio">
|
||||
<RatioInput
|
||||
master={state?.motor_ratios_state?.stepper4_master}
|
||||
slave={state?.motor_ratios_state?.stepper4_slave}
|
||||
title="Stepper 4 Motor Ratio"
|
||||
onRatioChange={(master, slave) => {
|
||||
setStepper4Master(master);
|
||||
setStepper4Slave(slave);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Addon Tension Arm">
|
||||
<TensionArm degrees={addonTensionArmAngle.current?.value} />
|
||||
<TimeSeriesValueNumeric
|
||||
label="Angle"
|
||||
unit="deg"
|
||||
timeseries={addonTensionArmAngle}
|
||||
renderValue={(value) => roundDegreesToDecimals(value, 0)}
|
||||
/>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
icon="lu:House"
|
||||
onClick={zeroAddonTensionArm}
|
||||
disabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Set Zero Point
|
||||
</TouchButton>
|
||||
{!state?.addon_tension_arm_state?.zeroed && (
|
||||
<StatusBadge variant="error">Not Zeroed</StatusBadge>
|
||||
)}
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { Page } from "@/components/Page";
|
||||
import React, { useState } from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
|
||||
import { TraverseBar } from "../TraverseBar";
|
||||
import {
|
||||
SelectionGroup,
|
||||
SelectionGroupBoolean,
|
||||
} from "@/control/SelectionGroup";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { Label } from "@/control/Label";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { StatusBadge } from "@/control/StatusBadge";
|
||||
import { useGluetex } from "./useGluetex";
|
||||
import {
|
||||
Mode,
|
||||
SpoolAutomaticActionMode,
|
||||
getGearRatioMultiplier,
|
||||
} from "./gluetexNamespace";
|
||||
import { TensionArm } from "../TensionArm";
|
||||
import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal";
|
||||
import { Spool } from "../Spool";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { getGluetexTraverseMax } from "./gluetexConfig";
|
||||
|
||||
export function GluetexControlPage() {
|
||||
const [showResetConfirmDialog, setShowResetConfirmDialog] = useState(false);
|
||||
const traverseMax = getGluetexTraverseMax();
|
||||
|
||||
// use optimistic state
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
enableTraverseLaserpointer,
|
||||
tensionArmAngle,
|
||||
zeroTensionArmAngle,
|
||||
spoolRpm,
|
||||
setMode,
|
||||
pullerSpeed,
|
||||
spoolProgress,
|
||||
setPullerTargetSpeed,
|
||||
traversePosition,
|
||||
resetSpoolProgress,
|
||||
setTraverseLimitInner,
|
||||
setTraverseLimitOuter,
|
||||
gotoTraverseLimitInner,
|
||||
gotoTraverseLimitOuter,
|
||||
gotoTraverseHome,
|
||||
setSpoolAutomaticRequiredMeters,
|
||||
setSpoolAutomaticAction,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
} = useGluetex();
|
||||
|
||||
// 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") {
|
||||
setShowResetConfirmDialog(true);
|
||||
} else {
|
||||
resetSpoolProgress();
|
||||
}
|
||||
};
|
||||
|
||||
const confirmResetProgress = () => {
|
||||
resetSpoolProgress();
|
||||
setShowResetConfirmDialog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid>
|
||||
<ControlCard title="Spool">
|
||||
<Spool rpm={spoolRpm.current?.value} />
|
||||
<TimeSeriesValueNumeric
|
||||
label="Spool Speed"
|
||||
unit="rpm"
|
||||
timeseries={spoolRpm}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
/>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard className="bg-red" height={2} title="Traverse">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Position"
|
||||
unit="mm"
|
||||
timeseries={traversePosition}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
/>
|
||||
{state?.traverse_state && (
|
||||
<TraverseBar
|
||||
inside={0}
|
||||
outside={traverseMax}
|
||||
min={state?.traverse_state.limit_inner}
|
||||
max={state?.traverse_state.limit_outer}
|
||||
current={traversePosition.current?.value ?? 0}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Outer Limit">
|
||||
<EditValue
|
||||
value={state?.traverse_state?.limit_outer}
|
||||
unit="mm"
|
||||
title="Outer Limit"
|
||||
defaultValue={defaultState?.traverse_state?.limit_outer}
|
||||
// Traverse limit validation: Outer limit must be at least 0.9mm greater than inner limit
|
||||
// We use 1mm buffer to ensure the backend validation (which requires >0.9mm) will pass
|
||||
// Formula: min_outer = inner_limit + 1mm
|
||||
min={Math.max(0, (state?.traverse_state?.limit_inner ?? 0) + 1)}
|
||||
minLabel="IN"
|
||||
maxLabel="OUT"
|
||||
max={traverseMax}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
onChange={setTraverseLimitOuter}
|
||||
/>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
icon="lu:ArrowLeftToLine"
|
||||
onClick={gotoTraverseLimitOuter}
|
||||
disabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Go to Outer Limit
|
||||
</TouchButton>
|
||||
</Label>
|
||||
<Label label="Inner Limit">
|
||||
<EditValue
|
||||
value={state?.traverse_state?.limit_inner}
|
||||
unit="mm"
|
||||
title="Inner Limit"
|
||||
min={0}
|
||||
// Traverse limit validation: Inner limit must be at least 0.9mm smaller than outer limit
|
||||
// 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(
|
||||
traverseMax,
|
||||
(state?.traverse_state?.limit_outer ?? traverseMax) - 1,
|
||||
)}
|
||||
defaultValue={defaultState?.traverse_state?.limit_inner}
|
||||
minLabel="IN"
|
||||
maxLabel="OUT"
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
inverted
|
||||
onChange={setTraverseLimitInner}
|
||||
/>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
icon="lu:ArrowRightToLine"
|
||||
onClick={gotoTraverseLimitInner}
|
||||
disabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Go to Inner Limit
|
||||
</TouchButton>
|
||||
</Label>
|
||||
</div>
|
||||
<Label label="Laserpointer">
|
||||
<SelectionGroupBoolean
|
||||
value={state?.traverse_state.laserpointer}
|
||||
disabled={isLoading || isDisabled}
|
||||
loading={isLoading}
|
||||
optionFalse={{ children: "Off", icon: "lu:LightbulbOff" }}
|
||||
optionTrue={{ children: "On", icon: "lu:Lightbulb" }}
|
||||
onChange={enableTraverseLaserpointer}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Home">
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
icon="lu:House"
|
||||
onClick={() => gotoTraverseHome()}
|
||||
disabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Go to Home
|
||||
</TouchButton>
|
||||
{state?.traverse_state?.is_homed !== true ? (
|
||||
<StatusBadge variant={"error"}>{"Not Homed"}</StatusBadge>
|
||||
) : null}
|
||||
</Label>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Tension Arm">
|
||||
<TensionArm degrees={tensionArmAngle.current?.value} />
|
||||
<TimeSeriesValueNumeric
|
||||
label="Tension Arm"
|
||||
unit="deg"
|
||||
timeseries={tensionArmAngle}
|
||||
renderValue={(value) => roundDegreesToDecimals(value, 0)}
|
||||
/>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
icon="lu:House"
|
||||
onClick={zeroTensionArmAngle}
|
||||
disabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Set Zero Point
|
||||
</TouchButton>
|
||||
{!state?.tension_arm_state?.zeroed && (
|
||||
<StatusBadge variant="error">Not Zeroed</StatusBadge>
|
||||
)}
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard className="bg-red" title="Winder Mode">
|
||||
<SelectionGroup<Mode>
|
||||
value={state?.mode_state.mode}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
onChange={setMode}
|
||||
orientation="vertical"
|
||||
className="grid h-full grid-cols-2 gap-2"
|
||||
options={{
|
||||
Standby: {
|
||||
children: "Standby",
|
||||
icon: "lu:Power",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
Hold: {
|
||||
children: "Hold",
|
||||
icon: "lu:CirclePause",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
Pull: {
|
||||
children: "Pull",
|
||||
icon: "lu:ChevronsLeft",
|
||||
isActiveClassName: "bg-green-600",
|
||||
className: "h-full",
|
||||
},
|
||||
Wind: {
|
||||
children: "Wind",
|
||||
icon: "lu:RefreshCcw",
|
||||
isActiveClassName: "bg-green-600",
|
||||
disabled: !state?.mode_state?.can_wind,
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard className="bg-red" title="Puller">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Speed"
|
||||
unit="m/min"
|
||||
timeseries={pullerSpeed}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
/>
|
||||
<Label label="Target Speed">
|
||||
<EditValue
|
||||
value={state?.puller_state?.target_speed}
|
||||
unit="m/min"
|
||||
title="Target Speed"
|
||||
defaultValue={defaultState?.puller_state?.target_speed}
|
||||
min={0}
|
||||
max={maxTargetSpeed}
|
||||
step={0.1}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={setPullerTargetSpeed}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard className="bg-red" title="Spool Autostop">
|
||||
<TimeSeriesValueNumeric
|
||||
label="Pulled Distance"
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
unit="m"
|
||||
timeseries={spoolProgress}
|
||||
/>
|
||||
|
||||
<Label label="Target Length">
|
||||
<EditValue
|
||||
value={state?.spool_automatic_action_state.spool_required_meters}
|
||||
unit="m"
|
||||
title="Expected Meters"
|
||||
defaultValue={250}
|
||||
min={10}
|
||||
max={10000}
|
||||
step={10}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={setSpoolAutomaticRequiredMeters}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
onClick={handleResetProgress}
|
||||
disabled={isDisabled}
|
||||
isLoading={isLoading || state?.traverse_state?.is_going_out}
|
||||
>
|
||||
Reset Progress
|
||||
</TouchButton>
|
||||
|
||||
<Label label="After Target Length Reached">
|
||||
<SelectionGroup<SpoolAutomaticActionMode>
|
||||
value={
|
||||
state?.spool_automatic_action_state.spool_automatic_action_mode
|
||||
}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
onChange={setSpoolAutomaticAction}
|
||||
orientation="vertical"
|
||||
options={{
|
||||
Hold: {
|
||||
children: "Hold",
|
||||
icon: "lu:CirclePause",
|
||||
className: "h-full",
|
||||
},
|
||||
Pull: {
|
||||
children: "Pull",
|
||||
icon: "lu:ChevronsLeft",
|
||||
className: "h-full",
|
||||
},
|
||||
|
||||
NoAction: {
|
||||
children: "No Action",
|
||||
icon: "lu:RefreshCcw",
|
||||
className: "h-full",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
|
||||
{/* Reset Progress Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={showResetConfirmDialog}
|
||||
onOpenChange={setShowResetConfirmDialog}
|
||||
>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reset Spool Progress?</DialogTitle>
|
||||
<DialogDescription>
|
||||
The machine is currently in Wind mode. Are you sure you want to
|
||||
reset the spool progress?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<TouchButton
|
||||
variant="destructive"
|
||||
onClick={confirmResetProgress}
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Yes, Reset Progress
|
||||
</TouchButton>
|
||||
<TouchButton
|
||||
variant="outline"
|
||||
onClick={() => setShowResetConfirmDialog(false)}
|
||||
>
|
||||
Cancel
|
||||
</TouchButton>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import {
|
||||
AutoSyncedBigGraph,
|
||||
SyncedFloatingControlPanel,
|
||||
useGraphSync,
|
||||
type GraphConfig,
|
||||
type GraphLine,
|
||||
} from "@/components/graph";
|
||||
import React from "react";
|
||||
import { useGluetex } from "./useGluetex";
|
||||
import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal";
|
||||
import { TimeSeries } from "@/lib/timeseries";
|
||||
import { Unit } from "@/control/units";
|
||||
|
||||
export function GluetexGraphsPage() {
|
||||
const {
|
||||
state,
|
||||
spoolRpm,
|
||||
traversePosition,
|
||||
tensionArmAngle,
|
||||
pullerSpeed,
|
||||
spoolProgress,
|
||||
} = useGluetex();
|
||||
|
||||
const syncHook = useGraphSync("gluetex-group");
|
||||
|
||||
return (
|
||||
<Page className="pb-27">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid gap-4">
|
||||
<PullerSpeedGraph
|
||||
syncHook={syncHook}
|
||||
newData={pullerSpeed}
|
||||
targetSpeed={state?.puller_state?.target_speed}
|
||||
unit="m/min"
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
/>
|
||||
|
||||
<TensionArmAngleGraph
|
||||
syncHook={syncHook}
|
||||
newData={tensionArmAngle}
|
||||
unit="deg"
|
||||
renderValue={(value) => roundDegreesToDecimals(value, 0)}
|
||||
/>
|
||||
|
||||
<SpoolRpmGraph
|
||||
syncHook={syncHook}
|
||||
newData={spoolRpm}
|
||||
unit="rpm"
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
/>
|
||||
|
||||
<TraversePositionGraph
|
||||
syncHook={syncHook}
|
||||
newData={traversePosition}
|
||||
limitInner={state?.traverse_state?.limit_inner}
|
||||
limitOuter={state?.traverse_state?.limit_outer}
|
||||
unit="mm"
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
/>
|
||||
|
||||
<SpoolProgressGraph
|
||||
syncHook={syncHook}
|
||||
newData={spoolProgress}
|
||||
unit="m"
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpoolRpmGraph({
|
||||
syncHook,
|
||||
newData,
|
||||
unit,
|
||||
renderValue,
|
||||
}: {
|
||||
syncHook: ReturnType<typeof useGraphSync>;
|
||||
newData: TimeSeries | null;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
}) {
|
||||
const config: GraphConfig = {
|
||||
title: "Spool Speed",
|
||||
icon: "lu:RotateCcw",
|
||||
colors: {
|
||||
primary: "#10b981",
|
||||
grid: "#e2e8f0",
|
||||
background: "#ffffff",
|
||||
},
|
||||
exportFilename: "spool_rpm_data",
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData,
|
||||
color: "#10b981",
|
||||
}}
|
||||
unit={unit}
|
||||
renderValue={renderValue}
|
||||
config={config}
|
||||
graphId="spool-rpm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TraversePositionGraph({
|
||||
syncHook,
|
||||
newData,
|
||||
limitInner,
|
||||
limitOuter,
|
||||
unit,
|
||||
renderValue,
|
||||
}: {
|
||||
syncHook: ReturnType<typeof useGraphSync>;
|
||||
newData: TimeSeries | null;
|
||||
limitInner?: number;
|
||||
limitOuter?: number;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
}) {
|
||||
const lines: GraphLine[] = [];
|
||||
|
||||
if (limitInner !== undefined) {
|
||||
lines.push({
|
||||
type: "threshold",
|
||||
value: limitInner,
|
||||
color: "#8b5cf6",
|
||||
dash: [5, 5],
|
||||
});
|
||||
}
|
||||
|
||||
if (limitOuter !== undefined) {
|
||||
lines.push({
|
||||
type: "threshold",
|
||||
value: limitOuter,
|
||||
color: "#8b5cf6",
|
||||
dash: [5, 5],
|
||||
});
|
||||
}
|
||||
|
||||
const config: GraphConfig = {
|
||||
title: "Traverse Position",
|
||||
icon: "lu:Move",
|
||||
colors: {
|
||||
primary: "#8b5cf6",
|
||||
},
|
||||
exportFilename: "traverse_position_data",
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData,
|
||||
color: "#8b5cf6",
|
||||
lines,
|
||||
}}
|
||||
unit={unit}
|
||||
renderValue={renderValue}
|
||||
config={config}
|
||||
graphId="traverse-position"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TensionArmAngleGraph({
|
||||
syncHook,
|
||||
newData,
|
||||
unit,
|
||||
renderValue,
|
||||
}: {
|
||||
syncHook: ReturnType<typeof useGraphSync>;
|
||||
newData: TimeSeries | null;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
}) {
|
||||
const config: GraphConfig = {
|
||||
title: "Tension Arm Angle",
|
||||
icon: "lu:RotateCw",
|
||||
colors: {
|
||||
primary: "#f59e0b",
|
||||
},
|
||||
exportFilename: "tension_arm_angle_data",
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData,
|
||||
color: "#f59e0b",
|
||||
}}
|
||||
unit={unit}
|
||||
renderValue={renderValue}
|
||||
config={config}
|
||||
graphId="tension-arm-angle"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpoolProgressGraph({
|
||||
syncHook,
|
||||
newData,
|
||||
unit,
|
||||
renderValue,
|
||||
}: {
|
||||
syncHook: ReturnType<typeof useGraphSync>;
|
||||
newData: TimeSeries | null;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
}) {
|
||||
const config: GraphConfig = {
|
||||
title: "Spool Progress",
|
||||
icon: "lu:RotateCw",
|
||||
colors: {
|
||||
primary: "#f59e0b",
|
||||
},
|
||||
exportFilename: "spool_progress",
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData,
|
||||
color: "#f59e0b",
|
||||
}}
|
||||
unit={unit}
|
||||
renderValue={renderValue}
|
||||
config={config}
|
||||
graphId="spool-progress"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function PullerSpeedGraph({
|
||||
syncHook,
|
||||
newData,
|
||||
targetSpeed,
|
||||
unit,
|
||||
renderValue,
|
||||
}: {
|
||||
syncHook: ReturnType<typeof useGraphSync>;
|
||||
newData: TimeSeries | null;
|
||||
targetSpeed?: number;
|
||||
unit?: Unit;
|
||||
renderValue?: (value: number) => string;
|
||||
}) {
|
||||
const lines: GraphLine[] = [];
|
||||
|
||||
if (targetSpeed !== undefined) {
|
||||
lines.push({
|
||||
type: "target",
|
||||
value: targetSpeed,
|
||||
label: "Target Speed",
|
||||
color: "#06b6d4", // Match series color
|
||||
});
|
||||
}
|
||||
|
||||
const config: GraphConfig = {
|
||||
title: "Puller Speed",
|
||||
icon: "lu:Gauge",
|
||||
colors: {
|
||||
primary: "#06b6d4",
|
||||
},
|
||||
exportFilename: "puller_speed_data",
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoSyncedBigGraph
|
||||
syncHook={syncHook}
|
||||
newData={{
|
||||
newData,
|
||||
color: "#06b6d4",
|
||||
lines,
|
||||
}}
|
||||
unit={unit}
|
||||
renderValue={renderValue}
|
||||
config={config}
|
||||
graphId="puller-speed"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import React from "react";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { HeatingZone } from "@/machines/extruder/HeatingZone";
|
||||
import { useGluetex } from "./useGluetex";
|
||||
|
||||
export function GluetexHeatersPage() {
|
||||
const {
|
||||
state,
|
||||
temperature1,
|
||||
temperature2,
|
||||
temperature3,
|
||||
temperature4,
|
||||
temperature5,
|
||||
temperature6,
|
||||
heater1Power,
|
||||
heater2Power,
|
||||
heater3Power,
|
||||
heater4Power,
|
||||
heater5Power,
|
||||
heater6Power,
|
||||
setHeatingZone1Temperature,
|
||||
setHeatingZone2Temperature,
|
||||
setHeatingZone3Temperature,
|
||||
setHeatingZone4Temperature,
|
||||
setHeatingZone5Temperature,
|
||||
setHeatingZone6Temperature,
|
||||
} = useGluetex();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid>
|
||||
<HeatingZone
|
||||
title={"Temperature 1"}
|
||||
heatingState={state?.heating_states.zone_1}
|
||||
heatingTimeSeries={temperature1}
|
||||
heatingPower={heater1Power}
|
||||
onChangeTargetTemp={setHeatingZone1Temperature}
|
||||
min={0}
|
||||
max={300}
|
||||
/>
|
||||
<HeatingZone
|
||||
title={"Temperature 2"}
|
||||
heatingState={state?.heating_states.zone_2}
|
||||
heatingTimeSeries={temperature2}
|
||||
heatingPower={heater2Power}
|
||||
onChangeTargetTemp={setHeatingZone2Temperature}
|
||||
min={0}
|
||||
max={300}
|
||||
/>
|
||||
<HeatingZone
|
||||
title={"Temperature 3"}
|
||||
heatingState={state?.heating_states.zone_3}
|
||||
heatingTimeSeries={temperature3}
|
||||
heatingPower={heater3Power}
|
||||
onChangeTargetTemp={setHeatingZone3Temperature}
|
||||
min={0}
|
||||
max={300}
|
||||
/>
|
||||
<HeatingZone
|
||||
title={"Temperature 4"}
|
||||
heatingState={state?.heating_states.zone_4}
|
||||
heatingTimeSeries={temperature4}
|
||||
heatingPower={heater4Power}
|
||||
onChangeTargetTemp={setHeatingZone4Temperature}
|
||||
min={0}
|
||||
max={300}
|
||||
/>
|
||||
<HeatingZone
|
||||
title={"Temperature 5"}
|
||||
heatingState={state?.heating_states.zone_5}
|
||||
heatingTimeSeries={temperature5}
|
||||
heatingPower={heater5Power}
|
||||
onChangeTargetTemp={setHeatingZone5Temperature}
|
||||
min={0}
|
||||
max={300}
|
||||
/>
|
||||
<HeatingZone
|
||||
title={"Temperature 6"}
|
||||
heatingState={state?.heating_states.zone_6}
|
||||
heatingTimeSeries={temperature6}
|
||||
heatingPower={heater6Power}
|
||||
onChangeTargetTemp={setHeatingZone6Temperature}
|
||||
min={0}
|
||||
max={300}
|
||||
/>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
|
||||
import React from "react";
|
||||
import winderManualContent from "@/assets/markdown/winder/manual.md?raw";
|
||||
|
||||
export function GluetexManualPage() {
|
||||
return (
|
||||
<Page>
|
||||
<MarkdownWithToc markdownContent={winderManualContent} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Topbar } from "@/components/Topbar";
|
||||
import { gluetexRoute } from "@/routes/routes";
|
||||
import React from "react";
|
||||
|
||||
export function GluetexPage() {
|
||||
const { serial } = gluetexRoute.useParams();
|
||||
return (
|
||||
<Topbar
|
||||
pathname={`/_sidebar/machines/gluetex/${serial}`}
|
||||
items={[
|
||||
{
|
||||
link: "control",
|
||||
activeLink: "control",
|
||||
title: "Control",
|
||||
icon: "lu:CirclePlay",
|
||||
},
|
||||
{
|
||||
link: "addons",
|
||||
activeLink: "addons",
|
||||
title: "Addons",
|
||||
icon: "lu:Puzzle",
|
||||
},
|
||||
{
|
||||
link: "heaters",
|
||||
activeLink: "heaters",
|
||||
title: "Heaters",
|
||||
icon: "lu:Flame",
|
||||
},
|
||||
{
|
||||
link: "graphs",
|
||||
activeLink: "graphs",
|
||||
title: "Graphs",
|
||||
icon: "lu:ChartSpline",
|
||||
},
|
||||
{
|
||||
link: "settings",
|
||||
activeLink: "settings",
|
||||
title: "Config",
|
||||
icon: "lu:Settings",
|
||||
},
|
||||
{
|
||||
link: "manual",
|
||||
activeLink: "manual",
|
||||
title: "Manual",
|
||||
icon: "lu:BookOpen",
|
||||
},
|
||||
{
|
||||
link: "presets",
|
||||
activeLink: "presets",
|
||||
title: "Presets",
|
||||
icon: "lu:Save",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import { Page } from "@/components/Page";
|
||||
|
||||
export function GluetexPresetsPage() {
|
||||
return (
|
||||
<Page>
|
||||
<div className="p-8">
|
||||
<h1 className="mb-4 text-2xl font-bold">Gluetex Presets</h1>
|
||||
<p className="text-gray-600">
|
||||
Preset functionality is not available in this test version. This is a
|
||||
hardcoded frontend for testing purposes only.
|
||||
</p>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,957 @@
|
||||
import { Page } from "@/components/Page";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import React, { useState } from "react";
|
||||
import { useGluetex } from "./useGluetex";
|
||||
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 {
|
||||
getGluetexXLMode,
|
||||
setGluetexXLMode,
|
||||
GLUETEX_TRAVERSE_MAX_STANDARD,
|
||||
GLUETEX_TRAVERSE_MAX_XL,
|
||||
} from "./gluetexConfig";
|
||||
|
||||
export function GluetexSettingPage() {
|
||||
const [xlMode, setXlMode] = useState(getGluetexXLMode());
|
||||
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
setTraverseStepSize,
|
||||
setTraversePadding,
|
||||
setTraverseLimitInner,
|
||||
setTraverseLimitOuter,
|
||||
gotoTraverseHome,
|
||||
setPullerForward,
|
||||
setPullerGearRatio,
|
||||
setSpoolRegulationMode,
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
setSpoolForward,
|
||||
setSpoolAdaptiveTensionTarget,
|
||||
setSpoolAdaptiveRadiusLearningRate,
|
||||
setSpoolAdaptiveMaxSpeedMultiplier,
|
||||
setSpoolAdaptiveAccelerationFactor,
|
||||
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
selectedMachine,
|
||||
filteredMachines,
|
||||
setConnectedMachine,
|
||||
disconnectMachine,
|
||||
setSlavePullerEnabled,
|
||||
setSlavePullerForward,
|
||||
setSlavePullerMinAngle,
|
||||
setSlavePullerMaxAngle,
|
||||
setSlavePullerMinSpeedFactor,
|
||||
setSlavePullerMaxSpeedFactor,
|
||||
zeroSlaveTensionArm,
|
||||
setHeatingPid,
|
||||
setTensionArmMonitorEnabled,
|
||||
setTensionArmMonitorMinAngle,
|
||||
setTensionArmMonitorMaxAngle,
|
||||
} = useGluetex();
|
||||
|
||||
const handleXlModeChange = (enabled: boolean) => {
|
||||
setGluetexXLMode(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 > GLUETEX_TRAVERSE_MAX_STANDARD &&
|
||||
defaultOuter !== undefined
|
||||
) {
|
||||
setTraverseLimitOuter(defaultOuter);
|
||||
setTraverseLimitInner(defaultInner);
|
||||
}
|
||||
if (
|
||||
currentInner > GLUETEX_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 (${GLUETEX_TRAVERSE_MAX_STANDARD}mm)`,
|
||||
icon: "lu:Settings",
|
||||
}}
|
||||
optionTrue={{
|
||||
children: `XL (${GLUETEX_TRAVERSE_MAX_XL}mm)`,
|
||||
icon: "lu:Maximize2",
|
||||
}}
|
||||
onChange={handleXlModeChange}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Step Size">
|
||||
<EditValue
|
||||
value={state?.traverse_state?.step_size}
|
||||
title={"Step Size"}
|
||||
unit="mm"
|
||||
step={0.05}
|
||||
min={0.1}
|
||||
max={75}
|
||||
defaultValue={defaultState?.traverse_state?.step_size}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={(value) => setTraverseStepSize(value)}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Padding">
|
||||
<EditValue
|
||||
value={state?.traverse_state?.padding}
|
||||
title={"Padding"}
|
||||
unit="mm"
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={5}
|
||||
defaultValue={defaultState?.traverse_state?.padding}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={(value) => setTraversePadding(value)}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Spool">
|
||||
<Label label="Speed Algorithm">
|
||||
<SelectionGroup
|
||||
value={state?.spool_speed_controller_state?.regulation_mode}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
options={{
|
||||
MinMax: {
|
||||
children: "Min/Max",
|
||||
icon: "lu:ArrowUpDown",
|
||||
},
|
||||
Adaptive: {
|
||||
children: "Adaptive",
|
||||
icon: "lu:Brain",
|
||||
},
|
||||
}}
|
||||
onChange={(value) =>
|
||||
setSpoolRegulationMode(value as "Adaptive" | "MinMax")
|
||||
}
|
||||
/>
|
||||
</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" && (
|
||||
<>
|
||||
<Label label="Minimum Speed">
|
||||
<EditValue
|
||||
value={state?.spool_speed_controller_state?.minmax_min_speed}
|
||||
title={"Minimum Speed"}
|
||||
unit="rpm"
|
||||
step={10}
|
||||
min={0}
|
||||
max={600}
|
||||
defaultValue={
|
||||
defaultState?.spool_speed_controller_state?.minmax_min_speed
|
||||
}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
onChange={(value) => setSpoolMinMaxMinSpeed(value)}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Maximum Speed">
|
||||
<EditValue
|
||||
value={state?.spool_speed_controller_state?.minmax_max_speed}
|
||||
title={"Maximum Speed"}
|
||||
unit="rpm"
|
||||
step={10}
|
||||
min={0}
|
||||
max={600}
|
||||
defaultValue={
|
||||
defaultState?.spool_speed_controller_state?.minmax_max_speed
|
||||
}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
onChange={(value) => setSpoolMinMaxMaxSpeed(value)}
|
||||
/>
|
||||
</Label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{state?.spool_speed_controller_state?.regulation_mode ===
|
||||
"Adaptive" && (
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Tension Target">
|
||||
<EditValue
|
||||
value={
|
||||
state?.spool_speed_controller_state?.adaptive_tension_target
|
||||
}
|
||||
title={"Tension Target"}
|
||||
unit={undefined}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={
|
||||
defaultState?.spool_speed_controller_state
|
||||
?.adaptive_tension_target
|
||||
}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={(value) => setSpoolAdaptiveTensionTarget(value)}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Learning Rate">
|
||||
<EditValue
|
||||
value={
|
||||
state?.spool_speed_controller_state
|
||||
?.adaptive_radius_learning_rate
|
||||
}
|
||||
title={"Radius Learning Rate"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={100}
|
||||
defaultValue={
|
||||
defaultState?.spool_speed_controller_state
|
||||
?.adaptive_radius_learning_rate
|
||||
}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={(value) =>
|
||||
setSpoolAdaptiveRadiusLearningRate(value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Max Speed Multiplier">
|
||||
<EditValue
|
||||
value={
|
||||
state?.spool_speed_controller_state
|
||||
?.adaptive_max_speed_multiplier
|
||||
}
|
||||
title={"Max Speed Multiplier"}
|
||||
unit={undefined}
|
||||
step={0.1}
|
||||
min={0.1}
|
||||
max={10}
|
||||
defaultValue={
|
||||
defaultState?.spool_speed_controller_state
|
||||
?.adaptive_max_speed_multiplier
|
||||
}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={(value) =>
|
||||
setSpoolAdaptiveMaxSpeedMultiplier(value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Acceleration Factor">
|
||||
<EditValue
|
||||
value={
|
||||
state?.spool_speed_controller_state
|
||||
?.adaptive_acceleration_factor
|
||||
}
|
||||
title={"Acceleration Factor"}
|
||||
unit={undefined}
|
||||
step={0.01}
|
||||
min={0.01}
|
||||
max={100}
|
||||
defaultValue={
|
||||
defaultState?.spool_speed_controller_state
|
||||
?.adaptive_acceleration_factor
|
||||
}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={(value) =>
|
||||
setSpoolAdaptiveAccelerationFactor(value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Deaccel. Urgency">
|
||||
<EditValue
|
||||
value={
|
||||
state?.spool_speed_controller_state
|
||||
?.adaptive_deacceleration_urgency_multiplier
|
||||
}
|
||||
title={"Deacceleration Urgency Multiplier"}
|
||||
unit={undefined}
|
||||
step={0.5}
|
||||
min={1}
|
||||
max={100}
|
||||
defaultValue={
|
||||
defaultState?.spool_speed_controller_state
|
||||
?.adaptive_deacceleration_urgency_multiplier
|
||||
}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={(value) =>
|
||||
setSpoolAdaptiveDeaccelerationUrgencyMultiplier(value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Puller">
|
||||
<Label label="Rotation Direction">
|
||||
<SelectionGroupBoolean
|
||||
value={state?.puller_state?.forward}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
optionFalse={{
|
||||
children: "Reverse",
|
||||
icon: "lu:RotateCcw",
|
||||
}}
|
||||
optionTrue={{
|
||||
children: "Forward",
|
||||
icon: "lu:RotateCw",
|
||||
}}
|
||||
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="Slave Puller">
|
||||
<Label label="Enable Slave Puller">
|
||||
<SelectionGroupBoolean
|
||||
value={state?.slave_puller_state?.enabled}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
optionFalse={{
|
||||
children: "Disabled",
|
||||
icon: "lu:Circle",
|
||||
}}
|
||||
optionTrue={{
|
||||
children: "Enabled",
|
||||
icon: "lu:Circle",
|
||||
}}
|
||||
onChange={(value) => setSlavePullerEnabled(value)}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Rotation Direction">
|
||||
<SelectionGroupBoolean
|
||||
value={state?.slave_puller_state?.forward}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
optionFalse={{
|
||||
children: "Reverse",
|
||||
icon: "lu:RotateCcw",
|
||||
}}
|
||||
optionTrue={{
|
||||
children: "Forward",
|
||||
icon: "lu:RotateCw",
|
||||
}}
|
||||
onChange={(value) => setSlavePullerForward(value)}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Min Angle (Detection Zone)">
|
||||
<EditValue
|
||||
value={state?.slave_puller_state?.min_angle}
|
||||
title={"Minimum Angle"}
|
||||
unit="deg"
|
||||
step={1}
|
||||
min={0}
|
||||
max={
|
||||
state?.slave_puller_state?.max_angle
|
||||
? state.slave_puller_state.max_angle - 5
|
||||
: 85
|
||||
}
|
||||
defaultValue={20}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
onChange={(value) => setSlavePullerMinAngle(value)}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Max Angle (Detection Zone)">
|
||||
<EditValue
|
||||
value={state?.slave_puller_state?.max_angle}
|
||||
title={"Maximum Angle"}
|
||||
unit="deg"
|
||||
step={1}
|
||||
min={
|
||||
state?.slave_puller_state?.min_angle
|
||||
? state.slave_puller_state.min_angle + 5
|
||||
: 25
|
||||
}
|
||||
max={180}
|
||||
defaultValue={90}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
onChange={(value) => setSlavePullerMaxAngle(value)}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Min Speed Factor (Optional)">
|
||||
<EditValue
|
||||
value={state?.slave_puller_state?.min_speed_factor ?? 0}
|
||||
title={"Minimum Speed Factor"}
|
||||
unit={undefined}
|
||||
step={0.05}
|
||||
min={0.1}
|
||||
max={
|
||||
state?.slave_puller_state?.max_speed_factor
|
||||
? state.slave_puller_state.max_speed_factor - 0.1
|
||||
: 2
|
||||
}
|
||||
defaultValue={0}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={(value) =>
|
||||
setSlavePullerMinSpeedFactor(value === 0 ? null : value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Max Speed Factor (Optional)">
|
||||
<EditValue
|
||||
value={state?.slave_puller_state?.max_speed_factor ?? 0}
|
||||
title={"Maximum Speed Factor"}
|
||||
unit={undefined}
|
||||
step={0.05}
|
||||
min={
|
||||
state?.slave_puller_state?.min_speed_factor
|
||||
? state.slave_puller_state.min_speed_factor + 0.1
|
||||
: 0.2
|
||||
}
|
||||
max={3}
|
||||
defaultValue={0}
|
||||
renderValue={(value) => roundToDecimals(value, 2)}
|
||||
onChange={(value) =>
|
||||
setSlavePullerMaxSpeedFactor(value === 0 ? null : value)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Heating Zone 1">
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Proportional Gain (Kp)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_1?.kp}
|
||||
title={"Kp"}
|
||||
unit={undefined}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={10}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_1?.kp}
|
||||
renderValue={(value) => roundToDecimals(value, 3)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_1",
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_1?.ki ?? 0,
|
||||
state?.heating_pid_settings?.zone_1?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Integral Gain (Ki)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_1?.ki}
|
||||
title={"Ki"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_1?.ki}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_1",
|
||||
state?.heating_pid_settings?.zone_1?.kp ?? 0,
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_1?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Derivative Gain (Kd)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_1?.kd}
|
||||
title={"Kd"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_1?.kd}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_1",
|
||||
state?.heating_pid_settings?.zone_1?.kp ?? 0,
|
||||
state?.heating_pid_settings?.zone_1?.ki ?? 0,
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Heating Zone 2">
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Proportional Gain (Kp)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_2?.kp}
|
||||
title={"Kp"}
|
||||
unit={undefined}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={10}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_2?.kp}
|
||||
renderValue={(value) => roundToDecimals(value, 3)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_2",
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_2?.ki ?? 0,
|
||||
state?.heating_pid_settings?.zone_2?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Integral Gain (Ki)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_2?.ki}
|
||||
title={"Ki"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_2?.ki}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_2",
|
||||
state?.heating_pid_settings?.zone_2?.kp ?? 0,
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_2?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Derivative Gain (Kd)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_2?.kd}
|
||||
title={"Kd"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_2?.kd}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_2",
|
||||
state?.heating_pid_settings?.zone_2?.kp ?? 0,
|
||||
state?.heating_pid_settings?.zone_2?.ki ?? 0,
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Heating Zone 3">
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Proportional Gain (Kp)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_3?.kp}
|
||||
title={"Kp"}
|
||||
unit={undefined}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={10}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_3?.kp}
|
||||
renderValue={(value) => roundToDecimals(value, 3)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_3",
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_3?.ki ?? 0,
|
||||
state?.heating_pid_settings?.zone_3?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Integral Gain (Ki)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_3?.ki}
|
||||
title={"Ki"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_3?.ki}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_3",
|
||||
state?.heating_pid_settings?.zone_3?.kp ?? 0,
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_3?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Derivative Gain (Kd)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_3?.kd}
|
||||
title={"Kd"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_3?.kd}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_3",
|
||||
state?.heating_pid_settings?.zone_3?.kp ?? 0,
|
||||
state?.heating_pid_settings?.zone_3?.ki ?? 0,
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Heating Zone 4">
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Proportional Gain (Kp)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_4?.kp}
|
||||
title={"Kp"}
|
||||
unit={undefined}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={10}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_4?.kp}
|
||||
renderValue={(value) => roundToDecimals(value, 3)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_4",
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_4?.ki ?? 0,
|
||||
state?.heating_pid_settings?.zone_4?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Integral Gain (Ki)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_4?.ki}
|
||||
title={"Ki"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_4?.ki}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_4",
|
||||
state?.heating_pid_settings?.zone_4?.kp ?? 0,
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_4?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Derivative Gain (Kd)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_4?.kd}
|
||||
title={"Kd"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_4?.kd}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_4",
|
||||
state?.heating_pid_settings?.zone_4?.kp ?? 0,
|
||||
state?.heating_pid_settings?.zone_4?.ki ?? 0,
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Heating Zone 5">
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Proportional Gain (Kp)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_5?.kp}
|
||||
title={"Kp"}
|
||||
unit={undefined}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={10}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_5?.kp}
|
||||
renderValue={(value) => roundToDecimals(value, 3)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_5",
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_5?.ki ?? 0,
|
||||
state?.heating_pid_settings?.zone_5?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Integral Gain (Ki)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_5?.ki}
|
||||
title={"Ki"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_5?.ki}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_5",
|
||||
state?.heating_pid_settings?.zone_5?.kp ?? 0,
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_5?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Derivative Gain (Kd)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_5?.kd}
|
||||
title={"Kd"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_5?.kd}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_5",
|
||||
state?.heating_pid_settings?.zone_5?.kp ?? 0,
|
||||
state?.heating_pid_settings?.zone_5?.ki ?? 0,
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<ControlCard title="Heating Zone 6">
|
||||
<div className="flex flex-row flex-wrap gap-4">
|
||||
<Label label="Proportional Gain (Kp)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_6?.kp}
|
||||
title={"Kp"}
|
||||
unit={undefined}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={10}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_6?.kp}
|
||||
renderValue={(value) => roundToDecimals(value, 3)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_6",
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_6?.ki ?? 0,
|
||||
state?.heating_pid_settings?.zone_6?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Integral Gain (Ki)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_6?.ki}
|
||||
title={"Ki"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_6?.ki}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_6",
|
||||
state?.heating_pid_settings?.zone_6?.kp ?? 0,
|
||||
value,
|
||||
state?.heating_pid_settings?.zone_6?.kd ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Derivative Gain (Kd)">
|
||||
<EditValue
|
||||
value={state?.heating_pid_settings?.zone_6?.kd}
|
||||
title={"Kd"}
|
||||
unit={undefined}
|
||||
step={0.001}
|
||||
min={0}
|
||||
max={1}
|
||||
defaultValue={defaultState?.heating_pid_settings?.zone_6?.kd}
|
||||
renderValue={(value) => roundToDecimals(value, 4)}
|
||||
onChange={(value) =>
|
||||
setHeatingPid(
|
||||
"zone_6",
|
||||
state?.heating_pid_settings?.zone_6?.kp ?? 0,
|
||||
state?.heating_pid_settings?.zone_6?.ki ?? 0,
|
||||
value,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</ControlCard>
|
||||
|
||||
<MachineSelector
|
||||
machines={filteredMachines}
|
||||
selectedMachine={selectedMachine}
|
||||
connectedMachineState={state?.connected_machine_state}
|
||||
setConnectedMachine={setConnectedMachine}
|
||||
clearConnectedMachine={() => {
|
||||
if (!selectedMachine) return;
|
||||
setConnectedMachine({
|
||||
machine_identification: { vendor: 0, machine: 0 },
|
||||
serial: 0,
|
||||
});
|
||||
disconnectMachine(selectedMachine.machine_identification_unique);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ControlCard title="Tension Arm Monitor">
|
||||
<Label label="Enable Monitoring">
|
||||
<SelectionGroupBoolean
|
||||
value={state?.tension_arm_monitor_state?.enabled}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
optionFalse={{
|
||||
children: "Disabled",
|
||||
icon: "lu:CircleOff",
|
||||
}}
|
||||
optionTrue={{
|
||||
children: "Enabled",
|
||||
icon: "lu:Shield",
|
||||
}}
|
||||
onChange={(value) => setTensionArmMonitorEnabled(value)}
|
||||
/>
|
||||
</Label>
|
||||
{state?.tension_arm_monitor_state?.triggered && (
|
||||
<div className="rounded-md bg-red-500/10 p-3 text-red-600 dark:text-red-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">⚠️</span>
|
||||
<span className="font-semibold">
|
||||
Tension Arm Limit Exceeded - Machine Stopped
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Label label="Minimum Angle">
|
||||
<EditValue
|
||||
value={state?.tension_arm_monitor_state?.min_angle}
|
||||
title={"Minimum Angle"}
|
||||
unit="deg"
|
||||
step={1}
|
||||
min={0}
|
||||
max={180}
|
||||
defaultValue={defaultState?.tension_arm_monitor_state?.min_angle}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={(value) => setTensionArmMonitorMinAngle(value)}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Maximum Angle">
|
||||
<EditValue
|
||||
value={state?.tension_arm_monitor_state?.max_angle}
|
||||
title={"Maximum Angle"}
|
||||
unit="deg"
|
||||
step={1}
|
||||
min={0}
|
||||
max={180}
|
||||
defaultValue={defaultState?.tension_arm_monitor_state?.max_angle}
|
||||
renderValue={(value) => roundToDecimals(value, 1)}
|
||||
onChange={(value) => setTensionArmMonitorMaxAngle(value)}
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Configuration helper for Gluetex machine settings
|
||||
*/
|
||||
|
||||
const GLUETEX_XL_MODE_KEY = "gluetex_xl_mode";
|
||||
|
||||
export const GLUETEX_TRAVERSE_MAX_STANDARD = 180; // mm
|
||||
export const GLUETEX_TRAVERSE_MAX_XL = 385; // mm
|
||||
|
||||
/**
|
||||
* Get the XL mode setting from localStorage
|
||||
* @returns true if XL mode is enabled, false otherwise
|
||||
*/
|
||||
export function getGluetexXLMode(): boolean {
|
||||
const stored = localStorage.getItem(GLUETEX_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 setGluetexXLMode(enabled: boolean): void {
|
||||
localStorage.setItem(GLUETEX_XL_MODE_KEY, enabled.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum traverse limit based on XL mode
|
||||
* @returns the maximum traverse limit in mm
|
||||
*/
|
||||
export function getGluetexTraverseMax(): number {
|
||||
return getGluetexXLMode()
|
||||
? GLUETEX_TRAVERSE_MAX_XL
|
||||
: GLUETEX_TRAVERSE_MAX_STANDARD;
|
||||
}
|
||||
@@ -0,0 +1,932 @@
|
||||
/**
|
||||
* @file gluetexNamespace.ts
|
||||
* @description TypeScript implementation of Gluetex namespace with real backend connection
|
||||
* Standard winder features connect to backend, addon features use local state
|
||||
*/
|
||||
|
||||
import { StoreApi } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
EventHandler,
|
||||
eventSchema,
|
||||
Event,
|
||||
handleUnhandledEventError,
|
||||
NamespaceId,
|
||||
createNamespaceHookImplementation,
|
||||
ThrottledStoreUpdater,
|
||||
} from "../../../client/socketioStore";
|
||||
import { MachineIdentificationUnique } from "@/machines/types";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
createTimeSeries,
|
||||
TimeSeries,
|
||||
TimeSeriesValue,
|
||||
} from "@/lib/timeseries";
|
||||
|
||||
// ========== Event Schema Definitions (Backend) ==========
|
||||
|
||||
/**
|
||||
* Consolidated live values event schema (30FPS data)
|
||||
*/
|
||||
export const liveValuesEventDataSchema = z.object({
|
||||
traverse_position: z.number().nullable(),
|
||||
puller_speed: z.number(),
|
||||
spool_rpm: z.number(),
|
||||
tension_arm_angle: z.number(),
|
||||
spool_progress: z.number(),
|
||||
temperature_1: z.number(),
|
||||
temperature_2: z.number(),
|
||||
temperature_3: z.number(),
|
||||
temperature_4: z.number(),
|
||||
temperature_5: z.number(),
|
||||
temperature_6: z.number(),
|
||||
heater_1_power: z.number(),
|
||||
heater_2_power: z.number(),
|
||||
heater_3_power: z.number(),
|
||||
heater_4_power: z.number(),
|
||||
heater_5_power: z.number(),
|
||||
heater_6_power: z.number(),
|
||||
slave_puller_speed: z.number(),
|
||||
slave_tension_arm_angle: z.number(),
|
||||
addon_tension_arm_angle: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Puller regulation type enum
|
||||
*/
|
||||
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
|
||||
*/
|
||||
export const modeSchema = z.enum(["Standby", "Hold", "Pull", "Wind"]);
|
||||
export type Mode = z.infer<typeof modeSchema>;
|
||||
|
||||
/**
|
||||
* Machine operation mode enum
|
||||
*/
|
||||
export const spoolAutomaticActionModeSchema = z.enum([
|
||||
"NoAction",
|
||||
"Pull",
|
||||
"Hold",
|
||||
]);
|
||||
|
||||
export type SpoolAutomaticActionMode = z.infer<
|
||||
typeof spoolAutomaticActionModeSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* Spool speed controller regulation mode enum
|
||||
*/
|
||||
export const spoolRegulationModeSchema = z.enum(["Adaptive", "MinMax"]);
|
||||
export type SpoolRegulationMode = z.infer<typeof spoolRegulationModeSchema>;
|
||||
|
||||
/**
|
||||
* Heating zone enum to match backend
|
||||
*/
|
||||
export type HeatingZone =
|
||||
| "Zone1"
|
||||
| "Zone2"
|
||||
| "Zone3"
|
||||
| "Zone4"
|
||||
| "Zone5"
|
||||
| "Zone6";
|
||||
|
||||
export const spoolAutomaticActionStateSchema = z.object({
|
||||
spool_required_meters: z.number(),
|
||||
spool_automatic_action_mode: spoolAutomaticActionModeSchema,
|
||||
});
|
||||
|
||||
export type SpoolAutomaticActionState = z.infer<
|
||||
typeof spoolAutomaticActionStateSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* Traverse state schema
|
||||
*/
|
||||
export const traverseStateSchema = z.object({
|
||||
limit_inner: z.number(),
|
||||
limit_outer: z.number(),
|
||||
position_in: z.number(),
|
||||
position_out: z.number(),
|
||||
is_going_in: z.boolean(),
|
||||
is_going_out: z.boolean(),
|
||||
is_homed: z.boolean(),
|
||||
is_going_home: z.boolean(),
|
||||
is_traversing: z.boolean(),
|
||||
laserpointer: z.boolean(),
|
||||
step_size: z.number(),
|
||||
padding: z.number(),
|
||||
can_go_in: z.boolean(),
|
||||
can_go_out: z.boolean(),
|
||||
can_go_home: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Puller state schema
|
||||
*/
|
||||
export const pullerStateSchema = z.object({
|
||||
regulation: pullerRegulationSchema,
|
||||
target_speed: z.number(),
|
||||
target_diameter: z.number(),
|
||||
forward: z.boolean(),
|
||||
gear_ratio: gearRatioSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Mode state schema
|
||||
*/
|
||||
export const modeStateSchema = z.object({
|
||||
mode: modeSchema,
|
||||
can_wind: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Connected machine state scheme
|
||||
*/
|
||||
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(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Tension arm state schema
|
||||
*/
|
||||
export const tensionArmStateSchema = z.object({
|
||||
zeroed: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Spool speed controller state schema
|
||||
*/
|
||||
export const spoolSpeedControllerStateSchema = z.object({
|
||||
regulation_mode: spoolRegulationModeSchema,
|
||||
minmax_min_speed: z.number(),
|
||||
minmax_max_speed: z.number(),
|
||||
adaptive_tension_target: z.number(),
|
||||
adaptive_radius_learning_rate: z.number(),
|
||||
adaptive_max_speed_multiplier: z.number(),
|
||||
adaptive_acceleration_factor: z.number(),
|
||||
adaptive_deacceleration_urgency_multiplier: z.number(),
|
||||
forward: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Heating state schema
|
||||
*/
|
||||
export const heatingStateSchema = z.object({
|
||||
target_temperature: z.number(),
|
||||
wiring_error: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Heating PID settings schema
|
||||
*/
|
||||
export const heatingPidSettingsSchema = z.object({
|
||||
ki: z.number(),
|
||||
kp: z.number(),
|
||||
kd: z.number(),
|
||||
zone: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Heating PID states schema
|
||||
*/
|
||||
export const heatingPidStatesSchema = z.object({
|
||||
zone_1: heatingPidSettingsSchema,
|
||||
zone_2: heatingPidSettingsSchema,
|
||||
zone_3: heatingPidSettingsSchema,
|
||||
zone_4: heatingPidSettingsSchema,
|
||||
zone_5: heatingPidSettingsSchema,
|
||||
zone_6: heatingPidSettingsSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Heating states schema
|
||||
*/
|
||||
export const heatingStatesSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
zone_1: heatingStateSchema,
|
||||
zone_2: heatingStateSchema,
|
||||
zone_3: heatingStateSchema,
|
||||
zone_4: heatingStateSchema,
|
||||
zone_5: heatingStateSchema,
|
||||
zone_6: heatingStateSchema,
|
||||
});
|
||||
|
||||
/**
|
||||
* Addon motor state schema (from backend)
|
||||
*/
|
||||
export const addonMotorStateSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
master_ratio: z.number(),
|
||||
slave_ratio: z.number(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Tension arm monitor state schema (from backend)
|
||||
*/
|
||||
export const tensionArmMonitorStateSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
min_angle: z.number(),
|
||||
max_angle: z.number(),
|
||||
triggered: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Consolidated state event schema (state changes only) - from backend
|
||||
*/
|
||||
export const stateEventDataSchema = z.object({
|
||||
is_default_state: z.boolean(),
|
||||
traverse_state: traverseStateSchema,
|
||||
puller_state: pullerStateSchema,
|
||||
mode_state: modeStateSchema,
|
||||
tension_arm_state: tensionArmStateSchema,
|
||||
spool_speed_controller_state: spoolSpeedControllerStateSchema,
|
||||
spool_automatic_action_state: spoolAutomaticActionStateSchema,
|
||||
heating_states: heatingStatesSchema,
|
||||
heating_pid_settings: heatingPidStatesSchema,
|
||||
connected_machine_state: connectedMachineStateSchema,
|
||||
addon_motor_3_state: addonMotorStateSchema,
|
||||
addon_motor_4_state: addonMotorStateSchema,
|
||||
addon_tension_arm_state: tensionArmStateSchema,
|
||||
tension_arm_monitor_state: tensionArmMonitorStateSchema,
|
||||
});
|
||||
|
||||
// ========== Event Schemas with Wrappers ==========
|
||||
|
||||
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
|
||||
export const stateEventSchema = eventSchema(stateEventDataSchema);
|
||||
|
||||
export type StateEvent = z.infer<typeof stateEventSchema>;
|
||||
export type StateEventData = z.infer<typeof stateEventDataSchema>;
|
||||
|
||||
// ========== Addon Types (Local State Only) ==========
|
||||
|
||||
export type StepperMode = "Standby" | "Run";
|
||||
export type HeatingMode = "Standby" | "Heating";
|
||||
|
||||
export type SlavePullerState = {
|
||||
enabled: boolean;
|
||||
forward: boolean;
|
||||
min_angle: number;
|
||||
max_angle: number;
|
||||
min_speed_factor: number | null;
|
||||
max_speed_factor: number | null;
|
||||
tension_arm: {
|
||||
zeroed: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type MotorRatiosState = {
|
||||
stepper3_master: number;
|
||||
stepper3_slave: number;
|
||||
stepper4_master: number;
|
||||
stepper4_slave: number;
|
||||
};
|
||||
|
||||
export type StepperState = {
|
||||
stepper3_mode: StepperMode;
|
||||
stepper4_mode: StepperMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to convert backend addon motor state to frontend MotorRatiosState
|
||||
*/
|
||||
function getMotorRatiosFromBackend(
|
||||
motor3: z.infer<typeof addonMotorStateSchema>,
|
||||
motor4: z.infer<typeof addonMotorStateSchema>,
|
||||
): MotorRatiosState {
|
||||
return {
|
||||
stepper3_master: motor3.master_ratio,
|
||||
stepper3_slave: motor3.slave_ratio,
|
||||
stepper4_master: motor4.master_ratio,
|
||||
stepper4_slave: motor4.slave_ratio,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to determine stepper3 mode from backend enabled state
|
||||
*/
|
||||
function getStepper3ModeFromBackend(
|
||||
motor3: z.infer<typeof addonMotorStateSchema>,
|
||||
): StepperMode {
|
||||
return motor3.enabled ? "Run" : "Standby";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to determine stepper4 mode from backend enabled state
|
||||
*/
|
||||
function getStepper4ModeFromBackend(
|
||||
motor4: z.infer<typeof addonMotorStateSchema>,
|
||||
): StepperMode {
|
||||
return motor4.enabled ? "Run" : "Standby";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to determine heating mode from backend enabled state
|
||||
*/
|
||||
function getHeatingModeFromBackend(
|
||||
heatingStates: z.infer<typeof heatingStatesSchema>,
|
||||
): HeatingMode {
|
||||
return heatingStates.enabled ? "Heating" : "Standby";
|
||||
}
|
||||
|
||||
export type HeatingState = {
|
||||
heating_mode: HeatingMode;
|
||||
};
|
||||
|
||||
export type TemperatureState = {
|
||||
current_temperature: number;
|
||||
min_temperature: number;
|
||||
max_temperature: number;
|
||||
};
|
||||
|
||||
export type QualityControlState = {
|
||||
temperature1: TemperatureState;
|
||||
temperature2: TemperatureState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extended state event data with addon fields
|
||||
*/
|
||||
export type ExtendedStateEventData = StateEventData & {
|
||||
slave_puller_state: SlavePullerState;
|
||||
motor_ratios_state: MotorRatiosState;
|
||||
stepper_state: StepperState;
|
||||
heating_state: HeatingState;
|
||||
quality_control_state: QualityControlState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extended state event with addon fields
|
||||
*/
|
||||
export type ExtendedStateEvent = {
|
||||
name: string;
|
||||
ts: number;
|
||||
data: ExtendedStateEventData;
|
||||
};
|
||||
|
||||
// ========== Store Definition ==========
|
||||
|
||||
export type GluetexNamespaceStore = {
|
||||
// State event from server (extended with addon fields)
|
||||
state: ExtendedStateEvent | null;
|
||||
defaultState: ExtendedStateEvent | null;
|
||||
|
||||
// Time series data for live values (from backend)
|
||||
traversePosition: TimeSeries;
|
||||
pullerSpeed: TimeSeries;
|
||||
spoolRpm: TimeSeries;
|
||||
tensionArmAngle: TimeSeries;
|
||||
spoolProgress: TimeSeries;
|
||||
temperature1: TimeSeries;
|
||||
temperature2: TimeSeries;
|
||||
temperature3: TimeSeries;
|
||||
temperature4: TimeSeries;
|
||||
temperature5: TimeSeries;
|
||||
temperature6: TimeSeries;
|
||||
heater1Power: TimeSeries;
|
||||
heater2Power: TimeSeries;
|
||||
heater3Power: TimeSeries;
|
||||
heater4Power: TimeSeries;
|
||||
heater5Power: TimeSeries;
|
||||
heater6Power: TimeSeries;
|
||||
|
||||
// Time series data for addons (local)
|
||||
slavePullerSpeed: TimeSeries;
|
||||
slaveTensionArmAngle: TimeSeries;
|
||||
addonTensionArmAngle: TimeSeries;
|
||||
};
|
||||
|
||||
// Constants for time durations
|
||||
const TWENTY_MILLISECOND = 20;
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECOND = 5 * ONE_SECOND;
|
||||
const ONE_HOUR = 60 * 60 * ONE_SECOND;
|
||||
|
||||
// Create time series for backend values
|
||||
const { initialTimeSeries: spoolProgress, insert: addSpoolProgress } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries(
|
||||
TWENTY_MILLISECOND,
|
||||
ONE_SECOND,
|
||||
FIVE_SECOND,
|
||||
ONE_HOUR,
|
||||
);
|
||||
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
// Create time series for temperature values (from backend)
|
||||
const { initialTimeSeries: temperature1, insert: addTemperature1 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: temperature2, insert: addTemperature2 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: temperature3, insert: addTemperature3 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: temperature4, insert: addTemperature4 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: temperature5, insert: addTemperature5 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: temperature6, insert: addTemperature6 } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
const { initialTimeSeries: heater1Power, insert: addHeater1Power } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: heater2Power, insert: addHeater2Power } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: heater3Power, insert: addHeater3Power } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: heater4Power, insert: addHeater4Power } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: heater5Power, insert: addHeater5Power } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const { initialTimeSeries: heater6Power, insert: addHeater6Power } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
// Create time series for addon values (local)
|
||||
const { initialTimeSeries: slavePullerSpeed, insert: addSlavePullerSpeed } =
|
||||
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const {
|
||||
initialTimeSeries: slaveTensionArmAngle,
|
||||
insert: addSlaveTensionArmAngle,
|
||||
} = createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
const {
|
||||
initialTimeSeries: addonTensionArmAngle,
|
||||
insert: addAddonTensionArmAngle,
|
||||
} = createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
|
||||
|
||||
// Default addon state
|
||||
const DEFAULT_ADDON_STATE = {
|
||||
slave_puller_state: {
|
||||
enabled: false,
|
||||
forward: true,
|
||||
min_angle: 20.0,
|
||||
max_angle: 90.0,
|
||||
min_speed_factor: null,
|
||||
max_speed_factor: null,
|
||||
tension_arm: {
|
||||
zeroed: false,
|
||||
},
|
||||
},
|
||||
motor_ratios_state: {
|
||||
stepper3_master: 1.0,
|
||||
stepper3_slave: 1.0,
|
||||
stepper4_master: 1.0,
|
||||
stepper4_slave: 1.0,
|
||||
},
|
||||
stepper_state: {
|
||||
stepper3_mode: "Standby" as StepperMode,
|
||||
stepper4_mode: "Standby" as StepperMode,
|
||||
},
|
||||
heating_state: {
|
||||
heating_mode: "Standby" as HeatingMode,
|
||||
},
|
||||
quality_control_state: {
|
||||
temperature1: {
|
||||
current_temperature: 85.0,
|
||||
min_temperature: 75.0,
|
||||
max_temperature: 95.0,
|
||||
},
|
||||
temperature2: {
|
||||
current_temperature: 125.0,
|
||||
min_temperature: 115.0,
|
||||
max_temperature: 135.0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory function to create a new Gluetex namespace store
|
||||
* @returns A new Zustand store instance for Gluetex namespace
|
||||
*/
|
||||
export const createGluetexNamespaceStore =
|
||||
(): StoreApi<GluetexNamespaceStore> =>
|
||||
create<GluetexNamespaceStore>(() => {
|
||||
return {
|
||||
// State event from server (will be extended with addon state)
|
||||
state: null,
|
||||
defaultState: null,
|
||||
|
||||
// Time series data for live values
|
||||
traversePosition,
|
||||
pullerSpeed,
|
||||
spoolRpm,
|
||||
tensionArmAngle,
|
||||
spoolProgress,
|
||||
temperature1,
|
||||
temperature2,
|
||||
temperature3,
|
||||
temperature4,
|
||||
temperature5,
|
||||
temperature6,
|
||||
heater1Power,
|
||||
heater2Power,
|
||||
heater3Power,
|
||||
heater4Power,
|
||||
heater5Power,
|
||||
heater6Power,
|
||||
|
||||
// Time series data for addons
|
||||
slavePullerSpeed,
|
||||
slaveTensionArmAngle,
|
||||
addonTensionArmAngle,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a message handler for Gluetex namespace events with validation and appropriate caching strategies
|
||||
* @param store The store to update when messages are received
|
||||
* @param throttledUpdater Throttled updater for batching updates at 30 FPS
|
||||
* @returns A message handler function
|
||||
*/
|
||||
export function gluetexMessageHandler(
|
||||
store: StoreApi<GluetexNamespaceStore>,
|
||||
throttledUpdater: ThrottledStoreUpdater<GluetexNamespaceStore>,
|
||||
): EventHandler {
|
||||
return (event: Event<any>) => {
|
||||
const eventName = event.name;
|
||||
|
||||
// Helper function to update store through buffer
|
||||
const updateStore = (
|
||||
updater: (state: GluetexNamespaceStore) => GluetexNamespaceStore,
|
||||
) => {
|
||||
throttledUpdater.updateWith(updater);
|
||||
};
|
||||
|
||||
try {
|
||||
if (eventName === "StateEvent") {
|
||||
console.log(event);
|
||||
// Parse and validate the state event
|
||||
const stateEvent = stateEventSchema.parse(event);
|
||||
|
||||
updateStore((state) => {
|
||||
// Derive motor ratios from backend addon motor state
|
||||
const motorRatiosState = getMotorRatiosFromBackend(
|
||||
stateEvent.data.addon_motor_3_state,
|
||||
stateEvent.data.addon_motor_4_state,
|
||||
);
|
||||
|
||||
// Derive stepper modes from backend enabled state
|
||||
const stepper3Mode = getStepper3ModeFromBackend(
|
||||
stateEvent.data.addon_motor_3_state,
|
||||
);
|
||||
const stepper4Mode = getStepper4ModeFromBackend(
|
||||
stateEvent.data.addon_motor_4_state,
|
||||
);
|
||||
|
||||
// Derive heating mode from backend enabled state
|
||||
const heatingMode = getHeatingModeFromBackend(
|
||||
stateEvent.data.heating_states,
|
||||
);
|
||||
|
||||
// Extend backend state with addon state (some local, some derived from backend)
|
||||
const extendedData: ExtendedStateEventData = {
|
||||
...stateEvent.data,
|
||||
// Preserve existing local-only addon state
|
||||
slave_puller_state:
|
||||
state.state?.data.slave_puller_state ||
|
||||
DEFAULT_ADDON_STATE.slave_puller_state,
|
||||
// Derive from backend addon motor state
|
||||
motor_ratios_state: motorRatiosState,
|
||||
stepper_state: {
|
||||
stepper3_mode: stepper3Mode,
|
||||
stepper4_mode: stepper4Mode,
|
||||
},
|
||||
heating_state: {
|
||||
heating_mode: heatingMode,
|
||||
},
|
||||
quality_control_state:
|
||||
state.state?.data.quality_control_state ||
|
||||
DEFAULT_ADDON_STATE.quality_control_state,
|
||||
};
|
||||
|
||||
const extendedState: ExtendedStateEvent = {
|
||||
name: stateEvent.name,
|
||||
ts: stateEvent.ts,
|
||||
data: extendedData,
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
state: extendedState,
|
||||
// only set default state if is_default_state is true
|
||||
defaultState: stateEvent.data.is_default_state
|
||||
? extendedState
|
||||
: state.defaultState,
|
||||
};
|
||||
});
|
||||
} else if (eventName === "LiveValuesEvent") {
|
||||
// Parse and validate the live values event
|
||||
const liveValuesEvent = liveValuesEventSchema.parse(event);
|
||||
|
||||
// Extract values and add to time series
|
||||
const {
|
||||
traverse_position,
|
||||
puller_speed,
|
||||
spool_rpm,
|
||||
tension_arm_angle,
|
||||
spool_progress,
|
||||
temperature_1,
|
||||
temperature_2,
|
||||
temperature_3,
|
||||
temperature_4,
|
||||
temperature_5,
|
||||
temperature_6,
|
||||
heater_1_power,
|
||||
heater_2_power,
|
||||
heater_3_power,
|
||||
heater_4_power,
|
||||
heater_5_power,
|
||||
heater_6_power,
|
||||
slave_puller_speed,
|
||||
slave_tension_arm_angle,
|
||||
addon_tension_arm_angle,
|
||||
} = liveValuesEvent.data;
|
||||
const timestamp = liveValuesEvent.ts;
|
||||
|
||||
updateStore((state) => {
|
||||
const newState = { ...state };
|
||||
|
||||
// Add traverse position if not null
|
||||
if (traverse_position !== null) {
|
||||
const timeseriesValue: TimeSeriesValue = {
|
||||
value: traverse_position,
|
||||
timestamp,
|
||||
};
|
||||
newState.traversePosition = addTraversePosition(
|
||||
state.traversePosition,
|
||||
timeseriesValue,
|
||||
);
|
||||
}
|
||||
|
||||
if (spool_progress !== null) {
|
||||
const timeseriesValue: TimeSeriesValue = {
|
||||
value: spool_progress,
|
||||
timestamp,
|
||||
};
|
||||
newState.spoolProgress = addSpoolProgress(
|
||||
state.spoolProgress,
|
||||
timeseriesValue,
|
||||
);
|
||||
}
|
||||
|
||||
// Add puller speed
|
||||
const pullerSpeedValue: TimeSeriesValue = {
|
||||
value: puller_speed,
|
||||
timestamp,
|
||||
};
|
||||
newState.pullerSpeed = addPullerSpeed(
|
||||
state.pullerSpeed,
|
||||
pullerSpeedValue,
|
||||
);
|
||||
|
||||
// Add spool RPM
|
||||
const spoolRpmValue: TimeSeriesValue = {
|
||||
value: spool_rpm,
|
||||
timestamp,
|
||||
};
|
||||
newState.spoolRpm = addSpoolRpm(state.spoolRpm, spoolRpmValue);
|
||||
|
||||
// Add tension arm angle
|
||||
const tensionArmAngleValue: TimeSeriesValue = {
|
||||
value: tension_arm_angle,
|
||||
timestamp,
|
||||
};
|
||||
newState.tensionArmAngle = addTensionArmAngle(
|
||||
state.tensionArmAngle,
|
||||
tensionArmAngleValue,
|
||||
);
|
||||
|
||||
// Add temperature readings from backend
|
||||
const temp1Value: TimeSeriesValue = {
|
||||
value: temperature_1,
|
||||
timestamp,
|
||||
};
|
||||
newState.temperature1 = addTemperature1(
|
||||
state.temperature1,
|
||||
temp1Value,
|
||||
);
|
||||
|
||||
const temp2Value: TimeSeriesValue = {
|
||||
value: temperature_2,
|
||||
timestamp,
|
||||
};
|
||||
newState.temperature2 = addTemperature2(
|
||||
state.temperature2,
|
||||
temp2Value,
|
||||
);
|
||||
|
||||
const temp3Value: TimeSeriesValue = {
|
||||
value: temperature_3,
|
||||
timestamp,
|
||||
};
|
||||
newState.temperature3 = addTemperature3(
|
||||
state.temperature3,
|
||||
temp3Value,
|
||||
);
|
||||
|
||||
const temp4Value: TimeSeriesValue = {
|
||||
value: temperature_4,
|
||||
timestamp,
|
||||
};
|
||||
newState.temperature4 = addTemperature4(
|
||||
state.temperature4,
|
||||
temp4Value,
|
||||
);
|
||||
|
||||
const temp5Value: TimeSeriesValue = {
|
||||
value: temperature_5,
|
||||
timestamp,
|
||||
};
|
||||
newState.temperature5 = addTemperature5(
|
||||
state.temperature5,
|
||||
temp5Value,
|
||||
);
|
||||
|
||||
const temp6Value: TimeSeriesValue = {
|
||||
value: temperature_6,
|
||||
timestamp,
|
||||
};
|
||||
newState.temperature6 = addTemperature6(
|
||||
state.temperature6,
|
||||
temp6Value,
|
||||
);
|
||||
|
||||
// Add heater power values
|
||||
const heater1Value: TimeSeriesValue = {
|
||||
value: heater_1_power,
|
||||
timestamp,
|
||||
};
|
||||
newState.heater1Power = addHeater1Power(
|
||||
state.heater1Power,
|
||||
heater1Value,
|
||||
);
|
||||
|
||||
const heater2Value: TimeSeriesValue = {
|
||||
value: heater_2_power,
|
||||
timestamp,
|
||||
};
|
||||
newState.heater2Power = addHeater2Power(
|
||||
state.heater2Power,
|
||||
heater2Value,
|
||||
);
|
||||
|
||||
const heater3Value: TimeSeriesValue = {
|
||||
value: heater_3_power,
|
||||
timestamp,
|
||||
};
|
||||
newState.heater3Power = addHeater3Power(
|
||||
state.heater3Power,
|
||||
heater3Value,
|
||||
);
|
||||
|
||||
const heater4Value: TimeSeriesValue = {
|
||||
value: heater_4_power,
|
||||
timestamp,
|
||||
};
|
||||
newState.heater4Power = addHeater4Power(
|
||||
state.heater4Power,
|
||||
heater4Value,
|
||||
);
|
||||
|
||||
const heater5Value: TimeSeriesValue = {
|
||||
value: heater_5_power,
|
||||
timestamp,
|
||||
};
|
||||
newState.heater5Power = addHeater5Power(
|
||||
state.heater5Power,
|
||||
heater5Value,
|
||||
);
|
||||
|
||||
const heater6Value: TimeSeriesValue = {
|
||||
value: heater_6_power,
|
||||
timestamp,
|
||||
};
|
||||
newState.heater6Power = addHeater6Power(
|
||||
state.heater6Power,
|
||||
heater6Value,
|
||||
);
|
||||
|
||||
// Update slave puller data from backend
|
||||
const slavePullerValue: TimeSeriesValue = {
|
||||
value: slave_puller_speed,
|
||||
timestamp,
|
||||
};
|
||||
newState.slavePullerSpeed = addSlavePullerSpeed(
|
||||
state.slavePullerSpeed,
|
||||
slavePullerValue,
|
||||
);
|
||||
|
||||
const slaveTensionArmValue: TimeSeriesValue = {
|
||||
value: slave_tension_arm_angle,
|
||||
timestamp,
|
||||
};
|
||||
newState.slaveTensionArmAngle = addSlaveTensionArmAngle(
|
||||
state.slaveTensionArmAngle,
|
||||
slaveTensionArmValue,
|
||||
);
|
||||
|
||||
const addonTensionArmValue: TimeSeriesValue = {
|
||||
value: addon_tension_arm_angle,
|
||||
timestamp,
|
||||
};
|
||||
newState.addonTensionArmAngle = addAddonTensionArmAngle(
|
||||
state.addonTensionArmAngle,
|
||||
addonTensionArmValue,
|
||||
);
|
||||
|
||||
return newState;
|
||||
});
|
||||
} else {
|
||||
handleUnhandledEventError(eventName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Unexpected error processing ${eventName} event:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the Gluetex namespace implementation
|
||||
*/
|
||||
const useGluetexNamespaceImplementation =
|
||||
createNamespaceHookImplementation<GluetexNamespaceStore>({
|
||||
createStore: createGluetexNamespaceStore,
|
||||
createEventHandler: gluetexMessageHandler,
|
||||
});
|
||||
|
||||
/**
|
||||
* Hook for a machine-specific Gluetex namespace
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function GluetexStatus({ machine }) {
|
||||
* const { state, traversePosition, pullerSpeed } = useGluetexNamespace(machine);
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {state && (
|
||||
* <div>Mode: {state.data.mode_state.mode}</div>
|
||||
* )}
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useGluetexNamespace(
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
): GluetexNamespaceStore {
|
||||
// Generate namespace ID from validated machine ID
|
||||
const namespaceId = useMemo<NamespaceId>(
|
||||
() => ({
|
||||
type: "machine",
|
||||
machine_identification_unique,
|
||||
}),
|
||||
[machine_identification_unique],
|
||||
);
|
||||
|
||||
// Use the implementation with validated namespace ID
|
||||
return useGluetexNamespaceImplementation(namespaceId);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -28,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 {
|
||||
@@ -102,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}
|
||||
@@ -121,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}
|
||||
/>
|
||||
@@ -145,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"
|
||||
|
||||
@@ -101,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",
|
||||
@@ -166,6 +171,7 @@ export function Winder2PresetsPage() {
|
||||
setPullerGearRatio,
|
||||
|
||||
setSpoolRegulationMode,
|
||||
setSpoolForward,
|
||||
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
@@ -194,6 +200,7 @@ export function Winder2PresetsPage() {
|
||||
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,25 +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,
|
||||
@@ -34,10 +46,58 @@ export function Winder2SettingPage() {
|
||||
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}
|
||||
@@ -88,6 +148,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" && (
|
||||
<>
|
||||
|
||||
@@ -134,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() }),
|
||||
);
|
||||
@@ -467,6 +470,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) => {
|
||||
@@ -652,6 +668,7 @@ export function useWinder2() {
|
||||
setSpoolRegulationMode,
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
setSpoolForward,
|
||||
setSpoolAdaptiveTensionTarget,
|
||||
setSpoolAdaptiveRadiusLearningRate,
|
||||
setSpoolAdaptiveMaxSpeedMultiplier,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -177,6 +177,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(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,14 @@ import { Winder2ControlPage } from "@/machines/winder/winder2/Winder2ControlPage
|
||||
import { Winder2ManualPage } from "@/machines/winder/winder2/Winder2Manual";
|
||||
import { Winder2SettingPage } from "@/machines/winder/winder2/Winder2Settings";
|
||||
import { Winder2GraphsPage } from "@/machines/winder/winder2/Winder2Graphs";
|
||||
import { GluetexPage } from "@/machines/winder/gluetex/GluetexPage";
|
||||
import { GluetexControlPage } from "@/machines/winder/gluetex/GluetexControlPage";
|
||||
import { GluetexManualPage } from "@/machines/winder/gluetex/GluetexManual";
|
||||
import { GluetexSettingPage } from "@/machines/winder/gluetex/GluetexSettings";
|
||||
import { GluetexGraphsPage } from "@/machines/winder/gluetex/GluetexGraphs";
|
||||
import { GluetexPresetsPage } from "@/machines/winder/gluetex/GluetexPresetsPage";
|
||||
import { GluetexAddonsPage } from "@/machines/winder/gluetex/GluetexAddonsPage";
|
||||
import { GluetexHeatersPage } from "@/machines/winder/gluetex/GluetexHeatersPage";
|
||||
import { Extruder2Page } from "@/machines/extruder/extruder2/Extruder2Page";
|
||||
import { Extruder2ControlPage } from "@/machines/extruder/extruder2/Extruder2ControlPage";
|
||||
import { Extruder2SettingsPage } from "@/machines/extruder/extruder2/Extruder2Settings";
|
||||
@@ -132,6 +140,54 @@ export const winder2PresetsRoute = createRoute({
|
||||
component: () => <Winder2PresetsPage />,
|
||||
});
|
||||
|
||||
export const gluetexRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "gluetex/$serial",
|
||||
component: () => <GluetexPage />,
|
||||
});
|
||||
|
||||
export const gluetexControlRoute = createRoute({
|
||||
getParentRoute: () => gluetexRoute,
|
||||
path: "control",
|
||||
component: () => <GluetexControlPage />,
|
||||
});
|
||||
|
||||
export const gluetexManualRoute = createRoute({
|
||||
getParentRoute: () => gluetexRoute,
|
||||
path: "manual",
|
||||
component: () => <GluetexManualPage />,
|
||||
});
|
||||
|
||||
export const gluetexSettingsRoute = createRoute({
|
||||
getParentRoute: () => gluetexRoute,
|
||||
path: "settings",
|
||||
component: () => <GluetexSettingPage />,
|
||||
});
|
||||
|
||||
export const gluetexGraphsRoute = createRoute({
|
||||
getParentRoute: () => gluetexRoute,
|
||||
path: "graphs",
|
||||
component: () => <GluetexGraphsPage />,
|
||||
});
|
||||
|
||||
export const gluetexPresetsRoute = createRoute({
|
||||
getParentRoute: () => gluetexRoute,
|
||||
path: "presets",
|
||||
component: () => <GluetexPresetsPage />,
|
||||
});
|
||||
|
||||
export const gluetexAddonsRoute = createRoute({
|
||||
getParentRoute: () => gluetexRoute,
|
||||
path: "addons",
|
||||
component: () => <GluetexAddonsPage />,
|
||||
});
|
||||
|
||||
export const gluetexHeatersRoute = createRoute({
|
||||
getParentRoute: () => gluetexRoute,
|
||||
path: "heaters",
|
||||
component: () => <GluetexHeatersPage />,
|
||||
});
|
||||
|
||||
export const laser1SerialRoute = createRoute({
|
||||
getParentRoute: () => machinesRoute,
|
||||
path: "laser1/$serial",
|
||||
@@ -327,6 +383,16 @@ export const rootTree = RootRoute.addChildren([
|
||||
winder2PresetsRoute,
|
||||
]),
|
||||
|
||||
gluetexRoute.addChildren([
|
||||
gluetexControlRoute,
|
||||
gluetexManualRoute,
|
||||
gluetexSettingsRoute,
|
||||
gluetexGraphsRoute,
|
||||
gluetexPresetsRoute,
|
||||
gluetexAddonsRoute,
|
||||
gluetexHeatersRoute,
|
||||
]),
|
||||
|
||||
extruder2Route.addChildren([
|
||||
extruder2ControlRoute,
|
||||
extruder2SettingsRoute,
|
||||
|
||||
@@ -147,7 +147,7 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
device.revision,
|
||||
machinePreset?.device_roles,
|
||||
),
|
||||
[device.product_id, device.revision, machinePreset],
|
||||
[device.vendor_id, device.product_id, device.revision, machinePreset],
|
||||
);
|
||||
|
||||
// if there is only one allowed role, set the role to that immediately
|
||||
@@ -161,10 +161,15 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
const index = filteredAllowedDevices.findIndex(
|
||||
(isAllowed) => isAllowed === true,
|
||||
);
|
||||
// set the device role of index
|
||||
form.setValue("role", index.toString());
|
||||
// set the device role using the actual role number, not the index
|
||||
if (machinePreset && machinePreset.device_roles[index]) {
|
||||
form.setValue(
|
||||
"role",
|
||||
machinePreset.device_roles[index].role.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [filteredAllowedDevices]);
|
||||
}, [filteredAllowedDevices, machinePreset, form]);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
@@ -234,7 +239,7 @@ export function DeviceEeepromDialogContent({ device, setOpen }: ContentProps) {
|
||||
<FormItem>
|
||||
<FormLabel>Device Role</FormLabel>
|
||||
<FormControl>
|
||||
<Select {...field}>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-min">
|
||||
<SelectValue placeholder="Device Role" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from "@/machines/properties";
|
||||
import { IconText } from "@/components/IconText";
|
||||
import { MachinesEventData, useMainNamespace } from "@/client/mainNamespace";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
export const columns: ColumnDef<
|
||||
NonNullable<MachinesEventData>["machines"][number]
|
||||
@@ -76,6 +78,7 @@ export const columns: ColumnDef<
|
||||
|
||||
export function MachinesPage() {
|
||||
const { machines } = useMainNamespace();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const data = useMemo(() => {
|
||||
return machines?.data?.machines || [];
|
||||
@@ -93,6 +96,25 @@ export function MachinesPage() {
|
||||
<RefreshIndicator ts={machines?.ts} />
|
||||
</SectionTitle>
|
||||
<MyTable table={table} key={data.toString()} />
|
||||
|
||||
<div className="mt-8 border-t border-gray-200 p-4">
|
||||
<h3 className="mb-2 text-lg font-semibold">Test Machines</h3>
|
||||
<p className="mb-4 text-sm text-gray-600">
|
||||
Hardcoded frontend machines for testing (no backend required)
|
||||
</p>
|
||||
<TouchButton
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/_sidebar/machines/gluetex/$serial/control",
|
||||
params: { serial: "999" },
|
||||
})
|
||||
}
|
||||
icon="lu:TestTube"
|
||||
variant="outline"
|
||||
>
|
||||
Open Gluetex Test Winder
|
||||
</TouchButton>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Page } from "@/components/Page";
|
||||
import { SectionTitle } from "@/components/SectionTitle";
|
||||
import { Terminal } from "@/components/Terminal";
|
||||
import { TouchButton } from "@/components/touch/TouchButton";
|
||||
import { UpdateProgressBar } from "@/components/UpdateProgressBar";
|
||||
import { updateExecute, updateCancelWithStore } from "@/helpers/update_helpers";
|
||||
import { useUpdateStore } from "@/stores/updateStore";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
@@ -18,12 +19,15 @@ export function UpdateExecutePage() {
|
||||
isUpdating,
|
||||
terminalLines,
|
||||
currentUpdateInfo,
|
||||
steps,
|
||||
overallProgress,
|
||||
setUpdateInfo,
|
||||
startUpdate,
|
||||
stopUpdate,
|
||||
addTerminalLine,
|
||||
clearTerminalLines,
|
||||
resetUpdateState,
|
||||
initializeSteps,
|
||||
} = useUpdateStore();
|
||||
|
||||
// Set update info from search params when component mounts or search changes
|
||||
@@ -50,6 +54,7 @@ export function UpdateExecutePage() {
|
||||
commit: search.commit,
|
||||
};
|
||||
|
||||
initializeSteps();
|
||||
startUpdate();
|
||||
// Perhaps we just need to clear the logs ?
|
||||
const res = await updateExecute(updateInfo, addTerminalLine);
|
||||
@@ -138,6 +143,15 @@ export function UpdateExecutePage() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Progress Bar */}
|
||||
{isUpdating && (
|
||||
<UpdateProgressBar
|
||||
steps={steps}
|
||||
overallProgress={overallProgress}
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Terminal
|
||||
lines={terminalLines}
|
||||
className="h-160"
|
||||
|
||||
@@ -10,10 +10,21 @@ export type UpdateInfo = {
|
||||
commit?: string;
|
||||
};
|
||||
|
||||
export type UpdateStep = {
|
||||
id: string;
|
||||
label: string;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
subsector: "nixos" | "rust" | "electron" | "general";
|
||||
progress?: number; // 0-100 for steps with detailed progress tracking
|
||||
};
|
||||
|
||||
export type UpdateState = {
|
||||
isUpdating: boolean;
|
||||
terminalLines: string[];
|
||||
currentUpdateInfo: UpdateInfo | null;
|
||||
steps: UpdateStep[];
|
||||
currentStepIndex: number;
|
||||
overallProgress: number; // 0-100
|
||||
};
|
||||
|
||||
export type UpdateActions = {
|
||||
@@ -23,14 +34,80 @@ export type UpdateActions = {
|
||||
addTerminalLine: (line: string) => void;
|
||||
clearTerminalLines: () => void;
|
||||
resetUpdateState: () => void;
|
||||
setStepStatus: (stepId: string, status: UpdateStep["status"]) => void;
|
||||
setStepProgress: (stepId: string, progress: number) => void;
|
||||
initializeSteps: () => void;
|
||||
updateProgress: (progress: number) => void;
|
||||
};
|
||||
|
||||
export type UpdateStore = UpdateState & UpdateActions;
|
||||
|
||||
const defaultSteps: UpdateStep[] = [
|
||||
{
|
||||
id: "clone-repo",
|
||||
label: "Clone repository",
|
||||
status: "pending",
|
||||
subsector: "general",
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: "rust-build",
|
||||
label: "Build system packages",
|
||||
status: "pending",
|
||||
subsector: "rust",
|
||||
progress: 0,
|
||||
},
|
||||
{
|
||||
id: "system-install",
|
||||
label: "Install system update",
|
||||
status: "pending",
|
||||
subsector: "nixos",
|
||||
progress: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// Helper function to calculate overall progress based on step weights and individual progress
|
||||
const calculateOverallProgress = (steps: UpdateStep[]): number => {
|
||||
// Define weights for each step (should sum to 100)
|
||||
const stepWeights: Record<string, number> = {
|
||||
"clone-repo": 10, // 10% for cloning
|
||||
"rust-build": 80, // 80% for building (goes up to 90% internally)
|
||||
"system-install": 10, // 10% for system installation
|
||||
};
|
||||
|
||||
let totalProgress = 0;
|
||||
|
||||
steps.forEach((step) => {
|
||||
const weight = stepWeights[step.id] || 0;
|
||||
|
||||
if (step.status === "completed") {
|
||||
// Completed steps contribute their full weight
|
||||
totalProgress += weight;
|
||||
} else if (step.status === "in-progress") {
|
||||
// In-progress steps: use progress value or 0 if not yet set
|
||||
const stepProgress = step.progress ?? 0;
|
||||
|
||||
if (step.id === "rust-build") {
|
||||
// Rust build progress is 0-90%, map it to 0-100% of its weight
|
||||
totalProgress += (stepProgress / 90) * weight;
|
||||
} else {
|
||||
// Other steps: progress is 0-100%
|
||||
totalProgress += (stepProgress / 100) * weight;
|
||||
}
|
||||
}
|
||||
// Pending and error steps contribute 0
|
||||
});
|
||||
|
||||
return Math.round(Math.min(100, Math.max(0, totalProgress)));
|
||||
};
|
||||
|
||||
const initialState: UpdateState = {
|
||||
isUpdating: false,
|
||||
terminalLines: [],
|
||||
currentUpdateInfo: null,
|
||||
steps: defaultSteps,
|
||||
currentStepIndex: 0,
|
||||
overallProgress: 0,
|
||||
};
|
||||
|
||||
export const useUpdateStore = create<UpdateStore>((set) => ({
|
||||
@@ -105,6 +182,63 @@ export const useUpdateStore = create<UpdateStore>((set) => ({
|
||||
state.isUpdating = false;
|
||||
state.terminalLines = [];
|
||||
state.currentUpdateInfo = null;
|
||||
state.steps = defaultSteps.map((step) => ({ ...step }));
|
||||
state.currentStepIndex = 0;
|
||||
state.overallProgress = 0;
|
||||
}),
|
||||
),
|
||||
|
||||
initializeSteps: () =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
state.steps = defaultSteps.map((step) => ({ ...step }));
|
||||
state.currentStepIndex = 0;
|
||||
state.overallProgress = 0;
|
||||
}),
|
||||
),
|
||||
|
||||
setStepStatus: (stepId: string, status: UpdateStep["status"]) =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
const stepIndex = state.steps.findIndex((s) => s.id === stepId);
|
||||
if (stepIndex !== -1) {
|
||||
state.steps[stepIndex].status = status;
|
||||
|
||||
// Update current step index and ensure progress is initialized
|
||||
if (status === "in-progress") {
|
||||
state.currentStepIndex = stepIndex;
|
||||
// Initialize progress to 0 if not already set
|
||||
if (state.steps[stepIndex].progress === undefined) {
|
||||
state.steps[stepIndex].progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate overall progress based on step weights and progress
|
||||
state.overallProgress = calculateOverallProgress(state.steps);
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
setStepProgress: (stepId: string, progress: number) =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
const stepIndex = state.steps.findIndex((s) => s.id === stepId);
|
||||
if (stepIndex !== -1) {
|
||||
state.steps[stepIndex].progress = Math.min(
|
||||
100,
|
||||
Math.max(0, progress),
|
||||
);
|
||||
|
||||
// Recalculate overall progress when step progress changes
|
||||
state.overallProgress = calculateOverallProgress(state.steps);
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
updateProgress: (progress: number) =>
|
||||
set(
|
||||
produce((state: UpdateState) => {
|
||||
state.overallProgress = Math.min(100, Math.max(0, progress));
|
||||
}),
|
||||
),
|
||||
}));
|
||||
Vendored
+7
@@ -46,6 +46,13 @@ interface UpdateContext {
|
||||
onEnd: (
|
||||
callback: (params: { success: boolean; error?: string }) => void,
|
||||
) => void;
|
||||
onStep: (
|
||||
callback: (params: {
|
||||
stepId: string;
|
||||
status: "pending" | "in-progress" | "completed" | "error";
|
||||
progress?: number;
|
||||
}) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface TroubleshootContext {
|
||||
|
||||
@@ -10,9 +10,9 @@ workspace = true
|
||||
anyhow = "1.0.100"
|
||||
ethercrab = "0.6"
|
||||
ethercat_hal_derive = { version = "0.1.0", path = "../ethercat-hal-derive" }
|
||||
units = { path = "../units" }
|
||||
bitvec = { version = "1.0.1", features = ["alloc"] }
|
||||
smol = "2.0.2"
|
||||
uom = { version = "0.36.0", default-features = false, features = ["f64","si"] }
|
||||
rand = "0.9.2"
|
||||
futures = "0.3.31"
|
||||
tracing = "0.1.41"
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
io::analog_input::{AnalogInputDevice, AnalogInputInput},
|
||||
};
|
||||
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
|
||||
use uom::si::{electric_potential::volt, f64::ElectricPotential};
|
||||
use units::{electric_potential::volt, f64::ElectricPotential};
|
||||
|
||||
#[derive(EthercatDevice)]
|
||||
pub struct EL3001 {
|
||||
|
||||
@@ -17,8 +17,8 @@ use crate::{
|
||||
io::analog_input::{AnalogInputDevice, AnalogInputInput},
|
||||
};
|
||||
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
|
||||
use uom::si::electric_current::milliampere;
|
||||
use uom::si::f64::ElectricCurrent;
|
||||
use units::electric_current::milliampere;
|
||||
use units::f64::ElectricCurrent;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EL3021Configuration {
|
||||
|
||||
@@ -17,8 +17,8 @@ use crate::{
|
||||
io::analog_input::{AnalogInputDevice, AnalogInputInput},
|
||||
};
|
||||
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
|
||||
use uom::si::electric_current::milliampere;
|
||||
use uom::si::f64::ElectricCurrent;
|
||||
use units::electric_current::milliampere;
|
||||
use units::f64::ElectricCurrent;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EL3024Configuration {
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::{
|
||||
io::analog_input::{AnalogInputDevice, AnalogInputInput},
|
||||
};
|
||||
use ethercat_hal_derive::EthercatDevice;
|
||||
use uom::si::{electric_potential::volt, f64::ElectricPotential};
|
||||
use units::{electric_potential::volt, f64::ElectricPotential};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EL3062_0030Configuration {
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::anyhow;
|
||||
use coe::EL7031_0030Configuration;
|
||||
use ethercat_hal_derive::EthercatDevice;
|
||||
use pdo::{EL7031_0030RxPdo, EL7031_0030TxPdo};
|
||||
use uom::si::{electric_potential::volt, f64::ElectricPotential};
|
||||
use units::{electric_potential::volt, f64::ElectricPotential};
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use uom::si::f64::{ElectricCurrent, ElectricPotential};
|
||||
use units::f64::{ElectricCurrent, ElectricPotential};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AnalogInputValue {
|
||||
@@ -77,9 +77,9 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
use uom::si::electric_current::milliampere;
|
||||
use uom::si::electric_potential::volt;
|
||||
use uom::si::f64::{ElectricCurrent, ElectricPotential};
|
||||
use units::electric_current::milliampere;
|
||||
use units::electric_potential::volt;
|
||||
use units::f64::{ElectricCurrent, ElectricPotential};
|
||||
|
||||
#[test]
|
||||
fn test_analog_input_getter_voltage() {
|
||||
|
||||
@@ -57,7 +57,7 @@ impl AnalogInputDummy {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use uom::si::{electric_potential::volt, f64::ElectricPotential};
|
||||
use units::{electric_potential::volt, f64::ElectricPotential};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -35,18 +35,17 @@ pub struct AiStandard {
|
||||
|
||||
impl TxPdoObject for AiStandard {
|
||||
fn read(&mut self, bits: &BitSlice<u8, Lsb0>) {
|
||||
// only read other values if txpdo_toggle is true
|
||||
self.txpdo_toggle = bits[8 + 7];
|
||||
if !self.txpdo_toggle {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read all values regardless of txpdo_toggle state.
|
||||
// The toggle bit is still read for informational purposes, but we always
|
||||
// update the value to prevent stale data when multiple analog inputs
|
||||
// are active on the same device.
|
||||
self.undervoltage = bits[0];
|
||||
self.overvoltage = bits[1];
|
||||
self.limit1 = bits[2..4].load_le::<u8>().into();
|
||||
self.limit2 = bits[4..6].load_le::<u8>().into();
|
||||
self.error = bits[7];
|
||||
self.txpdo_state = bits[8 + 6];
|
||||
self.txpdo_toggle = bits[8 + 7];
|
||||
self.value = bits[16..16 + 16].load_le::<u16>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
libpcap
|
||||
nodejs_22
|
||||
nodePackages.npm
|
||||
lldb
|
||||
];
|
||||
|
||||
hardeningDisable = [ "fortify" ];
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
[package]
|
||||
name = "machines"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ethercrab = "0.6"
|
||||
ethercat_hal = { path = "../ethercat-hal" }
|
||||
control_core = { path = "../control-core" }
|
||||
control_core_derive = { path = "../control-core-derive" }
|
||||
units = { path = "../units" }
|
||||
|
||||
serde = "1.0.217"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
bitvec = "1.0.1"
|
||||
lazy_static = "1.5.0"
|
||||
euclid = "0.22.11"
|
||||
|
||||
# concurrency
|
||||
smol = "2.0.2"
|
||||
futures = "0.3.31"
|
||||
|
||||
# web
|
||||
serde_json = "1.0.143"
|
||||
socketioxide = { version = "0.17.2", features = ["msgpack"] }
|
||||
|
||||
# serial
|
||||
serialport = "4.7.3"
|
||||
|
||||
#tracing
|
||||
tracing = { version = "0.1.41", features = ["attributes"] }
|
||||
tonic = { version = "0.13.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
textplots = "0.8.7"
|
||||
|
||||
[features]
|
||||
default = ["tracing-fmt"]
|
||||
never_type = []
|
||||
tracing-fmt = []
|
||||
mock-machine = []
|
||||
memory-locking = []
|
||||
io-uring = []
|
||||
development-build = []
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["tonic"]
|
||||
@@ -0,0 +1,59 @@
|
||||
use super::{AquaPathV1, AquaPathV1Mode};
|
||||
use crate::{MachineAct, MachineMessage};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
impl MachineAct for AquaPathV1 {
|
||||
fn act(&mut self, now_ts: Instant) {
|
||||
let msg = self.api_receiver.try_recv();
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
let _res = self.act_machine_message(msg);
|
||||
}
|
||||
Err(_) => (),
|
||||
};
|
||||
|
||||
match self.mode {
|
||||
AquaPathV1Mode::Standby => {
|
||||
self.switch_to_standby();
|
||||
}
|
||||
AquaPathV1Mode::Auto => {
|
||||
self.switch_to_auto();
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
self.front_controller.update(now_ts);
|
||||
self.back_controller.update(now_ts);
|
||||
|
||||
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
|
||||
self.emit_live_values();
|
||||
self.last_measurement_emit = now;
|
||||
}
|
||||
}
|
||||
|
||||
fn act_machine_message(&mut self, msg: MachineMessage) {
|
||||
match msg {
|
||||
MachineMessage::SubscribeNamespace(namespace) => {
|
||||
self.namespace.namespace = Some(namespace);
|
||||
self.emit_state();
|
||||
}
|
||||
MachineMessage::UnsubscribeNamespace => self.namespace.namespace = None,
|
||||
MachineMessage::HttpApiJsonRequest(value) => {
|
||||
use crate::MachineApi;
|
||||
|
||||
let _res = self.api_mutate(value);
|
||||
}
|
||||
MachineMessage::ConnectToMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Machine do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
MachineMessage::DisconnectMachine(_machine_connection) =>
|
||||
/*Doesnt connec to any Machine do nothing*/
|
||||
{
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use super::{AquaPathV1, AquaPathV1Mode};
|
||||
use control_core::{
|
||||
machines::api::MachineApi,
|
||||
socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
|
||||
cache_first_and_last_event,
|
||||
},
|
||||
use crate::{MachineApi, MachineMessage};
|
||||
use control_core::socketio::{
|
||||
event::{Event, GenericEvent},
|
||||
namespace::{
|
||||
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_first_and_last_event,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::lock::Mutex;
|
||||
use smol::channel::Sender;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
@@ -91,9 +87,9 @@ enum Mutation {
|
||||
SetBackFlow(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AquaPathV1Namespace {
|
||||
pub namespace: Arc<Mutex<Namespace>>,
|
||||
pub namespace: Option<Namespace>,
|
||||
}
|
||||
|
||||
impl NamespaceCacheingLogic<AquaPathV1Events> for AquaPathV1Namespace {
|
||||
@@ -101,9 +97,10 @@ impl NamespaceCacheingLogic<AquaPathV1Events> for AquaPathV1Namespace {
|
||||
fn emit(&mut self, events: AquaPathV1Events) {
|
||||
let event = Arc::new(events.event_value());
|
||||
let buffer_fn = events.event_cache_fn();
|
||||
|
||||
let mut namespace = self.namespace.lock_blocking();
|
||||
namespace.emit(event, &buffer_fn);
|
||||
match &mut self.namespace {
|
||||
Some(ns) => ns.emit(event, &buffer_fn),
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,17 +113,20 @@ impl CacheableEvents<AquaPathV1Events> for AquaPathV1Events {
|
||||
}
|
||||
|
||||
fn event_cache_fn(&self) -> CacheFn {
|
||||
let cache_one_hour = cache_duration(Duration::from_secs(60 * 60), Duration::from_secs(1));
|
||||
let cache_first_and_last = cache_first_and_last_event();
|
||||
|
||||
match self {
|
||||
AquaPathV1Events::LiveValues(_) => cache_one_hour,
|
||||
AquaPathV1Events::LiveValues(_) => cache_first_and_last,
|
||||
AquaPathV1Events::State(_) => cache_first_and_last,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MachineApi for AquaPathV1 {
|
||||
fn api_get_sender(&self) -> Sender<MachineMessage> {
|
||||
self.api_sender.clone()
|
||||
}
|
||||
|
||||
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
|
||||
let control: Mutation = serde_json::from_value(request_body)?;
|
||||
match control {
|
||||
@@ -149,7 +149,7 @@ impl MachineApi for AquaPathV1 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
|
||||
fn api_event_namespace(&mut self) -> Option<Namespace> {
|
||||
self.namespace.namespace.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
use crate::machines::aquapath1::VolumeRate;
|
||||
use crate::machines::aquapath1::{Flow, Temperature};
|
||||
use crate::aquapath1::VolumeRate;
|
||||
use crate::aquapath1::{Flow, Temperature};
|
||||
use control_core::controllers::pid::PidController;
|
||||
use ethercat_hal::io::encoder_input::EncoderInput;
|
||||
use ethercat_hal::io::{
|
||||
analog_output::AnalogOutput, digital_output::DigitalOutput, temperature_input::TemperatureInput,
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use uom::si::f64::ThermodynamicTemperature;
|
||||
use uom::si::thermodynamic_temperature::degree_celsius;
|
||||
use uom::si::volume_rate::liter_per_minute;
|
||||
use units::f64::ThermodynamicTemperature;
|
||||
use units::thermodynamic_temperature::degree_celsius;
|
||||
use units::volume_rate::liter_per_minute;
|
||||
#[derive(Debug)]
|
||||
|
||||
pub struct Controller {
|
||||
@@ -1,18 +1,11 @@
|
||||
use control_core::{
|
||||
machines::identification::{MachineIdentification, MachineIdentificationUnique},
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
|
||||
use control_core_derive::Machine;
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
use uom::si::{
|
||||
f64::{ThermodynamicTemperature, VolumeRate},
|
||||
thermodynamic_temperature::degree_celsius,
|
||||
volume_rate::liter_per_minute,
|
||||
};
|
||||
use units::f64::*;
|
||||
use units::{thermodynamic_temperature::degree_celsius, volume_rate::liter_per_minute};
|
||||
|
||||
use crate::machines::{
|
||||
use crate::{AsyncThreadMessage, Machine, MachineMessage};
|
||||
use crate::{
|
||||
MACHINE_AQUAPATH_V1, VENDOR_QITECH,
|
||||
aquapath1::{
|
||||
api::{
|
||||
@@ -20,10 +13,13 @@ use crate::machines::{
|
||||
ModeState, StateEvent, TempState, TempStates,
|
||||
},
|
||||
controller::Controller,
|
||||
// flow_controller::FlowController,
|
||||
// temperature_controller::TemperatureController,
|
||||
},
|
||||
machine_identification::MachineIdentification,
|
||||
};
|
||||
|
||||
use smol::channel::{Receiver, Sender};
|
||||
|
||||
use super::machine_identification::MachineIdentificationUnique;
|
||||
pub mod act;
|
||||
pub mod api;
|
||||
pub mod controller;
|
||||
@@ -50,6 +46,16 @@ pub struct Temperature {
|
||||
pub target_temperature: ThermodynamicTemperature,
|
||||
}
|
||||
|
||||
impl Machine for AquaPathV1 {
|
||||
fn get_machine_identification_unique(&self) -> MachineIdentificationUnique {
|
||||
self.machine_identification_unique.clone()
|
||||
}
|
||||
|
||||
fn get_main_sender(&self) -> Option<Sender<AsyncThreadMessage>> {
|
||||
self.main_sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Temperature {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -78,14 +84,17 @@ impl Default for Flow {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Machine)]
|
||||
#[derive(Debug)]
|
||||
pub struct AquaPathV1 {
|
||||
api_receiver: Receiver<MachineMessage>,
|
||||
api_sender: Sender<MachineMessage>,
|
||||
machine_identification_unique: MachineIdentificationUnique,
|
||||
namespace: AquaPathV1Namespace,
|
||||
mode: AquaPathV1Mode,
|
||||
last_measurement_emit: Instant,
|
||||
front_controller: Controller,
|
||||
back_controller: Controller,
|
||||
main_sender: Option<Sender<AsyncThreadMessage>>,
|
||||
}
|
||||
|
||||
impl AquaPathV1 {
|
||||
@@ -1,13 +1,13 @@
|
||||
use super::{AquaPathV1, AquaPathV1Mode};
|
||||
use crate::machines::{
|
||||
aquapath1::{Flow, Temperature, api::AquaPathV1Namespace, controller::Controller},
|
||||
get_ethercat_device,
|
||||
use crate::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_ethercat_device,
|
||||
get_subdevice_by_index, validate_no_role_dublicates,
|
||||
validate_same_machine_identification_unique,
|
||||
};
|
||||
|
||||
use super::{
|
||||
AquaPathV1, AquaPathV1Mode, Flow, Temperature, api::AquaPathV1Namespace, controller::Controller,
|
||||
};
|
||||
use anyhow::Error;
|
||||
use control_core::machines::new::{
|
||||
MachineNewHardware, MachineNewParams, MachineNewTrait, get_subdevice_by_index,
|
||||
validate_no_role_dublicates, validate_same_machine_identification_unique,
|
||||
};
|
||||
use ethercat_hal::{
|
||||
coe::ConfigurableDevice,
|
||||
devices::{
|
||||
@@ -26,7 +26,7 @@ use ethercat_hal::{
|
||||
},
|
||||
};
|
||||
use std::time::{Duration, Instant};
|
||||
use uom::si::{f64::ThermodynamicTemperature, thermodynamic_temperature::degree_celsius};
|
||||
use units::thermodynamic_temperature::{ThermodynamicTemperature, degree_celsius};
|
||||
|
||||
impl MachineNewTrait for AquaPathV1 {
|
||||
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
|
||||
@@ -160,8 +160,11 @@ impl MachineNewTrait for AquaPathV1 {
|
||||
do2,
|
||||
enc2,
|
||||
);
|
||||
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let mut water_cooling = Self {
|
||||
main_sender: params.main_thread_channel.clone(),
|
||||
api_receiver: receiver,
|
||||
api_sender: sender,
|
||||
machine_identification_unique: params.get_machine_identification_unique(),
|
||||
namespace: AquaPathV1Namespace {
|
||||
namespace: params.namespace.clone(),
|
||||
Loaded 100 of 235 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user