天天看點

openmv序列槽資料 序列槽助手_Qt實踐錄:序列槽調試助手

由于項目需要使用到序列槽調試及測試,為了練手,使用 Qt 編寫一個序列槽調試助手。本文按開發的過程進行簡單介紹,同時也涉及部分用到的子產品代碼。詳細代碼參考源碼倉庫。

# 工具特性

## 具體功能

- 具備序列槽收發功能。

- 序列槽參數設定。預設115200,N,8,1

- 接收區清空,接收區十六進制顯示,接收區時間戳。

- 發送區清空,十六進制發送,自動追加``,定時發送。

- 收發計數顯示及清零。

- 序列槽裝置自動檢測。運作前序列槽就緒則自動打開。運作中序列槽插入不會自動打開。運作過程中拔出裝置則自動關閉序列槽。

## 已知 Bug

接收區時間戳顯示不完善。

序列槽發送大量亂碼時,程式會崩潰。亂碼可能是真的亂碼,也可能是波特率錯誤設定。

## Qt 相關知識

- MainWindow設計。

- Qt序列槽類。

- 常用控件:按鈕、複選框、文本編輯框、控件貼圖。應用程式logo。

- Qt 檢測裝置熱插拔(Windows)。

運作結果如圖1所示:

openmv序列槽資料 序列槽助手_Qt實踐錄:序列槽調試助手

倉庫位址在此: https://github.com/latelee/QtSerialPort。

# 開發過程

## 工程相關

Qt 使用的序列槽類為`QSerialPort`,需要在工程檔案中添加對應的庫,如下:

```

QT += core gui serialport

```

logo圖示,注意是 ico 格式:

```

RC_ICONS = images/logo.ico

```

圖檔資源檔案:

```

RESOURCES +=

images.qrc

```

USB 裝置檢測依賴的庫:

```

win32: LIBS += -lSetupAPI -luser32

```

## 信号與槽

在 Qt Creator 中添加的控件,可點選控件右鍵,選擇“轉到槽...”,選擇适合的槽,點選“OK”,可自動添加槽函數聲明,并自動跳轉到槽函數實作代碼。系統自動添加的形式為`on__`,如打開序列槽的按鈕單擊事件槽函數為`on_btnOpen_clicked`。類似有`on_cbPortName_currentTextChanged`(序列槽裝置更改)、`on_ckRecvHex_stateChanged`(接收十六進制複選框變更),等等。

此機制及操作方式,可類比于 MFC 的界面設計和消息響應。實際上,筆者喜歡将槽函數稱為響應函數。

## 序列槽相關

序列槽類聲明:

```

#include

#include

QSerialPort serial;

```

枚舉本機序列槽裝置:

```

QSerialPortInfo::availablePorts()

```

序列槽參數設定:

```

serial.setPortName("com4"); // 序列槽名稱

serial.setBaudRate(115200); // 序列槽波特率

serial.setDataBits(QSerialPort::Data8); // 資料位

serial.setStopBits(QSerialPort::OneStop); // 停止位

serial.setParity(QSerialPort::NoParity); // 校驗位

serial.setFlowControl(QSerialPort::NoFlowControl); // 流控

```

注:Qt 似乎隻有無流控、軟體流控、硬體流控這三種,無法區分 RTS、DTR。

序列槽打開、關閉:

```

serial.open(QIODevice::ReadWrite);

serial.close();

```

序列槽資料發送:

```

QByteArray sendData;

serial.write(sendData);

```

序列槽資料接收:

```

// 序列槽資料到來時,會觸發QSerialPort::readyRead事件,添加相應的響應函數

QObject::connect(&serial, &QSerialPort::readyRead, this, &MainWindow::readyRead);

void MainWindow::readyRead()

{

QByteArray buffer = serial.readAll();

}

```

注意,序列槽資料類型為`QByteArray`。

## 自動檢測 USB

鑒于目前大部分場合使用的是 USB 序列槽線,是以添加對 USB 裝置的檢測。這裡檢測到 USB 裝置時,再使用`QSerialPortInfo::availablePorts()`檢測序列槽裝置。

