Compare commits

..
Author SHA1 Message Date
Snacj 252fb31d65 Fixed merge issues 2025-10-31 13:24:56 +01:00
snacj bf12a25af4 Fixed merge issues 2025-10-31 12:32:15 +01:00
snacj c66f046a9b Fixed Formatting 2025-10-31 12:31:40 +01:00
snacj a7b1158f8d Fixed merge issues. 2025-10-31 12:30:53 +01:00
snacj be87b377ba Changed frontend Formatting 2025-10-31 12:29:23 +01:00
snacj 111664185e Fixed Formatting 2025-10-31 12:29:23 +01:00
snacj 29ccaea7f2 fixed formatting 2025-10-31 12:29:23 +01:00
snacj 8402199601 Fixed Merge issues 2025-10-31 12:29:23 +01:00
snacj b7399c8d82 Fixed Formula 2025-10-31 12:29:23 +01:00
snacj db71805912 Changed P Controller to Pi controller with dead time 2025-10-31 12:29:23 +01:00
snacj 3fd1b09589 Added Pi and Ei to the update logic. 2025-10-31 12:29:23 +01:00
snacj c806f59df5 Changed P Controller to be Pi Controller. 2025-10-31 12:29:23 +01:00
snacj a5c0d75f33 Fixed Clippy and Formatting 2025-10-31 12:29:23 +01:00
snacj 9629652d4f fixed merge issue 2025-10-31 12:29:23 +01:00
snacj 5ee0b7e61b Fixed Clippy 2025-10-31 12:29:23 +01:00
snacj bb975081e8 Testing fix for frontend freeze 2025-10-31 12:29:23 +01:00
snacj 6629055f9f Fixed clippy 2025-10-31 12:29:23 +01:00
snacj 3b10790146 Removed unwrap and fixed clippy 2025-10-31 12:29:23 +01:00
snacj cb6a954e3b Removed inline import 2025-10-31 12:29:23 +01:00
snacj bbf58e6b18 Fixed Error Pid to P 2025-10-31 12:29:23 +01:00
snacj 9af2364528 Fixed Merge Issue 2025-10-31 12:29:23 +01:00
snacj 7d774b0025 Fixed Merge issue 2025-10-31 12:29:23 +01:00
snacj 94655093cc Added Algorithm Only speed calculation for Diameter Regulation 2025-10-31 12:29:23 +01:00
snacj 5b57273153 Removed logs for testing 2025-10-31 12:29:23 +01:00
snacj 02951794f1 Fixed issues with kp scaling factor 2025-10-31 12:29:23 +01:00
snacj d1f96cc816 changed PID controller to P controller 2025-10-31 12:29:23 +01:00
snacj 673b85420c Implemented deadtime_p_controller to regulate puller speed by filament
diameter.
2025-10-31 12:29:23 +01:00
snacj 493cb39396 Moved PID settings impl from Laser to Winder 2025-10-31 12:29:22 +01:00
snacj 34d6e85ec3 fixed PID impl 2025-10-31 12:27:09 +01:00
snacj dd79586b13 Adjusted default PID setting and replaced info! with trace! 2025-10-31 12:27:09 +01:00
snacj fab801e62f Fixed Formatting 2025-10-31 12:27:09 +01:00
snacj 37a1641450 Fixed formatting. 2025-10-31 12:27:09 +01:00
snacj 6e4baaa356 Fixed merge issue 2025-10-31 12:27:09 +01:00
snacj 3292c62212 Added Debug messages 2025-10-31 12:27:09 +01:00
snacj 3e1d83a0dd changed maximum velocity by diameter for testing. 2025-10-31 12:27:09 +01:00
snacj 47dcd34364 Implemented setting of measured diameter 2025-10-31 12:27:09 +01:00
snacj 2c134c4479 Added working PID settings. 2025-10-31 12:27:09 +01:00
snacj 8613e0c5a8 Fixed Laser Winder connection 2025-10-31 12:27:09 +01:00
snacj 02daf33e55 Testing laser winder connection. 2025-10-31 12:27:09 +01:00
snacj 7b7eafee3b Added debug messages to test the connection 2025-10-31 12:27:07 +01:00
snacj 400fc016ae fixed naming 2025-10-31 11:46:20 +01:00
snacj 8b66ee415a Maybe fix laser Machine is Serial 2025-10-31 11:46:20 +01:00
snacj 89fef65347 Fixed Comments 2025-10-31 11:46:20 +01:00
snacj 5fbf5b7b97 Fix PID settings through laser. 2025-10-31 11:46:12 +01:00
snacj 8278bf579c Implemented Diameter Regulation Mode logic 2025-10-31 11:29:59 +01:00
snacj 6f7611ada5 Added Connect Machine selector to laser frontend 2025-10-31 11:29:59 +01:00
snacj a3ff479941 Fixed Machine Connection 2025-10-31 11:29:59 +01:00
snacj d14e1183d5 Fixed Laser Settings Page 2025-10-31 11:29:59 +01:00
snacj c7495a7d92 Added Pid Settings Logic 2025-10-31 11:29:59 +01:00
snacj 5428e950b0 Added Winder Laser Connection Frontend & Backend 2025-10-31 11:29:59 +01:00
249 changed files with 5463 additions and 10211 deletions

No files matched your search

