Go 언어 Mod bus 개발 에 대한 전체 가 이드 : grid - x / mod bus 라이 브 러 리 TCP / R TU / AS C II / U DP 전체 프로토 콜

무료무료 기술 자료

이 글은 바로 읽을 수 있으며 기초 학습과 검색 유입에 적합합니다.

Go 언어 Mod bus 개발 에 대한 전체 가 이드 : grid - x / mod bus 라이 브 러 리 TCP / R TU / AS C II / U DP 전체 프로토 콜

grid-x/modbus是Go언어生态中最活跃、功能最完善的Modbus프로토콜库之一,支持Modbus TCP、RTU、ASCII、UDP四种传输모드,提供基于context.Context的超时控制和简洁一致的API。本文详细介绍grid-x/modbus库的安装설정、TCP/RTU클라이언트实现、데이터유형转换、并发采集、服务端实现以及完整的项目예제,帮助Go개발者快速掌握工业Modbus通讯개발。

一、grid-x/modbus库개요

1.1 核心特性

  • 多프로토콜支持:Modbus TCP、RTU、ASCII、UDP四种传输모드
  • Context支持:所有操作接受context.Context,支持超时和취소
  • 功能完整:支持01-16표준기능 코드,以及22(掩码写)、23(读写多个)、24(读FIFO)、43(设备识别)
  • 主从兼备:既可作为클라이언트(마스터),也可作为服务端(슬레이브)
  • 故障容错:fail-fast设计,연결失败바로报错,不静默重试
  • 跨平台:Windows、Linux、Mac OS、嵌入式Linux均可运行
  • 零依赖:仅依赖Go표준库,无第三方依赖
  • 活跃维护:持续更新,社区活跃,문서完善

1.2 与其他Go Modbus库对比

库名프로토콜支持Context服务端活跃度推荐시나리오
grid-x/modbusTCP/RTU/ASCII/UDP支持支持高新项目首选
goburrow/modbusTCP/RTU不支持支持中遗留项目
simonvetter/modbusTCP/RTU支持不支持中简单클라이언트
aldas/go-modbus-clientTCP支持不支持中请求/响应모드

二、安装与환경설정

2.1 安装grid-x/modbus

# 初始化Go模块
go mod init modbus_demo

# 安装grid-x/modbus
go get github.com/grid-x/modbus

# 验证安装
go list -m all | grep grid-x

2.2 导入包

import (
    "context"
    "github.com/grid-x/modbus"
)

三、Modbus TCP클라이언트

3.1 快速开始(简洁모드)

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/grid-x/modbus"
)

func main() {
    // 创建TCP클라이언트(简洁모드,自动管理연결)
    client := modbus.TCPClient("192.168.1.100:502")

    ctx := context.Background()

    // 读유지 레지스터(기능 코드03)
    // 注意:返回值是[]byte,需要手动분석为uint16
    results, err := client.ReadHolding회원가입s(ctx, 0, 10)
    if err != nil {
        log.Fatalf("读유지 레지스터失败:%v", err)
    }

    // 분석레지스터 값(每2바이트为一个16位레지스터,高바이트在前)
    fmt.Printf("읽기到%d바이트데이터:\n", len(results))
    for i := 0; i < len(results); i += 2 {
        value := uint16(results[i])<<8 | uint16(results[i+1])
        fmt.Printf("  레지스터%d:%d\n", i/2, value)
    }
}

3.2 高级모드(手动管理연결)

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/grid-x/modbus"
)

func main() {
    // 创建TCP Handler(可설정超时、슬레이브 주소、로그等)
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.Timeout = 10 * time.Second
    handler.SlaveID = 1
    handler.Logger = log.New(os.Stdout, "modbus: ", log.LstdFlags)

    ctx := context.Background()

    // 手动建立연결(多次请求复用同一연결)
    if err := handler.Connect(ctx); err != nil {
        log.Fatalf("연결失败:%v", err)
    }
    defer handler.닫기()

    // 创建클라이언트
    client := modbus.NewClient(handler)

    // 连续多次请求(复用연결)
    for i := 0; i < 5; i++ {
        results, err := client.ReadHolding회원가입s(ctx, uint16(i*10), 10)
        if err != nil {
            log.Printf("第%d次읽기失败:%v", i, err)
            continue
        }
        fmt.Printf("批次%d:읽기到%d바이트\n", i, len(results))
    }
}

