Compare commits

...
56 Commits
Author SHA1 Message Date
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
Oshgnacknak 70ce7625b2 Remove bad hardware acceleration flag 2025-10-13 15:00:21 +02:00
Robin Krämer 9936764125 Merge pull request #717 from qitechgmbh/700-add-a-show-from-option-for-graphs
700 add a show from option for graphs
2025-10-13 12:17:26 +02:00
Christian a0f09e1671 Improve formatting and adjust future time validation messages in TimeInput component 2025-10-13 12:14:09 +02:00
Christian 1100b880f9 Add validation to prevent future time selection in TimeInput component 2025-10-13 12:14:09 +02:00
Christian 6224f33ba5 fix eslint 2025-10-13 12:14:09 +02:00
Christian b0a9b5a6d2 Fix styling 2025-10-13 12:14:09 +02:00
ChristianandCopilot ac7247c87a Update electron/src/components/time/TimeInput.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 12:14:09 +02:00
Christian 2ea459f3be Refactor button styles and improve formatting in GraphControls and TimeInput components 2025-10-13 12:14:09 +02:00
Christian 25250f640f Add showFromTimestamp functionality to graph components and implement TimeInput for time selection 2025-10-13 12:14:09 +02:00
Robin Krämer 57a0cc8af4 Merge pull request #776 from qitechgmbh/775-add-explanation-of-padding-to-the-winder-manual
Add padding parameter explanation to manual for filament winding
2025-10-13 12:13:53 +02:00
Christian 5ebb978eae Add padding example image to the winder docs 2025-10-13 12:09:20 +02:00
Christian d647ee6ebe fix format 2025-10-13 12:09:20 +02:00
Christian fea203ae7a Add padding parameter explanation to manual for filament winding 2025-10-13 12:09:20 +02:00
Robin Krämer b5eb413d37 Merge pull request #791 from qitechgmbh/788-fix-remote-setting-reset-and-update-label-in-update-tab
Fix remote setting reset and update label to "Repository Owner" in Update tab
2025-10-13 12:08:43 +02:00
Christian aa1e5c4f39 Fix typos in GitHub repository owner label and description 2025-10-13 12:05:31 +02:00
Christian ee91934bf7 Refactor Github source management: integrate condition store and update UI labels 2025-10-13 12:05:31 +02:00
Milan von dem Bussche 1fa04eae9a Update README.md
Added YouTube Explainer Video about QiTech Control
2025-10-10 18:41:46 +02:00
Robin Krämer 98e1269f6f Merge pull request #800 from qitechgmbh/changelog-2.9.1
updated Changelog
2025-10-10 15:07:51 +02:00
kraemr 8f640fcb21 updated Changelog 2025-10-10 15:06:30 +02:00
Robin Krämer 6c120b7fe9 Merge pull request #796 from qitechgmbh/795-winding-when-puller-reverse-doesnt-work-in-adaptive
adaptive spool speed now takes negative speeds into account,
2025-10-08 18:12:23 +02:00
kraemr 033b93b9da puller speed now is always positive in frontend 2025-10-08 17:49:55 +02:00
kraemr 6092358e87 adaptive spool speed now takes negative speeds into account,
aswell as Auto Stop Spool
2025-10-08 17:40:50 +02:00
Robin Krämer 71de39e871 Merge pull request #793 from qitechgmbh/790-refactor-aquapath-newrs-to-match-others
790 refactor aquapath newrs to match others
2025-10-08 16:29:49 +02:00
joe 54ffa193c3 fixed fmt 2025-10-08 16:23:40 +02:00
joe 03164f1ceb refactor(aquapath1): cleanup and fix device initialization and configuration 2025-10-08 15:45:58 +02:00
Robin Krämer f67a46ab4c Merge pull request #787 from qitechgmbh/786-add-memory-locking
added CAP_IPC_LOCK and added memory locking
2025-10-07 13:54:02 +02:00
kraemr 610790a778 added CAP_IPC_LOCK and memory locking 2025-10-07 13:04:49 +02:00
Robin Krämer 6eaf451ad4 Merge pull request #692 from JanHBade/master
Update README.md
2025-10-07 12:57:27 +02:00
JanHBade 022e0adae3 Update README.md
Minor typo in EtherCAT
2025-10-07 12:57:16 +02:00
Robin Krämer 6e6d4b31e7 Merge pull request #746 from qitechgmbh/640-improve-scroll-bar-styling-for-manuals-to-resemble-macos-style
Update scrollbar styling to be similar to macos
2025-10-07 12:45:12 +02:00
Christian 0dafad3fb5 fix code stye 2025-10-07 12:40:01 +02:00
Christian 534de55239 Refactor scrollbar styles to apply only to html and body elements 2025-10-07 12:40:01 +02:00
Christian d9f92bcdf2 Refactor scrollbar styles for Markdown and Terminal components to be similar to MacOS styling 2025-10-07 12:40:01 +02:00
Robin Krämer 1570299e38 Merge pull request #782 from qitechgmbh/781-refactor-winderv2-emit-functions-to-emitrs
refactored winder2's emit methods to emit.rs
2025-10-06 17:01:38 +02:00
kraemr c95f6f3bd9 refactored winder2's emit methods to emit.rs 2025-10-06 16:36:59 +02:00
Robin Krämer 9fcde2a234 Merge pull request #780 from qitechgmbh/772-remove-dependencies-that-are-not-necessary
decreased compile time by removing jemalloc, and using only f64 uom
2025-10-06 16:05:08 +02:00
kraemr a2ffc41e05 decreased compile time by removing jemalloc, and using only f64 uom 2025-10-06 16:02:29 +02:00
51 changed files with 1507 additions and 1200 deletions

No files matched your search

