Modbus协议控制动态链接库
应用场景
基于各门语言都有各自的modbus协议库,且良莠不齐,而且在具体的框架下可能存在版本依赖问题, 而且对modbus协议存在比较多的细节处理,可以查看modbus slave、或者modbus poll中相关的配置可知, 数据类型对应读写寄存器个数、大小端的处理等等细节,所以将以常用的场景下的配置编写动态链接库供其他语言调用。
编程语言
因为我不会写C语言,这里使用Go语言编写代码,以C函数分享库的模式编译成动态链接库
将以Python与Node.js调用的示例演示
辅助工具下载地址
Modbus Poll: 激活码(仅供学习 5A5742575C5D10) -> 模拟主站
链接: https://pan.baidu.com/s/1Sk2m6HWTU0-hE82-BxPvKA 提取码: 1fwn
Modbus Slave: 激活码(仅供学习 5455415451475662) -> 模拟从站
链接: https://pan.baidu.com/s/137w1-2gGQ3bETvbyBefNQg 提取码: 9433
编写Go代码示例 使用Cgo引入C函数
- Modbus TCP示例
package main /* #include <stdlib.h> */ import ( "C" "bytes" "encoding/binary" "fmt" "log" "math" "time" "github.com/goburrow/modbus" ) import "strings" var handler *modbus.TCPClientHandler //export Connect func Connect(ip *C.char, port C.int) C.int { log.Printf("Connect ...") handler = modbus.NewTCPClientHandler(fmt.Sprintf("%s:%d", C.GoString(ip), int(port))) handler.Timeout = 250 * time.Millisecond err := handler.Connect() if err != nil { log.Printf("Connect error: %v", err) return -1 } return 0 } //export Close func Close() C.int { log.Printf("Close ...") err := handler.Close() if err != nil { log.Printf("Close error: %v", err) return -1 } return 0 } //ReadRegister 一般错误返回 -1 连接失败返回 -3 //export ReadRegister func ReadRegister(slaveId C.int, address C.int, dataType *C.char, data *C.double) C.int { var results []byte var err error handler.SlaveId = byte(slaveId) client := modbus.NewClient(handler) dt := C.GoString(dataType) switch dt { case "int16": results, err = client.ReadInputRegisters(uint16(address), 1) case "int32", "float": results, err = client.ReadInputRegisters(uint16(address), 2) case "int64", "double": results, err = client.ReadInputRegisters(uint16(address), 4) } if err != nil { switch { case strings.Contains(err.Error(), "connection"): log.Println("Connection error:", err) return -3 case strings.Contains(err.Error(), "timeout"): log.Println("Timeout error:", err) return -1 default: log.Println("Other error:", err) return -1 } } var value float64 switch dt { case "int16": var intValue int16 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "int32": var intValue int32 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "float": var floatValue float32 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &floatValue) value = float64(floatValue) case "int64": var intValue int64 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "double": err = binary.Read(bytes.NewReader(results), binary.BigEndian, &value) } if err != nil { log.Println(err) return -1 } *data = C.double(value) return 0 } //ReadHoldingRegister 一般错误返回 -1 连接失败返回 -3 //export ReadHoldingRegister func ReadHoldingRegister(slaveId C.int, address C.int, dataType *C.char, data *C.double) C.int { var results []byte var err error handler.SlaveId = byte(slaveId) client := modbus.NewClient(handler) dt := C.GoString(dataType) // startTime := time.Now() switch dt { case "int16": results, err = client.ReadHoldingRegisters(uint16(address), 1) case "int32", "float": results, err = client.ReadHoldingRegisters(uint16(address), 2) case "int64", "double": results, err = client.ReadHoldingRegisters(uint16(address), 4) } // endTime := time.Now() // // 计算时间差 // duration := endTime.Sub(startTime) // fmt.Printf("Takes: %vn", duration) if err != nil { switch { case strings.Contains(err.Error(), "connection"): log.Println("Connection error:", err) return -3 case strings.Contains(err.Error(), "timeout"): log.Println("Timeout error:", err) return -1 default: log.Println("Other error:", err) return -1 } } var value float64 switch dt { case "int16": var intValue int16 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "int32": var intValue int32 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "float": var floatValue float32 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &floatValue) value = float64(floatValue) case "int64": var intValue int64 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "double": err = binary.Read(bytes.NewReader(results), binary.BigEndian, &value) } if err != nil { log.Println(err) return -1 } *data = C.double(value) return 0 } //export WriteRegister func WriteRegister(slaveId C.int, address C.int, dataType *C.char, value C.double) C.int { var err error handler.SlaveId = byte(slaveId) client := modbus.NewClient(handler) dt := C.GoString(dataType) switch dt { case "int16": var intValue int16 = int16(value) _, err = client.WriteSingleRegister(uint16(address), uint16(intValue)) case "int32": var intValue int32 = int32(value) results := make([]byte, 4) binary.BigEndian.PutUint32(results, uint32(intValue)) _, err = client.WriteMultipleRegisters(uint16(address), uint16(len(results)/2), results) case "float": var floatValue float32 = float32(value) results := make([]byte, 4) binary.BigEndian.PutUint32(results, math.Float32bits(floatValue)) _, err = client.WriteMultipleRegisters(uint16(address), uint16(len(results)/2), results) case "int64": var intValue int64 = int64(value) results := make([]byte, 8) binary.BigEndian.PutUint64(results, uint64(intValue)) _, err = client.WriteMultipleRegisters(uint16(address), uint16(len(results)/2), results) case "double": var doubleValue float64 = float64(value) results := make([]byte, 8) binary.BigEndian.PutUint64(results, math.Float64bits(doubleValue)) _, err = client.WriteMultipleRegisters(uint16(address), uint16(len(results)/2), results) default: return -1 } if err != nil { switch { case strings.Contains(err.Error(), "connection"): log.Println("Connection error:", err) return -3 case strings.Contains(err.Error(), "timeout"): log.Println("Timeout error:", err) return -1 default: log.Println("Other error:", err) return -1 } } return 0 } func main() {}
- Modbus RTU示例
package main /* #include <stdlib.h> */ import ( "C" "bytes" "encoding/binary" "log" "math" "time" "github.com/goburrow/modbus" ) import "strings" var handler *modbus.RTUClientHandler //export Connect func Connect(port *C.char, baudrate C.int) C.int { log.Printf("Connect ...") handler = modbus.NewRTUClientHandler(C.GoString(port)) handler.BaudRate = int(baudrate) handler.DataBits = 8 handler.Parity = "N" handler.StopBits = 1 handler.Timeout = 250 * time.Millisecond err := handler.Connect() if err != nil { log.Printf("Connect error: %v", err) return -1 } return 0 } //export Close func Close() C.int { log.Printf("Close ...") err := handler.Close() if err != nil { log.Printf("Close error: %v", err) return -1 } return 0 } //ReadRegister 一般错误返回 -1 连接失败返回 -3 //export ReadRegister func ReadRegister(slaveId C.int, address C.int, dataType *C.char, data *C.double) C.int { var results []byte var err error handler.SlaveId = byte(slaveId) client := modbus.NewClient(handler) dt := C.GoString(dataType) switch dt { case "int16": results, err = client.ReadInputRegisters(uint16(address), 1) case "int32", "float": results, err = client.ReadInputRegisters(uint16(address), 2) case "int64", "double": results, err = client.ReadInputRegisters(uint16(address), 4) } if err != nil { switch { case strings.Contains(err.Error(), "connection"): log.Println("Connection error:", err) return -3 case strings.Contains(err.Error(), "timeout"): log.Println("Timeout error:", err) return -1 default: log.Println("Other error:", err) return -1 } } var value float64 switch dt { case "int16": var intValue int16 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "int32": var intValue int32 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "float": var floatValue float32 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &floatValue) value = float64(floatValue) case "int64": var intValue int64 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "double": err = binary.Read(bytes.NewReader(results), binary.BigEndian, &value) } if err != nil { log.Println(err) return -1 } *data = C.double(value) return 0 } //ReadHoldingRegister 一般错误返回 -1 连接失败返回 -3 //export ReadHoldingRegister func ReadHoldingRegister(slaveId C.int, address C.int, dataType *C.char, data *C.double) C.int { var results []byte var err error handler.SlaveId = byte(slaveId) client := modbus.NewClient(handler) dt := C.GoString(dataType) // startTime := time.Now() switch dt { case "int16": results, err = client.ReadHoldingRegisters(uint16(address), 1) case "int32", "float": results, err = client.ReadHoldingRegisters(uint16(address), 2) case "int64", "double": results, err = client.ReadHoldingRegisters(uint16(address), 4) } // endTime := time.Now() // // 计算时间差 // duration := endTime.Sub(startTime) // fmt.Printf("Takes: %vn", duration) if err != nil { switch { case strings.Contains(err.Error(), "connection"): log.Println("Connection error:", err) return -3 case strings.Contains(err.Error(), "timeout"): log.Println("Timeout error:", err) return -1 default: log.Println("Other error:", err) return -1 } } var value float64 switch dt { case "int16": var intValue int16 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "int32": var intValue int32 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "float": var floatValue float32 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &floatValue) value = float64(floatValue) case "int64": var intValue int64 err = binary.Read(bytes.NewReader(results), binary.BigEndian, &intValue) value = float64(intValue) case "double": err = binary.Read(bytes.NewReader(results), binary.BigEndian, &value) } if err != nil { log.Println(err) return -1 } *data = C.double(value) return 0 } //export WriteRegister func WriteRegister(slaveId C.int, address C.int, dataType *C.char, value C.double) C.int { var err error handler.SlaveId = byte(slaveId) client := modbus.NewClient(handler) dt := C.GoString(dataType) switch dt { case "int16": var intValue int16 = int16(value) _, err = client.WriteSingleRegister(uint16(address), uint16(intValue)) case "int32": var intValue int32 = int32(value) results := make([]byte, 4) binary.BigEndian.PutUint32(results, uint32(intValue)) _, err = client.WriteMultipleRegisters(uint16(address), uint16(len(results)/2), results) case "float": var floatValue float32 = float32(value) results := make([]byte, 4) binary.BigEndian.PutUint32(results, math.Float32bits(floatValue)) _, err = client.WriteMultipleRegisters(uint16(address), uint16(len(results)/2), results) case "int64": var intValue int64 = int64(value) results := make([]byte, 8) binary.BigEndian.PutUint64(results, uint64(intValue)) _, err = client.WriteMultipleRegisters(uint16(address), uint16(len(results)/2), results) case "double": var doubleValue float64 = float64(value) results := make([]byte, 8) binary.BigEndian.PutUint64(results, math.Float64bits(doubleValue)) _, err = client.WriteMultipleRegisters(uint16(address), uint16(len(results)/2), results) default: return -1 } if err != nil { switch { case strings.Contains(err.Error(), "connection"): log.Println("Connection error:", err) return -3 case strings.Contains(err.Error(), "timeout"): log.Println("Timeout error:", err) return -1 default: log.Println("Other error:", err) return -1 } } return 0 } func main() {}
编译动态链接库
Go语言支持交叉编译成各个平台的二进制运行程序,但是Cgo不支持,可以去寻找第三方库去处理这个问题,这里选择用Docker容器
- 编译命令
windows下是modbus.dll,Linux下是 modbus.so,Macos下是 modbus.dylib
go build -o modbus.so -buildmode=c-shared main.go
- 其他平台编译,比如树莓派,选择使用docker容器
这里找一个 arm32v7 的docker容器,可以通过 docker search arm32v7的方式去搜索,我是试了几个选了个node版本的容器, 不同版本里面的 GLIBC 可能不一样,需要先查看你的树莓派上 GLIBC版本,通过命令 strings /lib/*/libc.so.* | grep GLIBC, 根据版本我选择了node v16的版本下载镜像
- 下载镜像、创建容器
docker run -itd --name raspberry arm32v7/node:16
- 进入容器、安装go环境
wget -e http_proxy=127.0.0.1:10809 https://studygolang.com/dl/golang/go1.17.linux-armv6l.tar.gz tar -zxvf go1.17.linux-armv6l.tar.gz go/ mv go /usr/local/go # 安装vim apt-get update apt-get install vim -y # 加入系统环境 vim /etc/profile # 加入内容到最后一行 export PATH=$PATH:/usr/local/go/bin source /etc/profile # 每次进入容器都要执行一次,要是没有找到go命令的话
- 将Go代码文件拷贝至容器
docker cp modbus_dir raspberry:/ # 将modbus_dir拷贝至容器raspberry的 / 目录下
- 进入容器编译动态链接库
docker exec -it raspberry /bin/bash cd modbus_dir go build -o modbus.so -buildmode=c-shared main.go
- 将编译后的动态链接库拷贝到宿主机
exit # 退出容器 docker cp raspberry:/modbus_dir/modbus.so ./ # 拷贝至当前目录
应用示例
- Python
modbus tcp示例
import ctypes import platform # 加载共享库 print(platform.system()) if platform.system() == "Windows": modbus = ctypes.CDLL('./modbus.dll') elif platform.system() == "Darwin": modbus = ctypes.CDLL('./modbus.dylib') else: # 树莓派 modbus = ctypes.CDLL('./modbus.so') # 定义函数参数类型和返回类型 modbus.ReadRegister.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)] modbus.ReadRegister.restype = ctypes.c_int modbus.WriteRegister.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_double] modbus.WriteRegister.restype = ctypes.c_int modbus.ReadHoldingRegister.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)] modbus.ReadHoldingRegister.restype = ctypes.c_int modbus.Connect.argtypes = [ctypes.c_char_p, ctypes.c_int] modbus.Connect.restype = ctypes.c_int modbus.Close.argtypes = [] modbus.Close.restype = ctypes.c_int # 调用ReadRegister函数 modbus.Connect(b'192.168.1.138', 502) # 写控制寄存器 for i in range(50): data = ctypes.c_double() result = modbus.WriteRegister(1, 30, b'float', 123.45 + i) if result == -3: print('Connect error.') if result == 0: print('Write success.') # 读控制寄存器 | 读输入寄存器时改成 ReadRegister for i in range(50): data = ctypes.c_double() result = modbus.ReadHoldingRegister(1, 30, b'int16', ctypes.byref(data)) print('Read Result:', result, data.value) modbus.Close()
modbus rtu示例
import ctypes import platform # 加载共享库 print(platform.system()) if platform.system() == "Windows": modbus = ctypes.CDLL('./modbus-rtu.dll') elif platform.system() == "Darwin": modbus = ctypes.CDLL('./modbus-rtu.dylib') else: # 树莓派 modbus = ctypes.CDLL('./modbus-rtu.so') # 定义函数参数类型和返回类型 modbus.ReadRegister.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)] modbus.ReadRegister.restype = ctypes.c_int modbus.WriteRegister.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_double] modbus.WriteRegister.restype = ctypes.c_int modbus.ReadHoldingRegister.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)] modbus.ReadHoldingRegister.restype = ctypes.c_int modbus.Connect.argtypes = [ctypes.c_char_p, ctypes.c_int] modbus.Connect.restype = ctypes.c_int modbus.Close.argtypes = [] modbus.Close.restype = ctypes.c_int # 调用ReadRegister函数 modbus.Connect(b'COM2', 9600) # 写控制寄存器 for i in range(50): data = ctypes.c_double() result = modbus.WriteRegister(1, 30, b'float', 123.45 + i) if result == -3: print('Connect error.') if result == 0: print('Write success.') # 读控制寄存器 | 读输入寄存器时改成 ReadRegister for i in range(50): data = ctypes.c_double() result = modbus.ReadHoldingRegister(1, 30, b'int16', ctypes.byref(data)) print('Read Result:', result, data.value) modbus.Close()
- Node.js
这是我在electron进行软件开发过程中封装的函数
调用顺序: DefineModbus -> connectModbus -> writeRegisterAsync/readRegisterAsync/readHoldingRegisterAsync -> connectModbus
const ffi = require('ffi-napi'); const ref = require('ref-napi'); const double = ref.types.double; const doublePtr = ref.refType(double); const path = require('path'); import {checkPort} from './net'; let dllPath: string; console.log(`当前平台: ${process.platform}`) // darwin let dllName = 'modbus.dll'; if (process.platform === 'darwin') { dllName = 'modbus.dylib'; } if (process.env.MODE === 'development') { dllPath = path.resolve(`extraResources/dlls/${dllName}`); } else { dllPath = path.join(process.resourcesPath, 'extraResources', 'dlls', dllName); } let modbus: any; const closeModbus = () => { return new Promise((resolve, reject) => { modbus.Close.async( (err: any, result: any) => { if (err) { reject(err); } else { resolve(result); } }, ); }); }; const connectModbus = (ip: string, port: number) => { return new Promise((resolve, reject) => { modbus.Connect.async( ip, port, (err: any, result: any) => { if (err) { reject(err); } else { resolve(result); } }, ); }); }; const writeRegisterAsync = ( slaveId: number, startAddress: number, dataType: string, val: number, ) => { return new Promise((resolve, reject) => { modbus.WriteRegister.async( slaveId, startAddress, dataType, val, (err: any, result: any) => { if (err) { reject(err); } else { resolve(result); } }, ); }); }; const readRegisterAsync = ( slaveId: number, startAddress: number, dataType: string, data: any, ) => { return new Promise((resolve, reject) => { modbus.ReadRegister.async( slaveId, startAddress, dataType, data, (err: any, result: any) => { if (err) { reject(err); } else { resolve(result); } }, ); }); }; const readHoldingRegisterAsync = ( slaveId: number, startAddress: number, dataType: string, data: any, ) => { return new Promise((resolve, reject) => { modbus.ReadHoldingRegister.async( slaveId, startAddress, dataType, data, (err: any, result: any) => { if (err) { reject(err); } else { resolve(result); } }, ); }); }; export const ConnectModbus = async (ip: string, port: number) => { try { const result = await connectModbus(ip, port); return [result, 0]; } catch (e) { return [-1, 0]; } } export const CloseModbus = async () => { try { const result = await closeModbus(); return [result, 0]; } catch(e) { return [-1, 0]; } } export const ReadByModbusTcp = async ( slaveId: number, startAddress: number, dataType: string, method: string = 'Input', ) => { const data = ref.alloc(double); try { let result: any; if (method === 'Input') { result = await readRegisterAsync(slaveId, startAddress, dataType, data); } else { result = await readHoldingRegisterAsync(slaveId, startAddress, dataType, data); } return [result, data.deref() as number]; } catch (e) { console.log(`e :${e}`); return [0, 0]; } } export const WriteByModbusTcp = async ( slaveId: number, startAddress: number, dataType: string, val: number, ) => { try { const result: any = await writeRegisterAsync( slaveId, startAddress, dataType, val, ); return [result, val]; } catch (e) { return [0, 0]; } } export const DefineModbus = async () => { try { modbus = ffi.Library(dllPath, { ReadRegister: ['int', ['int', 'int', 'string', doublePtr]], ReadHoldingRegister: ['int', ['int', 'int', 'string', doublePtr]], WriteRegister: ['int', ['int', 'int', 'string', 'double']], Connect: ['int', ['string', 'int']], Close: ['int', []], }); console.log('Define Modbus Success.'); } catch (e) { console.log(`defineModbus error: ${e}`); } return dllPath; } export const CheckConnected = async (ip: string, port: number) => { try { const result = await checkPort(ip, port); return true; } catch (e) { return false; } }
最后
这里只写了 读写控制寄存器、读输入寄存器,支持数据类型 int16/int32/int64/float/double
可以根据需要加入自己的代码,比如线圈的读写控制设备启停等
动态链接库、源码地址
链接: https://pan.baidu.com/s/1q40gHnftqzGJtUgfws_sdw 提取码: 72o2