Remote commands
A host uses remote commands to tell equipment to do something. The host sends S2F41 (Host Command Send) or S2F49 (Enhanced Remote Command). The equipment checks the command, decides whether it will act, and answers with S2F42 or S2F50. When the work itself finishes, the equipment usually says so with a separate collection event.
In this tutorial you run the whole exchange with two PDComm examples, then repeat it with the Qt examples and in the simulator GUI.
What you’ll do
Section titled “What you’ll do”- Follow RCMD, CPNAME/CPVAL, HCACK and CPACK through an S2F41 exchange.
- Trace the equipment’s three stages in code: validation on receipt, the application’s decision, and the reply.
- Separate acceptance (HCACK 0 or 4) from completion, and report completion with an event report (S6F11).
- Send S2F49 enhanced commands with a target object and nested parameters, and read a recursive CEPACK reply.
Before you begin
Section titled “Before you begin”- Set
PDCOMM_ROOT,PDCOMM_SDK,PDCOMM_NATIVEandPDCOMM_EXAMPLESas described in Set up your environment. In PowerShell the variables are$Root,$Sdk,$Nativeand$Examples. - Install the .NET 10 SDK.
- To build the Qt examples, you also need Qt 6 Core, CMake 3.20 or later and a C++17 compiler.
- Completion events build on Reports and events. Read it first if event reports are new to you.
How a remote command flows
Section titled “How a remote command flows”Host Equipment | S2F41 RCMD + [CPNAME, CPVAL]... | |-------------------------------------->| 1. validation on receipt | | (command, online remote, parameters) | | 2. application decision (HCACK) | S2F42 HCACK + [CPNAME, CPACK]... |<--------------------------------------| 3. reply | | 4. work (HCACK 0: before the reply, | | HCACK 4: after the reply) | S6F11 completion event | |<--------------------------------------| 5. completion report | S6F12 ACKC6 | |-------------------------------------->|A PDComm equipment registers its command catalog up front with GemCommandDefinition entries. Each entry holds the command name, the parameter definitions (GemCommandParameterDefinition), whether the command needs the online remote control state, and an optional completion CEID. When an S2F41 arrives, PDComm checks it before your code sees it:
| Check | HCACK on failure | CPACK on failure |
|---|---|---|
| The command is registered | 1 | - |
| A command that needs online remote finds the equipment online remote | 2 | - |
| Required parameters are present, with a valid count and integer range | 3 | 2 |
| Each value has the defined format | 3 | 3 |
| Each parameter name is known | 3 | 1 |
Read the outcome from GemCommandResultInfo on the received result. When ValidationAccepted is false, the reply carries the HCACK and CPACK values that validation chose, and your decision is ignored. Only for an accepted command do you pick the HCACK, as a GemCommandDecision value:
| HCACK | GemCommandDecision |
Meaning |
|---|---|---|
| 0 | Completed |
The command was performed. |
| 1 | UnknownCommand |
The command does not exist. |
| 2 | CannotPerformNow |
The command can’t be performed now. |
| 3 | ParameterErrors |
At least one parameter is invalid. This is the only decision that can carry a CPACK list. |
| 4 | AcceptedForLater |
The command is accepted and will be performed; completion is signaled later by an event. |
| 5 | AlreadyInCondition |
The equipment is already in the requested condition. |
| 6 | NoSuchObject |
The addressed object does not exist. |
1. Build the examples
Section titled “1. Build the examples”Build both .NET examples against the SDK.
foreach ($Demo in 'PDComm.GemCommandsDemo', 'PDComm.GemEnhancedCommandsDemo') { dotnet build "$Examples\dotnet\$Demo\$Demo.csproj" -c Release "-p:PDCommSdk=$Sdk" --artifacts-path "$Root\build\managed"}for demo in PDComm.GemCommandsDemo PDComm.GemEnhancedCommandsDemo; do dotnet build "$PDCOMM_EXAMPLES/dotnet/$demo/$demo.csproj" -c Release "-p:PDCommSdk=$PDCOMM_SDK" --artifacts-path "$PDCOMM_ROOT/build/managed"donefor demo in PDComm.GemCommandsDemo PDComm.GemEnhancedCommandsDemo; do dotnet build "$PDCOMM_EXAMPLES/dotnet/$demo/$demo.csproj" -c Release "-p:PDCommSdk=$PDCOMM_SDK" --artifacts-path "$PDCOMM_ROOT/build/managed"doneExpected result: each project prints Build succeeded. and 0 Error(s). The assemblies land in build/managed/bin/<example>/release/<example>.dll.
If it fails
error CS0246: The type or namespace name 'PDComm' could not be found: make sure-p:PDCommSdkpoints at thesdkfolder, which containsmanaged/PDComm.dll.- The .NET SDK is missing or too old: run
dotnet --infoand install the .NET 10 SDK.
2. Run the S2F41 example
Section titled “2. Run the S2F41 example”Open two terminals. Start the equipment first, as the passive side. Port 0 lets the operating system pick a free port.
# Terminal A: equipmentdotnet "$Root\build\managed\bin\PDComm.GemCommandsDemo\release\PDComm.GemCommandsDemo.dll" $Native equipment passive 127.0.0.1:0# Terminal B: host; use the address that terminal A printeddotnet "$Root\build\managed\bin\PDComm.GemCommandsDemo\release\PDComm.GemCommandsDemo.dll" $Native host active 127.0.0.1:<port># Terminal A: equipmentdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemCommandsDemo/release/PDComm.GemCommandsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0# Terminal B: host; use the address that terminal A printeddotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemCommandsDemo/release/PDComm.GemCommandsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port># Terminal A: equipmentdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemCommandsDemo/release/PDComm.GemCommandsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0# Terminal B: host; use the address that terminal A printeddotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemCommandsDemo/release/PDComm.GemCommandsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port>Expected result: the equipment prints LISTEN 127.0.0.1:<port> first. Within about a second both programs print their PASS line and exit with code 0:
PASS host active: managed GEM commands worker completed 16 steps, simulated 3 executions, and shut downLISTEN 127.0.0.1:54615PASS equipment passive: managed GEM commands worker completed 16 steps, simulated 3 executions, and shut downThe example checks a fixed 16-step script. If any reply differs, it stops with an exception such as Command acknowledgement mismatch. You can also swap the connection roles (host passive, equipment active) and get the same result.
3. Walk through the script
Section titled “3. Walk through the script”At startup the equipment registers two commands:
SET_TEMPtakes a requiredVALUE(I4, 0 to 100) and an optionalTAG(ASCII, up to 32 bytes). It needs online remote, and its completion event is CEID 100.- U1
7is a command with a numeric name. It takes no parameters and works in any control state.
The host (RunHost and SendStep) sends the requests below. The equipment (RunEquipment) decides on each one and answers with GemEquipmentWorker.RespondCommandAsync.
| Step | Host request | HCACK | Why |
|---|---|---|---|
| 0 | SET_TEMP VALUE=10 |
2 | The equipment is still host-offline. Validation rejects the command, so the equipment’s decision is ignored. |
| 1–4 | S1F17 online request, S2F33 define report, S2F35 link event, S2F37 enable event | 0 | Brings the equipment online remote and prepares the CEID 100 report. |
| 5 | MISSING |
1 | The command isn’t registered. |
| 6 | SET_TEMP with no parameters |
3 | The required VALUE is missing. |
| 7 | SET_TEMP VALUE=101 |
3 | The value is outside 0 to 100. |
| 8 | SET_TEMP VALUE=20 TAG=NOW |
0 | The equipment sets status variable 3 to 20, replies, and then reports the completion event (DATAID 71). |
| 9 | SET_TEMP VALUE=30 TAG=LATER |
4 | The equipment replies first, then sets the value to 30 and reports the completion event (DATAID 72). |
| 10–13 | U1 7, four times |
5, 6, 2, 1 | Validation passes; the equipment application picks each HCACK. |
| 14 | SET_TEMP VALUE=40 TAG=BAD |
3 | The equipment answers ParameterErrors with two CPACK entries: VALUE 2 and TAG 3. |
| 15 | SET_TEMP VALUE=50 TAG=LAST |
0 | The equipment sets the value to 50 and reports the completion event (DATAID 73). |
“Simulated 3 executions” refers to steps 8, 9 and 15, where the equipment changes status variable 3 in place of a real machine action. The host checks the value in each completion event (20, 30, 50) and answers with S6F12. For the second event it deliberately sends ACKC6 7, to show that a host can return a nonzero acknowledgement.
On the equipment side, RunEquipment follows this order:
- Read
ValidationAcceptedfromGetCommandInfo(). - If validation accepted the command, read the name and parameters and pick a decision.
- For HCACK 0, do the work first and then reply with
RespondCommandAsync. - For HCACK 4, reply first and start the work afterwards.
- When the reply has a
CompletionEvent, report it withGemEquipmentWorker.PublishEventAsynconce the work is done.
The host sends each command with GemHostWorker.RemoteCommandAsync and reads Acknowledgement (HCACK) and GetCommandIssue (CPACK) from the reply.
Full source of PDComm.GemCommandsDemo/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.GemCommandsDemo <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 limits = new(4096, 4096, 256, 16);using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(25));await using GemWorker worker = role == "host" ? await GemHostWorker.StartAsync(args[3], DemoOptions.Transport(mode, limits), DemoOptions.Host(limits), DemoOptions.Worker(), deadline.Token) : await StartEquipment(args[3], mode, limits, deadline.Token);if (mode == HsmsMode.Passive){ Console.WriteLine($"LISTEN {worker.LocalAddress ?? throw new InvalidOperationException("Missing passive address.")}"); Console.Out.Flush();}await using IAsyncEnumerator<GemEvent> events = worker.ReadEventsAsync(deadline.Token).GetAsyncEnumerator();if (worker is GemHostWorker host) await RunHost(host, events, limits, deadline.Token);else await RunEquipment((GemEquipmentWorker)worker, events, limits, deadline.Token);while (await events.MoveNextAsync()) events.Current.Dispose();await worker.Completion.WaitAsync(deadline.Token);Console.WriteLine($"PASS {role} {args[2]}: managed GEM commands worker completed 16 steps, simulated 3 executions, and shut down");return 0;
static async Task<GemEquipmentWorker> StartEquipment(string address, HsmsMode mode, SecsLimits limits, CancellationToken token){ using SecsItem initial = SecsItem.FromI4([0], limits); using SecsItem command = SecsItem.FromAscii("SET_TEMP", limits); using SecsItem value = SecsItem.FromAscii("VALUE", limits); using SecsItem tag = SecsItem.FromAscii("TAG", limits); using SecsItem numeric = SecsItem.FromU1([7], limits); GemCommandParameterDefinition[] parameters = [ new(value, true, SecsFormat.I4, 1, 1, 0, 100), new(tag, false, SecsFormat.Ascii, 0, 32), ]; var commands = new GemCommandConfiguration( DemoOptions.Commands(), new GemCommandDefinition[] { new(command, parameters, true, 100), new(numeric, Array.Empty<GemCommandParameterDefinition>(), false), }); return await GemEquipmentWorker.StartAsync( address, DemoOptions.Transport(mode, limits), DemoOptions.Equipment(limits), DemoOptions.Worker(), [new GemStatusVariable(3, "CommandValue", "C", initial)], new GemReportConfiguration(DemoOptions.Reports(), new uint[] { 100, 200 }), null, commands, token);}
static async Task RunHost(GemHostWorker host, IAsyncEnumerator<GemEvent> events, SecsLimits limits, CancellationToken token){ bool began = false; bool established = false; bool responseReceived = false; bool eventReceived = false; int step = 0; int eventOrdinal = 0; HsmsToken pending = default; while (step < 16) { if (!began && (await host.GetTransportStateAsync(token)).State == HsmsConnectionState.Selected) { pending = await host.BeginCommunicationAsync(token); began = true; } if (!await events.MoveNextAsync()) throw new InvalidOperationException("Command host event stream ended early."); GemEvent current = events.Current; bool disposed = false; try { 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("Command response token mismatch."); if (!established) { if (info.Outcome != GemOutcome.CommunicationEstablished) throw new InvalidOperationException("Communication failed."); established = true; pending = await SendStep(host, step, limits, token); } else { CheckResponse(result, step); responseReceived = true; } } else if (info.Outcome == GemOutcome.EventReceived) { if (step is not (8 or 9 or 15)) throw new InvalidOperationException("Unexpected completion event."); GemEventInfo eventInfo = result.GetEventInfo(); if (eventInfo.DataId != (uint)(71 + eventOrdinal) || eventInfo.EventId != 100 || eventInfo.ReportCount != 1) throw new InvalidOperationException("Completion event metadata mismatch."); using SecsItem value = result.GetEventValue(0, 0); int expected = eventOrdinal == 0 ? 20 : eventOrdinal == 1 ? 30 : 50; if (value.GetI4()[0] != expected) throw new InvalidOperationException("Completion event value mismatch."); Task acknowledgement = host.AcknowledgeEventAsync(result, eventOrdinal == 1 ? (byte)7 : (byte)0, token); current.Dispose(); disposed = true; await acknowledgement; eventReceived = true; eventOrdinal++; } if (responseReceived && (step is not (8 or 9 or 15) || eventReceived)) { responseReceived = false; eventReceived = false; step++; if (step < 16) pending = await SendStep(host, step, limits, token); } } finally { if (!disposed) current.Dispose(); } } if (eventOrdinal != 3) throw new InvalidOperationException("Missing completion events."); await host.StopAsync(token);}
static async Task<HsmsToken> SendStep(GemHostWorker host, int step, SecsLimits limits, CancellationToken token){ if (step == 1) return await host.RequestOnlineAsync(token); if (step == 2) return await host.DefineReportsAsync(1, new GemReportSpec[] { new(10, new uint[] { 3 }) }, token); if (step == 3) return await host.LinkReportsAsync(2, new GemLinkSpec[] { new(100, new uint[] { 10 }) }, token); if (step == 4) return await host.EnableEventsAsync(true, new uint[] { 100 }, token); using SecsItem name = step is >= 10 and <= 13 ? SecsItem.FromU1([7], limits) : SecsItem.FromAscii(step == 5 ? "MISSING" : "SET_TEMP", limits); if (step is 5 or 6 or >= 10 and <= 13) return await host.RemoteCommandAsync(name, Array.Empty<GemCommandParameter>(), token); int commandValue = step switch { 0 => 10, 7 => 101, 8 => 20, 9 => 30, 14 => 40, _ => 50 }; string? tag = step switch { 8 => "NOW", 9 => "LATER", 14 => "BAD", 15 => "LAST", _ => null }; using SecsItem key = SecsItem.FromAscii("VALUE", limits); using SecsItem value = SecsItem.FromI4([commandValue], limits); if (tag is null) return await host.RemoteCommandAsync(name, new GemCommandParameter[] { new(key, value) }, token); using SecsItem tagName = SecsItem.FromAscii("TAG", limits); using SecsItem tagValue = SecsItem.FromAscii(tag, limits); return await host.RemoteCommandAsync(name, new GemCommandParameter[] { new(key, value), new(tagName, tagValue) }, token);}
static void CheckResponse(GemResult result, int step){ byte[] expected = [2, 0, 0, 0, 0, 1, 3, 3, 0, 4, 5, 6, 2, 1, 3, 0]; if (result.GetInfo().Acknowledgement != expected[step]) throw new InvalidOperationException("Command acknowledgement mismatch."); if (step == 14) { if (result.GetCommandInfo().IssueCount != 2) throw new InvalidOperationException("Command issue count mismatch."); using GemCommandError first = result.GetCommandIssue(0); using GemCommandError second = result.GetCommandIssue(1); if (first.Name.GetAscii() != "VALUE" || first.Code != 2 || second.Name.GetAscii() != "TAG" || second.Code != 3) throw new InvalidOperationException("Command issues mismatch."); }}
static async Task RunEquipment(GemEquipmentWorker equipment, IAsyncEnumerator<GemEvent> events, SecsLimits limits, CancellationToken token){ int primaryCount = 0; int commandCount = 0; int numeric = 0; int executions = 0; int eventAcknowledgements = 0; HsmsToken[] eventTokens = new HsmsToken[3]; while (eventAcknowledgements < 3) { if (!await events.MoveNextAsync()) throw new InvalidOperationException("Command equipment event stream ended early."); GemEvent current = events.Current; bool disposed = false; try { GemResult? result = current.Result; if (result is null) continue; GemResultInfo generic = result.GetInfo(); if (generic.Kind == GemResultKind.Primary) primaryCount++; if (generic.Outcome == GemOutcome.CommandReceived) { commandCount++; GemCommandResultInfo info = result.GetCommandInfo(); GemCommandDecision decision = GemCommandDecision.Completed; int value = 0; if (info.ValidationAccepted) { using SecsItem name = result.GetCommandName(); if (name.GetInfo().Format != SecsFormat.Ascii) { GemCommandDecision[] choices = [GemCommandDecision.AlreadyInCondition, GemCommandDecision.NoSuchObject, GemCommandDecision.CannotPerformNow, GemCommandDecision.UnknownCommand]; decision = choices[numeric++]; } else { using GemCommandArgument argument = result.GetCommandParameter(0); value = argument.Value.GetI4()[0]; decision = value == 30 ? GemCommandDecision.AcceptedForLater : value == 40 ? GemCommandDecision.ParameterErrors : GemCommandDecision.Completed; } } if (info.ValidationAccepted && value is 20 or 50) { using SecsItem updated = SecsItem.FromI4([value], limits); await equipment.UpdateStatusAsync(3, updated, token); executions++; } using SecsItem valueName = SecsItem.FromAscii("VALUE", limits); using SecsItem tagName = SecsItem.FromAscii("TAG", limits); GemCommandIssue[] issues = decision == GemCommandDecision.ParameterErrors ? [new(valueName, 2), new(tagName, 3)] : []; Task<GemCommandReply> response = equipment.RespondCommandAsync(result, decision, issues, token); current.Dispose(); disposed = true; GemCommandReply reply = await response; if (info.ValidationAccepted && value == 30) { using SecsItem updated = SecsItem.FromI4([value], limits); await equipment.UpdateStatusAsync(3, updated, token); executions++; } if (reply.CompletionEvent.HasValue) eventTokens[executions - 1] = await equipment.PublishEventAsync((uint)(70 + executions), reply.CompletionEvent.Value, token); } else if (generic.Kind == GemResultKind.Response && generic.RequestKind == GemRequestKind.EventReport) { byte expected = eventAcknowledgements == 1 ? (byte)7 : (byte)0; if (generic.Token != eventTokens[eventAcknowledgements] || generic.Acknowledgement != expected) throw new InvalidOperationException("Completion acknowledgement mismatch."); eventAcknowledgements++; } } finally { if (!disposed) current.Dispose(); } } if (primaryCount != 17 || commandCount != 12 || numeric != 4 || executions != 3) throw new InvalidOperationException("Command scenario counts mismatch."); await equipment.StopAsync(token);}
static class DemoOptions{ internal static GemCommandOptions Commands() => new() { MaxCommands = 8, MaxParametersPerCommand = 8, MaxTotalParameters = 32, MaxIdentifierBytes = 64, MaxTotalIdentifierBytes = 1024, MaxErrors = 8 }; internal static GemReportOptions Reports() => new() { MaxEvents = 4, MaxReports = 4, MaxVariablesPerReport = 8, MaxReportsPerEvent = 4, MaxTotalVariables = 16, MaxTotalLinks = 16 }; 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 HsmsWorkerOptions Worker() => new() { CommandCapacity = 64, EventCapacity = 64, MaxCommandBytes = 131072, MaxEventBytes = 131072, PollIntervalMilliseconds = 1, ShutdownTimeoutMilliseconds = 2000 }; 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 = 65536, MaxWriteFrames = 64, MaxWriteBytes = 65536, MaxIoChunk = 3, MaxApplicationEvents = 128, MaxApplicationBytes = 65536, PollBudget = 32, ItemLimits = limits };}4. Run the S2F49 example
Section titled “4. Run the S2F49 example”S2F49 extends S2F41. It carries a DATAID, a target object (OBJSPEC), the command name and parameters whose values (CEPVAL) can be lists. The equipment answers with S2F50: an HCACK plus a per-parameter acknowledgement (CEPACK) that can nest the same way as the value.
Run PDComm.GemEnhancedCommandsDemo the same way as in step 2:
dotnet "$Root\build\managed\bin\PDComm.GemEnhancedCommandsDemo\release\PDComm.GemEnhancedCommandsDemo.dll" $Native equipment passive 127.0.0.1:0dotnet "$Root\build\managed\bin\PDComm.GemEnhancedCommandsDemo\release\PDComm.GemEnhancedCommandsDemo.dll" $Native host active 127.0.0.1:<port>dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemEnhancedCommandsDemo/release/PDComm.GemEnhancedCommandsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemEnhancedCommandsDemo/release/PDComm.GemEnhancedCommandsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port>dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemEnhancedCommandsDemo/release/PDComm.GemEnhancedCommandsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemEnhancedCommandsDemo/release/PDComm.GemEnhancedCommandsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port>Run the first line in the equipment terminal and the second in the host terminal.
Expected result:
PASS host active: managed GEM commands worker completed 24 steps, simulated 3 executions, and shut downThe equipment registers commands under three objects. APPLY_RECIPE on CHAMBER/1 takes a required LIST parameter RECIPE (TEMP and STEPS) and needs online remote. APPLY_RECIPE on CHAMBER/2 takes a single F8 value. The numeric command U1 7 sits under the empty object name. The enhanced catalog is separate from the basic one.
| Step | Host request | HCACK | What it shows |
|---|---|---|---|
| 5 | Object chamber/1 |
6 | Object names are case sensitive. |
| 7 | No RECIPE |
3 | CEPACK RECIPE=2. |
| 8 | RECIPE sent as ASCII |
3 | A format error: CEPACK RECIPE=3. |
| 9 | CHAMBER/2, RECIPE=F8 42 |
0 | The same command name has a different definition on each object. |
| 11 | TEMP 30 | 4 | The work and the completion event follow the reply. |
| 12 | TEMP 40 | 3 | The equipment returns a recursive CEPACK tree: RECIPE → TEMP 0, STEPS [0, 4, 0]. |
| 19 | The same name sent as S2F41 | 1 | The basic catalog doesn’t contain it. |
| 20 | The basic command SET_TEMP sent as S2F49 |
1 | The enhanced catalog doesn’t contain it. |
| 21, 22 | Duplicate RECIPE, unknown EXTRA |
3 | CEPACK 2 and 1. |
The host sends with GemHostWorker.EnhancedRemoteCommandAsync, and the equipment answers with GemEquipmentWorker.RespondEnhancedCommandAsync. You pass a CEPACK tree only with ParameterErrors.
Full source of PDComm.GemEnhancedCommandsDemo/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.GemEnhancedCommandsDemo <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 limits = new(4096, 4096, 256, 16);using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(25));await using GemWorker worker = role == "host" ? await GemHostWorker.StartAsync(args[3], DemoOptions.Transport(mode, limits), DemoOptions.Host(limits), DemoOptions.Worker(), deadline.Token) : await StartEquipment(args[3], mode, limits, deadline.Token);if (mode == HsmsMode.Passive){ Console.WriteLine($"LISTEN {worker.LocalAddress ?? throw new InvalidOperationException("Missing passive address.")}"); Console.Out.Flush();}await using IAsyncEnumerator<GemEvent> events = worker.ReadEventsAsync(deadline.Token).GetAsyncEnumerator();if (worker is GemHostWorker host) await RunHost(host, events, limits, deadline.Token);else await RunEquipment((GemEquipmentWorker)worker, events, limits, deadline.Token);while (await events.MoveNextAsync()) events.Current.Dispose();await worker.Completion.WaitAsync(deadline.Token);Console.WriteLine($"PASS {role} {args[2]}: managed GEM commands worker completed 24 steps, simulated 3 executions, and shut down");return 0;
static async Task<GemEquipmentWorker> StartEquipment(string address, HsmsMode mode, SecsLimits limits, CancellationToken token){ using SecsItem initial = SecsItem.FromI4([0], limits); using SecsItem command = SecsItem.FromAscii("SET_TEMP", limits); using SecsItem value = SecsItem.FromAscii("VALUE", limits); using SecsItem tag = SecsItem.FromAscii("TAG", limits); using SecsItem numeric = SecsItem.FromU1([7], limits); GemCommandParameterDefinition[] parameters = [ new(value, true, SecsFormat.I4, 1, 1, 0, 100), new(tag, false, SecsFormat.Ascii, 0, 32), ]; var commands = new GemCommandConfiguration( DemoOptions.Commands(), new GemCommandDefinition[] { new(command, parameters, true, 100), new(numeric, Array.Empty<GemCommandParameterDefinition>(), false), }); using SecsItem first = SecsItem.FromAscii("CHAMBER/1", limits); using SecsItem second = SecsItem.FromAscii("CHAMBER/2", limits); using SecsItem empty = SecsItem.FromAscii("", limits); using SecsItem apply = SecsItem.FromAscii("APPLY_RECIPE", limits); using SecsItem recipe = SecsItem.FromAscii("RECIPE", limits); GemCommandParameterDefinition[] nested = [new(recipe, true, SecsFormat.List, 2, 2), new(tag, false, SecsFormat.Ascii, 0, 32)]; GemCommandParameterDefinition[] floating = [new(recipe, true, SecsFormat.F8, 1, 1)]; var enhanced = new GemEnhancedCommandConfiguration(DemoOptions.Enhanced(limits), new GemEnhancedObjectDefinition[] { new(first, new GemCommandDefinition[] { new(apply, nested, true, 100) }), new(second, new GemCommandDefinition[] { new(apply, floating, false) }), new(empty, new GemCommandDefinition[] { new(numeric, Array.Empty<GemCommandParameterDefinition>(), false) }), }); return await GemEquipmentWorker.StartAsync( address, DemoOptions.Transport(mode, limits), DemoOptions.Equipment(limits), DemoOptions.Worker(), [new GemStatusVariable(3, "CommandValue", "C", initial)], new GemReportConfiguration(DemoOptions.Reports(), new uint[] { 100, 200 }), null, commands, enhanced, token);}
static async Task RunHost(GemHostWorker host, IAsyncEnumerator<GemEvent> events, SecsLimits limits, CancellationToken token){ bool began = false; bool established = false; bool responseReceived = false; bool eventReceived = false; int step = 0; int eventOrdinal = 0; HsmsToken pending = default; while (step < 24) { if (!began && (await host.GetTransportStateAsync(token)).State == HsmsConnectionState.Selected) { pending = await host.BeginCommunicationAsync(token); began = true; } if (!await events.MoveNextAsync()) throw new InvalidOperationException("Command host event stream ended early."); GemEvent current = events.Current; bool disposed = false; try { 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("Command response token mismatch."); if (!established) { if (info.Outcome != GemOutcome.CommunicationEstablished) throw new InvalidOperationException("Communication failed."); established = true; pending = await SendStep(host, step, limits, token); } else { CheckResponse(result, step); responseReceived = true; } } else if (info.Outcome == GemOutcome.EventReceived) { if (step is not (10 or 11 or 23)) throw new InvalidOperationException("Unexpected completion event."); GemEventInfo eventInfo = result.GetEventInfo(); if (eventInfo.DataId != (uint)(71 + eventOrdinal) || eventInfo.EventId != 100 || eventInfo.ReportCount != 1) throw new InvalidOperationException("Completion event metadata mismatch."); using SecsItem value = result.GetEventValue(0, 0); int expected = eventOrdinal == 0 ? 20 : eventOrdinal == 1 ? 30 : 50; if (value.GetI4()[0] != expected) throw new InvalidOperationException("Completion event value mismatch."); Task acknowledgement = host.AcknowledgeEventAsync(result, eventOrdinal == 1 ? (byte)7 : (byte)0, token); current.Dispose(); disposed = true; await acknowledgement; eventReceived = true; eventOrdinal++; } if (responseReceived && (step is not (10 or 11 or 23) || eventReceived)) { responseReceived = false; eventReceived = false; step++; if (step < 24) pending = await SendStep(host, step, limits, token); } } finally { if (!disposed) current.Dispose(); } } if (eventOrdinal != 3) throw new InvalidOperationException("Missing completion events."); await host.StopAsync(token);}
static async Task<HsmsToken> SendStep(GemHostWorker host, int step, SecsLimits limits, CancellationToken token){ if (step == 1) return await host.RequestOnlineAsync(token); if (step == 2) return await host.DefineReportsAsync(1, new GemReportSpec[] { new(10, new uint[] { 3 }) }, token); if (step == 3) return await host.LinkReportsAsync(2, new GemLinkSpec[] { new(100, new uint[] { 10 }) }, token); if (step == 4) return await host.EnableEventsAsync(true, new uint[] { 100 }, token); using SecsItem name = step is >= 13 and <= 18 ? SecsItem.FromU1([7], limits) : SecsItem.FromAscii(step == 6 ? "MISSING" : step == 20 ? "SET_TEMP" : "APPLY_RECIPE", limits); if (step == 19) return await host.RemoteCommandAsync(name, Array.Empty<GemCommandParameter>(), token); using SecsItem scope = SecsItem.FromAscii(step == 5 ? "chamber/1" : step == 9 ? "CHAMBER/2" : step is >= 13 and <= 17 ? "" : "CHAMBER/1", limits); using SecsItem key = SecsItem.FromAscii("RECIPE", limits); int temperature = step switch { 10 => 20, 11 => 30, 12 => 40, 23 => 50, _ => 10 }; using SecsItem value = step == 8 ? SecsItem.FromAscii("invalid", limits) : step == 9 ? SecsItem.FromF8([42], limits) : Recipe(temperature, limits); using SecsItem tag = SecsItem.FromAscii(step == 22 ? "EXTRA" : "TAG", limits); using SecsItem tagValue = SecsItem.FromAscii(step == 10 ? "NOW" : step == 11 ? "LATER" : step == 12 ? "BAD" : "LAST", limits); GemCommandParameter[] parameters = step is 6 or 7 or >= 13 and <= 20 ? [] : step is 10 or 11 or 12 or >= 21 ? [new(key, value), new(step == 21 ? key : tag, step == 21 ? value : tagValue)] : [new(key, value)]; return await host.EnhancedRemoteCommandAsync((uint)(1000 + step), scope, name, parameters, token);}
static void CheckResponse(GemResult result, int step){ byte[] expected = [2, 0, 0, 0, 0, 6, 1, 3, 3, 0, 0, 4, 3, 5, 6, 2, 1, 0, 1, 1, 1, 3, 3, 0]; GemResultInfo info = result.GetInfo(); if (info.Acknowledgement != expected[step]) throw new InvalidOperationException("Enhanced acknowledgement mismatch."); if (step is >= 1 and <= 4) return; if (step == 19) { if (info.RequestKind != GemRequestKind.RemoteCommand || info.Outcome != GemOutcome.CommandAck) throw new InvalidOperationException("Basic command fallback detected."); return; } if (info.RequestKind != GemRequestKind.EnhancedRemoteCommand || info.Outcome != GemOutcome.EnhancedCommandAck) throw new InvalidOperationException("Enhanced response kind mismatch."); GemEnhancedCommandResultInfo command = result.GetEnhancedCommandInfo(); nuint count = step is 7 or 8 or 12 or 21 or 22 ? 1u : 0u; if (command.IsRequest || command.Responded || command.DataId != 0 || command.ParameterCount != 0 || command.IssueCount != count || info.EntryCount != count) throw new InvalidOperationException("Enhanced response metadata mismatch."); using SecsItem actual = result.GetEnhancedCommandAcknowledgements(); using SecsItem expectedItem = step == 12 ? RecursiveAck(DemoOptions.Limits) : count != 0 ? OwnedList(DemoOptions.Limits, Pair(step == 22 ? "EXTRA" : "RECIPE", SecsItem.FromU1([step == 8 ? (byte)3 : step == 22 ? (byte)1 : (byte)2], DemoOptions.Limits), DemoOptions.Limits)) : OwnedList(DemoOptions.Limits); if (!actual.Encode().AsSpan().SequenceEqual(expectedItem.Encode())) throw new InvalidOperationException("Enhanced ACK tree mismatch.");}
static SecsItem OwnedList(SecsLimits limits, params SecsItem[] children){ try { return SecsItem.CreateList(children, limits); } finally { foreach (SecsItem child in children) child.Dispose(); }}static SecsItem Pair(string name, SecsItem value, SecsLimits limits) => OwnedList(limits, SecsItem.FromAscii(name, limits), value);static SecsItem Recipe(int value, SecsLimits limits) => OwnedList(limits, Pair("TEMP", SecsItem.FromI4([value], limits), limits), Pair("STEPS", OwnedList(limits, SecsItem.FromI4([1], limits), SecsItem.FromI4([2], limits), SecsItem.FromI4([3], limits)), limits));static SecsItem RecursiveAck(SecsLimits limits) => OwnedList(limits, Pair("RECIPE", OwnedList(limits, Pair("TEMP", SecsItem.FromU1([0], limits), limits), Pair("STEPS", OwnedList(limits, SecsItem.FromU1([0], limits), SecsItem.FromU1([4], limits), SecsItem.FromU1([0], limits)), limits)), limits));
static async Task RunEquipment(GemEquipmentWorker equipment, IAsyncEnumerator<GemEvent> events, SecsLimits limits, CancellationToken token){ int primaryCount = 0; int commandCount = 0; int numeric = 0; int executions = 0; int eventAcknowledgements = 0; HsmsToken[] eventTokens = new HsmsToken[3]; while (eventAcknowledgements < 3) { if (!await events.MoveNextAsync()) throw new InvalidOperationException("Command equipment event stream ended early."); GemEvent current = events.Current; bool disposed = false; try { GemResult? result = current.Result; if (result is null) continue; GemResultInfo generic = result.GetInfo(); if (generic.Kind == GemResultKind.Primary) primaryCount++; if (generic.Outcome == GemOutcome.EnhancedCommandReceived) { commandCount++; GemEnhancedCommandResultInfo info = result.GetEnhancedCommandInfo(); if (!info.IsRequest || info.Responded || info.DataId < 1000 || info.DataId >= 1024) throw new InvalidOperationException("Enhanced request metadata mismatch."); GemCommandDecision decision = GemCommandDecision.Completed; int value = 0; if (info.ValidationAccepted) { using SecsItem name = result.GetEnhancedCommandName(); if (name.GetInfo().Format != SecsFormat.Ascii) { GemCommandDecision[] choices = [GemCommandDecision.AlreadyInCondition, GemCommandDecision.NoSuchObject, GemCommandDecision.CannotPerformNow, GemCommandDecision.UnknownCommand, GemCommandDecision.Completed]; decision = choices[numeric++]; } else { using SecsItem scope = result.GetEnhancedCommandObject(); using GemCommandArgument argument = result.GetEnhancedCommandParameter(0); if (name.GetAscii() != "APPLY_RECIPE" || argument.Name.GetAscii() != "RECIPE") throw new InvalidOperationException("Command identifier mismatch."); if (info.DataId == 1009) { if (scope.GetAscii() != "CHAMBER/2" || info.ParameterCount != 1 || argument.Value.GetF8()[0] != 42) throw new InvalidOperationException("Floating command mismatch."); } else { if (scope.GetAscii() != "CHAMBER/1" || info.ParameterCount != 2) throw new InvalidOperationException("Recipe object mismatch."); using SecsItem pair = argument.Value.GetChild(0); using SecsItem temperature = pair.GetChild(1); value = temperature.GetI4()[0]; using SecsItem expectedRecipe = Recipe(value, limits); if (!argument.Value.Encode().AsSpan().SequenceEqual(expectedRecipe.Encode())) throw new InvalidOperationException("Recipe mismatch."); decision = value == 30 ? GemCommandDecision.AcceptedForLater : value == 40 ? GemCommandDecision.ParameterErrors : GemCommandDecision.Completed; } } } if (info.ValidationAccepted && value is 20 or 50) { using SecsItem updated = SecsItem.FromI4([value], limits); await equipment.UpdateStatusAsync(3, updated, token); executions++; } using SecsItem? acknowledgements = decision == GemCommandDecision.ParameterErrors ? RecursiveAck(limits) : null; Task<GemCommandReply> response = equipment.RespondEnhancedCommandAsync(result, decision, acknowledgements, token); current.Dispose(); disposed = true; GemCommandReply reply = await response; if (info.ValidationAccepted && value == 30) { using SecsItem updated = SecsItem.FromI4([value], limits); await equipment.UpdateStatusAsync(3, updated, token); executions++; } if (reply.CompletionEvent.HasValue) eventTokens[executions - 1] = await equipment.PublishEventAsync((uint)(70 + executions), reply.CompletionEvent.Value, token); } else if (generic.Outcome == GemOutcome.CommandReceived) { if (result.GetCommandInfo().ValidationAccepted) throw new InvalidOperationException("Enhanced command fell back to basic registry."); await equipment.RespondCommandAsync(result, GemCommandDecision.Completed, ReadOnlyMemory<GemCommandIssue>.Empty, token); } else if (generic.Kind == GemResultKind.Response && generic.RequestKind == GemRequestKind.EventReport) { byte expected = eventAcknowledgements == 1 ? (byte)7 : (byte)0; if (generic.Token != eventTokens[eventAcknowledgements] || generic.Acknowledgement != expected) throw new InvalidOperationException("Completion acknowledgement mismatch."); eventAcknowledgements++; } } finally { if (!disposed) current.Dispose(); } } if (primaryCount != 25 || commandCount != 19 || numeric != 5 || executions != 3) throw new InvalidOperationException("Command scenario counts mismatch."); await equipment.StopAsync(token);}
static class DemoOptions{ internal static readonly SecsLimits Limits = new(4096, 4096, 256, 16); internal static GemEnhancedCommandOptions Enhanced(SecsLimits limits) => new() { MaxObjects = 8, MaxObjectBytes = 64, MaxTotalObjectBytes = 256, MaxCommandsPerObject = 8, MaxTotalCommands = 16, MaxParametersPerCommand = 8, MaxTotalParameters = 32, MaxIdentifierBytes = 64, MaxTotalIdentifierBytes = 1024, MaxErrors = 8, MaxRequestValueBytes = 2048, ItemLimits = limits, }; internal static GemCommandOptions Commands() => new() { MaxCommands = 8, MaxParametersPerCommand = 8, MaxTotalParameters = 32, MaxIdentifierBytes = 64, MaxTotalIdentifierBytes = 1024, MaxErrors = 8 }; internal static GemReportOptions Reports() => new() { MaxEvents = 4, MaxReports = 4, MaxVariablesPerReport = 8, MaxReportsPerEvent = 4, MaxTotalVariables = 16, MaxTotalLinks = 16 }; 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 HsmsWorkerOptions Worker() => new() { CommandCapacity = 64, EventCapacity = 64, MaxCommandBytes = 131072, MaxEventBytes = 131072, PollIntervalMilliseconds = 1, ShutdownTimeoutMilliseconds = 2000 }; 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 = 65536, MaxWriteFrames = 64, MaxWriteBytes = 65536, MaxIoChunk = 3, MaxApplicationEvents = 128, MaxApplicationBytes = 65536, PollBudget = 32, ItemLimits = limits };}5. Run the Qt version
Section titled “5. Run the Qt version”The Qt examples run the same 16-step script (24 for enhanced commands) through the C++ API. The GEM endpoint lives only on a QThread worker. When the equipment receives a command, the worker hands the decision to the Qt main thread (invokeQueued), waits for the HCACK the main thread picks, and replies with GemEquipment::respondCommand. The UI never touches the endpoint, so it stays responsive.
Put commands.cpp, enhanced_commands.cpp and queued_handoff.hpp in one folder and add this CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)project(pdcomm_qt_examples 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)foreach(example commands enhanced_commands) add_executable(pdcomm_qt_${example} ${example}.cpp) target_include_directories(pdcomm_qt_${example} PRIVATE "${PDCOMM_SDK}/include") target_link_libraries(pdcomm_qt_${example} PRIVATE Qt6::Core "${PDCOMM_FFI}") set_target_properties(pdcomm_qt_${example} PROPERTIES BUILD_RPATH "${PDCOMM_SDK}/lib")endforeach()Build and run it. Add -DCMAKE_PREFIX_PATH=<Qt install prefix> if CMake can’t find Qt.
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPDCOMM_SDK="$PDCOMM_SDK"cmake --build build./build/pdcomm_qt_commands equipment passive 127.0.0.1:0./build/pdcomm_qt_commands host active 127.0.0.1:<port>Run the last two commands in separate terminals. The extra name pdcomm_ffi.dll in NAMES finds the Windows import library, pdcomm_ffi.dll.lib. On Windows, add the sdk\lib folder to PATH before you run the programs.
Expected result: both sides print PASS QT COMMANDS; pdcomm_qt_enhanced_commands prints PASS QT ENHANCED PEER. You can mix languages: a Qt equipment (pdcomm_qt_commands equipment passive 127.0.0.1:0) with the .NET PDComm.GemCommandsDemo as the host passes the same script, and each side prints its own PASS line.
Without arguments the program prints its usage and exits with code 2:
usage: pdcomm_qt_commands ROLE MODE ADDRESS [--cancel]--cancel interrupts the worker after 20 milliseconds so you can exercise the cancellation path.
Full source of commands.cpp
#include "pdcomm_gem.hpp"#include "queued_handoff.hpp"
#include <QCoreApplication>#include <QThread>#include <QTimer>
#include <atomic>#include <chrono>#include <cstdint>#include <cstdio>#include <memory>#include <stdexcept>#include <string>#include <thread>#include <utility>#include <vector>
namespace {
void require(bool condition, const char *message) { if (!condition) throw std::runtime_error(message);}
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 = mode == PDCOMM_TCP_ACTIVE ? 100 : 500; 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 = 128; value.max_queued_bytes = 32768; value.max_write_frames = 8; value.max_write_bytes = 16384; value.max_io_chunk = 3; value.max_application_events = 128; value.max_application_bytes = 32768; value.poll_budget = 16; value.item_limits = itemLimits(); return value;}
pdcomm::SecsItem primitive(uint8_t format, const std::vector<uint8_t> &bytes) { return pdcomm::SecsItem::primitive(format, 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());}
pdcomm::SecsItem u1(uint8_t value) { return primitive(PDCOMM_SECS_U1, {value}); }
pdcomm::SecsItem i4(int32_t value) { const auto bits = static_cast<uint32_t>(value); return primitive(PDCOMM_SECS_I4, {static_cast<uint8_t>(bits >> 24), static_cast<uint8_t>(bits >> 16), static_cast<uint8_t>(bits >> 8), static_cast<uint8_t>(bits)});}
int32_t i4Value(const pdcomm::SecsItem &item) { require(item.info().format == PDCOMM_SECS_I4, "expected I4 item"); const auto bytes = item.payload(); require(bytes.size() == 4, "invalid I4 payload"); const uint32_t value = (static_cast<uint32_t>(bytes[0]) << 24) | (static_cast<uint32_t>(bytes[1]) << 16) | (static_cast<uint32_t>(bytes[2]) << 8) | bytes[3]; return static_cast<int32_t>(value);}
std::string asciiValue(const pdcomm::SecsItem &item) { require(item.info().format == PDCOMM_SECS_ASCII, "expected ASCII item"); const auto bytes = item.payload(); return std::string(bytes.begin(), bytes.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 && left.reserved == right.reserved;}
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)); }}
PdcommTcpToken sendCommand(pdcomm::GemHost &host, size_t step) { if (step == 5) { auto name = ascii("MISSING"); return host.remoteCommand(name); } if (step >= 10 && step <= 13) { auto name = u1(7); return host.remoteCommand(name); } auto command = ascii("SET_TEMP"); if (step == 6) return host.remoteCommand(command); auto valueName = ascii("VALUE"); auto value = i4(step == 0 ? 10 : step == 7 ? 101 : step == 8 ? 20 : step == 9 ? 30 : step == 14 ? 40 : 50); if (step == 0 || step == 7) { return host.remoteCommand(command, {{&valueName, &value}}); } auto tagName = ascii("TAG"); auto tag = ascii(step == 8 ? "NOW" : step == 9 ? "LATER" : step == 14 ? "BAD" : "LAST"); return host.remoteCommand( command, {{&valueName, &value}, {&tagName, &tag}});}
PdcommTcpToken sendStep(pdcomm::GemHost &host, size_t step) { if (step == 1) return host.request(pdcomm::GemRequest::Online); if (step == 2) return host.defineReports(1, {{10, {3}}}); if (step == 3) return host.linkReports(2, {{100, {10}}}); if (step == 4) return host.enableEvents(true, {100}); return sendCommand(host, step);}
uint8_t expectedAck(size_t step) { static const uint8_t values[] = {2, 1, 3, 3, 0, 4, 5, 6, 2, 1, 3, 0}; require(step == 0 || step >= 5, "invalid command step"); return values[step == 0 ? 0 : step - 4];}
pdcomm::GemCommandAcknowledgement decisionFor(size_t step) { using Ack = pdcomm::GemCommandAcknowledgement; if (step == 9) return Ack::AcceptedForLater; if (step == 10) return Ack::AlreadyInCondition; if (step == 11) return Ack::NoSuchObject; if (step == 12) return Ack::CannotPerform; if (step == 13) return Ack::Unknown; if (step == 14) return Ack::ParameterErrors; return Ack::Completed;}
void validateRequest(const pdcomm::GemResult &result, size_t step) { const auto info = result.info(); require(info.kind == pdcomm::GemResultKind::Primary && info.outcome == pdcomm::GemOutcome::CommandReceived, "command request classification mismatch"); const auto command = result.commandInfo(); require(command.isRequest && !command.responded, "command request flags mismatch"); require(command.parameterCount == info.entryCount, "command parameter count mismatch"); if (step == 5) { require(asciiValue(result.commandName()) == "MISSING" && !command.validationAccepted && static_cast<uint8_t>(command.validationAcknowledgement) == 1, "unknown command validation mismatch"); return; } if (step >= 10 && step <= 13) { require(result.commandName().payload() == std::vector<uint8_t>{7} && command.validationAccepted && command.parameterCount == 0, "numeric command snapshot mismatch"); return; } require(asciiValue(result.commandName()) == "SET_TEMP", "command name mismatch"); const bool rejected = step == 0 || step == 6 || step == 7; require(command.validationAccepted != rejected, "command validation acceptance mismatch"); require(command.completionEventPresent == !rejected, "command completion metadata mismatch"); if (!rejected) require(command.completionEvent == 100, "completion CEID mismatch"); if (step != 6) { auto parameter = result.commandParameter(0); require(asciiValue(parameter.name) == "VALUE", "VALUE name mismatch"); const int expected = step == 0 ? 10 : step == 7 ? 101 : step == 8 ? 20 : step == 9 ? 30 : step == 14 ? 40 : 50; require(i4Value(parameter.value) == expected, "VALUE snapshot mismatch"); } if (step == 8 || step == 9 || step == 14 || step == 15) { auto parameter = result.commandParameter(1); require(asciiValue(parameter.name) == "TAG", "TAG name mismatch"); }}
struct UiPayload { bool cancelled = false; bool host = false; std::vector<std::shared_ptr<pdcomm::GemResult>> results; std::vector<std::shared_ptr<pdcomm::SecsItem>> values;};
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 CommandsWorker final : public QThread {public: CommandsWorker(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 command 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; }
int requestCommandDecision( const std::shared_ptr<pdcomm::GemResult> &result, size_t step) { 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, step] { try { require(QThread::currentThread() == application->thread(), "command inspection missed the UI thread"); validateRequest(*result, step); const auto choice = decisionFor(step); shared->uiDeliveries.fetch_add(1); decision->store(static_cast<int>(choice), std::memory_order_release); } catch (...) { decision->store(-2, std::memory_order_release); } }), "failed to queue command UI decision"); 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 command decision failed"); return value; }
int requestEventDecision( 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->eventInfo(); const auto report = result->eventReport(0); const int values[] = {20, 30, 50}; require(info.dataId == 71 + index && info.eventId == 100 && info.reportCount == 1 && !info.acknowledged && report.id == 10 && report.valueCount == 1 && i4Value(result->value(0, 0)) == values[index], "command completion event mismatch"); shared->uiDeliveries.fetch_add(1); decision->store(index == 1 ? 7 : 0, std::memory_order_release); } catch (...) { decision->store(-2, std::memory_order_release); } }), "failed to queue command event decision"); 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 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 stepSent = false; PdcommTcpToken communicationToken = {}; PdcommTcpToken requested[16] = {}; size_t step = 0; size_t commandAcks = 0; size_t eventReports = 0; bool commandDone = false; bool eventDone = false; bool peerStopped = false; const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(15); while (std::chrono::steady_clock::now() < deadline && !isInterruptionRequested()) { host.poll(); if (!beginSent && host.tcp().state().state == PDCOMM_TCP_SELECTED) { communicationToken = host.beginCommunication(); beginSent = true; } std::shared_ptr<pdcomm::GemResult> eventReport; 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) { require(info.kind == pdcomm::GemResultKind::Response && info.request == pdcomm::GemRequest::Establish && sameToken(info.token, communicationToken), "communication response token mismatch"); if (!stepSent) { requested[0] = sendStep(host, 0); stepSent = true; } } else if (info.outcome == pdcomm::GemOutcome::EventReceived) { eventReport = std::make_shared<pdcomm::GemResult>( std::move(result)); } else if (info.kind == pdcomm::GemResultKind::Response) { require(stepSent && step < 16 && sameToken(info.token, requested[step]), "command response token mismatch"); if (step == 1) { require(info.request == pdcomm::GemRequest::Online && info.outcome == pdcomm::GemOutcome::OnlineAck, "online response mismatch"); } else if (step >= 2 && step <= 4) { const auto expected = step == 2 ? pdcomm::GemOutcome::DefineReportsAck : step == 3 ? pdcomm::GemOutcome::LinkReportsAck : pdcomm::GemOutcome::EnableEventsAck; require(info.outcome == expected && info.acknowledgement == 0, "configuration response mismatch"); } else { require(info.request == pdcomm::GemRequest::RemoteCommand && info.outcome == pdcomm::GemOutcome::CommandAck && info.acknowledgement == expectedAck(step), "command ACK mismatch"); const auto detail = result.commandInfo(); const size_t expectedIssues = step == 6 || step == 7 ? 1 : step == 14 ? 2 : 0; require(!detail.isRequest && detail.issueCount == expectedIssues, "command response issue count mismatch"); for (size_t index = 0; index < detail.issueCount; ++index) { const auto issue = result.commandIssue(index); require(issue.code >= 1 && issue.code <= 3, "command response issue mismatch"); } ++commandAcks; } commandDone = true; } } if (eventReport) { const int ack = requestEventDecision(eventReport, eventReports); payload.values.push_back( std::make_shared<pdcomm::SecsItem>( eventReport->value(0, 0))); host.acknowledgeEvent(*eventReport, static_cast<uint8_t>(ack)); payload.results.push_back(eventReport); ++eventReports; eventDone = true; } if (stepSent && commandDone && (!(step == 8 || step == 9 || step == 15) || eventDone)) { ++step; commandDone = false; eventDone = false; if (step < 16) requested[step] = sendStep(host, step); } if (step == 16 && host.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(step == 16 && commandAcks == 12 && eventReports == 3 && peerStopped, "host command 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(3, "CommandValue", "C", i4(0)); auto setTemp = ascii("SET_TEMP"); auto valueName = ascii("VALUE"); auto tagName = ascii("TAG"); auto numeric = u1(7); const auto zero = pdcomm::GemInt128::fromSigned(0); const auto hundred = pdcomm::GemInt128::fromSigned(100); equipment.configureCommands( pdcomm::gemCommandOptions(4, 4, 8, 32, 128, 8), {{&setTemp, {{&valueName, 1, 1, zero, hundred, true, true, PDCOMM_SECS_I4, true, true}, {&tagName, 0, 32, zero, zero, false, true, PDCOMM_SECS_ASCII, false, false}}, true, true, 100}, {&numeric, {}, false, false, 0}}); require(equipment.commandState().commandCount == 2, "command catalog mismatch"); equipment.start(); size_t step = 0; size_t commands = 0; size_t executions = 0; size_t eventAcks = 0; PdcommTcpToken eventTokens[3] = {}; bool peerStopped = false; const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(15); while (std::chrono::steady_clock::now() < deadline && !isInterruptionRequested()) { equipment.poll(); std::shared_ptr<pdcomm::GemResult> command; 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) { if (info.outcome == pdcomm::GemOutcome::CommandReceived) { command = std::make_shared<pdcomm::GemResult>( std::move(result)); } else if (step >= 1 && step <= 4) { const auto expected = step == 1 ? pdcomm::GemOutcome::OnlineAccepted : step == 2 ? pdcomm::GemOutcome::DefineReportsAck : step == 3 ? pdcomm::GemOutcome::LinkReportsAck : pdcomm::GemOutcome::EnableEventsAck; if (info.outcome == expected) ++step; } } else if (info.kind == pdcomm::GemResultKind::Response && info.outcome == pdcomm::GemOutcome::EventAck) { require(eventAcks < 3 && sameToken(info.token, eventTokens[eventAcks]) && info.acknowledgement == (eventAcks == 1 ? 7 : 0), "event acknowledgement mismatch"); ++eventAcks; } } if (command) { const size_t commandStep = step; const auto decision = static_cast<pdcomm::GemCommandAcknowledgement>( requestCommandDecision(command, commandStep)); auto issueValue = ascii("VALUE"); auto issueTag = ascii("TAG"); std::vector<pdcomm::GemCommandIssue> issues; if (commandStep == 14) { issues = {{&issueValue, 2}, {&issueTag, 3}}; } if (commandStep == 8 || commandStep == 15) { equipment.updateStatus(3, i4(commandStep == 8 ? 20 : 50)); ++executions; } const auto reply = equipment.respondCommand( *command, decision, issues); require(static_cast<uint8_t>(reply.acknowledgement) == expectedAck(commandStep), "actual command acknowledgement mismatch"); if (commandStep == 9) { equipment.updateStatus(3, i4(30)); ++executions; } if (commandStep == 8 || commandStep == 9 || commandStep == 15) { const size_t index = commandStep == 8 ? 0 : commandStep == 9 ? 1 : 2; eventTokens[index] = equipment.publishEvent( static_cast<uint32_t>(71 + index), 100); } if (commandStep == 15) payload.results.push_back(command); ++commands; ++step; } if (step == 16 && commands == 12 && eventAcks == 3) { equipment.stop(); peerStopped = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } if (isInterruptionRequested()) { payload.cancelled = true; } else { if (!(step == 16 && commands == 12 && executions == 3 && eventAcks == 3 && peerStopped && i4Value(equipment.status(3)) == 50)) { throw std::runtime_error( "equipment command exchange timed out: step=" + std::to_string(step) + " commands=" + std::to_string(commands) + " executions=" + std::to_string(executions) + " eventAcks=" + std::to_string(eventAcks) + " peerStopped=" + std::to_string(peerStopped ? 1 : 0)); } } 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.results.size() == 3 && payload.values.size() == 3, "retained command events are missing"); const int expected[] = {20, 30, 50}; for (size_t index = 0; index < 3; ++index) { require(i4Value(*payload.values[index]) == expected[index] && payload.results[index] ->eventInfo().acknowledged, "retained command event changed"); } } if (!payload.cancelled && !payload.host) { require(payload.results.size() == 1 && payload.results[0] ->commandInfo().responded && asciiValue(payload.results[0] ->commandName()) == "SET_TEMP", "retained command result changed"); } shared->uiDeliveries.fetch_add(1); completion->store(1); } catch (...) { completion->store(-1); } }), "failed to queue command 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_commands ROLE MODE ADDRESS [--cancel]\n"); return 2; }
const bool cancel = argc == 5; const auto shared = std::make_shared<SharedState>(); CommandsWorker 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 COMMANDS: %s\n", shared->failure.empty() ? "UI/worker validation failed" : shared->failure.c_str()); application.exit(1); return; } std::fprintf(stdout, "PASS QT COMMANDS\n"); std::fflush(stdout); application.quit(); }); });
worker.start(); const int result = application.exec(); worker.requestInterruption(); worker.wait(); return result;}Full source of enhanced_commands.cpp
#include "pdcomm_gem.hpp"#include "queued_handoff.hpp"
#include <QCoreApplication>#include <QMetaObject>#include <QThread>#include <QTimer>
#include <atomic>#include <chrono>#include <cstdint>#include <cstdio>#include <iostream>#include <memory>#include <stdexcept>#include <string>#include <thread>#include <utility>#include <vector>
namespace {void require(bool value, const char *message) { if (!value) throw std::runtime_error(message);}PdcommSecsLimits limits() { return pdcomm::secsLimits(4096, 4096, 128, 8); }PdcommTcpOptions tcpOptions(uint32_t mode) { PdcommTcpOptions v = {}; v.struct_size = sizeof(v); v.version = PDCOMM_TCP_OPTIONS_VERSION; v.mode = mode; v.session_id = 7; v.initial_system_bytes = mode == PDCOMM_TCP_ACTIVE ? 100 : 500; v.t3_ms = 2000; v.t5_ms = 50; v.t6_ms = 2000; v.t7_ms = 2000; v.t8_ms = 1000; v.connect_timeout_ms = 20; v.shutdown_timeout_ms = 200; v.write_timeout_ms = 2000; v.max_frame_bytes = 4110; v.max_events = 128; v.max_queued_bytes = 32768; v.max_write_frames = 8; v.max_write_bytes = 16384; v.max_io_chunk = 3; v.max_application_events = 128; v.max_application_bytes = 32768; v.poll_budget = 16; v.item_limits = limits(); return v;}pdcomm::SecsItem primitive(uint8_t f, const std::vector<uint8_t> &b) { return pdcomm::SecsItem::primitive(f, b, limits());}pdcomm::SecsItem ascii(const std::string &s) { return pdcomm::SecsItem::primitive( PDCOMM_SECS_ASCII, reinterpret_cast<const uint8_t *>(s.data()), s.size(), limits());}pdcomm::SecsItem u1(uint8_t v) { return primitive(PDCOMM_SECS_U1, {v}); }pdcomm::SecsItem i4(int32_t v) { const uint32_t b = static_cast<uint32_t>(v); return primitive(PDCOMM_SECS_I4, {static_cast<uint8_t>(b >> 24), static_cast<uint8_t>(b >> 16), static_cast<uint8_t>(b >> 8), static_cast<uint8_t>(b)});}pdcomm::SecsItem f8() { return primitive(PDCOMM_SECS_F8, {0x40, 0x45, 0, 0, 0, 0, 0, 0});}pdcomm::SecsItem list(const std::vector<const pdcomm::SecsItem *> &items) { return pdcomm::SecsItem::list(items, limits());}pdcomm::SecsItem pair(const std::string &name, const pdcomm::SecsItem &value) { auto n = ascii(name); return list({&n, &value});}pdcomm::SecsItem recipe(int32_t value) { auto temperature = i4(value); auto tempPair = pair("TEMP", temperature); auto one = i4(1); auto two = i4(2); auto three = i4(3); auto steps = list({&one, &two, &three}); auto stepsPair = pair("STEPS", steps); return list({&tempPair, &stepsPair});}pdcomm::SecsItem recursiveAck() { auto zero1 = u1(0), four = u1(4), zero2 = u1(0); auto codes = list({&zero1, &four, &zero2}); auto tempZero = u1(0); auto temp = pair("TEMP", tempZero); auto steps = pair("STEPS", codes); auto fields = list({&temp, &steps}); auto outer = pair("RECIPE", fields); return list({&outer});}pdcomm::SecsItem singleAck(const std::string &name, uint8_t code) { auto c = u1(code); auto p = pair(name, c); return list({&p});}std::string text(const pdcomm::SecsItem &item) { const auto b = item.payload(); return std::string(b.begin(), b.end());}int32_t integer(const pdcomm::SecsItem &item) { const auto b = item.payload(); require(b.size() == 4, "I4 width"); return static_cast<int32_t>((uint32_t(b[0]) << 24) | (uint32_t(b[1]) << 16) | (uint32_t(b[2]) << 8) | b[3]);}bool tokenEqual(const PdcommTcpToken &a, const PdcommTcpToken &b) { return a.endpoint_id == b.endpoint_id && a.generation == b.generation && a.system_bytes == b.system_bytes && a.reserved == b.reserved;}void stopAndDrain(pdcomm::GemEndpoint &e) { if (!e.tcp().state().closed) e.stop(); const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (true) { if (!e.tcp().state().closed) e.poll(); for (auto v = e.nextEvent(); v; v = e.nextEvent()) (void)e.handle(v); if (e.tcp().state().closed || std::chrono::steady_clock::now() >= d) break; std::this_thread::sleep_for(std::chrono::milliseconds(1)); }}
void configure(pdcomm::GemEquipment &e) { e.configureReports(pdcomm::gemReportOptions(4, 4, 8, 4, 16, 16), {100, 200}); e.registerStatus(3, "CommandValue", "C", i4(0)); auto set = ascii("SET_TEMP"), value = ascii("VALUE"), tag = ascii("TAG"), seven = u1(7); const auto z = pdcomm::GemInt128::fromSigned(0), h = pdcomm::GemInt128::fromSigned(100); e.configureCommands( pdcomm::gemCommandOptions(8, 8, 32, 64, 1024, 8), {{&set, {{&value, 1, 1, z, h, true, true, PDCOMM_SECS_I4, true, true}, {&tag, 0, 32, z, z, false, true, PDCOMM_SECS_ASCII, false, false}}, true, true, 100}, {&seven, {}, false, false, 0}}); auto chamber1 = ascii("CHAMBER/1"), chamber2 = ascii("CHAMBER/2"), empty = ascii(""), apply = ascii("APPLY_RECIPE"), recipeName = ascii("RECIPE"); e.configureEnhancedCommands( pdcomm::gemEnhancedCommandOptions(8, 64, 256, 8, 16, 8, 32, 64, 1024, 8, 2048, limits()), {{&chamber1, {{&apply, {{&recipeName, 2, 2, z, z, true, true, PDCOMM_SECS_LIST, false, false}, {&tag, 0, 32, z, z, false, true, PDCOMM_SECS_ASCII, false, false}}, true, true, 100}}}, {&chamber2, {{&apply, {{&recipeName, 1, 1, z, z, true, true, PDCOMM_SECS_F8, false, false}}, false, false, 0}}}, {&empty, {{&seven, {}, false, false, 0}}}});}
PdcommTcpToken sendStep(pdcomm::GemHost &h, size_t step) { if (step == 1) return h.request(pdcomm::GemRequest::Online); if (step == 2) return h.defineReports(1, {{10, {3}}}); if (step == 3) return h.linkReports(2, {{100, {10}}}); if (step == 4) return h.enableEvents(true, {100}); auto name = step >= 13 && step <= 18 ? u1(7) : ascii(step == 6 ? "MISSING" : step == 20 ? "SET_TEMP" : "APPLY_RECIPE"); if (step == 19) return h.remoteCommand(name); auto object = ascii(step == 5 ? "chamber/1" : step == 9 ? "CHAMBER/2" : step >= 13 && step <= 17 ? "" : "CHAMBER/1"); auto key = ascii("RECIPE"), value = step == 8 ? ascii("invalid") : step == 9 ? f8() : recipe(step == 10 ? 20 : step == 11 ? 30 : step == 12 ? 40 : step == 23 ? 50 : 10); auto tag = ascii(step == 22 ? "EXTRA" : "TAG"), tagValue = ascii(step == 10 ? "NOW" : step == 11 ? "LATER" : step == 12 ? "BAD" : "LAST"); std::vector<pdcomm::GemCommandParameter> p; if (!(step == 7 || step == 6 || (step >= 13 && step <= 20))) { p.push_back({&key, &value}); if (step == 10 || step == 11 || step == 12 || step >= 21) p.push_back( {step == 21 ? &key : &tag, step == 21 ? &value : &tagValue}); } return h.enhancedRemoteCommand(static_cast<uint32_t>(1000 + step), object, name, p);}uint8_t expectedAck(size_t s) { static const uint8_t a[24] = {2, 0, 0, 0, 0, 6, 1, 3, 3, 0, 0, 4, 3, 5, 6, 2, 1, 0, 1, 1, 1, 3, 3, 0}; return a[s];}bool eventStep(size_t s) { return s == 10 || s == 11 || s == 23; }bool accepted(size_t s) { return s == 9 || s == 10 || s == 11 || s == 12 || (s >= 13 && s <= 17) || s == 23;}pdcomm::GemCommandAcknowledgement decision(size_t s) { using A = pdcomm::GemCommandAcknowledgement; if (s == 11) return A::AcceptedForLater; if (s == 12) return A::ParameterErrors; if (s == 13) return A::AlreadyInCondition; if (s == 14) return A::NoSuchObject; if (s == 15) return A::CannotPerform; if (s == 16) return A::Unknown; return A::Completed;}void checkRequest(const pdcomm::GemResult &r, size_t step) { const auto i = r.enhancedCommandInfo(); require(i.isRequest && i.dataId == 1000 + step && !i.responded, "enhanced request info"); require(i.validationAccepted == accepted(step), "enhanced admission"); auto object = r.enhancedCommandObject(), name = r.enhancedCommandName(); if (step >= 13 && step <= 18) require(name.payload() == std::vector<uint8_t>{7}, "numeric name"); else require(text(name) == (step == 6 ? "MISSING" : step == 20 ? "SET_TEMP" : "APPLY_RECIPE"), "enhanced name"); if (i.validationAccepted && i.parameterCount) { auto p = r.enhancedCommandParameter(0); require(text(p.name) == "RECIPE", "recipe name"); }}void checkResponse(const pdcomm::GemResult &r, size_t step) { const auto i = r.info(); require(i.acknowledgement == expectedAck(step), "enhanced ACK"); if (step == 19) { require(i.outcome == pdcomm::GemOutcome::CommandAck, "basic separation"); return; } require(i.outcome == pdcomm::GemOutcome::EnhancedCommandAck, "enhanced outcome"); const auto d = r.enhancedCommandInfo(); const size_t count = step == 7 || step == 8 || step == 12 || step == 21 || step == 22 ? 1 : 0; require(!d.isRequest && d.issueCount == count, "enhanced issue count"); auto a = r.enhancedCommandAcknowledgements(); pdcomm::SecsItem expected; if (step == 12) expected = recursiveAck(); else if (count) expected = singleAck(step == 22 ? "EXTRA" : "RECIPE", step == 8 ? 3 : step == 22 ? 1 : 2); else expected = list({}); require(a.bytes() == expected.bytes(), "recursive acknowledgement");}} // namespace
struct Payload { bool cancelled = false, host = false; std::vector<std::shared_ptr<pdcomm::GemResult>> results; std::vector<std::shared_ptr<pdcomm::SecsItem>> values; std::shared_ptr<pdcomm::GemResult> recursive;};struct Shared { std::atomic<bool> active{false}, success{false}, cancelled{false}; std::atomic<int> deliveries{0}, heartbeats{0}; std::string failure;};
class Worker final : public QThread { public: Worker(QCoreApplication *app, std::string role, std::string mode, std::string address, std::shared_ptr<Shared> s) : app_(app), role_(std::move(role)), mode_(std::move(mode)), address_(std::move(address)), shared_(std::move(s)) {}
protected: void run() override { shared_->active.store(true); try { require(QThread::currentThread() != app_->thread(), "endpoint on UI thread"); auto p = role_ == "host" ? runHost() : runEquipment(); post(std::move(p)); } catch (const pdcomm::Error &e) { shared_->failure = "PDComm status " + std::to_string(e.status()); } catch (const std::exception &e) { shared_->failure = e.what(); } shared_->active.store(false); }
private: void listen(const std::string &a) { auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, a] { if (QThread::currentThread() != app->thread()) return; s->deliveries.fetch_add(1); std::fprintf(stdout, "LISTEN %s\n", a.c_str()); std::fflush(stdout); }), "queue listen notification"); } pdcomm::TcpEndpoint endpoint() { bool a = mode_ == "active"; pdcomm::TcpEndpoint e( address_, tcpOptions(a ? PDCOMM_TCP_ACTIVE : PDCOMM_TCP_PASSIVE)); if (!a) listen(e.localAddress()); return e; } int uiCommand(const std::shared_ptr<pdcomm::GemResult> &r, size_t step) { auto answer = std::make_shared<std::atomic<int>>(-1); auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, r, answer, step] { try { require(QThread::currentThread() == app->thread(), "command not on UI"); checkRequest(*r, step); s->deliveries.fetch_add(1); answer->store(static_cast<int>(decision(step)), std::memory_order_release); } catch (...) { answer->store(-2, std::memory_order_release); } }), "queue command decision"); return await(answer, "command UI decision"); } void uiResponse(const std::shared_ptr<pdcomm::GemResult> &r, size_t step) { auto answer = std::make_shared<std::atomic<int>>(-1); auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, r, answer, step] { try { require(QThread::currentThread() == app->thread(), "response not on UI"); checkResponse(*r, step); s->deliveries.fetch_add(1); answer->store(0, std::memory_order_release); } catch (...) { answer->store(-2, std::memory_order_release); } }), "queue response inspection"); (void)await(answer, "response UI inspection"); } int uiEvent(const std::shared_ptr<pdcomm::GemResult> &r, size_t index) { auto answer = std::make_shared<std::atomic<int>>(-1); auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, r, answer, index] { try { require(QThread::currentThread() == app->thread(), "event not on UI"); require( r->eventInfo().dataId == 71 + index && integer(r->value(0, 0)) == (index == 0 ? 20 : index == 1 ? 30 : 50), "event UI snapshot"); s->deliveries.fetch_add(1); answer->store(index == 1 ? 7 : 0, std::memory_order_release); } catch (...) { answer->store(-2, std::memory_order_release); } }), "queue event decision"); return await(answer, "event UI decision"); } int await(const std::shared_ptr<std::atomic<int>> &a, const char *m) { const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(2); int v = -1; while ((v = a->load(std::memory_order_acquire)) == -1 && std::chrono::steady_clock::now() < d && !isInterruptionRequested()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); require(v >= 0, m); return v; }
Payload runHost() { Payload p; p.host = true; { auto t = endpoint(); pdcomm::GemHost h(std::move(t), pdcomm::gemHostOptions(limits(), 64)); h.start(); bool began = false, sent = false, cmd = false, event = false, peer = false; size_t step = 0, events = 0; PdcommTcpToken comm = {}, tokens[24] = {}; const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(15); while (std::chrono::steady_clock::now() < d && !isInterruptionRequested()) { h.poll(); if (!began && h.tcp().state().state == PDCOMM_TCP_SELECTED) { comm = h.beginCommunication(); began = true; } std::shared_ptr<pdcomm::GemResult> report; for (auto ev = h.nextEvent(); ev; ev = h.nextEvent()) { auto value = h.handle(ev); auto i = value.info(); if (i.outcome == pdcomm::GemOutcome::CommunicationEstablished) { require(i.kind == pdcomm::GemResultKind::Response && i.request == pdcomm::GemRequest::Establish && tokenEqual(i.token, comm), "communication token"); tokens[0] = sendStep(h, 0); sent = true; } else if (i.outcome == pdcomm::GemOutcome::EventReceived) report = std::make_shared<pdcomm::GemResult>( std::move(value)); else if (i.kind == pdcomm::GemResultKind::Response) { require(sent && tokenEqual(i.token, tokens[step]), "response token"); if (step >= 1 && step <= 4) require(i.acknowledgement == 0, "configuration ACK"); else { auto r = std::make_shared<pdcomm::GemResult>( std::move(value)); uiResponse(r, step); if (step == 12) p.recursive = r; } cmd = true; } } if (report) { int ack = uiEvent(report, events); p.values.push_back(std::make_shared<pdcomm::SecsItem>( report->value(0, 0))); h.acknowledgeEvent(*report, static_cast<uint8_t>(ack)); p.results.push_back(report); ++events; event = true; } if (sent && cmd && (!eventStep(step) || event)) { ++step; cmd = false; event = false; if (step < 24) tokens[step] = sendStep(h, step); } if (step == 24 && h.tcp().state().state != PDCOMM_TCP_SELECTED) { peer = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } if (isInterruptionRequested()) p.cancelled = true; else require(step == 24 && events == 3 && peer, "host enhanced timeout"); stopAndDrain(h); } return p; }
Payload runEquipment() { Payload p; { auto t = endpoint(); pdcomm::GemEquipment e(std::move(t), limits(), pdcomm::GemControl::HostOffline, pdcomm::GemOnlineMode::Remote, "PDComm", "0.1.0", 8, 1024, 4096); configure(e); e.start(); size_t step = 0, primaries = 0, enhanced = 0, numeric = 0, executions = 0, eventAcks = 0; PdcommTcpToken eventTokens[3] = {}; bool stopped = false; const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(15); while (std::chrono::steady_clock::now() < d && !isInterruptionRequested()) { e.poll(); std::shared_ptr<pdcomm::GemResult> command; for (auto ev = e.nextEvent(); ev; ev = e.nextEvent()) { auto value = e.handle(ev); auto i = value.info(); if (i.kind == pdcomm::GemResultKind::Primary) { ++primaries; if (i.outcome == pdcomm::GemOutcome::EnhancedCommandReceived) { command = std::make_shared<pdcomm::GemResult>( std::move(value)); ++enhanced; } else if (i.outcome == pdcomm::GemOutcome::CommandReceived) { require(step == 19 && !value.commandInfo().validationAccepted, "basic separation"); e.respondCommand( value, pdcomm::GemCommandAcknowledgement::Completed); ++step; } else if (step >= 1 && step <= 4) { auto x = step == 1 ? pdcomm::GemOutcome::OnlineAccepted : step == 2 ? pdcomm::GemOutcome::DefineReportsAck : step == 3 ? pdcomm::GemOutcome::LinkReportsAck : pdcomm::GemOutcome::EnableEventsAck; if (i.outcome == x) ++step; } } else if (i.kind == pdcomm::GemResultKind::Response && i.outcome == pdcomm::GemOutcome::EventAck) { require( eventAcks < 3 && tokenEqual(i.token, eventTokens[eventAcks]) && i.acknowledgement == (eventAcks == 1 ? 7 : 0), "event ACK"); ++eventAcks; } } if (command) { size_t s = step; auto choice = static_cast<pdcomm::GemCommandAcknowledgement>( uiCommand(command, s)); if (s >= 13 && s <= 17) ++numeric; if (s == 10 || s == 23) { e.updateStatus(3, i4(s == 10 ? 20 : 50)); ++executions; } pdcomm::SecsItem ack; if (s == 12) ack = recursiveAck(); auto reply = e.respondEnhancedCommand( *command, choice, s == 12 ? &ack : nullptr); require(static_cast<uint8_t>(reply.acknowledgement) == expectedAck(s), "actual ACK"); if (s == 11) { e.updateStatus(3, i4(30)); ++executions; } if (eventStep(s)) eventTokens[s == 10 ? 0 : s == 11 ? 1 : 2] = e.publishEvent( static_cast<uint32_t>(70 + executions), 100); if (s == 23) p.results.push_back(command); ++step; } if (step == 24 && eventAcks == 3) { e.stop(); stopped = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } if (isInterruptionRequested()) p.cancelled = true; else require(step == 24 && primaries == 25 && enhanced == 19 && numeric == 5 && executions == 3 && eventAcks == 3 && stopped, "equipment enhanced timeout"); stopAndDrain(e); } return p; }
void post(Payload p) { auto done = std::make_shared<std::atomic<int>>(0); auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, p, done] { try { require(QThread::currentThread() == app->thread(), "completion not on UI"); if (!p.cancelled && p.host) { require(p.recursive && p.results.size() == 3 && p.values.size() == 3, "host retained payload"); auto ack = p.recursive ->enhancedCommandAcknowledgements(); require(ack.bytes() == recursiveAck().bytes(), "delayed recursive ACK"); const int v[3] = {20, 30, 50}; for (size_t i = 0; i < 3; ++i) require( integer(*p.values[i]) == v[i] && p.results[i] ->eventInfo() .acknowledged, "delayed event"); } if (!p.cancelled && !p.host) require( p.results.size() == 1 && p.results[0] ->enhancedCommandInfo() .responded && p.results[0] ->enhancedCommandInfo() .dataId == 1023, "delayed request"); s->cancelled.store(p.cancelled); s->deliveries.fetch_add(1); done->store(1); } catch (...) { done->store(-1); } }), "queue completion"); const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (done->load() == 0 && std::chrono::steady_clock::now() < d) std::this_thread::sleep_for(std::chrono::milliseconds(1)); require(done->load() == 1, "completion UI"); require(shared_->heartbeats.load() > 0, "UI heartbeat"); shared_->success.store(true); } QCoreApplication *app_; std::string role_, mode_, address_; std::shared_ptr<Shared> shared_;};
int main(int argc, char **argv) { QCoreApplication app(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_enhanced_commands ROLE MODE " "ADDRESS [--cancel]\n"); return 2; } bool cancel = argc == 5; auto shared = std::make_shared<Shared>(); Worker worker(&app, argv[1], argv[2], argv[3], shared); QTimer heartbeat; heartbeat.setInterval(1); QObject::connect(&heartbeat, &QTimer::timeout, &app, [&] { if (shared->active.load()) { shared->heartbeats.fetch_add(1); } }); heartbeat.start(); QTimer cancelTimer; cancelTimer.setSingleShot(true); QObject::connect(&cancelTimer, &QTimer::timeout, &app, [&] { worker.requestInterruption(); }); QTimer watchdog; watchdog.setSingleShot(true); QObject::connect(&watchdog, &QTimer::timeout, &app, [&] { worker.requestInterruption(); }); QObject::connect(&worker, &QThread::started, &app, [&] { watchdog.start(18000); if (cancel) cancelTimer.start(20); }); QObject::connect(&worker, &QThread::finished, &app, [&] { worker.wait(); QTimer::singleShot(0, &app, [&] { if (!shared->success.load() || shared->deliveries.load() == 0 || shared->heartbeats.load() == 0) { std::fprintf(stderr, "FAIL QT ENHANCED: %s\n", shared->failure.empty() ? "validation failed" : shared->failure.c_str()); app.exit(1); return; } std::fprintf(stdout, shared->cancelled.load() ? "PASS QT ENHANCED CANCEL\n" : "PASS QT ENHANCED PEER\n"); std::fflush(stdout); app.quit(); }); }); worker.start(); int result = app.exec(); worker.requestInterruption(); worker.wait(); return result;}6. Try it in the simulator GUI
Section titled “6. Try it in the simulator GUI”You can drive the same exchange by hand with two simulator windows. The simulator GUI guide describes every control.


-
In the equipment window, open Remote Commands → Catalog, click Load example, and then click Use on next start. The example defines the basic command
STARTwith a required ASCII parameterLOT(1 to 32 characters), and the enhanced commandMOVEon objectPORT-1. -
In Overview, set GEM role to Equipment, TCP mode to Passive - listen and Session ID to
7, and then click Start session. -
Start the host window as Host and Active - connect with the same address and Session ID, and then click Establish communication.
-
In the host window, open Remote Commands → Traffic and enter
STARTin Basic name. Under Basic parameters, click Add parameter, enter NameLOT, RepresentationASCIIand ValueLOT-0042, and then click Submit basic. -
In the equipment window, open the Traffic tab, select the received request, choose Decision 0, and click Respond. The Host submissions area is disabled in an equipment window; you only use the received-request list and Respond further down.


-
In the host window, check that the result shows State
Completedand ACK0, and then click Acknowledge selected result. As the status line says, this is the result of the submission; it doesn’t assert that the equipment finished any work.

If you leave out LOT, the equipment window lists the request as rejected by validation (HCACK 3). That request still needs Respond before a reply goes out; the decision you pick is ignored and HCACK 3 is sent.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
The example prints Usage: ... and exits with code 2 |
The native library path isn’t absolute, or an argument is missing. | Pass an absolute path such as $PDCOMM_NATIVE ($Native on Windows). |
A host request fails with PdcommException (InvalidState) |
The command was sent before GEM communication was established. | Wait for the S1F13 reply first. |
| Every command gets HCACK 2 | The command needs online remote, and the equipment is offline or online local. | Send the online request (S1F17) first and check that the equipment’s online mode is remote. |
| HCACK 1 although the name looks right | A basic command went out as S2F49, or an enhanced command as S2F41, or a numeric name has another format. | Match the catalog the command lives in and the message you send. |
| HCACK 3 with CPACK 2 or 3 | A required parameter is missing, a value is out of range, or a format differs. | Compare the values with the parameter definitions in the catalog. |
RespondCommandAsync fails with InvalidArgument |
You passed CPACK entries with a decision other than ParameterErrors. |
Send CPACK entries only with HCACK 3. |
RespondCommandAsync fails with StaleToken |
The request was already answered, or it belongs to an earlier connection. | Answer each result once; after a reconnection, wait for new requests. |
Backpressure |
The worker’s command queue or byte budget is full. | Keep reading events so the queue drains, and retry after earlier commands finish. |
| The host never sees a completion event | The event isn’t enabled, or the equipment never published it. An event enabled without defined and linked reports arrives with an empty report list and no values. | Repeat the preparation of steps 1–4 and make sure the equipment publishes the event after the work. |
Clean up
Section titled “Clean up”- The examples exit on their own when the script finishes. If one stops halfway, press Ctrl+C in each terminal.
- In the simulator, click Stop in both windows before you close them.
- Delete
build/managedand the Qtbuildfolder to remove the build output.
Next steps
Section titled “Next steps”- Exchange recipes with S7 in Process programs.
- Look up API details in the SECS/GEM API reference.