diff --git a/Cargo.toml b/Cargo.toml index 69b98d0..0192e42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "yams" description = "Yet Another Modbus Simulator" -version = "0.7.0" +version = "0.8.0" authors = ["Justin Huang "] license = "MIT/Apache-2.0" readme = "README.md" diff --git a/README.md b/README.md index 2a6aacc..2ca98fc 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ simulator works before running the simulator, including: ## Quick Demo: -[DEMO](https://asciinema.org/a/453058) +[DEMO](https://asciinema.org/a/453058) (slightly outdated, but good enough as a demo :)) ## Configurable Items See [YAML based Configurations](yaml.based.configurations.md) @@ -45,7 +45,7 @@ See [YAML based Configurations](yaml.based.configurations.md) - [x] implement support for repeated request(s) - [x] implement support for delay before request(s) - [x] implement error handling -- [ ] implement one-shot mode without config files +- [x] implement one-shot mode without config files - [x] implement verbose mode to print out more detail/data - [x] implement request/response counts printout - [ ] implement all data support of all current functions, with tests diff --git a/src/client.rs b/src/client.rs index f4ddbf5..b84a2fd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -45,6 +45,9 @@ pub async fn start_modbus_client( println!("failed in reading request file {}", request_file.display()); } } + if let Some(r) = request.request { + rlist.push(r); + } let mut section_repeat_times = request.repeat_times.or_else(|| Some(1)).unwrap(); #[allow(unused_parens)] diff --git a/src/config.rs b/src/config.rs index a04fcd8..47d2d90 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,12 +6,8 @@ use std::{fs, net::SocketAddr, path::PathBuf}; use tokio_serial::{SerialPort, SerialStream}; #[derive(Parser, Debug)] -#[clap(version = "0.7", author = "Justin Huang ")] +#[clap(version = "0.8", author = "Justin Huang ")] pub struct Opts { - /// Sets the simulator type: Client or Server - #[clap(arg_enum, short, long, required_unless_present("config-file"))] - device_type: Option, - // TODO: complete the list of manual options /// Sets the configuration file to load #[clap(short, long, required_unless_present_all(&["device-type"]))] config_file: Option, @@ -19,8 +15,85 @@ pub struct Opts { /// Verbose mode #[clap(short, long)] pub verbose_mode: bool, + + /// the modbus protocol type + #[clap(arg_enum, short('p'), long, required_unless_present("config-file"))] + pub protocol_type: Option, + /// the device type (only 'client' is supported in one-shot mode) + #[clap(arg_enum, short('t'), long, required_unless_present("config-file"))] + pub device_type: Option, + /// the id of the client/server + #[clap(short('i'), long, required_unless_present("config-file"))] + pub device_id: Option, + /// the socket address when using Modbus TCP + #[clap(short('a'), long, required_if_eq("protocol-type", "tcp"))] + pub ip_address: Option, + /// the serial port when using Modbus RTU + #[clap(short('s'), long, required_if_eq("protocol-type", "rtu"))] + pub serial_port: Option, + /// the baudrate when using Modbus RTU + #[clap(short('b'), long, required_if_eq("protocol-type", "rtu"))] + pub serial_baudrate: Option, + /// the parity of the serial port + #[clap(arg_enum, short('r'), long, required_if_eq("protocol-type", "rtu"))] + pub serial_parity: Option, + /// the stop bits of the serial port + #[clap(arg_enum, short('o'), long, required_if_eq("protocol-type", "rtu"))] + pub serial_stop_bits: Option, + /// the data bits of the serial port + #[clap(arg_enum, short('d'), long, required_if_eq("protocol-type", "rtu"))] + pub serial_data_bits: Option, + /// the function code to use in one-shot mode + #[clap(arg_enum, short('f'), long, required_unless_present("config-file"))] + pub function_code: Option, + /// the start register address to use in one-shot mode + #[clap(short('e'), long, required_if_eq_any(&[("function-code", "write-single-register"), + ("function-code", "write-multiple-registers"), + ("function-code", "write-single-coil"), + ("function-code", "write-multiple-coils"), + ("function-code", "read-coils"), + ("function-code", "read-discrete-inputs"), + ("function-code", "read-holding-registers"), + ("function-code", "read-input-registers"), + ("function-code", "write-multiple-coils")]))] + pub start_address: Option, + /// the quantity-of-registers-to-access to use in one-shot mode + #[clap(short('q'), long, required_if_eq_any(&[("function-code", "write-single-register"), + ("function-code", "write-multiple-registers"), + ("function-code", "write-single-coil"), + ("function-code", "write-multiple-coils"), + ("function-code", "read-coils"), + ("function-code", "read-discrete-inputs"), + ("function-code", "read-holding-registers"), + ("function-code", "read-input-registers"), + ("function-code", "write-multiple-coils")]))] + pub quantity: Option, + /// the new values to set in one-shot mode + #[clap(short('n'), long, required_if_eq_any(&[("function-code", "write-single-register"), + ("function-code", "write-multiple-registers"), + ("function-code", "write-single-coil"), + ("function-code", "write-multiple-coils")]))] + pub new_values: Option>, + /// the times to repeat the request in one-shot mode + #[clap(short('g'), long, required_unless_present("config-file"))] + pub repeat_times: Option, + /// the time (number of 100ms) to delay the request in one-shot mode + #[clap(short('y'), long, required_unless_present("config-file"))] + pub delay: Option, + /// the data type used in one-shot mode + #[clap(arg_enum, short('j'), long, required_if_eq_any(&[("function-code", "write-single-register"), + ("function-code", "write-multiple-registers")]))] + pub data_type: Option, + /// the server id used in one-shot mode + #[clap(short('k'), long, required_if_eq("device-type", "client"))] + pub server_id: Option, + /// the server address used in one-shot mode + #[clap(short('l'), long, required_if_eq_any(&[("device-type", "client"), + ("protocol-type", "tcp")]))] + pub server_address: Option, } + #[derive(Debug, Deserialize)] pub struct ModbusRequest { /// description of the request @@ -75,6 +148,8 @@ pub struct ModbusClientRequest { pub repeat_times: Option, /// request files pub request_files: Vec, + /// a single request + pub request: Option, } #[derive(Debug, Deserialize)] @@ -115,14 +190,53 @@ fn parse_config_str(config_str: &str) -> anyhow::Result { serde_yaml::from_str(&config_str).with_context(|| format!("failed to parse the config string")) } -pub fn configure(opts: &Opts) -> anyhow::Result { +pub fn configure(opts: &mut Opts) -> anyhow::Result { if let Some(config_file) = &opts.config_file { match fs::read_to_string(config_file) { Ok(config_str) => parse_config_str(&config_str), Err(e) => Err(e.into()), } } else { - todo!() + if opts.device_type == Some(DeviceType::Client) { + Ok(ModbusDeviceConfig { + common: ModbusCommonConfig { + protocol_type: opts.protocol_type.unwrap(), + device_type: DeviceType::Client, + device_id: opts.device_id.unwrap(), + ip_address: opts.ip_address, + serial_baudrate: opts.serial_baudrate, + serial_data_bits: opts.serial_data_bits, + serial_stop_bits: opts.serial_stop_bits, + serial_parity: opts.serial_parity, + serial_port: opts.serial_port.take(), + }, + server: None, + client: Some(ModbusClientConfig { + requests: vec![ ModbusClientRequest { + server_id: opts.server_id, + server_address: opts.server_address, + repeat_times: None, + request_files: vec![], + request: Some(ModbusRequest { + description: "".to_string(), + function_code: opts.function_code.unwrap(), + access_start_address: opts.start_address.unwrap(), + access_quantity: opts.quantity.unwrap(), + new_values: opts.new_values.take(), + repeat_times: opts.repeat_times, + delay: opts.delay, + data_type: opts.data_type, + }), + }], + register_data: None, + }), + verbose_mode: opts.verbose_mode, + }) + } else { + println!("server is not supported in one-shot mode"); + std::process::exit(1); + } + } } diff --git a/src/main.rs b/src/main.rs index edd245f..d7dd507 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,18 +11,22 @@ use crate::{client::*, config::*, server::*, types::*}; #[tokio::main] pub async fn main() -> Result<(), Box> { - let opts = Opts::parse(); - let mut config = configure(&opts).expect("failed to load the configuraion"); - - config.verbose_mode = opts.verbose_mode; - if config.common.device_type == DeviceType::Server { - if let Err(e) = start_modbus_server(config).await { - println!("exit with error: {}", e); - } - } else { - if let Err(e) = start_modbus_client(config).await { - println!("exit with error: {}", e); - } + let mut opts = Opts::parse(); + match configure(&mut opts) { + Ok(mut config) => { + config.verbose_mode = opts.verbose_mode; + if config.common.device_type == DeviceType::Server { + if let Err(e) = start_modbus_server(config).await { + println!("exit with error: {}", e); + } + } else { + if let Err(e) = start_modbus_client(config).await { + println!("exit with error: {}", e); + } + } + }, + Err(e) => println!("failed with error: {}", e), } + Ok(()) } diff --git a/src/types.rs b/src/types.rs index f1583d0..1619770 100644 --- a/src/types.rs +++ b/src/types.rs @@ -9,13 +9,13 @@ pub enum DeviceType { Server, } -#[derive(ArgEnum, Clone, PartialEq, Debug, Deserialize)] +#[derive(ArgEnum, Clone, Copy, PartialEq, Debug, Deserialize)] pub enum ProtocolType { RTU, TCP, } -#[derive(ArgEnum, Clone, PartialEq, Debug, Deserialize)] +#[derive(ArgEnum, Clone, Copy, PartialEq, Debug, Deserialize)] pub enum DataType { Float32, Float64, diff --git a/test/modbus.tcp.client.normal.config.yaml b/test/modbus.tcp.client.normal.config.yaml index 89e83b7..405918b 100644 --- a/test/modbus.tcp.client.normal.config.yaml +++ b/test/modbus.tcp.client.normal.config.yaml @@ -5,16 +5,16 @@ device_id: 1 client: requests: [ - #{ - # server_id: 3, - # server_address: 127.0.0.1:5502, - # repeat_times: 2, - # request_files: [ - # test/requests/read.multiple.request.yaml, - # test/requests/read.pi.request.yaml, - # test/requests/read.golden.ratio.request.yaml, - # ] - #}, + { + server_id: 3, + server_address: 127.0.0.1:5502, + repeat_times: 2, + request_files: [ + test/requests/read.multiple.request.yaml, + test/requests/read.pi.request.yaml, + test/requests/read.golden.ratio.request.yaml, + ] + }, { server_id: 2, server_address: 127.0.0.1:5503, @@ -23,16 +23,16 @@ test/requests/write.device.id.request.yaml, test/requests/read.device.id.request.yaml, test/requests/read.flowrate.request.yaml, - #test/requests/read.resettable.totalizer.request.yaml, - #test/requests/read.serial.number.request.yaml, - #test/requests/read.led.power.state.coil.request.yaml, - #test/requests/read.serial.number.lowest.4.bits.coil.request.yaml, + test/requests/read.resettable.totalizer.request.yaml, + test/requests/read.serial.number.request.yaml, + test/requests/read.led.power.state.coil.request.yaml, + test/requests/read.serial.number.lowest.4.bits.coil.request.yaml, test/requests/write.flowrate.request.yaml, - #test/requests/write.led.power.state.coil.request.yaml, - #test/requests/write.serial.number.lowest.4.bits.coil.request.yaml, + test/requests/write.led.power.state.coil.request.yaml, + test/requests/write.serial.number.lowest.4.bits.coil.request.yaml, test/requests/read.flowrate.request.yaml, - #test/requests/read.serial.number.request.yaml, - #test/requests/read.led.power.state.coil.request.yaml, + test/requests/read.serial.number.request.yaml, + test/requests/read.led.power.state.coil.request.yaml, ] }, ]