C#串口操作

C#串口操作(国外网站看来的,共享一下)

 前一阵,从国外网站看到一个用C#来操作串口的类。下载下来试了一下,觉得不错。共享一下。
/*
* Author: Marcus Lorentzon, 2001
*         d98malor@dtek.chalmers.se
*
* Freeware: Please do not remove this header
*
* File: SerialStream.cs
*
* Description: Implements a Stream for asynchronous
*              transfers and COMM. Stream version.
*
* Version: 2.4
*
*/

#region Using

using System;
using System.IO;
using System.Threading;
using System.Runtime.InteropServices;
using System.ComponentModel;

#endregion Using

namespace LoMaN.IO {

   
public class SerialStream : Stream {
       
       
#region Attributes

       
private IOCompletionCallback m_IOCompletionCallback;
       
private IntPtr m_hFile = IntPtr.Zero;
       
private string m_sPort;
       
private bool m_bRead;
       
private bool m_bWrite;

       
#endregion Attributes

       
#region Properties

       
public string Port {
           
get {
               
return m_sPort;
            }
           
set {
               
if (m_sPort != value) {
                    Close();
                    Open(value);
                }
            }
        }

       
public override bool CanRead {
           
get {
               
return m_bRead;
            }
        }

       
public override bool CanWrite {
           
get {
               
return m_bWrite;
            }
        }

       
public override bool CanSeek {
           
get {
               
return false;
            }
        }

       
public bool Closed  {
           
get {
               
return m_hFile.ToInt32()  0;
            }
        }

       
public bool Dsr {
           
get {
               
uint status;
               
if (!GetCommModemStatus(m_hFile, out status)) {
                   
throw new Win32Exception();
                }
               
return (status & MS_DSR_ON) > 0;
            }
        }

       
public bool Ring {
           
get {
               
uint status;
               
if (!GetCommModemStatus(m_hFile, out status)) {
                   
throw new Win32Exception();
                }
               
return (status & MS_RING_ON) > 0;
            }
        }

       
public bool Rlsd {
           
get {
               
uint status;
               
if (!GetCommModemStatus(m_hFile, out status)) {
                   
throw new Win32Exception();
                }
               
return (status & MS_RLSD_ON) > 0;
            }
        }

       
#endregion Properties

       
#region Constructors

       
public SerialStream() : this(FileAccess.ReadWrite) {
        }

       
public SerialStream(FileAccess access) {
            m_bRead 
= ((int)access & (int)FileAccess.Read) != 0;
            m_bWrite
= ((int)access & (int)FileAccess.Write) != 0;
           
unsafe {
                m_IOCompletionCallback
= new IOCompletionCallback(AsyncFSCallback);
            }
        }

       
public SerialStream(string port) : this(FileAccess.ReadWrite) {
            Open(port);
        }

       
public SerialStream(string port, FileAccess access) : this(access) {
            Open(port);
        }

       
#endregion Constructors

       
#region Methods

       
public void Open(string port) {
           
if (m_hFile != IntPtr.Zero) {
               
throw new IOException("Stream already opened.");
            }
            m_sPort
= port;
            m_hFile
= CreateFile(port, (uint)((m_bRead?GENERIC_READ:0)|(m_bWrite?GENERIC_WRITE:0)), 0, 0, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
           
if (m_hFile.ToInt32() == INVALID_HANDLE_VALUE) {
                m_hFile
= IntPtr.Zero;
               
throw new FileNotFoundException("Unable to open " + port);
            }

            ThreadPool.BindHandle(m_hFile);

            SetTimeouts(
0, 0, 0, 0, 0);
        }

       
public override void Close() {
            CloseHandle(m_hFile);
            m_hFile
= IntPtr.Zero;
            m_sPort
= null;
        }

       
public IAsyncResult BeginRead(byte[] buffer) {
           
return BeginRead(buffer, 0, buffer.Length, null, null);
        }

       
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) {
            GCHandle gchBuffer
= GCHandle.Alloc(buffer, GCHandleType.Pinned);
            SerialAsyncResult sar
= new SerialAsyncResult(this, state, callback, true, gchBuffer);
            Overlapped ov
= new Overlapped(0, 0, sar.AsyncWaitHandle.Handle.ToInt32(), sar);
           
unsafe {
                NativeOverlapped
* nov = ov.Pack(m_IOCompletionCallback);
               
byte* data = (byte*)((int)gchBuffer.AddrOfPinnedObject() + offset);

               
uint read = 0;
               
if (ReadFile(m_hFile, data, (uint)count, out read, nov)) {
                    sar.m_bCompletedSynchronously
= true;
                   
return sar;
                }
               
else if (GetLastError() == ERROR_IO_PENDING) {
                   
return sar;
                }
               
else
                   
throw new Exception("Unable to initialize read. Errorcode: " + GetLastError().ToString());
            }
        }

       
public IAsyncResult BeginWrite(byte[] buffer) {
           
return BeginWrite(buffer, 0, buffer.Length, null, null);
        }

       
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) {
            GCHandle gchBuffer
= GCHandle.Alloc(buffer, GCHandleType.Pinned);
            SerialAsyncResult sar
= new SerialAsyncResult(this, state, callback, false, gchBuffer);
            Overlapped ov
= new Overlapped(0, 0, sar.AsyncWaitHandle.Handle.ToInt32(), sar);
           
unsafe {
                NativeOverlapped
* nov = ov.Pack(m_IOCompletionCallback);
               
byte* data = (byte*)((int)gchBuffer.AddrOfPinnedObject() + offset);

               
uint written = 0;
               
if (WriteFile(m_hFile, data, (uint)count, out written, nov)) {
                    sar.m_bCompletedSynchronously
= true;
                   
return sar;
                }
               
else if (GetLastError() == ERROR_IO_PENDING) {
                   
return sar;
                }
               
else
                   
throw new Exception("Unable to initialize write. Errorcode: " + GetLastError().ToString());
            }
        }

       
private int EndOperation(IAsyncResult asyncResult, bool isRead) {
            SerialAsyncResult sar
= (SerialAsyncResult)asyncResult;
           
if (sar.m_bIsRead != isRead)
               
throw new IOException("Invalid parameter: IAsyncResult is not from a " + (isRead ? "read" : "write"));
           
if (sar.EndOperationCalled) {
               
throw new IOException("End" + (isRead ? "Read" : "Write") + " called twice for the same operation.");
            }
           
else {
                sar.m_bEndOperationCalled
= true;
            }

           
while (!sar.m_bCompleted) {
                sar.AsyncWaitHandle.WaitOne();
            }

            sar.Dispose();

           
if (sar.m_nErrorCode != ERROR_SUCCESS && sar.m_nErrorCode != ERROR_OPERATION_ABORTED) {
               
throw new IOException("Operation finished with errorcode: " + sar.m_nErrorCode);
            }

           
return sar.m_nReadWritten;
        }
       
       
public override int EndRead(IAsyncResult asyncResult) {
           
return EndOperation(asyncResult, true);
        }
       
       
public override void EndWrite(IAsyncResult asyncResult) {
            EndOperation(asyncResult,
false);
        }

       
public int EndWriteEx(IAsyncResult asyncResult) {
           
return EndOperation(asyncResult, false);
        }

       
public override int Read(byte[] buffer, int offset, int count) {
           
return EndRead(BeginRead(buffer, offset, count, null, null));
        }

       
public override void Write(byte[] buffer, int offset, int count) {
            EndWrite(BeginWrite(buffer, offset, count,
null, null));
        }

       
public int WriteEx(byte[] buffer, int offset, int count) {
           
return EndWriteEx(BeginWrite(buffer, offset, count, null, null));
        }

       
public int Read(byte[] buffer) {
           
return EndRead(BeginRead(buffer, 0, buffer.Length, null, null));
        }

       
public int Write(byte[] buffer) {
           
return EndOperation(BeginWrite(buffer, 0, buffer.Length, null, null), false);
        }

       
public override void Flush() {
            FlushFileBuffers(m_hFile);
        }

       
public bool PurgeRead() {
           
return PurgeComm(m_hFile, PURGE_RXCLEAR);
        }

       
public bool PurgeWrite() {
           
return PurgeComm(m_hFile, PURGE_TXCLEAR);
        }

       
public bool Purge() {
           
return PurgeRead() && PurgeWrite();
        }

       
public bool CancelRead() {
           
return PurgeComm(m_hFile, PURGE_RXABORT);
        }

       
public bool CancelWrite() {
           
return PurgeComm(m_hFile, PURGE_TXABORT);
        }

       
public bool CancelAll() {
           
return CancelRead() && CancelWrite();
        }

       
public override void SetLength(long nLength) {
           
throw new NotSupportedException("SetLength isn't supported on serial ports.");
        }

       
public override long Seek(long offset, SeekOrigin origin) {
           
throw new NotSupportedException("Seek isn't supported on serial ports.");
        }

       
public void SetTimeouts(int ReadIntervalTimeout,
                               
int ReadTotalTimeoutMultiplier,
                               
int ReadTotalTimeoutConstant,
                               
int WriteTotalTimeoutMultiplier,
                               
int WriteTotalTimeoutConstant) {
            SerialTimeouts Timeouts
= new SerialTimeouts(ReadIntervalTimeout,
                                                         ReadTotalTimeoutMultiplier,
                                                         ReadTotalTimeoutConstant,
                                                         WriteTotalTimeoutMultiplier,
                                                         WriteTotalTimeoutConstant);
           
unsafe { SetCommTimeouts(m_hFile, ref Timeouts); }
        }

       
public bool SetPortSettings(uint baudrate) {
           
return SetPortSettings(baudrate, FlowControl.Hardware);
        }

       
public bool SetPortSettings(uint baudrate, FlowControl flowControl) {
           
return SetPortSettings(baudrate, flowControl, Parity.None);
        }

       
public bool SetPortSettings(uint baudrate, FlowControl flowControl, Parity parity) {
           
return SetPortSettings(baudrate, flowControl, parity, 8, StopBits.One);
        }

       
public bool SetPortSettings(uint baudrate, FlowControl flowControl, Parity parity, byte databits, StopBits stopbits) {
           
unsafe {
                DCB dcb
= new DCB();
                dcb.DCBlength
= sizeof(DCB);
                dcb.BaudRate
= baudrate;
                dcb.ByteSize
= databits;
                dcb.StopBits
= (byte)stopbits;
                dcb.Parity
= (byte)parity;
                dcb.fParity
= (parity > 0)? 1U : 0U;
                dcb.fBinary
= dcb.fDtrControl = dcb.fTXContinueOnXoff = 1;
                dcb.fOutxCtsFlow
= dcb.fAbortOnError = (flowControl == FlowControl.Hardware)? 1U : 0U;
                dcb.fOutX
= dcb.fInX = (flowControl == FlowControl.XOnXOff)? 1U : 0U;
                dcb.fRtsControl
= (flowControl == FlowControl.Hardware)? 2U : 1U;
                dcb.XonLim
= 2048;
                dcb.XoffLim
= 512;
                dcb.XonChar
= 0x11; // Ctrl-Q
                dcb.XoffChar = 0x13; // Ctrl-S
                return SetCommState(m_hFile, ref dcb);
            }
        }

       
public bool SetPortSettings(DCB dcb) {
           
return SetCommState(m_hFile, ref dcb);
        }

       
public bool GetPortSettings(out DCB dcb) {
           
unsafe {
                DCB dcb2
= new DCB();
                dcb2.DCBlength
= sizeof(DCB);
               
bool ret = GetCommState(m_hFile, ref dcb2);
                dcb
= dcb2;
               
return ret;
            }
        }

       
public bool SetXOn() {
           
return EscapeCommFunction(m_hFile, SETXON);
        }

       
public bool SetXOff() {
           
return EscapeCommFunction(m_hFile, SETXOFF);
        }

       
private unsafe void AsyncFSCallback(uint errorCode, uint numBytes, NativeOverlapped* pOverlapped) {
            SerialAsyncResult sar
= (SerialAsyncResult)Overlapped.Unpack(pOverlapped).AsyncResult;

            sar.m_nErrorCode
= errorCode;
            sar.m_nReadWritten
= (int)numBytes;
            sar.m_bCompleted
= true;

           
if (sar.Callback != null)
                sar.Callback.Invoke(sar);

            Overlapped.Free(pOverlapped);
        }

       
#endregion Methods

       
#region Constants

       
private const uint PURGE_TXABORT = 0x0001// Kill the pending/current writes to the comm port.
        private const uint PURGE_RXABORT = 0x0002// Kill the pending/current reads to the comm port.
        private const uint PURGE_TXCLEAR = 0x0004// Kill the transmit queue if there.
        private const uint PURGE_RXCLEAR = 0x0008// Kill the typeahead buffer if there.

       
private const uint SETXOFF  = 1;    // Simulate XOFF received
        private const uint SETXON   = 2;    // Simulate XON received
        private const uint SETRTS    = 3;    // Set RTS high
        private const uint CLRRTS    = 4;    // Set RTS low
        private const uint SETDTR    = 5;    // Set DTR high
        private const uint CLRDTR    = 6;    // Set DTR low
        private const uint SETBREAK    = 8;    // Set the device break line.
        private const uint CLRBREAK    = 9;    // Clear the device break line.

       
private const uint MS_CTS_ON  = 0x0010;
       
private const uint MS_DSR_ON  = 0x0020;
       
private const uint MS_RING_ON = 0x0040;
       
private const uint MS_RLSD_ON = 0x0080;

       
private const uint FILE_FLAG_OVERLAPPED = 0x40000000;

       
private const uint OPEN_EXISTING = 3;

       
private const int  INVALID_HANDLE_VALUE = -1;

       
private const uint GENERIC_READ = 0x80000000;
       
private const uint GENERIC_WRITE = 0x40000000;

       
private const uint ERROR_SUCCESS = 0;
       
private const uint ERROR_OPERATION_ABORTED = 995;
       
private const uint ERROR_IO_PENDING = 997;

       
#endregion Constants

       
#region Enums

       
public enum Parity {None, Odd, Even, Mark, Space};
       
public enum StopBits {One, OneAndHalf, Two};
       
public enum FlowControl {None, XOnXOff, Hardware};

       
#endregion Enums

       
#region Classes

