串口类

串口类

2023年6月22日发(作者:)

深入浅出VC++串口编程之第三方类

作者:宋宝华出处:天极开发责任编辑: 方舟 [ 2006-02-21 11:16 ]

与通过WIN32 API进行串口访问相比,通过MScomm这个Activex控件进行串口访问要来的方便许多

串口类

从本系列文章连载三、四可以看出,与通过WIN32 API进行串口访问相比,通过MScomm这个Activex控件进行串口访问要来的方便许多,它基本上可以向用户屏蔽多线程的细节,以事件(发出OnComm消息)方式实现串口的异步访问。

尽管如此,MScomm控件的使用仍有诸多不便,譬如其发送和接收数据都要进行VARIANT类型对象与字符串的转化等。因此,国内外许多优秀的程序员 自己编写了一些串口类,使用这些类,我们将可以更方便的操作串口。在笔者的《深入浅出Win32多线程程序设计之综合实例》(网址:http: //)一文中,曾向读者展示了由Remon Spekreijse编写的CSerialPort串口类,而本文将向您展示由程序员llbird编写的cnComm(中国串口?)串口类。

llbird是一位优秀的程序员,他的代码风格简洁而紧凑,类的声明和实现都被定义在一个头文件中,使用这个类的朋友只需要在工程中包含这一头文件即可:

/*

Comm Base Library(WIN98/NT/2000) ver 1.1

Compile by: BC++ 5; C++ BUILDER 4, 5, 6, X; VC++ 5, 6; ; GCC;

copyright(c) 2004.5 - 2005.8 llbird wushaojian@

*/

#ifndef _CN_COMM_H_

#define _CN_COMM_H_

#pragma warning(disable: 4530)

#pragma warning(disable: 4786)

#pragma warning(disable: 4800)

#include

#include

#include

//送到窗口的消息 WPARAM 端口号

#define ON_COM_RECEIVE WM_USER + 618

#define ON_COM_CTS WM_USER + 619 //LPARAM 1 valid

#define ON_COM_DSR WM_USER + 621 //LPARAM 1 valid

#define ON_COM_RING WM_USER + 623 #define ON_COM_RLSD WM_USER + 624

#define ON_COM_BREAK WM_USER + 625

#define ON_COM_TXEMPTY WM_USER + 626

#define ON_COM_ERROR WM_USER + 627 //LPARAM save Error ID

#define DEFAULT_COM_MASK_EVENT EV_RXCHAR | EV_ERR | EV_CTS | EV_DSR |

EV_BREAK | EV_TXEMPTY | EV_RING | EV_RLSD

class cnComm

{

public:

//------------------------------Construction-----------------------------------

//第1个参数为是否在打开串口时启动监视线程, 第2个参数为IO方式 阻塞方式(0)/ 异步重叠方式(默认)

cnComm(bool fAutoBeginThread = true, DWORD dwIOMode =

FILE_FLAG_OVERLAPPED): _dwIOMode(dwIOMode),

_fAutoBeginThread(fAutoBeginThread)

{

Init();

}

virtual ~cnComm()

{

Close();

UnInit();

}

//----------------------------------Attributes----------------------------------

//判断串口是否打开

inline bool IsOpen()

{

return _hCommHandle != INVALID_HANDLE_VALUE;

}

//判断串口是否打开

operator bool()

{

return _hCommHandle != INVALID_HANDLE_VALUE;

}

//获得串口句炳

inline HANDLE GetHandle() {

return _hCommHandle;

}

//获得串口句炳

operator HANDLE()

{

return _hCommHandle;

}

//获得串口参数 DCB

DCB *GetState()

{

return IsOpen() && ::GetCommState(_hCommHandle, &_DCB) == TRUE ?

&_DCB: NULL;

}

//设置串口参数 DCB

bool SetState(DCB *pdcb = NULL)

{

return IsOpen() ? ::SetCommState(_hCommHandle, pdcb == NULL ?

&_DCB:pdcb) == TRUE: false;

}

//设置串口参数:波特率,停止位,等 支持设置字符串 "9600, 8, n, 1"

bool SetState(char *szSetStr)

{

if (IsOpen())

{

if (::GetCommState(_hCommHandle, &_DCB) != TRUE)

return false;

if (::BuildCommDCB(szSetStr, &_DCB) != TRUE)

return false;

return ::SetCommState(_hCommHandle, &_DCB) == TRUE;

}

return false;

}

//设置串口参数:波特率,停止位,等

bool SetState(DWORD dwBaudRate, DWORD dwByteSize = 8, DWORD dwParity

=

NOPARITY, DWORD dwStopBits = ONESTOPBIT)

{

if (IsOpen())

{

if (::GetCommState(_hCommHandle, &_DCB) != TRUE)

return false;

_te = dwBaudRate;

_ze = (unsigned char)dwByteSize; _ = (unsigned char)dwParity;

_ts = (unsigned char)dwStopBits;

return ::SetCommState(_hCommHandle, &_DCB) == TRUE;

}

return false;

}

//获得超时结构

LPCOMMTIMEOUTS GetTimeouts(void)

{

return IsOpen() && ::GetCommTimeouts(_hCommHandle, &_CO) == TRUE ?

&_CO: NULL;

}

//设置超时

bool SetTimeouts(LPCOMMTIMEOUTS lpCO)

{

return IsOpen() ? ::SetCommTimeouts(_hCommHandle, lpCO) ==

TRUE:false;

}

//设置串口的I/O缓冲区大小

bool SetBufferSize(DWORD dwInputSize, DWORD dwOutputSize)

{

return IsOpen() ? ::SetupComm(_hCommHandle, dwInputSize,

dwOutputSize)== TRUE: false;

}

//关联消息的窗口句柄

inline void SetWnd(HWND hWnd)

{

assert(::IsWindow(hWnd));

_hNotifyWnd = hWnd;

}

//设定发送通知, 接受字符最小值

inline void SetNotifyNum(DWORD dwNum)

{

_dwNotifyNum = dwNum;

}

//线程是否运行

inline bool IsThreadRunning()

{

return _hThreadHandle != NULL;

}

//获得线程句柄

inline HANDLE GetThread()

{

return _hThreadHandle; }

//设置要监视的事件, 打开前设置有效

void SetMaskEvent(DWORD dwEvent = DEFAULT_COM_MASK_EVENT)

{

_dwMaskEvent = dwEvent;

}

//获得读缓冲区的字符数

int GetInputSize()

{

COMSTAT Stat;

DWORD dwError;

return ::ClearCommError(_hCommHandle, &dwError, &Stat) == TRUE ?

e : (DWORD) - 1L;

}

//----------------------------------Operations----------------------------------

//打开串口 缺省 9600, 8, n, 1

bool Open(DWORD dwPort)

{

return Open(dwPort, 9600);

}

//打开串口 缺省 baud_rate, 8, n, 1

bool Open(DWORD dwPort, DWORD dwBaudRate)

{

if (dwPort < 1 || dwPort > 1024)

return false;

BindCommPort(dwPort);

if (!OpenCommPort())

return false;

if (!SetupPort())

return false;

return SetState(dwBaudRate);

}

//打开串口, 使用类似"9600, 8, n, 1"的设置字符串设置串口

bool Open(DWORD dwPort, char *szSetStr)

{

if (dwPort < 1 || dwPort > 1024)

return false; BindCommPort(dwPort);

if (!OpenCommPort())

return false;

if (!SetupPort())

return false;

return SetState(szSetStr);

}

//读取串口 dwBufferLength个字符到 Buffer 返回实际读到的字符数 可读任意数据

DWORD Read(LPVOID Buffer, DWORD dwBufferLength, DWORD dwWaitTime = 10)

{

if (!IsOpen())

return 0;

COMSTAT Stat;

DWORD dwError;

if (::ClearCommError(_hCommHandle, &dwError, &Stat) && dwError > 0)

{

::PurgeComm(_hCommHandle,PURGE_RXABORT | PURGE_RXCLEAR);

return 0;

}

if (!e)

// 缓冲区无数据

return 0;

unsigned long uReadLength = 0;

dwBufferLength = dwBufferLength > e ?

e :dwBufferLength;

if (!::ReadFile(_hCommHandle, Buffer, dwBufferLength,

&uReadLength,&_ReadOverlapped))

{

if (::GetLastError() == ERROR_IO_PENDING)

{

WaitForSingleObject(_, dwWaitTime);

// 结束异步I/O

if (!::GetOverlappedResult(_hCommHandle,

&_ReadOverlapped,&uReadLength, false)) {

if (::GetLastError() != ERROR_IO_INCOMPLETE)

uReadLength = 0;

}

}

else

uReadLength = 0;

}

return uReadLength;

}

//读取串口 dwBufferLength - 1 个字符到 szBuffer 返回ANSI C 模式字符串指针 适合一般字符通讯

char *ReadString(char *szBuffer, DWORD dwBufferLength, DWORD

dwWaitTime =20)

{

unsigned long uReadLength = Read(szBuffer, dwBufferLength -

1,dwWaitTime);

szBuffer[uReadLength] = '0';

return szBuffer;

}

//写串口 可写任意数据 "abcd" or "x0x1x2"

DWORD Write(LPVOID Buffer, DWORD dwBufferLength)

{

if (!IsOpen())

return 0;

DWORD dwError;

if (::ClearCommError(_hCommHandle, &dwError, NULL) && dwError > 0)

::PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_TXCLEAR);

unsigned long uWriteLength = 0;

if (!::WriteFile(_hCommHandle, Buffer, dwBufferLength,

&uWriteLength,&_WriteOverlapped))

if (::GetLastError() != ERROR_IO_PENDING)

uWriteLength = 0;

return uWriteLength;

}

//写串口 写ANSI C 模式字符串指针

DWORD Write(const char *szBuffer) {

assert(szBuffer);

return Write((void*)szBuffer, strlen(szBuffer));

}

//读串口 同步应用

DWORD ReadSync(LPVOID Buffer, DWORD dwBufferLength)

{

if (!IsOpen())

return 0;

DWORD dwError;

if (::ClearCommError(_hCommHandle, &dwError, NULL) && dwError > 0)

{

::PurgeComm(_hCommHandle,PURGE_RXABORT | PURGE_RXCLEAR);

return 0;

}

DWORD uReadLength = 0;

::ReadFile(_hCommHandle, Buffer, dwBufferLength, &uReadLength,

NULL);

return uReadLength;

}

//写串口 同步应用

DWORD WriteSync(LPVOID Buffer, DWORD dwBufferLength)

{

if (!IsOpen())

return 0;

DWORD dwError;

if (::ClearCommError(_hCommHandle, &dwError, NULL) && dwError > 0)

::PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_TXCLEAR);

unsigned long uWriteLength = 0;

::WriteFile(_hCommHandle, Buffer, dwBufferLength, &uWriteLength,

NULL);

return uWriteLength;

}

