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

보고서와 이벤트

호스트(host)는 이벤트 보고서(event report)로 “이 이벤트가 일어나면 이 값들을 보내라”고 장비(equipment)에 미리 알려 둡니다. 이 튜토리얼에서는 호스트가 보고서를 정의·연결·활성화하고, 장비가 보낸 S6F11을 받아 S6F12로 확인하는 전체 흐름을 직접 실행합니다.

  • S2F33(보고서 정의), S2F35(이벤트 연결), S2F37(이벤트 활성화)의 순서와 응답 코드를 이해합니다.
  • 장비가 S6F11을 보내는 조건과 호스트가 보고서 값을 읽는 방법을 익힙니다.
  • 같은 흐름을 .NET 워커(worker)와 Qt 스레드 모델로 구현한 예제를 읽습니다.
  • 환경 준비를 마친 SDK와 셸입니다. 이 페이지는 거기서 설정한 PDCOMM_ROOT, PDCOMM_SDK, PDCOMM_NATIVE, PDCOMM_EXAMPLES(Windows는 $Root, $Sdk, $Native, $Examples)를 그대로 씁니다.
  • .NET 10 SDK입니다. Qt 예제를 빌드하려면 Qt 6(Core 모듈), CMake 3.20 이상, Ninja 같은 빌드 도구가 추가로 필요합니다.
  • 첫 통신에서 통신 수립(S1F13)을 해 본 경험이 있으면 좋습니다.

PDComm.GemReportsDemo는 한 프로그램에 호스트와 장비 역할이 모두 들어 있습니다. 장비 쪽은 상태 변수(SVID) 두 개와 수집 이벤트(CEID) 두 개를 가지고 시작합니다.

장비 데이터 값
SVID 1 Temperature F4 21.5 (첫 이벤트 보고 직후 32.0으로 바뀝니다)
SVID 2 Lot ASCII LOT-001
CEID 100, 200

보고서 바인딩(binding)을 따로 설정하지 않았으므로 보고서의 VID는 같은 번호의 상태 변수를 가리킵니다. 호스트는 통신을 수립한 뒤 다음 9단계를 차례로 보냅니다.

Host Equipment
0 S2F33 DATAID 1 RPTID 10=[VID 2,1,2] RPTID 20=[VID 1] -> S2F34 DRACK 0
1 S2F35 DATAID 2 CEID 100->[10,20] CEID 200->[20] -> S2F36 LRACK 0
2 S2F37 enable all events -> S2F38 ERACK 0
<- S6F11 DATAID 41 CEID 100 RPT 10=[LOT-001, 21.5, LOT-001] RPT 20=[21.5]
S6F12 ACKC6 0 ->
3 S2F37 disable CEID 100 -> ERACK 0
4 S2F33 DATAID 3 RPTID 20=[] (delete report 20) -> DRACK 0
5 S2F37 enable CEID 100 -> ERACK 0
<- S6F11 DATAID 42 CEID 100 RPT 10=[LOT-001, 32.0, LOT-001]
S6F12 ACKC6 7 ->
6 S2F33 DATAID 4 (empty list: delete all reports) -> DRACK 0
7 S2F37 enable CEID 200 (no report linked any more) -> ERACK 0
8 S2F35 DATAID 5 CEID 200->[10] (RPTID 10 is gone) -> LRACK 5
  • 4단계처럼 VID 목록이 빈 보고서를 정의하면 그 보고서와 연결이 지워집니다. 6단계처럼 목록 자체가 비어 있으면 모든 보고서가 지워집니다.
  • 보고서를 정의·삭제하거나 연결을 바꿔도 이벤트의 활성화 상태는 바뀌지 않습니다. 활성화 상태는 S2F37로만 바꿉니다.
  • 장비는 이벤트 활성화 응답(S2F38)을 보낸 뒤 CEID 100이 켜져 있으면 S6F11을 보냅니다. 3단계에서 꺼 둔 동안에는 보내지 않습니다.
  • 5단계의 S6F12는 ACKC6 7을 보냅니다. ACKC6 0은 수락, 1–63은 거부입니다. 예제는 0이 아닌 값도 장비까지 그대로 전달되는지 확인합니다.
  • 7단계는 연결된 보고서가 하나도 없는 CEID 200도 켤 수 있음을 보여 줍니다. ERACK 1은 장비에 없는 CEID를 지정했을 때만 돌아옵니다. 켜진 이벤트에 연결된 보고서가 없으면 장비는 보고서 목록이 빈 S6F11을 보냅니다.
  • 8단계는 거부 응답을 보여 줍니다. 없는 RPTID를 연결하면 장비는 LRACK 5를 돌려줍니다.
터미널 창
dotnet build "$Examples\dotnet\PDComm.GemReportsDemo\PDComm.GemReportsDemo.csproj" -c Release "-p:PDCommSdk=$Sdk" --artifacts-path "$Root\build\managed"

-p:PDCommSdk를 주면 예제는 SDK의 managed/PDComm.dll을 참조합니다. 빌드가 끝나면 다음 줄이 보입니다.

PDComm.GemReportsDemo -> <PDCOMM_ROOT>/build/managed/bin/PDComm.GemReportsDemo/release/PDComm.GemReportsDemo.dll
Build succeeded.
0 Warning(s)
0 Error(s)

첫 번째 터미널에서 장비를 Passive 모드로 시작합니다. 포트 0을 주면 운영체제가 빈 포트를 고릅니다.

터미널 창
dotnet "$Root\build\managed\bin\PDComm.GemReportsDemo\release\PDComm.GemReportsDemo.dll" $Native equipment passive 127.0.0.1:0

장비가 듣는 주소를 출력하고 호스트를 기다립니다. 포트 번호는 실행할 때마다 다릅니다.

LISTEN 127.0.0.1:54475

두 번째 터미널에서 장비가 출력한 주소로 호스트를 Active 모드로 실행합니다.

