Rust+Slint 实现ModbusRTU从机调试助手源码分享

Rust+Slint 实现ModbusRTU从机调试助手源码分享

一、效果展示

二、源码分享

1、工程结构

2、comm_log.rs

rust 复制代码
use chrono::Local;

/// 通信日志条目
#[derive(Debug, Clone)]
pub struct LogEntryData {
    pub timestamp: String,
    pub direction: i32, // 0=TX, 1=RX, 2=ERROR
    pub raw_text: String,
}

/// 通信日志管理器
#[derive(Clone)]
pub struct CommLog {
    entries: Vec<LogEntryData>,
    max_entries: usize,
    version: u64,
}

impl Default for CommLog {
    fn default() -> Self {
        Self::new()
    }
}

impl CommLog {
    pub fn new() -> Self {
        Self {
            entries: Vec::new(),
            max_entries: 5000,
            version: 0,
        }
    }

    /// 记录发送数据
    pub fn log_tx(&mut self, description: &str) {
        self.add_entry(0, description);
    }

    /// 记录接收数据
    pub fn log_rx(&mut self, description: &str) {
        self.add_entry(1, description);
    }

    /// 记录错误
    pub fn log_error(&mut self, description: &str) {
        self.add_entry(2, description);
    }

    fn add_entry(&mut self, direction: i32, text: &str) {
        let entry = LogEntryData {
            timestamp: Local::now().format("%H:%M:%S%.3f").to_string(),
            direction,
            raw_text: text.to_string(),
        };

        self.entries.push(entry);
        self.version = self.version.wrapping_add(1);

        // 限制日志条目数
        if self.entries.len() > self.max_entries {
            self.entries.drain(0..self.entries.len() - self.max_entries);
        }
    }

    /// 获取所有条目
    pub fn entries(&self) -> &[LogEntryData] {
        &self.entries
    }

    /// 获取版本号(每次修改递增)
    pub fn version(&self) -> u64 {
        self.version
    }

    /// 清空日志
    pub fn clear(&mut self) {
        self.entries.clear();
        self.version = self.version.wrapping_add(1);
    }

    /// 格式化原始字节为十六进制字符串
    pub fn format_bytes(data: &[u8]) -> String {
        data.iter()
            .map(|b| format!("{:02X}", b))
            .collect::<Vec<_>>()
            .join(" ")
    }
}

5、data_models.rs

rust 复制代码
use crate::modbus_types::{FunctionCode, ReadResult};

/// 寄存器数据模型
#[derive(Clone)]
pub struct RegisterModel {
    /// 原始值
    pub raw_values: Vec<u16>,
    /// 起始地址
    pub start_address: u16,
    /// 当前功能码
    pub function_code: FunctionCode,
    /// 线圈数据(用于线圈/离散输入模式)
    pub coil_values: Vec<bool>,
}

impl RegisterModel {
    pub fn new() -> Self {
        Self {
            raw_values: Vec::new(),
            start_address: 0,
            function_code: FunctionCode::ReadHoldingRegisters,
            coil_values: Vec::new(),
        }
    }

    /// 从读取结果更新
    pub fn update_from_result(&mut self, result: &ReadResult, start_addr: u16, fc: FunctionCode) {
        self.start_address = start_addr;
        self.function_code = fc;

        match result {
            ReadResult::Registers(regs) => {
                self.raw_values = regs.clone();
                self.coil_values.clear();
            }
            ReadResult::Coils(coils) => {
                self.coil_values = coils.clone();
                self.raw_values.clear();
            }
        }
    }

    /// 转换为 Slint 显示模型
    pub fn to_slint_rows(&self) -> Vec<slint::ModelRc<slint::SharedString>> {
        // 这个函数返回的是每列的数据
        // 但更好的方式是返回 RegisterRow 结构体数组
        // 实际上我们应该直接生成 Slint 的 RegisterRow
        Vec::new()
    }

    /// 生成 Slint RegisterRow 数组
    pub fn to_register_rows(&self) -> Vec<slint::SharedString> {
        // 这个函数只是为了辅助,实际逻辑在 generate_rows 中
        Vec::new()
    }

    /// 生成用于 Slint 的行数据
    /// 返回 Vec<(address, unsigned, signed, hex, binary, float, ascii)>
    pub fn generate_rows(&self) -> Vec<(i32, String, String, String, String, String, String)> {
        if self.function_code.is_coil() {
            // 线圈模式
            return self.coil_values.iter().enumerate().map(|(i, &coil)| {
                let addr = self.start_address + i as u16;
                let val = if coil { 1u16 } else { 0u16 };
                (
                    addr as i32,
                    val.to_string(),
                    val.to_string(),
                    format!("{:04X}", val),
                    format!("{:016b}", val),
                    "-".to_string(),
                    if coil { "ON" } else { "OFF" }.to_string(),
                )
            }).collect();
        }

        // 寄存器模式
        self.raw_values.iter().enumerate().map(|(i, &raw)| {
            let addr = self.start_address + i as u16;
            let unsigned_val = raw;
            let signed_val = raw as i16;
            let hex_val = format!("{:04X}", raw);
            let binary_val = format!("{:016b}", raw);

            // IEEE 754 浮点(需要两个寄存器)
            let float_val = if i + 1 < self.raw_values.len() {
                let next_raw = self.raw_values[i + 1];
                let bits = ((raw as u32) << 16) | (next_raw as u32);
                let float = f32::from_bits(bits);
                if float.is_finite() && float.abs() < 1e10 && (float == 0.0 || float.abs() >= 1e-10) {
                    format!("{:.4}", float)
                } else {
                    format!("{:e}", float)
                }
            } else {
                "-".to_string()
            };

            // ASCII
            let ascii_val = {
                let hi = (raw >> 8) as u8;
                let lo = (raw & 0xFF) as u8;
                let hi_char = if hi >= 0x20 && hi <= 0x7E { hi as char } else { '.' };
                let lo_char = if lo >= 0x20 && lo <= 0x7E { lo as char } else { '.' };
                format!("{}{}", hi_char, lo_char)
            };

            (
                addr as i32,
                unsigned_val.to_string(),
                signed_val.to_string(),
                hex_val,
                binary_val,
                float_val,
                ascii_val,
            )
        }).collect()
    }

    /// 清空数据
    pub fn clear(&mut self) {
        self.raw_values.clear();
        self.coil_values.clear();
        self.start_address = 0;
    }
}

/// 格式转换工具
pub mod format_utils {
    /// u16 转无符号字符串
    pub fn to_unsigned(value: u16) -> String {
        value.to_string()
    }

    /// u16 转有符号字符串
    pub fn to_signed(value: u16) -> String {
        (value as i16).to_string()
    }

    /// u16 转十六进制
    pub fn to_hex(value: u16) -> String {
        format!("{:04X}", value)
    }

    /// u16 转二进制
    pub fn to_binary(value: u16) -> String {
        format!("{:016b}", value)
    }

    /// 两个 u16 转 IEEE 754 浮点
    pub fn to_float(high: u16, low: u16) -> f32 {
        let bits = ((high as u32) << 16) | (low as u32);
        f32::from_bits(bits)
    }

    /// f32 转两个 u16 寄存器
    pub fn from_float(value: f32) -> (u16, u16) {
        let bits = value.to_bits();
        let high = (bits >> 16) as u16;
        let low = bits as u16;
        (high, low)
    }

    /// u16 转 ASCII
    pub fn to_ascii(value: u16) -> String {
        let hi = (value >> 8) as u8;
        let lo = (value & 0xFF) as u8;
        let hi_char = if hi >= 0x20 && hi <= 0x7E { hi as char } else { '.' };
        let lo_char = if lo >= 0x20 && lo <= 0x7E { lo as char } else { '.' };
        format!("{}{}", hi_char, lo_char)
    }

    /// 解析字符串为 u16(支持十进制和十六进制)
    pub fn parse_value(s: &str) -> Result<u16, String> {
        let s = s.trim();
        if s.starts_with("0x") || s.starts_with("0X") {
            u16::from_str_radix(&s[2..], 16).map_err(|e| format!("无效的十六进制: {}", e))
        } else if s.starts_with("0b") || s.starts_with("0B") {
            u16::from_str_radix(&s[2..], 2).map_err(|e| format!("无效的二进制: {}", e))
        } else {
            s.parse::<u16>().map_err(|e| format!("无效的十进制: {}", e))
        }
    }
}

6、modbus_client.rs

rust 复制代码
use std::net::SocketAddr;
use std::str::FromStr;
use tokio_modbus::prelude::*;
use tokio_modbus::client::{self, Context};

use crate::modbus_types::*;

/// 统一的 Modbus 客户端封装
pub struct ModbusClientWrapper {
    ctx: Context,
    mode: String,
}

impl ModbusClientWrapper {
    /// 连接 TCP
    pub async fn connect_tcp(ip: &str, port: u16, slave_id: u8) -> Result<Self, ModbusAppError> {
        let addr_str = format!("{}:{}", ip, port);
        let socket_addr = SocketAddr::from_str(&addr_str)
            .map_err(|e| ModbusAppError::ConnectionError(format!("无效的地址: {}", e)))?;

        let slave = Slave(slave_id);
        let ctx = client::tcp::connect_slave(socket_addr, slave)
            .await
            .map_err(|e| ModbusAppError::ConnectionError(e.to_string()))?;

        Ok(Self {
            ctx,
            mode: format!("TCP {}:{}", ip, port),
        })
    }

    /// 连接 RTU
    pub async fn connect_rtu(
        port_name: &str,
        baud_rate: u32,
        data_bits: u8,
        stop_bits: u8,
        parity: ParityMode,
        slave_id: u8,
    ) -> Result<Self, ModbusAppError> {
        let parity_mode = match parity {
            ParityMode::None => tokio_serial::Parity::None,
            ParityMode::Even => tokio_serial::Parity::Even,
            ParityMode::Odd => tokio_serial::Parity::Odd,
        };

        let data_bits_mode = match data_bits {
            5 => tokio_serial::DataBits::Five,
            6 => tokio_serial::DataBits::Six,
            7 => tokio_serial::DataBits::Seven,
            _ => tokio_serial::DataBits::Eight,
        };

        let stop_bits_mode = match stop_bits {
            1 => tokio_serial::StopBits::One,
            _ => tokio_serial::StopBits::Two,
        };

        let builder = tokio_serial::new(port_name, baud_rate)
            .data_bits(data_bits_mode)
            .stop_bits(stop_bits_mode)
            .parity(parity_mode);

        let serial_port = tokio_serial::SerialStream::open(&builder)
            .map_err(|e| ModbusAppError::ConnectionError(format!("串口打开失败: {}", e)))?;

        let slave = Slave(slave_id);
        let ctx = client::rtu::attach_slave(serial_port, slave);

        Ok(Self {
            ctx,
            mode: format!("RTU {} {}-{}{}{}", port_name, baud_rate, data_bits, stop_bits, parity),
        })
    }