//写串口 szBuffer 可以输出格式字符串 包含缓冲区长度

DWORD Write(char *szBuffer, DWORD dwBufferLength, char

*szFormat, ...)

{

if (!IsOpen())

return 0; va_list va;

va_start(va, szFormat);

_vsnprintf(szBuffer, dwBufferLength, szFormat, va);

va_end(va);

return Write(szBuffer);

}

//写串口 szBuffer 可以输出格式字符串 不检查缓冲区长度 小心溢出

DWORD Write(char *szBuffer, char *szFormat, ...)

{

if (!IsOpen())

return 0;

va_list va;

va_start(va, szFormat);

vsprintf(szBuffer, szFormat, va);

va_end(va);

return Write(szBuffer);

}

//关闭串口 同时也关闭关联线程

virtual void Close()

{

if (IsOpen())

{

PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_TXCLEAR);

EndThread();

::CloseHandle(_hCommHandle);

_hCommHandle = INVALID_HANDLE_VALUE;

}

}

//DTR 电平控制

bool SetDTR(bool OnOrOff)

{

return IsOpen() ? EscapeCommFunction(_hCommHandle, OnOrOff ?

SETDTR :CLRDTR): false;

}

//RTS 电平控制

bool SetRTS(bool OnOrOff)

{

return IsOpen() ? EscapeCommFunction(_hCommHandle, OnOrOff ? SETRTS :CLRRTS): false;

}

