|
|
|
@ -261,34 +261,44 @@ fn get_ftp(app: AppHandle) -> u16 {
|
|
|
|
*app.state::<State>().ftp.lock().unwrap()
|
|
|
|
*app.state::<State>().ftp.lock().unwrap()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Вычисляет и отправляет целевые мощность/каденс сегмента, если изменились.
|
|
|
|
/// Вычисляет и отправляет целевую мощность сегмента, если она изменилась.
|
|
|
|
async fn apply_target(
|
|
|
|
async fn apply_power(
|
|
|
|
source: &Arc<dyn SensorSource>,
|
|
|
|
source: &Arc<dyn SensorSource>,
|
|
|
|
device: &Device,
|
|
|
|
device: &Device,
|
|
|
|
seg: &workout::Segment,
|
|
|
|
seg: &workout::Segment,
|
|
|
|
elapsed_secs: f64,
|
|
|
|
elapsed_secs: f64,
|
|
|
|
ftp: u16,
|
|
|
|
ftp: u16,
|
|
|
|
last_watts: Option<i16>,
|
|
|
|
last_watts: Option<i16>,
|
|
|
|
last_cadence: Option<u16>,
|
|
|
|
) -> Option<i16> {
|
|
|
|
) -> (Option<i16>, Option<u16>) {
|
|
|
|
|
|
|
|
let watts = seg.power.as_ref().map(|p| {
|
|
|
|
let watts = seg.power.as_ref().map(|p| {
|
|
|
|
workout::percent_to_watts(workout::target_percent(p, elapsed_secs, seg.duration_secs), ftp)
|
|
|
|
workout::percent_to_watts(workout::target_percent(p, elapsed_secs, seg.duration_secs), ftp)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
let cadence = seg.cadence_rpm;
|
|
|
|
if let Some(w) = watts.filter(|w| Some(*w) != last_watts) {
|
|
|
|
|
|
|
|
let _ = source.command(device, Command::FtmsSetTargetPower(w)).await;
|
|
|
|
if watts != last_watts {
|
|
|
|
|
|
|
|
if let Some(w) = watts {
|
|
|
|
|
|
|
|
let _ = source.command(device, Command::FtmsSetTargetPower(w)).await;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if cadence != last_cadence {
|
|
|
|
|
|
|
|
if let Some(rpm) = cadence {
|
|
|
|
|
|
|
|
if let Some(half) = rpm.checked_mul(2) {
|
|
|
|
|
|
|
|
let _ = source.command(device, Command::FtmsSetTargetCadence(half)).await;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
(watts, cadence)
|
|
|
|
watts
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Шлёт событие прогресса воркаута на фронт.
|
|
|
|
|
|
|
|
fn emit_progress(
|
|
|
|
|
|
|
|
app: &AppHandle,
|
|
|
|
|
|
|
|
idx: usize,
|
|
|
|
|
|
|
|
seg: &workout::Segment,
|
|
|
|
|
|
|
|
watts: Option<i16>,
|
|
|
|
|
|
|
|
elapsed_total: u64,
|
|
|
|
|
|
|
|
total: u64,
|
|
|
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
let _ = app.emit(
|
|
|
|
|
|
|
|
"workout-progress",
|
|
|
|
|
|
|
|
WorkoutProgressEvent {
|
|
|
|
|
|
|
|
step_index: idx,
|
|
|
|
|
|
|
|
step_name: seg.text.clone().unwrap_or_default(),
|
|
|
|
|
|
|
|
target_watts: watts,
|
|
|
|
|
|
|
|
target_cadence: seg.cadence_rpm,
|
|
|
|
|
|
|
|
elapsed_secs: elapsed_total,
|
|
|
|
|
|
|
|
total_secs: total,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Исполняет воркаут: request_control + start, тик-луп по сегментам, stop в конце.
|
|
|
|
/// Исполняет воркаут: request_control + start, тик-луп по сегментам, stop в конце.
|
|
|
|
@ -311,26 +321,20 @@ async fn run_workout(
|
|
|
|
|
|
|
|
|
|
|
|
for (idx, seg) in segments.iter().enumerate() {
|
|
|
|
for (idx, seg) in segments.iter().enumerate() {
|
|
|
|
let dur = seg.duration_secs as u64;
|
|
|
|
let dur = seg.duration_secs as u64;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// каденс — константа сегмента, шлём один раз (в 0.5-rpm единицах FTMS).
|
|
|
|
|
|
|
|
if let Some(half) = seg.cadence_rpm.and_then(|rpm| rpm.checked_mul(2)) {
|
|
|
|
|
|
|
|
let _ = source.command(&device, Command::FtmsSetTargetCadence(half)).await;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut last_watts: Option<i16> = None;
|
|
|
|
let mut last_watts: Option<i16> = None;
|
|
|
|
let mut last_cadence: Option<u16> = None;
|
|
|
|
|
|
|
|
let mut tick = time::interval(Duration::from_secs(1));
|
|
|
|
let mut tick = time::interval(Duration::from_secs(1));
|
|
|
|
tick.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
|
|
|
|
tick.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
|
|
|
|
|
|
|
|
|
|
|
|
// t = 0
|
|
|
|
// t = 0
|
|
|
|
let (w, c) = apply_target(&source, &device, seg, 0.0, ftp, last_watts, last_cadence).await;
|
|
|
|
let w = apply_power(&source, &device, seg, 0.0, ftp, last_watts).await;
|
|
|
|
last_watts = w;
|
|
|
|
last_watts = w;
|
|
|
|
last_cadence = c;
|
|
|
|
emit_progress(&app, idx, seg, w, elapsed_total, total);
|
|
|
|
let _ = app.emit(
|
|
|
|
|
|
|
|
"workout-progress",
|
|
|
|
|
|
|
|
WorkoutProgressEvent {
|
|
|
|
|
|
|
|
step_index: idx,
|
|
|
|
|
|
|
|
step_name: seg.text.clone().unwrap_or_default(),
|
|
|
|
|
|
|
|
target_watts: w,
|
|
|
|
|
|
|
|
target_cadence: c,
|
|
|
|
|
|
|
|
elapsed_secs: elapsed_total,
|
|
|
|
|
|
|
|
total_secs: total,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let mut elapsed_in_seg: u64 = 0;
|
|
|
|
let mut elapsed_in_seg: u64 = 0;
|
|
|
|
while elapsed_in_seg < dur {
|
|
|
|
while elapsed_in_seg < dur {
|
|
|
|
@ -339,23 +343,10 @@ async fn run_workout(
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elapsed_in_seg += 1;
|
|
|
|
elapsed_in_seg += 1;
|
|
|
|
let (w, c) =
|
|
|
|
let w = apply_power(&source, &device, seg, elapsed_in_seg as f64, ftp, last_watts).await;
|
|
|
|
apply_target(&source, &device, seg, elapsed_in_seg as f64, ftp, last_watts, last_cadence)
|
|
|
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
last_watts = w;
|
|
|
|
last_watts = w;
|
|
|
|
last_cadence = c;
|
|
|
|
|
|
|
|
elapsed_total += 1;
|
|
|
|
elapsed_total += 1;
|
|
|
|
let _ = app.emit(
|
|
|
|
emit_progress(&app, idx, seg, w, elapsed_total, total);
|
|
|
|
"workout-progress",
|
|
|
|
|
|
|
|
WorkoutProgressEvent {
|
|
|
|
|
|
|
|
step_index: idx,
|
|
|
|
|
|
|
|
step_name: seg.text.clone().unwrap_or_default(),
|
|
|
|
|
|
|
|
target_watts: w,
|
|
|
|
|
|
|
|
target_cadence: c,
|
|
|
|
|
|
|
|
elapsed_secs: elapsed_total,
|
|
|
|
|
|
|
|
total_secs: total,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|