    /// 读取线圈 (FC 01)
    pub async fn read_coils(&mut self, addr: u16, cnt: u16) -> Result<Vec<bool>, ModbusAppError> {
        self.ctx
            .read_coils(addr, cnt)
            .await
            .map_err(|e| ModbusAppError::ProtocolError(e.to_string()))?
            .map_err(|e| ModbusAppError::ProtocolError(format!("异常: {:?}", e)))
    }

    /// 读取离散输入 (FC 02)
    pub async fn read_discrete_inputs(&mut self, addr: u16, cnt: u16) -> Result<Vec<bool>, ModbusAppError> {
        self.ctx
            .read_discrete_inputs(addr, cnt)
            .await
            .map_err(|e| ModbusAppError::ProtocolError(e.to_string()))?
            .map_err(|e| ModbusAppError::ProtocolError(format!("异常: {:?}", e)))
    }

    /// 读取保持寄存器 (FC 03)
    pub async fn read_holding_registers(&mut self, addr: u16, cnt: u16) -> Result<Vec<u16>, ModbusAppError> {
        self.ctx
            .read_holding_registers(addr, cnt)
            .await
            .map_err(|e| ModbusAppError::ProtocolError(e.to_string()))?
            .map_err(|e| ModbusAppError::ProtocolError(format!("异常: {:?}", e)))
    }

    /// 读取输入寄存器 (FC 04)
    pub async fn read_input_registers(&mut self, addr: u16, cnt: u16) -> Result<Vec<u16>, ModbusAppError> {
        self.ctx
            .read_input_registers(addr, cnt)
            .await
            .map_err(|e| ModbusAppError::ProtocolError(e.to_string()))?
            .map_err(|e| ModbusAppError::ProtocolError(format!("异常: {:?}", e)))
    }

    /// 写单个线圈 (FC 05)
    pub async fn write_single_coil(&mut self, addr: u16, value: bool) -> Result<(), ModbusAppError> {
        self.ctx
            .write_single_coil(addr, value)
            .await
            .map_err(|e| ModbusAppError::ProtocolError(e.to_string()))?
            .map_err(|e| ModbusAppError::ProtocolError(format!("异常: {:?}", e)))
    }

    /// 写单个寄存器 (FC 06)
    pub async fn write_single_register(&mut self, addr: u16, value: u16) -> Result<(), ModbusAppError> {
        self.ctx
            .write_single_register(addr, value)
            .await
            .map_err(|e| ModbusAppError::ProtocolError(e.to_string()))?
            .map_err(|e| ModbusAppError::ProtocolError(format!("异常: {:?}", e)))
    }

    /// 写多个线圈 (FC 15)
    pub async fn write_multiple_coils(&mut self, addr: u16, values: &[bool]) -> Result<(), ModbusAppError> {
        self.ctx
            .write_multiple_coils(addr, values)
            .await
            .map_err(|e| ModbusAppError::ProtocolError(e.to_string()))?
            .map_err(|e| ModbusAppError::ProtocolError(format!("异常: {:?}", e)))
    }

    /// 写多个寄存器 (FC 16)
    pub async fn write_multiple_registers(&mut self, addr: u16, values: &[u16]) -> Result<(), ModbusAppError> {
        self.ctx
            .write_multiple_registers(addr, values)
            .await
            .map_err(|e| ModbusAppError::ProtocolError(e.to_string()))?
            .map_err(|e| ModbusAppError::ProtocolError(format!("异常: {:?}", e)))
    }

    /// 执行读取请求
    pub async fn execute_read(&mut self, req: &ReadRequest) -> Result<ReadResult, ModbusAppError> {
        match req.function_code {
            FunctionCode::ReadCoils => {
                let coils = self.read_coils(req.start_address, req.quantity).await?;
                Ok(ReadResult::Coils(coils))
            }
            FunctionCode::ReadDiscreteInputs => {
                let inputs = self.read_discrete_inputs(req.start_address, req.quantity).await?;
                Ok(ReadResult::Coils(inputs))
            }
            FunctionCode::ReadHoldingRegisters => {
                let regs = self.read_holding_registers(req.start_address, req.quantity).await?;
                Ok(ReadResult::Registers(regs))
            }
            FunctionCode::ReadInputRegisters => {
                let regs = self.read_input_registers(req.start_address, req.quantity).await?;
                Ok(ReadResult::Registers(regs))
            }
            _ => Err(ModbusAppError::InvalidParam("不是读取功能码".to_string())),
        }
    }

    /// 执行写入请求
    pub async fn execute_write(&mut self, req: &WriteRequest) -> Result<(), ModbusAppError> {
        match req.function_code {
            FunctionCode::WriteSingleCoil => {
                let value = req.values.first().copied().unwrap_or(0) != 0;
                self.write_single_coil(req.address, value).await
            }
            FunctionCode::WriteSingleRegister => {
                let value = req.values.first().copied().unwrap_or(0);
                self.write_single_register(req.address, value).await
            }
            FunctionCode::WriteMultipleCoils => {
                let values: Vec<bool> = req.values.iter().map(|&v| v != 0).collect();
                self.write_multiple_coils(req.address, &values).await
            }
            FunctionCode::WriteMultipleRegisters => {
                self.write_multiple_registers(req.address, &req.values).await
            }
            _ => Err(ModbusAppError::InvalidParam("不是写入功能码".to_string())),
        }
    }

    /// 获取连接描述
    pub fn connection_description(&self) -> &str {
        &self.mode
    }
}

7、modbus_server.rs

rust 复制代码
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::Mutex as TokioMutex;
use tokio_serial::SerialStream;

use crate::comm_log::CommLog;
use crate::statistics::CommStatistics;

