Reports and events
With event reports, the host tells the equipment in advance which values to send when something happens. In this tutorial you run the whole cycle: the host defines, links and enables reports, the equipment sends S6F11, and the host acknowledges it with S6F12.
What you learn
Section titled “What you learn”- The order of
S2F33(define report),S2F35(link event report) andS2F37(enable event report), and what their acknowledgement codes mean. - When the equipment sends
S6F11, and how the host reads the report values. - How the same flow looks with the .NET worker API and with a Qt worker thread.
Before you begin
Section titled “Before you begin”- Complete Set up your environment. This page uses the variables you set there:
PDCOMM_ROOT,PDCOMM_SDK,PDCOMM_NATIVEandPDCOMM_EXAMPLES($Root,$Sdk,$Nativeand$Examplesin PowerShell). - Install the .NET 10 SDK. For the Qt example you also need Qt 6 (Core), CMake 3.20 or later and a build tool such as Ninja.
- It helps if you have already established communication (
S1F13) in First communication.
What the example does
Section titled “What the example does”PDComm.GemReportsDemo contains both roles. The equipment starts with two status variables (SVIDs) and two collection events (CEIDs):
| Equipment data | Value |
|---|---|
SVID 1 Temperature |
F4 21.5 (changes to 32.0 right after the first event report) |
SVID 2 Lot |
ASCII LOT-001 |
| CEIDs | 100, 200 |
The example configures no report bindings, so each VID in a report refers to the status variable with the same number. After communication is established, the host sends nine steps:
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- A report defined with an empty VID list (step 4) deletes that report and its links. An empty report list (step 6) deletes every report.
- Defining, deleting or relinking reports never changes whether an event is enabled. Only
S2F37changes the enable state. - After it answers an enable request (
S2F38), the equipment sendsS6F11if CEID 100 is enabled. While step 3 keeps the event disabled, nothing is sent. - In step 5 the host answers with ACKC6 7. ACKC6 0 means accepted and 1–63 mean rejected. The example checks that a nonzero code reaches the equipment unchanged.
- Step 7 shows that you can enable CEID 200 even though no report is linked to it any more. The equipment returns ERACK 1 only for a CEID it doesn’t have. When an enabled event has no linked reports, the equipment sends its
S6F11with an empty report list. - Step 8 shows a rejection: the equipment returns LRACK 5 when you link a RPTID that doesn’t exist.
1. Build the example
Section titled “1. Build the example”dotnet build "$Examples\dotnet\PDComm.GemReportsDemo\PDComm.GemReportsDemo.csproj" -c Release "-p:PDCommSdk=$Sdk" --artifacts-path "$Root\build\managed"dotnet build "$PDCOMM_EXAMPLES/dotnet/PDComm.GemReportsDemo/PDComm.GemReportsDemo.csproj" -c Release "-p:PDCommSdk=$PDCOMM_SDK" --artifacts-path "$PDCOMM_ROOT/build/managed"dotnet build "$PDCOMM_EXAMPLES/dotnet/PDComm.GemReportsDemo/PDComm.GemReportsDemo.csproj" -c Release "-p:PDCommSdk=$PDCOMM_SDK" --artifacts-path "$PDCOMM_ROOT/build/managed"The -p:PDCommSdk property makes the project reference managed/PDComm.dll from your SDK. A successful build ends like this:
PDComm.GemReportsDemo -> <PDCOMM_ROOT>/build/managed/bin/PDComm.GemReportsDemo/release/PDComm.GemReportsDemo.dll
Build succeeded. 0 Warning(s) 0 Error(s)2. Start the equipment
Section titled “2. Start the equipment”In a first terminal, start the equipment in passive mode. Port 0 lets the operating system pick a free port.
dotnet "$Root\build\managed\bin\PDComm.GemReportsDemo\release\PDComm.GemReportsDemo.dll" $Native equipment passive 127.0.0.1:0dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemReportsDemo/release/PDComm.GemReportsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemReportsDemo/release/PDComm.GemReportsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0The equipment prints the address it listens on and waits. Your port number differs on every run.
LISTEN 127.0.0.1:544753. Run the host
Section titled “3. Run the host”In a second terminal, start the host in active mode with the address the equipment printed.
dotnet "$Root\build\managed\bin\PDComm.GemReportsDemo\release\PDComm.GemReportsDemo.dll" $Native host active 127.0.0.1:54475dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemReportsDemo/release/PDComm.GemReportsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:54475dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemReportsDemo/release/PDComm.GemReportsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:54475All nine steps finish in under a second, and both programs exit with code 0:
PASS host active: managed GEM reports worker completed scenario 9 and shut downThe equipment terminal prints:
PASS equipment passive: managed GEM reports worker completed scenario 9 and shut downBoth programs check every acknowledgement code, event value and token themselves. On any mismatch they throw InvalidOperationException and exit with a nonzero code.
4. Read the code
Section titled “4. Read the code”Equipment side
Section titled “Equipment side”StartEquipmentAsync starts the equipment worker with the status variables and a GemReportConfiguration. The second argument of GemReportConfiguration, { 100, 200 }, lists the CEIDs the equipment offers. The host creates the reports and links remotely.
RunEquipmentAsync reads the event stream and does three things:
- The SDK answers
S2F33,S2F35andS2F37automatically. The application only inspects theOutcome(DefineReportsAcktoEnableEventsAck) and theAcknowledgementof each result. - When the
EnableEventsAckof step 2 or step 5 is 0 and CEID 100 is enabled, it sendsS6F11withPublishEventAsync. After the first report it sets the temperature to 32.0 withUpdateStatusAsync. Reports already sent don’t change: the report body is a snapshot taken when you publish. - The host’s
S6F12arrives as aResponseresult with requestEventReportand outcomeEventAck. The example compares its token with the token returned when it published.
Host side
Section titled “Host side”SendStepAsync builds the nine requests:
DefineReportsAsyncsendsS2F33with a DATAID and a list ofGemReportSpecvalues.LinkReportsAsyncsendsS2F35with a list ofGemLinkSpecvalues.EnableEventsAsyncsendsS2F37. An empty CEID list addresses every event.
When RunHostAsync gets a result whose Outcome is EventReceived, CheckEvent validates the values and AcknowledgeEventAsync sends S6F12. The SDK never answers S6F11 on its own; your host application replies after it has processed the report.
S6F11 carries values without VIDs. Use GetEventReport for the report ID and value count and GetEventValue for each value, and map positions through the reports you defined. RPTID 10 in the example lists VIDs 2, 1, 2, so its values arrive as LOT-001, 21.5, LOT-001.
Full source: 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, };}In C, pdcomm_gem_host_define_reports, pdcomm_gem_host_link_reports, pdcomm_gem_host_enable_events, pdcomm_gem_equipment_publish_event and pdcomm_gem_host_acknowledge_event do the same jobs.
5. Run the same flow with Qt
Section titled “5. Run the same flow with Qt”examples/qt/reports.cpp implements the same nine steps with the C++ wrappers. Only a QThread worker touches the endpoint. The worker hands each received event to the UI thread with invokeQueued from queued_handoff.hpp, the UI thread inspects it and picks the ACKC6, and the worker then calls GemHost::acknowledgeEvent. Use this structure whenever a GUI application drives a PDComm endpoint.
Create the build file
Section titled “Create the build file”Copy reports.cpp and queued_handoff.hpp into an empty folder and add this CMakeLists.txt next to them. queued_handoff.hpp must sit next to 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")Build and run
Section titled “Build and run”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:0If CMake can’t find Qt, add -DCMAKE_PREFIX_PATH=<Qt installation prefix>. On Windows, link against sdk\lib\pdcomm_ffi.dll.lib and keep pdcomm_ffi.dll next to the executable.
When the equipment prints LISTEN 127.0.0.1:<port>, start the host in another terminal:
./build/pdcomm_qt_reports host active 127.0.0.1:54543Both sides print the following line and exit with code 0:
PASS QT REPORTSThe Qt equipment and the .NET host follow the same script, so you can also pair them. Run the program without arguments to see its usage:
usage: pdcomm_qt_reports ROLE MODE ADDRESS [--cancel]Full source: 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;}Try it in the simulator
Section titled “Try it in the simulator”You can walk through the same flow by hand in the GUI simulator:
- In an Equipment window, open the Catalog tab of the Reports & Events workspace. Click Load example, reserve it with Use on next start, and start the session. The example catalog holds RPTID 1 (VIDs 1, 2), RPTID 2 (VID 3), CEID 10 (linked to RPTIDs 1 and 2, enabled) and VID bindings (VIDs 1 and 2 to SVIDs 1001 and 1002, VID 3 to DVID 2001). After the start, Actual session catalog shows what the session really uses.
- In a Host window, establish communication. CEID 10 of the example catalog is already enabled, so the host can receive reports without any remote configuration. To build reports from the host instead, use Define reports, Link reports and Apply event state on the Remote configuration tab.
- In the Equipment window’s Traffic tab, enter a DATAID and a CEID (for example 10) and click Emit. In the Host window’s Traffic tab, select the received event, choose an ACKC6 value and click Acknowledge.




