use super::artifact::build_artifact;
use crate::model::{ArtifactGroup, GeneratedArtifact, TransportKind, ValueType};
use crate::planner::{
normalize_type_name, to_upper_snake_case, AreaKind, ContractPlan, ItemPlan, MethodPlan,
MethodWireKind, PlannedValue,
};
use std::collections::BTreeMap;
pub(super) fn render_rust_artifact(
plan: &ContractPlan,
transport: &TransportKind,
) -> GeneratedArtifact {
build_artifact(
format!(
"Docs/generated/modbus/rust/okm_modbus_{}_tokio_client.rs",
transport.file_segment()
),
None,
ArtifactGroup::RustTokioModbus,
transport.clone(),
render_rust_client(plan, transport),
)
}
fn render_rust_client(plan: &ContractPlan, transport: &TransportKind) -> String {
[
format!("// 自动生成:{} Rust 设备合同实现", transport.as_str()),
render_rust_prelude(transport),
render_rust_error_type(),
render_rust_manifest_constants(plan),
render_rust_common_types(plan),
render_rust_transport_trait(),
render_rust_transport_impl(transport),
render_rust_service_traits(plan),
render_rust_service_impls(plan),
render_rust_helpers(),
]
.join("\n\n")
}
fn render_rust_prelude(transport: &TransportKind) -> String {
match transport {
TransportKind::Rtu => [
"use serialport::{DataBits, Parity, StopBits};",
"use std::time::Duration;",
"use tokio_modbus::client::sync::rtu;",
"use tokio_modbus::prelude::{Slave, SyncReader, SyncWriter};",
"",
"#[derive(Debug, Clone)]",
"pub struct OkmModbusRtuConfig {",
" pub port_name: String,",
" pub baud_rate: u32,",
" pub data_bits: u8,",
" pub stop_bits: u8,",
" pub parity: String,",
" pub timeout: Option<Duration>,",
" pub slave: u8,",
"}",
"",
"pub fn connect_okm_modbus_rtu(config: &OkmModbusRtuConfig) -> std::io::Result<tokio_modbus::client::sync::Context> {",
" let data_bits = match config.data_bits {",
" 5 => DataBits::Five,",
" 6 => DataBits::Six,",
" 7 => DataBits::Seven,",
" _ => DataBits::Eight,",
" };",
" let stop_bits = match config.stop_bits {",
" 2 => StopBits::Two,",
" _ => StopBits::One,",
" };",
" let parity = match config.parity.trim().to_uppercase().as_str() {",
" \"ODD\" => Parity::Odd,",
" \"EVEN\" => Parity::Even,",
" _ => Parity::None,",
" };",
" let builder = serialport::new(config.port_name.trim(), config.baud_rate)",
" .data_bits(data_bits)",
" .stop_bits(stop_bits)",
" .parity(parity)",
" .timeout(config.timeout.unwrap_or_else(|| Duration::from_millis(300)));",
" let mut context = rtu::connect_slave(&builder, Slave(config.slave))?;",
" context.set_timeout(config.timeout);",
" Ok(context)",
"}",
]
.join("\n"),
TransportKind::Tcp => [
"use std::net::{IpAddr, SocketAddr};",
"use std::time::Duration;",
"use tokio_modbus::client::sync::tcp;",
"use tokio_modbus::prelude::{Slave, SyncReader, SyncWriter};",
"",
"#[derive(Debug, Clone)]",
"pub struct OkmModbusTcpConfig {",
" pub host: String,",
" pub port: u16,",
" pub timeout: Option<Duration>,",
" pub slave: u8,",
"}",
"",
"pub fn connect_okm_modbus_tcp(config: &OkmModbusTcpConfig) -> std::io::Result<tokio_modbus::client::sync::Context> {",
" let ip = config.host.parse::<IpAddr>().map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error.to_string()))?;",
" let socket = SocketAddr::new(ip, config.port);",
" tcp::connect_slave_with_timeout(socket, Slave(config.slave), config.timeout)",
"}",
]
.join("\n"),
}
}
fn render_rust_error_type() -> String {
[
"#[derive(Debug, Clone, PartialEq, Eq)]",
"pub struct DeviceApiError {",
" pub message: String,",
"}",
"",
"impl DeviceApiError {",
" pub fn new(message: impl Into<String>) -> Self {",
" Self {",
" message: message.into(),",
" }",
" }",
"}",
"",
"impl std::fmt::Display for DeviceApiError {",
" fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {",
" formatter.write_str(&self.message)",
" }",
"}",
"",
"impl std::error::Error for DeviceApiError {}",
]
.join("\n")
}
fn render_rust_manifest_constants(plan: &ContractPlan) -> String {
let mut lines = Vec::new();
for method in &plan.methods {
let base = rust_method_manifest_prefix(method);
lines.push(format!(
"pub const {base}_START_ADDRESS: u16 = {};",
method.start_address
));
lines.push(format!(
"pub const {base}_QUANTITY: u16 = {};",
method.quantity
));
lines.push(String::new());
}
lines.join("\n")
}
fn render_rust_common_types(plan: &ContractPlan) -> String {
let mut lines = vec![
"#[derive(Debug, Clone, PartialEq, Eq)]".to_string(),
"pub struct OkmModbusString32 {".to_string(),
" pub value: String,".to_string(),
"}".to_string(),
"".to_string(),
"#[derive(Debug, Clone, PartialEq, Eq)]".to_string(),
"pub struct OkmModbusBytes32 {".to_string(),
" pub data: Vec<u8>,".to_string(),
"}".to_string(),
"".to_string(),
];
let mut rendered_types = BTreeMap::new();
for method in &plan.methods {
if let PlannedValue::Dto { type_name, items } = &method.planned_value {
rendered_types.entry(type_name.clone()).or_insert_with(|| {
let mut content = vec![
"#[derive(Debug, Clone, PartialEq, Eq)]".to_string(),
format!("pub struct {} {{", normalize_type_name(type_name)),
];
for item in items {
content.push(format!(
" pub {}: {},",
item.name,
rust_value_type(&item.value_type)
));
}
content.push("}".to_string());
content.join("\n")
});
}
}
lines.extend(rendered_types.into_values());
lines.join("\n")
}
fn render_rust_transport_trait() -> String {
[
"pub trait RegisterTransport: Send + Sync {",
" fn read_coils(&self, start: u16, quantity: u16) -> Result<Vec<bool>, DeviceApiError>;",
" fn read_discrete_inputs(&self, start: u16, quantity: u16) -> Result<Vec<bool>, DeviceApiError>;",
" fn read_holding_registers(&self, start: u16, quantity: u16) -> Result<Vec<u16>, DeviceApiError>;",
" fn read_input_registers(&self, start: u16, quantity: u16) -> Result<Vec<u16>, DeviceApiError>;",
" fn write_coils(&self, start: u16, values: &[bool]) -> Result<(), DeviceApiError>;",
" fn write_holding_registers(&self, start: u16, values: &[u16]) -> Result<(), DeviceApiError>;",
"}",
]
.join("\n")
}
fn render_rust_transport_impl(transport: &TransportKind) -> String {
let (transport_name, config_name, connect_function) = match transport {
TransportKind::Rtu => (
"ModbusRtuTransport",
"OkmModbusRtuConfig",
"connect_okm_modbus_rtu",
),
TransportKind::Tcp => (
"ModbusTcpTransport",
"OkmModbusTcpConfig",
"connect_okm_modbus_tcp",
),
};
[
"#[derive(Debug, Clone)]".to_string(),
format!("pub struct {transport_name} {{"),
format!(" pub config: {config_name},"),
"}".to_string(),
"".to_string(),
format!("impl {transport_name} {{"),
format!(" pub fn new(config: {config_name}) -> Self {{"),
" Self {".to_string(),
" config,".to_string(),
" }".to_string(),
" }".to_string(),
"".to_string(),
" fn with_context<T>(".to_string(),
" &self,".to_string(),
" task: impl FnOnce(&mut tokio_modbus::client::sync::Context) -> Result<T, DeviceApiError>,"
.to_string(),
" ) -> Result<T, DeviceApiError> {".to_string(),
format!(" let mut context = {connect_function}(&self.config)")
.to_string(),
" .map_err(|error| DeviceApiError::new(error.to_string()))?;".to_string(),
" task(&mut context)".to_string(),
" }".to_string(),
"}".to_string(),
"".to_string(),
format!("impl RegisterTransport for {transport_name} {{"),
" fn read_coils(&self, start: u16, quantity: u16) -> Result<Vec<bool>, DeviceApiError> {"
.to_string(),
" self.with_context(|context| {".to_string(),
" context".to_string(),
" .read_coils(start, quantity)".to_string(),
" .map_err(|error| DeviceApiError::new(error.to_string()))?".to_string(),
" .map_err(|error| DeviceApiError::new(format!(\"Modbus 异常: {error:?}\")))"
.to_string(),
" })".to_string(),
" }".to_string(),
"".to_string(),
" fn read_discrete_inputs(&self, start: u16, quantity: u16) -> Result<Vec<bool>, DeviceApiError> {"
.to_string(),
" self.with_context(|context| {".to_string(),
" context".to_string(),
" .read_discrete_inputs(start, quantity)".to_string(),
" .map_err(|error| DeviceApiError::new(error.to_string()))?".to_string(),
" .map_err(|error| DeviceApiError::new(format!(\"Modbus 异常: {error:?}\")))"
.to_string(),
" })".to_string(),
" }".to_string(),
"".to_string(),
" fn read_holding_registers(&self, start: u16, quantity: u16) -> Result<Vec<u16>, DeviceApiError> {"
.to_string(),
" self.with_context(|context| {".to_string(),
" context".to_string(),
" .read_holding_registers(start, quantity)".to_string(),
" .map_err(|error| DeviceApiError::new(error.to_string()))?".to_string(),
" .map_err(|error| DeviceApiError::new(format!(\"Modbus 异常: {error:?}\")))"
.to_string(),
" })".to_string(),
" }".to_string(),
"".to_string(),
" fn read_input_registers(&self, start: u16, quantity: u16) -> Result<Vec<u16>, DeviceApiError> {"
.to_string(),
" self.with_context(|context| {".to_string(),
" context".to_string(),
" .read_input_registers(start, quantity)".to_string(),
" .map_err(|error| DeviceApiError::new(error.to_string()))?".to_string(),
" .map_err(|error| DeviceApiError::new(format!(\"Modbus 异常: {error:?}\")))"
.to_string(),
" })".to_string(),
" }".to_string(),
"".to_string(),
" fn write_coils(&self, start: u16, values: &[bool]) -> Result<(), DeviceApiError> {"
.to_string(),
" self.with_context(|context| match values {".to_string(),
" [] => Err(DeviceApiError::new(\"至少需要一个线圈值\")),".to_string(),
" [value] => {".to_string(),
" context".to_string(),
" .write_single_coil(start, *value)".to_string(),
" .map_err(|error| DeviceApiError::new(error.to_string()))?"
.to_string(),
" .map_err(|error| DeviceApiError::new(format!(\"Modbus 异常: {error:?}\")))?;"
.to_string(),
" Ok(())".to_string(),
" }".to_string(),
" _ => {".to_string(),
" context".to_string(),
" .write_multiple_coils(start, values)".to_string(),
" .map_err(|error| DeviceApiError::new(error.to_string()))?"
.to_string(),
" .map_err(|error| DeviceApiError::new(format!(\"Modbus 异常: {error:?}\")))?;"
.to_string(),
" Ok(())".to_string(),
" }".to_string(),
" })".to_string(),
" }".to_string(),
"".to_string(),
" fn write_holding_registers(&self, start: u16, values: &[u16]) -> Result<(), DeviceApiError> {"
.to_string(),
" self.with_context(|context| match values {".to_string(),
" [] => Err(DeviceApiError::new(\"至少需要一个保持寄存器值\")),".to_string(),
" [value] => {".to_string(),
" context".to_string(),
" .write_single_register(start, *value)".to_string(),
" .map_err(|error| DeviceApiError::new(error.to_string()))?"
.to_string(),
" .map_err(|error| DeviceApiError::new(format!(\"Modbus 异常: {error:?}\")))?;"
.to_string(),
" Ok(())".to_string(),
" }".to_string(),
" _ => {".to_string(),
" context".to_string(),
" .write_multiple_registers(start, values)".to_string(),
" .map_err(|error| DeviceApiError::new(error.to_string()))?"
.to_string(),
" .map_err(|error| DeviceApiError::new(format!(\"Modbus 异常: {error:?}\")))?;"
.to_string(),
" Ok(())".to_string(),
" }".to_string(),
" })".to_string(),
" }".to_string(),
"}".to_string(),
]
.join("\n")
}
fn render_rust_service_traits(plan: &ContractPlan) -> String {
let mut lines = Vec::new();
for service in &plan.services {
let methods = service_methods(plan, service);
lines.push(render_rust_service_trait(service, &methods));
}
lines.join("\n\n")
}
fn render_rust_service_trait(
service: &crate::model::ContractService,
methods: &[&MethodPlan],
) -> String {
let trait_name = normalize_type_name(&service.interface_name);
let mut lines = vec![format!("pub trait {trait_name}: Send + Sync {{")];
for method in methods {
lines.push(render_rust_trait_method_signature(method));
}
lines.push("}".to_string());
lines.join("\n")
}
fn render_rust_service_impls(plan: &ContractPlan) -> String {
let mut lines = Vec::new();
for service in &plan.services {
let methods = service_methods(plan, service);
lines.push(render_rust_service_impl(service, &methods));
}
lines.join("\n\n")
}
fn render_rust_service_impl(
service: &crate::model::ContractService,
methods: &[&MethodPlan],
) -> String {
let trait_name = normalize_type_name(&service.interface_name);
let impl_name = format!("Modbus{trait_name}");
let mut lines = vec![
format!("pub struct {impl_name}<T> {{"),
" transport: T,".to_string(),
"}".to_string(),
"".to_string(),
format!("impl<T> {impl_name}<T> {{"),
" pub fn new(transport: T) -> Self {".to_string(),
" Self {".to_string(),
" transport,".to_string(),
" }".to_string(),
" }".to_string(),
"}".to_string(),
"".to_string(),
format!("impl<T> {trait_name} for {impl_name}<T>"),
"where".to_string(),
" T: RegisterTransport,".to_string(),
"{".to_string(),
];
for method in methods {
lines.push(render_rust_impl_method(method));
}
lines.push("}".to_string());
lines.join("\n")
}
fn service_methods<'a>(
plan: &'a ContractPlan,
service: &crate::model::ContractService,
) -> Vec<&'a MethodPlan> {
let interface_name = normalize_type_name(&service.interface_name);
plan.methods
.iter()
.filter(|method| method.interface_name == interface_name)
.collect::<Vec<_>>()
}
fn render_rust_trait_method_signature(method: &MethodPlan) -> String {
let parameter_list = rust_method_parameter_list(method);
let return_type = rust_method_return_type(method);
if parameter_list.is_empty() {
format!(
" fn {}(&self) -> Result<{}, DeviceApiError>;",
method.rust_method_name, return_type
)
} else {
format!(
" fn {}(&self, {}) -> Result<{}, DeviceApiError>;",
method.rust_method_name, parameter_list, return_type
)
}
}
fn render_rust_impl_method(method: &MethodPlan) -> String {
let parameter_list = rust_method_parameter_list(method);
let return_type = rust_method_return_type(method);
let mut lines = Vec::new();
if parameter_list.is_empty() {
lines.push(format!(
" fn {}(&self) -> Result<{}, DeviceApiError> {{",
method.rust_method_name, return_type
));
} else {
lines.push(format!(
" fn {}(&self, {}) -> Result<{}, DeviceApiError> {{",
method.rust_method_name, parameter_list, return_type
));
}
match &method.planned_value {
PlannedValue::Parameters { .. } => lines.push(render_rust_write_body(method)),
PlannedValue::Dto { .. } | PlannedValue::Scalar { .. } => {
lines.push(render_rust_read_body(method))
}
}
lines.push(" }".to_string());
lines.join("\n")
}
fn render_rust_read_body(method: &MethodPlan) -> String {
let base = rust_method_manifest_prefix(method);
let transport_method = rust_transport_read_method(&method.area_kind);
match &method.planned_value {
PlannedValue::Dto { type_name, items } => {
let return_type = normalize_type_name(type_name);
match method.wire_kind {
MethodWireKind::Bit => {
let mut lines = vec![format!(
" let values = self.transport.{transport_method}({base}_START_ADDRESS, {base}_QUANTITY)?;"
)];
lines.push(format!(" Ok({return_type} {{"));
for item in items {
lines.push(format!(" {}: values[{}],", item.name, item.offset));
}
lines.push(" })".to_string());
lines.join("\n")
}
MethodWireKind::Register => {
let mut lines = vec![format!(
" let registers = self.transport.{transport_method}({base}_START_ADDRESS, {base}_QUANTITY)?;"
)];
lines.push(format!(" Ok({return_type} {{"));
for item in items {
lines.push(format!(
" {}: {},",
item.name,
rust_decode_expression(item, "registers")
));
}
lines.push(" })".to_string());
lines.join("\n")
}
}
}
PlannedValue::Scalar { item } => match method.wire_kind {
MethodWireKind::Bit => [
format!(
" let values = self.transport.{transport_method}({base}_START_ADDRESS, {base}_QUANTITY)?;"
),
" Ok(values[0])".to_string(),
]
.join("\n"),
MethodWireKind::Register => [
format!(
" let registers = self.transport.{transport_method}({base}_START_ADDRESS, {base}_QUANTITY)?;"
),
format!(" Ok({})", rust_decode_expression(item, "registers")),
]
.join("\n"),
},
PlannedValue::Parameters { .. } => String::new(),
}
}
fn render_rust_write_body(method: &MethodPlan) -> String {
let PlannedValue::Parameters { items } = &method.planned_value else {
return String::new();
};
let base = rust_method_manifest_prefix(method);
let transport_method = rust_transport_write_method(&method.area_kind);
match method.wire_kind {
MethodWireKind::Bit => {
let values = items
.iter()
.map(|item| item.name.clone())
.collect::<Vec<_>>()
.join(", ");
[
format!(" let values = vec![{values}];"),
format!(
" self.transport.{transport_method}({base}_START_ADDRESS, &values)?;"
),
" Ok(())".to_string(),
]
.join("\n")
}
MethodWireKind::Register => {
let mut lines = vec![" let mut registers = Vec::new();".to_string()];
for item in items {
lines.push(format!(" {}", rust_register_push(item)));
}
lines.push(format!(
" self.transport.{transport_method}({base}_START_ADDRESS, ®isters)?;"
));
lines.push(" Ok(())".to_string());
lines.join("\n")
}
}
}
fn rust_method_manifest_prefix(method: &MethodPlan) -> String {
format!(
"OKM_MODBUS_{}_{}",
to_upper_snake_case(&method.interface_name),
to_upper_snake_case(&method.method_name)
)
}
fn rust_method_return_type(method: &MethodPlan) -> String {
match &method.planned_value {
PlannedValue::Dto { type_name, .. } => normalize_type_name(type_name),
PlannedValue::Scalar { item } => rust_value_type(&item.value_type).to_string(),
PlannedValue::Parameters { .. } => "()".to_string(),
}
}
fn rust_method_parameter_list(method: &MethodPlan) -> String {
match &method.planned_value {
PlannedValue::Parameters { items } => items
.iter()
.map(|item| format!("{}: {}", item.name, rust_argument_type(&item.value_type)))
.collect::<Vec<_>>()
.join(", "),
PlannedValue::Dto { .. } | PlannedValue::Scalar { .. } => String::new(),
}
}
fn rust_transport_read_method(area_kind: &AreaKind) -> &'static str {
match area_kind {
AreaKind::Coil => "read_coils",
AreaKind::DiscreteInput => "read_discrete_inputs",
AreaKind::HoldingRegister => "read_holding_registers",
AreaKind::InputRegister => "read_input_registers",
}
}
fn rust_transport_write_method(area_kind: &AreaKind) -> &'static str {
match area_kind {
AreaKind::Coil => "write_coils",
AreaKind::HoldingRegister => "write_holding_registers",
AreaKind::DiscreteInput | AreaKind::InputRegister => {
unreachable!("读区域不能生成写调用")
}
}
}
fn render_rust_helpers() -> String {
[
"fn decode_i32(registers: &[u16], offset: usize) -> i32 {",
" (((registers[offset] as u32) << 16) | registers[offset + 1] as u32) as i32",
"}",
"",
"fn encode_i32(value: i32) -> [u16; 2] {",
" let raw = value as u32;",
" [((raw >> 16) & 0xFFFF) as u16, (raw & 0xFFFF) as u16]",
"}",
"",
"fn decode_string32(registers: &[u16], offset: usize) -> OkmModbusString32 {",
" let length = registers[offset].min(32) as usize;",
" let mut bytes = Vec::with_capacity(length);",
" for index in 0..length {",
" let register = registers[offset + 1 + (index / 2)];",
" let value = if index % 2 == 0 { (register >> 8) as u8 } else { (register & 0x00FF) as u8 };",
" bytes.push(value);",
" }",
" OkmModbusString32 { value: String::from_utf8_lossy(&bytes).to_string() }",
"}",
"",
"fn decode_bytes32(registers: &[u16], offset: usize) -> OkmModbusBytes32 {",
" let length = registers[offset].min(32) as usize;",
" let mut bytes = Vec::with_capacity(length);",
" for index in 0..length {",
" let register = registers[offset + 1 + (index / 2)];",
" let value = if index % 2 == 0 { (register >> 8) as u8 } else { (register & 0x00FF) as u8 };",
" bytes.push(value);",
" }",
" OkmModbusBytes32 { data: bytes }",
"}",
"",
"fn encode_string32(value: &OkmModbusString32) -> Vec<u16> {",
" let bytes = value.value.as_bytes();",
" let length = bytes.len().min(32);",
" let mut registers = vec![0_u16; 17];",
" registers[0] = length as u16;",
" for index in (0..length).step_by(2) {",
" let high = bytes[index] as u16;",
" let low = if index + 1 < length { bytes[index + 1] as u16 } else { 0 };",
" registers[1 + (index / 2)] = (high << 8) | low;",
" }",
" registers",
"}",
"",
"fn encode_bytes32(value: &OkmModbusBytes32) -> Vec<u16> {",
" let length = value.data.len().min(32);",
" let mut registers = vec![0_u16; 17];",
" registers[0] = length as u16;",
" for index in (0..length).step_by(2) {",
" let high = value.data[index] as u16;",
" let low = if index + 1 < length { value.data[index + 1] as u16 } else { 0 };",
" registers[1 + (index / 2)] = (high << 8) | low;",
" }",
" registers",
"}",
"",
]
.join("\n")
}
fn rust_register_push(item: &ItemPlan) -> String {
match item.value_type {
ValueType::Boolean => format!(" registers.push(if {} {{ 1 }} else {{ 0 }});", item.name),
ValueType::Int => format!(
" registers.extend_from_slice(&encode_i32({}));",
item.name
),
ValueType::String => format!(
" registers.extend_from_slice(&encode_string32({}));",
item.name
),
ValueType::Bytes => format!(
" registers.extend_from_slice(&encode_bytes32({}));",
item.name
),
}
}
fn rust_decode_expression(item: &ItemPlan, buffer_name: &str) -> String {
match item.value_type {
ValueType::Boolean => format!("{buffer_name}[{}] != 0", item.offset),
ValueType::Int => format!("decode_i32(&{buffer_name}, {})", item.offset),
ValueType::String => format!("decode_string32(&{buffer_name}, {})", item.offset),
ValueType::Bytes => format!("decode_bytes32(&{buffer_name}, {})", item.offset),
}
}
fn rust_value_type(value_type: &ValueType) -> &'static str {
match value_type {
ValueType::Boolean => "bool",
ValueType::Int => "i32",
ValueType::String => "OkmModbusString32",
ValueType::Bytes => "OkmModbusBytes32",
}
}
fn rust_argument_type(value_type: &ValueType) -> &'static str {
match value_type {
ValueType::Boolean => "bool",
ValueType::Int => "i32",
ValueType::String => "&OkmModbusString32",
ValueType::Bytes => "&OkmModbusBytes32",
}
}