1、概述
一臺(tái)典型的工控設(shè)備通常包括若干通訊接口(網(wǎng)絡(luò)、串口、CAN等),以及若干數(shù)字IO、AD通道等。運(yùn)行于設(shè)備核心平臺(tái)的應(yīng)用程序通過(guò)操作這些接口,實(shí)現(xiàn)特定的功能。通常為了高效高精度完成整個(gè)通訊控制流程,應(yīng)用程序采用C/C++語(yǔ)言來(lái)編寫(xiě)。圖1表現(xiàn)了典型工控設(shè)備的組成關(guān)系。
典型工控設(shè)備框圖
工控設(shè)備的另一個(gè)特點(diǎn)是鑒于設(shè)備大多是24小時(shí)連續(xù)運(yùn)行,且無(wú)人值守,所以基本的工控設(shè)備是無(wú)顯示的。英創(chuàng)的工控主板ESM6800、ESM335x等都大量的應(yīng)用于這類(lèi)無(wú)頭工控設(shè)備之中。
在實(shí)際應(yīng)用中,部分客戶需要基于已有的無(wú)頭工控設(shè)備,增加顯示界面功能,以滿足新的應(yīng)用需求。顯然保持已有的基本工控處理程序不變,通過(guò)相對(duì)獨(dú)立的技術(shù)手段來(lái)實(shí)現(xiàn)顯示功能,最符合客戶的利益訴求。為此我們發(fā)展了一種雙進(jìn)程的程序設(shè)計(jì)方案來(lái)滿足客戶的這一需求。該方案的第一個(gè)進(jìn)程,以客戶已有的用C/C++寫(xiě)的基礎(chǔ)工控進(jìn)程為基礎(chǔ),僅增加一個(gè)面向本地IP(127.0.0.1)的偵聽(tīng)線程,用于向顯示進(jìn)程提供必要的運(yùn)行工況數(shù)據(jù)。圖2為增添了服務(wù)線程的工控進(jìn)程:
帶有偵聽(tīng)線程的基礎(chǔ)工控進(jìn)程
方案的第二個(gè)進(jìn)程則主要用于實(shí)現(xiàn)顯示界面,可以采用各種手段來(lái)實(shí)現(xiàn),本文中介紹了使用Qt的QML語(yǔ)言加通訊插件的界面設(shè)計(jì)方法。第二個(gè)進(jìn)程(具體是通訊插件單元)通過(guò)本地IP,以客戶端方式與基礎(chǔ)工控進(jìn)程進(jìn)行Socket通訊,完成進(jìn)程間數(shù)據(jù)交換。顯示進(jìn)程以及與工控進(jìn)程的關(guān)系如圖3所示:
顯示進(jìn)程與工控進(jìn)程
2、系統(tǒng)設(shè)計(jì)
鑒于工業(yè)控制領(lǐng)域?qū)ο到y(tǒng)運(yùn)行的穩(wěn)定性要求,控制系統(tǒng)更加傾向于將底層硬件控制部分與上層界面顯示分開(kāi),兩部分以雙進(jìn)程的形式各自獨(dú)立運(yùn)行。底層硬件控制部分將會(huì)監(jiān)控系統(tǒng)硬件,管理外設(shè)等,同時(shí)收集系統(tǒng)的狀態(tài);而上層界面顯示部分主要用于顯示系統(tǒng)狀態(tài),并實(shí)現(xiàn)少量的系統(tǒng)控制功能,方便維護(hù)人員查看系統(tǒng)運(yùn)行狀態(tài)并且根據(jù)當(dāng)前狀態(tài)進(jìn)行系統(tǒng)的調(diào)整。由于顯示界面不一定是所有設(shè)備都配置,而且顯示部分的程序更加復(fù)雜,從而更容易出現(xiàn)程序運(yùn)行時(shí)的錯(cuò)誤,將控制與顯示分開(kāi)能夠避免由于顯示部分的程序問(wèn)題而影響到整個(gè)控制系統(tǒng)的運(yùn)行,而且沒(méi)有配置顯示屏的設(shè)備也可以直接運(yùn)行底層的控制程序,增加了系統(tǒng)程序的兼容性。顯示與控制分離后,由于顯示界面程序不需要處理底層硬件的管理控制,在設(shè)計(jì)時(shí)可以更加注重于界面的美化,而且界面程序可以采用不同的編程語(yǔ)言進(jìn)行開(kāi)發(fā),比如使用Qt C++或者Android java,本文將介紹基于Linux + Qt的雙進(jìn)程示例程序供客戶在實(shí)際開(kāi)發(fā)中參考,關(guān)于Android程序請(qǐng)參考我們官網(wǎng)的另一篇文章:《Android雙應(yīng)用進(jìn)程Demo程序設(shè)計(jì)》。
如上圖所示。整個(gè)系統(tǒng)分為控制和顯示兩個(gè)進(jìn)程,底層硬件控制部分可以獨(dú)立運(yùn)行,使用多線程管理不同的硬件設(shè)備,監(jiān)控硬件狀態(tài),將狀態(tài)發(fā)送給socket服務(wù)器,并且從socket服務(wù)器接收命令來(lái)更改設(shè)備狀態(tài)。Socket服務(wù)器也是一個(gè)獨(dú)立的線程,通過(guò)本地網(wǎng)絡(luò)通信集中處理來(lái)自硬件控制線程以及顯示程序的消息。顯示界面需要連接上socket服務(wù)器才能正確的顯示設(shè)備的狀態(tài),同時(shí)提供必須的人工控制接口,供設(shè)備使用過(guò)程中人為調(diào)整設(shè)備運(yùn)行狀態(tài)。目前在ESM6802工控主板上,界面程序可以采用Qt C++編寫(xiě),也可以使用Android java進(jìn)行開(kāi)發(fā),本文僅介紹采用Qt的界面程序。顯示程序界面用QML搭建,與底層通信的部分用獨(dú)立的Qt QML插件實(shí)現(xiàn),這樣顯示部分進(jìn)一部分離為數(shù)據(jù)處理和界面開(kāi)發(fā),使得界面設(shè)計(jì)可以更加快捷。程序的整體界面效果如下圖所示:
目前我們只提供了串口(SERIAL)和GPIO兩部分的例程。下面將集中介紹程序中通過(guò)本地IP實(shí)現(xiàn)兩個(gè)進(jìn)程通信的部分供客戶在實(shí)際開(kāi)發(fā)中參考。
3、控制端C程序
控制端程序主要分為兩個(gè)部分,一個(gè)部分用于控制具體的硬件運(yùn)行(下文稱為控制器),另一個(gè)部分為socket服務(wù)器,用于與顯示程序之間進(jìn)行通信。由于本方案主要是為了展示在已有控制程序的基礎(chǔ)上,增加顯示界面功能,以滿足新的應(yīng)用需求,所以我們?cè)诖酥攸c(diǎn)介紹在已有控制程序中加入socket服務(wù)器的部分,不再詳細(xì)介紹各硬件的具體控制的實(shí)現(xiàn)。
增加本地IP通信的功能,首先需要在控制進(jìn)程中新加入一個(gè)socket服務(wù)器線程,用于消息的集中管理,實(shí)現(xiàn)底層硬件與上層的界面程序的信息交換,socket服務(wù)器線程運(yùn)行的函數(shù)體代碼如下:
static void *_init_server(void *param) { int server_sockfd, client_sockfd; int server_len; struct sockaddr_in server_address; struct sockaddr_in client_address;
server_sockfd = socket(AF_INET, SOCK_STREAM, 0); server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = inet_addr("127.0.0.1");//通過(guò)本地ip通信 server_address.sin_port = htons(9733); server_len = sizeof(server_address); bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
listen(server_sockfd, 5);
int res; pthread_t client_thread; pthread_attr_t attr; char id[4]; client_element *client_t;
while(1) { if(!client_has_space(clients)) { printf("to many client, wait for one to quit...\n"); sleep(2); continue; } printf("server waiting\n"); client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, (socklen_t *)&server_len);
//get and save client id read(client_sockfd, &id, 4); if((id[0]!='I') && (id[1]!='D')) { printf("illegal client id, drop it\n"); close(client_sockfd); continue; }
client_t = accept_client(clients, id, client_sockfd); printf("client: %s connected\n", id);
//create a new thread to handle this connection res = pthread_attr_init(&attr); if( res!=0 ) { printf("Create attribute failed\n" ); } // 設(shè)置線程綁定屬性 res = pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM ); // 設(shè)置線程分離屬性 res += pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_DETACHED ); if( res!=0 ) { printf( "Setting attribute failed\n" ); }
res = pthread_create( &client_thread, &attr, (void *(*) (void *))socked_thread_func, (void*)client_t ); if( res!=0 ) { close( client_sockfd ); del_client(clients, client_sockfd); continue; } pthread_attr_destroy( &attr ); } } |
此函數(shù)創(chuàng)建一個(gè)socket用于監(jiān)聽(tīng)(listen)等待顯示程序連接,當(dāng)接受(accept)一個(gè)連接之后創(chuàng)建一個(gè)新的線程用于消息處理,主要用于維護(hù)socket連接的狀態(tài),解析消息的收發(fā)方,并將消息轉(zhuǎn)送到對(duì)應(yīng)的接收方,在顯示程序建立連接之前或者連接斷開(kāi)之后,控制器發(fā)送的消息將不會(huì)進(jìn)行發(fā)送了,而控制器依然在正常運(yùn)行,用于處理消息的新線程如下:
static void *socked_thread_func(void *p) { client_element *client_p = (client_element *)p; printf("started socked_thread_func for client: %s\n", client_p->id); fd_set fdRead; int ret, lenth; struct timeval aTime; struct msg_head msg_h; char *buf = (char *)&msg_h; //from:2 char to 2 char msglenth:1 int buf[0] = client_p->id[2]; buf[1] = client_p->id[3]; char msg[100]; client_element *send_to; struct tcp_info info; int tcp_info_len=sizeof(info); while(1) { FD_ZERO(&fdRead); FD_SET(client_p->sockfd, &fdRead);
aTime.tv_sec = 2; aTime.tv_usec = 0;
getsockopt(client_p->sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&tcp_info_len); if(info.tcpi_state == 1) { //printf("$$$%d tcp connection established...\n", client_p->sockfd); ; } else { printf("$$$%d tcp connection closed...\n", client_p->sockfd); break; }
ret = select( client_p->sockfd+1,&fdRead,NULL,NULL,&aTime );
if (ret > 0) { //判斷是否讀事件 if (FD_ISSET(client_p->sockfd, &fdRead)) { //data available, so get it! lenth = read( client_p->sockfd, buf+2, 6 ); if( lenth != 6 ) { continue; } // 對(duì)接收的數(shù)據(jù)進(jìn)行處理,這里為簡(jiǎn)單的數(shù)據(jù)轉(zhuǎn)發(fā) lenth = read(client_p->sockfd, msg, msg_h.lenth); if(lenth == msg_h.lenth) { send_to = find_client(clients, msg_h.to); //printf("try to send to client %s\n", msg_h.to); if(send_to == NULL) { printf("can't find target client\n"); continue; } write(send_to->sockfd, &msg_h, sizeof(struct msg_head)); write(send_to->sockfd, msg, lenth); } // 處理完畢 } } } close( client_p->sockfd ); del_client(clients, client_p->sockfd); pthread_exit( NULL ); } |
這里收到消息后就解析消息頭,發(fā)送到指定的端口去(控制器或者顯示進(jìn)程),由于實(shí)際應(yīng)用中socket傳送數(shù)據(jù)可能存在分包的情況,客戶需要自行定義消息的數(shù)據(jù)格式來(lái)保證數(shù)據(jù)的完整性,以及對(duì)數(shù)據(jù)進(jìn)行更嚴(yán)格的驗(yàn)證。
另一方面對(duì)于已有的控制器來(lái)說(shuō),需要在原來(lái)的基礎(chǔ)上進(jìn)行修改,在主線程中與socket服務(wù)器建立連接:
sockedfd = socket(AF_INET, SOCK_STREAM, 0); address.sin_family = AF_INET; address.sin_addr.s_addr = inet_addr("127.0.0.1"); address.sin_port = htons(9733); len = sizeof(address); do { res = connect(sockedfd, (struct sockaddr *)&address, len); if(res == -1) { perror("oops: connect error"); } }while(res == -1); write(sockedfd, "IDG1", 4); printf("###connected to server\n"); |
然后建立兩個(gè)線程分別處理數(shù)據(jù)(data_thread_func)和命令(command_thread_func),其中data_thread_func用于監(jiān)聽(tīng)硬件狀態(tài),并且發(fā)送相應(yīng)的狀態(tài)消息給socket服務(wù)器,而command_thread_func用于監(jiān)聽(tīng)socket服務(wù)器的消息等待命令,用于改變硬件運(yùn)行狀態(tài),不需要界面帶有控制功能的客戶可以不實(shí)現(xiàn)commad_thread_func。以GPIO控制器為例:
void *gpio_controller::data_thread_func(void* lparam) { gpio_controller *pSer = (gpio_controller*)lparam;
fd_set fdRead; int ret=0; struct timeval aTime; unsigned int pinstates = 0; struct msg_head buf_h;
while( 1 ) { FD_ZERO(&fdRead); FD_SET(pSer->interface_fd,&fdRead);
aTime.tv_sec = 2; aTime.tv_usec = 0;
//等待硬件消息,這里是GPIO狀態(tài)改變 ret = select( pSer->interface_fd+1,&fdRead,NULL,NULL,&aTime );
if (ret < 0 ) { //關(guān)閉 perror("select wrong"); pSer->close_interface(pSer->interface_fd); break; }
else { //select超時(shí)或者GPIO狀態(tài)發(fā)生了改變,讀取GPIO狀態(tài),發(fā)送給socket服務(wù)器 pinstates = INPINS; ret = GPIO_PinState(pSer->interface_fd, &pinstates); if(ret < 0) { printf("GPIO_PinState::failed %d\n", ret); break; } sprintf((char *)&buf_h.to[0], "D1"); buf_h.lenth = sizeof(pinstates); write(pSer->sockedfd, (void *)&buf_h.to[0], 6); write(pSer->sockedfd, (void *)&pinstates, sizeof(pinstates)); } } printf( "ReceiveThreadFunc finished\n"); pthread_exit( NULL ); }
void *gpio_controller::command_thread_func(void* lparam) { gpio_controller *pSer = (gpio_controller*)lparam;
fd_set fdRead; int ret, len; struct timeval aTime; struct outcom{ unsigned int outpin; unsigned int outstate; }; struct outcom out; struct msg_head buf_h;
while( 1 ) { FD_ZERO(&fdRead); FD_SET(pSer->sockedfd,&fdRead);
aTime.tv_sec = 3; aTime.tv_usec = 300000;
//等待socket服務(wù)器的消息 ret = select( pSer->sockedfd+1,&fdRead,NULL,NULL,&aTime ); if (ret < 0 ) { //關(guān)閉 pSer->close_interface(pSer->interface_fd); break; }
if (ret > 0) { //判斷是否讀事件 if (FD_ISSET(pSer->sockedfd,&fdRead)) { len = read(pSer->sockedfd, &buf_h, sizeof(buf_h)); //獲取socket服務(wù)器發(fā)送的信息,進(jìn)行解析 if(len != sizeof(struct outcom)) { printf("###invalid command lenth: %d, terminate\n", len); } len = read(pSer->sockedfd, &out, buf_h.lenth);
//write command switch(out.outstate) { case 0: GPIO_OutClear(pSer->interface_fd, out.outpin); if(ret < 0) printf("GPIO_OutClear::failed %d\n", ret); //printf("GPIO_OutClear::succeed %d\n", ret); break; case 1: GPIO_OutSet(pSer->interface_fd, out.outpin); if(ret < 0) printf("GPIO_OutSet::failed %d\n", ret); //printf("GPIO_OutSet::succeed %d\n", ret); break; default: printf("###wrong gpio state %d, no operation\n", out.outstate); ret = -1; break; } if(ret < 0) break; } } } printf( "ReceiveThreadFunc finished\n"); pthread_exit( NULL ); } |
這里兩個(gè)函數(shù)主要任務(wù)都是處理數(shù)據(jù),data_thread_func使用select函數(shù)來(lái)等待輸入GPIO的狀態(tài)改變事件,如果有狀態(tài)改變或者select等待超時(shí)都讀取一次GPIO的狀態(tài),然后發(fā)送給socket服務(wù)器;command_thread_func監(jiān)聽(tīng)服務(wù)器的消息,收到消息后進(jìn)行解析,然后根據(jù)消息來(lái)操作GPIO輸出信號(hào)。
通過(guò)這兩個(gè)函數(shù)便與socket服務(wù)器建立了消息溝通通道,而socket服務(wù)器會(huì)自動(dòng)將數(shù)據(jù)轉(zhuǎn)發(fā)到顯示進(jìn)程,這種實(shí)現(xiàn)可以使得對(duì)已有程序的改動(dòng)降到很低的程度。實(shí)際實(shí)現(xiàn)中,可以在socket服務(wù)器中增加狀態(tài)機(jī)等其他功能,記錄硬件狀態(tài)信息等。
4、顯示程序
顯示部分我們采用Qt來(lái)搭建,主要分為QML搭建的界面以及Qt c++編寫(xiě)的數(shù)據(jù)處理插件。QML是Qt提供的一種描述性的腳本語(yǔ)言,類(lèi)似于css,可以在腳本里創(chuàng)建圖形對(duì)象,并且支持各種圖形特效,以及狀態(tài)機(jī)等,同時(shí)又能跟Qt寫(xiě)的C++代碼進(jìn)行方便的交互,使用起來(lái)非常方便。采用QML加插件的方式主要是為了將界面設(shè)計(jì)與程序邏輯解耦,一般的系統(tǒng)開(kāi)發(fā)中界面設(shè)計(jì)的變動(dòng)往往多于后臺(tái)邏輯,因此采用QML加插件的方式將界面設(shè)計(jì)與邏輯分離有利于開(kāi)發(fā)人員的分工,加速產(chǎn)品迭代速度,降低后期維護(hù)成本。而且QML解釋性語(yǔ)言的特性使得其語(yǔ)法更加簡(jiǎn)單,可以將界面設(shè)計(jì)部分交給專業(yè)的設(shè)計(jì)人員開(kāi)發(fā),而不要求設(shè)計(jì)人員會(huì)c++等編程語(yǔ)言。Qt底層對(duì)QML做了優(yōu)化,將會(huì)優(yōu)先使用硬件圖形加速器進(jìn)行界面的渲染,也針對(duì)觸摸屏應(yīng)用做了優(yōu)化,使用QML能夠更簡(jiǎn)單快捷的搭建流暢、優(yōu)美的界面。QML也支持嵌入Javascript處理邏輯,但是底層邏輯處理使用Qt C++編寫(xiě)插件,能夠更好的控制數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)處理也更加高效,Qt提供了多種方式將C++數(shù)據(jù)類(lèi)型導(dǎo)入QML腳本中,更多詳細(xì)資料可以查看Qt官方的文檔。由于篇幅原因,我們?cè)诹硗庖黄恼拢?a href='http://www.www.jsjflaw.com/article/article20181109.html' target='_blank'>《使用QML進(jìn)行界面開(kāi)發(fā)》中更詳細(xì)地介紹了QML及插件的實(shí)現(xiàn),在此我們還是集中介紹socket消息處理部分。
本例程中數(shù)據(jù)處理插件的任務(wù)就是連接socket服務(wù)器,與服務(wù)器進(jìn)行通信,接收消息進(jìn)行解析然后提供給QML界面,以及從QML界面獲取消息給socket服務(wù)器發(fā)送命令。插件中通過(guò)socket進(jìn)行通信的部分代碼如下:
void MsgClient::cServer(void* param) { MsgClient *client = (MsgClient *)param; int ret; int len; struct sockaddr_in address; int sockedfd = socket(AF_INET, SOCK_STREAM, 0); printf("sockedfd: %d\n", sockedfd); client->sockedfd = sockedfd; address.sin_family = AF_INET; address.sin_addr.s_addr = inet_addr("127.0.0.1"); //本地IP通信 address.sin_port = htons(9733); len = sizeof(address); do { printf("Client: connecting...\n"); ret = ::connect(sockedfd, (struct sockaddr *)&address, len); //建立連接 if(ret == -1) { perror("oops: connect to server error"); } sleep(2); }while(ret == -1); write(sockedfd, "IDD1", 4); printf("Client: connected to server\n"); emit client->serverConnected(); fd_set fdRead; struct timeval aTime; char buf[100]; unsigned int pinstates; struct msg_head buf_h; while(!client->exit_flag) { FD_ZERO(&fdRead); FD_SET(sockedfd, &fdRead); aTime.tv_sec = 3; aTime.tv_usec = 0; ret = select(sockedfd+1, &fdRead, NULL, NULL, &aTime); //等待消息 if(ret < 0) { perror("someting wrong with select"); } if(ret > 0) { if(FD_ISSET(sockedfd, &fdRead)) { len = read(sockedfd, &buf_h, sizeof(buf_h)); int i; switch (buf_h.from[0]) { //解析消息 case 'S': //串口信息 i = buf_h.from[1] - '0'; len = read(sockedfd, buf, buf_h.lenth); client->rmsgQueue[i] << buf; if(i == client->m_interface) emit client->newMsgRcved(); memset(buf, 0, sizeof(buf)); break; case 'G': //GPIO信息 len = read(sockedfd, &pinstates, buf_h.lenth); printf("get GPIO pinstates\n"); client->updateGPIOState(pinstates); break; default: break; } } } } close(sockedfd); pthread_exit(NULL); } |
如代碼所示,插件首先通過(guò)本地IP127.0.0.1與socket服務(wù)器建立連接(connect),然后等待socket服務(wù)器的消息(select),收到消息后進(jìn)行解析,判斷是哪個(gè)硬件控制器發(fā)送的消息,然后更新相應(yīng)的顯示界面,這里的代碼相對(duì)簡(jiǎn)單,只是為了展示通過(guò)本地IP實(shí)現(xiàn)顯示進(jìn)程與控制進(jìn)程之間的通信,實(shí)際使用中客戶需要對(duì)數(shù)據(jù)進(jìn)行更嚴(yán)格的檢驗(yàn)。
使用QML搭建串口控制界面如下圖所示:
GPIO控制器的顯示效果如下:
由于篇幅原因,我們?cè)诖瞬辉敿?xì)介紹實(shí)現(xiàn)界面的QML腳本了,將會(huì)在另一篇文章中進(jìn)行專門(mén)的介紹,感興趣的用戶可以關(guān)注我們官網(wǎng)上的文章更新,或者向我們要取程序源碼。用戶在實(shí)際開(kāi)發(fā)中可以參考此方式實(shí)現(xiàn)顯示進(jìn)程與控制進(jìn)程之間的通信,從而實(shí)現(xiàn)單獨(dú)的顯示進(jìn)程,對(duì)已有的控制進(jìn)程的更改控制到很小的程度,一方面減少了由于程序修改而造成控制程序的不穩(wěn)定,另一方面使用QML又能快速的搭建界面,解決顯示設(shè)備狀態(tài)的需求。
5、總結(jié)
實(shí)際測(cè)試過(guò)程中,我們?cè)贓SM6802工控板上運(yùn)行本文介紹的程序,底層控制程序直接可以開(kāi)機(jī)后臺(tái)運(yùn)行,顯示程序開(kāi)機(jī)后手動(dòng)加載,通過(guò)本地IP地址與控制程序的socket服務(wù)器連接,然后實(shí)時(shí)更新系統(tǒng)狀態(tài),也能及時(shí)響應(yīng)人工控制,如改變輸出GPIO的輸出狀態(tài),關(guān)掉顯示程序之后,控制程序繼續(xù)正常運(yùn)行,之后還可以再次啟動(dòng)顯示程序。
將底層控制與顯示分開(kāi)后,程序開(kāi)發(fā)分工可以更加細(xì)致,也一定程度上增加了控制系統(tǒng)的穩(wěn)定性,減小了維護(hù)成本。同時(shí)使用QML進(jìn)行界面開(kāi)發(fā)能夠更加方便快速的更新系統(tǒng)的顯示效果,完成產(chǎn)品迭代。由于底層控制與顯示之間采用socket進(jìn)行通信,顯示部分也可以采用其他的開(kāi)發(fā)環(huán)境,比如ESM6802也支持Android開(kāi)發(fā),用戶在產(chǎn)品升級(jí)換代的時(shí)候就能夠直接沿用底層控制部分的程序,而只對(duì)上層顯示部分的程序進(jìn)行調(diào)整。
有興趣的客戶可以和我們的工程師進(jìn)行溝通獲取更多信息以及程序代碼。
本文PDF下載:Linux雙進(jìn)程應(yīng)用示例
成都英創(chuàng)信息技術(shù)有限公司 028-8618 0660