|
|
|
@ -18,7 +18,7 @@ use crate::ble::{BleSource, MockSource, SensorSource};
|
|
|
|
use crate::commands::Command;
|
|
|
|
use crate::commands::Command;
|
|
|
|
use crate::device::Device;
|
|
|
|
use crate::device::Device;
|
|
|
|
use crate::device_repo::{repo_from_devices, Repo};
|
|
|
|
use crate::device_repo::{repo_from_devices, Repo};
|
|
|
|
use crate::state::{State, TrainerState};
|
|
|
|
use crate::state::{State, TrainerTargets};
|
|
|
|
use crate::telemetry::Telemetry;
|
|
|
|
use crate::telemetry::Telemetry;
|
|
|
|
use btleplug::api::Manager as ApiManager;
|
|
|
|
use btleplug::api::Manager as ApiManager;
|
|
|
|
use btleplug::platform::Manager as BleManager;
|
|
|
|
use btleplug::platform::Manager as BleManager;
|
|
|
|
@ -40,7 +40,7 @@ struct BatteryEvent {
|
|
|
|
|
|
|
|
|
|
|
|
// Watchdog: how long without telemetry before the connection is considered dead.
|
|
|
|
// Watchdog: how long without telemetry before the connection is considered dead.
|
|
|
|
const WATCHDOG_STALE_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
const WATCHDOG_STALE_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
// How many consecutive failed/silent cycles before giving up on reconnect.
|
|
|
|
// How many times to retry reconnect before giving up.
|
|
|
|
const RECONNECT_MAX_ATTEMPTS: u32 = 5;
|
|
|
|
const RECONNECT_MAX_ATTEMPTS: u32 = 5;
|
|
|
|
// Base backoff between reconnect attempts.
|
|
|
|
// Base backoff between reconnect attempts.
|
|
|
|
const RECONNECT_BACKOFF: Duration = Duration::from_secs(2);
|
|
|
|
const RECONNECT_BACKOFF: Duration = Duration::from_secs(2);
|
|
|
|
@ -103,24 +103,12 @@ async fn connect(app: AppHandle, device_id: String, category: String) -> Result<
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let source = state.source.clone();
|
|
|
|
let source = state.source.clone();
|
|
|
|
|
|
|
|
let source_for_spawn = source.clone();
|
|
|
|
// Battery first, before the watchdog task touches the peripheral (avoids a
|
|
|
|
|
|
|
|
// connect/discover race on the same peripheral).
|
|
|
|
|
|
|
|
if let Ok(percent) = source.battery(&device).await {
|
|
|
|
|
|
|
|
let _ = app.emit(
|
|
|
|
|
|
|
|
"battery",
|
|
|
|
|
|
|
|
BatteryEvent {
|
|
|
|
|
|
|
|
device_id: device.id.clone(),
|
|
|
|
|
|
|
|
percent,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let app_t = app.clone();
|
|
|
|
let app_t = app.clone();
|
|
|
|
let device_t = device.clone();
|
|
|
|
let device_t = device.clone();
|
|
|
|
let category_t = category.clone();
|
|
|
|
let category_t = category.clone();
|
|
|
|
let handle = async_runtime::spawn(async move {
|
|
|
|
let handle = async_runtime::spawn(async move {
|
|
|
|
run_watchdog(app_t, source, device_t, category_t).await;
|
|
|
|
run_watchdog(app_t, source_for_spawn, device_t, category_t).await;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
state
|
|
|
|
state
|
|
|
|
@ -129,10 +117,20 @@ async fn connect(app: AppHandle, device_id: String, category: String) -> Result<
|
|
|
|
.unwrap()
|
|
|
|
.unwrap()
|
|
|
|
.insert(category.clone(), handle);
|
|
|
|
.insert(category.clone(), handle);
|
|
|
|
|
|
|
|
|
|
|
|
// Trainer: remember the device id. Control is requested (with confirmation)
|
|
|
|
if let Ok(percent) = source.battery(&device).await {
|
|
|
|
// inside run_watchdog, after the notification subscription is live.
|
|
|
|
let _ = app.emit(
|
|
|
|
|
|
|
|
"battery",
|
|
|
|
|
|
|
|
BatteryEvent {
|
|
|
|
|
|
|
|
device_id: device.id.clone(),
|
|
|
|
|
|
|
|
percent,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// станок: запрашиваем контроль и запоминаем
|
|
|
|
if category == "trainer" {
|
|
|
|
if category == "trainer" {
|
|
|
|
*state.trainer.lock().unwrap() = Some(device.id.clone());
|
|
|
|
let _ = source.command(&device, Command::FtmsRequestControl).await;
|
|
|
|
|
|
|
|
*state.trainer.lock().unwrap() = Some(device_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
@ -142,16 +140,13 @@ async fn connect(app: AppHandle, device_id: String, category: String) -> Result<
|
|
|
|
#[tauri::command]
|
|
|
|
#[tauri::command]
|
|
|
|
async fn set_target_power(app: AppHandle, watts: i16) -> Result<(), String> {
|
|
|
|
async fn set_target_power(app: AppHandle, watts: i16) -> Result<(), String> {
|
|
|
|
let state = app.state::<State>();
|
|
|
|
let state = app.state::<State>();
|
|
|
|
if watts < 0 {
|
|
|
|
|
|
|
|
return Err("мощность не может быть отрицательной".to_string());
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
let device = trainer_device(&state)?;
|
|
|
|
let device = trainer_device(&state)?;
|
|
|
|
state
|
|
|
|
state
|
|
|
|
.source
|
|
|
|
.source
|
|
|
|
.command(&device, Command::FtmsSetTargetPower(watts))
|
|
|
|
.command(&device, Command::FtmsSetTargetPower(watts))
|
|
|
|
.await
|
|
|
|
.await
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
state.trainer_state.lock().unwrap().power = Some(watts);
|
|
|
|
state.trainer_targets.lock().unwrap().power = Some(watts);
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -159,14 +154,13 @@ async fn set_target_power(app: AppHandle, watts: i16) -> Result<(), String> {
|
|
|
|
#[tauri::command]
|
|
|
|
#[tauri::command]
|
|
|
|
async fn set_target_cadence(app: AppHandle, rpm: u16) -> Result<(), String> {
|
|
|
|
async fn set_target_cadence(app: AppHandle, rpm: u16) -> Result<(), String> {
|
|
|
|
let state = app.state::<State>();
|
|
|
|
let state = app.state::<State>();
|
|
|
|
let half_rpm = rpm.checked_mul(2).ok_or("каденс слишком большой")?;
|
|
|
|
|
|
|
|
let device = trainer_device(&state)?;
|
|
|
|
let device = trainer_device(&state)?;
|
|
|
|
state
|
|
|
|
state
|
|
|
|
.source
|
|
|
|
.source
|
|
|
|
.command(&device, Command::FtmsSetTargetCadence(half_rpm))
|
|
|
|
.command(&device, Command::FtmsSetTargetCadence(rpm * 2))
|
|
|
|
.await
|
|
|
|
.await
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
state.trainer_state.lock().unwrap().cadence = Some(rpm);
|
|
|
|
state.trainer_targets.lock().unwrap().cadence = Some(rpm);
|
|
|
|
Ok(())
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -179,9 +173,7 @@ async fn trainer_start(app: AppHandle) -> Result<(), String> {
|
|
|
|
.source
|
|
|
|
.source
|
|
|
|
.command(&device, Command::FtmsStart)
|
|
|
|
.command(&device, Command::FtmsStart)
|
|
|
|
.await
|
|
|
|
.await
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
state.trainer_state.lock().unwrap().running = true;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Остановить станок (FTMS Stop or Pause, opcode 0x08).
|
|
|
|
/// Остановить станок (FTMS Stop or Pause, opcode 0x08).
|
|
|
|
@ -193,9 +185,7 @@ async fn trainer_stop(app: AppHandle) -> Result<(), String> {
|
|
|
|
.source
|
|
|
|
.source
|
|
|
|
.command(&device, Command::FtmsStop)
|
|
|
|
.command(&device, Command::FtmsStop)
|
|
|
|
.await
|
|
|
|
.await
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
state.trainer_state.lock().unwrap().running = false;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Возвращает Device подключённого станка.
|
|
|
|
/// Возвращает Device подключённого станка.
|
|
|
|
@ -222,25 +212,19 @@ enum StreamOutcome {
|
|
|
|
Stale,
|
|
|
|
Stale,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Consumes a telemetry stream, invoking `on_item` per item, until it ends or goes
|
|
|
|
|
|
|
|
/// silent for `timeout`. Returns the outcome and whether at least one item was seen.
|
|
|
|
|
|
|
|
async fn consume_with_watchdog<F>(
|
|
|
|
async fn consume_with_watchdog<F>(
|
|
|
|
stream: &mut Pin<Box<dyn Stream<Item = Telemetry> + Send>>,
|
|
|
|
stream: &mut Pin<Box<dyn Stream<Item = Telemetry> + Send>>,
|
|
|
|
timeout: Duration,
|
|
|
|
timeout: Duration,
|
|
|
|
mut on_item: F,
|
|
|
|
mut on_item: F,
|
|
|
|
) -> (StreamOutcome, bool)
|
|
|
|
) -> StreamOutcome
|
|
|
|
where
|
|
|
|
where
|
|
|
|
F: FnMut(Telemetry),
|
|
|
|
F: FnMut(Telemetry),
|
|
|
|
{
|
|
|
|
{
|
|
|
|
let mut saw = false;
|
|
|
|
|
|
|
|
loop {
|
|
|
|
loop {
|
|
|
|
match time::timeout(timeout, stream.next()).await {
|
|
|
|
match time::timeout(timeout, stream.next()).await {
|
|
|
|
Ok(Some(t)) => {
|
|
|
|
Ok(Some(t)) => on_item(t),
|
|
|
|
saw = true;
|
|
|
|
Ok(None) => return StreamOutcome::Ended,
|
|
|
|
on_item(t);
|
|
|
|
Err(_) => return StreamOutcome::Stale,
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(None) => return (StreamOutcome::Ended, saw),
|
|
|
|
|
|
|
|
Err(_) => return (StreamOutcome::Stale, saw),
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -252,88 +236,71 @@ async fn run_watchdog(
|
|
|
|
category: String,
|
|
|
|
category: String,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
let device_id = device.id.clone();
|
|
|
|
let device_id = device.id.clone();
|
|
|
|
let mut silent_cycles: u32 = 0;
|
|
|
|
let mut attempts: u32 = 0;
|
|
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
loop {
|
|
|
|
// stream() does ensure_connected() internally, so a fresh call = reconnect.
|
|
|
|
// stream() does ensure_connected() internally, so a fresh call = reconnect.
|
|
|
|
let mut stream = match source.stream(&device).await {
|
|
|
|
let mut stream = match source.stream(&device).await {
|
|
|
|
Ok(s) => {
|
|
|
|
Ok(s) => {
|
|
|
|
|
|
|
|
attempts = 0;
|
|
|
|
emit_connection(&app, &device_id, &category, "reconnected");
|
|
|
|
emit_connection(&app, &device_id, &category, "reconnected");
|
|
|
|
s
|
|
|
|
s
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(_) => {
|
|
|
|
Err(_) => {
|
|
|
|
silent_cycles += 1;
|
|
|
|
attempts += 1;
|
|
|
|
emit_connection(&app, &device_id, &category, "reconnecting");
|
|
|
|
emit_connection(&app, &device_id, &category, "reconnecting");
|
|
|
|
if silent_cycles >= RECONNECT_MAX_ATTEMPTS {
|
|
|
|
if attempts >= RECONNECT_MAX_ATTEMPTS {
|
|
|
|
emit_connection(&app, &device_id, &category, "gave_up");
|
|
|
|
emit_connection(&app, &device_id, &category, "gave_up");
|
|
|
|
app.state::<State>().streams.lock().unwrap().remove(&category);
|
|
|
|
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
time::sleep(RECONNECT_BACKOFF * silent_cycles).await;
|
|
|
|
time::sleep(RECONNECT_BACKOFF * attempts).await;
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Trainer: request control (with confirmation) and restore targets/start.
|
|
|
|
// Trainer: re-request control and re-apply last targets after reconnect.
|
|
|
|
if category == "trainer" {
|
|
|
|
if category == "trainer" {
|
|
|
|
let _ = source.request_control(&device).await;
|
|
|
|
let _ = source.command(&device, Command::FtmsRequestControl).await;
|
|
|
|
reapply_trainer_state(&app, &source, &device).await;
|
|
|
|
reapply_trainer_targets(&app, &source, &device).await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let app_emit = app.clone();
|
|
|
|
let app_emit = app.clone();
|
|
|
|
let dev_id = device_id.clone();
|
|
|
|
let dev_id = device_id.clone();
|
|
|
|
let (outcome, saw_data) = consume_with_watchdog(
|
|
|
|
let outcome = consume_with_watchdog(
|
|
|
|
&mut stream,
|
|
|
|
&mut stream,
|
|
|
|
WATCHDOG_STALE_TIMEOUT,
|
|
|
|
WATCHDOG_STALE_TIMEOUT,
|
|
|
|
move |t| {
|
|
|
|
move |t| {
|
|
|
|
let _ = app_emit.emit(
|
|
|
|
let _ = app_emit.emit("telemetry", TelemetryEvent {
|
|
|
|
"telemetry",
|
|
|
|
device_id: dev_id.clone(),
|
|
|
|
TelemetryEvent {
|
|
|
|
telemetry: t,
|
|
|
|
device_id: dev_id.clone(),
|
|
|
|
});
|
|
|
|
telemetry: t,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
).await;
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Stream broke (Ended or Stale). Loop back and reconnect.
|
|
|
|
let _ = outcome;
|
|
|
|
let _ = outcome;
|
|
|
|
// A healthy cycle (received data) resets the counter; a silent one does not.
|
|
|
|
|
|
|
|
if saw_data {
|
|
|
|
|
|
|
|
silent_cycles = 0;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
silent_cycles += 1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
emit_connection(&app, &device_id, &category, "reconnecting");
|
|
|
|
emit_connection(&app, &device_id, &category, "reconnecting");
|
|
|
|
if silent_cycles >= RECONNECT_MAX_ATTEMPTS {
|
|
|
|
|
|
|
|
emit_connection(&app, &device_id, &category, "gave_up");
|
|
|
|
|
|
|
|
app.state::<State>().streams.lock().unwrap().remove(&category);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
time::sleep(RECONNECT_BACKOFF).await;
|
|
|
|
time::sleep(RECONNECT_BACKOFF).await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn emit_connection(app: &AppHandle, device_id: &str, category: &str, status: &'static str) {
|
|
|
|
fn emit_connection(app: &AppHandle, device_id: &str, category: &str, status: &'static str) {
|
|
|
|
let _ = app.emit(
|
|
|
|
let _ = app.emit("connection", ConnectionEvent {
|
|
|
|
"connection",
|
|
|
|
device_id: device_id.to_string(),
|
|
|
|
ConnectionEvent {
|
|
|
|
category: category.to_string(),
|
|
|
|
device_id: device_id.to_string(),
|
|
|
|
status,
|
|
|
|
category: category.to_string(),
|
|
|
|
});
|
|
|
|
status,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async fn reapply_trainer_state(
|
|
|
|
async fn reapply_trainer_targets(
|
|
|
|
app: &AppHandle,
|
|
|
|
app: &AppHandle,
|
|
|
|
source: &Arc<dyn SensorSource>,
|
|
|
|
source: &Arc<dyn SensorSource>,
|
|
|
|
device: &Device,
|
|
|
|
device: &Device,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
let state = app.state::<State>();
|
|
|
|
let state = app.state::<State>();
|
|
|
|
let (power, cadence, running) = {
|
|
|
|
let (power, cadence) = {
|
|
|
|
let t = state.trainer_state.lock().unwrap();
|
|
|
|
let targets = state.trainer_targets.lock().unwrap();
|
|
|
|
(t.power, t.cadence, t.running)
|
|
|
|
(targets.power, targets.cadence)
|
|
|
|
};
|
|
|
|
};
|
|
|
|
if let Some(watts) = power {
|
|
|
|
if let Some(watts) = power {
|
|
|
|
let _ = source.command(device, Command::FtmsSetTargetPower(watts)).await;
|
|
|
|
let _ = source.command(device, Command::FtmsSetTargetPower(watts)).await;
|
|
|
|
@ -341,9 +308,6 @@ async fn reapply_trainer_state(
|
|
|
|
if let Some(rpm) = cadence {
|
|
|
|
if let Some(rpm) = cadence {
|
|
|
|
let _ = source.command(device, Command::FtmsSetTargetCadence(rpm * 2)).await;
|
|
|
|
let _ = source.command(device, Command::FtmsSetTargetCadence(rpm * 2)).await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if running {
|
|
|
|
|
|
|
|
let _ = source.command(device, Command::FtmsStart).await;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
|
|
@ -366,7 +330,7 @@ pub fn run() {
|
|
|
|
devices: RwLock::new(Vec::new()),
|
|
|
|
devices: RwLock::new(Vec::new()),
|
|
|
|
trainer: Mutex::new(None),
|
|
|
|
trainer: Mutex::new(None),
|
|
|
|
streams: Mutex::new(std::collections::HashMap::new()),
|
|
|
|
streams: Mutex::new(std::collections::HashMap::new()),
|
|
|
|
trainer_state: Mutex::new(TrainerState::default()),
|
|
|
|
trainer_targets: Mutex::new(TrainerTargets::default()),
|
|
|
|
});
|
|
|
|
});
|
|
|
|
Ok::<(), Box<dyn std::error::Error>>(())
|
|
|
|
Ok::<(), Box<dyn std::error::Error>>(())
|
|
|
|
})
|
|
|
|
})
|
|
|
|
@ -398,9 +362,8 @@ mod tests {
|
|
|
|
let items = vec![Telemetry::HeartRate { bpm: 120 }];
|
|
|
|
let items = vec![Telemetry::HeartRate { bpm: 120 }];
|
|
|
|
let mut s = pin(stream::iter(items));
|
|
|
|
let mut s = pin(stream::iter(items));
|
|
|
|
let mut seen = Vec::new();
|
|
|
|
let mut seen = Vec::new();
|
|
|
|
let (outcome, saw) = consume_with_watchdog(&mut s, Duration::from_secs(5), |t| seen.push(t)).await;
|
|
|
|
let outcome = consume_with_watchdog(&mut s, Duration::from_secs(5), |t| seen.push(t)).await;
|
|
|
|
assert!(matches!(outcome, StreamOutcome::Ended));
|
|
|
|
assert!(matches!(outcome, StreamOutcome::Ended));
|
|
|
|
assert!(saw);
|
|
|
|
|
|
|
|
assert_eq!(seen, vec![Telemetry::HeartRate { bpm: 120 }]);
|
|
|
|
assert_eq!(seen, vec![Telemetry::HeartRate { bpm: 120 }]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -408,9 +371,8 @@ mod tests {
|
|
|
|
async fn stale_when_silent_past_timeout() {
|
|
|
|
async fn stale_when_silent_past_timeout() {
|
|
|
|
let mut s = pin(stream::pending::<Telemetry>());
|
|
|
|
let mut s = pin(stream::pending::<Telemetry>());
|
|
|
|
let mut seen = Vec::new();
|
|
|
|
let mut seen = Vec::new();
|
|
|
|
let (outcome, saw) = consume_with_watchdog(&mut s, Duration::from_millis(50), |t| seen.push(t)).await;
|
|
|
|
let outcome = consume_with_watchdog(&mut s, Duration::from_millis(50), |t| seen.push(t)).await;
|
|
|
|
assert!(matches!(outcome, StreamOutcome::Stale));
|
|
|
|
assert!(matches!(outcome, StreamOutcome::Stale));
|
|
|
|
assert!(!saw);
|
|
|
|
|
|
|
|
assert!(seen.is_empty());
|
|
|
|
assert!(seen.is_empty());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -422,8 +384,7 @@ mod tests {
|
|
|
|
];
|
|
|
|
];
|
|
|
|
let mut s = pin(stream::iter(items));
|
|
|
|
let mut s = pin(stream::iter(items));
|
|
|
|
let mut seen = Vec::new();
|
|
|
|
let mut seen = Vec::new();
|
|
|
|
let (_outcome, saw) = consume_with_watchdog(&mut s, Duration::from_secs(5), |t| seen.push(t)).await;
|
|
|
|
let _ = consume_with_watchdog(&mut s, Duration::from_secs(5), |t| seen.push(t)).await;
|
|
|
|
assert!(saw);
|
|
|
|
|
|
|
|
assert_eq!(seen.len(), 2);
|
|
|
|
assert_eq!(seen.len(), 2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|