3.3 完整기능 코드예제

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/grid-x/modbus"
)

func main() {
    client := modbus.TCPClient("192.168.1.100:502")
    ctx := context.Background()

    // === 기능 코드01:读코일 ===
    // 返回[]byte,每一位对应一个코일 상태
    coils, err := client.ReadCoils(ctx, 0, 8)
    if err != nil {
        log.Fatalf("读코일失败:%v", err)
    }
    fmt.Println("=== 读코일(기능 코드01)===")
    for i := 0; i < 8; i++ {
        byteIdx := i / 8
        bitIdx := uint(i % 8)
        state := (coils[byteIdx] >> bitIdx) & 0x01
        fmt.Printf("  코일%d:%s\n", i, map[byte]string{0: "断开", 1: "吸合"}[state])
    }

    // === 기능 코드02:读이산 입력 ===
    inputs, err := client.ReadDiscreteInputs(ctx, 0, 4)
    if err != nil {
        log.Fatalf("读이산 입력失败:%v", err)
    }
    fmt.Println("\n=== 读이산 입력(기능 코드02)===")
    for i := 0; i < 4; i++ {
        state := (inputs[i/8] >> uint(i%8)) & 0x01
        fmt.Printf("  입력%d:%d\n", i, state)
    }

    // === 기능 코드03:读유지 레지스터 ===
    regs, err := client.ReadHolding회원가입s(ctx, 0, 10)
    if err != nil {
        log.Fatalf("读유지 레지스터失败:%v", err)
    }
    fmt.Println("\n=== 读유지 레지스터(기능 코드03)===")
    for i := 0; i < len(regs); i += 2 {
        value := uint16(regs[i])<<8 | uint16(regs[i+1])
        fmt.Printf("  레지스터%d:%d\n", i/2, value)
    }

    // === 기능 코드04:读입력 레지스터 ===
    inputRegs, err := client.ReadInput회원가입s(ctx, 0, 2)
    if err != nil {
        log.Fatalf("读입력 레지스터失败:%v", err)
    }
    fmt.Println("\n=== 读입력 레지스터(기능 코드04)===")
    for i := 0; i < len(inputRegs); i += 2 {
        value := uint16(inputRegs[i])<<8 | uint16(inputRegs[i+1])
        fmt.Printf("  입력 레지스터%d:%d\n", i/2, value)
    }

    // === 기능 코드05:写单个코일 ===
    // 0xFF00 = 吸合,0x0000 = 断开
    _, err = client.WriteSingleCoil(ctx, 0, 0xFF00)
    if err != nil {
        log.Fatalf("写코일失败:%v", err)
    }
    fmt.Println("\n=== 写单个코일(기능 코드05)===")
    fmt.Println("  코일0已置为吸合")

    // === 기능 코드06:写单个레지스터 ===
    _, err = client.WriteSingle회원가입(ctx, 100, 1234)
    if err != nil {
        log.Fatalf("写레지스터失败:%v", err)
    }
    fmt.Println("\n=== 写单个레지스터(기능 코드06)===")
    fmt.Println("  레지스터100已쓰기1234")

    // === 기능 코드0F:写多个코일 ===
    // value매개변수为[]byte,按位打包
    coil데이터 := []byte{0x05} // 2진수00000101 = 코일0和코일2吸合
    _, err = client.WriteMultipleCoils(ctx, 0, 8, coil데이터)
    if err != nil {
        log.Fatalf("写多个코일失败:%v", err)
    }
    fmt.Println("\n=== 写多个코일(기능 코드0F)===")
    fmt.Println("  코일0-7已批量쓰기")

    // === 기능 코드10:写多个레지스터 ===
    // value매개변수为[]byte,每2바이트一个레지스터
    reg데이터 := []byte{0x00, 0x64, 0x00, 0xC8, 0x01, 0x2C} // 100, 200, 300
    _, err = client.WriteMultiple회원가입s(ctx, 200, 3, reg데이터)
    if err != nil {
        log.Fatalf("写多个레지스터失败:%v", err)
    }
    fmt.Println("\n=== 写多个레지스터(기능 코드10)===")
    fmt.Println("  레지스터200-202已批量쓰기")
}

