headless 시뮬레이터
pdcomm-simulator-headless는 화면 없이 실행하는 SECS/GEM 시뮬레이터입니다. 정해진 시나리오를 실행하고 결과를 한 줄에 JSON 객체 하나씩(JSON Lines) 표준 출력에 씁니다. 종료 코드로 성공과 실패를 알려 주므로 자동 시험이나 CI 스크립트에 넣기 좋습니다.
headless 시뮬레이터로 다음을 할 수 있습니다.
- 내부에서 호스트와 장비 한 쌍을 만들어 기능별 왕복을 확인합니다.
- 시뮬레이터 장비나 호스트 하나만 띄워 자신의 프로그램과 연결합니다.
- GUI에서 저장한 프로젝트의 프로필과 시나리오를 실행하고 추적(trace) 파일을 남깁니다.
시작하기 전에
섹션 제목: “시작하기 전에”- 개발 환경 준비의 환경 변수(
PDCOMM_SIMULATOR)를 정합니다. - 실행 파일은 Windows에서
$Simulator\pdcomm-simulator-headless.exe, Linux에서"$PDCOMM_SIMULATOR/pdcomm-simulator-headless"입니다. Linux의 이 파일은 라이브러리 경로를 정한 뒤bin/의 실제 프로그램을 실행하는 스크립트입니다.
이 페이지의 명령은 Linux와 macOS 셸 기준입니다. Windows에서는 & "$Simulator\pdcomm-simulator-headless.exe" ...로 실행하고 $LASTEXITCODE로 종료 코드를 확인합니다.
내장 시나리오를 실행합니다
섹션 제목: “내장 시나리오를 실행합니다”"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario status-round-trip; echo "exit=$?"{"schema":1,"sequence":1,"scenario":"status-round-trip","scope":"pair","kind":"start","detail":"scenario-started","truncated":false}{"schema":1,"sequence":2,"scenario":"status-round-trip","scope":"equipment","kind":"endpoint","local_address":"127.0.0.1:49900","truncated":false}...{"schema":1,"sequence":7,"scenario":"status-round-trip","scope":"host","kind":"result","detail":"status-name","id":1001,"id_present":true,"value_present":false,"format":0,"preview":"","name":"EquipmentState",...}...{"schema":1,"sequence":10,"scenario":"status-round-trip","scope":"host","kind":"result","detail":"status-value","id":1001,"id_present":true,"value_present":true,"format":16,"preview":"IDLE",...}...{"schema":1,"sequence":15,"scenario":"status-round-trip","scope":"pair","kind":"summary","outcome":"passed","exit_code":0,"elapsed_ms":26,"records_emitted":15,"max_records":128,"truncated":false,...}exit=0--scenario를 생략하면 smoke를 실행합니다. 주소의 포트 번호와 elapsed_ms는 실행할 때마다 다릅니다.
시나리오 목록
섹션 제목: “시나리오 목록”| 범위 | --scenario 값 |
확인하는 내용 |
|---|---|---|
| 기본 | smoke |
연결, Select, 통신 수립 |
status-round-trip |
상태 변수 이름(S1F11)과 값(S1F3) | |
data-report-round-trip |
데이터 변수와 이벤트 보고 | |
directory-round-trip |
데이터·이벤트 디렉터리(S1F21, S1F23) | |
| 조회·설정·명령 | report-query-round-trip |
보고서 조회 |
remote-report-config-round-trip |
호스트의 보고서 정의·연결·사용 설정(S2F33, S2F35, S2F37) | |
typed-command-round-trip |
원격 명령 | |
| 저장·상수 | project-round-trip |
프로젝트 문서 저장과 복원 |
constants-snapshot-parity |
장비 상수 스냅샷 | |
| 가상·선언형 | secs1-virtual-pair |
SECS-I 가상 쌍 |
gem300-capability-profile-lifecycle |
GEM300 기능 카탈로그 | |
| 정형 공정 프로그램 | verification-report-round-trip, formatted-program-transfer-round-trip, formatted-program-store-reverse-round-trip, formatted-program-query-round-trip |
검증 보고, 전송, 저장, 조회 |
formatted-program-selection-lifecycle, formatted-program-dry-run, formatted-program-execution-lifecycle |
선택, 구조 분석, 실행 | |
formatted-program-verification-request-round-trip, formatted-program-capability-round-trip |
검증 요청, 기능 조회 | |
formatted-program-verification-negotiation-round-trip, formatted-program-verification-negotiation-reverse-round-trip, formatted-program-verification-negotiation-denied |
검증 협상 | |
| 전송 장애 | transport-backpressure-recovery |
쓰기 배압(backpressure)과 회복. --transport-fault-profile write_backpressure가 필요합니다. |
| 외부 프로그램 | external-peer |
외부 호스트의 S1F13과 S1F1을 기다립니다. 아래 절을 참고합니다. |
...-denied 시나리오의 성공(exit_code 0)은 거부가 의도대로 일어나고 상태가 보존되었다는 뜻입니다. 각 시나리오의 내부 장비 카탈로그는 시나리오마다 정해져 있으며, GUI의 기본 카탈로그와 같다고 가정하지 않습니다.
출력 형식
섹션 제목: “출력 형식”모든 줄에 다음 필드가 있습니다.
| 필드 | 설명 |
|---|---|
schema |
출력 형식 버전. 현재 1입니다. |
sequence |
1부터 증가하는 줄 번호 |
scenario |
실행한 시나리오 이름. 프로젝트 모드에서는 시나리오 ID, 인자 오류에서는 invalid입니다. |
scope |
기록의 주체: pair, host, equipment, secs1, project, transport, trace, cli 등 |
kind |
start, endpoint, request, result, error, summary 등 |
detail |
단계 이름(예: begin-communication, status-value) |
truncated |
이 줄의 값이 잘렸는지 여부 |
마지막 줄은 항상 "kind":"summary"입니다.
| 필드 | 설명 |
|---|---|
outcome |
passed, failed, timeout, interrupted, invalid-arguments(인자 오류), invalid-input(입력 파일 오류) |
exit_code |
프로세스 종료 코드와 같습니다. |
elapsed_ms |
실행 시간(밀리초) |
records_emitted, max_records |
출력한 줄 수와 상한(128). 상한에 닿으면 이후 기록은 버리고 truncated가 true가 됩니다. |
종료 코드
섹션 제목: “종료 코드”| 코드 | 의미 |
|---|---|
| 0 | 성공 |
| 2 | 인자 또는 입력 파일 오류 |
| 3 | 시나리오 실패(프로토콜 오류, 실패한 요청 등) |
| 4 | 시간 초과(--timeout-ms) |
| 130 | Ctrl+C 또는 SIGTERM으로 중단 |
알 수 없는 시나리오를 주면 다음처럼 끝납니다.
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario nope; echo "exit=$?"{"schema":1,"sequence":1,"scenario":"invalid","scope":"cli","kind":"error","detail":"scenario is unsupported","truncated":false}{"schema":1,"sequence":2,"scenario":"invalid","scope":"cli","kind":"summary","outcome":"invalid-arguments","exit_code":2,...}exit=2| 옵션 | 설명 |
|---|---|
--scenario <이름> |
실행할 시나리오. 기본값은 smoke입니다. 프로젝트 모드에서는 프로젝트의 시나리오 ID입니다. |
--session-id <0–65535> |
HSMS 세션 ID. 기본값은 0입니다. 외부 프로그램과 연결할 때 상대와 맞춥니다. |
--timeout-ms <100–60000> |
시나리오 제한 시간. 기본값은 8000입니다. |
--role host|equipment 와 --address <IPv4:port> |
한쪽 역할만 실행합니다. 두 옵션을 함께 주며 smoke 시나리오에만 쓸 수 있습니다. |
--project <파일> |
GUI에서 저장한 프로젝트 파일. --scenario에 프로젝트의 시나리오 ID를 줍니다. |
--trace-output <파일> |
프로젝트 모드에서 실행한 세션의 추적을 파일로 저장합니다. |
--trace-input <파일> |
저장한 추적 파일의 형식만 검사합니다. 다른 옵션과 함께 쓸 수 없습니다. |
--transport-fault-profile write_backpressure |
transport-backpressure-recovery 시나리오에서 쓰기 배압을 일으킵니다. |
--help 옵션은 없습니다. 알 수 없는 옵션을 주면 종료 코드 2로 끝납니다.
내 호스트 프로그램을 시험합니다
섹션 제목: “내 호스트 프로그램을 시험합니다”external-peer 시나리오는 시뮬레이터 장비를 127.0.0.1의 빈 포트에 띄우고, 외부 호스트가 S1F13(통신 수립)과 S1F1(Are You There)을 보낸 뒤 연결을 끊을 때까지 기다립니다. 자신의 호스트 프로그램을 자동으로 시험할 때 씁니다.
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario external-peer --session-id 7 --timeout-ms 20000두 번째 줄에 장비 주소가 나옵니다.
{"schema":1,"sequence":2,"scenario":"external-peer","scope":"equipment","kind":"endpoint","local_address":"127.0.0.1:50092","truncated":false}이 주소로 호스트를 연결합니다. 여기서는 튜토리얼 C++ 호스트로 establish, identity, quit을 차례로 입력합니다.
"$PDCOMM_ROOT/build/pdcomm-tutorial" host active 127.0.0.1:50092호스트가 끊으면 시뮬레이터가 관찰한 내용과 요약을 출력하고 종료 코드 0으로 끝납니다.
{"schema":1,"sequence":3,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"s1f13","truncated":false}{"schema":1,"sequence":4,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"s1f13-system-bytes=101","truncated":false}{"schema":1,"sequence":5,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"s1f1","truncated":false}{"schema":1,"sequence":6,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"s1f1-system-bytes=102","truncated":false}{"schema":1,"sequence":7,"scenario":"external-peer","scope":"equipment","kind":"result","detail":"disconnect","truncated":false}{"schema":1,"sequence":8,"scenario":"external-peer","scope":"pair","kind":"summary","outcome":"passed","exit_code":0,"elapsed_ms":2942,...}- 시스템 바이트(system bytes)는 호스트가 정한 값입니다. 튜토리얼 호스트는 100부터 시작하므로 101, 102가 나옵니다.
- 제한 시간 안에 두 메시지를 받지 못하면
"outcome":"timeout", 종료 코드 4로 끝납니다. 기본 제한 시간은 8초이므로 손으로 입력할 때는--timeout-ms를 늘립니다. - 세션 ID가 다르면 데이터 메시지를 처리하지 못합니다. 튜토리얼 예제는 세션 ID 7을 씁니다.
한쪽 역할만 실행합니다
섹션 제목: “한쪽 역할만 실행합니다”--role과 --address를 주면 시뮬레이터가 장비나 호스트 하나만 실행합니다.
장비로 실행하면 지정한 주소에서 기다립니다. 호스트가 통신을 수립한 뒤 연결을 끊으면 성공으로 끝납니다. 이 장비는 상태 변수 1001(EquipmentState, ASCII IDLE)을 등록합니다.
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --role equipment --address 127.0.0.1:5001 --session-id 7 --timeout-ms 5000호스트로 실행하면 지정한 주소의 장비에 연결해 통신을 수립하고 끝납니다. 튜토리얼 장비(equipment passive 127.0.0.1:5006)를 먼저 실행한 뒤 다음을 실행합니다.
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --role host --address 127.0.0.1:5006 --session-id 7{"schema":1,"sequence":1,"scenario":"smoke","scope":"host","kind":"start","detail":"scenario-started","truncated":false}{"schema":1,"sequence":2,"scenario":"smoke","scope":"host","kind":"endpoint","local_address":"127.0.0.1:55752","truncated":false}{"schema":1,"sequence":3,"scenario":"smoke","scope":"host","kind":"request","detail":"begin-communication","truncated":false}{"schema":1,"sequence":4,"scenario":"smoke","scope":"host","kind":"summary","outcome":"passed","exit_code":0,...}호스트 역할은 0이 아닌 포트가 필요합니다. 주소는 숫자 IPv4와 포트만 받습니다.
프로젝트의 시나리오를 실행합니다
섹션 제목: “프로젝트의 시나리오를 실행합니다”GUI의 Scenarios & Profiles에서 만든 프로필(profile)과 시나리오(scenario)는 File > **Save Project As…**로 저장한 프로젝트 파일에 들어 있습니다. headless 시뮬레이터는 이 파일을 읽어 장비 세션을 시작하고, 프로필을 적용하고, 시나리오의 단계를 실행합니다.
다음은 프로필 PROFILE-DEMO와 시나리오 count-cycle이 들어 있는 최소 프로젝트 파일입니다. 시나리오는 데이터 변수 2001에 7을 넣었다가 지웁니다.
{ "kind": "pdcomm-simulator-project", "schema_version": 9, "equipment_alarms": null, "equipment_commands": null, "equipment_constants": null, "equipment_data_variables": null, "equipment_directories": null, "equipment_programs": null, "equipment_reports_events": null, "equipment_spooling": null, "profiles": [ { "profile_id": "PROFILE-DEMO", "description": "Tutorial profile", "variables": [ {"kind": "status", "id": 1001, "name": "State", "units": "", "format": "ascii", "min_count": 0, "max_count": 32, "initial_value": "IDLE"}, {"kind": "data", "id": 2001, "name": "GoodCount", "units": "count", "format": "u4", "min_count": 1, "max_count": 1, "initial_value": "0"} ] } ], "scenarios": [ { "scenario_id": "count-cycle", "description": "Set and clear GoodCount", "profile_id": "PROFILE-DEMO", "steps": [ {"kind": "set_data_variable", "variable_id": 2001, "value": "7"}, {"kind": "clear_data_variable", "variable_id": 2001, "value": ""} ] } ]}"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --project tutorial.pdcomm.json --scenario count-cycle --trace-output count-cycle.pdcomm-trace.json{"schema":1,"sequence":2,"scenario":"count-cycle","scope":"project","kind":"result","detail":"project-file-loaded","truncated":false}...{"schema":1,"sequence":8,"scenario":"count-cycle","scope":"project","kind":"result","detail":"project-step","step_index":0,"variable_id":2001,"action":"set-data-variable","passed":true,"truncated":false}{"schema":1,"sequence":9,"scenario":"count-cycle","scope":"project","kind":"result","detail":"project-step","step_index":1,"variable_id":2001,"action":"clear-data-variable","passed":true,"truncated":false}{"schema":1,"sequence":10,"scenario":"count-cycle","scope":"project","kind":"result","detail":"project-terminal","state":"passed","completed_steps":2,"total_steps":2,"truncated":false}{"schema":1,"sequence":11,"scenario":"count-cycle","scope":"project","kind":"summary","outcome":"passed","exit_code":0,...}저장한 추적 파일은 GUI의 File > **Open Trace…**로 열어 볼 수 있습니다. 파일 형식만 확인하려면 --trace-input을 씁니다.
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --trace-input count-cycle.pdcomm-trace.json{"schema":1,"sequence":1,"scenario":"trace-validation","scope":"trace","kind":"summary","outcome":"passed","exit_code":0,...}쓰기 배압을 재현합니다
섹션 제목: “쓰기 배압을 재현합니다”transport-backpressure-recovery는 송신 버퍼가 가득 찬 상황을 일부러 만들고 회복을 확인합니다.
"$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario transport-backpressure-recovery --transport-fault-profile write_backpressure출력의 detail은 transport-fault-armed → first-send-accepted → second-send-limit-exceeded → transport-fault-released → transport-recovered → retry-send-accepted 순서로 진행하고 "outcome":"passed"로 끝납니다. 두 번째 송신은 한도 초과로 거부되고, 장애를 풀면 같은 요청을 다시 보낼 수 있습니다. 실제 프로그램에서도 Backpressure를 받으면 수신 이벤트를 먼저 처리한 뒤 정해진 횟수만 다시 시도합니다.
CI에서 실행합니다
섹션 제목: “CI에서 실행합니다”종료 코드만 확인하면 됩니다. 결과 파일은 실패 원인을 볼 수 있도록 남겨 둡니다.
set -emkdir -p resultsfor scenario in smoke status-round-trip data-report-round-trip directory-round-trip; do "$PDCOMM_SIMULATOR/pdcomm-simulator-headless" --scenario "$scenario" > "results/$scenario.jsonl"done