        [StructLayout(LayoutKind.Sequential)]
       
public struct DCB {

           
#region Attributes

           
public int DCBlength;
           
public uint BaudRate;
           
public uint Flags;
           
public ushort wReserved;
           
public ushort XonLim;
           
public ushort XoffLim;
           
public byte ByteSize;
           
public byte Parity;
           
public byte StopBits;
           
public sbyte XonChar;
           
public sbyte XoffChar;
           
public sbyte ErrorChar;
           
public sbyte EofChar;
           
public sbyte EvtChar;
           
public ushort wReserved1;

           
#endregion Attributes

           
#region Properties

           
public uint fBinary { get { return Flags&0x0001; }
                                 
set { Flags = Flags & ~1U | value; } }
           
public uint fParity { get { return (Flags>>1)&1; }
                                 
set { Flags = Flags & ~(1U >2)&1; }
                                 
set { Flags = Flags & ~(1U >3)&1; }
                                 
set { Flags = Flags & ~(1U >4)&3; }
                                 
set { Flags = Flags & ~(3U >6)&1; }
                                 
set { Flags = Flags & ~(1U >7)&1; }
                                 
set { Flags = Flags & ~(1U >8)&1; }
                                 
set { Flags = Flags & ~(1U >9)&1; }
                                 
set { Flags = Flags & ~(1U >10)&1; }
                                 
set { Flags = Flags & ~(1U >11)&1; }
                                 
set { Flags = Flags & ~(1U >12)&3; }
                                 
set { Flags = Flags & ~(3U >14)&1; }
                                 
set { Flags = Flags & ~(1U << 14) | (value << 14); } }

           
#endregion Properties

           
#region Methods

           
public override string ToString() {
               
return "DCBlength: " + DCBlength + "/r/n" +
                   
"BaudRate: " + BaudRate + "/r/n" +
                   
"fBinary: " + fBinary + "/r/n" +
                   
"fParity: " + fParity + "/r/n" +
                   
"fOutxCtsFlow: " + fOutxCtsFlow + "/r/n" +
                   
"fOutxDsrFlow: " + fOutxDsrFlow + "/r/n" +
                   
"fDtrControl: " + fDtrControl + "/r/n" +
                   
"fDsrSensitivity: " + fDsrSensitivity + "/r/n" +
                   
"fTXContinueOnXoff: " + fTXContinueOnXoff + "/r/n" +
                   
"fOutX: " + fOutX + "/r/n" +
                   
"fInX: " + fInX + "/r/n" +
                   
"fErrorChar: " + fErrorChar + "/r/n" +
                   
"fNull: " + fNull + "/r/n" +
                   
"fRtsControl: " + fRtsControl + "/r/n" +
                   
"fAbortOnError: " + fAbortOnError + "/r/n" +
                   
"XonLim: " + XonLim + "/r/n" +
                   
"XoffLim: " + XoffLim + "/r/n" +
                   
"ByteSize: " + ByteSize + "/r/n" +
                   
"Parity: " + Parity + "/r/n" +
                   
"StopBits: " + StopBits + "/r/n" +
                   
"XonChar: " + XonChar + "/r/n" +
                   
"XoffChar: " + XoffChar + "/r/n" +
                   
"EofChar: " + EofChar + "/r/n" +
                   
"EvtChar: " + EvtChar + "/r/n";
            }

           
#endregion Methods
        }

       
private class SerialAsyncResult : IAsyncResult, IDisposable {

           
#region Attributes

           
internal bool m_bEndOperationCalled = false;
           
internal bool m_bIsRead;
           
internal int m_nReadWritten = 0;
           
internal bool m_bCompleted = false;
           
internal bool m_bCompletedSynchronously = false;
           
internal uint m_nErrorCode = ERROR_SUCCESS;

           
private object m_AsyncObject;
           
private object m_StateObject;
           
private ManualResetEvent m_WaitHandle = new ManualResetEvent(false);
           
private AsyncCallback m_Callback;
           
private GCHandle m_gchBuffer;

           
#endregion Attributes

           
#region Properties

           
internal bool EndOperationCalled { get { return m_bEndOperationCalled; } }

           
public bool IsCompleted { get { return m_bCompleted; } }

           
public bool CompletedSynchronously { get { return m_bCompletedSynchronously; } }

           
public object AsyncObject { get { return m_AsyncObject; } }

           
public object AsyncState { get { return m_StateObject; } }

           
public WaitHandle AsyncWaitHandle { get { return m_WaitHandle; } }
           
internal ManualResetEvent WaitHandle { get { return m_WaitHandle; } }

           
public AsyncCallback Callback { get { return m_Callback; } }

           
#endregion Properties

           
#region Constructors

           
public SerialAsyncResult(object asyncObject,
               
object stateObject,
                AsyncCallback callback,
               
bool bIsRead,
                GCHandle gchBuffer) {

                m_AsyncObject
= asyncObject;
                m_StateObject
= stateObject;
                m_Callback
= callback;
                m_bIsRead
= bIsRead;
                m_gchBuffer
= gchBuffer;
            }

           
#endregion Constructors

           
#region Methods

           
public void Dispose() {
                m_WaitHandle.Close();
                m_gchBuffer.Free();
            }

           
#endregion Methods
        }

       
#endregion Classes

       
#region Imports