터미널 창
dotnet "$Root\build\managed\bin\PDComm.GemReportsDemo\release\PDComm.GemReportsDemo.dll" $Native host active 127.0.0.1:54475

9단계가 1초 안에 끝나고 양쪽이 종료 코드 0으로 끝납니다.

PASS host active: managed GEM reports worker completed scenario 9 and shut down

장비 터미널에는 다음 줄이 이어집니다.

PASS equipment passive: managed GEM reports worker completed scenario 9 and shut down

두 프로그램은 각 단계의 응답 코드, 이벤트 값, 토큰(token)을 스스로 검사합니다. 하나라도 다르면 InvalidOperationException을 던지고 0이 아닌 코드로 끝납니다.

StartEquipmentAsync는 상태 변수와 GemReportConfiguration를 넘겨 장비 워커를 시작합니다. GemReportConfiguration의 두 번째 인수 { 100, 200 }이 장비가 제공하는 CEID 목록입니다. 보고서와 연결은 호스트가 원격으로 만듭니다.

RunEquipmentAsync는 이벤트 스트림을 읽으며 다음 일을 합니다.

  • 호스트의 S2F33/S2F35/S2F37은 SDK가 자동으로 응답합니다. 애플리케이션은 결과의 Outcome(DefineReportsAck–EnableEventsAck)과 Acknowledgement만 확인합니다.
  • 2단계와 5단계의 EnableEventsAck가 0이고 CEID 100이 켜져 있으면 PublishEventAsync로 S6F11을 보냅니다. 첫 보고 뒤에는 UpdateStatusAsync로 온도를 32.0으로 바꿉니다. 이미 보낸 보고서에는 영향이 없습니다. 보고서 값은 보내는 순간의 스냅숏(snapshot)입니다.
  • 호스트의 S6F12는 Response 결과(EventReport 요청, EventAck 결과)로 돌아옵니다. 예제는 보낼 때 받은 토큰과 응답의 토큰을 비교합니다.

SendStepAsync가 9단계 요청을 만듭니다.

RunHostAsync는 Outcome이 EventReceived인 결과를 받으면 CheckEvent로 값을 확인하고 AcknowledgeEventAsync로 S6F12를 보냅니다. SDK는 S6F11에 자동으로 응답하지 않습니다. 호스트 애플리케이션이 보고서를 처리한 뒤 직접 응답합니다.

S6F11의 값에는 VID가 들어 있지 않습니다. GetEventReport로 보고서 ID와 값 개수를, GetEventValue로 값을 순서대로 꺼내고, 위치는 호스트가 정의한 보고서와 맞춰 해석합니다. 예제의 RPTID 10은 VID 2, 1, 2 순서이므로 값도 LOT-001, 21.5, LOT-001 순서입니다.

