java-modbus之modbus4j输出报文!!!

新公司新需求

各位老铁好! 时隔多日,又来更新了,新公司搞起工业物联网了。。。

需求:让主程序输出modbus通信报文

需求解析:
一开始以为就是打日志,直到同事给了一段报文
Tx:83661-15:21:03.390-00 0F 00 00 00 06 01 01 02 58 00 12 Rx:83662-15:21:03.442-00 0F 00 00 00 06 01 01 03 00 00 00
???
需求落地:
了解到代码底层和modbus通信用的tcp方式,modbus4j这个包,github可以搜到,第701星是我给的
好了,不bb,show code
com.serotonin.modbus4j.ip.tcp.TcpSlave:

/*
/*
 * ============================================================================
 * GNU General Public License
 * ============================================================================
 *
 * Copyright (C) 2006-2011 Serotonin Software Technologies Inc. http://serotoninsoftware.com
 * @author Matthew Lohbihler
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.serotonin.modbus4j.ip.tcp;

import com.serotonin.modbus4j.ModbusSlaveSet;
import com.serotonin.modbus4j.base.BaseMessageParser;
import com.serotonin.modbus4j.base.BaseRequestHandler;
import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.ip.encap.EncapMessageParser;
import com.serotonin.modbus4j.ip.encap.EncapRequestHandler;
import com.serotonin.modbus4j.ip.xa.XaMessageParser;
import com.serotonin.modbus4j.ip.xa.XaRequestHandler;
import com.serotonin.modbus4j.sero.log.IOLog;
import com.serotonin.modbus4j.sero.messaging.MessageControl;
import com.serotonin.modbus4j.sero.messaging.TestableTransport;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * <p>TcpSlave class.</p>
 *
 * @author Matthew Lohbihler
 * @version 5.0.0
 */
public class TcpSlave extends ModbusSlaveSet {
    // Configuration fields
    private final int port;
    final String logPath;
    final boolean encapsulated;

    // Runtime fields.
    private ServerSocket serverSocket;
    final ExecutorService executorService;
    final List<TcpConnectionHandler> listConnections = new ArrayList<>();

    /**
     * <p>Constructor for TcpSlave.</p>
     *
     * @param encapsulated a boolean.
     */
    public TcpSlave(boolean encapsulated) {
        this(ModbusUtils.TCP_PORT, null, encapsulated);
    }

    /**
     * <p>Constructor for TcpSlave.</p>
     *
     * @param port a int.
     * @param encapsulated a boolean.
     */
    public TcpSlave(int port, String logPath, boolean encapsulated) {
        this.port = port;
        this.logPath = logPath;
        this.encapsulated = encapsulated;
        executorService = Executors.newCachedThreadPool();
    }

