bacnet-cli 0.13.0

BACnet command-line tool for device discovery, reading, and writing
use super::*;

/// Handle register in BIP shell with auto-renewal via session.
pub(super) async fn handle_bip_register(
    client: &std::sync::Arc<BACnetClient<BipTransport>>,
    args: &[String],
    session: &mut Session,
    format: OutputFormat,
) {
    if args.is_empty() {
        output::print_error("Usage: register <bbmd-address> [--ttl N]");
        return;
    }

    let mac = match resolve_target_mac(client, &args[0]).await {
        Ok(m) => m,
        Err(e) => {
            output::print_error(&e);
            return;
        }
    };

    let mut ttl: u16 = 300;
    let mut i = 1;
    while i < args.len() {
        if args[i] == "--ttl" {
            if i + 1 < args.len() {
                match args[i + 1].parse::<u16>() {
                    Ok(t) => ttl = t,
                    Err(_) => {
                        output::print_error("--ttl requires a numeric value");
                        return;
                    }
                }
                i += 2;
                continue;
            } else {
                output::print_error("--ttl requires a value");
                return;
            }
        }
        i += 1;
    }

    if let Err(e) = commands::router::register_cmd(client, &mac, ttl, format).await {
        output::print_error(&e.to_string());
        return;
    }

    // Set up auto-renewal in the session.
    let bbmd_display = args[0].clone();
    let renewal_mac = mac.clone();
    let renewal_client = std::sync::Arc::clone(client);
    session.set_bbmd_registration(mac, bbmd_display, ttl, move || {
        let client = std::sync::Arc::clone(&renewal_client);
        let mac = renewal_mac.clone();
        Box::pin(async move {
            client
                .register_foreign_device_bvlc(&mac, ttl)
                .await
                .map(|_| ())
                .map_err(|e| e.to_string())
        })
    });
}

pub(super) async fn handle_unregister(
    client: &BACnetClient<BipTransport>,
    args: &[String],
    format: OutputFormat,
) {
    if args.is_empty() {
        output::print_error("Usage: unregister <bbmd-address>");
        return;
    }

    let mac = match resolve_target_mac(client, &args[0]).await {
        Ok(m) => m,
        Err(e) => {
            output::print_error(&e);
            return;
        }
    };

    if let Err(e) = commands::router::unregister_cmd(client, &mac, format).await {
        output::print_error(&e.to_string());
    }
}

pub(super) async fn handle_bdt(
    client: &BACnetClient<BipTransport>,
    args: &[String],
    format: OutputFormat,
) {
    if args.is_empty() {
        output::print_error("Usage: bdt <bbmd-address>");
        return;
    }

    let mac = match resolve_target_mac(client, &args[0]).await {
        Ok(m) => m,
        Err(e) => {
            output::print_error(&e);
            return;
        }
    };

    if let Err(e) = commands::router::bdt_cmd(client, &mac, format).await {
        output::print_error(&e.to_string());
    }
}

pub(super) async fn handle_fdt(
    client: &BACnetClient<BipTransport>,
    args: &[String],
    format: OutputFormat,
) {
    if args.is_empty() {
        output::print_error("Usage: fdt <bbmd-address>");
        return;
    }

    let mac = match resolve_target_mac(client, &args[0]).await {
        Ok(m) => m,
        Err(e) => {
            output::print_error(&e);
            return;
        }
    };

    if let Err(e) = commands::router::fdt_cmd(client, &mac, format).await {
        output::print_error(&e.to_string());
    }
}

