카테고리 없음

[11주차] 라즈베리파이 원격 장치 제어 시스템 만들기

2026. 6. 7. 08:28

VEDA 주간 학습 정리

[11주차] 라즈베리파이 원격 장치 제어 시스템 만들기

이번 주 학습을 한 문장으로 정리하면: 라즈베리파이에 GPIO 핀으로 연결된 LED, 부저, 조도 센서, 7 세그먼트 FND를 TCP 통신을 이용하여 원격으로 제어하는 시스템을 만들었다.

작성일: 2026.06.06

주차 이동

◀ 저번주차: [10주차] 샤통 통신 규약 설계

GitHub: TCP-controlled-raspberrypi [비공개]

▶ 다음주차: 작성 후 링크 연결 예정

이번 주 학습 목표

  • TCP 통신을 이용하여 원격 장치 제어 명령을 주고받는 서버와 클라이언트를 구현한다.
  • 라즈베리파이 GPIO로 제어할 수 있도록 LED, 부저, 조도 센서, 7 세그먼트 FND 회로를 연결하고 각 장치 제어 코드를 작성한다.
  • 라즈베리 파이 GPIO 장치 제어 코드를 동적 공유 라이브러리 플러그인으로 분리한다.
  • 부저 재생과 FND 카운트다운처럼 오래 걸리는 작업을 스레드로 분리해 서버 응답성을 유지한다.
  • 크로스 컴파일, 배포, 데몬 실행, 핫 업데이트까지 포함한 실제 실행 가능한 구조를 완성한다.

1. 프로젝트 개요와 전체 구조

이번 프로젝트는 PC에서 실행하는 CLI 클라이언트가 TCP로 라즈베리 파이 서버에 접속하고, 서버가 전달받은 명령어를 각 하드웨어 플러그인에 라우팅하는 방식으로 동작한다. 제어 대상은 LED, 부저, 조도 센서, 7세그먼트 FND이며, 서버는 백그라운드 데몬으로 실행되도록 구성했다.

구분 핵심 역할 정리 포인트
client/ 사용자 입력을 받아 TCP 서버로 전송하는 CLI 프로그램 select()로 표준 입력과 서버 응답을 동시에 감시한다.
server/ TCP 서버, 데몬화, 명령어 파싱, 플러그인 로딩 담당 클라이언트별 스레드를 생성하고 장치 명령을 라우팅한다.
plugins/ LED, 부저, 조도 센서, FND 제어 로직 각 장치를 libled.so, libbuzzer.so, liblux.so, libfnd.so로 분리했다.
configure.sh 호스트 환경을 감지해 Makefile을 생성 macOS에서는 zig cc 기반 크로스 컴파일을 사용하도록 구성했다.
TCP 원격 장치 제어 시스템 구성 이미지

라즈베리 파이와 외부 장치를 TCP 서버를 통해 제어하는 실습 구성

2. TCP 서버와 클라이언트 흐름

서버는 60000번 포트에서 TCP 연결을 기다린다. 클라이언트가 접속하면 서버는 별도 스레드를 만들어 해당 클라이언트의 명령어를 처리하고, 입력 문자열을 분석해 LED, 부저, 조도 센서, FND 중 알맞은 플러그인으로 전달한다.

  1. 서버 기동: daemonize()로 터미널에서 분리한 뒤 백그라운드 서버로 실행한다.
  2. 클라이언트 접속: socket(), connect()로 라즈베리 파이 서버의 60000번 포트에 접속한다.
  3. 명령어 처리: 서버는 수신 문자열을 파싱하고 target 값에 따라 플러그인 함수를 호출한다.
  4. 응답 반환: 플러그인은 처리 결과를 같은 버퍼에 기록하고, 서버는 이 문자열을 클라이언트로 돌려준다.
#define TCP_PORT 60000

