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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
use anyhow::Result;
use clap::{Parser, Subcommand};
use clap_verbosity_flag::{InfoLevel, Verbosity};
use r413d08_lib::protocol as proto;
use std::time::Duration;
fn default_device_name() -> String {
if cfg!(target_os = "windows") {
String::from("COM1")
} else {
String::from("/dev/ttyUSB0")
}
}
fn parse_relay(s: &str) -> Result<proto::Port, String> {
proto::Port::try_from(clap_num::maybe_hex::<u8>(s)?).map_err(|e| format!("{e}"))
}
fn parse_address(s: &str) -> Result<proto::Address, String> {
proto::Address::try_from(clap_num::maybe_hex::<u8>(s)?).map_err(|e| format!("{e}"))
}
/// Defines the connection type and parameters (Modbus TCP or RTU).
#[derive(Subcommand, Debug, Clone, PartialEq)]
pub enum CliConnection {
/// Connect via Modbus/TCP.
Tcp {
/// TCP address (e.g. 192.168.0.222:502)
address: String,
#[command(subcommand)]
command: CliCommands,
},
/// Connect via Modbus/RTU (Serial).
Rtu {
/// The serial device path.
#[arg(short, long, default_value_t = default_device_name())]
device: String,
/// RS485 address from 1 to 247
#[arg(short, long, default_value_t = proto::Address::default(), value_parser = parse_address)]
address: proto::Address,
#[command(subcommand)]
command: CliCommands,
},
}
#[derive(Subcommand, Debug, Clone, PartialEq)]
pub enum CliCommands {
/// Read and display the current state (ON/OFF) of all 8 relays.
Status,
/// Turn a specific relay ON (Close circuit).
On {
/// Relay number 0 to 7
#[arg(value_parser = parse_relay)]
relay: proto::Port,
},
/// Turn a specific relay OFF (Open circuit).
Off {
/// Relay number 0 to 7
#[arg(value_parser = parse_relay)]
relay: proto::Port,
},
/// Toggle the state of a specific relay (ON->OFF, OFF->ON).
Toggle {
/// Relay number 0 to 7
#[arg(value_parser = parse_relay)]
relay: proto::Port,
},
/// Turn all 8 relays ON simultaneously.
AllOn,
/// Turn all 8 relays OFF simultaneously.
AllOff,
/// Latch a relay ON and turn all other relays OFF (Inter-locking).
Latch {
/// Relay number 0 to 7
#[arg(value_parser = parse_relay)]
relay: proto::Port,
},
/// Turn a relay ON momentarily (~1 second), then automatically OFF (Non-locking).
Momentary {
/// Relay number 0 to 7
#[arg(value_parser = parse_relay)]
relay: proto::Port,
},
/// Turn a relay ON, then automatically OFF after a specified delay.
Delay {
/// Relay number 0 to 7
#[arg(value_parser = parse_relay)]
relay: proto::Port,
/// Delay duration in seconds (0-255).
delay: u8,
},
/// Query the device's current Modbus address.
/// IMPORTANT: Ensure only ONE device is connected to the bus!
QueryAddress,
/// Set a new Modbus address for the device.
/// The new address must be unique on the bus. Requires addressing the device with its CURRENT address.
SetAddress {
/// The new Modbus address (1-247) or hex (0x01-0xF7).
#[arg(value_parser = parse_address)]
address: proto::Address,
},
}
const fn about_text() -> &'static str {
"A command-line tool to control R413D08 8-channel relay modules via Modbus TCP or RTU."
}
#[derive(Parser, Debug)]
#[command(version, about=about_text(), long_about = None)]
pub struct CliArgs {
/// Verbosity level (-v, -vv, -vvv).
#[command(flatten)]
pub verbose: Verbosity<InfoLevel>,
/// Connection type (TCP or RTU) and associated command.
#[command(subcommand)]
pub connection: CliConnection,
/// Modbus I/O timeout duration (e.g., "200ms", "1s").
#[arg(value_parser = humantime::parse_duration, long, default_value = "200ms")]
pub timeout: Duration,
}