Mod bus 가 이드 튜 토 리 얼 : grid - x / mod bus 에서 0 으로 프로젝트 전투 까지

무료무료 기술 자료

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

Mod bus 가 이드 튜 토 리 얼 : grid - x / mod bus 에서 0 으로 프로젝트 전투 까지

grid-x/modbus是Go언어工业Modbus通讯개발的首选库,以简洁API、完善프로토콜支持和优秀并发性能著称。本文从零基础出发,通过完整的项目实战案例,详细讲解grid-x/modbus的환경搭建、TCP/RTU通讯实现、데이터유형处理、错误排查和性能优化,帮助Go개발者快速上手工业Modbus개발。

一、为什么选择grid-x/modbus

1.1 Go언어在工业控制中的优势

  • 部署简单:编译为单个2진수文件,无需运行时환경,适合嵌入式和工业现场部署
  • 并发原生:goroutine天然支持多设备并发采集,无需复杂的线程管理
  • 性能优秀:编译型언어,运行速度接近C,适合高频데이터采集
  • 跨平台:Windows、Linux、ARM、MIPS均可编译运行
  • 生态丰富:HTTP、MQTT、데이터库、시퀀스化等표준库齐全
  • 内存安全:自动垃圾回收,避免C언어的内存泄漏和指针错误

1.2 grid-x/modbus库的核心设计

grid-x/modbus采用Handler + Client的分层架构:Handler负责底层传输(TCP연결、시리얼 포트 설정、CRC 검증),Client负责프로토콜层(기능 코드、데이터编码)。这种设计使得전환传输방식只需更换Handler,业务代码无需修改。

// 架构示意
Handler(传输层)  →  Client(프로토콜层)  →  业务代码
  TCP/RTU/ASCII       기능 코드01-16         레지스터读写
  연결管理            데이터编码/解码       业务逻辑
  超时控制            异常处理            데이터存储

二、환경搭建与第一个程序

2.1 安装Go개발환경

# 检查Go버전(需要Go 1.18+)
go version

# 创建项目目录
mkdir modbus_demo && cd modbus_demo

# 初始化Go模块
go mod init modbus_demo

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

2.2 第一个Modbus TCP程序

package main