/// CRC16 Modbus 计算
fn crc16(data: &[u8]) -> u16 {
    let mut crc: u16 = 0xFFFF;
    for &byte in data {
        crc ^= byte as u16;
        for _ in 0..8 {
            if crc & 1 != 0 {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    crc
}

/// 寄存器组
#[derive(Clone)]
pub struct RegisterBank {
    pub coils: Vec<bool>,
    pub discrete_inputs: Vec<bool>,
    pub holding_registers: Vec<u16>,
    pub input_registers: Vec<u16>,
    pub slave_id: u8,
    pub version: u64,
}

impl RegisterBank {
    pub fn new(slave_id: u8, reg_count: usize) -> Self {
        Self {
            coils: vec![false; reg_count],
            discrete_inputs: vec![false; reg_count],
            holding_registers: vec![0; reg_count],
            input_registers: vec![0; reg_count],
            slave_id,
            version: 0,
        }
    }

    pub fn resize(&mut self, count: usize) {
        self.coils.resize(count, false);
        self.discrete_inputs.resize(count, false);
        self.holding_registers.resize(count, 0);
        self.input_registers.resize(count, 0);
    }
}

/// 从机服务端
pub struct ModbusSlaveServer {
    serial: SerialStream,
    bank: Arc<TokioMutex<RegisterBank>>,
    log: Arc<TokioMutex<CommLog>>,
    stats: Arc<TokioMutex<CommStatistics>>,
}

impl ModbusSlaveServer {
    pub fn new(
        serial: SerialStream,
        bank: Arc<TokioMutex<RegisterBank>>,
        log: Arc<TokioMutex<CommLog>>,
        stats: Arc<TokioMutex<CommStatistics>>,
    ) -> Self {
        Self { serial, bank, log, stats }
    }

    /// 运行从机服务循环
    pub async fn run(&mut self) -> std::io::Result<()> {
        let mut buf = vec![0u8; 256];
        let mut frame_buf = Vec::with_capacity(256);
        let mut last_activity = std::time::Instant::now();

        loop {
            let n = match tokio::time::timeout(
                std::time::Duration::from_millis(100),
                self.serial.read(&mut buf),
            ).await {
                Ok(Ok(0)) => return Ok(()),
                Ok(Ok(n)) => n,
                Ok(Err(e)) => return Err(e),
                Err(_) => {
                    // 超时 - 检查帧间隔(RTU 3.5字符时间)
                    if !frame_buf.is_empty() && last_activity.elapsed() > std::time::Duration::from_millis(50) {
                        self.process_frame(&frame_buf).await;
                        frame_buf.clear();
                    }
                    continue;
                }
            };

            last_activity = std::time::Instant::now();
            frame_buf.extend_from_slice(&buf[..n]);

            // 帧结束检测:超过 50ms 无新数据视为一帧
            // 这里简化处理,收到数据后立即尝试处理完整帧
            if frame_buf.len() >= 4 {
                // 最小 Modbus RTU 帧长度 = 地址(1) + 功能码(1) + CRC(2) = 4
                let len = frame_buf.len();
                let received_crc = u16::from_le_bytes([frame_buf[len - 2], frame_buf[len - 1]]);
                let calculated_crc = crc16(&frame_buf[..len - 2]);

                if received_crc == calculated_crc {
                    self.process_frame(&frame_buf).await;
                    frame_buf.clear();
                } else if len > 256 {
                    // 帧过长,丢弃
                    frame_buf.clear();
                }
            }
        }
    }

    async fn process_frame(&mut self, frame: &[u8]) {
        if frame.len() < 4 {
            return;
        }

        let slave_addr = frame[0];
        let function_code = frame[1];

        // 检查是否是本从机的地址(或广播地址0)
        let bank = self.bank.lock().await;
        if slave_addr != bank.slave_id && slave_addr != 0 {
            return;
        }
        let my_slave_id = bank.slave_id;
        drop(bank);

        // 记录接收
        let rx_hex: String = frame.iter().map(|b| format!("{:02X}", b)).collect::<Vec<_>>().join(" ");
        {
            let mut log = self.log.lock().await;
            log.log_rx(&format!("← {}", rx_hex));
        }
        {
            let mut stats = self.stats.lock().await;
            stats.record_rx();
        }

        // 解析并处理请求
        let response = match function_code {
            0x01 => self.handle_read_coils(frame).await,
            0x02 => self.handle_read_discrete_inputs(frame).await,
            0x03 => self.handle_read_holding_registers(frame).await,
            0x04 => self.handle_read_input_registers(frame).await,
            0x05 => self.handle_write_single_coil(frame).await,
            0x06 => self.handle_write_single_register(frame).await,
            0x0F => self.handle_write_multiple_coils(frame).await,
            0x10 => self.handle_write_multiple_registers(frame).await,
            _ => {
                // 不支持的功能码 - 异常响应
                Some(vec![my_slave_id, function_code | 0x80, 0x01])
            }
        };

        if let Some(resp) = response {
            // 计算CRC并发送
            let crc = crc16(&resp);
            let mut frame = resp;
            frame.extend_from_slice(&crc.to_le_bytes());

            let tx_hex: String = frame.iter().map(|b| format!("{:02X}", b)).collect::<Vec<_>>().join(" ");
            {
                let mut log = self.log.lock().await;
                log.log_tx(&format!("→ {}", tx_hex));
            }
            {
                let mut stats = self.stats.lock().await;
                stats.record_tx();
            }

            if let Err(e) = self.serial.write_all(&frame).await {
                let mut log = self.log.lock().await;
                log.log_error(&format!("发送失败: {}", e));
            }
            let _ = self.serial.flush().await;
        }
    }

    /// FC01: 读线圈
    async fn handle_read_coils(&self, frame: &[u8]) -> Option<Vec<u8>> {
        if frame.len() < 8 { return None; }
        let bank = self.bank.lock().await;
        let start = u16::from_be_bytes([frame[2], frame[3]]) as usize;
        let count = u16::from_be_bytes([frame[4], frame[5]]) as usize;

        if start + count > bank.coils.len() {
            return Some(vec![bank.slave_id, 0x81, 0x02]); // 异常:地址越界
        }

        let byte_count = (count + 7) / 8;
        let mut data = vec![0u8; byte_count];
        for i in 0..count {
            if bank.coils[start + i] {
                data[i / 8] |= 1 << (i % 8);
            }
        }

        let mut resp = vec![bank.slave_id, 0x01, byte_count as u8];
        resp.extend_from_slice(&data);
        Some(resp)
    }

    /// FC02: 读离散输入
    async fn handle_read_discrete_inputs(&self, frame: &[u8]) -> Option<Vec<u8>> {
        if frame.len() < 8 { return None; }
        let bank = self.bank.lock().await;
        let start = u16::from_be_bytes([frame[2], frame[3]]) as usize;
        let count = u16::from_be_bytes([frame[4], frame[5]]) as usize;

        if start + count > bank.discrete_inputs.len() {
            return Some(vec![bank.slave_id, 0x82, 0x02]);
        }

        let byte_count = (count + 7) / 8;
        let mut data = vec![0u8; byte_count];
        for i in 0..count {
            if bank.discrete_inputs[start + i] {
                data[i / 8] |= 1 << (i % 8);
            }
        }

        let mut resp = vec![bank.slave_id, 0x02, byte_count as u8];
        resp.extend_from_slice(&data);
        Some(resp)
    }

    /// FC03: 读保持寄存器
    async fn handle_read_holding_registers(&self, frame: &[u8]) -> Option<Vec<u8>> {
        if frame.len() < 8 { return None; }
        let bank = self.bank.lock().await;
        let start = u16::from_be_bytes([frame[2], frame[3]]) as usize;
        let count = u16::from_be_bytes([frame[4], frame[5]]) as usize;

        if start + count > bank.holding_registers.len() {
            return Some(vec![bank.slave_id, 0x83, 0x02]);
        }

        let byte_count = count * 2;
        let mut resp = vec![bank.slave_id, 0x03, byte_count as u8];
        for i in 0..count {
            let val = bank.holding_registers[start + i];
            resp.extend_from_slice(&val.to_be_bytes());
        }
        Some(resp)
    }

    /// FC04: 读输入寄存器
    async fn handle_read_input_registers(&self, frame: &[u8]) -> Option<Vec<u8>> {
        if frame.len() < 8 { return None; }
        let bank = self.bank.lock().await;
        let start = u16::from_be_bytes([frame[2], frame[3]]) as usize;
        let count = u16::from_be_bytes([frame[4], frame[5]]) as usize;

        if start + count > bank.input_registers.len() {
            return Some(vec![bank.slave_id, 0x84, 0x02]);
        }

        let byte_count = count * 2;
        let mut resp = vec![bank.slave_id, 0x04, byte_count as u8];
        for i in 0..count {
            let val = bank.input_registers[start + i];
            resp.extend_from_slice(&val.to_be_bytes());
        }
        Some(resp)
    }

    /// FC05: 写单个线圈
    async fn handle_write_single_coil(&self, frame: &[u8]) -> Option<Vec<u8>> {
        if frame.len() < 8 { return None; }
        let mut bank = self.bank.lock().await;
        let addr = u16::from_be_bytes([frame[2], frame[3]]) as usize;
        let value = u16::from_be_bytes([frame[4], frame[5]]);

        if addr >= bank.coils.len() {
            return Some(vec![bank.slave_id, 0x85, 0x02]);
        }

        bank.coils[addr] = value == 0xFF00;
        bank.version = bank.version.wrapping_add(1);
        // 回显请求
        Some(vec![bank.slave_id, 0x05, frame[2], frame[3], frame[4], frame[5]])
    }

    /// FC06: 写单个寄存器
    async fn handle_write_single_register(&self, frame: &[u8]) -> Option<Vec<u8>> {
        if frame.len() < 8 { return None; }
        let mut bank = self.bank.lock().await;
        let addr = u16::from_be_bytes([frame[2], frame[3]]) as usize;
        let value = u16::from_be_bytes([frame[4], frame[5]]);

        if addr >= bank.holding_registers.len() {
            return Some(vec![bank.slave_id, 0x86, 0x02]);
        }

        bank.holding_registers[addr] = value;
        bank.version = bank.version.wrapping_add(1);
        Some(vec![bank.slave_id, 0x06, frame[2], frame[3], frame[4], frame[5]])
    }

    /// FC0F: 写多个线圈
    async fn handle_write_multiple_coils(&self, frame: &[u8]) -> Option<Vec<u8>> {
        if frame.len() < 9 { return None; }
        let mut bank = self.bank.lock().await;
        let start = u16::from_be_bytes([frame[2], frame[3]]) as usize;
        let count = u16::from_be_bytes([frame[4], frame[5]]) as usize;
        let byte_count = frame[6] as usize;

        if start + count > bank.coils.len() || frame.len() < 7 + byte_count {
            return Some(vec![bank.slave_id, 0x8F, 0x02]);
        }

        for i in 0..count {
            let byte_idx = 7 + i / 8;
            let bit_idx = i % 8;
            if byte_idx < frame.len() {
                bank.coils[start + i] = (frame[byte_idx] >> bit_idx) & 1 != 0;
            }
        }
        bank.version = bank.version.wrapping_add(1);

        Some(vec![bank.slave_id, 0x0F, frame[2], frame[3], frame[4], frame[5]])
    }

    /// FC10: 写多个寄存器
    async fn handle_write_multiple_registers(&self, frame: &[u8]) -> Option<Vec<u8>> {
        if frame.len() < 9 { return None; }
        let mut bank = self.bank.lock().await;
        let start = u16::from_be_bytes([frame[2], frame[3]]) as usize;
        let count = u16::from_be_bytes([frame[4], frame[5]]) as usize;
        let byte_count = frame[6] as usize;

        if start + count > bank.holding_registers.len() || frame.len() < 7 + byte_count {
            return Some(vec![bank.slave_id, 0x90, 0x02]);
        }

        for i in 0..count {
            let offset = 7 + i * 2;
            if offset + 1 < frame.len() {
                bank.holding_registers[start + i] = u16::from_be_bytes([frame[offset], frame[offset + 1]]);
            }
        }
        bank.version = bank.version.wrapping_add(1);

        Some(vec![bank.slave_id, 0x10, frame[2], frame[3], frame[4], frame[5]])
    }
}

8、modbus_types.rs

rust 复制代码
use thiserror::Error;

/// Modbus 功能码
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FunctionCode {
    ReadCoils = 0x01,
    ReadDiscreteInputs = 0x02,
    ReadHoldingRegisters = 0x03,
    ReadInputRegisters = 0x04,
    WriteSingleCoil = 0x05,
    WriteSingleRegister = 0x06,
    WriteMultipleCoils = 0x0F,
    WriteMultipleRegisters = 0x10,
}

impl FunctionCode {
    pub fn from_u8(v: u8) -> Option<Self> {
        match v {
            0x01 => Some(Self::ReadCoils),
            0x02 => Some(Self::ReadDiscreteInputs),
            0x03 => Some(Self::ReadHoldingRegisters),
            0x04 => Some(Self::ReadInputRegisters),
            0x05 => Some(Self::WriteSingleCoil),
            0x06 => Some(Self::WriteSingleRegister),
            0x0F => Some(Self::WriteMultipleCoils),
            0x10 => Some(Self::WriteMultipleRegisters),
            _ => None,
        }
    }

    pub fn is_read(&self) -> bool {
        matches!(
            self,
            Self::ReadCoils
                | Self::ReadDiscreteInputs
                | Self::ReadHoldingRegisters
                | Self::ReadInputRegisters
        )
    }

    pub fn is_coil(&self) -> bool {
        matches!(self, Self::ReadCoils | Self::ReadDiscreteInputs | Self::WriteSingleCoil | Self::WriteMultipleCoils)
    }

    pub fn display_name(&self) -> &'static str {
        match self {
            Self::ReadCoils => "01 读线圈 (Read Coils)",
            Self::ReadDiscreteInputs => "02 读离散输入 (Read Discrete Inputs)",
            Self::ReadHoldingRegisters => "03 读保持寄存器 (Read Holding Registers)",
            Self::ReadInputRegisters => "04 读输入寄存器 (Read Input Registers)",
            Self::WriteSingleCoil => "05 写单个线圈 (Write Single Coil)",
            Self::WriteSingleRegister => "06 写单个寄存器 (Write Single Register)",
            Self::WriteMultipleCoils => "15 写多个线圈 (Write Multiple Coils)",
            Self::WriteMultipleRegisters => "16 写多个寄存器 (Write Multiple Registers)",
        }
    }

    /// 用于 ComboBox 显示
    pub fn short_name(&self) -> &'static str {
        match self {
            Self::ReadCoils => "01 - Read Coils",
            Self::ReadDiscreteInputs => "02 - Read Discrete Inputs",
            Self::ReadHoldingRegisters => "03 - Read Holding Registers",
            Self::ReadInputRegisters => "04 - Read Input Registers",
            Self::WriteSingleCoil => "05 - Write Single Coil",
            Self::WriteSingleRegister => "06 - Write Single Register",
            Self::WriteMultipleCoils => "15 - Write Multiple Coils",
            Self::WriteMultipleRegisters => "16 - Write Multiple Registers",
        }
    }
}

/// 连接模式
#[derive(Debug, Clone)]
pub enum ConnectionMode {
    Tcp {
        ip: String,
        port: u16,
    },
    Rtu {
        port: String,
        baud_rate: u32,
        data_bits: u8,
        stop_bits: u8,
        parity: ParityMode,
    },
}

/// 校验模式
#[derive(Debug, Clone, Copy)]
pub enum ParityMode {
    None,
    Even,
    Odd,
}

impl std::fmt::Display for ParityMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ParityMode::None => write!(f, "N"),
            ParityMode::Even => write!(f, "E"),
            ParityMode::Odd => write!(f, "O"),
        }
    }
}

/// Modbus 错误类型
#[derive(Debug, Error)]
pub enum ModbusAppError {
    #[error("连接错误: {0}")]
    ConnectionError(String),

    #[error("通信超时")]
    Timeout,

    #[error("协议错误: {0}")]
    ProtocolError(String),

