웹에서 라즈베리파이 LED 유뮤선 제어(macOS) > 스마트기기시스템

본문 바로가기

[실습] 웹에서 라즈베리파이 LED 유뮤선 제어(macOS)

필기자
2026-05-07 14:59 14 0

본문

웹에서 라즈베리파이 LED 유무선 제어

목적
  • 아파치 웹서버와 파이썬 Socket.IO 서버를 구성한다.
  • 내 PC(macOS)에서 웹 브라우저(크롬)로 라즈베리파이 LED를 제어한다.

목차
1. 전체 시스템 이해 및 시스템 설치
2. 프론트엔드 및 서버프로그램 개발

1. 전체 시스템 이해 및 시스템 설치
  • 구성요소
    • 브라우저 (Chrome)
      • 사용자가 시스템에 접속하는 클라이언트 도구임.
        • Apache 웹 서버에 접속해 프론트엔드 프로그램을 로드함.
        • 브라우저는 사용자가 웹 주소를 입력할 때 처음 실행되는 프로그램으로, HTML/CSS/JS로 구성된 화면을 보여주는 역할을 함.
    • Apache 웹 서버 (포트 80)
      • HTML, CSS, JavaScript 등 정적 리소스를 브라우저에 제공함.
      • 실시간 통신은 담당하지 않음.
      • Apache는 가장 널리 사용되는 오픈소스 웹 서버로, 정적 파일을 브라우저에 전달해 웹페이지를 구성하는 역할을 함.
    • Python Socket.IO 서버 (포트 5000)
      • 웹소켓 기반의 실시간 양방향 통신을 처리함.
      • 프론트엔드의 LED 제어 요청 및 상태 응답을 처리함.
      • Socket.IO는 WebSocket을 이용해 서버와 클라이언트 간 실시간 데이터를 빠르게 주고받도록 도와주는 라이브러리임.

3531074817_lEsKo7Am_4be42e06f202df0c8d1a2baf32296e798d8b407b.png
  • 동작 흐름
    • 브라우저가 Apache 서버에 HTTP 요청을 보냄.
    • Apache 서버가 프론트엔드 프로그램을 브라우저에 제공함.
    • 프론트엔드는 Socket.IO 서버와 WebSocket 연결을 생성함.
    • 사용자 동작에 따라 LED 제어 명령을 서버에 전송함.
    • 서버는 GPIO를 통해 하드웨어를 제어하고 상태를 응답함.
3531074817_lcOs42I0_53340321967e922d7af86b8d7e2c0335da25b3d1.png
  • 구성 이유
    • 정적 콘텐츠와 동적 통신 분리
      • Apache는 HTML, CSS, JS 같은 정적인 파일만 제공하므로 요청 처리 속도가 빠르고 안정적임.
      • 실시간 통신은 별도의 Socket.IO 서버가 담당하여 처리 부담을 분산함.
    • 역할별 모듈화
      • 웹 서버와 실시간 서버를 분리하면 유지보수와 확장에 유리함.
      • 각 모듈의 기능이 명확하게 분리되어 구조가 단순하고 이해하기 쉬움.
    • 실시간 제어에 적합한 구조
      • LED 같은 물리적 장치는 빠른 반응이 필요하므로 WebSocket 기반 통신이 적합함.
      • Socket.IO는 실시간 이벤트 기반 구조로 지연 없이 상태 전송이 가능함.
    • 개발 및 테스트 용이
      • 프론트엔드는 Apache로 언제든 새로고침 테스트 가능하고,
      • 서버 로직은 Python 단에서 독립적으로 개발 및 디버깅 가능함.
  • macOS에서 Apache 서버 활성화
    • macOS는 Apache가 기본 내장되어 있어 별도 설치(Homebrew 등) 불필요
    • apachectl 명령으로 시작·중지·재시작·문법검사 제어
    • HTTP 응답은 curl로, 프로세스는 ps로, 에러는 /var/log/apache2/error_log로 확인
    • DocumentRoot 기본 경로: /Library/WebServer/Documents
    • launchctl로 부팅 시 자동 시작 등록/해제 가능
    • DocumentRoot 권한을 현재 사용자로 변경해 sudo 없이 파일 편집 가능하게 함