    /** {@inheritDoc} */
    @Override
    public void start() throws ModbusInitException {
        try {
            serverSocket = new ServerSocket(port);

            Socket socket;
            while (true) {
                socket = serverSocket.accept();
                TcpConnectionHandler handler = new TcpConnectionHandler(socket, logPath);
                executorService.execute(handler);
                synchronized (listConnections) {
                    listConnections.add(handler);
                }
            }
        }
        catch (IOException e) {
            throw new ModbusInitException(e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void stop() {
        // Close the socket first to prevent new messages.
        try {
            serverSocket.close();
        }
        catch (IOException e) {
            getExceptionHandler().receivedException(e);
        }

        // Close all open connections.
        synchronized (listConnections) {
            for (TcpConnectionHandler tch : listConnections)
                tch.kill();
            listConnections.clear();
        }

        // Now close the executor service.
        executorService.shutdown();
        try {
            executorService.awaitTermination(3, TimeUnit.SECONDS);
        }
        catch (InterruptedException e) {
            getExceptionHandler().receivedException(e);
        }
    }

    class TcpConnectionHandler implements Runnable {
        private final Socket socket;
        private TestableTransport transport;
        private MessageControl conn;
        private String logPath;

        TcpConnectionHandler(Socket socket, String logPath) throws ModbusInitException {
            this.socket = socket;
            this.logPath = logPath;
            try {
                transport = new TestableTransport(socket.getInputStream(), socket.getOutputStream());
            }
            catch (IOException e) {
                throw new ModbusInitException(e);
            }
        }

        @Override
        public void run() {
            BaseMessageParser messageParser;
            BaseRequestHandler requestHandler;

            if (encapsulated) {
                messageParser = new EncapMessageParser(false);
                requestHandler = new EncapRequestHandler(TcpSlave.this);
            }
            else {
                messageParser = new XaMessageParser(false);
                requestHandler = new XaRequestHandler(TcpSlave.this);
            }

            conn = new MessageControl();
            if(null != logPath) {
                conn.setIoLog(new IOLog(logPath));
            }
            conn.setExceptionHandler(getExceptionHandler());

            try {
                conn.start(transport, messageParser, requestHandler, null);
                executorService.execute(transport);
            }
            catch (IOException e) {
                getExceptionHandler().receivedException(new ModbusInitException(e));
            }

            // Monitor the socket to detect when it gets closed.
            while (true) {
                try {
                    transport.testInputStream();
                }
                catch (IOException e) {
                    break;
                }

                try {
                    Thread.sleep(500);
                }
                catch (InterruptedException e) {
                    // no op
                }
            }

            conn.close();
            kill();
            synchronized (listConnections) {
                listConnections.remove(this);
            }
        }

        void kill() {
            try {
                socket.close();
            }
            catch (IOException e) {
                getExceptionHandler().receivedException(new ModbusInitException(e));
            }
        }
    }
}

com/serotonin/modbus4j/sero/messaging/MessageControl.java

package com.serotonin.modbus4j.sero.messaging;

import java.io.IOException;

import com.serotonin.modbus4j.sero.io.StreamUtils;
import com.serotonin.modbus4j.sero.log.BaseIOLog;
import com.serotonin.modbus4j.sero.timer.SystemTimeSource;
import com.serotonin.modbus4j.sero.timer.TimeSource;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;

/**
 * In general there are three messaging activities:
 * <ol>
 * <li>Send a message for which no reply is expected, e.g. a broadcast.</li>
 * <li>Send a message and wait for a response with timeout and retries.</li>
 * <li>Listen for unsolicited requests.</li>
 * </ol>
 *
 * @author Matthew Lohbihler
 * @version 5.0.0
 */
public class MessageControl implements DataConsumer {
    private static int DEFAULT_RETRIES = 2;
    private static int DEFAULT_TIMEOUT = 500;

    public boolean DEBUG = false;

    private Transport transport;
    private MessageParser messageParser;
    private RequestHandler requestHandler;
    private WaitingRoomKeyFactory waitingRoomKeyFactory;
    private MessagingExceptionHandler exceptionHandler = new DefaultMessagingExceptionHandler();
    private int retries = DEFAULT_RETRIES;
    private int timeout = DEFAULT_TIMEOUT;
    private int discardDataDelay = 0;
    private long lastDataTimestamp;

    private BaseIOLog ioLog;
    private TimeSource timeSource = new SystemTimeSource();

    private final WaitingRoom waitingRoom = new WaitingRoom();
    private final ByteQueue dataBuffer = new ByteQueue();

    /**
     * <p>start.</p>
     *
     * @param transport a {@link com.serotonin.modbus4j.sero.messaging.Transport} object.
     * @param messageParser a {@link com.serotonin.modbus4j.sero.messaging.MessageParser} object.
     * @param handler a {@link com.serotonin.modbus4j.sero.messaging.RequestHandler} object.
     * @param waitingRoomKeyFactory a {@link com.serotonin.modbus4j.sero.messaging.WaitingRoomKeyFactory} object.
     * @throws java.io.IOException if any.
     */
    public void start(Transport transport, MessageParser messageParser, RequestHandler handler,
            WaitingRoomKeyFactory waitingRoomKeyFactory) throws IOException {
        this.transport = transport;
        this.messageParser = messageParser;
        this.requestHandler = handler;
        this.waitingRoomKeyFactory = waitingRoomKeyFactory;
        waitingRoom.setKeyFactory(waitingRoomKeyFactory);
        transport.setConsumer(this);
    }

    /**
     * <p>close.</p>
     */
    public void close() {
        transport.removeConsumer();
    }

    /**
     * <p>Setter for the field <code>exceptionHandler</code>.</p>
     *
     * @param exceptionHandler a {@link com.serotonin.modbus4j.sero.messaging.MessagingExceptionHandler} object.
     */
    public void setExceptionHandler(MessagingExceptionHandler exceptionHandler) {
        if (exceptionHandler == null)
            this.exceptionHandler = new DefaultMessagingExceptionHandler();
        else
            this.exceptionHandler = exceptionHandler;
    }

    /**
     * <p>Getter for the field <code>retries</code>.</p>
     *
     * @return a int.
     */
    public int getRetries() {
        return retries;
    }

    /**
     * <p>Setter for the field <code>retries</code>.</p>
     *
     * @param retries a int.
     */
    public void setRetries(int retries) {
        this.retries = retries;
    }

    /**
     * <p>Getter for the field <code>timeout</code>.</p>
     *
     * @return a int.
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * <p>Setter for the field <code>timeout</code>.</p>
     *
     * @param timeout a int.
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    /**
     * <p>Getter for the field <code>discardDataDelay</code>.</p>
     *
     * @return a int.
     */
    public int getDiscardDataDelay() {
        return discardDataDelay;
    }

    /**
     * <p>Setter for the field <code>discardDataDelay</code>.</p>
     *
     * @param discardDataDelay a int.
     */
    public void setDiscardDataDelay(int discardDataDelay) {
        this.discardDataDelay = discardDataDelay;
    }

    /**
     * <p>Getter for the field <code>ioLog</code>.</p>
     *
     * @return a {@link com.serotonin.modbus4j.sero.log.BaseIOLog} object.
     */
    public BaseIOLog getIoLog() {
        return ioLog;
    }

    /**
     * <p>Setter for the field <code>ioLog</code>.</p>
     *
     * @param ioLog a {@link com.serotonin.modbus4j.sero.log.BaseIOLog} object.
     */
    public void setIoLog(BaseIOLog ioLog) {
        this.ioLog = ioLog;
    }

    /**
     * <p>Getter for the field <code>timeSource</code>.</p>
     *
     * @return a {@link com.serotonin.modbus4j.sero.timer.TimeSource} object.
     */
    public TimeSource getTimeSource() {
        return timeSource;
    }

    /**
     * <p>Setter for the field <code>timeSource</code>.</p>
     *
     * @param timeSource a {@link com.serotonin.modbus4j.sero.timer.TimeSource} object.
     */
    public void setTimeSource(TimeSource timeSource) {
        this.timeSource = timeSource;
    }

    /**
     * <p>send.</p>
     *
     * @param request a {@link com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage} object.
     * @return a {@link com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage} object.
     * @throws java.io.IOException if any.
     */
    public IncomingResponseMessage send(OutgoingRequestMessage request) throws IOException {
        return send(request, timeout, retries);
    }

    /**
     * <p>send.</p>
     *
     * @param request a {@link com.serotonin.modbus4j.sero.messaging.OutgoingRequestMessage} object.
     * @param timeout a int.
     * @param retries a int.
     * @return a {@link com.serotonin.modbus4j.sero.messaging.IncomingResponseMessage} object.
     * @throws java.io.IOException if any.
     */
    public IncomingResponseMessage send(OutgoingRequestMessage request, int timeout, int retries) throws IOException {
        byte[] data = request.getMessageData();
        if (DEBUG)
            System.out.println("MessagingControl.send: " + StreamUtils.dumpHex(data));

        IncomingResponseMessage response = null;

        if (request.expectsResponse()) {
            WaitingRoomKey key = waitingRoomKeyFactory.createWaitingRoomKey(request);

            // Enter the waiting room
            waitingRoom.enter(key);

            try {
                do {
                    // Send the request.
                    write(data);

                    // Wait for the response.
                    response = waitingRoom.getResponse(key, timeout);

                    if (DEBUG && response == null)
                        System.out.println("Timeout waiting for response");
                }
                while (response == null && retries-- > 0);
            }
            finally {
                // Leave the waiting room.
                waitingRoom.leave(key);
            }

            if (response == null)
                throw new TimeoutException("request=" + request);
        }
        else
            write(data);

        return response;
    }

    /**
     * <p>send.</p>
     *
     * @param response a {@link com.serotonin.modbus4j.sero.messaging.OutgoingResponseMessage} object.
     * @throws java.io.IOException if any.
     */
    public void send(OutgoingResponseMessage response) throws IOException {
        write(response.getMessageData());
    }

    /**
     * {@inheritDoc}
     *
     * Incoming data from the transport. Single-threaded.
     */
    public void data(byte[] b, int len) {
        if (DEBUG)
            System.out.println("MessagingConnection.read: " + StreamUtils.dumpHex(b, 0, len));
        if (ioLog != null)
            ioLog.input(b, 0, len);

        if (discardDataDelay > 0) {
            long now = timeSource.currentTimeMillis();
            if (now - lastDataTimestamp > discardDataDelay)
                dataBuffer.clear();
            lastDataTimestamp = now;
        }

        dataBuffer.push(b, 0, len);

        // There may be multiple messages in the data, so enter a loop.
        while (true) {
            // Attempt to parse a message.
            try {
                // Mark where we are in the buffer. The entire message may not be in yet, but since the parser
                // will consume the buffer we need to be able to backtrack.
                dataBuffer.mark();

                IncomingMessage message = messageParser.parseMessage(dataBuffer);

                if (message == null) {
                    // Nothing to do. Reset the buffer and exit the loop.
                    dataBuffer.reset();
                    break;
                }

                if (message instanceof IncomingRequestMessage) {
                    // Received a request. Give it to the request handler
                    if (requestHandler != null) {
                        OutgoingResponseMessage response = requestHandler
                                .handleRequest((IncomingRequestMessage) message);

                        if (response != null)
                            send(response);
                    }
                }
                else
                    // Must be a response. Give it to the waiting room.
                    waitingRoom.response((IncomingResponseMessage) message);
            }
            catch (Exception e) {
                exceptionHandler.receivedException(e);
                // Clear the buffer
                //                dataBuffer.clear();
            }
        }
    }

    private void write(byte[] data) throws IOException {
        if (ioLog != null)
            ioLog.output(data);

        synchronized (transport) {
            transport.write(data);
        }
    }

    /** {@inheritDoc} */
    public void handleIOException(IOException e) {
        exceptionHandler.receivedException(e);
    }
}

com/serotonin/modbus4j/sero/log/BaseIOLog.java

/**
 * Copyright (C) 2014 Infinite Automation Software and Serotonin Software. All rights reserved.
 * @author Terry Packer, Matthew Lohbihler
 */
package com.serotonin.modbus4j.sero.log;

import com.serotonin.modbus4j.sero.io.NullWriter;
import com.serotonin.modbus4j.sero.io.StreamUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * <p>Abstract BaseIOLog class.</p>
 *
 * @author Terry Packer
 * @version 5.0.0
 */
public abstract class BaseIOLog {

	private static final Log LOG = LogFactory.getLog(BaseIOLog.class);

	/** Constant <code>DATE_FORMAT="yyyy/MM/dd-HH:mm:ss,SSS"</code> */
	protected static final String DATE_FORMAT = "yyyy/MM/dd-HH:mm:ss,SSS";
    protected final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
    protected PrintWriter out;
    protected final File file;
    protected final StringBuilder sb = new StringBuilder();
    protected final Date date = new Date();

    /**
     * <p>Constructor for BaseIOLog.</p>
     *
     * @param logFile a {@link java.io.File} object.
     */
    public BaseIOLog(File logFile){
    	this.file = logFile;
        createOut();
    }

    /**
     * Create the Print Writer output
     */
    protected void createOut() {
        try {
            out = new PrintWriter(new FileWriter(file, true));
        }
        catch (IOException e) {
            out = new PrintWriter(new NullWriter());
            LOG.error("Error while creating process log", e);
        }
    }

    /**
     * <p>close.</p>
     */
    public void close() {
        out.close();
    }

    /**
     * <p>input.</p>
     *
     * @param b an array of {@link byte} objects.
     */
    public void input(byte[] b) {
        log(true, b, 0, b.length);
    }

    /**
     * <p>input.</p>
     *
     * @param b an array of {@link byte} objects.
     * @param pos a int.
     * @param len a int.
     */
    public void input(byte[] b, int pos, int len) {
        log(true, b, pos, len);
    }

    /**
     * <p>output.</p>
     *
     * @param b an array of {@link byte} objects.
     */
    public void output(byte[] b) {
        log(false, b, 0, b.length);
    }

    /**
     * <p>output.</p>
     *
     * @param b an array of {@link byte} objects.
     * @param pos a int.
     * @param len a int.
     */
    public void output(byte[] b, int pos, int len) {
        log(false, b, pos, len);
    }

    /**
     * <p>log.</p>
     *
     * @param input a boolean.
     * @param b an array of {@link byte} objects.
     */
    public void log(boolean input, byte[] b) {
        log(input, b, 0, b.length);
    }

    /**
     * <p>log.</p>
     *
     * @param input a boolean.
     * @param b an array of {@link byte} objects.
     * @param pos a int.
     * @param len a int.
     */
    public synchronized void log(boolean input, byte[] b, int pos, int len) {
        sizeCheck();

        sb.delete(0, sb.length());
        date.setTime(System.currentTimeMillis());
        sb.append(input ? "Tx:" : "Rx:");
        sb.append(sdf.format(date)).append("-");
        sb.append(StreamUtils.dumpHex(b, pos, len));
        out.println(sb.toString());
        out.flush();
    }

    /**
     * <p>log.</p>
     *
     * @param message a {@link java.lang.String} object.
     */
    public synchronized void log(String message) {
        sizeCheck();

        sb.delete(0, sb.length());
        date.setTime(System.currentTimeMillis());
        sb.append(sdf.format(date)).append(" ");
        sb.append(message);
        out.println(sb.toString());
        out.flush();
    }

    /**
     * Check the size of the logfile and perform adjustments
     * as necessary
     */
    protected abstract void sizeCheck();
}

好了。。以上基本是需改内容
看使用

package com.serotonin.modbus4j.test;

import com.serotonin.modbus4j.BasicProcessImage;
import com.serotonin.modbus4j.ModbusFactory;
import com.serotonin.modbus4j.ModbusSlaveSet;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.code.RegisterRange;
import com.serotonin.modbus4j.exception.ModbusInitException;

import java.util.Random;

public class ListenerTest2 {
    static Random random = new Random();
    static float ir1Value = -100;

    public static void main(String[] args) throws Exception {
        ModbusFactory modbusFactory = new ModbusFactory();
        final ModbusSlaveSet listener = modbusFactory.createTcpSlave(false);
        listener.addProcessImage(getModscanProcessImage(1));
        listener.addProcessImage(getModscanProcessImage(2));

        // When the "listener" is started it will use the current thread to run. So, if an exception is not thrown
        // (and we hope it won't be), the method call will not return. Therefore, we start the listener in a separate
        // thread so that we can use this thread to modify the values.
        new Thread(new Runnable() {
            public void run() {
                try {
                    listener.start();
                }
                catch (ModbusInitException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        while (true) {
            updateProcessImage1((BasicProcessImage) listener.getProcessImage(1));
            updateProcessImage2((BasicProcessImage) listener.getProcessImage(2));

            synchronized (listener) {
                listener.wait(5000);
            }
        }
    }

    static void updateProcessImage1(BasicProcessImage processImage) {
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 0, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 2, DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 10, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 12, DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 20, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 22, DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED,
                random.nextInt(10000));
    }

    static void updateProcessImage2(BasicProcessImage processImage) {
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 0, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 3, DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 10, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 12, DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 20, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 22, DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 99, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 100, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 101, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
    }

    static BasicProcessImage getModscanProcessImage(int slaveId) {
        BasicProcessImage processImage = new BasicProcessImage(slaveId);
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 0, DataType.TWO_BYTE_INT_UNSIGNED,
                random.nextInt(10000));
        processImage.setNumeric(RegisterRange.HOLDING_REGISTER, 3, DataType.FOUR_BYTE_INT_UNSIGNED_SWAPPED,
                random.nextInt(10000));
        processImage.setAllowInvalidAddress(true);
        processImage.setInvalidAddressValue(Short.MIN_VALUE);
        processImage.setExceptionStatus((byte) 151);

        return processImage;
    }
}

代码已开源,欢迎点星!多多交流,感谢感谢!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值