SECS/GEM 빠른 시작
이 페이지에서는 SDK에 들어 있는 튜토리얼 예제 하나로 장비(equipment)와 호스트(host)를 실행합니다. 두 프로그램이 HSMS로 연결되고, 호스트가 통신 수립(S1F13), 장비 확인(S1F1), 상태 변수 읽기(S1F3)를 차례로 보냅니다. 10분 정도 걸립니다.
준비물
섹션 제목: “준비물”- PDComm SECS/GEM SDK 묶음을 압축 해제한 폴더. 이 페이지에서는 이 폴더를
PDCOMM_ROOT라고 부릅니다. - C++ 컴파일러. Windows는 Visual Studio 2022의 x64 Developer PowerShell, Linux는
g++, macOS는 Xcode Command Line Tools의c++를 씁니다. 컴파일러가 없으면 묶음의bin/폴더에 있는 미리 빌드된pdcomm-tutorial을 그대로 써도 됩니다. - 평가판이나 개발 라이선스 파일. 이 페이지에서는
PDCOMM_ROOT폴더에pdcomm.license로 저장했다고 가정합니다. 라이선스 설치와 확인을 참고합니다. - 터미널 두 개.
SDK 묶음의 구성은 개발 환경 준비에서 자세히 설명합니다.
1. 환경 변수를 정합니다
섹션 제목: “1. 환경 변수를 정합니다”두 터미널에서 같은 변수를 정합니다. 경로는 실제로 압축을 푼 위치로 바꿉니다.
$Root = 'C:\pdcomm-tutorial'$Sdk = Join-Path $Root 'sdk'$env:PATH = "$Sdk\lib;$env:PATH"$env:PDCOMM_LICENSE_FILE = Join-Path $Root 'pdcomm.license'PDCOMM_ROOT="$HOME/pdcomm-tutorial"PDCOMM_SDK="$PDCOMM_ROOT/sdk"export LD_LIBRARY_PATH="$PDCOMM_SDK/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"export PDCOMM_LICENSE_FILE="$PDCOMM_ROOT/pdcomm.license"PDCOMM_ROOT="$HOME/pdcomm-tutorial"PDCOMM_SDK="$PDCOMM_ROOT/sdk"export PDCOMM_LICENSE_FILE="$PDCOMM_ROOT/pdcomm.license"2. 튜토리얼 예제를 빌드합니다
섹션 제목: “2. 튜토리얼 예제를 빌드합니다”예제 소스는 SDK의 examples/tutorial/pdcomm_tutorial.cpp입니다. 한 프로그램이 인자에 따라 호스트 또는 장비로 동작합니다.
New-Item -ItemType Directory -Force "$Root\build" | Out-Nullcl /nologo /W4 /WX /EHsc /std:c++14 "/I$Sdk\include" "$Sdk\examples\tutorial\pdcomm_tutorial.cpp" "$Sdk\lib\pdcomm_ffi.dll.lib" "/Fe:$Root\build\pdcomm-tutorial.exe" "/Fo:$Root\build\tutorial.obj"mkdir -p "$PDCOMM_ROOT/build"g++ -std=c++14 -Wall -Wextra -Werror -pedantic -I "$PDCOMM_SDK/include" \ "$PDCOMM_SDK/examples/tutorial/pdcomm_tutorial.cpp" \ -L "$PDCOMM_SDK/lib" -lpdcomm_ffi -o "$PDCOMM_ROOT/build/pdcomm-tutorial"mkdir -p "$PDCOMM_ROOT/build"c++ -std=c++14 -Wall -Wextra -Werror -pedantic -I "$PDCOMM_SDK/include" \ "$PDCOMM_SDK/examples/tutorial/pdcomm_tutorial.cpp" \ -L "$PDCOMM_SDK/lib" -lpdcomm_ffi -Wl,-rpath,"$PDCOMM_SDK/lib" \ -o "$PDCOMM_ROOT/build/pdcomm-tutorial"명령이 아무것도 출력하지 않고 끝나면 성공입니다. build/ 폴더에 pdcomm-tutorial 실행 파일이 생깁니다.
3. 장비를 시작합니다
섹션 제목: “3. 장비를 시작합니다”첫 번째 터미널에서 장비를 Passive 모드로 시작합니다. Passive 쪽은 포트를 열고 연결을 기다립니다.
"$PDCOMM_ROOT/build/pdcomm-tutorial" equipment passive 127.0.0.1:5000Windows PowerShell에서는 & "$Root\build\pdcomm-tutorial.exe" equipment passive 127.0.0.1:5000을 실행합니다.
다음 두 줄이 보이면 장비가 기다리는 중입니다.
LISTEN 127.0.0.1:5000Commands: state, establish, identity, values, names, online, offline, linktest, quit장비 예제는 상태 변수(status variable) 두 개를 등록합니다. SVID 1은 Temperature(F4, 21.5), SVID 2는 Lot(ASCII, LOT-001)입니다.
4. 호스트를 연결하고 명령을 보냅니다
섹션 제목: “4. 호스트를 연결하고 명령을 보냅니다”두 번째 터미널에서 호스트를 Active 모드로 시작합니다. Active 쪽이 TCP 연결을 만들고 HSMS Select를 자동으로 보냅니다.
"$PDCOMM_ROOT/build/pdcomm-tutorial" host active 127.0.0.1:5000Commands: 줄이 보이면 아래 명령을 한 줄씩 입력합니다. 각 명령의 결과가 나온 뒤 다음 명령을 입력합니다.
stateestablishstateidentityvalues 1 2호스트 터미널에 다음과 같이 출력됩니다.
STATE tcp=4 gem=1RESULT kind=2 request=1 outcome=2SECS-II 01 02 21 01 00 01 02 41 06 50 44 43 6f 6d 6d 41 05 30 2e 31 2e 30STATE tcp=4 gem=3RESULT kind=2 request=2 outcome=4SECS-II 01 02 41 06 50 44 43 6f 6d 6d 41 05 30 2e 31 2e 30RESULT kind=2 request=3 outcome=5SECS-II 01 02 91 04 41 ac 00 00 41 07 4c 4f 54 2d 30 30 31장비 터미널에는 받은 요청(primary)마다 결과가 출력됩니다.
RESULT kind=1 request=0 outcome=2SECS-II 01 00RESULT kind=1 request=0 outcome=1RESULT kind=1 request=0 outcome=13SECS-II 01 02 b1 04 00 00 00 01 b1 04 00 00 00 025. 출력을 읽습니다
섹션 제목: “5. 출력을 읽습니다”| 출력 | 의미 |
|---|---|
STATE tcp=4 gem=1 |
HSMS는 Selected(4), GEM 통신 상태는 NotCommunicating(1)입니다. TCP 연결과 Select가 끝났지만 아직 GEM 통신을 수립하지 않았습니다. |
RESULT kind=2 request=1 outcome=2 |
통신 수립 요청(1, S1F13)에 대한 응답(2)입니다. 결과는 CommunicationEstablished(2)입니다. |
SECS-II 01 02 21 01 00 01 02 41 06 ... |
받은 S1F14 본문입니다. L[2] { B 0x00, L[2] { A "PDComm", A "0.1.0" } }이며 COMMACK 0(수락)과 장비 모델명, 소프트웨어 버전입니다. |
STATE tcp=4 gem=3 |
GEM 통신 상태가 Communicating(3)이 되었습니다. |
RESULT kind=2 request=2 outcome=4 |
S1F1(Are You There) 응답으로 장비 식별 정보(EquipmentIdentity, 4)를 받았습니다. |
SECS-II 01 02 91 04 41 ac 00 00 41 07 ... |
S1F4 본문입니다. F4 21.5(41 ac 00 00)와 A "LOT-001"입니다. 요청한 SVID 순서대로 값이 옵니다. |
16진수 본문의 형식 바이트(01=리스트, 21=바이너리, 41=ASCII, 91=F4, b1=U4)는 SECS-II 메시지에서 설명합니다. 숫자 코드의 전체 목록은 상태 코드와 결과 읽기에 있습니다.
6. 자동 모드로 한 번에 확인합니다
섹션 제목: “6. 자동 모드로 한 번에 확인합니다”마지막 인자로 --auto를 주면 두 프로그램이 같은 흐름을 스스로 실행하고 끝납니다. 두 터미널에서 각각 quit을 입력해 앞의 프로그램을 끝낸 뒤 실행합니다.
"$PDCOMM_ROOT/build/pdcomm-tutorial" equipment passive 127.0.0.1:5000 --auto"$PDCOMM_ROOT/build/pdcomm-tutorial" host active 127.0.0.1:5000 --auto각 터미널의 마지막 줄이 다음과 같고 종료 코드가 0이면 성공입니다. 자동 모드는 20초 안에 끝나지 않으면 실패로 끝납니다.
PASS tutorial equipment passivePASS tutorial host active예제 코드
섹션 제목: “예제 코드”예제는 C++ 래퍼(wrapper) TcpEndpoint로 HSMS 끝점(endpoint)을 만들고, 역할에 따라 GemHost 또는 GemEquipment를 붙입니다. 별도 스레드 없이 poll()로 입출력을 진행하고, nextEvent()로 꺼낸 이벤트를 handle()에 넘겨 GEM 결과를 받습니다.
#pragma once#include "pdcomm_gem.hpp"namespace tutorial {PdcommSecsLimits itemLimits() { return pdcomm::secsLimits(4096, 4096, 128, 8);}
PdcommTcpOptions tcpOptions(uint32_t mode) { PdcommTcpOptions value = {}; value.struct_size = sizeof(value); value.version = PDCOMM_TCP_OPTIONS_VERSION; value.mode = mode; value.session_id = 7; value.initial_system_bytes = 100; value.t3_ms = 5000; value.t5_ms = 1000; value.t6_ms = 2000; value.t7_ms = 2000; value.t8_ms = 1000; value.connect_timeout_ms = 20; value.shutdown_timeout_ms = 200; value.write_timeout_ms = 2000; value.max_frame_bytes = 4110; value.max_events = 96; value.max_queued_bytes = 32768; value.max_write_frames = 8; value.max_write_bytes = 16384; value.max_io_chunk = 4096; value.max_application_events = 96; value.max_application_bytes = 32768; value.poll_budget = 16; value.item_limits = itemLimits(); return value;}
}#include "tutorial_options.hpp"#include <atomic>#include <chrono>#include <iomanip>#include <iostream>#include <memory>#include <mutex>#include <queue>#include <sstream>#include <thread>
struct Input { std::mutex mutex; std::queue<std::string> lines; };int main(int argc, char **argv) { if (argc < 4 || argc > 5) { std::cerr << "Usage: pdcomm-tutorial <host|equipment> <active|passive> <numeric-IP:port> [--auto]\n"; return 2; } const bool hostRole = std::string(argv[1]) == "host"; const bool active = std::string(argv[2]) == "active"; const bool automatic = argc == 5 && std::string(argv[4]) == "--auto"; if ((!hostRole && std::string(argv[1]) != "equipment") || (!active && std::string(argv[2]) != "passive") || (argc == 5 && !automatic)) return 2; try { const auto limits = tutorial::itemLimits(); pdcomm::TcpEndpoint transport(argv[3], tutorial::tcpOptions(active ? PDCOMM_TCP_ACTIVE : PDCOMM_TCP_PASSIVE)); std::unique_ptr<pdcomm::GemHost> host; std::unique_ptr<pdcomm::GemEquipment> equipment; if (hostRole) host.reset(new pdcomm::GemHost(std::move(transport), pdcomm::gemHostOptions(limits, 64))); else { equipment.reset(new pdcomm::GemEquipment(std::move(transport), limits, pdcomm::GemControl::HostOffline, pdcomm::GemOnlineMode::Remote, "PDComm", "0.1.0", 8, 1024, 4096)); const std::vector<uint8_t> temperature = {0x41, 0xac, 0, 0}; auto value = pdcomm::SecsItem::primitive(PDCOMM_SECS_F4, temperature, limits); equipment->registerStatus(1, "Temperature", "C", value); auto lot = pdcomm::SecsItem::primitive(PDCOMM_SECS_ASCII, std::vector<uint8_t>{'L','O','T','-','0','0','1'}, limits); equipment->registerStatus(2, "Lot", "", lot); } pdcomm::GemEndpoint *endpoint = hostRole ? static_cast<pdcomm::GemEndpoint*>(host.get()) : equipment.get(); endpoint->start(); if (!active) std::cout << "LISTEN " << endpoint->tcp().localAddress() << std::endl; auto input = std::make_shared<Input>(); if (!automatic) { std::cout << "Commands: state, establish, identity, values, names, online, offline, linktest, quit\n" << std::flush; std::thread([input] { std::string line; while (std::getline(std::cin, line)) { { std::lock_guard<std::mutex> lock(input->mutex); input->lines.push(line); } if (line == "quit") return; } std::lock_guard<std::mutex> lock(input->mutex); input->lines.push("quit"); }).detach(); } unsigned step = 0, received = 0; auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(20); auto finish = deadline; bool finishing = false; while (true) { endpoint->poll(); while (auto event = endpoint->tcp().nextEvent()) { auto result = endpoint->handle(event); auto info = result.info(); if (info.kind != pdcomm::GemResultKind::Ignored) { std::cout << "RESULT kind=" << static_cast<unsigned>(info.kind) << " request=" << static_cast<unsigned>(info.request) << " outcome=" << static_cast<unsigned>(info.outcome) << "\n"; auto body = result.body(); if (body) { std::cout << "SECS-II"; for (auto byte : body.bytes()) std::cout << ' ' << std::hex << std::setw(2) << std::setfill('0') << static_cast<unsigned>(byte); std::cout << std::dec << "\n"; } std::cout.flush(); } if (info.kind == pdcomm::GemResultKind::Unhandled) { // No GEM handler took this primary. Abort it with SxF0 so the peer does not // wait for T3 and the primary does not keep holding an incoming slot. const PdcommTcpEventInfo primary = event.info(); if (primary.reply_expected) { try { endpoint->tcp().reply(event, primary.stream, 0, false, nullptr, 0); std::cout << "ABORT S" << static_cast<unsigned>(primary.stream) << "F0 for S" << static_cast<unsigned>(primary.stream) << 'F' << static_cast<unsigned>(primary.function) << std::endl; } catch (const pdcomm::Error &error) { std::cerr << "REPLY ERROR status=" << error.status() << std::endl; } } } if (automatic) { if (hostRole && info.kind == pdcomm::GemResultKind::Response) { if (step == 1 && static_cast<unsigned>(info.outcome) == PDCOMM_GEM_COMMUNICATION_ESTABLISHED) { host->request(pdcomm::GemRequest::AreYouOnline); step = 2; } else if (step == 2 && static_cast<unsigned>(info.outcome) == PDCOMM_GEM_EQUIPMENT_IDENTITY) { host->request(pdcomm::GemRequest::StatusValues, {1,2}); step = 3; } else if (step == 3 && static_cast<unsigned>(info.outcome) == PDCOMM_GEM_VALUES_RECEIVED) { finishing = true; finish = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); } } if (!hostRole && info.kind == pdcomm::GemResultKind::Primary && ++received == 3) { finishing = true; finish = std::chrono::steady_clock::now() + std::chrono::milliseconds(500); } } } if (automatic && hostRole && step == 0 && endpoint->tcp().state().state == PDCOMM_TCP_SELECTED) { host->beginCommunication(); step = 1; } if (automatic && finishing && std::chrono::steady_clock::now() >= finish) break; if (automatic && std::chrono::steady_clock::now() > deadline) throw std::runtime_error("Tutorial peer response deadline expired"); std::string line; { std::lock_guard<std::mutex> lock(input->mutex); if (!input->lines.empty()) { line = input->lines.front(); input->lines.pop(); } } if (line == "quit") break; try { std::vector<uint32_t> ids = {1, 2}; if (line.find("values ") == 0 || line.find("names ") == 0) { std::istringstream arguments(line.substr(line.find(' ') + 1)); ids.clear(); uint64_t id = 0; while (arguments >> id) { if (id > UINT32_MAX) throw std::runtime_error("Status ID exceeds uint32"); ids.push_back(static_cast<uint32_t>(id)); } if (!arguments.eof() || ids.empty()) throw std::runtime_error("Status IDs must be unsigned integers"); line = line.substr(0, line.find(' ')); } if (line == "state") std::cout << "STATE tcp=" << endpoint->tcp().state().state << " gem=" << static_cast<unsigned>(endpoint->state().communication) << std::endl; else if (line == "linktest") endpoint->tcp().linktest(); else if (!line.empty()) { if (!host) throw std::runtime_error("This command requires the Host role"); if (line == "establish") host->beginCommunication(); else if (line == "identity") host->request(pdcomm::GemRequest::AreYouOnline); else if (line == "values") host->request(pdcomm::GemRequest::StatusValues,ids); else if (line == "names") host->request(pdcomm::GemRequest::StatusNames,ids); else if (line == "online") host->request(pdcomm::GemRequest::Online); else if (line == "offline") host->request(pdcomm::GemRequest::Offline); else throw std::runtime_error("Unknown command"); } } catch (const std::exception &error) { std::cerr << "COMMAND ERROR " << error.what() << std::endl; } std::this_thread::sleep_for(std::chrono::milliseconds(2)); } endpoint->stop(); if (automatic) std::cout << "PASS tutorial " << argv[1] << ' ' << argv[2] << std::endl; return 0; } catch (const std::exception &error) { std::cerr << error.what() << std::endl; return 1; }}자주 생기는 문제
섹션 제목: “자주 생기는 문제”| 증상 | 해결 방법 |
|---|---|
호스트의 state가 tcp=5에 머뭅니다. |
장비가 실행 중이 아니거나 주소가 다릅니다. 호스트는 T5 간격으로 다시 연결합니다. 장비를 먼저 시작하고 같은 IP와 포트를 씁니다. |
COMMAND ERROR This command requires the Host role |
장비 터미널에 호스트 명령을 입력했습니다. 호스트 터미널에서 입력합니다. |
establish 뒤 결과가 없습니다. |
장비 프로그램이 멈췄거나 응답하지 않습니다. 장비 터미널을 확인합니다. 응답이 없으면 5초(T3) 뒤 RESULT kind=3이 출력됩니다. |
| 라이브러리를 찾지 못한다는 오류 | Windows는 PATH, Linux는 LD_LIBRARY_PATH에 sdk/lib를 넣었는지, macOS는 -Wl,-rpath로 빌드했는지 확인합니다. |
더 많은 사례는 연결과 타임아웃 문제를 참고합니다.
두 터미널에서 quit을 입력합니다. 프로그램이 HSMS 연결을 정리하고 종료 코드 0으로 끝납니다.