赞
踩
之前用Python写过Modbus服务端,用的是pymodbus库,但这个库里面使用的是协程,小弟水平不够,有些hold不住,所以决定用C++把之前那块代码给重构了
将源代码拉下来放到项目里面,源代码下载地址:https://github.com/stephane/libmodbus
编译源代码,先执行./autogen.sh,再执行./configure
编写CMakeLists.txt,链接的时候加入modbus库,这里是我的一个例子
set(TARGET modbus_tcp_server_test)
file(GLOB_RECURSE SRC *.cpp)
include_directories(${PROJECT_SOURCE_DIR}/src)
add_executable(${TARGET} ${SRC} ${PROJECT_SOURCE_DIR}/src/server/modbus_tcp_server.cpp)
target_include_directories(${TARGET} PUBLIC
${GTEST_INCLUDE_DIRS})
message("project source dir: ${PROJECT_SOURCE_DIR}")
target_link_libraries(${TARGET} PUBLIC gtest gtest_main pthread modbus)
先定义modbus_tcp_server.h文件,将modbus_server封装成类,定义以下这些接口
#pragma once #include <iostream> #include <vector> #include <mutex> #include "libmodbus/src/modbus-tcp.h" class ModbusTcpServer { public: ModbusTcpServer(int port, int slaveCount, bool debug);//端口号,从机个数,是否开启调试 ~ModbusTcpServer(); // 判断从机地址,寄存器地址是否合法 bool isAddressValid(int slaveId, int address); // 线圈寄存器 void setCoilValue(int slaveId, int address, uint8_t value); uint8_t getCoilValue(int slaveId, int address); // 离散输入寄存器 void setDiscreteInputValue(int slaveId, int address, uint8_t value); uint8_t getDiscreteInputValue(int slaveId, int address); // 输入寄存器 void setInputRegisterValue(int slaveId, int address, uint16_t value); uint16_t getInputRegisterValue(int slaveId, int address); // 保持寄存器 void setHoldRegisterValue(int slaveId, int address, uint16_t value); uint16_t getHoldRegisterValue(int slaveId, int address); void recieveMessages(); void start(); private: modbus_t *_ctx; uint16_t *_registers; // 假设所有寄存器的值都是0 int _slaveCount; std::mutex _slavemutex; int _errCount{0}; int _modbusSocket{-1}; std::vector<modbus_mapping_t *> _mappingList; bool _initialized{false}; /*Mapping*/ int _numBits{65535}; int _numInputBits{65535}; int _numRegisters{65535}; int _numInputRegisters{65535}; };
构造函数实现
ModbusTcpServer::ModbusTcpServer(int port, int slaveCount, bool debug) { this->_slaveCount = slaveCount; _ctx = modbus_new_tcp("10.8.0.126", port); //ip地址+端口号 modbus_set_debug(_ctx, debug); if (_ctx == nullptr) { LOG_DEBUG("初始化ModbusServer错误!"); throw -1; } if (_modbusSocket < 0) { _modbusSocket = modbus_tcp_listen(_ctx, 1); } // 设置从机0-slaveCount的地址映射 for (int i = 0; i <= slaveCount; ++i) { /*设置线圈, 离散输入, 输入寄存器, 保持寄存器个数(数组元素个数))*/ modbus_mapping_t *_mapping = modbus_mapping_new(_numBits, _numInputBits, _numInputRegisters, _numRegisters); _mappingList.push_back(_mapping); if (_mapping == nullptr) { LOG_ERROR("初始化ModbusMap失败!"); modbus_free(_ctx); _initialized = false; return; } } _initialized = true; }
析构函数实现
ModbusTcpServer::~ModbusTcpServer()
{
// 清理资源
modbus_close(_ctx);
modbus_free(_ctx);
for (auto &mapping : _mappingList)
{
modbus_mapping_free(mapping);
}
}
接收消息处理函数(重点)
之前我在网上搜遍资料,都没有找到一个ip和端口号设置多个从机的教程,后面又看了一整天libmodbus的源码,发现源码里根本就没有处理多个从机地址的情况。最后我发现是要自己手动处理接收到报文里的从机地址,否则所有回复的报文都是一台从机的情况。
这里支持多个主站连接的代码参考了这篇博客:https://blog.csdn.net/qq_38158479/article/details/120928043
void ModbusTcpServer::recieveMessages() { // 初始化 uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH]; fd_set reads, cpy_reads; FD_ZERO(&reads); FD_SET(_modbusSocket, &reads); int fdMax = _modbusSocket; while (true) { memset(query, 0, sizeof(query)); cpy_reads = reads; int fdNum = select(fdMax + 1, &cpy_reads, 0, 0, 0); if (fdNum == -1) break; else if (fdNum == 0) continue; for (int i = 0; i < fdMax + 1; i++) { if (FD_ISSET(i, &cpy_reads)) { if (i == _modbusSocket) { int clntSock = modbus_tcp_accept(_ctx, &_modbusSocket); if ((_modbusSocket == -1) || (clntSock == -1)) { LOG_ERROR("accept error!"); std::cerr << modbus_strerror(errno) << std::endl; continue; } FD_SET(clntSock, &reads); if (fdMax < clntSock) fdMax = clntSock; } else { int ret = modbus_receive(_ctx, query); if (ret == 0) { _errCount = 0; continue; } else if (ret > 0) { _errCount = 0; uint8_t slave = query[6]; // 获取收到问询的从机号 if (slave <= static_cast<uint8_t>(_slaveCount)) { // 只回复当前从机的报文,从机号不符合就不回复 modbus_set_slave(_ctx, slave); modbus_reply(_ctx, query, sizeof(query), _mappingList[slave]); } } else { modbus_set_error_recovery(_ctx, MODBUS_ERROR_RECOVERY_NONE); modbus_set_error_recovery(_ctx, MODBUS_ERROR_RECOVERY_LINK); modbus_close(_ctx); FD_CLR(i, &reads); LOG_ERROR("报文错误,服务端断开连接!"); close(i); _errCount++; } if (_errCount > 5) { _initialized = false; break; } } } } } _initialized = false; }
启动服务
void ModbusTcpServer::start() { std::thread loop([this]() { while (true) { if (_initialized) { recieveMessages(); } else { _initialized = true; } } }); // 将线程放在后台运行,防止阻塞主线程 loop.detach(); }
判断从机地址和寄存器地址是否合法
bool ModbusTcpServer::isAddressValid(int slaveId, int address)
{
if (slaveId < 0 || slaveId > _slaveCount)
{
LOG_ERROR(fmt::format("从机号错误, 从机号: {}!", slaveId));
return false;
}
if (address > (_numRegisters - 1))
{
LOG_ERROR(fmt::format("寄存器地址错误, 寄存器地址: {}!", address));
return true;
}
return true;
}
读写线圈寄存器
void ModbusTcpServer::setCoilValue(int slaveId, int address, uint8_t value)
{
if (!isAddressValid(slaveId, address))
return;
_mappingList[slaveId]->tab_bits[address] = value;
}
uint8_t ModbusTcpServer::getCoilValue(int slaveId, int address)
{
if (!isAddressValid(slaveId, address))
return 0;
return _mappingList[slaveId]->tab_bits[address];
}
读写离散输入寄存器
void ModbusTcpServer::setDiscreteInputValue(int slaveId, int address, uint8_t value)
{
if (!isAddressValid(slaveId, address))
return;
_mappingList[slaveId]->tab_input_bits[address] = value;
}
uint8_t ModbusTcpServer::getDiscreteInputValue(int slaveId, int address)
{
if (!isAddressValid(slaveId, address))
return 0;
return _mappingList[slaveId]->tab_input_bits[address];
}
读写输入寄存器
void ModbusTcpServer::setInputRegisterValue(int slaveId, int address, uint16_t value)
{
if (!isAddressValid(slaveId, address))
return;
_mappingList[slaveId]->tab_registers[address] = value;
}
uint16_t ModbusTcpServer::getInputRegisterValue(int slaveId, int address)
{
if (!isAddressValid(slaveId, address))
return 0;
return _mappingList[slaveId]->tab_registers[address];
}
读写保持寄存器
void ModbusTcpServer::setHoldRegisterValue(int slaveId, int address, uint16_t value)
{
if (!isAddressValid(slaveId, address))
return;
_slavemutex.lock();
_mappingList[slaveId]->tab_registers[address] = value;
_slavemutex.unlock();
}
uint16_t ModbusTcpServer::getHoldRegisterValue(int slaveId, int address)
{
if (!isAddressValid(slaveId, address))
return 0;
return _mappingList[slaveId]->tab_registers[address];
}
modbus_tcp_server.cpp所有代码
#include "modbus_tcp_server.h" #include <cstring> #include "log.h" ModbusTcpServer::ModbusTcpServer(int port, int slaveCount, bool debug) { this->_slaveCount = slaveCount; _ctx = modbus_new_tcp("10.8.0.126", port); // 设置从机地址 modbus_set_slave(_ctx, 1); modbus_set_debug(_ctx, debug); if (_ctx == nullptr) { LOG_DEBUG("初始化ModbusServer错误!"); throw -1; } if (_modbusSocket < 0) { _modbusSocket = modbus_tcp_listen(_ctx, 1); } // 设置从机0-slaveCount的地址映射 for (int i = 0; i <= slaveCount; ++i) { /*设置线圈, 离散输入, 输入寄存器, 保持寄存器个数(数组元素个数))*/ modbus_mapping_t *_mapping = modbus_mapping_new(_numBits, _numInputBits, _numInputRegisters, _numRegisters); _mappingList.push_back(_mapping); if (_mapping == nullptr) { LOG_ERROR("初始化ModbusMap失败!"); modbus_free(_ctx); _initialized = false; return; } } _initialized = true; } ModbusTcpServer::~ModbusTcpServer() { // 清理资源 modbus_close(_ctx); modbus_free(_ctx); for (auto &mapping : _mappingList) { modbus_mapping_free(mapping); } } bool ModbusTcpServer::isAddressValid(int slaveId, int address) { if (slaveId < 0 || slaveId > _slaveCount) { LOG_ERROR(fmt::format("从机号错误, 从机号: {}!", slaveId)); return false; } if (address > (_numRegisters - 1)) { LOG_ERROR(fmt::format("寄存器地址错误, 寄存器地址: {}!", address)); return true; } return true; } void ModbusTcpServer::setCoilValue(int slaveId, int address, uint8_t value) { if (!isAddressValid(slaveId, address)) return; _mappingList[slaveId]->tab_bits[address] = value; } uint8_t ModbusTcpServer::getCoilValue(int slaveId, int address) { if (!isAddressValid(slaveId, address)) return 0; return _mappingList[slaveId]->tab_bits[address]; } void ModbusTcpServer::setDiscreteInputValue(int slaveId, int address, uint8_t value) { if (!isAddressValid(slaveId, address)) return; _mappingList[slaveId]->tab_input_bits[address] = value; } uint8_t ModbusTcpServer::getDiscreteInputValue(int slaveId, int address) { if (!isAddressValid(slaveId, address)) return 0; return _mappingList[slaveId]->tab_input_bits[address]; } void ModbusTcpServer::setInputRegisterValue(int slaveId, int address, uint16_t value) { if (!isAddressValid(slaveId, address)) return; _mappingList[slaveId]->tab_registers[address] = value; } uint16_t ModbusTcpServer::getInputRegisterValue(int slaveId, int address) { if (!isAddressValid(slaveId, address)) return 0; return _mappingList[slaveId]->tab_registers[address]; } void ModbusTcpServer::setHoldRegisterValue(int slaveId, int address, uint16_t value) { if (!isAddressValid(slaveId, address)) return; _slavemutex.lock(); _mappingList[slaveId]->tab_registers[address] = value; _slavemutex.unlock(); } uint16_t ModbusTcpServer::getHoldRegisterValue(int slaveId, int address) { if (!isAddressValid(slaveId, address)) return 0; return _mappingList[slaveId]->tab_registers[address]; } void ModbusTcpServer::recieveMessages() { // 初始化 uint8_t query[MODBUS_TCP_MAX_ADU_LENGTH]; fd_set reads, cpy_reads; FD_ZERO(&reads); FD_SET(_modbusSocket, &reads); int fdMax = _modbusSocket; while (true) { memset(query, 0, sizeof(query)); cpy_reads = reads; int fdNum = select(fdMax + 1, &cpy_reads, 0, 0, 0); if (fdNum == -1) break; else if (fdNum == 0) continue; for (int i = 0; i < fdMax + 1; i++) { if (FD_ISSET(i, &cpy_reads)) { if (i == _modbusSocket) { int clntSock = modbus_tcp_accept(_ctx, &_modbusSocket); if ((_modbusSocket == -1) || (clntSock == -1)) { LOG_ERROR("accept error!"); std::cerr << modbus_strerror(errno) << std::endl; continue; } FD_SET(clntSock, &reads); if (fdMax < clntSock) fdMax = clntSock; } else { int ret = modbus_receive(_ctx, query); if (ret == 0) { _errCount = 0; continue; } else if (ret > 0) { _errCount = 0; uint8_t slave = query[6]; // 获取收到问询的从机子号 if (slave <= static_cast<uint8_t>(_slaveCount)) { modbus_set_slave(_ctx, slave); modbus_reply(_ctx, query, sizeof(query), _mappingList[slave]); } } else { modbus_set_error_recovery(_ctx, MODBUS_ERROR_RECOVERY_NONE); modbus_set_error_recovery(_ctx, MODBUS_ERROR_RECOVERY_LINK); modbus_close(_ctx); FD_CLR(i, &reads); LOG_ERROR("报文错误,服务端断开连接!"); close(i); _errCount++; } if (_errCount > 5) { _initialized = false; break; } } } } } _initialized = false; } void ModbusTcpServer::start() { std::thread loop([this]() { while (true) { if (_initialized) { recieveMessages(); } else { _initialized = true; } } }); loop.detach(); }
使用gtest进行单元测试,测试代码如下
#include "server/modbus_tcp_server.h" #include "gtest/gtest.h" class ModbusTcpServerTest : public testing::Test { public: std::unique_ptr<ModbusTcpServer> server; protected: virtual void SetUp() { try { server = std::make_unique<ModbusTcpServer>(5020, 10, true); // 创建Modbus服务器实例,监听端口5020,配置10台从机 server->start(); // 启动服务器 } catch (const std::exception &e) { std::cerr << "Exception: " << e.what() << std::endl; } } }; TEST_F(ModbusTcpServerTest, TestSetRegisterValue) { server->setHoldRegisterValue(1, 1, 100); ASSERT_EQ(100, server->getHoldRegisterValue(1, 1)); server->setInputRegisterValue(1, 1, 200); ASSERT_EQ(200, server->getInputRegisterValue(1, 1)); server->setCoilValue(1, 1, 1); ASSERT_TRUE(server->getCoilValue(1, 1)); server->setDiscreteInputValue(1, 1, 1); ASSERT_TRUE(server->getDiscreteInputValue(1, 1)); } int main() { testing::InitGoogleTest(); return RUN_ALL_TESTS(); }
测试结果
为了判断我们多从机是否设置成功,我们可以用网络调试助手手发报文测试
为了让服务一直运行,我们可以把启动服务里的detach改成join,让其阻塞主线程
用网络调试助手连接modbus服务端
设置从机地址1,输入寄存器地址1的值为1
报文格式:00 00 00 00 00 06 01 06 00 01 00 01
重点看后面7位
06:后面有6个值
01:从机地址为1
06:功能码,写输入寄存器
00 01:输入寄存器的地址,4位16进制数,转换成10进制就是0-65535
00 01:寄存器值,范围与上面寄存器地址范围相同
使用06功能码的时候,回复的值是一大串数据,这个没有影响
读取从机地址1,输入寄存器地址1的值,判断刚才手发的报文是否成功将值写入
报文格式:00 00 00 00 00 06 01 03 00 01 00 01
报文回复的后四位为00 01,值为1,说明值被成功写入了
读取从机地址2,输入寄存器地址1的值,判断之前的写命令是否干扰了其他从机
报文格式:00 00 00 00 00 06 02 03 00 01 00 01
报文回复的后四位为00 00,值为0,说明之前的写命令没有干扰其他从机
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。