Java 와 PLC 의 통신 예제 코드: 효율적이고 신뢰할 수 있는 크로스 플랫폼 데이터 상호 작용

무료무료 기술 자료

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

Java 와 PLC 의 통신 예제 코드: 효율적이고 신뢰할 수 있는 크로스 플랫폼 데이터 상호 작용

Java作为跨平台编程언어,在산업 자동화上位机개발中적용广泛。通过Modbus프로토콜,Java程序可以与各种品牌的PLC(西门子、三菱、台达、欧姆龙等)进行高效可靠的데이터交互。本文提供完整的Java与PLC통신예제代码,包括Modbus TCP클라이언트实现、多PLC管理、데이터采集、断线重连、批量读写、浮点数处理以及Spring Boot통합方案。

一、Java Modbus통신方案选型

1.1 常用Java Modbus库对比

库名프로토콜支持主从支持Maven坐标推荐시나리오
EasyModbusTCP/RTU/UDP主+从com.github.s7connector:easymodbus新项目首选,API简洁
JamodTCP/RTU/ASCII主+从net.wimpi:jamod遗留项目,稳定
j2modTCP/RTU/ASCII主+从com.ghgande:j2mod需要ASCII프로토콜
Modbus4JTCP/RTU主+从com.infiniteautomation:modbus4jSCADA系统
modbus-tcpTCP仅主com.digitalpetri.modbus:modbus-tcp高性能Netty实现

1.2 本文使用EasyModbus

EasyModbus是目前最活跃、API最简洁的Java Modbus库,支持TCP、RTU和UDP프로토콜,既可作为클라이언트也可作为服务端,문서完善,适合大多数项目。

<!-- Maven依赖 -->
<dependency>
    <groupId>com.github.s7connector</groupId>
    <artifactId>easymodbus</artifactId>
    <version>1.0.0</version>
</dependency>

<!-- 시리얼 포트支持(RTU모드需要) -->
<dependency>
    <groupId>com.fazecast</groupId>
    <artifactId>jSerialComm</artifactId>
    <version>2.9.2</version>
</dependency>

二、Modbus TCP클라이언트封装

package com.industrial.modbus;

import de.re.easymodbus.modbusclient.ModbusClient;
import de.re.easymodbus.데이터types.데이터Type;
import java.io.IOException;
import java.net.SocketException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Modbus TCP클라이언트封装
 * 支持연결管理、自动重连、批量读写、데이터유형转换
 */
public class PlcModbusClient {
    
    private final String ip주소;
    private final int port;
    private final int unitId;
    private ModbusClient client;
    private final AtomicBoolean connected = new AtomicBoolean(false);
    private Thread reconnectThread;
    private volatile boolean running = true;
    
    // 연결매개변수
    private static final int CONNECT_TIMEOUT = 3000;
    private static final int RECONNECT_INTERVAL = 5000;
    private static final int MAX_RETRY = 3;
    
    public PlcModbusClient(String ip주소, int port, int unitId) {
        this.ip주소 = ip주소;
        this.port = port;
        this.unitId = unitId;
    }
    
    /**
     * 연결PLC
     */
    public synchronized boolean connect() {
        try {
            if (client != null && client.isConnected()) {
                return true;
            }
            client = new ModbusClient(ip주소, port);
            client.setConnectionTimeout(CONNECT_TIMEOUT);
            client.setUnitIdentifier(unitId);
            client.Connect();
            connected.set(true);
            System.out.printf("[%s:%d] PLC연결成功,슬레이브 주소:%d%n", 
                ip주소, port, unitId);
            return true;
        } catch (Exception e) {
            connected.set(false);
            System.err.printf("[%s:%d] 연결失败:%s%n", ip주소, e.getMessage());
            return false;
        }
    }
    
    /**
     * 断开연결
     */
    public synchronized void disconnect() {
        running = false;
        if (reconnectThread != null) {
            reconnectThread.interrupt();
        }
        try {
            if (client != null && client.isConnected()) {
                client.Disconnect();
            }
        } catch (IOException e) {
            // 忽略
        }
        connected.set(false);
        System.out.printf("[%s:%d] 연결已닫기%n", ip주소);
    }
    