+1 -1
View File
@@ -4,4 +4,4 @@ dist/
Cargo.lock
dhat-heap.json
build.sh
shell.nix
-16
View File
@@ -1,19 +1,3 @@
# `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_
Generated
+1058 -504
View File
File diff suppressed because it is too large. Load diff
+1 -4
View File
@@ -4,11 +4,8 @@ members = [
"ethercat-hal",
"ethercat-hal-derive",
"control-core",
"machines",
#"ethercat-eeprom-dump",
"ethercat-eeprom-dump",
"control-core-derive",
"units",
"utils",
]
exclude = []
resolver = "2"
-52
View File
@@ -152,55 +152,3 @@ We could combine the code of the frontend and backend using [Doxius](https://dio
- [x] [Ethercat Basics](./docs/ethercat-basics.md)
- [x] [NixOS Operating System](./docs/nixos/README.md)
## Minimal Example
# Hello EtherCAT - LED Control on the EL2004
1. Introduction
2. Requirements
3. Wiring / Hardware Setup
4. Software Overview
5. Step-by-Step Implementation
6. Final Code Listing
7. Expected Behavior
---
## 1. Introduction
The EL2004 LED Toggle Example is a minimal demonstration showing how to control digital outputs on a Beckhoff EL2004 EtherCAT terminal using the existing QiTech machine framework. It is designed as the simplest possible working example of hardware interaction: turning an LED on and off.
---
## 2. Requirements
### Hardware
- Beckhoff EL2004 EtherCAT Terminal (Digital Output, 4 channels)
- Beckhoff EK1100 EtherCAT Coupler
- Power supply for EK1100 and EL2004 module (AC/DC Adapter with 24V Output )
- Connecting wires
- A PC connected to the EtherCAT network
### Software
- Rust toolchain (for building backend machine code)
- Node.js and npm/yarn (for frontend control interface)
- The project repository cloned locally
- Access to EtherCAT drivers and any required HAL bindings (available in the QITech control repository)
---
## 3. Wiring / Hardware Setup
### Schematic
<img width="1123" height="859" alt="Screenshot 2025-12-03 at 16 12 26" src="https://github.com/user-attachments/assets/a04278d9-7f6a-442d-9ea7-7ace9ac44872" />
### Needed Hardware Overview
<img width="813" height="1079" alt="Screenshot 2025-12-03 at 16 12 42" src="https://github.com/user-attachments/assets/af34a581-a668-4c6b-9d7f-062355813b6a" />
Figure 1: Beckhoff EK1100 Bus Coupler — the device circles in red is a simple … [caption truncated in source]
+3 -1
View File
@@ -12,15 +12,17 @@ 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"
@@ -0,0 +1,124 @@
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct TimeAgnosticDeadTimePiController {
// Params
/// Proportional gain
kp: f64,
/// Integral gain
ki: f64,
// State
/// Proportional error
ep: f64,
/// Integral error
ei: f64,
/// Dead Time
dead: Duration,
last: Option<Instant>,
}
impl TimeAgnosticDeadTimePiController {
pub fn new(kp: f64, ki: f64, dead: Duration) -> Self {
Self {
kp,
ki,
dead,
ep: 0.0,
ei: 0.0,
last: None,
}
}
pub fn configure(&mut self, kp: f64, ki: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
}
pub fn get_kp(&self) -> f64 {
self.kp
}
pub fn get_ki(&self) -> f64 {
self.ki
}
pub fn get_dead(&self) -> Duration {
self.dead
}
pub fn set_kp(&mut self, kp: f64) {
self.kp = kp;
}
pub fn set_ki(&mut self, ki: f64) {
self.ki = ki;
}
pub fn set_dead(&mut self, dead: Duration) {
self.dead = dead;
}
pub fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
match (min, max) {
(Some(min), Some(max)) => value.clamp(min, max),
(Some(min), None) => value.max(min),
(None, Some(max)) => value.min(max),
(None, None) => value,
}
}
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
let kp = self.kp / 10000.0;
let ki = self.ki / 10000.0;
let signal = match self.last {
// First update
None => {
let ep = error;
// Initialize integral error
self.ei = 0.0;
// Proportional + Integral
let signal = kp * ep + ki * self.ei;
// Save state
self.ep = ep;
self.last = Some(t);
-signal
}
// Subsequent updates
Some(last) => {
// Calculate elapsed time
let dt = t.duration_since(last).as_secs_f64();
// Dead-time scaling factor
let dead_secs = self.dead.as_secs_f64();
let scale = (dt / dead_secs).min(1.0);
// Smoothed proportional error
let ep = self.ep + (error - self.ep) * scale;
// Integrate error over time
self.ei += error * dt;
// Proportional + Integral
let signal = kp * ep + ki * self.ei;
// Save state
self.ep = ep;
self.last = Some(t);
-signal
}
};
-signal
}
pub fn reset(&mut self) {
self.ep = 0.0;
//self.last = None;
}
}
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
angular_acceleration::radian_per_second_squared,
angular_velocity::radian_per_second,
f64::{AngularAcceleration, AngularVelocity},
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Velocity},
velocity::meter_per_second,
+1
View File
@@ -1,4 +1,5 @@
pub mod clamping_timeagnostic_pid;
pub mod deadtime_pi_controller;
pub mod first_degree_motion;
pub mod pid;
pub mod second_degree_motion;
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
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 units::f64::Angle;
/// use units::angle::degree;
/// use uom::si::f64::Angle;
/// use uom::si::angle::degree;
///
/// let mut controller = AngularAccelerationPositionController::new_simple(
/// Some(Angle::new::<degree>(180.0)),
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
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 units::{
/// use uom::si::{
/// 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 units::{
/// use uom::si::{
/// 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 units::{
/// use uom::si::{
/// 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 units::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
/// use uom::si::{angular_velocity::revolution_per_minute, f64::AngularVelocity};
///
/// let mut controller = AngularJerkSpeedController::new_simple(/* ... */);
/// let target = AngularVelocity::new::<revolution_per_minute>(1500.0);
@@ -1,6 +1,6 @@
use std::time::Instant;
use units::{
use uom::si::{
acceleration::meter_per_second_squared,
f64::{Acceleration, Length, Velocity},
length::meter,
@@ -22,8 +22,8 @@ use super::acceleration_position_controller::{
///
/// # Example
/// ```ignore
/// use units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use units::f64::{Length, Velocity, Acceleration};
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::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 units::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use units::f64::{Length, Velocity, Acceleration};
/// use uom::si::{length::meter, velocity::meter_per_second, acceleration::meter_per_second_squared};
/// use uom::si::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 units::{length::meter, f64::Length};
/// use uom::si::{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 units::{length::meter, velocity::meter_per_second, f64::{Length, Velocity}};
/// use uom::si::{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 units::{
use uom::si::{
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 units::{
/// use uom::si::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -65,7 +65,7 @@ impl LinearJerkSpeedController {
///
/// # Example
/// ```ignore
/// use units::{
/// use uom::si::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -127,7 +127,7 @@ impl LinearJerkSpeedController {
///
/// # Example
/// ```ignore
/// use units::{
/// use uom::si::{
/// velocity::meter_per_second,
/// acceleration::meter_per_second_squared,
/// jerk::meter_per_second_cubed,
@@ -1,4 +1,4 @@
use units::{angle::radian, f64::Angle};
use uom::si::{angle::radian, f64::Angle};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AngleConverter {
@@ -1,4 +1,4 @@
use units::{
use uom::si::{
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 units::{
use uom::si::{
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 units::{f64::{Length, Velocity}, length::meter, velocity::meter_per_second};
//! use uom::si::{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 units::{
use uom::si::{
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 units::length::centimeter;
use uom::si::length::centimeter;
#[test]
fn test_constructors() {
@@ -1,4 +1,4 @@
use units::{
use uom::si::{
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 units::{
use uom::si::{
acceleration::meter_per_second_squared, angular_acceleration::radian_per_second_squared,
length::meter, velocity::meter_per_second,
};
@@ -1,117 +1,3 @@
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,
}
use anyhow::Error;
use anyhow::anyhow;
use ethercat_hal::devices::ek1100::EK1100_IDENTITY_A;
@@ -147,6 +33,10 @@ use ethercat_hal::helpers::ethercrab_types::{
use ethercrab::MainDevice;
use ethercrab::SubDeviceIdentity;
use crate::machines::identification::DeviceMachineIdentification;
use crate::machines::identification::MachineIdentification;
use crate::machines::identification::MachineIdentificationUnique;
#[derive(Debug)]
pub struct MachineIdentificationAddresses {
pub vendor_word: u16,
@@ -1,9 +1,10 @@
use std::{process::Command, 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
@@ -91,6 +92,9 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
let mut interface: Option<&str> = None;
for i in 0..interfaces.len() {
// Avoid NetworkManager hangups
set_interface_managed(&interfaces[i].name, false);
match test_interface(&interfaces[i].name) {
Ok(_) => {
// if interface found with ethercat Exit early, we expect only one interface with ethercat
@@ -103,7 +107,7 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
for i in 0..interfaces.len() {
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
set_interface_managed(&interfaces[i].name, false);
continue;
} else {
set_interface_managed(&interfaces[i].name, true);
}
+1
View File
@@ -1 +1,2 @@
pub mod eeprom_identification;
pub mod interface_discovery;
+6
View File
@@ -4,11 +4,17 @@ 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;
+27
View File
@@ -0,0 +1,27 @@
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
}
}
+208
View File
@@ -0,0 +1,208 @@
use crate::downcast::Downcast;
use crate::machines::identification::MachineIdentificationUnique;
use crate::machines::manager::MachineManager;
use crate::socketio::{event::GenericEvent, namespace::Namespace};
use serde::Serialize;
use smol::block_on;
use smol::lock::RwLock;
use std::sync::Arc;
use std::sync::Weak;
use super::Machine;
use anyhow::anyhow;
use smol::{channel::Sender, lock::Mutex};
use socketioxide::extract::SocketRef;
#[derive(Debug)]
pub enum MachineConnection<M: Machine + ?Sized>
where
Self: Sized,
{
Error(anyhow::Error),
Disconnected,
Connected(Arc<Mutex<M>>),
}
#[derive(Debug)]
pub struct MachineSlot<M: Machine + ?Sized>
where
Self: Sized,
{
pub machine_connection: MachineConnection<M>,
pub namespace: Arc<Mutex<Namespace>>,
}
pub type MachineConnectionGeneric = MachineConnection<dyn Machine>;
pub type MachineSlotGeneric = MachineSlot<dyn Machine>;
impl<M: Machine> Clone for MachineConnection<M> {
fn clone(&self) -> Self {
match self {
Self::Connected(m) => Self::Connected(m.clone()),
Self::Error(e) => Self::Error(anyhow!(e.to_string())),
Self::Disconnected => Self::Disconnected,
}
}
}
impl<M: Machine + ?Sized> MachineConnection<M> {
pub fn to_error(&self) -> Option<anyhow::Error> {
match self {
MachineConnection::Error(err) => Some(anyhow!(err.to_string())),
Self::Disconnected => Some(anyhow!("Machine is disconnected")),
_ => None,
}
}
pub fn to_machine(&self) -> Option<Arc<Mutex<M>>> {
match self {
Self::Connected(machine) => Some(machine.clone()),
_ => None,
}
}
}
impl<M: Machine + ?Sized> MachineSlot<M> {
pub fn new(socket_queue_tx: Sender<(SocketRef, Arc<GenericEvent>)>) -> Self {
let namespace = Namespace::new(socket_queue_tx);
Self {
machine_connection: MachineConnection::Disconnected,
namespace: Arc::new(Mutex::new(namespace)),
}
}
pub const fn is_connected(&self) -> bool {
matches!(self.machine_connection, MachineConnection::Connected(_))
}
}
pub trait CrossConnectableMachine<
F: CrossConnectableMachine<F, T>,
T: CrossConnectableMachine<T, F>,
>: Machine
{
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<F, T>;
}
#[derive(Debug)]
pub struct MachineCrossConnection<
F: CrossConnectableMachine<F, T>,
T: CrossConnectableMachine<T, F>,
> {
machine_manager: Weak<RwLock<MachineManager>>,
machine_identification_unique: MachineIdentificationUnique,
pub connected_machine: Weak<Mutex<MachineSlot<T>>>,
_make_compiler_happy: std::marker::PhantomData<F>, // This marks tells the typechecker, that we
// need F in our type arguemnts. We are ok with
// it being used only in a fake field. Without
// 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, Default)]
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,
}
}
}
+113
View File
@@ -0,0 +1,113 @@
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,
}
+297
View File
@@ -0,0 +1,297 @@
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>,
}
+84
View File
@@ -0,0 +1,84 @@
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,
}
}
}
+23
View File
@@ -0,0 +1,23 @@
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;
}
+279
View File
@@ -0,0 +1,279 @@
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,22 +1,10 @@
#[cfg(feature = "mock-machine")]
use crate::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
use crate::{
Machine, MachineNewParams, extruder1::ExtruderV2, machine_identification::MachineIdentification,
};
#[cfg(not(feature = "mock-machine"))]
use crate::{
aquapath1::AquaPathV1, buffer1::BufferV1, extruder2::ExtruderV3, laser::LaserMachine,
test_machine::TestMachine, winder2::Winder2,
};
use lazy_static::lazy_static;
use super::{Machine, identification::MachineIdentification, new::MachineNewParams};
use anyhow::Error;
use std::{any::TypeId, collections::HashMap};
use smol::lock::Mutex;
use std::{any::TypeId, collections::HashMap, sync::Arc};
pub type MachineNewClosure =
Box<dyn Fn(&MachineNewParams) -> Result<Box<dyn Machine>, Error> + Send + Sync>;
Box<dyn Fn(&MachineNewParams) -> Result<Arc<Mutex<dyn Machine>>, Error> + Send + Sync>;
pub struct MachineRegistry {
type_map: HashMap<TypeId, (MachineIdentification, MachineNewClosure)>,
@@ -44,7 +32,9 @@ impl MachineRegistry {
(
machine_identficiation.clone(),
// create a machine construction closure
Box::new(|machine_new_params| Ok(Box::new(T::new(machine_new_params)?))),
Box::new(|machine_new_params| {
Ok(Arc::new(Mutex::new(T::new(machine_new_params)?)))
}),
),
);
}
@@ -52,7 +42,7 @@ impl MachineRegistry {
pub fn new_machine(
&self,
machine_new_params: &MachineNewParams,
) -> Result<Box<dyn Machine>, anyhow::Error> {
) -> Result<Arc<Mutex<dyn Machine>>, anyhow::Error> {
// get machiine identification
let device_identification =
&machine_new_params
@@ -82,28 +72,3 @@ impl MachineRegistry {
(machine_new_closure)(machine_new_params)
}
}
lazy_static! {
pub static ref MACHINE_REGISTRY: MachineRegistry = {
let mut mc = MachineRegistry::new();
mc.register::<Winder2>(Winder2::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV2>(ExtruderV2::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV3>(ExtruderV3::MACHINE_IDENTIFICATION);
#[cfg(feature = "mock-machine")]
mc.register::<MockMachine>(MockMachine::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<LaserMachine>(LaserMachine::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<BufferV1>(BufferV1::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
mc.register::<TestMachine>(TestMachine::MACHINE_IDENTIFICATION);
mc
};
}
+1
View File
@@ -0,0 +1 @@
pub mod mutation;
@@ -2,7 +2,7 @@ use std::fmt::Debug;
use serde::Serialize;
use machines::machine_identification::MachineIdentificationUnique;
use crate::machines::identification::MachineIdentificationUnique;
#[derive(Debug, Serialize, Clone)]
pub struct MutationResponse {
+2 -2
View File
@@ -1,10 +1,11 @@
/*use std::{any::Any, sync::Arc};
use std::{any::Any, sync::Arc};
use smol::lock::RwLock;
use std::fmt::Debug;
use crate::machines::identification::DeviceIdentification;
pub mod registry;
pub mod serial_detection;
pub trait SerialDevice: Any + Send + Sync + SerialDeviceNew + Debug {}
@@ -30,4 +31,3 @@ pub struct SerialDeviceIdentification {
pub vendor_id: u16,
pub product_id: u16,
}
*/
@@ -1,14 +1,8 @@
use lazy_static::lazy_static;
use super::SerialDeviceNewParams;
use crate::{
SerialDevice, SerialDeviceIdentification, machine_identification::DeviceIdentification,
serial::devices::laser::Laser,
machines::identification::DeviceIdentification,
serial::{SerialDevice, SerialDeviceIdentification},
};
#[cfg(feature = "mock-machine")]
use crate::serial::devices::mock::MockSerialDevice;
use crate::SerialDeviceNewParams;
use anyhow::{Error, Result};
use smol::lock::RwLock;
use std::{any::TypeId, collections::HashMap, sync::Arc};
@@ -100,22 +94,3 @@ impl SerialDeviceRegistry {
}
}
}
lazy_static! {
pub static ref SERIAL_DEVICE_REGISTRY: SerialDeviceRegistry = {
let mut sdr = SerialDeviceRegistry::new();
sdr.register::<Laser>(SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
});
// Register MockSerialDevice when mock-machine feature is enabled
#[cfg(feature = "mock-machine")]
sdr.register::<MockSerialDevice>(SerialDeviceIdentification {
vendor_id: 0x0001, // VENDOR_QITECH
product_id: 0x0007, // MACHINE_MOCK
});
sdr
};
}
+1
View File
@@ -1,2 +1,3 @@
pub mod event;
pub mod namespace;
pub mod namespace_id;
+2 -2
View File
@@ -4,7 +4,7 @@ use socketioxide::extract::SocketRef;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tracing::instrument;
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct Namespace {
pub sockets: Vec<SocketRef>,
pub events: HashMap<String, Vec<Arc<GenericEvent>>>,
@@ -167,7 +167,7 @@ impl Namespace {
impl Drop for Namespace {
fn drop(&mut self) {
//self.disconnect_all();
self.disconnect_all();
}
}
@@ -3,7 +3,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
use machines::machine_identification::{MachineIdentification, MachineIdentificationUnique};
use crate::machines::identification::{MachineIdentification, MachineIdentificationUnique};
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum NamespaceId {
+1 -1
View File
@@ -1,4 +1,4 @@
use units::{
use uom::si::{
angle::radian,
angular_velocity::radian_per_second,
f64::{Angle, AngularVelocity, Length, Velocity},
@@ -0,0 +1,7 @@
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";
}
@@ -0,0 +1,7 @@
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";
}
@@ -0,0 +1,7 @@
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";
}
+7
View File
@@ -0,0 +1,7 @@
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";
}
+5
View File
@@ -0,0 +1,5 @@
pub mod acceleration;
pub mod angular_acceleration;
pub mod angular_jerk;
pub mod jerk;
pub mod velocity;
@@ -0,0 +1,7 @@
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.
-69
View File
@@ -1,69 +0,0 @@
# 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.
+21 -84
View File
@@ -8,7 +8,7 @@
"name": "qitech-control-electron",
"version": "1.0.0",
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@hookform/resolvers": "^4.1.2",
"@icons-pack/react-simple-icons": "^11.2.0",
"@jub3i/tree-kill": "^0.0.15",
"@radix-ui/react-collapsible": "^1.1.12",
@@ -64,10 +64,10 @@
"@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.10.2",
"@electron-forge/plugin-fuses": "^7.8.0",
"@electron-forge/plugin-vite": "^7.6.1",
"@electron-forge/shared-types": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@electron/fuses": "^2.0.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.56.1",
@@ -91,7 +91,7 @@
"globals": "^16.2.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.2",
"prettier": "^3.7.3",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
@@ -926,62 +926,19 @@
}
},
"node_modules/@electron-forge/plugin-fuses": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-fuses/-/plugin-fuses-7.10.2.tgz",
"integrity": "sha512-X8FaBL5pVvKCTBNaa9EjbH6vuaeIU7UcPSmP9501XF4zcKPCfTbQKz49LTMl7gd5YzUm82IlqRjte12LLpcSDQ==",
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-fuses/-/plugin-fuses-7.8.2.tgz",
"integrity": "sha512-BrN26y8xRuz43aJasktLvze2rh9XIxN6m4FR5NJvXbdcS0vUQvLSOgRm9koV5YL0/zr99OS5FXLiw04U+kKy1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/plugin-base": "7.10.2",
"@electron-forge/shared-types": "7.10.2"
"@electron-forge/plugin-base": "7.8.2",
"@electron-forge/shared-types": "7.8.2"
},
"engines": {
"node": ">= 16.4.0"
},
"peerDependencies": {
"@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"
"@electron/fuses": ">=1.0.0"
}
},
"node_modules/@electron-forge/plugin-vite": {
@@ -1140,34 +1097,16 @@
}
},
"node_modules/@electron/fuses": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz",
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-2.0.0.tgz",
"integrity": "sha512-lyb1zK3YHeWUjaz7yiK0GnxSPduwASKMyiDbCtbn3spP6EEt+UWtktggWehG0icFrXAk3GwvcJ4nCrJO0N9IhQ==",
"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": ">=10"
"node": ">=22.12.0"
}
},
"node_modules/@electron/get": {
@@ -2106,15 +2045,14 @@
"dev": true
},
"node_modules/@hookform/resolvers": {
"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",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz",
"integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
},
"peerDependencies": {
"react-hook-form": "^7.55.0"
"react-hook-form": "^7.0.0"
}
},
"node_modules/@humanfs/core": {
@@ -12045,11 +11983,10 @@
}
},
"node_modules/prettier": {
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.3.tgz",
"integrity": "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
+4 -4
View File
@@ -28,10 +28,10 @@
"@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.10.2",
"@electron-forge/plugin-fuses": "^7.8.0",
"@electron-forge/plugin-vite": "^7.6.1",
"@electron-forge/shared-types": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@electron/fuses": "^2.0.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.56.1",
@@ -55,7 +55,7 @@
"globals": "^16.2.0",
"jsdom": "^26.0.0",
"postcss": "^8.5.2",
"prettier": "^3.7.3",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4.1.4",
"ts-node": "^10.9.2",
@@ -65,7 +65,7 @@
"vitest": "^3.0.6"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@hookform/resolvers": "^4.1.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.

Before

Width:  |  Height:  |  Size: 74 KiB

@@ -720,7 +720,3 @@ The following persons must not work on the machine:
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 8](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_8-008.png)
![QiTech Pro Extruder Electrical Diagram 2025 - Sheet 9](images/manuals/extruder/QiTech_Pro_Extruder_Electrical_Diagram_2025_9-009.png)
## Mechanical Diagrams
![QiTech Pro Extruder Nozzle 2025](images/manuals/extruder/Nozzle.png)
+1 -10
View File
@@ -145,17 +145,8 @@ 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: () => mainNamespaceStore, // use the exported store
createStore: createMainNamespaceStore,
createEventHandler: mainMessageHandler,
});
+21 -24
View File
@@ -8,8 +8,6 @@ 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
@@ -307,13 +305,27 @@ 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);
// Its hacky but i do not care, electron + react is annoying ...
window.location.reload();
// 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);
});
socket.on("event", (event: unknown) => {
@@ -322,9 +334,11 @@ 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) => {
@@ -342,25 +356,8 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
}),
);
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();
}
// finally connect
socket.connect();
},
incrementNamespace: (namespaceId: NamespaceId) => {
const namespace_path = serializeNamespaceId(namespaceId);
-17
View File
@@ -1,17 +0,0 @@
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");
}
@@ -10,7 +10,7 @@ import {
import React from "react";
import { Icon } from "@/components/Icon";
import { ControlCard } from "@/control/ControlCard";
import { TouchButton } from "./touch/TouchButton";
interface MachineIdentificationUnique {
machine_identification: {
@@ -49,67 +49,64 @@ export const MachineSelector: React.FC<MachineSelectorProps> = ({
connectedMachineState,
setConnectedMachine,
clearConnectedMachine,
title = "Machine Connection",
}) => {
return (
<ControlCard title={title}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 rounded border px-4 py-2 text-left">
<Icon name="lu:Settings" className="text-xl" />
<span>
{selectedMachine?.name ?? "Select a Machine"}{" "}
{selectedMachine?.machine_identification_unique.serial ?? ""}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>Available Machines</DropdownMenuLabel>
<DropdownMenuSeparator />
{machines.map((machine) => {
const isSelected =
connectedMachineState?.machine_identification_unique
?.machine_identification.machine ===
machine.machine_identification_unique.machine_identification
.machine;
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TouchButton className="text-md h-max px-6 py-6" variant="outline">
<div className="flex flex-row items-center gap-2 text-wrap">
<Icon name="lu:Link2" className="size-6" />
{selectedMachine?.name ?? "Select a Machine"}{" "}
{selectedMachine?.machine_identification_unique.serial ?? ""}
</div>
</TouchButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>Available Machines</DropdownMenuLabel>
<DropdownMenuSeparator />
{machines.map((machine) => {
const isSelected =
connectedMachineState?.machine_identification_unique
?.machine_identification.machine ===
machine.machine_identification_unique.machine_identification
.machine;
return (
<DropdownMenuItem
key={machine.machine_identification_unique.serial}
onClick={() =>
setConnectedMachine(machine.machine_identification_unique)
}
className={`flex min-h-[48px] cursor-pointer items-center gap-2 py-2${
isSelected ? "bg-blue-50" : ""
}`}
>
{connectedMachineState?.is_available ? (
<Icon name="lu:Power" className="text-lg text-green-600" />
) : (
<Icon name="lu:Power" className="text-lg text-gray-400" />
)}
<span>
{machine.name} – Serial:{" "}
{machine.machine_identification_unique.serial}
</span>
</DropdownMenuItem>
);
})}
return (
<DropdownMenuItem
key={machine.machine_identification_unique.serial}
onClick={() =>
setConnectedMachine(machine.machine_identification_unique)
}
className={`flex h-max cursor-pointer items-center gap-2 px-6 py-6${
isSelected ? "bg-blue-50" : ""
}`}
>
{connectedMachineState?.is_available ? (
<Icon name="lu:Link2" className="size-6 text-green-600" />
) : (
<Icon name="lu:Link2Off" className="size-6 text-black" />
)}
<span>
{machine.name} – Serial:{" "}
{machine.machine_identification_unique.serial}
</span>
</DropdownMenuItem>
);
})}
{clearConnectedMachine && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={clearConnectedMachine}
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
>
<Icon name="lu:Settings" className="text-lg" />
<span>Clear Selection</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</ControlCard>
{clearConnectedMachine && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={clearConnectedMachine}
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
>
<Icon name="lu:X" className="size-6 text-red-600" />
<span>Disconnect Machine</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -1,137 +0,0 @@
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>
);
}
+1 -1
View File
@@ -109,7 +109,7 @@ export function EditValue({
type FormSchema = z.infer<typeof formSchema>;
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
resolver: zodResolver<FormSchema>(formSchema),
values: { value: value ?? defaultOrZero },
defaultValues: { value: defaultValue },
mode: "all",
-173
View File
@@ -1,173 +0,0 @@
import { Icon, IconName } from "@/components/Icon";
import React, { useState, useMemo } from "react";
import {
getUnitIcon,
renderValueToReactNode,
renderUnitSymbol,
Unit,
} from "./units";
import { Label } from "./Label";
import { TimeSeries, TimeSeriesValue } from "@/lib/timeseries";
type TimeframeOption = {
label: string;
value: number; // in milliseconds
};
export const TIMEFRAME_OPTIONS: TimeframeOption[] = [
{ label: "1 min", value: 1 * 60 * 1000 },
{ label: "5 min", value: 5 * 60 * 1000 },
{ label: "10 min", value: 10 * 60 * 1000 },
{ label: "30 min", value: 30 * 60 * 1000 },
{ label: "1 hour", value: 60 * 60 * 1000 },
];
type Props = {
label: string;
unit?: Unit;
timeseries: TimeSeries;
icon?: IconName;
renderValue?: (value: number) => string;
defaultTimeframe?: number; // in milliseconds, defaults to 5 minutes
/** Optional externally-controlled timeframe (ms). When provided, component becomes controlled. */
timeframe?: number;
/** Optional callback when timeframe is changed via this component's selector */
onTimeframeChange?: (t: number) => void;
/** When true, hide the built-in timeframe selector (useful when sharing a selector outside) */
hideSelector?: boolean;
};
export function MinMaxValue({
unit,
timeseries,
icon,
label,
renderValue,
defaultTimeframe = 5 * 60 * 1000, // 5 minutes default
timeframe,
onTimeframeChange,
hideSelector = false,
}: Props) {
const [internalTimeframe, setInternalTimeframe] = useState(defaultTimeframe);
const selectedTimeframe = timeframe ?? internalTimeframe;
// Calculate min/max from the timeseries using the selected timeframe
const { min, max, hasData } = useMemo(() => {
// Use short series for timeframes <= 5 seconds, otherwise use long series
// Short series typically has ~5 second retention with high resolution
// Long series typically has ~1 hour retention with lower resolution
const series =
selectedTimeframe <= 5000 ? timeseries.short : timeseries.long;
// Manually calculate min/max to exclude zero values (which might be invalid readings)
const cutoffTime = series.lastTimestamp - selectedTimeframe;
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
let hasValidData = false;
let dataCount = 0;
const { values: raw, index, size, validCount } = series;
const startIdx = validCount < size ? 0 : index;
for (let i = 0; i < validCount; i++) {
const idx = (startIdx + i) % size;
const val = raw[idx];
if (
val &&
val.timestamp > 0 &&
val.timestamp >= cutoffTime &&
val.value > 0
) {
hasValidData = true;
dataCount++;
if (val.value < min) min = val.value;
if (val.value > max) max = val.value;
}
}
if (!hasValidData) {
return { min: 0, max: 0, hasData: false };
}
return { min, max, hasData: true };
}, [
timeseries.short,
timeseries.long,
timeseries.current,
selectedTimeframe,
]);
return (
<div className="flex w-full flex-col gap-3">
<Label label={label}>
<div className="flex flex-row items-center gap-6">
{/* Min Value */}
<div className="flex flex-row items-center gap-2">
<Icon
name={icon ?? (unit ? getUnitIcon(unit) : undefined)}
className="size-5 text-blue-500"
/>
<div className="flex flex-col">
<span className="text-xs text-gray-500">Min</span>
<div className="flex flex-row items-center gap-1">
<span className="font-mono text-xl font-bold">
{hasData
? renderValueToReactNode(min, unit, renderValue)
: "-"}
</span>
{hasData && (
<span className="text-sm">{renderUnitSymbol(unit)}</span>
)}
</div>
</div>
</div>
{/* Max Value */}
<div className="flex flex-row items-center gap-2">
<Icon
name={icon ?? (unit ? getUnitIcon(unit) : undefined)}
className="size-5 text-red-500"
/>
<div className="flex flex-col">
<span className="text-xs text-gray-500">Max</span>
<div className="flex flex-row items-center gap-1">
<span className="font-mono text-xl font-bold">
{hasData
? renderValueToReactNode(max, unit, renderValue)
: "-"}
</span>
{hasData && (
<span className="text-sm">{renderUnitSymbol(unit)}</span>
)}
</div>
</div>
</div>
</div>
</Label>
{/* Timeframe Selector (hidden when sharing selector externally) */}
{!hideSelector && (
<div className="flex flex-row flex-wrap gap-2">
{TIMEFRAME_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => {
if (onTimeframeChange) onTimeframeChange(option.value);
else setInternalTimeframe(option.value);
}}
className={`rounded-md px-3 py-1 text-sm transition-colors ${
selectedTimeframe === option.value
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
{option.label}
</button>
))}
</div>
)}
</div>
);
}
+10 -2
View File
@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority";
import React from "react";
type Props = {
variant: "error" | "success";
variant: "error" | "success" | "info";
children: React.ReactNode;
};
@@ -14,10 +14,18 @@ export function StatusBadge({ variant, children }: Props) {
variant: {
error: "bg-red-500",
success: "bg-green-600",
info: "bg-yellow-400 text-black",
},
},
});
const icon: IconName = variant === "error" ? "lu:TriangleAlert" : "lu:Check";
const icon: IconName =
variant === "error"
? "lu:TriangleAlert"
: variant === "success"
? "lu:Check"
: "lu:Info";
return (
<Badge
className={badgeStyle({
@@ -2,4 +2,3 @@ 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,7 +3,6 @@ import {
UPDATE_END,
UPDATE_EXECUTE,
UPDATE_LOG,
UPDATE_STEP,
} from "./update-channels";
type UpdateExecuteInvokeParams = {
@@ -17,20 +16,11 @@ 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) =>
@@ -62,17 +52,5 @@ 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,7 +4,6 @@ import {
UPDATE_END,
UPDATE_EXECUTE,
UPDATE_LOG,
UPDATE_STEP,
} from "./update-channels";
import { spawn, ChildProcess } from "child_process";
import tkill from "@jub3i/tree-kill";
@@ -117,13 +116,6 @@ 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;
@@ -137,8 +129,6 @@ 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,
@@ -149,10 +139,6 @@ async function update(
}
// 2. clone the repository
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "in-progress",
});
const cloneResult = await cloneRepository(
{
githubRepoOwner,
@@ -165,19 +151,11 @@ 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 (not tracked in progress UI)
// 3. make the nixos-install.sh script executable
const chmodResult = await runCommand(
"chmod",
["+x", "nixos-install.sh"],
@@ -190,35 +168,17 @@ async function update(
}
// 4. run the nixos-install.sh script
// 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(
const installResult = await runCommand(
"./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);
@@ -284,7 +244,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", "--progress", repoUrl];
const cloneArgs = ["clone", repoUrl];
if (tag) {
// Clone a specific tag
@@ -373,11 +333,6 @@ 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
@@ -431,249 +386,6 @@ 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,
-25
View File
@@ -2,13 +2,6 @@ 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,
@@ -21,32 +14,15 @@ 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);
});
});
@@ -79,7 +55,6 @@ 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,6 +62,7 @@ export const connectedMachineStateSchema = z.object({
/**
* Consolidated state event schema (state changes only)
*/
export const stateEventDataSchema = z.object({
mode_state: modeStateSchema,
connected_machine_state: connectedMachineStateSchema,
@@ -152,7 +152,7 @@ export function Extruder2ControlPage() {
unit="rpm"
title="Target Output RPM"
min={0}
max={44}
max={106}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetRpm}
/>
@@ -239,50 +239,6 @@ export function Extruder2SettingsPage() {
/>
</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>
</>
)}
@@ -149,12 +149,6 @@ export const pidSettingsSchema = z.object({
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(),
@@ -294,7 +294,7 @@ export function useExtruder2() {
};
const setTemperaturePidValue = (
zone: "front" | "middle" | "back" | "nozzle",
zone: "front" | "middle" | "back",
key: "kp" | "ki" | "kd",
value: number,
) => {
@@ -1,258 +0,0 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import {
SelectionGroup,
SelectionGroupBoolean,
} from "@/control/SelectionGroup";
import { HeatingZone } from "../HeatingZone";
import { Label } from "@/control/Label";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { useExtruder3 } from "./useExtruder";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { StatusBadge } from "@/control/StatusBadge";
export function Extruder3ControlPage() {
const {
state,
defaultState,
nozzleTemperature,
nozzlePower,
frontTemperature,
frontPower,
backTemperature,
backPower,
middleTemperature,
middlePower,
pressure,
motorScrewRpm,
motorPower,
motorCurrent,
totalEnergyKWh,
combinedPower,
setExtruderMode,
setBackHeatingTemperature,
setFrontHeatingTemperature,
setMiddleHeatingTemperature,
setNozzleHeatingTemperature,
setInverterRegulation,
setInverterTargetPressure,
setInverterTargetRpm,
isLoading,
isDisabled,
} = useExtruder3();
function isZoneReadyForExtrusion(
temperature: number,
targetTemperature: number,
) {
// if temperature is 90% of the target temperature, then we are ready for extrusion
return temperature >= 0.9 * targetTemperature && targetTemperature > 0.0;
}
const allZonesReadyForExtrude = () => {
const frontReady = isZoneReadyForExtrusion(
frontTemperature.current?.value ?? 0,
state?.heating_states.front.target_temperature ?? 1,
);
const middleReady = isZoneReadyForExtrusion(
middleTemperature.current?.value ?? 0,
state?.heating_states.middle.target_temperature ?? 1,
);
const backReady = isZoneReadyForExtrusion(
backTemperature.current?.value ?? 0,
state?.heating_states.back.target_temperature ?? 1,
);
const nozzleReady = isZoneReadyForExtrusion(
nozzleTemperature.current?.value ?? 0,
state?.heating_states.nozzle.target_temperature ?? 1,
);
return frontReady && middleReady && backReady && nozzleReady;
};
return (
<Page>
<ControlGrid>
<HeatingZone
title={"Heating Front"}
heatingState={state?.heating_states.front}
heatingTimeSeries={frontTemperature}
heatingPower={frontPower}
onChangeTargetTemp={setFrontHeatingTemperature}
min={0}
max={300}
/>
<HeatingZone
title={"Heating Middle"}
heatingState={state?.heating_states.middle}
heatingTimeSeries={middleTemperature}
heatingPower={middlePower}
onChangeTargetTemp={setMiddleHeatingTemperature}
min={0}
max={300}
/>
<HeatingZone
title={"Heating Back"}
heatingState={state?.heating_states.back}
heatingTimeSeries={backTemperature}
heatingPower={backPower}
onChangeTargetTemp={setBackHeatingTemperature}
min={0}
max={300}
/>
<HeatingZone
title={"Heating Nozzle"}
heatingState={state?.heating_states.nozzle}
heatingTimeSeries={nozzleTemperature}
heatingPower={nozzlePower}
onChangeTargetTemp={setNozzleHeatingTemperature}
min={0}
max={300}
/>
<ControlCard className="bg-red" title="Screw Drive">
{state?.inverter_status_state.overload_warning == true ? (
<StatusBadge variant="error">
Inverter is overloaded! Please check the extruder and reduce load
if necessary.
</StatusBadge>
) : state?.inverter_status_state.fault_occurence == true ? (
<StatusBadge variant="error">
Inverter encountered an error!! Press the restart button in Config
</StatusBadge>
) : state?.inverter_status_state.running == true &&
state.inverter_status_state.fault_occurence == false ? (
<StatusBadge variant="success">Running</StatusBadge>
) : null}
{state?.inverter_status_state.running == false &&
state.inverter_status_state.fault_occurence == false && (
<StatusBadge variant="success">Healthy</StatusBadge>
)}
<Label label="Regulation">
<SelectionGroupBoolean
value={state?.regulation_state.uses_rpm}
optionTrue={{ children: "RPM" }}
optionFalse={{ children: "Pressure" }}
onChange={setInverterRegulation}
disabled={isDisabled}
loading={isLoading}
/>
</Label>
<div className="flex flex-row flex-wrap gap-4">
<Label label="Target Output RPM">
<EditValue
value={state?.screw_state.target_rpm}
defaultValue={defaultState?.screw_state.target_rpm}
unit="rpm"
title="Target Output RPM"
min={0}
max={100}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetRpm}
/>
</Label>
<Label label="Target Pressure">
<EditValue
value={state?.pressure_state.target_bar}
defaultValue={defaultState?.pressure_state.target_bar}
unit="bar"
title="Target Pressure"
min={0.0}
max={270.0}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setInverterTargetPressure}
/>
</Label>
</div>
<div className="flex flex-row flex-wrap gap-4">
<TimeSeriesValueNumeric
label="Rpm"
unit="rpm"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={motorScrewRpm}
/>
{state?.pressure_state?.wiring_error && (
<StatusBadge variant="error">
Cant Measure Pressure! Check Pressure Sensor Wiring!
</StatusBadge>
)}
<TimeSeriesValueNumeric
label="Pressure"
unit="bar"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={pressure}
/>
</div>
</ControlCard>
<ControlCard className="bg-red" title="Mode">
<SelectionGroup<"Standby" | "Heat" | "Extrude">
value={state?.mode_state.mode}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Standby: {
children: "Standby",
icon: "lu:CirclePause",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Heat: {
children: "Heat",
icon: "lu:Flame",
isActiveClassName: "bg-green-600",
className: "h-full",
},
Extrude: {
children: "Extrude",
icon: "lu:ArrowBigLeftDash",
isActiveClassName: "bg-green-600",
className: "h-full",
confirmation: allZonesReadyForExtrude()
? undefined
: "Temperature is too low. Are you sure you want to extrude?",
},
}}
onChange={setExtruderMode}
disabled={isDisabled}
loading={isLoading}
/>
</ControlCard>
<ControlCard className="bg-blue" title="Power Consumption">
<TimeSeriesValueNumeric
label="Total Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={combinedPower}
/>
<TimeSeriesValueNumeric
label="Motor Power"
unit="W"
renderValue={(value) => roundToDecimals(value, 0)}
timeseries={motorPower}
/>
<TimeSeriesValueNumeric
label="Motor Current"
unit="A"
renderValue={(value) => roundToDecimals(value, 1)}
timeseries={motorCurrent}
/>
<TimeSeriesValueNumeric
label="Total Energy Consumption"
unit="kWh"
renderValue={(value) => roundToDecimals(value, 3)}
timeseries={totalEnergyKWh}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -1,325 +0,0 @@
import { Page } from "@/components/Page";
import {
AutoSyncedBigGraph,
SyncedFloatingControlPanel,
useGraphSync,
type GraphConfig,
} from "@/components/graph";
import React from "react";
import { useExtruder3 } from "./useExtruder";
export function Extruder3GraphsPage() {
const {
state,
nozzleTemperature,
nozzlePower,
frontTemperature,
frontPower,
backTemperature,
backPower,
middleTemperature,
middlePower,
pressure,
motorScrewRpm,
motorCurrent,
motorPower,
combinedPower,
} = useExtruder3();
const syncHook = useGraphSync("extruder-graphs");
// Base config
const baseConfig: GraphConfig = {
defaultTimeWindow: 30 * 60 * 1000, // 30 minutes
exportFilename: "extruder_data",
showLegend: true,
title: "extruder data",
};
const temperatureData = [
...(nozzleTemperature
? [
{
newData: nozzleTemperature,
title: "Nozzle",
color: "#ef4444",
lines:
state?.heating_states.nozzle?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
color: "#ef4444",
show: true,
},
]
: [],
},
]
: []),
...(frontTemperature
? [
{
newData: frontTemperature,
title: "Front",
color: "#f59e0b",
lines:
state?.heating_states.front?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
color: "#f59e0b",
show: true,
},
]
: [],
},
]
: []),
...(middleTemperature
? [
{
newData: middleTemperature,
title: "Middle",
color: "#8b5cf6",
lines:
state?.heating_states.middle?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
color: "#8b5cf6",
show: true,
},
]
: [],
},
]
: []),
...(backTemperature
? [
{
newData: backTemperature,
title: "Back",
color: "#3b82f6",
lines:
state?.heating_states.back?.target_temperature !== undefined
? [
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
color: "#3b82f6",
show: true,
},
]
: [],
},
]
: []),
];
const temperatureConfig: GraphConfig = {
...baseConfig,
title: "Temperatures",
exportFilename: "temperatures_data",
colors: {
primary: "#ef4444",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// Combined Power Graph (no target lines for power)
const powerData = [
...(nozzlePower
? [
{
newData: nozzlePower,
title: "Nozzle",
color: "#ef4444",
},
]
: []),
...(frontPower
? [
{
newData: frontPower,
title: "Front",
color: "#f59e0b",
},
]
: []),
...(middlePower
? [
{
newData: middlePower,
title: "Middle",
color: "#8b5cf6",
},
]
: []),
...(backPower
? [
{
newData: backPower,
title: "Back",
color: "#3b82f6",
},
]
: []),
...(motorPower
? [
{
newData: motorPower,
title: "Motor",
color: "#10b981",
},
]
: []),
...(combinedPower
? [
{
newData: combinedPower,
title: "Total",
color: "#000000",
},
]
: []),
];
const powerConfig: GraphConfig = {
...baseConfig,
title: "Power Outputs",
exportFilename: "power_data",
colors: {
primary: "#10b981",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
const currentConfig: GraphConfig = {
...baseConfig,
title: "Motor Current",
exportFilename: "motor_current_data",
colors: {
primary: "#3b82f6",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// Pressure Graph with connected target line
const pressureConfig: GraphConfig = {
...baseConfig,
title: "Pressure",
exportFilename: "pressure_data",
colors: {
primary: "#3b82f6",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
// RPM Graph with connected target line
const rpmConfig: GraphConfig = {
...baseConfig,
title: "RPM",
exportFilename: "rpm_data",
colors: {
primary: "#8b5cf6",
grid: "#e2e8f0",
axis: "#64748b",
background: "#ffffff",
},
};
return (
<Page className="pb-25">
<div className="flex flex-col gap-4">
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: pressure,
color: "#3b82f6",
lines:
state?.pressure_state.target_bar !== undefined
? [
{
type: "target" as const,
value: state.pressure_state.target_bar,
color: "#3b82f6",
show: true,
},
]
: [],
}}
config={pressureConfig}
unit="bar"
renderValue={(value) => value.toFixed(2)}
graphId="pressure-graph"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={temperatureData}
config={temperatureConfig}
unit="C"
renderValue={(value) => value.toFixed(1)}
graphId="combined-temperatures"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={powerData}
config={powerConfig}
unit="W"
renderValue={(value) => value.toFixed(1)}
graphId="combined-power"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: motorCurrent,
color: "#3b82f6",
}}
config={currentConfig}
unit="A"
renderValue={(value) => value.toFixed(2)}
graphId="motor-current"
/>
<AutoSyncedBigGraph
syncHook={syncHook}
newData={{
newData: motorScrewRpm,
color: "#8b5cf6",
lines:
state?.screw_state.target_rpm !== undefined
? [
{
type: "target" as const,
value: state.screw_state.target_rpm,
color: "#8b5cf6",
show: true,
},
]
: [],
}}
config={rpmConfig}
unit="rpm"
renderValue={(value) => value.toFixed(0)}
graphId="rpm-graph"
/>
</div>
<SyncedFloatingControlPanel controlProps={syncHook.controlProps} />
</Page>
);
}
@@ -1,12 +0,0 @@
import { Page } from "@/components/Page";
import { MarkdownWithToc } from "@/components/MarkdownWithToc";
import React from "react";
import extruderManualContent from "@/assets/markdown/extruder/manual.md?raw";
export function ExtruderV3ManualPage() {
return (
<Page>
<MarkdownWithToc markdownContent={extruderManualContent} />
</Page>
);
}
@@ -1,44 +0,0 @@
import { Topbar } from "@/components/Topbar";
import { extruder3Route } from "@/routes/routes";
import React from "react";
export function Extruder3Page() {
const { serial } = extruder3Route.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/extruder3/${serial}`}
items={[
{
link: "control",
title: "Control",
icon: "lu:CirclePlay",
activeLink: "control",
},
{
link: "graphs",
title: "Graphs",
icon: "lu:ChartSpline",
activeLink: "graphs",
},
{
link: "settings",
title: "Config",
icon: "lu:Settings",
activeLink: "settings",
},
{
link: "manual",
title: "Manual",
icon: "lu:BookOpen",
activeLink: "manual",
},
{
link: "presets",
title: "Presets",
icon: "lu:Save",
activeLink: "presets",
},
]}
/>
);
}
@@ -1,203 +0,0 @@
import React from "react";
import { PresetsPage } from "@/components/preset/PresetsPage";
import { extruder3 } from "@/machines/properties";
import { useExtruder3 } from "./useExtruder";
import { z } from "zod";
import {
PresetPreviewEntries,
previewSeparator,
} from "@/components/preset/PresetPreviewTable";
import { Preset, PresetData } from "@/lib/preset/preset";
const extruder3PresetDataSchema = z
.object({
targetFrontHeatingTemperature: z.number(),
targetMiddleHeatingTemperature: z.number(),
targetBackHeatingTemperature: z.number(),
targetNozzleHeatingTemperature: z.number(),
targetInverterRpm: z.number(),
targetInverterPressure: z.number(),
inverterRegulation: z.enum(["RPM", "Pressure"]),
inverterRotationDirection: z.enum(["Forward", "Backward"]),
pressureLimit: z.number(),
pressureLimitEnabled: z.boolean(),
pidPressureKp: z.number(),
pidPressureKi: z.number(),
pidPressureKd: z.number(),
})
.partial();
type Extruder3 = typeof extruder3PresetDataSchema;
const schemas = new Map([[1, extruder3PresetDataSchema]]);
const previewEntries: PresetPreviewEntries<Extruder3> = [
{
name: "Target Front Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetFrontHeatingTemperature?.toFixed(1),
},
{
name: "Target Middle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetMiddleHeatingTemperature?.toFixed(1),
},
{
name: "Target Back Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetBackHeatingTemperature?.toFixed(1),
},
{
name: "Target Nozzle Temperature",
unit: "C",
renderValue: (data: PresetData<Extruder3>) =>
data.targetNozzleHeatingTemperature?.toFixed(1),
},
previewSeparator,
{
name: "Inverter Regulation",
renderValue: (data: PresetData<Extruder3>) => data.inverterRegulation,
},
{
name: "Target Inverter RPM",
unit: "rpm",
renderValue: (data: PresetData<Extruder3>) =>
data.targetInverterRpm?.toFixed(2),
},
{
name: "Target Inverter Pressure",
unit: "bar",
renderValue: (data: PresetData<Extruder3>) =>
data.targetInverterPressure?.toFixed(1),
},
{
name: "Inverter Direction",
renderValue: (data: PresetData<Extruder3>) =>
data.inverterRotationDirection,
},
previewSeparator,
{
name: "Pressure Limit",
unit: "bar",
renderValue: (data: PresetData<Extruder3>) =>
data.pressureLimit?.toFixed(1),
},
{
name: "Enable Pressure Limit",
renderValue: (data: PresetData<Extruder3>) =>
data.pressureLimitEnabled ? "on" : "off",
},
previewSeparator,
{
name: "PID Pressue Kp",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKp?.toFixed(4),
},
{
name: "PID Pressue Ki",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKi?.toFixed(4),
},
{
name: "PID Pressue Kd",
renderValue: (data: PresetData<Extruder3>) =>
data.pidPressureKd?.toFixed(4),
},
];
export function Extruder3PresetsPage() {
const {
state,
defaultState,
setFrontHeatingTemperature,
setMiddleHeatingTemperature,
setBackHeatingTemperature,
setNozzleHeatingTemperature,
setInverterTargetRpm,
setInverterTargetPressure,
setInverterRegulation,
setInverterRotationDirection,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setPressurePidKd,
setPressurePidKi,
setPressurePidKp,
} = useExtruder3();
const toPresetData = (s?: typeof state): PresetData<Extruder3> => ({
targetFrontHeatingTemperature: s?.heating_states.front.target_temperature,
targetMiddleHeatingTemperature: s?.heating_states.middle.target_temperature,
targetBackHeatingTemperature: s?.heating_states.back.target_temperature,
targetNozzleHeatingTemperature: s?.heating_states.nozzle.target_temperature,
targetInverterRpm: s?.screw_state.target_rpm,
targetInverterPressure: s?.pressure_state.target_bar,
inverterRegulation: s?.regulation_state.uses_rpm ? "RPM" : "Pressure",
inverterRotationDirection: s?.rotation_state?.forward
? "Forward"
: "Backward",
pressureLimit: s?.extruder_settings_state.pressure_limit,
pressureLimitEnabled: s?.extruder_settings_state.pressure_limit_enabled,
pidPressureKp: s?.pid_settings.pressure.kp,
pidPressureKi: s?.pid_settings.pressure.ki,
pidPressureKd: s?.pid_settings.pressure.kd,
});
const defaults = toPresetData(defaultState);
const applyPreset = (preset: Preset<Extruder3>) => {
setFrontHeatingTemperature(
preset.data.targetFrontHeatingTemperature ?? 150.0,
);
setMiddleHeatingTemperature(
preset.data.targetMiddleHeatingTemperature ?? 150.0,
);
setBackHeatingTemperature(
preset.data.targetBackHeatingTemperature ?? 150.0,
);
setNozzleHeatingTemperature(
preset.data.targetNozzleHeatingTemperature ?? 150.0,
);
setInverterTargetRpm(preset.data.targetInverterRpm ?? 0);
setInverterTargetPressure(preset.data.targetInverterPressure ?? 0);
setInverterRegulation(preset.data.inverterRegulation === "RPM");
setInverterRotationDirection(
preset.data.inverterRotationDirection === "Forward",
);
setExtruderPressureLimit(preset.data.pressureLimit ?? 100.0);
setExtruderPressureLimitEnabled(preset.data.pressureLimitEnabled ?? true);
setPressurePidKp(preset.data.pidPressureKp ?? 0.16);
setPressurePidKi(preset.data.pidPressureKi ?? 0.0);
setPressurePidKd(preset.data.pidPressureKd ?? 0.008);
};
return (
<PresetsPage
machine_identification={extruder3.machine_identification}
currentState={toPresetData(state)}
schemas={schemas}
schemaVersion={1}
applyPreset={applyPreset}
previewEntries={previewEntries}
defaultState={defaults}
/>
);
}
@@ -1,291 +0,0 @@
import React, { useState } from "react";
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { Label } from "@/control/Label";
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { useExtruder3 } from "./useExtruder";
import { ControlGrid } from "@/control/ControlGrid";
export function Extruder3SettingsPage() {
const {
state,
defaultState,
setInverterRotationDirection,
resetInverter,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
} = useExtruder3();
const [showAdvanced, setShowAdvanced] = useState(false);
return (
<Page>
<ControlCard className="bg-red" title="Inverter Settings">
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.rotation_state.forward}
optionTrue={{ children: "Forward" }}
optionFalse={{ children: "Backward" }}
onChange={setInverterRotationDirection}
/>
</Label>
<Label label="Reset Inverter">
<button
onClick={resetInverter}
className="inline-block w-fit max-w-max rounded bg-red-600 px-4 py-4 text-base whitespace-nowrap text-white hover:bg-red-700"
style={{ minWidth: "auto", width: "fit-content" }}
>
Reset Inverter
</button>
</Label>
</ControlCard>
<ControlCard className="bg-red" title="Extruder Settings">
<Label label="Nozzle Pressure Limit">
<EditValue
value={state?.extruder_settings_state.pressure_limit}
defaultValue={defaultState?.extruder_settings_state.pressure_limit}
unit="bar"
title="Nozzle Pressure Limit"
min={0}
max={350}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setExtruderPressureLimit}
/>
</Label>
<Label label="Nozzle Pressure Limit Enabled">
<SelectionGroupBoolean
value={state?.extruder_settings_state.pressure_limit_enabled}
optionTrue={{ children: "Enabled" }}
optionFalse={{ children: "Disabled" }}
onChange={setExtruderPressureLimitEnabled}
/>
</Label>
<Label label="Show Advanced PID Settings">
<SelectionGroupBoolean
value={showAdvanced}
optionTrue={{ children: "Show" }}
optionFalse={{ children: "Hide" }}
onChange={setShowAdvanced}
/>
</Label>
</ControlCard>
{showAdvanced && (
<>
<ControlCard title="Pressure PID Settings ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.pressure.kp}
defaultValue={defaultState?.pid_settings.pressure.kp}
min={0}
max={100}
step={0.01}
renderValue={(v) => roundToDecimals(v, 2)}
onChange={setPressurePidKp}
title="Pressure PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.pressure.ki}
defaultValue={defaultState?.pid_settings.pressure.ki}
min={0}
max={100}
step={0.01}
renderValue={(v) => roundToDecimals(v, 2)}
onChange={setPressurePidKi}
title="Pressure PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.pressure.kd}
defaultValue={defaultState?.pid_settings.pressure.kd}
min={0}
max={100}
step={0.01}
renderValue={(v) => roundToDecimals(v, 2)}
onChange={setPressurePidKd}
title="Pressure PID KD"
/>
</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>
);
}
@@ -1,433 +0,0 @@
import { StoreApi } from "zustand";
import { create } from "zustand";
import { z } from "zod";
import {
EventHandler,
eventSchema,
Event,
NamespaceId,
createNamespaceHookImplementation,
ThrottledStoreUpdater,
handleUnhandledEventError,
} from "../../../client/socketioStore";
import { MachineIdentificationUnique } from "@/machines/types";
import { createTimeSeries, TimeSeries } from "@/lib/timeseries";
import { useMemo } from "react";
// ========== Event Schema Definitions ==========
/**
* Machine operation mode enum
*/
export const modeSchema = z.enum(["Standby", "Heat", "Extrude"]);
export type Mode = z.infer<typeof modeSchema>;
export const liveMotorStatusDataSchema = z.object({
screw_rpm: z.number(),
frequency: z.number(),
current: z.number(),
power: z.number(),
});
export type MotorStatus = z.infer<typeof liveMotorStatusDataSchema>;
/**
* Consolidated live values event schema (30FPS data)
*/
export const liveValuesEventDataSchema = z.object({
motor_status: liveMotorStatusDataSchema,
pressure: z.number(),
nozzle_temperature: z.number(),
front_temperature: z.number(),
back_temperature: z.number(),
middle_temperature: z.number(),
nozzle_power: z.number(),
front_power: z.number(),
back_power: z.number(),
middle_power: z.number(),
combined_power: z.number(),
total_energy_kwh: z.number(),
});
/**
* Rotation state schema
*/
export const rotationStateSchema = z.object({
forward: z.boolean(),
});
/**
* Mode state schema
*/
export const modeStateSchema = z.object({
mode: modeSchema,
});
/**
* Regulation state schema
*/
export const regulationStateSchema = z.object({
uses_rpm: z.boolean(),
});
/**
* Pressure state schema
*/
export const pressureStateSchema = z.object({
target_bar: z.number(),
wiring_error: z.boolean(),
});
/**
* Screw state schema
*/
export const screwStateSchema = z.object({
target_rpm: z.number(),
});
/**
* Heating state schema
*/
export const heatingStateSchema = z.object({
target_temperature: z.number(),
wiring_error: z.boolean(),
});
export type HeatingState = z.infer<typeof heatingStateSchema>;
/**
* Heating states schema
*/
export const heatingStatesSchema = z.object({
nozzle: heatingStateSchema,
front: heatingStateSchema,
back: heatingStateSchema,
middle: heatingStateSchema,
});
/**
* Extruder settings state schema
*/
export const extruderSettingsStateSchema = z.object({
pressure_limit: z.number(),
pressure_limit_enabled: z.boolean(),
});
/**
* Inverter status state schema
*/
export const inverterStatusStateSchema = z.object({
running: z.boolean(),
forward_running: z.boolean(),
reverse_running: z.boolean(),
up_to_frequency: z.boolean(),
overload_warning: z.boolean(),
no_function: z.boolean(),
output_frequency_detection: z.boolean(),
abc_fault: z.boolean(),
fault_occurence: z.boolean(),
});
/**
* PID settings schema
*/
export const pidSettingsSchema = z.object({
temperature: z.object({
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(),
kp: z.number(),
kd: z.number(),
}),
});
/**
* Consolidated state event schema (state changes only)
*/
export const stateEventDataSchema = z.object({
is_default_state: z.boolean(),
rotation_state: rotationStateSchema,
mode_state: modeStateSchema,
regulation_state: regulationStateSchema,
pressure_state: pressureStateSchema,
screw_state: screwStateSchema,
heating_states: heatingStatesSchema,
extruder_settings_state: extruderSettingsStateSchema,
inverter_status_state: inverterStatusStateSchema,
pid_settings: pidSettingsSchema,
});
// ========== Event Schemas with Wrappers ==========
export const liveValuesEventSchema = eventSchema(liveValuesEventDataSchema);
export const stateEventSchema = eventSchema(stateEventDataSchema);
// ========== Type Inferences ==========
export type StateEvent = z.infer<typeof stateEventSchema>;
// Additional exports for backward compatibility
export const SetRegulationSchema = z.object({
uses_rpm: z.boolean(),
});
export const mode = z.object({
mode: modeSchema,
});
export type Extruder3NamespaceStore = {
// Single state event from server
state: StateEvent | null;
defaultState: StateEvent | null;
// Time series data for live values
motorFrequency: TimeSeries;
motorCurrent: TimeSeries;
motorScrewRpm: TimeSeries;
motorPower: TimeSeries;
pressure: TimeSeries;
nozzleTemperature: TimeSeries;
frontTemperature: TimeSeries;
backTemperature: TimeSeries;
middleTemperature: TimeSeries;
nozzlePower: TimeSeries;
frontPower: TimeSeries;
middlePower: TimeSeries;
backPower: TimeSeries;
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
};
// Constants for time durations
const TWENTY_MILLISECOND = 20;
const ONE_SECOND = 1000;
const FIVE_SECOND = 5 * ONE_SECOND;
const ONE_HOUR = 60 * 60 * ONE_SECOND;
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: backTemperature, insert: addBackTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: frontTemperature, insert: addFrontTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: middleTemperature, insert: addMiddleTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: nozzleTemperature, insert: addNozzleTemperature } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: nozzlePower, insert: addNozzlePower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: frontPower, insert: addFrontPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: middlePower, insert: addMiddlePower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: backPower, insert: addBackPower } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: combinedPower, insert: addCombinedPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: totalEnergyKWh, insert: addTotalEnergyKWh } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorCurrent, insert: addMotorCurrent } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorFrequency, insert: addMotorFrequency } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
export function extruder3MessageHandler(
store: StoreApi<Extruder3NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder3NamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const eventName = event.name;
// Helper function to update store through buffer
const updateStore = (
updater: (state: Extruder3NamespaceStore) => Extruder3NamespaceStore,
) => {
throttledUpdater.updateWith(updater);
};
try {
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
updateStore((state) => ({
...state,
state: stateEvent,
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
const timestamp = event.ts;
updateStore((state) => ({
...state,
motorScrewRpm: addMotorScrewRpm(state.motorScrewRpm, {
value: liveValuesEvent.data.motor_status.screw_rpm,
timestamp,
}),
motorCurrent: addMotorCurrent(state.motorCurrent, {
value: liveValuesEvent.data.motor_status.current,
timestamp,
}),
motorFrequency: addMotorFrequency(state.motorFrequency, {
value: liveValuesEvent.data.motor_status.frequency,
timestamp,
}),
motorPower: addMotorPower(state.motorPower, {
value: liveValuesEvent.data.motor_status.power,
timestamp,
}),
pressure: addPressure(state.pressure, {
value: liveValuesEvent.data.pressure,
timestamp,
}),
nozzleTemperature: addNozzleTemperature(state.nozzleTemperature, {
value: liveValuesEvent.data.nozzle_temperature,
timestamp,
}),
frontTemperature: addFrontTemperature(state.frontTemperature, {
value: liveValuesEvent.data.front_temperature,
timestamp,
}),
backTemperature: addBackTemperature(state.backTemperature, {
value: liveValuesEvent.data.back_temperature,
timestamp,
}),
middleTemperature: addMiddleTemperature(state.middleTemperature, {
value: liveValuesEvent.data.middle_temperature,
timestamp,
}),
nozzlePower: addNozzlePower(state.nozzlePower, {
value: liveValuesEvent.data.nozzle_power,
timestamp,
}),
frontPower: addFrontPower(state.frontPower, {
value: liveValuesEvent.data.front_power,
timestamp,
}),
middlePower: addMiddlePower(state.middlePower, {
value: liveValuesEvent.data.middle_power,
timestamp,
}),
backPower: addBackPower(state.backPower, {
value: liveValuesEvent.data.back_power,
timestamp,
}),
combinedPower: addCombinedPower(state.combinedPower, {
value: liveValuesEvent.data.combined_power,
timestamp,
}),
totalEnergyKWh: addTotalEnergyKWh(state.totalEnergyKWh, {
value: liveValuesEvent.data.total_energy_kwh,
timestamp,
}),
}));
} else {
handleUnhandledEventError(eventName);
}
} catch (error) {
console.error(`Unexpected error processing ${eventName} event:`, error);
throw error;
}
};
}
export const createExtruder3NamespaceStore =
(): StoreApi<Extruder3NamespaceStore> =>
create<Extruder3NamespaceStore>(() => {
return {
state: null,
defaultState: null,
motorCurrent,
motorFrequency,
motorScrewRpm,
motorPower,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
middleTemperature,
nozzlePower,
frontPower,
backPower,
middlePower,
combinedPower,
totalEnergyKWh,
};
});
const useExtruder3NamespaceImplementation =
createNamespaceHookImplementation<Extruder3NamespaceStore>({
createStore: createExtruder3NamespaceStore,
createEventHandler: extruder3MessageHandler,
});
export function useExtruder3Namespace(
machine_identification_unique: MachineIdentificationUnique,
): Extruder3NamespaceStore {
// 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 useExtruder3NamespaceImplementation(namespaceId);
}
@@ -1,448 +0,0 @@
import { toastError } from "@/components/Toast";
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { MachineIdentificationUnique } from "@/machines/types";
import { extruder3 } from "@/machines/properties";
import { extruder3Route } from "@/routes/routes";
import { z } from "zod";
import { StateEvent, Mode, useExtruder3Namespace } from "./extruder3Namespace";
import { useEffect, useMemo } from "react";
import { produce } from "immer";
export function useExtruder3() {
const { serial: serialString } = extruder3Route.useParams();
// Memoize the machine identification to keep it stable between renders
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: {
vendor: 0,
machine: 0,
},
serial: 0,
};
}
return {
machine_identification: extruder3.machine_identification,
serial,
};
}, [serialString]);
// Get consolidated state and live values from namespace
const {
state,
defaultState,
motorCurrent,
motorFrequency,
motorScrewRpm,
motorPower,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
middleTemperature,
nozzlePower,
frontPower,
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
} = useExtruder3Namespace(machineIdentification);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) {
stateOptimistic.setReal(state);
}
}, [state]);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState && !stateOptimistic.isOptimistic) {
stateOptimistic.setOptimistic(produce(currentState, producer));
}
serverRequest();
};
// Action functions with verb-first names
const setInverterRotationDirection = (forward: boolean) => {
updateStateOptimistically(
(current) => {
current.data.rotation_state.forward = forward;
},
() =>
requestInverterRotationDirection({
machine_identification_unique: machineIdentification,
data: { SetInverterRotationDirection: forward },
}),
);
};
const setExtruderMode = (mode: Mode) => {
updateStateOptimistically(
(current) => {
current.data.mode_state.mode = mode;
},
() =>
requestExtruderMode({
machine_identification_unique: machineIdentification,
data: { SetExtruderMode: mode },
}),
);
};
const setInverterRegulation = (usesRpm: boolean) => {
updateStateOptimistically(
(current) => {
current.data.regulation_state.uses_rpm = usesRpm;
},
() =>
requestInverterRegulation({
machine_identification_unique: machineIdentification,
data: { SetInverterRegulation: usesRpm },
}),
);
};
const setInverterTargetRpm = (rpm: number) => {
updateStateOptimistically(
(current) => {
current.data.screw_state.target_rpm = rpm;
},
() =>
requestInverterTargetRpm({
machine_identification_unique: machineIdentification,
data: { SetInverterTargetRpm: rpm },
}),
);
};
const setInverterTargetPressure = (pressure: number) => {
updateStateOptimistically(
(current) => {
current.data.pressure_state.target_bar = pressure;
},
() =>
requestInverterTargetPressure({
machine_identification_unique: machineIdentification,
data: { SetInverterTargetPressure: pressure },
}),
);
};
const setNozzleHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.nozzle.target_temperature = temperature;
},
() =>
requestNozzleHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetNozzleHeatingTemperature: temperature },
}),
);
};
const setFrontHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.front.target_temperature = temperature;
},
() =>
requestFrontHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetFrontHeatingTargetTemperature: temperature },
}),
);
};
const setBackHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.back.target_temperature = temperature;
},
() =>
requestBackHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetBackHeatingTargetTemperature: temperature },
}),
);
};
const setMiddleHeatingTemperature = (temperature: number) => {
updateStateOptimistically(
(current) => {
current.data.heating_states.middle.target_temperature = temperature;
},
() =>
requestMiddleHeatingTemperature({
machine_identification_unique: machineIdentification,
data: { SetMiddleHeatingTemperature: temperature },
}),
);
};
const setExtruderPressureLimit = (pressureLimit: number) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.pressure_limit = pressureLimit;
},
() =>
requestExtruderPressureLimit({
machine_identification_unique: machineIdentification,
data: { SetExtruderPressureLimit: pressureLimit },
}),
);
};
const setExtruderPressureLimitEnabled = (enabled: boolean) => {
updateStateOptimistically(
(current) => {
current.data.extruder_settings_state.pressure_limit_enabled = enabled;
},
() =>
requestExtruderPressureLimitEnabled({
machine_identification_unique: machineIdentification,
data: { SetExtruderPressureLimitIsEnabled: enabled },
}),
);
};
const setPressurePidKp = (kp: number) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.pressure.kp = kp;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.pressure,
(draft) => {
draft.kp = kp;
},
);
requestPressurePidSettings({
machine_identification_unique: machineIdentification,
data: { SetPressurePidSettings: settings },
});
}
},
);
};
const setPressurePidKi = (ki: number) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.pressure.ki = ki;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.pressure,
(draft) => {
draft.ki = ki;
},
);
requestPressurePidSettings({
machine_identification_unique: machineIdentification,
data: { SetPressurePidSettings: settings },
});
}
},
);
};
const setPressurePidKd = (kd: number) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.pressure.kd = kd;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.pressure,
(draft) => {
draft.kd = kd;
},
);
requestPressurePidSettings({
machine_identification_unique: machineIdentification,
data: { SetPressurePidSettings: settings },
});
}
},
);
};
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({
machine_identification_unique: machineIdentification,
data: { ResetInverter: true },
});
};
// Mutation hooks
const { request: requestInverterRotationDirection } = useMachineMutation(
z.object({ SetInverterRotationDirection: z.boolean() }),
);
const { request: requestExtruderMode } = useMachineMutation(
z.object({ SetExtruderMode: z.enum(["Heat", "Extrude", "Standby"]) }),
);
const { request: requestInverterRegulation } = useMachineMutation(
z.object({ SetInverterRegulation: z.boolean() }),
);
const { request: requestInverterTargetRpm } = useMachineMutation(
z.object({ SetInverterTargetRpm: z.number() }),
);
const { request: requestInverterTargetPressure } = useMachineMutation(
z.object({ SetInverterTargetPressure: z.number() }),
);
const { request: requestNozzleHeatingTemperature } = useMachineMutation(
z.object({ SetNozzleHeatingTemperature: z.number() }),
);
const { request: requestFrontHeatingTemperature } = useMachineMutation(
z.object({ SetFrontHeatingTargetTemperature: z.number() }),
);
const { request: requestBackHeatingTemperature } = useMachineMutation(
z.object({ SetBackHeatingTargetTemperature: z.number() }),
);
const { request: requestMiddleHeatingTemperature } = useMachineMutation(
z.object({ SetMiddleHeatingTemperature: z.number() }),
);
const { request: requestExtruderPressureLimit } = useMachineMutation(
z.object({ SetExtruderPressureLimit: z.number() }),
);
const { request: requestExtruderPressureLimitEnabled } = useMachineMutation(
z.object({ SetExtruderPressureLimitIsEnabled: z.boolean() }),
);
const { request: requestPressurePidSettings } = useMachineMutation(
z.object({
SetPressurePidSettings: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
}),
}),
);
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() }),
);
return {
// Consolidated state
state: stateOptimistic.value?.data,
// Default state for initial values
defaultState: defaultState?.data,
// Individual live values (TimeSeries)
motorCurrent,
motorFrequency,
motorScrewRpm,
motorPower,
pressure,
nozzleTemperature,
frontTemperature,
backTemperature,
middleTemperature,
nozzlePower,
frontPower,
middlePower,
backPower,
combinedPower,
totalEnergyKWh,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
// Action functions (verb-first)
setInverterRotationDirection,
setExtruderMode,
setInverterRegulation,
setInverterTargetRpm,
setInverterTargetPressure,
setNozzleHeatingTemperature,
setFrontHeatingTemperature,
setBackHeatingTemperature,
setMiddleHeatingTemperature,
setExtruderPressureLimit,
setExtruderPressureLimitEnabled,
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
resetInverter,
};
}
@@ -3,7 +3,6 @@ import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { TimeSeriesValueNumeric } from "@/control/TimeSeriesValue";
import { MinMaxValue, TIMEFRAME_OPTIONS } from "@/control/MinMaxValue";
import { EditValue } from "@/control/EditValue";
import { Label } from "@/control/Label";
@@ -29,12 +28,6 @@ export function Laser1ControlPage() {
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
// Detect if this is a 2-axis laser
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
// Shared timeframe state (default 5 minutes)
const [timeframe, setTimeframe] = React.useState<number>(5 * 60 * 1000);
return (
<Page>
<ControlGrid columns={2}>
@@ -79,62 +72,12 @@ export function Laser1ControlPage() {
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Roundness"
icon="lu:Circle"
timeseries={roundness}
renderValue={(value) => value.toFixed(3)}
/>
</div>
)}
<div className="mt-4 border-t pt-4">
{isTwoAxis ? (
// For 2-axis lasers: show diameter and roundness min/max side by side
<div className="grid grid-cols-2 gap-4">
<MinMaxValue
label="Diameter Range"
unit="mm"
timeseries={diameter}
renderValue={(value) => value.toFixed(3)}
timeframe={timeframe}
hideSelector
/>
{roundness?.current && (
<MinMaxValue
label="Roundness Range"
timeseries={roundness}
renderValue={(value) => value.toFixed(3)}
timeframe={timeframe}
hideSelector
/>
)}
</div>
) : (
// For single-axis lasers: show only diameter min/max with shared selector
<MinMaxValue
label="Diameter Range"
unit="mm"
timeseries={diameter}
renderValue={(value) => value.toFixed(3)}
timeframe={timeframe}
hideSelector
/>
)}
{/* Shared timeframe selector for diameter/roundness */}
<div className="mt-3 flex flex-row flex-wrap gap-2">
{TIMEFRAME_OPTIONS.map((option) => (
<button
key={option.value}
onClick={() => setTimeframe(option.value)}
className={`rounded-md px-3 py-1 text-sm transition-colors ${
timeframe === option.value
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
}`}
>
{option.label}
</button>
))}
</div>
</div>
</ControlCard>
<ControlCard title="Settings">
<Label label="Set Target Diameter">
@@ -20,6 +20,12 @@ export function Laser1Page() {
title: "Graphs",
icon: "lu:ChartSpline",
},
{
link: "settings",
activeLink: "settings",
title: "Config",
icon: "lu:Settings",
},
// {
// link: "manual",
// activeLink: "manual",
@@ -0,0 +1,42 @@
import React from "react";
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { Label } from "@/control/Label";
import { useLaser1 } from "./useLaser1";
import { MachineSelector } from "@/components/MachineConnectionDropdown";
import { ControlGrid } from "@/control/ControlGrid";
export function Laser1SettingsPage() {
const {
state,
selectedMachine,
filteredMachines,
setConnectedWinder,
disconnectWinder,
} = useLaser1();
return (
<Page>
<ControlGrid>
<ControlCard title="Speed by Diameter Settings">
<Label label="Associated Winder">
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_winder_state}
setConnectedMachine={setConnectedWinder}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedWinder({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectWinder(selectedMachine.machine_identification_unique);
}}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -23,6 +23,24 @@ import {
} from "@/lib/timeseries";
// ========== Event Schema Definitions ==========
/**
* Connected machine state schema
*/
export const machineIdentificationSchema = z.object({
vendor: z.number(),
machine: z.number(),
});
export const machineIdentificationUniqueSchema = z.object({
machine_identification: machineIdentificationSchema,
serial: z.number(),
});
export const connectedMachineStateSchema = z.object({
machine_identification_unique: machineIdentificationUniqueSchema.nullable(),
is_available: z.boolean(),
});
/**
* Live values from Laser (30 FPS)
*/
@@ -43,6 +61,7 @@ export const stateEventDataSchema = z.object({
lower_tolerance: z.number(),
target_diameter: z.number(),
}),
connected_winder_state: connectedMachineStateSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -1,13 +1,18 @@
import { toastError } from "@/components/Toast";
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
import { MachineIdentificationUnique } from "@/machines/types";
import { laser1 } from "@/machines/properties";
import {
machineIdentification,
machineIdentificationUnique,
MachineIdentificationUnique,
} from "@/machines/types";
import { laser1, VENDOR_QITECH } from "@/machines/properties";
import { laser1SerialRoute } from "@/routes/routes";
import { z } from "zod";
import { useLaser1Namespace, StateEvent } from "./laser1Namespace";
import { useEffect, useMemo } from "react";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { produce } from "immer";
import { useMachines } from "@/client/useMachines";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
@@ -54,6 +59,16 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
const { request: requestHigherTolerance } = useMachineMutation(
schemaHigherTolerance,
);
const { request: requestConnectedWinder } = useMachineMutation(
z.object({
SetConnectedWinder: machineIdentificationUnique,
}),
);
const { request: requestDisconnectWinder } = useMachineMutation(
z.object({
DisconnectWinder: machineIdentificationUnique,
}),
);
// Action functions with verb-first names
const setTargetDiameter = (target_diameter: number) => {
@@ -101,10 +116,80 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
);
};
const setConnectedWinder = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_winder_state.machine_identification_unique =
machineIdentificationUnique;
},
() =>
requestConnectedWinder({
machine_identification_unique,
data: { SetConnectedWinder: machineIdentificationUnique },
}),
);
};
const disconnectWinder = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_winder_state.machine_identification_unique =
null;
},
() =>
requestDisconnectWinder({
machine_identification_unique,
data: { DisconnectWinder: machineIdentificationUnique },
}),
);
};
// General Helper functions
const machines = useMachines();
// Filter machines for the correct type
const filteredMachines = useMemo(
() =>
machines.filter(
(m) =>
m.machine_identification_unique.machine_identification.vendor ===
VENDOR_QITECH &&
m.machine_identification_unique.machine_identification.machine ===
0x0002,
),
[machines, machineIdentification],
);
// Get selected machine by serial
const selectedMachine = useMemo(() => {
const serial =
state?.data.connected_winder_state?.machine_identification_unique?.serial;
return (
filteredMachines.find(
(m) => m.machine_identification_unique.serial === serial,
) ?? null
);
}, [filteredMachines, state]);
return {
// Consolidated state
state: stateOptimistic.value?.data,
filteredMachines,
selectedMachine,
// Default state for initial values
defaultState: defaultState?.data,
@@ -122,6 +207,8 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
setTargetDiameter,
setLowerTolerance,
setHigherTolerance,
setConnectedWinder,
disconnectWinder,
};
}
-124
View File
@@ -103,94 +103,6 @@ export const winder2: MachineProperties = {
],
};
export const extruder3: MachineProperties = {
name: "Extruder V3",
version: "V3",
slug: "extruder3",
icon: "qi:Extruder",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0016,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "Inverter Interface",
allowed_devices: [
{
vendor_id: 2,
product_id: 394604626,
revision: 1376256,
},
{
vendor_id: 2,
product_id: 394604626,
revision: 0x140000,
},
{
vendor_id: 2,
product_id: 394604626,
revision: 0x160000,
},
{
vendor_id: 2,
product_id: 394604626,
revision: 0x100000,
},
],
},
{
role: 2,
role_label: "Heating Elements",
allowed_devices: [
{
vendor_id: 2,
product_id: 131346514,
revision: 1179648,
},
],
},
{
role: 3,
role_label: "Pressure Sensor",
allowed_devices: [
{
vendor_id: 2,
product_id: 197996626,
revision: 1310720,
},
],
},
{
role: 4,
role_label: "Thermometers",
allowed_devices: [
{
vendor_id: 2,
product_id: 0xc843052,
revision: 1441792,
},
{
vendor_id: 2,
product_id: 0xc843052,
revision: 0x150000,
},
],
},
],
};
export const extruder2: MachineProperties = {
name: "Extruder",
version: "V2",
@@ -427,50 +339,14 @@ export const aquapath1: MachineProperties = {
},
],
};
export const testmachine: MachineProperties = {
name: "TestMachine",
version: "V1",
slug: "testmachine",
icon: "lu:Disc3",
machine_identification: {
vendor: VENDOR_QITECH,
machine: 0x0033,
},
device_roles: [
{
role: 0,
role_label: "Bus Coupler",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x44c2c52,
revision: 0x120000,
},
],
},
{
role: 1,
role_label: "EL2004",
allowed_devices: [
{
vendor_id: 2,
product_id: 0x7d43052,
revision: 0x120000,
},
],
},
],
};
export const machineProperties: MachineProperties[] = [
winder2,
extruder3,
extruder2,
laser1,
mock1,
buffer1,
aquapath1,
testmachine,
];
export const getMachineProperties = (
@@ -1,62 +0,0 @@
import { ControlCard } from "@/control/ControlCard";
import { Page } from "@/components/Page";
import React from "react";
import { ControlGrid } from "@/control/ControlGrid";
import { SelectionGroup } from "@/control/SelectionGroup";
import { Label } from "@/control/Label";
import { useTestMachine } from "./useTestMachine";
export function TestMachineControlPage() {
const { state, setLed, setAllLeds } = useTestMachine();
const safeState = state ?? { led_on: [false, false, false, false] };
return (
<Page>
<ControlGrid columns={2}>
{/* LED Controls */}
<ControlCard title="Machine LEDs">
<div className="grid grid-cols-2 gap-6">
{safeState.led_on.map((led, index) => (
<Label key={index} label={`LED ${index + 1}`}>
<SelectionGroup<"On" | "Off">
value={led ? "On" : "Off"}
orientation="vertical"
className="grid h-full grid-cols-2 gap-2"
options={{
Off: {
children: "Off",
icon: "lu:CirclePause",
isActiveClassName: "bg-red-600",
className: "h-full",
},
On: {
children: "On",
icon: "lu:CirclePlay",
isActiveClassName: "bg-green-600",
className: "h-full",
},
}}
onChange={(value) => setLed(index, value === "On")}
/>
</Label>
))}
</div>
</ControlCard>
{/* Master LED Control */}
<ControlCard title="Master LED Control">
<SelectionGroup<"On" | "Off">
value={safeState.led_on.every(Boolean) ? "On" : "Off"}
orientation="horizontal"
options={{
Off: { children: "Turn All Off" },
On: { children: "Turn All On" },
}}
onChange={(value) => setAllLeds(value === "On")}
/>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -1,20 +0,0 @@
import { Topbar } from "@/components/Topbar";
import { testMachineSerialRoute } from "@/routes/routes";
import React from "react";
export function TestMachinePage() {
const { serial } = testMachineSerialRoute.useParams();
return (
<Topbar
pathname={`/_sidebar/machines/testmachine/${serial}`}
items={[
{
link: "control",
activeLink: "control",
title: "Control",
icon: "lu:CirclePlay",
},
]}
/>
);
}
@@ -1,76 +0,0 @@
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";
// ========== Event Schema ==========
export const stateEventDataSchema = z.object({
led_on: z.array(z.boolean()).length(4),
});
export const stateEventSchema = eventSchema(stateEventDataSchema);
export type StateEvent = z.infer<typeof stateEventDataSchema>;
// ========== Store ==========
export type TestMachineNamespaceStore = {
state: StateEvent | null;
};
export const createTestMachineNamespaceStore =
(): StoreApi<TestMachineNamespaceStore> =>
create<TestMachineNamespaceStore>(() => ({
state: null,
}));
// ========== Message Handler ==========
export function testMachineMessageHandler(
store: StoreApi<TestMachineNamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<TestMachineNamespaceStore>,
): EventHandler {
return (event: Event<any>) => {
const updateStore = (
updater: (state: TestMachineNamespaceStore) => TestMachineNamespaceStore,
) => throttledUpdater.updateWith(updater);
try {
if (event.name === "StateEvent") {
const parsed = stateEventSchema.parse(event);
updateStore(() => ({ state: parsed.data }));
} else {
handleUnhandledEventError(event.name);
}
} catch (error) {
console.error(`Error processing ${event.name}:`, error);
throw error;
}
};
}
// ========== Namespace Hook ==========
const useTestMachineNamespaceImplementation =
createNamespaceHookImplementation<TestMachineNamespaceStore>({
createStore: createTestMachineNamespaceStore,
createEventHandler: testMachineMessageHandler,
});
export function useTestMachineNamespace(
machine_identification_unique: MachineIdentificationUnique,
): TestMachineNamespaceStore {
const namespaceId: NamespaceId = {
type: "machine",
machine_identification_unique,
};
return useTestMachineNamespaceImplementation(namespaceId);
}
@@ -1,95 +0,0 @@
import { toastError } from "@/components/Toast";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { testMachineSerialRoute } from "@/routes/routes";
import { MachineIdentificationUnique } from "@/machines/types";
import { useTestMachineNamespace, StateEvent } from "./testMachineNamespace";
import { useMachineMutate } from "@/client/useClient";
import { produce } from "immer";
import { useEffect, useMemo } from "react";
import { testmachine } from "@/machines/properties";
import { z } from "zod";
export function useTestMachine() {
const { serial: serialString } = testMachineSerialRoute.useParams();
// Memoize machine identification
const machineIdentification: MachineIdentificationUnique = useMemo(() => {
const serial = parseInt(serialString);
if (isNaN(serial)) {
toastError(
"Invalid Serial Number",
`"${serialString}" is not a valid serial number.`,
);
return {
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
};
}
return {
machine_identification: testmachine.machine_identification,
serial,
};
}, [serialString]);
// Namespace state from backend
const { state } = useTestMachineNamespace(machineIdentification);
// Optimistic state
const stateOptimistic = useStateOptimistic<StateEvent>();
useEffect(() => {
if (state) stateOptimistic.setReal(state);
}, [state, stateOptimistic]);
// Generic mutation sender
const { request: sendMutation } = useMachineMutate(
z.object({
action: z.string(),
value: z.any(),
}),
);
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
serverRequest?: () => void,
) => {
const currentState = stateOptimistic.value;
if (currentState)
stateOptimistic.setOptimistic(produce(currentState, producer));
serverRequest?.();
};
const setLed = (index: number, on: boolean) => {
updateStateOptimistically(
(current) => {
current.led_on[index] = on;
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetLed", value: { index, on } },
}),
);
};
const setAllLeds = (on: boolean) => {
updateStateOptimistically(
(current) => {
current.led_on = [on, on, on, on];
},
() =>
sendMutation({
machine_identification_unique: machineIdentification,
data: { action: "SetAllLeds", value: { on } },
}),
);
};
return {
state: stateOptimistic.value,
setLed,
setAllLeds,
};
}
@@ -272,13 +272,18 @@ export function Winder2ControlPage() {
Diameter: {
children: "Diameter",
icon: "lu:Sun",
disabled: true,
disabled: !state?.connected_laser_state.is_available,
},
}}
onChange={setPullerRegulationMode}
disabled={isDisabled}
loading={isLoading}
/>
{state?.connected_laser_state?.is_available !== true ? (
<StatusBadge variant={"info"}>
{"No associated Laser found"}
</StatusBadge>
) : null}
</Label>
<Label label="Target Speed">
<EditValue
@@ -40,10 +40,13 @@ export function Winder2SettingPage() {
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
isLoading,
isDisabled,
setSpeedPiKp,
setSpeedPiKi,
selectedMachine,
filteredMachines,
setConnectedLaser,
disconnectLaser,
setConnectedMachine,
disconnectMachine,
} = useWinder2();
const handleXlModeChange = (enabled: boolean) => {
@@ -354,20 +357,48 @@ export function Winder2SettingPage() {
/>
</Label>
</ControlCard>
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_machine_state}
setConnectedMachine={setConnectedMachine}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedMachine({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectMachine(selectedMachine.machine_identification_unique);
}}
/>
<ControlCard title="Diameter-Based Winder Control">
<Label label="Associated Laser">
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_laser_state}
setConnectedMachine={setConnectedLaser}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedMachine({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectLaser(selectedMachine.machine_identification_unique);
}}
/>
</Label>
<Label label="PI Controller Kp Factor">
<EditValue
value={state?.pi_settings?.kp}
defaultValue={defaultState?.pi_settings?.kp}
min={0}
max={1000}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={setSpeedPiKp}
title="Speed P KP"
/>
</Label>
<Label label="PI Controller Ki Factor">
<EditValue
value={state?.pi_settings?.ki}
defaultValue={defaultState?.pi_settings?.ki}
min={0}
max={1000}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={setSpeedPiKi}
title="Speed P KI"
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
@@ -179,6 +179,27 @@ export function useWinder2() {
}),
);
const { request: requestConnectedLaser } = useMachineMutation(
z.object({
SetConnectedLaser: machineIdentificationUnique,
}),
);
const { request: requestDisconnectLaser } = useMachineMutation(
z.object({
DisconnectLaser: machineIdentificationUnique,
}),
);
const { request: requestSpeedPiSettings } = useMachineMutation(
z.object({
SetSpeedPiSettings: z.object({
kp: z.number(),
ki: z.number(),
}),
}),
);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
@@ -593,6 +614,85 @@ export function useWinder2() {
);
};
const setSpeedPiKp = (kp: number) => {
updateStateOptimistically(
(current) => {
current.data.pi_settings.kp = kp;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(currentState.data.pi_settings, (draft) => {
draft.kp = kp;
});
requestSpeedPiSettings({
machine_identification_unique: machineIdentification,
data: { SetSpeedPiSettings: settings },
});
}
},
);
};
const setSpeedPiKi = (ki: number) => {
updateStateOptimistically(
(current) => {
current.data.pi_settings.ki = ki;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(currentState.data.pi_settings, (draft) => {
draft.ki = ki;
});
requestSpeedPiSettings({
machine_identification_unique: machineIdentification,
data: { SetSpeedPiSettings: settings },
});
}
},
);
};
const setConnectedLaser = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_laser_state.machine_identification_unique =
machineIdentificationUnique;
},
() =>
requestConnectedLaser({
machine_identification_unique,
data: { SetConnectedLaser: machineIdentificationUnique },
}),
);
};
const disconnectLaser = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_laser_state.machine_identification_unique = null;
},
() =>
requestDisconnectLaser({
machine_identification_unique,
data: { DisconnectLaser: machineIdentificationUnique },
}),
);
};
// Calculate loading states
const isLoading = stateOptimistic.isOptimistic;
const isDisabled = !stateOptimistic.isInitialized;
@@ -607,7 +707,7 @@ export function useWinder2() {
m.machine_identification_unique.machine_identification.vendor ===
VENDOR_QITECH &&
m.machine_identification_unique.machine_identification.machine ===
0x0008,
0x0006,
),
[machines, machineIdentification],
);
@@ -615,8 +715,7 @@ export function useWinder2() {
// Get selected machine by serial
const selectedMachine = useMemo(() => {
const serial =
state?.data.connected_machine_state?.machine_identification_unique
?.serial;
state?.data.connected_laser_state?.machine_identification_unique?.serial;
return (
filteredMachines.find(
@@ -674,7 +773,11 @@ export function useWinder2() {
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
setSpeedPiKp,
setSpeedPiKi,
setConnectedMachine,
disconnectMachine,
setConnectedLaser,
disconnectLaser,
};
}
@@ -39,7 +39,10 @@ export const liveValuesEventDataSchema = z.object({
/**
* Puller regulation type enum
*/
export const pullerRegulationSchema = z.enum(["Speed", "Diameter"]);
export const pullerRegulationSchema = z.enum([
"Speed",
"Diameter",
]);
export type PullerRegulation = z.infer<typeof pullerRegulationSchema>;
/**
@@ -141,7 +144,7 @@ export const modeStateSchema = z.object({
});
/**
* Connected machine state scheme
* Connected machine state schema
*/
export const machineIdentificationSchema = z.object({
vendor: z.number(),
@@ -158,6 +161,14 @@ export const connectedMachineStateSchema = z.object({
is_available: z.boolean(),
});
/**
* PI Controller settings schema
*/
export const piSettingsSchema = z.object({
kp: z.number(),
ki: z.number(),
});
/**
* Tension arm state schema
*/
@@ -192,6 +203,8 @@ export const stateEventDataSchema = z.object({
spool_speed_controller_state: spoolSpeedControllerStateSchema,
spool_automatic_action_state: spoolAutomaticActionStateSchema,
connected_machine_state: connectedMachineStateSchema,
connected_laser_state: connectedMachineStateSchema,
pi_settings: piSettingsSchema,
});
// ========== Event Schemas with Wrappers ==========
+18 -83
View File
@@ -1,62 +1,47 @@
import { createRoute, Outlet } from "@tanstack/react-router";
import { RootRoute } from "./__root";
import React from "react";
import { z } from "zod";
import { ChooseVersionPage } from "@/setup/ChooseVersionPage";
import { fallback, zodValidator } from "@tanstack/zod-adapter";
import { githubSourceSchema } from "@/setup/GithubSourceDialog";
import { SidebarLayout } from "@/components/SidebarLayout";
import { SetupPage } from "@/setup/SetupPage";
import { EthercatPage } from "@/setup/EthercatPage";
import { MachinesPage } from "@/setup/MachinesPage";
import { ChangelogPage } from "@/setup/ChangelogPage";
import { TroubleshootPage } from "@/setup/Trobleshoot";
import { UpdateExecutePage } from "@/setup/UpdateExecutePage";
import { Winder2Page } from "@/machines/winder/winder2/Winder2Page";
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 { Winder2PresetsPage } from "@/machines/winder/winder2/Winder2PresetsPage";
import { Extruder2Page } from "@/machines/extruder/extruder2/Extruder2Page";
import { Extruder2ControlPage } from "@/machines/extruder/extruder2/Extruder2ControlPage";
import { Extruder2SettingsPage } from "@/machines/extruder/extruder2/Extruder2Settings";
import { ExtruderV2ManualPage } from "@/machines/extruder/extruder2/Extruder2Manual";
import { Extruder2GraphsPage } from "@/machines/extruder/extruder2/Extruder2Graph";
import { Extruder2PresetsPage } from "@/machines/extruder/extruder2/Extruder2PresetsPage";
import { Extruder3Page } from "@/machines/extruder/extruder3/Extruder3Page";
import { Extruder3ControlPage } from "@/machines/extruder/extruder3/Extruder3ControlPage";
import { Extruder3SettingsPage } from "@/machines/extruder/extruder3/Extruder3Settings";
import { ExtruderV3ManualPage } from "@/machines/extruder/extruder3/Extruder3Manual";
import { Extruder3GraphsPage } from "@/machines/extruder/extruder3/Extruder3Graph";
import { Extruder3PresetsPage } from "@/machines/extruder/extruder3/Extruder3PresetsPage";
import { ChooseVersionPage } from "@/setup/ChooseVersionPage";
import { fallback, zodValidator } from "@tanstack/zod-adapter";
import { Buffer1ControlPage } from "@/machines/buffer/buffer1/Buffer1ControlPage";
import { Buffer1Page } from "@/machines/buffer/buffer1/Buffer1Page";
import { Buffer1SettingsPage } from "@/machines/buffer/buffer1/Buffer1Settings";
import { ChangelogPage } from "@/setup/ChangelogPage";
import { Extruder2GraphsPage } from "@/machines/extruder/extruder2/Extruder2Graph";
import { Extruder2PresetsPage } from "@/machines/extruder/extruder2/Extruder2PresetsPage";
import { githubSourceSchema } from "@/setup/GithubSourceDialog";
import { Laser1ControlPage } from "@/machines/laser/laser1/Laser1ControlPage";
import { Laser1GraphsPage } from "@/machines/laser/laser1/Laser1Graph";
import { Laser1Page } from "@/machines/laser/laser1/Laser1Page";
import { Mock1ControlPage } from "@/machines/mock/mock1/Mock1ControlPage";
import { Mock1GraphPage } from "@/machines/mock/mock1/Mock1Graph";
import { Mock1ManualPage } from "@/machines/mock/mock1/Mock1Manual";
import { Mock1Page } from "@/machines/mock/mock1/Mock1Page";
import { Mock1PresetsPage } from "@/machines/mock/mock1/Mock1PresetsPage";
import { TroubleshootPage } from "@/setup/Trobleshoot";
import { UpdateExecutePage } from "@/setup/UpdateExecutePage";
import { Winder2PresetsPage } from "@/machines/winder/winder2/Winder2PresetsPage";
import { Aquapath1ControlPage } from "@/machines/aquapath/aquapath1/Aquapath1ControlPage";
import { Aquapath1Page } from "@/machines/aquapath/aquapath1/Aquapath1Page";
import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph";
import { TestMachinePage } from "@/machines/testmachine/TestMachinePage";
import { TestMachineControlPage } from "@/machines/testmachine/TestMachineControlPage";
import { z } from "zod";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
import { Laser1SettingsPage } from "@/machines/laser/laser1/Laser1Settings";
// make a route tree like this
// _mainNavigation/machines/winder2/$serial/control
// _mainNavigation/configuration/a
@@ -64,18 +49,6 @@ import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
// the mainNavigation has a custom layout
// the winder2 winder2 and configuration also have a custom layout
// the leaf routes are just pages
export const testMachineSerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "testmachine/$serial",
component: () => <TestMachinePage />,
});
// Leaf route: control page
export const testMachineControlRoute = createRoute({
getParentRoute: () => testMachineSerialRoute,
path: "control",
component: () => <TestMachineControlPage />,
});
export const sidebarRoute = createRoute({
getParentRoute: () => RootRoute,
@@ -124,42 +97,6 @@ export const extruder2PresetsRoute = createRoute({
component: () => <Extruder2PresetsPage />,
});
export const extruder3Route = createRoute({
getParentRoute: () => machinesRoute,
path: "extruder3/$serial",
component: () => <Extruder3Page />,
});
export const extruder3ControlRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "control",
component: () => <Extruder3ControlPage />,
});
export const extruder3SettingsRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "settings",
component: () => <Extruder3SettingsPage />,
});
export const extruder3ManualRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "manual",
component: () => <ExtruderV3ManualPage />,
});
export const extruder3GraphsRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "graphs",
component: () => <Extruder3GraphsPage />,
});
export const extruder3PresetsRoute = createRoute({
getParentRoute: () => extruder3Route,
path: "presets",
component: () => <Extruder3PresetsPage />,
});
export const winder2SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "winder2/$serial",
@@ -220,6 +157,12 @@ export const laser1PresetsRoute = createRoute({
component: () => <Laser1PresetsPage />,
});
export const laser1SettingsRoute = createRoute({
getParentRoute: () => laser1SerialRoute,
path: "settings",
component: () => <Laser1SettingsPage />,
});
export const mock1SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "mock1/$serial",
@@ -376,8 +319,8 @@ export const rootTree = RootRoute.addChildren([
laser1ControlRoute,
laser1GraphsRoute,
laser1PresetsRoute,
laser1SettingsRoute,
]),
testMachineSerialRoute.addChildren([testMachineControlRoute]),
aquapath1SerialRoute.addChildren([
aquapath1ControlRoute,
@@ -400,14 +343,6 @@ export const rootTree = RootRoute.addChildren([
extruder2PresetsRoute,
]),
extruder3Route.addChildren([
extruder3ControlRoute,
extruder3SettingsRoute,
extruder3ManualRoute,
extruder3GraphsRoute,
extruder3PresetsRoute,
]),
mock1SerialRoute.addChildren([
mock1ControlRoute,
mock1GraphRoute,
-14
View File
@@ -3,7 +3,6 @@ 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";
@@ -19,15 +18,12 @@ 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
@@ -54,7 +50,6 @@ export function UpdateExecutePage() {
commit: search.commit,
};
initializeSteps();
startUpdate();
// Perhaps we just need to clear the logs ?
const res = await updateExecute(updateInfo, addTerminalLine);
@@ -143,15 +138,6 @@ export function UpdateExecutePage() {
</Alert>
)}
{/* Progress Bar */}
{isUpdating && (
<UpdateProgressBar
steps={steps}
overallProgress={overallProgress}
className="mb-4"
/>
)}
<Terminal
lines={terminalLines}
className="h-160"
-134
View File
@@ -10,21 +10,10 @@ 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 = {
@@ -34,80 +23,14 @@ 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) => ({
@@ -182,63 +105,6 @@ 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));
}),
),
}));
-7
View File
@@ -46,13 +46,6 @@ 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 {
+1 -1
View File
@@ -10,10 +10,10 @@ workspace = true
clap = { version = "4.5.40", features = ["derive"] }
ethercrab = "0.6.0"
interfaces = "0.0.9"
smol = "2.0.2"
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
futures = "0.3.31"
anyhow = "1.0.100"
+3 -3
View File
@@ -7,7 +7,7 @@ use ethercat_hal::{
helpers::ethercrab_types::EthercrabSubDeviceGroupPreoperational,
};
use ethercrab::MainDevice;
use smol;
use futures::executor::block_on;
use crate::{MAX_SUBDEVICES, PDI_LEN, print::print_markdown_table};
@@ -34,7 +34,7 @@ pub fn ls(
let driver = device_from_subdevice_identity(&identity);
let identification_adresses = get_identification_addresses(&identity, device.name());
let identification = match identification_adresses {
Ok(_) => smol::block_on(machine_device_identification(&device, maindevice)),
Ok(_) => block_on(machine_device_identification(&device, maindevice)),
Err(_) => Err(anyhow!("")),
};
@@ -122,7 +122,7 @@ pub fn ls(
print_markdown_table(&table, true);
// Put group into op state
match smol::block_on(group.into_op(maindevice)) {
match block_on(group.into_op(maindevice)) {
Ok(_) => println!("Successfully put group into OP state"),
Err(e) => println!("Failed to put group into OP state: {}", e),
}
+6 -7
View File
@@ -2,18 +2,18 @@ use std::sync::Arc;
use dump::dump_eeprom;
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, std::ethercat_now};
use futures::executor::block_on;
use read::read_eeprom;
use restore::restore_eeoprom;
use smol;
/// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1.
const MAX_SUBDEVICES: usize = 16;
/// Maximum PDU data payload size - set this to the max PDI size or higher.
const MAX_PDU_DATA: usize = PduStorage::element_size(1024);
const MAX_PDU_DATA: usize = PduStorage::element_size(256);
/// Maximum number of EtherCAT frames that can be in flight at any one time.
const MAX_FRAMES: usize = 16;
/// Maximum total PDI length.
const PDI_LEN: usize = 1024;
const PDI_LEN: usize = 256;
static PDU_STORAGE: PduStorage<MAX_FRAMES, MAX_PDU_DATA> = PduStorage::new();
@@ -57,7 +57,7 @@ async fn main() {
.get_one::<usize>("SUBDEVICE")
.expect("subdevice index is required");
let file = sub_matches.get_one::<String>("file");
let result = smol::block_on(dump_eeprom(&group, &maindevice, *subdevice_index, file));
let result = block_on(dump_eeprom(&group, &maindevice, *subdevice_index, file));
if let Err(e) = result {
eprintln!("Error reading EEPROM: {}", e);
}
@@ -69,8 +69,7 @@ async fn main() {
let file = sub_matches
.get_one::<String>("file")
.expect("file is required");
let result =
smol::block_on(restore_eeoprom(&group, &maindevice, *subdevice_index, file));
let result = block_on(restore_eeoprom(&group, &maindevice, *subdevice_index, file));
if let Err(e) = result {
eprintln!("Error writing EEPROM: {}", e);
}
@@ -79,7 +78,7 @@ async fn main() {
let file = sub_matches
.get_one::<String>("file")
.expect("file is required");
let result = smol::block_on(read_eeprom(file));
let result = block_on(read_eeprom(file));
if let Err(e) = result {
eprintln!("Error parsing EEPROM: {}", e);
}
+2 -1
View File
@@ -10,10 +10,11 @@ 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"
[dev-dependencies]
@@ -1,229 +0,0 @@
use ethercrab::{SubDevice, SubDeviceRef};
use std::fmt;
const DIAGNOSIS_HISTORY_INDEX: u16 = 0x10f3;
const NEWEST_MESSAGE: u8 = 02;
const NEW_MESSAGE_AVAILABLE: u8 = 04;
const DIAG_MESSAGE_LENGTH: usize = 26;
pub struct SubdeviceDiagnosisEntry {
pub diag_code: u32,
pub flags: u16,
pub text_id: u16,
pub timestamp: u64,
pub data_type_p1: u16,
pub p1: u8,
pub flags_p2: u16,
pub p2: [u8; 5],
}
impl fmt::Display for SubdeviceDiagnosisEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SubdeviceDiagnosisEntry {{ diag_code: {}, flags: {}, text_id: {}, timestamp: {}, data_type_p1: {}, p1: {}, flags_p2: {}, p2: {:?} }}",
self.diag_code,
self.flags,
self.text_id,
self.timestamp,
self.data_type_p1,
self.p1,
self.flags_p2,
self.p2
)
}
}
impl SubdeviceDiagnosisEntry {
pub fn to_pretty_string(&self) -> String {
// Decode message type
let msg_type = match self.flags {
0x0000 => "Info",
0x0001 => "Warning",
0x0002 => "Error",
_ => "Unknown",
};
// Decode data type of P1
let p1_type = match self.data_type_p1 {
0x0005 => "UINT8",
_ => "Unknown",
};
// Decode data type of P2
let p2_type = match self.flags_p2 {
0x0005 => "BYTE[5]",
_ => "Unknown",
};
// Format P2 bytes as hex list
let p2_hex: Vec<String> = self.p2.iter().map(|b| format!("0x{:02X}", b)).collect();
format!(
"Diagnosis Entry:\n\
├─ diag_code: 0x{:08X}\n\
├─ message type: {} (0x{:04X})\n\
├─ text_id: {}{}\n\
├─ timestamp: {} (64-bit)\n\
├─ P1 type: {} (0x{:04X})\n\
├─ P1 value: {}\n\
├─ P2 type: {} (0x{:04X})\n\
└─ P2 data: [{}]",
self.diag_code,
msg_type,
self.flags,
self.text_id,
if self.text_id == 0 { " (no text)" } else { "" },
self.timestamp,
// P1
p1_type,
self.data_type_p1,
self.p1,
// P2
p2_type,
self.flags_p2,
p2_hex.join(", ")
)
}
}
/*
Could be tested with this:
00 E8 00 A0 02 00 00 00 5F FB D5 A5 A6 0B 00 00 05 00 11 05 10 08 2A 00 2A 00
*/
pub fn convert_raw_diagnosis_bytes(
message: [u8; DIAG_MESSAGE_LENGTH],
) -> Result<SubdeviceDiagnosisEntry, anyhow::Error> {
let diag_code_bytes = [message[0], message[1], message[2], message[3]];
let diag_code = u32::from_le_bytes(diag_code_bytes);
let flags_code_bytes = [message[4], message[5]];
let flags = u16::from_le_bytes(flags_code_bytes);
let text_id_bytes = [message[6], message[7]];
let text_id = u16::from_le_bytes(text_id_bytes);
let timestamp_bytes = [
message[8],
message[9],
message[10],
message[11],
message[12],
message[13],
message[14],
message[15],
];
let timestamp = u64::from_le_bytes(timestamp_bytes);
let data_type_p1_bytes = [message[16], message[17]];
let data_type_p1 = u16::from_le_bytes(data_type_p1_bytes);
// TODO: Find the Constants
// 0x0005 Apparently Means p1 variable is u8
// For now we only handle u8
let p1 = match data_type_p1 {
0x0005 => message[18],
_ => 0,
};
if p1 == 0 {
return Err(anyhow::anyhow!(
"convert_raw_diagnosis_bytes: Datatype_p1 is unknown {:?}",
data_type_p1
));
}
// flags_p2 is packed, whatever thats supposed to mean
let flags_p2_bytes = [message[19], message[20]];
let flags_p2 = u16::from_le_bytes(flags_p2_bytes);
let p2 = [
message[21],
message[22],
message[23],
message[24],
message[25],
];
return Ok(SubdeviceDiagnosisEntry {
diag_code,
flags,
text_id,
timestamp,
data_type_p1,
p1,
flags_p2,
p2,
});
}
/// Expects the SubdeviceRef to be the Coupler or any other device with a diag history index
pub async fn get_most_recent_diagnosis_message(
device: &SubDeviceRef<'_, &SubDevice>,
) -> Option<String> {
let new_message_available = device
.sdo_read::<bool>(DIAGNOSIS_HISTORY_INDEX, NEW_MESSAGE_AVAILABLE)
.await;
let new_message_available = match new_message_available {
Ok(m) => m,
Err(e) => {
tracing::error!(
"get_most_recent_diagnosis_message: Failed to read new_message_available {:?}",
e
);
return None;
}
};
if !new_message_available {
tracing::info!("get_most_recent_diagnosis_message: No Diagnosis Messages found");
// No messages available
return None;
}
let newest_message_index = device
.sdo_read::<u8>(DIAGNOSIS_HISTORY_INDEX, NEWEST_MESSAGE)
.await;
let newest_message_index = match newest_message_index {
Ok(m) => m,
Err(e) => {
tracing::error!(
"get_most_recent_diagnosis_message: Failed to read newest_message_index {:?}",
e
);
return None;
}
};
tracing::info!(
"get_most_recent_diagnosis_message: Reading newest message at index: {}",
newest_message_index
);
let res = device
.sdo_read::<[u8; DIAG_MESSAGE_LENGTH]>(DIAGNOSIS_HISTORY_INDEX, newest_message_index)
.await;
let message: [u8; DIAG_MESSAGE_LENGTH] = match res {
Ok(res) => res,
Err(_) => {
tracing::error!("get_most_recent_diagnosis_message: Failed to read Diagnosis Message");
return None;
}
};
let message = convert_raw_diagnosis_bytes(message);
let message = match message {
Ok(msg) => msg,
Err(e) => {
tracing::error!(
"get_most_recent_diagnosis_message: Failed to convert Diagnosis Message bytes to struct {:?}",
e
);
return None;
}
};
tracing::info!(
"get_most_recent_diagnosis_message: {}",
message.to_pretty_string()
);
return Some(message.to_pretty_string());
}
-1
View File
@@ -1 +0,0 @@
pub mod diagnosis_history;
+1 -1
View File
@@ -14,7 +14,7 @@ use crate::{
io::analog_input::{AnalogInputDevice, AnalogInputInput},
};
use ethercat_hal_derive::{EthercatDevice, RxPdo, TxPdo};
use units::{electric_potential::volt, f64::ElectricPotential};
use uom::si::{electric_potential::volt, f64::ElectricPotential};
#[derive(EthercatDevice)]
pub struct EL3001 {
Loaded 100 of 249 files, more files were not shown because too many files have changed in this diff. Show more