四、데이터유형转换

grid-x/modbus的所有读方法返回[]byte,需要개발者手动분석为目标데이터유형。以下提供完整的转换도구函数。

4.1 레지스터与바이트转换

package modbusutil

import "encoding/binary"

// BytesTo회원가입s 将[]byte분석为[]uint16(大端序)
func BytesTo회원가입s(데이터 []byte) []uint16 {
    regs := make([]uint16, len(데이터)/2)
    for i := 0; i < len(regs); i++ {
        regs[i] = binary.BigEndian.Uint16(데이터[i*2 : i*2+2])
    }
    return regs
}

// 회원가입sToBytes 将[]uint16转换为[]byte(大端序)
func 회원가입sToBytes(regs []uint16) []byte {
    데이터 := make([]byte, len(regs)*2)
    for i, v := range regs {
        binary.BigEndian.PutUint16(데이터[i*2:i*2+2], v)
    }
    return 데이터
}

// BytesToBools 将[]byte분석为[]bool(低位在前)
func BytesToBools(데이터 []byte, count int) []bool {
    bools := make([]bool, count)
    for i := 0; i < count; i++ {
        bools[i] = (데이터[i/8]>>uint(i%8))&0x01 == 1
    }
    return bools
}

// BoolsToBytes 将[]bool打包为[]byte
func BoolsToBytes(bools []bool) []byte {
    byteCount := (len(bools) + 7) / 8
    데이터 := make([]byte, byteCount)
    for i, v := range bools {
        if v {
            데이터[i/8] |= 1 << uint(i%8)
        }
    }
    return 데이터
}

4.2 32位데이터유형(浮点数、整数)

import "math"

// BytesToFloat32ABCD 분석32位浮点数(ABCD大端序)
func BytesToFloat32ABCD(데이터 []byte) float32 {
    bits := uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
        uint32(데이터[2])<<8 | uint32(데이터[3])
    return math.Float32frombits(bits)
}

// BytesToFloat32CDAB 분석32位浮点数(CDAB字交换序)
func BytesToFloat32CDAB(데이터 []byte) float32 {
    bits := uint32(데이터[2])<<24 | uint32(데이터[3])<<16 |
        uint32(데이터[0])<<8 | uint32(데이터[1])
    return math.Float32frombits(bits)
}

// BytesToFloat32DCBA 분석32位浮点数(DCBA小端序)
func BytesToFloat32DCBA(데이터 []byte) float32 {
    bits := uint32(데이터[3])<<24 | uint32(데이터[2])<<16 |
        uint32(데이터[1])<<8 | uint32(데이터[0])
    return math.Float32frombits(bits)
}

// Float32ToBytesABCD 将float32转换为4바이트(ABCD大端序)
func Float32ToBytesABCD(f float32) []byte {
    bits := math.Float32bits(f)
    return []byte{
        byte(bits >> 24), byte(bits >> 16),
        byte(bits >> 8), byte(bits),
    }
}

// BytesToInt32 분석32位有符号整数(大端序)
func BytesToInt32(데이터 []byte) int32 {
    return int32(uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
        uint32(데이터[2])<<8 | uint32(데이터[3]))
}

// BytesToUint32 분석32位无符号整数(大端序)
func BytesToUint32(데이터 []byte) uint32 {
    return uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
        uint32(데이터[2])<<8 | uint32(데이터[3])
}

// Int32ToBytes 将int32转换为4바이트(大端序)
func Int32ToBytes(v int32) []byte {
    u := uint32(v)
    return []byte{byte(u >> 24), byte(u >> 16), byte(u >> 8), byte(u)}
}

4.3 바이트序对照表

바이트序설명32位值0x41420000的存储방식
ABCD大端序(표준)reg[0]=0x4142, reg[1]=0x0000
CDAB字交换reg[0]=0x0000, reg[1]=0x4142
DCBA小端序reg[0]=0x0000, reg[1]=0x4241
BADC바이트交换reg[0]=0x4241, reg[1]=0x0000

