콘텐츠로 이동
무료 평가 시작

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 묶음의 구성은 개발 환경 준비에서 자세히 설명합니다.

두 터미널에서 같은 변수를 정합니다. 경로는 실제로 압축을 푼 위치로 바꿉니다.

터미널 창
$Root = 'C:\pdcomm-tutorial'
$Sdk = Join-Path $Root 'sdk'
$env:PATH = "$Sdk\lib;$env:PATH"
$env:PDCOMM_LICENSE_FILE = Join-Path $Root 'pdcomm.license'

예제 소스는 SDK의 examples/tutorial/pdcomm_tutorial.cpp입니다. 한 프로그램이 인자에 따라 호스트 또는 장비로 동작합니다.

터미널 창
New-Item -ItemType Directory -Force "$Root\build" | Out-Null
cl /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"

명령이 아무것도 출력하지 않고 끝나면 성공입니다. build/ 폴더에 pdcomm-tutorial 실행 파일이 생깁니다.

첫 번째 터미널에서 장비를 Passive 모드로 시작합니다. Passive 쪽은 포트를 열고 연결을 기다립니다.

터미널 창
"$PDCOMM_ROOT/build/pdcomm-tutorial" equipment passive 127.0.0.1:5000

Windows PowerShell에서는 & "$Root\build\pdcomm-tutorial.exe" equipment passive 127.0.0.1:5000을 실행합니다.

다음 두 줄이 보이면 장비가 기다리는 중입니다.

LISTEN 127.0.0.1:5000
Commands: 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:5000

Commands: 줄이 보이면 아래 명령을 한 줄씩 입력합니다. 각 명령의 결과가 나온 뒤 다음 명령을 입력합니다.

state
establish
state
identity
values 1 2

호스트 터미널에 다음과 같이 출력됩니다.

STATE tcp=4 gem=1
RESULT kind=2 request=1 outcome=2
SECS-II 01 02 21 01 00 01 02 41 06 50 44 43 6f 6d 6d 41 05 30 2e 31 2e 30
STATE tcp=4 gem=3
RESULT kind=2 request=2 outcome=4
SECS-II 01 02 41 06 50 44 43 6f 6d 6d 41 05 30 2e 31 2e 30
RESULT kind=2 request=3 outcome=5
SECS-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=2
SECS-II 01 00
RESULT kind=1 request=0 outcome=1
RESULT kind=1 request=0 outcome=13
SECS-II 01 02 b1 04 00 00 00 01 b1 04 00 00 00 02
출력 의미
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 passive
PASS tutorial host active

예제는 C++ 래퍼(wrapper) TcpEndpoint로 HSMS 끝점(endpoint)을 만들고, 역할에 따라 GemHost 또는 GemEquipment를 붙입니다. 별도 스레드 없이 poll()로 입출력을 진행하고, nextEvent()로 꺼낸 이벤트를 handle()에 넘겨 GEM 결과를 받습니다.

examples/tutorial/tutorial_options.hpp
#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;
}
}
examples/tutorial/pdcomm_tutorial.cpp
#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으로 끝납니다.

  • 개발 환경 준비: Windows, Linux, macOS에서 SDK와 C#(.NET) 예제를 준비합니다.
  • 첫 통신: C++와 C#으로 같은 통신을 하고 시뮬레이터 GUI와 연결합니다.
  • HSMS: 방금 본 Selected, T3, T5가 무엇인지 알아봅니다.