Compare commits

...
Author SHA1 Message Date
93094049+kraemr@users.noreply.github.com 9683882775 added documentation for adding a machine 2025-09-25 15:32:33 +02:00
Robin Krämer 893e01532d Merge pull request #753 from qitechgmbh/633-update-documentation-for-adding-new-devices
updated devices documentation by adding an example and explaining the architecture
2025-09-25 14:53:17 +02:00
kraemr 713f78fa4e updated devices documentation by adding an example
and explaining how devices fit into the architecture
2025-09-25 14:50:32 +02:00
Robin Krämer f444347fb9 Merge pull request #752 from qitechgmbh/751-document-performancestability-tweaks-made
added performance and stabillity documentation
2025-09-25 11:37:04 +02:00
kraemr ea03c0f755 added performance and stabillity documentation 2025-09-25 11:31:53 +02:00
Robin Krämer e283a0f164 Merge pull request #745 from qitechgmbh/744-windermode-switch-from-modewind-to-modepull-or-modehold-disables-the-wind-button
fixed emit state after state change
2025-09-23 15:41:28 +02:00
snacj 6820206d78 fixed emit state after state change 2025-09-23 15:38:47 +02:00
Robin Krämer 5069ee425c Merge pull request #742 from qitechgmbh/681-remove-unused-or-redundant-crates
removed unused crates
2025-09-23 13:37:48 +02:00
kraemr f93f342508 added ignores for cargo machete 2025-09-23 13:17:29 +02:00
kraemr c6d3563101 removed unused crates 2025-09-23 12:56:24 +02:00
Robin Krämer f2f893c917 Merge pull request #741 from qitechgmbh/739-add-roundness-value-to-laser-graph-and-control-page
Added Roundness Value to 2 Axis Laser
2025-09-22 14:27:30 +02:00
Robin KrämerandCopilot bbbca4085d Update server/src/machines/laser/mod.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 14:24:42 +02:00
Robin KrämerandCopilot 4fdf461520 Update server/src/machines/laser/mod.rs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-22 14:23:37 +02:00
snacj dcfc6728ba Made Roundness unitless 2025-09-22 14:10:25 +02:00
snacj 2673a92f50 Added Roundness Value to 2 Axis Laser 2025-09-22 13:39:59 +02:00
17 changed files with 582 additions and 186 deletions

No files matched your search