```

#include

#include

#include

#include

#include

#include

static const GUID GUID_DEVINTERFACE_LIST[] =

{

// GUID_DEVINTERFACE_USB_DEVICE

{ 0xA5DCBF10, 0x6530, 0x11D2, { 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED } },

// GUID_DEVINTERFACE_DISK

{ 0x53f56307, 0xb6bf, 0x11d0, { 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b } },

// GUID_DEVINTERFACE_HID,

{ 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } },

// GUID_NDIS_LAN_CLASS

{ 0xad498944, 0x762f, 0x11d0, { 0x8d, 0xcb, 0x00, 0xc0, 0x4f, 0xc3, 0x35, 0x8c } }

GUID_DEVINTERFACE_COMPORT

//{ 0x86e0d1e0, 0x8089, 0x11d0, { 0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73 } },

GUID_DEVINTERFACE_SERENUM_BUS_ENUMERATOR

//{ 0x4D36E978, 0xE325, 0x11CE, { 0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18 } },

GUID_DEVINTERFACE_PARALLEL

//{ 0x97F76EF0, 0xF883, 0x11D0, { 0xAF, 0x1F, 0x00, 0x00, 0xF8, 0x00, 0x84, 0x5C } },

GUID_DEVINTERFACE_PARCLASS

//{ 0x811FC6A5, 0xF728, 0x11D0, { 0xA5, 0x37, 0x00, 0x00, 0xF8, 0x75, 0x3E, 0xD1 } }

};

//注冊插拔事件

HDEVNOTIFY hDevNotify;

DEV_BROADCAST_DEVICEINTERFACE NotifacationFiler;

ZeroMemory(&NotifacationFiler,sizeof(DEV_BROADCAST_DEVICEINTERFACE));

NotifacationFiler.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);

NotifacationFiler.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;

for(int i=0;i

{

NotifacationFiler.dbcc_classguid = GUID_DEVINTERFACE_LIST[i];//GetCurrentUSBGUID();//m_usb->GetDriverGUID();

hDevNotify = RegisterDeviceNotification((HANDLE)this->winId(),&NotifacationFiler,DEVICE_NOTIFY_WINDOW_HANDLE);

if(!hDevNotify)

{

DWORD Err = GetLastError();

}

}

```

響應`nativeEvent`事件:

```

bool MainWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)

{

MSG* msg = reinterpret_cast(message);

int msgType = msg->message;

if(msgType==WM_DEVICECHANGE) // 裝置插入事件

{

PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)msg->lParam;

switch (msg->wParam) {

case DBT_DEVICEARRIVAL:

if(lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)

{

//PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb;

//QString strname = QString::fromWCharArray(pDevInf->dbcc_name,pDevInf->dbcc_size);

//qDebug() << "arrive" + strname;

printDebugInfo("USB device arrive");

emit sig_deviceChanged(1);

}

break;

case DBT_DEVICEREMOVECOMPLETE: // 裝置移除事件

if(lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE)

{

//PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)lpdb;

//QString strname = QString::fromWCharArray(pDevInf->dbcc_name,pDevInf->dbcc_size);

printDebugInfo("USB device removed");

emit sig_deviceChanged(0);

}

break;

}

}

return false;

}

```

這裡使用自定義的信号`sig_deviceChanged`,連接配接到函數`on_deviceChanged`:

```

QObject::connect(this, &MainWindow::sig_deviceChanged, this, &MainWindow::on_deviceChanged);

void MainWindow::on_deviceChanged(int flag)

{

if (flag == 1)

{

foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts())

{

if (-1 == ui->cbPortName->findText(info.portName()))

ui->cbPortName->addItem(info.portName());

}

}

else

{

serial.close();

ui->btnOpen->setText(tr("打開序列槽"));

ui->btnOpen->setIcon(QIcon(":images/notopened.ico"));

}

}

```

## 界面邏輯

### 界面設計

界面使用設計師進行設計,如圖2所示。

openmv序列槽資料 序列槽助手_Qt實踐錄:序列槽調試助手

### 界面基本設定

在`initMainWindow`函數中對視窗進行基本設定,如标題、視窗大小,最小化最大化按鈕,等等。

```

// 對主視窗的初始化

void MainWindow::initMainWindow()

{

setWindowTitle(tr("QtSerialPort"));

setMinimumSize(480, 320);

Qt::WindowFlags winFlags = Qt::Dialog;

winFlags = winFlags | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint;

setWindowFlags(winFlags);

}

```

### 狀态欄

狀态欄主要用于調試資訊、提示資訊的顯示,另外顯示收發計數(及清零)。

狀态欄相關函數和變量聲明如下:

```

void initStatusBar();

int m_rxCnt;

int m_txCnt;

// 狀态欄相關

QLabel* m_stsPinned;

QLabel* m_stsDebugInfo;

QLabel* m_stsRx;

QLabel* m_stsTx;

QLabel* m_stsResetCnt;

QLabel* m_stsCopyright;

QLabel* m_stsExit;

```

