github.com/simonvetter/modbus v1.6.0
client.go
这是一个Modbus客户端程序,用于与Modbus设备通信。
1type ModbusClient struct {
2 conf *ClientConfiguration
3 trans transport
4 isConnected bool
5 connectedAt time.Time
6 timeout time.Duration
7 logger *logger
8}
定义了一个ModbusClient
结构体,该结构体保存与Modbus客户端相关的状态和配置信息。它包括以下字段:
conf
: 客户端的配置信息。trans
: 用于与Modbus设备通信的传输层实例。isConnected
: 一个布尔值,指示客户端是否已连接。connectedAt
: 客户端上次连接的时间。timeout
: 客户端通信的超时时间。logger
: 用于日志记录的实例。
接下来,我们看到了一些关于ClientConfiguration
的定义,这是一个结构体,用于保存客户端的配置信息。
1type ClientConfiguration struct {
2 URL string
3 ...
4 Speed uint
5 ...
6 Parity Parity
7 ...
8 Timeout time.Duration
9 ...
10}
这个结构体包含了Modbus客户端的各种配置选项,如URL
(设备的URL)、Speed
(通信速率)、Parity
(奇偶校验)等。
NewClient 函数分析
简介
NewClient
函数用于基于提供的配置创建、配置并返回一个 Modbus 客户端对象。
函数签名
1func NewClient(conf *ClientConfiguration) (mc *ModbusClient, err error)
参数
conf
: 指向ClientConfiguration
结构的指针,其中包含Modbus客户端的配置。
返回值
mc
: 指向创建的ModbusClient
对象的指针。err
: 在客户端创建过程中出现的任何错误。
函数分析
-
初始化:
1var clientType string 2var splitURL []string 3 4mc = &ModbusClient{ 5 conf: *conf, 6}
- 声明了两个局部变量,
clientType
和splitURL
。 - 为
ModbusClient
对象分配内存并使用提供的配置进行初始化。
- 声明了两个局部变量,
-
URL 解析:
1splitURL = strings.SplitN(mc.conf.URL, "://", 2) 2if len(splitURL) == 2 { 3 clientType = splitURL[0] 4 mc.conf.URL = splitURL[1] 5}
- 使用 “://” 作为分隔符将配置中的URL分成两部分。
clientType
保存协议类型(例如 “rtu”, “tcp”)。mc.conf.URL
存储URL的其余部分。
-
日志记录器初始化:
1mc.logger = newLogger( 2 fmt.Sprintf("modbus-client(%s)", mc.conf.URL), conf.Logger)
- 使用指定的URL和配置中提供的日志记录器为Modbus客户端初始化一个日志记录器。
-
客户端类型配置:
-
根据
clientType
为Modbus客户端设置不同的默认值和配置。switch-case结构处理不同的客户端类型,对于一些未指定的配置,在这里设置默认值:-
RTU (Remote Terminal Unit):
- 为速度、数据位、停止位和超时设置默认值。
- 注意:“modbus over serial line v1.02” 文档规定了一个11位的字符帧,默认使用偶校验和1个停止位。当不使用奇偶校验时,必须使用2个停止位。这个协议栈默认设置为8/N/2,因为大多数设备似乎不使用奇偶校验,但尝试使用8/N/1、8/E/1和8/O/1可能有助于解决串行通信问题。。
- 传输类型设置为
modbusRTU
。
-
RTU Over TCP:
- 设置速度和超时的默认值。
- 传输类型设置为
modbusRTUOverTCP
。
-
RTU Over UDP:
- 则设置速度和超时的默认值。
- 传输类型设置为
modbusRTUOverUDP
。
-
TCP:
- 设置超时的默认值。
- 传输类型设置为
modbusTCP
。
-
TCP with TLS:
- 设置超时的默认值。
- 检查是否存在客户端证书和CA/服务器证书。如果其中之一缺失,则返回错误。
- 传输类型设置为
modbusTCPOverTLS
。
-
UDP:
- 设置超时的默认值。
-
-
最后,设置客户端的单位ID、字节序和字顺序,并返回客户端对象。
ModbusClient 的 Open 方法
功能:打开底层的传输连接,这可以是网络连接或串行连接。
1. 锁定互斥锁
这确保在同一时间只有一个线程可以执行此方法。
1mc.lock.Lock()
2defer mc.lock.Unlock()
2. 根据 mc.transportType
的值进行操作
mc.transportType
决定了 Modbus 的通信方式。
1switch mc.transportType {
3. 使用串行 Modbus RTU
- 创建串行端口包装对象。
- 打开串行设备。
- 丢弃可能过时的串行数据。
- 创建 RTU 传输。
1case modbusRTU:
4. 使用 Modbus RTU over TCP
- 连接到远程主机。
- 丢弃可能的过时数据。
- 创建 RTU 传输。
1case modbusRTUOverTCP:
5. 使用 Modbus RTU over UDP
- 打开到远程主机的套接字(UDP 是无连接的)。
- 创建 RTU 传输。
1case modbusRTUOverUDP:
6. 使用标准的 Modbus TCP
- 连接到远程主机。
- 创建 TCP 传输。
1case modbusTCP:
7. 使用 TLS 加密的 Modbus TCP
- 使用TLS连接到远程主机。
- 完成 TLS 握手。
- 创建 TCP 传输。
1case modbusTCPOverTLS:
8. 使用 Modbus TCP over UDP
- 打开到远程主机的套接字。
- 创建 TCP 传输。
1case modbusTCPOverUDP:
9. 默认处理
如果不是以上已知的任何类型,则返回配置错误。
1default:
10. 结束
返回可能的错误。
1return
Open
方法为ModbusClient
提供了一个方式来选择与远程设备或服务器之间的通信方式。这为实现不同的 Modbus 通信场景提供了很大的灵活性。
client中对线圈和寄存器的读写到最后都是通过调用以下两个方法实现的:
1func (mc *ModbusClient) readRegisters(addr uint16, quantity uint16, regType RegType) (bytes []byte, err error)
2
3func (mc *ModbusClient) writeRegisters(addr uint16, values []byte) (err error)
接下来我们对这两个方法进行分析 首先来看 readRegisters 方法
readRegisters
方法分析
功能:这个函数读取 Modbus 寄存器,并根据所提供的寄存器类型返回相应的字节数据。
1. 锁定互斥锁
确保同一时间只有一个协程可以执行此方法。
1mc.lock.Lock()
2defer mc.lock.Unlock()
2. 创建并初始化请求对象
根据客户端的 unitId
来创建请求对象。
1req = &pdu{
2 unitId: mc.unitId,
3}
3. 根据寄存器类型设置功能码
根据提供的寄存器类型,设置功能码。如果给定的寄存器类型不是预期的类型,则返回错误。
1switch regType {
4. 检查寄存器数量
确保提供的寄存器数量是有效的。对于 Modbus 协议,最多可以一次读取123个寄存器。
1if quantity == 0 || quantity > 123 {
5. 检查地址范围
确保地址加上数量不超过最大的 Modbus 地址。
1if uint32(addr)+uint32(quantity)-1 > 0xffff {
6. 设置请求的载荷
这将请求的开始地址和数量添加到载荷中。
1req.payload = uint16ToBytes(BIG_ENDIAN, addr)
2req.payload = append(req.payload, uint16ToBytes(BIG_ENDIAN, quantity)...)
7. 执行请求并等待响应
使用 mc.executeRequest
方法发送请求并等待响应。
数据传输操作发生在这个函数里。最终会调用到 mc.transport.ExecuteRequest(req)
transport.ExecuteRequest 是一个接口方法,具体实现可以看 rtu_transport.go或者tcp_transport.go中的ExecuteRequest实现
1res, err = mc.executeRequest(req)
8. 校验响应代码
检查响应代码 res.functionCode
- 如果响应代码与请求代码相匹配,验证载荷长度和字节计数字段。
- 如果响应代码与请求代码的异常版本匹配,则映射异常代码以获得相应的错误。
- 对于所有其他情况,返回协议错误。
9. 结束
返回读取到的字节或可能的错误。
1return
readRegisters
函数提供了一个方式,根据所提供的寄存器类型从 Modbus 设备读取数据。函数首先校验参数的有效性,然后执行请求,并对响应数据进行相应的验证和处理。
writeRegisters
方法分析
这个函数写入 Modbus 寄存器。
Modbus RTU协议请示中的每个字段的描述如下:
-
从机地址 (Slave Address):
- 这是一个字节,代表在Modbus RTU网络中特定的从机或设备的地址。它用于标识网络上的特定设备,以便主机可以向它发送命令或从它那里获取数据。
-
功能码 (Function Code):
- 这也是一个字节,指示请求的操作类型。例如,功能码可以指示读取多个保持寄存器、写入单个寄存器等操作。
-
起始地址 (Starting Address):
- 这由两个字节组成,包括高位(起始地址高位)和低位(起始地址低位)。它表示您想要开始读取或写入的寄存器的16位地址。这通常是连续读/写操作的起始点。
-
寄存器数量 (Number of Registers):
- 这同样由两个字节组成,包括高位(寄存器数量高位)和低位(寄存器数量低位)。它指示您想要读取或写入的寄存器的数量。
在Modbus RTU模式下,请求的结尾还会包含两个字节的CRC(循环冗余校验)来检查数据的完整性。CRC确保数据在传输过程中没有被损坏。
总之,Modbus RTU请求格式的核心是:从机地址、功能码、起始地址和寄存器数量,最后是CRC校验。这个格式确保了从站可以准确地解析主站的请求并提供适当的响应。
接下来我们来看一下这个函数的实现
1. 锁定互斥锁
确保同一时间只有一个线程可以执行此方法。
1mc.lock.Lock()
2defer mc.lock.Unlock()
2. 计算载荷长度和寄存器数量
1payloadLength = uint16(len(values))
2quantity = payloadLength / 2
3. 检查寄存器数量
确保提供的寄存器数量是有效的。对于 Modbus 协议,最多可以一次写入123个寄存器。
1if quantity == 0 || quantity > 123 {
4. 检查地址范围
确保地址加上数量不超过最大的 Modbus 地址。
1if uint32(addr)+uint32(quantity)-1 > 0xffff {
5. 创建并初始化请求对象
设置单元 ID 和功能码。
1req = &pdu{
2 unitId: mc.unitId,
3 functionCode: fcWriteMultipleRegisters,
4}
6. 设置请求的载荷
这将请求的开始地址、寄存器数量、字节计数和寄存器值添加到载荷中。
1req.payload = uint16ToBytes(BIG_ENDIAN, addr)
2req.payload = append(req.payload, uint16ToBytes(BIG_ENDIAN, quantity)...)
3req.payload = append(req.payload, values...)
7. 执行请求并等待响应
使用 mc.executeRequest
方法发送请求并等待响应。
1res, err = mc.executeRequest(req)
8. 校验响应代码
- 如果响应代码与请求代码相匹配,验证载荷长度和返回的寄存器地址与数量。
- 如果响应代码与请求代码的异常版本匹配,则映射异常代码以获得相应的错误。
- 对于所有其他情况,返回协议错误。