五、Modbus RTU시리얼 포트通讯

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/grid-x/modbus"
)

func main() {
    // 创建RTU Handler
    handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    handler.BaudRate = 9600      // 통신 속도
    handler.데이터Bits = 8         // 데이터 비트
    handler.Parity = "N"         // 패리티:N=无,E=偶,O=奇
    handler.StopBits = 1         // 정지 비트
    handler.SlaveID = 1          // 슬레이브 주소
    handler.Timeout = 5 * time.Second

    ctx := context.Background()

    // 연결
    if err := handler.Connect(ctx); err != nil {
        log.Fatalf("RTU연결失败:%v", err)
    }
    defer handler.닫기()

    client := modbus.NewClient(handler)

    // 유지 레지스터 읽기 (FC03)
    results, err := client.ReadHolding회원가입s(ctx, 0, 10)
    if err != nil {
        log.Fatalf("읽기失败:%v", err)
    }

    regs := make([]uint16, len(results)/2)
    for i := range regs {
        regs[i] = uint16(results[i*2])<<8 | uint16(results[i*2+1])
    }
    fmt.Printf("레지스터 값:%v\n", regs)

    // Windows시리얼 포트예제:
    // handler := modbus.NewRTUClientHandler("COM3")
}

六、Modbus ASCII与UDP

// === Modbus ASCII ===
// ASCII모드使用可打印문자,适合디버깅和低可靠性链路
client := modbus.ASCIIClient("/dev/ttyUSB0")
// 설정방식与RTU相同,通过Handler设置

// === Modbus UDP ===
// UDP모드无연결,适合广播和简单查询
client := modbus.RTUOverUDPClient("192.168.1.255:502")

// === RTU over TCP ===
// 将RTU帧封装在TCP中(部分设备使用此모드)
client := modbus.RTUOverTCPClient("192.168.1.100:502")

// === ASCII over TCP ===
client := modbus.ASCIIOverTCPClient("192.168.1.100:502")

七、设备识别(기능 코드43)

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/grid-x/modbus"
)

func main() {
    client := modbus.TCPClient("192.168.1.100:502")
    ctx := context.Background()

    // 읽기基本设备信息(厂商名、产品代码、버전号)
    deviceInfo, err := client.ReadDeviceIdentification(ctx, modbus.ReadDeviceIDCodeBasic)
    if err != nil {
        log.Fatalf("읽기设备信息失败:%v", err)
    }

    fmt.Printf("设备信息:%+v\n", deviceInfo)

    // 읽기常规设备信息(厂商URL、产品名、型号、적용名)
    regularInfo, err := client.ReadDeviceIdentification(ctx, modbus.ReadDeviceIDCodeRegular)
    if err != nil {
        log.Printf("읽기常规信息失败:%v", err)
    } else {
        fmt.Printf("常规信息:%+v\n", regularInfo)
    }

    // 읽기特定对象(对象ID=0通常是厂商名)
    specificInfo, err := client.ReadDeviceIdentificationSpecificObject(ctx, 0)
    if err != nil {
        log.Printf("읽기特定对象失败:%v", err)
    } else {
        fmt.Printf("对象0:%s\n", specificInfo)
    }
}

八、完整도구类封装

package modbusutil

import (
    "context"
    "encoding/binary"
    "math"
    "sync"
    "time"

    "github.com/grid-x/modbus"
)

// ModbusClient 封装grid-x/modbus的도구类
type ModbusClient struct {
    handler *modbus.TCPClientHandler
    client  modbus.Client
    mu      sync.Mutex
}

// NewModbusClient 创建TCP클라이언트
func NewModbusClient(주소 string, slaveID byte, timeout time.Duration) (*ModbusClient, error) {
    handler := modbus.NewTCPClientHandler(주소)
    handler.SlaveID = slaveID
    handler.Timeout = timeout

    ctx := context.Background()
    if err := handler.Connect(ctx); err != nil {
        return nil, err
    }

    return &ModbusClient{
        handler: handler,
        client:  modbus.NewClient(handler),
    }, nil
}

