Merge pull request #873 from qitechgmbh/872-add-cycle-target-time

872 add cycle target time
This commit is contained in:
Robin Krämer authored and GitHub committed 2025-11-04 16:19:15 +01:00
commit 024f751ef1
4 files changed
+120 -63

No files matched your search

Generated
+30 -5
View File
@@ -476,7 +476,7 @@ dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -2688,6 +2688,7 @@ dependencies = [
"serialport",
"smol",
"socketioxide",
"spin_sleep",
"textplots",
"tokio",
"tonic",
@@ -2884,6 +2885,15 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
[[package]]
name = "spin_sleep"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c07347b7c0301b9adba4350bdcf09c039d0e7160922050db0439b3c6723c8ab"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -3591,7 +3601,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-link 0.1.3",
"windows-result",
"windows-strings 0.4.2",
]
@@ -3624,6 +3634,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-registry"
version = "0.4.0"
@@ -3641,7 +3657,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -3650,7 +3666,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -3659,7 +3675,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -3693,6 +3709,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
+1
View File
@@ -54,6 +54,7 @@ opentelemetry-otlp = { version = "0.30.0", features = [
"grpc-tonic",
], optional = true }
tonic = { version = "0.13.1", optional = true }
spin_sleep = "1.3.3"
[dev-dependencies]
approx = "0.5.1"
+84 -53
View File
@@ -1,20 +1,29 @@
use crate::app_state::AppState;
use bitvec::prelude::*;
use control_core::machines::connection::MachineConnection;
use control_core::machines::manager::MachineManager;
use control_core::realtime::set_core_affinity;
#[cfg(not(feature = "development-build"))]
use control_core::realtime::set_realtime_priority;
use std::sync::Arc;
use smol::lock::RwLockReadGuard;
use spin_sleep::SpinSleeper;
use std::time::Instant;
use std::{sync::Arc, time::Duration};
use tracing::{instrument, trace_span};
// 300 us loop cycle target
pub fn start_loop_thread(
app_state: Arc<AppState>,
cycle_target: Duration,
) -> Result<std::thread::JoinHandle<()>, std::io::Error> {
// Start control loop
let res = std::thread::Builder::new()
.name("loop".to_owned())
.spawn(move || {
let sleeper =
SpinSleeper::new(3_333_333) // frequency in Hz ~ 1 / 300µs, Basically specifies the accuracy of our sleep
.with_spin_strategy(spin_sleep::SpinStrategy::YieldThread);
let _ = set_core_affinity(2);
#[cfg(not(feature = "development-build"))]
if let Err(e) = set_realtime_priority() {
@@ -35,23 +44,15 @@ pub fn start_loop_thread(
smol::block_on(async {
rt.run(async {
loop {
if let Err(e) = loop_once(app_state.clone()).await {
if let Err(e) = loop_once(app_state.clone(), &sleeper, cycle_target).await {
tracing::error!("Loop failed\n{:?}", e);
std::process::exit(1);
break;
}
}
})
.await;
});
/*
// If timeouts keep happenning do a spin_sleep, better then thread_sleep and allows for more deterministic timing
let elapsed = start.elapsed();
if elapsed < target_cycle {
spin_sleep::sleep(target_cycle - elapsed);
}
*/
if let Some(last_loop_start) = app_state
.performance_metrics
.read_arc_blocking()
@@ -66,36 +67,21 @@ pub fn start_loop_thread(
return res;
}
#[instrument(skip(app_state))]
pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
// Record cycle start for performance metrics
{
let mut metrics = app_state.performance_metrics.write().await;
metrics.cycle_start();
}
let ethercat_setup_guard = app_state.ethercat_setup.read().await;
pub async fn copy_ethercat_inputs(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
//println!("copy_ethercat_intputs");
// only if we have an ethercat setup
// - tx/rx cycle
// - copy inputs to devices
let ethercat_setup_guard = app_state.ethercat_setup.read().await;
//tracing::info!("EtherCAT setup available: {}", ethercat_setup.is_some());
if let Some(ethercat_setup) = ethercat_setup_guard.as_ref() {
let span = trace_span!("loop_once_inputs");
let _enter = span.enter();
// Measure tx_rx performance
let txrx_start = Instant::now();
ethercat_setup
.group
.tx_rx(&ethercat_setup.maindevice)
.await?;
let txrx_duration = txrx_start.elapsed();
// Record tx_rx performance metrics
{
let mut metrics = app_state.performance_metrics.write().await;
metrics.record_txrx_time(txrx_duration);
}
// copy inputs to devices
for (i, subdevice) in ethercat_setup
@@ -137,32 +123,14 @@ pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyh
})?;
}
}
Ok(())
}
// execute machines
{
let span = trace_span!("loop_once_act");
let _enter = span.enter();
let machine_guard = app_state.machines.read().await;
let now = std::time::Instant::now();
for machine in machine_guard.iter() {
let connection = &machine.1.lock_blocking().machine_connection;
if let MachineConnection::Connected(machine) = connection {
// if the machine is currenlty locked (likely processing API call)
// we skip the machine
if let Some(mut machine_guard) = machine.try_lock() {
let span = trace_span!("loop_once_act_machine",);
let _enter = span.enter();
// execute machine
machine_guard.act(now);
}
}
}
}
pub async fn copy_ethercat_outputs(app_state: Arc<AppState>) -> Result<(), anyhow::Error> {
// only if we have an ethercat setup
// - copy outputs from devices
let ethercat_setup_guard = app_state.ethercat_setup.read().await;
if let Some(ethercat_setup) = ethercat_setup_guard.as_ref() {
let span = trace_span!("loop_once_outputs");
let _enter = span.enter();
@@ -207,6 +175,69 @@ pub async fn loop_once<'maindevice>(app_state: Arc<AppState>) -> Result<(), anyh
})?;
}
}
Ok(())
}
pub async fn execute_machines(machine_guard: &RwLockReadGuard<'_, MachineManager>) {
let now = std::time::Instant::now();
for machine in machine_guard.iter() {
let connection = &machine.1.lock_blocking().machine_connection;
if let MachineConnection::Connected(machine) = connection {
// if the machine is currenlty locked (likely processing API call)
// we skip the machine
if let Some(mut machine_guard) = machine.try_lock() {
let span = trace_span!("loop_once_act_machine",);
let _enter = span.enter();
// execute machine
machine_guard.act(now);
}
}
}
}
#[instrument(skip(app_state))]
pub async fn loop_once<'maindevice>(
app_state: Arc<AppState>,
sleeper: &SpinSleeper,
cycle_target: Duration,
) -> Result<(), anyhow::Error> {
let loop_once_start = std::time::Instant::now();
// Record cycle start for performance metrics
{
let mut metrics = app_state.performance_metrics.write().await;
metrics.cycle_start();
}
{
let txrx_start = Instant::now();
copy_ethercat_inputs(app_state.clone()).await?;
let txrx_duration = txrx_start.elapsed();
let mut metrics = app_state.performance_metrics.write().await;
metrics.record_txrx_time(txrx_duration);
}
// execute machines
{
let span = trace_span!("loop_once_act");
let _enter = span.enter();
let machine_guard = app_state.machines.read().await;
execute_machines(&machine_guard).await;
}
{
copy_ethercat_outputs(app_state.clone()).await?;
}
if app_state.ethercat_setup.read().await.is_some() {
// spin_sleep so we have a cycle time of ~300us
// This does push usage to 100% if completely busy, but provides much better accuracy then thread sleep or async sleep
sleeper.sleep_until(loop_once_start + cycle_target);
} else {
// if we dont have an ethercat setup or other rt relevant stuff do the "worse" async sleep or later if we get rid of async thread::sleep or yielding
// We do this, so that when no rt relevant code runs the cpu doesnt spin at 100% for no reason
let loop_duration = loop_once_start.elapsed();
if cycle_target > loop_once_start.elapsed() {
smol::Timer::after(cycle_target - loop_duration).await;
}
}
Ok(())
}
+5 -5
View File
@@ -5,17 +5,18 @@ use mock::init::init_mock;
#[cfg(feature = "mock-machine")]
pub mod mock;
use crate::{ethercat::setup::setup_loop, panic::init_panic_handling};
use app_state::AppState;
use ethercat::{
ethercat_discovery_info::{send_ethercat_discovering, send_ethercat_found},
init::start_interface_discovery,
setup::setup_loop,
};
use r#loop::start_loop_thread;
use panic::init_panic_handling;
use rest::init::start_api_thread;
use serial::init::{handle_serial_device_hotplug, start_serial_discovery};
use socketio::queue::start_socketio_queue;
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
pub mod app_state;
pub mod ethercat;
pub mod logging;
@@ -31,20 +32,19 @@ fn main() {
logging::init_tracing();
tracing::info!("Tracing initialized successfully");
init_panic_handling();
const CYCLE_TARGET_TIME: Duration = Duration::from_micros(300);
let app_state = Arc::new(AppState::new());
#[cfg(feature = "mock-machine")]
init_mock(app_state.clone()).expect("Failed to initialize mock machines");
let _ = start_api_thread(app_state.clone());
let _ = start_loop_thread(app_state.clone());
let mut socketio_fut = start_socketio_queue(app_state.clone()).fuse();
let mut ethercat_fut = start_interface_discovery().fuse();
let mut serial_fut = start_serial_discovery().fuse();
let _loop_thread = start_loop_thread(app_state.clone(), CYCLE_TARGET_TIME);
smol::block_on(async { send_ethercat_discovering(app_state.clone()).await });
smol::block_on(async {
loop {
// lets the async runtime decide which future to run next