SpringBoot集成ModbusTCP

SpringBoot集成ModbusTCP

1. jar依赖

    <dependencies>
        <dependency>
            <groupId>com.digitalpetri.modbus</groupId>
            <artifactId>modbus-master-tcp</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.intelligt.modbus</groupId>
            <artifactId>jlibmodbus</artifactId>
            <version>1.2.9.7</version>
        </dependency>
        <dependency>
            <groupId>com.infiniteautomation</groupId>
            <artifactId>modbus4j</artifactId>
            <version>3.0.3</version>
        </dependency>
    </dependencies>
    
    <!--    modbus4j的下载地址-->
    <repositories>
        <repository>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
            <id>ias-snapshots</id>
            <name>Infinite Automation Snapshot Repository</name>
            <url>https://maven.mangoautomation.net/repository/ias-snapshot/</url>
        </repository>
        <repository>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <id>ias-releases</id>
            <name>Infinite Automation Release Repository</name>
            <url>https://maven.mangoautomation.net/repository/ias-release/</url>
        </repository>
    </repositories>

2. modbusSlave

package com.liming.app.modbusTcp.slave;


public interface ModbusEventListener {
    int[] readHoldingRegisterRange(int offset, int quantity);

    int[] readInputRegisterRange(int offset, int quantity);

    void onWriteToSingleCoil(int address, boolean value);

    void onWriteToMultipleCoils(int address, int quantity, boolean[] values);

    void onWriteToSingleHoldingRegister(int address, int value);

    void onWriteToMultipleHoldingRegisters(int address, int quantity, int[] values);

}
package com.liming.app.modbusTcp.slave;

import com.intelligt.modbus.jlibmodbus.Modbus;
import com.intelligt.modbus.jlibmodbus.data.ModbusHoldingRegisters;
import com.intelligt.modbus.jlibmodbus.exception.IllegalDataAddressException;
import com.intelligt.modbus.jlibmodbus.exception.IllegalDataValueException;
import com.intelligt.modbus.jlibmodbus.exception.ModbusIOException;
import com.intelligt.modbus.jlibmodbus.slave.ModbusSlave;
import com.intelligt.modbus.jlibmodbus.slave.ModbusSlaveFactory;
import com.intelligt.modbus.jlibmodbus.tcp.TcpParameters;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;

/**
 * @Author:liming.she
 * @Date:2023/11/10 9:51
 * @Description:
 */