// 닫기 닫기연결
func (m *ModbusClient) 닫기() error {
    return m.handler.닫기()
}

// Read회원가입s 读유지 레지스터,返回[]uint16
func (m *ModbusClient) Read회원가입s(ctx context.Context, addr, qty uint16) ([]uint16, error) {
    m.mu.Lock()
    defer m.mu.Unlock()

    데이터, err := m.client.ReadHolding회원가입s(ctx, addr, qty)
    if err != nil {
        return nil, err
    }
    return BytesTo회원가입s(데이터), nil
}

// ReadInput회원가입s 读입력 레지스터
func (m *ModbusClient) ReadInput회원가입s(ctx context.Context, addr, qty uint16) ([]uint16, error) {
    m.mu.Lock()
    defer m.mu.Unlock()

    데이터, err := m.client.ReadInput회원가입s(ctx, addr, qty)
    if err != nil {
        return nil, err
    }
    return BytesTo회원가입s(데이터), nil
}

// ReadCoils 读코일,返回[]bool
func (m *ModbusClient) ReadCoils(ctx context.Context, addr, qty uint16) ([]bool, error) {
    m.mu.Lock()
    defer m.mu.Unlock()

    데이터, err := m.client.ReadCoils(ctx, addr, qty)
    if err != nil {
        return nil, err
    }
    return BytesToBools(데이터, int(qty)), nil
}

// ReadDiscreteInputs 读이산 입력
func (m *ModbusClient) ReadDiscreteInputs(ctx context.Context, addr, qty uint16) ([]bool, error) {
    m.mu.Lock()
    defer m.mu.Unlock()

    데이터, err := m.client.ReadDiscreteInputs(ctx, addr, qty)
    if err != nil {
        return nil, err
    }
    return BytesToBools(데이터, int(qty)), nil
}

// Write회원가입 写单个레지스터
func (m *ModbusClient) Write회원가입(ctx context.Context, addr, value uint16) error {
    m.mu.Lock()
    defer m.mu.Unlock()

    _, err := m.client.WriteSingle회원가입(ctx, addr, value)
    return err
}

// Write회원가입s 写多个레지스터
func (m *ModbusClient) Write회원가입s(ctx context.Context, addr uint16, values []uint16) error {
    m.mu.Lock()
    defer m.mu.Unlock()

    데이터 := 회원가입sToBytes(values)
    _, err := m.client.WriteMultiple회원가입s(ctx, addr, uint16(len(values)), 데이터)
    return err
}

// WriteCoil 写单个코일
func (m *ModbusClient) WriteCoil(ctx context.Context, addr uint16, value bool) error {
    m.mu.Lock()
    defer m.mu.Unlock()

    var v uint16
    if value {
        v = 0xFF00
    }
    _, err := m.client.WriteSingleCoil(ctx, addr, v)
    return err
}

// WriteCoils 写多个코일
func (m *ModbusClient) WriteCoils(ctx context.Context, addr uint16, values []bool) error {
    m.mu.Lock()
    defer m.mu.Unlock()

    데이터 := BoolsToBytes(values)
    _, err := m.client.WriteMultipleCoils(ctx, addr, uint16(len(values)), 데이터)
    return err
}

// ReadFloat32 读32位浮点数(ABCD大端序)
func (m *ModbusClient) ReadFloat32(ctx context.Context, addr uint16) (float32, error) {
    데이터, err := m.client.ReadHolding회원가입s(ctx, addr, 2)
    if err != nil {
        return 0, err
    }
    return BytesToFloat32ABCD(데이터), nil
}

// WriteFloat32 写32位浮点数(ABCD大端序)
func (m *ModbusClient) WriteFloat32(ctx context.Context, addr uint16, value float32) error {
    데이터 := Float32ToBytesABCD(value)
    _, err := m.client.WriteMultiple회원가입s(ctx, addr, 2, 데이터)
    return err
}

