1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
use io::Write;
use std::fmt;
use std::io;
use thiserror::Error;

mod enttec;
mod offline;

pub use enttec::EnttecDmxPort;
pub use offline::OfflineDmxPort;

/// Trait for the general notion of a DMX port.
/// This enables creation of an "offline" port to slot into place if an API requires an output.
#[typetag::serde(tag = "type")]
pub trait DmxPort: fmt::Display {
    /// Return the available ports.  The ports will need to be opened before use.
    fn available_ports() -> anyhow::Result<PortListing>
    where
        Self: Sized;

    /// Open the port for writing.  Implementations should no-op if this is
    /// called twice rather than returning an error.  Primarily used to re-open
    /// a port that has be deserialized.
    fn open(&mut self) -> Result<(), OpenError>;

    /// Close the port.
    fn close(&mut self);

    /// Write a DMX frame out to the port.  If the frame is smaller than the minimum universe size,
    /// it will be padded with zeros.  If the frame is larger than the maximum universe size, the
    /// values beyond the max size will be ignored.
    fn write(&mut self, frame: &[u8]) -> Result<(), WriteError>;
}

/// A listing of available ports.
type PortListing = Vec<Box<dyn DmxPort>>;

/// Gather up all of the providers and use them to get listings of all ports they have available.
/// Return them as a vector of names plus opener functions.
/// This function does not check whether or not any of the ports are in use already.
pub fn available_ports() -> anyhow::Result<PortListing> {
    let mut ports = Vec::new();
    ports.extend(OfflineDmxPort::available_ports()?);
    ports.extend(EnttecDmxPort::available_ports()?);
    Ok(ports)
}

/// Prompt the user to select a port via the command prompt.
pub fn select_port() -> anyhow::Result<Box<dyn DmxPort>> {
    let mut ports = available_ports()?;
    println!("Available DMX ports:");
    for (i, port) in ports.iter().enumerate() {
        println!("{}: {}", i, port);
    }
    let mut port = loop {
        print!("Select a port: ");
        io::stdout().flush()?;
        let input = read_string()?;
        let index = match input.trim().parse::<usize>() {
            Ok(num) => num,
            Err(e) => {
                println!("{}; please enter an integer.", e);
                continue;
            }
        };
        if index >= ports.len() {
            println!("Please enter a value less than {}.", ports.len());
            continue;
        }
        break ports.swap_remove(index);
    };
    port.open()?;
    Ok(port)
}

/// Read a line of input from stdin.
fn read_string() -> Result<String, io::Error> {
    let mut line = String::new();
    io::stdin().read_line(&mut line)?;
    Ok(line.trim().to_string())
}

#[derive(Error, Debug)]
pub enum OpenError {
    #[error("the DMX port is not connected")]
    NotConnected,
    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

#[derive(Error, Debug)]
pub enum WriteError {
    #[error("the DMX port is not connected")]
    Disconnected,
    #[error(transparent)]
    Other(#[from] anyhow::Error),
}