//

bool SetBreak(bool OnOrOff)

{

return IsOpen() ? EscapeCommFunction(_hCommHandle, OnOrOff ?

SETBREAK: CLRBREAK): false;

}

//辅助线程控制 建监视线程

bool BeginThread()

{

if (!IsThreadRunning())

{

_fRunFlag = true;

_hThreadHandle = NULL;

DWORD id;

_hThreadHandle = ::CreateThread(NULL, 0, CommThreadProc, this,

0,&id);

return (_hThreadHandle != NULL);

}

return false;

}

//暂停监视线程

inline bool SuspendThread()

{

return

IsThreadRunning() ? ::SuspendThread(_hThreadHandle) !=0xFFFFFFFF:

false;

}

//恢复监视线程

inline bool ResumeThread()

{

return

IsThreadRunning() ? ::ResumeThread(_hThreadHandle) !=0xFFFFFFFF: false;

}

//终止线程

bool EndThread(DWORD dwWaitTime = 100)

{

if (IsThreadRunning())

{

_fRunFlag = false; ::SetCommMask(_hCommHandle, 0);

::SetEvent(_);

if (::WaitForSingleObject(_hThreadHandle,

dwWaitTime) !=WAIT_OBJECT_0)

if (!::TerminateThread(_hThreadHandle, 0))

return false;

::CloseHandle(_hThreadHandle);

::ResetEvent(_);

_hThreadHandle = NULL;

return true;

}

return false;

}