// ReadInt32 读32位有符号整数
func (m *ModbusClient) ReadInt32(ctx context.Context, addr uint16) (int32, error) {
    데이터, err := m.client.ReadHolding회원가입s(ctx, addr, 2)
    if err != nil {
        return 0, err
    }
    return BytesToInt32(데이터), nil
}

// ReadString 读문자串(每个레지스터存2个문자)
func (m *ModbusClient) ReadString(ctx context.Context, addr, regCount uint16) (string, error) {
    데이터, err := m.client.ReadHolding회원가입s(ctx, addr, regCount)
    if err != nil {
        return "", err
    }
    return string(데이터), nil
}

// 辅助函数(同前)
func BytesTo회원가입s(데이터 []byte) []uint16 {
    regs := make([]uint16, len(데이터)/2)
    for i := range regs {
        regs[i] = binary.BigEndian.Uint16(데이터[i*2 : i*2+2])
    }
    return regs
}

func 회원가입sToBytes(regs []uint16) []byte {
    데이터 := make([]byte, len(regs)*2)
    for i, v := range regs {
        binary.BigEndian.PutUint16(데이터[i*2:i*2+2], v)
    }
    return 데이터
}

func BytesToBools(데이터 []byte, count int) []bool {
    bools := make([]bool, count)
    for i := 0; i < count; i++ {
        bools[i] = (데이터[i/8]>>uint(i%8))&0x01 == 1
    }
    return bools
}

func BoolsToBytes(bools []bool) []byte {
    byteCount := (len(bools) + 7) / 8
    데이터 := make([]byte, byteCount)
    for i, v := range bools {
        if v {
            데이터[i/8] |= 1 << uint(i%8)
        }
    }
    return 데이터
}

func BytesToFloat32ABCD(데이터 []byte) float32 {
    bits := uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
        uint32(데이터[2])<<8 | uint32(데이터[3])
    return math.Float32frombits(bits)
}

func Float32ToBytesABCD(f float32) []byte {
    bits := math.Float32bits(f)
    return []byte{byte(bits >> 24), byte(bits >> 16), byte(bits >> 8), byte(bits)}
}

func BytesToInt32(데이터 []byte) int32 {
    return int32(uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
        uint32(데이터[2])<<8 | uint32(데이터[3]))
}

九、并发与多设备采集

package main

import (
    "context"
    "fmt"
    "log"
    "sync"
    "time"

    "github.com/grid-x/modbus"
)

// DeviceConfig 设备설정
type DeviceConfig struct {
    이름    string
    주소 string
    SlaveID byte
}

// ReadResult 읽기结果
type ReadResult struct {
    Device string
    Regs   []uint16
    오답  error
}

func readDevice(ctx context.Context, cfg DeviceConfig, wg *sync.WaitGroup, results chan<- ReadResult) {
    defer wg.Done()

    handler := modbus.NewTCPClientHandler(cfg.주소)
    handler.SlaveID = cfg.SlaveID
    handler.Timeout = 3 * time.Second

    if err := handler.Connect(ctx); err != nil {
        results <- ReadResult{Device: cfg.이름, 오답: err}
        return
    }
    defer handler.닫기()

    client := modbus.NewClient(handler)
    데이터, err := client.ReadHolding회원가입s(ctx, 0, 10)
    if err != nil {
        results <- ReadResult{Device: cfg.이름, 오답: err}
        return
    }

    regs := make([]uint16, len(데이터)/2)
    for i := range regs {
        regs[i] = uint16(데이터[i*2])<<8 | uint16(데이터[i*2+1])
    }
    results <- ReadResult{Device: cfg.이름, Regs: regs}
}

func main() {
    devices := []DeviceConfig{
        {이름: "PLC1", 주소: "192.168.1.100:502", SlaveID: 1},
        {이름: "PLC2", 주소: "192.168.1.101:502", SlaveID: 1},
        {이름: "PLC3", 주소: "192.168.1.102:502", SlaveID: 1},
    }

    ctx := context.Background()
    var wg sync.WaitGroup
    results := make(chan ReadResult, len(devices))

    // 并发읽기所有设备
    for _, dev := range devices {
        wg.Add(1)
        go readDevice(ctx, dev, &wg, results)
    }

    wg.Wait()
    close(results)

    // 处理结果
    for r := range results {
        if r.오답 != nil {
            fmt.Printf("设备%s읽기失败:%v\n", r.Device, r.오답)
        } else {
            fmt.Printf("设备%s:%v\n", r.Device, r.Regs)
        }
    }
}