+2
View File
@@ -3,3 +3,5 @@ dist/
.DS_Store
Cargo.lock
dhat-heap.json
build.sh
+23
View File
@@ -1,3 +1,26 @@
# `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_
## Winder V2
- [#796](https://github.com/qitechgmbh/control/pull/796) Winding in adaptive mode, while puller is in reverse mode is now possible
## General
- [#780](https://github.com/qitechgmbh/control/pull/780) Updates are faster now
- [#746](https://github.com/qitechgmbh/control/pull/746) Improved scrollbar styling for manuals
# `2.9.0`
_06.10.2025_
Generated
-33
View File
@@ -2689,8 +2689,6 @@ dependencies = [
"smol",
"socketioxide",
"textplots",
"tikv-jemalloc-ctl",
"tikv-jemallocator",
"tokio",
"tonic",
"tower-http",
@@ -3013,37 +3011,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tikv-jemalloc-ctl"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f21f216790c8df74ce3ab25b534e0718da5a1916719771d3fec23315c99e468b"
dependencies = [
"libc",
"paste",
"tikv-jemalloc-sys",
]
[[package]]
name = "tikv-jemalloc-sys"
version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "tikv-jemallocator"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865"
dependencies = [
"libc",
"tikv-jemalloc-sys",
]
[[package]]
name = "timerfd"
version = "1.6.0"
+4 -1
View File
@@ -7,6 +7,9 @@ QiTech Control is an industrial control panel software for the next generation o
[![](https://img.youtube.com/vi/KI3YeBwfV-s/maxresdefault.jpg)](https://www.youtube.com/watch?v=KI3YeBwfV-s)
*Click here to watch a video demo of our software.*
[![](https://img.youtube.com/vi/55egCAkQgyM/maxresdefault.jpg)](https://youtu.be/55egCAkQgyM)
*Click here to watch a full explainer Video of our Software.*
# Repo Structure
Frontend
@@ -32,7 +35,7 @@ Other
## Backend
To interface with Beckhoff and other EtherCAT devices we need an EtherCAY master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
To interface with Beckhoff and other EtherCAT devices we need an EtherCAT master stoftware. Possibilities are [PySOEM](https://github.com/bnjmnp/pysoem) (Python), [SOEM](https://github.com/OpenEtherCATsociety/SOEM) (C) and [Ethercrab](https://github.com/ethercrab-rs/ethercrab) (Rust). For realtime operation only C and Rust are suitable. We chose Rust because of safety and confidence in the written code.
[SocketIO](https://socket.io/) was chosen for performant event driven communication from the backend to the server. But we still use REST with [axum](https://docs.rs/axum/latest/axum/) for the communication thet benefits from the request/response model.
+5 -3
View File
@@ -21,10 +21,12 @@ 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
+1 -1
View File
@@ -19,7 +19,7 @@ serde_json = "1.0.143"
serialport = "4.7.3"
smol = "2.0.2"
socketioxide = "0.17.2"
uom = "0.36.0"
uom = { version = "0.36.0", default-features = false, features = ["f64"] }
bitvec = { version = "1.0.1", features = ["alloc"] }
crc = "3.3.0"
serial = "0.4.0"
+14 -32
View File
@@ -1,4 +1,4 @@
use uom::si::angle::radian;
use uom::si::{angle::radian, f64::Angle};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AngleConverter {
@@ -63,7 +63,7 @@ impl AngleConverter {
}
/// Convert angle from mathematical system to this system (f32)
pub fn degrees_encode(&self, math_angle_degrees: f32) -> f32 {
pub fn degrees_encode(&self, math_angle_degrees: f64) -> f64 {
let normalized_input = self.normalize_angle(math_angle_degrees);
let mut result = normalized_input;
@@ -85,7 +85,7 @@ impl AngleConverter {
}
/// Convert angle from this system to mathematical system (f32)
pub fn degrees_decode(&self, system_angle_degrees: f32) -> f32 {
pub fn degrees_decode(&self, system_angle_degrees: f64) -> f64 {
let normalized_input = self.normalize_angle(system_angle_degrees);
let mut result = normalized_input;
@@ -107,14 +107,14 @@ impl AngleConverter {
}
/// Convert angle in radians from mathematical system to this system (f32)
pub fn radians_encode(&self, math_angle_radians: f32) -> f32 {
pub fn radians_encode(&self, math_angle_radians: f64) -> f64 {
let degrees = math_angle_radians.to_degrees();
let converted_degrees = self.degrees_encode(degrees);
converted_degrees.to_radians()
}
/// Convert angle in radians from this system to mathematical system (f32)
pub fn radians_decode(&self, system_angle_radians: f32) -> f32 {
pub fn radians_decode(&self, system_angle_radians: f64) -> f64 {
let degrees = system_angle_radians.to_degrees();
let converted_degrees = self.degrees_decode(degrees);
converted_degrees.to_radians()
@@ -179,7 +179,7 @@ impl AngleConverter {
}
/// Normalize angle to [0, 360) range (f32)
fn normalize_angle(&self, angle: f32) -> f32 {
fn normalize_angle(&self, angle: f64) -> f64 {
let mut normalized = angle % 360.0;
if normalized < 0.0 {
normalized += 360.0;
@@ -207,30 +207,12 @@ impl AngleConverterUom {
Self { angle_converter }
}
pub fn encode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
uom::si::f32::Angle::new::<radian>(
self.angle_converter.radians_encode(angle.get::<radian>()),
)
pub fn encode(&self, angle: Angle) -> Angle {
Angle::new::<radian>(self.angle_converter.radians_encode(angle.get::<radian>()))
}
pub fn decode(&self, angle: uom::si::f32::Angle) -> uom::si::f32::Angle {
uom::si::f32::Angle::new::<radian>(
self.angle_converter.radians_decode(angle.get::<radian>()),
)
}
pub fn encode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
uom::si::f64::Angle::new::<radian>(
self.angle_converter
.radians_encode_f64(angle.get::<radian>()),
)
}
pub fn decode_f64(&self, angle: uom::si::f64::Angle) -> uom::si::f64::Angle {
uom::si::f64::Angle::new::<radian>(
self.angle_converter
.radians_decode_f64(angle.get::<radian>()),
)
pub fn decode(&self, angle: Angle) -> Angle {
Angle::new::<radian>(self.angle_converter.radians_decode(angle.get::<radian>()))
}
}
@@ -297,8 +279,8 @@ mod tests {
let converter = AngleConverter::screen();
// Test radians_encode
let result = converter.radians_encode(PI / 2.0); // 90° in radians
let expected = 270.0 * PI / 180.0; // 270° in radians
let result = converter.radians_encode(PI as f64 / 2.0) as f64; // 90° in radians
let expected: f64 = 270.0f64 * PI as f64 / 180.0f64; // 270° in radians
assert!((result - expected).abs() < 1e-6);
}
@@ -306,10 +288,10 @@ mod tests {
fn test_roundtrip_conversion() {
let converter = AngleConverter::new(true, false, true); // X-flip + CW
let original = 75.0_f32;
let original = 75.0_f64;
let converted = converter.degrees_encode(original);
let roundtrip = converter.degrees_decode(converted);
assert!((original - roundtrip).abs() < 1e-6);
assert!((original - roundtrip as f64).abs() < 1e-6);
}
}
@@ -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),
},
));
Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

@@ -416,6 +416,29 @@ Then start the machine as follows:
<br>
**Understanding Padding**
The **padding** parameter creates a safety margin that prevents the filament from winding too close to the spool edges. When you set the inner and outer limits during calibration, the padding value is automatically subtracted from the actual travel distance.
**How padding works:**
- When you have a 1.75mm diameter filament and the traverse moves to a flank position, the **center** of the filament aligns with that flank
- If you set 1mm padding, the system automatically maintains at least 1mm distance from the flanks
- This prevents the filament from overlapping or winding beyond the spool edges
**Example:**
- Outer limit: 90mm, Inner limit: 22mm
- Padding: 1mm
- **Actual winding area:** 89mm to 23mm (reduced by 1mm on each side)
- This ensures the filament stays within the spool boundaries with a safe margin
The default padding is 0.88mm, but you can adjust this value in the Control settings based on your filament diameter and winding requirements. Larger filaments may require more padding, while smaller filaments can use less padding for maximum spool utilization.
![Padding example](images/manuals/winder/winder_padding_example_image.jpg)
<br>
**4. Start pulling**
- Select “Control” in the function bar
+2 -7
View File
@@ -58,15 +58,10 @@ export function MarkdownWithToc({
className={`sticky ${stickyTop}`}
style={{ maxHeight: "calc(100vh - 6rem)" }}
>
{/* Hide scrollbar for the TOC container across browsers */}
<style>{`
[data-toc-scroll-container] { -ms-overflow-style: none; scrollbar-width: none; }
[data-toc-scroll-container]::-webkit-scrollbar { display: none; }
`}</style>
{/* Table of Contents with hidden scrollbar */}
<div
className="overflow-y-auto"
className="scrollbar-hidden overflow-y-auto"
style={{ maxHeight: "calc(100vh - 8rem)" }}
data-toc-scroll-container
>
<MarkdownToc
markdownContent={markdownContent}
+1 -1
View File
@@ -199,7 +199,7 @@ export function Terminal({
</div>
{/* Virtualized List */}
<div className="flex-grow bg-black text-white">
<div className="flex-grow overflow-y-auto bg-neutral-900 p-4 text-neutral-300">
<AutoSizer>
{({ width, height }) => (
<List
@@ -474,6 +474,7 @@ export function BigGraph({
isLiveMode,
selectedTimeWindow,
visibleSeries, // Keep for initial creation
showFromTimestamp: syncGraph?.showFromTimestamp,
setSelectedTimeWindow,
setViewMode,
setIsLiveMode,
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { TouchButton } from "@/components/touch/TouchButton";
import { Icon } from "@/components/Icon";
import { ControlCard } from "@/control/ControlCard";
import { TimeInput } from "@/components/time/TimeInput";
import {
DropdownMenu,
DropdownMenuContent,
@@ -21,8 +22,14 @@ export function GraphControls({
onSwitchToHistorical,
onExport,
timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS,
showFromTimestamp,
onShowFromChange,
}: ControlProps) {
const getSelectedTimeWindowLabel = () => {
// If Show from Time is set, display "Show All" for the time window
if (showFromTimestamp) {
return "Show All";
}
const option = timeWindowOptions.find((opt) => opt.value === timeWindow);
return option ? option.label : "1m";
};
@@ -35,7 +42,10 @@ export function GraphControls({
<DropdownMenuTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
className={`h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50 ${
showFromTimestamp ? "cursor-not-allowed opacity-50" : ""
}`}
disabled={!!showFromTimestamp}
>
{getSelectedTimeWindowLabel()}
<Icon name="lu:ChevronDown" className="ml-2 size-4" />
@@ -60,6 +70,15 @@ export function GraphControls({
</DropdownMenuContent>
</DropdownMenu>
{/* Show From Time Input - only visible in historical mode */}
{!isLiveMode && onShowFromChange && (
<TimeInput
timestamp={showFromTimestamp}
onTimeChange={onShowFromChange}
onClear={() => onShowFromChange(null)}
/>
)}
<TouchButton
onClick={onSwitchToHistorical}
variant="outline"
@@ -109,10 +128,16 @@ export function FloatingControlPanel({
onSwitchToHistorical,
onExport,
timeWindowOptions = DEFAULT_TIME_WINDOW_OPTIONS,
showFromTimestamp,
onShowFromChange,
}: ControlProps) {
const [isExpanded, setIsExpanded] = useState(false);
const getSelectedTimeWindowLabel = () => {
// If Show from Time is set, display "Show All" for the time window
if (showFromTimestamp) {
return "Show All";
}
const option = timeWindowOptions.find((opt) => opt.value === timeWindow);
return option ? option.label : "1m";
};
@@ -134,7 +159,10 @@ export function FloatingControlPanel({
<DropdownMenuTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
className={`h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50 ${
showFromTimestamp ? "cursor-not-allowed opacity-50" : ""
}`}
disabled={!!showFromTimestamp}
>
{getSelectedTimeWindowLabel()}
<Icon name="lu:ChevronDown" className="ml-2 size-4" />
@@ -159,6 +187,15 @@ export function FloatingControlPanel({
</DropdownMenuContent>
</DropdownMenu>
{/* Show From Time Input - only visible in historical mode */}
{!isLiveMode && onShowFromChange && (
<TimeInput
timestamp={showFromTimestamp}
onTimeChange={onShowFromChange}
onClear={() => onShowFromChange(null)}
/>
)}
<TouchButton
onClick={onSwitchToHistorical}
variant="outline"
+6 -7
View File
@@ -31,10 +31,8 @@ export function createChart({
setCursorValue,
setCursorValues,
visibleSeries,
}: CreateChartParams & {
setCursorValues?: React.Dispatch<React.SetStateAction<(number | null)[]>>;
visibleSeries?: boolean[];
}): (() => void) | undefined {
showFromTimestamp,
}: CreateChartParams): (() => void) | undefined {
if (!containerRef.current) return;
// Get ALL series from original data
@@ -73,7 +71,7 @@ export function createChart({
initialMin = manualScaleRef.current.x.min;
initialMax = manualScaleRef.current.x.max;
} else if (selectedTimeWindow === "all") {
initialMin = fullStart;
initialMin = showFromTimestamp ?? fullStart;
initialMax = getHistoricalEndTimestamp();
} else {
const endTimestamp = getHistoricalEndTimestamp();
@@ -83,9 +81,10 @@ export function createChart({
endTimestamp - (selectedTimeWindow as number),
fullStart,
);
initialMin = defaultViewStart;
initialMin = showFromTimestamp ?? defaultViewStart;
} else {
initialMin = endTimestamp - (selectedTimeWindow as number);
initialMin =
showFromTimestamp ?? endTimestamp - (selectedTimeWindow as number);
}
initialMax = endTimestamp;
+6
View File
@@ -10,6 +10,7 @@ export type PropGraphSync = {
isLiveMode: boolean;
xRange?: { min: number; max: number };
historicalFreezeTimestamp?: number | null;
showFromTimestamp?: number | null;
onTimeWindowChange?: (graphId: string, timeWindow: number | "all") => void;
onViewModeChange?: (
graphId: string,
@@ -81,6 +82,8 @@ export type ControlProps = {
onSwitchToHistorical: () => void;
onExport?: () => void;
timeWindowOptions?: TimeWindowOption[];
showFromTimestamp?: number | null;
onShowFromChange?: (timestamp: number | null) => void;
};
export interface LiveModeHandlers {
@@ -128,6 +131,9 @@ export interface CreateChartParams {
>;
setIsLiveMode: React.Dispatch<React.SetStateAction<boolean>>;
setCursorValue: React.Dispatch<React.SetStateAction<number | null>>;
setCursorValues: React.Dispatch<React.SetStateAction<(number | null)[]>>;
visibleSeries: boolean[];
showFromTimestamp?: number | null;
}
export interface AnimationState {
@@ -41,6 +41,7 @@ interface UseBigGraphEffectsProps {
isLiveMode: boolean;
selectedTimeWindow: number | "all";
visibleSeries: boolean[];
showFromTimestamp?: number | null;
// State setters
setSelectedTimeWindow: (value: number | "all") => void;
@@ -90,6 +91,7 @@ export function useBigGraphEffects({
isLiveMode,
selectedTimeWindow,
visibleSeries,
showFromTimestamp,
setSelectedTimeWindow,
setViewMode,
@@ -275,6 +277,7 @@ export function useBigGraphEffects({
setCursorValue,
setCursorValues,
visibleSeries,
showFromTimestamp,
});
setIsChartCreated(true);
@@ -15,6 +15,9 @@ export function useGraphSync(exportGroupId?: string) {
const [xRange, setXRange] = useState<
{ min: number; max: number } | undefined
>();
const [showFromTimestamp, setShowFromTimestamp] = useState<number | null>(
null,
);
const historicalFreezeTimestampRef = useRef<number | null>(null);
const graphDataRef = useRef<Map<string, () => GraphExportData | null>>(
@@ -59,6 +62,7 @@ export function useGraphSync(exportGroupId?: string) {
viewMode?: "default" | "all" | "manual";
isLiveMode?: boolean;
xRange?: { min: number; max: number } | undefined;
showFromTimestamp?: number | null;
clearHistoricalFreeze?: boolean;
setHistoricalFreeze?: boolean;
},
@@ -90,6 +94,8 @@ export function useGraphSync(exportGroupId?: string) {
if (updates.viewMode !== undefined) setViewMode(updates.viewMode);
if (updates.isLiveMode !== undefined) setIsLiveMode(updates.isLiveMode);
if (updates.xRange !== undefined) setXRange(updates.xRange);
if (updates.showFromTimestamp !== undefined)
setShowFromTimestamp(updates.showFromTimestamp);
// Handle historical freeze timestamp
if (updates.clearHistoricalFreeze) {
@@ -195,6 +201,7 @@ export function useGraphSync(exportGroupId?: string) {
viewMode: timeWindow === "all" ? "all" : "default",
xRange: undefined,
clearHistoricalFreeze: true,
showFromTimestamp: null, // Clear the show from timestamp when switching to live
});
}, [updateSyncState, timeWindow]);
@@ -206,6 +213,15 @@ export function useGraphSync(exportGroupId?: string) {
});
}, [updateSyncState]);
const handleShowFromChange = useCallback(
(timestamp: number | null) => {
updateSyncState("control", {
showFromTimestamp: timestamp,
});
},
[updateSyncState],
);
const handleControlTimeWindowChange = useCallback(
(newTimeWindow: number | "all") => {
updateSyncState("control", {
@@ -225,6 +241,7 @@ export function useGraphSync(exportGroupId?: string) {
isLiveMode,
xRange,
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
showFromTimestamp,
onTimeWindowChange: handleTimeWindowChange,
onViewModeChange: handleViewModeChange,
onZoomChange: handleZoomChangeThrottled,
@@ -237,6 +254,8 @@ export function useGraphSync(exportGroupId?: string) {
onSwitchToLive: handleSwitchToLive,
onSwitchToHistorical: handleSwitchToHistorical,
onExport: handleExport,
showFromTimestamp,
onShowFromChange: handleShowFromChange,
};
return {
@@ -250,5 +269,6 @@ export function useGraphSync(exportGroupId?: string) {
isLiveMode,
xRange,
historicalFreezeTimestamp: historicalFreezeTimestampRef.current,
showFromTimestamp,
};
}
+367
View File
@@ -0,0 +1,367 @@
import React, { useState, useCallback, useEffect } from "react";
import { TouchButton } from "@/components/touch/TouchButton";
import { Icon } from "@/components/Icon";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { useForm, useWatch } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Separator } from "@/components/ui/separator";
type TimeInputProps = {
timestamp?: number | null;
onTimeChange?: (timestamp: number | null) => void;
onClear?: () => void;
};
const timeSchema = z.object({
hours: z.number().min(0).max(23),
minutes: z.number().min(0).max(59),
seconds: z.number().min(0).max(59),
});
type TimeFormData = z.infer<typeof timeSchema>;
export function TimeInput({
timestamp,
onTimeChange,
onClear,
}: TimeInputProps) {
const [isOpen, setIsOpen] = useState(false);
// Get current time for initial values
const getCurrentTime = useCallback(() => {
const now = new Date();
return {
hours: now.getHours(),
minutes: now.getMinutes(),
seconds: now.getSeconds(),
};
}, []);
// Get initial values based on timestamp or current time
const getInitialValues = useCallback(() => {
if (timestamp) {
const date = new Date(timestamp);
return {
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
};
}
return getCurrentTime();
}, [timestamp, getCurrentTime]);
const form = useForm<TimeFormData>({
resolver: zodResolver(timeSchema),
defaultValues: getInitialValues(),
mode: "all",
});
// Convert timestamp to time components
const timestampToTime = useCallback((ts: number) => {
const date = new Date(ts);
return {
hours: date.getHours(),
minutes: date.getMinutes(),
seconds: date.getSeconds(),
};
}, []);
// Check if the selected time would be in the future (today)
const isTimeInFuture = useCallback((time: TimeFormData) => {
const now = new Date();
const date = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
time.hours,
time.minutes,
time.seconds,
);
return date.getTime() > now.getTime();
}, []);
// Convert time components to timestamp (today's date only)
const timeToTimestamp = useCallback((time: TimeFormData) => {
const now = new Date();
const date = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
time.hours,
time.minutes,
time.seconds,
);
return date.getTime();
}, []);
// Initialize form with timestamp or current time when dialog opens
useEffect(() => {
if (isOpen) {
const initialValues = getInitialValues();
form.reset(initialValues);
}
}, [isOpen, getInitialValues, form]);
const formatTimeDisplay = useCallback(() => {
if (!timestamp) return "Show from Time";
const time = timestampToTime(timestamp);
return `${time.hours.toString().padStart(2, "0")}:${time.minutes.toString().padStart(2, "0")}:${time.seconds.toString().padStart(2, "0")}`;
}, [timestamp, timestampToTime]);
// Increment/decrement functions with bounds checking and cascading
const adjustTimeValue = useCallback(
(field: "hours" | "minutes" | "seconds", increment: boolean) => {
// Define the order of fields for cascading
const fields: ("hours" | "minutes" | "seconds")[] = [
"seconds",
"minutes",
"hours",
];
const startIndex = fields.indexOf(field);
let carry = increment ? 1 : -1;
for (let i = startIndex; i < fields.length; i++) {
const currentField = fields[i];
const currentValue = form.getValues(currentField);
const max = currentField === "hours" ? 23 : 59;
let newValue = currentValue + carry;
carry = 0;
if (increment) {
if (newValue > max) {
newValue = 0;
carry = 1;
}
} else {
if (newValue < 0) {
newValue = max;
carry = -1;
}
}
form.setValue(currentField, newValue, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
// If no carry, stop cascading
if (carry === 0) {
break;
}
}
},
[form],
);
const handleSubmit = () => {
form.handleSubmit((data) => {
// Prevent submission if time is in the future
if (isTimeInFuture(data)) {
return;
}
const newTimestamp = timeToTimestamp(data);
onTimeChange?.(newTimestamp);
setIsOpen(false);
})();
};
const handleAbort = () => {
if (timestamp) {
const time = timestampToTime(timestamp);
form.reset(time);
} else {
const currentTime = getCurrentTime();
form.reset(currentTime);
}
setIsOpen(false);
};
const handleClear = () => {
onClear?.();
setIsOpen(false);
};
// Watch individual form fields for real-time updates
const hours = useWatch({
control: form.control,
name: "hours",
defaultValue: getInitialValues().hours,
});
const minutes = useWatch({
control: form.control,
name: "minutes",
defaultValue: getInitialValues().minutes,
});
const seconds = useWatch({
control: form.control,
name: "seconds",
defaultValue: getInitialValues().seconds,
});
// Get current form values with fallback
const currentValues = {
hours: hours ?? 0,
minutes: minutes ?? 0,
seconds: seconds ?? 0,
};
// TimeValueInput component for individual time fields
const TimeValueInput = ({
field,
label,
value,
}: {
field: "hours" | "minutes" | "seconds";
label: string;
value: number;
}) => (
<div className="flex flex-col items-center gap-3">
<label className="text-sm font-medium text-gray-700">{label}</label>
<div className="flex flex-col items-center gap-2">
<TouchButton
variant="outline"
size="sm"
className="h-10 w-16 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
adjustTimeValue(field, true);
}}
>
<Icon name="lu:ChevronUp" className="size-4" />
</TouchButton>
<div className="flex h-16 w-20 items-center justify-center rounded border-2 border-gray-300 bg-white font-mono text-3xl">
{value.toString().padStart(2, "0")}
</div>
<TouchButton
variant="outline"
size="sm"
className="h-10 w-16 p-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
adjustTimeValue(field, false);
}}
>
<Icon name="lu:ChevronDown" className="size-4" />
</TouchButton>
</div>
</div>
);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<TouchButton
variant="outline"
className="h-auto border-gray-300 bg-white px-3 py-3 text-base text-gray-900 hover:bg-gray-50"
>
<Icon name="lu:Clock" className="mr-2 size-4" />
{formatTimeDisplay()}
</TouchButton>
</DialogTrigger>
<DialogContent className="max-w-2xl p-8">
<div className="flex flex-col gap-8">
{/* Header */}
<div className="flex min-h-[2.5rem] items-center justify-between">
<div className="flex items-center gap-3">
<Icon name="lu:Clock" className="size-6" />
<h3 className="text-xl font-semibold">Show from Time</h3>
</div>
{timestamp && (
<TouchButton
variant="ghost"
size="sm"
onClick={handleClear}
className="text-red-600 hover:text-red-700"
>
<Icon name="lu:X" className="mr-2 size-4" />
Clear
</TouchButton>
)}
</div>
{/* Description */}
<div className="-mt-4 text-base text-gray-600">
Set a specific time to display data from that point forward in the
graph.
</div>
{/* Error for future time */}
{isTimeInFuture(currentValues) && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<div className="flex items-start gap-3">
<Icon name="lu:X" className="mt-0.5 size-5 text-red-600" />
<div>
<div className="font-medium text-red-800">
Cannot set future time
</div>
<div className="mt-1 text-sm text-red-700">
The selected time is in the future. Please choose a time
that has already occurred today.
</div>
</div>
</div>
</div>
)}
<Separator />
{/* Time inputs */}
<div className="flex items-center justify-center gap-8">
<TimeValueInput
field="hours"
label="Hours"
value={currentValues.hours}
/>
<div className="mt-8 text-4xl font-bold">:</div>
<TimeValueInput
field="minutes"
label="Minutes"
value={currentValues.minutes}
/>
<div className="mt-8 text-4xl font-bold">:</div>
<TimeValueInput
field="seconds"
label="Seconds"
value={currentValues.seconds}
/>
</div>
<Separator />
{/* Action buttons */}
<div className="flex gap-4">
<TouchButton
variant="outline"
className="h-14 flex-1"
onClick={handleAbort}
>
<Icon name="lu:X" className="mr-2 size-4" />
Cancel
</TouchButton>
<TouchButton
className={`h-14 flex-1 ${
isTimeInFuture(currentValues)
? "cursor-not-allowed bg-gray-300 text-gray-500"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
onClick={handleSubmit}
disabled={isTimeInFuture(currentValues)}
>
<Icon name="lu:Check" className="mr-2 size-4" />
Apply
</TouchButton>
</div>
</div>
</DialogContent>
</Dialog>
);
}
@@ -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";
@@ -54,6 +58,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") {
@@ -274,7 +285,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",
@@ -153,6 +163,7 @@ export function Winder2PresetsPage() {
setPullerTargetDiameter,
setPullerForward,
setPullerTargetSpeed,
setPullerGearRatio,
setSpoolRegulationMode,
@@ -177,6 +188,7 @@ 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(
@@ -17,6 +17,7 @@ export function Winder2SettingPage() {
setTraverseStepSize,
setTraversePadding,
setPullerForward,
setPullerGearRatio,
setSpoolRegulationMode,
setSpoolMinMaxMinSpeed,
setSpoolMinMaxMaxSpeed,
@@ -249,6 +250,32 @@ 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>
<MachineSelector
machines={filteredMachines}
@@ -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 }),
);
@@ -366,6 +371,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) => {
@@ -619,6 +646,7 @@ export function useWinder2() {
setPullerTargetDiameter,
setPullerRegulationMode,
setPullerForward,
setPullerGearRatio,
setSpoolAutomaticRequiredMeters,
setSpoolAutomaticAction,
setSpoolRegulationMode,
@@ -42,6 +42,30 @@ export const liveValuesEventDataSchema = z.object({
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 +129,7 @@ export const pullerStateSchema = z.object({
target_speed: z.number(),
target_diameter: z.number(),
forward: z.boolean(),
gear_ratio: gearRatioSchema,
});
/**
+3 -7
View File
@@ -3,11 +3,7 @@ import { Page } from "@/components/Page";
import { SectionTitle } from "@/components/SectionTitle";
import { TouchButton } from "@/components/touch/TouchButton";
import React, { useEffect, useState } from "react";
import {
defaultGithubSource,
GithubSource,
GithubSourceDialog,
} from "./GithubSourceDialog";
import { GithubSourceDialog } from "./GithubSourceDialog";
import { LoadingSpinner } from "@/components/LoadingSpinner";
import { Alert } from "@/components/Alert";
import { useNavigate } from "@tanstack/react-router";
@@ -21,6 +17,7 @@ import {
isNixOSAvailable,
} from "@/helpers/nixos_helpers";
import { useUpdateStore } from "@/stores/updateStore";
import { useGithubSourceStore } from "@/stores/githubSourceStore";
export function ChooseVersionPage() {
const navigate = useNavigate();
@@ -54,8 +51,7 @@ export function ChooseVersionPage() {
string | null
>(null);
const [githubSource, setGithubSource] =
useState<GithubSource>(defaultGithubSource);
const { githubSource, setGithubSource } = useGithubSourceStore();
const githubApiUrl = `https://api.github.com/repos/${githubSource.githubRepoOwner}/${githubSource.githubRepoName}`;
+4 -2
View File
@@ -114,11 +114,13 @@ export function GithubSourceDialogContent({
name="githubRepoOwner"
render={({ field }) => (
<FormItem>
<FormLabel>Serial</FormLabel>
<FormLabel>GitHub Repository Owner</FormLabel>
<FormControl>
<Input placeholder="qitechgmbh" {...field} />
</FormControl>
<FormDescription>Serial number of the machine.</FormDescription>
<FormDescription>
Name of the GitHub repository owner.
</FormDescription>
<FormMessage />
</FormItem>
)}
+29
View File
@@ -0,0 +1,29 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { GithubSource, defaultGithubSource } from "@/setup/GithubSourceDialog";
export type GithubSourceState = {
githubSource: GithubSource;
};
export type GithubSourceActions = {
setGithubSource: (source: GithubSource) => void;
resetGithubSource: () => void;
};
export type GithubSourceStore = GithubSourceState & GithubSourceActions;
export const useGithubSourceStore = create<GithubSourceStore>()(
persist(
(set) => ({
githubSource: defaultGithubSource,
setGithubSource: (source: GithubSource) => set({ githubSource: source }),
resetGithubSource: () => set({ githubSource: defaultGithubSource }),
}),
{
name: "github-source-storage",
},
),
);
+104
View File
@@ -177,3 +177,107 @@
@apply bg-background text-foreground;
}
}
/* macOS-style scrollbars */
@layer base {
/* WebKit browsers (Safari, Chrome, newer Edge) */
::-webkit-scrollbar {
width: 8px;
height: 16px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
border: 2px solid transparent;
background-clip: content-box;
transition: background-color 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.4);
background-clip: content-box;
}
::-webkit-scrollbar-thumb:active {
background: rgba(0, 0, 0, 0.6);
background-clip: content-box;
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* Dark mode adjustments */
.dark ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
background-clip: content-box;
}
.dark ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
background-clip: content-box;
}
.dark ::-webkit-scrollbar-thumb:active {
background: rgba(255, 255, 255, 0.6);
background-clip: content-box;
}
/* Firefox */
html,
body {
scrollbar-width: thin;
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}
.dark html,
.dark body {
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
/* Auto-hide behavior for overlay scrollbars */
@media (pointer: fine) {
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar:hover {
width: 8px;
height: 8px;
}
/* Container hover shows scrollbars */
.scrollbar-visible:hover::-webkit-scrollbar {
width: 8px;
height: 8px;
}
}
/* Utility classes for scrollbar customization */
.scrollbar-hidden {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hidden::-webkit-scrollbar {
display: none;
}
.scrollbar-always-visible::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
}
@media (pointer: fine) {
.scrollbar-always-visible::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
}
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ ethercrab = "0.6"
ethercat_hal_derive = { version = "0.1.0", path = "../ethercat-hal-derive" }
bitvec = { version = "1.0.1", features = ["alloc"] }
smol = "2.0.2"
uom = "0.36.0"
uom = { version = "0.36.0", default-features = false, features = ["f64","si"] }
rand = "0.9.2"
futures = "0.3.31"
tracing = "0.1.41"
+1
View File
@@ -64,6 +64,7 @@
libpcap
nodejs_22
nodePackages.npm
lldb
];
hardeningDisable = [ "fortify" ];
+5 -3
View File
@@ -78,18 +78,20 @@ serviceConfig = {
Restart = "always";
RestartSec = "10s";
# Capabilities (unchanged, as you wanted)
CapabilityBoundingSet = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
AmbientCapabilities = "CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
# Capabilities
CapabilityBoundingSet = "CAP_NET_RAW CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
AmbientCapabilities = "CAP_NET_RAW CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
# Hardening options
NoNewPrivileges = true;
ProtectSystem = "strict";
# Open only /proc/irq explicitly
ReadWritePaths = [ "/proc/irq" ];
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = false;
# Must disable this to allow /proc/irq writes
ProtectKernelTunables = false;
ProtectControlGroups = true;
+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
+2 -3
View File
@@ -102,11 +102,10 @@ installPhase = ''
--name="QiTech Control" \
--single-instance \
--winrm-class="de.qitech.control-electron" \
--enable-gpu \
--enable-gpu \
--ignore-gpu-blocklist \
--enable-gpu-rasterization \
--disable-software-rasterizer \
--use-gl=desktop \
--disable-software-rasterizer \
"\$@"
EOF
chmod +x $out/bin/qitech-control-electron
+4 -3
View File
@@ -55,9 +55,9 @@ 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"
#[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"
@@ -77,6 +77,7 @@ tracing-otel = [
"dep:tonic",
]
mock-machine = []
memory-locking = []
io-uring = []
development-build = []
+2 -14
View File
@@ -1,7 +1,6 @@
use super::setup::setup_loop;
use crate::{
app_state::AppState,
panic::{PanicDetails, send_panic},
socketio::main_namespace::{
MainNamespaceEvents, ethercat_interface_discovery_event::EthercatInterfaceDiscoveryEvent,
},
@@ -10,21 +9,14 @@ use control_core::{
ethercat::interface_discovery::discover_ethercat_interface,
socketio::namespace::NamespaceCacheingLogic,
};
use smol::channel::Sender;
use std::sync::Arc;
pub fn init_ethercat(
thread_panic_tx: Sender<PanicDetails>,
app_state: Arc<AppState>,
) -> Result<(), anyhow::Error> {
let thread_panic_tx_clone = thread_panic_tx;
pub fn init_ethercat(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
let app_state_clone = app_state;
std::thread::Builder::new()
.name("init_ethercat".to_string())
.spawn(move || {
send_panic(thread_panic_tx_clone.clone());
// Notify client via socketio
let app_state_socketio = app_state_clone.clone();
smol::block_on(async move {
@@ -69,11 +61,7 @@ pub fn init_ethercat(
main_namespace.emit(MainNamespaceEvents::EthercatInterfaceDiscoveryEvent(event));
});
if let Err(e) = smol::block_on(setup_loop(
thread_panic_tx_clone,
&interface,
app_state_clone,
)) {
if let Err(e) = smol::block_on(setup_loop(&interface, app_state_clone)) {
tracing::error!("EtherCAT setup failed: {:?}", e);
}
})?;
+2 -10
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;
@@ -20,17 +19,12 @@ use control_core::socketio::namespace::NamespaceCacheingLogic;
use ethercat_hal::devices::devices_from_subdevices;
use ethercrab::std::ethercat_now;
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),
-64
View File
@@ -1,64 +0,0 @@
use std::{thread::sleep, time::Duration};
pub fn init_jemalloc_stats() {
std::thread::Builder::new()
.name("jemalloc-stats".to_string())
.spawn(|| {
let mut last_allocated = 0usize;
let mut last_time = std::time::Instant::now();
loop {
smol::block_on(async {
sleep(Duration::from_secs(10));
// Advance epoch and get allocated bytes
let _ = tikv_jemalloc_ctl::epoch::advance();
if let Ok(allocated_bytes) =
unsafe { tikv_jemalloc_ctl::raw::read::<usize>(b"stats.allocated\0") }
{
let current_time = std::time::Instant::now();
let time_diff = current_time.duration_since(last_time).as_secs_f64();
let change = allocated_bytes as i64 - last_allocated as i64;
let change_per_sec = if time_diff > 0.0 {
(change as f64 / time_diff) as i64
} else {
0
};
let change_str = if change_per_sec >= 0 {
format!("+{} KB/s", format_bytes(change_per_sec))
} else {
format!("-{} KB/s", format_bytes(-change_per_sec))
};
let formatted = format_bytes(allocated_bytes as i64);
tracing::debug!("Memory: {} KB ({})", formatted, change_str);
last_allocated = allocated_bytes;
last_time = current_time;
}
});
}
})
.expect("Failed to spawn jemalloc-stats thread");
}
fn format_bytes(bytes: i64) -> String {
let kb = bytes / 1024;
format!("{}", kb)
.chars()
.rev()
.enumerate()
.map(|(i, c)| {
if i > 0 && i % 3 == 0 {
format!("_{}", c)
} else {
c.to_string()
}
})
.collect::<String>()
.chars()
.rev()
.collect::<String>()
}
+2 -14
View File
@@ -1,22 +1,16 @@
use crate::app_state::AppState;
use crate::panic::{PanicDetails, send_panic};
use bitvec::prelude::*;
use control_core::machines::connection::MachineConnection;
use control_core::realtime::{set_core_affinity, set_realtime_priority};
use smol::channel::Sender;
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>,
app_state: Arc<AppState>,
) -> Result<(), anyhow::Error> {
pub fn init_loop(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
// Start control loop
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
@@ -128,12 +122,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
+37 -213
View File
@@ -1,20 +1,16 @@
use super::{AquaPathV1, AquaPathV1Mode};
use crate::machines::aquapath1::{
Flow, Temperature, api::AquaPathV1Namespace, controller::Controller,
use crate::machines::{
aquapath1::{Flow, Temperature, api::AquaPathV1Namespace, controller::Controller},
get_ethercat_device,
};
use anyhow::Error;
use control_core::machines::{
identification::DeviceHardwareIdentification,
new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_device_identification_by_role,
get_ethercat_device_by_index, get_subdevice_by_index, validate_no_role_dublicates,
validate_same_machine_identification_unique,
},
use control_core::machines::new::{
MachineNewHardware, MachineNewParams, MachineNewTrait, get_subdevice_by_index,
validate_no_role_dublicates, validate_same_machine_identification_unique,
};
use ethercat_hal::{
coe::ConfigurableDevice,
devices::{
EthercatDeviceUsed, downcast_device,
ek1100::{EK1100, EK1100_IDENTITY_A},
el2008::{EL2008, EL2008_IDENTITY_A, EL2008Port},
el3204::{EL3204, EL3204_IDENTITY_A, EL3204_IDENTITY_B, EL3204Port},
@@ -23,7 +19,6 @@ use ethercat_hal::{
EL5152, EL5152_IDENTITY_A, EL5152Configuration, EL5152Port,
EL5152PredefinedPdoAssignment,
},
subdevice_identity_to_tuple,
},
io::{
analog_output::AnalogOutput, digital_output::DigitalOutput, encoder_input::EncoderInput,
@@ -56,217 +51,46 @@ impl MachineNewTrait for AquaPathV1 {
smol::block_on(async {
// Role 0 - Buscoupler EK1100
{
let device_identification =
get_device_identification_by_role(params.device_group, 0)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 0 is not Ethercat",
module_path!()
))?,
};
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
let subdevice_identity = subdevice.identity();
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EK1100_IDENTITY_A => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EK1100>(ethercat_device).await?
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 0 is not an EK1100",
module_path!()
));
}
};
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
}
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
// Role 1 - EL2008 Digital Output Module
let el2008 = {
let device_identification =
get_device_identification_by_role(params.device_group, 1)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 1 is not Ethercat",
module_path!()
))?,
};
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
let subdevice_identity = subdevice.identity();
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EL2008_IDENTITY_A => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EL2008>(ethercat_device).await?
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 1 is not an EL2008",
module_path!()
));
}
};
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
device
};
let el2008 =
get_ethercat_device::<EL2008>(hardware, params, 1, [EL2008_IDENTITY_A].to_vec())
.await?
.0;
// Role 2 - EL4002 Analog Output Module
let el4002 = {
let device_identification =
get_device_identification_by_role(params.device_group, 2)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 2 is not Ethercat",
module_path!()
))?,
};
let el4002 =
get_ethercat_device::<EL4002>(hardware, params, 2, [EL4002_IDENTITY_A].to_vec())
.await?
.0;
let subdevice = get_subdevice_by_index(
hardware.subdevices,
device_hardware_identification_ethercat.subdevice_index,
)?;
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice_identity = subdevice.identity();
let el3204 = get_ethercat_device::<EL3204>(
hardware,
params,
3,
[EL3204_IDENTITY_A, EL3204_IDENTITY_B].to_vec(),
)
.await?
.0;
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EL4002_IDENTITY_A => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EL4002>(ethercat_device).await?
}
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 2 is not an EL4002",
module_path!()
))?,
};
let el5152 =
get_ethercat_device::<EL5152>(hardware, params, 4, [EL5152_IDENTITY_A].to_vec())
.await?
.0;
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
device
};
let el3204 = {
let device_identification =
get_device_identification_by_role(params.device_group, 3)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 3 is not Ethercat",
module_path!()
))?, //uncommented
};
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
let subdevice_identity = subdevice.identity();
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EL3204_IDENTITY_A | EL3204_IDENTITY_B => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EL3204>(ethercat_device).await?
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 3 is not an EL3204",
module_path!()
));
}
};
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
device
let config = EL5152Configuration {
pdo_assignment: EL5152PredefinedPdoAssignment::Frequency,
..Default::default()
};
let subdevice = get_subdevice_by_index(hardware.subdevices, 4)?;
el5152
.write()
.await
.write_config(&subdevice, &config)
.await?;
let el5152 = {
let device_identification =
get_device_identification_by_role(params.device_group, 4)?;
let device_hardware_identification_ethercat = match &device_identification
.device_hardware_identification
{
DeviceHardwareIdentification::Ethercat(
device_hardware_identification_ethercat,
) => device_hardware_identification_ethercat,
_ => Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 4 is not Ethercat",
module_path!()
))?,
};
let subdevice_index = device_hardware_identification_ethercat.subdevice_index;
let subdevice = get_subdevice_by_index(hardware.subdevices, subdevice_index)?;
let subdevice_identity = subdevice.identity();
let device = match subdevice_identity_to_tuple(&subdevice_identity) {
EL5152_IDENTITY_A => {
let ethercat_device = get_ethercat_device_by_index(
&hardware.ethercat_devices,
subdevice_index,
)?;
downcast_device::<EL5152>(ethercat_device).await?
}
_ => {
return Err(anyhow::anyhow!(
"[{}::MachineNewTrait/AquaPath1::new] Device with role 4 is not an EL5152",
module_path!()
));
}
};
let config = EL5152Configuration {
pdo_assignment: EL5152PredefinedPdoAssignment::Frequency,
..Default::default()
};
device
.write()
.await
.write_config(&subdevice, &config)
.await?;
{
let mut device_guard = device.write().await;
device_guard.set_used(true);
}
device
};
let enc1 = EncoderInput::new(el5152.clone(), EL5152Port::ENC1);
let enc2 = EncoderInput::new(el5152.clone(), EL5152Port::ENC2);
+1 -3
View File
@@ -86,9 +86,7 @@ impl MachineNewTrait for ExtruderV2 {
// if its async the compiler thinks &subdevices is persisted in the future which might never execute
// so we can't drop subdevices unless this machine is dropped, which is bad
smol::block_on(async {
// Role 0
// Buscoupler
// EK1100
// Role 0 - Buscoupler EK1100
let _ek1100 =
get_ethercat_device::<EK1100>(hardware, params, 0, [EK1100_IDENTITY_A].to_vec());
@@ -186,7 +186,7 @@ impl AdaptiveSpoolSpeedController {
puller_speed_controller: &PullerSpeedController,
) -> AngularVelocity {
let min_speed = AngularVelocity::ZERO;
let max_speed = self.get_max_speed(puller_speed_controller);
let max_speed = self.get_max_speed(puller_speed_controller).abs();
// Calculate filament tension from arm angle
let tension_arm_angle = tension_arm.get_angle();
+5
View File
@@ -1,3 +1,4 @@
use super::puller_speed_controller::GearRatio;
use super::{Winder2, Winder2Mode, puller_speed_controller::PullerRegulationMode};
use control_core::{
machines::{
@@ -76,6 +77,7 @@ enum Mutation {
SetPullerTargetSpeed(f64),
SetPullerTargetDiameter(f64),
SetPullerForward(bool),
SetPullerGearRatio(GearRatio),
// Spool Speed Controller
SetSpoolRegulationMode(super::spool_speed_controller::SpoolSpeedControllerType),
@@ -190,6 +192,8 @@ 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)]
@@ -296,6 +300,7 @@ 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),
+512
View File
@@ -0,0 +1,512 @@
use std::time::Instant;
use crate::machines::buffer1::BufferV1;
use super::puller_speed_controller::PullerRegulationMode;
use super::{TraverseMode, Winder2, Winder2Mode, api, spool_speed_controller};
use api::{
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode, SpoolAutomaticActionState,
SpoolSpeedControllerState, StateEvent, TensionArmState, TraverseState, Winder2Events,
};
use control_core::socketio::event::BuildEvent;
use control_core::{
machines::identification::MachineIdentificationUnique,
socketio::namespace::NamespaceCacheingLogic, uom_extensions::velocity::meter_per_minute,
};
use uom::si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
length::{meter, millimeter},
};
impl Winder2 {
/// Implement Spool
/// called by `act`
pub fn sync_spool_speed(&mut self, t: Instant) {
let angular_velocity = self.spool_speed_controller.update_speed(
t,
&self.tension_arm,
&self.puller_speed_controller,
);
let steps_per_second = self
.spool_step_converter
.angular_velocity_to_steps(angular_velocity);
let _ = self.spool.set_speed(steps_per_second);
}
pub fn stop_or_pull_spool(&mut self, now: Instant) {
if matches!(
self.spool_automatic_action.mode,
SpoolAutomaticActionMode::NoAction
) {
self.calculate_spool_auto_progress_(now);
return;
}
match self.mode {
Winder2Mode::Pull => self.calculate_spool_auto_progress_(now),
Winder2Mode::Wind => self.calculate_spool_auto_progress_(now),
_ => {
self.spool_automatic_action.progress_last_check = now;
return;
}
}
if self.spool_automatic_action.progress >= self.spool_automatic_action.target_length {
match self.spool_automatic_action.mode {
SpoolAutomaticActionMode::NoAction => (),
SpoolAutomaticActionMode::Pull => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&Winder2Mode::Pull);
}
SpoolAutomaticActionMode::Hold => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&Winder2Mode::Hold);
}
}
}
}
/// Implement Mode
pub fn set_mode(&mut self, mode: &Winder2Mode) {
let should_update = *mode != Winder2Mode::Wind || self.can_wind();
if should_update {
// all transitions are allowed
self.mode = mode.clone();
// Apply the mode changes to the spool and puller
self.set_spool_mode(mode);
self.set_puller_mode(mode);
self.set_traverse_mode(mode);
}
self.emit_state();
}
/// Implement Traverse
pub fn set_laser(&mut self, value: bool) {
self.laser.set(value);
self.emit_state();
}
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
let new_inner = Length::new::<millimeter>(limit);
let current_outer = self.traverse_controller.get_limit_outer();
// Validate the new inner limit against current outer limit
if !Self::validate_traverse_limits(new_inner, current_outer) {
// Don't update if validation fails - keep the current value
return;
}
self.traverse_controller.set_limit_inner(new_inner);
self.emit_state();
}
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
let new_outer = Length::new::<millimeter>(limit);
let current_inner = self.traverse_controller.get_limit_inner();
// Validate the new outer limit against current inner limit
if !Self::validate_traverse_limits(current_inner, new_outer) {
// Don't update if validation fails - keep the current value
return;
}
self.traverse_controller.set_limit_outer(new_outer);
self.emit_state();
}
pub fn traverse_set_step_size(&mut self, step_size: f64) {
let step_size = Length::new::<millimeter>(step_size);
self.traverse_controller.set_step_size(step_size);
self.emit_state();
}
pub fn traverse_set_padding(&mut self, padding: f64) {
let padding = Length::new::<millimeter>(padding);
self.traverse_controller.set_padding(padding);
self.emit_state();
}
pub fn traverse_goto_limit_inner(&mut self) {
if self.can_go_in() {
self.traverse_controller.goto_limit_inner();
}
self.emit_state();
}
pub fn traverse_goto_limit_outer(&mut self) {
if self.can_go_out() {
self.traverse_controller.goto_limit_outer();
}
self.emit_state();
}
pub fn traverse_goto_home(&mut self) {
if self.can_go_home() {
self.traverse_controller.goto_home();
}
self.emit_state();
}
pub fn emit_live_values(&mut self) {
let angle_deg = self.tension_arm.get_angle().get::<degree>();
// Wrap [270;<360] to [-90; 0]
// This is done to reduce flicker in the graphs around the zero point
let angle_deg = if angle_deg >= 270.0 {
angle_deg - 360.0
} else {
angle_deg
};
// Calculate puller speed from current motor steps
let steps_per_second = self.puller.get_speed();
let angular_velocity = self
.puller_speed_controller
.converter
.steps_to_angular_velocity(steps_per_second as f64);
let motor_speed = self
.puller_speed_controller
.angular_velocity_to_speed(angular_velocity);
// 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
let spool_rpm = self
.spool_step_converter
.steps_to_angular_velocity(self.spool.get_speed() as f64)
.get::<revolution_per_minute>();
let live_values = LiveValuesEvent {
traverse_position: self
.traverse_controller
.get_current_position()
.map(|x| x.get::<millimeter>()),
puller_speed: puller_speed.get::<meter_per_minute>().abs(),
spool_rpm,
tension_arm_angle: angle_deg,
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
};
let event = live_values.build();
self.namespace.emit(Winder2Events::LiveValues(event));
}
pub fn build_state_event(&mut self) -> StateEvent {
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
traverse_state: TraverseState {
limit_inner: self
.traverse_controller
.get_limit_inner()
.get::<millimeter>(),
limit_outer: self
.traverse_controller
.get_limit_outer()
.get::<millimeter>(),
position_in: self
.traverse_controller
.get_limit_inner()
.get::<millimeter>(),
position_out: self
.traverse_controller
.get_limit_outer()
.get::<millimeter>(),
is_going_in: self.traverse_controller.is_going_in(),
is_going_out: self.traverse_controller.is_going_out(),
is_homed: self.traverse_controller.is_homed(),
is_going_home: self.traverse_controller.is_going_home(),
is_traversing: self.traverse_controller.is_traversing(),
laserpointer: self.laser.get(),
step_size: self.traverse_controller.get_step_size().get::<millimeter>(),
padding: self.traverse_controller.get_padding().get::<millimeter>(),
can_go_in: self.can_go_in(),
can_go_out: self.can_go_out(),
can_go_home: self.can_go_home(),
},
puller_state: PullerState {
regulation: self.puller_speed_controller.regulation_mode.clone(),
target_speed: self
.puller_speed_controller
.target_speed
.get::<meter_per_minute>(),
target_diameter: self
.puller_speed_controller
.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(),
can_wind: self.can_wind(),
},
tension_arm_state: TensionArmState {
zeroed: self.tension_arm.zeroed,
},
spool_speed_controller_state: SpoolSpeedControllerState {
regulation_mode: self.spool_speed_controller.get_type().clone(),
minmax_min_speed: self
.spool_speed_controller
.get_minmax_min_speed()
.get::<revolution_per_minute>(),
minmax_max_speed: self
.spool_speed_controller
.get_minmax_max_speed()
.get::<revolution_per_minute>(),
adaptive_tension_target: self.spool_speed_controller.get_adaptive_tension_target(),
adaptive_radius_learning_rate: self
.spool_speed_controller
.get_adaptive_radius_learning_rate(),
adaptive_max_speed_multiplier: self
.spool_speed_controller
.get_adaptive_max_speed_multiplier(),
adaptive_acceleration_factor: self
.spool_speed_controller
.get_adaptive_acceleration_factor(),
adaptive_deacceleration_urgency_multiplier: self
.spool_speed_controller
.get_adaptive_deacceleration_urgency_multiplier(),
},
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(),
}
}
pub fn emit_state(&mut self) {
let state_event = self.build_state_event();
let event = state_event.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) {
// Convert to `Winder2Mode` to `TraverseMode`
let mode: TraverseMode = mode.clone().into();
// If coming out of standby
if self.traverse_mode == TraverseMode::Standby && mode != TraverseMode::Standby {
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
}
// If going into standby
if mode == TraverseMode::Standby && self.traverse_mode != TraverseMode::Standby {
// If we are going into standby, we need to stop the traverse
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
// Transition matrix
match self.traverse_mode {
TraverseMode::Standby => match mode {
TraverseMode::Standby => {}
TraverseMode::Hold => {
// From [`TraverseMode::Standby`] to [`TraverseMode::Hold`]
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
self.traverse_controller.goto_home();
}
TraverseMode::Traverse => {
// From [`TraverseMode::Standby`] to [`TraverseMode::Wind`]
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
self.traverse_controller.start_traversing();
}
},
TraverseMode::Hold => match mode {
TraverseMode::Standby => {
// From [`TraverseMode::Hold`] to [`TraverseMode::Standby`]
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
TraverseMode::Hold => {}
TraverseMode::Traverse => {
// From [`TraverseMode::Hold`] to [`TraverseMode::Wind`]
self.traverse_controller.start_traversing();
}
},
TraverseMode::Traverse => match mode {
TraverseMode::Standby => {
// From [`TraverseMode::Wind`] to [`TraverseMode::Standby`]
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
TraverseMode::Hold => {
// From [`TraverseMode::Wind`] to [`TraverseMode::Hold`]
self.traverse_controller.goto_home();
}
TraverseMode::Traverse => {}
},
}
// Update the internal state
self.traverse_mode = mode;
self.emit_state();
}
/// Implement Tension Arm
pub fn tension_arm_zero(&mut self) {
self.tension_arm.zero();
self.emit_live_values(); // For angle update
// For state update
self.emit_state();
}
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
self.spool_automatic_action.target_length = Length::new::<meter>(meters);
self.emit_state();
}
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
self.spool_automatic_action.mode = mode;
self.emit_state();
}
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
self.puller_speed_controller
.set_regulation_mode(puller_regulation_mode);
self.emit_state();
}
/// Set target speed in m/min
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
// Convert m/min to velocity
let target_speed = Velocity::new::<meter_per_minute>(target_speed);
self.puller_speed_controller.set_target_speed(target_speed);
self.emit_state();
}
/// Set target diameter in mm
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
// Convert m/min to velocity
let target_diameter = Length::new::<millimeter>(target_diameter);
self.puller_speed_controller
.set_target_diameter(target_diameter);
self.emit_state();
}
/// Set forward direction
pub fn puller_set_forward(&mut self, forward: bool) {
self.puller_speed_controller.set_forward(forward);
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,
regulation_mode: spool_speed_controller::SpoolSpeedControllerType,
) {
self.spool_speed_controller.set_type(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) {
let min_speed = uom::si::f64::AngularVelocity::new::<revolution_per_minute>(min_speed_rpm);
if let Err(e) = self.spool_speed_controller.set_minmax_min_speed(min_speed) {
tracing::error!("Failed to set spool min speed: {:?}", e);
}
self.emit_state();
}
/// Set maximum speed for minmax mode in RPM
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
let max_speed = uom::si::f64::AngularVelocity::new::<revolution_per_minute>(max_speed_rpm);
if let Err(e) = self.spool_speed_controller.set_minmax_max_speed(max_speed) {
tracing::error!("Failed to set spool max speed: {:?}", e);
}
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
.set_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
.set_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
.set_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
.set_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
.set_adaptive_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
self.emit_state();
}
/// implement machine connection
/// set connected buffer
pub fn set_connected_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
BufferV1::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_buffer
.set_connected_machine(&machine_identification_unique);
self.connected_buffer.reverse_connect();
self.emit_state();
}
/// disconnect buffer
pub fn disconnect_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
BufferV1::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_buffer.reverse_disconnect();
self.connected_buffer.disconnect();
self.emit_state();
}
}
@@ -77,7 +77,7 @@ impl FilamentTensionCalculator {
pub fn calc_filament_length(&self, tension_arm_angle: Angle) -> Length {
let tension_arm_angle_rad = self
.angle_converter
.decode_f64(tension_arm_angle)
.decode(tension_arm_angle)
.get::<radian>();
// Calculate tension arm tip position (flipped Y-axis: 0° = down, 90° = left)
+5 -492
View File
@@ -2,6 +2,7 @@ pub mod act;
pub mod adaptive_spool_speed_controller;
pub mod api;
pub mod clamp_revolution;
pub mod emit;
pub mod filament_tension;
pub mod minmax_spool_speed_controller;
pub mod new;
@@ -12,12 +13,7 @@ pub mod traverse_controller;
use std::{fmt::Debug, sync::Weak, time::Instant};
use api::{
LiveValuesEvent, ModeState, PullerState, SpoolAutomaticActionMode, SpoolAutomaticActionState,
SpoolSpeedControllerState, StateEvent, TensionArmState, TraverseState, Winder2Events,
Winder2Namespace,
};
use control_core::socketio::event::BuildEvent;
use api::{SpoolAutomaticActionMode, Winder2Namespace};
use control_core::{
converters::angular_step_converter::AngularStepConverter,
machines::{
@@ -25,15 +21,13 @@ use control_core::{
identification::{MachineIdentification, MachineIdentificationUnique},
manager::MachineManager,
},
socketio::namespace::NamespaceCacheingLogic,
uom_extensions::velocity::meter_per_minute,
};
use control_core_derive::Machine;
use ethercat_hal::io::{
digital_input::DigitalInput, digital_output::DigitalOutput,
stepper_velocity_el70x1::StepperVelocityEL70x1,
};
use puller_speed_controller::{PullerRegulationMode, PullerSpeedController};
use puller_speed_controller::PullerSpeedController;
use smol::lock::RwLock;
use spool_speed_controller::SpoolSpeedController;
use tension_arm::TensionArm;
@@ -41,9 +35,7 @@ use traverse_controller::TraverseController;
use uom::{
ConstZero,
si::{
angle::degree,
angular_velocity::revolution_per_minute,
f64::{Length, Velocity},
f64::Length,
length::{meter, millimeter},
velocity::meter_per_second,
},
@@ -116,12 +108,6 @@ impl Winder2 {
machine: MACHINE_WINDER_V1,
};
/// Implement Traverse
fn set_laser(&mut self, value: bool) {
self.laser.set(value);
self.emit_state();
}
/// Validates that traverse limits maintain proper constraints:
/// - Inner limit must be smaller than outer limit
/// - At least 0.9mm difference between inner and outer limits
@@ -129,197 +115,6 @@ impl Winder2 {
outer > inner + Length::new::<millimeter>(0.9)
}
pub fn traverse_set_limit_inner(&mut self, limit: f64) {
let new_inner = Length::new::<millimeter>(limit);
let current_outer = self.traverse_controller.get_limit_outer();
// Validate the new inner limit against current outer limit
if !Self::validate_traverse_limits(new_inner, current_outer) {
// Don't update if validation fails - keep the current value
return;
}
self.traverse_controller.set_limit_inner(new_inner);
self.emit_state();
}
pub fn traverse_set_limit_outer(&mut self, limit: f64) {
let new_outer = Length::new::<millimeter>(limit);
let current_inner = self.traverse_controller.get_limit_inner();
// Validate the new outer limit against current inner limit
if !Self::validate_traverse_limits(current_inner, new_outer) {
// Don't update if validation fails - keep the current value
return;
}
self.traverse_controller.set_limit_outer(new_outer);
self.emit_state();
}
pub fn traverse_set_step_size(&mut self, step_size: f64) {
let step_size = Length::new::<millimeter>(step_size);
self.traverse_controller.set_step_size(step_size);
self.emit_state();
}
pub fn traverse_set_padding(&mut self, padding: f64) {
let padding = Length::new::<millimeter>(padding);
self.traverse_controller.set_padding(padding);
self.emit_state();
}
pub fn traverse_goto_limit_inner(&mut self) {
if self.can_go_in() {
self.traverse_controller.goto_limit_inner();
}
self.emit_state();
}
pub fn traverse_goto_limit_outer(&mut self) {
if self.can_go_out() {
self.traverse_controller.goto_limit_outer();
}
self.emit_state();
}
pub fn traverse_goto_home(&mut self) {
if self.can_go_home() {
self.traverse_controller.goto_home();
}
self.emit_state();
}
pub fn emit_live_values(&mut self) {
let angle_deg = self.tension_arm.get_angle().get::<degree>();
// Wrap [270;<360] to [-90; 0]
// This is done to reduce flicker in the graphs around the zero point
let angle_deg = if angle_deg >= 270.0 {
angle_deg - 360.0
} else {
angle_deg
};
// Calculate puller speed from current motor steps
let steps_per_second = self.puller.get_speed();
let angular_velocity = self
.puller_speed_controller
.converter
.steps_to_angular_velocity(steps_per_second as f64);
let puller_speed = self
.puller_speed_controller
.angular_velocity_to_speed(angular_velocity);
// Calculate spool RPM from current motor steps
let spool_rpm = self
.spool_step_converter
.steps_to_angular_velocity(self.spool.get_speed() as f64)
.get::<revolution_per_minute>();
let live_values = LiveValuesEvent {
traverse_position: self
.traverse_controller
.get_current_position()
.map(|x| x.get::<millimeter>()),
puller_speed: puller_speed.get::<meter_per_minute>(),
spool_rpm,
tension_arm_angle: angle_deg,
spool_progress: self.spool_automatic_action.progress.get::<meter>(),
};
let event = live_values.build();
self.namespace.emit(Winder2Events::LiveValues(event));
}
pub fn build_state_event(&mut self) -> StateEvent {
StateEvent {
is_default_state: !std::mem::replace(&mut self.emitted_default_state, true),
traverse_state: TraverseState {
limit_inner: self
.traverse_controller
.get_limit_inner()
.get::<millimeter>(),
limit_outer: self
.traverse_controller
.get_limit_outer()
.get::<millimeter>(),
position_in: self
.traverse_controller
.get_limit_inner()
.get::<millimeter>(),
position_out: self
.traverse_controller
.get_limit_outer()
.get::<millimeter>(),
is_going_in: self.traverse_controller.is_going_in(),
is_going_out: self.traverse_controller.is_going_out(),
is_homed: self.traverse_controller.is_homed(),
is_going_home: self.traverse_controller.is_going_home(),
is_traversing: self.traverse_controller.is_traversing(),
laserpointer: self.laser.get(),
step_size: self.traverse_controller.get_step_size().get::<millimeter>(),
padding: self.traverse_controller.get_padding().get::<millimeter>(),
can_go_in: self.can_go_in(),
can_go_out: self.can_go_out(),
can_go_home: self.can_go_home(),
},
puller_state: PullerState {
regulation: self.puller_speed_controller.regulation_mode.clone(),
target_speed: self
.puller_speed_controller
.target_speed
.get::<meter_per_minute>(),
target_diameter: self
.puller_speed_controller
.target_diameter
.get::<millimeter>(),
forward: self.puller_speed_controller.forward,
},
mode_state: ModeState {
mode: self.mode.clone().into(),
can_wind: self.can_wind(),
},
tension_arm_state: TensionArmState {
zeroed: self.tension_arm.zeroed,
},
spool_speed_controller_state: SpoolSpeedControllerState {
regulation_mode: self.spool_speed_controller.get_type().clone(),
minmax_min_speed: self
.spool_speed_controller
.get_minmax_min_speed()
.get::<revolution_per_minute>(),
minmax_max_speed: self
.spool_speed_controller
.get_minmax_max_speed()
.get::<revolution_per_minute>(),
adaptive_tension_target: self.spool_speed_controller.get_adaptive_tension_target(),
adaptive_radius_learning_rate: self
.spool_speed_controller
.get_adaptive_radius_learning_rate(),
adaptive_max_speed_multiplier: self
.spool_speed_controller
.get_adaptive_max_speed_multiplier(),
adaptive_acceleration_factor: self
.spool_speed_controller
.get_adaptive_acceleration_factor(),
adaptive_deacceleration_urgency_multiplier: self
.spool_speed_controller
.get_adaptive_deacceleration_urgency_multiplier(),
},
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(),
}
}
pub fn emit_state(&mut self) {
let state_event = self.build_state_event();
let event = state_event.build();
self.namespace.emit(Winder2Events::State(event));
}
pub fn sync_traverse_speed(&mut self) {
self.traverse_controller.update_speed(
&mut self.traverse,
@@ -372,22 +167,6 @@ impl Winder2 {
&& self.mode != Winder2Mode::Wind
}
/// Implement Mode
fn set_mode(&mut self, mode: &Winder2Mode) {
let should_update = *mode != Winder2Mode::Wind || self.can_wind();
if should_update {
// all transitions are allowed
self.mode = mode.clone();
// Apply the mode changes to the spool and puller
self.set_spool_mode(mode);
self.set_puller_mode(mode);
self.set_traverse_mode(mode);
}
self.emit_state();
}
/// Apply the mode changes to the spool
///
/// It contains a transition matrix for atomic changes.
@@ -440,75 +219,6 @@ impl Winder2 {
self.spool_mode = mode;
}
/// 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) {
// Convert to `Winder2Mode` to `TraverseMode`
let mode: TraverseMode = mode.clone().into();
// If coming out of standby
if self.traverse_mode == TraverseMode::Standby && mode != TraverseMode::Standby {
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
}
// If going into standby
if mode == TraverseMode::Standby && self.traverse_mode != TraverseMode::Standby {
// If we are going into standby, we need to stop the traverse
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
// Transition matrix
match self.traverse_mode {
TraverseMode::Standby => match mode {
TraverseMode::Standby => {}
TraverseMode::Hold => {
// From [`TraverseMode::Standby`] to [`TraverseMode::Hold`]
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
self.traverse_controller.goto_home();
}
TraverseMode::Traverse => {
// From [`TraverseMode::Standby`] to [`TraverseMode::Wind`]
self.traverse.set_enabled(true);
self.traverse_controller.set_enabled(true);
self.traverse_controller.start_traversing();
}
},
TraverseMode::Hold => match mode {
TraverseMode::Standby => {
// From [`TraverseMode::Hold`] to [`TraverseMode::Standby`]
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
TraverseMode::Hold => {}
TraverseMode::Traverse => {
// From [`TraverseMode::Hold`] to [`TraverseMode::Wind`]
self.traverse_controller.start_traversing();
}
},
TraverseMode::Traverse => match mode {
TraverseMode::Standby => {
// From [`TraverseMode::Wind`] to [`TraverseMode::Standby`]
self.traverse.set_enabled(false);
self.traverse_controller.set_enabled(false);
}
TraverseMode::Hold => {
// From [`TraverseMode::Wind`] to [`TraverseMode::Hold`]
self.traverse_controller.goto_home();
}
TraverseMode::Traverse => {}
},
}
// Update the internal state
self.traverse_mode = mode;
self.emit_state();
}
/// Apply the mode changes to the puller
///
/// It contains a transition matrix for atomic changes.
@@ -560,71 +270,6 @@ impl Winder2 {
self.puller_mode = mode;
}
/// Implement Tension Arm
fn tension_arm_zero(&mut self) {
self.tension_arm.zero();
self.emit_live_values(); // For angle update
// For state update
self.emit_state();
}
/// Implement Spool
/// called by `act`
pub fn sync_spool_speed(&mut self, t: Instant) {
let angular_velocity = self.spool_speed_controller.update_speed(
t,
&self.tension_arm,
&self.puller_speed_controller,
);
let steps_per_second = self
.spool_step_converter
.angular_velocity_to_steps(angular_velocity);
let _ = self.spool.set_speed(steps_per_second);
}
pub fn set_spool_automatic_required_meters(&mut self, meters: f64) {
self.spool_automatic_action.target_length = Length::new::<meter>(meters);
self.emit_state();
}
pub fn set_spool_automatic_mode(&mut self, mode: SpoolAutomaticActionMode) {
self.spool_automatic_action.mode = mode;
self.emit_state();
}
pub fn stop_or_pull_spool(&mut self, now: Instant) {
if matches!(
self.spool_automatic_action.mode,
SpoolAutomaticActionMode::NoAction
) {
self.calculate_spool_auto_progress_(now);
return;
}
match self.mode {
Winder2Mode::Pull => self.calculate_spool_auto_progress_(now),
Winder2Mode::Wind => self.calculate_spool_auto_progress_(now),
_ => {
self.spool_automatic_action.progress_last_check = now;
return;
}
}
if self.spool_automatic_action.progress >= self.spool_automatic_action.target_length {
match self.spool_automatic_action.mode {
SpoolAutomaticActionMode::NoAction => (),
SpoolAutomaticActionMode::Pull => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&Winder2Mode::Pull);
}
SpoolAutomaticActionMode::Hold => {
self.stop_or_pull_spool_reset(now);
self.set_mode(&Winder2Mode::Hold);
}
}
}
}
pub const fn stop_or_pull_spool_reset(&mut self, now: Instant) {
self.spool_automatic_action.progress = Length::ZERO;
self.spool_automatic_action.progress_last_check = now;
@@ -645,7 +290,7 @@ impl Winder2 {
);
// Update total meters pulled
self.spool_automatic_action.progress += meters_pulled_this_interval;
self.spool_automatic_action.progress += meters_pulled_this_interval.abs();
self.spool_automatic_action.progress_last_check = now;
}
@@ -659,138 +304,6 @@ impl Winder2 {
.angular_velocity_to_steps(angular_velocity);
let _ = self.puller.set_speed(steps_per_second);
}
pub fn puller_set_regulation(&mut self, puller_regulation_mode: PullerRegulationMode) {
self.puller_speed_controller
.set_regulation_mode(puller_regulation_mode);
self.emit_state();
}
/// Set target speed in m/min
pub fn puller_set_target_speed(&mut self, target_speed: f64) {
// Convert m/min to velocity
let target_speed = Velocity::new::<meter_per_minute>(target_speed);
self.puller_speed_controller.set_target_speed(target_speed);
self.emit_state();
}
/// Set target diameter in mm
pub fn puller_set_target_diameter(&mut self, target_diameter: f64) {
// Convert m/min to velocity
let target_diameter = Length::new::<millimeter>(target_diameter);
self.puller_speed_controller
.set_target_diameter(target_diameter);
self.emit_state();
}
/// Set forward direction
pub fn puller_set_forward(&mut self, forward: bool) {
self.puller_speed_controller.set_forward(forward);
self.emit_state();
}
// Spool Speed Controller API methods
pub fn spool_set_regulation_mode(
&mut self,
regulation_mode: spool_speed_controller::SpoolSpeedControllerType,
) {
self.spool_speed_controller.set_type(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) {
let min_speed = uom::si::f64::AngularVelocity::new::<revolution_per_minute>(min_speed_rpm);
if let Err(e) = self.spool_speed_controller.set_minmax_min_speed(min_speed) {
tracing::error!("Failed to set spool min speed: {:?}", e);
}
self.emit_state();
}
/// Set maximum speed for minmax mode in RPM
pub fn spool_set_minmax_max_speed(&mut self, max_speed_rpm: f64) {
let max_speed = uom::si::f64::AngularVelocity::new::<revolution_per_minute>(max_speed_rpm);
if let Err(e) = self.spool_speed_controller.set_minmax_max_speed(max_speed) {
tracing::error!("Failed to set spool max speed: {:?}", e);
}
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
.set_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
.set_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
.set_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
.set_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
.set_adaptive_deacceleration_urgency_multiplier(deacceleration_urgency_multiplier);
self.emit_state();
}
/// implement machine connection
/// set connected buffer
pub fn set_connected_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
BufferV1::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_buffer
.set_connected_machine(&machine_identification_unique);
self.connected_buffer.reverse_connect();
self.emit_state();
}
/// disconnect buffer
pub fn disconnect_buffer(
&mut self,
machine_identification_unique: MachineIdentificationUnique,
) {
if !matches!(
machine_identification_unique.machine_identification,
BufferV1::MACHINE_IDENTIFICATION
) {
return;
}
self.connected_buffer.reverse_disconnect();
self.connected_buffer.disconnect();
self.emit_state();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -14,6 +14,30 @@ use uom::{
si::f64::{Acceleration, AngularVelocity, Jerk, Length, Velocity},
};
#[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,
@@ -22,6 +46,8 @@ pub struct PullerSpeedController {
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
@@ -45,6 +71,7 @@ impl PullerSpeedController {
target_diameter,
regulation_mode: PullerRegulationMode::Speed,
forward: true,
gear_ratio: GearRatio::default(),
acceleration_controller: LinearJerkSpeedController::new_simple(
Some(speed),
acceleration,
@@ -75,8 +102,16 @@ 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 {
let base_speed = match self.enabled {
true => match self.regulation_mode {
PullerRegulationMode::Speed => self.target_speed,
PullerRegulationMode::Diameter => unimplemented!(),
@@ -84,6 +119,9 @@ impl PullerSpeedController {
false => Velocity::ZERO,
};
// 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);
+18 -41
View File
@@ -1,30 +1,17 @@
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
use tikv_jemallocator::Jemalloc;
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
use app_state::AppState;
use r#loop::init_loop;
#[cfg(feature = "mock-machine")]
use mock::init::init_mock;
use std::{sync::Arc, time::Duration};
use r#loop::init_loop;
use rest::init::init_api;
#[cfg(not(feature = "mock-machine"))]
use serial::init::init_serial;
use std::sync::Arc;
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
use jemalloc_stats::init_jemalloc_stats;
#[cfg(not(feature = "mock-machine"))]
use crate::ethercat::init::init_ethercat;
use crate::panic::init_panic;
use crate::panic::init_panic_handling;
use crate::socketio::queue::init_socketio_queue;
pub mod app_state;
@@ -40,53 +27,42 @@ pub mod rest;
pub mod serial;
pub mod socketio;
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
pub mod jemalloc_stats;
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");
// lock memory (not working thus commented out)
// if let Err(e) = lock_memory() {
// tracing::error!("[{}::main] Failed to lock memory: {:?}", module_path!(), e);
// } else {
// tracing::info!("[{}::main] Memory locked successfully", module_path!());
// }
#[cfg(all(not(target_env = "msvc"), not(feature = "dhat-heap")))]
init_jemalloc_stats();
init_panic_handling();
#[cfg(feature = "memory-locking")]
if let Err(e) = control_core::realtime::lock_memory() {
tracing::error!("[{}::main] Failed to lock memory: {:?}", module_path!(), e);
} else {
tracing::info!("[{}::main] Memory locked successfully", module_path!());
}
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();
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");
init_socketio_queue(app_state.clone());
init_api(app_state.clone()).expect("Failed to initialize API");
init_loop(app_state.clone()).expect("Failed to initialize loop");
#[cfg(feature = "mock-machine")]
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
#[cfg(not(feature = "mock-machine"))]
init_serial(thread_panic_tx.clone(), app_state.clone())
.expect("Failed to initialize Serial");
init_serial(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");
init_ethercat(app_state).expect("Failed to initialize EtherCAT");
}
})
.expect("Failed to spawn init thread");
@@ -97,7 +73,8 @@ fn main() {
// Keep the main thread alive indefinitely
// The program should only exit via panic handling or external signals
loop {
std::thread::sleep(Duration::from_secs(u64::MAX));
// better way to "sleep", uses basically
std::thread::park();
}
}
+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));
}
+1 -8
View File
@@ -1,26 +1,19 @@
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 axum::routing::post;
use smol::channel::Sender;
use std::sync::Arc;
use std::thread::JoinHandle;
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> {
pub fn init_api(app_state: Arc<AppState>) -> Result<JoinHandle<()>, anyhow::Error> {
std::thread::Builder::new()
.name("api".to_string())
.spawn(|| {
send_panic(thread_panic_tx);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
+1 -10
View File
@@ -1,17 +1,10 @@
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};
pub fn init_serial(
thread_panic_tx: Sender<PanicDetails>,
app_state: Arc<AppState>,
) -> Result<(), anyhow::Error> {
let thread_panic_tx_clone = thread_panic_tx;
pub fn init_serial(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
let app_state_clone = app_state.clone();
thread::Builder::new()
@@ -19,8 +12,6 @@ pub fn init_serial(
.spawn(move || {
let app_state_clone = app_state_clone;
smol::block_on(async move {
send_panic(thread_panic_tx_clone);
let rt = smol::LocalExecutor::new();
rt.run(async {
loop {
+2 -7
View File
@@ -1,8 +1,4 @@
use crate::{
app_state::AppState,
panic::{PanicDetails, send_panic},
};
use smol::channel::Sender;
use crate::app_state::AppState;
use std::{sync::Arc, time::Instant};
use tracing::{debug, error, info, instrument, trace};
@@ -62,11 +58,10 @@ async fn send_event_with_retry(
}
}
pub fn init_socketio_queue(thread_panic_tx: Sender<PanicDetails>, app_state: Arc<AppState>) {
pub fn init_socketio_queue(app_state: Arc<AppState>) {
std::thread::Builder::new()
.name("socketio-queue".to_string())
.spawn(move || {
send_panic(thread_panic_tx);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()