Generated
-105
View File
@@ -1678,15 +1678,6 @@ dependencies = [
"serde",
]
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-terminal"
version = "0.4.16"
@@ -1698,16 +1689,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@@ -2001,17 +1982,6 @@ version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "open"
version = "5.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "opentelemetry"
version = "0.30.0"
@@ -2121,12 +2091,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pcap"
version = "2.2.0"
@@ -2699,14 +2663,12 @@ dependencies = [
"bitvec",
"control_core",
"control_core_derive",
"crc",
"dhat",
"ethercat_hal",
"ethercrab",
"euclid",
"futures",
"lazy_static",
"open",
"opentelemetry",
"opentelemetry-otlp",
"opentelemetry_sdk",
@@ -2717,7 +2679,6 @@ dependencies = [
"smol",
"socketioxide",
"textplots",
"thread-priority",
"tikv-jemalloc-ctl",
"tikv-jemallocator",
"tokio",
@@ -3032,20 +2993,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
[[package]]
name = "thread-priority"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd4ef372b29fbcc6cb0cef97bcbf3ffa9da90e02eb23dbf14db48ce30d11d2d5"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
"libc",
"log",
"rustversion",
"windows",
]
[[package]]
name = "thread_local"
version = "1.1.8"
@@ -3659,28 +3606,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.61.2"
@@ -3694,17 +3619,6 @@ dependencies = [
"windows-strings 0.4.2",
]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
@@ -3733,16 +3647,6 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.4.0"
@@ -3844,15 +3748,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
+5
View File
@@ -8,7 +8,9 @@ workspace = true
[dependencies]
anyhow = "1.0.99"
axum = "0.8.4"
ethercat_hal = { path = "../ethercat-hal" }
ethercrab = "0.6"
interfaces = "0.0.9"
@@ -40,3 +42,6 @@ tokio = { version = "1.45.1", features = ["rt-multi-thread", "macros", "time"] }
[features]
default = []
video-streaming = ["dep:tokio"]
[package.metadata.cargo-machete]
ignored = ["axum","bitvec","control_core_derive"]
+145
View File
@@ -0,0 +1,145 @@
# How can I add a new Machine?
Adding a new machine follows a common structure in `server/machines/`.
# TLDR
Summary
When adding a new machine:
- Create Folder server/machines/YOUR_MACHINE_NAME/.
- Add the 4 files (mod.rs, new.rs, act.rs, api.rs).
- Register it in machines/mod.rs with a unique constant.
- mod.rs: define the struct + emit functions.
- new.rs: assemble devices/controllers into the machine.
- act.rs: implement the machine loop (MachineAct).
- api.rs: define request (mutations) and response (events) types.
Tip: Start simple (copy MockMachine) and grow complexity as needed (add EtherCAT devices, controllers, etc.).
---
## 1. Create the folder
Create a new directory:
server/machines/YOUR_MACHINE_NAME
At a minimum, this folder needs the following files:
- `mod.rs`
- `new.rs`
- `act.rs`
- `api.rs`
---
## 2. Register the module
Add your machine to `server/machines/mod.rs`:
```rust
// mod.rs
pub mod buffer1;
pub mod extruder1;
pub mod laser;
pub mod mock;
pub mod registry;
pub mod winder2;
pub mod YOUR_MACHINE;
pub const VENDOR_QITECH: u16 = 0x0001;
pub const MACHINE_WINDER_V1: u16 = 0x0002;
pub const MACHINE_EXTRUDER_V1: u16 = 0x0004;
pub const MACHINE_LASER_V1: u16 = 0x0006;
pub const MACHINE_MOCK: u16 = 0x0007;
pub const MACHINE_BUFFER_V1: u16 = 0x0008;
pub const YOUR_MACHINE_V1: u16 = 0xffff; // pick a unique ID to avoid collisions also DO NOT touch any already defined ids!!
```
## 3. mod.rs
This file defines your machine struct and the required event emitters.
Look at MockMachine’s mod.rs for the simplest possible version.
At a minimum, you need:
- emit_state
- emit_live_values
- A machine struct for state(e.g. MockMachine, Extruder, Laser, Winder, …)
More complex machines (e.g. Extruder, Winder) encapsulate controllers that in turn manage devices like EtherCAT terminals.
## 4 new.rs
This file implements MachineNewTrait for your machine.
Here you assemble your machine from its parts — devices, controllers, and any state.
Minimal example: see MockMachine → basically no devices, just returns a struct.
Medium complexity: see Laser → wraps a single serial USB device.
High complexity: see Extruder → configures multiple EtherCAT devices inside controllers.
Think of new.rs as the factory:
This is the place to declare which devices make up your machine and how they’re configured.
## 5. act.rs
This file implements the machine control loop using MachineAct.
The act() method is called repeatedly by the control runtime.
```rs
impl MachineAct for YourMachine {
fn act(&mut self, _now_ts: Instant) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
Box::pin(async move {
let now = Instant::now();
// Update state, e.g. step a controller
self.some_controller.update();
// Emit live values ~30 Hz if machine is in Running mode
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.maybe_emit_state_event();
self.emit_live_values();
self.last_measurement_emit = now;
}
})
}
}
```
## 6. api.rs
Defines the frontend-facing API for your machine:
Mutations → requests that the machine accepts
(e.g. start/stop commands, setpoints, configuration updates).
Events → responses emitted to the frontend
(e.g. live sensor data, alarms, machine state changes).
Look at MockMachine’s api.rs for a minimal version.
```rs
/// Mutations are API requests to control the machine
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum YourMachineMutation {
Start,
Stop,
SetSpeed { rpm: f64 },
}
/// Events are responses sent to the frontend
#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum YourMachineEvent {
StateChanged { state: String },
LiveValues { rpm: f64, torque: f64 },
}
```
@@ -0,0 +1,128 @@
# Performance/Stability Tweaks
## EtherCrab Config
We set ethercrab to default settings for everything except for RetryBehaviour, so that one unlikely failure doesnt destroy the EtherCAT network.
```rust
let maindevice = MainDevice::new(
pdu,
Timeouts {
// Default 5000ms
state_transition: Duration::from_millis(5000),
// Default 30_000us
pdu: Duration::from_micros(30_000),
// Default 10ms
eeprom: Duration::from_millis(10),
// Default 0ms
wait_loop_delay: Duration::from_millis(0),
// Default 100ms
mailbox_echo: Duration::from_millis(100),
// Default 1000ms
mailbox_response: Duration::from_millis(1000),
},
MainDeviceConfig {
// Default RetryBehaviour::None
retry_behaviour: RetryBehaviour::Count(5),
// Default 10_000
dc_static_sync_iterations: 10_000,
},
);
```
wait_loop_delay CAN in theory also decrease CPU usage if set exactly as the loop_delay we set, however this setting has caused us some issues, so we just leave it as is.
## Realtime Kernel
In our nixOs Config we use the latest realtime kernel and isolate TWO cores out of the Four available for the machines we use.
```nix
#From configuration.nix
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages-rt_latest;
boot.kernelModules = [ "i915" ];
```
Notice how we hardcode the intel gpu kernel module. This is because only the GPU driver gets blacklisted for some reason. By specifying it manually it loads like before.
One Core is used for the EtherCrab TXRX Thread and one is used for the machine Thread.
```
"isolcpus=2,3" # Isolate cpus 2 and 3 from scheduler for better latency, 2 runs ethercatthread and 3 runs server control-loop
"nohz_full=2,3" # In this mode, the periodic scheduler tick is stopped when only one task is running, reducing kernel interruptions on those CPUs.
"rcu_nocbs=2,3" # Moves RCU (Read-Copy Update) callback processing away from CPUs 2 and 3.
```
Essentially we completely disable acces from the operating system to these two cpus, which saves scheduler overhead, which would introduce noticable latency.
Its not entirely clear yet if what we did here is overkill. Maybe we could use only one core and get away with that?
Processor load normally hovers around 18% for one of the cores and like 9% for the other.
We also have some minor cpu tweaks in configuration.nix
```
# Graphical
"logo.nologo" # Remove kernel logo during boot
# Performance
# Specific Vulnerabilities Addressed by Mitigations:
# - Spectre variants (V1, V2, V4, SWAPGS, SpectreRSB, etc.)
# - Meltdown (Rogue Data Cache Load)
# - Foreshadow/L1TF (L1 Terminal Fault)
# - Microarchitectural Data Sampling (MDS, RIDL, Fallout, ZombieLoad)
# - SRBDS (Special Register Buffer Data Sampling)
# - TSX Asynchronous Abort (TAA)
# - iTLB Multihit
# - And others as they're discovered and mitigated
#
# With mitigations=off
# - PROS: Maximum performance, equivalent to pre-2018 behavior
# - CONS: Vulnerable to Spectre, Meltdown, Foreshadow, ZombieLoad, etc.
# Should ONLY be used in completely trusted environments
# - Improves performance by 7-43%
"mitigation=off"
"intel_pstate=performance" # Intel CPU-specific performance mode (if applicable)
# Memory Management
"transparent_hugepage=always" # Use larger memory pages for memory intense applications
"nmi_watchdog=0" # Disable NMI watchdog for reduced CPU overhead and realtime execution
# High-throughput ethernet parameters
"pcie_aspm=off" # Disable PCIe power management for NICs
"intel_iommu=off" # Disable IOMMU (performance gain)
# Reliability
"panic=10" # Auto-reboot 10 seconds after kernel panic
"oops=panic" # Treat kernel oops as panic for auto-recovery
"usbcore.autosuspend=-1" # Possibly fixes dre disconnect issue?
```
We should check if its really required to turn mitigations off as well.
## Rust Code Optimizations
In General we try to avoid memory allocations in our loop as much as possible, as it is not deterministic
One problem we found, is that due to the realtime kernel our ethernet driver caused us quite large spikes in our tx_rx cycle time for EtherCAT.
These spikes happen when CPU Load is very high on the two usable cores, causing the interrupt handler for Ethernet to stall.
Which is why we reassign the interrupt handler on startup to the same core that the EtherCatTxRx Thread runs on:
```rust
#[cfg(target_os = "linux")]
match set_irq_affinity(&interface, 3) {
Ok(_) => tracing::info!("ethernet interrupt handler now runs on cpu:{}", 3),
Err(e) => tracing::error!("set_irq_affinity failed: {:?}", e),
}
// Set core affinity to 4th core
let _ = set_core_affinity(3);
// Set the thread to real-time priority
let _ = set_realtime_priority();
```
This means that the Interrupt Handler ALWAYS has enough ressources to cope with our demand for realtime communication with EtherCAT
One Problem we have at the moment (we think) is the locking and async code blocking our server loop.
Note that this doesnt affect the EtherCAT communication at all.
## Electron Optimizations (Not Yet Implemented)
Currently we are struggling with the fact, that apparently Electron doesnt really have good support for HW acceleration on Linux.
Electron runs with the software-renderer, which is VERY slow compared to a gpu.
This would eliminate the weird stuttering seen on Graphs and make them run buttery smooth, while also alleviating load off of the CPU.
+127
View File
@@ -115,3 +115,130 @@ pub fn (
}
}
```
# Device Implementation Guide
This section describes how to implement EtherCAT devices in the framework used by the QiTech Control project (via `ethercat-hal`, `server`, etc.). We include a running example of a simple digital output device (EL2004) and discuss how it fits into the larger architecture.
## Architecture & Context
The QiTech Control system is built as:
- `ethercat-hal` / `ethercat-hal-derive`: Hardware abstraction, PDO / CoE handling, device traits.
- `server`: Glue code that reads device identities, instantiates the correct device types, performs CoE configuration, and runs cyclic process data.
- The `docs` directory documents APIs and device patterns.
:contentReference[oaicite:0]{index=0}
In this architecture:
1. During initialization, subdevice identity tuples (Vendor ID, Product ID, Revision) are read.
2. The `server` matches identity tuples to concrete `Device` types and constructs them (via `NewEthercatDevice`).
3. The device instance then handles PDO exchange, and optional CoE (configuration) writes.
4. Higher-level application code interacts with devices via traits like `DigitalOutputDevice`, `DigitalInputDevice`, etc.
---
## Example: Simple Digital Output Device (EL2004)
Below is a minimal example of a digital-output-only EtherCAT device. It shows how to map PDOs, implement traits, and define identity constants.
```rust
use super::{EthercatDeviceProcessing, NewEthercatDevice, SubDeviceIdentityTuple};
use crate::helpers::ethercrab_types::EthercrabSubDevicePreoperational;
use crate::io::digital_output::{DigitalOutputDevice, DigitalOutputOutput};
use crate::pdo::{RxPdo, basic::BoolPdoObject};
use ethercat_hal_derive::{EthercatDevice, RxPdo};
/// EL2004 4-channel digital output device — 24 V DC, 0.5 A per channel
#[derive(EthercatDevice)]
pub struct EL2004 {
/// The Rx PDO where output bits are written by the master
pub rxpdo: EL2004RxPdo,
is_used: bool,
}
impl EthercatDeviceProcessing for EL2004 {}
impl std::fmt::Debug for EL2004 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "EL2004")
}
}
impl NewEthercatDevice for EL2004 {
fn new() -> Self {
Self {
rxpdo: EL2004RxPdo::default(),
is_used: false,
}
}
}
impl DigitalOutputDevice<EL2004Port> for EL2004 {
fn set_output(&mut self, port: EL2004Port, value: DigitalOutputOutput) {
let expect_text = "All channels should be Some(_)";
match port {
EL2004Port::DO1 => {
self.rxpdo.channel1.as_mut().expect(expect_text).value = value.into()
}
EL2004Port::DO2 => {
self.rxpdo.channel2.as_mut().expect(expect_text).value = value.into()
}
EL2004Port::DO3 => {
self.rxpdo.channel3.as_mut().expect(expect_text).value = value.into()
}
EL2004Port::DO4 => {
self.rxpdo.channel4.as_mut().expect(expect_text).value = value.into()
}
}
}
fn get_output(&self, port: EL2004Port) -> DigitalOutputOutput {
let expect_text = "All channels should be Some(_)";
DigitalOutputOutput(match port {
EL2004Port::DO1 => self.rxpdo.channel1.as_ref().expect(expect_text).value,
EL2004Port::DO2 => self.rxpdo.channel2.as_ref().expect(expect_text).value,
EL2004Port::DO3 => self.rxpdo.channel3.as_ref().expect(expect_text).value,
EL2004Port::DO4 => self.rxpdo.channel4.as_ref().expect(expect_text).value,
})
}
}
#[derive(Debug, Clone)]
pub enum EL2004Port {
DO1,
DO2,
DO3,
DO4,
}
#[derive(Debug, Clone, RxPdo)]
pub struct EL2004RxPdo {
#[pdo_object_index(0x1600)]
pub channel1: Option<BoolPdoObject>,
#[pdo_object_index(0x1601)]
pub channel2: Option<BoolPdoObject>,
#[pdo_object_index(0x1602)]
pub channel3: Option<BoolPdoObject>,
#[pdo_object_index(0x1603)]
pub channel4: Option<BoolPdoObject>,
}
impl Default for EL2004RxPdo {
fn default() -> Self {
Self {
channel1: Some(BoolPdoObject::default()),
channel2: Some(BoolPdoObject::default()),
channel3: Some(BoolPdoObject::default()),
channel4: Some(BoolPdoObject::default()),
}
}
}
/// Identity constants for EL2004
pub const EL2004_VENDOR_ID: u32 = 0x2;
pub const EL2004_PRODUCT_ID: u32 = 131346514;
pub const EL2004_REVISION_A: u32 = 1179648;
pub const EL2004_IDENTITY_A: SubDeviceIdentityTuple =
(EL2004_VENDOR_ID, EL2004_PRODUCT_ID, EL2004_REVISION_A);
@@ -7,8 +7,8 @@ type DiameterVisualisationProps = {
lowTolerance: number;
highTolerance: number;
diameter: TimeSeries;
x_value?: TimeSeries;
y_value?: TimeSeries;
x_diameter?: TimeSeries;
y_diameter?: TimeSeries;
};
// Ring color depending on tolerance
@@ -36,12 +36,12 @@ export function DiameterVisualisation({
lowTolerance,
highTolerance,
diameter,
x_value,
y_value,
x_diameter,
y_diameter,
}: DiameterVisualisationProps) {
const actualDiameter = diameter.current?.value ?? 0.0;
const actualX = x_value?.current?.value ?? actualDiameter;
const actualY = y_value?.current?.value ?? actualDiameter;
const actualX = x_diameter?.current?.value ?? actualDiameter;
const actualY = y_diameter?.current?.value ?? actualDiameter;
const minDia = targetDiameter - lowTolerance;
const maxDia = targetDiameter + highTolerance;
@@ -55,9 +55,15 @@ export function DiameterVisualisation({
inputMax: number,
outputMin: number,
outputMax: number,
) =>
outputMin +
((value - inputMin) * (outputMax - outputMin)) / (inputMax - inputMin);
) => {
if (inputMax === inputMin) {
return (outputMin + outputMax) / 2;
}
return (
outputMin +
((value - inputMin) * (outputMax - outputMin)) / (inputMax - inputMin)
);
};
const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
@@ -14,8 +14,9 @@ import { DiameterVisualisation } from "../DiameterVisualisation";
export function Laser1ControlPage() {
const {
diameter,
x_value,
y_value,
x_diameter,
y_diameter,
roundness,
state,
defaultState,
setTargetDiameter,
@@ -36,8 +37,8 @@ export function Laser1ControlPage() {
lowTolerance={lowerTolerance}
highTolerance={higherTolerance}
diameter={diameter}
x_value={x_value}
y_value={y_value}
x_diameter={x_diameter}
y_diameter={y_diameter}
/>
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
@@ -47,22 +48,31 @@ export function Laser1ControlPage() {
renderValue={(value) => value.toFixed(3)}
/>
</div>
{x_value?.current && (
{x_diameter?.current && (
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="X-Diameter"
unit="mm"
timeseries={x_value}
timeseries={x_diameter}
renderValue={(value) => value.toFixed(3)}
/>
</div>
)}
{y_value?.current && (
{y_diameter?.current && (
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Y-Diameter"
unit="mm"
timeseries={y_value}
timeseries={y_diameter}
renderValue={(value) => value.toFixed(3)}
/>
</div>
)}
{roundness?.current && (
<div className="flex flex-row items-center gap-6">
<TimeSeriesValueNumeric
label="Roundness"
timeseries={roundness}
renderValue={(value) => value.toFixed(3)}
/>
</div>
@@ -10,18 +10,19 @@ import React from "react";
import { useLaser1 } from "./useLaser1";
export function Laser1GraphsPage() {
const { diameter, x_value, y_value, state } = useLaser1();
const { diameter, x_diameter, y_diameter, roundness, state } = useLaser1();
const syncHook = useGraphSync("diameter-group");
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
const isTwoAxis = !!x_value?.current || !!y_value?.current;
const isTwoAxis = !!x_diameter?.current || !!y_diameter?.current;
const diameterColor = "#3b82f6";
const xDiameterColor = "#ef4444";
const yDiameterColor = "#22c55e";
const roundnessColor = "#eab308";
const config: GraphConfig = {
title: "Diameter",
@@ -110,15 +111,20 @@ export function Laser1GraphsPage() {
],
},
{
newData: x_value,
newData: x_diameter,
color: xDiameterColor,
title: "X-Diameter",
},
{
newData: y_value,
newData: y_diameter,
color: yDiameterColor,
title: "Y-Diameter",
},
{
newData: roundness,
color: roundnessColor,
title: "Roundness",
},
]}
unit="mm"
renderValue={(value) => value.toFixed(3)}
@@ -28,8 +28,9 @@ import {
*/
export const liveValuesEventDataSchema = z.object({
diameter: z.number(),
x_value: z.number().nullable(),
y_value: z.number().nullable(),
x_diameter: z.number().nullable(),
y_diameter: z.number().nullable(),
roundness: z.number().nullable(),
});
/**
@@ -59,8 +60,9 @@ export type Laser1NamespaceStore = {
// Time series data for live values
diameter: TimeSeries;
x_value: TimeSeries;
y_value: TimeSeries;
x_diameter: TimeSeries;
y_diameter: TimeSeries;
roundness: TimeSeries;
};
// Constants for time durations
@@ -75,19 +77,19 @@ const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries(
ONE_HOUR,
);
const { initialTimeSeries: x_value, insert: addXValue } = createTimeSeries(
const { initialTimeSeries: x_diameter, insert: addXDiameter } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: y_diameter, insert: addYDiameter } =
createTimeSeries(TWENTY_MILLISECOND, ONE_SECOND, FIVE_SECOND, ONE_HOUR);
const { initialTimeSeries: roundness, insert: addRoundness } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
const { initialTimeSeries: y_value, insert: addYValue } = createTimeSeries(
TWENTY_MILLISECOND,
ONE_SECOND,
FIVE_SECOND,
ONE_HOUR,
);
/**
* Factory function to create a new Laser1 namespace store
* @returns A new Zustand store instance for Laser1 namespace
@@ -98,8 +100,9 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
state: null,
defaultState: null,
diameter: diameter,
x_value: x_value,
y_value: y_value,
x_diameter,
y_diameter,
roundness: roundness,
};
});
@@ -143,31 +146,43 @@ export function laser1MessageHandler(
value: liveValuesEvent.data.diameter,
timestamp: event.ts,
};
if (liveValuesEvent.data.x_value !== null) {
const xValue: TimeSeriesValue = {
value: liveValuesEvent.data.x_value,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
x_value: addXValue(state.x_value, xValue),
}));
}
if (liveValuesEvent.data.y_value !== null) {
const yValue: TimeSeriesValue = {
value: liveValuesEvent.data.y_value,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
y_value: addYValue(state.y_value, yValue),
}));
}
updateStore((state) => ({
...state,
diameter: addDiameter(state.diameter, diameterValue),
}));
if (liveValuesEvent.data.x_diameter !== null) {
const xValue: TimeSeriesValue = {
value: liveValuesEvent.data.x_diameter,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
x_diameter: addXDiameter(state.x_diameter, xValue),
}));
}
if (liveValuesEvent.data.y_diameter !== null) {
const yValue: TimeSeriesValue = {
value: liveValuesEvent.data.y_diameter,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
y_diameter: addYDiameter(state.y_diameter, yValue),
}));
}
if (liveValuesEvent.data.roundness !== null) {
const roundnessValue: TimeSeriesValue = {
value: liveValuesEvent.data.roundness,
timestamp: event.ts,
};
updateStore((state) => ({
...state,
roundness: addRoundness(state.roundness, roundnessValue),
}));
}
} else {
handleUnhandledEventError(eventName);
}
@@ -11,7 +11,7 @@ import { produce } from "immer";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, diameter, x_value, y_value } =
const { state, defaultState, diameter, x_diameter, y_diameter, roundness } =
useLaser1Namespace(machine_identification_unique);
// Single optimistic state for all state management
@@ -110,8 +110,9 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Live values (TimeSeries)
diameter,
x_value,
y_value,
x_diameter,
y_diameter,
roundness,
// Loading states
isLoading: stateOptimistic.isOptimistic,
+4
View File
@@ -15,3 +15,7 @@ ethercat_hal = { path = "../ethercat-hal" }
control_core = { path = "../control-core" }
futures = "0.3.31"
anyhow = "1.0.99"
[package.metadata.cargo-machete]
ignored = ["interfaces"]
+3 -3
View File
@@ -18,7 +18,6 @@ uom = { version = "0.36.0", default-features = false, features = ["f64"] }
serde = "1.0.217"
anyhow = "1.0.99"
open = "5.3.2"
bitvec = "1.0.1"
lazy_static = "1.5.0"
dhat = { version = "0.3.3", optional = true }
@@ -27,7 +26,6 @@ euclid = "0.22.11"
# concurrency
smol = "2.0.2"
tokio = { version = "1.45.1", features = ["rt-multi-thread"] }
thread-priority = "2.1.0"
regex = "1.11.2"
futures = "0.3.31"
@@ -38,7 +36,6 @@ tower-http = { version = "0.6.6", features = ["cors", "trace", "fs"] }
axum = { version = "0.8.4", features = ["macros"] }
# serial
crc = "3.3.0"
serialport = "4.7.3"
#tracing
@@ -85,3 +82,6 @@ io-uring = []
[profile.release]
codegen-units = 1
lto = "fat"
[package.metadata.cargo-machete]
ignored = ["tonic"]
+1
View File
@@ -19,6 +19,7 @@ use std::time::{Duration, Instant};
///
impl MachineAct for LaserMachine {
fn act(&mut self, now: Instant) {
self.update();
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();
+3 -2
View File
@@ -21,8 +21,9 @@ use tracing::instrument;
pub struct LiveValuesEvent {
/// diameter measurement in mm
pub diameter: f64,
pub x_value: Option<f64>,
pub y_value: Option<f64>,
pub x_diameter: Option<f64>,
pub y_diameter: Option<f64>,
pub roundness: Option<f64>,
}
impl LiveValuesEvent {
+58 -17
View File
@@ -24,6 +24,12 @@ pub struct LaserMachine {
namespace: LaserMachineNamespace,
last_measurement_emit: Instant,
// laser values
diameter: Length,
x_diameter: Option<Length>,
y_diameter: Option<Length>,
roundness: Option<f64>,
//laser target configuration
laser_target: LaserTarget,
@@ -42,26 +48,16 @@ impl LaserMachine {
impl LaserMachine {
///diameter in mm
pub fn emit_live_values(&mut self) {
let laser_data = smol::block_on(async { self.laser.read().await.get_data().await });
let diameter = laser_data
.as_ref()
.map(|data| data.diameter.get::<millimeter>())
.unwrap_or(0.0);
let x_value = laser_data
.as_ref()
.and_then(|data| data.x_axis.as_ref())
.map(|x| x.get::<millimeter>());
let y_value = laser_data
.as_ref()
.and_then(|data| data.y_axis.as_ref())
.map(|y| y.get::<millimeter>());
let diameter = self.diameter.get::<millimeter>();
let x_diameter = self.x_diameter.map(|x| x.get::<millimeter>());
let y_diameter = self.y_diameter.map(|y| y.get::<millimeter>());
let roundness = self.roundness;
let live_values = LiveValuesEvent {
diameter,
x_value,
y_value,
x_diameter,
y_diameter,
roundness,
};
self.namespace
.emit(LaserEvents::LiveValues(live_values.build()));
@@ -107,7 +103,52 @@ impl LaserMachine {
self.laser_target.diameter = Length::new::<millimeter>(target_diameter);
self.emit_state();
}
///
/// Roundness = min(x, y) / max(x, y)
///
fn calculate_roundness(&mut self) -> Option<f64> {
match (self.x_diameter, self.y_diameter) {
(Some(x), Some(y)) => {
let x_val = x.get::<millimeter>();
let y_val = y.get::<millimeter>();
if x_val > 0.0 && y_val > 0.0 {
let roundness = f64::min(x_val, y_val) / f64::max(x_val, y_val);
Some(roundness)
} else if x_val == 0.0 && y_val == 0.0 {
Some(0.0)
} else {
None
}
}
_ => None,
}
}
pub fn update(&mut self) {
let laser_data = smol::block_on(async { self.laser.read().await.get_data().await });
self.diameter = Length::new::<millimeter>(
laser_data
.as_ref()
.map(|data| data.diameter.get::<millimeter>())
.unwrap_or(0.0),
);
self.x_diameter = laser_data
.as_ref()
.and_then(|data| data.x_axis.as_ref())
.cloned();
self.y_diameter = laser_data
.as_ref()
.and_then(|data| data.y_axis.as_ref())
.cloned();
self.roundness = self.calculate_roundness();
}
}
#[derive(Debug, Clone)]
pub struct LaserTarget {
diameter: Length,
+8 -1
View File
@@ -5,7 +5,10 @@ use crate::serial::{devices::laser::Laser, registry::SERIAL_DEVICE_REGISTRY};
use super::{LaserMachine, LaserTarget, api::LaserMachineNamespace};
use anyhow::Error;
use control_core::machines::new::MachineNewTrait;
use uom::si::{f64::Length, length::millimeter};
use uom::{
ConstZero,
si::{f64::Length, length::millimeter},
};
impl MachineNewTrait for LaserMachine {
fn new(
@@ -39,6 +42,10 @@ impl MachineNewTrait for LaserMachine {
last_measurement_emit: Instant::now(),
laser_target,
emitted_default_state: false,
diameter: Length::ZERO,
x_diameter: None,
y_diameter: None,
roundness: None,
};
// Emit initial state
+4
View File
@@ -16,6 +16,10 @@ impl MachineAct for Winder2 {
// automatically stops or pulls after N Meters if enabled
self.stop_or_pull_spool(now);
if self.traverse_controller.did_change_state() {
self.emit_state();
}
// more than 33ms have passed since last emit (30 "fps" target)
if now.duration_since(self.last_measurement_emit) > Duration::from_secs_f64(1.0 / 30.0) {
self.emit_live_values();