    /**
     * 启动自动重连
     */
    public void 시작AutoReconnect() {
        reconnectThread = new Thread(() -> {
            while (running) {
                try {
                    if (!isConnected()) {
                        System.out.printf("[%s:%d] 检测到断开,尝试重连...%n", ip주소);
                        connect();
                    }
                    Thread.sleep(RECONNECT_INTERVAL);
                } catch (InterruptedException e) {
                    break;
                }
            }
        }, "Modbus-Reconnect-" + ip주소);
        reconnectThread.setDaemon(true);
        reconnectThread.시작();
    }
    
    /**
     * 检查연결状态
     */
    public boolean isConnected() {
        if (client == null) return false;
        try {
            // 보내기一个简单的读请求测试연결
            client.ReadHolding회원가입s(0, 1);
            return true;
        } catch (Exception e) {
            connected.set(false);
            return false;
        }
    }
    
    // ==================== 读操作 ====================
    
    /**
     * 读유지 레지스터(기능 코드03)
     */
    public int[] readHolding회원가입s(int 시작Addr, int count) throws IOException {
        for (int retry = 0; retry < MAX_RETRY; retry++) {
            try {
                ensureConnected();
                return client.ReadHolding회원가입s(시작Addr, count);
            } catch (SocketException e) {
                connected.set(false);
                if (retry < MAX_RETRY - 1) {
                    connect();
                    continue;
                }
                throw e;
            }
        }
        throw new IOException("읽기失败,重试次数超限");
    }
    
    /**
     * 读입력 레지스터(기능 코드04)
     */
    public int[] readInput회원가입s(int 시작Addr, int count) throws IOException {
        ensureConnected();
        return client.ReadInput회원가입s(시작Addr, count);
    }
    
    /**
     * 读코일(기능 코드01)
     */
    public boolean[] readCoils(int 시작Addr, int count) throws IOException {
        ensureConnected();
        return client.ReadCoils(시작Addr, count);
    }
    
    /**
     * 读이산 입력(기능 코드02)
     */
    public boolean[] readDiscreteInputs(int 시작Addr, int count) throws IOException {
        ensureConnected();
        return client.ReadDiscreteInputs(시작Addr, count);
    }
    
    // ==================== 写操作 ====================
    
    /**
     * 写单个레지스터(기능 코드06)
     */
    public void writeSingle회원가입(int addr, int value) throws IOException {
        ensureConnected();
        client.WriteSingle회원가입(addr, value);
    }
    
    /**
     * 写多个레지스터(기능 코드16)
     */
    public void writeMultiple회원가입s(int 시작Addr, int[] values) throws IOException {
        ensureConnected();
        client.WriteMultiple회원가입s(시작Addr, values);
    }
    
    /**
     * 写单个코일(기능 코드05)
     */
    public void writeSingleCoil(int addr, boolean value) throws IOException {
        ensureConnected();
        client.WriteSingleCoil(addr, value);
    }
    
    /**
     * 写多个코일(기능 코드15)
     */
    public void writeMultipleCoils(int 시작Addr, boolean[] values) throws IOException {
        ensureConnected();
        client.WriteMultipleCoils(시작Addr, values);
    }
    
    // ==================== 데이터유형转换 ====================
    
    /**
     * 읽기32位有符号整数(占用2个레지스터)
     */
    public int readInt32(int 시작Addr) throws IOException {
        int[] regs = readHolding회원가입s(시작Addr, 2);
        return (regs[0] << 16) | (regs[1] & 0xFFFF);
    }
    
    /**
     * 읽기32位无符号整数
     */
    public long readUInt32(int 시작Addr) throws IOException {
        int[] regs = readHolding회원가입s(시작Addr, 2);
        return ((long)(regs[0] & 0xFFFF) << 16) | (regs[1] & 0xFFFF);
    }
    
