Compare commits

...
Author SHA1 Message Date
kraemr d4c82bfd7d refactored ethercat input ouput into funcs, and added spin_sleep
with default target time of 300us
2025-11-04 15:23:19 +01:00
Robin Krämer a49f19133d Merge pull request #874 from qitechgmbh/855-decouple-serial-devices-from-ethercat-logic-in-looprs-mainrs
855 decouple serial devices from ethercat logic in looprs mainrs
2025-11-04 15:08:49 +01:00
snacj a94009514c Changed Error message of moved code 2025-11-04 12:46:03 +01:00
Snacj d1d3724f52 Fixed Formatting 2025-11-03 18:49:51 +01:00
Snacj 9f4177aff9 Fixed implementation errors 2025-11-03 18:48:01 +01:00
Snacj e323fd76f8 Moved loop thread out of main loop 2025-11-03 18:45:08 +01:00
Snacj b0d24d8502 fixed mock-machine init after app_state 2025-11-03 18:29:27 +01:00
Snacj 8ae20c2aae Added init_mock back to main.rs and fixed serial devices relying on ethercat interface 2025-11-03 18:27:28 +01:00
Snacj 89843514b8 debugging main.rs 2025-11-03 17:41:26 +01:00
Robin Krämer 1a9e39a49e Merge pull request #817 from qitechgmbh/811-add-a-simple-updating-tab
811 add a simple updating progress bar
2025-11-03 14:23:36 +01:00
Christian f4d5e74c59 Add update step tracking and progress bar component 2025-11-02 14:47:05 +01:00
Oshgnacknak eb1301df99 Merge pull request #650 from qitechgmbh/dependabot/npm_and_yarn/electron/electron-forge/plugin-fuses-7.8.3
Bump @electron-forge/plugin-fuses from 7.8.2 to 7.10.2 in /electron
2025-10-31 19:02:46 +01:00
Oshgnacknak 2885fa91cc Bump electron fuses to 17.10.2 2025-10-31 18:41:38 +01:00
dependabot[bot] d89e4681db Bump @electron-forge/plugin-fuses from 7.8.2 to 7.8.3 in /electron
Bumps [@electron-forge/plugin-fuses](https://github.com/electron/forge) from 7.8.2 to 7.8.3.
- [Release notes](https://github.com/electron/forge/releases)
- [Changelog](https://github.com/electron/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/electron/forge/compare/v7.8.2...v7.8.3)

---
updated-dependencies:
- dependency-name: "@electron-forge/plugin-fuses"
  dependency-version: 7.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 17:26:00 +00:00
Oshgnacknak dedcd76241 Merge pull request #761 from qitechgmbh/dependabot/npm_and_yarn/electron/hookform/resolvers-5.2.2
Bump @hookform/resolvers from 4.1.3 to 5.2.2 in /electron
2025-10-31 18:23:33 +01:00
Oshgnacknak e987dd07f9 Fix resolver type argument 2025-10-31 18:22:03 +01:00
dependabot[bot] 6747433100 Bump @hookform/resolvers from 4.1.3 to 5.2.2 in /electron
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 4.1.3 to 5.2.2.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v4.1.3...v5.2.2)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.2.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 18:22:02 +01:00
Robin Krämer 3ffd67918c Merge pull request #854 from qitechgmbh/853-add-laser-dre-manual-md-to-docs
853 add laser dre manual md to docs
2025-10-31 10:39:33 +01:00
Snacj 3fe6556037 Added Settings options to DRE md 2025-10-31 10:34:42 +01:00
snacj 5ccc65241d Added Markdown for laser DRE 2025-10-31 10:34:42 +01:00
Robin Krämer be92fd0665 Merge pull request #857 from qitechgmbh/856-dre-laser-registers-are-read-wrong-in-modbus-mode
Fixed Register Vec to work with 2 Axis Laser in Modbus mode
2025-10-29 21:32:01 +01:00
Snacj 8a6d4d1385 Fixed Register Vec to work with 2 Axis Laser in Modbus mode 2025-10-29 18:10:52 +01:00
Robin Krämer 13117a5d94 Merge pull request #845 from qitechgmbh/844-add-winder-mock-machine
added Winder mock
2025-10-28 17:58:25 +01:00
kraemr 6b74c7ec7a removed default mock-machine 2025-10-28 17:53:04 +01:00
kraemr e8b561f722 added Winder mock 2025-10-28 17:53:04 +01:00
Robin Krämer 75ee5af180 Merge pull request #852 from qitechgmbh/851-improve-caching
851 improve caching
2025-10-28 17:46:21 +01:00
kraemr 29f924f6cc removed socketioManager 2025-10-27 16:42:03 +01:00
kraemr 6e058c8e95 Keep namespaces alive 2025-10-27 15:11:30 +01:00
Robin Krämer 6822da80cf Merge pull request #838 from qitechgmbh/dependabot/npm_and_yarn/electron/multi-a50d7f32cf
Bump playwright and @playwright/test in /electron
2025-10-27 11:24:43 +01:00
dependabot[bot] 37e3302f61 Bump playwright and @playwright/test in /electron
Bumps [playwright](https://github.com/microsoft/playwright) to 1.56.1 and updates ancestor dependency [@playwright/test](https://github.com/microsoft/playwright). These dependencies need to be updated together.


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

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

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

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

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

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

fixed compile errors

refactored serial device detection successfully

added serial_detection functions

fixed  Killed error, and improved serial_detection

fixed all remaining issues

fix

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

No files matched your search

Generated
+30 -5
View File
@@ -476,7 +476,7 @@ dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -2688,6 +2688,7 @@ dependencies = [
"serialport",
"smol",
"socketioxide",
"spin_sleep",
"textplots",
"tokio",
"tonic",
@@ -2884,6 +2885,15 @@ 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"
@@ -3591,7 +3601,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-link 0.1.3",
"windows-result",
"windows-strings 0.4.2",
]
@@ -3624,6 +3634,12 @@ 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"
@@ -3641,7 +3657,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -3650,7 +3666,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -3659,7 +3675,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -3693,6 +3709,15 @@ 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
View File
@@ -15,7 +15,7 @@ if [[ "$1" == "release" ]]; then
cargo build --release
else
echo "Building Debug Code"
cargo build $FEATURE
cargo build $FEATURE --features development-build
fi
# Only setcap if not building with mock-machine
+1 -1
View File
@@ -102,7 +102,7 @@ pub struct MachineCrossConnection<
// to an unknown type. See also https://github.com/qitechgmbh/control/pull/625#discussion_r2379566315
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct MachineCrossConnectionState {
/// Connected Machine
pub machine_identification_unique: Option<MachineIdentificationUnique>,
+3 -7
View File
@@ -1,13 +1,10 @@
use std::{any::Any, sync::Arc};
use smol::{channel::Sender, lock::RwLock};
use smol::lock::RwLock;
use std::fmt::Debug;
use crate::{
machines::identification::DeviceIdentification, serial::serial_detection::SerialDeviceRemoval,
};
use crate::machines::identification::DeviceIdentification;
pub mod panic;
pub mod registry;
pub mod serial_detection;
@@ -27,10 +24,9 @@ pub trait SerialDeviceThread {
pub struct SerialDeviceNewParams {
pub path: String,
pub device_thread_panic_tx: Sender<SerialDeviceRemoval<String>>,
}
#[derive(PartialEq, Eq, Clone, Debug)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct SerialDeviceIdentification {
pub vendor_id: u16,
pub product_id: u16,
-53
View File
@@ -1,53 +0,0 @@
use smol::channel::Sender;
use crate::serial::serial_detection::SerialDeviceRemoval;
/// Device-level panic handler for individual device crashes
/// Sends device identifier and error information for cleanup
/// Used by serial devices and other individual components that need to be removed on panic
pub fn send_serial_device_panic<T>(
device_identifier: T,
thread_panic_tx: Sender<SerialDeviceRemoval<T>>,
) where
T: Send + Sync + Clone + 'static,
{
// Ensure backtrace is enabled for panics
if std::env::var("RUST_BACKTRACE").is_err() {
unsafe {
std::env::set_var("RUST_BACKTRACE", "full");
}
}
std::panic::set_hook(Box::new(move |panic_info| {
let thread_name = std::thread::current()
.name()
.unwrap_or("unnamed")
.to_string();
let location = panic_info.location().map_or("unknown".to_string(), |loc| {
format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
});
let message = panic_info
.payload()
.downcast_ref::<&str>()
.unwrap_or(&"Box<dyn Any>")
.to_string();
let panic_message = format!(
"Device panicked in thread '{}' at {}: {}",
thread_name, location, message
);
// Send device panic info through channel
smol::block_on(async {
let _ = thread_panic_tx
.send(SerialDeviceRemoval::Error(
device_identifier.clone(),
anyhow::anyhow!("{}", panic_message),
))
.await;
});
// Note: We don't call old_hook to avoid duplicate logging
// All logging is handled in the main thread when receiving panic details
}));
}
+48 -172
View File
@@ -7,194 +7,70 @@
*/
use serialport::{SerialPortInfo, SerialPortType, UsbPortInfo};
use smol::{
channel::{Receiver, Sender, unbounded},
lock::RwLock,
};
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
use crate::{
helpers::compare_lists::compare_lists, machines::identification::DeviceIdentification,
};
use super::SerialDeviceIdentification;
/// Handles scanning and caching of connected USB serial devices.
pub struct SerialDetection {}
use super::{
SerialDevice, SerialDeviceIdentification, SerialDeviceNewParams, registry::SerialDeviceRegistry,
};
pub enum SerialDeviceRemoval<T> {
Disconnect(T),
Error(T, anyhow::Error),
}
pub struct SerialDetection<'serialdeviceregistry> {
pub serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry,
pub ports: HashMap<
String,
(
UsbPortInfo,
DeviceIdentification,
Arc<RwLock<dyn SerialDevice>>,
),
>,
pub device_removal_signal_rx: Receiver<SerialDeviceRemoval<String>>,
pub device_removal_signal_tx: Sender<SerialDeviceRemoval<String>>,
}
impl<'serialdeviceregistry> SerialDetection<'serialdeviceregistry> {
pub fn new(serial_device_registry: &'serialdeviceregistry SerialDeviceRegistry) -> Self {
let (device_removal_signal_tx, device_removal_signal_rx) = unbounded();
SerialDetection {
serial_device_registry,
ports: HashMap::new(),
device_removal_signal_rx,
device_removal_signal_tx,
impl SerialDetection {
/// Returns all serial ports on the system (minus macOS "cu" noise).
fn get_ports() -> Vec<SerialPortInfo> {
match serialport::available_ports() {
Ok(ports) => ports
.into_iter()
.filter(|port| !port.port_name.contains("cu"))
.collect(),
Err(_) => Vec::new(),
}
}
/*
*@return: Option::None if there are no available ports on device
*@return: Option::Some(Vec) with all available ports
*
*@description: detects all available ports and returns them as list
*/
fn get_ports() -> Vec<SerialPortInfo> {
let all = match serialport::available_ports() {
Ok(ports) => ports,
Err(_) => return Vec::new(),
};
// exclude ports that contai "cu" in their name
all.into_iter()
.filter(|port| !port.port_name.contains("cu"))
.collect::<Vec<_>>()
}
/* @param: port_list -> list of available ports
*
* @return: Vec<(String, UsbPortInfo)> -> list of ports with their names and usb information
*
* @description: This function extracts the USB serial devices from the given list of ports.
*/
fn extract_usb_serial_devices(
&self,
port_list: Vec<SerialPortInfo>,
) -> Vec<(String, UsbPortInfo)> {
let mut usb_ports: Vec<(String, UsbPortInfo)> = Vec::new();
/// Extracts only USB serial devices from the port list.
fn extract_usb_serial_devices(port_list: Vec<SerialPortInfo>) -> HashMap<String, UsbPortInfo> {
let mut map = HashMap::new();
for port in port_list {
if let SerialPortType::UsbPort(usb_info) = &port.port_type {
usb_ports.push((port.port_name.to_string(), usb_info.clone()));
};
if let SerialPortType::UsbPort(usb_info) = port.port_type {
map.insert(port.port_name, usb_info);
}
}
usb_ports
map
}
pub async fn check_ports(&mut self) -> CheckPortsResult {
// get available ports
/// Scans for connected USB serial devices and updates the internal map.
/// Returns a clone of the map for quick access.
pub fn detect_devices() -> HashMap<String, UsbPortInfo> {
let ports = Self::get_ports();
// extract all ports that are usb ports
let usb_ports = self.extract_usb_serial_devices(ports);
let last_usb_ports = self
.ports
.iter()
.map(|last_usb_port| (last_usb_port.0.clone(), last_usb_port.1.0.clone()))
.collect::<Vec<_>>();
let usb_ports_diff = compare_lists(&last_usb_ports, &usb_ports);
let mut result = CheckPortsResult {
added: Vec::new(),
removed: Vec::new(),
};
// reamove ports
for removed in usb_ports_diff.removed {
if let Some((_, device_identification, _)) = self.ports.remove(&removed.0) {
result.removed.push(device_identification);
}
tracing::trace!("Removed port: {}", removed.0);
}
// add new ports
for added in usb_ports_diff.added {
// add the port to the list
let serial_device_identification = SerialDeviceIdentification {
vendor_id: added.1.vid,
product_id: added.1.pid,
};
let device_result = self.serial_device_registry.new_serial_device(
&SerialDeviceNewParams {
path: added.0.clone(),
device_thread_panic_tx: self.device_removal_signal_tx.clone(),
},
&serial_device_identification,
);
// only if created device driver sucessfully
if let Ok((device_identification, device)) = device_result {
// add the device to the ports list
self.ports.insert(
added.0.clone(),
(
added.1.clone(),
device_identification.clone(),
device.clone(),
),
);
// add to result list
result.added.push((device_identification, device.clone()));
tracing::trace!("Added port: {}", added.0);
}
}
result
let usb_devices = Self::extract_usb_serial_devices(ports);
usb_devices
}
pub async fn check_remove_signals(&mut self) -> Vec<DeviceIdentification> {
let mut removed_signals: Vec<DeviceIdentification> = Vec::new();
pub fn device_port_exists(device_path: &str) -> bool {
let ports = Self::get_ports();
return ports.iter().any(|p| p.port_name == device_path);
}
let mut add_removal_signal = |path: &String| {
if let Some(info) = self.ports.get(path) {
removed_signals.push(info.1.clone());
};
self.ports.remove(path);
};
if self.device_removal_signal_rx.is_empty() {
return removed_signals;
}
let removal = self
.device_removal_signal_rx
.recv()
.await
.expect("Failed to receive removal signals");
match removal {
SerialDeviceRemoval::Disconnect(path) => {
add_removal_signal(&path);
tracing::trace!("Removed serial device after disconnect: {:?}", path);
}
SerialDeviceRemoval::Error(path, error) => {
add_removal_signal(&path);
tracing::error!("Removed erroring serial device: {:?}: {}", path, error);
pub fn device_id_exists(sdevid: SerialDeviceIdentification) -> bool {
let ports = Self::get_ports();
for port in ports {
if let SerialPortType::UsbPort(usb_info) = port.port_type {
if usb_info.vid == sdevid.vendor_id && usb_info.pid == sdevid.product_id {
return true;
}
}
}
return false;
}
removed_signals
pub fn get_path_by_id(
sdevid: SerialDeviceIdentification,
map: HashMap<String, UsbPortInfo>,
) -> Option<String> {
for (port, usbport) in map.iter() {
if usbport.vid == sdevid.vendor_id && usbport.pid == sdevid.product_id {
return Some(port.clone());
}
}
return None;
}
}
#[derive(Debug)]
pub struct CheckPortsResult {
pub added: Vec<(DeviceIdentification, Arc<RwLock<dyn SerialDevice>>)>,
pub removed: Vec<DeviceIdentification>,
}
-1
View File
@@ -118,7 +118,6 @@ impl Namespace {
) {
// cache the event
self.cache(event.clone(), buffer_fn);
// emit the event - inlined from emit function
// Send to global queue for each socket in the namespace
for socket in self.sockets.clone() {
+69
View File
@@ -0,0 +1,69 @@
# Laser DRE Device Documentation
## Overview
The **Laser DRE** is a precision measurement device designed for real-time diameter measurement of rotating objects. It communicates over a **USB Serial interface** using the **Modbus RTU** protocol.
Key features include:
- High-speed diameter measurement
- Dual-axis capability
- USB Serial communication
- Modbus RTU protocol support
---
## Technical Specifications
- **Communication Interface:** USB Serial
- **Protocol:** Modbus RTU
- **Axes:** 2-axis measurement
- **Polling Rate:** Every 16 ms
- **Measurement Rate:** 320 measurements per second (manufacturer specification)
- **Measurement Type:** Diameter measurement | X/Y Axis width
---
## Operation
### Polling
The device **polls the diameter every 16 milliseconds**, ensuring near real-time data acquisition for precise monitoring.
### Measurement
According to the manufacturer, the Laser DRE can measure the diameter **320 times per second**, providing high-resolution data for process control and analysis.
### Axes
The device is capable of measuring across **two axes**, allowing for comprehensive profiling of the object’s diameter and shape.
---
## Communication Protocol
The device uses **Modbus RTU** over the USB Serial connection. Key points for communication:
- **Master/Slave Model:** The Laser DRE acts as a Modbus slave device.
- **Function Codes:** Supports standard Modbus RTU function codes for reading measurements.
- **Data Format:** Diameter measurements are transmitted in the format specified by the manufacturer (consult the device manual for exact register addresses and scaling).
---
## Settings
The device includes a small display that allows changing several operational settings:
- **Unit:**
The laser can operate in **millimeters (mm)** or **inches (in)**.
Our software uses **millimeters**.
- **Alarm:**
The laser can emit an audible alarm if the measured diameter is out of tolerance.
⚠️ **Note:** The tolerance settings on the laser may differ from the tolerance configured in our software.
- **Communication:**
Several communication parameters can be configured:
- **Set UART:** Configure **parity (8N1)** and **baud rate (38400)**.
- **Set Protocol:** Choose the communication protocol. *(We use **Modbus RTU**.)*
- **Set Slave Address:** The slave address of our laser is **1**.
- **Calibration:**
The laser can be calibrated using **4 mm** and **8 mm** reference rods.
+98 -33
View File
@@ -8,7 +8,7 @@
"name": "qitech-control-electron",
"version": "1.0.0",
"dependencies": {
"@hookform/resolvers": "^4.1.2",
"@hookform/resolvers": "^5.2.2",
"@icons-pack/react-simple-icons": "^11.2.0",
"@jub3i/tree-kill": "^0.0.15",
"@radix-ui/react-collapsible": "^1.1.12",
@@ -64,13 +64,13 @@
"@electron-forge/maker-squirrel": "^7.6.1",
"@electron-forge/maker-zip": "^7.6.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
"@electron-forge/plugin-fuses": "^7.8.0",
"@electron-forge/plugin-fuses": "^7.10.2",
"@electron-forge/plugin-vite": "^7.6.1",
"@electron-forge/shared-types": "^7.4.0",
"@electron/fuses": "^2.0.0",
"@electron/fuses": "^1.8.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "^4.0.6",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -97,7 +97,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vitest": "^3.0.6"
}
},
@@ -929,19 +929,62 @@
}
},
"node_modules/@electron-forge/plugin-fuses": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-fuses/-/plugin-fuses-7.8.2.tgz",
"integrity": "sha512-BrN26y8xRuz43aJasktLvze2rh9XIxN6m4FR5NJvXbdcS0vUQvLSOgRm9koV5YL0/zr99OS5FXLiw04U+kKy1g==",
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-fuses/-/plugin-fuses-7.10.2.tgz",
"integrity": "sha512-X8FaBL5pVvKCTBNaa9EjbH6vuaeIU7UcPSmP9501XF4zcKPCfTbQKz49LTMl7gd5YzUm82IlqRjte12LLpcSDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/plugin-base": "7.8.2",
"@electron-forge/shared-types": "7.8.2"
"@electron-forge/plugin-base": "7.10.2",
"@electron-forge/shared-types": "7.10.2"
},
"engines": {
"node": ">= 16.4.0"
},
"peerDependencies": {
"@electron/fuses": ">=1.0.0"
"@electron/fuses": "^1.0.0"
}
},
"node_modules/@electron-forge/plugin-fuses/node_modules/@electron-forge/plugin-base": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-7.10.2.tgz",
"integrity": "sha512-+4YLmkLZxvS6JFXYNI4dHt8Il8iIvwk2o6lCJGwNysOUq2KOZ3Wu1He4Ko8HhKcO1VWbFvslbh57oQn963Aryw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/shared-types": "7.10.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/plugin-fuses/node_modules/@electron-forge/shared-types": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.10.2.tgz",
"integrity": "sha512-e2pd9RsdbKwsNf6UtKoolmJGy92Nc0/XO4SI91doV8cM954hM2XSYz3VHoqXebMFAF1JDfXoEUt6UCRbEDgMgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@electron-forge/tracer": "7.10.2",
"@electron/packager": "^18.3.5",
"@electron/rebuild": "^3.7.0",
"listr2": "^7.0.2"
},
"engines": {
"node": ">= 16.4.0"
}
},
"node_modules/@electron-forge/plugin-fuses/node_modules/@electron-forge/tracer": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.10.2.tgz",
"integrity": "sha512-jhLLQbttfZViSOYn/3SJc8HML+jNZAytPVJwgGGd3coUiFysWJ2Xald99iqOiouPAhIigBfNPxQb/q/EbcDu4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chrome-trace-event": "^1.0.3"
},
"engines": {
"node": ">= 14.17.5"
}
},
"node_modules/@electron-forge/plugin-vite": {
@@ -1100,17 +1143,35 @@
}
},
"node_modules/@electron/fuses": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-2.0.0.tgz",
"integrity": "sha512-lyb1zK3YHeWUjaz7yiK0GnxSPduwASKMyiDbCtbn3spP6EEt+UWtktggWehG0icFrXAk3GwvcJ4nCrJO0N9IhQ==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz",
"integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==",
"dev": true,
"license": "MIT",
"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": ">=22.12.0"
"node": ">=10"
}
},
"node_modules/@electron/get": {
@@ -2049,14 +2110,15 @@
"dev": true
},
"node_modules/@hookform/resolvers": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz",
"integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
"integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
"license": "MIT",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
},
"peerDependencies": {
"react-hook-form": "^7.0.0"
"react-hook-form": "^7.55.0"
}
},
"node_modules/@humanfs/core": {
@@ -2282,12 +2344,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.54.2"
"playwright": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -11882,12 +11945,13 @@
}
},
"node_modules/playwright": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.54.2"
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -11900,10 +11964,11 @@
}
},
"node_modules/playwright-core": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
@@ -14683,9 +14748,9 @@
}
},
"node_modules/vite": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
+5 -5
View File
@@ -28,13 +28,13 @@
"@electron-forge/maker-squirrel": "^7.6.1",
"@electron-forge/maker-zip": "^7.6.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
"@electron-forge/plugin-fuses": "^7.8.0",
"@electron-forge/plugin-fuses": "^7.10.2",
"@electron-forge/plugin-vite": "^7.6.1",
"@electron-forge/shared-types": "^7.4.0",
"@electron/fuses": "^2.0.0",
"@electron/fuses": "^1.8.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "^4.0.6",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -61,11 +61,11 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vitest": "^3.0.6"
},
"dependencies": {
"@hookform/resolvers": "^4.1.2",
"@hookform/resolvers": "^5.2.2",
"@icons-pack/react-simple-icons": "^11.2.0",
"@jub3i/tree-kill": "^0.0.15",
"@radix-ui/react-collapsible": "^1.1.12",
+2 -2
View File
@@ -386,7 +386,7 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
// check if the namespace exists
const namespace = get().namespaces[namespace_path];
if (namespace) {
/*if (namespace) {
set(
produce((state: SocketioStore) => {
// decrement the count
@@ -430,7 +430,7 @@ const useSocketioStore = create<SocketioStore>()((set, get) => ({
}
}),
);
}
}*/
},
}));
@@ -0,0 +1,137 @@
import React from "react";
import { Icon } from "./Icon";
import { cn } from "@/lib/utils";
import type { UpdateStep } from "@/stores/updateStore";
interface UpdateProgressBarProps {
steps: UpdateStep[];
overallProgress: number;
className?: string;
}
export function UpdateProgressBar({
steps,
overallProgress,
className,
}: UpdateProgressBarProps) {
return (
<div className={cn("space-y-4", className)}>
{/* Overall Progress Bar */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Overall Progress</span>
<span className="text-sm font-semibold tabular-nums">
{overallProgress}%
</span>
</div>
<div className="relative h-2.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800">
<div
className="h-full rounded-full bg-gray-900 transition-all duration-500 ease-out dark:bg-gray-100"
style={{ width: `${overallProgress}%` }}
/>
</div>
</div>
{/* Steps List */}
<div className="space-y-2">
{steps.map((step) => (
<div
key={step.id}
className={cn(
"flex items-center gap-3 rounded border px-3 py-2.5 transition-all duration-200",
step.status === "in-progress" &&
"border-gray-400 bg-gray-50 dark:border-gray-600 dark:bg-gray-900",
step.status === "completed" &&
"border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-950",
step.status === "error" &&
"border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950/30",
step.status === "pending" &&
"border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-950",
)}
>
{/* Status Icon */}
<div className="flex-shrink-0">
{step.status === "pending" && (
<div className="size-5 rounded-full border-2 border-gray-300 dark:border-gray-700" />
)}
{step.status === "in-progress" && (
<Icon
name="lu:Loader"
className="size-5 animate-spin text-gray-600 dark:text-gray-400"
/>
)}
{step.status === "completed" && (
<div className="flex size-5 items-center justify-center rounded-full bg-gray-700 dark:bg-gray-300">
<Icon
name="lu:Check"
className="size-3 text-white dark:text-gray-900"
/>
</div>
)}
{step.status === "error" && (
<div className="flex size-5 items-center justify-center rounded-full bg-red-600 dark:bg-red-500">
<Icon name="lu:X" className="size-3 text-white" />
</div>
)}
</div>
{/* Step Content */}
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span
className={cn(
"text-sm",
step.status === "completed" &&
"text-gray-600 dark:text-gray-400",
step.status === "error" &&
"font-medium text-red-700 dark:text-red-400",
step.status === "in-progress" &&
"font-medium text-gray-900 dark:text-gray-100",
step.status === "pending" &&
"text-gray-500 dark:text-gray-500",
)}
>
{step.label}
</span>
{/* Subsector Badge - minimal inline display */}
<span
className={cn("text-xs text-gray-400 dark:text-gray-600")}
>
(
{step.id === "rust-build"
? "rust & electron"
: step.subsector}
)
</span>
</div>
{/* Progress percentage for in-progress steps */}
{step.status === "in-progress" &&
step.progress !== undefined &&
step.progress > 0 && (
<span className="text-xs font-medium text-gray-600 tabular-nums dark:text-gray-400">
{Math.round(step.progress)}%
</span>
)}
</div>
{/* Progress bar for steps with detailed tracking */}
{step.status === "in-progress" &&
step.progress !== undefined &&
step.progress > 0 && (
<div className="mt-2">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div
className="h-full rounded-full bg-gray-700 transition-all duration-500 dark:bg-gray-300"
style={{ width: `${step.progress}%` }}
/>
</div>
</div>
)}
</div>
</div>
))}
</div>
</div>
);
}
+1 -1
View File
@@ -109,7 +109,7 @@ export function EditValue({
type FormSchema = z.infer<typeof formSchema>;
const form = useForm<FormSchema>({
resolver: zodResolver<FormSchema>(formSchema),
resolver: zodResolver(formSchema),
values: { value: value ?? defaultOrZero },
defaultValues: { value: defaultValue },
mode: "all",
@@ -2,3 +2,4 @@ export const UPDATE_EXECUTE = "update:execute";
export const UPDATE_LOG = "update:log";
export const UPDATE_END = "update:end";
export const UPDATE_CANCEL = "update:cancel";
export const UPDATE_STEP = "update:step";
@@ -3,6 +3,7 @@ import {
UPDATE_END,
UPDATE_EXECUTE,
UPDATE_LOG,
UPDATE_STEP,
} from "./update-channels";
type UpdateExecuteInvokeParams = {
@@ -16,11 +17,20 @@ type UpdateExecuteInvokeParams = {
};
};
type UpdateStepParams = {
stepId: string;
status: "pending" | "in-progress" | "completed" | "error";
progress?: number;
};
export function exposeUpdateContext() {
const { contextBridge, ipcRenderer } = window.require("electron");
let currentLogListener: ((event: any, log: string) => void) | null = null;
let currentEndListener: ((event: any, params: any) => void) | null = null;
let currentStepListener:
| ((event: any, params: UpdateStepParams) => void)
| null = null;
contextBridge.exposeInMainWorld("update", {
execute: (params: UpdateExecuteInvokeParams) =>
@@ -52,5 +62,17 @@ export function exposeUpdateContext() {
ipcRenderer.on(UPDATE_END, currentEndListener);
},
onStep: (callback: (params: UpdateStepParams) => void) => {
if (currentStepListener) {
ipcRenderer.removeListener(UPDATE_STEP, currentStepListener);
}
currentStepListener = (_event, params) => {
callback(params);
};
ipcRenderer.on(UPDATE_STEP, currentStepListener);
},
});
}
@@ -4,6 +4,7 @@ import {
UPDATE_END,
UPDATE_EXECUTE,
UPDATE_LOG,
UPDATE_STEP,
} from "./update-channels";
import { spawn, ChildProcess } from "child_process";
import tkill from "@jub3i/tree-kill";
@@ -116,6 +117,13 @@ async function update(
commit,
});
// Reset Rust build progress tracking
rustBuildProgress = {
totalDerivations: 0,
builtDerivations: 0,
maxPercent: 0,
};
const qitechControlEnv = process.env.QITECH_CONTROL_ENV;
const homeDir =
qitechControlEnv === "control-os" ? "/home/qitech" : process.env.HOME;
@@ -129,6 +137,8 @@ async function update(
// 1. first make sure the clone path is empty by deleting it if it containsa .git folder
const repoDir = `${homeDir}/${githubRepoName}`;
// Clear repo (not tracked in progress UI)
const clearResult = await clearRepoDirectory(
`${homeDir}/${githubRepoName}`,
event,
@@ -139,6 +149,10 @@ async function update(
}
// 2. clone the repository
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "in-progress",
});
const cloneResult = await cloneRepository(
{
githubRepoOwner,
@@ -151,11 +165,19 @@ async function update(
event,
);
if (!cloneResult.success) {
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "error",
});
event.sender.send(UPDATE_LOG, cloneResult.error);
return;
}
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "completed",
});
// 3. make the nixos-install.sh script executable
// 3. make the nixos-install.sh script executable (not tracked in progress UI)
const chmodResult = await runCommand(
"chmod",
["+x", "nixos-install.sh"],
@@ -168,17 +190,35 @@ async function update(
}
// 4. run the nixos-install.sh script
const installResult = await runCommand(
// This script will handle rust-build, electron-build, and system-install
// Start with rust-build (cargo builds)
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "in-progress",
});
const installResult = await runCommandWithStepTracking(
"./nixos-install.sh",
[],
repoDir,
event,
);
if (!installResult.success) {
// Mark current and remaining steps as error
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "error",
});
event.sender.send(UPDATE_STEP, {
stepId: "system-install",
status: "error",
});
event.sender.send(UPDATE_LOG, installResult.error);
return;
}
// Success - steps are already marked as completed by runCommandWithStepTracking
resolve();
} catch (error: any) {
reject(error);
@@ -244,7 +284,7 @@ async function cloneRepository(
: `https://github.com/${githubRepoOwner}/${githubRepoName}.git`;
// Determine clone arguments based on whether tag, branch, or commit is specified
const cloneArgs = ["clone", repoUrl];
const cloneArgs = ["clone", "--progress", repoUrl];
if (tag) {
// Clone a specific tag
@@ -333,6 +373,11 @@ async function runCommand(
const log = data.toString();
console.error(log);
event.sender.send(UPDATE_LOG, log);
// Git outputs progress to stderr
if (cmd === "git" && args.includes("--progress")) {
parseGitProgress(log, event);
}
});
// Handle process completion
@@ -386,6 +431,249 @@ async function runCommand(
}
}
// Parse Git clone progress
function parseGitProgress(
log: string,
event: Electron.IpcMainInvokeEvent,
): void {
// Git progress format: "Receiving objects: 45% (234/520)"
// or "Resolving deltas: 100% (150/150)"
const receivingMatch = log.match(/Receiving objects:\s*(\d+)%/);
const resolvingMatch = log.match(/Resolving deltas:\s*(\d+)%/);
if (receivingMatch) {
const percent = parseInt(receivingMatch[1], 10);
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "in-progress",
progress: Math.floor(percent * 0.8), // Receiving is 80% of clone
});
} else if (resolvingMatch) {
const percent = parseInt(resolvingMatch[1], 10);
event.sender.send(UPDATE_STEP, {
stepId: "clone-repo",
status: "in-progress",
progress: Math.floor(80 + percent * 0.2), // Resolving is last 20%
});
}
}
// Track Rust build progress
let rustBuildProgress = {
totalDerivations: 0,
builtDerivations: 0,
maxPercent: 0, // Track max to prevent backward movement
};
// Parse Rust build output for progress
function parseRustBuildOutput(
log: string,
event: Electron.IpcMainInvokeEvent,
): void {
// Track derivations to build
const derivationsMatch = log.match(/these (\d+) derivations? will be built/i);
if (derivationsMatch) {
rustBuildProgress.totalDerivations = parseInt(derivationsMatch[1], 10);
rustBuildProgress.builtDerivations = 0;
rustBuildProgress.maxPercent = 0;
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "in-progress",
progress: 0,
});
return;
}
// Track building packages
if (
log.includes("building '/nix/store/") ||
log.includes("building /nix/store/")
) {
rustBuildProgress.builtDerivations++;
// Check if this is the server-deps package (one of the last builds)
const isServerDeps = log.includes("-server-deps");
let percent = 15;
if (isServerDeps) {
// server-deps indicates we're at 85%
percent = 85;
} else if (rustBuildProgress.totalDerivations > 0) {
const derivationProgress =
rustBuildProgress.builtDerivations / rustBuildProgress.totalDerivations;
percent = 15 + Math.floor(derivationProgress * 70); // Map to 15-85%
}
// Only move forward
percent = Math.max(percent, rustBuildProgress.maxPercent);
rustBuildProgress.maxPercent = percent;
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "in-progress",
progress: percent,
});
}
// Track installing phase - go up to 90%
if (log.includes("installing") || log.includes("Installing")) {
const percent = Math.max(90, rustBuildProgress.maxPercent);
rustBuildProgress.maxPercent = percent;
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "in-progress",
progress: percent,
});
}
}
// Enhanced version of runCommand that tracks build steps based on log output
async function runCommandWithStepTracking(
cmd: string,
args: string[],
workingDir: string,
event: Electron.IpcMainInvokeEvent,
): Promise<{ success: boolean; error?: string }> {
try {
const completeCommand = `${cmd} ${args.join(" ")}`;
const workingDirText = terminalGray(workingDir);
event.sender.send(
UPDATE_LOG,
`🚀 ${workingDirText} ${terminalColor("blue", completeCommand)}`,
);
const childProcess = spawn(cmd, args, {
cwd: workingDir,
});
// Store reference to current process for cancellation
currentUpdateProcess = childProcess;
// Track which steps have been marked as in-progress
// Note: rust-build is already marked as in-progress before calling this function
let systemInstallStarted = false;
// Function to process log output and update steps
const processLogForSteps = (log: string) => {
const logLower = log.toLowerCase();
// Parse Rust build progress
parseRustBuildOutput(log, event);
// Check for system install indicators
// System install happens after Rust build completes (at ~90%)
// Look for bootloader/activation messages that indicate final system installation
if (
!systemInstallStarted &&
rustBuildProgress.maxPercent >= 90 &&
(logLower.includes("updating grub") ||
logLower.includes("installing bootloader") ||
logLower.includes("updating bootloader") ||
logLower.includes("activating the configuration") ||
logLower.includes("building the system configuration") ||
logLower.includes("these 0 derivations"))
) {
console.log("🟢 Detected system install start:", log.substring(0, 100));
// Mark rust as complete, start system install
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "completed",
});
event.sender.send(UPDATE_STEP, {
stepId: "system-install",
status: "in-progress",
});
systemInstallStarted = true;
}
};
// Stream stdout logs back to renderer
childProcess.stdout.on("data", (data) => {
const log = data.toString();
console.log(log);
event.sender.send(UPDATE_LOG, log);
processLogForSteps(log);
});
// Stream stderr logs back to renderer
childProcess.stderr.on("data", (data) => {
const log = data.toString();
console.error(log);
event.sender.send(UPDATE_LOG, log);
processLogForSteps(log);
});
// Handle process completion
return new Promise((resolve, reject) => {
childProcess.on("close", (code, signal) => {
// Clear process reference when completed
if (currentUpdateProcess === childProcess) {
currentUpdateProcess = null;
}
if (signal === "SIGTERM" || signal === "SIGKILL") {
event.sender.send(UPDATE_LOG, terminalInfo("Command was cancelled"));
reject({
success: false,
error: "Command was cancelled",
});
} else if (code === 0) {
// Mark all remaining steps as completed on success
if (!systemInstallStarted) {
event.sender.send(UPDATE_STEP, {
stepId: "rust-build",
status: "completed",
});
event.sender.send(UPDATE_STEP, {
stepId: "system-install",
status: "completed",
});
} else {
event.sender.send(UPDATE_STEP, {
stepId: "system-install",
status: "completed",
});
}
event.sender.send(
UPDATE_LOG,
terminalSuccess("Command completed successfully"),
);
resolve({ success: true, error: undefined });
} else {
event.sender.send(
UPDATE_LOG,
terminalError(`Command failed with code ${code}`),
);
reject({
success: false,
error: terminalError(code?.toString() ?? "NO_CODE"),
});
}
});
childProcess.on("error", (err) => {
// Clear process reference on error
if (currentUpdateProcess === childProcess) {
currentUpdateProcess = null;
}
event.sender.send(
UPDATE_LOG,
terminalError(`Command error: ${err.message}`),
);
reject({
success: false,
error: terminalError(err.message),
});
});
});
} catch (error: any) {
event.sender.send(UPDATE_LOG, terminalError(`Error: ${error.toString()}`));
return { success: false, error: error.toString() };
}
}
function terminalColor(
color: "blue" | "green" | "red" | "cyan" | "gray",
text: string,
+25
View File
@@ -2,6 +2,13 @@ import type { UpdateInfo } from "@/stores/updateStore";
import { useUpdateStore } from "@/stores/updateStore";
let currentLogListener: ((line: string) => void) | null = null;
let currentStepListener:
| ((params: {
stepId: string;
status: "pending" | "in-progress" | "completed" | "error";
progress?: number;
}) => void)
| null = null;
export async function updateExecute(
source: UpdateInfo,
@@ -14,15 +21,32 @@ export async function updateExecute(
currentLogListener = null;
}
if (currentStepListener) {
window.update.onStep(() => {}); // clear previous
currentStepListener = null;
}
currentLogListener = onLog;
window.update.onLog(currentLogListener);
// Set up step listener
currentStepListener = (params) => {
const { setStepStatus, setStepProgress } = useUpdateStore.getState();
setStepStatus(params.stepId, params.status);
if (params.progress !== undefined) {
setStepProgress(params.stepId, params.progress);
}
};
window.update.onStep(currentStepListener);
window.update.execute(source);
window.update.onEnd((params) => {
window.update.onLog(() => {}); // remove listener
window.update.onEnd(() => {});
window.update.onStep(() => {}); // remove step listener
currentLogListener = null;
currentStepListener = null;
resolve(params);
});
});
@@ -55,6 +79,7 @@ export async function updateCancel(): Promise<{
const result = await window.update.cancel();
window.update.onLog(() => {}); // remove listener
window.update.onEnd(() => {}); // remove listener
window.update.onStep(() => {}); // remove step listener
return result;
}
+14
View File
@@ -3,6 +3,7 @@ import { Page } from "@/components/Page";
import { SectionTitle } from "@/components/SectionTitle";
import { Terminal } from "@/components/Terminal";
import { TouchButton } from "@/components/touch/TouchButton";
import { UpdateProgressBar } from "@/components/UpdateProgressBar";
import { updateExecute, updateCancelWithStore } from "@/helpers/update_helpers";
import { useUpdateStore } from "@/stores/updateStore";
import { useSearch } from "@tanstack/react-router";
@@ -18,12 +19,15 @@ export function UpdateExecutePage() {
isUpdating,
terminalLines,
currentUpdateInfo,
steps,
overallProgress,
setUpdateInfo,
startUpdate,
stopUpdate,
addTerminalLine,
clearTerminalLines,
resetUpdateState,
initializeSteps,
} = useUpdateStore();
// Set update info from search params when component mounts or search changes
@@ -50,6 +54,7 @@ export function UpdateExecutePage() {
commit: search.commit,
};
initializeSteps();
startUpdate();
// Perhaps we just need to clear the logs ?
const res = await updateExecute(updateInfo, addTerminalLine);
@@ -138,6 +143,15 @@ export function UpdateExecutePage() {
</Alert>
)}
{/* Progress Bar */}
{isUpdating && (
<UpdateProgressBar
steps={steps}
overallProgress={overallProgress}
className="mb-4"
/>
)}
<Terminal
lines={terminalLines}
className="h-160"
+134
View File
@@ -10,10 +10,21 @@ export type UpdateInfo = {
commit?: string;
};
export type UpdateStep = {
id: string;
label: string;
status: "pending" | "in-progress" | "completed" | "error";
subsector: "nixos" | "rust" | "electron" | "general";
progress?: number; // 0-100 for steps with detailed progress tracking
};
export type UpdateState = {
isUpdating: boolean;
terminalLines: string[];
currentUpdateInfo: UpdateInfo | null;
steps: UpdateStep[];
currentStepIndex: number;
overallProgress: number; // 0-100
};
export type UpdateActions = {
@@ -23,14 +34,80 @@ export type UpdateActions = {
addTerminalLine: (line: string) => void;
clearTerminalLines: () => void;
resetUpdateState: () => void;
setStepStatus: (stepId: string, status: UpdateStep["status"]) => void;
setStepProgress: (stepId: string, progress: number) => void;
initializeSteps: () => void;
updateProgress: (progress: number) => void;
};
export type UpdateStore = UpdateState & UpdateActions;
const defaultSteps: UpdateStep[] = [
{
id: "clone-repo",
label: "Clone repository",
status: "pending",
subsector: "general",
progress: 0,
},
{
id: "rust-build",
label: "Build system packages",
status: "pending",
subsector: "rust",
progress: 0,
},
{
id: "system-install",
label: "Install system update",
status: "pending",
subsector: "nixos",
progress: 0,
},
];
// Helper function to calculate overall progress based on step weights and individual progress
const calculateOverallProgress = (steps: UpdateStep[]): number => {
// Define weights for each step (should sum to 100)
const stepWeights: Record<string, number> = {
"clone-repo": 10, // 10% for cloning
"rust-build": 80, // 80% for building (goes up to 90% internally)
"system-install": 10, // 10% for system installation
};
let totalProgress = 0;
steps.forEach((step) => {
const weight = stepWeights[step.id] || 0;
if (step.status === "completed") {
// Completed steps contribute their full weight
totalProgress += weight;
} else if (step.status === "in-progress") {
// In-progress steps: use progress value or 0 if not yet set
const stepProgress = step.progress ?? 0;
if (step.id === "rust-build") {
// Rust build progress is 0-90%, map it to 0-100% of its weight
totalProgress += (stepProgress / 90) * weight;
} else {
// Other steps: progress is 0-100%
totalProgress += (stepProgress / 100) * weight;
}
}
// Pending and error steps contribute 0
});
return Math.round(Math.min(100, Math.max(0, totalProgress)));
};
const initialState: UpdateState = {
isUpdating: false,
terminalLines: [],
currentUpdateInfo: null,
steps: defaultSteps,
currentStepIndex: 0,
overallProgress: 0,
};
export const useUpdateStore = create<UpdateStore>((set) => ({
@@ -105,6 +182,63 @@ export const useUpdateStore = create<UpdateStore>((set) => ({
state.isUpdating = false;
state.terminalLines = [];
state.currentUpdateInfo = null;
state.steps = defaultSteps.map((step) => ({ ...step }));
state.currentStepIndex = 0;
state.overallProgress = 0;
}),
),
initializeSteps: () =>
set(
produce((state: UpdateState) => {
state.steps = defaultSteps.map((step) => ({ ...step }));
state.currentStepIndex = 0;
state.overallProgress = 0;
}),
),
setStepStatus: (stepId: string, status: UpdateStep["status"]) =>
set(
produce((state: UpdateState) => {
const stepIndex = state.steps.findIndex((s) => s.id === stepId);
if (stepIndex !== -1) {
state.steps[stepIndex].status = status;
// Update current step index and ensure progress is initialized
if (status === "in-progress") {
state.currentStepIndex = stepIndex;
// Initialize progress to 0 if not already set
if (state.steps[stepIndex].progress === undefined) {
state.steps[stepIndex].progress = 0;
}
}
// Calculate overall progress based on step weights and progress
state.overallProgress = calculateOverallProgress(state.steps);
}
}),
),
setStepProgress: (stepId: string, progress: number) =>
set(
produce((state: UpdateState) => {
const stepIndex = state.steps.findIndex((s) => s.id === stepId);
if (stepIndex !== -1) {
state.steps[stepIndex].progress = Math.min(
100,
Math.max(0, progress),
);
// Recalculate overall progress when step progress changes
state.overallProgress = calculateOverallProgress(state.steps);
}
}),
),
updateProgress: (progress: number) =>
set(
produce((state: UpdateState) => {
state.overallProgress = Math.min(100, Math.max(0, progress));
}),
),
}));
+7
View File
@@ -46,6 +46,13 @@ interface UpdateContext {
onEnd: (
callback: (params: { success: boolean; error?: string }) => void,
) => void;
onStep: (
callback: (params: {
stepId: string;
status: "pending" | "in-progress" | "completed" | "error";
progress?: number;
}) => void,
) => void;
}
interface TroubleshootContext {
+1 -4
View File
@@ -54,10 +54,7 @@ opentelemetry-otlp = { version = "0.30.0", features = [
"grpc-tonic",
], optional = true }
tonic = { version = "0.13.1", optional = true }
#[target.'cfg(not(target_env = "msvc"))'.dependencies]
#tikv-jemallocator = { version = "0.6", features = ["profiling", "stats"] }
#tikv-jemalloc-ctl = "0.6"
spin_sleep = "1.3.3"
[dev-dependencies]
approx = "0.5.1"
+2 -3
View File
@@ -6,7 +6,6 @@ use crate::socketio::namespaces::Namespaces;
use control_core::machines::Machine;
use control_core::machines::identification::{DeviceIdentification, MachineIdentificationUnique};
use control_core::machines::manager::MachineManager;
use control_core::serial::serial_detection::SerialDetection;
use control_core::socketio::event::GenericEvent;
use ethercat_hal::devices::EthercatDevice;
use ethercrab::{MainDevice, SubDeviceGroup, subdevice_group::Op};
@@ -25,7 +24,7 @@ pub struct SocketioSetup {
}
pub struct SerialSetup {
pub serial_detection: SerialDetection<'static>,
pub serial_registry: &'static SERIAL_DEVICE_REGISTRY,
}
pub struct AppState {
@@ -85,7 +84,7 @@ impl AppState {
},
ethercat_setup: Arc::new(RwLock::new(None)),
serial_setup: Arc::new(RwLock::new(SerialSetup {
serial_detection: SerialDetection::new(&SERIAL_DEVICE_REGISTRY),
serial_registry: &SERIAL_DEVICE_REGISTRY,
})),
machines: Arc::new(RwLock::new(MachineManager::new())),
performance_metrics: Arc::new(RwLock::new(EthercatPerformanceMetrics::new())),
@@ -0,0 +1,38 @@
use std::sync::Arc;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use crate::{
app_state::AppState,
socketio::main_namespace::{
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
},
};
pub async fn send_ethercat_discovering(app_state: Arc<AppState>) {
{
let main_namespace = &mut app_state
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Discovering(true).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
}
}
pub async fn send_ethercat_found(app_state: Arc<AppState>, interface: &str) {
// Notify client via socketio
let interface = interface.to_string();
let app_state_socketio = app_state.clone();
smol::block_on(async move {
let main_namespace = &mut app_state_socketio
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Done(interface).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
});
}
+19 -66
View File
@@ -1,70 +1,23 @@
use super::setup::setup_loop;
use crate::{
app_state::AppState,
socketio::main_namespace::{
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
},
};
use control_core::{
ethercat::interface_discovery::discover_ethercat_interface,
socketio::namespace::NamespaceCacheingLogic,
};
use std::sync::Arc;
use control_core::ethercat::interface_discovery::discover_ethercat_interface;
use smol::{Task, Timer};
use std::time::Duration;
pub fn init_ethercat(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
let app_state_clone = app_state;
std::thread::Builder::new()
.name("init_ethercat".to_string())
.spawn(move || {
// Notify client via socketio
let app_state_socketio = app_state_clone.clone();
smol::block_on(async move {
let main_namespace = &mut app_state_socketio
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Discovering(true).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
});
// tries to find a suitable interface in a loop
let interface = smol::block_on(async {
loop {
match discover_ethercat_interface().await {
Ok(interface) => {
tracing::info!("Found working interface: {}", interface);
break interface;
}
Err(_) => {
tracing::warn!("No working interface found, retrying...");
// wait 1 seconds before retrying
smol::Timer::after(std::time::Duration::from_secs(1)).await;
}
}
}
});
// Notify client via socketio
let interface_clone = interface.clone();
let app_state_socketio = app_state_clone.clone();
smol::block_on(async move {
let main_namespace = &mut app_state_socketio
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Done(interface_clone).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
});
if let Err(e) = smol::block_on(setup_loop(&interface, app_state_clone)) {
tracing::error!("EtherCAT setup failed: {:?}", e);
pub async fn find_ethercat_interface() -> Result<String, anyhow::Error> {
loop {
match discover_ethercat_interface().await {
Ok(interface) => {
tracing::info!("Found EtherCAT Interface at: {}", interface);
return Ok(interface);
}
})?;
Err(e) => {
tracing::warn!("No working interface found: {}. Retrying...", e);
Timer::after(Duration::from_secs(1)).await;
}
}
}
}
Ok(())
// Returns a Future to the interface
pub fn start_interface_discovery() -> Task<Result<String, anyhow::Error>> {
smol::spawn(find_ethercat_interface())
}
+1
View File
@@ -1,3 +1,4 @@
pub mod config;
pub mod ethercat_discovery_info;
pub mod init;
pub mod setup;
+4 -3
View File
@@ -52,6 +52,7 @@ 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")))]
@@ -85,13 +86,13 @@ pub async fn setup_loop(interface: &str, app_state: Arc<AppState>) -> Result<(),
// Default 30_000us
pdu: Duration::from_micros(30_000),
// Default 10ms
eeprom: Duration::from_millis(100),
eeprom: Duration::from_millis(10),
// Default 0ms
wait_loop_delay: Duration::from_millis(0),
// Default 100ms
mailbox_echo: Duration::from_millis(1000),
mailbox_echo: Duration::from_millis(100),
// Default 1000ms
mailbox_response: Duration::from_millis(10000),
mailbox_response: Duration::from_millis(1000),
},
MainDeviceConfig {
// Default RetryBehaviour::None
+114 -69
View File
@@ -1,87 +1,87 @@
use crate::app_state::AppState;
use bitvec::prelude::*;
use control_core::machines::connection::MachineConnection;
use control_core::realtime::{set_core_affinity, set_realtime_priority};
use std::sync::Arc;
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 tracing::{instrument, trace_span};
pub fn init_loop(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
// 300 us loop cycle target
pub fn start_loop_thread(
app_state: Arc<AppState>,
cycle_target: Duration,
) -> Result<std::thread::JoinHandle<()>, std::io::Error> {
// Start control loop
std::thread::Builder::new()
let res = std::thread::Builder::new()
.name("loop".to_owned())
.spawn(move || {
let rt = smol::LocalExecutor::new();
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);
// Set core affinity to third core
let _ = set_core_affinity(2);
// Set the thread to real-time priority
#[cfg(not(feature = "development-build"))]
if let Err(e) = set_realtime_priority() {
tracing::error!(
"[{}::init_loop] Failed to set real-time priority \n{:?}",
"[{}::init_loop] Failed to set thread to real-time priority \n{:?}",
module_path!(),
e
);
} else {
tracing::info!(
"[{}::init_loop] Real-time priority set successfully",
"[{}::init_loop] Real-time priority set successfully for current thread",
module_path!()
);
}
loop {
let res = smol::block_on(rt.run(async { loop_once(app_state.clone()).await }));
let rt = smol::LocalExecutor::new();
// Instead of creating a NEW async task every iteration of our realtime txrx loop, create it ONCE
smol::block_on(async {
rt.run(async {
loop {
if let Err(e) = loop_once(app_state.clone(), &sleeper, cycle_target).await {
tracing::error!("Loop failed\n{:?}", e);
break;
}
}
})
.await;
});
if let Err(err) = res {
tracing::error!("Loop failed\n{:?}", err);
break;
}
if let Some(last_loop_start) = app_state
.performance_metrics
.read_arc_blocking()
.last_loop_start
{
tracing::info!("Failing Loop Took {:?}", last_loop_start.elapsed());
}
// Exit the entire program if the Loop fails (gets restarted by systemd if running on NixOS)
// Exit the entire program if the Loop fails
// gets restarted by systemd if running on NixOS, or different distro wtih the same sysd service
std::process::exit(1);
})
.map_err(|e| {
anyhow::anyhow!(
"[{}::init_loop] Failed to spawn loop thread\n{:?}",
module_path!(),
e
)
})?;
Ok(())
});
return res;
}
#[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;
pub async fn copy_ethercat_inputs(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
//println!("copy_ethercat_intputs");
// 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(&ethercat_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
@@ -123,32 +123,14 @@ pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyh
})?;
}
}
Ok(())
}
// execute machines
{
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 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);
}
}
}
}
pub async fn copy_ethercat_outputs(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
// 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();
@@ -193,6 +175,69 @@ pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyh
})?;
}
}
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(())
}
+2 -2
View File
@@ -3,6 +3,8 @@ pub mod api;
pub mod buffer_tower_controller;
pub mod new;
#[cfg(not(feature = "mock-machine"))]
use crate::machines::{MACHINE_BUFFER_V1, VENDOR_QITECH, winder2::Winder2};
use api::{Buffer1Namespace, BufferV1Events, LiveValuesEvent, ModeState, StateEvent};
use buffer_tower_controller::BufferTowerController;
use control_core::machines::connection::{CrossConnectableMachine, MachineCrossConnection};
@@ -18,8 +20,6 @@ use serde::{Deserialize, Serialize};
use smol::lock::RwLock;
use std::{sync::Weak, time::Instant};
use crate::machines::{MACHINE_BUFFER_V1, VENDOR_QITECH, winder2::Winder2};
#[derive(Debug, Machine)]
pub struct BufferV1 {
// controllers
+6 -1
View File
@@ -14,7 +14,10 @@ use ethercrab::{SubDevice, SubDeviceRef};
use smol::lock::RwLock;
pub mod aquapath1;
#[cfg(not(feature = "mock-machine"))]
pub mod buffer1;
pub mod extruder1;
pub mod laser;
pub mod mock;
@@ -26,9 +29,11 @@ pub const MACHINE_WINDER_V1: u16 = 0x0002;
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
pub const MACHINE_LASER_V1: u16 = 0x0006;
pub const MACHINE_MOCK: u16 = 0x0007;
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
pub const MACHINE_AQUAPATH_V1: u16 = 0x0009;
#[cfg(not(feature = "mock-machine"))]
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
async fn get_device_ident<
'maindevice,
'subdevices,
+12 -5
View File
@@ -1,12 +1,12 @@
#[cfg(feature = "mock-machine")]
use crate::machines::{extruder1::mock::ExtruderV2, mock::MockMachine};
use crate::machines::{extruder1::mock::ExtruderV2, mock::MockMachine, winder2::mock::Winder2};
#[cfg(not(feature = "mock-machine"))]
use crate::machines::extruder1::ExtruderV2;
use crate::machines::{
aquapath1::AquaPathV1, buffer1::BufferV1, laser::LaserMachine, winder2::Winder2,
aquapath1::AquaPathV1, buffer1::BufferV1, extruder1::ExtruderV2, laser::LaserMachine,
winder2::Winder2,
};
use control_core::machines::registry::MachineRegistry;
use lazy_static::lazy_static;
@@ -14,11 +14,18 @@ lazy_static! {
pub static ref MACHINE_REGISTRY: MachineRegistry = {
let mut mc = MachineRegistry::new();
mc.register::<Winder2>(Winder2::MACHINE_IDENTIFICATION);
mc.register::<LaserMachine>(LaserMachine::MACHINE_IDENTIFICATION);
mc.register::<ExtruderV2>(ExtruderV2::MACHINE_IDENTIFICATION);
#[cfg(feature = "mock-machine")]
mc.register::<MockMachine>(MockMachine::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<LaserMachine>(LaserMachine::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<BufferV1>(BufferV1::MACHINE_IDENTIFICATION);
#[cfg(not(feature = "mock-machine"))]
mc.register::<AquaPathV1>(AquaPathV1::MACHINE_IDENTIFICATION);
mc
};
+4
View File
@@ -1,7 +1,11 @@
#[cfg(not(feature = "mock-machine"))]
use super::Winder2;
#[cfg(not(feature = "mock-machine"))]
use control_core::machines::new::MachineAct;
#[cfg(not(feature = "mock-machine"))]
use std::time::{Duration, Instant};
#[cfg(not(feature = "mock-machine"))]
impl MachineAct for Winder2 {
fn act(&mut self, now: Instant) {
// sync the spool speed
+66 -32
View File
@@ -1,31 +1,63 @@
use super::puller_speed_controller::GearRatio;
use super::{Winder2, Winder2Mode, puller_speed_controller::PullerRegulationMode};
use control_core::{
machines::{
api::MachineApi, connection::MachineCrossConnectionState,
identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
#[cfg(feature = "mock-machine")]
mod winder2_imports {
pub use super::super::Winder2Mode;
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use serde::Deserialize;
pub use serde::Serialize;
pub use std::{sync::Arc, time::Duration};
pub use control_core::{
machines::{
connection::MachineCrossConnectionState, identification::MachineIdentificationUnique,
},
},
};
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
},
};
pub use control_core_derive::BuildEvent;
pub use smol::lock::Mutex;
pub use tracing::instrument;
}
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::lock::Mutex;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use tracing::instrument;
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use super::super::{Winder2, Winder2Mode};
pub use control_core::{
machines::{
api::MachineApi, connection::MachineCrossConnectionState,
identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
},
};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub use control_core_derive::BuildEvent;
pub use serde::{Deserialize, Serialize};
pub use serde_json::Value;
pub use smol::lock::Mutex;
pub use std::{
sync::Arc,
time::{Duration, Instant},
};
pub use tracing::instrument;
}
pub use winder2_imports::*;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum Mode {
#[default]
Standby,
Hold,
Pull,
@@ -55,7 +87,7 @@ impl From<Mode> for Winder2Mode {
}
#[derive(Deserialize, Serialize)]
enum Mutation {
pub enum Mutation {
// Traverse
/// Position in mm from home point
SetTraverseLimitOuter(f64),
@@ -149,7 +181,7 @@ pub struct StateEvent {
pub connected_machine_state: MachineCrossConnectionState,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct TraverseState {
/// min position in mm
pub limit_inner: f64,
@@ -183,7 +215,7 @@ pub struct TraverseState {
pub can_go_home: bool,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct PullerState {
/// regulation type
pub regulation: PullerRegulationMode,
@@ -197,20 +229,21 @@ pub struct PullerState {
pub gear_ratio: GearRatio,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub enum SpoolAutomaticActionMode {
#[default]
NoAction,
Pull,
Hold,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolAutomaticActionState {
pub spool_required_meters: f64,
pub spool_automatic_action_mode: SpoolAutomaticActionMode,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct ModeState {
/// mode
pub mode: Mode,
@@ -218,13 +251,13 @@ pub struct ModeState {
pub can_wind: bool,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct TensionArmState {
/// is zeroed
pub zeroed: bool,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolSpeedControllerState {
/// regulation mode
pub regulation_mode: super::spool_speed_controller::SpoolSpeedControllerType,
@@ -286,6 +319,7 @@ impl CacheableEvents<Self> for Winder2Events {
}
}
#[cfg(not(feature = "mock-machine"))]
impl MachineApi for Winder2 {
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
+26 -19
View File
@@ -1,26 +1,33 @@
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::puller_speed_controller::PullerRegulationMode;
use crate::machines::buffer1::BufferV1;
pub use super::super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
pub use crate::machines::buffer1::BufferV1;
use super::puller_speed_controller::PullerRegulationMode;
use super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
use api::{
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode, SpoolAutomaticActionState,
SpoolSpeedControllerState, StateEvent, TensionArmState, TraverseState, Winder2Events,
};
use control_core::socketio::event::BuildEvent;
use control_core::{
machines::identification::MachineIdentificationUnique,
socketio::namespace::NamespaceCacheingLogic, uom_extensions::velocity::meter_per_minute,
};
pub use api::{
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode,
SpoolAutomaticActionState, SpoolSpeedControllerState, StateEvent, TensionArmState,
TraverseState, Winder2Events,
};
pub use control_core::socketio::event::BuildEvent;
pub use control_core::{
machines::identification::MachineIdentificationUnique,
socketio::namespace::NamespaceCacheingLogic, uom_extensions::velocity::meter_per_minute,
};
pub use std::time::Instant;
pub use uom::si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
length::{meter, millimeter},
};
}
use uom::si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
length::{meter, millimeter},
};
#[cfg(not(feature = "mock-machine"))]
pub use winder2_imports::*;
#[cfg(not(feature = "mock-machine"))]
impl Winder2 {
/// Implement Spool
/// called by `act`
+13
View File
@@ -0,0 +1,13 @@
use crate::machines::winder2::mock::Winder2;
use control_core::machines::new::MachineAct;
use std::time::{Duration, Instant};
impl MachineAct for Winder2 {
fn act(&mut self, now: Instant) {
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
}
}
}
+67
View File
@@ -0,0 +1,67 @@
use std::{sync::Arc, time::Instant};
use control_core::{machines::api::MachineApi, socketio::namespace::Namespace};
use serde_json::Value;
use smol::lock::Mutex;
use crate::machines::winder2::api::Mutation;
use super::Winder2;
impl MachineApi for Winder2 {
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
match mutation {
Mutation::EnableTraverseLaserpointer(enable) => self.set_laser(enable),
Mutation::SetMode(mode) => self.set_mode(&mode.into()),
Mutation::SetTraverseLimitOuter(limit) => self.traverse_set_limit_outer(limit),
Mutation::SetTraverseLimitInner(limit) => self.traverse_set_limit_inner(limit),
Mutation::SetTraverseStepSize(size) => self.traverse_set_step_size(size),
Mutation::SetTraversePadding(padding) => self.traverse_set_padding(padding),
Mutation::GotoTraverseLimitOuter => self.traverse_goto_limit_outer(),
Mutation::GotoTraverseLimitInner => self.traverse_goto_limit_inner(),
Mutation::GotoTraverseHome => self.traverse_goto_home(),
Mutation::SetPullerRegulationMode(regulation) => self.puller_set_regulation(regulation),
Mutation::SetPullerTargetSpeed(value) => self.puller_set_target_speed(value),
Mutation::SetPullerTargetDiameter(_) => todo!(),
Mutation::SetPullerForward(value) => self.puller_set_forward(value),
Mutation::SetPullerGearRatio(gear_ratio) => self.puller_set_gear_ratio(gear_ratio),
Mutation::SetSpoolRegulationMode(mode) => self.spool_set_regulation_mode(mode),
Mutation::SetSpoolMinMaxMinSpeed(speed) => self.spool_set_minmax_min_speed(speed),
Mutation::SetSpoolMinMaxMaxSpeed(speed) => self.spool_set_minmax_max_speed(speed),
Mutation::SetSpoolForward(value) => self.spool_set_forward(value),
Mutation::SetSpoolAdaptiveTensionTarget(value) => {
self.spool_set_adaptive_tension_target(value)
}
Mutation::SetSpoolAdaptiveRadiusLearningRate(value) => {
self.spool_set_adaptive_radius_learning_rate(value)
}
Mutation::SetSpoolAdaptiveMaxSpeedMultiplier(value) => {
self.spool_set_adaptive_max_speed_multiplier(value)
}
Mutation::SetSpoolAdaptiveAccelerationFactor(value) => {
self.spool_set_adaptive_acceleration_factor(value)
}
Mutation::SetSpoolAdaptiveDeaccelerationUrgencyMultiplier(value) => {
self.spool_set_adaptive_deacceleration_urgency_multiplier(value)
}
Mutation::SetSpoolAutomaticRequiredMeters(meters) => {
self.set_spool_automatic_required_meters(meters)
}
Mutation::SetSpoolAutomaticAction(mode) => self.set_spool_automatic_mode(mode),
Mutation::ResetSpoolProgress => self.stop_or_pull_spool_reset(Instant::now()),
Mutation::ZeroTensionArmAngle => self.tension_arm_zero(),
Mutation::SetConnectedMachine(machine_identification_unique) => {
self.set_connected_buffer(machine_identification_unique)
}
Mutation::DisconnectMachine(machine_identification_unique) => {
self.disconnect_buffer(machine_identification_unique)
}
}
Ok(())
}
fn api_event_namespace(&mut self) -> Arc<Mutex<Namespace>> {
self.namespace.namespace.clone()
}
}
@@ -0,0 +1,242 @@
use std::time::Instant;
use crate::machines::winder2::Winder2Mode;
use crate::machines::winder2::api::LiveValuesEvent;
use crate::machines::winder2::api::{
ModeState, SpoolAutomaticActionMode, StateEvent, Winder2Events,
};
use crate::machines::winder2::puller_speed_controller::{GearRatio, PullerRegulationMode};
use crate::machines::winder2::spool_speed_controller::SpoolSpeedControllerType;
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH};
use control_core::machines::identification::{MachineIdentification, MachineIdentificationUnique};
use control_core::socketio::event::BuildEvent;
use control_core::socketio::namespace::NamespaceCacheingLogic;
use super::Winder2;
impl Winder2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_WINDER_V1,
};
pub fn stop_or_pull_spool_reset(&mut self, now: Instant) {}
/// Implement Mode
pub fn set_mode(&mut self, mode: &Winder2Mode) {
self.mode_state = ModeState {
mode: mode.clone().into(),
can_wind: true,
};
self.emit_state();
}
/// Implement Traverse
pub fn set_laser(&mut self, value: bool) {
self.traverse_state.laserpointer = value;
self.emit_state();
}
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
self.traverse_state.limit_inner = limit;
self.emit_state();
}
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
self.traverse_state.limit_outer = limit;
self.emit_state();
}
pub fn traverse_set_step_size(&mut self, step_size: f64) {
self.traverse_state.step_size = step_size;
self.emit_state();
}
pub fn traverse_set_padding(&mut self, padding: f64) {
self.traverse_state.padding = padding;
self.emit_state();
}
pub fn traverse_goto_limit_inner(&mut self) {
self.traverse_state.position_in = self.traverse_state.limit_inner;
self.emit_state();
}
pub fn traverse_goto_limit_outer(&mut self) {
self.traverse_state.position_in = self.traverse_state.limit_outer;
self.emit_state();
}
pub fn traverse_goto_home(&mut self) {
self.traverse_state.position_in = 0.0;
self.emit_state();
}
pub fn emit_live_values(&mut self) {
let event = LiveValuesEvent {
traverse_position: Some(0.0),
puller_speed: 0.0,
spool_rpm: 0.0,
tension_arm_angle: 0.0,
spool_progress: 0.0,
};
let event = event.build();
self.namespace.emit(Winder2Events::LiveValues(event));
}
pub fn build_state_event(&mut self) -> StateEvent {
StateEvent {
is_default_state: self.is_default_state,
traverse_state: self.traverse_state.clone(),
puller_state: self.puller_state.clone(),
spool_automatic_action_state: self.spool_automatic_action_state.clone(),
mode_state: self.mode_state.clone(),
tension_arm_state: self.tension_arm_state.clone(),
spool_speed_controller_state: self.spool_speed_controller_state.clone(),
connected_machine_state: self.connected_machine_state.clone(),
}
}
pub fn emit_state(&mut self) {
let state = self.build_state_event();
let event = state.build();
self.namespace.emit(Winder2Events::State(event));
}
/// Apply the mode changes to the spool
///
/// It contains a transition matrix for atomic changes.
/// It will set [`Self::spool_mode`]
fn set_traverse_mode(&mut self, mode: &Winder2Mode) {
self.mode_state = ModeState {
mode: crate::machines::winder2::api::Mode::Standby,
can_wind: true,
};
self.emit_state();
}
/// Implement Tension Arm
pub fn tension_arm_zero(&mut self) {
self.tension_arm_state.zeroed = true;
self.emit_state();
}
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
self.spool_automatic_action_state.spool_required_meters = meters;
self.emit_state();
}
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
self.spool_automatic_action_state
.spool_automatic_action_mode = mode;
self.emit_state();
}
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
self.puller_state.regulation = puller_regulation_mode;
self.emit_state();
}
/// Set target speed in m/min
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
self.puller_state.target_speed = target_speed;
self.emit_state();
}
/// Set target diameter in mm
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
self.puller_state.target_diameter = target_diameter;
self.emit_state();
}
/// Set forward direction
pub fn puller_set_forward(&mut self, forward: bool) {
self.puller_state.forward = forward;
self.emit_state();
}
/// Set gear ratio for winding speed
pub fn puller_set_gear_ratio(&mut self, gear_ratio: GearRatio) {
self.puller_state.gear_ratio = gear_ratio;
self.emit_state();
}
// Spool Speed Controller API methods
pub fn spool_set_regulation_mode(&mut self, regulation_mode: SpoolSpeedControllerType) {
self.spool_speed_controller_state.regulation_mode = regulation_mode;
self.emit_state();
}
/// Set minimum speed for minmax mode in RPM
pub fn spool_set_minmax_min_speed(&mut self, min_speed_rpm: f64) {
self.spool_speed_controller_state.minmax_min_speed = min_speed_rpm;
self.emit_state();
}
/// Set maximum speed for minmax mode in RPM
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
self.spool_speed_controller_state.minmax_max_speed = max_speed_rpm;
self.emit_state();
}
/// Set tension target for adaptive mode (0.0-1.0)
pub fn spool_set_adaptive_tension_target(&mut self, tension_target: f64) {
self.spool_speed_controller_state.adaptive_tension_target = tension_target;
self.emit_state();
}
/// Set radius learning rate for adaptive mode
pub fn spool_set_adaptive_radius_learning_rate(&mut self, radius_learning_rate: f64) {
self.spool_speed_controller_state
.adaptive_radius_learning_rate = radius_learning_rate;
self.emit_state();
}
/// Set max speed multiplier for adaptive mode
pub fn spool_set_adaptive_max_speed_multiplier(&mut self, max_speed_multiplier: f64) {
self.spool_speed_controller_state
.adaptive_max_speed_multiplier = max_speed_multiplier;
self.emit_state();
}
/// Set acceleration factor for adaptive mode
pub fn spool_set_adaptive_acceleration_factor(&mut self, acceleration_factor: f64) {
self.spool_speed_controller_state
.adaptive_acceleration_factor = acceleration_factor;
self.emit_state();
}
/// Set deacceleration urgency multiplier for adaptive mode
pub fn spool_set_adaptive_deacceleration_urgency_multiplier(
&mut self,
deacceleration_urgency_multiplier: f64,
) {
self.spool_speed_controller_state
.adaptive_deacceleration_urgency_multiplier = deacceleration_urgency_multiplier;
self.emit_state();
}
/// Set forward rotation direction
pub fn spool_set_forward(&mut self, forward: bool) {
self.spool_speed_controller_state.forward = forward;
self.emit_state();
}
/// implement machine connection
/// set connected buffer
pub fn set_connected_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
self.emit_state();
}
/// disconnect buffer
pub fn disconnect_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
self.emit_state();
}
}
+44
View File
@@ -0,0 +1,44 @@
use std::time::Instant;
use control_core::machines::{
connection::MachineCrossConnectionState, identification::MachineIdentificationUnique,
};
use control_core_derive::Machine;
pub mod act;
pub mod api;
pub mod mock_emit;
pub mod new;
use crate::machines::winder2::api::Winder2Namespace;
use super::api::{
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState, TensionArmState,
TraverseState,
};
#[derive(Debug, Machine)]
pub struct Winder2 {
machine_identification_unique: MachineIdentificationUnique,
namespace: Winder2Namespace,
last_measurement_emit: Instant,
pub is_default_state: bool,
/// traverse state
pub traverse_state: TraverseState,
/// puller state
pub puller_state: PullerState,
/// spool automatic action state and progress
pub spool_automatic_action_state: SpoolAutomaticActionState,
/// mode state
pub mode_state: ModeState,
/// tension arm state
pub tension_arm_state: TensionArmState,
/// spool speed controller state
pub spool_speed_controller_state: SpoolSpeedControllerState,
/// connected machine state
pub connected_machine_state: MachineCrossConnectionState,
}
impl std::fmt::Display for Winder2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Winder2")
}
}
+53
View File
@@ -0,0 +1,53 @@
use crate::machines::winder2::{
api::{
ModeState, PullerState, SpoolAutomaticActionState, SpoolSpeedControllerState,
TensionArmState, TraverseState, Winder2Namespace,
},
mock::Winder2,
};
use control_core::machines::connection::MachineCrossConnectionState;
impl control_core::machines::new::MachineNewTrait for Winder2 {
fn new(
params: &control_core::machines::new::MachineNewParams<'_, '_, '_, '_, '_, '_, '_>,
) -> Result<Self, anyhow::Error>
where
Self: Sized,
{
// Mock machine can work with either Serial or Ethercat hardware
// For the mock machine, we don't need to actually use the hardware
// We just validate that we have the expected hardware type
match params.hardware {
control_core::machines::new::MachineNewHardware::Serial(_) => {
// For serial mode, we could potentially use the serial device if needed
// but for a mock machine, we'll just note it and proceed
}
control_core::machines::new::MachineNewHardware::Ethercat(_) => {
// For ethercat mode, we could potentially use the ethercat devices
// but for a mock machine, we'll just note it and proceed
}
}
let now = std::time::Instant::now();
let mut extruder_mock_machine = Self {
machine_identification_unique: params.get_machine_identification_unique(),
namespace: Winder2Namespace {
namespace: params.namespace.clone(),
},
last_measurement_emit: now,
is_default_state: true,
traverse_state: TraverseState::default(),
puller_state: PullerState::default(),
spool_automatic_action_state: SpoolAutomaticActionState::default(),
mode_state: ModeState::default(),
tension_arm_state: TensionArmState::default(),
spool_speed_controller_state: SpoolSpeedControllerState::default(),
connected_machine_state: MachineCrossConnectionState::default(),
};
extruder_mock_machine.emit_state();
Ok(extruder_mock_machine)
}
}
+56 -29
View File
@@ -11,37 +11,47 @@ pub mod spool_speed_controller;
pub mod tension_arm;
pub mod traverse_controller;
use std::{fmt::Debug, sync::Weak, time::Instant};
#[cfg(feature = "mock-machine")]
pub mod mock;
use api::{SpoolAutomaticActionMode, Winder2Namespace};
use control_core::{
converters::angular_step_converter::AngularStepConverter,
machines::{
connection::{CrossConnectableMachine, MachineCrossConnection},
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
};
use control_core_derive::Machine;
use ethercat_hal::io::{
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1,
};
use puller_speed_controller::PullerSpeedController;
use smol::lock::RwLock;
use spool_speed_controller::SpoolSpeedController;
use tension_arm::TensionArm;
use traverse_controller::TraverseController;
use uom::{
ConstZero,
si::{
f64::Length,
length::{meter, millimeter},
velocity::meter_per_second,
},
};
#[cfg(feature = "mock-machine")]
mod winder2_imports {
pub use super::api::SpoolAutomaticActionMode;
pub use std::time::Instant;
pub use uom::si::f64::Length;
}
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH, buffer1::BufferV1};
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::api::SpoolAutomaticActionMode;
pub use super::api::Winder2Namespace;
pub use super::puller_speed_controller::PullerSpeedController;
pub use super::spool_speed_controller::SpoolSpeedController;
pub use super::tension_arm::TensionArm;
pub use super::traverse_controller::TraverseController;
pub use control_core::{
converters::angular_step_converter::AngularStepConverter,
machines::{
connection::{CrossConnectableMachine, MachineCrossConnection},
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
};
pub use control_core_derive::Machine;
pub use ethercat_hal::io::{
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1,
};
pub use smol::lock::RwLock;
pub use std::{fmt::Debug, sync::Weak, time::Instant};
pub use uom::si::f64::Length;
pub use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH, buffer1::BufferV1};
pub use uom::ConstZero;
pub use uom::si::{length::meter, length::millimeter, velocity::meter_per_second};
}
pub use winder2_imports::*;
#[derive(Debug)]
pub struct SpoolAutomaticAction {
@@ -51,6 +61,18 @@ pub struct SpoolAutomaticAction {
pub mode: SpoolAutomaticActionMode,
}
impl Default for SpoolAutomaticAction {
fn default() -> Self {
SpoolAutomaticAction {
progress: Length::new::<uom::si::length::meter>(0.0),
progress_last_check: Instant::now(),
target_length: Length::new::<uom::si::length::meter>(0.0),
mode: SpoolAutomaticActionMode::default(),
}
}
}
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug, Machine)]
pub struct Winder2 {
// drivers
@@ -96,12 +118,14 @@ pub struct Winder2 {
emitted_default_state: bool,
}
#[cfg(not(feature = "mock-machine"))]
impl CrossConnectableMachine<Winder2, BufferV1> for Winder2 {
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<Winder2, BufferV1> {
&mut self.connected_buffer
}
}
#[cfg(not(feature = "mock-machine"))]
impl Winder2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
@@ -277,6 +301,7 @@ impl Winder2 {
pub fn calculate_spool_auto_progress_(&mut self, now: Instant) {
// Calculate time elapsed since last progress check (in minutes)
let dt = now
.duration_since(self.spool_automatic_action.progress_last_check)
.as_secs_f64();
@@ -368,12 +393,14 @@ impl From<Winder2Mode> for PullerMode {
}
}
#[cfg(not(feature = "mock-machine"))]
impl std::fmt::Display for Winder2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Winder2")
}
}
#[cfg(not(feature = "mock-machine"))]
#[cfg(test)]
mod tests {
use super::*;
+54 -41
View File
@@ -1,50 +1,63 @@
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::api::Winder2Namespace;
pub use super::super::tension_arm::TensionArm;
pub use super::super::{Winder2, Winder2Mode};
pub use crate::machines::winder2::puller_speed_controller::PullerSpeedController;
pub use crate::machines::winder2::spool_speed_controller::SpoolSpeedController;
pub use crate::machines::winder2::traverse_controller::TraverseController;
pub use anyhow::Error;
pub use control_core::converters::angular_step_converter::AngularStepConverter;
pub use control_core::converters::linear_step_converter::LinearStepConverter;
pub use control_core::machines::connection::MachineCrossConnection;
pub use control_core::machines::new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
use super::api::Winder2Namespace;
use super::tension_arm::TensionArm;
use super::{Winder2, Winder2Mode};
pub use control_core::uom_extensions::velocity::meter_per_minute;
pub use ethercat_hal::coe::ConfigurableDevice;
pub use ethercat_hal::devices::ek1100::EK1100;
pub use ethercat_hal::devices::el2002::{EL2002, EL2002_IDENTITY_B, EL2002Port};
pub use ethercat_hal::devices::el7031::coe::EL7031Configuration;
pub use ethercat_hal::devices::el7031::pdo::EL7031PredefinedPdoAssignment;
pub use ethercat_hal::devices::el7031::{
EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B, EL7031DigitalInputPort, EL7031StepperPort,
};
pub use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
pub use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
pub use ethercat_hal::devices::el7031_0030::{
self, EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030AnalogInputPort,
EL7031_0030StepperPort,
};
pub use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration;
pub use ethercat_hal::devices::el7041_0052::{
EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port,
};
pub use ethercat_hal::devices::{ek1100::EK1100_IDENTITY_A, el2002::EL2002_IDENTITY_A};
pub use ethercat_hal::io::analog_input::AnalogInput;
pub use ethercat_hal::io::digital_input::DigitalInput;
pub use ethercat_hal::io::digital_output::DigitalOutput;
pub use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
pub use ethercat_hal::shared_config;
pub use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
pub use std::time::Instant;
pub use uom::ConstZero;
pub use uom::si::f64::{Length, Velocity};
pub use uom::si::length::{centimeter, meter, millimeter};
}
#[cfg(not(feature = "mock-machine"))]
pub use winder2_imports::*;
#[cfg(not(feature = "mock-machine"))]
use crate::machines::get_ethercat_device;
use crate::machines::winder2::puller_speed_controller::PullerSpeedController;
use crate::machines::winder2::spool_speed_controller::SpoolSpeedController;
use crate::machines::winder2::traverse_controller::TraverseController;
use anyhow::Error;
use control_core::converters::angular_step_converter::AngularStepConverter;
use control_core::converters::linear_step_converter::LinearStepConverter;
use control_core::machines::connection::MachineCrossConnection;
use control_core::machines::new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, validate_no_role_dublicates,
validate_same_machine_identification_unique,
};
use control_core::uom_extensions::velocity::meter_per_minute;
use ethercat_hal::coe::ConfigurableDevice;
use ethercat_hal::devices::ek1100::EK1100;
use ethercat_hal::devices::el2002::{EL2002, EL2002_IDENTITY_B, EL2002Port};
use ethercat_hal::devices::el7031::coe::EL7031Configuration;
use ethercat_hal::devices::el7031::pdo::EL7031PredefinedPdoAssignment;
use ethercat_hal::devices::el7031::{
EL7031, EL7031_IDENTITY_A, EL7031_IDENTITY_B, EL7031DigitalInputPort, EL7031StepperPort,
};
use ethercat_hal::devices::el7031_0030::coe::EL7031_0030Configuration;
use ethercat_hal::devices::el7031_0030::pdo::EL7031_0030PredefinedPdoAssignment;
use ethercat_hal::devices::el7031_0030::{
self, EL7031_0030, EL7031_0030_IDENTITY_A, EL7031_0030AnalogInputPort, EL7031_0030StepperPort,
};
use ethercat_hal::devices::el7041_0052::coe::EL7041_0052Configuration;
use ethercat_hal::devices::el7041_0052::{EL7041_0052, EL7041_0052_IDENTITY_A, EL7041_0052Port};
use ethercat_hal::devices::{ek1100::EK1100_IDENTITY_A, el2002::EL2002_IDENTITY_A};
use ethercat_hal::io::analog_input::AnalogInput;
use ethercat_hal::io::digital_input::DigitalInput;
use ethercat_hal::io::digital_output::DigitalOutput;
use ethercat_hal::io::stepper_velocity_el70x1::StepperVelocityEL70x1;
use ethercat_hal::shared_config;
use ethercat_hal::shared_config::el70x1::{EL70x1OperationMode, StmMotorConfiguration};
use uom::ConstZero;
use uom::si::f64::{Length, Velocity};
use uom::si::length::{centimeter, meter, millimeter};
#[cfg(not(feature = "mock-machine"))]
impl MachineNewTrait for Winder2 {
fn new<'maindevice>(params: &MachineNewParams) -> Result<Self, Error> {
// validate general stuff
let device_identification = params.device_group.to_vec();
validate_same_machine_identification_unique(&device_identification)?;
@@ -150,8 +150,9 @@ impl PullerSpeedController {
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum PullerRegulationMode {
#[default]
Speed,
Diameter,
}
@@ -10,8 +10,9 @@ use serde::{Deserialize, Serialize};
use std::time::Instant;
use uom::si::f64::AngularVelocity;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum SpoolSpeedControllerType {
#[default]
Adaptive,
MinMax,
}
+57 -77
View File
@@ -1,106 +1,86 @@
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
use app_state::AppState;
use r#loop::init_loop;
use futures::{FutureExt, select};
#[cfg(feature = "mock-machine")]
use mock::init::init_mock;
use rest::init::init_api;
#[cfg(not(feature = "mock-machine"))]
use serial::init::init_serial;
use std::sync::Arc;
use crate::panic::init_panic_handling;
use crate::socketio::queue::init_socketio_queue;
#[cfg(feature = "mock-machine")]
pub mod mock;
use app_state::AppState;
use ethercat::{
ethercat_discovery_info::{send_ethercat_discovering, send_ethercat_found},
init::start_interface_discovery,
setup::setup_loop,
};
use r#loop::start_loop_thread;
use panic::init_panic_handling;
use rest::init::start_api_thread;
use serial::init::{handle_serial_device_hotplug, start_serial_discovery};
use socketio::queue::start_socketio_queue;
use std::{sync::Arc, time::Duration};
pub mod app_state;
pub mod ethercat;
pub mod logging;
pub mod r#loop;
pub mod machines;
#[cfg(feature = "mock-machine")]
pub mod mock;
pub mod panic;
pub mod performance_metrics;
pub mod rest;
pub mod serial;
pub mod socketio;
use crate::ethercat::init::init_ethercat;
fn main() {
logging::init_tracing();
tracing::info!("Tracing initialized successfully");
init_panic_handling();
#[cfg(feature = "memory-locking")]
if let Err(e) = control_core::realtime::lock_memory() {
tracing::error!("[{}::main] Failed to lock memory: {:?}", module_path!(), e);
} else {
tracing::info!("[{}::main] Memory locked successfully", module_path!());
}
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(300);
let app_state = Arc::new(AppState::new());
// Spawn init thread
let init_thread = std::thread::Builder::new()
.name("init".to_string())
.spawn({
move || {
#[cfg(feature = "dhat-heap")]
init_dhat_heap_profiling();
#[cfg(feature = "mock-machine")]
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
init_socketio_queue(app_state.clone());
init_api(app_state.clone()).expect("Failed to initialize API");
init_loop(app_state.clone()).expect("Failed to initialize loop");
#[cfg(feature = "mock-machine")]
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
#[cfg(not(feature = "mock-machine"))]
init_serial(app_state.clone()).expect("Failed to initialize Serial");
#[cfg(not(feature = "mock-machine"))]
init_ethercat(app_state).expect("Failed to initialize EtherCAT");
}
})
.expect("Failed to spawn init thread");
// Wait for init thread to complete
init_thread.join().expect("Init thread panicked");
// Keep the main thread alive indefinitely
// The program should only exit via panic handling or external signals
loop {
// better way to "sleep", uses basically
std::thread::park();
}
}
#[cfg(feature = "dhat-heap")]
fn init_dhat_heap_profiling() {
use std::{process::exit, time::Duration};
let profiler = dhat::Profiler::new_heap();
let _ = start_api_thread(app_state.clone());
let mut socketio_fut = start_socketio_queue(app_state.clone()).fuse();
let mut ethercat_fut = start_interface_discovery().fuse();
let mut serial_fut = start_serial_discovery().fuse();
let _loop_thread = start_loop_thread(app_state.clone(), CYCLE_TARGET_TIME);
smol::block_on(async { send_ethercat_discovering(app_state.clone()).await });
smol::block_on(async {
let dhat_analysis_time = std::time::Duration::from_secs(60);
let dhat_analysis_start = std::time::Instant::now();
tracing::info!(
"Starting dhat heap profiler for {} seconds",
dhat_analysis_time.as_secs()
);
loop {
use std::time::Duration;
smol::Timer::after(Duration::from_secs(1)).await;
// lets the async runtime decide which future to run next
select! {
res = ethercat_fut => {
tracing::info!("EtherCAT task finished: {:?}", res);
match res {
Ok(interface) =>
{
tracing::info!("Calling setup_loop");
let res = setup_loop(&interface, app_state.clone()).await;
match res {
Ok(_) => tracing::info!("Successfully initialized EtherCAT network"),
Err(e) => {
tracing::error!(
"[{}::main] Failed to initialize EtherCAT network \n{:?}",
module_path!(),
e
);
}
}
send_ethercat_found(app_state.clone(), &interface).await;
},
Err(_) => (),
};
// if `dhat-heap` is enabled, we will analyze the heap for 60 seconds and then exit
if dhat_analysis_start.elapsed() > dhat_analysis_time {
drop(profiler);
exit(0)
},
res = serial_fut => {
let _ = handle_serial_device_hotplug(app_state.clone(),res).await;
serial_fut = start_serial_discovery().fuse();
},
res = socketio_fut => {
// In theory it should never finish
tracing::warn!("SocketIO task finished: {:?}", res);
},
}
}
})
});
}
+35 -3
View File
@@ -1,5 +1,6 @@
#[cfg(feature = "mock-machine")]
use crate::serial::devices::extruder_mock::ExtruderMockSerialDevice;
use crate::serial::devices::winder_mock::WinderMockSerialDevice;
#[cfg(feature = "mock-machine")]
use {
crate::app_state::AppState,
@@ -12,16 +13,15 @@ use {
},
std::sync::Arc,
};
#[cfg(feature = "mock-machine")]
pub fn init_mock(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
// For mock devices, we need to manually create and add them to the machine manager
// since they won't be detected by the serial detection loop
return smol::block_on(async {
// Create a mock serial device manually
let (device_thread_panic_tx, _device_thread_panic_rx) = smol::channel::unbounded();
let serial_params = SerialDeviceNewParams {
path: "/dev/mock-serial".to_string(),
device_thread_panic_tx,
};
// Create the mock serial device
@@ -57,7 +57,39 @@ pub fn init_mock(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
}
};
match ExtruderMockSerialDevice::new_serial(&serial_params) {
let _ = match ExtruderMockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
{
let mut machine_guard = app_state.machines.write().await;
machine_guard.add_serial_device(
&device_identification,
mock_serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
);
}
// Notify clients via socketio about the new machine
let app_state_event = app_state.clone();
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
Ok::<(), anyhow::Error>(())
}
Err(e) => {
tracing::error!("Failed to create extruder mock serial device: {}", e);
return Err(e);
}
};
match WinderMockSerialDevice::new_serial(&serial_params) {
Ok((device_identification, mock_serial_device)) => {
// Add the mock device to the machine manager
{
+2 -2
View File
@@ -10,8 +10,8 @@ const METRICS_LOG_INTERVAL_SECS: u64 = 30; // Log every 30 seconds
pub struct EthercatPerformanceMetrics {
txrx_times: VecDeque<Duration>,
loop_times: VecDeque<Duration>,
last_loop_start: Option<Instant>,
last_log_time: Instant,
pub last_loop_start: Option<Instant>,
pub last_log_time: Instant,
}
impl Default for EthercatPerformanceMetrics {
+44 -52
View File
@@ -1,65 +1,57 @@
use super::handlers::machine_mutation::post_machine_mutate;
use super::handlers::write_machine_device_identification::post_write_machine_device_identification;
use crate::app_state::AppState;
use crate::socketio::init::init_socketio;
use anyhow::anyhow;
use anyhow::Result;
use axum::routing::post;
use std::sync::Arc;
use std::thread::JoinHandle;
use std::thread;
use tower_http::cors::CorsLayer;
use tower_http::trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::Level;
pub fn init_api(app_state: Arc<AppState>) -> Result<JoinHandle<()>, anyhow::Error> {
std::thread::Builder::new()
.name("api".to_string())
.spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.expect("Failed to create runtime");
use super::handlers::machine_mutation::post_machine_mutate;
use super::handlers::write_machine_device_identification::post_write_machine_device_identification;
use crate::app_state::AppState;
use crate::socketio::init::init_socketio;
rt.block_on(async {
// allow all CORS requests
let cors = CorsLayer::permissive();
async fn init_api(app_state: Arc<AppState>) -> Result<()> {
let cors = CorsLayer::permissive();
let socketio_layer = init_socketio(&app_state).await;
// creat socketio layer
let socketio_layer = init_socketio(&app_state).await;
let trace_layer = TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::DEBUG))
.on_request(DefaultOnRequest::new().level(Level::TRACE))
.on_response(DefaultOnResponse::new().level(Level::TRACE));
// Create a more detailed trace layer for HTTP requests
let trace_layer = TraceLayer::new_for_http()
.make_span_with(
DefaultMakeSpan::new()
.level(Level::DEBUG)
.include_headers(false),
)
.on_request(DefaultOnRequest::new().level(Level::TRACE))
.on_response(
DefaultOnResponse::new()
.level(Level::TRACE)
.include_headers(false),
);
let app = axum::Router::new()
.route(
"/api/v1/write_machine_device_identification",
post(post_write_machine_device_identification),
)
.route("/api/v1/machine/mutate", post(post_machine_mutate))
.layer(socketio_layer)
.layer(cors)
.layer(trace_layer)
.with_state(app_state);
// make axum server to serve the data on /ethercat
let app = axum::Router::new()
.route(
"/api/v1/write_machine_device_identification",
post(post_write_machine_device_identification),
)
.route("/api/v1/machine/mutate", post(post_machine_mutate))
.layer(socketio_layer)
.layer(cors)
.layer(trace_layer)
.with_state(app_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001")
.await
.expect("Failed to bind to port 3001");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001")
.await
.expect("Failed to bind to port 3001");
tracing::info!("HTTP server running on 0.0.0.0:3001");
tracing::info!("Starting HTTP server on 0.0.0.0:3001");
axum::serve(listener, app).await.expect("Failed to serve");
});
})
.map_err(|e| anyhow!("Failed to spawn API thread: {}", e))
axum::serve(listener, app)
.await
.map_err(|e| anyhow::anyhow!("Server error: {}", e))
}
/// Starts the API server in its own thread with a single-threaded Tokio runtime
pub fn start_api_thread(app_state: Arc<AppState>) -> std::thread::JoinHandle<()> {
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
if let Err(err) = rt.block_on(init_api(app_state)) {
eprintln!("API server exited with error: {err:?}");
}
})
}
+7 -21
View File
@@ -17,10 +17,7 @@ use control_core::{
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
modbus::{self, ModbusRequest, ModbusResponse},
serial::{
SerialDevice, SerialDeviceNew, SerialDeviceNewParams, panic::send_serial_device_panic,
serial_detection::SerialDeviceRemoval,
},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
};
use serialport::SerialPort;
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, StopBits};
@@ -83,7 +80,10 @@ impl From<LaserModbusRequsts> for modbus::ModbusRequest {
LaserModbusRequsts::ReadDiameter => Self {
slave_id: 1,
function_code: modbus::ModbusFunctionCode::ReadInputRegister,
data: vec![(0 >> 8) as u8, (0 & 0xFF) as u8],
data: vec![
0x00, 0x0E, // Start register = 0x000E
0x00, 0x03, // Read 3 registers (AvgDiameter, X, Y)
],
},
}
}
@@ -125,27 +125,13 @@ impl SerialDeviceNew for Laser {
path: params.path.clone(),
}));
// Spawn the device thread
let device_thread_panic_tx = params.device_thread_panic_tx.clone();
//// Spawn the device thread
let _self_clone = _self.clone();
let path = params.path.clone();
thread::Builder::new()
.name("laser".to_owned())
.spawn(move || {
send_serial_device_panic(path.clone(), device_thread_panic_tx.clone());
smol::block_on(async {
let process_result = Self::process(_self_clone).await;
let removal = match process_result {
Ok(_) => SerialDeviceRemoval::Disconnect(path),
Err(e) => SerialDeviceRemoval::Error(path, e),
};
// if the task exists we want to remove the device
device_thread_panic_tx
.send(removal)
.await
.expect("Failed to send device removal signal");
let _ = Self::process(_self_clone).await;
});
})?;
+2
View File
@@ -3,3 +3,5 @@ pub mod extruder_mock;
pub mod laser;
#[cfg(feature = "mock-machine")]
pub mod mock;
#[cfg(feature = "mock-machine")]
pub mod winder_mock;
@@ -0,0 +1,59 @@
use std::sync::Arc;
use control_core::{
helpers::hashing::{byte_folding_u16, hash_djb2},
machines::identification::{
DeviceHardwareIdentification, DeviceHardwareIdentificationSerial, DeviceIdentification,
DeviceMachineIdentification, MachineIdentification, MachineIdentificationUnique,
},
serial::{SerialDevice, SerialDeviceNew, SerialDeviceNewParams},
};
use smol::lock::RwLock;
use crate::machines::{MACHINE_WINDER_V1, VENDOR_QITECH};
/// Mock serial device for testing MockMachine
/// This provides a minimal SerialDevice implementation that doesn't require actual hardware
#[derive(Debug)]
pub struct WinderMockSerialDevice {
pub path: String,
}
impl SerialDevice for WinderMockSerialDevice {}
impl SerialDeviceNew for WinderMockSerialDevice {
fn new_serial(
params: &SerialDeviceNewParams,
) -> Result<(DeviceIdentification, Arc<RwLock<Self>>), anyhow::Error>
where
Self: Sized,
{
// Generate a unique serial number based on the path
let hash = hash_djb2(params.path.as_bytes());
let serial = byte_folding_u16(&hash.to_le_bytes());
let device_identification = DeviceIdentification {
device_machine_identification: Some(DeviceMachineIdentification {
machine_identification_unique: MachineIdentificationUnique {
machine_identification: MachineIdentification {
vendor: VENDOR_QITECH,
machine: MACHINE_WINDER_V1,
},
serial,
},
role: 0,
}),
device_hardware_identification: DeviceHardwareIdentification::Serial(
DeviceHardwareIdentificationSerial {
path: params.path.clone(),
},
),
};
let mock_serial_device = Arc::new(RwLock::new(WinderMockSerialDevice {
path: params.path.clone(),
}));
Ok((device_identification, mock_serial_device))
}
}
+135 -70
View File
@@ -1,74 +1,139 @@
use crate::socketio::main_namespace::MainNamespaceEvents;
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
use crate::{app_state::AppState, machines::registry::MACHINE_REGISTRY};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use std::{sync::Arc, thread, time::Duration};
use control_core::{
machines::identification::MachineIdentificationUnique,
serial::{
SerialDeviceIdentification, SerialDeviceNew, SerialDeviceNewParams,
serial_detection::SerialDetection,
},
socketio::namespace::NamespaceCacheingLogic,
};
use serialport::UsbPortInfo;
use smol::Task;
use std::{collections::HashMap, sync::Arc, time::Duration};
pub fn init_serial(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
let app_state_clone = app_state.clone();
use crate::{
app_state::AppState,
machines::{laser::LaserMachine, registry::MACHINE_REGISTRY},
socketio::main_namespace::{MainNamespaceEvents, machines_event::MachinesEventBuilder},
};
thread::Builder::new()
.name("SerialDetectionThread".to_owned())
.spawn(move || {
let app_state_clone = app_state_clone;
smol::block_on(async move {
let rt = smol::LocalExecutor::new();
rt.run(async {
loop {
let result = {
let mut serial_setup_guard = app_state_clone.serial_setup.write().await;
use super::devices::laser::Laser;
let mut port_result =
serial_setup_guard.serial_detection.check_ports().await;
let mut removed_signals = serial_setup_guard
.serial_detection
.check_remove_signals()
.await;
port_result.removed.append(&mut removed_signals);
port_result
};
if !result.added.is_empty() || !result.removed.is_empty() {
// sync serial device discovery to machine manager
{
let mut machine_guard = app_state_clone.machines.write().await;
// turn added devices into machines
for (device_identifiaction, device) in result.added {
machine_guard.add_serial_device(
&device_identifiaction,
device.clone(),
&MACHINE_REGISTRY,
app_state_clone.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
)
}
for device_identification in result.removed {
machine_guard.remove_serial_device(&device_identification)
}
}
// Notify client via socketio
let app_state_event = app_state.clone();
smol::block_on(async {
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
});
}
smol::Timer::after(Duration::from_millis(300)).await;
}
})
.await;
});
})
.expect("Failed to spawn SerialTxRxThread");
Ok(())
// Async function that runs until at least one serial device is found.
pub async fn find_serial() -> Option<HashMap<String, UsbPortInfo>> {
smol::Timer::after(Duration::from_secs(1)).await;
let devices = SerialDetection::detect_devices();
if !devices.is_empty() {
return Some(devices);
} else {
return None;
}
}
// Returns a smol::Task that resolves when atleast one device is found.
pub fn start_serial_discovery() -> Task<Option<HashMap<String, UsbPortInfo>>> {
smol::spawn(find_serial())
}
// This function handles a disconnection and or reconnection of Laser
// You can easily rewrite it when we have more serialdevices
// TODO: Add A static list of known SeriadeviceIdents
pub async fn handle_serial_device_hotplug(
app_state: Arc<AppState>,
map: Option<HashMap<String, UsbPortInfo>>,
) {
let laser_ident = SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
};
let laser = match map {
Some(map) => SerialDetection::get_path_by_id(laser_ident, map),
None => None,
};
let mut unique_ident: Option<MachineIdentificationUnique> = None;
{
let machines = app_state.machines.read_arc_blocking();
for (id, _) in machines.iter() {
if id.machine_identification == LaserMachine::MACHINE_IDENTIFICATION {
unique_ident = Some(id.clone());
break;
}
}
}
// Machine isnt connected, so add it back
if laser.is_some() && unique_ident.is_none() {
let serial_params = SerialDeviceNewParams {
path: laser.unwrap(),
};
let _ = match Laser::new_serial(&serial_params) {
Ok((device_identification, serial_device)) => {
{
let mut machines = app_state.machines.write().await;
machines.add_serial_device(
&device_identification,
serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
);
}
let app_state_event = app_state.clone();
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
}
_ => (),
};
} else if laser.is_none() && unique_ident.is_some() {
let serial_params = SerialDeviceNewParams {
path: "".to_string(),
};
let _ = match Laser::new_serial(&serial_params) {
Ok((device_identification, _)) => {
app_state
.machines
.write()
.await
.remove_serial_device(&device_identification);
drop(app_state.machines.clone());
app_state
.machines
.write_arc()
.await
.serial_machines
.remove(&unique_ident.clone().unwrap());
app_state
.machines
.write_arc()
.await
.ethercat_machines
.remove(&unique_ident.clone().unwrap());
drop(app_state.machines.clone());
let main_namespace = &mut app_state
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
}
_ => (),
};
}
}
+29 -47
View File
@@ -1,6 +1,7 @@
use crate::app_state::AppState;
use smol::Task;
use std::{sync::Arc, time::Instant};
use tracing::{debug, error, info, instrument, trace};
use tracing::{debug, info, instrument, trace};
/// Send a single event with retry logic
#[instrument(skip_all)]
@@ -58,55 +59,36 @@ async fn send_event_with_retry(
}
}
pub fn init_socketio_queue(app_state: Arc<AppState>) {
std::thread::Builder::new()
.name("socketio-queue".to_string())
.spawn(move || {
async fn socketio_queue_worker(app_state: Arc<AppState>) {
tracing::info!("SocketIO global queue listener started");
let mut event_count = 0;
let mut batch_start = Instant::now();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.expect("Failed to create runtime");
while let Ok((socket, event)) = app_state.socketio_setup.socket_queue_rx.recv().await {
event_count += 1;
rt.block_on(async {
info!("SocketIO global queue listener started");
send_event_with_retry(&socket, &event).await;
let mut event_count = 0;
let mut batch_start = Instant::now();
if batch_start.elapsed().as_secs() >= 5 {
let elapsed = batch_start.elapsed();
if event_count > 0 {
debug!(
"[{}::socketio_queue_worker] Processed {} events in {:.2?} ({:.1} events/s)",
module_path!(),
event_count,
elapsed,
event_count as f64 / elapsed.as_secs_f64(),
);
}
event_count = 0;
batch_start = Instant::now();
}
}
loop {
match app_state.socketio_setup.socket_queue_rx.recv().await {
Ok((socket, event)) => {
event_count += 1;
info!("SocketIO global queue listener stopped");
}
// Handle the received message with retry logic
send_event_with_retry(&socket, &event).await;
// Log batch statistics every 5 seconds
if batch_start.elapsed().as_secs() >= 5 {
let elapsed = batch_start.elapsed();
if event_count > 0 {
debug!(
"[{}::init_socketio_queue] Processed {} events in {:.2?} ({:.1} events/s)",
module_path!(),
event_count,
elapsed,
event_count as f64 / elapsed.as_secs_f64(),
);
}
event_count = 0;
batch_start = Instant::now();
}
}
Err(e) => {
error!(error = %e, "Error receiving from global socketio queue");
info!("SocketIO global queue listener stopping");
break;
}
}
}
});
})
.expect("Failed to spawn socketio queue thread");
/// Returns a spawned async task handling the Socket.IO event queue
pub fn start_socketio_queue(app_state: Arc<AppState>) -> Task<()> {
smol::spawn(socketio_queue_worker(app_state))
}