    #[error("IO 错误: {0}")]
    IoError(#[from] std::io::Error),

    #[error("未连接")]
    NotConnected,

    #[error("参数错误: {0}")]
    InvalidParam(String),
}

/// 读取结果
#[derive(Debug, Clone)]
pub enum ReadResult {
    Registers(Vec<u16>),
    Coils(Vec<bool>),
}

/// 读取请求参数
#[derive(Debug, Clone)]
pub struct ReadRequest {
    pub function_code: FunctionCode,
    pub slave_id: u8,
    pub start_address: u16,
    pub quantity: u16,
}

/// 写入请求参数
#[derive(Debug, Clone)]
pub struct WriteRequest {
    pub function_code: FunctionCode,
    pub slave_id: u8,
    pub address: u16,
    pub values: Vec<u16>,
}

9、statistics.rs

rust 复制代码
/// 通信统计
#[derive(Clone, Default)]
pub struct CommStatistics {
    pub tx_count: i32,
    pub rx_count: i32,
    pub error_count: i32,
    pub last_error: String,
}

impl CommStatistics {
    pub fn new() -> Self {
        Self {
            tx_count: 0,
            rx_count: 0,
            error_count: 0,
            last_error: String::new(),
        }
    }

    pub fn record_tx(&mut self) {
        self.tx_count += 1;
    }

    pub fn record_rx(&mut self) {
        self.rx_count += 1;
    }

    pub fn record_error(&mut self, error: &str) {
        self.error_count += 1;
        self.last_error = error.to_string();
    }