전체 코드: PDComm.GemReportsDemo/Program.cs
PDComm.GemReportsDemo/Program.cs
using PDComm;
if (args.Length != 4 || !Path.IsPathFullyQualified(args[0]) ||
args[1] is not ("host" or "equipment") ||
args[2] is not ("active" or "passive"))
{
Console.Error.WriteLine(
"Usage: PDComm.GemReportsDemo <absolute-native-library-path> <host|equipment> <active|passive> <numeric-address>");
return 2;
}
PdcommRuntime.Initialize(args[0]);
string role = args[1];
HsmsMode mode = args[2] == "active" ? HsmsMode.Active : HsmsMode.Passive;
SecsLimits itemLimits = new(4096, 4096, 256, 16);
HsmsOptions transportOptions = DemoOptions.Transport(mode, itemLimits);
HsmsWorkerOptions workerOptions = DemoOptions.Worker();
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(20));
await using GemWorker worker = role == "host"
? await GemHostWorker.StartAsync(
args[3], transportOptions, DemoOptions.Host(itemLimits), workerOptions, deadline.Token)
: await StartEquipmentAsync(args[3], transportOptions, itemLimits, workerOptions, deadline.Token);
if (mode == HsmsMode.Passive)
{
string address = worker.LocalAddress ??
throw new InvalidOperationException("The passive endpoint did not publish its listening address.");
Console.WriteLine($"LISTEN {address}");
Console.Out.Flush();
}
await using IAsyncEnumerator<GemEvent> events =
worker.ReadEventsAsync(deadline.Token).GetAsyncEnumerator();
if (worker is GemHostWorker host)
await RunHostAsync(host, events, deadline.Token);
else if (worker is GemEquipmentWorker equipment)
await RunEquipmentAsync(equipment, events, itemLimits, deadline.Token);
while (await events.MoveNextAsync()) events.Current.Dispose();
await worker.Completion.WaitAsync(deadline.Token);
Console.WriteLine($"PASS {role} {args[2]}: managed GEM reports worker completed scenario 9 and shut down");
return 0;
static async Task<GemEquipmentWorker> StartEquipmentAsync(
string address,
HsmsOptions transport,
SecsLimits limits,
HsmsWorkerOptions worker,
CancellationToken cancellationToken)
{
using SecsItem temperature = SecsItem.FromF4([21.5f], limits);
using SecsItem lot = SecsItem.FromAscii("LOT-001", limits);
GemStatusVariable[] variables =
[
new(1, "Temperature", "C", temperature),
new(2, "Lot", "", lot),
];
var reports = new GemReportConfiguration(
DemoOptions.Reports(),
new uint[] { 100, 200 });
return await GemEquipmentWorker.StartAsync(
address,
transport,
DemoOptions.Equipment(limits),
worker,
variables,
reports,
cancellationToken);
}
static async Task RunHostAsync(
GemHostWorker host,
IAsyncEnumerator<GemEvent> events,
CancellationToken cancellationToken)
{
bool began = false;
bool established = false;
int step = 0;
int received = 0;
HsmsToken pending = default;
while (step < 9)
{
if (!began && (await host.GetTransportStateAsync(cancellationToken)).State == HsmsConnectionState.Selected)
{
pending = await host.BeginCommunicationAsync(cancellationToken);
began = true;
}
if (!await events.MoveNextAsync())
throw new InvalidOperationException("The GEM event stream ended before the reports scenario completed.");
GemEvent current = events.Current;
bool disposed = false;
try
{
_ = ValidateEvent(current);
GemResult? result = current.Result;
if (result is null) continue;
GemResultInfo info = result.GetInfo();
if (info.Kind == GemResultKind.Response)
{
if (info.Token != pending)
throw new InvalidOperationException("The GEM response token did not match the current request.");
if (!established)
{
if (info.RequestKind != GemRequestKind.Establish ||
info.Outcome != GemOutcome.CommunicationEstablished)
throw new InvalidOperationException("GEM communication establishment failed.");
established = true;
pending = await SendStepAsync(host, step, cancellationToken);
}
else
{
CheckResponse(info, step);
if (step is not (2 or 5))
{
step++;
if (step < 9) pending = await SendStepAsync(host, step, cancellationToken);
}
}
}
else if (info.Outcome == GemOutcome.EventReceived)
{
int expectedStep = received == 0 ? 2 : 5;
if (step != expectedStep || info.Kind != GemResultKind.Primary ||
info.ReplySent || info.Acknowledgement is not null)
throw new InvalidOperationException("The received event report state is incorrect.");
CheckEvent(result, received);
Task acknowledgement = host.AcknowledgeEventAsync(
result, received == 0 ? (byte)0 : (byte)7, cancellationToken);
current.Dispose();
disposed = true;
await acknowledgement;
received++;
step++;
pending = await SendStepAsync(host, step, cancellationToken);
}
}
finally
{
if (!disposed) current.Dispose();
}
}
if (received != 2) throw new InvalidOperationException("The host did not receive both event reports.");
await host.StopAsync(cancellationToken);
}
static async Task RunEquipmentAsync(
GemEquipmentWorker equipment,
IAsyncEnumerator<GemEvent> events,
SecsLimits limits,
CancellationToken cancellationToken)
{
int configured = 0;
int published = 0;
int acknowledged = 0;
bool communicated = false;
HsmsToken[] tokens = new HsmsToken[2];
while (true)
{
if (!await events.MoveNextAsync())
throw new InvalidOperationException("The GEM event stream ended before the reports scenario completed.");
using GemEvent current = events.Current;
HsmsEventInfo transportEvent = ValidateEvent(current);
GemResult? result = current.Result;
if (result is not null)
{
GemResultInfo info = result.GetInfo();
if (info.State.Communication == GemCommunicationState.Communicating) communicated = true;
if (info.Kind == GemResultKind.Primary &&
info.Outcome is >= GemOutcome.DefineReportsAck and <= GemOutcome.EnableEventsAck)
{
configured++;
// Publish CEID 100 after the enable steps 2 (all events) and 5 (CEID 100) only.
if (info.Outcome == GemOutcome.EnableEventsAck && info.Acknowledgement == 0 &&
configured is 3 or 6 && (await equipment.GetEventAtAsync(0, cancellationToken)).Enabled)
{
if (published >= 2) throw new InvalidOperationException("Too many reports were published.");
tokens[published] = await equipment.PublishEventAsync(
(uint)(41 + published), 100, cancellationToken);
published++;
if (published == 1)
{
using SecsItem updated = SecsItem.FromF4([32.0f], limits);
await equipment.UpdateStatusAsync(1, updated, cancellationToken);
}
}
}
else if (info.Kind == GemResultKind.Response)
{
if (acknowledged >= 2 || info.RequestKind != GemRequestKind.EventReport ||
info.Outcome != GemOutcome.EventAck || info.Acknowledgement != (acknowledged == 0 ? 0 : 7) ||
info.Token != tokens[acknowledged])
throw new InvalidOperationException("The event acknowledgement response is incorrect.");
acknowledged++;
}
}
if (communicated && transportEvent.Kind == HsmsEventKind.StateChanged &&
transportEvent.FromState == HsmsConnectionState.Selected &&
transportEvent.ToState != HsmsConnectionState.Selected)
{
GemReportState state = await equipment.GetReportStateAsync(cancellationToken);
if (configured != 9 || published != 2 || acknowledged != 2 ||
state != new GemReportState(2, 0, 0, 0))
throw new InvalidOperationException("The final equipment report state is incorrect.");
await equipment.StopAsync(cancellationToken);
return;
}
}
}
static async Task<HsmsToken> SendStepAsync(
GemHostWorker host,
int step,
CancellationToken cancellationToken) => step switch
{
0 => await host.DefineReportsAsync(1,
new GemReportSpec[] { new(10, new uint[] { 2, 1, 2 }), new(20, new uint[] { 1 }) }, cancellationToken),
1 => await host.LinkReportsAsync(2,
new GemLinkSpec[] { new(100, new uint[] { 10, 20 }), new(200, new uint[] { 20 }) }, cancellationToken),
2 => await host.EnableEventsAsync(true, Array.Empty<uint>(), cancellationToken),
3 => await host.EnableEventsAsync(false, new uint[] { 100 }, cancellationToken),
4 => await host.DefineReportsAsync(3,
new GemReportSpec[] { new(20, ReadOnlyMemory<uint>.Empty) }, cancellationToken),
5 => await host.EnableEventsAsync(true, new uint[] { 100 }, cancellationToken),
6 => await host.DefineReportsAsync(4, Array.Empty<GemReportSpec>(), cancellationToken),
7 => await host.EnableEventsAsync(true, new uint[] { 200 }, cancellationToken),
8 => await host.LinkReportsAsync(5,
new GemLinkSpec[] { new(200, new uint[] { 10 }) }, cancellationToken),
_ => throw new ArgumentOutOfRangeException(nameof(step)),
};
static void CheckResponse(GemResultInfo info, int step)
{
GemRequestKind kind = step switch
{
0 or 4 or 6 => GemRequestKind.DefineReports,
1 or 8 => GemRequestKind.LinkReports,
_ => GemRequestKind.EnableEvents,
};
GemOutcome outcome = step switch
{
0 or 4 or 6 => GemOutcome.DefineReportsAck,
1 or 8 => GemOutcome.LinkReportsAck,
_ => GemOutcome.EnableEventsAck,
};
// Step 7 enables CEID 200 after S2F33 removed every report: the CEID exists, so ERACK is 0.
// Step 8 links an RPTID that no longer exists, so LRACK is 5.
byte acknowledgement = step == 8 ? (byte)5 : (byte)0;
if (info.Kind != GemResultKind.Response || info.RequestKind != kind ||
info.Outcome != outcome || info.Acknowledgement != acknowledgement || info.ReplySent)
throw new InvalidOperationException($"Unexpected reports response at scenario step {step}.");
}
static void CheckEvent(GemResult result, int ordinal)
{
GemEventInfo info = result.GetEventInfo();
nuint expectedReports = ordinal == 0 ? 2u : 1u;
if (info != new GemEventInfo((uint)(41 + ordinal), 100, expectedReports, false))
throw new InvalidOperationException("The event report metadata is incorrect.");
for (nuint reportIndex = 0; reportIndex < info.ReportCount; reportIndex++)
{
GemEventReport report = result.GetEventReport(reportIndex);
uint expectedId = reportIndex == 0 ? 10u : 20u;
nuint expectedValues = reportIndex == 0 ? 3u : 1u;
if (report != new GemEventReport(expectedId, expectedValues))
throw new InvalidOperationException("The event report definition is incorrect.");
for (nuint valueIndex = 0; valueIndex < report.ValueCount; valueIndex++)
{
using SecsItem value = result.GetEventValue(reportIndex, valueIndex);
if (reportIndex == 0 && valueIndex != 1)
{
if (value.GetAscii() != "LOT-001")
throw new InvalidOperationException("The event lot value is incorrect.");
}
else
{
float[] values = value.GetF4();
float expected = ordinal == 0 ? 21.5f : 32.0f;
if (values.Length != 1 || BitConverter.SingleToInt32Bits(values[0]) != BitConverter.SingleToInt32Bits(expected))
throw new InvalidOperationException("The event temperature value is incorrect.");
}
}
}
}
static HsmsEventInfo ValidateEvent(GemEvent current)
{
if (current.ProcessingStatus != PdcommStatus.Ok)
throw new PdcommException(current.ProcessingStatus);
HsmsEventInfo raw = current.TransportEvent.GetInfo();
if (raw.Kind == HsmsEventKind.Failed)
throw new InvalidOperationException($"HSMS transaction failed with reason {raw.Code}.");
if (raw.Kind == HsmsEventKind.ProtocolError)
throw new InvalidOperationException($"HSMS protocol error {raw.Code}, detail {raw.Detail}.");
if (raw.Kind == HsmsEventKind.TransportError)
throw new InvalidOperationException($"HSMS transport error {raw.Code}, detail {raw.Detail}.");
return raw;
}
static class DemoOptions
{
internal static HsmsOptions Transport(HsmsMode mode, SecsLimits limits) => new()
{
Mode = mode,
SessionId = 7,
InitialSystemBytes = 1,
T3Milliseconds = 2000,
T5Milliseconds = 20,
T6Milliseconds = 200,
T7Milliseconds = 1000,
T8Milliseconds = 500,
ConnectTimeoutMilliseconds = 50,
ShutdownTimeoutMilliseconds = 100,
WriteTimeoutMilliseconds = 1000,
MaxFrameBytes = 4110,
MaxEvents = 128,
MaxQueuedBytes = 32768,
MaxWriteFrames = 64,
MaxWriteBytes = 16384,
MaxIoChunk = 3,
MaxApplicationEvents = 128,
MaxApplicationBytes = 32768,
PollBudget = 32,
ItemLimits = limits,
};
internal static HsmsWorkerOptions Worker() => new()
{
CommandCapacity = 64,
MaxCommandBytes = 65536,
EventCapacity = 128,
MaxEventBytes = 65536,
PollIntervalMilliseconds = 1,
ShutdownTimeoutMilliseconds = 5000,
};
internal static GemHostOptions Host(SecsLimits limits) => new()
{
MaxPendingIds = 64,
ItemLimits = limits,
};
internal static GemEquipmentOptions Equipment(SecsLimits limits) => new()
{
Model = "PDComm",
SoftwareRevision = "0.1.0",
InitialControl = GemControlState.HostOffline,
OnlineMode = GemOnlineMode.Remote,
MaxVariables = 8,
MaxMetadataBytes = 1024,
MaxValueBytes = 4096,
ItemLimits = limits,
};
internal static GemReportOptions Reports() => new()
{
MaxEvents = 4,
MaxReports = 4,
MaxVariablesPerReport = 8,
MaxReportsPerEvent = 4,
MaxTotalVariables = 16,
MaxTotalLinks = 16,
};
}