protected:

volatile DWORD _dwPort; //串口号

volatile HANDLE _hCommHandle; //串口句柄

char _szCommStr[20]; //保存COM1类似的字符串

DCB _DCB; //波特率,停止位,等

COMMTIMEOUTS _CO; //超时结构

DWORD _dwIOMode; // 0 同步 默认 FILE_FLAG_OVERLAPPED重叠I/O异步

OVERLAPPED _ReadOverlapped, _WriteOverlapped; // 重叠I/O

volatile HANDLE _hThreadHandle; //辅助线程

volatile HWND _hNotifyWnd; // 通知窗口

volatile DWORD _dwNotifyNum; //接受多少字节(>=_dwNotifyNum)发送通知消息

volatile DWORD _dwMaskEvent; //监视的事件

volatile bool _fRunFlag; //线程运行循环标志

bool _fAutoBeginThread; //Open() 自动 BeginThread();

OVERLAPPED _WaitOverlapped; //WaitCommEvent use

//初始化

void Init()

{

memset(_szCommStr, 0, 20);

memset(&_DCB, 0, sizeof(_DCB));

_gth = sizeof(_DCB);

_hCommHandle = INVALID_HANDLE_VALUE;

memset(&_ReadOverlapped, 0, sizeof(_ReadOverlapped));

memset(&_WriteOverlapped, 0, sizeof(_WriteOverlapped));

_ = ::CreateEvent(NULL, true, false, NULL);

assert(_ != INVALID_HANDLE_VALUE);

_ = ::CreateEvent(NULL, true, false, NULL);

assert(_ != INVALID_HANDLE_VALUE);

_hNotifyWnd = NULL;

_dwNotifyNum = 0;

_dwMaskEvent = DEFAULT_COM_MASK_EVENT;

_hThreadHandle = NULL;

memset(&_WaitOverlapped, 0, sizeof(_WaitOverlapped));

_ = ::CreateEvent(NULL, true, false, NULL);

assert(_ != INVALID_HANDLE_VALUE);

}

//析构

void UnInit()

{

if (_ != INVALID_HANDLE_VALUE)

CloseHandle(_);

if (_ != INVALID_HANDLE_VALUE)

CloseHandle(_);

if (_ != INVALID_HANDLE_VALUE)

CloseHandle(_);

}

//绑定串口

void BindCommPort(DWORD dwPort)

{

assert(dwPort >= 1 && dwPort <= 1024);

char p[5];

_dwPort = dwPort;

strcpy(_szCommStr, ".COM");

ltoa(_dwPort, p, 10);

strcat(_szCommStr, p);

}

//打开串口 virtual bool OpenCommPort()

{

if (IsOpen())

Close();

_hCommHandle = ::CreateFile(_szCommStr, GENERIC_READ |

GENERIC_WRITE, 0, NULL,

OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | _dwIOMode,NULL);

if (_fAutoBeginThread)

{

if (IsOpen() && BeginThread())

return true;

else

{

Close(); //创建线程失败

return false;

}

}

return IsOpen();

}