        [DllImport(
"kernel32.dll", EntryPoint="CreateFileW",  SetLastError=true,
            CharSet
=CharSet.Unicode, ExactSpelling=true)]
       
static extern IntPtr CreateFile(string filename, uint access, uint sharemode, uint security_attributes, uint creation, uint flags, uint template);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern bool CloseHandle(IntPtr handle);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern unsafe bool ReadFile(IntPtr hFile, byte* lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, NativeOverlapped* lpOverlapped);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern unsafe bool WriteFile(IntPtr hFile, byte* lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, NativeOverlapped* lpOverlapped);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern bool SetCommTimeouts(IntPtr hFile, ref SerialTimeouts lpCommTimeouts);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern bool SetCommState(IntPtr hFile, ref DCB lpDCB);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern bool GetCommState(IntPtr hFile, ref DCB lpDCB);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern bool BuildCommDCB(string def, ref DCB lpDCB);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern int GetLastError();

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern bool FlushFileBuffers(IntPtr hFile);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern bool PurgeComm(IntPtr hFile, uint dwFlags);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern bool EscapeCommFunction(IntPtr hFile, uint dwFunc);

        [DllImport(
"kernel32.dll", SetLastError=true)]
       
static extern bool GetCommModemStatus(IntPtr hFile, out uint modemStat);

       
#endregion Imports
    }

    [StructLayout(LayoutKind.Sequential)]
   