C API는 pdcomm_gem_host_define_reports, pdcomm_gem_host_link_reports, pdcomm_gem_host_enable_events, pdcomm_gem_equipment_publish_event, pdcomm_gem_host_acknowledge_event가 같은 역할을 합니다.

examples/qt/reports.cpp는 같은 9단계를 C++ 래퍼로 구현합니다. 끝점(endpoint)은 QThread 작업 스레드에서만 다루고, 받은 이벤트는 queued_handoff.hpp의 invokeQueued로 UI 스레드에 넘겨 검사합니다. UI 스레드가 고른 ACKC6를 작업 스레드가 받아 GemHost::acknowledgeEvent를 호출합니다. GUI 애플리케이션에서 PDComm 끝점을 다루는 기본 구조입니다.

빈 폴더에 reports.cpp와 queued_handoff.hpp를 복사하고, 같은 폴더에 다음 CMakeLists.txt를 만듭니다. queued_handoff.hpp는 반드시 reports.cpp 옆에 있어야 합니다.

cmake_minimum_required(VERSION 3.20)
project(pdcomm_qt_reports LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PDCOMM_SDK "" CACHE PATH "Folder that contains include/ and lib/ of the PDComm SDK")
find_package(Qt6 REQUIRED COMPONENTS Core)
find_library(PDCOMM_FFI NAMES pdcomm_ffi pdcomm_ffi.dll PATHS "${PDCOMM_SDK}/lib" NO_DEFAULT_PATH REQUIRED)
add_executable(pdcomm_qt_reports reports.cpp)
target_include_directories(pdcomm_qt_reports PRIVATE "${PDCOMM_SDK}/include")
target_link_libraries(pdcomm_qt_reports PRIVATE Qt6::Core "${PDCOMM_FFI}")
set_target_properties(pdcomm_qt_reports PROPERTIES BUILD_RPATH "${PDCOMM_SDK}/lib")
터미널 창
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPDCOMM_SDK="$PDCOMM_SDK"
cmake --build build
./build/pdcomm_qt_reports equipment passive 127.0.0.1:0

