1. 오늘 한 것
① 시스템 데이터베이스(MariaDB/MySQL) 설계 및 구축
데이터의 성격과 보존 주기에 따라 3개의 핵심 테이블 구조를 설계함.
- sensor_logs: 아두이노 센서 수치 실시간 기록 (주기적 삭제 필요)
CREATE TABLE sensor_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
sensor_type VARCHAR(20) NOT NULL, -- TEMP, HUMI, PIR, ULTRA, FALL, SOS, FAN
value FLOAT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- chat_logs: 보호자 ↔ 거주자 간 양방향 통신 메시지 이력 기록
CREATE TABLE chat_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
sender ENUM('S', 'G') NOT NULL, -- S: Senior, G: Guardian
message TEXT NOT NULL,
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- alert_logs: 고온, 낙상, SOS 등 위험 이벤트 발생 이력 영구 보관
CREATE TABLE alert_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
alert_type VARCHAR(20) NOT NULL, -- 고온, 주의, 경보, 비상
detail VARCHAR(200) NOT NULL,
status VARCHAR(20) DEFAULT '확인필요',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
② 리눅스 서버 - 윈도우 클라이언트 간 TCP 소켓 통신 구현
- 서버(Port 9000)는 클라이언트 접속 시마다 개별 스레드를 생성(tcpAcceptThread)하여 다중 접속을 처리하는 구조 구현.
- 접속 시 첫 패킷으로 역할(SENIOR 또는 GUARDIAN)을 부여받아 클라이언트 배열에 등록.
- 특정 역할의 모든 클라이언트에게 패킷을 브로드캐스팅하는 sendToRole() 함수 구현.
③ 통신 프로토콜 상세 설계 및 데이터 중계(Relay)
- 채팅 중계: 거주자의 메시지는 보호자에게, 보호자의 메시지는 거주자에게 교차 포워딩하는 로직 구현.
: 거주자가 메시지를 보내면 서버가 보호자에게 포워딩하고, 보호자가 메시지를 보내면 거주자에게 포워딩한다.
if (pkt.type == PT_CHAT_MSG) {
char sender = (clients[idx].role == ROLE_SENIOR) ? 'S' : 'G';
saveChat(db, sender, pkt.payload.chat.message);
int target = (clients[idx].role == ROLE_SENIOR)
? ROLE_GUARDIAN : ROLE_SENIOR;
NetPacket relay = pkt;
snprintf(relay.sender_id, 16, "%s",
sender == 'S' ? "SENIOR" : "GUARDIAN");
sendToRole(target, &relay);
}
sender_id에 발신자 정보를 담아서 중계하면, 수신 측에서 "거주자 : 메시지" 형태로 구분해 출력할 수 있다.
- HISTORY 프로토콜: 과거 기록 조회 시 패킷 제한(128Byte)을 우회하기 위해 단일 문자열 대신 HISTORY_START ➔ 데이터 개별 전송 ➔ HISTORY_END 형태로 행(Row) 단위 분할 전송 기법 적용.
서버 → 클라이언트 전송 순서:
"HISTORY_START"
"HISTORY|03/19 14:30|비상|SOS 호출|확인필요"
"HISTORY|03/19 11:20|경보|낙상위험 경보|확인필요"
... (최대 20건)
"HISTORY_END"
클라이언트는 HISTORY_START를 받으면 벡터를 초기화하고,
HISTORY|로 시작하는 패킷을 파싱해 행 단위로 누적한다.
HISTORY_END를 받으면 ListView 팝업을 띄운다.
if (msg == "HISTORY_START") {
g_history.clear();
} else if (msg.rfind("HISTORY|", 0) == 0) {
// pipe로 분리해서 HistRow에 저장
auto parts = splitPipe(msg.substr(8));
g_history.push_back({ parts[0], parts[1], parts[2], parts[3] });
} else if (msg == "HISTORY_END") {
PostMessage(g_hWnd, WM_SHOWHISTORY, 0, 0);
}
-> 128바이트 제한을 행 단위 분리로 우회.
- CARD 프로토콜: UI 상단 센서 위젯의 갱신을 위해 CARD:TEMP:21.5 형태의 접두사 패킷을 전송. 수신 측에서는 해당 접두사를 필터링해 채팅 로그창을 오염시키지 않고 위젯만 조용히 갱신(WM_UPDATESENSOR)하도록 분리.
▼ 서버에서 CARD: 접두사 패킷으로 업데이트
서버 → 보호자:
"CARD:TEMP:21.5"
"CARD:HUMI:56.0"
"CARD:PIR:0"
"CARD:FALL:0.0"
▼ 클라이언트는 CARD:를 감지하면 로그창에 출력하지 않고 카드 값만 갱신
if (msg.rfind("CARD:", 0) == 0) {
std::string body = msg.substr(5);
if (body.rfind("TEMP:", 0) == 0) {
float v = atof(body.substr(5).c_str());
swprintf(buf, 32, L"%.1f C", v);
g_valTemp = buf;
}
// ... HUMI, PIR, FALL 동일
PostMessage(g_hWnd, WM_UPDATESENSOR, 0, 0);
continue; // 로그창 출력 스킵
}
-> 이 분리 덕분에 대화 로그가 센서 데이터로 오염되지 않음.
2. 문제 / 헷갈린 점
① 멀티스레드 환경의 MySQL 크래시 (세그멘테이션 폴트)
- 상황: 시리얼 스레드와 클라이언트 스레드가 하나의 DB 연결 객체(MYSQL*)를 공유한 상태에서 동시에 쿼리를 날리자 서버가 다운됨.
- 해결: MySQL의 연결 객체는 스레드 안전(Thread-Safe)하지 않음.
- main() 최상단에서 mysql_library_init(0, NULL, NULL); 최초 1회 의무 호출.
- 각 스레드 내부에서 mysql_thread_init() 및 mysql_init()을 통해 스레드별 독립적인 DB 커넥션을 생성하여 충돌 완벽 해결.
// main()에서 최초 1회
mysql_library_init(0, NULL, NULL);
// 각 스레드에서 독립 연결
MYSQL* dbConnect() {
mysql_thread_init();
MYSQL *conn = mysql_init(NULL);
mysql_real_connect(conn, ...);
return conn;
}
void dbDisconnect(MYSQL *conn) {
if (conn) mysql_close(conn);
mysql_thread_end();
}
② 상태조회기능 오류 : MySQL 8.0 only_full_group_by 모드 오류
- 상황: 센서별 최신값을 조회하기 위해 GROUP BY sensor_type을 사용했으나, MySQL 8.0의 엄격한 모드 설정으로 인해 문법 에러(ERROR 1055) 발생.
ERROR 1055: Expression #2 of SELECT list is not in GROUP BY clause
and contains nonaggregated column — incompatible with sql_mode=only_full_group_by
- 해결: 서브 쿼리와 INNER JOIN을 활용해 SQL 모드에 의존하지 않는 표준 쿼리로 튜닝.
SELECT s1.sensor_type, s1.value, s1.created_at
FROM sensor_logs s1
INNER JOIN (
SELECT sensor_type, MAX(id) AS max_id
FROM sensor_logs
GROUP BY sensor_type
) s2 ON s1.sensor_type = s2.sensor_type AND s1.id = s2.max_id;
③ 공유 자원(클라이언트 배열) 동시 접근 이슈
- 상황: 여러 스레드가 동시에 클라이언트 배열에 접근하여 패킷을 전송할 때 데이터가 꼬일 위험 존재.
- 해결: pthread_mutex_t를 활용하여 sendToRole() 함수 내 루프 실행 구간을 임계 영역(Critical Section)으로 묶어 상호배제(Mutex) 보장.
- TCP 소켓 통신 구조
Windows UI → connect(서버IP:9000)
→ 첫 패킷으로 "SENIOR" 또는 "GUARDIAN" 전송
→ 이후 NetPacket 주고받기
- 서버는 클라이언트마다 별도 스레드를 생성
void *tcpAcceptThread(void *arg) {
while (1) {
int client_fd = accept(server_fd, ...);
ClientArg *ca = malloc(sizeof(ClientArg));
ca->idx = addClient(client_fd);
pthread_t tid;
pthread_create(&tid, NULL, clientThread, ca);
pthread_detach(tid);
}
}
- 클라이언트 배열은 여러 스레드가 동시에 접근하므로 뮤텍스로 보호
pthread_mutex_t clients_lock = PTHREAD_MUTEX_INITIALIZER;
void sendToRole(int role, NetPacket *pkt) {
pthread_mutex_lock(&clients_lock);
for (int i = 0; i < MAX_CLIENTS; i++) {
if (clients[i].active && clients[i].role == role)
send(clients[i].fd, pkt, sizeof(NetPacket), MSG_NOSIGNAL);
}
pthread_mutex_unlock(&clients_lock);
}
sendToRole(ROLE_GUARDIAN, &pkt)을 호출하면 현재 접속 중인 모든 보호자 클라이언트에게 패킷이 전송
3. 오늘 배운 핵심
- 멀티스레드는 항상 의심해야 한다: DB 커넥션, 클라이언트 배열 등 스레드가 공유 자원을 건드릴 때는 반드시 독립 객체를 할당하거나 뮤텍스로 잠가야 시스템 크래시를 막을 수 있다.
- 데이터 한계를 설계로 극복하라: 패킷 사이즈(128Byte) 한계를 늘리는 대신, START/END 플래그를 활용한 데이터 분할 전송(HISTORY 프로토콜) 구조를 고안하여 네트워크 효율과 한계를 동시에 해결했다.
- 데이터의 목적지에 따라 채널을 분리하라: 센서 데이터(CARD:)와 채팅 데이터(CHAT)의 패킷 헤더를 분리하여, UI 상단 위젯과 하단 로그창이 독립적으로 동작하도록 설계하는 것이 깔끔한 클라이언트 렌더링의 핵심이다.
4. 다음 목표
- 한글 인코딩(UTF-8 ↔ CP949) 해결하기.
- Win32 API를 활용하여 Windows 클라이언트(SeniorUI, GuardianUI) 화면 디자인 및 컴포넌트(ListView 등) 구성.
- 수신된 CARD: 패킷과 통신하여 UI 화면의 센서 수치가 실시간으로 변하는지 연동 테스트 진행.
'포트폴리오 > [IoT 기반 피지컬 AI 교육일지]' 카테고리의 다른 글
| [ C언어 IoT 프로젝트 ] #5 Win32 API 대시보드 구현 및 최종 기능 점검 (29일차 기록) (0) | 2026.03.24 |
|---|---|
| [ C언어 IoT 프로젝트 ] #4 인코딩 에러수정, WinAPI 비동기 UI 개발 (28일차 기록) (0) | 2026.03.24 |
| [ C언어 IoT 프로젝트 ] #2 스마트홈 센서 테스트 & 시리얼 통신 (26일차 기록) (0) | 2026.03.23 |
| [ C언어 IoT 프로젝트 ] #1 고령 1인가구 안심 스마트홈 아키텍처 설계 (25일차 기록) (0) | 2026.03.23 |
| [ C++ 윈도우 소켓 프로그래밍 ] 콜백 함수와 WPARAM / LPARAM 차이 정리 (24일차 기록) (0) | 2026.03.20 |