    pub fn reset(&mut self) {
        self.tx_count = 0;
        self.rx_count = 0;
        self.error_count = 0;
        self.last_error.clear();
    }
}

10、main.rs

rust 复制代码
slint::include_modules!();

mod modbus_types;
mod modbus_client;
mod modbus_server;
mod data_model;
mod comm_log;
mod statistics;

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::Mutex as TokioMutex;
use tokio::task::JoinHandle;

use modbus_types::*;
use modbus_server::{RegisterBank, ModbusSlaveServer};
use data_model::RegisterModel;
use comm_log::CommLog;
use statistics::CommStatistics;

/// 应用全局状态
struct AppState {
    server_handle: Arc<TokioMutex<Option<JoinHandle<()>>>>,
    bank: Arc<TokioMutex<RegisterBank>>,
    log: Arc<TokioMutex<CommLog>>,
    stats: Arc<TokioMutex<CommStatistics>>,
    cancel: Arc<AtomicBool>,
}

impl AppState {
    fn new() -> Self {
        Self {
            server_handle: Arc::new(TokioMutex::new(None)),
            bank: Arc::new(TokioMutex::new(RegisterBank::new(1, 100))),
            log: Arc::new(TokioMutex::new(CommLog::new())),
            stats: Arc::new(TokioMutex::new(CommStatistics::new())),
            cancel: Arc::new(AtomicBool::new(false)),
        }
    }
}

/// 将寄存器数据更新到 UI(默认显示保持寄存器)
fn update_ui_registers(ui: &MainWindow, bank: &RegisterBank) {
    update_ui_registers_with_type(ui, bank, 2); // 2 = 保持寄存器
}

/// 根据寄存器类型更新 UI
fn update_ui_registers_with_type(ui: &MainWindow, bank: &RegisterBank, type_index: i32) {
    let rows: Vec<RegisterRow> = match type_index {
        0 => {
            // 线圈 (Coils)
            bank.coils.iter().enumerate().map(|(i, &val)| {
                let raw = if val { 1u16 } else { 0u16 };
                RegisterRow {
                    address: i as i32,
                    value_unsigned: raw.to_string().into(),
                    value_signed: raw.to_string().into(),
                    value_hex: format!("{:04X}", raw).into(),
                    value_binary: format!("{:016b}", raw).into(),
                    value_float: "-".into(),
                    value_ascii: if val { "ON" } else { "OFF" }.into(),
                }
            }).collect()
        }
        1 => {
            // 离散输入 (Discrete Inputs)
            bank.discrete_inputs.iter().enumerate().map(|(i, &val)| {
                let raw = if val { 1u16 } else { 0u16 };
                RegisterRow {
                    address: i as i32,
                    value_unsigned: raw.to_string().into(),
                    value_signed: raw.to_string().into(),
                    value_hex: format!("{:04X}", raw).into(),
                    value_binary: format!("{:016b}", raw).into(),
                    value_float: "-".into(),
                    value_ascii: if val { "ON" } else { "OFF" }.into(),
                }
            }).collect()
        }
        2 => {
            // 保持寄存器 (Holding Registers)
            bank.holding_registers.iter().enumerate().map(|(i, &raw)| {
                let unsigned_val = raw;
                let signed_val = raw as i16;
                let hex_val = format!("{:04X}", raw);
                let binary_val = format!("{:016b}", raw);

                // IEEE 754 浮点(需要两个寄存器)
                let float_val = if i + 1 < bank.holding_registers.len() {
                    let next_raw = bank.holding_registers[i + 1];
                    let bits = ((raw as u32) << 16) | (next_raw as u32);
                    let float = f32::from_bits(bits);
                    if float.is_finite() && float.abs() < 1e10 && (float == 0.0 || float.abs() >= 1e-10) {
                        format!("{:.4}", float)
                    } else {
                        format!("{:e}", float)
                    }
                } else {
                    "-".to_string()
                };

                // ASCII
                let ascii_val = {
                    let hi = (raw >> 8) as u8;
                    let lo = (raw & 0xFF) as u8;
                    let hi_char = if hi >= 0x20 && hi <= 0x7E { hi as char } else { '.' };
                    let lo_char = if lo >= 0x20 && lo <= 0x7E { lo as char } else { '.' };
                    format!("{}{}", hi_char, lo_char)
                };

                RegisterRow {
                    address: i as i32,
                    value_unsigned: unsigned_val.to_string().into(),
                    value_signed: signed_val.to_string().into(),
                    value_hex: hex_val.into(),
                    value_binary: binary_val.into(),
                    value_float: float_val.into(),
                    value_ascii: ascii_val.into(),
                }
            }).collect()
        }
        3 => {
            // 输入寄存器 (Input Registers)
            bank.input_registers.iter().enumerate().map(|(i, &raw)| {
                let unsigned_val = raw;
                let signed_val = raw as i16;
                let hex_val = format!("{:04X}", raw);
                let binary_val = format!("{:016b}", raw);

                let float_val = if i + 1 < bank.input_registers.len() {
                    let next_raw = bank.input_registers[i + 1];
                    let bits = ((raw as u32) << 16) | (next_raw as u32);
                    let float = f32::from_bits(bits);
                    if float.is_finite() && float.abs() < 1e10 && (float == 0.0 || float.abs() >= 1e-10) {
                        format!("{:.4}", float)
                    } else {
                        format!("{:e}", float)
                    }
                } else {
                    "-".to_string()
                };

                let ascii_val = {
                    let hi = (raw >> 8) as u8;
                    let lo = (raw & 0xFF) as u8;
                    let hi_char = if hi >= 0x20 && hi <= 0x7E { hi as char } else { '.' };
                    let lo_char = if lo >= 0x20 && lo <= 0x7E { lo as char } else { '.' };
                    format!("{}{}", hi_char, lo_char)
                };

                RegisterRow {
                    address: i as i32,
                    value_unsigned: unsigned_val.to_string().into(),
                    value_signed: signed_val.to_string().into(),
                    value_hex: hex_val.into(),
                    value_binary: binary_val.into(),
                    value_float: float_val.into(),
                    value_ascii: ascii_val.into(),
                }
            }).collect()
        }
        _ => vec![],
    };

    ui.set_registers(slint::ModelRc::new(slint::VecModel::from(rows)));
}

/// 将日志更新到 UI
fn update_ui_log(ui: &MainWindow, log: &CommLog) {
    let entries: Vec<LogEntry> = log
        .entries()
        .iter()
        .map(|e| LogEntry {
            timestamp: e.timestamp.clone().into(),
            direction: e.direction,
            raw_text: e.raw_text.clone().into(),
        })
        .collect();

    ui.set_log_entries(slint::ModelRc::new(slint::VecModel::from(entries)));
}

fn main() {
    env_logger::init();
    log::info!("Modbus RTU 从机调试助手启动");

    let runtime = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
    let _enter = runtime.enter();

    let ui = MainWindow::new().expect("Failed to create main window");
    let state = AppState::new();

    // 串口参数选项
    let baud_rates: Vec<slint::SharedString> = vec![
        "1200".into(), "2400".into(), "4800".into(), "9600".into(),
        "19200".into(), "38400".into(), "57600".into(), "115200".into(),
    ];
    let data_bits_list: Vec<slint::SharedString> = vec!["5".into(), "6".into(), "7".into(), "8".into()];
    let stop_bits_list: Vec<slint::SharedString> = vec!["1".into(), "2".into()];
    let parity_list: Vec<slint::SharedString> = vec!["None".into(), "Even".into(), "Odd".into()];

    // 枚举可用串口
    let available_ports = serialport::available_ports().unwrap_or(vec![]);
    let port_names: Vec<slint::SharedString> = if available_ports.is_empty() {
        vec!["COM1".into(), "COM2".into(), "COM3".into()]
    } else {
        available_ports.iter().map(|p| p.port_name.as_str().into()).collect()
    };

    // ===== 注册回调 =====
    register_cell_edit_callback(&ui, &state);
    register_type_change_callback(&ui, &state);

    // 设置寄存器类型列表
    // ui.set_register_types(slint::ModelRc::new(slint::VecModel::from(register_types)));

    // 初始化显示寄存器表格(默认显示保持寄存器)
    {
        let bank_snapshot = state.bank.try_lock().unwrap().clone();
        update_ui_registers_with_type(&ui, &bank_snapshot, 2);
    }

    let baud_rates_for_dialog = baud_rates.clone();
    let data_bits_for_dialog = data_bits_list.clone();
    let stop_bits_for_dialog = stop_bits_list.clone();
    let parity_for_dialog = parity_list.clone();
    let ports_for_dialog = port_names.clone();

    // 启动按钮回调 → 显示从机连接对话框
    {
        let ui_weak = ui.as_weak();
        let state_handle = state.server_handle.clone();
        let state_bank = state.bank.clone();
        let state_log = state.log.clone();
        let state_stats = state.stats.clone();
        let state_cancel = state.cancel.clone();
        let ports = ports_for_dialog.clone();
        let baud_rates = baud_rates_for_dialog.clone();
        let data_bits_list = data_bits_for_dialog.clone();
        let stop_bits_list = stop_bits_for_dialog.clone();
        let parity_list = parity_for_dialog.clone();

        ui.on_on_connect(move || {
            show_connection_dialog(
                &ui_weak,
                &state_handle,
                &state_bank,
                &state_log,
                &state_stats,
                &state_cancel,
                &ports,
                &baud_rates,
                &data_bits_list,
                &stop_bits_list,
                &parity_list,
            );
        });
    }

    // 停止按钮
    {
        let ui_weak = ui.as_weak();
        let state_handle = state.server_handle.clone();
        let state_log = state.log.clone();
        let state_cancel = state.cancel.clone();

        ui.on_on_disconnect(move || {
            let ui_weak = ui_weak.clone();
            let handle = state_handle.clone();
            let log = state_log.clone();
            let cancel = state_cancel.clone();

            slint::invoke_from_event_loop(move || {
                if let Some(ui) = ui_weak.upgrade() {
                    cancel.store(true, Ordering::SeqCst);

                    // 终止服务器任务
                    let handle_clone = handle.clone();
                    tokio::spawn(async move {
                        let mut guard = handle_clone.lock().await;
                        if let Some(h) = guard.take() {
                            h.abort();
                        }
                    });

                    ui.set_connected(false);
                    ui.set_connection_mode("".into());
                    ui.set_connection_info("".into());

                    if let Ok(mut log_guard) = log.try_lock() {
                        log_guard.log_error("已停止从机服务");
                        let snapshot = (*log_guard).clone();
                        drop(log_guard);
                        update_ui_log(&ui, &snapshot);
                    }
                }
            })
            .ok();
        });
    }

    // 清空日志
    {
        let ui_weak = ui.as_weak();
        let state_log = state.log.clone();

        ui.on_on_clear_log(move || {
            let ui_weak = ui_weak.clone();
            let log = state_log.clone();

            slint::invoke_from_event_loop(move || {
                if let Some(ui) = ui_weak.upgrade() {
                    if let Ok(mut log_guard) = log.try_lock() {
                        log_guard.clear();
                        let snapshot = (*log_guard).clone();
                        drop(log_guard);
                        update_ui_log(&ui, &snapshot);
                    }
                }
            })
            .ok();
        });
    }

    log::info!("UI 初始化完成,启动事件循环");
    ui.run().expect("Failed to run event loop");
}

// ============================================================
// 寄存器类型切换回调
// ============================================================

fn register_type_change_callback(ui: &MainWindow, state: &AppState) {
    let ui_weak = ui.as_weak();
    let state_bank = state.bank.clone();

    ui.on_on_register_type_changed(move |type_index| {
        let ui_weak = ui_weak.clone();
        let bank = state_bank.clone();

        slint::invoke_from_event_loop(move || {
            if let Some(ui) = ui_weak.upgrade() {
                let bank_snapshot = bank.try_lock().unwrap().clone();
                update_ui_registers_with_type(&ui, &bank_snapshot, type_index);
            }
        })
        .ok();
    });
}

// ============================================================
// 单元格编辑回调
// ============================================================

fn register_cell_edit_callback(ui: &MainWindow, state: &AppState) {
    let ui_weak = ui.as_weak();
    let state_bank = state.bank.clone();

    ui.on_on_cell_edit(move |address, value| {
        let ui_weak = ui_weak.clone();
        let bank = state_bank.clone();
        let value_str = value.to_string();

        slint::invoke_from_event_loop(move || {
            let addr = address as usize;
            let parsed = data_model::format_utils::parse_value(&value_str);
            let val = match parsed {
                Ok(v) => v,
                Err(_) => return,
            };

            // 获取当前寄存器类型
            let type_index = ui_weak.upgrade().map(|ui| ui.get_register_type_index()).unwrap_or(2);

            // 根据类型更新对应的寄存器
            {
                let mut guard = bank.try_lock().unwrap();
                match type_index {
                    0 => {
                        // 线圈
                        if addr < guard.coils.len() {
                            guard.coils[addr] = val != 0;
                            guard.version = guard.version.wrapping_add(1);
                        }
                    }
                    1 => {
                        // 离散输入
                        if addr < guard.discrete_inputs.len() {
                            guard.discrete_inputs[addr] = val != 0;
                            guard.version = guard.version.wrapping_add(1);
                        }
                    }
                    2 => {
                        // 保持寄存器
                        if addr < guard.holding_registers.len() {
                            guard.holding_registers[addr] = val;
                            guard.version = guard.version.wrapping_add(1);
                        }
                    }
                    3 => {
                        // 输入寄存器
                        if addr < guard.input_registers.len() {
                            guard.input_registers[addr] = val;
                            guard.version = guard.version.wrapping_add(1);
                        }
                    }
                    _ => return,
                }
            }

            // 刷新UI
            if let Some(ui) = ui_weak.upgrade() {
                let bank_snapshot = bank.try_lock().unwrap().clone();
                update_ui_registers_with_type(&ui, &bank_snapshot, type_index);
            }
        })
        .ok();
    });
}

// ============================================================
// 连接对话框
// ============================================================

fn show_connection_dialog(
    ui_weak: &slint::Weak<MainWindow>,
    server_handle: &Arc<TokioMutex<Option<JoinHandle<()>>>>,
    bank: &Arc<TokioMutex<RegisterBank>>,
    log: &Arc<TokioMutex<CommLog>>,
    stats: &Arc<TokioMutex<CommStatistics>>,
    cancel: &Arc<AtomicBool>,
    port_names: &[slint::SharedString],
    baud_rates: &[slint::SharedString],
    data_bits_list: &[slint::SharedString],
    stop_bits_list: &[slint::SharedString],
    parity_list: &[slint::SharedString],
) {
    let dialog = RtuSetupDialog::new().expect("Failed to create RTU dialog");
    dialog.set_port_names(slint::ModelRc::new(slint::VecModel::from(port_names.to_vec())));
    dialog.set_baud_rates(slint::ModelRc::new(slint::VecModel::from(baud_rates.to_vec())));
    dialog.set_data_bits_list(slint::ModelRc::new(slint::VecModel::from(data_bits_list.to_vec())));
    dialog.set_stop_bits_list(slint::ModelRc::new(slint::VecModel::from(stop_bits_list.to_vec())));
    dialog.set_parity_list(slint::ModelRc::new(slint::VecModel::from(parity_list.to_vec())));
    dialog.set_port_index(0);
    dialog.set_baud_index(3); // 9600
    dialog.set_data_bits_index(3); // 8
    dialog.set_stop_bits_index(0); // 1
    dialog.set_parity_index(0); // None
    dialog.set_slave_id(1);
    dialog.set_register_count(100);

    let ui_weak = ui_weak.clone();
    let server_handle = server_handle.clone();
    let bank = bank.clone();
    let log = log.clone();
    let stats = stats.clone();
    let cancel = cancel.clone();
    let port_names = port_names.to_vec();
    let baud_rates = baud_rates.to_vec();
    let data_bits_list = data_bits_list.to_vec();
    let stop_bits_list = stop_bits_list.to_vec();
    let parity_list = parity_list.to_vec();

    dialog.on_on_confirm({
        let dialog = dialog.as_weak();
        let ui_weak = ui_weak.clone();
        let server_handle = server_handle.clone();
        let bank = bank.clone();
        let log = log.clone();
        let stats = stats.clone();
        let cancel = cancel.clone();

        move || {
            if let Some(dlg) = dialog.upgrade() {
                let port_index = dlg.get_port_index() as usize;
                let baud_index = dlg.get_baud_index() as usize;
                let data_bits_index = dlg.get_data_bits_index() as usize;
                let stop_bits_index = dlg.get_stop_bits_index() as usize;
                let parity_index = dlg.get_parity_index() as usize;
                let slave_id = dlg.get_slave_id() as u8;
                let register_count = dlg.get_register_count() as usize;

                let port_name = port_names.get(port_index).map(|s| s.to_string()).unwrap_or_else(|| "COM1".to_string());
                let baud_rate: u32 = baud_rates.get(baud_index).map(|s| s.parse().unwrap_or(9600)).unwrap_or(9600);
                let data_bits: u8 = data_bits_list.get(data_bits_index).map(|s| s.parse().unwrap_or(8)).unwrap_or(8);
                let stop_bits: u8 = stop_bits_list.get(stop_bits_index).map(|s| s.parse().unwrap_or(1)).unwrap_or(1);
                let parity = match parity_index {
                    1 => ParityMode::Even,
                    2 => ParityMode::Odd,
                    _ => ParityMode::None,
                };

                let ui_weak = ui_weak.clone();
                let server_handle = server_handle.clone();
                let bank = bank.clone();
                let log = log.clone();
                let stats = stats.clone();
                let cancel = cancel.clone();

                let dialog_weak = dialog.clone();
                tokio::spawn(async move {
                    // 打开串口
                    let parity_mode = match parity {
                        ParityMode::None => tokio_serial::Parity::None,
                        ParityMode::Even => tokio_serial::Parity::Even,
                        ParityMode::Odd => tokio_serial::Parity::Odd,
                    };
                    let data_bits_mode = match data_bits {
                        5 => tokio_serial::DataBits::Five,
                        6 => tokio_serial::DataBits::Six,
                        7 => tokio_serial::DataBits::Seven,
                        _ => tokio_serial::DataBits::Eight,
                    };
                    let stop_bits_mode = match stop_bits {
                        1 => tokio_serial::StopBits::One,
                        _ => tokio_serial::StopBits::Two,
                    };

                    let builder = tokio_serial::new(&port_name, baud_rate)
                        .data_bits(data_bits_mode)
                        .stop_bits(stop_bits_mode)
                        .parity(parity_mode);

                    let serial_result = tokio_serial::SerialStream::open(&builder);

                    let serial = match serial_result {
                        Ok(s) => s,
                        Err(e) => {
                            let err_msg = format!("串口打开失败: {}", e);
                            slint::invoke_from_event_loop(move || {
                                if let Some(ui) = ui_weak.upgrade() {
                                    ui.set_last_error(err_msg.clone().into());
                                    if let Ok(mut log_guard) = log.try_lock() {
                                        log_guard.log_error(&err_msg);
                                        let snapshot = (*log_guard).clone();
                                        drop(log_guard);
                                        update_ui_log(&ui, &snapshot);
                                    }
                                }
                            }).ok();
                            return;
                        }
                    };

                    // 设置寄存器数量
                    {
                        let mut bank_guard = bank.lock().await;
                        bank_guard.slave_id = slave_id;
                        bank_guard.resize(register_count);
                    }

                    cancel.store(false, Ordering::SeqCst);

                    // 启动从机服务
                    let mut server = ModbusSlaveServer::new(serial, bank.clone(), log.clone(), stats.clone());
                    let handle = tokio::spawn(async move {
                        let _ = server.run().await;
                    });

                    {
                        let mut guard = server_handle.lock().await;
                        *guard = Some(handle);
                    }

                    // 启动UI定时刷新
                    let ui_weak2 = ui_weak.clone();
                    let bank2 = bank.clone();
                    let stats2 = stats.clone();
                    let log2 = log.clone();
                    let cancel2 = cancel.clone();
                    tokio::spawn(async move {
                        let mut interval = tokio::time::interval(std::time::Duration::from_millis(500));
                        let mut last_bank_version: u64 = 0;
                        let mut last_log_version: u64 = 0;
                        let mut last_stats_tx: i32 = 0;
                        let mut last_stats_rx: i32 = 0;
                        let mut last_stats_err: i32 = 0;
                        loop {
                            interval.tick().await;
                            if cancel2.load(Ordering::SeqCst) {
                                break;
                            }

                            // 只读版本号判断是否需要更新
                            let (bank_version, log_version, stats_snapshot) = {
                                let bv = { let b = bank2.try_lock(); b.as_ref().map(|g| g.version).unwrap_or(0) };
                                let lv = { let l = log2.try_lock(); l.as_ref().map(|g| g.version()).unwrap_or(0) };
                                let ss = stats2.try_lock().map(|s| s.clone()).unwrap_or_default();
                                (bv, lv, ss)
                            };

                            let bank_changed = bank_version != last_bank_version;
                            let log_changed = log_version != last_log_version;
                            let stats_changed = stats_snapshot.tx_count != last_stats_tx
                                || stats_snapshot.rx_count != last_stats_rx
                                || stats_snapshot.error_count != last_stats_err;

                            if !bank_changed && !log_changed && !stats_changed {
                                continue;
                            }

                            // 只在需要时克隆数据
                            let bank_snap = if bank_changed {
                                Some(bank2.lock().await.clone())
                            } else {
                                None
                            };
                            let log_snap = if log_changed {
                                Some(log2.try_lock().map(|l| l.clone()).unwrap_or_default())
                            } else {
                                None
                            };

                            last_bank_version = bank_version;
                            last_log_version = log_version;
                            last_stats_tx = stats_snapshot.tx_count;
                            last_stats_rx = stats_snapshot.rx_count;
                            last_stats_err = stats_snapshot.error_count;

                            let ui_weak = ui_weak2.clone();
                            slint::invoke_from_event_loop(move || {
                                if let Some(ui) = ui_weak.upgrade() {
                                    if let Some(bank) = bank_snap {
                                        let type_index = ui.get_register_type_index();
                                        update_ui_registers_with_type(&ui, &bank, type_index);
                                    }
                                    if let Some(log) = log_snap {
                                        update_ui_log(&ui, &log);
                                    }
                                    if stats_changed {
                                        ui.set_tx_count(stats_snapshot.tx_count);
                                        ui.set_rx_count(stats_snapshot.rx_count);
                                        ui.set_error_count(stats_snapshot.error_count);
                                        ui.set_last_error(stats_snapshot.last_error.clone().into());
                                    }
                                }
                            }).ok();
                        }
                    });

                    slint::invoke_from_event_loop(move || {
                        if let Some(ui) = ui_weak.upgrade() {
                            ui.set_connected(true);
                            ui.set_connection_mode("RTU Slave".into());
                            ui.set_connection_info(
                                format!("{} {}-{}{}{} 从站:{}", port_name, baud_rate, data_bits, stop_bits, parity, slave_id).into(),
                            );
                            ui.set_slave_id(slave_id as i32);

                            if let Ok(mut log_guard) = log.try_lock() {
                                log_guard.log_tx(&format!(
                                    "RTU 从机启动 {} {}-{}{}{} 从站ID:{} 寄存器数:{}",
                                    port_name, baud_rate, data_bits, stop_bits, parity, slave_id, register_count
                                ));
                                let snapshot = (*log_guard).clone();
                                drop(log_guard);
                                update_ui_log(&ui, &snapshot);
                            }
                        }
                    }).ok();

                    // 关闭对话框
                    if let Some(dlg) = dialog_weak.upgrade() {
                        let _ = dlg.hide();
                    }
                });
            }
        }
    });

    dialog.on_on_cancel({
        let dialog = dialog.as_weak();
        move || {
            if let Some(dlg) = dialog.upgrade() {
                let _ = dlg.hide();
            }
        }
    });

    dialog.on_on_refresh_ports({
        let dialog = dialog.as_weak();
        move || {
            if let Some(dlg) = dialog.upgrade() {
                let available_ports = serialport::available_ports().unwrap_or(vec![]);
                let new_port_names: Vec<slint::SharedString> = if available_ports.is_empty() {
                    vec!["COM1".into(), "COM2".into(), "COM3".into()]
                } else {
                    available_ports.iter().map(|p| p.port_name.as_str().into()).collect()
                };
                dlg.set_port_names(slint::ModelRc::new(slint::VecModel::from(new_port_names)));
                dlg.set_port_index(0);
            }
        }
    });

    dialog.show().expect("Failed to show RTU dialog");
}

11、build.rs

rust 复制代码
fn main() {
    // 编译 Slint UI
    slint_build::compile("ui/main.slint").unwrap();
}

12、main.slint

rust 复制代码
import { Button, LineEdit, SpinBox, ComboBox, ScrollView } from "std-widgets.slint";

// ==================== 主题 ====================
export global Theme {
    in property <color> bg-primary: #0d1117;
    in property <color> bg-secondary: #161b22;
    in property <color> bg-tertiary: #1c2333;
    in property <color> bg-elevated: #21283b;
    in property <color> bg-hover: #292e3e;
    in property <color> bg-active: #333a4d;
    in property <color> border: #2a3040;
    in property <color> border-focus: #58a6ff;
    in property <color> text-primary: #e6edf3;
    in property <color> text-secondary: #8b949e;
    in property <color> text-muted: #636c76;
    in property <color> accent: #58a6ff;
    in property <color> success: #3fb950;
    in property <color> warning: #d29922;
    in property <color> error: #f85149;
    in property <color> info: #58a6ff;
    in property <color> tx-color: #58a6ff;
    in property <color> rx-color: #3fb950;
    in property <color> table-header-bg: #1c2333;
    in property <color> table-row-even: #0d1117;
    in property <color> table-row-odd: #111820;
    in property <length> spacing-xs: 4px;
    in property <length> spacing-sm: 8px;
    in property <length> spacing-md: 12px;
    in property <length> spacing-lg: 16px;
    in property <length> spacing-xl: 24px;
    in property <length> radius-sm: 4px;
    in property <length> font-size-sm: 12px;
    in property <length> font-size-md: 13px;
    in property <length> font-size-lg: 14px;
}

// ==================== 数据结构 ====================

// 寄存器类型切换按钮
export component RegisterTypeButton inherits Rectangle {
    in property <string> label;
    in property <bool> active: false;
    callback clicked;

    width: 120px;
    height: 28px;
    border-radius: 6px;
    border-width: 1px;
    border-color: active ? Theme.accent : Theme.border;
    background: active ? Theme.accent : (ta.has-hover ? Theme.bg-hover : Theme.bg-tertiary);

    HorizontalLayout {
        alignment: center;
        Text {
            text: label;
            color: active ? #ffffff : Theme.text-secondary;
            font-size: Theme.font-size-sm;
            font-weight: active ? 600 : 400;
            vertical-alignment: center;
            horizontal-alignment: center;
        }
    }

    ta := TouchArea {
        clicked => { root.clicked(); }
        mouse-cursor: pointer;
    }
}

export struct RegisterRow {
    address: int,
    value-unsigned: string,
    value-signed: string,
    value-hex: string,
    value-binary: string,
    value-float: string,
    value-ascii: string,
}

export struct LogEntry {
    timestamp: string,
    direction: int,
    raw-text: string,
}

// ==================== 基础组件(先定义,后使用) ====================

component Sep inherits Rectangle {
    width: 1px;
    background: Theme.border;
}

component HeaderCell inherits Rectangle {
    in property <string> text;
    background: transparent;
    border-width: 1px;
    border-color: Theme.border;
    Text { text: root.text; color: Theme.text-secondary; font-size: Theme.font-size-sm; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; }
}

component CellText inherits Rectangle {
    in property <string> text;
    in property <color> cell-color: Theme.text-primary;
    in property <length> cell-font-size: Theme.font-size-md;
    background: transparent;
    border-width: 1px;
    border-color: Theme.border;
    Text { text: root.text; color: root.cell-color; font-size: root.cell-font-size; horizontal-alignment: center; vertical-alignment: center; font-family: "monospace"; }
}

component EditableCell inherits Rectangle {
    in property <string> text;
    callback edited(string);
    private property <bool> editing;
    background: transparent;
    border-width: 1px;
    border-color: Theme.border;

    if editing == false : Text {
        text: root.text; color: Theme.text-primary; font-size: Theme.font-size-md;
        horizontal-alignment: center; vertical-alignment: center;
    }
    if editing : LineEdit {
        text: root.text;
        accepted(value) => { editing = false; root.edited(value); }
    }
    TouchArea {
        visible: editing == false;
        clicked => { editing = true; }
        mouse-cursor: pointer;
    }
}

component StatusItem inherits Rectangle {
    in property <string> label;
    in property <string> value;
    in property <color> value-color: Theme.text-primary;
    HorizontalLayout {
        spacing: 4px;
        Text { text: root.label + ":"; color: Theme.text-muted; font-size: Theme.font-size-sm; vertical-alignment: center; }
        Text { text: root.value; color: root.value-color; font-size: Theme.font-size-sm; font-weight: 600; vertical-alignment: center; }
    }
}

component ToolButton inherits Rectangle {
    in property <string> label;
    in property <bool> enabled: true;
    in property <color> btn-color: Theme.accent;
    callback clicked();
    private property <bool> is-hovered;
    height: 32px;
    width: 100px;
    background: is-hovered && enabled ? Theme.bg-hover : Theme.bg-tertiary;
    border-radius: Theme.radius-sm;
    border-width: 1px;
    border-color: enabled ? btn-color : Theme.border;
    Text { text: root.label; color: root.enabled ? Theme.text-primary : Theme.text-muted; font-size: Theme.font-size-sm; horizontal-alignment: center; vertical-alignment: center; }
    TouchArea {
        mouse-cursor: root.enabled ? pointer : default;
        clicked => { root.clicked(); }
        moved => { is-hovered = self.mouse-x >= 0 && self.mouse-x <= root.width && self.mouse-y >= 0 && self.mouse-y <= root.height; }
    }
}

// ==================== 复合组件 ====================

component ToolBar inherits Rectangle {
    in property <bool> connected;
    in property <string> connection-mode;
    callback on-connect();
    callback on-disconnect();
    callback on-clear-log();

    background: Theme.bg-secondary;
    height: 48px;
    border-width: 1px;
    border-color: Theme.border;

    HorizontalLayout {
        padding-left: Theme.spacing-md; padding-right: Theme.spacing-md;
        padding-top: Theme.spacing-sm; padding-bottom: Theme.spacing-sm;
        spacing: Theme.spacing-sm; alignment: start;

        ToolButton { label: "启动"; enabled: root.connected == false; btn-color: Theme.accent; clicked => { root.on-connect(); } }
        ToolButton { label: "停止"; enabled: root.connected; btn-color: Theme.error; clicked => { root.on-disconnect(); } }
        Sep {}
        ToolButton { label: "清空日志"; btn-color: Theme.text-secondary; clicked => { root.on-clear-log(); } }

        Rectangle { horizontal-stretch: 1; }

        Rectangle {
            Rectangle {
                x: 0; y: parent.height / 2 - 4px;
                width: 8px; height: 8px; border-radius: 4px;
                background: root.connected ? Theme.success : Theme.text-muted;
            }
            Text {
                x: 14px; y: parent.height / 2 - self.height / 2;
                text: root.connected ? root.connection-mode : "未连接";
                color: root.connected ? Theme.success : Theme.text-muted;
                font-size: Theme.font-size-sm;
            }
        }
    }
}

component RegisterTable inherits Rectangle {
    in property <[RegisterRow]> registers;
    callback on-cell-edit(int, string);
    background: Theme.bg-primary;

    VerticalLayout {
        Rectangle {
            height: 36px; background: Theme.table-header-bg; border-width: 1px; border-color: Theme.border;
            HorizontalLayout {
                padding-left: Theme.spacing-sm; padding-right: Theme.spacing-sm;
                HeaderCell { text: "地址"; width: 80px; }
                HeaderCell { text: "无符号"; width: 90px; }
                HeaderCell { text: "有符号"; width: 90px; }
                HeaderCell { text: "十六进制"; width: 90px; }
                HeaderCell { text: "二进制"; width: 150px; }
                HeaderCell { text: "浮点数"; width: 130px; }
                HeaderCell { text: "ASCII"; width: 60px; }
            }
        }
        ScrollView {
            viewport := VerticalLayout {
                spacing: 0;
                for row[idx] in root.registers : Rectangle {
                    height: 30px;
                    background: Math.mod(idx, 2) == 0 ? Theme.table-row-even : Theme.table-row-odd;
                    HorizontalLayout {
                        padding-left: Theme.spacing-sm; padding-right: Theme.spacing-sm;
                        CellText { text: row.address + ""; width: 80px; cell-color: Theme.accent; }
                        EditableCell { text: row.value-unsigned; width: 90px; edited(value) => { root.on-cell-edit(row.address, value); } }
                        CellText { text: row.value-signed; width: 90px; cell-color: Theme.text-secondary; }
                        CellText { text: row.value-hex; width: 90px; cell-color: Theme.warning; }
                        CellText { text: row.value-binary; width: 150px; cell-color: Theme.info; cell-font-size: Theme.font-size-sm; }
                        CellText { text: row.value-float; width: 130px; cell-color: Theme.success; }
                        CellText { text: row.value-ascii; width: 60px; cell-color: Theme.text-primary; }
                    }
                }
            }
        }
    }
}

component StatusBar inherits Rectangle {
    in property <bool> connected;
    in property <string> connection-info;
    in property <int> slave-id;
    in property <int> tx-count;
    in property <int> rx-count;
    in property <int> error-count;
    in property <string> last-error;

    height: 28px; background: Theme.bg-secondary; border-width: 1px; border-color: Theme.border;

    HorizontalLayout {
        padding-left: Theme.spacing-md; padding-right: Theme.spacing-md; spacing: Theme.spacing-lg;
        Rectangle {y:parent.height/2 - self.height/2;  width: 8px; height: 8px; border-radius: 4px; background: root.connected ? Theme.success : Theme.error; }
        Text { text: root.connected ? root.connection-info : "未连接"; color: root.connected ? Theme.text-primary : Theme.text-muted; font-size: Theme.font-size-sm; vertical-alignment: center; }
        if root.connected : Text { text: "从站:" + root.slave-id; color: Theme.text-secondary; font-size: Theme.font-size-sm; vertical-alignment: center; }
        Sep {}
        StatusItem { label: "TX"; value: root.tx-count + ""; value-color: Theme.tx-color; }
        StatusItem { label: "RX"; value: root.rx-count + ""; value-color: Theme.rx-color; }
        StatusItem { label: "错误"; value: root.error-count + ""; value-color: root.error-count > 0 ? Theme.error : Theme.text-secondary; }
        Rectangle { horizontal-stretch: 1; }
        if root.last-error != "" : Text { text: root.last-error; color: Theme.error; font-size: Theme.font-size-sm; vertical-alignment: center; overflow: elide; max-width: 300px; }
    }
}

component LogPanel inherits Rectangle {
    in property <[LogEntry]> log-entries;
    height: 180px;
    VerticalLayout {
        Rectangle {
            height: 32px; background: Theme.bg-tertiary; border-width: 1px; border-color: Theme.border;
            HorizontalLayout {
                padding-left: Theme.spacing-md; padding-right: Theme.spacing-md;
                Text { text: "通信日志"; color: Theme.text-secondary; font-size: Theme.font-size-sm; font-weight: 600; vertical-alignment: center; }
                Rectangle { horizontal-stretch: 1; }
                Text { text: root.log-entries.length + " 条记录"; color: Theme.text-muted; font-size: Theme.font-size-sm; vertical-alignment: center; }
            }
        }
        log-scroll := ScrollView {
            viewport := VerticalLayout {
                spacing: 0; padding: Theme.spacing-xs;
                for entry in root.log-entries : Rectangle {
                    height: 22px;
                    background: entry.direction == 2 ? #1a0a0a : transparent;
                    HorizontalLayout {
                        padding-left: Theme.spacing-sm; spacing: Theme.spacing-sm;
                        Text { text: entry.timestamp; color: Theme.text-muted; font-size: 11px; font-family: "monospace"; vertical-alignment: center; width: 100px; }
                        Rectangle {
                            width: 28px; height: 16px; border-radius: 3px;
                            background: entry.direction == 0 ? Theme.tx-color : (entry.direction == 1 ? Theme.rx-color : Theme.error);
                            Text { text: entry.direction == 0 ? "TX" : (entry.direction == 1 ? "RX" : "ERR"); color: white; font-size: 10px; font-weight: 700; horizontal-alignment: center; vertical-alignment: center; }
                        }
                        Text { text: entry.raw-text; color: entry.direction == 0 ? Theme.tx-color : (entry.direction == 1 ? Theme.rx-color : Theme.error); font-size: 11px; font-family: "monospace"; vertical-alignment: center; overflow: elide; }
                    }
                }
                changed height => {
                    if self.height > parent.height {
                        // 内容溢出:滚动到底部
                        parent.viewport-y = parent.height - self.height;
                    } else {
                        // 内容未溢出:置顶
                        parent.viewport-y = 0px;
                    }
                }

            }
        }
    }
}

// ==================== 主窗口 ====================
export component MainWindow inherits Window {
    title: "Modbus RTU 从机调试助手";
    preferred-width: 1200px; preferred-height: 800px;
    min-width: 960px; min-height: 640px;
    background: Theme.bg-primary;

    in property <bool> connected: false;
    in property <string> connection-mode: "";
    in property <string> connection-info: "";
    in property <int> slave-id: 1;
    in property <int> tx-count: 0;
    in property <int> rx-count: 0;
    in property <int> error-count: 0;
    in property <string> last-error: "";
    in property <[RegisterRow]> registers;
    in property <[LogEntry]> log-entries;
    in property <int> function-code: 3;
    in-out property <int> register-type-index: 0;

    callback on-connect();
    callback on-disconnect();
    callback on-clear-log();
    callback on-cell-edit(int, string);
    callback on-register-type-changed(int);

    // 当寄存器类型改变时触发刷新
    changed register-type-index => { root.on-register-type-changed(register-type-index); }

    VerticalLayout {
        ToolBar {
            connected: root.connected; connection-mode: root.connection-mode;
            on-connect => { root.on-connect(); } on-disconnect => { root.on-disconnect(); }
            on-clear-log => { root.on-clear-log(); }
        }
        VerticalLayout {
            vertical-stretch: 1;
            // 寄存器类型选择器
            Rectangle {
                height: 36px;
                background: Theme.bg-secondary;
                border-width: 1px;
                border-color: Theme.border;
                HorizontalLayout {
                    padding-left: Theme.spacing-md;
                    padding-right: Theme.spacing-md;
                    spacing: Theme.spacing-sm;
                    alignment: start;
                    Text { text: "寄存器类型:"; color: Theme.text-secondary; font-size: Theme.font-size-sm; vertical-alignment: center; }
                    RegisterTypeButton {
                        y:parent.height / 2 - self.height / 2;
                        label: "线圈 (0x)";
                        active: root.register-type-index == 0;
                        clicked => { root.register-type-index = 0; }
                    }
                    RegisterTypeButton {
                        y:parent.height / 2 - self.height / 2;
                        label: "离散输入 (1x)";
                        active: root.register-type-index == 1;
                        clicked => { root.register-type-index = 1; }
                    }
                    RegisterTypeButton {
                        y:parent.height / 2 - self.height / 2;
                        label: "保持寄存器 (4x)";
                        active: root.register-type-index == 2;
                        clicked => { root.register-type-index = 2; }
                    }
                    RegisterTypeButton {
                        y:parent.height / 2 - self.height / 2;
                        label: "输入寄存器 (3x)";
                        active: root.register-type-index == 3;
                        clicked => { root.register-type-index = 3; }
                    }
                    Rectangle { horizontal-stretch: 1; }
                }
            }
            RegisterTable { vertical-stretch: 1; registers: root.registers; on-cell-edit(a, v) => { root.on-cell-edit(a, v); } }
            Rectangle { height: 1px; background: Theme.border; }
            LogPanel { log-entries: root.log-entries; }
        }
        StatusBar {
            connected: root.connected; connection-info: root.connection-info;
            slave-id: root.slave-id; tx-count: root.tx-count; rx-count: root.rx-count;
            error-count: root.error-count; last-error: root.last-error;
        }
    }
}

// ==================== RTU 从机 串口连接对话框 ====================
export component RtuSetupDialog inherits Window {
    title: "RTU 从机 连接设置";
    preferred-width: 460px; preferred-height: 440px;
    background: Theme.bg-primary;
    in property <[string]> port-names;
    in property <[string]> baud-rates; in property <[string]> data-bits-list;
    in property <[string]> stop-bits-list; in property <[string]> parity-list;
    in-out property <int> port-index <=> port-combo.current-index;
    in-out property <int> baud-index <=> baud-combo.current-index;
    in-out property <int> data-bits-index <=> data-combo.current-index;
    in-out property <int> stop-bits-index <=> stop-combo.current-index;
    in-out property <int> parity-index <=> parity-combo.current-index;
    in-out property <int> slave-id <=> slave-input.value;
    in-out property <int> register-count <=> reg-count-input.value;
    callback on-confirm(); callback on-cancel(); callback on-refresh-ports();

    VerticalLayout {
        padding: Theme.spacing-xl; spacing: Theme.spacing-lg;
        Text { text: "Modbus RTU 从机 连接设置"; color: Theme.text-primary; font-size: 18px; font-weight: 700; horizontal-alignment: center; }
        Rectangle { height: 1px; background: Theme.border; }
        VerticalLayout {
            spacing: Theme.spacing-md;
            HorizontalLayout {
                spacing: Theme.spacing-sm; alignment: start;
                Text { text: "串口:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; }
                port-combo := ComboBox { model: root.port-names; current-index: 0; horizontal-stretch: 1; }
                Button {
                    text: "刷新";
                    width: 60px;
                    clicked => { root.on-refresh-ports(); }
                }
            }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "波特率:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } baud-combo := ComboBox { model: root.baud-rates; current-index: 3; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "数据位:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } data-combo := ComboBox { model: root.data-bits-list; current-index: 3; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "停止位:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } stop-combo := ComboBox { model: root.stop-bits-list; current-index: 0; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "校验位:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } parity-combo := ComboBox { model: root.parity-list; current-index: 0; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "从站 ID:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } slave-input := SpinBox { value: 1; minimum: 1; maximum: 247; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "寄存器数量:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } reg-count-input := SpinBox { value: 100; minimum: 1; maximum: 10000; horizontal-stretch: 1; } }
        }
        Rectangle { vertical-stretch: 1; }
        HorizontalLayout { spacing: Theme.spacing-md; alignment: end; Button { text: "取消"; clicked => { root.on-cancel(); } } Button { text: "连接"; primary: true; clicked => { root.on-confirm(); } } }
    }
}