十、Modbus TCP服务端(슬레이브)

grid-x/modbus库本身不直接提供服务端实现,但可以基于其프로토콜编码/解码功能构建服务端。以下是一个简单的TCP슬레이브实现예제。

package main

import (
    "encoding/binary"
    "fmt"
    "log"
    "net"
    "sync"
)

// ModbusServer 简单的Modbus TCP슬레이브
type ModbusServer struct {
    coils       []bool
    inputs      []bool
    holdingRegs []uint16
    inputRegs   []uint16
    mu          sync.RWMutex
}

func NewModbusServer() *ModbusServer {
    return &ModbusServer{
        coils:       make([]bool, 1024),
        inputs:      make([]bool, 1024),
        holdingRegs: make([]uint16, 1024),
        inputRegs:   make([]uint16, 1024),
    }
}

func (s *ModbusServer) handleConnection(conn net.Conn) {
    defer conn.닫기()
    buf := make([]byte, 260) // Modbus TCP最大ADU길이

    for {
        n, err := conn.Read(buf)
        if err != nil {
            return
        }
        if n < 8 {
            continue
        }

        // 분석MBAP头
        transID := binary.BigEndian.Uint16(buf[0:2])
        // protoID := binary.BigEndian.Uint16(buf[2:4]) // 固定0x0000
        // length := binary.BigEndian.Uint16(buf[4:6])
        unitID := buf[6]
        funcCode := buf[7]

        // 处理请求
        response := s.processRequest(funcCode, buf[8:n])

        // 构建响应MBAP头
        resp := make([]byte, 7+len(response))
        binary.BigEndian.PutUint16(resp[0:2], transID)
        binary.BigEndian.PutUint16(resp[2:4], 0x0000) // 프로토콜ID
        binary.BigEndian.PutUint16(resp[4:6], uint16(1+len(response)))
        resp[6] = unitID
        copy(resp[7:], response)

        conn.Write(resp)
    }
}

func (s *ModbusServer) processRequest(funcCode byte, 데이터 []byte) []byte {
    s.mu.Lock()
    defer s.mu.Unlock()

    switch funcCode {
    case 0x03: // 读유지 레지스터
        addr := binary.BigEndian.Uint16(데이터[0:2])
        qty := binary.BigEndian.Uint16(데이터[2:4])
        byteCount := qty * 2
        resp := make([]byte, 2+byteCount)
        resp[0] = funcCode
        resp[1] = byte(byteCount)
        for i := uint16(0); i < qty; i++ {
            binary.BigEndian.PutUint16(resp[2+i*2:], s.holdingRegs[addr+i])
        }
        return resp

    case 0x06: // 写单个레지스터
        addr := binary.BigEndian.Uint16(데이터[0:2])
        value := binary.BigEndian.Uint16(데이터[2:4])
        s.holdingRegs[addr] = value
        resp := make([]byte, 5)
        resp[0] = funcCode
        binary.BigEndian.PutUint16(resp[1:3], addr)
        binary.BigEndian.PutUint16(resp[3:5], value)
        return resp

    case 0x01: // 读코일
        addr := binary.BigEndian.Uint16(데이터[0:2])
        qty := binary.BigEndian.Uint16(데이터[2:4])
        byteCount := (qty + 7) / 8
        resp := make([]byte, 2+byteCount)
        resp[0] = funcCode
        resp[1] = byte(byteCount)
        for i := uint16(0); i < qty; i++ {
            if s.coils[addr+i] {
                resp[2+i/8] |= 1 << (i % 8)
            }
        }
        return resp

    case 0x05: // 写单个코일
        addr := binary.BigEndian.Uint16(데이터[0:2])
        value := binary.BigEndian.Uint16(데이터[2:4])
        s.coils[addr] = value == 0xFF00
        resp := make([]byte, 5)
        resp[0] = funcCode
        binary.BigEndian.PutUint16(resp[1:3], addr)
        binary.BigEndian.PutUint16(resp[3:5], value)
        return resp

    default:
        // 异常响应:非法기능 코드
        return []byte{funcCode | 0x80, 0x01}
    }
}

