use clap::Parser;
use epics_ca_rs::DbFieldType;
use epics_ca_rs::client::CaClient;
const VERSION_INFO: &str = concat!(
"\nEPICS Version epics-rs ",
env!("CARGO_PKG_VERSION"),
", CA Protocol version 4.13"
);
#[derive(Parser)]
#[command(
name = "cainfo-rs",
about = "Show EPICS PV channel information and client diagnostics",
disable_version_flag = true
)]
struct Args {
#[arg(short = 'V', long, hide = true)]
version: bool,
#[arg(short = 'w', long = "wait")]
timeout: Option<f64>,
#[arg(short = 's', long = "stat-level", value_name = "LEVEL")]
stat_level: Option<String>,
#[arg(short = 'p', long)]
priority: Option<u8>,
#[arg(short = 'd', long)]
diag: bool,
pv_names: Vec<String>,
}
fn parse_stat_level(s: &str) -> u32 {
let mut chars = s.trim_start().chars().peekable();
let neg = match chars.peek() {
Some('+') => {
chars.next();
false
}
Some('-') => {
chars.next();
true
}
_ => false,
};
let digits: String = chars.take_while(|c| c.is_ascii_digit()).collect();
if digits.is_empty() {
eprintln!("'{s}' is not a valid interest level - ignored. ('cainfo -h' for help.)");
return 0;
}
let mag = (digits.parse::<u64>().unwrap_or(u64::MAX)) as u32;
if neg { mag.wrapping_neg() } else { mag }
}
#[tokio::main]
async fn main() {
let args = Args::parse();
if args.version {
println!("{VERSION_INFO}");
return;
}
let stat_level = args
.stat_level
.as_deref()
.map(parse_stat_level)
.unwrap_or(0);
let stat_mode = stat_level != 0;
if !stat_mode && !args.diag && args.pv_names.is_empty() {
eprintln!("No pv name specified. ('cainfo -h' for help.)");
std::process::exit(1);
}
let client = CaClient::new().await.expect("failed to create CA client");
if stat_mode {
println!("--- Client Diagnostics ---");
println!("{}", client.diagnostics());
return;
}
let timeout = epics_ca_rs::cli::timeout_duration(
args.timeout
.unwrap_or_else(epics_ca_rs::cli::env_default_timeout),
);
let priority = args.priority.unwrap_or(0);
let mut failed = false;
for pv_name in &args.pv_names {
let ch = client.create_channel_with_priority(pv_name, priority);
match ch.wait_connected(timeout).await {
Ok(()) => match ch.info().await {
Ok(info) => {
let read_prefix = if info.access_rights.read { "" } else { "no " };
let write_prefix = if info.access_rights.write { "" } else { "no " };
println!(
"{name}\n \
State: connected\n \
Host: {host}\n \
Access: {rp}read, {wp}write\n \
Native data type: {dbf}\n \
Request type: {dbr}\n \
Element count: {n}",
name = info.pv_name,
host = info.server_addr,
rp = read_prefix,
wp = write_prefix,
dbf = dbf_name(info.native_type),
dbr = dbr_name(info.native_type),
n = info.element_count,
);
}
Err(e) => {
eprintln!("{pv_name}: {e}");
failed = true;
}
},
Err(_) => {
println!(
"{pv_name}\n \
State: never connected"
);
failed = true;
}
}
}
if args.diag {
if !args.pv_names.is_empty() {
println!();
}
println!("--- Client Diagnostics ---");
println!("{}", client.diagnostics());
}
if failed {
std::process::exit(1);
}
}
fn dbf_name(t: DbFieldType) -> &'static str {
match t {
DbFieldType::String => "DBF_STRING",
DbFieldType::Short => "DBF_SHORT",
DbFieldType::Float => "DBF_FLOAT",
DbFieldType::Enum => "DBF_ENUM",
DbFieldType::Char => "DBF_CHAR",
DbFieldType::Long => "DBF_LONG",
DbFieldType::Double => "DBF_DOUBLE",
DbFieldType::Int64 => "DBF_INT64",
DbFieldType::UInt64 => "DBF_UINT64",
DbFieldType::UShort => "DBF_USHORT",
DbFieldType::ULong => "DBF_ULONG",
}
}
fn dbr_name(t: DbFieldType) -> &'static str {
match t {
DbFieldType::String => "DBR_STRING",
DbFieldType::Short => "DBR_SHORT",
DbFieldType::Float => "DBR_FLOAT",
DbFieldType::Enum => "DBR_ENUM",
DbFieldType::Char => "DBR_CHAR",
DbFieldType::Long => "DBR_LONG",
DbFieldType::Double => "DBR_DOUBLE",
DbFieldType::Int64 => "DBR_DOUBLE", DbFieldType::UInt64 => "DBR_DOUBLE", DbFieldType::UShort => "DBR_LONG", DbFieldType::ULong => "DBR_DOUBLE", }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stat_level_parses_like_sscanf_u() {
assert_eq!(parse_stat_level("0"), 0);
assert_eq!(parse_stat_level("1"), 1);
assert_eq!(parse_stat_level("10"), 10);
assert_eq!(parse_stat_level("3abc"), 3);
assert_eq!(parse_stat_level("abc"), 0);
assert_eq!(parse_stat_level(""), 0);
}
#[test]
fn stat_level_accepts_signed_unsigned_prefix() {
assert_eq!(parse_stat_level("-1"), 4_294_967_295);
assert_eq!(parse_stat_level("+3abc"), 3);
assert_eq!(parse_stat_level(" -5"), 5u32.wrapping_neg());
assert_eq!(parse_stat_level("+7"), 7);
assert_eq!(parse_stat_level("-"), 0);
assert_eq!(parse_stat_level("+"), 0);
assert_eq!(parse_stat_level("-0"), 0);
assert_eq!(parse_stat_level("99999999999"), 1_215_752_191);
}
#[test]
fn signed_stat_level_selects_status_mode() {
assert_ne!(parse_stat_level("-1"), 0, "-s -1 must enter status mode");
assert_ne!(
parse_stat_level("+3abc"),
0,
"-s +3abc must enter status mode"
);
}
}