在上一章我們實現了下位機的協議制定,並通過串口通訊工具完成了對設備內外設(LED)的狀態修改,下面就要進行上位機軟件的實現了(事實上這部分不屬於嵌入式Linux的內容,所以只在本章節講述下上位機實現的流程和思路,后續維護更新不在進行詳細說明,不過下位機界面實現肯定還會涉及這些技術),上位機的界面方案一般指在Windows平台的軟件界面開發,如UWP,WINFORM/C#, WPF/C#, QT/C++等,如果說我的個人傾向的話,當然更喜歡的WINFORM/C#技術,一方面C#相對於C++更簡單,不會因為復雜的模板和繼承機制,導致出問題的報錯比代碼都長,另一方面網上的資料也多,遇到問題很容易找到解決辦法,在我之前實現的應用中,也都是使用WINFORM技術,再加上對於QT/C++根本沒有了解過,算是第一次接觸(之前接觸的都是無界面應用或者使用的Android/Java),不過對於嵌入式Linux來說,QT/C++是也是十分需要掌握的,既然都要學習,那么上位機選擇QT/C++先來熟悉語法和基礎,完成上位機QT界面和通訊協議的實現,這也是這篇文章耽誤一段時間的原因,在QT還沒有熟悉之前,參考例程寫應用還可以,清晰的講清楚還是很困難的,在應用接近大半個月后,也算有些心得,可以進行后續的進度了,下面開始本節的實現吧。
參考資料
1. 開源QT例程項目
2. 《QT5開發和實例》 -- 參考這本書不是因為寫的有深度,而是因為里面全是例程,適合初學者了解
3. 《C++ Primer Plus》
QT界面布局實現
基於從Winform的界面開發經驗,QT界面的布局也是類似,參考上面的例程項目,主要涉及的的窗體有:
QFrame:基本控件的基類,用於將功能類似的結構整理在一起
QLabel:標簽控件,用於顯示文字說明
QPushButton:按鍵控件,執行按鍵動作
QTextEdit:編輯文本框控件,用於輸入或者顯示文本
QComboBox:選擇框控件,支持下拉菜單的選擇
QLineEdit:行編輯框,用於行輸入和顯示文本
在掌握基礎的基本的布局編輯框后,就可以使用設計欄左邊的控件框中,拖出如下的編輯框。
在構建完成上述編輯框后,下面就要實現界面內容的填充,主要包含頁面布局的顯示,下拉框的完善,代碼如下:

1 //添加COM口 2 QStringList comList; 3 for (int i = 1; i <= 20; i++) { 4 comList << QString("COM%1").arg(i); 5 } 6 ui->combo_box_com->addItems(comList); 7 8 //波特率選項 9 QStringList BaudList; 10 BaudList <<"9600"<<"38400"<<"76800"<<"115200"<<"230400"; 11 ui->combo_box_baud->addItems(BaudList); 12 ui->combo_box_baud->setCurrentIndex(3); 13 14 //數據位選項 15 QStringList dataBitsList; 16 dataBitsList <<"6" << "7" << "8"<<"9"; 17 ui->combo_box_data->addItems(dataBitsList); 18 ui->combo_box_data->setCurrentIndex(2); 19 20 //停止位選項 21 QStringList StopBitsList; 22 StopBitsList<<"1"<<"2"; 23 ui->combo_box_stop->addItems(StopBitsList); 24 ui->combo_box_stop->setCurrentIndex(0); 25 26 //校驗位 27 QStringList ParityList; 28 ParityList<<"N"<<"Odd"<<"Even"; 29 ui->combo_box_parity->addItems(ParityList); 30 ui->combo_box_parity->setCurrentIndex(0); 31 32 //設置協議類型 33 QStringList SocketTypeList; 34 SocketTypeList<<"TCP"<<"UDP"; 35 ui->combo_box_socket_type->addItems(SocketTypeList); 36 ui->combo_box_parity->setCurrentIndex(0); 37 38 // //正則限制部分輸入需要為數據 39 // QRegExp regx("[0-9]+$"); 40 // QValidator *validator_time = new QRegExpValidator(regx, ui->line_edit_time); 41 // ui->line_edit_time->setValidator( validator_time ); 42 // QValidator *validator_id = new QRegExpValidator(regx, ui->line_edit_dev_id); 43 // ui->line_edit_dev_id->setValidator( validator_id ); 44 45 //默認按鍵配置不可操作 46 init_btn_disable(ui); 47 ui->btn_uart_close->setDisabled(true); 48 ui->btn_socket_close->setDisabled(true);
至此,我們就完成了布局相關的代碼。
數據和操作邏輯
對於無界面的軟件或者方案實現,我們主要關注的是數據在整個邏輯模型之間的流通,轉移和處理,對於有界面的軟件實現,其實這套邏輯也是存在的。除了涉及界面的處理,其它部分其實也是這套邏輯,不過是將部分數據的源頭來自於界面的動作,並且將最后的輸出結果從命令行轉移到界面的窗口中,如果理解了這一點,就會發現其實帶界面的應用實現並沒有太困難,這也是我接觸QT/C++很短時間就能將Winform和下位機經驗快速轉換的原因。對於這個項目來說,主要實現的背后數據邏輯包含以下三個方面:
1.按鍵動作的信號和界面的輸入信息處理
2.硬件通訊相關的串口知識,socket通訊以及涉及的TCP和UDP協議傳輸
3.協議相關的硬件實現和數據處理
4.處理結果的界面輸出顯示
其中按鍵部分的動作和界面輸出顯示都是QT界面背后的邏輯,包含信號槽的綁定和界面變量的操作方法,如下所示