func main() {
    server := NewModbusServer()

    // 设置初始值
    server.holdingRegs[0] = 100
    server.holdingRegs[1] = 200
    server.coils[0] = true

    listener, err := net.Listen("tcp", ":502")
    if err != nil {
        log.Fatalf("监听失败:%v", err)
    }
    defer listener.닫기()

    fmt.Println("Modbus TCP服务端已启动,监听:502")

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Printf("接受연결失败:%v", err)
            continue
        }
        go server.handleConnection(conn)
    }
}

十一、常见문제与最佳实践

11.1 常见문제

  • 返回데이터是[]byte不是[]uint16:grid-x/modbus的设计哲学是返回原始바이트,由调用方분석。使用本文提供的BytesTo회원가입s도구函数转换
  • 연결超时:检查IP주소、포트、防火墙,确认设备在线。通过handler.Timeout设置超时时间
  • 异常响应:检查SlaveID是否正确,기능 코드和주소是否在设备支持范围内
  • 浮点数분석错误:确认设备바이트序(ABCD/CDAB/DCBA/BADC),使用对应的转换函数
  • RTU시리얼 포트打不开:检查시리얼 포트设备名,确认权限(Linux需加入dialout组)
  • 并发冲突:同一个handler不要在多个goroutine中同时使用,加互斥锁或每个goroutine用独立연결
  • 简洁모드每次新建연결:modbus.TCPClient()简洁모드每次请求都建立新연결,高频请求请用高级모드复用연결

11.2 最佳实践

  • 使用context控制超时:所有操作传入带超时的context,避免永久阻塞
  • 高频请求复用연결:使用NewTCPClientHandler + Connect/닫기手动管理연결,减少TCP握手开销
  • 批量읽기:尽量一次읽기多个连续레지스터,减少通讯次数
  • 合理设置超时:局域网1-3秒,RTU시리얼 포트3-5秒
  • 并发加锁:多goroutine共享同一클라이언트时使用sync.Mutex保护
  • 错误处理:每次调用都检查error,不要忽略
  • 로그记录:개발阶段设置handler.Logger,보기原始通讯데이터
  • 자료释放:使用defer handler.닫기()确保연결释放
  • 주소规划:提前规划레지스터주소,编写주소映射문서
  • 测试验证:先用Modbus Poll确认设备通讯正常,再통합Go代码

十二、grid-x/modbus与其他언어Modbus库对比

언어库名返回유형Context支持部署复杂度
Gogrid-x/modbus[]byte(需手动분석)支持低(单2진수)
Clibmodbusuint16_t数组不支持中(需编译)
Pythonpymodbus对象(.registers/.bits)支持(asyncio)低(解释执行)
JavaEasyModbusint[]/boolean[]不支持高(需JVM)
C#NModbusushort[]/bool[]支持(async/await)中(需.NET)

grid-x/modbus以其简洁的API、完善的프로토콜支持和优秀的并发性能,成为Go언어工业Modbus개발的首选库。本文提供的도구类封装可直接用于项目개발,建议先使用Modbus Poll确认设备通讯매개변수,再将Go代码통합到实际系统中。

相关标签
실제 프로젝트에 이 자료를 활용하시겠습니까?

도구 센터에서 프레임 분석, CRC 검증, 장비 디버깅을 진행하거나 요청을 제출하여 선정 및 연동 제안을 받아보세요.

엔지니어 회원

이 글을 실행 가능한 디버깅 자료로 활용하세요

가입하면 고급 프레임 분석, 자료 패키지 다운로드, 코드 예제, 엔지니어링 사례, 우선 기술 지원을 이용할 수 있어 실제 프로젝트 납품에 적합합니다.

고급 도구 무제한 이용
자료 패키지와 코드 패키지
완전한 엔지니어링 사례 라이브러리
우선 기술 지원 채널

답글 작성

이메일 주소는 공개되지 않습니다. 必填项已用 * 标注