이전 글까지 AiotClient의 Tab1~Tab7을 구현하는 과정을 정리했습니다.
이번 글에서는 수업에서 배운 내용을 바탕으로 진행한 간단한 시험 내용에 대해 정리해보려합니다.
기존 AiotClient의 소켓 클라이언트(Tab2)와 센서 차트(Tab4), SQLite DB(Tab5) 기능을 새 프로젝트에 Tab1, Tab2, Tab3으로 재구성하는 내용입니다.
시험때 캡쳐해둔 내용도 없고, 교수님 서버가 지금은 꺼져있어서, 내용으로만 정리해보겠습니다.

과제를 진행하면서 Ubuntu(VirtualBox) 환경에서 문제가 발생했습니다.
새 프로젝트를 생성하고 Tab1, Tab2, Tab3 클래스를 추가한 뒤 .pro 파일에 모듈을 추가하는 과정에서 문제가 생겼습니다.
QT += widgets network charts sql
기존에 작업하던 위젯 파일들은 정상적으로 동작했지만, 새로 추가한 파일들에서 charts 모듈을 추가하면 계속 빌드 오류가 발생했습니다.
교수님께 말씀드렸고, 교수님 컴퓨터에서 새 파일에 charts만 추가해서 정상 동작하는 것을 확인한 후 파일을 보내주셨습니다. 하지만 해당 파일을 제 Ubuntu 환경에서 실행하면 동일한 오류가 발생했습니다.
원인을 파악하지 못한 채로, 교수님 지시에 따라 Windows 환경의 Qt Creator로 과제를 진행했습니다.
AiotClient와 비교하면 아래와 같이 탭 구성이 변경됐습니다.
| AiotClient | SensorManKYM | 기능 |
|---|---|---|
| Tab2SocketClient | Tab1Socket | 소켓 연결/수신/송신 |
| Tab4SensorChart | Tab2Sensor | 실시간 센서 차트 |
| Tab5SensorDatabase | Tab3Sqlite | SQLite DB 저장/조회 |
AiotClient에서는 Tab2가 메시지를 파싱한 뒤 mainwidget을 통해 각 탭으로 라우팅하는 구조였습니다.
이번 과제에서는 Tab1이 직접 파싱하여 Tab2, Tab3으로 동시에 emit하는 구조로 단순화했습니다.
수신 메시지 : [HM_CON]SENSOR@71@26.6@51.2
↓
Tab1에서 파싱
↓
tab2RecvDataSig + tab3RecvDataSig 동시 emit
↓ ↓
Tab2Sensor Tab3Sqlite
QT += widgets network charts sql
==============================================
| [서버연결(Checkable)] |
|----------------------------------------------|
| 수신 데이터 표시 (읽기전용) |
|----------------------------------------------|
| 수신ID입력창 | 송신데이터입력창 | [Send] |
|----------------------------------------------|
| [수신Clear] |
==============================================

| 위젯 | objectName | 역할 |
|---|---|---|
| QPushButton | pPBserverConnect | 서버 연결/해제 (Checkable) |
| QTextEdit | pTErecvData | 수신 데이터 표시 (읽기전용) |
| QLineEdit | pLErecvId | 수신 ID 입력 |
| QLineEdit | pLEsendData | 송신 데이터 입력 |
| QPushButton | pPBsend | 송신 (초기 비활성화) |
| QPushButton | pPBrecvClear | 수신 창 초기화 |
AiotClient의 Tab2SocketClient와 비교해 수신 창 Clear 버튼(pPBrecvClear)이 추가됐습니다.
#ifndef TAB1SOCKET_H
#define TAB1SOCKET_H
#include <QWidget>
#include <QTime>
#include "socketclient.h"
namespace Ui {
class Tab1Socket;
}
class Tab1Socket : public QWidget
{
Q_OBJECT
public:
explicit Tab1Socket(QWidget *parent = nullptr);
~Tab1Socket();
private slots:
void updateRecvDataSlot(QString);
void on_pPBserverConnect_clicked(bool checked);
void on_pPBsend_clicked();
void on_pPBrecvClear_clicked();
public slots:
void socketWriteDataSlot(QString);
private:
Ui::Tab1Socket *ui;
SocketClient *pSocketClient;
signals:
void tab2RecvDataSig(QStringList&);
void tab3RecvDataSig(QStringList&);
};
#endif // TAB1SOCKET_H
생성자에서 SocketClient를 생성하고 수신 시그널을 연결합니다.
Tab1Socket::Tab1Socket(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Tab1Socket)
{
ui->setupUi(this);
pSocketClient = new SocketClient(this);
ui->pPBsend->setEnabled(false);
connect(pSocketClient, SIGNAL(socketRecvDataSig(QString)), this, SLOT(updateRecvDataSlot(QString)));
}
수신 데이터를 파싱하여 메시지 종류가 SENSOR이면 Tab2와 Tab3으로 동시에 emit합니다.
void Tab1Socket::updateRecvDataSlot(QString strRecvData)
{
strRecvData.chop(1);
QTime time = QTime::currentTime();
QString strTime = time.toString() + " " + strRecvData;
ui->pTErecvData->append(strTime);
strRecvData.replace("[", "@");
strRecvData.replace("]", "@");
QStringList strList = strRecvData.split("@");
if(strList[2] == "SENSOR")
{
emit tab2RecvDataSig(strList);
emit tab3RecvDataSig(strList);
}
}
서버 연결/해제, 송신, 수신 Clear 버튼 슬롯입니다.
void Tab1Socket::on_pPBserverConnect_clicked(bool checked)
{
bool bFlag;
if(checked)
{
pSocketClient->connectToServerSlot(bFlag);
if(bFlag)
{
ui->pPBserverConnect->setText("서버해제");
ui->pPBsend->setEnabled(true);
}
else
ui->pPBserverConnect->setChecked(false);
}
else
{
pSocketClient->socketClosedServerSlot();
ui->pPBserverConnect->setText("서버연결");
ui->pPBsend->setEnabled(false);
}
}
void Tab1Socket::on_pPBsend_clicked()
{
QString strRecvId = ui->pLErecvId->text();
QString strSendData = ui->pLEsendData->text();
if(strSendData.isEmpty())
return;
if(strRecvId.isEmpty())
strSendData = "[ALLMSG]" + strSendData;
else
strSendData = "[" + strRecvId + "]" + strSendData;
pSocketClient->socketWriteDataSlot(strSendData);
ui->pLEsendData->clear();
}
void Tab1Socket::on_pPBrecvClear_clicked()
{
ui->pTErecvData->clear();
}
AiotClient의 Tab4SensorChart와 동일한 구조입니다.
자세한 구현 내용은 아래 글을 참고해 주세요.