    /**
     * 읽기32位浮点数
     */
    public float readFloat32(int 시작Addr) throws IOException {
        int[] regs = readHolding회원가입s(시작Addr, 2);
        int bits = (regs[0] << 16) | (regs[1] & 0xFFFF);
        return Float.intBitsToFloat(bits);
    }
    
    /**
     * 읽기64位浮点数(占用4个레지스터)
     */
    public double readFloat64(int 시작Addr) throws IOException {
        int[] regs = readHolding회원가입s(시작Addr, 4);
        long bits = ((long)(regs[0] & 0xFFFF) << 48) |
                    ((long)(regs[1] & 0xFFFF) << 32) |
                    ((long)(regs[2] & 0xFFFF) << 16) |
                    (regs[3] & 0xFFFF);
        return Double.longBitsToDouble(bits);
    }
    
    /**
     * 읽기문자串(占用N个레지스터,每个레지스터存2个문자)
     */
    public String readString(int 시작Addr, int registerCount) throws IOException {
        int[] regs = readHolding회원가입s(시작Addr, registerCount);
        StringBuilder sb = new StringBuilder();
        for (int reg : regs) {
            char high = (char)((reg >> 8) & 0xFF);
            char low = (char)(reg & 0xFF);
            if (high != 0) sb.append(high);
            if (low != 0) sb.append(low);
        }
        return sb.toString().trim();
    }
    
    /**
     * 쓰기32位浮点数
     */
    public void writeFloat32(int 시작Addr, float value) throws IOException {
        int bits = Float.floatToIntBits(value);
        int[] regs = new int[2];
        regs[0] = (bits >> 16) & 0xFFFF;
        regs[1] = bits & 0xFFFF;
        writeMultiple회원가입s(시작Addr, regs);
    }
    
    private void ensureConnected() throws IOException {
        if (!connected.get() || client == null || !client.isConnected()) {
            if (!connect()) {
                throw new IOException("PLC연결되지 않음:" + ip주소);
            }
        }
    }
    
    public String getIp주소() { return ip주소; }
    public int getPort() { return port; }
    public int getUnitId() { return unitId; }
}

三、多PLC管理

package com.industrial.modbus;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 多PLC연결管理器
 * 支持多台PLC的统一管理、批量데이터采集
 */
public class PlcConnectionManager {
    
    private final Map<String, PlcModbusClient> connections = new ConcurrentHashMap<>();
    
    /**
     * 추가PLC연결
     */
    public void addPlc(String name, String ip, int port, int unitId) {
        PlcModbusClient client = new PlcModbusClient(ip, port, unitId);
        connections.put(name, client);
        if (client.connect()) {
            client.시작AutoReconnect();
        }
    }
    
    /**
     * 获取PLC클라이언트
     */
    public PlcModbusClient getPlc(String name) {
        return connections.get(name);
    }
    
    /**
     * 批量읽기所有PLC的指定레지스터
     */
    public Map<String, int[]> batchReadHolding회원가입s(int 시작Addr, int count) {
        Map<String, int[]> results = new ConcurrentHashMap<>();
        connections.forEach((name, client) -> {
            try {
                int[] values = client.readHolding회원가입s(시작Addr, count);
                results.put(name, values);
            } catch (IOException e) {
                System.err.printf("읽기PLC[%s]失败:%s%n", name, e.getMessage());
            }
        });
        return results;
    }
    
    /**
     * 断开所有연결
     */
    public void disconnectAll() {
        connections.values().forEach(PlcModbusClient::disconnect);
        connections.clear();
    }
    
    /**
     * 获取연결状态
     */
    public Map<String, Boolean> getConnectionStatus() {
        Map<String, Boolean> status = new ConcurrentHashMap<>();
        connections.forEach((name, client) -> {
            status.put(name, client.isConnected());
        });
        return status;
    }
}

四、데이터采集服务

package com.industrial.modbus;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;

/**
 * PLC데이터采集服务
 * 定时采集PLC데이터,支持데이터缓存、变化检测、回调通知
 */
public class Plc데이터Collector {
    