if ((ssock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    syslog(LOG_ERR, "socket() failed: %m");
    return -1;
}

servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(TCP_PORT);

bind(ssock, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(ssock, 8);

정리 메모

서버를 단순히 실행 파일로만 두지 않고 데몬으로 만들면서 실제 임베디드 장비에서 계속 떠 있는 서비스에 가까운 형태가 되었다. 대신 데몬 환경에서는 표준 출력이 사라지기 때문에 syslog와 별도 로그 파일을 활용해 상태를 확인하도록 설계했다.

3. 동적 공유 라이브러리 기반 플러그인 구조

장치 제어 코드를 서버 코드에 모두 넣으면 기능이 늘어날수록 서버가 복잡해지고, 일부 장치 로직만 수정해도 전체 서버를 다시 빌드해야 한다. 그래서 장치별 코드를 공유 라이브러리로 분리하고, 서버가 실행 중에 dlopen(), dlsym(), dlclose()로 플러그인을 관리하도록 만들었다.

각 플러그인은 device_main()을 통해 클라이언트 명령 문자열을 전달받은 뒤, 내부에서 서버와 독립적으로 명령어를 다시 파싱하고 처리 결과 문자열까지 직접 만든다. 덕분에 서버의 공통 라우팅 구조는 유지하면서도 타겟별로 필요한 명령어를 자유롭게 추가하거나, 장치 특성에 맞게 반환 형식을 바꿀 수 있다.

  • 각 플러그인은 공통 진입점인 device_main()과 정리 함수 device_cleanup()을 제공한다.
  • 서버는 시작 시 libled.so, libbuzzer.so, liblux.so, libfnd.so를 메모리에 로드한다.
  • update 명령이 들어오면 플러그인을 다시 컴파일하고 기존 핸들을 닫은 뒤 새 라이브러리를 로드할 수 있게 설계했다.
struct device_plugin {
    void *handle;
    int (*device_main)(char *);
    void (*cleanup)(void);
};

plugin->handle = dlopen(path, RTLD_LAZY);
plugin->device_main = (int (*)(char *))dlsym(plugin->handle, "device_main");
plugin->cleanup = (void (*)(void))dlsym(plugin->handle, "device_cleanup");

주의 / 기억할 점

  • 플러그인을 교체하는 순간에 다른 스레드가 기존 함수 포인터를 사용하면 크래시가 날 수 있다.
  • 이 문제를 막기 위해 서버는 plugin_mutex로 명령 실행과 플러그인 리로드 구간을 보호한다.

4. 장치별 플러그인 구현

클라이언트에서 입력하는 기본 명령어는 [명령어] [대상] [옵션...] 형식을 따른다. 서버는 대상 값만 보고 해당 플러그인으로 명령 문자열을 넘기고, 실제 세부 옵션 처리는 각 플러그인 내부에서 담당한다.

명령어 대상 옵션 기능
set led, buzzer, lux, fnd GPIO 핀 번호 각 장치를 제어하기 전에 GPIO 핀을 등록한다. FND는 a, b, c, d, e, f, g, dp 순서로 8개 핀을 입력한다.
do led, buzzer, fnd ON, OFF, HIGH, MID, LOW, 0~100, 1~5, 0~9 LED 밝기 제어, 부저 멜로디 재생 및 중지, FND 카운트다운 실행에 사용한다. 부저와 FND는 스레드로 실행해 다른 명령 처리가 막히지 않게 했다.
get led, lux, fnd 없음 LED 밝기, 조도 센서 HIGH/LOW 값, FND 현재 카운트 값을 조회한다. 조도 값이 LOW이면 서버가 LED ON 명령을 자동으로 주입한다.
exit 없음 없음 클라이언트 연결을 정상 종료한다.
poweroff 없음 없음 서버 데몬을 정상 종료하고 소켓과 플러그인 자원을 정리한다.
update 없음 없음 전용 스크립트(update_plugins.sh)를 실행해 최신 소스 코드를 가져오고, 플러그인을 다시 빌드한 뒤 dlclose()/dlopen()으로 서버 재시작 없이 다시 로드한다.
set led 25
do led ON
do led 50
set lux 23
get lux
set buzzer 24
do buzzer 2
set fnd 5 6 28 27 26 4 1 29
do fnd 9

정리 메모

가장 중요한 부분은 오래 걸리는 하드웨어 동작을 서버의 메인 처리 흐름에서 분리한 점이다. FND가 카운트다운 중이어도 LED 제어나 조도 조회가 가능하고, 부저가 멜로디를 재생 중이어도 OFF 명령으로 즉시 중단할 수 있다.

5. 빌드, 배포, 실행 과정

빌드는 configure.sh로 환경을 먼저 설정한 뒤 make로 진행한다. macOS arm64 환경에서는 zig cc를 이용해 aarch64-linux-gnu 타겟으로 서버와 플러그인을 크로스 컴파일했고, 빌드 결과물은 SSH와 SCP를 통해 라즈베리 파이의 작업 디렉터리로 전송되도록 구성했다.

chmod +x configure.sh
./configure.sh
make

# Raspberry Pi
cd ~/Documents/TCP-controlled-raspberrypi
./server

# Host PC
client/client Rappa3

실제 실행 로그에서는 FND, 조도 센서, 부저, LED 초기화가 정상적으로 완료되었고, LED 밝기 제어와 부저 멜로디 재생, 조도 센서 기반 LED 자동 ON, FND 카운트다운이 확인되었다.

client> set led 25
DONE: LED configured
client> do led 50
DONE: LED brightness set to 50%
client> get lux
DONE : Lux value is LOW
client> get led
DONE: LED brightness is 100%
client> do fnd 9
DONE: FND countdown started from 9

6. 구현하며 마주친 문제와 개선 포인트

기능 구현보다 더 신경 쓴 부분은 실제 장비에서 안정적으로 돌아가게 만드는 일이었다. 단순히 로컬에서 컴파일되는 코드가 아니라, 라즈베리 파이에서 데몬으로 떠 있고 여러 클라이언트 요청을 받으며 장치 제어를 수행해야 했기 때문이다.

  • 상대 경로 문제: 데몬화 후 작업 디렉터리가 바뀌면 plugins/*.so를 찾을 수 없어서 HOME 기준 프로젝트 경로로 chdir 하도록 처리했다.
  • 포트 재사용 문제: 서버 재시작 직후 Address already in use가 발생할 수 있어 SO_REUSEADDR 옵션을 적용했다.
  • 플러그인 교체 경쟁 상태: 리로드 중 명령어가 들어오면 함수 포인터 접근 문제가 생길 수 있어 plugin_mutex로 보호했다.
  • 핫 업데이트 실패 가능성: update 명령 자체는 구현했지만, 라즈베리 파이의 curl 등 실행 환경 의존성이 맞지 않으면 플러그인 업데이트 스크립트가 실패할 수 있다.

주의 / 기억할 점

  • 서버 실행 경로와 플러그인 배포 경로가 맞지 않으면 dlopen()이 실패한다.
  • 장치 제어 전에는 반드시 set 명령으로 GPIO 핀을 먼저 등록해야 한다.
  • 스레드로 동작하는 장치의 상태 변수는 mutex나 volatile 플래그로 제어 흐름을 명확히 해야 한다.

마무리

이번 실습은 TCP 통신, pthread, 동적 라이브러리, 데몬 프로세스, GPIO 제어가 한 프로젝트 안에서 연결된 점이 핵심이었다. 각각은 따로 보면 익숙한 개념일 수 있지만, 실제 장비 제어 프로그램으로 묶으니 경로, 로그, 배포, 동기화 같은 운영 관점의 문제가 함께 드러났다.

다음에 보완한다면 update_plugins.sh의 의존성 검사를 강화하고, 명령어 프로토콜을 조금 더 구조화해서 잘못된 입력에 대한 에러 메시지를 일관되게 만들고 싶다. 또한 현재 stub 상태인 rappa 자연어 제어 기능을 실제 AI 서버와 연결하면 CLI 명령어뿐 아니라 자연어 기반 장치 제어까지 확장할 수 있을 것 같다.