//设置串口

virtual bool SetupPort()

{

if (!IsOpen())

return false;

if (!::SetupComm(_hCommHandle, 4096, 4096))

return false;

if (!::GetCommTimeouts(_hCommHandle, &_CO))

return false;

_tervalTimeout = 0;

_talTimeoutMultiplier = 1;

_talTimeoutConstant = 1000;

_otalTimeoutMultiplier = 1;

_otalTimeoutConstant = 1000;

if (!::SetCommTimeouts(_hCommHandle, &_CO))

return false;

if (!::PurgeComm(_hCommHandle, PURGE_TXABORT | PURGE_RXABORT

|PURGE_TXCLEAR | PURGE_RXCLEAR))

return false;

return true; }

//---------------------------------------threads

callback-----------------------------------

//线程收到消息自动调用, 如窗口句柄有效, 送出消息, 包含串口编号,

均为虚函数可以在基层类中扩展

virtual void OnReceive() //EV_RXCHAR

{

if (::IsWindow(_hNotifyWnd))

::PostMessage(_hNotifyWnd, ON_COM_RECEIVE, WPARAM(_dwPort),

LPARAM (0));

}

virtual void OnDSR()

{

if (::IsWindow(_hNotifyWnd))

{

DWORD Status;

if (GetCommModemStatus(_hCommHandle, &Status))

::PostMessage(_hNotifyWnd, ON_COM_DSR,

WPARAM(_dwPort),LPARAM((Status &MS_DSR_ON) ? 1 : 0));

}

}

virtual void OnCTS()

{

if (::IsWindow(_hNotifyWnd))

{

DWORD Status;

if (GetCommModemStatus(_hCommHandle, &Status))

::PostMessage(_hNotifyWnd, ON_COM_CTS, WPARAM(_dwPort),

LPARAM( (Status &MS_CTS_ON) ? 1 : 0));

}

}

virtual void OnBreak()

{

if (::IsWindow(_hNotifyWnd))

{

::PostMessage(_hNotifyWnd, ON_COM_BREAK, WPARAM(_dwPort),

LPARAM(0));

}

}

virtual void OnTXEmpty()

{

if (::IsWindow(_hNotifyWnd))

::PostMessage(_hNotifyWnd, ON_COM_TXEMPTY, WPARAM(_dwPort),

LPARAM (0));

}

virtual void OnError()

{

DWORD dwError;

::ClearCommError(_hCommHandle, &dwError, NULL);

if (::IsWindow(_hNotifyWnd))

::PostMessage(_hNotifyWnd, ON_COM_ERROR, WPARAM(_dwPort),

LPARAM (dwError));

}

virtual void OnRing()

{

if (::IsWindow(_hNotifyWnd))

::PostMessage(_hNotifyWnd, ON_COM_RING, WPARAM(_dwPort),

LPARAM(0));

}

virtual void OnRLSD()

{

if (::IsWindow(_hNotifyWnd))

::PostMessage(_hNotifyWnd, ON_COM_RLSD, WPARAM(_dwPort),

LPARAM(0));

}

virtual DWORD ThreadFunc()

{

if (!::SetCommMask(_hCommHandle, _dwMaskEvent))

{

char szBuffer[256];

_snprintf(szBuffer, 255,

"%s(%d) : COM%d Call WINAPI SetCommMask(%x, %x) Fail, thread work invalid!

GetLastError() = %d;", __FILE__, __LINE__, _dwPort, _hCommHandle,

_dwMaskEvent, GetLastError());

MessageBox(NULL, szBuffer, "Class cnComm", MB_OK);

return 1;

}

COMSTAT Stat; DWORD dwError;

for (DWORD dwLength, dwMask = 0; _fRunFlag && IsOpen(); dwMask =

0)

{

if (!::WaitCommEvent(_hCommHandle, &dwMask, &_WaitOverlapped))

{

if (::GetLastError() == ERROR_IO_PENDING)

// asynchronous

::GetOverlappedResult(_hCommHandle,

&_WaitOverlapped,&dwLength, TRUE);

else

continue;

}

if (dwMask == 0)

continue;

switch (dwMask)

{

case EV_RXCHAR:

::ClearCommError(_hCommHandle, &dwError, &Stat);

if (e >= _dwNotifyNum)

OnReceive();

break;

case EV_TXEMPTY:

OnTXEmpty();

break;

case EV_CTS:

OnCTS();

break;

case EV_DSR:

OnDSR();

break;

case EV_RING:

OnRing();

break;

case EV_RLSD:

OnRLSD(); break;

case EV_BREAK:

OnBreak();

break;

case EV_ERR:

OnError();

break;

} //case

} //for

return 0;

}

