Skip to content
Start free evaluation

SECS/GEM quickstart

In this quickstart, you run one small program twice: once as the equipment and once as the host. The two processes connect over HSMS, and the host establishes communication (S1F13), asks the equipment to identify itself (S1F1), and reads two status variables (S1F3). It takes about ten minutes.

You need:

  • The PDComm SECS/GEM SDK bundle, extracted to a folder. This page calls that folder PDCOMM_ROOT.
  • A C++ compiler: the x64 Developer PowerShell of Visual Studio 2022 on Windows, g++ on Linux, or c++ from the Xcode Command Line Tools on macOS. If you don’t have one, use the prebuilt pdcomm-tutorial in the bundle’s bin/ folder instead.
  • An evaluation or developer license file. This page assumes you saved it as pdcomm.license in the PDCOMM_ROOT folder. See Install and check a license.
  • Two terminal windows.

Set up your environment explains the bundle layout in detail.

Run these in both terminals, using the folder where you extracted the bundle.

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

The source is examples/tutorial/pdcomm_tutorial.cpp in the SDK. The same program acts as a host or an equipment, depending on its arguments.

터미널 창
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"

The command prints nothing when it succeeds, and build/ now contains the pdcomm-tutorial executable.

In the first terminal, start the equipment in passive mode. A passive endpoint opens the port and waits for a connection.

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

In PowerShell, run & "$Root\build\pdcomm-tutorial.exe" equipment passive 127.0.0.1:5000.

The equipment is waiting when you see these two lines:

LISTEN 127.0.0.1:5000
Commands: state, establish, identity, values, names, online, offline, linktest, quit

The example equipment registers two status variables: SVID 1 Temperature (F4, 21.5) and SVID 2 Lot (ASCII, LOT-001).

In the second terminal, start the host in active mode. The active side opens the TCP connection and sends the HSMS Select request for you.

터미널 창
"$PDCOMM_ROOT/build/pdcomm-tutorial" host active 127.0.0.1:5000

When the Commands: line appears, type these commands one at a time. Wait for each result before you type the next one.

state
establish
state
identity
values 1 2

The host prints:

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

The equipment prints one result for each primary message it receives:

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
Output What it means
STATE tcp=4 gem=1 HSMS is Selected (4) and GEM is NotCommunicating (1). The TCP connection and Select are done, but GEM communication isn’t established yet.
RESULT kind=2 request=1 outcome=2 A response (2) to the establish request (1, S1F13). The outcome is CommunicationEstablished (2).
SECS-II 01 02 21 01 00 01 02 41 06 ... The S1F14 body: L[2] { B 0x00, L[2] { A "PDComm", A "0.1.0" } }, which is COMMACK 0 (accepted) followed by the model name and software revision.
STATE tcp=4 gem=3 GEM is now Communicating (3).
RESULT kind=2 request=2 outcome=4 The S1F1 (Are You There) reply carried the equipment identity (EquipmentIdentity, 4).
SECS-II 01 02 91 04 41 ac 00 00 41 07 ... The S1F4 body: F4 21.5 (41 ac 00 00) and A "LOT-001", in the order of the SVIDs you asked for.

The format bytes in the hex dump (01 list, 21 binary, 41 ASCII, 91 F4, b1 U4) are explained in SECS-II messages. For every numeric code, see Status codes and results.

With --auto as the last argument, both programs run the same flow on their own and exit. Type quit in both terminals to stop the interactive programs first, then run:

터미널 창
"$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

Each program ends with a line like the following and exit code 0. Automatic mode fails if the flow doesn’t finish within 20 seconds.

PASS tutorial equipment passive
PASS tutorial host active

The example uses the C++ wrapper TcpEndpoint for the HSMS endpoint and adds a GemHost or a GemEquipment on top, depending on the role. There are no background threads: poll() moves bytes, nextEvent() returns the next event, and handle() turns it into a GEM result.

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; }
}
Symptom Fix
The host’s state stays at tcp=5. The equipment isn’t running or the address differs. The host retries every T5. Start the equipment first and use the same IP address and port.
COMMAND ERROR This command requires the Host role You typed a host command in the equipment terminal. Use the host terminal.
establish prints nothing. The equipment stopped or doesn’t answer. Check the equipment terminal. Without an answer, RESULT kind=3 appears after five seconds (T3).
An error says a library can’t be found. Check that sdk/lib is on PATH (Windows) or LD_LIBRARY_PATH (Linux), or that you built with -Wl,-rpath (macOS).

For more cases, see Connection and timeout problems.

Type quit in both terminals. Each program closes its HSMS connection and exits with code 0.

  • Set up your environment for Windows, Linux or macOS, including the C# (.NET) examples.
  • First communication repeats this exchange in C++ and C# and connects to the simulator GUI.
  • HSMS explains Selected, T3 and T5, which you just saw.