Process programs
A process program tells equipment how to process material; on the shop floor it’s usually called a recipe. Each program has a PPID (process program ID) and a body, the PPBODY. GEM uses stream 7 (S7) messages to move programs between host and equipment.
S7 comes in two flavors:
- Unformatted programs travel as one opaque PPBODY value. They use S7F1–S7F6 and S7F17–S7F20.
- Formatted programs are structured as a list of command codes (CCODE) with parameters (PPARM). They use S7F21–S7F32.
In this tutorial you run both, then repeat a request in the simulator GUI.
What you’ll do
Section titled “What you’ll do”- Query the program directory, request a transfer grant, and send, request and delete unformatted programs.
- See how your application decides on each received S7 request, and what PPGNT and ACKC7 mean.
- Learn the durability results of a file-backed store and how to handle
PROGRAM_UNCONFIRMED. - Run the formatted capability query, send, request, verification request, verification report and verification negotiation.
- Confirm that the equipment’s local store, selection, dry run and execution never move a machine.
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.
- Build the C# tutorial example
PDComm.Tutorialas shown in First communication.
Unformatted program messages
Section titled “Unformatted program messages”| Message | Sent by | Purpose | Reply code |
|---|---|---|---|
| S7F1/S7F2 Process Program Load Inquire/Grant | Host or equipment | Announce a PPID and its length, and ask for permission. | PPGNT |
| S7F3/S7F4 Process Program Send/Acknowledge | Host or equipment | Send a PPID with its PPBODY. | ACKC7 |
| S7F5/S7F6 Process Program Request/Data | Host or equipment | Ask for a PPID and receive its PPBODY. | - |
| S7F17/S7F18 Delete Process Program Send/Acknowledge | Host | Send the PPIDs to delete. An empty list deletes all programs. | ACKC7 |
| S7F19/S7F20 Current EPPD Request/Data | Host | Ask for the PPIDs the equipment stores. | - |
The receiving application picks one of these codes:
| Value | PPGNT (S7F2) | ACKC7 (S7F4, S7F18) |
|---|---|---|
| 0 | Granted | Accepted |
| 1 | Already have | Permission not granted |
| 2 | No space | Length error |
| 3 | Invalid PPID | Matrix overflow |
| 4 | Busy | PPID not found |
| 5 | Will not accept | Mode unsupported |
PDComm never answers a program request by itself. After you configure a store with GemEquipment.ConfigurePrograms, each received S7F1, S7F3, S7F5, S7F17 and S7F19 arrives as a ProgramReceived result. Your application inspects it and calls GemEquipment.RespondProgram with a decision and a store mode (Create, Replace or Upsert).
- The store’s own checks win over an accepting decision. The grant policy, the store limits, the declared length, the mode and the existence of the PPID can still turn an accept into a rejection code.
- Storing a program, or acknowledging one, never selects or runs a process.
- An accepted S7F3 or S7F17 is applied to the store before the acknowledgement goes out.
- Every received S7F17 gets an S7F18. More PPIDs than
GemProgramOptions.MaxRequestIdsproduce ACKC7 3 (matrix overflow); a PPID that isn’t stored, or is longer than the store accepts, produces ACKC7 4 (PPID not found). A PPID listed twice is deleted once. A rejected request deletes nothing.
1. Build the example
Section titled “1. Build the example”dotnet build "$Examples\dotnet\PDComm.GemProgramsDemo\PDComm.GemProgramsDemo.csproj" -c Release "-p:PDCommSdk=$Sdk" --artifacts-path "$Root\build\managed"dotnet build "$PDCOMM_EXAMPLES/dotnet/PDComm.GemProgramsDemo/PDComm.GemProgramsDemo.csproj" -c Release "-p:PDCommSdk=$PDCOMM_SDK" --artifacts-path "$PDCOMM_ROOT/build/managed"dotnet build "$PDCOMM_EXAMPLES/dotnet/PDComm.GemProgramsDemo/PDComm.GemProgramsDemo.csproj" -c Release "-p:PDCommSdk=$PDCOMM_SDK" --artifacts-path "$PDCOMM_ROOT/build/managed"Expected result: Build succeeded. and 0 Error(s).
2. Run the unformatted program example
Section titled “2. Run the unformatted program example”In two terminals, start the equipment first and then connect the host to the address it prints.
# Terminal A: equipmentdotnet "$Root\build\managed\bin\PDComm.GemProgramsDemo\release\PDComm.GemProgramsDemo.dll" $Native equipment passive 127.0.0.1:0# Terminal B: hostdotnet "$Root\build\managed\bin\PDComm.GemProgramsDemo\release\PDComm.GemProgramsDemo.dll" $Native host active 127.0.0.1:<port># Terminal A: equipmentdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemProgramsDemo/release/PDComm.GemProgramsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0# Terminal B: hostdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemProgramsDemo/release/PDComm.GemProgramsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port># Terminal A: equipmentdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemProgramsDemo/release/PDComm.GemProgramsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0# Terminal B: hostdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemProgramsDemo/release/PDComm.GemProgramsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port>Expected result: the equipment prints LISTEN 127.0.0.1:<port> first. When the exchange ends, each side prints its PASS line and exits with code 0:
PASS host active DOTNET PROGRAMS PEERPASS equipment passive DOTNET PROGRAMS PEER3. Walk through the script
Section titled “3. Walk through the script”The same code runs both roles. Instead of a worker, this example polls the endpoint (GemHost or GemEquipment) directly. Before starting, each side configures an in-memory store (no path is passed to ConfigurePrograms) and seeds one program in SeedProgram:
- Equipment: PPID
BASEwith the ASCII body"BASE". - Host: PPID
HOSTwith the binary body48(the characterH).
Once communication is up, the host sends five requests (SendStep):
| Order | Host request | Equipment reply |
|---|---|---|
| 1 | S7F19 directory request | S7F20: BASE |
| 2 | S7F1 grant request for TRANSFER, 4 bytes |
S7F2: PPGNT 0 |
| 3 | S7F3 send TRANSFER = U4 42 |
S7F4: ACKC7 0, TRANSFER stored |
| 4 | S7F5 request BASE |
S7F6: ASCII "BASE" |
| 5 | S7F17 delete BASE |
S7F18: ACKC7 0, BASE deleted |
After serving the host, the equipment sends three requests of its own, because S7F1, S7F3 and S7F5 work in both directions:
| Order | Equipment request | Host reply |
|---|---|---|
| 1 | S7F1 grant request for UPLOAD, 9 bytes |
S7F2: PPGNT 0 |
| 2 | S7F3 send UPLOAD = ASCII "EQUIPMENT" |
S7F4: ACKC7 0, UPLOAD stored |
| 3 | S7F5 request HOST |
S7F6: binary 48 |
Finally the host reads status variable 1 with S1F3 to confirm that the equipment finished all its work. At the end the equipment store holds TRANSFER, and the host store holds HOST and UPLOAD (VerifyCatalog). Every write has Volatile durability, because neither store has a file.
Both sides answer in Respond with RespondProgram(result, GemProgramPutMode.Upsert, 0); decision 0 grants or accepts. The host request APIs are GemHost.RequestProgramDirectory, GemHost.GrantProgram, GemHost.SendProgram, GemHost.RequestProgram and GemHost.DeletePrograms.
Full source of PDComm.GemProgramsDemo/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.GemProgramsDemo <absolute-native-library-path> <host|equipment> <active|passive> <numeric-address>"); return 2;}
PdcommRuntime.Initialize(args[0]);bool isHost = args[1] == "host";HsmsMode mode = args[2] == "active" ? HsmsMode.Active : HsmsMode.Passive;using GemEndpoint endpoint = isHost ? GemHost.Create(args[3], Options.Transport(mode), Options.Host) : GemEquipment.Create(args[3], Options.Transport(mode), Options.Equipment);ConfigurePrograms(endpoint);SeedProgram(endpoint, isHost);if (endpoint is GemEquipment equipment){ using SecsItem value = SecsItem.FromF4([21.5f], Options.Limits); equipment.RegisterStatus(1, "Temperature", "C", value);}
endpoint.Start();if (mode == HsmsMode.Passive){ Console.WriteLine($"LISTEN {endpoint.GetLocalAddress()}"); Console.Out.Flush();}
int total = isHost ? 5 : 3;int incoming = isHost ? 3 : 5;int step = 0;int served = 0;bool began = false;bool communicating = false;bool started = false;bool barrier = false;bool stopping = false;HsmsToken pending = default;DateTime deadline = DateTime.UtcNow.AddSeconds(30);while (DateTime.UtcNow < deadline){ endpoint.Poll(); HsmsEvent? transportEvent; while ((transportEvent = endpoint.NextEvent()) is not null) { using (transportEvent) using (GemResult result = endpoint.HandleEvent(transportEvent)) { GemResultInfo info = result.GetInfo(); if (info.Kind is GemResultKind.Failed or GemResultKind.InvalidResponse or GemResultKind.Unhandled) throw new InvalidOperationException( $"Program protocol failed: kind={info.Kind}, code={info.Code}, request={info.RequestKind}."); if (info.Kind == GemResultKind.Primary && info.Outcome == GemOutcome.ProgramReceived) { if (served >= incoming) throw new InvalidOperationException("The peer sent an extra program request."); VerifyRequest(result, !isHost, served); Respond(endpoint, result); served++; } else if (info.Kind == GemResultKind.Response) { if (info.RequestKind == GemRequestKind.Establish) { if (!isHost || info.Outcome != GemOutcome.CommunicationEstablished) throw new InvalidOperationException("Communication establishment failed."); communicating = true; } else if (info.RequestKind == GemRequestKind.StatusValues) { if (!isHost || !barrier || step != total || served != incoming || info.Outcome != GemOutcome.ValuesReceived) throw new InvalidOperationException("The completion barrier failed."); endpoint.Stop(); stopping = true; } else { if (step >= total || info.Token != pending) throw new InvalidOperationException("Program response correlation failed."); VerifyResponse(result, isHost, step); step++; if (step < total) pending = SendStep(endpoint, isHost, step); } } else if (!isHost && info.Outcome == GemOutcome.StatusReplied && (step != total || served != incoming)) { throw new InvalidOperationException("The host ended before all program work completed."); } } }
GemState state = endpoint.GetState(); if (state.Communication == GemCommunicationState.Communicating) communicating = true; HsmsState transport = endpoint.GetTransportState(); if (isHost && !began && transport.State == HsmsConnectionState.Selected) { pending = endpoint.BeginCommunication(); began = true; } if (communicating && !started && (isHost || served == incoming)) { pending = SendStep(endpoint, isHost, 0); started = true; } if (isHost && step == total && served == incoming && !barrier) { pending = ((GemHost)endpoint).RequestStatusValues([1]); barrier = true; } if (!isHost && communicating && transport.State != HsmsConnectionState.Selected && !stopping) { if (step != total || served != incoming) throw new InvalidOperationException("The host disconnected before the program exchange completed."); endpoint.Stop(); stopping = true; } if (stopping && endpoint.GetTransportState().Closed) { VerifyCatalog(endpoint, isHost); Console.WriteLine($"PASS {args[1]} {args[2]} DOTNET PROGRAMS PEER"); return 0; } Thread.Sleep(1);}
throw new TimeoutException( $"Program peer timed out: role={args[1]}, step={step}/{total}, served={served}/{incoming}.");
static void ConfigurePrograms(GemEndpoint endpoint){ if (endpoint is GemHost host) host.ConfigurePrograms(Options.Programs); else ((GemEquipment)endpoint).ConfigurePrograms(Options.Programs);}
static void SeedProgram(GemEndpoint endpoint, bool host){ using SecsItem id = SecsItem.FromAscii(host ? "HOST" : "BASE", Options.Limits); using SecsItem body = host ? SecsItem.FromBinary([(byte)'H'], Options.Limits) : SecsItem.FromAscii("BASE", Options.Limits); GemProgramWriteInfo result = endpoint is GemHost hostEndpoint ? hostEndpoint.PutProgram(id, body, GemProgramPutMode.Upsert) : ((GemEquipment)endpoint).PutProgram(id, body, GemProgramPutMode.Upsert); if (!result.Applied || result.Durability != GemProgramDurability.Volatile) throw new InvalidOperationException("The initial program was not stored in memory.");}
static HsmsToken SendStep(GemEndpoint endpoint, bool host, int step){ if (host && step == 0) return ((GemHost)endpoint).RequestProgramDirectory(); string name = host ? step is 3 or 4 ? "BASE" : "TRANSFER" : step == 2 ? "HOST" : "UPLOAD"; using SecsItem id = SecsItem.FromAscii(name, Options.Limits); return (host, step) switch { (true, 1) => ((GemHost)endpoint).GrantProgram(id, 4), (true, 2) => Send(endpoint, id, SecsItem.FromU4([42], Options.Limits)), (true, 3) => ((GemHost)endpoint).RequestProgram(id), (true, 4) => ((GemHost)endpoint).DeletePrograms([id]), (false, 0) => ((GemEquipment)endpoint).GrantProgram(id, 9), (false, 1) => Send(endpoint, id, SecsItem.FromAscii("EQUIPMENT", Options.Limits)), (false, 2) => ((GemEquipment)endpoint).RequestProgram(id), _ => throw new ArgumentOutOfRangeException(nameof(step)), };}
static HsmsToken Send(GemEndpoint endpoint, SecsItem id, SecsItem body){ using (body) return endpoint is GemHost host ? host.SendProgram(id, body) : ((GemEquipment)endpoint).SendProgram(id, body);}
static GemRequestKind RequestKind(bool host, int step) => host ? step switch { 0 => GemRequestKind.ProgramDirectory, 1 => GemRequestKind.ProgramGrant, 2 => GemRequestKind.ProgramSend, 3 => GemRequestKind.ProgramRequest, 4 => GemRequestKind.ProgramDelete, _ => throw new ArgumentOutOfRangeException(nameof(step)), } : step switch { 0 => GemRequestKind.ProgramGrant, 1 => GemRequestKind.ProgramSend, 2 => GemRequestKind.ProgramRequest, _ => throw new ArgumentOutOfRangeException(nameof(step)), };
static void VerifyRequest(GemResult result, bool senderHost, int step){ GemResultInfo generic = result.GetInfo(); GemProgramInfo program = result.GetProgramInfo(); GemRequestKind requestKind = RequestKind(senderHost, step); if (generic.RequestKind != RequestKind(senderHost, step) || !program.IsRequest || program.Responded || program.RequestKind != generic.RequestKind) throw new InvalidOperationException("Unexpected incoming program request."); nuint expectedCount = requestKind == GemRequestKind.ProgramDirectory ? 0u : 1u; if (program.IdCount != expectedCount || generic.EntryCount != expectedCount) throw new InvalidOperationException("Unexpected incoming program identifier count."); if (expectedCount != 0) { string expectedId = senderHost ? step is 3 or 4 ? "BASE" : "TRANSFER" : step == 2 ? "HOST" : "UPLOAD"; using SecsItem id = result.GetProgramId(0); using SecsItem expected = SecsItem.FromAscii(expectedId, Options.Limits); if (!id.Encode().SequenceEqual(expected.Encode())) throw new InvalidOperationException("Unexpected incoming program identifier."); } if (requestKind == GemRequestKind.ProgramGrant && program.DeclaredLength != (senderHost ? 4u : 9u)) throw new InvalidOperationException("Unexpected incoming program grant length."); if (requestKind == GemRequestKind.ProgramSend) { using SecsItem? body = result.GetProgramBody(); using SecsItem expected = senderHost ? SecsItem.FromU4([42], Options.Limits) : SecsItem.FromAscii("EQUIPMENT", Options.Limits); if (body is null || !body.Encode().SequenceEqual(expected.Encode())) throw new InvalidOperationException("Unexpected incoming program body."); }}
static void Respond(GemEndpoint endpoint, GemResult result){ if (endpoint is GemHost host) host.RespondProgram(result, GemProgramPutMode.Upsert, 0); else ((GemEquipment)endpoint).RespondProgram(result, GemProgramPutMode.Upsert, 0);}
static void VerifyResponse(GemResult result, bool senderHost, int step){ GemResultInfo generic = result.GetInfo(); GemProgramInfo program = result.GetProgramInfo(); if (generic.Kind != GemResultKind.Response || generic.RequestKind != RequestKind(senderHost, step) || program.IsRequest || program.RequestKind != generic.RequestKind) throw new InvalidOperationException("Unexpected program response."); if (generic.RequestKind is GemRequestKind.ProgramGrant or GemRequestKind.ProgramSend or GemRequestKind.ProgramDelete) { if (program.Acknowledgement != 0) throw new InvalidOperationException("The program request was rejected."); } else if (generic.RequestKind == GemRequestKind.ProgramRequest) { using SecsItem? body = result.GetProgramBody(); using SecsItem expected = senderHost ? SecsItem.FromAscii("BASE", Options.Limits) : SecsItem.FromBinary([(byte)'H'], Options.Limits); if (body is null || !body.Encode().SequenceEqual(expected.Encode())) throw new InvalidOperationException("The program response body differs."); } else { if (program.IdCount != 1) throw new InvalidOperationException("The program directory size differs."); using SecsItem id = result.GetProgramId(0); using SecsItem expected = SecsItem.FromAscii("BASE", Options.Limits); if (!id.Encode().SequenceEqual(expected.Encode())) throw new InvalidOperationException("The program directory identifier differs."); }}
static void VerifyCatalog(GemEndpoint endpoint, bool host){ GemProgramState state = endpoint is GemHost hostEndpoint ? hostEndpoint.GetProgramState() : ((GemEquipment)endpoint).GetProgramState(); nuint expected = host ? 2u : 1u; if (state.ProgramCount != expected || state.Persistent || state.GrantCount != 0 || state.ReservedBodyBytes != 0) throw new InvalidOperationException("The final in-memory program catalog differs.");}
internal static class Options{ internal static readonly SecsLimits Limits = new(4096, 4096, 256, 16); internal static readonly GemHostOptions Host = new() { MaxPendingIds = 64, ItemLimits = Limits, }; internal static readonly GemEquipmentOptions Equipment = new() { Model = "PDComm", SoftwareRevision = "0.1.0", InitialControl = GemControlState.HostOffline, OnlineMode = GemOnlineMode.Remote, MaxVariables = 8, MaxMetadataBytes = 1024, MaxValueBytes = 4096, ItemLimits = Limits, }; internal static readonly GemProgramOptions Programs = new() { MaxPrograms = 16, MaxIdentifierBytes = 120, MaxTotalIdentifierBytes = 4096, MaxBodyBytes = 4096, MaxTotalBodyBytes = 16384, MaxRequestIds = 32, MaxGrants = 8, MaxReservedBodyBytes = 8192, MaxPendingIds = 64, MaxPendingIdentifierBytes = 4096, MaxSerializedBytes = 65536, GrantPolicy = GemProgramGrantPolicy.Optional, }; internal static HsmsOptions Transport(HsmsMode mode) => 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. Store programs in a file
Section titled “4. Store programs in a file”Pass a file path to ConfigurePrograms to back the store with that file. After every change, check the durability of the write result (GemProgramDurability):
| Durability | Meaning |
|---|---|
None |
No stored change belongs to this operation, for example a rejection or a read. |
Volatile |
The store has no file, so the change lives in memory only. |
Synced |
The change was written to the file, and the parent folder was synchronized. |
Unconfirmed |
The change is installed, but its persistence couldn’t be confirmed. |
When persistence can’t be confirmed, RespondProgram sends no reply and fails with PdcommStatus.ProgramUnconfirmed. The change is already in the store. Handle it in this order:
- Optionally call
GemEquipment.ResyncProgramsto bring the store and the file back in line. - Call
GemEquipment.ConfirmProgramReplyto release the held reply. - Call
RespondProgramagain with the same arguments to send the saved reply.
If sending fails for another reason, such as Backpressure, call RespondProgram again with the same mode and decision. Don’t redo the work.
The Qt example programs.cpp in the examples bundle follows the same sequence. It backs the equipment store with the SNAPSHOT file named on the command line (configurePrograms(options(false), path)), runs the GEM endpoint on a QThread worker, and hands each response check to the Qt main thread. Because it shares scenario definitions with PDComm’s internal tests, it doesn’t build from the SDK alone and isn’t reproduced here. For Qt examples you can build and run, see commands.cpp in Remote commands and reports.cpp in Reports and events.
5. Run formatted programs
Section titled “5. Run formatted programs”Formatted program messages each go in one direction:
| Message | Direction | Purpose |
|---|---|---|
| S7F21/S7F22 Process Capabilities Request/Data | Host → equipment | The command codes and parameter formats the equipment understands |
| S7F23/S7F24 Formatted Process Program Send/Acknowledge | Host → equipment | Send a formatted program |
| S7F25/S7F26 Formatted Process Program Request/Data | Host → equipment | Request a formatted program |
| S7F27/S7F28 Process Program Verification Send/Acknowledge | Equipment → host | Report a verification result and its issues |
| S7F29/S7F30 Process Program Verification Inquire/Grant | Equipment → host | Ask whether a verification report may be sent |
| S7F31/S7F32 Verification Request Send/Acknowledge | Host → equipment | Ask the equipment to verify a formatted program |
The C# tutorial example PDComm.Tutorial turns these messages into interactive commands. It uses one program: PPID PP-DEMO, MDLN PDComm, SOFTREV 0.1.0, with a single command RUN that takes no parameters. At startup the equipment side registers a query entry and a capability catalog (SeedQuery and ReplaceCapabilities).
Run the C# example on both sides, in two terminals:
# Terminal A: equipmentdotnet "$Root\build\managed\bin\PDComm.Tutorial\release\PDComm.Tutorial.dll" $Native equipment passive 127.0.0.1:5000# Terminal B: hostdotnet "$Root\build\managed\bin\PDComm.Tutorial\release\PDComm.Tutorial.dll" $Native host active 127.0.0.1:5000# Terminal A: equipmentdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.Tutorial/release/PDComm.Tutorial.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:5000# Terminal B: hostdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.Tutorial/release/PDComm.Tutorial.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:5000# Terminal A: equipmentdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.Tutorial/release/PDComm.Tutorial.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:5000# Terminal B: hostdotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.Tutorial/release/PDComm.Tutorial.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:5000Type the host commands one line at a time and wait for each reply. Start with establish:
establishRESULT kind=2 request=1 outcome=2 status=CommunicationEstablishedSECS-II 0102210100010241065044436F6D6D4105302E312E30capabilityRESULT kind=2 request=39 outcome=65 status=FormattedCapabilityResultSECS-II 010541065044436F6D6D4105302E312E30A50104B104000010000101010B410352554E410352756E250101650100A900A900A900A9020000A900A9000100transferRESULT kind=2 request=36 outcome=59 status=FormattedTransferAckSECS-II 210100queryRESULT kind=2 request=37 outcome=61 status=FormattedQueryResultSECS-II 0104410750502D44454D4F41065044436F6D6D4105302E312E3001010102410352554E0100verify-requestRESULT kind=2 request=38 outcome=63 status=FormattedVerificationRequestAckSECS-II 210100The equipment terminal shows each received request as kind=1 (a primary). The example equipment answers formatted requests right away with RespondCapabilityQuery, RespondTransferAndStore, RespondQuery and RespondVerificationRequest:
RESULT kind=1 request=0 outcome=2 status=CommunicationEstablishedSECS-II 0100RESULT kind=1 request=39 outcome=64 status=FormattedCapabilityReceivedRESULT kind=1 request=36 outcome=58 status=FormattedTransferReceivedSECS-II 0104410750502D44454D4F41065044436F6D6D4105302E312E3001010102410352554E0100RESULT kind=1 request=37 outcome=60 status=FormattedQueryReceivedSECS-II 410750502D44454D4FRESULT kind=1 request=38 outcome=62 status=FormattedVerificationRequestReceivedSECS-II 0104410750502D44454D4F41065044436F6D6D4105302E312E3001010102410352554E0100Now type the equipment commands in the equipment terminal:
verify-reportRESULT kind=2 request=35 outcome=57 status=FormattedVerificationReportAcknegotiateRESULT kind=2 request=40 outcome=67 status=FormattedVerificationGrantReceivedSECS-II 210100RESULT kind=2 request=35 outcome=68 status=FormattedVerificationNegotiationAckstoreSTORE entries=1dryrunDRYRUN outcome=Pass issues=0executeDRYRUN outcome=Pass issues=0APPLICATION DEMO: acknowledge RUN without controlling equipmentEXECUTION state=CompletedThe host terminal shows the equipment’s verification report and inquiry:
RESULT kind=1 request=35 outcome=56 status=FormattedVerificationReportReceivedSECS-II 0102410750502D44454D4F01010103B10400000001B10400000001410A64656D6F206973737565RESULT kind=1 request=40 outcome=66 status=FormattedVerificationInquireReceivedSECS-II B1040000000DRESULT kind=1 request=35 outcome=56 status=FormattedVerificationReportReceivedSECS-II 0102410750502D44454D4F0100| Command | Typed on | Messages and result |
|---|---|---|
capability |
Host | S7F21 → S7F22. Returns the equipment’s capability catalog (the RUN command). |
transfer |
Host | S7F23 → S7F24 ACK 0. The equipment stores the program with Upsert. |
query |
Host | S7F25 → S7F26. Returns the RUN content registered for PP-DEMO. |
verify-request |
Host | S7F31 → S7F32 ACK 0. |
verify-report |
Equipment | S7F27 → S7F28, reporting one issue (demo issue). S7F28 is a header-only reply. |
negotiate |
Equipment | S7F29 (the report length, 13 bytes) → S7F30 PPGNT 0 → the prepared S7F27 → S7F28. |
store |
Equipment | Puts PP-DEMO into the local formatted store, with no network traffic. |
dryrun |
Equipment | Selects the stored PP-DEMO and checks it against the capability catalog; Pass means no issues. |
execute |
Equipment | Starts an execution from the dry-run report. The example application handles RUN as Applied and completes the execution as Completed. |
The request and outcome numbers are GemRequestKind and GemOutcome values; status is the outcome name.
You reach the formatted program API through the endpoint’s FormattedPrograms property (GemFormattedPrograms). Equipment replies use GemFormattedPrograms.RespondTransferAndStore and GemFormattedPrograms.RespondQuery; the local execution flow starts with GemFormattedPrograms.SelectProgram, GemFormattedPrograms.DryRun and GemFormattedPrograms.StartExecution.
Full source of PDComm.Tutorial/Program.cs
using System.Collections.Concurrent;using PDComm;
if (args.Length is < 4 or > 5 || args[1] is not ("host" or "equipment") || args[2] is not ("active" or "passive") || args.Length == 5 && args[4] != "--auto"){ Console.Error.WriteLine("Usage: PDComm.Tutorial <absolute-library-path> <host|equipment> <active|passive> <numeric-IP:port> [--auto]"); return 2;}try{ PdcommRuntime.Initialize(Path.GetFullPath(args[0])); bool automatic=args.Length==5; var mode=args[2]=="active"?HsmsMode.Active:HsmsMode.Passive; using GemEndpoint endpoint=args[1]=="host"? GemHost.Create(args[3],TutorialOptions.Transport(mode),TutorialOptions.HostOptions()): GemEquipment.Create(args[3],TutorialOptions.Transport(mode),TutorialOptions.EquipmentOptions()); var api=endpoint.FormattedPrograms; TutorialOptions.Configuration().Configure(endpoint); using var id=SecsItem.FromAscii("PP-DEMO",TutorialOptions.Limits); using var code=SecsItem.FromAscii("RUN",TutorialOptions.Limits); var payload=new GemFormattedProgramPayload(id,"PDComm","0.1.0",[new(code,[])]); if(endpoint is GemEquipment equipment) { api.SeedQuery(payload); using var temperature=SecsItem.FromF4([21.5f],TutorialOptions.Limits); using var lot=SecsItem.FromAscii("LOT-001",TutorialOptions.Limits); equipment.RegisterStatus(1,"Temperature","C",temperature); equipment.RegisterStatus(2,"Lot","",lot); api.ReplaceCapabilities("PDComm","0.1.0",4,4096,[TutorialOptions.Capability()]); } endpoint.Start(); if(mode==HsmsMode.Passive) Console.WriteLine($"LISTEN {endpoint.GetLocalAddress()}"); var input=new ConcurrentQueue<string>(); if(!automatic) { Console.WriteLine("Commands: state, establish, identity, values, names, online, offline, linktest, directories, alarms, transfer, query, verify-request, verify-report, capability, negotiate, store, dryrun, execute, quit"); _=Task.Run(()=> { string? line; while((line=Console.ReadLine()) is not null) { input.Enqueue(line); if(line=="quit") return; } input.Enqueue("quit"); }); } int step=0,received=0; DateTime deadline=DateTime.UtcNow.AddSeconds(20),finish=deadline; bool finishing=false,quit=false; while(!quit) { endpoint.Poll(); HsmsEvent? current; while((current=endpoint.NextEvent()) is not null) { using(current) using(var result=endpoint.HandleEvent(current)) { var info=result.GetInfo(); if(info.Kind!=GemResultKind.Ignored) { Console.WriteLine($"RESULT kind={(uint)info.Kind} request={(uint)info.RequestKind} outcome={(uint)info.Outcome} status={info.Outcome}"); using var body=result.GetBody(); if(body is not null) Console.WriteLine("SECS-II "+Convert.ToHexString(body.Encode())); } if(info.Kind==GemResultKind.Unhandled && current.GetInfo().Header is { ReplyExpected: true } primary) { // No GEM handler took this primary. Abort it with SxF0 so the peer does not // wait for T3 and the primary does not keep holding an incoming slot. try { endpoint.ReplyRaw(current,primary.Stream,0,false,ReadOnlySpan<byte>.Empty); Console.WriteLine($"ABORT S{primary.Stream}F0 for S{primary.Stream}F{primary.Function}"); } catch(PdcommException error) { Console.Error.WriteLine($"REPLY ERROR status={error.Status}"); } } if(info.Kind==GemResultKind.Primary) { switch(info.RequestKind) { case GemRequestKind.FormattedTransfer: api.RespondTransferAndStore(result,0,GemFormattedProgramStoreAction.Upsert); break; case GemRequestKind.FormattedQuery: api.RespondQuery(result); break; case GemRequestKind.FormattedVerificationRequest: api.RespondVerificationRequest(result,0); break; case GemRequestKind.FormattedVerificationReport: api.RespondVerificationReport(result); break; case GemRequestKind.FormattedCapability: api.RespondCapabilityQuery(result); break; case GemRequestKind.FormattedVerificationNegotiation: api.RespondVerification(result,0); break; } if(automatic && endpoint is GemEquipment && ++received==3) { finishing=true; finish=DateTime.UtcNow.AddMilliseconds(500); } } if(automatic && endpoint is GemHost host && info.Kind==GemResultKind.Response) { if(step==1 && info.Outcome==GemOutcome.CommunicationEstablished) { host.AreYouOnline(); step=2; } else if(step==2 && info.Outcome==GemOutcome.EquipmentIdentity) { host.RequestStatusValues([1,2]); step=3; } else if(step==3 && info.Outcome==GemOutcome.ValuesReceived) { finishing=true; finish=DateTime.UtcNow.AddMilliseconds(500); } } } } if(automatic && endpoint is GemHost autoHost && step==0 && endpoint.GetTransportState().State==HsmsConnectionState.Selected) { autoHost.BeginCommunication(); step=1; } if(automatic && finishing && DateTime.UtcNow>=finish) break; if(automatic && DateTime.UtcNow>deadline) throw new TimeoutException("Tutorial peer response deadline expired"); if(input.TryDequeue(out string? line)) { try { uint[] ids=[1,2]; if(line.StartsWith("values ",StringComparison.Ordinal) || line.StartsWith("names ",StringComparison.Ordinal)) { string[] fields=line.Split(' ',StringSplitOptions.RemoveEmptyEntries); ids=fields.Skip(1).Select(uint.Parse).ToArray(); line=fields[0]; } if(line=="quit") quit=true; else if(line=="state") Console.WriteLine($"STATE tcp={endpoint.GetTransportState().State} gem={endpoint.GetState().Communication}"); else if(line=="linktest") endpoint.Linktest(); else if(line=="transfer") api.SubmitTransfer(payload); else if(line=="query") api.SubmitQuery(id); else if(line=="verify-request") api.SubmitVerificationRequest(payload); else if(line=="verify-report") api.SubmitVerificationReport(id,[new(1,1,"demo issue")]); else if(line=="capability") api.SubmitCapabilityQuery(); else if(line=="negotiate") { using var prepared=api.PrepareVerification(id,[]); api.SubmitPreparedVerification(prepared); } else if(line=="store") { api.PutProgram(payload,GemFormattedProgramStoreAction.Upsert); Console.WriteLine($"STORE entries={api.GetStoreInfo().EntryCount}"); } else if(line is "dryrun" or "execute") { var selection=api.SelectProgram(id); using var report=api.DryRun(selection,new(1,32,8192)); Console.WriteLine($"DRYRUN outcome={(GemFormattedProgramDryRunOutcome)report.GetInfo().Outcome} issues={report.GetInfo().IssueCount}"); if(line=="execute") { using var start=api.StartExecution(selection,report); api.SubmitActionResult(GemFormattedPrograms.GetActionToken(start.Action),GemFormattedProgramActionResult.Accepted); using var action=api.StepExecution(start.Token); Console.WriteLine("APPLICATION DEMO: acknowledge RUN without controlling equipment"); api.SubmitActionResult(GemFormattedPrograms.GetActionToken(action),GemFormattedProgramActionResult.Applied); api.CompleteExecution(start.Token,1,GemFormattedProgramTerminalOutcome.Complete,ReadOnlyMemory<byte>.Empty); using var snapshot=api.GetExecutionSnapshot(); Console.WriteLine($"EXECUTION state={(GemFormattedProgramExecutionState)snapshot.GetInfo().State}"); } } else if(endpoint is GemHost host) { switch(line) { case "establish": host.BeginCommunication(); break; case "identity": host.AreYouOnline(); break; case "values": host.RequestStatusValues(ids); break; case "names": host.RequestStatusNames(ids); break; case "online": host.RequestOnline(); break; case "offline": host.RequestOffline(); break; case "directories": host.RequestDataDirectory([]); host.RequestEventDirectory([]); break; case "alarms": host.RequestAlarmList([]); break; default: throw new ArgumentException("Unknown command"); } } else throw new ArgumentException("This command requires the Host role"); } catch(Exception error) { Console.Error.WriteLine("COMMAND ERROR "+error.Message); } } Thread.Sleep(2); } endpoint.Stop(); if(automatic) Console.WriteLine($"PASS tutorial {args[1]} {args[2]}"); return 0;}catch(Exception error) { Console.Error.WriteLine(error); return 1; }6. Try it in the simulator GUI
Section titled “6. Try it in the simulator GUI”Use the two-window setup from the simulator GUI guide.
-
In the equipment window, open Process Programs → Catalog and click Load example. Equipment program draft gets one entry: PPID
DEMO-PPwith PPBODYDEMO. Click Use on next start, and then start the equipment session. Once it runs, Actual session store shows the same entry.

-
In the host window, establish communication and open Process Programs → Traffic. Set Kind to Request, keep the PPID format Ascii, enter
DEMO-PP, and click Submit. -
In the equipment window, open Traffic, select the received request, and click Respond.
-
The host’s result table gets a row with Kind
Requestand StateCompleted. S7F6 has no reply code, so ACK shows-.

Switch Kind to Directory or Delete to send S7F19 or S7F17; only a host window offers those two. For formatted programs, use the Formatted transfer, Formatted verification, Verification negotiation, Formatted query, Selection, Dry run, Execution and Capabilities tabs.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
A request fails with InvalidState |
It was sent before GEM communication, or the program store isn’t configured. | Send after communication is established, and call ConfigurePrograms before you start the endpoint. |
transfer in the equipment terminal prints COMMAND ERROR ... InvalidState |
Only a host can send S7F23. | Type formatted request commands in the host terminal. |
| S7F2 carries PPGNT 1 to 5 | The PPID exists, the store is full, the grant policy forbids it, and so on. | Check the store limits (GemProgramOptions) and the existing PPIDs. |
| S7F4 carries ACKC7 1 | The grant policy is Required, and S7F3 arrived without an S7F1 grant. |
Ask for a grant with S7F1 first, or use the Optional policy. |
| S7F4 carries ACKC7 2 | The length declared in S7F1 differs from the actual PPBODY length. | Declare the real body size in the grant request. |
| S7F6 has an empty PPID and PPBODY | The requested PPID isn’t stored. | Check the PPIDs with S7F19 first. |
| S7F18 carries ACKC7 3 | The delete list has more PPIDs than the equipment’s GemProgramOptions.MaxRequestIds. The equipment deletes nothing. |
Split the list over several requests. |
| S7F18 carries ACKC7 4 | The S7F17 delete list contains a PPID that isn’t stored, or one longer than the store accepts. The equipment answers ACKC7 4 (PPID not found) and deletes nothing. | Build the delete list from an S7F19 result. |
A host program request fails with InvalidArgument |
The PPID is empty or isn’t an ASCII or binary item. | Use a non-empty ASCII or binary PPID. |
A host program request fails with LimitExceeded |
The PPID is longer than 120 bytes, or the PPID count or the message size exceeds a limit (MaxRequestIds, the pending request limits or the item limits). |
Shorten the PPIDs or the request, or raise the limits. |
ConfigurePrograms fails with InvalidArgument |
The store path has no file name, for example because you passed a folder. | Pass a path that ends in a file name. |
A verification negotiation ends with NegotiationTerminal or VerificationTerminal still 0 |
The host’s S7F30 or S7F28 was malformed. A reply closes the transaction, so the negotiation ends there and releases its slot. | Check the host’s reply format and start a new negotiation. |
RespondVerification or RespondVerificationRequest fails with StaleToken |
You answered a request again with another code after its S7F30 or S7F32 was sent. | A sent reply can’t change. Retry with the code you used the first time. |
RespondProgram fails with ProgramUnconfirmed |
Persistence to the file couldn’t be confirmed. | Follow step 4: ResyncPrograms, ConfirmProgramReply, then RespondProgram with the same arguments. |
Backpressure |
A retained transport event must be read first, or the transport can’t take the reply yet. | Keep reading events and call again with the same mode and decision. |
The example prints Usage: ... and exits with code 2 |
The native library path isn’t absolute. | Pass an absolute path such as $PDCOMM_NATIVE. |
Clean up
Section titled “Clean up”- Type
quitin both terminals to end the interactive example. PDComm.GemProgramsDemouses in-memory stores only, so it leaves no files behind.- In the simulator, click Stop in both windows. To drop a store that a stopped equipment session keeps, click End session.
Next steps
Section titled “Next steps”- Hold messages while the host is away and send them later in Spooling.
- Look up API details in the SECS/GEM API reference.