public struct SerialTimeouts {

       
#region Attributes

       
public int ReadIntervalTimeout;
       
public int ReadTotalTimeoutMultiplier;
       
public int ReadTotalTimeoutConstant;
       
public int WriteTotalTimeoutMultiplier;
       
public int WriteTotalTimeoutConstant;

       
#endregion Attributes

       
#region Constructors

       
public SerialTimeouts(int r1, int r2, int r3, int w1, int w2) {
            ReadIntervalTimeout
= r1;
            ReadTotalTimeoutMultiplier
= r2;
            ReadTotalTimeoutConstant
= r3;
            WriteTotalTimeoutMultiplier
= w1;
            WriteTotalTimeoutConstant
= w2;
        }

       
#endregion Constructors

       
#region Methods

       
public override string ToString() {
           
return "ReadIntervalTimeout: " + ReadIntervalTimeout + "/r/n" +
                  
"ReadTotalTimeoutMultiplier: " + ReadTotalTimeoutMultiplier + "/r/n" +
                  
"ReadTotalTimeoutConstant: " + ReadTotalTimeoutConstant + "/r/n" +
                  
"WriteTotalTimeoutMultiplier: " + WriteTotalTimeoutMultiplier + "/r/n" +
                  
"WriteTotalTimeoutConstant: " + WriteTotalTimeoutConstant + "/r/n";
        }

       
#endregion Methods
    }
}