Qt를 찾지 못하면 -DCMAKE_PREFIX_PATH=<Qt 설치 경로>를 추가합니다. Windows에서는 sdk\lib\pdcomm_ffi.dll.lib에 링크하고 실행 파일 옆에 pdcomm_ffi.dll을 둡니다.

장비가 LISTEN 127.0.0.1:<port>를 출력하면 다른 터미널에서 호스트를 실행합니다.

터미널 창
./build/pdcomm_qt_reports host active 127.0.0.1:54543

양쪽 모두 다음 줄을 출력하고 0으로 끝납니다.

PASS QT REPORTS

Qt 장비와 .NET 호스트는 같은 시나리오를 쓰므로 서로 짝지어 실행할 수도 있습니다. 인수 없이 실행하면 사용법이 나옵니다.

usage: pdcomm_qt_reports ROLE MODE ADDRESS [--cancel]
전체 코드: qt/reports.cpp
qt/reports.cpp
#include "pdcomm_gem.hpp"
#include "queued_handoff.hpp"
#include <QCoreApplication>
#include <QThread>
#include <QTimer>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <stdexcept>
#include <string>
#include <thread>
#include <utility>
#include <vector>
namespace {
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 = 1000;
value.t3_ms = 2000;
value.t5_ms = 50;
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 = 3;
value.max_application_events = 96;
value.max_application_bytes = 32768;
value.poll_budget = 16;
value.item_limits = itemLimits();
return value;
}
pdcomm::SecsItem f4(float value) {
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
const std::vector<uint8_t> bytes = {
static_cast<uint8_t>(bits >> 24),
static_cast<uint8_t>(bits >> 16),
static_cast<uint8_t>(bits >> 8),
static_cast<uint8_t>(bits)};
return pdcomm::SecsItem::primitive(PDCOMM_SECS_F4, bytes, itemLimits());
}
pdcomm::SecsItem ascii(const std::string &value) {
return pdcomm::SecsItem::primitive(
PDCOMM_SECS_ASCII,
reinterpret_cast<const uint8_t *>(value.data()), value.size(),
itemLimits());
}
bool asciiEquals(const pdcomm::SecsItem &item, const std::string &value) {
return item.info().format == PDCOMM_SECS_ASCII &&
item.payload() == std::vector<uint8_t>(value.begin(), value.end());
}
bool sameToken(const PdcommTcpToken &left, const PdcommTcpToken &right) {
return left.endpoint_id == right.endpoint_id &&
left.generation == right.generation &&
left.system_bytes == right.system_bytes;
}
void require(bool condition, const char *message) {
if (!condition) throw std::runtime_error(message);
}
void stopAndDrain(pdcomm::GemEndpoint &endpoint) {
if (!endpoint.tcp().state().closed) endpoint.stop();
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
while (true) {
if (!endpoint.tcp().state().closed) endpoint.poll();
for (auto event = endpoint.nextEvent(); event;
event = endpoint.nextEvent()) {
(void)endpoint.handle(event);
}
if (endpoint.tcp().state().closed ||
std::chrono::steady_clock::now() >= deadline) break;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
require(endpoint.tcp().state().closed, "GEM shutdown timed out");
}
PdcommTcpToken sendStep(pdcomm::GemHost &host, size_t step) {
switch (step) {
case 0:
return host.defineReports(1, {{10, {2, 1, 2}}, {20, {1}}});
case 1:
return host.linkReports(2, {{100, {10, 20}}, {200, {20}}});
case 2:
return host.enableEvents(true);
case 3:
return host.enableEvents(false, {100});
case 4:
return host.defineReports(3, {{20, {}}});
case 5:
return host.enableEvents(true, {100});
case 6:
return host.defineReports(4, {});
case 7:
return host.enableEvents(true, {200});
default:
return host.linkReports(5, {{200, {10}}});
}
}
struct UiPayload {
bool cancelled = false;
bool host = false;
std::vector<std::shared_ptr<pdcomm::GemResult>> events;
std::vector<std::shared_ptr<pdcomm::SecsItem>> retainedValues;
};
struct SharedState {
std::atomic<bool> active{false};
std::atomic<bool> succeeded{false};
std::atomic<int> uiDeliveries{0};
std::atomic<int> activeHeartbeats{0};
std::string failure;
};
class ReportsWorker final : public QThread {
public:
ReportsWorker(QCoreApplication *application, std::string role,
std::string mode, std::string address,
std::shared_ptr<SharedState> shared)
: application_(application), role_(std::move(role)),
mode_(std::move(mode)), address_(std::move(address)),
shared_(std::move(shared)) {}
protected:
void run() override {
shared_->active.store(true);
try {
require(QThread::currentThread() != application_->thread(),
"GEM endpoint ran on the UI thread");
auto payload = role_ == "host" ? runHost() : runEquipment();
postCompletion(std::move(payload));
} catch (const pdcomm::Error &error) {
shared_->failure = "PDComm status " +
std::to_string(error.status());
} catch (const std::exception &error) {
shared_->failure = error.what();
}
shared_->active.store(false);
}
private:
void postListen(const std::string &address) {
auto *application = application_;
const auto shared = shared_;
require(pdcomm_qt_example::invokeQueued(
application,
[application, shared, address] {
if (QThread::currentThread() != application->thread()) return;
shared->uiDeliveries.fetch_add(1);
std::fprintf(stdout, "LISTEN %s\n", address.c_str());
std::fflush(stdout);
}),
"failed to queue listen notification");
}
pdcomm::TcpEndpoint makeEndpoint() {
const bool activeMode = mode_ == "active";
pdcomm::TcpEndpoint endpoint(
address_, tcpOptions(activeMode ? PDCOMM_TCP_ACTIVE
: PDCOMM_TCP_PASSIVE));
if (!activeMode) postListen(endpoint.localAddress());
return endpoint;
}
uint8_t requestUiDecision(
const std::shared_ptr<pdcomm::GemResult> &result, size_t index) {
const auto decision = std::make_shared<std::atomic<int>>(-1);
auto *application = application_;
const auto shared = shared_;
require(pdcomm_qt_example::invokeQueued(
application,
[application, shared, result, decision, index] {
try {
require(QThread::currentThread() == application->thread(),
"event inspection missed the UI thread");
const auto info = result->info();
const auto event = result->eventInfo();
require(info.outcome == pdcomm::GemOutcome::EventReceived &&
!info.replySent && !event.acknowledged,
"event result state mismatch");
require(event.dataId == (index == 0 ? 41U : 42U) &&
event.eventId == 100 &&
event.reportCount == (index == 0 ? 2U : 1U),
"event identity mismatch");
const auto report = result->eventReport(0);
require(report.id == 10 && report.valueCount == 3,
"event report shape mismatch");
require(asciiEquals(result->value(0, 0), "LOT-001") &&
asciiEquals(result->value(0, 2), "LOT-001"),
"event lot snapshot mismatch");
const auto temperature = result->value(0, 1);
require(temperature.info().format == PDCOMM_SECS_F4 &&
temperature.payload() ==
f4(index == 0 ? 21.5F : 32.0F).payload(),
"event temperature snapshot mismatch");
if (index == 0) {
const auto second = result->eventReport(1);
require(second.id == 20 && second.valueCount == 1 &&
result->value(1, 0).payload() ==
f4(21.5F).payload(),
"second report snapshot mismatch");
}
shared->uiDeliveries.fetch_add(1);
decision->store(index == 0 ? 0 : 7,
std::memory_order_release);
} catch (...) {
decision->store(-2, std::memory_order_release);
}
}),
"failed to queue event inspection");
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
int value = -1;
while ((value = decision->load(std::memory_order_acquire)) == -1 &&
std::chrono::steady_clock::now() < deadline &&
!isInterruptionRequested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
require(value >= 0, "queued event decision failed");
return static_cast<uint8_t>(value);
}
UiPayload runHost() {
UiPayload payload;
payload.host = true;
{
auto endpoint = makeEndpoint();
pdcomm::GemHost host(
std::move(endpoint), pdcomm::gemHostOptions(itemLimits(), 64));
host.start();
bool beginSent = false;
bool sentFirst = false;
bool waitingForEvent = false;
size_t step = 0;
PdcommTcpToken requested[9] = {};
// Step 7 enables CEID 200 after S2F33 removed every report; the CEID
// exists, so ERACK is 0. Step 8 links an RPTID that no longer exists.
const uint8_t expectedAck[] = {0, 0, 0, 0, 0, 0, 0, 0, 5};
const pdcomm::GemRequest expectedRequest[] = {
pdcomm::GemRequest::DefineReports,
pdcomm::GemRequest::LinkReports,
pdcomm::GemRequest::EnableEvents,
pdcomm::GemRequest::EnableEvents,
pdcomm::GemRequest::DefineReports,
pdcomm::GemRequest::EnableEvents,
pdcomm::GemRequest::DefineReports,
pdcomm::GemRequest::EnableEvents,
pdcomm::GemRequest::LinkReports};
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(15);
while (step < 9 && std::chrono::steady_clock::now() < deadline &&
!isInterruptionRequested()) {
host.poll();
if (!beginSent &&
host.tcp().state().state == PDCOMM_TCP_SELECTED) {
host.beginCommunication();
beginSent = true;
}
std::shared_ptr<pdcomm::GemResult> pendingEvent;
for (auto event = host.nextEvent(); event;
event = host.nextEvent()) {
auto result = host.handle(event);
const auto info = result.info();
if (info.outcome ==
pdcomm::GemOutcome::CommunicationEstablished) {
if (!sentFirst) {
requested[0] = sendStep(host, 0);
sentFirst = true;
}
} else if (info.outcome ==
pdcomm::GemOutcome::EventReceived) {
require(waitingForEvent && payload.events.size() < 2,
"unexpected event report");
pendingEvent =
std::make_shared<pdcomm::GemResult>(
std::move(result));
} else if (info.kind ==
pdcomm::GemResultKind::Response) {
require(step < 9 &&
info.request == expectedRequest[step] &&
sameToken(info.token, requested[step]) &&
info.acknowledgementPresent &&
info.acknowledgement == expectedAck[step],
"configuration response mismatch");
++step;
if (step == 3 || step == 6) {
waitingForEvent = true;
} else if (step < 9) {
requested[step] = sendStep(host, step);
}
}
}
if (pendingEvent) {
const size_t index = payload.events.size();
const uint8_t ack = requestUiDecision(pendingEvent, index);
payload.retainedValues.push_back(
std::make_shared<pdcomm::SecsItem>(
pendingEvent->value(0, 1)));
host.acknowledgeEvent(*pendingEvent, ack);
payload.events.push_back(std::move(pendingEvent));
waitingForEvent = false;
requested[step] = sendStep(host, step);
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (isInterruptionRequested()) {
payload.cancelled = true;
} else {
require(step == 9 && payload.events.size() == 2,
"host report exchange timed out");
}
stopAndDrain(host);
}
return payload;
}
UiPayload runEquipment() {
UiPayload payload;
{
auto endpoint = makeEndpoint();
pdcomm::GemEquipment equipment(
std::move(endpoint), itemLimits(),
pdcomm::GemControl::HostOffline,
pdcomm::GemOnlineMode::Remote, "PDComm", "0.1.0", 8,
1024, 4096);
equipment.configureReports(
pdcomm::gemReportOptions(4, 4, 8, 4, 16, 16), {100, 200});
equipment.registerStatus(1, "Temperature", "C", f4(21.5F));
equipment.registerStatus(2, "Lot", "", ascii("LOT-001"));
equipment.start();
size_t primaries = 0;
size_t eventAcks = 0;
PdcommTcpToken published[2] = {};
bool peerStopped = false;
// Step 7 enables CEID 200 after S2F33 removed every report; the CEID
// exists, so ERACK is 0. Step 8 links an RPTID that no longer exists.
const uint8_t expectedAck[] = {0, 0, 0, 0, 0, 0, 0, 0, 5};
const pdcomm::GemOutcome expectedPrimaryOutcome[] = {
pdcomm::GemOutcome::DefineReportsAck,
pdcomm::GemOutcome::LinkReportsAck,
pdcomm::GemOutcome::EnableEventsAck,
pdcomm::GemOutcome::EnableEventsAck,
pdcomm::GemOutcome::DefineReportsAck,
pdcomm::GemOutcome::EnableEventsAck,
pdcomm::GemOutcome::DefineReportsAck,
pdcomm::GemOutcome::EnableEventsAck,
pdcomm::GemOutcome::LinkReportsAck};
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(15);
while (std::chrono::steady_clock::now() < deadline &&
!isInterruptionRequested()) {
equipment.poll();
for (auto event = equipment.nextEvent(); event;
event = equipment.nextEvent()) {
auto result = equipment.handle(event);
const auto info = result.info();
if (info.kind == pdcomm::GemResultKind::Primary &&
(info.outcome ==
pdcomm::GemOutcome::DefineReportsAck ||
info.outcome == pdcomm::GemOutcome::LinkReportsAck ||
info.outcome ==
pdcomm::GemOutcome::EnableEventsAck)) {
require(primaries < 9 &&
info.outcome ==
expectedPrimaryOutcome[primaries] &&
info.acknowledgementPresent &&
info.acknowledgement == expectedAck[primaries],
"equipment configuration mismatch");
if (primaries == 2) {
published[0] = equipment.publishEvent(41, 100);
equipment.updateStatus(1, f4(32.0F));
} else if (primaries == 5) {
published[1] = equipment.publishEvent(42, 100);
}
++primaries;
} else if (info.kind ==
pdcomm::GemResultKind::Response &&
info.outcome == pdcomm::GemOutcome::EventAck) {
require(eventAcks < 2 &&
info.request ==
pdcomm::GemRequest::EventReport &&
info.acknowledgementPresent &&
info.acknowledgement ==
(eventAcks == 0 ? 0 : 7) &&
sameToken(info.token,
published[eventAcks]),
"equipment event ACK mismatch");
++eventAcks;
}
}
if (primaries == 9 && eventAcks == 2 &&
equipment.tcp().state().state != PDCOMM_TCP_SELECTED) {
peerStopped = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (isInterruptionRequested()) {
payload.cancelled = true;
} else {
require(primaries == 9 && eventAcks == 2 && peerStopped,
"equipment report exchange timed out");
}
stopAndDrain(equipment);
}
return payload;
}
void postCompletion(UiPayload payload) {
const auto completion = std::make_shared<std::atomic<int>>(0);
auto *application = application_;
const auto shared = shared_;
require(pdcomm_qt_example::invokeQueued(
application,
[application, shared, payload, completion] {
try {
require(QThread::currentThread() == application->thread(),
"completion missed the UI thread");
if (!payload.cancelled && payload.host) {
require(payload.events.size() == 2 &&
payload.retainedValues.size() == 2,
"retained host results are missing");
require(payload.events[0]->eventInfo().acknowledged &&
payload.events[1]->eventInfo().acknowledged,
"retained result ACK state mismatch");
require(payload.retainedValues[0]->payload() ==
f4(21.5F).payload() &&
payload.retainedValues[1]->payload() ==
f4(32.0F).payload(),
"retained event values changed");
}
shared->uiDeliveries.fetch_add(1);
completion->store(1);
} catch (...) {
completion->store(-1);
}
}),
"failed to queue completion");
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
while (completion->load() == 0 &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
require(completion->load() == 1,
"queued completion validation failed");
require(shared_->activeHeartbeats.load() > 0,
"UI heartbeat did not run while worker was active");
shared_->succeeded.store(true);
}
QCoreApplication *application_;
std::string role_;
std::string mode_;
std::string address_;
std::shared_ptr<SharedState> shared_;
};
} // namespace
int main(int argc, char **argv) {
QCoreApplication application(argc, argv);
if (argc < 4 || argc > 5 ||
(std::string(argv[1]) != "host" &&
std::string(argv[1]) != "equipment") ||
(std::string(argv[2]) != "active" &&
std::string(argv[2]) != "passive") ||
(argc == 5 && std::string(argv[4]) != "--cancel")) {
std::fprintf(stderr,
"usage: pdcomm_qt_reports ROLE MODE ADDRESS [--cancel]\n");
return 2;
}
const bool cancel = argc == 5;
const auto shared = std::make_shared<SharedState>();
ReportsWorker worker(&application, argv[1], argv[2], argv[3], shared);
QTimer heartbeat;
heartbeat.setInterval(1);
QObject::connect(&heartbeat, &QTimer::timeout, &application,
[shared, &application] {
if (QThread::currentThread() == application.thread() &&
shared->active.load()) {
shared->activeHeartbeats.fetch_add(1);
}
});
heartbeat.start();
QTimer cancelTimer;
cancelTimer.setSingleShot(true);
QObject::connect(&cancelTimer, &QTimer::timeout, &application,
[&worker] { worker.requestInterruption(); });
QTimer watchdog;
watchdog.setSingleShot(true);
QObject::connect(&watchdog, &QTimer::timeout, &application,
[&worker] { worker.requestInterruption(); });
QObject::connect(&worker, &QThread::started, &application,
[&] {
watchdog.start(18000);
if (cancel) cancelTimer.start(20);
});
QObject::connect(&worker, &QThread::finished, &application,
[&] {
worker.wait();
QTimer::singleShot(0, &application, [&] {
if (!shared->succeeded.load() ||
shared->uiDeliveries.load() == 0 ||
shared->activeHeartbeats.load() == 0) {
std::fprintf(stderr, "FAIL QT REPORTS: %s\n",
shared->failure.empty()
? "UI/worker validation failed"
: shared->failure.c_str());
application.exit(1);
return;
}
std::fprintf(stdout, "PASS QT REPORTS\n");
std::fflush(stdout);
application.quit();
});
});
worker.start();
const int result = application.exec();
worker.requestInterruption();
worker.wait();
return result;
}

GUI 시뮬레이터에서도 같은 흐름을 손으로 따라갈 수 있습니다.

  1. 장비 역할 창에서 Reports & Events 작업공간의 Catalog 탭을 엽니다. Load example을 누르고 Use on next start로 예약한 뒤 세션을 시작합니다. 예제 카탈로그는 RPTID 1(VID 1, 2), RPTID 2(VID 3), CEID 10(RPTID 1, 2 연결, 켜짐)과 VID 바인딩(VID 1·2는 SVID 1001·1002, VID 3은 DVID 2001)입니다. 시작하면 Actual session catalog에 실제 구성이 나타납니다.
  2. 호스트 역할 창에서 통신을 수립합니다. 예제 카탈로그의 CEID 10은 이미 켜져 있으므로 원격 설정 없이 바로 보고를 받을 수 있습니다. 호스트가 직접 보고서를 만들어 보려면 Remote configuration 탭의 Define reports, Link reports, Apply event state를 씁니다.
  3. 장비 창의 Traffic 탭에서 DATAID와 CEID(예: 10)를 넣고 Emit을 누릅니다. 호스트 창의 Traffic 탭에서 받은 이벤트를 고르고 ACKC6 값을 정한 뒤 Acknowledge를 누릅니다.
장비의 Reports & Events Catalog 탭. 예제 초안과 실제 세션 카탈로그(보고서 2개, 이벤트 1개, 바인딩 3개)가 보입니다.장비의 Reports & Events Catalog 탭. 예제 초안과 실제 세션 카탈로그(보고서 2개, 이벤트 1개, 바인딩 3개)가 보입니다.
호스트의 Reports & Events Traffic 탭. DATAID 100, CEID 10 이벤트가 확인을 기다리는 Pending 상태입니다.호스트의 Reports & Events Traffic 탭. DATAID 100, CEID 10 이벤트가 확인을 기다리는 Pending 상태입니다.

자세한 화면 설명은 시뮬레이터 GUI 가이드를 참고합니다.

증상 원인 해결
요청이 PdcommException(InvalidState)으로 실패합니다. 통신 수립(S1F13) 전에 요청했습니다. CommunicationEstablished를 받은 뒤 보냅니다.
S2F38 ERACK 1 장비에 없는 CEID를 지정했습니다. 장비가 제공하는 CEID인지 확인합니다. 보고서 연결 여부와는 관계없습니다.
S2F36 LRACK 5 연결하려는 RPTID가 없습니다. S2F33으로 보고서를 먼저 정의합니다.
장비가 S6F11을 보내지 않습니다. 이벤트가 꺼져 있습니다. 장비 쪽 PublishEvent도 이때 실패합니다. S2F37로 켰는지, S2F38 응답이 0이었는지 확인합니다.
S6F11에 보고서가 하나도 없습니다. 켜진 이벤트에 연결된 보고서가 없습니다. S2F35로 보고서를 연결하고 S2F36 응답이 0인지 확인합니다.
장비가 S6F12를 받지 못하고 T3 뒤에 실패합니다. 호스트가 AcknowledgeEventAsync를 호출하지 않았습니다. 보고서를 처리한 뒤 반드시 응답합니다.
보고서 값의 순서가 예상과 다릅니다. S6F11 값에는 VID가 없습니다. 호스트가 정의한 VID 순서대로 해석합니다.
  • 예제는 끝나면 스스로 종료합니다. 중간에 멈췄다면 각 터미널에서 Ctrl+C로 끝냅니다.
  • 빌드 결과물은 $PDCOMM_ROOT/build/managed와 Qt 예제 폴더의 build에 있습니다. 필요 없으면 지웁니다.
  • 알람에서 알람 보고와 알람 연동 이벤트를 다룹니다.
  • 데이터 수집에서 데이터 변수, 장비 상수, 보고서 조회를 다룹니다.
  • API 목록은 API 레퍼런스에서 찾습니다.