키오스크 PC와 PCB(하드웨어) 사이를 시리얼(RS232) 로 연결하고, 필요 시 백엔드 WebSocket 과 연동할 수 있는 Python 패키지입니다. 조명·도어·스피커 제어와 PCB 상태 조회를 담당합니다.
| 영역 | 설명 |
|---|---|
| 시리얼 통신 | PCB와 포트 연결, 프레임 송수신 (SerialManager) |
| 기기 제어 | Command L: AC/DC 조명, 도어, 스피커 (Controllerer) |
| 상태 모니터링 | Command S: 주기 폴링 또는 1회 조회, 변화 시 콜백 (StatusMonitor) |
| 프로토콜 | STX/ETX 프레임, BCC(XOR), 제어·상태·GPS(옵션) 프레임 조립·파싱 (protocol) |
| WebSocket 브릿지 | 백엔드와 WS 연결, 재연결, JSON 송수신 뼈대 (WSBridge) |
| 통합 WebView | Windows에서 내장 pywebview UI, 웹뷰 전용 WS, 스크린샷, 트레이, 오류 복구 |
| 설정 | .env 기반 포트, WS URL, 폴링 주기 등 (config) |
main.py는 시리얼 연결 → 상태 폴링을 기본으로 돌리는 실행 예시입니다. PCB 제어는 앱 코드에서 Controllerer로 시리얼만 쓰면 되고, WebSocket URL 이 있으면 백엔드 원격 제어 브릿지도 함께 붙습니다. 그때 수신한 type: "control" JSON은 PcbControlInput 검증 후 send_control로 PCB에 반영됩니다. (형식은 백엔드 → 키오스크 제어 메시지 참고.)
제어 필드 미설정 시 동작: Command
L제어 프레임은 "제어하는 모듈만" 0/1 등의 값을 싣고, 설정하지 않은(=None) 필드는 자동으로NO_CHANGE(값9) 로 전송됩니다. PCB는9를 받은 모듈의 기존 상태를 유지합니다. 따라서 이 모듈은 더 이상 "PCB 상태를 캐시해두고 합쳐 보내는" 방식이 아닙니다.
- Python 3.13 이상 (필수)
- uv — 패키지·가상환경 관리
- OS: Windows(일반적으로
COMn포트), macOS/Linux(tty.*등)
uv 설치 후 프로젝트 루트에서:
uv sync테스트까지 쓰려면 개발 그룹 포함:
uv sync --group dev의존성은 pyproject.toml에 정의되어 있으며, 잠금 파일은 uv.lock입니다.
주요 패키지: pyserial, websockets, python-dotenv, pydantic, pywebview, pillow, pystray.
프로젝트 루트에 .env를 두면 kiosk_module.config가 읽습니다.
| 변수 | 기본값 | 설명 |
|---|---|---|
SERIAL_PORT |
COM3 |
시리얼 포트 이름 |
WS_RECONNECT_INTERVAL |
5.0 |
끊김 후 재연결 대기(초) |
DEVICE_ID |
(비움) | 백엔드 장치 식별자. API/WebSocket 모두 device_id로 전송 |
WEBSOCKET_ADDR |
(비움) | 백엔드·웹뷰 공통 WS URL. device_id 쿼리는 실행 시 자동 추가 |
WEBVIEW_DEVTOOLS |
false |
pywebview 개발자 도구 노출 |
LOG_LEVEL |
INFO |
로그 레벨 (DEBUG, INFO, …) |
uv run python main.py- 시리얼 연결에 실패하면 프로세스가 종료됩니다.
- Ctrl+C로 종료하면 (켜져 있으면) WS 종료, 시리얼 닫기 순으로 정리합니다.
- 실행 모드는
ASSET_DEVICE_TYPE에 따라 결정됩니다.
uv add --group dev pyinstaller
uv run pyinstaller --onefile main.py일회성으로만 쓰려면 uv tool run pyinstaller --onefile main.py 도 가능합니다.
main.py 를 PyInstaller 로 단일 .exe 로 묶고 default.env 를 함께 번들합니다.
반드시 Windows 에서 빌드해야 Windows .exe 가 나옵니다.
:: Windows 에서
build_exe.bat또는 수동으로:
uv sync
uv add --group dev pyinstaller
uv run pyinstaller kiosk_gui.spec --clean --noconfirm산출물: dist/JDoneKiosk.exe
버전은 VERSION 파일에서 관리합니다. 배포 버전을 올릴 때는 이 파일의 값을 변경한 뒤 빌드하면
JDoneKiosk.exe 파일명은 그대로 유지되고, Windows 파일 속성의 버전 정보만 갱신됩니다.
런타임 동작:
.exe옆 디렉터리 가 사용자 데이터 루트.- 첫 실행 시 번들된
default.env(= 현재.env) 가.exe 옆/.env로 1회 복사됩니다. 이미.env가 있으면 그대로 둡니다 — 업데이트 후에도 사용자 설정 유지. - 설정 변경은
.env파일을 직접 수정한 뒤 앱을 재시작합니다.
watchdog_gui.py 를 PyInstaller 로 단일 .exe 로 묶습니다.
Watchdog는 watchdog_config.json 에 등록된 프로그램을 주기적으로 확인하고,
프로그램이 종료되면 지정한 대기 시간 뒤 다시 실행합니다.
:: Windows 에서
build_watchdog.bat또는 수동으로:
uv sync --group watchdog
uv add --group dev pyinstaller
uv run pyinstaller watchdog_gui.spec --clean --noconfirm산출물: dist/Watchdog.exe
사용 흐름:
Watchdog.exe실행[등록]버튼으로JDoneKiosk.exe와 함께 감시할 보조 프로그램 2~3개를 추가[저장]후[감시 시작]- 부팅 후 자동 감시가 필요하면
Windows 로그인 시 Watchdog 자동 실행,Watchdog 시작 시 감시 자동 시작,자동 실행 시 트레이로 시작체크
런타임 파일:
watchdog_config.json— 등록 프로그램/옵션 저장watchdog.log— 시작, 종료 감지, 재시작 실패/성공 로그- 개발 모드에서는 프로젝트 루트, exe 모드에서는
Watchdog.exe옆에 생성됩니다.
uv sync --group dev
uv run pytestunittest만 쓸 경우:
uv run python -m unittest tests.test_protocol -vpyproject.toml # 프로젝트 메타·의존성 (requires-python >= 3.13)
uv.lock # 잠금 파일 (uv)
.python-version # 로컬 기본 Python (3.13)
kiosk_module/
__init__.py # 공개 API re-export
protocol.py # 명령·프레임·파서·응답 dataclass
serial_manager.py # 시리얼 열기/닫기, 송수신
device_controller.py # 제어 + PcbControlInput
status_monitor.py # 상태 폴링·콜백
ws_bridge.py # WebSocket 연결·송수신
config.py # 환경변수 설정
main.py # 데모 엔트리포인트
프레임 공통 형식: STX(0x02) | COMMAND | DATA… | BCC | ETX(0x03)
BCC는 COMMAND부터 BCC 직전까지 바이트를 XOR합니다.
| 명령 | 코드 | 용도 |
|---|---|---|
| 제어 | L (0x4C) |
아래 표 참고 (조명·도어·스피커) |
| 상태 | S (0x53) |
PCB 상태 요청/응답 |
송신 프레임은 총 12바이트입니다. DATA 각 필드는 해당 모듈을 제어할 때만 값(0/1 등)을 싣고, 제어하지 않는 모듈은 9(NO_CHANGE) 를 실어 보내면 PCB가 해당 모듈의 기존 상태를 유지합니다.
| 순서 | 필드 | 설명 |
|---|---|---|
| 1 | STX | 시작 (0x02) |
| 2 | COMMAND | 문자 L (0x4C) |
| 3 | AC 조명1 | 1바이트 (LightMode, 미제어=9) |
| 4 | AC 조명2 | 1바이트 (미제어=9) |
| 5 | DC 조명1 | 1바이트 (LightMode, 미제어=9) |
| 6 | DC 조명2 | 1바이트 (미제어=9) |
| 7 | DC 조명1 밝기 | 1바이트 (0~10, 미제어=9) |
| 8 | DC 조명2 밝기 | 1바이트 (0~10, 미제어=9) |
| 9 | DOOR 동작 | 1바이트 (DoorAction, 미제어=9) |
| 10 | 스피커 전환 | 1바이트 (SpeakerMode, 미제어=9) |
| 11 | BCC | 체크섬 |
| 12 | ETX | 종료 (0x03) |
열거형 값은 protocol 모듈의 LightMode, DoorAction, SpeakerMode를 참고하세요. NO_CHANGE = 9 상수도 같은 모듈에 정의되어 있습니다.
코드에서는 PcbControlInput(Pydantic, None = 미제어) 필드명이 위 순서와 대응하며, FrameBuilder.build_control_frame(...)이 미설정 필드를 자동으로 9로 채워 송신합니다.
PCB 제어 필드는 PcbControlInput 한 모델로 고정되어 있습니다. 정의되지 않은 키는 허용하지 않습니다(extra="forbid").
| 필드 | 타입 | 설명 |
|---|---|---|
ac_light1 |
LightMode |
AC 조명1 (OFF/ON) |
ac_light2 |
LightMode |
AC 조명2 |
dc_light1 |
LightMode |
DC 조명1 (OFF/ON/DIMMING) |
dc_light2 |
LightMode |
DC 조명2 |
dc_light_brightness1 |
int (0~10) |
DC 조명1 밝기 |
dc_light_brightness2 |
int (0~10) |
DC 조명2 밝기 |
door |
DoorAction |
OFF / OPEN / CLOSE |
speaker |
SpeakerMode |
OFF / MAIN |
send_control에 넘긴 모델에서 실제로 설정된 필드만 해당 모듈의 제어 값으로 송신되고, 설정하지 않은(=None) 필드는 프레임에 NO_CHANGE(9)가 실려 PCB가 해당 모듈의 상태를 건드리지 않습니다. 별도로 PCB 상태를 읽어 합쳐 보내는 단계는 없습니다.
set_ac_light(on: bool, *, channel=1|2)— AC 조명 1 또는 2 (channel기본값 1)set_dc_light(mode=..., brightness=..., *, channel=1|2)— DC 조명·밝기 1 또는 2open_door()/close_door()set_speaker(on: bool)all_off()/all_on()
현재 PCB 상태는 StatusMonitor.last_status / StatusMonitor.to_dict()로 확인하세요.
poll_once(): 동기로 1회 상태 요청 후StatusResponse또는None.start_polling(interval)/stop_polling(): 비동기 주기 폴링.
| 콜백 | 시점 |
|---|---|
on_status_received |
상태 응답을 받을 때마다 |
on_status_changed |
이전 대비 상태 필드가 바뀐 경우 |
on_person_detected |
사람 감지 값이 바뀐 경우 |
on_button_pressed |
좌·우 중 하나라도 0→눌림 엣지일 때 ButtonPressEvent 1회 |
JSON으로 넘기기 좋은 형태는 monitor.to_dict() (마지막 상태 없으면 None).
connect(): 연결 후 수신 루프 (끊기면WS_RECONNECT_INTERVAL만큼 대기 후 재시도).disconnect(): 연결 종료.send(data: dict): JSON 직렬화 후 전송.send_status():monitor.to_dict()를"type": "status"형태로 전송 (필요 시 수정 가능).
수신 처리는 on_message: Callable[[dict], None] 에 핸들러를 등록합니다. WebSocket URL 이 있을 때 연결하고, 아래 규약의 제어 메시지를 처리합니다.
백엔드가 키오스크 모듈로 장비 제어를 보낼 때는 최상위에 "type": "control" 을 두고, PcbControlInput과 동일한 필드명을 사용합니다. 문자열 값은 LightMode / DoorAction / SpeakerMode의 이름과 같아야 합니다 (OFF, ON, DIMMING, OPEN, CLOSE, MAIN 등).
| 필드 | 예시 값 | 설명 |
|---|---|---|
ac_light1, ac_light2 |
"ON", "OFF" |
AC 조명 |
dc_light1, dc_light2 |
"ON", "OFF", "DIMMING" |
DC 조명 모드 |
dc_light_brightness1, dc_light_brightness2 |
0~255 (정수) |
DC 밝기 (프로토콜 전송 시 하드웨어 범위로 클램프됨) |
door |
"OPEN", "CLOSE", "OFF" |
도어 |
speaker |
"MAIN", "OFF" |
스피커 |
부분 제어: 메시지에 실제로 넣은 필드만 해당 모듈 제어 명령으로 전송되고, 빠진 필드는 프레임에 9(NO_CHANGE)가 실려 PCB가 해당 모듈의 기존 상태를 유지합니다 (send_control 동작과 동일).
예시 (한 번에 하나만내도 되고, 필요하면 여러 필드를 한 객체에 넣어도 됩니다).
{"type": "control", "ac_light1": "ON"}{"type": "control", "dc_light1": "ON", "dc_light_brightness1": 10}{"type": "control", "dc_light2": "ON", "dc_light_brightness2": 10}{"type": "control", "door": "OPEN"}{"type": "control", "speaker": "MAIN"}type이 "control"이 아니거나, 제어 필드가 하나도 없으면 핸들러는 무시합니다. 필드명·값이 스키마와 맞지 않으면 로그에 검증 오류가 남고 전송하지 않습니다.
kiosk_module/__init__.py에서 다음 심볼을 제공합니다.
FrameBuilder,FrameParser,calc_bcc,NO_CHANGESerialManagerControllerer,PcbControlInputStatusMonitorWSBridge
- 시리얼 열기 실패: 포트 이름(
SERIAL_PORT)과 다른 프로그램의 포트 점유 여부를 확인하세요. - 상태 응답 없음: 케이블, 보드레이트, PCB 전원 및 프로토콜 일치 여부를 확인하세요.
- WebSocket만 필요 없음:
SerialManager+Controllerer/StatusMonitor만 임포트해 별도 앱에서 사용할 수 있습니다.
[DOOR] uv run pytest tests/hw/test_door_open.py -v -s uv run pytest tests/hw/test_door_close.py -v -s
[LIGHT1] uv run pytest tests/hw/test_light1_on.py -v -s uv run pytest tests/hw/test_light1_off.py -v -s
[LIGHT2] uv run pytest tests/hw/test_light2_on.py -v -s uv run pytest tests/hw/test_light2_off.py -v -s
[LIGHT3] uv run pytest tests/hw/test_light3_on.py -v -s uv run pytest tests/hw/test_light3_dim.py -v -s --dim-level=3 uv run pytest tests/hw/test_light3_off.py -v -s
[LIGHT4] uv run pytest tests/hw/test_light4_on.py -v -s uv run pytest tests/hw/test_light4_dim.py -v -s --dim-level=3 uv run pytest tests/hw/test_light4_off.py -v -s
[SPEAKER] uv run pytest tests/hw/test_speaker_on.py -v -s uv run pytest tests/hw/test_speaker_off.py -v -s