슬롯 이름만 tab4RecvDataSlot → tab2RecvDataSlot으로 변경됐습니다.
AiotClient의 Tab5SensorDatabase와 대부분 동일하며, 삭제 버튼 동작에 차이가 있습니다.

Tab5SensorDatabase와의 차이점:
Tab5의 삭제 버튼은 화면(테이블, 차트)만 초기화했지만, Tab3Sqlite에서는 DB에서도 실제로 DELETE 쿼리를 실행합니다.
void Tab3Sqlite::on_pPBDbDelete_clicked()
{
QString strFromDateTime = ui->pDateTimeEditFrom->dateTime().toString("yyyy/MM/dd hh:mm:ss");
QString strToDateTime = ui->pDateTimeEditTo->dateTime().toString("yyyy/MM/dd hh:mm:ss");
QString strQuery = "delete from sensor_tb where '"
+ strFromDateTime + "' <= date AND date < '"
+ strToDateTime + "' ";
QSqlQuery qSqlQuery;
if(qSqlQuery.exec(strQuery))
qDebug() << "delete query ok";
ui->pTBsensor->clearContents();
illuLine->clear();
humiLine->clear();
tempLine->clear();
}
#include <tab1socket.h>
#include <tab2sensor.h>
#include <tab3sqlite.h>
Tab1Socket *pTab1Socket;
Tab2Sensor *pTab2Sensor;
Tab3Sqlite *pTab3Sqlite;
Tab1이 파싱과 라우팅을 담당하므로 mainwidget에서의 connect 구조가 단순해졌습니다.
pTab1Socket = new Tab1Socket(ui->pTab1);
ui->pTab1->setLayout(pTab1Socket->layout());
pTab2Sensor = new Tab2Sensor(ui->pTab2);
ui->pTab2->setLayout(pTab2Sensor->layout());
pTab3Sqlite = new Tab3Sqlite(ui->pTab3);
ui->pTab3->setLayout(pTab3Sqlite->layout());
ui->tabWidget->setCurrentIndex(0);
connect(pTab1Socket, SIGNAL(tab2RecvDataSig(QStringList&)),
pTab2Sensor, SLOT(tab2RecvDataSlot(QStringList&)));
connect(pTab1Socket, SIGNAL(tab3RecvDataSig(QStringList&)),
pTab3Sqlite, SLOT(tab3RecvDataSlot(QStringList&)));
새 프로젝트 생성 (SensorManKYM, qmake)
↓
.pro에 모듈 추가 (widgets network charts sql)
↓
Ubuntu 환경 charts 오류 발생 → Windows Qt Creator로 환경 전환
↓
Tab1Socket 구현 (소켓 연결/수신/송신/파싱/라우팅)
↓
Tab2Sensor 구현 (실시간 센서 차트)
↓
Tab3Sqlite 구현 (SQLite DB 저장/조회/삭제)
↓
mainwidget에 Tab1~3 추가 및 Signal/Slot 연결
↓
Tab1 수신 → SENSOR 메시지 파싱 → Tab2, Tab3 동시 emit
이제 다음 시간부터는 터틀봇을 제공받고, ROS2 수업에 대해 정리할 예정입니다...