    private final PlcModbusClient client;
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private final Map<String, Object> 데이터Cache = new ConcurrentHashMap<>();
    private final List<데이터ChangeListener> listeners = new 복사OnWriteArrayList<>();
    private volatile boolean running = false;
    
    // 采集설정
    private final List<CollectItem> collectItems = new ArrayList<>();
    
    public interface 데이터ChangeListener {
        void on데이터Changed(String key, Object oldValue, Object newValue);
    }
    
    static class CollectItem {
        String key;
        int 주소;
        String type;  // INT16, INT32, FLOAT32, COIL
        
        CollectItem(String key, int 주소, String type) {
            this.key = key;
            this.주소 = 주소;
            this.type = type;
        }
    }
    
    public Plc데이터Collector(PlcModbusClient client) {
        this.client = client;
    }
    
    /**
     * 추가采集项
     */
    public void addCollectItem(String key, int 주소, String type) {
        collectItems.add(new CollectItem(key, 주소, type));
    }
    
    /**
     * 추가데이터变化监听器
     */
    public void add데이터ChangeListener(데이터ChangeListener listener) {
        listeners.add(listener);
    }
    
    /**
     * 启动定时采集
     */
    public void 시작(long intervalMs) {
        running = true;
        scheduler.scheduleAtFixedRate(this::collectAll, 0, intervalMs, TimeUnit.MILLISECONDS);
        System.out.println("데이터采集服务已启动,间隔:" + intervalMs + "ms");
    }
    
    /**
     * 정지采集
     */
    public void stop() {
        running = false;
        scheduler.shutdown();
        System.out.println("데이터采集服务已정지");
    }
    
    /**
     * 采集所有데이터
     */
    private void collectAll() {
        if (!running) return;
        
        for (CollectItem item : collectItems) {
            try {
                Object value = readValue(item);
                Object oldValue = 데이터Cache.put(item.key, value);
                
                // 检测变化并通知
                if (oldValue == null || !oldValue.equals(value)) {
                    final Object finalOldValue = oldValue;
                    final Object finalValue = value;
                    listeners.forEach(l -> l.on데이터Changed(item.key, finalOldValue, finalValue));
                }
            } catch (Exception e) {
                System.err.printf("采集[%s]失败:%s%n", item.key, e.getMessage());
            }
        }
    }
    
    private Object readValue(CollectItem item) throws IOException {
        switch (item.type) {
            case "INT16":
                return client.readHolding회원가입s(item.주소, 1)[0];
            case "INT32":
                return client.readInt32(item.주소);
            case "FLOAT32":
                return client.readFloat32(item.주소);
            case "COIL":
                return client.readCoils(item.주소, 1)[0];
            default:
                throw new IllegalArgumentException("不支持的데이터유형:" + item.type);
        }
    }
    
    /**
     * 获取缓存데이터
     */
    public Map<String, Object> getAll데이터() {
        return new HashMap<>(데이터Cache);
    }
    
    /**
     * 获取单个데이터
     */
    public Object get데이터(String key) {
        return 데이터Cache.get(key);
    }
}

五、完整使用예제

package com.industrial.modbus;

import java.util.Arrays;
import java.util.Map;

public class PlcCommunicationDemo {
    