private:

//the function protected

cnComm(const cnComm &);

cnComm &operator = (const cnComm &);

//base function for thread

static DWORD WINAPI CommThreadProc(LPVOID lpPara)

{

return ((cnComm*)lpPara)->ThreadFunc();

}

};

#endif //_CN_COMM_H_

2.实例

程序的功能和界面(如下图)都与本文连载三、四中《基于WIN32 API的串口编程》和《基于控件的串口编程》相同,不同的只是本节的串口通信要以llbird定义的cnComm类来实现。

我们需要为串口的接收事件定义一个用户消息ON_COM_RECEIVE,因此对话框的消息映射为:

BEGIN_MESSAGE_MAP(CSerialPortClassDlg, CDialog)

//{{AFX_MSG_MAP(CSerialPortClassDlg)

ON_WM_SYSCOMMAND() ON_WM_PAINT()

ON_WM_QUERYDRAGICON()

ON_BN_CLICKED(IDC_CLEAR_BUTTON, OnClearButton)

ON_BN_CLICKED(IDC_SEND_BUTTON, OnSendButton)

ON_MESSAGE(ON_COM_RECEIVE,OnCommRecv)

//}}AFX_MSG_MAP

END_MESSAGE_MAP()

同时,我们需要在对话框类的头文件中定义cnComm类的成员变量com和接收数据消息处理函数OnCommRecv:

cnComm com;

afx_msg void OnCommRecv(WPARAM wParam, LPARAM lParam);

在对话框初始化时调用打开串口:

BOOL CSerialPortClassDlg::OnInitDialog()

{

CDialog::OnInitDialog();

// Add "" menu item to system menu.

// IDM_ABOUTBOX must be in the system command range.

ASSERT((IDM_ABOUTBOX &0xFFF0) == IDM_ABOUTBOX);

ASSERT(IDM_ABOUTBOX < 0xF000);

CMenu *pSysMenu = GetSystemMenu(FALSE);

if (pSysMenu != NULL)

{

CString strAboutMenu;

ring(IDS_ABOUTBOX);

if (!y())

{

pSysMenu->AppendMenu(MF_SEPARATOR);

pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX,

strAboutMenu);

}

}

// Set the icon for this dialog. The framework does this

automatically

// when the application's main window is not a dialog

SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon

// TODO: Add extra initialization here

(1); //打开串口1并使用默认设置

(AfxGetMainWnd()->m_hWnd); //设置消息处理窗口

return TRUE; // return TRUE unless you set the focus to a control

}

发送字符串的过程很简单,只需要调用cnComm类的Write函数:

//"发送"按钮函数(完成数据的发送功能)

void CSerialPortClassDlg::OnSendButton()

{

// TODO: Add your control notification handler code here

UpdateData(true);

(m_send); //发送字符串

}

接收字符串的过程也很简单,只需要调用cnComm类的ReadString函数:

void CSerialPortClassDlg::OnCommRecv(WPARAM wParam, LPARAM

lParam)

{

UpdateData(true);

//读取串口上的字符

char str[100];

ring(str, 100);

m_recv += str;

UpdateData(false);

}

读者朋友们这时一定会发出感慨:使用cnComm类后,进行串口数据收发的程序是多么简单啊!的确,串口的初始化、读写几乎都是用1~2条语句搞定的!

这就是我们要特别用一次连载来讲述使用第三方类来进行串口通信的原因。实际上,笔者在进行网络通信程序编程时,也不认为MS提供的CSocket类是最方便的选择,照样习惯使用第三方的网络通信类。它们的确有非常简洁明快的接口,这一点也是值得MS哥哥们学习的。

发布者:admin,转转请注明出处:http://www.yc00.com/news/1687426532a9195.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信