/// BIP-specific discover handler that supports --bbmd for foreign device registration.
pub(super) async fn handle_bip_discover(
    client: &std::sync::Arc<BACnetClient<BipTransport>>,
    args: &[String],
    format: OutputFormat,
) {
    let mut range = None;
    let mut wait_secs = 3;
    let mut target: Option<String> = None;
    let mut dnet: Option<u16> = None;
    let mut bbmd: Option<String> = None;
    let mut ttl: u16 = 300;

    let mut i = 0;
    while i < args.len() {
        match args[i].as_str() {
            "--wait" => {
                if i + 1 < args.len() {
                    match args[i + 1].parse::<u64>() {
                        Ok(w) => wait_secs = w,
                        Err(_) => {
                            output::print_error("--wait requires a numeric value");
                            return;
                        }
                    }
                    i += 2;
                    continue;
                } else {
                    output::print_error("--wait requires a value");
                    return;
                }
            }
            "--target" => {
                if i + 1 < args.len() {
                    target = Some(args[i + 1].clone());
                    i += 2;
                    continue;
                } else {
                    output::print_error("--target requires an address");
                    return;
                }
            }
            "--dnet" => {
                if i + 1 < args.len() {
                    match args[i + 1].parse::<u16>() {
                        Ok(n) => dnet = Some(n),
                        Err(_) => {
                            output::print_error("--dnet requires a network number");
                            return;
                        }
                    }
                    i += 2;
                    continue;
                } else {
                    output::print_error("--dnet requires a value");
                    return;
                }
            }
            "--bbmd" => {
                if i + 1 < args.len() {
                    bbmd = Some(args[i + 1].clone());
                    i += 2;
                    continue;
                } else {
                    output::print_error("--bbmd requires an address");
                    return;
                }
            }
            "--ttl" => {
                if i + 1 < args.len() {
                    match args[i + 1].parse::<u16>() {
                        Ok(t) => ttl = t,
                        Err(_) => {
                            output::print_error("--ttl requires a numeric value");
                            return;
                        }
                    }
                    i += 2;
                    continue;
                } else {
                    output::print_error("--ttl requires a value");
                    return;
                }
            }
            s if s.starts_with("--") => {
                output::print_error(&format!("unknown option: '{s}'"));
                return;
            }
            _ => {
                if args[i].contains('-') {
                    match crate::core::range::parse_discover_range(Some(&args[i])) {
                        Ok(parsed) => range = parsed,
                        Err(e) => {
                            output::print_error(&e.to_string());
                            return;
                        }
                    }
                } else {
                    output::print_error(&format!(
                        "unexpected argument: '{}'. Use 'discover [low-high] [--wait N] [--target ADDR] [--dnet N] [--bbmd ADDR] [--ttl N]'",
                        args[i]
                    ));
                    return;
                }
            }
        }
        i += 1;
    }

    if let Some(bbmd_addr) = &bbmd {
        let bbmd_mac = match resolve::address_mac(
            client.transport(),
            bbmd_addr,
            "--bbmd requires an IP address, not a device instance",
        )
        .await
        {
            Ok(m) => m,
            Err(e) => {
                output::print_error(&e);
                return;
            }
        };
        match client.register_foreign_device_bvlc(&bbmd_mac, ttl).await {
            Ok(result) => {
                eprintln!(
                    "{}",
                    format!("Registered as foreign device with BBMD: {result:?}").green()
                );
            }
            Err(e) => {
                output::print_error(&format!("BBMD registration failed: {e}"));
                return;
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    let result = if let Some(target_str) = &target {
        match resolve::address_mac(
            client.transport(),
            target_str,
            resolve::TARGET_NOT_AN_ADDRESS,
        )
        .await
        {
            Ok(mac) => {
                commands::discover::discover_directed(client, &mac, range, wait_secs, format).await
            }
            Err(e) => {
                output::print_error(&e);
                return;
            }
        }
    } else if let Some(network) = dnet {
        commands::discover::discover_network(client, network, range, wait_secs, format).await
    } else {
        commands::discover::discover(client, range, wait_secs, format).await
    };

    if let Err(e) = result {
        output::print_error(&e.to_string());
    }
}

/// Report how a `--foreign-bbmd` registration went once the BBMD answers
/// the first attempt (#938). The transport keeps trying either way.
pub(super) async fn report_foreign_start(
    client: &BACnetClient<BipTransport>,
    bbmd: std::net::SocketAddrV4,
) {
    match crate::foreign::await_registration(client, bbmd).await {
        Ok(()) => eprintln!(
            "{}",
            format!(
                "BBMD {bbmd} accepted the foreign-device registration; it renews automatically"
            )
            .green()
        ),
        Err(e) => output::print_error(&format!("{e}; retrying in the background")),
    }
}

/// The `--foreign-bbmd` registration, for `status`.
pub(super) fn print_foreign_status(
    client: &BACnetClient<BipTransport>,
    bbmd: std::net::SocketAddrV4,
) {
    let Ok(registration) = client.foreign_registration() else {
        return;
    };
    let state = match registration.lease_remaining {
        Some(left) if registration.registered => {
            format!("(registered, {}s of the lease left)", left.as_secs())
        }
        _ => format!(
            "(not registered; last attempt {:?})",
            registration.last_outcome
        ),
    };
    println!(
        "{} {} {}",
        "Foreign device of BBMD:".dimmed(),
        bbmd.to_string().cyan(),
        state.dimmed()
    );
}