For a tour of every control, see the simulator GUI guide.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
A request fails with PdcommException (InvalidState). |
You sent it before communication (S1F13) was established. |
Wait for CommunicationEstablished first. |
S2F38 returns ERACK 1. |
The CEID doesn’t exist on the equipment. | Check that the equipment offers that CEID. Linked reports don’t matter here. |
S2F36 returns LRACK 5. |
The RPTID you link doesn’t exist. | Define the report with S2F33 first. |
The equipment never sends S6F11. |
The event is disabled. The equipment’s PublishEvent call fails in that case too. |
Check that you enabled it with S2F37 and that S2F38 returned 0. |
S6F11 arrives without any reports. |
The enabled event has no linked reports. | Link reports with S2F35 and check that S2F36 returned 0. |
| The equipment’s report fails after T3. | The host never called AcknowledgeEventAsync. |
Always reply after you process the report. |
| Values appear in an unexpected order. | S6F11 carries no VIDs. |
Interpret values in the VID order of your report definitions. |
Clean up
Section titled “Clean up”- The examples exit on their own. If one hangs, stop it with Ctrl+C.
- Build output lives in
$PDCOMM_ROOT/build/managedand in thebuildfolder of your Qt project. Delete them when you no longer need them.
Next steps
Section titled “Next steps”- Alarms covers alarm reports and alarm-linked events.
- Data collection covers data variables, equipment constants and report queries.
- Look up any API in the API reference.