#![allow(clippy::too_many_lines, clippy::doc_markdown)]
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use clap::Parser;
use matter_controller::{FileStore, MatterController, ReadPath, SubscriptionEvent};
const ONOFF_ENDPOINT: u16 = 1;
const ONOFF_CLUSTER: u32 = 0x0006;
#[derive(Parser)]
#[command(about = "SH.2b subscription-survives-reboot hardware validation")]
struct Args {
#[arg(long)]
store: PathBuf,
#[arg(long)]
node: u64,
#[arg(long, default_value_t = 150)]
watch_secs: u64,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let store = Arc::new(FileStore::new(&args.store));
let controller = MatterController::builder(store)
.build()
.await
.context("opening controller")?;
let node = controller.node(args.node);
println!(
"== SH.2b subscription-survives-reboot against node 0x{:016X} ==\n",
args.node
);
let mut sub = node
.subscribe(
&[ReadPath::cluster(ONOFF_ENDPOINT, ONOFF_CLUSTER)],
&[],
1,
10,
)
.await
.context("subscribe")?;
println!(
"[1] subscription opened. Streaming for {}s.",
args.watch_secs
);
println!(" >>> REBOOT THE DEVICE partway through this window <<<");
println!(" (on the rig: esptool --after hard-reset read-mac, from a second shell)\n");
let mut established_ids: Vec<u32> = Vec::new();
let mut resubscribing_seen = false;
let mut reports_after_resubscribe = 0usize;
let start = Instant::now();
let deadline = start + Duration::from_secs(args.watch_secs);
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
let next = tokio::time::timeout(remaining, sub.next()).await;
let t = start.elapsed().as_secs_f32();
match next {
Err(_) => break, Ok(None) => {
println!("[{t:6.1}s] subscription ended (handle closed)");
break;
}
Ok(Some(ev)) => match ev {
SubscriptionEvent::Established { subscription_id } => {
let phase = if established_ids.is_empty() {
"initial"
} else {
"RE-ESTABLISHED (SH.2b auto-resubscribe)"
};
println!("[{t:6.1}s] Established id=0x{subscription_id:08X} <- {phase}");
established_ids.push(subscription_id);
}
SubscriptionEvent::Resubscribing { cause } => {
resubscribing_seen = true;
println!("[{t:6.1}s] Resubscribing cause: {cause}");
}
SubscriptionEvent::Report(r) => {
if resubscribing_seen {
reports_after_resubscribe += 1;
}
println!("[{t:6.1}s] Report {:?} = {:?}", r.path, r.value);
}
SubscriptionEvent::Event(e) => {
println!("[{t:6.1}s] Event {e:?}");
}
SubscriptionEvent::Lagged { dropped } => {
println!("[{t:6.1}s] Lagged (dropped {dropped})");
}
other => println!("[{t:6.1}s] {other:?}"),
},
}
}
println!("\n== verdict ==");
println!(
" Established events: {} (ids {:#010X?})",
established_ids.len(),
established_ids
);
println!(" Resubscribing seen: {resubscribing_seen}");
println!(" reports after resubscribe: {reports_after_resubscribe}");
let survived = resubscribing_seen && established_ids.len() >= 2;
if survived {
println!("\nPASS — one handle observed Established -> Resubscribing -> Established across the reboot ✓");
} else if established_ids.len() >= 2 {
println!("\nPARTIAL — re-established but no Resubscribing control event was observed");
} else {
println!(
"\nINCONCLUSIVE — no re-establishment seen (did the device reboot inside the window?)"
);
}
sub.cancel().await.ok();
Ok(())
}