// ==================== 读取设置对话框 ====================
export component ReadSetupDialog inherits Window {
    title: "读取设置";
    preferred-width: 400px; preferred-height: 320px;
    background: Theme.bg-primary;
    in property <[string]> function-codes;
    in-out property <int> function-index <=> func-combo.current-index;
    in-out property <int> start-address <=> addr-input.value;
    in-out property <int> quantity <=> qty-input.value;
    in-out property <int> slave-id <=> slave-input.value;
    callback on-confirm(); callback on-cancel();

    VerticalLayout {
        padding: Theme.spacing-xl; spacing: Theme.spacing-lg;
        Text { text: "读取参数设置"; color: Theme.text-primary; font-size: 18px; font-weight: 700; horizontal-alignment: center; }
        Rectangle { height: 1px; background: Theme.border; }
        VerticalLayout {
            spacing: Theme.spacing-md;
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "功能码:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } func-combo := ComboBox { model: root.function-codes; current-index: 2; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "从站 ID:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } slave-input := SpinBox { value: 1; minimum: 1; maximum: 247; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "起始地址:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } addr-input := SpinBox { value: 0; minimum: 0; maximum: 65535; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "数量:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } qty-input := SpinBox { value: 10; minimum: 1; maximum: 125; horizontal-stretch: 1; } }
        }
        Rectangle { vertical-stretch: 1; }
        Text { text: "寄存器地址从 0 开始, 数量最大 125"; color: Theme.text-muted; font-size: Theme.font-size-sm; horizontal-alignment: center; }
        HorizontalLayout { spacing: Theme.spacing-md; alignment: end; Button { text: "取消"; clicked => { root.on-cancel(); } } Button { text: "读取"; primary: true; clicked => { root.on-confirm(); } } }
    }
}

