Skip to main content

twinleaf_tools/cli/
proxy.rs

1use clap::{Parser, Subcommand, ValueHint};
2use twinleaf::device::DeviceRoute;
3
4use crate::{parse_device_route, TioOpts};
5
6#[derive(Parser, Debug)]
7#[command(
8    version,
9    about = "Multiplexes access to a sensor, exposing the functionality of tio::proxy via TCP",
10    args_conflicts_with_subcommands = true
11)]
12pub struct ProxyCli {
13    #[command(subcommand)]
14    pub subcommands: Option<ProxySubcommands>,
15
16    /// Sensor URL (e.g., tcp://localhost, serial:///dev/ttyUSB0); defaults to auto-detecting a single connected device
17    #[arg(value_hint = ValueHint::Url, conflicts_with = "mounts")]
18    pub(crate) sensor_url: Option<String>,
19
20    /// Mount a sensor at a route prefix to multiplex multiple devices (repeatable)
21    #[arg(long = "mount", value_name = "LOCATOR=/N", value_parser = parse_mount)]
22    pub(crate) mounts: Vec<MountArg>,
23
24    /// TCP port to listen on for clients
25    #[arg(short = 'p', long = "port", default_value = "7855")]
26    pub(crate) port: u16,
27
28    /// Kick off slow clients instead of dropping traffic
29    #[arg(short = 'k', long)]
30    pub(crate) kick_slow: bool,
31
32    /// Sensor subtree to look at
33    #[arg(
34        short = 's',
35        long = "subtree",
36        default_value = "/",
37        value_parser = parse_device_route,
38        conflicts_with = "mounts",
39    )]
40    pub(crate) subtree: DeviceRoute,
41
42    /// Verbose output
43    #[arg(short = 'v', long)]
44    pub(crate) verbose: bool,
45
46    /// Debugging output
47    #[arg(short = 'd', long)]
48    pub(crate) debug: bool,
49
50    /// Deprecated; timestamps are now emitted by the logger (set RUST_LOG to control verbosity)
51    #[arg(
52        short = 't',
53        long = "timestamp",
54        default_value = "%T%.3f ",
55        hide = true
56    )]
57    pub(crate) timestamp_format: String,
58
59    /// Time limit for sensor reconnection attempts (seconds)
60    #[arg(short = 'T', long = "timeout", default_value = "30")]
61    pub(crate) reconnect_timeout: u64,
62
63    /// Dump packet traffic except sample data/metadata or heartbeats
64    #[arg(long)]
65    pub(crate) dump: bool,
66
67    /// Dump sample data traffic
68    #[arg(long)]
69    pub(crate) dump_data: bool,
70
71    /// Dump sample metadata traffic
72    #[arg(long)]
73    pub(crate) dump_meta: bool,
74
75    /// Dump heartbeat traffic
76    #[arg(long)]
77    pub(crate) dump_hb: bool,
78
79    /// Deprecated; running without -s <url> now auto-detects by default.
80    #[arg(short = 'a', long = "auto", hide = true)]
81    pub(crate) auto: bool,
82
83    /// Deprecated; use `tio list` instead.
84    #[arg(short = 'e', long = "enumerate", name = "enum", hide = true)]
85    pub(crate) enumerate: bool,
86}
87
88/// A `--mount LOCATOR=/N` argument: a sensor URL bound to a route prefix.
89#[derive(Debug, Clone)]
90pub struct MountArg {
91    pub locator: String,
92    pub prefix: DeviceRoute,
93}
94
95fn parse_mount(s: &str) -> Result<MountArg, String> {
96    let Some((locator, prefix_str)) = s.split_once('=') else {
97        return Err(format!(
98            "expected LOCATOR=/N (e.g. serial:///dev/ttyUSB0=/1), got {s:?}"
99        ));
100    };
101    if locator.is_empty() {
102        return Err(format!("missing sensor locator before '=' in {s:?}"));
103    }
104    let prefix = DeviceRoute::from_str(prefix_str)
105        .map_err(|_| format!("invalid route prefix: {prefix_str:?}"))?;
106    if prefix.len() != 1 {
107        return Err(format!(
108            "mount prefix must be a single segment like /1, got {prefix_str:?}"
109        ));
110    }
111    Ok(MountArg {
112        locator: locator.to_string(),
113        prefix,
114    })
115}
116
117#[derive(Subcommand, Debug)]
118pub enum ProxySubcommands {
119    /// Bridge Twinleaf sensor data to NMEA TCP stream
120    Nmea {
121        #[command(flatten)]
122        tio: TioOpts,
123
124        /// TCP port to listen on
125        #[arg(short = 'p', long = "port", default_value = "7800")]
126        tcp_port: u16,
127    },
128}