@Component
public class ModbusSlaveUtil implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        Thread.sleep(2000);
        new Thread(new Runnable() {
            @Override
            public void run() {
                createSalve();
            }
        }).start();
    }

    private static int[] arr = new int[]{0,0};

    public static ModbusSlave slave ;

    public void createSalve(){
        try{
            initSlave();
            System.out.println("slave start...........");
            createDataHolder();
            closeSlave();
        }catch (Exception e){

        }
    }

    public static void initSlave() throws UnknownHostException, ModbusIOException {
        TcpParameters tcpParameters = new TcpParameters();// 设置从机TCP参数
        InetAddress adress = InetAddress.getByName("127.0.0.1");// 设置TCP的ip地址
        // getLocalHost()返回的是本机地址
        // tcpParameters.setHost(InetAddress.getLocalHost());
        tcpParameters.setHost(adress); // 为从机TCP设置上述ip地址参数
        // 设置从机TCP的是否长连接,通俗点讲就是一直保持连接,一次连接完下次就不要在连接了
        tcpParameters.setKeepAlive(true);
        tcpParameters.setPort(503);// 设置从机TCP的端口
        // 创建一个从机
        slave = ModbusSlaveFactory.createModbusSlaveTCP(tcpParameters);
        // 设置控制台输出主机和从机命令交互日志
        Modbus.setLogLevel(Modbus.LogLevel.LEVEL_DEBUG);

    }

    public void createDataHolder() throws ModbusIOException {
        // 创建从机的寄存器
        MyOwnDataHolder dh = new MyOwnDataHolder();

        // 为从机寄存器添加监听事件,这里的监听事件主要是主机如果发送写命令修改从机则控制台输出
        dh.addEventListener(new ModbusEventListener() {
            @Override
            public int[] readHoldingRegisterRange(int offset, int quantity){
                System.out.println("readHoldingRegisterRange: 读取信息 offset = " + offset+";quantity"+quantity);
//                try{
//                    updateHoldingRegisters(offset, quantity);
//                }catch (Exception e){
//                    e.printStackTrace();
//                }
                return arr;
            }

            @Override
            public int[] readInputRegisterRange(int offset, int quantity){
                System.out.println("readInputRegisterRange: 读取信息 offset = " + offset+";quantity"+quantity);
//                try{
//                    updateInputRegisters(offset, quantity);
//                }catch (Exception e){
//                    e.printStackTrace();
//                }
                return arr;
            }

            @Override
            public void onWriteToSingleCoil(int address, boolean value) {
                System.out
                        .print("onWriteToSingleCoil: address " + address + ", value " + value);
            }

            @Override
            public void onWriteToMultipleCoils(int address, int quantity, boolean[] values) {
                System.out.print("onWriteToMultipleCoils: address " + address + ", quantity "
                        + quantity);
            }

            @Override
            public void onWriteToSingleHoldingRegister(int address, int value) {
                System.out.print("onWriteToSingleHoldingRegister: address " + address
                        + ", value " + value);
            }

            @Override
            public void onWriteToMultipleHoldingRegisters(int address, int quantity,
                                                          int[] values) {
                System.out.print("onWriteToMultipleHoldingRegisters: address " + address
                        + ", quantity " + quantity);
            }

        });
        // 为从机设置寄存器
        slave.setDataHolder(dh);
        // 设置从机的读超时时间,建议主机读的超时时间小于该值
        slave.setReadTimeout(15000);
        // 为从机设置从机服务地址slaveid
        slave.setServerAddress(1);
        // 开启从机监听事件,必须要这一句
        slave.listen();
    }

    public static void closeSlave() throws InterruptedException, ModbusIOException {
        //这部分代码主要是设置Java虚拟机关闭的时候需要做的事情,即本程序关闭的时候需要做的事情,直接使用即可
        if (slave.isListening()) {
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                synchronized (slave) {
                    slave.notifyAll();
                }
            }));

            synchronized (slave) {
                slave.wait();
            }
            slave.shutdown();
            System.out.println("slave shutdown........");
        }
    }

    public void updateHoldingRegisters(int offset, int quantity) throws IllegalDataAddressException, IllegalDataValueException {
        List<Float> list =  Arrays.asList(10.1f,20.3f,89.5f,77.353f);
        ModbusHoldingRegisters hr = new ModbusHoldingRegisters(10000);
        // 修改数值寄存器对应位置的值,第一个参数代表寄存器地址,第二个代表修改的数值
        //hr.set有几个方法,根据自己要赋值的数据类型选择,此处示例的是赋值float类型,一个float是4个字节,32bit,对应2个寄存器所以i*2
        for(int i=0;i<list.size();i++){
            hr.setFloat32At(i*2, list.get(i));
        }
        slave.getDataHolder().setHoldingRegisters(hr);
    }

    public void updateInputRegisters(int offset, int quantity) throws IllegalDataAddressException, IllegalDataValueException, ModbusIOException {
        List<Float> list =  Arrays.asList(10.1f,20.3f,89.5f,77.353f);
        ModbusHoldingRegisters hr = new ModbusHoldingRegisters(10000);
        // 修改数值寄存器对应位置的值,第一个参数代表寄存器地址,第二个代表修改的数值
        for(int i=0;i<list.size();i++){
            hr.setFloat32At(i*2, list.get(i));
        }
        slave.getDataHolder().setInputRegisters(hr);
    }
}

package com.liming.app.modbusTcp.slave;