// ==================== 写入设置对话框 ====================
export component WriteSetupDialog inherits Window {
    title: "写入设置";
    preferred-width: 420px; preferred-height: 320px;
    background: Theme.bg-primary;
    in property <[string]> write-function-codes;
    in-out property <int> function-index <=> func-combo.current-index;
    in-out property <int> slave-id <=> slave-input.value;
    in-out property <int> start-address <=> addr-input.value;
    in-out property <string> write-value <=> value-input.text;
    callback on-confirm(); callback on-cancel();

    VerticalLayout {
        padding: Theme.spacing-xl; spacing: Theme.spacing-lg;
        Text { text: "写入参数设置"; color: Theme.text-primary; font-size: 18px; font-weight: 700; horizontal-alignment: center; }
        Rectangle { height: 1px; background: Theme.border; }
        VerticalLayout {
            spacing: Theme.spacing-md;
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "功能码:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } func-combo := ComboBox { model: root.write-function-codes; current-index: 1; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "从站 ID:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } slave-input := SpinBox { value: 1; minimum: 1; maximum: 247; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "地址:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } addr-input := SpinBox { value: 0; minimum: 0; maximum: 65535; horizontal-stretch: 1; } }
            HorizontalLayout { spacing: Theme.spacing-md; alignment: start; Text { text: "写入值:"; color: Theme.text-secondary; vertical-alignment: center; width: 100px; } value-input := LineEdit { text: "0"; horizontal-stretch: 1; } }
        }
        Rectangle { vertical-stretch: 1; }
        Text { text: "线圈: 0=断开 1=闭合 | 寄存器: 0~65535"; color: Theme.text-muted; font-size: Theme.font-size-sm; horizontal-alignment: center; }
        HorizontalLayout { spacing: Theme.spacing-md; alignment: end; Button { text: "取消"; clicked => { root.on-cancel(); } } Button { text: "写入"; primary: true; clicked => { root.on-confirm(); } } }
    }
}