# 아파치 버전 확인
httpd -v

Server version: Apache/2.4.58 (Unix)
Server built:   Aug 30 2024 04:27:22
 


# 아파치 시작
sudo apachectl start

Password:
httpd: Could not reliably determine the server's fully qualified domain name, using ::1. Set the 'ServerName' directive globally to suppress this message
 


# HTTP 응답 확인 (기본 페이지)
curl http://localhost

<html><body><h1>It works!</h1></body></html>
 


# 아파치 동작 확인 (프로세스)
ps -ef | grep httpd

   0  1234     1   0  9:00AM ??        0:00.05 /usr/sbin/httpd -D FOREGROUND
  70  1235  1234   0  9:00AM ??        0:00.00 /usr/sbin/httpd -D FOREGROUND
  70  1236  1234   0  9:00AM ??        0:00.00 /usr/sbin/httpd -D FOREGROUND
 501  1237  1100   0  9:01AM ttys000    0:00.00 grep httpd
 


# 아파치 설정 파일 문법 검사 (재시작 전 권장)
sudo apachectl configtest

Password:
Syntax OK
 


# 아파치 재시작 (설정 변경 후)
sudo apachectl restart

httpd: Could not reliably determine the server's fully qualified domain name, using ::1. Set the 'ServerName' directive globally to suppress this message
 


# 아파치 중지
sudo apachectl stop

 


# 에러 로그 확인 (최근 10줄)
tail /var/log/apache2/error_log

[Mon Nov 04 09:00:01.123456 2024] [mpm_prefork:notice] [pid 1234] AH00163: Apache/2.4.58 (Unix) configured -- resuming normal operations
[Mon Nov 04 09:00:01.234567 2024] [core:notice] [pid 1234] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND'
 


# 아파치 부팅 시 자동 시작 등록
sudo launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist

 


# 아파치 부팅 시 자동 시작 해제
sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist

 


# 도큐먼트 루트 폴더 소유권 변경
sudo chown -R $(whoami):staff /Library/WebServer/Documents


2. 프론트엔드 및 서버프로그램 개발
  • Apache DocumentRoot에 프론트프로그램 개발
    • VS Code로 /Library/WebServer/Documents 폴더 전체를 열어 작업 (index.html 외 style.css·app.js 등 추가 파일도 같이 만들기 위함)
    • code 명령은 VS Code에서 [Cmd+Shift+P] → 'Shell Command: Install code command in PATH' 활성화 후 사용 가능
    • VS Code 사이드바의 'New File' 아이콘으로 index.html 등 파일 생성
    • 저장 후 브라우저에서 http://localhost 접속하여 확인


# VS Code로 DocumentRoot 폴더 열기
code /Library/WebServer/Documents/


<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>LED 제어</title>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
</head>
<body>
  <h3>LED 제어</h3>
  <label><input type="radio" name="led" value="on"> ON</label>
  <label><input type="radio" name="led" value="off"> OFF</label>
  <p id="status">LED 상태: 알 수 없음</p>

  <script>
    const socket = io("http://라즈베리파이_IP:5000");

    socket.on("connect", function() {
      socket.emit("get_led_status");
    });

    socket.on("led_status", function(data) {
      $("#status").text("LED 상태: " + data.state);
      $("input[name='led'][value='" + data.state + "']").prop("checked", true);
    });

    $("input[name='led']").change(function() {
      const state = $(this).val();
      socket.emit("led_control", { state: state });
    });
  </script>
</body>
</html>


Raspberry Pi 기반 목업(mock-up) 시스템이므로 macOS에 파이썬 가상환경 구성
  • 파이썬 가상환경 설정
    • 가상환경 설정을 위해 홈폴더 이동: cd ~
    • macOS에는 Python 3가 기본 포함 (Xcode Command Line Tools 또는 Homebrew). 미설치 시 brew install python 또는 python.org에서 설치
    • 가상환경 디렉토리 생성 및 이동: mkdir iot && cd iot
    • 가상환경 설정: python3 -m venv iot
    • 가상환경 활성화: source iot/bin/activate
    • 가상환경 비활성화: deactivate


