2026-04-19 03:28:23 -03:00
|
|
|
use anyhow::{Context, Result, bail};
|
|
|
|
|
use lesavka_common::lesavka::{
|
2026-05-02 13:44:32 -03:00
|
|
|
CalibrationAction, CalibrationRequest, CalibrationState, CapturePowerCommand, HandshakeSet,
|
2026-05-03 14:00:58 -03:00
|
|
|
OutputDelayProbeRequest, SetCapturePowerRequest, relay_client::RelayClient,
|
2026-04-19 03:28:23 -03:00
|
|
|
};
|
2026-04-23 03:49:49 -03:00
|
|
|
#[cfg(not(coverage))]
|
2026-05-01 16:06:52 -03:00
|
|
|
use lesavka_common::lesavka::{Empty, handshake_client::HandshakeClient};
|
|
|
|
|
#[cfg(not(coverage))]
|
2026-04-23 03:49:49 -03:00
|
|
|
use tonic::Request;
|
|
|
|
|
use tonic::transport::Channel;
|
2026-04-19 03:28:23 -03:00
|
|
|
|
2026-04-30 08:16:57 -03:00
|
|
|
use lesavka_client::relay_transport;
|
|
|
|
|
|
2026-04-19 03:28:23 -03:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
enum CommandKind {
|
|
|
|
|
Status,
|
2026-05-01 16:06:52 -03:00
|
|
|
Version,
|
2026-05-02 13:44:32 -03:00
|
|
|
CalibrationStatus,
|
|
|
|
|
CalibrationAdjust,
|
|
|
|
|
CalibrationRestoreDefault,
|
|
|
|
|
CalibrationRestoreFactory,
|
|
|
|
|
CalibrationSaveDefault,
|
2026-04-19 03:28:23 -03:00
|
|
|
Auto,
|
|
|
|
|
On,
|
|
|
|
|
Off,
|
2026-04-30 18:38:34 -03:00
|
|
|
RecoverUsb,
|
|
|
|
|
RecoverUac,
|
|
|
|
|
RecoverUvc,
|
2026-04-20 08:38:26 -03:00
|
|
|
ResetUsb,
|
2026-05-01 19:16:40 -03:00
|
|
|
UpstreamSync,
|
2026-05-03 14:00:58 -03:00
|
|
|
OutputDelayProbe,
|
2026-04-19 03:28:23 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CommandKind {
|
2026-04-30 18:38:34 -03:00
|
|
|
/// Parse the intentionally small CLI vocabulary into safe relay actions.
|
2026-04-19 03:28:23 -03:00
|
|
|
fn parse(value: &str) -> Option<Self> {
|
|
|
|
|
match value {
|
|
|
|
|
"status" | "get" => Some(Self::Status),
|
2026-05-01 16:06:52 -03:00
|
|
|
"version" | "versions" => Some(Self::Version),
|
2026-05-02 13:44:32 -03:00
|
|
|
"calibration" | "calibration-status" => Some(Self::CalibrationStatus),
|
|
|
|
|
"calibrate" | "calibration-adjust" => Some(Self::CalibrationAdjust),
|
|
|
|
|
"calibration-restore-default" | "restore-calibration" => {
|
|
|
|
|
Some(Self::CalibrationRestoreDefault)
|
|
|
|
|
}
|
|
|
|
|
"calibration-restore-factory" | "factory-calibration" => {
|
|
|
|
|
Some(Self::CalibrationRestoreFactory)
|
|
|
|
|
}
|
|
|
|
|
"calibration-save-default" | "save-calibration" => Some(Self::CalibrationSaveDefault),
|
2026-04-19 03:28:23 -03:00
|
|
|
"auto" => Some(Self::Auto),
|
|
|
|
|
"on" | "force-on" => Some(Self::On),
|
|
|
|
|
"off" | "force-off" => Some(Self::Off),
|
2026-04-30 18:38:34 -03:00
|
|
|
"recover-usb" => Some(Self::RecoverUsb),
|
2026-05-09 16:52:31 -03:00
|
|
|
"recover-uac" | "heal-av" | "heal-upstream" | "recover-upstream" => {
|
|
|
|
|
Some(Self::RecoverUac)
|
|
|
|
|
}
|
2026-04-30 18:38:34 -03:00
|
|
|
"recover-uvc" => Some(Self::RecoverUvc),
|
|
|
|
|
"reset-usb" | "hard-reset-usb" => Some(Self::ResetUsb),
|
2026-05-01 19:16:40 -03:00
|
|
|
"upstream-sync" | "sync" => Some(Self::UpstreamSync),
|
2026-05-03 14:00:58 -03:00
|
|
|
"output-delay-probe" | "probe-output-delay" => Some(Self::OutputDelayProbe),
|
2026-04-19 03:28:23 -03:00
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 03:49:49 -03:00
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
2026-04-19 03:28:23 -03:00
|
|
|
struct Config {
|
|
|
|
|
server: String,
|
|
|
|
|
command: CommandKind,
|
2026-05-02 13:44:32 -03:00
|
|
|
audio_delta_us: i64,
|
|
|
|
|
video_delta_us: i64,
|
|
|
|
|
note: String,
|
2026-05-03 14:00:58 -03:00
|
|
|
probe_duration_seconds: u32,
|
|
|
|
|
probe_warmup_seconds: u32,
|
|
|
|
|
probe_pulse_period_ms: u32,
|
|
|
|
|
probe_pulse_width_ms: u32,
|
|
|
|
|
probe_event_width_codes: String,
|
2026-05-03 17:15:18 -03:00
|
|
|
probe_audio_delay_us: i64,
|
|
|
|
|
probe_video_delay_us: i64,
|
2026-05-03 14:00:58 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Default, Eq, PartialEq)]
|
|
|
|
|
struct ParsedCommandArgs {
|
|
|
|
|
audio_delta_us: i64,
|
|
|
|
|
video_delta_us: i64,
|
|
|
|
|
note: String,
|
|
|
|
|
probe_duration_seconds: u32,
|
|
|
|
|
probe_warmup_seconds: u32,
|
|
|
|
|
probe_pulse_period_ms: u32,
|
|
|
|
|
probe_pulse_width_ms: u32,
|
|
|
|
|
probe_event_width_codes: String,
|
2026-05-03 17:15:18 -03:00
|
|
|
probe_audio_delay_us: i64,
|
|
|
|
|
probe_video_delay_us: i64,
|
2026-04-19 03:28:23 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 03:49:49 -03:00
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
|
|
|
enum ParseOutcome {
|
|
|
|
|
Run(Config),
|
|
|
|
|
Help,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 03:28:23 -03:00
|
|
|
fn usage() -> &'static str {
|
2026-05-09 16:52:31 -03:00
|
|
|
"Usage: lesavka-relayctl [--server http://HOST:50051] <status|version|upstream-sync|output-delay-probe [duration_s warmup_s period_ms width_ms codes audio_delay_us video_delay_us]|calibration|calibrate <audio_delta_us> <video_delta_us> [note]|calibration-save-default|calibration-restore-default|calibration-restore-factory|auto|on|off|recover-usb|recover-uac|heal-av|recover-uvc|reset-usb>"
|
2026-04-19 03:28:23 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `parse_args_outcome_from` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-04-23 03:49:49 -03:00
|
|
|
fn parse_args_outcome_from<I, S>(args: I) -> Result<ParseOutcome>
|
|
|
|
|
where
|
|
|
|
|
I: IntoIterator<Item = S>,
|
|
|
|
|
S: Into<String>,
|
|
|
|
|
{
|
|
|
|
|
let mut args = args.into_iter().map(Into::into);
|
2026-04-19 03:28:23 -03:00
|
|
|
let mut server = "http://127.0.0.1:50051".to_string();
|
|
|
|
|
let mut command = None;
|
2026-05-02 13:44:32 -03:00
|
|
|
let mut command_args = Vec::new();
|
2026-04-19 03:28:23 -03:00
|
|
|
|
|
|
|
|
while let Some(arg) = args.next() {
|
|
|
|
|
match arg.as_str() {
|
|
|
|
|
"--server" => {
|
|
|
|
|
server = args
|
|
|
|
|
.next()
|
|
|
|
|
.context("missing value after --server")?
|
|
|
|
|
.trim()
|
|
|
|
|
.to_string();
|
|
|
|
|
}
|
|
|
|
|
"--help" | "-h" => {
|
2026-04-23 03:49:49 -03:00
|
|
|
return Ok(ParseOutcome::Help);
|
2026-04-19 03:28:23 -03:00
|
|
|
}
|
|
|
|
|
_ if command.is_none() => {
|
|
|
|
|
command = CommandKind::parse(arg.as_str());
|
|
|
|
|
if command.is_none() {
|
|
|
|
|
bail!("unknown command `{arg}`\n{}", usage());
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-02 13:44:32 -03:00
|
|
|
_ => command_args.push(arg),
|
2026-04-19 03:28:23 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 13:44:32 -03:00
|
|
|
let command = command.unwrap_or(CommandKind::Status);
|
2026-05-03 14:00:58 -03:00
|
|
|
let parsed = parse_command_args(command, command_args)?;
|
2026-04-23 03:49:49 -03:00
|
|
|
Ok(ParseOutcome::Run(Config {
|
2026-04-19 03:28:23 -03:00
|
|
|
server,
|
2026-05-02 13:44:32 -03:00
|
|
|
command,
|
2026-05-03 14:00:58 -03:00
|
|
|
audio_delta_us: parsed.audio_delta_us,
|
|
|
|
|
video_delta_us: parsed.video_delta_us,
|
|
|
|
|
note: parsed.note,
|
|
|
|
|
probe_duration_seconds: parsed.probe_duration_seconds,
|
|
|
|
|
probe_warmup_seconds: parsed.probe_warmup_seconds,
|
|
|
|
|
probe_pulse_period_ms: parsed.probe_pulse_period_ms,
|
|
|
|
|
probe_pulse_width_ms: parsed.probe_pulse_width_ms,
|
|
|
|
|
probe_event_width_codes: parsed.probe_event_width_codes,
|
2026-05-03 17:15:18 -03:00
|
|
|
probe_audio_delay_us: parsed.probe_audio_delay_us,
|
|
|
|
|
probe_video_delay_us: parsed.probe_video_delay_us,
|
2026-04-23 03:49:49 -03:00
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `parse_command_args` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-03 14:00:58 -03:00
|
|
|
fn parse_command_args(command: CommandKind, args: Vec<String>) -> Result<ParsedCommandArgs> {
|
|
|
|
|
if command == CommandKind::OutputDelayProbe {
|
|
|
|
|
return parse_output_delay_probe_args(args);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 13:44:32 -03:00
|
|
|
if command != CommandKind::CalibrationAdjust {
|
|
|
|
|
if let Some(arg) = args.first() {
|
|
|
|
|
bail!("unexpected argument `{arg}`\n{}", usage());
|
|
|
|
|
}
|
2026-05-03 14:00:58 -03:00
|
|
|
return Ok(ParsedCommandArgs::default());
|
2026-05-02 13:44:32 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if args.len() < 2 {
|
|
|
|
|
bail!(
|
|
|
|
|
"calibrate requires audio_delta_us and video_delta_us\n{}",
|
|
|
|
|
usage()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let audio_delta_us = args[0]
|
|
|
|
|
.parse::<i64>()
|
|
|
|
|
.with_context(|| format!("parsing audio_delta_us `{}`", args[0]))?;
|
|
|
|
|
let video_delta_us = args[1]
|
|
|
|
|
.parse::<i64>()
|
|
|
|
|
.with_context(|| format!("parsing video_delta_us `{}`", args[1]))?;
|
|
|
|
|
let note = args[2..].join(" ");
|
2026-05-03 14:00:58 -03:00
|
|
|
Ok(ParsedCommandArgs {
|
|
|
|
|
audio_delta_us,
|
|
|
|
|
video_delta_us,
|
|
|
|
|
note,
|
|
|
|
|
..ParsedCommandArgs::default()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `parse_output_delay_probe_args` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-03 14:00:58 -03:00
|
|
|
fn parse_output_delay_probe_args(args: Vec<String>) -> Result<ParsedCommandArgs> {
|
2026-05-03 17:15:18 -03:00
|
|
|
if args.len() > 7 {
|
2026-05-03 14:00:58 -03:00
|
|
|
bail!(
|
2026-05-03 17:15:18 -03:00
|
|
|
"output-delay-probe accepts at most seven arguments\n{}",
|
2026-05-03 14:00:58 -03:00
|
|
|
usage()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let parse_u32 = |index: usize, name: &str| -> Result<u32> {
|
|
|
|
|
args.get(index)
|
|
|
|
|
.map(|value| {
|
|
|
|
|
value
|
|
|
|
|
.parse::<u32>()
|
|
|
|
|
.with_context(|| format!("parsing {name} `{value}`"))
|
|
|
|
|
})
|
|
|
|
|
.transpose()
|
|
|
|
|
.map(|value| value.unwrap_or(0))
|
|
|
|
|
};
|
2026-05-03 17:15:18 -03:00
|
|
|
let parse_i64 = |index: usize, name: &str| -> Result<i64> {
|
|
|
|
|
args.get(index)
|
|
|
|
|
.map(|value| {
|
|
|
|
|
value
|
|
|
|
|
.parse::<i64>()
|
|
|
|
|
.with_context(|| format!("parsing {name} `{value}`"))
|
|
|
|
|
})
|
|
|
|
|
.transpose()
|
|
|
|
|
.map(|value| value.unwrap_or(0))
|
|
|
|
|
};
|
2026-05-03 14:00:58 -03:00
|
|
|
Ok(ParsedCommandArgs {
|
|
|
|
|
probe_duration_seconds: parse_u32(0, "duration_s")?,
|
|
|
|
|
probe_warmup_seconds: parse_u32(1, "warmup_s")?,
|
|
|
|
|
probe_pulse_period_ms: parse_u32(2, "period_ms")?,
|
|
|
|
|
probe_pulse_width_ms: parse_u32(3, "width_ms")?,
|
|
|
|
|
probe_event_width_codes: args.get(4).cloned().unwrap_or_default(),
|
2026-05-03 17:15:18 -03:00
|
|
|
probe_audio_delay_us: parse_i64(5, "audio_delay_us")?,
|
|
|
|
|
probe_video_delay_us: parse_i64(6, "video_delay_us")?,
|
2026-05-03 14:00:58 -03:00
|
|
|
..ParsedCommandArgs::default()
|
|
|
|
|
})
|
2026-05-02 13:44:32 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 07:00:06 -03:00
|
|
|
#[cfg(test)]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `parse_args_from` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-04-23 03:49:49 -03:00
|
|
|
fn parse_args_from<I, S>(args: I) -> Result<Config>
|
|
|
|
|
where
|
|
|
|
|
I: IntoIterator<Item = S>,
|
|
|
|
|
S: Into<String>,
|
|
|
|
|
{
|
|
|
|
|
match parse_args_outcome_from(args)? {
|
|
|
|
|
ParseOutcome::Run(config) => Ok(config),
|
|
|
|
|
ParseOutcome::Help => bail!("{}", usage()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_args() -> Result<ParseOutcome> {
|
|
|
|
|
parse_args_outcome_from(std::env::args().skip(1))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `capture_power_request` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-04-23 03:49:49 -03:00
|
|
|
fn capture_power_request(command: CommandKind) -> Option<SetCapturePowerRequest> {
|
|
|
|
|
let (enabled, command) = match command {
|
2026-04-30 18:38:34 -03:00
|
|
|
CommandKind::Status
|
2026-05-01 16:06:52 -03:00
|
|
|
| CommandKind::Version
|
2026-05-02 13:44:32 -03:00
|
|
|
| CommandKind::CalibrationStatus
|
|
|
|
|
| CommandKind::CalibrationAdjust
|
|
|
|
|
| CommandKind::CalibrationRestoreDefault
|
|
|
|
|
| CommandKind::CalibrationRestoreFactory
|
|
|
|
|
| CommandKind::CalibrationSaveDefault
|
2026-04-30 18:38:34 -03:00
|
|
|
| CommandKind::RecoverUsb
|
|
|
|
|
| CommandKind::RecoverUac
|
|
|
|
|
| CommandKind::RecoverUvc
|
2026-05-01 19:16:40 -03:00
|
|
|
| CommandKind::ResetUsb
|
2026-05-03 14:00:58 -03:00
|
|
|
| CommandKind::UpstreamSync
|
|
|
|
|
| CommandKind::OutputDelayProbe => return None,
|
2026-04-23 03:49:49 -03:00
|
|
|
CommandKind::Auto => (false, CapturePowerCommand::Auto),
|
|
|
|
|
CommandKind::On => (true, CapturePowerCommand::ForceOn),
|
|
|
|
|
CommandKind::Off => (false, CapturePowerCommand::ForceOff),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Some(SetCapturePowerRequest {
|
|
|
|
|
enabled,
|
|
|
|
|
command: command as i32,
|
2026-04-19 03:28:23 -03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 03:49:49 -03:00
|
|
|
#[cfg(not(coverage))]
|
2026-04-19 03:28:23 -03:00
|
|
|
async fn connect(server_addr: &str) -> Result<RelayClient<Channel>> {
|
2026-04-30 08:16:57 -03:00
|
|
|
let channel = relay_transport::endpoint(server_addr)?
|
2026-04-19 03:28:23 -03:00
|
|
|
.tcp_nodelay(true)
|
|
|
|
|
.connect()
|
|
|
|
|
.await
|
|
|
|
|
.with_context(|| format!("connecting to relay at {server_addr}"))?;
|
|
|
|
|
Ok(RelayClient::new(channel))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 16:06:52 -03:00
|
|
|
#[cfg(not(coverage))]
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `get_server_capabilities` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-01 16:06:52 -03:00
|
|
|
async fn get_server_capabilities(server_addr: &str) -> Result<HandshakeSet> {
|
|
|
|
|
let channel = relay_transport::endpoint(server_addr)?
|
|
|
|
|
.tcp_nodelay(true)
|
|
|
|
|
.connect()
|
|
|
|
|
.await
|
|
|
|
|
.with_context(|| format!("connecting to handshake at {server_addr}"))?;
|
|
|
|
|
let mut client = HandshakeClient::new(channel);
|
|
|
|
|
Ok(client
|
|
|
|
|
.get_capabilities(Request::new(Empty {}))
|
|
|
|
|
.await
|
|
|
|
|
.context("querying server capabilities")?
|
|
|
|
|
.into_inner())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 03:49:49 -03:00
|
|
|
#[cfg(coverage)]
|
|
|
|
|
async fn connect(server_addr: &str) -> Result<RelayClient<Channel>> {
|
2026-04-30 08:16:57 -03:00
|
|
|
let channel = relay_transport::endpoint(server_addr)?
|
2026-04-23 03:49:49 -03:00
|
|
|
.tcp_nodelay(true)
|
|
|
|
|
.connect_lazy();
|
|
|
|
|
Ok(RelayClient::new(channel))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 03:28:23 -03:00
|
|
|
fn print_state(state: lesavka_common::lesavka::CapturePowerState) {
|
|
|
|
|
println!("available={}", state.available);
|
|
|
|
|
println!("enabled={}", state.enabled);
|
|
|
|
|
println!("mode={}", state.mode);
|
2026-04-20 01:41:57 -03:00
|
|
|
println!("detected_devices={}", state.detected_devices);
|
2026-04-19 03:28:23 -03:00
|
|
|
println!("active_leases={}", state.active_leases);
|
|
|
|
|
println!("unit={}", state.unit);
|
|
|
|
|
println!("detail={}", state.detail);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `print_versions` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-01 16:06:52 -03:00
|
|
|
fn print_versions(server_addr: &str, caps: &HandshakeSet) {
|
|
|
|
|
let server_version = if caps.server_version.is_empty() {
|
|
|
|
|
"unknown"
|
|
|
|
|
} else {
|
|
|
|
|
caps.server_version.as_str()
|
|
|
|
|
};
|
2026-05-01 20:25:56 -03:00
|
|
|
let server_revision = if caps.server_revision.is_empty() {
|
|
|
|
|
"unknown"
|
|
|
|
|
} else {
|
|
|
|
|
caps.server_revision.as_str()
|
|
|
|
|
};
|
2026-05-01 16:06:52 -03:00
|
|
|
println!("client_version={}", lesavka_client::VERSION);
|
2026-05-01 20:25:56 -03:00
|
|
|
println!("client_revision={}", lesavka_client::REVISION);
|
2026-05-01 16:06:52 -03:00
|
|
|
println!("server_addr={server_addr}");
|
|
|
|
|
println!("server_version={server_version}");
|
2026-05-01 20:25:56 -03:00
|
|
|
println!("server_revision={server_revision}");
|
2026-05-01 16:06:52 -03:00
|
|
|
println!("server_camera_output={}", caps.camera_output);
|
|
|
|
|
println!("server_camera_codec={}", caps.camera_codec);
|
2026-05-03 05:37:16 -03:00
|
|
|
println!("server_camera_width={}", caps.camera_width);
|
|
|
|
|
println!("server_camera_height={}", caps.camera_height);
|
|
|
|
|
println!("server_camera_fps={}", caps.camera_fps);
|
2026-05-03 02:13:39 -03:00
|
|
|
println!("server_bundled_webcam_media={}", caps.bundled_webcam_media);
|
2026-05-01 16:06:52 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
include!("lesavka_relayctl/upstream_sync_formatting.rs");
|
|
|
|
|
/// Keeps `print_calibration_state` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-02 13:44:32 -03:00
|
|
|
fn print_calibration_state(state: CalibrationState) {
|
|
|
|
|
println!("calibration_profile={}", state.profile);
|
|
|
|
|
println!(
|
|
|
|
|
"calibration_factory_audio_offset_us={}",
|
|
|
|
|
state.factory_audio_offset_us
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
"calibration_factory_video_offset_us={}",
|
|
|
|
|
state.factory_video_offset_us
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
"calibration_default_audio_offset_us={}",
|
|
|
|
|
state.default_audio_offset_us
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
"calibration_default_video_offset_us={}",
|
|
|
|
|
state.default_video_offset_us
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
"calibration_active_audio_offset_us={}",
|
|
|
|
|
state.active_audio_offset_us
|
|
|
|
|
);
|
|
|
|
|
println!(
|
|
|
|
|
"calibration_active_video_offset_us={}",
|
|
|
|
|
state.active_video_offset_us
|
|
|
|
|
);
|
|
|
|
|
println!("calibration_source={}", state.source);
|
|
|
|
|
println!("calibration_confidence={}", state.confidence);
|
|
|
|
|
println!("calibration_updated_at={}", state.updated_at);
|
|
|
|
|
println!("calibration_detail={}", state.detail);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
/// Keeps `calibration_request_for` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
|
|
|
|
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
2026-05-02 13:44:32 -03:00
|
|
|
fn calibration_request_for(config: &Config) -> Option<CalibrationRequest> {
|
|
|
|
|
let action = match config.command {
|
|
|
|
|
CommandKind::CalibrationAdjust => CalibrationAction::AdjustActive,
|
|
|
|
|
CommandKind::CalibrationRestoreDefault => CalibrationAction::RestoreDefault,
|
|
|
|
|
CommandKind::CalibrationRestoreFactory => CalibrationAction::RestoreFactory,
|
|
|
|
|
CommandKind::CalibrationSaveDefault => CalibrationAction::SaveActiveAsDefault,
|
|
|
|
|
CommandKind::Status
|
|
|
|
|
| CommandKind::Version
|
|
|
|
|
| CommandKind::CalibrationStatus
|
|
|
|
|
| CommandKind::Auto
|
|
|
|
|
| CommandKind::On
|
|
|
|
|
| CommandKind::Off
|
|
|
|
|
| CommandKind::RecoverUsb
|
|
|
|
|
| CommandKind::RecoverUac
|
|
|
|
|
| CommandKind::RecoverUvc
|
|
|
|
|
| CommandKind::ResetUsb
|
2026-05-03 14:00:58 -03:00
|
|
|
| CommandKind::UpstreamSync
|
|
|
|
|
| CommandKind::OutputDelayProbe => return None,
|
2026-05-02 13:44:32 -03:00
|
|
|
};
|
|
|
|
|
Some(CalibrationRequest {
|
|
|
|
|
action: action as i32,
|
|
|
|
|
audio_delta_us: config.audio_delta_us,
|
|
|
|
|
video_delta_us: config.video_delta_us,
|
|
|
|
|
observed_delivery_skew_ms: 0.0,
|
|
|
|
|
observed_enqueue_skew_ms: 0.0,
|
|
|
|
|
note: config.note.clone(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 05:50:59 -03:00
|
|
|
include!("lesavka_relayctl/command_dispatch.rs");
|
2026-04-23 03:49:49 -03:00
|
|
|
#[cfg(coverage)]
|
|
|
|
|
fn main() {
|
|
|
|
|
let _ = parse_args as fn() -> Result<ParseOutcome>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
2026-05-06 05:50:59 -03:00
|
|
|
#[path = "tests/lesavka_relayctl.rs"]
|
|
|
|
|
mod tests;
|