import com.intelligt.modbus.jlibmodbus.data.DataHolder;
import com.intelligt.modbus.jlibmodbus.exception.IllegalDataAddressException;
import com.intelligt.modbus.jlibmodbus.exception.IllegalDataValueException;
import com.liming.app.common.cache.CacheSingleton;
import com.liming.app.modbusTcp.HexUtil;
import com.liming.app.common.config.ApplicationContextGetBeanHelper;
import org.springframework.integration.ip.udp.UnicastSendingMessageHandler;
import org.springframework.integration.support.MessageBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MyOwnDataHolder extends DataHolder {

    final List<ModbusEventListener> modbusEventListenerList = new ArrayList<>();

    public MyOwnDataHolder() {
        // you can place the initialization code here
        /*
         * something like that: setHoldingRegisters(new
         * SimpleHoldingRegisters(10)); setCoils(new Coils(128)); ... etc.
         */
    }

    public void addEventListener(ModbusEventListener listener) {
        modbusEventListenerList.add(listener);
    }

    public boolean removeEventListener(ModbusEventListener listener) {
        return modbusEventListenerList.remove(listener);
    }

    @Override
    public int[] readHoldingRegisterRange(int offset, int quantity) throws IllegalDataAddressException {
//        for (ModbusEventListener l : modbusEventListenerList) {
//            l.readHoldingRegisterRange(offset, quantity);
//        }
//        return super.readHoldingRegisterRange(offset, quantity);

		return handlerData(offset,quantity);
	}

	public int[] handlerData(int offset, int quantity){
		int[] ints = allData();
		int[] ints1 = Arrays.copyOfRange(ints, offset, offset+quantity);
		System.out.println("ints1 = " + ints1);
		return ints1;
	}

	public int[] allData(){
		int[] result = new int[20000];
		int number = 0;
		for (int i = 0; i < 20000; i++) {
			if (i%4==1){
				number = 16384;
			}else if (i%4==2){
				number = 16968;
			}else if (i%4==3){
				number = 12740;
			}else if (i%4==0){
				number = 17455;
			}
			result[i] = number;
		}
		return result;
	}

    @Override
    public int[] readInputRegisterRange(int offset, int quantity) throws IllegalDataAddressException{
//        for (ModbusEventListener l : modbusEventListenerList) {
//            l.readInputRegisterRange(offset, quantity);
//        }
//        return super.readInputRegisterRange(offset, quantity);
		return new int[]{16422,14417,60293,7864,6,5,4,3,2,1};
    }

	@Override
	public void writeHoldingRegister(int offset, int value) throws IllegalDataAddressException,
			IllegalDataValueException {
//		for (ModbusEventListener l : modbusEventListenerList) {
//			l.onWriteToSingleHoldingRegister(offset, value);
//		}
		String message = "hello world";
		UnicastSendingMessageHandler messageHandler = (UnicastSendingMessageHandler)ApplicationContextGetBeanHelper.getBean("unicastSendingMessageHandler");
		messageHandler.handleMessage(MessageBuilder.withPayload(message).build());
		super.writeHoldingRegister(offset, value);
	}

	@Override
	public void writeHoldingRegisterRange(int offset, int[] range) {
		//ModbusMasterTCP.initModbusTcpMaster();
		int count = 0;
		ArrayList<String> hexList = new ArrayList<>();
		for (int i : range) {
			System.out.println("i = " + i);
//			ModbusMasterTCP.writeHoldingRegisters(count++,i,1);
			hexList.add(HexUtil.integerToHex(i));
		}
		System.out.println("hexList = " + hexList);
		List<Double> doubles = HexUtil.hexStrArrToDoubleArr(hexList);
	}

	@Override
	public void writeCoil(int offset, boolean value) throws IllegalDataAddressException,
			IllegalDataValueException {
		for (ModbusEventListener l : modbusEventListenerList) {
			l.onWriteToSingleCoil(offset, value);
		}
		super.writeCoil(offset, value);
	}

	@Override
	public void writeCoilRange(int offset, boolean[] range) throws IllegalDataAddressException,
			IllegalDataValueException {
		for (ModbusEventListener l : modbusEventListenerList) {
			l.onWriteToMultipleCoils(offset, range.length, range);
		}
		super.writeCoilRange(offset, range);
	}

}