import (
    "context"
    "fmt"
    "log"

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

func main() {
    // 步骤1:创建TCP클라이언트
    // 简洁모드:每次请求自动建立和닫기연결
    client := modbus.TCPClient("192.168.1.100:502")

    ctx := context.Background()

    // 步骤2:유지 레지스터 읽기 (FC03)(기능 코드03)
    // 从주소0开始,읽기10个레지스터
    // 返回值是[]byte,每2바이트为一个레지스터(高바이트在前)
    데이터, err := client.ReadHolding회원가입s(ctx, 0, 10)
    if err != nil {
        log.Fatalf("읽기失败:%v", err)
    }

    // 步骤3:분석데이터
    fmt.Println("=== 읽기结果 ===")
    for i := 0; i < len(데이터); i += 2 {
        regAddr := i / 2
        regValue := uint16(데이터[i])<<8 | uint16(데이터[i+1])
        fmt.Printf("레지스터%d(4%04d):%d\n", regAddr, regAddr+1, regValue)
    }
}

2.3 运行与测试

# 运行程序
go run main.go

# 如果没有真实设备,可以用Modbus Slave模拟
# 下载주소:https://www.modbustools.com/modbus_slave.html
# 설정:TCP Slave,포트502,슬레이브 주소1,유지 레지스터10个

三、Modbus TCP深入实战

3.1 연결管理与性能优化

简洁모드(modbus.TCPClient)每次请求都会建立新的TCP연결,高频请求时性能较差。生产환경应使用Handler모드复用연결。

package main

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

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

func main() {
    // 创建Handler(可설정超时、슬레이브 주소等)
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.Timeout = 5 * time.Second  // 超时5秒
    handler.SlaveID = 1                 // 슬레이브 주소1

    ctx := context.Background()

    // 建立연결(一次建立,多次使用)
    if err := handler.Connect(ctx); err != nil {
        log.Fatalf("연결失败:%v", err)
    }
    defer handler.닫기()  // 程序종료时닫기

    client := modbus.NewClient(handler)

    // 连续읽기100次(复用同一연결)
    시작 := time.Now()
    for i := 0; i < 100; i++ {
        _, err := client.ReadHolding회원가입s(ctx, 0, 10)
        if err != nil {
            log.Printf("第%d次읽기失败:%v", i, err)
        }
    }
    fmt.Printf("100次읽기耗时:%v\n", time.Since(시작))
}

3.2 完整기능 코드实战

package main

import (
    "context"
    "fmt"
    "log"

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

func main() {
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveID = 1
    ctx := context.Background()
    handler.Connect(ctx)
    defer handler.닫기()
    client := modbus.NewClient(handler)

    // ====== 读코일(기능 코드01)======
    // 읽기코일0-7的状态
    coil데이터, err := client.ReadCoils(ctx, 0, 8)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("=== 코일 상태 ===")
    for i := 0; i < 8; i++ {
        state := (coil데이터[i/8] >> uint(i%8)) & 0x01
        fmt.Printf("코일%d:%s\n", i, map[byte]string{0: "OFF", 1: "ON"}[state])
    }

    // ====== 写单个코일(기능 코드05)======
    // 0xFF00 = ON,0x0000 = OFF
    _, err = client.WriteSingleCoil(ctx, 0, 0xFF00) // 코일0置ON
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("코일0已置ON")

    // ====== 写多个코일(기능 코드0F)======
    // 一次设置코일0-7:0x55 = 01010101 = 코일0,2,4,6为ON
    _, err = client.WriteMultipleCoils(ctx, 0, 8, []byte{0x55})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("코일0-7已批量设置")

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

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

    // ====== 读입력 레지스터(기능 코드04)======
    analog데이터, err := client.ReadInput회원가입s(ctx, 0, 2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("=== 입력 레지스터(模拟量)===")
    for i := 0; i < len(analog데이터); i += 2 {
        value := uint16(analog데이터[i])<<8 | uint16(analog데이터[i+1])
        fmt.Printf("模拟量%d:%d(原始值)\n", i/2, value)
    }

    // ====== 写单个레지스터(기능 코드06)======
    _, err = client.WriteSingle회원가입(ctx, 10, 500)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("레지스터10已쓰기500")

    // ====== 写多个레지스터(기능 코드10)======
    // 쓰기레지스터20-22:值为100, 200, 300
    write데이터 := []byte{
        0x00, 0x64, // 100
        0x00, 0xC8, // 200
        0x01, 0x2C, // 300
    }
    _, err = client.WriteMultiple회원가입s(ctx, 20, 3, write데이터)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("레지스터20-22已批量쓰기")
}

四、데이터유형处理实战

4.1 16位整数(有符号/无符号)

// 분석16位无符号整数(0-65535)
func parseUint16(데이터 []byte, index int) uint16 {
    return uint16(데이터[index*2])<<8 | uint16(데이터[index*2+1])
}

// 분석16位有符号整数(-32768到32767)
func parseInt16(데이터 []byte, index int) int16 {
    return int16(uint16(데이터[index*2])<<8 | uint16(데이터[index*2+1]))
}

// 使用예제
데이터, _ := client.ReadHolding회원가입s(ctx, 0, 4)
temp := parseInt16(데이터, 0)      // 온도(有符号,可能为负)
humidity := parseUint16(데이터, 1) // 湿度(无符号)
fmt.Printf("온도:%d°C,湿度:%d%%\n", temp, humidity)

4.2 32位浮点数(最常见)

import "math"

// 32位浮点数有4种바이트序,必须确认设备使用哪种
// 最常见的是ABCD(大端序)和CDAB(字交换)

// ABCD大端序:reg[0]存高16位,reg[1]存低16位
func parseFloat32ABCD(데이터 []byte) float32 {
    bits := uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
        uint32(데이터[2])<<8 | uint32(데이터[3])
    return math.Float32frombits(bits)
}

// CDAB字交换:reg[0]存低16位,reg[1]存高16位
func parseFloat32CDAB(데이터 []byte) float32 {
    bits := uint32(데이터[2])<<24 | uint32(데이터[3])<<16 |
        uint32(데이터[0])<<8 | uint32(데이터[1])
    return math.Float32frombits(bits)
}

// 쓰기32位浮点数(ABCD大端序)
func float32ToBytesABCD(f float32) []byte {
    bits := math.Float32bits(f)
    return []byte{
        byte(bits >> 24), byte(bits >> 16),
        byte(bits >> 8), byte(bits),
    }
}

// 实战:읽기온도 센서(浮点数,占2个레지스터)
데이터, err := client.ReadHolding회원가입s(ctx, 100, 2) // 读레지스터100-101
if err != nil {
    log.Fatal(err)
}
temperature := parseFloat32ABCD(데이터)
fmt.Printf("当前온도:%.2f°C\n", temperature)

// 쓰기设定온도
setTemp := float32(25.5)
_, err = client.WriteMultiple회원가입s(ctx, 200, 2, float32ToBytesABCD(setTemp))
if err != nil {
    log.Fatal(err)
}
fmt.Printf("设定온도已쓰기:%.1f°C\n", setTemp)

4.3 32位整数与64位데이터

// 32位有符号整数(占2个레지스터)
func parseInt32(데이터 []byte) int32 {
    return int32(uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
        uint32(데이터[2])<<8 | uint32(데이터[3]))
}

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

// 64位整数(占4个레지스터)
func parseInt64(데이터 []byte) int64 {
    return int64(uint64(데이터[0])<<56 | uint64(데이터[1])<<48 |
        uint64(데이터[2])<<40 | uint64(데이터[3])<<32 |
        uint64(데이터[4])<<24 | uint64(데이터[5])<<16 |
        uint64(데이터[6])<<8 | uint64(데이터[7]))
}

// 64位浮点数(double,占4个레지스터)
func parseFloat64(데이터 []byte) float64 {
    bits := uint64(데이터[0])<<56 | uint64(데이터[1])<<48 |
        uint64(데이터[2])<<40 | uint64(데이터[3])<<32 |
        uint64(데이터[4])<<24 | uint64(데이터[5])<<16 |
        uint64(데이터[6])<<8 | uint64(데이터[7])
    return math.Float64frombits(bits)
}

4.4 문자串与位操作

// 읽기문자串(每个레지스터存2个ASCII문자)
func readString(client modbus.Client, ctx context.Context, addr, regCount uint16) (string, error) {
    데이터, err := client.ReadHolding회원가입s(ctx, addr, regCount)
    if err != nil {
        return "", err
    }
    // 去除末尾的空바이트
    result := string(데이터)
    for len(result) > 0 && result[len(result)-1] == 0 {
        result = result[:len(result)-1]
    }
    return result, nil
}

// 位操作:읽기레지스터中特定位的状态
func readBit(데이터 []byte, regIndex, bitIndex int) bool {
    regValue := uint16(데이터[regIndex*2])<<8 | uint16(데이터[regIndex*2+1])
    return (regValue>>uint(bitIndex))&0x01 == 1
}

// 位操作:设置레지스터中特定位
func setBit(regValue uint16, bitIndex int, value bool) uint16 {
    if value {
        return regValue | (1 << uint(bitIndex))
    }
    return regValue &^ (1 << uint(bitIndex))
}

五、Modbus RTU시리얼 포트实战

5.1 시리얼 포트 설정与연결

package main

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

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

func main() {
    // 创建RTU Handler
    handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
    // Windows: handler := modbus.NewRTUClientHandler("COM3")

    // 시리얼 포트 매개변수설정(必须与슬레이브设备一致)
    handler.BaudRate = 9600   // 통신 속도:常见9600, 19200, 38400, 115200
    handler.데이터Bits = 8      // 데이터 비트:通常为8
    handler.Parity = "N"      // 패리티:N=无,E=偶,O=奇
    handler.StopBits = 1      // 정지 비트:1或2
    handler.SlaveID = 1       // 슬레이브 주소:1-247
    handler.Timeout = 5 * time.Second

    ctx := context.Background()

    // 시리얼 포트 열기
    if err := handler.Connect(ctx); err != nil {
        log.Fatalf("시리얼 포트열기失败:%v", err)
    }
    defer handler.닫기()

    client := modbus.NewClient(handler)

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

    for i := 0; i < len(데이터); i += 2 {
        value := uint16(데이터[i])<<8 | uint16(데이터[i+1])
        fmt.Printf("레지스터%d:%d\n", i/2, value)
    }
}

5.2 RTU常见문제排查

문제可能原因解决方法
시리얼 포트打不开设备名错误/权限不足检查/dev/ttyUSB0是否存在;Linux执行sudo usermod -aG dialout $USER
无响应A/B线接反/주소错误交换A/B线;确认슬레이브 주소DIP 스위치设置
CRC错误통신 속도/패리티不일치确认主슬레이브시리얼 포트 매개변수完全一致
偶尔丢包干扰/终端电阻总线两端加120Ω终端电阻;使用屏蔽双绞线
响应超时슬레이브处理慢增大handler.Timeout到5-10秒

六、完整项目实战:환경监测系统

以下是一个完整的工业환경监测系统,通过Modbus TCP읽기온습도센서데이터,支持多设备并发采集、데이터存储和异常告警。

package main

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

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

// SensorConfig 센서설정
type SensorConfig struct {
    이름     string
    주소  string
    SlaveID  byte
    TempReg  uint16 // 온도레지스터주소
    HumReg   uint16 // 湿度레지스터주소
    TempType string // "int16" 或 "float32"
}

// Sensor데이터 센서데이터
type Sensor데이터 struct {
    이름      string
    Timestamp time.Time
    Temp      float64
    Humidity  float64
    오답     error
}

// ModbusReader 封装Modbus읽기
type ModbusReader struct {
    handler *modbus.TCPClientHandler
    client  modbus.Client
}

func NewModbusReader(주소 string, slaveID byte) (*ModbusReader, error) {
    handler := modbus.NewTCPClientHandler(주소)
    handler.SlaveID = slaveID
    handler.Timeout = 3 * time.Second

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

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

func (r *ModbusReader) 닫기() {
    r.handler.닫기()
}

// ReadFloat32 읽기32位浮点数(ABCD大端序)
func (r *ModbusReader) ReadFloat32(ctx context.Context, addr uint16) (float32, error) {
    데이터, err := r.client.ReadHolding회원가입s(ctx, addr, 2)
    if err != nil {
        return 0, err
    }
    bits := uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
        uint32(데이터[2])<<8 | uint32(데이터[3])
    return math.Float32frombits(bits), nil
}

// ReadInt16 읽기16位有符号整数
func (r *ModbusReader) ReadInt16(ctx context.Context, addr uint16) (int16, error) {
    데이터, err := r.client.ReadHolding회원가입s(ctx, addr, 1)
    if err != nil {
        return 0, err
    }
    return int16(binary.BigEndian.Uint16(데이터)), nil
}

// readSensor 읽기单个센서
func readSensor(ctx context.Context, cfg SensorConfig, wg *sync.WaitGroup, results chan<- Sensor데이터) {
    defer wg.Done()

    reader, err := NewModbusReader(cfg.주소, cfg.SlaveID)
    if err != nil {
        results <- Sensor데이터{이름: cfg.이름, 오답: err}
        return
    }
    defer reader.닫기()

    데이터 := Sensor데이터{
        이름:      cfg.이름,
        Timestamp: time.Now(),
    }

    // 읽기온도
    if cfg.TempType == "float32" {
        temp, err := reader.ReadFloat32(ctx, cfg.TempReg)
        if err != nil {
            데이터.오답 = fmt.오답f("온도읽기失败:%w", err)
            results <- 데이터
            return
        }
        데이터.Temp = float64(temp)
    } else {
        temp, err := reader.ReadInt16(ctx, cfg.TempReg)
        if err != nil {
            데이터.오답 = fmt.오답f("온도읽기失败:%w", err)
            results <- 데이터
            return
        }
        데이터.Temp = float64(temp) / 10.0 // 假设分辨率0.1°C
    }

    // 읽기湿度
    hum, err := reader.ReadInt16(ctx, cfg.HumReg)
    if err != nil {
        데이터.오답 = fmt.오답f("湿度읽기失败:%w", err)
        results <- 데이터
        return
    }
    데이터.Humidity = float64(hum) / 10.0 // 假设分辨率0.1%

    results <- 데이터
}

func main() {
    sensors := []SensorConfig{
        {이름: "车间A", 주소: "192.168.1.100:502", SlaveID: 1, TempReg: 0, HumReg: 2, TempType: "float32"},
        {이름: "车间B", 주소: "192.168.1.101:502", SlaveID: 1, TempReg: 0, HumReg: 2, TempType: "float32"},
        {이름: "仓库", 주소: "192.168.1.102:502", SlaveID: 1, TempReg: 0, HumReg: 2, TempType: "int16"},
    }

    ctx := context.Background()
    ticker := time.NewTicker(10 * time.Second) // 每10秒采集一次
    defer ticker.Stop()

    fmt.Println("환경监测系统启动,每10秒采集一次...")

    for range ticker.C {
        var wg sync.WaitGroup
        results := make(chan Sensor데이터, len(sensors))

        // 并发읽기所有센서
        for _, cfg := range sensors {
            wg.Add(1)
            go readSensor(ctx, cfg, &wg, results)
        }

        wg.Wait()
        close(results)

        // 处理结果
        fmt.Printf("\n=== %s ===\n", time.Now().Format("2006-01-02 15:04:05"))
        for 데이터 := range results {
            if 데이터.오답 != nil {
                fmt.Printf("[%s] 错误:%v\n", 데이터.이름, 데이터.오답)
                continue
            }
            fmt.Printf("[%s] 온도:%.1f°C,湿度:%.1f%%\n", 데이터.이름, 데이터.Temp, 데이터.Humidity)

            // 异常告警
            if 데이터.Temp > 35 {
                fmt.Printf("  ⚠️  온도过高告警:%.1f°C\n", 데이터.Temp)
            }
            if 데이터.Humidity > 80 {
                fmt.Printf("  ⚠️  湿度过高告警:%.1f%%\n", 데이터.Humidity)
            }
        }
    }
}

七、错误处理与异常码

7.1 Modbus异常码

异常码名称含义常见原因
0x01非法기능 코드슬레이브不支持该기능 코드设备不支持写操作或特定功能
0x02非法데이터주소请求주소超出范围레지스터주소错误或数量超限
0x03非法데이터值쓰기值不合法쓰기超出量程的值
0x04슬레이브设备故障슬레이브处理时出错设备硬件故障或内部错误
0x05确认슬레이브已接受但需时间处理쓰기操作需要较长时间
0x06슬레이브忙슬레이브正在处理其他请求请求过于频繁

7.2 带重试的健壮읽기

// ReadWithRetry 带重试的레지스터읽기
func ReadWithRetry(client modbus.Client, ctx context.Context, addr, qty uint16, maxRetries int) ([]byte, error) {
    var lastErr error
    for attempt := 0; attempt < maxRetries; attempt++ {
        데이터, err := client.ReadHolding회원가입s(ctx, addr, qty)
        if err == nil {
            return 데이터, nil
        }
        lastErr = err
        log.Printf("第%d次읽기失败:%v,重试中...", attempt+1, err)
        time.Sleep(time.Duration(attempt+1) * 500 * time.Millisecond) // 退避重试
    }
    return nil, fmt.오답f("重试%d次后仍失败:%w", maxRetries, lastErr)
}

// 使用예제
데이터, err := ReadWithRetry(client, ctx, 0, 10, 3) // 最多重试3次
if err != nil {
    log.Printf("最终읽기失败:%v", err)
    // 记录告警、전환备用设备等
}

八、디버깅与로그

package main

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

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

func main() {
    handler := modbus.NewTCPClientHandler("192.168.1.100:502")
    handler.SlaveID = 1
    handler.Timeout = 5 * time.Second

    // 활성화로그(출력原始通讯데이터)
    handler.Logger = log.New(os.Stdout, "[MODBUS] ", log.LstdFlags|log.Lmicroseconds)

    ctx := context.Background()
    handler.Connect(ctx)
    defer handler.닫기()

    client := modbus.NewClient(handler)

    // 此时所有请求和响应都会打印到控制台
    // 예제출력:
    // [MODBUS] 2024/01/01 12:00:00. 00 000 1 Tx : [ 00 01 00 00 00 06 01 03 00 00 00 00 0 A ]
    [ MO DB US ] 2024 / 01 / 01 12: 00 : 00 . 00 5000 R x : [ 00 01 00 00 17 01 03 14 00 64 00 C 8 ...]

    client . Read H old ing Reg ister s (ct x , 0, 10)
}

IX . 성 능 최적 화 기술

  • 批量읽기한 번에 10 개의 레 지 스터 를 읽는 것은 1 개의 레 지 스터 를 10 번 읽는 것보다 10 배 빠 릅니다 .
  • 연결复用
  • 合理超时
  • 并发采集
  • 减少写操作
  • 주소对齐
  • 避免频繁전환SlaveID

Gr id - x / Mod bus 학습

阶段
入门
基础
进阶
高级

grid - x / mod bus 는 간 결 한 API 와 완벽한 프로토 콜 지원 으로 Go 언어 의 Mod bus 개발 을위한 최고의 선택 입니다 .이 기 사는 제 로 기반 에서 완전한 프로젝트 실 전에 이르 기까지 개발 중에 만나는 모든 일반적인 시 나 리오 를 다 룹 니다 .먼저 시 뮬 레이 션 환경에서 코드를 검 증 한 다음 실제 장비 에 접속 하여 개발 과정에서 로 그 기능을 잘 활용 하여 통신 문제를 해결 하는 것이 좋습니다 .

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

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

엔지니어 회원

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

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

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

답글 작성

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