Compare commits

...
Author SHA1 Message Date
Snacj 252fb31d65 Fixed merge issues 2025-10-31 13:24:56 +01:00
snacj bf12a25af4 Fixed merge issues 2025-10-31 12:32:15 +01:00
snacj c66f046a9b Fixed Formatting 2025-10-31 12:31:40 +01:00
snacj a7b1158f8d Fixed merge issues. 2025-10-31 12:30:53 +01:00
snacj be87b377ba Changed frontend Formatting 2025-10-31 12:29:23 +01:00
snacj 111664185e Fixed Formatting 2025-10-31 12:29:23 +01:00
snacj 29ccaea7f2 fixed formatting 2025-10-31 12:29:23 +01:00
snacj 8402199601 Fixed Merge issues 2025-10-31 12:29:23 +01:00
snacj b7399c8d82 Fixed Formula 2025-10-31 12:29:23 +01:00
snacj db71805912 Changed P Controller to Pi controller with dead time 2025-10-31 12:29:23 +01:00
snacj 3fd1b09589 Added Pi and Ei to the update logic. 2025-10-31 12:29:23 +01:00
snacj c806f59df5 Changed P Controller to be Pi Controller. 2025-10-31 12:29:23 +01:00
snacj a5c0d75f33 Fixed Clippy and Formatting 2025-10-31 12:29:23 +01:00
snacj 9629652d4f fixed merge issue 2025-10-31 12:29:23 +01:00
snacj 5ee0b7e61b Fixed Clippy 2025-10-31 12:29:23 +01:00
snacj bb975081e8 Testing fix for frontend freeze 2025-10-31 12:29:23 +01:00
snacj 6629055f9f Fixed clippy 2025-10-31 12:29:23 +01:00
snacj 3b10790146 Removed unwrap and fixed clippy 2025-10-31 12:29:23 +01:00
snacj cb6a954e3b Removed inline import 2025-10-31 12:29:23 +01:00
snacj bbf58e6b18 Fixed Error Pid to P 2025-10-31 12:29:23 +01:00
snacj 9af2364528 Fixed Merge Issue 2025-10-31 12:29:23 +01:00
snacj 7d774b0025 Fixed Merge issue 2025-10-31 12:29:23 +01:00
snacj 94655093cc Added Algorithm Only speed calculation for Diameter Regulation 2025-10-31 12:29:23 +01:00
snacj 5b57273153 Removed logs for testing 2025-10-31 12:29:23 +01:00
snacj 02951794f1 Fixed issues with kp scaling factor 2025-10-31 12:29:23 +01:00
snacj d1f96cc816 changed PID controller to P controller 2025-10-31 12:29:23 +01:00
snacj 673b85420c Implemented deadtime_p_controller to regulate puller speed by filament
diameter.
2025-10-31 12:29:23 +01:00
snacj 493cb39396 Moved PID settings impl from Laser to Winder 2025-10-31 12:29:22 +01:00
snacj 34d6e85ec3 fixed PID impl 2025-10-31 12:27:09 +01:00
snacj dd79586b13 Adjusted default PID setting and replaced info! with trace! 2025-10-31 12:27:09 +01:00
snacj fab801e62f Fixed Formatting 2025-10-31 12:27:09 +01:00
snacj 37a1641450 Fixed formatting. 2025-10-31 12:27:09 +01:00
snacj 6e4baaa356 Fixed merge issue 2025-10-31 12:27:09 +01:00
snacj 3292c62212 Added Debug messages 2025-10-31 12:27:09 +01:00
snacj 3e1d83a0dd changed maximum velocity by diameter for testing. 2025-10-31 12:27:09 +01:00
snacj 47dcd34364 Implemented setting of measured diameter 2025-10-31 12:27:09 +01:00
snacj 2c134c4479 Added working PID settings. 2025-10-31 12:27:09 +01:00
snacj 8613e0c5a8 Fixed Laser Winder connection 2025-10-31 12:27:09 +01:00
snacj 02daf33e55 Testing laser winder connection. 2025-10-31 12:27:09 +01:00
snacj 7b7eafee3b Added debug messages to test the connection 2025-10-31 12:27:07 +01:00
snacj 400fc016ae fixed naming 2025-10-31 11:46:20 +01:00
snacj 8b66ee415a Maybe fix laser Machine is Serial 2025-10-31 11:46:20 +01:00
snacj 89fef65347 Fixed Comments 2025-10-31 11:46:20 +01:00
snacj 5fbf5b7b97 Fix PID settings through laser. 2025-10-31 11:46:12 +01:00
snacj 8278bf579c Implemented Diameter Regulation Mode logic 2025-10-31 11:29:59 +01:00
snacj 6f7611ada5 Added Connect Machine selector to laser frontend 2025-10-31 11:29:59 +01:00
snacj a3ff479941 Fixed Machine Connection 2025-10-31 11:29:59 +01:00
snacj d14e1183d5 Fixed Laser Settings Page 2025-10-31 11:29:59 +01:00
snacj c7495a7d92 Added Pid Settings Logic 2025-10-31 11:29:59 +01:00
snacj 5428e950b0 Added Winder Laser Connection Frontend & Backend 2025-10-31 11:29:59 +01:00
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
Robin Krämer 97811a12d1 Merge pull request #842 from qitechgmbh/changelog-2.11.0
updated Changelog
2025-10-21 13:11:26 +02:00
kraemr 1f7e01820a updated Changelog 2025-10-21 13:10:37 +02:00
Robin Krämer 2f73771bbd Merge pull request #827 from qitechgmbh/825-add-button-to-change-the-spool-motor-direction-of-the-winder
825 add button to change the spool motor direction of the winder
2025-10-21 13:01:25 +02:00
Christian 503ac18e7b Ensure spool RPM is always displayed as a positive value 2025-10-21 12:37:34 +02:00
Christian 6a99b21c15 Fix spool speed rendering to display absolute values 2025-10-21 12:37:34 +02:00
Christian 170667f2b4 Add spool direction control to Winder2
- Introduced a new setting for spool direction (Forward/Reverse) in Winder2PresetsPage and Winder2Settings.
- Updated the state management to include forward direction in useWinder.
- Enhanced the API to handle SetSpoolForward mutation.
- Adjusted the spool speed controller to apply direction based on the forward setting.
2025-10-21 12:37:34 +02:00
Robin Krämer 314b7f2484 Merge pull request #840 from qitechgmbh/828-add-xl-winder-button-to-the-winder-config
828 add xl winder button to the winder config
2025-10-21 12:28:22 +02:00
Christian 9353b12707 Fix traverse limit reset logic in handleXlModeChange function 2025-10-21 11:51:02 +02:00
Christian 063810f906 Add gotoTraverseHome call when switching from XL to normal mode 2025-10-21 11:51:02 +02:00
Christian ac7ce126e6 Refactor handleXlModeChange to reset traverse limits when switching from XL mode 2025-10-21 11:51:02 +02:00
Christian a920830dcf Update WINDER2_TRAVERSE_MAX_XL to 385 mm for improved configuration 2025-10-21 11:51:02 +02:00
Christian 15d74fcee0 Add configuration for Winder2 settings and implement XL mode functionality 2025-10-21 11:51:02 +02:00
Robin Krämer c598e3fc9f Merge pull request #826 from qitechgmbh/818-smooth-fluctuating-temperature-extruder
818 smooth fluctuating temperature extruder
2025-10-20 18:16:08 +02:00
snacj 0dba0f2774 Removed package-lock from root folder 2025-10-20 18:12:06 +02:00
snacj e2cdb6a94c Removed package-lock from root folder 2025-10-20 18:12:06 +02:00
snacj 5dd2011e78 removed file 2025-10-20 18:12:06 +02:00
snacj 7bc142dc83 Fixed error in Extruder mock and removed zone from UpdateStateOptimistically 2025-10-20 18:12:06 +02:00
snacj 1931f00806 Changed rounding and step size to 0.001 for temperature PID 2025-10-20 18:12:06 +02:00
snacj 253336b02c Refactored frontend code of new PID settings 2025-10-20 18:12:06 +02:00
snacj b0c3a97f7f Fixed 3 New Pid Controllers for Extruder front, middle, back, heating. 2025-10-20 18:12:06 +02:00
snacj 22887d4f7a Added 3 New Pid Controllers for Extruder front, middle, back, heating. 2025-10-20 18:12:06 +02:00
Robin Krämer 00dbc90c85 Merge pull request #834 from qitechgmbh/lag-spike-fix
removed async sleep from control loop
2025-10-20 18:07:21 +02:00
kraemr 9ade1d8993 removed async sleep from control loop 2025-10-20 18:04:52 +02:00
Robin Krämer b2fb803033 Merge pull request #829 from qitechgmbh/808-lock-kernel-in-nix-to-last-working-rt-kernel
Newest kernel with realtime preemption
2025-10-20 13:42:38 +02:00
nixos-install.sh 988ed5b959 Newest kernel with realtime preemption 2025-10-17 17:36:14 +02:00
Oshgnacknak 2e62f99724 Merge pull request #806 from qitechgmbh/fix-caps
Fix caps in cargo run Linux
2025-10-17 15:01:53 +02:00
Oshgnacknak 530327b1e8 Fix caps in cargo run Linux 2025-10-17 14:55:44 +02:00
Robin Krämer 2f75b31fa9 Merge pull request #816 from qitechgmbh/812-improve-interface-discovery-to-only-use-ethernet-devices
fixed the atrocious interface_discovery
2025-10-17 12:42:31 +02:00
kraemr ce8f0a8c4b fixed the atrocious interface_discovery 2025-10-17 12:40:18 +02:00
Robin Krämer 321aae5b86 Merge pull request #820 from qitechgmbh/changelog-210
updated Changelog
2025-10-16 15:16:05 +02:00
kraemr 7712ea2d1f updated Changelog 2025-10-16 15:15:36 +02:00
Robin Krämer 175f64f784 Merge pull request #804 from qitechgmbh/803-add-winder-gearbox-conversion
803 add winder gearbox conversion
2025-10-16 12:39:14 +02:00
ChristianandCopilot 9357960463 Update electron/src/machines/winder/winder2/useWinder.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 12:22:54 +02:00
Christian 4d043906bc Remove unnecessary whitespace in puller speed calculation 2025-10-16 12:22:54 +02:00
Christian b99b7f796b Reset target speed to 0 when setting gear ratio and adjust puller speed calculation based on gear ratio 2025-10-16 12:22:54 +02:00
Christian a7b0414613 Add gear ratio calculation and display in Winder2 control and presets 2025-10-16 12:22:54 +02:00
Christian 13c608b11a Add gear ratio functionality to puller speed controller
- Introduced gear ratio enum with options for 1:5 and 1:10.
- Updated puller speed controller to include gear ratio state.
- Implemented gear ratio selection in Winder2Settings component.
- Added corresponding mutation and state management in useWinder2 hook.
2025-10-16 12:22:54 +02:00
Oshgnacknak d14fb91736 Simpliy panic handling and fix backtrace (#809)
* Hide memory locking behind feature flag

* Simplify panic handling
2025-10-15 21:09:05 +02:00
Oshgnacknak 6beaa444af Simplify panic handling 2025-10-15 17:08:37 +02:00
Oshgnacknak 3b552558d4 Hide memory locking behind feature flag 2025-10-15 17:08:37 +02:00
Robin Krämer 9cc63bf8fb Merge pull request #805 from qitechgmbh/743-electron-doesnt-use-gpu-for-hw-acceleration
Remove bad hardware acceleration flag
2025-10-15 10:31:04 +02:00
74 changed files with 2813 additions and 1197 deletions

No files matched your search

+2
View File
@@ -3,3 +3,5 @@ dist/
.DS_Store
Cargo.lock
dhat-heap.json
build.sh
+27
View File
@@ -1,3 +1,30 @@
# `2.11.0`
_21.10.2025_
## General
- [#829](https://github.com/qitechgmbh/control/pull/829), [#834](https://github.com/qitechgmbh/control/pull/834), Improved overall system stability
## Winder V2
- [#840](https://github.com/qitechgmbh/control/pull/840) Added XL Winder option
- [#827](https://github.com/qitechgmbh/control/pull/827) Spool can now be run in reverse
## Extruder V2
- [#826](https://github.com/qitechgmbh/control/pull/826) Fine grained Temperature-PID configuration now possible
# `2.10.0`
_16.10.2025_
## Winder V2
- [#776](https://github.com/qitechgmbh/control/pull/776) Added explanation for padding in winder manual
- [#804](https://github.com/qitechgmbh/control/pull/804) Added gearbox conversion
## General
- [#717](https://github.com/qitechgmbh/control/pull/717) Show from option for graphs added
- [#805](https://github.com/qitechgmbh/control/pull/805) Graphs run better now
# `2.9.1`
_10.10.2025_
+6 -4
View File
@@ -15,16 +15,18 @@ if [[ "$1" == "release" ]]; then
cargo build --release
else
echo "Building Debug Code"
cargo build $FEATURE
cargo build $FEATURE --features development-build
fi
# Only setcap if not building with mock-machine
if [ "$1" != "mock-machine" ]; then
echo "Setting capabilities for server executable"
sudo setcap 'cap_net_raw,cap_sys_nice=eip' ./target/debug/server
sudo setcap 'cap_dac_override,cap_net_raw,cap_sys_nice,cap_ipc_lock=eip' ./target/debug/server
echo "Setting permissions for serial ports"
# sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
if compgen -G "/dev/ttyUSB*" > /dev/null; then
echo "Setting permissions for serial ports"
sudo chown "root:$(groups | cut -f1 -d' ')" /dev/ttyUSB*
fi
fi
# run
@@ -0,0 +1,124 @@
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct TimeAgnosticDeadTimePiController {
// Params
/// Proportional gain
kp: f64,
/// Integral gain
ki: f64,
// State
/// Proportional error
ep: f64,
/// Integral error
ei: f64,
/// Dead Time
dead: Duration,
last: Option<Instant>,
}
impl TimeAgnosticDeadTimePiController {
pub fn new(kp: f64, ki: f64, dead: Duration) -> Self {
Self {
kp,
ki,
dead,
ep: 0.0,
ei: 0.0,
last: None,
}
}
pub fn configure(&mut self, kp: f64, ki: f64) {
self.reset();
self.kp = kp;
self.ki = ki;
}
pub fn get_kp(&self) -> f64 {
self.kp
}
pub fn get_ki(&self) -> f64 {
self.ki
}
pub fn get_dead(&self) -> Duration {
self.dead
}
pub fn set_kp(&mut self, kp: f64) {
self.kp = kp;
}
pub fn set_ki(&mut self, ki: f64) {
self.ki = ki;
}
pub fn set_dead(&mut self, dead: Duration) {
self.dead = dead;
}
pub fn optional_clamp(value: f64, min: Option<f64>, max: Option<f64>) -> f64 {
match (min, max) {
(Some(min), Some(max)) => value.clamp(min, max),
(Some(min), None) => value.max(min),
(None, Some(max)) => value.min(max),
(None, None) => value,
}
}
pub fn update(&mut self, error: f64, t: Instant) -> f64 {
let kp = self.kp / 10000.0;
let ki = self.ki / 10000.0;
let signal = match self.last {
// First update
None => {
let ep = error;
// Initialize integral error
self.ei = 0.0;
// Proportional + Integral
let signal = kp * ep + ki * self.ei;
// Save state
self.ep = ep;
self.last = Some(t);
-signal
}
// Subsequent updates
Some(last) => {
// Calculate elapsed time
let dt = t.duration_since(last).as_secs_f64();
// Dead-time scaling factor
let dead_secs = self.dead.as_secs_f64();
let scale = (dt / dead_secs).min(1.0);
// Smoothed proportional error
let ep = self.ep + (error - self.ep) * scale;
// Integrate error over time
self.ei += error * dt;
// Proportional + Integral
let signal = kp * ep + ki * self.ei;
// Save state
self.ep = ep;
self.last = Some(t);
-signal
}
};
-signal
}
pub fn reset(&mut self) {
self.ep = 0.0;
//self.last = None;
}
}
+1
View File
@@ -1,4 +1,5 @@
pub mod clamping_timeagnostic_pid;
pub mod deadtime_pi_controller;
pub mod first_degree_motion;
pub mod pid;
pub mod second_degree_motion;
@@ -1,4 +1,4 @@
use std::{sync::Arc, time::Duration};
use std::{process::Command, sync::Arc, time::Duration};
use ethercrab::{
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
@@ -12,14 +12,46 @@ const IFACE_DISCOVERY_MAX_PDU_DATA: usize = PduStorage::element_size(1100);
const IFACE_DISCOVERY_MAX_FRAMES: usize = 16;
const IFACE_DISCOVERY_MAX_PDI_LEN: usize = 128;
/// Finds an ethernet interface that is suitable for EtherCAT communication.
///
/// ```ignore
/// match discover_ethercat_interface().await {
/// Ok(interface) => println!("Found working interface: {}", interface),
/// Err(_) => println!("No working interface found"),
/// }
/// ```
/// Sets a network interface to unmanaged by NetworkManager.
/// Returns true if the command succeeded.
pub fn set_interface_managed(interface: &str, managed: bool) -> bool {
let managed_str = match managed {
true => "yes",
false => "no",
};
tracing::info!(
"set_interface_managed for {} managed was set to: {}",
interface,
managed_str
);
let status = Command::new("nmcli")
.args(["dev", "set", interface, "managed", managed_str])
.status();
matches!(status, Ok(s) if s.success())
}
/// Returns true if the given network interface is Ethernet.
/// Prevents testing wlan,loopback and other non ethernet devices
fn is_ethernet(interface: &str) -> std::io::Result<bool> {
#[cfg(target_os = "linux")]
{
let base_path = format!("/sys/class/net/{}", interface);
let type_path = std::path::Path::new(&base_path).join("type");
let iface_type = std::fs::read_to_string(&type_path)?.trim().to_string();
// 1 means Ethernet
let reports_as_ethernet = iface_type == "1";
// Double-check that it's not a wireless interface
let uevent_path = std::path::Path::new(&base_path).join("uevent");
let uevent = std::fs::read_to_string(&uevent_path).unwrap_or_default();
// If "DEVTYPE=wlan" appears, it's lying
let actually_wifi = uevent.contains("DEVTYPE=wlan");
return Ok(reports_as_ethernet && !actually_wifi);
}
#[cfg(not(target_os = "linux"))]
return Ok(true);
}
pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
tracing::info!("Discovering EtherCAT interface...");
@@ -37,7 +69,6 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
} else {
tracing::debug!("Suppressed panic in interface test: {}", panic_info);
}
// Don't call the default hook, which would print the backtrace
}));
// Get eligible interfaces
@@ -48,6 +79,7 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
iface.is_up()
&& iface.is_running()
&& !iface.is_loopback()
&& is_ethernet(&iface.name).unwrap_or(false)
&& !iface.name.starts_with("bridge")
&& !iface.name.starts_with("utun") // Exclude tunnel interfaces
&& !iface.name.starts_with("awdl") // Exclude Apple Wireless Direct Link
@@ -56,57 +88,42 @@ pub async fn discover_ethercat_interface() -> Result<String, anyhow::Error> {
})
.collect::<Vec<_>>();
// Sort interfaces by name
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
let mut interface: Option<&str> = None;
// Try all interfaces concurrently
let tasks = interfaces
.iter()
.map(|iface| {
let name = iface.name.clone();
std::thread::Builder::new()
.name(format!("ethercat-test-{}", name))
.spawn(move || {
std::panic::catch_unwind(|| {
smol::block_on(async {
match test_interface(&name) {
Ok(_) => {
tracing::info!("Found working EtherCAT interface: {}", name);
Some(name.clone())
}
Err(_) => {
tracing::debug!("Interface {} failed", name);
None
}
}
})
})
.unwrap_or(None)
})
.expect("Failed to spawn thread")
})
.collect::<Vec<_>>();
for i in 0..interfaces.len() {
// Avoid NetworkManager hangups
set_interface_managed(&interfaces[i].name, false);
// Wait for all tasks to complete and collect successful results
let successful_interfaces: Vec<String> = tasks
.into_iter()
.filter_map(|task| task.join().expect("Should join thread"))
.collect();
match test_interface(&interfaces[i].name) {
Ok(_) => {
// if interface found with ethercat Exit early, we expect only one interface with ethercat
interface = Some(&interfaces[i].name);
break;
}
Err(_) => (),
}
}
// Return the first successful interface (they're sorted by name)
let result = successful_interfaces.into_iter().next();
for i in 0..interfaces.len() {
if interface.is_some() && interface.unwrap() == &interfaces[i].name {
continue;
} else {
set_interface_managed(&interfaces[i].name, true);
}
}
// Restore the default panic hook
std::panic::set_hook(default_hook);
match result {
Some(name) => Ok(name),
None => Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
match interface {
Some(interface) => return Ok(interface.to_string()),
None => return Err(anyhow::anyhow!("No suitable EtherCAT interface found")),
}
}
fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
tracing::trace!("Testing interface: {}", interface);
tracing::info!("Testing interface: {}", interface);
let pdu_storage = Box::leak(Box::new(PduStorage::<
IFACE_DISCOVERY_MAX_FRAMES,
@@ -144,7 +161,7 @@ fn test_interface(interface: &str) -> Result<(), anyhow::Error> {
// Default 10000
dc_static_sync_iterations: 10000,
// Default None
retry_behaviour: ethercrab::RetryBehaviour::Count(5),
retry_behaviour: ethercrab::RetryBehaviour::Count(3),
},
));
+2 -2
View File
@@ -92,7 +92,7 @@ pub struct MachineCrossConnection<
> {
machine_manager: Weak<RwLock<MachineManager>>,
machine_identification_unique: MachineIdentificationUnique,
connected_machine: Weak<Mutex<MachineSlot<T>>>,
pub connected_machine: Weak<Mutex<MachineSlot<T>>>,
_make_compiler_happy: std::marker::PhantomData<F>, // This marks tells the typechecker, that we
// need F in our type arguemnts. We are ok with
// it being used only in a fake field. Without
@@ -102,7 +102,7 @@ pub struct MachineCrossConnection<
// to an unknown type. See also https://github.com/qitechgmbh/control/pull/625#discussion_r2379566315
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct MachineCrossConnectionState {
/// Connected Machine
pub machine_identification_unique: Option<MachineIdentificationUnique>,
+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() {
+19 -26
View File
@@ -70,7 +70,7 @@
"@electron/fuses": "^2.0.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "^4.0.6",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -97,7 +97,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vitest": "^3.0.6"
}
},
@@ -2278,12 +2278,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.54.2"
"playwright": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -7014,16 +7015,6 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -11877,12 +11868,13 @@
}
},
"node_modules/playwright": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.54.2"
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -11895,10 +11887,11 @@
}
},
"node_modules/playwright-core": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
@@ -14672,9 +14665,9 @@
}
},
"node_modules/vite": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
+2 -2
View File
@@ -34,7 +34,7 @@
"@electron/fuses": "^2.0.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.50.1",
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "^4.0.6",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -61,7 +61,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.3.6",
"vite": "^6.4.1",
"vitest": "^3.0.6"
},
"dependencies": {
+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) => ({
}
}),
);
}
}*/
},
}));
@@ -10,7 +10,7 @@ import {
import React from "react";
import { Icon } from "@/components/Icon";
import { ControlCard } from "@/control/ControlCard";
import { TouchButton } from "./touch/TouchButton";
interface MachineIdentificationUnique {
machine_identification: {
@@ -49,67 +49,64 @@ export const MachineSelector: React.FC<MachineSelectorProps> = ({
connectedMachineState,
setConnectedMachine,
clearConnectedMachine,
title = "Machine Connection",
}) => {
return (
<ControlCard title={title}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 rounded border px-4 py-2 text-left">
<Icon name="lu:Settings" className="text-xl" />
<span>
{selectedMachine?.name ?? "Select a Machine"}{" "}
{selectedMachine?.machine_identification_unique.serial ?? ""}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>Available Machines</DropdownMenuLabel>
<DropdownMenuSeparator />
{machines.map((machine) => {
const isSelected =
connectedMachineState?.machine_identification_unique
?.machine_identification.machine ===
machine.machine_identification_unique.machine_identification
.machine;
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TouchButton className="text-md h-max px-6 py-6" variant="outline">
<div className="flex flex-row items-center gap-2 text-wrap">
<Icon name="lu:Link2" className="size-6" />
{selectedMachine?.name ?? "Select a Machine"}{" "}
{selectedMachine?.machine_identification_unique.serial ?? ""}
</div>
</TouchButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuLabel>Available Machines</DropdownMenuLabel>
<DropdownMenuSeparator />
{machines.map((machine) => {
const isSelected =
connectedMachineState?.machine_identification_unique
?.machine_identification.machine ===
machine.machine_identification_unique.machine_identification
.machine;
return (
<DropdownMenuItem
key={machine.machine_identification_unique.serial}
onClick={() =>
setConnectedMachine(machine.machine_identification_unique)
}
className={`flex min-h-[48px] cursor-pointer items-center gap-2 py-2${
isSelected ? "bg-blue-50" : ""
}`}
>
{connectedMachineState?.is_available ? (
<Icon name="lu:Power" className="text-lg text-green-600" />
) : (
<Icon name="lu:Power" className="text-lg text-gray-400" />
)}
<span>
{machine.name} – Serial:{" "}
{machine.machine_identification_unique.serial}
</span>
</DropdownMenuItem>
);
})}
return (
<DropdownMenuItem
key={machine.machine_identification_unique.serial}
onClick={() =>
setConnectedMachine(machine.machine_identification_unique)
}
className={`flex h-max cursor-pointer items-center gap-2 px-6 py-6${
isSelected ? "bg-blue-50" : ""
}`}
>
{connectedMachineState?.is_available ? (
<Icon name="lu:Link2" className="size-6 text-green-600" />
) : (
<Icon name="lu:Link2Off" className="size-6 text-black" />
)}
<span>
{machine.name} – Serial:{" "}
{machine.machine_identification_unique.serial}
</span>
</DropdownMenuItem>
);
})}
{clearConnectedMachine && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={clearConnectedMachine}
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
>
<Icon name="lu:Settings" className="text-lg" />
<span>Clear Selection</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</ControlCard>
{clearConnectedMachine && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={clearConnectedMachine}
className="flex cursor-pointer items-center gap-2 text-red-600 hover:text-red-600"
>
<Icon name="lu:X" className="size-6 text-red-600" />
<span>Disconnect Machine</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
+10 -2
View File
@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority";
import React from "react";
type Props = {
variant: "error" | "success";
variant: "error" | "success" | "info";
children: React.ReactNode;
};
@@ -14,10 +14,18 @@ export function StatusBadge({ variant, children }: Props) {
variant: {
error: "bg-red-500",
success: "bg-green-600",
info: "bg-yellow-400 text-black",
},
},
});
const icon: IconName = variant === "error" ? "lu:TriangleAlert" : "lu:Check";
const icon: IconName =
variant === "error"
? "lu:TriangleAlert"
: variant === "success"
? "lu:Check"
: "lu:Info";
return (
<Badge
className={badgeStyle({
@@ -6,6 +6,7 @@ import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { EditValue } from "@/control/EditValue";
import { roundToDecimals } from "@/lib/decimal";
import { useExtruder2 } from "./useExtruder";
import { ControlGrid } from "@/control/ControlGrid";
export function Extruder2SettingsPage() {
const {
@@ -18,6 +19,7 @@ export function Extruder2SettingsPage() {
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
} = useExtruder2();
const [showAdvanced, setShowAdvanced] = useState(false);
@@ -116,6 +118,128 @@ export function Extruder2SettingsPage() {
/>
</Label>
</ControlCard>
<ControlGrid>
<ControlCard title="Temperature PID Settings (Front) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.front.kp}
defaultValue={defaultState?.pid_settings.temperature.front.kp}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("front", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.front.ki}
defaultValue={defaultState?.pid_settings.temperature.front.ki}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("front", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.front.kd}
defaultValue={defaultState?.pid_settings.temperature.front.kd}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("front", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
<ControlCard title="Temperature PID Settings (Middle) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.middle.kp}
defaultValue={
defaultState?.pid_settings.temperature.middle.kp
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("middle", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.middle.ki}
defaultValue={
defaultState?.pid_settings.temperature.middle.ki
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("middle", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.middle.kd}
defaultValue={
defaultState?.pid_settings.temperature.middle.kd
}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("middle", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
<ControlCard title="Temperature PID Settings (Back) ">
<Label label="Kp">
<EditValue
value={state?.pid_settings.temperature.back.kp}
defaultValue={defaultState?.pid_settings.temperature.back.kp}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("back", "kp", v)}
title="Temperature PID KP"
/>
</Label>
<Label label="Ki">
<EditValue
value={state?.pid_settings.temperature.back.ki}
defaultValue={defaultState?.pid_settings.temperature.back.ki}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("back", "ki", v)}
title="Temperature PID KI"
/>
</Label>
<Label label="Kd">
<EditValue
value={state?.pid_settings.temperature.back.kd}
defaultValue={defaultState?.pid_settings.temperature.back.kd}
min={0}
max={100}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={(v) => setTemperaturePidValue("back", "kd", v)}
title="Temperature PID KD"
/>
</Label>
</ControlCard>
</ControlGrid>
</>
)}
</Page>
@@ -131,9 +131,24 @@ export const inverterStatusStateSchema = z.object({
*/
export const pidSettingsSchema = z.object({
temperature: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
front: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
middle: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
back: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
}),
pressure: z.object({
ki: z.number(),
@@ -293,6 +293,33 @@ export function useExtruder2() {
);
};
const setTemperaturePidValue = (
zone: "front" | "middle" | "back",
key: "kp" | "ki" | "kd",
value: number,
) => {
updateStateOptimistically(
(current) => {
current.data.pid_settings.temperature[zone][key] = value;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(
currentState.data.pid_settings.temperature[zone],
(draft) => {
draft[key] = value;
},
);
requestTemperaturePidSettings({
machine_identification_unique: machineIdentification,
data: { SetTemperaturePidSettings: settings },
});
}
},
);
};
const resetInverter = () => {
// No optimistic update needed for reset
requestResetInverter({
@@ -356,6 +383,17 @@ export function useExtruder2() {
}),
);
const { request: requestTemperaturePidSettings } = useMachineMutation(
z.object({
SetTemperaturePidSettings: z.object({
ki: z.number(),
kp: z.number(),
kd: z.number(),
zone: z.string(),
}),
}),
);
const { request: requestResetInverter } = useMachineMutation(
z.object({ ResetInverter: z.boolean() }),
);
@@ -404,6 +442,7 @@ export function useExtruder2() {
setPressurePidKp,
setPressurePidKi,
setPressurePidKd,
setTemperaturePidValue,
resetInverter,
};
}
@@ -72,6 +72,7 @@ export function Laser1ControlPage() {
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Roundness"
icon="lu:Circle"
timeseries={roundness}
renderValue={(value) => value.toFixed(3)}
/>
@@ -20,6 +20,12 @@ export function Laser1Page() {
title: "Graphs",
icon: "lu:ChartSpline",
},
{
link: "settings",
activeLink: "settings",
title: "Config",
icon: "lu:Settings",
},
// {
// link: "manual",
// activeLink: "manual",
@@ -0,0 +1,42 @@
import React from "react";
import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { Label } from "@/control/Label";
import { useLaser1 } from "./useLaser1";
import { MachineSelector } from "@/components/MachineConnectionDropdown";
import { ControlGrid } from "@/control/ControlGrid";
export function Laser1SettingsPage() {
const {
state,
selectedMachine,
filteredMachines,
setConnectedWinder,
disconnectWinder,
} = useLaser1();
return (
<Page>
<ControlGrid>
<ControlCard title="Speed by Diameter Settings">
<Label label="Associated Winder">
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_winder_state}
setConnectedMachine={setConnectedWinder}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedWinder({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectWinder(selectedMachine.machine_identification_unique);
}}
/>
</Label>
</ControlCard>
</ControlGrid>
</Page>
);
}
@@ -23,6 +23,24 @@ import {
} from "@/lib/timeseries";
// ========== Event Schema Definitions ==========
/**
* Connected machine state schema
*/
export const machineIdentificationSchema = z.object({
vendor: z.number(),
machine: z.number(),
});
export const machineIdentificationUniqueSchema = z.object({
machine_identification: machineIdentificationSchema,
serial: z.number(),
});
export const connectedMachineStateSchema = z.object({
machine_identification_unique: machineIdentificationUniqueSchema.nullable(),
is_available: z.boolean(),
});
/**
* Live values from Laser (30 FPS)
*/
@@ -43,6 +61,7 @@ export const stateEventDataSchema = z.object({
lower_tolerance: z.number(),
target_diameter: z.number(),
}),
connected_winder_state: connectedMachineStateSchema,
});
// ========== Event Schemas with Wrappers ==========
@@ -1,13 +1,18 @@
import { toastError } from "@/components/Toast";
import { useMachineMutate as useMachineMutation } from "@/client/useClient";
import { MachineIdentificationUnique } from "@/machines/types";
import { laser1 } from "@/machines/properties";
import {
machineIdentification,
machineIdentificationUnique,
MachineIdentificationUnique,
} from "@/machines/types";
import { laser1, VENDOR_QITECH } from "@/machines/properties";
import { laser1SerialRoute } from "@/routes/routes";
import { z } from "zod";
import { useLaser1Namespace, StateEvent } from "./laser1Namespace";
import { useEffect, useMemo } from "react";
import { useStateOptimistic } from "@/lib/useStateOptimistic";
import { produce } from "immer";
import { useMachines } from "@/client/useMachines";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
@@ -54,6 +59,16 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
const { request: requestHigherTolerance } = useMachineMutation(
schemaHigherTolerance,
);
const { request: requestConnectedWinder } = useMachineMutation(
z.object({
SetConnectedWinder: machineIdentificationUnique,
}),
);
const { request: requestDisconnectWinder } = useMachineMutation(
z.object({
DisconnectWinder: machineIdentificationUnique,
}),
);
// Action functions with verb-first names
const setTargetDiameter = (target_diameter: number) => {
@@ -101,10 +116,80 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
);
};
const setConnectedWinder = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_winder_state.machine_identification_unique =
machineIdentificationUnique;
},
() =>
requestConnectedWinder({
machine_identification_unique,
data: { SetConnectedWinder: machineIdentificationUnique },
}),
);
};
const disconnectWinder = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_winder_state.machine_identification_unique =
null;
},
() =>
requestDisconnectWinder({
machine_identification_unique,
data: { DisconnectWinder: machineIdentificationUnique },
}),
);
};
// General Helper functions
const machines = useMachines();
// Filter machines for the correct type
const filteredMachines = useMemo(
() =>
machines.filter(
(m) =>
m.machine_identification_unique.machine_identification.vendor ===
VENDOR_QITECH &&
m.machine_identification_unique.machine_identification.machine ===
0x0002,
),
[machines, machineIdentification],
);
// Get selected machine by serial
const selectedMachine = useMemo(() => {
const serial =
state?.data.connected_winder_state?.machine_identification_unique?.serial;
return (
filteredMachines.find(
(m) => m.machine_identification_unique.serial === serial,
) ?? null
);
}, [filteredMachines, state]);
return {
// Consolidated state
state: stateOptimistic.value?.data,
filteredMachines,
selectedMachine,
// Default state for initial values
defaultState: defaultState?.data,
@@ -122,6 +207,8 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
setTargetDiameter,
setLowerTolerance,
setHigherTolerance,
setConnectedWinder,
disconnectWinder,
};
}
@@ -13,7 +13,11 @@ import { Label } from "@/control/Label";
import { TouchButton } from "@/components/touch/TouchButton";
import { StatusBadge } from "@/control/StatusBadge";
import { useWinder2 } from "./useWinder";
import { Mode, SpoolAutomaticActionMode } from "./winder2Namespace";
import {
Mode,
SpoolAutomaticActionMode,
getGearRatioMultiplier,
} from "./winder2Namespace";
import { TensionArm } from "../TensionArm";
import { roundDegreesToDecimals, roundToDecimals } from "@/lib/decimal";
import { Spool } from "../Spool";
@@ -24,9 +28,11 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { getWinder2TraverseMax } from "./winder2Config";
export function Winder2ControlPage() {
const [showResetConfirmDialog, setShowResetConfirmDialog] = useState(false);
const traverseMax = getWinder2TraverseMax();
// use optimistic state
const {
@@ -54,6 +60,13 @@ export function Winder2ControlPage() {
isDisabled,
} = useWinder2();
// Calculate max speed based on gear ratio
const gearRatioMultiplier = getGearRatioMultiplier(
state?.puller_state?.gear_ratio,
);
const maxMotorSpeed = 75; // Maximum motor speed in m/min
const maxTargetSpeed = maxMotorSpeed / gearRatioMultiplier;
const handleResetProgress = () => {
// Check if the machine is currently in Wind mode
if (state?.mode_state?.mode === "Wind") {
@@ -91,7 +104,7 @@ export function Winder2ControlPage() {
{state?.traverse_state && (
<TraverseBar
inside={0}
outside={180}
outside={traverseMax}
min={state?.traverse_state.limit_inner}
max={state?.traverse_state.limit_outer}
current={traversePosition.current?.value ?? 0}
@@ -110,7 +123,7 @@ export function Winder2ControlPage() {
min={Math.max(0, (state?.traverse_state?.limit_inner ?? 0) + 1)}
minLabel="IN"
maxLabel="OUT"
max={180}
max={traverseMax}
renderValue={(value) => roundToDecimals(value, 0)}
onChange={setTraverseLimitOuter}
/>
@@ -134,8 +147,8 @@ export function Winder2ControlPage() {
// We use 1mm buffer to ensure the backend validation (which requires outer > inner + 0.9mm) will pass
// Formula: max_inner = outer_limit - 1mm
max={Math.min(
180,
(state?.traverse_state?.limit_outer ?? 180) - 1,
traverseMax,
(state?.traverse_state?.limit_outer ?? traverseMax) - 1,
)}
defaultValue={defaultState?.traverse_state?.limit_inner}
minLabel="IN"
@@ -259,13 +272,18 @@ export function Winder2ControlPage() {
Diameter: {
children: "Diameter",
icon: "lu:Sun",
disabled: true,
disabled: !state?.connected_laser_state.is_available,
},
}}
onChange={setPullerRegulationMode}
disabled={isDisabled}
loading={isLoading}
/>
{state?.connected_laser_state?.is_available !== true ? (
<StatusBadge variant={"info"}>
{"No associated Laser found"}
</StatusBadge>
) : null}
</Label>
<Label label="Target Speed">
<EditValue
@@ -274,7 +292,7 @@ export function Winder2ControlPage() {
title="Target Speed"
defaultValue={defaultState?.puller_state?.target_speed}
min={0}
max={75}
max={maxTargetSpeed}
step={0.1}
renderValue={(value) => roundToDecimals(value, 1)}
onChange={setPullerTargetSpeed}
@@ -67,6 +67,16 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
renderValue: (data: PresetData<Winder2>) =>
data.puller_state?.forward ? "Forward" : "Backward",
},
{
name: "Puller Gear Ratio",
renderValue: (data: PresetData<Winder2>) => {
const ratio = data.puller_state?.gear_ratio;
if (ratio === "OneToOne") return "1:1";
if (ratio === "OneToFive") return "1:5";
if (ratio === "OneToTen") return "1:10";
return "N/A";
},
},
{
name: "Puller Target Speed",
unit: "m/min",
@@ -91,6 +101,11 @@ const previewEntries: PresetPreviewEntries<Winder2> = [
renderValue: (data: PresetData<Winder2>) =>
data.spool_speed_controller_state?.regulation_mode,
},
{
name: "Spool Direction",
renderValue: (data: PresetData<Winder2>) =>
data.spool_speed_controller_state?.forward ? "Forward" : "Reverse",
},
{
name: "Spool Min Speed",
unit: "rpm",
@@ -153,8 +168,10 @@ export function Winder2PresetsPage() {
setPullerTargetDiameter,
setPullerForward,
setPullerTargetSpeed,
setPullerGearRatio,
setSpoolRegulationMode,
setSpoolForward,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
@@ -177,11 +194,13 @@ export function Winder2PresetsPage() {
setPullerRegulationMode(preset.data?.puller_state?.regulation ?? "Speed");
setPullerForward(preset.data?.puller_state?.forward ?? true);
setPullerTargetSpeed(preset.data?.puller_state?.target_speed ?? 1.0);
setPullerGearRatio(preset.data?.puller_state?.gear_ratio ?? "OneToOne");
// setPullerTargetDiameter(preset.data?.puller_state?.target_diameter ?? 1.75);
setSpoolRegulationMode(
preset.data?.spool_speed_controller_state?.regulation_mode ?? "MinMax",
);
setSpoolForward(preset.data?.spool_speed_controller_state?.forward ?? true);
setSpoolMinMaxMinSpeed(
preset.data?.spool_speed_controller_state?.minmax_min_speed ?? 0,
);
@@ -2,24 +2,37 @@ import { Page } from "@/components/Page";
import { ControlCard } from "@/control/ControlCard";
import { ControlGrid } from "@/control/ControlGrid";
import { EditValue } from "@/control/EditValue";
import React from "react";
import React, { useState } from "react";
import { useWinder2 } from "./useWinder";
import { roundToDecimals } from "@/lib/decimal";
import { Label } from "@/control/Label";
import { SelectionGroupBoolean } from "@/control/SelectionGroup";
import { SelectionGroup } from "@/control/SelectionGroup";
import { MachineSelector } from "@/components/MachineConnectionDropdown";
import {
getWinder2XLMode,
setWinder2XLMode,
WINDER2_TRAVERSE_MAX_STANDARD,
WINDER2_TRAVERSE_MAX_XL,
} from "./winder2Config";
export function Winder2SettingPage() {
const [xlMode, setXlMode] = useState(getWinder2XLMode());
const {
state,
defaultState,
setTraverseStepSize,
setTraversePadding,
setTraverseLimitInner,
setTraverseLimitOuter,
gotoTraverseHome,
setPullerForward,
setPullerGearRatio,
setSpoolRegulationMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
setSpoolForward,
setSpoolAdaptiveTensionTarget,
setSpoolAdaptiveRadiusLearningRate,
setSpoolAdaptiveMaxSpeedMultiplier,
@@ -27,16 +40,67 @@ export function Winder2SettingPage() {
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
isLoading,
isDisabled,
setSpeedPiKp,
setSpeedPiKi,
selectedMachine,
filteredMachines,
setConnectedLaser,
disconnectLaser,
setConnectedMachine,
disconnectMachine,
} = useWinder2();
const handleXlModeChange = (enabled: boolean) => {
setWinder2XLMode(enabled);
setXlMode(enabled);
// When switching from XL to normal mode, reset traverse limits to default values
if (!enabled && defaultState) {
// Only reset if current values exceed the standard max
const currentOuter = state?.traverse_state?.limit_outer ?? 0;
const currentInner = state?.traverse_state?.limit_inner ?? 0;
const defaultOuter = defaultState.traverse_state?.limit_outer;
const defaultInner = defaultState.traverse_state?.limit_inner;
if (
currentOuter > WINDER2_TRAVERSE_MAX_STANDARD &&
defaultOuter !== undefined
) {
setTraverseLimitOuter(defaultOuter);
setTraverseLimitInner(defaultInner);
}
if (
currentInner > WINDER2_TRAVERSE_MAX_STANDARD &&
defaultInner !== undefined
) {
setTraverseLimitOuter(defaultOuter);
setTraverseLimitInner(defaultInner);
}
// Home the traverse when switching from XL to normal mode
gotoTraverseHome();
}
};
return (
<Page>
<ControlGrid>
<ControlCard title="Traverse">
<Label label="Traverse Size">
<SelectionGroupBoolean
value={xlMode}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
children: `Standard (${WINDER2_TRAVERSE_MAX_STANDARD}mm)`,
icon: "lu:Settings",
}}
optionTrue={{
children: `XL (${WINDER2_TRAVERSE_MAX_XL}mm)`,
icon: "lu:Maximize2",
}}
onChange={handleXlModeChange}
/>
</Label>
<Label label="Step Size">
<EditValue
value={state?.traverse_state?.step_size}
@@ -87,6 +151,23 @@ export function Winder2SettingPage() {
/>
</Label>
<Label label="Rotation Direction">
<SelectionGroupBoolean
value={state?.spool_speed_controller_state?.forward}
disabled={isDisabled}
loading={isLoading}
optionFalse={{
children: "Reverse",
icon: "lu:RotateCcw",
}}
optionTrue={{
children: "Forward",
icon: "lu:RotateCw",
}}
onChange={(value) => setSpoolForward(value)}
/>
</Label>
{state?.spool_speed_controller_state?.regulation_mode ===
"MinMax" && (
<>
@@ -249,21 +330,75 @@ export function Winder2SettingPage() {
onChange={(value) => setPullerForward(value)}
/>
</Label>
<Label label="Gear Ratio">
<SelectionGroup
value={state?.puller_state?.gear_ratio}
disabled={isDisabled}
loading={isLoading}
options={{
OneToOne: {
children: "1:1",
icon: "lu:Circle",
},
OneToFive: {
children: "1:5",
icon: "lu:Cog",
},
OneToTen: {
children: "1:10",
icon: "lu:Cog",
},
}}
onChange={(value) =>
setPullerGearRatio(
value as "OneToOne" | "OneToFive" | "OneToTen",
)
}
/>
</Label>
</ControlCard>
<ControlCard title="Diameter-Based Winder Control">
<Label label="Associated Laser">
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_laser_state}
setConnectedMachine={setConnectedLaser}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedMachine({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectLaser(selectedMachine.machine_identification_unique);
}}
/>
</Label>
<Label label="PI Controller Kp Factor">
<EditValue
value={state?.pi_settings?.kp}
defaultValue={defaultState?.pi_settings?.kp}
min={0}
max={1000}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={setSpeedPiKp}
title="Speed P KP"
/>
</Label>
<Label label="PI Controller Ki Factor">
<EditValue
value={state?.pi_settings?.ki}
defaultValue={defaultState?.pi_settings?.ki}
min={0}
max={1000}
step={0.001}
renderValue={(v) => roundToDecimals(v, 3)}
onChange={setSpeedPiKi}
title="Speed P KI"
/>
</Label>
</ControlCard>
<MachineSelector
machines={filteredMachines}
selectedMachine={selectedMachine}
connectedMachineState={state?.connected_machine_state}
setConnectedMachine={setConnectedMachine}
clearConnectedMachine={() => {
if (!selectedMachine) return;
setConnectedMachine({
machine_identification: { vendor: 0, machine: 0 },
serial: 0,
});
disconnectMachine(selectedMachine.machine_identification_unique);
}}
/>
</ControlGrid>
</Page>
);
@@ -19,6 +19,8 @@ import {
PullerRegulation,
SpoolAutomaticActionMode,
spoolAutomaticActionModeSchema,
gearRatioSchema,
GearRatio,
} from "./winder2Namespace";
import { useEffect, useMemo } from "react";
import { produce } from "immer";
@@ -120,6 +122,9 @@ export function useWinder2() {
const { request: requestPullerSetForward } = useMachineMutation(
z.object({ SetPullerForward: z.boolean() }),
);
const { request: requestPullerSetGearRatio } = useMachineMutation(
z.object({ SetPullerGearRatio: gearRatioSchema }),
);
const { request: requestSpoolSetRegulationMode } = useMachineMutation(
z.object({ SetSpoolRegulationMode: spoolRegulationModeSchema }),
);
@@ -129,6 +134,9 @@ export function useWinder2() {
const { request: requestSpoolSetMinMaxMaxSpeed } = useMachineMutation(
z.object({ SetSpoolMinMaxMaxSpeed: z.number() }),
);
const { request: requestSpoolSetForward } = useMachineMutation(
z.object({ SetSpoolForward: z.boolean() }),
);
const { request: requestSpoolSetAdaptiveTensionTarget } = useMachineMutation(
z.object({ SetSpoolAdaptiveTensionTarget: z.number() }),
);
@@ -171,6 +179,27 @@ export function useWinder2() {
}),
);
const { request: requestConnectedLaser } = useMachineMutation(
z.object({
SetConnectedLaser: machineIdentificationUnique,
}),
);
const { request: requestDisconnectLaser } = useMachineMutation(
z.object({
DisconnectLaser: machineIdentificationUnique,
}),
);
const { request: requestSpeedPiSettings } = useMachineMutation(
z.object({
SetSpeedPiSettings: z.object({
kp: z.number(),
ki: z.number(),
}),
}),
);
// Helper function for optimistic updates using produce
const updateStateOptimistically = (
producer: (current: StateEvent) => void,
@@ -366,6 +395,28 @@ export function useWinder2() {
);
};
const setPullerGearRatio = (gearRatio: GearRatio) => {
updateStateOptimistically(
(current) => {
current.data.puller_state.gear_ratio = gearRatio;
// Reset target speed to 0 to prevent sudden speed changes
current.data.puller_state.target_speed = 0;
},
async () => {
// First set the target speed to 0
await requestPullerSetTargetSpeed({
machine_identification_unique: machineIdentification,
data: { SetPullerTargetSpeed: 0 },
});
// Then set the gear ratio
await requestPullerSetGearRatio({
machine_identification_unique: machineIdentification,
data: { SetPullerGearRatio: gearRatio },
});
},
);
};
const setSpoolAutomaticRequiredMeters = (meters: number) => {
updateStateOptimistically(
(current) => {
@@ -440,6 +491,19 @@ export function useWinder2() {
);
};
const setSpoolForward = (forward: boolean) => {
updateStateOptimistically(
(current) => {
current.data.spool_speed_controller_state.forward = forward;
},
() =>
requestSpoolSetForward({
machine_identification_unique: machineIdentification,
data: { SetSpoolForward: forward },
}),
);
};
const setSpoolAdaptiveTensionTarget = (value: number) => {
updateStateOptimistically(
(current) => {
@@ -550,6 +614,85 @@ export function useWinder2() {
);
};
const setSpeedPiKp = (kp: number) => {
updateStateOptimistically(
(current) => {
current.data.pi_settings.kp = kp;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(currentState.data.pi_settings, (draft) => {
draft.kp = kp;
});
requestSpeedPiSettings({
machine_identification_unique: machineIdentification,
data: { SetSpeedPiSettings: settings },
});
}
},
);
};
const setSpeedPiKi = (ki: number) => {
updateStateOptimistically(
(current) => {
current.data.pi_settings.ki = ki;
},
() => {
const currentState = stateOptimistic.value;
if (currentState) {
const settings = produce(currentState.data.pi_settings, (draft) => {
draft.ki = ki;
});
requestSpeedPiSettings({
machine_identification_unique: machineIdentification,
data: { SetSpeedPiSettings: settings },
});
}
},
);
};
const setConnectedLaser = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_laser_state.machine_identification_unique =
machineIdentificationUnique;
},
() =>
requestConnectedLaser({
machine_identification_unique,
data: { SetConnectedLaser: machineIdentificationUnique },
}),
);
};
const disconnectLaser = (machineIdentificationUnique: {
machine_identification: {
vendor: number;
machine: number;
};
serial: number;
}) => {
updateStateOptimistically(
(current) => {
current.data.connected_laser_state.machine_identification_unique = null;
},
() =>
requestDisconnectLaser({
machine_identification_unique,
data: { DisconnectLaser: machineIdentificationUnique },
}),
);
};
// Calculate loading states
const isLoading = stateOptimistic.isOptimistic;
const isDisabled = !stateOptimistic.isInitialized;
@@ -564,7 +707,7 @@ export function useWinder2() {
m.machine_identification_unique.machine_identification.vendor ===
VENDOR_QITECH &&
m.machine_identification_unique.machine_identification.machine ===
0x0008,
0x0006,
),
[machines, machineIdentification],
);
@@ -572,8 +715,7 @@ export function useWinder2() {
// Get selected machine by serial
const selectedMachine = useMemo(() => {
const serial =
state?.data.connected_machine_state?.machine_identification_unique
?.serial;
state?.data.connected_laser_state?.machine_identification_unique?.serial;
return (
filteredMachines.find(
@@ -619,17 +761,23 @@ export function useWinder2() {
setPullerTargetDiameter,
setPullerRegulationMode,
setPullerForward,
setPullerGearRatio,
setSpoolAutomaticRequiredMeters,
setSpoolAutomaticAction,
setSpoolRegulationMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
setSpoolForward,
setSpoolAdaptiveTensionTarget,
setSpoolAdaptiveRadiusLearningRate,
setSpoolAdaptiveMaxSpeedMultiplier,
setSpoolAdaptiveAccelerationFactor,
setSpoolAdaptiveDeaccelerationUrgencyMultiplier,
setSpeedPiKp,
setSpeedPiKi,
setConnectedMachine,
disconnectMachine,
setConnectedLaser,
disconnectLaser,
};
}
@@ -0,0 +1,35 @@
/**
* Configuration helper for Winder2 machine settings
*/
const WINDER2_XL_MODE_KEY = "winder2_xl_mode";
export const WINDER2_TRAVERSE_MAX_STANDARD = 180; // mm
export const WINDER2_TRAVERSE_MAX_XL = 385; // mm
/**
* Get the XL mode setting from localStorage
* @returns true if XL mode is enabled, false otherwise
*/
export function getWinder2XLMode(): boolean {
const stored = localStorage.getItem(WINDER2_XL_MODE_KEY);
return stored === "true";
}
/**
* Set the XL mode setting in localStorage
* @param enabled - true to enable XL mode, false to disable
*/
export function setWinder2XLMode(enabled: boolean): void {
localStorage.setItem(WINDER2_XL_MODE_KEY, enabled.toString());
}
/**
* Get the maximum traverse limit based on XL mode
* @returns the maximum traverse limit in mm
*/
export function getWinder2TraverseMax(): number {
return getWinder2XLMode()
? WINDER2_TRAVERSE_MAX_XL
: WINDER2_TRAVERSE_MAX_STANDARD;
}
@@ -39,9 +39,36 @@ export const liveValuesEventDataSchema = z.object({
/**
* Puller regulation type enum
*/
export const pullerRegulationSchema = z.enum(["Speed", "Diameter"]);
export const pullerRegulationSchema = z.enum([
"Speed",
"Diameter",
]);
export type PullerRegulation = z.infer<typeof pullerRegulationSchema>;
/**
* Gear ratio enum for winding speed
*/
export const gearRatioSchema = z.enum(["OneToOne", "OneToFive", "OneToTen"]);
export type GearRatio = z.infer<typeof gearRatioSchema>;
/**
* Get the multiplier for a gear ratio
*/
export function getGearRatioMultiplier(
gearRatio: GearRatio | undefined,
): number {
switch (gearRatio) {
case "OneToOne":
return 1.0;
case "OneToFive":
return 5.0;
case "OneToTen":
return 10.0;
default:
return 1.0;
}
}
/**
* Machine operation mode enum
*/
@@ -105,6 +132,7 @@ export const pullerStateSchema = z.object({
target_speed: z.number(),
target_diameter: z.number(),
forward: z.boolean(),
gear_ratio: gearRatioSchema,
});
/**
@@ -116,7 +144,7 @@ export const modeStateSchema = z.object({
});
/**
* Connected machine state scheme
* Connected machine state schema
*/
export const machineIdentificationSchema = z.object({
vendor: z.number(),
@@ -133,6 +161,14 @@ export const connectedMachineStateSchema = z.object({
is_available: z.boolean(),
});
/**
* PI Controller settings schema
*/
export const piSettingsSchema = z.object({
kp: z.number(),
ki: z.number(),
});
/**
* Tension arm state schema
*/
@@ -152,6 +188,7 @@ export const spoolSpeedControllerStateSchema = z.object({
adaptive_max_speed_multiplier: z.number(),
adaptive_acceleration_factor: z.number(),
adaptive_deacceleration_urgency_multiplier: z.number(),
forward: z.boolean(),
});
/**
@@ -166,6 +203,8 @@ export const stateEventDataSchema = z.object({
spool_speed_controller_state: spoolSpeedControllerStateSchema,
spool_automatic_action_state: spoolAutomaticActionStateSchema,
connected_machine_state: connectedMachineStateSchema,
connected_laser_state: connectedMachineStateSchema,
pi_settings: piSettingsSchema,
});
// ========== Event Schemas with Wrappers ==========
+8
View File
@@ -41,6 +41,7 @@ import { Aquapath1GraphPage } from "@/machines/aquapath/aquapath1/Aquapath1Graph
import { z } from "zod";
import { Laser1PresetsPage } from "@/machines/laser/laser1/Laser1PresetsPage";
import { Laser1SettingsPage } from "@/machines/laser/laser1/Laser1Settings";
// make a route tree like this
// _mainNavigation/machines/winder2/$serial/control
// _mainNavigation/configuration/a
@@ -156,6 +157,12 @@ export const laser1PresetsRoute = createRoute({
component: () => <Laser1PresetsPage />,
});
export const laser1SettingsRoute = createRoute({
getParentRoute: () => laser1SerialRoute,
path: "settings",
component: () => <Laser1SettingsPage />,
});
export const mock1SerialRoute = createRoute({
getParentRoute: () => machinesRoute,
path: "mock1/$serial",
@@ -312,6 +319,7 @@ export const rootTree = RootRoute.addChildren([
laser1ControlRoute,
laser1GraphsRoute,
laser1PresetsRoute,
laser1SettingsRoute,
]),
aquapath1SerialRoute.addChildren([
+1
View File
@@ -64,6 +64,7 @@
libpcap
nodejs_22
nodePackages.npm
lldb
];
hardeningDisable = [ "fortify" ];
+4 -1
View File
@@ -15,12 +15,15 @@ in
consoleMode = "max"; # Use the highest available resolution
};
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages-rt_latest;
boot.kernelPackages = pkgs.linuxPackages_6_13;
boot.kernelModules = [ "i915" ];
boot.kernelParams = [
# Realtime Preemption
"preempt=full"
# Graphical
"logo.nologo" # Remove kernel logo during boot
+1 -4
View File
@@ -55,10 +55,6 @@ opentelemetry-otlp = { version = "0.30.0", features = [
], optional = true }
tonic = { version = "0.13.1", optional = true }
#[target.'cfg(not(target_env = "msvc"))'.dependencies]
#tikv-jemallocator = { version = "0.6", features = ["profiling", "stats"] }
#tikv-jemalloc-ctl = "0.6"
[dev-dependencies]
approx = "0.5.1"
textplots = "0.8.7"
@@ -77,6 +73,7 @@ tracing-otel = [
"dep:tonic",
]
mock-machine = []
memory-locking = []
io-uring = []
development-build = []
+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 -78
View File
@@ -1,82 +1,23 @@
use super::setup::setup_loop;
use crate::{
app_state::AppState,
panic::{PanicDetails, send_panic},
socketio::main_namespace::{
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
},
};
use control_core::{
ethercat::interface_discovery::discover_ethercat_interface,
socketio::namespace::NamespaceCacheingLogic,
};
use smol::channel::Sender;
use std::sync::Arc;
use control_core::ethercat::interface_discovery::discover_ethercat_interface;
use smol::{Task, Timer};
use std::time::Duration;
pub fn init_ethercat(
thread_panic_tx: Sender<PanicDetails>,
app_state: Arc<AppState>,
) -> Result<(), anyhow::Error> {
let thread_panic_tx_clone = thread_panic_tx;
let app_state_clone = app_state;
std::thread::Builder::new()
.name("init_ethercat".to_string())
.spawn(move || {
send_panic(thread_panic_tx_clone.clone());
// Notify client via socketio
let app_state_socketio = app_state_clone.clone();
smol::block_on(async move {
let main_namespace = &mut app_state_socketio
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Discovering(true).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
});
// tries to find a suitable interface in a loop
let interface = smol::block_on(async {
loop {
match discover_ethercat_interface().await {
Ok(interface) => {
tracing::info!("Found working interface: {}", interface);
break interface;
}
Err(_) => {
tracing::warn!("No working interface found, retrying...");
// wait 1 seconds before retrying
smol::Timer::after(std::time::Duration::from_secs(1)).await;
}
}
}
});
// Notify client via socketio
let interface_clone = interface.clone();
let app_state_socketio = app_state_clone.clone();
smol::block_on(async move {
let main_namespace = &mut app_state_socketio
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = EthercatInterfaceDiscoveryEvent::Done(interface_clone).build();
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
});
if let Err(e) = smol::block_on(setup_loop(
thread_panic_tx_clone,
&interface,
app_state_clone,
)) {
tracing::error!("EtherCAT setup failed: {:?}", e);
pub async fn find_ethercat_interface() -> Result<String, anyhow::Error> {
loop {
match discover_ethercat_interface().await {
Ok(interface) => {
tracing::info!("Found EtherCAT Interface at: {}", interface);
return Ok(interface);
}
})?;
Err(e) => {
tracing::warn!("No working interface found: {}. Retrying...", e);
Timer::after(Duration::from_secs(1)).await;
}
}
}
}
Ok(())
// Returns a Future to the interface
pub fn start_interface_discovery() -> Task<Result<String, anyhow::Error>> {
smol::spawn(find_ethercat_interface())
}
+1
View File
@@ -1,3 +1,4 @@
pub mod config;
pub mod ethercat_discovery_info;
pub mod init;
pub mod setup;
+13 -34
View File
@@ -1,6 +1,5 @@
use crate::app_state::EthercatSetup;
use crate::machines::registry::MACHINE_REGISTRY;
use crate::panic::{PanicDetails, send_panic};
use crate::socketio::main_namespace::MainNamespaceEvents;
use crate::socketio::main_namespace::ethercat_devices_event::EthercatDevicesEventBuilder;
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
@@ -18,19 +17,14 @@ use control_core::machines::new::MachineNewHardwareEthercat;
use control_core::realtime::{set_core_affinity, set_realtime_priority};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::devices::devices_from_subdevices;
use ethercrab::std::ethercat_now;
use ethercrab::std::{ethercat_now, tx_rx_task};
use ethercrab::{MainDevice, MainDeviceConfig, PduStorage, RetryBehaviour, Timeouts};
use smol::channel::Sender;
use std::{sync::Arc, time::Duration};
const SM_OUTPUT: u16 = 0x1C32;
const SM_INPUT: u16 = 0x1C33;
pub async fn setup_loop(
thread_panic_tx: Sender<PanicDetails>,
interface: &str,
app_state: Arc<AppState>,
) -> Result<(), anyhow::Error> {
pub async fn setup_loop(interface: &str, app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
tracing::info!("Starting Ethercat PDU loop");
// Erase all all setup data from `app_state`
@@ -44,12 +38,10 @@ pub async fn setup_loop(
let pdu_storage = Box::leak(Box::new(PduStorage::<MAX_FRAMES, MAX_PDU_DATA>::new()));
let (tx, rx, pdu) = pdu_storage.try_split().expect("can only split once");
let interface = interface.to_string();
let thread_panic_tx_clone = thread_panic_tx.clone();
std::thread::Builder::new()
.name("EthercatTxRxThread".to_owned())
.spawn(move || {
send_panic(thread_panic_tx_clone);
#[cfg(all(target_os = "linux", not(feature = "development-build")))]
match set_irq_affinity(&interface, 3) {
Ok(_) => tracing::info!("ethernet interrupt handler now runs on cpu:{}", 3),
@@ -60,27 +52,14 @@ pub async fn setup_loop(
let _ = set_core_affinity(3);
// Set the thread to real-time priority
#[cfg(not(feature = "development-build"))]
let _ = set_realtime_priority();
#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
{
use ethercrab::std::tx_rx_task;
use futures::executor::block_on;
let rt = smol::LocalExecutor::new();
let _ = block_on(rt.run(async {
tx_rx_task(&interface, tx, rx)
.expect("spawn TX/RX task")
.await
}));
}
#[cfg(all(target_os = "linux", feature = "io-uring"))]
{
use ethercrab::std::tx_rx_task_io_uring;
let _ = tx_rx_task_io_uring(&interface, tx, rx)
.expect("Failed to spawn TX/RX task (io_uring)");
}
let rt = smol::LocalExecutor::new();
let _ = smol::block_on(rt.run(async {
tx_rx_task(&interface, tx, rx)
.expect("spawn TX/RX task")
.await
}));
})
.expect("Building thread");
@@ -93,13 +72,13 @@ pub async fn setup_loop(
// Default 30_000us
pdu: Duration::from_micros(30_000),
// Default 10ms
eeprom: Duration::from_millis(100),
eeprom: Duration::from_millis(10),
// Default 0ms
wait_loop_delay: Duration::from_millis(0),
// Default 100ms
mailbox_echo: Duration::from_millis(1000),
mailbox_echo: Duration::from_millis(100),
// Default 1000ms
mailbox_response: Duration::from_millis(10000),
mailbox_response: Duration::from_millis(1000),
},
MainDeviceConfig {
// Default RetryBehaviour::None
+54 -40
View File
@@ -1,61 +1,81 @@
use crate::app_state::AppState;
use crate::panic::{PanicDetails, send_panic};
use crate::{app_state::AppState, ethercat::setup::setup_loop};
use bitvec::prelude::*;
use control_core::machines::connection::MachineConnection;
use control_core::realtime::{set_core_affinity, set_realtime_priority};
use smol::channel::Sender;
use control_core::realtime::set_core_affinity;
#[cfg(not(feature = "development-build"))]
use control_core::realtime::set_realtime_priority;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::time::Instant;
use tracing::{instrument, trace_span};
pub fn init_loop(
thread_panic_tx: Sender<PanicDetails>,
pub fn start_loop_thread(
interface: &str,
app_state: Arc<AppState>,
) -> Result<(), anyhow::Error> {
) -> Result<std::thread::JoinHandle<()>, std::io::Error> {
let res = smol::block_on(async { setup_loop(interface, app_state.clone()).await });
match res {
Ok(_) => tracing::info!("Successfully initialized EtherCAT network"),
Err(e) => {
tracing::error!(
"[{}::init_loop] Failed to initialize EtherCAT network \n{:?}",
module_path!(),
e
);
}
}
// Start control loop
std::thread::Builder::new()
let res = std::thread::Builder::new()
.name("loop".to_owned())
.spawn(move || {
send_panic(thread_panic_tx.clone());
let rt = smol::LocalExecutor::new();
// Set core affinity to third core
let _ = set_core_affinity(2);
// Set the thread to real-time priority
#[cfg(not(feature = "development-build"))]
if let Err(e) = set_realtime_priority() {
tracing::error!(
"[{}::init_loop] Failed to set real-time priority \n{:?}",
"[{}::init_loop] Failed to set thread to real-time priority \n{:?}",
module_path!(),
e
);
} else {
tracing::info!(
"[{}::init_loop] Real-time priority set successfully",
"[{}::init_loop] Real-time priority set successfully for current thread",
module_path!()
);
}
loop {
let res = smol::block_on(rt.run(async { loop_once(app_state.clone()).await }));
let rt = smol::LocalExecutor::new();
// Instead of creating a NEW async task every iteration of our realtime txrx loop, create it ONCE
smol::block_on(async {
rt.run(async {
loop {
if let Err(e) = loop_once(app_state.clone()).await {
tracing::error!("Loop failed\n{:?}", e);
std::process::exit(1);
}
}
})
.await;
});
if let Err(err) = res {
tracing::error!("Loop failed\n{:?}", err);
break;
/*
// If timeouts keep happenning do a spin_sleep, better then thread_sleep and allows for more deterministic timing
let elapsed = start.elapsed();
if elapsed < target_cycle {
spin_sleep::sleep(target_cycle - elapsed);
}
}
// Exit the entire program if the Loop fails (gets restarted by systemd if running on NixOS)
std::process::exit(1);
})
.map_err(|e| {
anyhow::anyhow!(
"[{}::init_loop] Failed to spawn loop thread\n{:?}",
module_path!(),
e
)
})?;
*/
Ok(())
if let Some(last_loop_start) = app_state
.performance_metrics
.read_arc_blocking()
.last_loop_start
{
tracing::info!("Failing Loop Took {:?}", last_loop_start.elapsed());
}
// Exit the entire program if the Loop fails
// gets restarted by systemd if running on NixOS, or different distro wtih the same sysd service
std::process::exit(1);
});
return res;
}
#[instrument(skip(app_state))]
@@ -128,12 +148,6 @@ pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyh
)
})?;
}
#[cfg(feature = "development-build")]
smol::Timer::after(Duration::from_micros(2000)).await;
#[cfg(not(feature = "development-build"))]
smol::Timer::after(Duration::from_micros(500)).await;
}
// execute machines
+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
+21 -1
View File
@@ -183,9 +183,24 @@ pub struct PidSettings {
pub kd: f64,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct TemperaturePidStates {
pub front: TemperaturePid,
pub middle: TemperaturePid,
pub back: TemperaturePid,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct TemperaturePid {
pub ki: f64,
pub kp: f64,
pub kd: f64,
pub zone: String,
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub struct PidSettingsStates {
pub temperature: PidSettings,
pub temperature: TemperaturePidStates,
pub pressure: PidSettings,
}
@@ -217,6 +232,7 @@ pub enum Mutation {
// Pid Configure
SetPressurePidSettings(PidSettings),
SetTemperaturePidSettings(TemperaturePid),
// Reset
ResetInverter(bool),
@@ -292,6 +308,10 @@ impl MachineApi for ExtruderV2 {
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
}
Ok(())
}
+50 -5
View File
@@ -5,7 +5,7 @@ use crate::machines::extruder1::{
api::{
ExtruderSettingsState, ExtruderV2Events, HeatingState, HeatingStates, InverterStatusState,
LiveValuesEvent, ModeState, PidSettings, PidSettingsStates, PressureState, RegulationState,
RotationState, ScrewState, StateEvent,
RotationState, ScrewState, StateEvent, TemperaturePid,
},
};
#[cfg(not(feature = "mock-machine"))]
@@ -27,6 +27,8 @@ use uom::si::thermodynamic_temperature::ThermodynamicTemperature;
#[cfg(not(feature = "mock-machine"))]
impl ExtruderV2 {
pub fn build_state_event(&mut self) -> StateEvent {
use crate::machines::extruder1::api::{TemperaturePid, TemperaturePidStates};
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
rotation_state: RotationState {
@@ -106,10 +108,25 @@ impl ExtruderV2 {
fault_occurence: self.screw_speed_controller.inverter.status.fault_occurence,
},
pid_settings: PidSettingsStates {
temperature: PidSettings {
ki: 0.0,
kp: 0.0,
kd: 0.0,
temperature: TemperaturePidStates {
front: TemperaturePid {
ki: self.temperature_controller_front.pid.get_ki(),
kp: self.temperature_controller_front.pid.get_kp(),
kd: self.temperature_controller_front.pid.get_kd(),
zone: String::from("front"),
},
middle: TemperaturePid {
ki: self.temperature_controller_middle.pid.get_ki(),
kp: self.temperature_controller_middle.pid.get_kp(),
kd: self.temperature_controller_middle.pid.get_kd(),
zone: String::from("middle"),
},
back: TemperaturePid {
ki: self.temperature_controller_back.pid.get_ki(),
kp: self.temperature_controller_back.pid.get_kp(),
kd: self.temperature_controller_back.pid.get_kd(),
zone: String::from("back"),
},
},
pressure: PidSettings {
ki: self.screw_speed_controller.pid.get_ki(),
@@ -279,4 +296,32 @@ impl ExtruderV2 {
.configure(settings.ki, settings.kp, settings.kd);
self.emit_state();
}
pub fn configure_temperature_pid(&mut self, settings: TemperaturePid) {
match settings.zone.as_str() {
"front" => {
self.temperature_controller_front.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
"middle" => {
self.temperature_controller_middle.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
"back" => {
self.temperature_controller_back.pid.configure(
settings.ki,
settings.kp,
settings.kd,
);
}
_ => tracing::warn!("Unknown zone: {}", settings.zone),
}
self.emit_state();
}
}
@@ -37,6 +37,10 @@ impl MachineApi for ExtruderV2 {
Mutation::SetPressurePidSettings(settings) => {
self.configure_pressure_pid(settings);
}
Mutation::SetTemperaturePidSettings(settings) => {
self.configure_temperature_pid(settings);
}
}
Ok(())
}
@@ -1,6 +1,6 @@
use crate::machines::extruder1::{
ExtruderV2Mode, HeatingType,
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent},
api::{ExtruderV2Events, LiveValuesEvent, ModeState, PidSettings, StateEvent, TemperaturePid},
mock::ExtruderV2,
};
use control_core::{
@@ -133,4 +133,26 @@ impl ExtruderV2 {
self.pid_settings.pressure.kd = settings.kd;
self.emit_state();
}
pub fn configure_temperature_pid(&mut self, settings: TemperaturePid) {
match settings.zone.as_str() {
"front" => {
self.pid_settings.temperature.front.ki = settings.ki;
self.pid_settings.temperature.front.kp = settings.kp;
self.pid_settings.temperature.front.kd = settings.kd;
}
"middle" => {
self.pid_settings.temperature.middle.ki = settings.ki;
self.pid_settings.temperature.middle.kp = settings.kp;
self.pid_settings.temperature.middle.kd = settings.kd;
}
"back" => {
self.pid_settings.temperature.back.ki = settings.ki;
self.pid_settings.temperature.back.kp = settings.kp;
self.pid_settings.temperature.back.kd = settings.kd;
}
_ => tracing::warn!("Unknown zone: {}", settings.zone),
}
self.emit_state();
}
}
+21 -5
View File
@@ -3,7 +3,8 @@ use crate::machines::extruder1::{
api::{
ExtruderSettingsState, ExtruderV2Namespace, HeatingState, HeatingStates,
InverterStatusState, ModeState, MotorStatusValues, PidSettings, PidSettingsStates,
PressureState, RegulationState, RotationState, ScrewState,
PressureState, RegulationState, RotationState, ScrewState, TemperaturePid,
TemperaturePidStates,
},
mock::ExtruderV2,
};
@@ -88,10 +89,25 @@ impl control_core::machines::new::MachineNewTrait for ExtruderV2 {
fault_occurence: false,
},
pid_settings: PidSettingsStates {
temperature: PidSettings {
ki: 0.0,
kp: 0.0,
kd: 0.0,
temperature: TemperaturePidStates {
front: TemperaturePid {
ki: 0.0,
kp: 0.0,
kd: 0.0,
zone: String::from("front"),
},
middle: TemperaturePid {
ki: 0.0,
kp: 0.0,
kd: 0.0,
zone: String::from("middle"),
},
back: TemperaturePid {
ki: 0.0,
kp: 0.0,
kd: 0.0,
zone: String::from("back"),
},
},
pressure: PidSettings {
ki: 0.0,
+3 -1
View File
@@ -19,8 +19,10 @@ use std::time::{Duration, Instant};
///
impl MachineAct for LaserMachine {
fn act(&mut self, now: Instant) {
// sync the diameter
self.set_measured_diameter();
self.update();
// more than 33ms have passed since last emit (30 "fps" target)
// The live values are updated approximately 60 times per second
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
self.last_measurement_emit = now;
+24 -1
View File
@@ -1,6 +1,9 @@
use super::LaserMachine;
use control_core::{
machines::api::MachineApi,
machines::{
api::MachineApi, connection::MachineCrossConnectionState,
identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
@@ -36,6 +39,8 @@ pub struct StateEvent {
pub is_default_state: bool,
/// laser state
pub laser_state: LaserState,
/// connected winder state
pub connected_winder_state: MachineCrossConnectionState,
}
impl StateEvent {
@@ -54,6 +59,13 @@ pub struct LaserState {
pub target_diameter: f64,
}
#[derive(Serialize, Debug, Clone)]
pub struct ConnectedMachineState {
/// Connected Machine
pub machine_identification_unique: Option<MachineIdentificationUnique>,
pub is_available: bool,
}
pub enum LaserEvents {
LiveValues(Event<LiveValuesEvent>),
State(Event<StateEvent>),
@@ -91,6 +103,11 @@ enum Mutation {
SetTargetDiameter(f64),
SetLowerTolerance(f64),
SetHigherTolerance(f64),
// Connect Machine
SetConnectedWinder(MachineIdentificationUnique),
// Disconnect Machine
DisconnectWinder(MachineIdentificationUnique),
}
impl NamespaceCacheingLogic<LaserEvents> for LaserMachineNamespace {
@@ -117,6 +134,12 @@ impl MachineApi for LaserMachine {
Mutation::SetTargetDiameter(target_diameter) => {
self.set_target_diameter(target_diameter);
}
Mutation::SetConnectedWinder(machine_identification_unique) => {
self.set_connected_winder(machine_identification_unique);
}
Mutation::DisconnectWinder(machine_identification_unique) => {
self.disconnect_winder(machine_identification_unique);
}
}
Ok(())
}
+89 -5
View File
@@ -1,15 +1,22 @@
use crate::{
machines::{MACHINE_LASER_V1, VENDOR_QITECH},
machines::{MACHINE_LASER_V1, VENDOR_QITECH, winder2::Winder2},
serial::devices::laser::Laser,
};
use api::{LaserEvents, LaserMachineNamespace, LaserState, LiveValuesEvent, StateEvent};
use control_core::{
machines::identification::{MachineIdentification, MachineIdentificationUnique},
machines::{
connection::{CrossConnectableMachine, MachineConnection, MachineCrossConnection},
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
socketio::namespace::NamespaceCacheingLogic,
};
use control_core_derive::Machine;
use smol::lock::RwLock;
use std::{sync::Arc, time::Instant};
use std::{
sync::{Arc, Weak},
time::Instant,
};
use uom::si::{f64::Length, length::millimeter};
pub mod act;
@@ -18,8 +25,6 @@ pub mod new;
#[derive(Debug, Machine)]
pub struct LaserMachine {
machine_identification_unique: MachineIdentificationUnique,
// drivers
laser: Arc<RwLock<Laser>>,
@@ -27,6 +32,13 @@ pub struct LaserMachine {
namespace: LaserMachineNamespace,
last_measurement_emit: Instant,
// machine connection
pub machine_manager: Weak<RwLock<MachineManager>>,
pub machine_identification_unique: MachineIdentificationUnique,
// connected machines
pub connected_winder: MachineCrossConnection<LaserMachine, Winder2>,
// laser values
diameter: Length,
x_diameter: Option<Length>,
@@ -41,6 +53,12 @@ pub struct LaserMachine {
emitted_default_state: bool,
}
impl CrossConnectableMachine<LaserMachine, Winder2> for LaserMachine {
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<LaserMachine, Winder2> {
&mut self.connected_winder
}
}
impl LaserMachine {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
@@ -74,6 +92,7 @@ impl LaserMachine {
StateEvent {
is_default_state: false,
laser_state: laser,
connected_winder_state: self.connected_winder.to_state(),
}
}
@@ -85,6 +104,7 @@ impl LaserMachine {
lower_tolerance: self.laser_target.lower_tolerance.get::<millimeter>(),
target_diameter: self.laser_target.diameter.get::<millimeter>(),
},
connected_winder_state: self.connected_winder.to_state(),
};
self.namespace.emit(LaserEvents::State(state.build()));
@@ -150,6 +170,70 @@ impl LaserMachine {
}
}
/// implement machine connection
impl LaserMachine {
/// set connected winder
pub fn set_connected_winder(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
Winder2::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_winder
.set_connected_machine(&machine_identification_unique);
self.connected_winder.reverse_connect();
self.emit_state();
}
/// disconnect winder
pub fn disconnect_winder(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
Winder2::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_winder.reverse_disconnect();
self.connected_winder.disconnect();
self.emit_state();
}
}
impl LaserMachine {
fn set_measured_diameter(&mut self) {
let diameter = smol::block_on(async {
self.laser
.read()
.await
.get_data()
.await
.map(|laser_data| laser_data.diameter.get::<millimeter>())
});
if let Some(slot) = self.get_cross_connection().connected_machine.upgrade() {
let slot = slot.lock_blocking();
if let MachineConnection::Connected(machine) = &slot.machine_connection {
let mut winder = machine.lock_blocking();
winder
.puller_speed_controller
.set_measured_diameter(diameter.unwrap_or(0.0));
}
}
}
}
#[derive(Debug, Clone)]
pub struct LaserTarget {
diameter: Length,
+7
View File
@@ -4,6 +4,7 @@ use crate::serial::{devices::laser::Laser, registry::SERIAL_DEVICE_REGISTRY};
use super::{LaserMachine, LaserTarget, api::LaserMachineNamespace};
use anyhow::Error;
use control_core::machines::connection::MachineCrossConnection;
use control_core::machines::new::{MachineNewHardware, MachineNewTrait};
use uom::ConstZero;
use uom::si::{f64::Length, length::millimeter};
@@ -34,6 +35,7 @@ impl MachineNewTrait for LaserMachine {
lower_tolerance: Length::new::<millimeter>(0.05),
diameter: Length::new::<millimeter>(1.75),
};
let mut laser_machine = Self {
machine_identification_unique: params.get_machine_identification_unique(),
laser,
@@ -43,6 +45,11 @@ impl MachineNewTrait for LaserMachine {
last_measurement_emit: Instant::now(),
laser_target,
emitted_default_state: false,
machine_manager: params.machine_manager.clone(),
connected_winder: MachineCrossConnection::new(
params.machine_manager.clone(),
&params.get_machine_identification_unique(),
),
diameter: Length::ZERO,
x_diameter: None,
y_diameter: None,
+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
+111 -31
View File
@@ -1,30 +1,63 @@
use super::{Winder2, Winder2Mode, puller_speed_controller::PullerRegulationMode};
use control_core::{
machines::{
api::MachineApi, connection::MachineCrossConnectionState,
identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
#[cfg(feature = "mock-machine")]
mod winder2_imports {
pub use super::super::Winder2Mode;
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use serde::Deserialize;
pub use serde::Serialize;
pub use std::{sync::Arc, time::Duration};
pub use control_core::{
machines::{
connection::MachineCrossConnectionState, identification::MachineIdentificationUnique,
},
},
};
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
},
};
pub use control_core_derive::BuildEvent;
pub use smol::lock::Mutex;
pub use tracing::instrument;
}
use control_core_derive::BuildEvent;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::lock::Mutex;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use tracing::instrument;
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::puller_speed_controller::{GearRatio, PullerRegulationMode};
pub use super::super::{Winder2, Winder2Mode};
pub use control_core::{
machines::{
api::MachineApi, connection::MachineCrossConnectionState,
identification::MachineIdentificationUnique,
},
socketio::{
event::{Event, GenericEvent},
namespace::{
CacheFn, CacheableEvents, Namespace, NamespaceCacheingLogic, cache_duration,
cache_first_and_last_event,
},
},
};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub use control_core_derive::BuildEvent;
pub use serde::{Deserialize, Serialize};
pub use serde_json::Value;
pub use smol::lock::Mutex;
pub use std::{
sync::Arc,
time::{Duration, Instant},
};
pub use tracing::instrument;
}
pub use winder2_imports::*;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum Mode {
#[default]
Standby,
Hold,
Pull,
@@ -54,7 +87,7 @@ impl From<Mode> for Winder2Mode {
}
#[derive(Deserialize, Serialize)]
enum Mutation {
pub enum Mutation {
// Traverse
/// Position in mm from home point
SetTraverseLimitOuter(f64),
@@ -76,11 +109,13 @@ enum Mutation {
SetPullerTargetSpeed(f64),
SetPullerTargetDiameter(f64),
SetPullerForward(bool),
SetPullerGearRatio(GearRatio),
// Spool Speed Controller
SetSpoolRegulationMode(super::spool_speed_controller::SpoolSpeedControllerType),
SetSpoolMinMaxMinSpeed(f64),
SetSpoolMinMaxMaxSpeed(f64),
SetSpoolForward(bool),
// Adaptive Spool Speed Controller Parameters
SetSpoolAdaptiveTensionTarget(f64),
@@ -100,11 +135,16 @@ enum Mutation {
// Mode
SetMode(Mode),
// Pid Configure
SetSpeedPiSettings(PiSettings),
// Connected Machine
SetConnectedMachine(MachineIdentificationUnique),
SetConnectedLaser(MachineIdentificationUnique),
// Disconnect Machine
DisconnectMachine(MachineIdentificationUnique),
DisconnectLaser(MachineIdentificationUnique),
}
#[derive(Serialize, Debug, Clone, Default)]
@@ -142,11 +182,15 @@ pub struct StateEvent {
pub tension_arm_state: TensionArmState,
/// spool speed controller state
pub spool_speed_controller_state: SpoolSpeedControllerState,
/// connected machine state
/// connected machine state (Buffer)
pub connected_machine_state: MachineCrossConnectionState,
/// connected laser state
pub connected_laser_state: MachineCrossConnectionState,
/// pi settings
pub pi_settings: PiSettingsStates,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct TraverseState {
/// min position in mm
pub limit_inner: f64,
@@ -180,7 +224,7 @@ pub struct TraverseState {
pub can_go_home: bool,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct PullerState {
/// regulation type
pub regulation: PullerRegulationMode,
@@ -190,22 +234,25 @@ pub struct PullerState {
pub target_diameter: f64,
/// forward rotation direction
pub forward: bool,
/// gear ratio for winding speed
pub gear_ratio: GearRatio,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub enum SpoolAutomaticActionMode {
#[default]
NoAction,
Pull,
Hold,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolAutomaticActionState {
pub spool_required_meters: f64,
pub spool_automatic_action_mode: SpoolAutomaticActionMode,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct ModeState {
/// mode
pub mode: Mode,
@@ -219,7 +266,7 @@ pub struct TensionArmState {
pub zeroed: bool,
}
#[derive(Serialize, Debug, Clone)]
#[derive(Serialize, Debug, Clone, Default)]
pub struct SpoolSpeedControllerState {
/// regulation mode
pub regulation_mode: super::spool_speed_controller::SpoolSpeedControllerType,
@@ -237,6 +284,27 @@ pub struct SpoolSpeedControllerState {
pub adaptive_acceleration_factor: f64,
/// deacceleration urgency multiplier for adaptive mode
pub adaptive_deacceleration_urgency_multiplier: f64,
/// forward rotation direction
pub forward: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct ConnectedMachineState {
/// Connected Machine
pub machine_identification_unique: Option<MachineIdentificationUnique>,
pub is_available: bool,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct PiSettings {
pub kp: f64,
pub ki: f64,
}
#[derive(Serialize, Debug, Clone)]
pub struct PiSettingsStates {
pub kp: f64,
pub ki: f64,
}
pub enum Winder2Events {
@@ -279,6 +347,7 @@ impl CacheableEvents<Self> for Winder2Events {
}
}
#[cfg(not(feature = "mock-machine"))]
impl MachineApi for Winder2 {
fn api_mutate(&mut self, request_body: Value) -> Result<(), anyhow::Error> {
let mutation: Mutation = serde_json::from_value(request_body)?;
@@ -296,9 +365,11 @@ impl MachineApi for Winder2 {
Mutation::SetPullerTargetSpeed(value) => self.puller_set_target_speed(value),
Mutation::SetPullerTargetDiameter(_) => todo!(),
Mutation::SetPullerForward(value) => self.puller_set_forward(value),
Mutation::SetPullerGearRatio(gear_ratio) => self.puller_set_gear_ratio(gear_ratio),
Mutation::SetSpoolRegulationMode(mode) => self.spool_set_regulation_mode(mode),
Mutation::SetSpoolMinMaxMinSpeed(speed) => self.spool_set_minmax_min_speed(speed),
Mutation::SetSpoolMinMaxMaxSpeed(speed) => self.spool_set_minmax_max_speed(speed),
Mutation::SetSpoolForward(value) => self.spool_set_forward(value),
Mutation::SetSpoolAdaptiveTensionTarget(value) => {
self.spool_set_adaptive_tension_target(value)
}
@@ -326,6 +397,15 @@ impl MachineApi for Winder2 {
Mutation::DisconnectMachine(machine_identification_unique) => {
self.disconnect_buffer(machine_identification_unique)
}
Mutation::SetConnectedLaser(machine_identification_unique) => {
self.set_connected_laser(machine_identification_unique)
}
Mutation::DisconnectLaser(machine_identification_unique) => {
self.disconnect_laser(machine_identification_unique)
}
Mutation::SetSpeedPiSettings(settings) => {
self.configure_pi_controller(settings);
}
}
Ok(())
}
+96 -23
View File
@@ -1,26 +1,35 @@
use std::time::Instant;
#[cfg(not(feature = "mock-machine"))]
mod winder2_imports {
pub use super::super::puller_speed_controller::PullerRegulationMode;
use crate::machines::buffer1::BufferV1;
pub use super::super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
pub use crate::machines::buffer1::BufferV1;
pub use crate::machines::laser::LaserMachine;
pub use crate::machines::winder2::api::{PiSettings, PiSettingsStates};
use super::puller_speed_controller::PullerRegulationMode;
use super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
use api::{
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode, SpoolAutomaticActionState,
SpoolSpeedControllerState, StateEvent, TensionArmState, TraverseState, Winder2Events,
};
use control_core::socketio::event::BuildEvent;
use control_core::{
machines::identification::MachineIdentificationUnique,
socketio::namespace::NamespaceCacheingLogic, uom_extensions::velocity::meter_per_minute,
};
pub use api::{
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode,
SpoolAutomaticActionState, SpoolSpeedControllerState, StateEvent, TensionArmState,
TraverseState, Winder2Events,
};
pub use control_core::socketio::event::BuildEvent;
pub use control_core::{
machines::identification::MachineIdentificationUnique,
socketio::namespace::NamespaceCacheingLogic, uom_extensions::velocity::meter_per_minute,
};
pub use std::time::Instant;
pub use uom::si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
length::{meter, millimeter},
};
}
use uom::si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
length::{meter, millimeter},
};
#[cfg(not(feature = "mock-machine"))]
pub use winder2_imports::*;
#[cfg(not(feature = "mock-machine"))]
impl Winder2 {
/// Implement Spool
/// called by `act`
@@ -30,9 +39,17 @@ impl Winder2 {
&self.tension_arm,
&self.puller_speed_controller,
);
// Apply direction based on forward setting
let directed_angular_velocity = if self.spool_speed_controller.get_forward() {
angular_velocity
} else {
-angular_velocity
};
let steps_per_second = self
.spool_step_converter
.angular_velocity_to_steps(angular_velocity);
.angular_velocity_to_steps(directed_angular_velocity);
let _ = self.spool.set_speed(steps_per_second);
}
@@ -167,15 +184,19 @@ impl Winder2 {
.puller_speed_controller
.converter
.steps_to_angular_velocity(steps_per_second as f64);
let puller_speed = self
let motor_speed = self
.puller_speed_controller
.angular_velocity_to_speed(angular_velocity);
// Calculate spool RPM from current motor steps
// Divide by gear ratio to get actual puller/material speed
let puller_speed = motor_speed / self.puller_speed_controller.get_gear_ratio().multiplier();
// Calculate spool RPM from current motor steps (always positive regardless of direction)
let spool_rpm = self
.spool_step_converter
.steps_to_angular_velocity(self.spool.get_speed() as f64)
.get::<revolution_per_minute>();
.get::<revolution_per_minute>()
.abs();
let live_values = LiveValuesEvent {
traverse_position: self
@@ -235,6 +256,7 @@ impl Winder2 {
.target_diameter
.get::<millimeter>(),
forward: self.puller_speed_controller.forward,
gear_ratio: self.puller_speed_controller.gear_ratio,
},
mode_state: ModeState {
mode: self.mode.clone().into(),
@@ -266,12 +288,18 @@ impl Winder2 {
adaptive_deacceleration_urgency_multiplier: self
.spool_speed_controller
.get_adaptive_deacceleration_urgency_multiplier(),
forward: self.spool_speed_controller.get_forward(),
},
spool_automatic_action_state: SpoolAutomaticActionState {
spool_required_meters: self.spool_automatic_action.target_length.get::<meter>(),
spool_automatic_action_mode: self.spool_automatic_action.mode.clone(),
},
connected_machine_state: self.connected_buffer.to_state(),
connected_laser_state: self.connected_laser.to_state(),
pi_settings: PiSettingsStates {
kp: self.puller_speed_controller.p_dead_controller.get_kp(),
ki: self.puller_speed_controller.p_dead_controller.get_ki(),
},
}
}
@@ -397,6 +425,12 @@ impl Winder2 {
self.emit_state();
}
/// Set gear ratio for winding speed
pub fn puller_set_gear_ratio(&mut self, gear_ratio: super::puller_speed_controller::GearRatio) {
self.puller_speed_controller.set_gear_ratio(gear_ratio);
self.emit_state();
}
// Spool Speed Controller API methods
pub fn spool_set_regulation_mode(
&mut self,
@@ -462,6 +496,12 @@ impl Winder2 {
self.emit_state();
}
/// Set forward rotation direction
pub fn spool_set_forward(&mut self, forward: bool) {
self.spool_speed_controller.set_forward(forward);
self.emit_state();
}
/// implement machine connection
/// set connected buffer
pub fn set_connected_buffer(
@@ -497,6 +537,39 @@ impl Winder2 {
self.connected_buffer.reverse_disconnect();
self.connected_buffer.disconnect();
self.emit_state();
}
/// set connected laser
pub fn set_connected_laser(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
LaserMachine::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_laser
.set_connected_machine(&machine_identification_unique);
self.connected_laser.reverse_connect();
self.emit_state();
}
/// disconnect laser
pub fn disconnect_laser(&mut self, machine_identification_unique: MachineIdentificationUnique) {
if !matches!(
machine_identification_unique.machine_identification,
LaserMachine::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_laser.reverse_disconnect();
self.connected_laser.disconnect();
self.emit_state();
}
}
+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)
}
}
+74 -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 uom::ConstZero;
pub use uom::si::{length::meter, length::millimeter, velocity::meter_per_second};
pub use crate::machines::{buffer1::BufferV1, laser::LaserMachine, winder2::api::PiSettings, MACHINE_WINDER_V1, VENDOR_QITECH};
}
pub use winder2_imports::*;
#[derive(Debug)]
pub struct SpoolAutomaticAction {
@@ -51,6 +61,18 @@ pub struct SpoolAutomaticAction {
pub mode: SpoolAutomaticActionMode,
}
impl Default for SpoolAutomaticAction {
fn default() -> Self {
SpoolAutomaticAction {
progress: Length::new::<uom::si::length::meter>(0.0),
progress_last_check: Instant::now(),
target_length: Length::new::<uom::si::length::meter>(0.0),
mode: SpoolAutomaticActionMode::default(),
}
}
}
#[cfg(not(feature = "mock-machine"))]
#[derive(Debug, Machine)]
pub struct Winder2 {
// drivers
@@ -74,6 +96,7 @@ pub struct Winder2 {
// connected machines
pub connected_buffer: MachineCrossConnection<Winder2, BufferV1>,
pub connected_laser: MachineCrossConnection<Winder2, LaserMachine>,
// mode
pub mode: Winder2Mode,
@@ -96,12 +119,20 @@ pub struct Winder2 {
emitted_default_state: bool,
}
#[cfg(not(feature = "mock-machine"))]
impl CrossConnectableMachine<Winder2, BufferV1> for Winder2 {
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<Winder2, BufferV1> {
&mut self.connected_buffer
}
}
#[cfg(not(feature = "mock-machine"))]
impl CrossConnectableMachine<Winder2, LaserMachine> for Winder2 {
fn get_cross_connection(&mut self) -> &mut MachineCrossConnection<Winder2, LaserMachine> {
&mut self.connected_laser
}
}
impl Winder2 {
pub const MACHINE_IDENTIFICATION: MachineIdentification = MachineIdentification {
vendor: VENDOR_QITECH,
@@ -277,6 +308,7 @@ impl Winder2 {
pub fn calculate_spool_auto_progress_(&mut self, now: Instant) {
// Calculate time elapsed since last progress check (in minutes)
let dt = now
.duration_since(self.spool_automatic_action.progress_last_check)
.as_secs_f64();
@@ -306,6 +338,17 @@ impl Winder2 {
}
}
impl Winder2 {
pub fn configure_pi_controller(&mut self, settings: PiSettings) {
// Implement pid to controll speed of winder
self.puller_speed_controller
.p_dead_controller
.configure(settings.kp, settings.ki);
self.emit_state();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Winder2Mode {
Standby,
@@ -368,12 +411,14 @@ impl From<Winder2Mode> for PullerMode {
}
}
#[cfg(not(feature = "mock-machine"))]
impl std::fmt::Display for Winder2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Winder2")
}
}
#[cfg(not(feature = "mock-machine"))]
#[cfg(test)]
mod tests {
use super::*;
+59 -50
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)?;
@@ -177,14 +190,6 @@ impl MachineNewTrait for Winder2 {
let mode = Winder2Mode::Standby;
let machine_id = params
.device_group
.first()
.expect("device group must have at least one device")
.device_machine_identification
.machine_identification_unique
.clone();
let mut new = Self {
traverse: StepperVelocityEL70x1::new(el7031.clone(), EL7031StepperPort::STM1),
traverse_end_stop: DigitalInput::new(el7031, EL7031DigitalInputPort::DI1),
@@ -229,11 +234,15 @@ impl MachineNewTrait for Winder2 {
mode: super::api::SpoolAutomaticActionMode::NoAction,
},
machine_manager: params.machine_manager.clone(),
machine_identification_unique: machine_id,
machine_identification_unique: params.get_machine_identification_unique(),
connected_buffer: MachineCrossConnection::new(
params.machine_manager.clone(),
&params.get_machine_identification_unique(),
),
connected_laser: MachineCrossConnection::new(
params.machine_manager.clone(),
&params.get_machine_identification_unique(),
),
};
// initalize events
@@ -1,7 +1,12 @@
use std::time::Instant;
use std::{
time::{Duration, Instant},
};
use control_core::{
controllers::second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController,
controllers::{
deadtime_pi_controller::TimeAgnosticDeadTimePiController,
second_degree_motion::linear_jerk_speed_controller::LinearJerkSpeedController,
},
converters::linear_step_converter::LinearStepConverter,
uom_extensions::{
acceleration::meter_per_minute_per_second, jerk::meter_per_minute_per_second_squared,
@@ -11,22 +16,54 @@ use control_core::{
use serde::{Deserialize, Serialize};
use uom::{
ConstZero,
si::f64::{Acceleration, AngularVelocity, Jerk, Length, Velocity},
si::{
f64::{Acceleration, AngularVelocity, Jerk, Length, Velocity},
length::{meter, millimeter},
time::second,
},
};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum GearRatio {
OneToOne,
OneToFive,
OneToTen,
}
impl GearRatio {
/// Get the speed multiplier for this gear ratio
pub fn multiplier(&self) -> f64 {
match self {
GearRatio::OneToOne => 1.0,
GearRatio::OneToFive => 5.0,
GearRatio::OneToTen => 10.0,
}
}
}
impl Default for GearRatio {
fn default() -> Self {
GearRatio::OneToOne
}
}
#[derive(Debug)]
pub struct PullerSpeedController {
enabled: bool,
pub target_speed: Velocity,
pub target_diameter: Length,
pub measured_diameter: Length,
pub regulation_mode: PullerRegulationMode,
/// Forward rotation direction. If false, applies negative sign to speed
pub forward: bool,
/// Gear ratio for winding speed (1:5 or 1:10)
pub gear_ratio: GearRatio,
/// Linear acceleration controller to dampen speed change
acceleration_controller: LinearJerkSpeedController,
/// Converter for linear to angular transformations
pub converter: LinearStepConverter,
pub last_speed: Velocity,
pub p_dead_controller: TimeAgnosticDeadTimePiController,
}
impl PullerSpeedController {
@@ -43,8 +80,10 @@ impl PullerSpeedController {
enabled: false,
target_speed,
target_diameter,
measured_diameter: Length::ZERO,
regulation_mode: PullerRegulationMode::Speed,
forward: true,
gear_ratio: GearRatio::default(),
acceleration_controller: LinearJerkSpeedController::new_simple(
Some(speed),
acceleration,
@@ -52,6 +91,11 @@ impl PullerSpeedController {
),
converter,
last_speed: Velocity::ZERO,
p_dead_controller: TimeAgnosticDeadTimePiController::new(
0.1 / 10000.0,
0.0,
Duration::ZERO,
),
}
}
@@ -67,6 +111,10 @@ impl PullerSpeedController {
self.target_diameter = target;
}
pub fn set_measured_diameter(&mut self, target: f64) {
self.measured_diameter = Length::new::<millimeter>(target);
}
pub const fn set_regulation_mode(&mut self, regulation: PullerRegulationMode) {
self.regulation_mode = regulation;
}
@@ -75,15 +123,41 @@ impl PullerSpeedController {
self.forward = forward;
}
pub const fn set_gear_ratio(&mut self, gear_ratio: GearRatio) {
self.gear_ratio = gear_ratio;
}
pub const fn get_gear_ratio(&self) -> GearRatio {
self.gear_ratio
}
fn update_speed(&mut self, t: Instant) -> Velocity {
let speed = match self.enabled {
true => match self.regulation_mode {
PullerRegulationMode::Speed => self.target_speed,
PullerRegulationMode::Diameter => unimplemented!(),
},
false => Velocity::ZERO,
if !self.enabled {
return Velocity::ZERO;
}
let base_speed = match self.regulation_mode {
PullerRegulationMode::Speed => {
let directional_target = if self.forward {
self.target_speed
} else {
-self.target_speed
};
self.acceleration_controller.update(directional_target, t)
}
PullerRegulationMode::Diameter => {
let diameter_speed = self.speed_from_diameter(t);
if self.forward {
diameter_speed
} else {
-diameter_speed
}
}
};
// Apply gear ratio multiplier
let speed = base_speed * self.gear_ratio.multiplier();
let speed = if self.forward { speed } else { -speed };
let speed = self.acceleration_controller.update(speed, t);
@@ -92,6 +166,49 @@ impl PullerSpeedController {
speed
}
fn speed_from_diameter(&mut self, now: Instant) -> Velocity {
// get current error
let error =
self.measured_diameter.get::<millimeter>() - self.target_diameter.get::<millimeter>();
// calculate/set deadtime based of speed and distance
let deadtime = Self::calc_deadtime(self.last_speed, Length::new::<meter>(2.0));
self.p_dead_controller.set_dead(deadtime);
// get speed change from p controller
let speed_change = self.p_dead_controller.update(error, now);
// apply speed change to target speed
let next_speed = self.last_speed + Velocity::new::<meter_per_minute>(speed_change);
// clamp the speed to 0 - 50 for safety
Self::clamp_speed(
next_speed,
Velocity::new::<meter_per_minute>(0.0),
Velocity::new::<meter_per_minute>(50.0),
)
}
fn calc_deadtime(speed: Velocity, distance: Length) -> Duration {
// check if speed is 0 => This should not happen
if speed <= Velocity::ZERO {
return Duration::from_secs(180);
}
// get the duration based of distance and speed
let secs = (distance / speed).get::<second>().max(0.0);
Duration::from_secs_f64(secs)
}
fn clamp_speed(speed: Velocity, min: Velocity, max: Velocity) -> Velocity {
if speed < min {
min
} else if speed > max {
max
} else {
speed
}
}
pub fn speed_to_angular_velocity(&self, speed: Velocity) -> AngularVelocity {
// Use the converter to transform from linear velocity to angular velocity
self.converter.velocity_to_angular_velocity(speed)
@@ -112,8 +229,9 @@ impl PullerSpeedController {
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum PullerRegulationMode {
#[default]
Speed,
Diameter,
}
@@ -10,8 +10,9 @@ use serde::{Deserialize, Serialize};
use std::time::Instant;
use uom::si::f64::AngularVelocity;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub enum SpoolSpeedControllerType {
#[default]
Adaptive,
MinMax,
}
@@ -21,6 +22,7 @@ pub struct SpoolSpeedController {
adaptive_controller: AdaptiveSpoolSpeedController,
minmax_controller: MinMaxSpoolSpeedController,
r#type: SpoolSpeedControllerType,
forward: bool,
}
impl Default for SpoolSpeedController {
@@ -35,6 +37,7 @@ impl SpoolSpeedController {
adaptive_controller: AdaptiveSpoolSpeedController::new(),
minmax_controller: MinMaxSpoolSpeedController::new(),
r#type: SpoolSpeedControllerType::Adaptive,
forward: true,
}
}
@@ -180,4 +183,12 @@ impl SpoolSpeedController {
self.adaptive_controller
.set_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
}
pub const fn get_forward(&self) -> bool {
self.forward
}
pub const fn set_forward(&mut self, forward: bool) {
self.forward = forward;
}
}
+44 -83
View File
@@ -1,110 +1,71 @@
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
use app_state::AppState;
use r#loop::init_loop;
use futures::{FutureExt, select};
#[cfg(feature = "mock-machine")]
use mock::init::init_mock;
use rest::init::init_api;
#[cfg(not(feature = "mock-machine"))]
use serial::init::init_serial;
#[cfg(feature = "mock-machine")]
pub mod mock;
use crate::panic::init_panic_handling;
use app_state::AppState;
use ethercat::{
ethercat_discovery_info::{send_ethercat_discovering, send_ethercat_found},
init::start_interface_discovery,
};
use r#loop::start_loop_thread;
use rest::init::start_api_thread;
use serial::init::{handle_serial_device_hotplug, start_serial_discovery};
use socketio::queue::start_socketio_queue;
use std::sync::Arc;
use crate::panic::init_panic;
use crate::socketio::queue::init_socketio_queue;
pub mod app_state;
pub mod ethercat;
pub mod logging;
pub mod r#loop;
pub mod machines;
#[cfg(feature = "mock-machine")]
pub mod mock;
pub mod panic;
pub mod performance_metrics;
pub mod rest;
pub mod serial;
pub mod socketio;
use crate::ethercat::init::init_ethercat;
fn main() {
// Initialize panic handling
let thread_panic_tx = init_panic();
logging::init_tracing();
tracing::info!("Tracing initialized successfully");
if let Err(e) = control_core::realtime::lock_memory() {
tracing::error!("[{}::main] Failed to lock memory: {:?}", module_path!(), e);
} else {
tracing::info!("[{}::main] Memory locked successfully", module_path!());
}
init_panic_handling();
let app_state = Arc::new(AppState::new());
// Spawn init thread
let init_thread = std::thread::Builder::new()
.name("init".to_string())
.spawn({
let thread_panic_tx = thread_panic_tx;
let app_state = app_state;
move || {
#[cfg(feature = "dhat-heap")]
init_dhat_heap_profiling();
let _ = start_api_thread(app_state.clone());
let mut socketio_fut = start_socketio_queue(app_state.clone()).fuse();
let mut ethercat_fut = start_interface_discovery().fuse();
let mut serial_fut = start_serial_discovery().fuse();
init_socketio_queue(thread_panic_tx.clone(), app_state.clone());
init_api(thread_panic_tx.clone(), app_state.clone())
.expect("Failed to initialize API");
init_loop(thread_panic_tx.clone(), app_state.clone())
.expect("Failed to initialize loop");
#[cfg(feature = "mock-machine")]
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
#[cfg(not(feature = "mock-machine"))]
init_serial(thread_panic_tx.clone(), app_state.clone())
.expect("Failed to initialize Serial");
#[cfg(not(feature = "mock-machine"))]
init_ethercat(thread_panic_tx.clone(), app_state)
.expect("Failed to initialize EtherCAT");
}
})
.expect("Failed to spawn init thread");
// Wait for init thread to complete
init_thread.join().expect("Init thread panicked");
// Keep the main thread alive indefinitely
// The program should only exit via panic handling or external signals
loop {
// better way to "sleep", uses basically
std::thread::park();
}
}
#[cfg(feature = "dhat-heap")]
fn init_dhat_heap_profiling() {
use std::{process::exit, time::Duration};
let profiler = dhat::Profiler::new_heap();
smol::block_on(async { send_ethercat_discovering(app_state.clone()).await });
smol::block_on(async {
let dhat_analysis_time = std::time::Duration::from_secs(60);
let dhat_analysis_start = std::time::Instant::now();
tracing::info!(
"Starting dhat heap profiler for {} seconds",
dhat_analysis_time.as_secs()
);
loop {
use std::time::Duration;
smol::Timer::after(Duration::from_secs(1)).await;
// lets the async runtime decide which future to run next
select! {
res = ethercat_fut => {
tracing::info!("EtherCAT task finished: {:?}", res);
match res {
Ok(interface) =>
{
send_ethercat_found(app_state.clone(), &interface).await;
let _ = start_loop_thread(&interface, app_state.clone());
},
Err(_) => (),
};
// if `dhat-heap` is enabled, we will analyze the heap for 60 seconds and then exit
if dhat_analysis_start.elapsed() > dhat_analysis_time {
drop(profiler);
exit(0)
},
res = serial_fut => {
let _ = handle_serial_device_hotplug(app_state.clone(),res).await;
serial_fut = start_serial_discovery().fuse();
},
res = socketio_fut => {
// In theory it should never finish
tracing::warn!("SocketIO task finished: {:?}", res);
},
}
}
})
});
}
+35 -3
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
{
+32 -162
View File
@@ -1,173 +1,43 @@
use smol::channel::{Sender, unbounded};
use std::process::exit;
use tracing::error;
use std::{backtrace::Backtrace, panic::PanicHookInfo, thread};
#[derive(Debug, Clone)]
pub struct PanicDetails {
pub thread_name: Option<String>,
pub message: Option<String>,
pub location: Option<PanicDetailsLocation>,
pub backtrace: String,
}
fn panic_hook(panic_info: &PanicHookInfo) {
let backtrace = Backtrace::capture().to_string();
#[derive(Clone, Debug)]
pub struct PanicDetailsLocation {
pub file: String,
pub line: u32,
pub column: u32,
}
let thread = thread::current();
let thread = thread.name().unwrap_or("<unknown>");
let locataion = panic_info
.location()
.map_or_else(|| "<unknown>".to_string(), ToString::to_string);
impl std::fmt::Display for PanicDetailsLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
let message = panic_info
.payload()
.downcast_ref::<String>()
.cloned()
.or_else(|| {
panic_info
.payload()
.downcast_ref::<&str>()
.map(ToString::to_string)
})
.unwrap_or_else(|| "<unknown>".to_string());
impl From<&std::panic::Location<'_>> for PanicDetailsLocation {
fn from(location: &std::panic::Location<'_>) -> Self {
Self {
file: location.file().to_string(),
line: location.line(),
column: location.column(),
}
}
}
tracing::error!("thread '{}' panicked at {}:", thread, locataion);
eprintln!("{}\n", message);
eprintln!("Backtrace:\n{}", backtrace);
impl std::fmt::Display for PanicDetails {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let fmt_thread_name = match &self.thread_name {
Some(name) => format!("'{}'", name),
None => "<unknown>".to_string(),
};
let fmt_message = match &self.message {
Some(message) => format!("'{}'", message),
None => "<no message>".to_string(),
};
let fmt_location = match &self.location {
Some(location) => location.to_string(),
None => "<no location>".to_string(),
};
// Apply syntax highlighting to backtrace
let highlighted_backtrace = self.highlight_backtrace();
write!(
f,
"thread {} panicked at {}, {}\n{}",
fmt_thread_name, fmt_message, fmt_location, highlighted_backtrace
)
}
}
impl PanicDetails {
/// Highlights backtrace lines that contain server or control-core crate references
/// and the line immediately following them
fn highlight_backtrace(&self) -> String {
use regex::Regex;
// Regex to match lines starting with digits followed by colon
let line_regex = Regex::new(r"^\s*(\d+):\s*(.*)$").unwrap();
let lines: Vec<&str> = self.backtrace.lines().collect();
let mut result = Vec::new();
let mut highlight_next = false;
for line in lines.iter() {
if let Some(captures) = line_regex.captures(line) {
let line_num = &captures[1];
let content = &captures[2];
// Check if the line contains server or control-core
if content.contains("server") || content.contains("control-core") {
// Red highlighting for relevant lines
result.push(format!("\x1b[91m{}: {}\x1b[0m", line_num, content));
highlight_next = true;
} else {
// Dim gray for other numbered lines
result.push(format!("\x1b[90m{}: {}\x1b[0m", line_num, content));
}
} else {
// Non-numbered lines (headers, location info, etc.)
if highlight_next {
// Highlight the line after a matched regex line in yellow
result.push(format!("\x1b[93m{}\x1b[0m", line));
highlight_next = false;
} else {
// Default color for other non-numbered lines
result.push(line.to_string());
}
}
}
result.join("\n")
}
}
/// Thread-level panic handler for general thread crashes
/// Sends detailed panic information including backtrace
pub fn send_panic(thread_panic_tx: Sender<PanicDetails>) {
// Ensure backtrace is enabled for panics
if std::env::var("RUST_BACKTRACE").is_err() {
unsafe {
std::env::set_var("RUST_BACKTRACE", "full");
}
}
std::panic::set_hook(Box::new(move |panic_info| {
// Capture backtrace at panic site
let backtrace = std::backtrace::Backtrace::force_capture();
let thread_name = std::thread::current().name().map(|name| name.to_string());
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
Some(s.to_string())
} else {
panic_info.payload().downcast_ref::<String>().cloned()
};
let panic_details = PanicDetails {
thread_name,
message,
location: panic_info.location().map(|loc| loc.into()),
backtrace: backtrace.to_string(),
};
// Send detailed panic info through channel
let _ = smol::block_on(thread_panic_tx.send(panic_details));
}));
std::process::exit(1);
}
/// Initialize panic handling system
/// Sets up panic handler and starts dedicated panic monitoring thread
pub fn init_panic() -> Sender<PanicDetails> {
let (thread_panic_tx, thread_panic_rx) = unbounded::<PanicDetails>();
send_panic(thread_panic_tx.clone());
pub fn init_panic_handling() {
// Ensure backtrace is enabled for panics
if std::env::var("RUST_BACKTRACE").is_err() {
unsafe {
std::env::set_var("RUST_BACKTRACE", "full");
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
}
// Start panic monitoring thread
let thread_panic_rx_clone = thread_panic_rx;
std::thread::Builder::new()
.name("panic".to_string())
.spawn(move || {
// Create an executor for this thread
let rt = smol::Executor::new();
smol::block_on(rt.run(async {
loop {
match thread_panic_rx_clone.recv().await {
Ok(panic_details) => {
error!("{}", panic_details);
exit(1);
}
Err(_) => {
// Channel closed, exit gracefully
break;
}
}
}
}))
})
.expect("Failed to spawn panic monitoring thread");
thread_panic_tx
std::panic::set_hook(Box::new(panic_hook));
}
+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 {
+43 -58
View File
@@ -1,72 +1,57 @@
use super::handlers::machine_mutation::post_machine_mutate;
use super::handlers::write_machine_device_identification::post_write_machine_device_identification;
use crate::app_state::AppState;
use crate::panic::{PanicDetails, send_panic};
use crate::socketio::init::init_socketio;
use anyhow::anyhow;
use anyhow::Result;
use axum::routing::post;
use smol::channel::Sender;
use std::sync::Arc;
use std::thread::JoinHandle;
use std::thread;
use tower_http::cors::CorsLayer;
use tower_http::trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::Level;
pub fn init_api(
thread_panic_tx: Sender<PanicDetails>,
app_state: Arc<AppState>,
) -> Result<JoinHandle<()>, anyhow::Error> {
std::thread::Builder::new()
.name("api".to_string())
.spawn(|| {
send_panic(thread_panic_tx);
use super::handlers::machine_mutation::post_machine_mutate;
use super::handlers::write_machine_device_identification::post_write_machine_device_identification;
use crate::app_state::AppState;
use crate::socketio::init::init_socketio;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.expect("Failed to create runtime");
async fn init_api(app_state: Arc<AppState>) -> Result<()> {
let cors = CorsLayer::permissive();
let socketio_layer = init_socketio(&app_state).await;
rt.block_on(async {
// allow all CORS requests
let cors = CorsLayer::permissive();
let trace_layer = TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::DEBUG))
.on_request(DefaultOnRequest::new().level(Level::TRACE))
.on_response(DefaultOnResponse::new().level(Level::TRACE));
// creat socketio layer
let socketio_layer = init_socketio(&app_state).await;
let app = axum::Router::new()
.route(
"/api/v1/write_machine_device_identification",
post(post_write_machine_device_identification),
)
.route("/api/v1/machine/mutate", post(post_machine_mutate))
.layer(socketio_layer)
.layer(cors)
.layer(trace_layer)
.with_state(app_state);
// Create a more detailed trace layer for HTTP requests
let trace_layer = TraceLayer::new_for_http()
.make_span_with(
DefaultMakeSpan::new()
.level(Level::DEBUG)
.include_headers(false),
)
.on_request(DefaultOnRequest::new().level(Level::TRACE))
.on_response(
DefaultOnResponse::new()
.level(Level::TRACE)
.include_headers(false),
);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001")
.await
.expect("Failed to bind to port 3001");
// make axum server to serve the data on /ethercat
let app = axum::Router::new()
.route(
"/api/v1/write_machine_device_identification",
post(post_write_machine_device_identification),
)
.route("/api/v1/machine/mutate", post(post_machine_mutate))
.layer(socketio_layer)
.layer(cors)
.layer(trace_layer)
.with_state(app_state);
tracing::info!("HTTP server running on 0.0.0.0:3001");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001")
.await
.expect("Failed to bind to port 3001");
axum::serve(listener, app)
.await
.map_err(|e| anyhow::anyhow!("Server error: {}", e))
}
tracing::info!("Starting HTTP server on 0.0.0.0:3001");
axum::serve(listener, app).await.expect("Failed to serve");
});
})
.map_err(|e| anyhow!("Failed to spawn API thread: {}", e))
/// Starts the API server in its own thread with a single-threaded Tokio runtime
pub fn start_api_thread(app_state: Arc<AppState>) -> std::thread::JoinHandle<()> {
thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
if let Err(err) = rt.block_on(init_api(app_state)) {
eprintln!("API server exited with error: {err:?}");
}
})
}
+3 -20
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};
@@ -125,27 +122,13 @@ impl SerialDeviceNew for Laser {
path: params.path.clone(),
}));
// Spawn the device thread
let device_thread_panic_tx = params.device_thread_panic_tx.clone();
//// Spawn the device thread
let _self_clone = _self.clone();
let path = params.path.clone();
thread::Builder::new()
.name("laser".to_owned())
.spawn(move || {
send_serial_device_panic(path.clone(), device_thread_panic_tx.clone());
smol::block_on(async {
let process_result = Self::process(_self_clone).await;
let removal = match process_result {
Ok(_) => SerialDeviceRemoval::Disconnect(path),
Err(e) => SerialDeviceRemoval::Error(path, e),
};
// if the task exists we want to remove the device
device_thread_panic_tx
.send(removal)
.await
.expect("Failed to send device removal signal");
let _ = Self::process(_self_clone).await;
});
})?;
+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))
}
}
+128 -72
View File
@@ -1,83 +1,139 @@
use crate::panic::{PanicDetails, send_panic};
use crate::socketio::main_namespace::MainNamespaceEvents;
use crate::socketio::main_namespace::machines_event::MachinesEventBuilder;
use crate::{app_state::AppState, machines::registry::MACHINE_REGISTRY};
use control_core::socketio::namespace::NamespaceCacheingLogic;
use smol::channel::Sender;
use std::{sync::Arc, thread, time::Duration};
use control_core::{
machines::identification::MachineIdentificationUnique,
serial::{
SerialDeviceIdentification, SerialDeviceNew, SerialDeviceNewParams,
serial_detection::SerialDetection,
},
socketio::namespace::NamespaceCacheingLogic,
};
use serialport::UsbPortInfo;
use smol::Task;
use std::{collections::HashMap, sync::Arc, time::Duration};
pub fn init_serial(
thread_panic_tx: Sender<PanicDetails>,
app_state: Arc<AppState>,
) -> Result<(), anyhow::Error> {
let thread_panic_tx_clone = thread_panic_tx;
use crate::{
app_state::AppState,
machines::{laser::LaserMachine, registry::MACHINE_REGISTRY},
socketio::main_namespace::{MainNamespaceEvents, machines_event::MachinesEventBuilder},
};
let app_state_clone = app_state.clone();
use super::devices::laser::Laser;
thread::Builder::new()
.name("SerialDetectionThread".to_owned())
.spawn(move || {
let app_state_clone = app_state_clone;
smol::block_on(async move {
send_panic(thread_panic_tx_clone);
// Async function that runs until at least one serial device is found.
pub async fn find_serial() -> Option<HashMap<String, UsbPortInfo>> {
smol::Timer::after(Duration::from_secs(1)).await;
let devices = SerialDetection::detect_devices();
if !devices.is_empty() {
return Some(devices);
} else {
return None;
}
}
let rt = smol::LocalExecutor::new();
rt.run(async {
loop {
let result = {
let mut serial_setup_guard = app_state_clone.serial_setup.write().await;
// Returns a smol::Task that resolves when atleast one device is found.
pub fn start_serial_discovery() -> Task<Option<HashMap<String, UsbPortInfo>>> {
smol::spawn(find_serial())
}
let mut port_result =
serial_setup_guard.serial_detection.check_ports().await;
// This function handles a disconnection and or reconnection of Laser
// You can easily rewrite it when we have more serialdevices
// TODO: Add A static list of known SeriadeviceIdents
pub async fn handle_serial_device_hotplug(
app_state: Arc<AppState>,
map: Option<HashMap<String, UsbPortInfo>>,
) {
let laser_ident = SerialDeviceIdentification {
vendor_id: 0x0403,
product_id: 0x6001,
};
let mut removed_signals = serial_setup_guard
.serial_detection
.check_remove_signals()
.await;
let laser = match map {
Some(map) => SerialDetection::get_path_by_id(laser_ident, map),
None => None,
};
port_result.removed.append(&mut removed_signals);
let mut unique_ident: Option<MachineIdentificationUnique> = None;
port_result
};
{
let machines = app_state.machines.read_arc_blocking();
for (id, _) in machines.iter() {
if id.machine_identification == LaserMachine::MACHINE_IDENTIFICATION {
unique_ident = Some(id.clone());
break;
}
}
}
if !result.added.is_empty() || !result.removed.is_empty() {
// sync serial device discovery to machine manager
{
let mut machine_guard = app_state_clone.machines.write().await;
// turn added devices into machines
for (device_identifiaction, device) in result.added {
machine_guard.add_serial_device(
&device_identifiaction,
device.clone(),
&MACHINE_REGISTRY,
app_state_clone.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
)
}
for device_identification in result.removed {
machine_guard.remove_serial_device(&device_identification)
}
}
// Machine isnt connected, so add it back
if laser.is_some() && unique_ident.is_none() {
let serial_params = SerialDeviceNewParams {
path: laser.unwrap(),
};
let _ = match Laser::new_serial(&serial_params) {
Ok((device_identification, serial_device)) => {
{
let mut machines = app_state.machines.write().await;
machines.add_serial_device(
&device_identification,
serial_device,
&MACHINE_REGISTRY,
app_state.socketio_setup.socket_queue_tx.clone(),
Arc::downgrade(&app_state.machines),
);
}
// Notify client via socketio
let app_state_event = app_state.clone();
smol::block_on(async {
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
});
}
smol::Timer::after(Duration::from_millis(300)).await;
}
})
.await;
});
})
.expect("Failed to spawn SerialTxRxThread");
Ok(())
let app_state_event = app_state.clone();
let main_namespace = &mut app_state_event
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state_event.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
}
_ => (),
};
} else if laser.is_none() && unique_ident.is_some() {
let serial_params = SerialDeviceNewParams {
path: "".to_string(),
};
let _ = match Laser::new_serial(&serial_params) {
Ok((device_identification, _)) => {
app_state
.machines
.write()
.await
.remove_serial_device(&device_identification);
drop(app_state.machines.clone());
app_state
.machines
.write_arc()
.await
.serial_machines
.remove(&unique_ident.clone().unwrap());
app_state
.machines
.write_arc()
.await
.ethercat_machines
.remove(&unique_ident.clone().unwrap());
drop(app_state.machines.clone());
let main_namespace = &mut app_state
.socketio_setup
.namespaces
.write()
.await
.main_namespace;
let event = MachinesEventBuilder().build(app_state.clone());
main_namespace.emit(MainNamespaceEvents::MachinesEvent(event));
}
_ => (),
};
}
}
+30 -53
View File
@@ -1,10 +1,7 @@
use crate::{
app_state::AppState,
panic::{PanicDetails, send_panic},
};
use smol::channel::Sender;
use crate::app_state::AppState;
use smol::Task;
use std::{sync::Arc, time::Instant};
use tracing::{debug, error, info, instrument, trace};
use tracing::{debug, info, instrument, trace};
/// Send a single event with retry logic
#[instrument(skip_all)]
@@ -62,56 +59,36 @@ async fn send_event_with_retry(
}
}
pub fn init_socketio_queue(thread_panic_tx: Sender<PanicDetails>, app_state: Arc<AppState>) {
std::thread::Builder::new()
.name("socketio-queue".to_string())
.spawn(move || {
send_panic(thread_panic_tx);
async fn socketio_queue_worker(app_state: Arc<AppState>) {
tracing::info!("SocketIO global queue listener started");
let mut event_count = 0;
let mut batch_start = Instant::now();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.expect("Failed to create runtime");
while let Ok((socket, event)) = app_state.socketio_setup.socket_queue_rx.recv().await {
event_count += 1;
rt.block_on(async {
info!("SocketIO global queue listener started");
send_event_with_retry(&socket, &event).await;
let mut event_count = 0;
let mut batch_start = Instant::now();
if batch_start.elapsed().as_secs() >= 5 {
let elapsed = batch_start.elapsed();
if event_count > 0 {
debug!(
"[{}::socketio_queue_worker] Processed {} events in {:.2?} ({:.1} events/s)",
module_path!(),
event_count,
elapsed,
event_count as f64 / elapsed.as_secs_f64(),
);
}
event_count = 0;
batch_start = Instant::now();
}
}
loop {
match app_state.socketio_setup.socket_queue_rx.recv().await {
Ok((socket, event)) => {
event_count += 1;
info!("SocketIO global queue listener stopped");
}
// Handle the received message with retry logic
send_event_with_retry(&socket, &event).await;
// Log batch statistics every 5 seconds
if batch_start.elapsed().as_secs() >= 5 {
let elapsed = batch_start.elapsed();
if event_count > 0 {
debug!(
"[{}::init_socketio_queue] Processed {} events in {:.2?} ({:.1} events/s)",
module_path!(),
event_count,
elapsed,
event_count as f64 / elapsed.as_secs_f64(),
);
}
event_count = 0;
batch_start = Instant::now();
}
}
Err(e) => {
error!(error = %e, "Error receiving from global socketio queue");
info!("SocketIO global queue listener stopping");
break;
}
}
}
});
})
.expect("Failed to spawn socketio queue thread");
/// Returns a spawned async task handling the Socket.IO event queue
pub fn start_socketio_queue(app_state: Arc<AppState>) -> Task<()> {
smol::spawn(socketio_queue_worker(app_state))
}