初始化函數如下:

```

void MainWindow::initStatusBar()

{

// 狀态欄分别為:

// 提示資訊(可多個)

// RX、TX

// 版本資訊(或版權聲明)

// 退出圖示

ui->statusbar->setMinimumHeight(22);

//ui->statusbar->setStyleSheet(QString("QStatusBar::item{border: 0px}")); // 不顯示邊框

ui->statusbar->setSizeGripEnabled(false);//去掉狀态欄右下角的三角

m_stsDebugInfo = new QLabel();

m_stsRx = new QLabel();

m_stsTx = new QLabel();

m_stsResetCnt = new QLabel();

m_stsCopyright = new QLabel();

m_stsExit = new QLabel();

m_stsDebugInfo->setMinimumWidth(this->width()/2);

ui->statusbar->addWidget(m_stsDebugInfo);

m_stsRx->setMinimumWidth(64);

ui->statusbar->addWidget(m_stsRx);

m_stsRx->setText("RX: 0");

m_stsTx->setMinimumWidth(64);

ui->statusbar->addWidget(m_stsTx);

m_stsTx->setText("TX: 0");

m_stsResetCnt->installEventFilter(this);

m_stsResetCnt->setFrameStyle(QFrame::Plain);

m_stsResetCnt->setText("複位計數");

m_stsResetCnt->setMinimumWidth(32);

ui->statusbar->addWidget(m_stsResetCnt);

printDebugInfo("歡迎使用");

// 版權資訊

m_stsCopyright->setFrameStyle(QFrame::NoFrame);

m_stsCopyright->setText(tr(" 技術首頁 "));

m_stsCopyright->setOpenExternalLinks(true);

ui->statusbar->addPermanentWidget(m_stsCopyright);

// 退出圖示

m_stsExit->installEventFilter(this); // 安裝事件過濾,以便擷取其單擊事件

m_stsExit->setToolTip("Exit App");

m_stsExit->setMinimumWidth(32);

// 貼圖

QPixmap exitIcon(":/images/exit.png");

m_stsExit->setPixmap(exitIcon);

ui->statusbar->addPermanentWidget(m_stsExit);

connect(this, &MainWindow::sig_exit, qApp, &QApplication::quit); // 直接關聯到全局的退出槽

}

```

為響應狀态欄控件事件,需要重載并實作函數`eventFilter`:

```

bool eventFilter(QObject *watched, QEvent *event);

bool MainWindow::eventFilter(QObject *watched, QEvent *event)

{

if (watched == m_stsExit) // 程式退出

{

//判斷事件

if(event->type() == QEvent::MouseButtonPress)

{

// TODO:直接退出還是發信号?

emit sig_exit();

return true; // 事件處理完畢

}

else

{

return false;

}

}

else if (watched == m_stsResetCnt)

{

if(event->type() == QEvent::MouseButtonPress)

{

m_stsRx->setText("RX: 0");

m_stsTx->setText("TX: 0");

m_rxCnt = m_txCnt = 0;

return true;

}

else

{

return false;

}

}

else

{

return QWidget::eventFilter(watched, event);

}

}

```

目前處理的事件有單擊退出圖示,以及點選清零計數QLabel。

### 控件貼圖

建立資源檔案 images.qrc,内容如下,再在 Qt Creator 中添加該檔案:

```

images/logo.jpg

images/notopened.ico

images/opened.ico

images/unpinned.bmp

images/pinned.bmp

images/exit.png

```

也可以在 Qt Creator 建立資源檔案,右鍵添加圖檔。效果一樣。

以打開序列槽按鈕為例,設定文字和圖示代碼如下:

```

ui->btnOpen->setText(tr("打開序列槽"));

ui->btnOpen->setIconSize(ui->btnOpen->rect().size());

ui->btnOpen->setIcon(QIcon(":images/notopened.ico"));

```

注:資源檔案 qrc 的字首為`/`,images為工程下的目錄,使用 QIcon的參數形式為`:images/xxx`。

### QComboBox

序列槽參數使用 QComboBox 控件,為友善添加資料,使用 QStringList 類,再利用 addItems 添加資料項。其初始化如下:

```

QStringList list;

list.clear();

list << "2400" << "4800" << "9600" << "14400" <<

"19200" << "38400" << "43000" << "57600" << "76800" <<

"115200" << "230400" << "256000" << "460800" << "921600";

ui->cbBaudrate->addItems(list);

ui->cbBaudrate->setCurrentText(tr("115200"));

list.clear();

list << "5" << "6" << "7" << "8";

ui->cbDatabit->addItems(list);

ui->cbDatabit->setCurrentText(tr("8"));

list.clear();

list << "1" << "1.5" << "2";

ui->cbStopbit->addItems(list);

ui->cbStopbit->setCurrentText(tr("1"));

list.clear();

list << "none" << "odd" << "even";

ui->cbParity->addItems(list);

ui->cbParity->setCurrentText(tr("none"));

list.clear();

list << "off" << "hardware" << "software";

ui->cbFlow->addItems(list);

ui->cbFlow->setCurrentText(tr("off"));

```

### 序列槽參數自動更新

為序列槽打開時,可以實時更改參數,但序列槽裝置除外。響應 QComboBox 的`currentTextChanged`或`currentIndexChanged`事件。如下:

```

// 序列槽裝置直接用文本文字形式即可

void MainWindow::on_cbPortName_currentTextChanged(const QString &arg1)

{

serial.setPortName(arg1);

}

// 如停止位等,需要用索引轉換

void MainWindow::on_cbStopbit_currentIndexChanged(int index)

{

//qDebug()<< index;

//設定停止位

switch(index)

{

case 0: serial.setStopBits(QSerialPort::OneStop); break;

case 1: serial.setStopBits(QSerialPort::OneAndHalfStop); break;

case 2: serial.setStopBits(QSerialPort::TwoStop); break;

default: break;

}

}

```

### QCheckBox

複選框用于發送、顯示的特性設定。如十六進制發送、顯示,定時發送,等等。設計上使用标志變量,在進行發送、顯示時加以判斷。主要響應 QCheckBox 的 `stateChanged`函數。

```

void MainWindow::on_ckRecvHex_stateChanged(int arg1)

{

if (arg1 == Qt::Checked)

{

m_recvHex = 1;

}

else if (arg1 == Qt::Unchecked)

{

m_recvHex = 0;

}

}

```

### 定時器

重載`timerEvent`函數:

```

#include

void timerEvent(QTimerEvent *event);

```

函數實作:

```

void MainWindow::timerEvent(QTimerEvent *event)

{

//qDebug() << "Timer ID:" << event->timerId();

sendData();

}

```

開啟、停止定時器:

```

m_sendTimerId = startTimer(ui->txtInterval->text().toInt());

killTimer(m_sendTimerId);

```

由于本程式隻使用一個定時器,故不用判斷`event->timerId()`。

### 十六進制

為友善調試,工具支援字元、十六進制資料的發送和顯示。“十六進制字元串”轉字元串等函數集如下:

```

int hexStringToString(QString& hexStr, QString& str)

{

int ret = 0;

bool ok;

QByteArray retByte;

hexStr = hexStr.trimmed();

hexStr = hexStr.simplified();

QStringList sl = hexStr.split(" ");

foreach (QString s, sl)

{

if(!s.isEmpty())

{

char c = (s.toInt(&ok,16))&0xFF;

if (ok)

{

retByte.append(c);

}

else

{

ret = -1;

}

}

}

str = retByte;

return ret;

}

int hexStringToHexArray(QString& hexStr, QByteArray& arr)

{

int ret = 0;

bool ok;

hexStr = hexStr.trimmed();

hexStr = hexStr.simplified();

QStringList sl = hexStr.split(" ");

foreach (QString s, sl)

{

if(!s.isEmpty())

{

char c = (s.toInt(&ok,16))&0xFF;

if (ok)

{

arr.append(c);

}

else

{

ret = -1;

}

}

}

return ret;

}

int hexArrayToString(QByteArray& hexArr, QString& str)

{

int ret = 0;

str = hexArr.toHex(' ').toLower();

return ret;

}

```

## 其它

實際上,程式難度不大,特别是序列槽類的操作,因為`QSerialPort`提供了十分友好、友善的接口進行序列槽的設定、收發。如果要說有難度,可能在于界面邏輯的設計。如定時發送與單次發送,控件提示文字和圖示顯示,十六進制與字元串之間的轉換,等等。

筆者在此工具基礎上實作了對 ESP8266 的操作,包括訓示LED燈、繼電器、出廠恢複以及運作态的功能測試驗證等操作。由于與本文關聯不大,不再展開。

# 代碼倉庫

代碼以倉庫為準,本文不一定全部囊括。本工程所有源碼均可自由自主使用,包括但不限于添加、删除、修改,商用、自用。由此帶來的成果/後果概與作者無關。限于水準能力,本程式無任何品質保證,本程式作者無提供服務之義務。

繼續閱讀