# 홈폴더로 이동
cd ~
# Python 3 설치 확인 (버전 출력되면 정상)
python3 --version
# 가상환경 디렉토리 생성 및 이동
mkdir iot && cd iot
# 가상환경 생성
python3 -m venv iot
# 가상환경 활성화
source iot/bin/activate
# 가상환경 비활성화 (필요할 때만)
deactivate

user@MacBook ~ % python3 --version
Python 3.11.6
 
  • 파이썬 가상환경 환경변수 등록
    • 환경변수 등록을 위해 ~/.zshrc 파일 수정 (macOS Catalina+ 기본 셸은 zsh, bash 사용자는 ~/.bash_profile 수정)
    • VS Code로 직접 편집


# VS Code로 zshrc 편집
code ~/.zshrc
# 제일 하단에 아래 한 줄 추가
alias iot_='source ~/iot/iot/bin/activate && cd ~/iot/'
# 저장하고 종료

# 환경변수 적용
source ~/.zshrc
# iot_ 명령 입력 시 가상환경 활성화 + 가상환경 폴더로 이동
iot_

 
  • macOS 환경에서 가상 GPIO 사용
    • 라즈베리파이 환경이 아닌 macOS 같은 테스트 환경에서 사용
    • GPIO 코드 사용 시 에러 대체
    • ~/iot 폴더 안에 mockgpio.py 파일 생성 (VS Code로 ~/iot 폴더 열어두면 편리)


# VS Code로 iot 폴더 열기
code ~/iot


# mockgpio.py

class MockGPIO:
    BCM = 'BCM'
    BOARD = 'BOARD'
    OUT = 'OUT'
    IN = 'IN'
    LOW = 0
    HIGH = 1

    _pin_states = {}

    def setmode(self, mode):
        print(f"[MOCK] GPIO mode set to {mode}")

    def setup(self, pin, mode):
        self._pin_states[pin] = self.LOW
        print(f"[MOCK] GPIO pin {pin} set as {mode}")

    def output(self, pin, value):
        self._pin_states[pin] = value
        print(f"[MOCK] GPIO pin {pin} output set to {value}")

    def input(self, pin):
        value = self._pin_states.get(pin, self.LOW)
        print(f"[MOCK] GPIO pin {pin} read as {value}")
        return value

    def cleanup(self):
        self._pin_states.clear()
        print("[MOCK] GPIO cleanup complete")

GPIO = MockGPIO()

 
  • iot 가상환경에 Socket.IO 서버 프로그램 개발
    • iot_ 명령으로 가상환경 진입 후 pip install flask flask-socketio eventlet 설치
    • ~/iot 폴더 안에 iot_socket.py 파일 생성


user@MacBook ~ % iot_
(iot) user@MacBook iot % pip install flask flask-socketio eventlet


from flask import Flask
from flask_socketio import SocketIO
from flask import request
try:
    import RPi.GPIO as GPIO
except (ImportError, RuntimeError):
    from mockgpio import GPIO  # 인스턴스를 직접 가져옴
# GPIO 초기화
LED_PIN = 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)
# Flask 및 SocketIO 설정
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*", async_mode="eventlet")
# 현재 LED 상태 반환 함수
def get_led_state():
    return "on" if GPIO.input(LED_PIN) else "off"
# 클라이언트로부터 제어 요청 수신
@socketio.on("led_control")
def control_led(data):
    state = data.get("state")
    if state == "on":
        GPIO.output(LED_PIN, GPIO.HIGH)
    elif state == "off":
        GPIO.output(LED_PIN, GPIO.LOW)
    # 상태 응답
    state = get_led_state()
    print(f"led 상태 : {state}")
    socketio.emit("led_status", {"state": state}, room=request.sid)
# 초기 상태 요청 처리
@socketio.on("get_led_status")
def handle_status_request():
    state = get_led_state()
    print(f"led 상태 : { get_led_state()}")
    socketio.emit("led_status", {"state": state}, room=request.sid)
# 서버 실행
if __name__ == "__main__":
    socketio.run(app, host="0.0.0.0", port=5000)

댓글목록0

등록된 댓글이 없습니다.
게시판 전체검색