    public static void main(String[] args) {
        // 1. 创建PLC클라이언트
        PlcModbusClient plc = new PlcModbusClient("192.168.1.100", 502, 1);
        
        try {
            // 2. 연결PLC
            if (!plc.connect()) {
                System.err.println("无法연결到PLC");
                return;
            }
            plc.시작AutoReconnect();
            System.out.println("PLC연결成功!\n");
            
            // 3. 유지 레지스터 읽기 (FC03)
            System.out.println("=== 유지 레지스터 읽기 (FC03) ===");
            int[] regs = plc.readHolding회원가입s(0, 10);
            System.out.println("D0-D9:" + Arrays.toString(regs));
            
            // 4. 읽기浮点数
            System.out.println("\n=== 읽기浮点数 ===");
            float temperature = plc.readFloat32(10);
            float humidity = plc.readFloat32(12);
            System.out.printf("온도:%.2f℃%n", temperature);
            System.out.printf("湿度:%.2f%%%n", humidity);
            
            // 5. 읽기32位整数
            System.out.println("\n=== 읽기32位整数 ===");
            int counter = plc.readInt32(20);
            System.out.println("计数器值:" + counter);
            
            // 6. 읽기코일
            System.out.println("\n=== 읽기코일 ===");
            boolean[] coils = plc.readCoils(0, 8);
            for (int i = 0; i < coils.length; i++) {
                System.out.printf("M%d:%s%n", i, coils[i] ? "ON" : "OFF");
            }
            
            // 7. 이산 입력 읽기 (FC02)
            System.out.println("\n=== 이산 입력 읽기 (FC02) ===");
            boolean[] inputs = plc.readDiscreteInputs(0, 4);
            for (int i = 0; i < inputs.length; i++) {
                System.out.printf("X%d:%s%n", i, inputs[i] ? "ON" : "OFF");
            }
            
            // 8. 쓰기레지스터
            System.out.println("\n=== 쓰기레지스터 ===");
            plc.writeSingle회원가입(100, 1234);
            System.out.println("D100 쓰기 1234");
            
            int check = plc.readHolding회원가입s(100, 1)[0];
            System.out.println("D100 回读:" + check);
            
            // 9. 批量쓰기
            plc.writeMultiple회원가입s(200, new int[]{100, 200, 300, 400});
            System.out.println("D200-D203 批量쓰기完成");
            
            // 10. 쓰기浮点数
            plc.writeFloat32(300, 25.5f);
            System.out.println("D300 쓰기浮点数 25.5");
            float readBack = plc.readFloat32(300);
            System.out.printf("D300 回读:%.2f%n", readBack);
            
            // 11. 控制코일
            System.out.println("\n=== 控制코일 ===");
            plc.writeSingleCoil(0, true);
            System.out.println("M0 置为 ON");
            Thread.sleep(500);
            plc.writeSingleCoil(0, false);
            System.out.println("M0 置为 OFF");
            
            // 12. 批量控制코일
            plc.writeMultipleCoils(10, new boolean[]{true, false, true, false});
            System.out.println("M10-M13 批量쓰기完成");
            
            // 13. 읽기문자串
            System.out.println("\n=== 읽기문자串 ===");
            String device이름 = plc.readString(400, 10);
            System.out.println("设备名称:" + device이름);
            
        } catch (Exception e) {
            System.err.println("通讯异常:" + e.getMessage());
            e.printStackTrace();
        } finally {
            plc.disconnect();
            System.out.println("\nPLC연결已닫기");
        }
    }
}

六、데이터采集服务使用예제

package com.industrial.modbus;

public class 데이터CollectorDemo {
    
    public static void main(String[] args) throws InterruptedException {
        // 创建PLC클라이언트
        PlcModbusClient plc = new PlcModbusClient("192.168.1.100", 502, 1);
        plc.connect();
        plc.시작AutoReconnect();
        
        // 创建데이터采集服务
        Plc데이터Collector collector = new Plc데이터Collector(plc);
        
        // 추가采集项
        collector.addCollectItem("temperature", 10, "FLOAT32");
        collector.addCollectItem("humidity", 12, "FLOAT32");
        collector.addCollectItem("pressure", 14, "FLOAT32");
        collector.addCollectItem("counter", 20, "INT32");
        collector.addCollectItem("status", 0, "COIL");
        collector.addCollectItem("mode", 50, "INT16");
        
        // 추가데이터变化监听器
        collector.add데이터ChangeListener((key, oldValue, newValue) -> {
            System.out.printf("[데이터变化] %s: %s -> %s%n", key, oldValue, newValue);
        });
        
        // 启动采集,每1秒采集一次
        collector.시작(1000);
        
        // 运行30秒
        Thread.sleep(30000);
        
        // 打印当前所有데이터
        System.out.println("\n=== 当前데이터快照 ===");
        collector.getAll데이터().forEach((k, v) -> System.out.printf("%s = %s%n", k, v));
        
        // 정지采集
        collector.stop();
        plc.disconnect();
    }
}