using System;
using System.IO;
using System.Threading;

using LoMaN.IO;

namespace SerialStreamReader {

   
class App {

       
// The main serial stream
        static SerialStream ss;

        [STAThread]
       
static void Main(string[] args) {

           
// Create a serial port
            ss = new SerialStream();
           
try {
                ss.Open(
"COM4"); //我对猫进行了调用
            }
           
catch (Exception e) {
                Console.WriteLine(
"Error: " + e.Message);
               
return;
            }

           
// Set port settings
            ss.SetPortSettings(9600);

           
// Set timeout so read ends after 20ms of silence after a response
            ss.SetTimeouts(20, 0, 0, 0, 0);

           
// Create the StreamWriter used to send commands
            StreamWriter sw = new StreamWriter(ss, System.Text.Encoding.ASCII);

           
// Create the Thread used to read responses
            Thread responseReaderThread = new Thread(new ThreadStart(ReadResponseThread));
            responseReaderThread.Start();

           
// Read all returned lines
            for (;;) {
               
// Read command from console
                string command = Console.ReadLine();

               
// Check for exit command
                if (command.Trim().ToLower() == "exit") {
                    responseReaderThread.Abort();
                   
break;
                }

               
// Write command to modem
                sw.WriteLine(command);
                sw.Flush();
            }
        }

       
// Main loop for reading responses
        static void ReadResponseThread() {
            StreamReader sr
= new StreamReader(ss, System.Text.Encoding.ASCII);
           
try {
               
for (;;) {
                   
// Read response from modem
                    string response = sr.ReadLine();
                    Console.WriteLine(
"Response: " + response);
                }
            }
           
catch (ThreadAbortException) {
            }
        }
    }
}


程序运行后,你可以对设备进行指令操作。(具体设备接受的指令)
 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值