1 //獲取設備ID信息 2 pMainUartProtocolThreadInfo->SetId(ui->line_edit_dev_id->text().toShort()); 3 4 //界面顯示的操作 5 if(ui->text_edit_test->document()->lineCount() > 20) 6 { 7 qDebug()<<"lines do"; 8 ui->text_edit_test->setText(s); 9 } 10 else 11 { 12 ui->text_edit_test->append(s); 13 }
這部分是涉及QT的基礎知識,主要都是積累的技巧,難度不高,建議參考《QT5開發和實例》實例去學習。
硬件串口知識和Socket知識就是應用實現需要的其它能力,包含對QextSerialPort和Socket接口的應用,此外為了滿足多接口應用同時操作的需求,需要實現多線程的編程,其中串口的應用初始化配置主要包含的有
flush:清空緩存區
setBaudRate:設置波特率
setDataBits:設置數據位
setParity:設置奇偶校驗位
setStopBits:設置停止位
setFlowControl:設置流量控制
setTimeout:設置接收和發送超時時間

1 pMainUartProtocolThreadInfo->m_pSerialPortCom = new QextSerialPort(ui->combo_box_com->currentText(), QextSerialPort::Polling); 2 pMainUartProtocolThreadInfo->m_bComStatus = pMainUartProtocolThreadInfo->m_pSerialPortCom->open(QIODevice::ReadWrite); 3 4 if(pMainUartProtocolThreadInfo->m_bComStatus) 5 { 6 //清除緩存區 7 pMainUartProtocolThreadInfo->m_pSerialPortCom ->flush(); 8 //設置波特率 9 pMainUartProtocolThreadInfo->m_pSerialPortCom ->setBaudRate((BaudRateType)ui->combo_box_baud->currentText().toInt()); 10 //設置數據位 11 pMainUartProtocolThreadInfo->m_pSerialPortCom->setDataBits((DataBitsType)ui->combo_box_data->currentText().toInt()); 12 //設置校驗位 13 pMainUartProtocolThreadInfo->m_pSerialPortCom->setParity((ParityType)ui->combo_box_parity->currentText().toInt()); 14 //設置停止位 15 pMainUartProtocolThreadInfo->m_pSerialPortCom->setStopBits((StopBitsType)ui->combo_box_stop->currentText().toInt()); 16 pMainUartProtocolThreadInfo->m_pSerialPortCom->setFlowControl(FLOW_OFF); 17 pMainUartProtocolThreadInfo->m_pSerialPortCom ->setTimeout(10); 18 init_btn_enable(ui); 19 pMainUartProtocolThreadInfo->SetId(ui->line_edit_dev_id->text().toShort()); 20 ui->btn_uart_close->setEnabled(true); 21 ui->btn_uart_open->setDisabled(true); 22 ui->btn_socket_open->setDisabled(true); 23 ui->btn_socket_close->setDisabled(true); 24 ui->combo_box_com->setDisabled(true); 25 ui->combo_box_baud->setDisabled(true); 26 ui->combo_box_data->setDisabled(true); 27 ui->combo_box_stop->setDisabled(true); 28 ui->combo_box_parity->setDisabled(true); 29 append_text_edit_test(QString::fromLocal8Bit("serial open success!")); 30 protocol_flag = PROTOCOL_UART; 31 } 32 else 33 { 34 pMainUartProtocolThreadInfo->m_pSerialPortCom->deleteLater(); 35 pMainUartProtocolThreadInfo->m_bComStatus = false; 36 append_text_edit_test(QString::fromLocal8Bit("serial open failed!")); 37 }
串口的通訊讀寫接口主要包含
Write:數據發送接口
Read:數據讀取接口

1 //設備寫數據 2 int CUartProtocolThreadInfo::DeviceWrite(uint8_t *pStart, uint16_t nSize) 3 { 4 m_pSerialPortCom->write((char *)pStart, nSize); 5 return nSize; 6 } 7 8 //設備讀數據 9 int CUartProtocolThreadInfo::DeviceRead(uint8_t *pStart, uint16_t nMaxSize) 10 { 11 return m_pSerialPortCom->read((char *)pStart, nMaxSize); 12 }
socket通訊的的初始化配置包含
abort:中斷當前的所有連接
connectToHost:指定連接到指定的IP地址和端口
waitForConnect:等待服務器的連接
waitForBytesWritten:等待數據發送完成
waitForReadyRead:等待數據可以接收
此外,還包含和Socket通訊相關的
信號:connect <-> 槽函數:slotConnected
信號:diconnect <-> 槽函數:slotDisConnected
信號:readyRead <-> 槽函數:dataReceived
具體代碼實現如下:

1 void CTcpSocketThreadInfo::run() 2 { 3 bool is_connect; 4 int nLen; 5 int nStatus; 6 7 m_pTcpSocket = new QTcpSocket(); 8 m_pServerIp = new QHostAddress(); 9 connect(m_pTcpSocket, SIGNAL(connected()), this, SLOT(slotConnected())); 10 connect(m_pTcpSocket, SIGNAL(disconnected()), this, SLOT(slotDisconnected())); 11 connect(m_pTcpSocket, SIGNAL(readyRead()), this, SLOT(dataReceived())); 12 13 for(;;) 14 { 15 if(m_nIsStop) 16 return; 17 18 nStatus = m_pQueue->QueuePend(&SendBufferInfo); 19 if(nStatus == QUEUE_INFO_OK) 20 { 21 m_pTcpSocket->abort(); 22 m_pTcpSocket->connectToHost(*m_pServerIp, m_nPort); 23 nLen = this->CreateSendBuffer(this->GetId(), SendBufferInfo.m_nSize, 24 SendBufferInfo.m_pBuffer, SendBufferInfo.m_IsWriteThrough); 25 is_connect = m_pTcpSocket->waitForConnected(300); 26 if(is_connect) 27 { 28 emit send_edit_test(QString("socket client ok")); 29 this->DeviceWrite(tx_buffer, nLen); 30 31 //通知主線程更新窗口 32 emit send_edit_test(byteArrayToHexString("Sendbuf:", tx_buffer, nLen, "\n")); 33 34 //等待發送和接收完成 35 m_pTcpSocket->waitForBytesWritten(); 36 m_pTcpSocket->waitForReadyRead(); 37 38 } 39 else 40 { 41 emit send_edit_test(QString("socket client fail\n")); 42 } 43 qDebug()<<"thread queue test OK\n"; 44 } 45 } 46 }
完成上述接口應用的實現,后續的邏輯就是涉及協議實現的部分,這部分的實現與協議相關的章節實現一致,具體如下,包含
CreateSendBuffer:生成發送數據
DeviceRead:數據接收
DeviceWrite:數據發送
CheckReceiveData:接收數據,並校驗
ExecuteCommand:執行指令的處理
如此變完整實現了整個數據邏輯的框架,完成了從按鍵數據發送觸發,協議數據發送和接收處理,接收界面顯示的完整流程,最后實現如圖所示的功能:
至此,我們對於QT上位機界面的基本應用框架已經實現完畢,后續就是在該平台的基礎上構建新的接口實現,滿足不同應用的需求,因為本身這個系列是學習嵌入式Linux開發的,雖然后續肯定會在這份代碼的基礎上區完善上位機的應用,但對於上位機的說明目前就淺嘗輒止了(畢竟本系列的實現並非嵌入式開發),不過在應用開發中,我對曾經學到的類的繼承和派生,模板,lambda表達式都有了進一步的實踐和應用,加深了相關的理解,也算是意義非凡了,至於代碼的地址,參考第一章節的github開源地址中upper_app的內容,包含全部的代碼實現。