Skip to content

Commit

Permalink
Merge pull request #8 from justinyhuang/server.data.update.triggered.…
Browse files Browse the repository at this point in the history
…by.register.writes

added support for user defined server behavior
  • Loading branch information
justinyhuang authored Jan 26, 2022
2 parents 3e75420 + 5607c22 commit 9573416
Show file tree
Hide file tree
Showing 19 changed files with 425 additions and 206 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "yams"
description = "Yet Another Modbus Simulator"
version = "0.8.0"
version = "0.9.0"
authors = ["Justin Huang <[email protected]>"]
license = "MIT/Apache-2.0"
readme = "README.md"
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ simulator works before running the simulator, including:
- supported function codes
- supported registers/coils and their values
- overlapping of coil and register is supported
- support user defined server behavior triggered by register/coil writes:
With this feature enabled: YAMS will share the register/coil data with a user program via data (yaml) files.
Each time a register/coil is updated, the user specified program will be invoked to update the register/coil data
based on user-defined logic, as long as the external program writes the new data back to the files, YAMS will
pick up the new data and continue running.
- Modbus client behaviors:
- requests to send to server(s)
- support repeated request (single/multi request repeat)
Expand All @@ -35,7 +40,7 @@ To support quick testing, Oneshot mode is provided so that one can run YAMS with

## Quick Demo:

[DEMO](https://asciinema.org/a/453058) (slightly outdated, but good enough as a demo :))
[DEMO](https://asciinema.org/a/464218)

## Configurable Items
See [YAML based Configurations](yaml.based.configurations.md)
Expand All @@ -51,7 +56,7 @@ See [YAML based Configurations](yaml.based.configurations.md)
- [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 support for server behavior triggered by register/coil writes, defined by user
- [x] implement support for server behavior triggered by register/coil writes, defined by user
- [ ] implement all data support of all current functions, with tests
- implement function code support below:
- [x] Read Coils
Expand Down
1 change: 1 addition & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
chain_width=40
104 changes: 69 additions & 35 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ pub async fn start_modbus_client(
.requests;
let mut counter: u16 = 0;
for mut request in client_requests {
let server_id = request.server_id.take().expect("server id missing");
let server_id = request
.server_id
.take()
.expect("server id missing");
let server = Slave(server_id);
let mut ctx = match config.common.protocol_type {
ProtocolType::TCP => {
Expand Down Expand Up @@ -49,7 +52,10 @@ pub async fn start_modbus_client(
rlist.push(r);
}

let mut section_repeat_times = request.repeat_times.or_else(|| Some(1)).unwrap();
let mut section_repeat_times = request
.repeat_times
.or_else(|| Some(1))
.unwrap();
#[allow(unused_parens)]
let section_indefinite_loop = (section_repeat_times == REPEAT_TIME_INDEFINITE);
while section_repeat_times > 0 {
Expand All @@ -59,7 +65,10 @@ pub async fn start_modbus_client(
for r in &mut rlist {
let start_addr = r.access_start_address;
let count = r.access_quantity;
let mut file_repeat_times = r.repeat_times.or_else(|| Some(1)).unwrap();
let mut file_repeat_times = r
.repeat_times
.or_else(|| Some(1))
.unwrap();
#[allow(unused_parens)]
let file_indefinite_loop = (file_repeat_times == REPEAT_TIME_INDEFINITE);
let delay_in_100ms = r.delay.or_else(|| Some(0)).unwrap();
Expand All @@ -83,7 +92,8 @@ pub async fn start_modbus_client(
config.verbose_mode,
);
ModbusRequestReturnType::ResultWithU16Vec(
ctx.read_input_registers(start_addr, count).await,
ctx.read_input_registers(start_addr, count)
.await,
)
}
FunctionCode::ReadHoldingRegisters => {
Expand All @@ -95,19 +105,24 @@ pub async fn start_modbus_client(
config.verbose_mode,
);
ModbusRequestReturnType::ResultWithU16Vec(
ctx.read_holding_registers(start_addr, count).await,
ctx.read_holding_registers(start_addr, count)
.await,
)
}
FunctionCode::WriteMultipleRegisters => {
let new_values =
r.new_values.take().expect("missing value for write");
let new_values = r
.new_values
.take()
.expect("missing value for write");
let mut data = Vec::<u16>::new();
for v in new_values {
let d = ModbusRegisterData {
data_description: "".to_string(),
data_model_type: DataModelType::HoldingOrInputRegister,
data_access_type: None,
data_type: r.data_type.expect("missing data type for write"),
data_type: r
.data_type
.expect("missing data type for write"),
data_value: v,
};
d.write_into_be_u16(&mut data);
Expand All @@ -121,45 +136,52 @@ pub async fn start_modbus_client(
);
vprintln(&format!("{:?}", &data), config.verbose_mode);
ModbusRequestReturnType::ResultWithNothing(
ctx.write_multiple_registers(start_addr, &data).await,
ctx.write_multiple_registers(start_addr, &data)
.await,
)
}
FunctionCode::WriteSingleRegister => {
let new_values =
r.new_values.take().expect("missing value for write");
let new_values = r
.new_values
.take()
.expect("missing value for write");
let mut data = Vec::<u16>::new();
for v in new_values {
let d = ModbusRegisterData {
data_description: "".to_string(),
data_model_type: DataModelType::HoldingOrInputRegister,
data_access_type: None,
data_type: r.data_type.expect("missing data type for write"),
data_type: r
.data_type
.expect("missing data type for write"),
data_value: v,
};
d.write_into_be_u16(&mut data);
}
vprintln(
&format!(
"writing register at {} with value:",
start_addr
),
&format!("writing register at {} with value:", start_addr),
config.verbose_mode,
);
vprintln(&format!("{:?}", data), config.verbose_mode);
ModbusRequestReturnType::ResultWithNothing(
ctx.write_single_register(start_addr, data[0]).await,
ctx.write_single_register(start_addr, data[0])
.await,
)
}
FunctionCode::ReadWriteMultipleRegisters => {
let new_values =
r.new_values.take().expect("missing value for write");
FunctionCode::ReadWriteMultipleRegisters => {
let new_values = r
.new_values
.take()
.expect("missing value for write");
let mut data = Vec::<u16>::new();
for v in new_values {
let d = ModbusRegisterData {
data_description: "".to_string(),
data_model_type: DataModelType::HoldingOrInputRegister,
data_access_type: None,
data_type: r.data_type.expect("missing data type for write"),
data_type: r
.data_type
.expect("missing data type for write"),
data_value: v,
};
d.write_into_be_u16(&mut data);
Expand All @@ -173,28 +195,34 @@ pub async fn start_modbus_client(
);
vprintln(&format!("{:?}", &data), config.verbose_mode);
ModbusRequestReturnType::ResultWithU16Vec(
ctx.read_write_multiple_registers(start_addr, data.len() as u16, start_addr, &data).await,
ctx.read_write_multiple_registers(
start_addr,
data.len() as u16,
start_addr,
&data,
)
.await,
)
}
FunctionCode::WriteMultipleCoils => {
let new_values =
r.new_values.as_ref().expect("missing value for write");
let new_values = r
.new_values
.as_ref()
.expect("missing value for write");
let mut data = Vec::<bool>::new();
for v in new_values {
if let Ok(f) = v.parse::<bool>() {
data.push(f);
}
}
vprintln(
&format!(
"writing coils starting at {} with values:",
start_addr
),
&format!("writing coils starting at {} with values:", start_addr),
config.verbose_mode,
);
vprintln(&format!("{:?}", &data), config.verbose_mode);
ModbusRequestReturnType::ResultWithNothing(
ctx.write_multiple_coils(start_addr, &data).await,
ctx.write_multiple_coils(start_addr, &data)
.await,
)
}
FunctionCode::ReadCoils => {
Expand All @@ -212,12 +240,15 @@ pub async fn start_modbus_client(
config.verbose_mode,
);
ModbusRequestReturnType::ResultWithBoolVec(
ctx.read_discrete_inputs(start_addr, count).await,
ctx.read_discrete_inputs(start_addr, count)
.await,
)
}
FunctionCode::WriteSingleCoil => {
let new_values =
r.new_values.as_ref().expect("missing value for write");
let new_values = r
.new_values
.as_ref()
.expect("missing value for write");
let data = new_values[0]
.parse::<bool>()
.expect("incorrect value for bool");
Expand All @@ -227,16 +258,19 @@ pub async fn start_modbus_client(
);
vprintln(&format!("{:?}", &data), config.verbose_mode);
ModbusRequestReturnType::ResultWithNothing(
ctx.write_single_coil(start_addr, data).await,
ctx.write_single_coil(start_addr, data)
.await,
)
}
_ => todo!(),
};
println!("{}", r.description);
match response {
ModbusRequestReturnType::ResultWithU16Vec(Ok(response)) => {
let data_type =
r.data_type.as_ref().expect("missing data type for write");
let data_type = r
.data_type
.as_ref()
.expect("missing data type for write");
match data_type {
DataType::Float32 => println!(
"===> {:?}",
Expand Down
40 changes: 20 additions & 20 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{data::*, types::*};
use anyhow::{self, Context};
use clap::Parser;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::{fs, net::SocketAddr, path::PathBuf};
use tokio_serial::{SerialPort, SerialStream};

Expand Down Expand Up @@ -93,8 +93,7 @@ pub struct Opts {
pub server_address: Option<SocketAddr>,
}


#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct ModbusRequest {
/// description of the request
pub description: String,
Expand All @@ -114,7 +113,7 @@ pub struct ModbusRequest {
pub data_type: Option<DataType>,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct ModbusCommonConfig {
/// the modbus protocol type
pub protocol_type: ProtocolType,
Expand All @@ -138,7 +137,7 @@ pub struct ModbusCommonConfig {

pub const REPEAT_TIME_INDEFINITE: u16 = 0xFFFF;

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct ModbusClientRequest {
/// requested server ID for Modbus RTU
pub server_id: Option<u8>,
Expand All @@ -152,28 +151,29 @@ pub struct ModbusClientRequest {
pub request: Option<ModbusRequest>,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct ModbusClientConfig {
/// requests send by the client
pub requests: Vec<ModbusClientRequest>,
/// the register database
pub register_data: Option<ModbusRegisterDatabase>,
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ModbusServerConfig {
/// the register and coil database
/// the register database
pub register_data: ModbusRegisterDatabase,
/// the coil database
pub coil_data: ModbusCoilDatabase,
/// the external file to hold register data
pub register_data_file: Option<String>,
/// the external file to hold coil data
pub coil_data_file: Option<String>,
/// the external program to run upon data updates
pub external_program: Option<String>,
}

impl ModbusServerConfig {
pub fn get_db(self) -> (ModbusRegisterDatabase, ModbusCoilDatabase) {
(self.register_data, self.coil_data)
}
}

#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct ModbusDeviceConfig {
/// common configuration for all Modbus devices
pub common: ModbusCommonConfig,
Expand Down Expand Up @@ -212,7 +212,7 @@ pub fn configure(opts: &mut Opts) -> anyhow::Result<ModbusDeviceConfig> {
},
server: None,
client: Some(ModbusClientConfig {
requests: vec![ ModbusClientRequest {
requests: vec![ModbusClientRequest {
server_id: opts.server_id,
server_address: opts.server_address,
repeat_times: None,
Expand All @@ -228,15 +228,14 @@ pub fn configure(opts: &mut Opts) -> anyhow::Result<ModbusDeviceConfig> {
data_type: opts.data_type,
}),
}],
register_data: None,
register_data: None,
}),
verbose_mode: opts.verbose_mode,
})
})
} else {
println!("server is not supported in one-shot mode");
std::process::exit(1);
}

}
}

Expand All @@ -246,7 +245,8 @@ pub fn build_serial(config: &ModbusDeviceConfig) -> Option<SerialStream> {
let builder = tokio_serial::new(device, baudrate);
let mut port = SerialStream::open(&builder).unwrap();

port.set_parity(config.common.serial_parity?.into()).ok();
port.set_parity(config.common.serial_parity?.into())
.ok();
port.set_stop_bits(config.common.serial_stop_bits?.into())
.ok();
port.set_data_bits(config.common.serial_data_bits?.into())
Expand Down
Loading

0 comments on commit 9573416

Please sign in to comment.