Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4404a622f2 | ||
|
|
196aae3704 |
No files matched your search
@@ -3,5 +3,3 @@ dist/
|
||||
.DS_Store
|
||||
Cargo.lock
|
||||
dhat-heap.json
|
||||
build.sh
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
# `2.11.0`
|
||||
_21.10.2025_
|
||||
|
||||
## General
|
||||
- [#829](https://github.com/qitechgmbh/control/pull/829), [#834](https://github.com/qitechgmbh/control/pull/834), Improved overall system stability
|
||||
|
||||
## Winder V2
|
||||
- [#840](https://github.com/qitechgmbh/control/pull/840) Added XL Winder option
|
||||
- [#827](https://github.com/qitechgmbh/control/pull/827) Spool can now be run in reverse
|
||||
|
||||
## Extruder V2
|
||||
- [#826](https://github.com/qitechgmbh/control/pull/826) Fine grained Temperature-PID configuration now possible
|
||||
|
||||
|
||||
# `2.10.0`
|
||||
_16.10.2025_
|
||||
|
||||
|
||||
Generated
+5
-30
@@ -476,7 +476,7 @@ dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"windows-link 0.1.3",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2688,7 +2688,6 @@ dependencies = [
|
||||
"serialport",
|
||||
"smol",
|
||||
"socketioxide",
|
||||
"spin_sleep",
|
||||
"textplots",
|
||||
"tokio",
|
||||
"tonic",
|
||||
@@ -2885,15 +2884,6 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
|
||||
[[package]]
|
||||
name = "spin_sleep"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c07347b7c0301b9adba4350bdcf09c039d0e7160922050db0439b3c6723c8ab"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -3601,7 +3591,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link 0.1.3",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
@@ -3634,12 +3624,6 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.4.0"
|
||||
@@ -3657,7 +3641,7 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3666,7 +3650,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3675,7 +3659,7 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3709,15 +3693,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ if [[ "$1" == "release" ]]; then
|
||||
cargo build --release
|
||||
else
|
||||
echo "Building Debug Code"
|
||||
cargo build $FEATURE --features development-build
|
||||
cargo build $FEATURE
|
||||
fi
|
||||
|
||||
# Only setcap if not building with mock-machine
|
||||
|
||||
@@ -102,7 +102,7 @@ pub struct MachineCrossConnection<
|
||||
// to an unknown type. See also https://github.com/qitechgmbh/control/pull/625#discussion_r2379566315
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default)]
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct MachineCrossConnectionState {
|
||||
/// Connected Machine
|
||||
pub machine_identification_unique: Option<MachineIdentificationUnique>,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use smol::lock::RwLock;
|
||||
use smol::{channel::Sender, lock::RwLock};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::machines::identification::DeviceIdentification;
|
||||
use crate::{
|
||||
machines::identification::DeviceIdentification, serial::serial_detection::SerialDeviceRemoval,
|
||||
};
|
||||
|
||||
pub mod panic;
|
||||
pub mod registry;
|
||||
pub mod serial_detection;
|
||||
|
||||
@@ -24,9 +27,10 @@ pub trait SerialDeviceThread {
|
||||
|
||||
pub struct SerialDeviceNewParams {
|
||||
pub path: String,
|
||||
pub device_thread_panic_tx: Sender<SerialDeviceRemoval<String>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct SerialDeviceIdentification {
|
||||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use smol::channel::Sender;
|
||||
|
||||
use crate::serial::serial_detection::SerialDeviceRemoval;
|
||||
|
||||
/// Device-level panic handler for individual device crashes
|
||||
/// Sends device identifier and error information for cleanup
|
||||
/// Used by serial devices and other individual components that need to be removed on panic
|
||||
pub fn send_serial_device_panic<T>(
|
||||
device_identifier: T,
|
||||
thread_panic_tx: Sender<SerialDeviceRemoval<T>>,
|
||||
) where
|
||||
T: Send + Sync + Clone + 'static,
|
||||
{
|
||||
// Ensure backtrace is enabled for panics
|
||||
if std::env::var("RUST_BACKTRACE").is_err() {
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
}
|
||||
}
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
let thread_name = std::thread::current()
|
||||
.name()
|
||||
.unwrap_or("unnamed")
|
||||
.to_string();
|
||||
let location = panic_info.location().map_or("unknown".to_string(), |loc| {
|
||||
format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
|
||||
});
|
||||
let message = panic_info
|
||||
.payload()
|
||||
.downcast_ref::<&str>()
|
||||
.unwrap_or(&"Box<dyn Any>")
|
||||
.to_string();
|
||||
|
||||
let panic_message = format!(
|
||||
"Device panicked in thread '{}' at {}: {}",
|
||||
thread_name, location, message
|
||||
);
|
||||
|
||||
// Send device panic info through channel
|
||||
smol::block_on(async {
|
||||
let _ = thread_panic_tx
|
||||
.send(SerialDeviceRemoval::Error(
|
||||
device_identifier.clone(),
|
||||
anyhow::anyhow!("{}", panic_message),
|
||||
))
|
||||
.await;
|
||||
});
|
||||
|
||||
// Note: We don't call old_hook to avoid duplicate logging
|
||||
// All logging is handled in the main thread when receiving panic details
|
||||
}));
|
||||
}
|
||||
@@ -7,70 +7,194 @@
|
||||
*/
|
||||
|
||||
use serialport::{SerialPortInfo, SerialPortType, UsbPortInfo};
|
||||
use std::collections::HashMap;
|
||||
use smol::{
|
||||
channel::{Receiver, Sender, unbounded},
|
||||
lock::RwLock,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use super::SerialDeviceIdentification;
|
||||
/// Handles scanning and caching of connected USB serial devices.
|
||||
pub struct SerialDetection {}
|
||||
use crate::{
|
||||
helpers::compare_lists::compare_lists, machines::identification::DeviceIdentification,
|
||||
};
|
||||
|
||||
impl SerialDetection {
|
||||
/// Returns all serial ports on the system (minus macOS "cu" noise).
|
||||
fn get_ports() -> Vec<SerialPortInfo> {
|
||||
match serialport::available_ports() {
|
||||
Ok(ports) => ports
|
||||
.into_iter()
|
||||
.filter(|port| !port.port_name.contains("cu"))
|
||||
.collect(),
|
||||
Err(_) => Vec::new(),
|
||||
use super::{
|
||||
SerialDevice, SerialDeviceIdentification, SerialDeviceNewParams, registry::SerialDeviceRegistry,
|
||||
};
|
||||
|
||||
pub enum SerialDeviceRemoval<T> {
|
||||
Disconnect(T),
|
||||
Error(T, anyhow::Error),
|
||||
}
|
||||
|
||||
pub struct SerialDetection<'serialdeviceregistry> {
|
||||
pub serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry,
|
||||
pub ports: HashMap<
|
||||
String,
|
||||
(
|
||||
UsbPortInfo,
|
||||
DeviceIdentification,
|
||||
Arc<RwLock<dyn SerialDevice>>,
|
||||
),
|
||||
>,
|
||||
pub device_removal_signal_rx: Receiver<SerialDeviceRemoval<String>>,
|
||||
pub device_removal_signal_tx: Sender<SerialDeviceRemoval<String>>,
|
||||
}
|
||||
|
||||
impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
|
||||
pub fn new(serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry) -> Self {
|
||||
let (device_removal_signal_tx, device_removal_signal_rx) = unbounded();
|
||||
SerialDetection {
|
||||
serial_device_registry,
|
||||
ports: HashMap::new(),
|
||||
device_removal_signal_rx,
|
||||
device_removal_signal_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts only USB serial devices from the port list.
|
||||
fn extract_usb_serial_devices(port_list: Vec<SerialPortInfo>) -> HashMap<String, UsbPortInfo> {
|
||||
let mut map = HashMap::new();
|
||||
/*
|
||||
*@return: Option::None if there are no available ports on device
|
||||
*@return: Option::Some(Vec) with all available ports
|
||||
*
|
||||
*@description: detects all available ports and returns them as list
|
||||
*/
|
||||
fn get_ports() -> Vec<SerialPortInfo> {
|
||||
let all = match serialport::available_ports() {
|
||||
Ok(ports) => ports,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
// exclude ports that contai "cu" in their name
|
||||
all.into_iter()
|
||||
.filter(|port| !port.port_name.contains("cu"))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/* @param: port_list -> list of available ports
|
||||
*
|
||||
* @return: Vec<(String, UsbPortInfo)> -> list of ports with their names and usb information
|
||||
*
|
||||
* @description: This function extracts the USB serial devices from the given list of ports.
|
||||
*/
|
||||
fn extract_usb_serial_devices(
|
||||
&self,
|
||||
port_list: Vec<SerialPortInfo>,
|
||||
) -> Vec<(String, UsbPortInfo)> {
|
||||
let mut usb_ports: Vec<(String, UsbPortInfo)> = Vec::new();
|
||||
|
||||
for port in port_list {
|
||||
if let SerialPortType::UsbPort(usb_info) = port.port_type {
|
||||
map.insert(port.port_name, usb_info);
|
||||
if let SerialPortType::UsbPort(usb_info) = &port.port_type {
|
||||
usb_ports.push((port.port_name.to_string(), usb_info.clone()));
|
||||
};
|
||||
}
|
||||
|
||||
usb_ports
|
||||
}
|
||||
|
||||
pub async fn check_ports(&mut self) -> CheckPortsResult {
|
||||
// get available ports
|
||||
let ports = Self::get_ports();
|
||||
|
||||
// extract all ports that are usb ports
|
||||
let usb_ports = self.extract_usb_serial_devices(ports);
|
||||
|
||||
let last_usb_ports = self
|
||||
.ports
|
||||
.iter()
|
||||
.map(|last_usb_port| (last_usb_port.0.clone(), last_usb_port.1.0.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let usb_ports_diff = compare_lists(&last_usb_ports, &usb_ports);
|
||||
|
||||
let mut result = CheckPortsResult {
|
||||
added: Vec::new(),
|
||||
removed: Vec::new(),
|
||||
};
|
||||
|
||||
// reamove ports
|
||||
for removed in usb_ports_diff.removed {
|
||||
if let Some((_, device_identification, _)) = self.ports.remove(&removed.0) {
|
||||
result.removed.push(device_identification);
|
||||
}
|
||||
|
||||
tracing::trace!("Removed port: {}", removed.0);
|
||||
}
|
||||
|
||||
// add new ports
|
||||
for added in usb_ports_diff.added {
|
||||
// add the port to the list
|
||||
let serial_device_identification = SerialDeviceIdentification {
|
||||
vendor_id: added.1.vid,
|
||||
product_id: added.1.pid,
|
||||
};
|
||||
|
||||
let device_result = self.serial_device_registry.new_serial_device(
|
||||
&SerialDeviceNewParams {
|
||||
path: added.0.clone(),
|
||||
device_thread_panic_tx: self.device_removal_signal_tx.clone(),
|
||||
},
|
||||
&serial_device_identification,
|
||||
);
|
||||
|
||||
// only if created device driver sucessfully
|
||||
if let Ok((device_identification, device)) = device_result {
|
||||
// add the device to the ports list
|
||||
self.ports.insert(
|
||||
added.0.clone(),
|
||||
(
|
||||
added.1.clone(),
|
||||
device_identification.clone(),
|
||||
device.clone(),
|
||||
),
|
||||
);
|
||||
|
||||
// add to result list
|
||||
result.added.push((device_identification, device.clone()));
|
||||
|
||||
tracing::trace!("Added port: {}", added.0);
|
||||
}
|
||||
}
|
||||
map
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Scans for connected USB serial devices and updates the internal map.
|
||||
/// Returns a clone of the map for quick access.
|
||||
pub fn detect_devices() -> HashMap<String, UsbPortInfo> {
|
||||
let ports = Self::get_ports();
|
||||
let usb_devices = Self::extract_usb_serial_devices(ports);
|
||||
usb_devices
|
||||
}
|
||||
pub async fn check_remove_signals(&mut self) -> Vec<DeviceIdentification> {
|
||||
let mut removed_signals: Vec<DeviceIdentification> = Vec::new();
|
||||
|
||||
pub fn device_port_exists(device_path: &str) -> bool {
|
||||
let ports = Self::get_ports();
|
||||
return ports.iter().any(|p| p.port_name == device_path);
|
||||
}
|
||||
let mut add_removal_signal = |path: &String| {
|
||||
if let Some(info) = self.ports.get(path) {
|
||||
removed_signals.push(info.1.clone());
|
||||
};
|
||||
self.ports.remove(path);
|
||||
};
|
||||
|
||||
pub fn device_id_exists(sdevid: SerialDeviceIdentification) -> bool {
|
||||
let ports = Self::get_ports();
|
||||
for port in ports {
|
||||
if let SerialPortType::UsbPort(usb_info) = port.port_type {
|
||||
if usb_info.vid == sdevid.vendor_id && usb_info.pid == sdevid.product_id {
|
||||
return true;
|
||||
}
|
||||
if self.device_removal_signal_rx.is_empty() {
|
||||
return removed_signals;
|
||||
}
|
||||
|
||||
let removal = self
|
||||
.device_removal_signal_rx
|
||||
.recv()
|
||||
.await
|
||||
.expect("Failed to receive removal signals");
|
||||
|
||||
match removal {
|
||||
SerialDeviceRemoval::Disconnect(path) => {
|
||||
add_removal_signal(&path);
|
||||
tracing::trace!("Removed serial device after disconnect: {:?}", path);
|
||||
}
|
||||
|
||||
SerialDeviceRemoval::Error(path, error) => {
|
||||
add_removal_signal(&path);
|
||||
tracing::error!("Removed erroring serial device: {:?}: {}", path, error);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn get_path_by_id(
|
||||
sdevid: SerialDeviceIdentification,
|
||||
map: HashMap<String, UsbPortInfo>,
|
||||
) -> Option<String> {
|
||||
for (port, usbport) in map.iter() {
|
||||
if usbport.vid == sdevid.vendor_id && usbport.pid == sdevid.product_id {
|
||||
return Some(port.clone());
|
||||
}
|
||||
}
|
||||
return None;
|
||||
removed_signals
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CheckPortsResult {
|
||||
pub added: Vec<(DeviceIdentification, Arc<RwLock<dyn SerialDevice>>)>,
|
||||
pub removed: Vec<DeviceIdentification>,
|
||||
}
|
||||
@@ -118,6 +118,7 @@ impl Namespace {
|
||||
) {
|
||||
// cache the event
|
||||
self.cache(event.clone(), buffer_fn);
|
||||
|
||||
// emit the event - inlined from emit function
|
||||
// Send to global queue for each socket in the namespace
|
||||
for socket in self.sockets.clone() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
+55
-134
@@ -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,13 +64,13 @@
|
||||
"@electron-forge/maker-squirrel": "^7.6.1",
|
||||
"@electron-forge/maker-zip": "^7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
|
||||
"@electron-forge/plugin-fuses": "^7.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",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@tailwindcss/postcss": "^4.0.6",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -97,7 +97,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.4.1",
|
||||
"vite": "^6.3.6",
|
||||
"vitest": "^3.0.6"
|
||||
}
|
||||
},
|
||||
@@ -175,7 +175,6 @@
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -639,7 +638,6 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -662,7 +660,6 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -929,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": {
|
||||
@@ -1143,35 +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",
|
||||
"peer": true,
|
||||
"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": {
|
||||
@@ -2110,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": {
|
||||
@@ -2344,13 +2278,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
|
||||
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
|
||||
"version": "1.54.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
|
||||
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.56.1"
|
||||
"playwright": "1.54.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -4095,7 +4028,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.130.17.tgz",
|
||||
"integrity": "sha512-F1tyMEbX7PJclnA25hAFXhLKq4jqeODvvaogsd0/NX02Kmz3QoT9GV+rgpv2TxamU6RwjRgihSH06Dn5hM6SOw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.130.12",
|
||||
"@tanstack/react-store": "^0.7.0",
|
||||
@@ -4177,7 +4109,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.130.17.tgz",
|
||||
"integrity": "sha512-0AH4s/52X2shYLifr9hR+6R6NpGsBp1M3LyjBTZm6Jlkud+nOhkkRxKxYS7l+JErRlffdxVHxMdoTnKcQ+duiQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/history": "1.130.12",
|
||||
"@tanstack/store": "^0.7.0",
|
||||
@@ -4411,7 +4342,8 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -4568,7 +4500,6 @@
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
@@ -4585,7 +4516,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz",
|
||||
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -4595,7 +4525,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
|
||||
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
@@ -4678,7 +4607,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
|
||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
@@ -5033,7 +4961,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5481,7 +5408,6 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
@@ -6174,8 +6100,7 @@
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"peer": true
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
@@ -6448,7 +6373,8 @@
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
@@ -7088,6 +7014,16 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/encoding": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
@@ -7448,7 +7384,6 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
|
||||
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -7509,7 +7444,6 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -8816,7 +8750,6 @@
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -8879,7 +8812,6 @@
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -9554,7 +9486,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -10120,6 +10051,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -11945,13 +11877,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
|
||||
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
|
||||
"version": "1.54.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
|
||||
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.56.1"
|
||||
"playwright-core": "1.54.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -11964,11 +11895,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
|
||||
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
|
||||
"version": "1.54.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
|
||||
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
@@ -12064,7 +11994,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -12178,6 +12107,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -12192,6 +12122,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -12320,7 +12251,6 @@
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -12329,7 +12259,6 @@
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -12341,7 +12270,6 @@
|
||||
"version": "7.62.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -12382,7 +12310,8 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
@@ -13133,7 +13062,6 @@
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
|
||||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -13923,8 +13851,7 @@
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.11",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
||||
"peer": true
|
||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
"version": "1.0.7",
|
||||
@@ -14026,8 +13953,7 @@
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"peer": true
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
@@ -14078,7 +14004,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -14390,7 +14315,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -14748,11 +14672,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -14874,7 +14797,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15418,7 +15340,6 @@
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
"@electron-forge/maker-squirrel": "^7.6.1",
|
||||
"@electron-forge/maker-zip": "^7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
|
||||
"@electron-forge/plugin-fuses": "^7.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",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@tailwindcss/postcss": "^4.0.6",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -61,11 +61,11 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"vite": "^6.4.1",
|
||||
"vite": "^6.3.6",
|
||||
"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",
|
||||
|
||||
@@ -386,7 +386,7 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
|
||||
|
||||
// check if the namespace exists
|
||||
const namespace = get().namespaces[namespace_path];
|
||||
/*if (namespace) {
|
||||
if (namespace) {
|
||||
set(
|
||||
produce((state: SocketioStore) => {
|
||||
// decrement the count
|
||||
@@ -430,7 +430,7 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
|
||||
}
|
||||
}),
|
||||
);
|
||||
}*/
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SelectionGroupBoolean } from "@/control/SelectionGroup";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import { roundToDecimals } from "@/lib/decimal";
|
||||
import { useExtruder2 } from "./useExtruder";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
|
||||
export function Extruder2SettingsPage() {
|
||||
const {
|
||||
@@ -19,7 +18,6 @@ export function Extruder2SettingsPage() {
|
||||
setPressurePidKp,
|
||||
setPressurePidKi,
|
||||
setPressurePidKd,
|
||||
setTemperaturePidValue,
|
||||
} = useExtruder2();
|
||||
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
@@ -118,128 +116,6 @@ export function Extruder2SettingsPage() {
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
<ControlGrid>
|
||||
<ControlCard title="Temperature PID Settings (Front) ">
|
||||
<Label label="Kp">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.front.kp}
|
||||
defaultValue={defaultState?.pid_settings.temperature.front.kp}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("front", "kp", v)}
|
||||
title="Temperature PID KP"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Ki">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.front.ki}
|
||||
defaultValue={defaultState?.pid_settings.temperature.front.ki}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("front", "ki", v)}
|
||||
title="Temperature PID KI"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Kd">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.front.kd}
|
||||
defaultValue={defaultState?.pid_settings.temperature.front.kd}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("front", "kd", v)}
|
||||
title="Temperature PID KD"
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
<ControlCard title="Temperature PID Settings (Middle) ">
|
||||
<Label label="Kp">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.middle.kp}
|
||||
defaultValue={
|
||||
defaultState?.pid_settings.temperature.middle.kp
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("middle", "kp", v)}
|
||||
title="Temperature PID KP"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Ki">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.middle.ki}
|
||||
defaultValue={
|
||||
defaultState?.pid_settings.temperature.middle.ki
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("middle", "ki", v)}
|
||||
title="Temperature PID KI"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Kd">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.middle.kd}
|
||||
defaultValue={
|
||||
defaultState?.pid_settings.temperature.middle.kd
|
||||
}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("middle", "kd", v)}
|
||||
title="Temperature PID KD"
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
<ControlCard title="Temperature PID Settings (Back) ">
|
||||
<Label label="Kp">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.back.kp}
|
||||
defaultValue={defaultState?.pid_settings.temperature.back.kp}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("back", "kp", v)}
|
||||
title="Temperature PID KP"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Ki">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.back.ki}
|
||||
defaultValue={defaultState?.pid_settings.temperature.back.ki}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("back", "ki", v)}
|
||||
title="Temperature PID KI"
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Kd">
|
||||
<EditValue
|
||||
value={state?.pid_settings.temperature.back.kd}
|
||||
defaultValue={defaultState?.pid_settings.temperature.back.kd}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.001}
|
||||
renderValue={(v) => roundToDecimals(v, 3)}
|
||||
onChange={(v) => setTemperaturePidValue("back", "kd", v)}
|
||||
title="Temperature PID KD"
|
||||
/>
|
||||
</Label>
|
||||
</ControlCard>
|
||||
</ControlGrid>
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
|
||||
@@ -131,24 +131,9 @@ export const inverterStatusStateSchema = z.object({
|
||||
*/
|
||||
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(),
|
||||
}),
|
||||
ki: z.number(),
|
||||
kp: z.number(),
|
||||
kd: z.number(),
|
||||
}),
|
||||
pressure: z.object({
|
||||
ki: z.number(),
|
||||
|
||||
@@ -293,33 +293,6 @@ export function useExtruder2() {
|
||||
);
|
||||
};
|
||||
|
||||
const setTemperaturePidValue = (
|
||||
zone: "front" | "middle" | "back",
|
||||
key: "kp" | "ki" | "kd",
|
||||
value: number,
|
||||
) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.pid_settings.temperature[zone][key] = value;
|
||||
},
|
||||
() => {
|
||||
const currentState = stateOptimistic.value;
|
||||
if (currentState) {
|
||||
const settings = produce(
|
||||
currentState.data.pid_settings.temperature[zone],
|
||||
(draft) => {
|
||||
draft[key] = value;
|
||||
},
|
||||
);
|
||||
requestTemperaturePidSettings({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetTemperaturePidSettings: settings },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const resetInverter = () => {
|
||||
// No optimistic update needed for reset
|
||||
requestResetInverter({
|
||||
@@ -383,17 +356,6 @@ export function useExtruder2() {
|
||||
}),
|
||||
);
|
||||
|
||||
const { request: requestTemperaturePidSettings } = useMachineMutation(
|
||||
z.object({
|
||||
SetTemperaturePidSettings: z.object({
|
||||
ki: z.number(),
|
||||
kp: z.number(),
|
||||
kd: z.number(),
|
||||
zone: z.string(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const { request: requestResetInverter } = useMachineMutation(
|
||||
z.object({ ResetInverter: z.boolean() }),
|
||||
);
|
||||
@@ -442,7 +404,6 @@ export function useExtruder2() {
|
||||
setPressurePidKp,
|
||||
setPressurePidKi,
|
||||
setPressurePidKd,
|
||||
setTemperaturePidValue,
|
||||
resetInverter,
|
||||
};
|
||||
}
|
||||
@@ -28,11 +28,9 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { getWinder2TraverseMax } from "./winder2Config";
|
||||
|
||||
export function Winder2ControlPage() {
|
||||
const [showResetConfirmDialog, setShowResetConfirmDialog] = useState(false);
|
||||
const traverseMax = getWinder2TraverseMax();
|
||||
|
||||
// use optimistic state
|
||||
const {
|
||||
@@ -104,7 +102,7 @@ export function Winder2ControlPage() {
|
||||
{state?.traverse_state && (
|
||||
<TraverseBar
|
||||
inside={0}
|
||||
outside={traverseMax}
|
||||
outside={180}
|
||||
min={state?.traverse_state.limit_inner}
|
||||
max={state?.traverse_state.limit_outer}
|
||||
current={traversePosition.current?.value ?? 0}
|
||||
@@ -123,7 +121,7 @@ export function Winder2ControlPage() {
|
||||
min={Math.max(0, (state?.traverse_state?.limit_inner ?? 0) + 1)}
|
||||
minLabel="IN"
|
||||
maxLabel="OUT"
|
||||
max={traverseMax}
|
||||
max={180}
|
||||
renderValue={(value) => roundToDecimals(value, 0)}
|
||||
onChange={setTraverseLimitOuter}
|
||||
/>
|
||||
@@ -147,8 +145,8 @@ export function Winder2ControlPage() {
|
||||
// We use 1mm buffer to ensure the backend validation (which requires outer > inner + 0.9mm) will pass
|
||||
// Formula: max_inner = outer_limit - 1mm
|
||||
max={Math.min(
|
||||
traverseMax,
|
||||
(state?.traverse_state?.limit_outer ?? traverseMax) - 1,
|
||||
180,
|
||||
(state?.traverse_state?.limit_outer ?? 180) - 1,
|
||||
)}
|
||||
defaultValue={defaultState?.traverse_state?.limit_inner}
|
||||
minLabel="IN"
|
||||
|
||||
@@ -101,11 +101,6 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
data.spool_speed_controller_state?.regulation_mode,
|
||||
},
|
||||
{
|
||||
name: "Spool Direction",
|
||||
renderValue: (data: PresetData<Winder2>) =>
|
||||
data.spool_speed_controller_state?.forward ? "Forward" : "Reverse",
|
||||
},
|
||||
{
|
||||
name: "Spool Min Speed",
|
||||
unit: "rpm",
|
||||
@@ -171,7 +166,6 @@ export function Winder2PresetsPage() {
|
||||
setPullerGearRatio,
|
||||
|
||||
setSpoolRegulationMode,
|
||||
setSpoolForward,
|
||||
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
@@ -200,7 +194,6 @@ export function Winder2PresetsPage() {
|
||||
setSpoolRegulationMode(
|
||||
preset.data?.spool_speed_controller_state?.regulation_mode ?? "MinMax",
|
||||
);
|
||||
setSpoolForward(preset.data?.spool_speed_controller_state?.forward ?? true);
|
||||
setSpoolMinMaxMinSpeed(
|
||||
preset.data?.spool_speed_controller_state?.minmax_min_speed ?? 0,
|
||||
);
|
||||
|
||||
@@ -2,37 +2,25 @@ import { Page } from "@/components/Page";
|
||||
import { ControlCard } from "@/control/ControlCard";
|
||||
import { ControlGrid } from "@/control/ControlGrid";
|
||||
import { EditValue } from "@/control/EditValue";
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { useWinder2 } from "./useWinder";
|
||||
import { roundToDecimals } from "@/lib/decimal";
|
||||
import { Label } from "@/control/Label";
|
||||
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
|
||||
import { SelectionGroup } from "@/control/SelectionGroup";
|
||||
import { MachineSelector } from "@/components/MachineConnectionDropdown";
|
||||
import {
|
||||
getWinder2XLMode,
|
||||
setWinder2XLMode,
|
||||
WINDER2_TRAVERSE_MAX_STANDARD,
|
||||
WINDER2_TRAVERSE_MAX_XL,
|
||||
} from "./winder2Config";
|
||||
|
||||
export function Winder2SettingPage() {
|
||||
const [xlMode, setXlMode] = useState(getWinder2XLMode());
|
||||
|
||||
const {
|
||||
state,
|
||||
defaultState,
|
||||
setTraverseStepSize,
|
||||
setTraversePadding,
|
||||
setTraverseLimitInner,
|
||||
setTraverseLimitOuter,
|
||||
gotoTraverseHome,
|
||||
setPullerForward,
|
||||
setPullerGearRatio,
|
||||
setSpoolRegulationMode,
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
setSpoolForward,
|
||||
setSpoolAdaptiveTensionTarget,
|
||||
setSpoolAdaptiveRadiusLearningRate,
|
||||
setSpoolAdaptiveMaxSpeedMultiplier,
|
||||
@@ -46,58 +34,10 @@ export function Winder2SettingPage() {
|
||||
disconnectMachine,
|
||||
} = useWinder2();
|
||||
|
||||
const handleXlModeChange = (enabled: boolean) => {
|
||||
setWinder2XLMode(enabled);
|
||||
setXlMode(enabled);
|
||||
|
||||
// When switching from XL to normal mode, reset traverse limits to default values
|
||||
if (!enabled && defaultState) {
|
||||
// Only reset if current values exceed the standard max
|
||||
const currentOuter = state?.traverse_state?.limit_outer ?? 0;
|
||||
const currentInner = state?.traverse_state?.limit_inner ?? 0;
|
||||
const defaultOuter = defaultState.traverse_state?.limit_outer;
|
||||
const defaultInner = defaultState.traverse_state?.limit_inner;
|
||||
|
||||
if (
|
||||
currentOuter > WINDER2_TRAVERSE_MAX_STANDARD &&
|
||||
defaultOuter !== undefined
|
||||
) {
|
||||
setTraverseLimitOuter(defaultOuter);
|
||||
setTraverseLimitInner(defaultInner);
|
||||
}
|
||||
if (
|
||||
currentInner > WINDER2_TRAVERSE_MAX_STANDARD &&
|
||||
defaultInner !== undefined
|
||||
) {
|
||||
setTraverseLimitOuter(defaultOuter);
|
||||
setTraverseLimitInner(defaultInner);
|
||||
}
|
||||
|
||||
// Home the traverse when switching from XL to normal mode
|
||||
gotoTraverseHome();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<ControlGrid>
|
||||
<ControlCard title="Traverse">
|
||||
<Label label="Traverse Size">
|
||||
<SelectionGroupBoolean
|
||||
value={xlMode}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
optionFalse={{
|
||||
children: `Standard (${WINDER2_TRAVERSE_MAX_STANDARD}mm)`,
|
||||
icon: "lu:Settings",
|
||||
}}
|
||||
optionTrue={{
|
||||
children: `XL (${WINDER2_TRAVERSE_MAX_XL}mm)`,
|
||||
icon: "lu:Maximize2",
|
||||
}}
|
||||
onChange={handleXlModeChange}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Step Size">
|
||||
<EditValue
|
||||
value={state?.traverse_state?.step_size}
|
||||
@@ -148,23 +88,6 @@ export function Winder2SettingPage() {
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label label="Rotation Direction">
|
||||
<SelectionGroupBoolean
|
||||
value={state?.spool_speed_controller_state?.forward}
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
optionFalse={{
|
||||
children: "Reverse",
|
||||
icon: "lu:RotateCcw",
|
||||
}}
|
||||
optionTrue={{
|
||||
children: "Forward",
|
||||
icon: "lu:RotateCw",
|
||||
}}
|
||||
onChange={(value) => setSpoolForward(value)}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{state?.spool_speed_controller_state?.regulation_mode ===
|
||||
"MinMax" && (
|
||||
<>
|
||||
|
||||
@@ -134,9 +134,6 @@ export function useWinder2() {
|
||||
const { request: requestSpoolSetMinMaxMaxSpeed } = useMachineMutation(
|
||||
z.object({ SetSpoolMinMaxMaxSpeed: z.number() }),
|
||||
);
|
||||
const { request: requestSpoolSetForward } = useMachineMutation(
|
||||
z.object({ SetSpoolForward: z.boolean() }),
|
||||
);
|
||||
const { request: requestSpoolSetAdaptiveTensionTarget } = useMachineMutation(
|
||||
z.object({ SetSpoolAdaptiveTensionTarget: z.number() }),
|
||||
);
|
||||
@@ -470,19 +467,6 @@ export function useWinder2() {
|
||||
);
|
||||
};
|
||||
|
||||
const setSpoolForward = (forward: boolean) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
current.data.spool_speed_controller_state.forward = forward;
|
||||
},
|
||||
() =>
|
||||
requestSpoolSetForward({
|
||||
machine_identification_unique: machineIdentification,
|
||||
data: { SetSpoolForward: forward },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const setSpoolAdaptiveTensionTarget = (value: number) => {
|
||||
updateStateOptimistically(
|
||||
(current) => {
|
||||
@@ -668,7 +652,6 @@ export function useWinder2() {
|
||||
setSpoolRegulationMode,
|
||||
setSpoolMinMaxMinSpeed,
|
||||
setSpoolMinMaxMaxSpeed,
|
||||
setSpoolForward,
|
||||
setSpoolAdaptiveTensionTarget,
|
||||
setSpoolAdaptiveRadiusLearningRate,
|
||||
setSpoolAdaptiveMaxSpeedMultiplier,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Configuration helper for Winder2 machine settings
|
||||
*/
|
||||
|
||||
const WINDER2_XL_MODE_KEY = "winder2_xl_mode";
|
||||
|
||||
export const WINDER2_TRAVERSE_MAX_STANDARD = 180; // mm
|
||||
export const WINDER2_TRAVERSE_MAX_XL = 385; // mm
|
||||
|
||||
/**
|
||||
* Get the XL mode setting from localStorage
|
||||
* @returns true if XL mode is enabled, false otherwise
|
||||
*/
|
||||
export function getWinder2XLMode(): boolean {
|
||||
const stored = localStorage.getItem(WINDER2_XL_MODE_KEY);
|
||||
return stored === "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XL mode setting in localStorage
|
||||
* @param enabled - true to enable XL mode, false to disable
|
||||
*/
|
||||
export function setWinder2XLMode(enabled: boolean): void {
|
||||
localStorage.setItem(WINDER2_XL_MODE_KEY, enabled.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum traverse limit based on XL mode
|
||||
* @returns the maximum traverse limit in mm
|
||||
*/
|
||||
export function getWinder2TraverseMax(): number {
|
||||
return getWinder2XLMode()
|
||||
? WINDER2_TRAVERSE_MAX_XL
|
||||
: WINDER2_TRAVERSE_MAX_STANDARD;
|
||||
}
|
||||
@@ -177,7 +177,6 @@ export const spoolSpeedControllerStateSchema = z.object({
|
||||
adaptive_max_speed_multiplier: z.number(),
|
||||
adaptive_acceleration_factor: z.number(),
|
||||
adaptive_deacceleration_urgency_multiplier: z.number(),
|
||||
forward: z.boolean(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
}),
|
||||
),
|
||||
}));
|
||||
Vendored
-7
@@ -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 {
|
||||
|
||||
+4
-1
@@ -54,7 +54,10 @@ opentelemetry-otlp = { version = "0.30.0", features = [
|
||||
"grpc-tonic",
|
||||
], optional = true }
|
||||
tonic = { version = "0.13.1", optional = true }
|
||||
spin_sleep = "1.3.3"
|
||||
|
||||
#[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
#tikv-jemallocator = { version = "0.6", features = ["profiling", "stats"] }
|
||||
#tikv-jemalloc-ctl = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::socketio::namespaces::Namespaces;
|
||||
use control_core::machines::Machine;
|
||||
use control_core::machines::identification::{DeviceIdentification, MachineIdentificationUnique};
|
||||
use control_core::machines::manager::MachineManager;
|
||||
use control_core::serial::serial_detection::SerialDetection;
|
||||
use control_core::socketio::event::GenericEvent;
|
||||
use ethercat_hal::devices::EthercatDevice;
|
||||
use ethercrab::{MainDevice, SubDeviceGroup, subdevice_group::Op};
|
||||
@@ -24,7 +25,7 @@ pub struct SocketioSetup {
|
||||
}
|
||||
|
||||
pub struct SerialSetup {
|
||||
pub serial_registry: &'static SERIAL_DEVICE_REGISTRY,
|
||||
pub serial_detection: SerialDetection<'static>,
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
@@ -84,7 +85,7 @@ impl AppState {
|
||||
},
|
||||
ethercat_setup: Arc::new(RwLock::new(None)),
|
||||
serial_setup: Arc::new(RwLock::new(SerialSetup {
|
||||
serial_registry: &SERIAL_DEVICE_REGISTRY,
|
||||
serial_detection: SerialDetection::new(&SERIAL_DEVICE_REGISTRY),
|
||||
})),
|
||||
machines: Arc::new(RwLock::new(MachineManager::new())),
|
||||
performance_metrics: Arc::new(RwLock::new(EthercatPerformanceMetrics::new())),
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use control_core::socketio::namespace::NamespaceCacheingLogic;
|
||||
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
socketio::main_namespace::{
|
||||
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn send_ethercat_discovering(app_state: Arc<AppState>) {
|
||||
{
|
||||
let main_namespace = &mut app_state
|
||||
.socketio_setup
|
||||
.namespaces
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event = EthercatInterfaceDiscoveryEvent::Discovering(true).build();
|
||||
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
|
||||
}
|
||||
}
|
||||
pub async fn send_ethercat_found(app_state: Arc<AppState>, interface: &str) {
|
||||
// Notify client via socketio
|
||||
let interface = interface.to_string();
|
||||
let app_state_socketio = app_state.clone();
|
||||
smol::block_on(async move {
|
||||
let main_namespace = &mut app_state_socketio
|
||||
.socketio_setup
|
||||
.namespaces
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event = EthercatInterfaceDiscoveryEvent::Done(interface).build();
|
||||
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
|
||||
});
|
||||
}
|
||||
+67
-20
@@ -1,23 +1,70 @@
|
||||
use control_core::ethercat::interface_discovery::discover_ethercat_interface;
|
||||
use smol::{Task, Timer};
|
||||
use std::time::Duration;
|
||||
use super::setup::setup_loop;
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
socketio::main_namespace::{
|
||||
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
|
||||
},
|
||||
};
|
||||
use control_core::{
|
||||
ethercat::interface_discovery::discover_ethercat_interface,
|
||||
socketio::namespace::NamespaceCacheingLogic,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn find_ethercat_interface() -> Result<String, anyhow::Error> {
|
||||
loop {
|
||||
match discover_ethercat_interface().await {
|
||||
Ok(interface) => {
|
||||
tracing::info!("Found EtherCAT Interface at: {}", interface);
|
||||
return Ok(interface);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("No working interface found: {}. Retrying...", e);
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn init_ethercat(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
let app_state_clone = app_state;
|
||||
|
||||
// Returns a Future to the interface
|
||||
pub fn start_interface_discovery() -> Task<Result<String, anyhow::Error>> {
|
||||
smol::spawn(find_ethercat_interface())
|
||||
std::thread::Builder::new()
|
||||
.name("init_ethercat".to_string())
|
||||
.spawn(move || {
|
||||
// Notify client via socketio
|
||||
let app_state_socketio = app_state_clone.clone();
|
||||
smol::block_on(async move {
|
||||
let main_namespace = &mut app_state_socketio
|
||||
.socketio_setup
|
||||
.namespaces
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event = EthercatInterfaceDiscoveryEvent::Discovering(true).build();
|
||||
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
|
||||
});
|
||||
|
||||
// tries to find a suitable interface in a loop
|
||||
let interface = smol::block_on(async {
|
||||
loop {
|
||||
match discover_ethercat_interface().await {
|
||||
Ok(interface) => {
|
||||
tracing::info!("Found working interface: {}", interface);
|
||||
break interface;
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::warn!("No working interface found, retrying...");
|
||||
// wait 1 seconds before retrying
|
||||
smol::Timer::after(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Notify client via socketio
|
||||
let interface_clone = interface.clone();
|
||||
let app_state_socketio = app_state_clone.clone();
|
||||
smol::block_on(async move {
|
||||
let main_namespace = &mut app_state_socketio
|
||||
.socketio_setup
|
||||
.namespaces
|
||||
.write()
|
||||
.await
|
||||
.main_namespace;
|
||||
let event = EthercatInterfaceDiscoveryEvent::Done(interface_clone).build();
|
||||
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
|
||||
});
|
||||
|
||||
if let Err(e) = smol::block_on(setup_loop(&interface, app_state_clone)) {
|
||||
tracing::error!("EtherCAT setup failed: {:?}", e);
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod config;
|
||||
pub mod ethercat_discovery_info;
|
||||
pub mod init;
|
||||
pub mod setup;
|
||||
@@ -52,7 +52,6 @@ pub async fn setup_loop(interface: &str, app_state: Arc<AppState>) -> Result<(),
|
||||
let _ = set_core_affinity(3);
|
||||
|
||||
// Set the thread to real-time priority
|
||||
#[cfg(not(feature = "development-build"))]
|
||||
let _ = set_realtime_priority();
|
||||
|
||||
#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
|
||||
@@ -86,13 +85,13 @@ pub async fn setup_loop(interface: &str, app_state: Arc<AppState>) -> Result<(),
|
||||
// Default 30_000us
|
||||
pdu: Duration::from_micros(30_000),
|
||||
// Default 10ms
|
||||
eeprom: Duration::from_millis(10),
|
||||
eeprom: Duration::from_millis(100),
|
||||
// Default 0ms
|
||||
wait_loop_delay: Duration::from_millis(0),
|
||||
// Default 100ms
|
||||
mailbox_echo: Duration::from_millis(100),
|
||||
mailbox_echo: Duration::from_millis(1000),
|
||||
// Default 1000ms
|
||||
mailbox_response: Duration::from_millis(1000),
|
||||
mailbox_response: Duration::from_millis(10000),
|
||||
},
|
||||
MainDeviceConfig {
|
||||
// Default RetryBehaviour::None
|
||||
|
||||
+92
-117
@@ -1,87 +1,88 @@
|
||||
use crate::app_state::AppState;
|
||||
use bitvec::prelude::*;
|
||||
use control_core::machines::connection::MachineConnection;
|
||||
use control_core::machines::manager::MachineManager;
|
||||
use control_core::realtime::set_core_affinity;
|
||||
#[cfg(not(feature = "development-build"))]
|
||||
use control_core::realtime::set_realtime_priority;
|
||||
use smol::lock::RwLockReadGuard;
|
||||
use spin_sleep::SpinSleeper;
|
||||
use std::time::Instant;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use control_core::realtime::{set_core_affinity, set_realtime_priority};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{instrument, trace_span};
|
||||
|
||||
// 300 us loop cycle target
|
||||
pub fn start_loop_thread(
|
||||
app_state: Arc<AppState>,
|
||||
cycle_target: Duration,
|
||||
) -> Result<std::thread::JoinHandle<()>, std::io::Error> {
|
||||
static LOOP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
pub fn init_loop(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
// Start control loop
|
||||
let res = std::thread::Builder::new()
|
||||
std::thread::Builder::new()
|
||||
.name("loop".to_owned())
|
||||
.spawn(move || {
|
||||
let sleeper =
|
||||
SpinSleeper::new(3_333_333) // frequency in Hz ~ 1 / 300µs, Basically specifies the accuracy of our sleep
|
||||
.with_spin_strategy(spin_sleep::SpinStrategy::YieldThread);
|
||||
let rt = smol::LocalExecutor::new();
|
||||
|
||||
// Set core affinity to third core
|
||||
let _ = set_core_affinity(2);
|
||||
#[cfg(not(feature = "development-build"))]
|
||||
|
||||
// Set the thread to real-time priority
|
||||
if let Err(e) = set_realtime_priority() {
|
||||
tracing::error!(
|
||||
"[{}::init_loop] Failed to set thread to real-time priority \n{:?}",
|
||||
"[{}::init_loop] Failed to set real-time priority \n{:?}",
|
||||
module_path!(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
tracing::info!(
|
||||
"[{}::init_loop] Real-time priority set successfully for current thread",
|
||||
"[{}::init_loop] Real-time priority set successfully",
|
||||
module_path!()
|
||||
);
|
||||
}
|
||||
|
||||
let rt = smol::LocalExecutor::new();
|
||||
// Instead of creating a NEW async task every iteration of our realtime txrx loop, create it ONCE
|
||||
smol::block_on(async {
|
||||
rt.run(async {
|
||||
loop {
|
||||
if let Err(e) = loop_once(app_state.clone(), &sleeper, cycle_target).await {
|
||||
tracing::error!("Loop failed\n{:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
});
|
||||
loop {
|
||||
let res = smol::block_on(rt.run(async { loop_once(app_state.clone()).await }));
|
||||
|
||||
if let Some(last_loop_start) = app_state
|
||||
.performance_metrics
|
||||
.read_arc_blocking()
|
||||
.last_loop_start
|
||||
{
|
||||
tracing::info!("Failing Loop Took {:?}", last_loop_start.elapsed());
|
||||
if let Err(err) = res {
|
||||
tracing::error!("Loop failed\n{:?}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Exit the entire program if the Loop fails
|
||||
// gets restarted by systemd if running on NixOS, or different distro wtih the same sysd service
|
||||
// Exit the entire program if the Loop fails (gets restarted by systemd if running on NixOS)
|
||||
std::process::exit(1);
|
||||
});
|
||||
return res;
|
||||
})
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"[{}::init_loop] Failed to spawn loop thread\n{:?}",
|
||||
module_path!(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn copy_ethercat_inputs(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
//println!("copy_ethercat_intputs");
|
||||
#[instrument(skip(app_state))]
|
||||
pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
// Record cycle start for performance metrics
|
||||
{
|
||||
let mut metrics = app_state.performance_metrics.write().await;
|
||||
metrics.cycle_start();
|
||||
}
|
||||
|
||||
let ethercat_setup_guard = app_state.ethercat_setup.read().await;
|
||||
|
||||
// only if we have an ethercat setup
|
||||
// - tx/rx cycle
|
||||
// - copy inputs to devices
|
||||
let ethercat_setup_guard = app_state.ethercat_setup.read().await;
|
||||
//tracing::info!("EtherCAT setup available: {}", ethercat_setup.is_some());
|
||||
if let Some(ethercat_setup) = ethercat_setup_guard.as_ref() {
|
||||
let span = trace_span!("loop_once_inputs");
|
||||
let _enter = span.enter();
|
||||
|
||||
// Measure tx_rx performance
|
||||
let txrx_start = Instant::now();
|
||||
ethercat_setup
|
||||
.group
|
||||
.tx_rx(ðercat_setup.maindevice)
|
||||
.await?;
|
||||
let txrx_duration = txrx_start.elapsed();
|
||||
|
||||
// Record tx_rx performance metrics
|
||||
{
|
||||
let mut metrics = app_state.performance_metrics.write().await;
|
||||
metrics.record_txrx_time(txrx_duration);
|
||||
}
|
||||
|
||||
// copy inputs to devices
|
||||
for (i, subdevice) in ethercat_setup
|
||||
@@ -122,15 +123,52 @@ pub async fn copy_ethercat_inputs(app_state: Arc<AppState>) -> Result<(), anyhow
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn copy_ethercat_outputs(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
|
||||
#[cfg(feature = "development-build")]
|
||||
smol::Timer::after(Duration::from_micros(2000)).await;
|
||||
|
||||
#[cfg(not(feature = "development-build"))]
|
||||
smol::Timer::after(Duration::from_micros(500)).await;
|
||||
}
|
||||
|
||||
{
|
||||
let cycle = LOOP_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let start = if cycle % 1000 == 0 {
|
||||
Some(Instant::now())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let span = trace_span!("loop_once_act");
|
||||
let _enter = span.enter();
|
||||
|
||||
let machine_guard = app_state.machines.read().await;
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
for machine in machine_guard.iter() {
|
||||
let connection = &machine.1.lock_blocking().machine_connection;
|
||||
if let MachineConnection::Connected(machine) = connection {
|
||||
if let Some(mut machine_guard) = machine.try_lock() {
|
||||
let span = trace_span!("loop_once_act_machine");
|
||||
let _enter = span.enter();
|
||||
machine_guard.act(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(start) = start {
|
||||
let elapsed = start.elapsed();
|
||||
tracing::info!(
|
||||
"loop_once_act duration on cycle {}: {} ns",
|
||||
cycle,
|
||||
elapsed.as_nanos()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// only if we have an ethercat setup
|
||||
// - copy outputs from devices
|
||||
let ethercat_setup_guard = app_state.ethercat_setup.read().await;
|
||||
|
||||
if let Some(ethercat_setup) = ethercat_setup_guard.as_ref() {
|
||||
let span = trace_span!("loop_once_outputs");
|
||||
let _enter = span.enter();
|
||||
@@ -175,69 +213,6 @@ pub async fn copy_ethercat_outputs(app_state: Arc<AppState>) -> Result<(), anyho
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn execute_machines(machine_guard: &RwLockReadGuard<'_, MachineManager>) {
|
||||
let now = std::time::Instant::now();
|
||||
for machine in machine_guard.iter() {
|
||||
let connection = &machine.1.lock_blocking().machine_connection;
|
||||
if let MachineConnection::Connected(machine) = connection {
|
||||
// if the machine is currenlty locked (likely processing API call)
|
||||
// we skip the machine
|
||||
if let Some(mut machine_guard) = machine.try_lock() {
|
||||
let span = trace_span!("loop_once_act_machine",);
|
||||
let _enter = span.enter();
|
||||
// execute machine
|
||||
machine_guard.act(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(app_state))]
|
||||
pub async fn loop_once<'maindevice>(
|
||||
app_state: Arc<AppState>,
|
||||
sleeper: &SpinSleeper,
|
||||
cycle_target: Duration,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let loop_once_start = std::time::Instant::now();
|
||||
// Record cycle start for performance metrics
|
||||
{
|
||||
let mut metrics = app_state.performance_metrics.write().await;
|
||||
metrics.cycle_start();
|
||||
}
|
||||
{
|
||||
let txrx_start = Instant::now();
|
||||
copy_ethercat_inputs(app_state.clone()).await?;
|
||||
let txrx_duration = txrx_start.elapsed();
|
||||
let mut metrics = app_state.performance_metrics.write().await;
|
||||
metrics.record_txrx_time(txrx_duration);
|
||||
}
|
||||
|
||||
// execute machines
|
||||
{
|
||||
let span = trace_span!("loop_once_act");
|
||||
let _enter = span.enter();
|
||||
let machine_guard = app_state.machines.read().await;
|
||||
execute_machines(&machine_guard).await;
|
||||
}
|
||||
|
||||
{
|
||||
copy_ethercat_outputs(app_state.clone()).await?;
|
||||
}
|
||||
|
||||
if app_state.ethercat_setup.read().await.is_some() {
|
||||
// spin_sleep so we have a cycle time of ~300us
|
||||
// This does push usage to 100% if completely busy, but provides much better accuracy then thread sleep or async sleep
|
||||
sleeper.sleep_until(loop_once_start + cycle_target);
|
||||
} else {
|
||||
// if we dont have an ethercat setup or other rt relevant stuff do the "worse" async sleep or later if we get rid of async thread::sleep or yielding
|
||||
// We do this, so that when no rt relevant code runs the cpu doesnt spin at 100% for no reason
|
||||
let loop_duration = loop_once_start.elapsed();
|
||||
if cycle_target > loop_once_start.elapsed() {
|
||||
smol::Timer::after(cycle_target - loop_duration).await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,8 +3,6 @@ pub mod api;
|
||||
pub mod buffer_tower_controller;
|
||||
pub mod new;
|
||||
|
||||
#[cfg(not(feature = "mock-machine"))]
|
||||
use crate::machines::{MACHINE_BUFFER_V1, VENDOR_QITECH, winder2::Winder2};
|
||||
| ||||