ledcat 0.2.0

Control lots of LED's over lots of protocols
use crate::device::*;
use std::collections;
use std::io::{self, Write};
use std::net;
use std::str::FromStr;
use std::sync;
use std::thread;
use std::time;

mod target;
mod unicast;
use self::target::*;
use self::unicast::*;

pub fn command<'a, 'b>() -> clap::App<'a, 'b> {
    clap::SubCommand::with_name("artnet")
        .about("Control artnet DMX nodes via unicast and broadcast")
        .arg(
            clap::Arg::with_name("target")
                .short("t")
                .long("target")
                .takes_value(true)
                .min_values(1)
                .multiple(true)
                .validator(|addr| match net::IpAddr::from_str(addr.as_str()) {
                    Ok(_) => Ok(()),
                    Err(err) => Err(format!("{} ({})", err, addr)),
                })
                .conflicts_with_all(&["discover", "target-list", "broadcast"])
                .help("One or more target IP addresses"),
        )
        .arg(
            clap::Arg::with_name("target-list")
                .long("target-list")
                .takes_value(true)
                .conflicts_with_all(&["target", "discover", "broadcast"])
                .help(
                    "Specify a file containing 1 IP address per line to unicast to. \
                     Changes to the file are read automatically",
                ),
        )
        .arg(
            clap::Arg::with_name("broadcast")
                .short("b")
                .long("broadcast")
                .conflicts_with_all(&["target", "target-list", "discover"])
                .help("Broadcast to all devices in the network"),
        )
        .arg(
            clap::Arg::with_name("discover")
                .short("d")
                .long("discover")
                .conflicts_with_all(&["target", "target-list", "broadcast"])
                .help("Discover artnet nodes"),
        )
        .arg(
            clap::Arg::with_name("universe")
                .short("u")
                .long("universe")
                .validator(regex_validator!(r"^\d+$"))
                .default_value("0")
                .help("Discover artnet nodes"),
        )
}

pub fn from_command(args: &clap::ArgMatches, gargs: &GlobalArgs) -> io::Result<FromCommand> {
    if args.is_present("discover") {
        if let Err(err) = artnet_discover() {
            eprintln!("{}", err);
        }
        return Ok(FromCommand::SubcommandHandled);
    }

    let dev = Box::new(generic::Generic {
        format: generic::Format::RGB24,
    });
    let artnet_target: Box<dyn Target> = if args.is_present("broadcast") {
        Box::new(Broadcast {})
    } else if let Some(list_path) = args.value_of("target-list") {
        Box::new(ListFile::new(list_path))
    } else if args.is_present("target") {
        let addresses: Vec<_> = args
            .values_of("target")
            .unwrap()
            .map(|addr| net::SocketAddr::new(addr.parse().unwrap(), PORT))
            .collect();
        Box::new(addresses)
    } else {
        eprintln!("Missing artnet target. Please set --target IP or --broadcast");
        return Ok(FromCommand::SubcommandHandled);
    };
    let universe = args.value_of("universe").unwrap().parse().unwrap();

    let output = Unicast::to(artnet_target, gargs.dimensions()?.size() * 3, universe)?;
    Ok(FromCommand::Output(Box::new((dev, output))))
}

fn artnet_discover() -> io::Result<()> {
    let discovery_stream = unicast::discover();
    let mut discovered: collections::HashSet<net::SocketAddr> = collections::HashSet::new();

    let (close_tx, close_rx) = sync::mpsc::sync_channel(0);
    thread::spawn(move || {
        let mut out = io::stderr();
        for ch in ['|', '/', '-', '\\'].iter().cycle() {
            if close_rx.try_recv().is_ok() {
                break;
            }
            write!(&mut out, "\r{}", ch).unwrap();
            out.flush().unwrap();
            thread::sleep(time::Duration::from_millis(100));
        }
    });

    for result in discovery_stream {
        let node = match result {
            Ok(node) => node,
            Err(err) => {
                close_tx.send(()).unwrap();
                eprint!("\r");
                return Err(err);
            }
        };
        if !discovered.contains(&node.0) {
            let ip_str = format!("{}", node.0.ip()); // Padding only works with strings. :(
            match node.1 {
                Some(name) => eprintln!("\r{: <15} -> {}", ip_str, name),
                None => eprintln!("\r{: <15}", ip_str),
            };
        }
        discovered.insert(node.0);
    }
    Ok(())
}