3. modbusMaster

package com.liming.app.modbusTcp.master;

import com.digitalpetri.modbus.FunctionCode;
import com.digitalpetri.modbus.codec.Modbus;
import com.digitalpetri.modbus.master.ModbusTcpMaster;
import com.digitalpetri.modbus.master.ModbusTcpMasterConfig;
import com.digitalpetri.modbus.requests.*;
import com.digitalpetri.modbus.responses.*;
import com.liming.app.common.cache.CacheSingleton;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * @Author:liming.she
 * @Date:2023/11/10 9:11
 * @Description:
 */
public class ModbusMasterTCP {

    public static ModbusTcpMaster master;


    /**
     * 获取TCP协议的Master
     */
    public static void initModbusTcpMaster() {
        if (master == null) {
            // 创建配置
            ModbusTcpMasterConfig config = new ModbusTcpMasterConfig.Builder("127.0.0.1").setPort(503).build();
            master = new ModbusTcpMaster(config);
        }
    }


    /**
     * 释放资源
     */
    public static void release() {
        if (master != null) {
            master.disconnect();
        }
        Modbus.releaseSharedResources();
    }


    /**
     * 读取Coils开关量  功能码为0x01
     *
     * @param slaveId  从机ID
     * @param address  数据起始地址
     * @param quantity 需要读取的数量
     */
    public static void readCoils(int slaveId, int address, int quantity) throws Exception {
        CompletableFuture<ReadCoilsResponse> future = master.sendRequest(new ReadCoilsRequest(address, quantity), slaveId);

        // 工具类做的同步返回。实际使用推荐结合业务进行异步处理
        ReadCoilsResponse response = future.get();
        if (response != null) {
            System.out.println("\nCoils寄存器(0x01)第"+address+"至"+quantity+"数据如下:");
            ByteBuf buf = response.getCoilStatus();
            toStringOnByteBuf(buf);
            ReferenceCountUtil.release(response);
        }
    }


    /**
     * 读取DiscreteInputs开关量  功能码为0x02
     *
     * @param slaveId  从机ID
     * @param address  数据起始地址
     * @param quantity 需要读取的数量
     */
    public static void readDiscreteInputs(int slaveId, int address, int quantity) throws Exception {
        CompletableFuture<ReadDiscreteInputsResponse> future =
                master.sendRequest(new ReadDiscreteInputsRequest(address, quantity), slaveId);

        // 工具类做的同步返回。实际使用推荐结合业务进行异步处理
        ReadDiscreteInputsResponse response = future.get();
        if (response != null) {
            System.out.println("\nDiscreteInputs寄存器(0x02)第"+address+"至"+quantity+"数据如下:");
            ByteBuf buf = response.getInputStatus();
            toStringOnByteBuf(buf);
            ReferenceCountUtil.release(response);
        }
    }


    /**
     * 读取HoldingRegister数据  功能码为0x03
     *
     * @param slaveId  从机ID
     * @param address  数据起始地址
     * @param quantity 需要读取的数量
     */
    public static void readHoldingRegisters(int slaveId, int address, int quantity) throws Exception {
        CompletableFuture<ReadHoldingRegistersResponse> future =
                master.sendRequest(new ReadHoldingRegistersRequest(address, quantity), slaveId);

        // 工具类做的同步返回。实际使用推荐结合业务进行异步处理
        ReadHoldingRegistersResponse response = future.get();
        if (response != null) {
            System.out.println("\nHoldingRegisters寄存器(0x03)第"+address+"至"+quantity+"数据如下:");
            ByteBuf buf = response.getRegisters();
            toStringOnByteBuf(buf);
            ReferenceCountUtil.release(response);
        }
    }