13、Cargo.toml

rust 复制代码
[package]
name = "modbus-debug-assistant"
version = "0.1.0"
edition = "2024"

[dependencies]
slint = "1.16.1"
tokio = { version = "1", features = ["full"] }
chrono = "0.4"
hex = "0.4"
tokio-modbus = "0.17"
serialport = "4"
tokio-serial = "5"
anyhow = "1"
thiserror = "2"
log = "0.4"
env_logger = "0.11"

[build-dependencies]
slint-build = "1.16.1"

三、实现原理

1、整体架构

本工具基于 Rust + Slint 构建,采用 Tokio 异步运行时 驱动 Modbus 通信,整体分为三层:

  • UI 层(main.slint):负责界面渲染与用户交互,包括连接设置、寄存器表格、通信日志、状态栏等组件。
  • 业务逻辑层(main.rs:负责界面与通信模块之间的数据流转,处理用户操作事件,维护寄存器数据模型与通信统计。
  • 通信层(modbus_client.rs / modbus_server.rs) :封装 Modbus TCP/RTU 客户端与 RTU 从机服务,基于 tokio-modbustokio-serial 实现异步收发。

三者通过 Slint 的 callbackin/out property 机制解耦:UI 触发回调 → 业务层调用通信层 → 结果写回 UI 属性,实现单向数据流。

2、Modbus 协议帧结构

Modbus RTU 报文由四部分组成:

字段 长度 说明
从站地址 1 字节 0x01 ~ 0xF7,标识目标从站
功能码 1 字节 如 0x03 读保持寄存器、0x06 写单个寄存器
数据区 N 字节 寄存器地址、数量、数据值等
CRC16 2 字节 低字节在前,校验整个报文

读保持寄存器(FC 03) 为例,请求帧为:

复制代码
01 03 00 00 00 0A C5 CD
│  │  │     │     └── CRC16
│  │  │     └──────── 数量 = 10
│  │  └────────────── 起始地址 = 0x0000
│  └───────────────── 功能码 = 0x03
└──────────────────── 从站地址 = 0x01

从站响应帧为:

复制代码
01 03 14 00 01 00 02 ... 9A 3F
│  │  │  └──────────────┘  └── CRC16
│  │  │    数据区(14 字节 = 7 个寄存器)
│  │  └───────────────── 字节数 = 0x14(20 字节)
│  └──────────────────── 功能码 = 0x03
└─────────────────────── 从站地址 = 0x01

3、CRC16 校验算法

Modbus RTU 使用 CRC16-IBM 算法,多项式为 0xA001(反转后的 0x8005)。核心实现如下:

rust 复制代码
fn crc16(data: &[u8]) -> u16 {
    let mut crc: u16 = 0xFFFF;
    for &byte in data {
        crc ^= byte as u16;
        for _ in 0..8 {
            if crc & 0x0001 != 0 {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    crc
}

发送时 CRC 低字节在前、高字节在后;接收时对整帧(含 CRC)重新计算,结果为 0x0000 则校验通过。

4、RTU 从机服务实现

modbus_server.rs 中通过 tokio_serial::SerialStream 打开串口,在独立异步任务中循环读取数据:

rust 复制代码
// 伪代码示意
loop {
    let mut buf = [0u8; 256];
    let n = serial.read(&mut buf).await?;
    let frame = &buf[..n];

    // 1. 校验 CRC
    if crc16(frame) != 0 { continue; }

    // 2. 解析从站地址,不匹配则忽略
    if frame[0] != slave_id { continue; }

    // 3. 按功能码分发处理
    let response = match frame[1] {
        0x01 => handle_read_coils(frame),
        0x03 => handle_read_holding_registers(frame),
        0x06 => handle_write_single_register(frame),
        _ => build_exception(frame[1], 0x01), // 非法功能码
    };

    // 4. 回写响应帧
    serial.write_all(&response).await?;
}

关键点

  • 每个请求帧必须完整接收,通过帧间隔(3.5 字符时间)判断帧边界;
  • 从站地址不匹配时直接丢弃,不产生响应;
  • 异常时返回异常响应帧:从站地址 + 0x80|功能码 + 异常码 + CRC

5、寄存器数据模型与多格式解析

data_models.rs 中的 RegisterModel 负责将原始 u16 值转换为多种展示格式:

格式 说明 示例(0x1234)
无符号 直接十进制 4660
有符号 按 i16 解释 4660
十六进制 4 位 HEX 1234
二进制 16 位 BIN 0001001000110100
浮点 两个寄存器拼 IEEE 754 见下
ASCII 高/低字节转字符 12

IEEE 754 浮点解析:将相邻两个寄存器拼接为 32 位:

rust 复制代码
let bits = ((high as u32) << 16) | (low as u32);
let float = f32::from_bits(bits);

ASCII 解析 :将寄存器高字节、低字节分别映射为可打印字符,不可打印的显示为 .

6、通信日志与统计

  • CommLog :环形缓冲结构,最多保留 5000 条记录,每条包含时间戳、方向(TX/RX/ERR)和原始数据,通过 version 字段通知 UI 增量刷新。
  • CommStatistics:维护 TX 帧数、RX 帧数、错误计数,每次收发后更新,并通过 Slint 属性绑定实时刷新状态栏。

7、UI 与业务层的数据绑定

Slint 通过 callback 将 UI 事件传递给 Rust 业务层,业务层处理完成后通过 in property 回写数据:

rust 复制代码
// main.rs 中绑定回调
ui.on_connect({
    let ui_handle = ui.as_weak();
    move || {
        // 打开连接对话框,建立 RTU 从机服务
    }
});

// 更新寄存器表格
ui.set_registers(model_rc);

寄存器表格支持双击单元格直接编辑 ,编辑值通过 on-cell-edit 回调写回寄存器模型,并触发写操作。

8、异步任务调度

整个应用基于 Tokio 运行时,核心异步任务包括:

  • 串口监听任务:持续读取串口数据,解析并响应 Modbus 请求;
  • UI 事件循环 :Slint 的 run_event_loop 驱动界面刷新;
  • 定时刷新任务:周期性更新寄存器表格与通信统计。

通过 tokio::spawn 创建独立任务,任务间使用 Arc<Mutex<>> 共享寄存器数据与统计信息,避免数据竞争。

相关推荐
疯狂打码的少年1 小时前
【计算机网络】OSI/RM七层模型(层次结构、各层功能速览)
开发语言·笔记·计算机网络·php
程序喵大人1 小时前
【C++入门】值类别与表达式 - 03 引用绑定:为什么有些参数能接住临时对象
开发语言·c++·引用绑定
子非鱼a1 小时前
【WEB】[RoarCTF 2019]Easy Java
java·开发语言
软件黑马王子1 小时前
19.资源加载模块:主要作用和基本原理
开发语言·前端框架·c#
fengkai45452 小时前
七、华为云网络类 + 计算类云服务实验总结
开发语言·php
神明不懂浪漫2 小时前
【第四章】索引——B+树、回表,加快数据库的查找能力的利器
开发语言·数据结构·数据库·经验分享·笔记·b树
博、、2 小时前
全民健身解决方案系统源码实战指南:从架构设计到部署全流程解析
开发语言·需求分析
liuze4082 小时前
安装boss-zhipin-mcp(招聘)
开发语言·python