Skip to content

github.com/simonvetter/modbus client.go源码分析

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: 在客户端创建过程中出现的任何错误。

函数分析

  1. 初始化:

    1var clientType string
    2var splitURL   []string
    3
    4mc = &ModbusClient{
    5    conf: *conf,
    6}
    
    • 声明了两个局部变量,clientTypesplitURL
    • ModbusClient对象分配内存并使用提供的配置进行初始化。
  2. 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的其余部分。
  3. 日志记录器初始化:

    1mc.logger = newLogger(
    2    fmt.Sprintf("modbus-client(%s)", mc.conf.URL), conf.Logger)
    
    • 使用指定的URL和配置中提供的日志记录器为Modbus客户端初始化一个日志记录器。
  4. 客户端类型配置:

    • 根据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协议请示中的每个字段的描述如下:

  1. 从机地址 (Slave Address):

    • 这是一个字节,代表在Modbus RTU网络中特定的从机或设备的地址。它用于标识网络上的特定设备,以便主机可以向它发送命令或从它那里获取数据。
  2. 功能码 (Function Code):

    • 这也是一个字节,指示请求的操作类型。例如,功能码可以指示读取多个保持寄存器、写入单个寄存器等操作。
  3. 起始地址 (Starting Address):

    • 这由两个字节组成,包括高位(起始地址高位)和低位(起始地址低位)。它表示您想要开始读取或写入的寄存器的16位地址。这通常是连续读/写操作的起始点。
  4. 寄存器数量 (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. 校验响应代码

  • 如果响应代码与请求代码相匹配,验证载荷长度和返回的寄存器地址与数量。
  • 如果响应代码与请求代码的异常版本匹配,则映射异常代码以获得相应的错误。
  • 对于所有其他情况,返回协议错误。

Related Posts

  1. github.com/simonvetter/modbus rtu_transport.go源码分析
  2. github.com/simonvetter/modbus server.go源码分析
  3. github.com/simonvetter/modbus tcp_transport.go源码分析