    /**
     * 读取InputRegisters模拟量数据  功能码为0x04
     *
     * @param slaveId  从机ID
     * @param address  数据起始地址
     * @param quantity 需要读取的数量
     */
    public static void readInputRegisters(int slaveId, int address, int quantity) throws Exception {
        CompletableFuture<ReadInputRegistersResponse> future =
                master.sendRequest(new ReadInputRegistersRequest(address, quantity), slaveId);

        // 工具类做的同步返回。实际使用推荐结合业务进行异步处理
        ReadInputRegistersResponse response = future.get();
        if (response != null) {
            System.out.println("\nInputRegisters寄存器(0x04)第"+address+"至"+quantity+"数据如下:");
            ByteBuf buf = response.getRegisters();
            toStringOnByteBuf(buf);
            ReferenceCountUtil.release(response);
        }
    }


    /**
     * 成功代码
     */
    private static final String SUCCESS_CODE = "0x000000";
    /**
     * 与modubs连接异常
     */
    private static final String COON_FAIL_CODE = "0x000001";
    /**
     * 向modubs发送命令执行异常
     */
    private static final String EXEC_FAIL_CODE = "0x000002";

    /**
     * 数据写入失败
     */
    private static final String WRITE_FAIL_CODE = "0x000004";

    /**
     * @param address 寄存器地址
     * @param value   写入值
     * @param unitId  slaveId
     * @description: 写HoldingRegister数据
     * @return: 结果值
     */
    public static String writeHoldingRegisters(Integer address, Integer value, Integer unitId) {
        ModbusResponse modbusResponse;
        try {
            // 发送单个寄存器数据,一般是无符号16位值:比如10
            CompletableFuture<ModbusResponse> future = master.sendRequest(new WriteSingleRegisterRequest(address, value), unitId);

            //获取写入的响应流
            modbusResponse = future.get();
            if (modbusResponse == null) {
                System.out.println("FCSC-ExternalConnection WriteHoldingRegisters:modbusResponse is null ");
                return WRITE_FAIL_CODE;
            }
            //获取写入的响应FunctionCode
            FunctionCode functionCode = modbusResponse.getFunctionCode();
            System.out.println("FCSC-ExternalConnection functionCode=" + functionCode + " value=" + value);
            if (functionCode == FunctionCode.WriteSingleRegister) {
                return SUCCESS_CODE;
            } else {
                return WRITE_FAIL_CODE;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return EXEC_FAIL_CODE;
        } finally {
            // String releaseRes = release();
            // //如果释放连接失败,返回执行失败
            // if (!SUCCESS_CODE.equals(releaseRes)) {
            //     return releaseRes;
            // }
        }
    }


    /**
     * modubs从站ID
     */
    private static final Integer UNIT_ID = 1;

    /**
     * @param address  寄存器地址
     * @param quantity 写位数
     * @param values   写入值
     * @description: 写HoldingRegister数据
     * @return: 结果值
     */
    public static String WriteMultipleRegisters(Integer address, Integer quantity, byte[] values) {
        try {
            WriteMultipleRegistersRequest request = new WriteMultipleRegistersRequest(address, quantity, values);
            // 发送单个寄存器数据,一般是无符号16位值:比如10
            CompletableFuture<ModbusResponse> future = master.sendRequest(request, UNIT_ID);
            ModbusResponse modbusResponse;
            //获取写入的响应流
            modbusResponse = future.get();
            if (modbusResponse == null) {
                System.out.println("FCSC-ExternalConnection WriteMultipleRegisters:modbusResponse is null ");
                return WRITE_FAIL_CODE;
            }
            //获取写入的响应FunctionCode
            FunctionCode functionCode = modbusResponse.getFunctionCode();
            System.out.println("FCSC-ExternalConnection functionCode.getCode()===" + functionCode.getCode() + "=" + functionCode);
            if (functionCode == FunctionCode.WriteMultipleRegisters) {
                return SUCCESS_CODE;
            } else {
                return WRITE_FAIL_CODE;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            return EXEC_FAIL_CODE;
        } catch (ExecutionException e) {
            e.printStackTrace();
            return EXEC_FAIL_CODE;
        } finally {
            // String releaseRes = release();
            // //如果释放连接失败,返回执行失败
            // if (!SUCCESS_CODE.equals(releaseRes)) {
            //     return releaseRes;
            // }
        }
    }

    /**
     * 请注意,这个方法假设输入的int数组的元素都是非负数,并且可以安全地转换为byte。如果输入的int数组包含负数或大于255的值,那么转换的结果可能不正确。
     * 此外,这个方法会忽略int数组中的高位字节。例如,如果一个int值为0xFFFFFFFE,那么它被转换为byte时将变为-2。如果你需要保留高位字节,那么你需要修改这个方法来处理这种情况。
     * @param src
     * @param offset
     * @param length
     * @return
     */
    public static byte[] intArrayToByteArray(int[] src, int offset, int length) {
        if (src == null || src.length <= 0 || offset < 0 || length <= 0 || offset + length > src.length) {
            return null;
        }
        byte[] dest = new byte[length];
        for (int i = 0; i < length; i++) {
            dest[i] = (byte) (src[offset + i] & 0xFF);
        }
        return dest;
    }

    /**
     * ByteBuf转String
     *
     * @param buf
     */
    public static void toStringOnByteBuf(ByteBuf buf,String threadName) {
        // 两个byte为一组,同组的不能换行
        boolean whole = false;
        String binary = "";
        while (buf.isReadable()) {
            whole = buf.readerIndex() % 2 == 1 ? true : false;
            if (!whole) {
                System.out.print("index=" + buf.readerIndex() / 2);
            }
            binary += Integer.toBinaryString((buf.readByte() & 0xFF) + 0x100).substring(1);

            if (whole) {
                System.out.println(","+threadName+"binary=" + binary + ",value=" + Integer.parseInt(binary, 2));
                binary = "";
            }
        }
    }

    public static void toStringOnByteBuf(ByteBuf buf) {
        // 两个byte为一组,同组的不能换行
        boolean whole = false;
        String binary = "";
        while (buf.isReadable()) {
            whole = buf.readerIndex() % 2 == 1 ? true : false;
            if (!whole) {
                System.out.print("index=" + buf.readerIndex() / 2);
            }
            binary += Integer.toBinaryString((buf.readByte() & 0xFF) + 0x100).substring(1);

            if (whole) {
                System.out.println(",binary=" + binary + ",value=" + Integer.parseInt(binary, 2));
                binary = "";
            }
        }
    }


    public static void main(String[] args) throws Exception {
        // 初始化资源
//        initModbusTcpMaster();

//        readCoils(1, 0, 10);
//        readDiscreteInputs(1, 0, 10);
//        readHoldingRegisters(1, 0, 10);
//        readInputRegisters(1, 0, 10);

        // 释放资源
//        release();
    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Spring Boot 是一个开源的Java框架,可以帮助开发者快速搭建基于Java的Web应用程序。Modbus TCP 是一种用于工业自动化领域中的通信协议,可以用于实现设备之间的数据读取和控制。 在Spring Boot中整合Modbus TCP实现数据读取可以分为以下几个步骤: 1. 引入相关依赖:在项目的pom.xml文件中添加Modbus TCP的依赖,如jamod或modbus4j等。 2. 配置Modbus TCP连接参数:在Spring Boot的配置文件中,配置Modbus TCP的IP地址和端口号等连接参数。 3. 创建Modbus TCP连接:使用Java代码,在Spring Boot应用程序中创建Modbus TCP的连接对象。 4. 读取数据:通过连接对象,使用Modbus TCP协议发送读取数据的指令,并获取返回的数据。 5. 数据处理:对于得到的数据,可以进行相应的处理,如解析数据格式、存储到数据库或展示在前端页面等。 6. 异常处理:在进行Modbus TCP通信时,可能会出现网络异常或通信错误,需要进行相应的异常处理,包括错误日志记录、重试机制等。 在实际应用中,可以根据具体的业务需求对数据读取进行定制化开发。例如,可以通过设置读取起始地址和读取长度等参数,读取一定范围内的数据。同时,也可以创建定时任务,在固定的时间点进行数据的读取操作。 总之,通过Spring Boot的便捷开发和Modbus TCP的通信协议,可以实现方便快速地进行数据读取,并能灵活应对不同的业务场景和需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值