Skip to content
Start free evaluation

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.

  • 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.
  • Set PDCOMM_ROOT, PDCOMM_SDK, PDCOMM_NATIVE and PDCOMM_EXAMPLES as described in Set up your environment. In PowerShell the variables are $Root, $Sdk, $Native and $Examples.
  • Install the .NET 10 SDK.
  • Build the C# tutorial example PDComm.Tutorial as shown in First communication.
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.MaxRequestIds produce 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.
터미널 창
dotnet build "$Examples\dotnet\PDComm.GemProgramsDemo\PDComm.GemProgramsDemo.csproj" -c Release "-p:PDCommSdk=$Sdk" --artifacts-path "$Root\build\managed"

Expected result: Build succeeded. and 0 Error(s).

In two terminals, start the equipment first and then connect the host to the address it prints.

터미널 창
# Terminal A: equipment
dotnet "$Root\build\managed\bin\PDComm.GemProgramsDemo\release\PDComm.GemProgramsDemo.dll" $Native equipment passive 127.0.0.1:0
# Terminal B: host
dotnet "$Root\build\managed\bin\PDComm.GemProgramsDemo\release\PDComm.GemProgramsDemo.dll" $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 PEER
PASS equipment passive DOTNET PROGRAMS PEER

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 BASE with the ASCII body "BASE".
  • Host: PPID HOST with the binary body 48 (the character H).

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
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,
};
}

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:

  1. Optionally call GemEquipment.ResyncPrograms to bring the store and the file back in line.
  2. Call GemEquipment.ConfirmProgramReply to release the held reply.
  3. Call RespondProgram again 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.

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: equipment
dotnet "$Root\build\managed\bin\PDComm.Tutorial\release\PDComm.Tutorial.dll" $Native equipment passive 127.0.0.1:5000
# Terminal B: host
dotnet "$Root\build\managed\bin\PDComm.Tutorial\release\PDComm.Tutorial.dll" $Native host active 127.0.0.1:5000

Type the host commands one line at a time and wait for each reply. Start with establish:

establish
RESULT kind=2 request=1 outcome=2 status=CommunicationEstablished
SECS-II 0102210100010241065044436F6D6D4105302E312E30
capability
RESULT kind=2 request=39 outcome=65 status=FormattedCapabilityResult
SECS-II 010541065044436F6D6D4105302E312E30A50104B104000010000101010B410352554E410352756E250101650100A900A900A900A9020000A900A9000100
transfer
RESULT kind=2 request=36 outcome=59 status=FormattedTransferAck
SECS-II 210100
query
RESULT kind=2 request=37 outcome=61 status=FormattedQueryResult
SECS-II 0104410750502D44454D4F41065044436F6D6D4105302E312E3001010102410352554E0100
verify-request
RESULT kind=2 request=38 outcome=63 status=FormattedVerificationRequestAck
SECS-II 210100

The 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=CommunicationEstablished
SECS-II 0100
RESULT kind=1 request=39 outcome=64 status=FormattedCapabilityReceived
RESULT kind=1 request=36 outcome=58 status=FormattedTransferReceived
SECS-II 0104410750502D44454D4F41065044436F6D6D4105302E312E3001010102410352554E0100
RESULT kind=1 request=37 outcome=60 status=FormattedQueryReceived
SECS-II 410750502D44454D4F
RESULT kind=1 request=38 outcome=62 status=FormattedVerificationRequestReceived
SECS-II 0104410750502D44454D4F41065044436F6D6D4105302E312E3001010102410352554E0100

Now type the equipment commands in the equipment terminal:

verify-report
RESULT kind=2 request=35 outcome=57 status=FormattedVerificationReportAck
negotiate
RESULT kind=2 request=40 outcome=67 status=FormattedVerificationGrantReceived
SECS-II 210100
RESULT kind=2 request=35 outcome=68 status=FormattedVerificationNegotiationAck
store
STORE entries=1
dryrun
DRYRUN outcome=Pass issues=0
execute
DRYRUN outcome=Pass issues=0
APPLICATION DEMO: acknowledge RUN without controlling equipment
EXECUTION state=Completed

The host terminal shows the equipment’s verification report and inquiry:

RESULT kind=1 request=35 outcome=56 status=FormattedVerificationReportReceived
SECS-II 0102410750502D44454D4F01010103B10400000001B10400000001410A64656D6F206973737565
RESULT kind=1 request=40 outcome=66 status=FormattedVerificationInquireReceived
SECS-II B1040000000D
RESULT kind=1 request=35 outcome=56 status=FormattedVerificationReportReceived
SECS-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
examples/tutorial/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; }

Use the two-window setup from the simulator GUI guide.

  1. In the equipment window, open Process Programs → Catalog and click Load example. Equipment program draft gets one entry: PPID DEMO-PP with PPBODY DEMO. Click Use on next start, and then start the equipment session. Once it runs, Actual session store shows the same entry.

    Equipment window with the DEMO-PP program in the draft and in the actual session store of the Process Programs Catalog tabEquipment window with the DEMO-PP program in the draft and in the actual session store of the Process Programs Catalog tab
  2. 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.

  3. In the equipment window, open Traffic, select the received request, and click Respond.

  4. The host’s result table gets a row with Kind Request and State Completed. S7F6 has no reply code, so ACK shows -.

    Process Programs Traffic tab after the host requested DEMO-PP, with the result shown as CompletedProcess Programs Traffic tab after the host requested DEMO-PP, with the result shown as Completed

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.

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.
  • Type quit in both terminals to end the interactive example.
  • PDComm.GemProgramsDemo uses 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.