七、Spring Boot통합

package com.industrial.config;

import com.industrial.modbus.PlcModbusClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ModbusConfig {
    
    @Value("${modbus.plc.ip:192.168.1.100}")
    private String plcIp;
    
    @Value("${modbus.plc.port:502}")
    private int plcPort;
    
    @Value("${modbus.plc.unit-id:1}")
    private int unitId;
    
    @Bean(destroyMethod = "disconnect")
    public PlcModbusClient plcModbusClient() {
        PlcModbusClient client = new PlcModbusClient(plcIp, plcPort, unitId);
        if (client.connect()) {
            client.시작AutoReconnect();
        }
        return client;
    }
}

// Service层
package com.industrial.service;

import com.industrial.modbus.PlcModbusClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Service
public class Plc데이터Service {
    
    @Autowired
    private PlcModbusClient plc;
    
    /**
     * 获取设备运行데이터
     */
    public Map<String, Object> getDevice데이터() throws IOException {
        Map<String, Object> 데이터 = new HashMap<>();
        데이터.put("temperature", plc.readFloat32(10));
        데이터.put("humidity", plc.readFloat32(12));
        데이터.put("pressure", plc.readFloat32(14));
        데이터.put("runningHours", plc.readInt32(20));
        데이터.put("status", plc.readCoils(0, 1)[0]);
        return 데이터;
    }
    
    /**
     * 设置온도设定值
     */
    public void setTemperature(float value) throws IOException {
        plc.writeFloat32(100, value);
    }
    
    /**
     * 启动设备
     */
    public void 시작Device() throws IOException {
        plc.writeSingleCoil(0, true);
    }
    
    /**
     * 정지设备
     */
    public void stopDevice() throws IOException {
        plc.writeSingleCoil(0, false);
    }
}

// 컨트롤러层
package com.industrial.controller;

import com.industrial.service.Plc데이터Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.Map;

@Rest컨트롤러
@RequestMapping("/api/plc")
public class Plc컨트롤러 {
    
    @Autowired
    private Plc데이터Service plcService;
    
    @GetMapping("/데이터")
    public Map<String, Object> get데이터() throws IOException {
        return plcService.getDevice데이터();
    }
    
    @PostMapping("/temperature")
    public String setTemperature(@RequestParam float value) throws IOException {
        plcService.setTemperature(value);
        return "온도设定值已更新为:" + value;
    }
    
    @PostMapping("/시작")
    public String 시작() throws IOException {
        plcService.시작Device();
        return "设备已启动";
    }
    
    @PostMapping("/stop")
    public String stop() throws IOException {
        plcService.stopDevice();
        return "设备已정지";
    }
}

八、常见문제与最佳实践

: 4}

8. 1 자주 묻는 질문

  • 연결超时
  • 返回异常码02
  • 返回异常码03
  • 浮点数분석错误
  • 并发冲突
  • 연결断开
  • 중국어乱码

8. 2 Best Pra cti ces

  • 批量읽기
  • 合理轮询间隔
  • 自动重连
  • 超时设置
  • 异常处理
  • 데이터缓存
  • 로그记录
  • 연결池
  • 주소规划
  • 测试验证

브랜드 별 P LC Mod bus 주소 참조

品牌
西门子S7-200
三菱FX
台达DVP
欧姆龙CP
施耐德M221

이 문서 에서 제공 되는 Java 코 드는 연결 관리 , 자동 재 연 결 , 대 량 읽기 및 쓰 기 , 다양한 데이터 형 식 변환 , 데이터 수집 서비스 및 Spring Boot 통합 을 포함한 P LC 와 의 완전한 Mod bus TCP 통신 기능을 구현 합니다 .이 코 드는 산업 자동 화 프로젝트 개발 에 직접 사용할 수 있으며 , Mod bus Poll 도구를 사용하여 P LC 통신 매 개 변 수 및 주소 매 핑 을 확인 한 다음 Java 코드를 실제 프로젝트에 통합 하는 것이 좋습니다 .

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

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

엔지니어 회원

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

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

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

답글 작성

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