完善modbus-tcp的配置方法

This commit is contained in:
GuanYuankai 2025-10-14 17:44:38 +08:00
parent 3aaec6a42c
commit 9890d33e0b
5 changed files with 83 additions and 42 deletions

View File

@ -44,8 +44,9 @@
"slave_id": 1,
"poll_interval_ms": 1000,
"data_points": [
{"name": "motor_speed", "address": 100, "type": "UINT16"},
{"name": "pressure", "address": 102, "type": "FLOAT32", "scale": 0.01}
{"name": "motor_speed", "address": 100, "type": "UINT16", "scale": 1.0},
{"name": "pressure", "address": 102, "type": "FLOAT32", "scale": 0.01},
{"name": "valve_status","address": 104, "type": "UINT16", "scale": 1.0}
]
}
]

View File

@ -76,18 +76,22 @@ void DeviceManager::load_and_start(const std::string& config_path) {
for (const auto& dev_json : config_json["modbus_tcp_devices"]) {
if (!dev_json.value("enabled", false)) continue;
ModbusDeviceConfig config; // This is the TCP config struct
ModbusTcpDeviceConfig config;
config.device_id = dev_json.at("device_id").get<std::string>();
config.ip_address = dev_json.at("ip_address").get<std::string>();
config.port = dev_json.at("port").get<uint16_t>();
config.slave_id = dev_json.at("slave_id").get<uint8_t>();
config.poll_interval_ms = dev_json.at("poll_interval_ms").get<int>();
// Note: The current ModbusMasterPoller doesn't use data_points yet.
// This is a point for future enhancement to make it also configuration-driven.
// For now, we just start it based on connection params.
// You would need to refactor ModbusMasterPoller similarly to ModbusRtuPollerService
// to make it fully generic.
// 解析 data_points
for (const auto& dp_json : dev_json.at("data_points")) {
config.data_points.push_back({
dp_json.at("name").get<std::string>(),
(uint16_t)dp_json.at("address").get<int>(),
string_to_modbus_data_type(dp_json.at("type").get<std::string>()),
dp_json.value("scale", 1.0)
});
}
auto poller = std::make_shared<ModbusMasterPoller>(m_io_context, config, m_report_callback);
poller->start();

View File

@ -33,4 +33,17 @@ struct ModbusRtuDeviceConfig {
std::vector<DataPointConfig> data_points; // 该设备上所有需要采集的数据点
};
// ==================== [ 2025.10.14新增 ] ===================
// vvv 描述一个完整的Modbus TCP设备包含多个采集点 vvv
struct ModbusTcpDeviceConfig {
std::string device_id;
std::string ip_address;
uint16_t port;
uint8_t slave_id;
int poll_interval_ms;
std::vector<DataPointConfig> data_points;
};
//
// ==========================================================
#endif // MODBUS_COMMON_H

View File

@ -2,13 +2,14 @@
#include "modbus_master_poller.h"
#include "protocol/modbus/modbus_protocol.h"
#include "spdlog/spdlog.h"
#include "generic_modbus_parser.h"
#include <nlohmann/json.hpp>
#include <chrono>
using boost::asio::ip::tcp;
ModbusMasterPoller::ModbusMasterPoller(boost::asio::io_context& io_context,
ModbusDeviceConfig config,
ModbusTcpDeviceConfig config,
ReportDataCallback report_cb)
: m_io_context(io_context),
m_socket(io_context),
@ -17,9 +18,14 @@ ModbusMasterPoller::ModbusMasterPoller(boost::asio::io_context& io_context,
m_report_callback(std::move(report_cb)) {}
void ModbusMasterPoller::start() {
if (m_config.data_points.empty()) {
spdlog::warn("[Modbus TCP] Device '{}' has no data points configured. Poller will not start.", m_config.device_id);
return;
}
do_connect();
}
void ModbusMasterPoller::do_connect() {
auto self = shared_from_this();
tcp::resolver resolver(m_io_context);
@ -57,13 +63,25 @@ void ModbusMasterPoller::on_error(const std::string& stage) {
do_connect();
});
}
// modifier: 2025.10.14 重写do_poll
void ModbusMasterPoller::do_poll() {
// 1. 创建PDU
auto pdu = modbus::protocol::create_read_holding_registers_pdu(m_config.start_address, m_config.quantity);
// 1. 动态计算需要读取的寄存器范围,提高效率
auto [min_it, max_it] = std::minmax_element(m_config.data_points.begin(), m_config.data_points.end(),
[](const DataPointConfig& a, const DataPointConfig& b) {
return a.address < b.address;
});
uint16_t start_address = min_it->address;
uint16_t last_address = max_it->address;
if (max_it->type == ModbusDataType::UINT32 || max_it->type == ModbusDataType::INT32 || max_it->type == ModbusDataType::FLOAT32) {
last_address += 1;
}
uint16_t quantity = last_address - start_address + 1;
// 2. 创建PDU
auto pdu = modbus::protocol::create_read_holding_registers_pdu(start_address, quantity);
// 2. 构建MBAP头
// 3. 构建MBAP头
m_write_buffer.clear();
m_write_buffer.resize(7 + pdu.size());
@ -78,10 +96,10 @@ void ModbusMasterPoller::do_poll() {
m_write_buffer[5] = static_cast<uint8_t>(length & 0xFF);
m_write_buffer[6] = m_config.slave_id;
// 3. 附加PDU
// 4. 附加PDU
std::copy(pdu.begin(), pdu.end(), m_write_buffer.begin() + 7);
// 4. 发送请求
// 5. 发送请求
do_write();
}
@ -98,6 +116,7 @@ void ModbusMasterPoller::do_write() {
});
}
// do_read_header() 稍作修改,传递 start_address
void ModbusMasterPoller::do_read_header() {
auto self = shared_from_this();
boost::asio::async_read(m_socket, boost::asio::buffer(m_read_buffer, 7),
@ -114,14 +133,22 @@ void ModbusMasterPoller::do_read_header() {
return;
}
uint16_t pdu_len = (m_read_buffer[4] << 8) | m_read_buffer[5];
do_read_pdu(pdu_len - 1); // Length in header includes UnitID
// <<< MODIFIED: 为了构建地址->值的映射,我们需要知道这次轮询的起始地址
// (这段代码可以优化,比如将 poll 请求的信息暂存起来,但为了简单直观,我们重新计算)
auto min_it = std::min_element(m_config.data_points.begin(), m_config.data_points.end(),
[](const DataPointConfig& a, const DataPointConfig& b) { return a.address < b.address; });
uint16_t start_address = min_it->address;
do_read_pdu(pdu_len - 1, start_address); // Length in header includes UnitID
});
}
void ModbusMasterPoller::do_read_pdu(std::size_t pdu_length) {
// <<< MODIFIED: do_read_pdu() 方法被完全重写
void ModbusMasterPoller::do_read_pdu(std::size_t pdu_length, uint16_t poll_start_addr) {
auto self = shared_from_this();
boost::asio::async_read(m_socket, boost::asio::buffer(m_read_buffer.data() + 7, pdu_length),
[this, self, pdu_length](const boost::system::error_code& ec, std::size_t /*length*/) {
[this, self, poll_start_addr, pdu_length](const boost::system::error_code& ec, std::size_t /*length*/) {
if (ec) {
spdlog::error("[{}] Read PDU failed: {}", m_config.device_id, ec.message());
on_error("read_pdu");
@ -129,22 +156,26 @@ void ModbusMasterPoller::do_read_pdu(std::size_t pdu_length) {
}
try {
// 解析并上报数据
// 1. 解析PDU获取原始寄存器值
std::vector<uint8_t> pdu_data(m_read_buffer.data() + 7, m_read_buffer.data() + 7 + pdu_length);
auto registers = modbus::protocol::parse_read_holding_registers_response(pdu_data);
// 将数据转换为JSON
nlohmann::json data_j;
for(size_t i = 0; i < registers.size(); ++i) {
data_j[std::to_string(m_config.start_address + i)] = registers[i];
// 2. 将原始数组转换为 (地址 -> 值) 的映射
std::map<uint16_t, uint16_t> registers_map;
for (uint16_t i = 0; i < registers.size(); ++i) {
registers_map[poll_start_addr + i] = registers[i];
}
// 3. 使用通用解析器将映射转换为JSON
nlohmann::json data_j = GenericModbusParser::parse(registers_map, m_config.data_points);
// 4. 封装并上报 UnifiedData
UnifiedData report_data;
report_data.device_id = m_config.device_id;
report_data.timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
report_data.data_json = data_j.dump();
if(m_report_callback) {
if (m_report_callback) {
m_report_callback(report_data);
}
@ -152,7 +183,7 @@ void ModbusMasterPoller::do_read_pdu(std::size_t pdu_length) {
spdlog::error("[{}] Failed to parse Modbus response: {}", m_config.device_id, e.what());
}
// 安排下一次轮询
// 5. 安排下一次轮询
m_timer.expires_after(std::chrono::milliseconds(m_config.poll_interval_ms));
m_timer.async_wait([this, self](const boost::system::error_code& /*ec*/) {
do_poll();

View File

@ -3,28 +3,19 @@
#define MODBUS_MASTER_POLLER_H
#include "protocol/iprotocol_adapter.h" // For UnifiedData and ReportDataCallback
#include "modbus_common.h" // <<< MODIFIED: 包含通用配置定义
#include <boost/asio.hpp>
#include <memory>
#include <string>
// 配置一个Modbus设备的连接和轮询参数
struct ModbusDeviceConfig {
std::string device_id; // 自定义设备ID用于MQTT主题
std::string ip_address;
uint16_t port;
uint8_t slave_id;
uint16_t start_address;
uint16_t quantity;
int poll_interval_ms;
};
#include <vector>
class ModbusMasterPoller : public std::enable_shared_from_this<ModbusMasterPoller> {
public:
// <<< MODIFIED: 构造函数接收新的配置结构体
ModbusMasterPoller(boost::asio::io_context& io_context,
ModbusDeviceConfig config,
ModbusTcpDeviceConfig config,
ReportDataCallback report_cb);
// 开始轮询器的工作 (连接和定时)
void start();
private:
@ -32,18 +23,19 @@ private:
void do_poll();
void do_write();
void do_read_header();
void do_read_pdu(std::size_t pdu_length);
void do_read_pdu(std::size_t pdu_length, uint16_t poll_start_addr); // <<< MODIFIED: 需要知道轮询的起始地址
void on_error(const std::string& stage);
boost::asio::io_context& m_io_context;
boost::asio::ip::tcp::socket m_socket;
boost::asio::steady_timer m_timer;
ModbusDeviceConfig m_config;
// <<< MODIFIED: 成员变量使用新的配置结构体
ModbusTcpDeviceConfig m_config;
ReportDataCallback m_report_callback;
std::vector<uint8_t> m_write_buffer;
std::array<uint8_t, 260> m_read_buffer; // Max Modbus TCP frame size
std::array<uint8_t, 260> m_read_buffer;
uint16_t m_transaction_id = 0;
};