Skip to content
Start free evaluation

Data collection

GEM equipment exposes its values as variables, and the host reads them directly or receives them in reports. This tutorial runs four .NET examples that cover the PDComm data collection features.

Section Example Messages
Data variables and report bindings PDComm.GemDataDemo S2F33, S2F35, S2F37, S6F11, S1F3
Equipment constants PDComm.GemConstantsDemo S2F13, S2F15, S2F29
Variable and event directories PDComm.GemDirectoriesDemo S1F21, S1F23
Report queries PDComm.GemQueriesDemo S6F15, S6F19, S6F21

The shortest way to read status variables (SVIDs) with S1F3 and S1F11 is in First communication.

  • How status variables, data variables (DVIDs) and equipment constants (ECIDs) differ, and how you register each one in PDComm.
  • How report bindings connect the VIDs in a report to the real source of each value.
  • How to query and change equipment constants, read directories and query reports, and what the acknowledgement codes mean.
  • Complete Set up your environment. This page uses PDCOMM_ROOT, PDCOMM_SDK, PDCOMM_NATIVE and PDCOMM_EXAMPLES ($Root, $Sdk, $Native and $Examples in PowerShell).
  • Install the .NET 10 SDK.
  • Sections 2 and 5 are easier to follow once you know the S2F33, S2F35 and S2F37 flow from Reports and events.
터미널 창
foreach ($demo in 'PDComm.GemDataDemo', 'PDComm.GemConstantsDemo', 'PDComm.GemDirectoriesDemo', 'PDComm.GemQueriesDemo') {
dotnet build "$Examples\dotnet\$demo\$demo.csproj" -c Release "-p:PDCommSdk=$Sdk" --artifacts-path "$Root\build\managed"
}

Each build prints Build succeeded. and writes build/managed/bin/<example>/release/<example>.dll.

You run every example the same way. Start the equipment on 127.0.0.1:0 in a first terminal, then pass the LISTEN address it prints to the host in a second terminal. Ports such as 127.0.0.1:54459 below change on every run.

  • A status variable (SV) is a current value the equipment always knows. The host can read it with S1F3 at any time.
  • A data variable (DV) is only meaningful when a specific event happens, for example the measurement of the wafer that just finished. In GEM the host receives it only through event reports.
  • Report bindings connect each VID the host uses in S2F33 to its real source: a status variable, a data variable or an equipment constant. Without bindings, PDComm treats a VID as the status variable with the same number.

Fixture.StartEquipment installs five data variables and twelve bindings with a GemDataConfiguration:

External VID Source (GemDataSourceKind) Content
101, 102 Status 1, 2 Temperature (F4 21.5), Lot (ASCII LOT-001)
201–204 Data 1–4 Count (U4, cached 10), Sample (ASCII, no cache), Tree (list), Bytes (binary)
211 Data 1 An alias for the same Count as VID 201
301–305 Constant Equipment constants CycleLimit, Label, Gain, Counter, Empty

The host defines RPTID 10 as VIDs [102, 201, 301, 202, 201, 203, 204, 101] and RPTID 20 as VIDs [211, 302, 303, 304, 305], links both to CEID 100 and enables it. The equipment then sends six S6F11 reports with DATAIDs 700 to 705:

DATAID Equipment API Count (VIDs 201, 211) Sample (VID 202) Host ACKC6
700 PublishEventWithDataAsync sample 20 sample SHOT-A 0
701 PublishEventAsync cache 30 cache CACHE-B 7
702 PublishEventWithDataAsync cache 40 sample SHOT-C 0
703 PublishEventWithDataAsync cache 50 sample "" (empty string) 63
704 PublishEventAsync cache 50 cache CACHE-E 0
705 PublishEventWithDataAsync sample 60 sample LAST 0

Finally the host reads SVID 1 with S1F3 and checks the 32.0 the equipment set right after the first report.

터미널 창
# Terminal 1
dotnet "$Root\build\managed\bin\PDComm.GemDataDemo\release\PDComm.GemDataDemo.dll" $Native equipment passive 127.0.0.1:0
# Terminal 2
dotnet "$Root\build\managed\bin\PDComm.GemDataDemo\release\PDComm.GemDataDemo.dll" $Native host active 127.0.0.1:54459

The equipment prints LISTEN 127.0.0.1:<port> and waits. When the host finishes, each program prints one line and exits with code 0:

PASS host active
PASS equipment passive
  • PublishEventWithDataAsync takes GemDataSample values (an internal DV ID and a value). Each value comes from the sample first, then the cache; if neither exists, the whole report fails. Samples apply to this report only and never change the cache.
  • The SDK validates every sample, including samples the report doesn’t use, such as the DV 5 Unused sample in the example.
  • Reporting a data variable whose cache was cleared, without a sample, fails the whole report with InvalidState. A missing cache differs from an empty value.
  • With an empty sample list, PublishEventWithDataAsync behaves like PublishEventAsync and needs no data variable configuration (GemDataConfiguration). As soon as you pass a sample, the worker needs that configuration and fails with InvalidState without it.
  • The event you report must be enabled. When an enabled event has no linked reports, the equipment sends its S6F11 with an empty report list.
  • UpdateDataVariableAsync and ClearDataVariableAsync change the cache. The SDK never samples values on its own; your application reads the device and stores the value.
Full source: PDComm.GemDataDemo/Program.cs
PDComm.GemDataDemo/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.GemDataDemo <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;
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(25));
await using GemWorker worker = role == "host"
? await GemHostWorker.StartAsync(args[3], Options.Transport(mode), Options.Host,
Options.Worker, deadline.Token)
: await Fixture.StartEquipment(args[3], mode, 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 Fixture.RunHost(host, events, deadline.Token);
else
await Fixture.RunEquipment((GemEquipmentWorker)worker, events, deadline.Token);
while (await events.MoveNextAsync()) events.Current.Dispose();
await worker.Completion.WaitAsync(deadline.Token);
Console.WriteLine($"PASS {role} {args[2]}");
return 0;
internal static class Fixture
{
private const int EventCount = 6;
internal static async Task<GemEquipmentWorker> StartEquipment(
string address, HsmsMode mode, CancellationToken token)
{
var owned = new List<SecsItem>();
try
{
SecsItem Own(SecsItem item) { owned.Add(item); return item; }
var status = new GemStatusVariable[]
{
new(1, "Temperature", "C", Own(SecsItem.FromF4([21.5f], Options.Limits))),
new(2, "Lot", "", Own(SecsItem.FromAscii("LOT-001", Options.Limits))),
};
var constants = new GemConstantDefinition[8];
for (int index = 0; index < constants.Length; index++)
{
SecsItem id = Own(ConstantId(index));
SecsItem current = Own(ConstantValue(index));
SecsItem? defaultValue = index < 4 || index == 5
? Own(ConstantMetadata(index, GemConstantField.Default)) : null;
SecsItem? minimum = index is 0 or 2 or 5 or 6
? Own(ConstantMetadata(index, GemConstantField.Minimum)) : null;
SecsItem? maximum = index is 0 or 2 or 5 or 6
? Own(ConstantMetadata(index, GemConstantField.Maximum)) : null;
constants[index] = new GemConstantDefinition(id, ConstantNames[index],
index is 0 or 3 ? "count" : "", current, defaultValue, minimum, maximum,
index is 0 or 2 or 3 ? 1u : index == 5 ? 2u : 0u,
index == 1 ? 16u : index is 4 or 6 ? 4u : index == 7 ? 8u :
index == 5 ? 2u : 1u);
}
var dataDefinitions = new GemDataDefinition[5];
for (int index = 0; index < dataDefinitions.Length; index++)
{
SecsItem? cached = InitialData(index);
if (cached is not null) Own(cached);
dataDefinitions[index] = new GemDataDefinition((uint)index + 1,
DataNames[index], index == 0 ? "count" : "", DataFormats[index],
index == 0 ? 1u : 0u, index == 0 ? 1u : index == 2 ? 4u : 32u, cached);
}
var bindings = new GemReportBinding[12];
uint[] vids = [101, 102, 201, 202, 203, 204, 301, 302, 303, 304, 305, 211];
int[] constantIndices = [0, 1, 2, 3, 7];
for (int index = 0; index < bindings.Length; index++)
{
if (index < 2)
bindings[index] = new GemReportBinding(vids[index],
GemDataSourceKind.Status, (uint)index + 1, null);
else if (index < 6 || index == 11)
bindings[index] = new GemReportBinding(vids[index],
GemDataSourceKind.Data, index == 11 ? 1u : (uint)index - 1, null);
else
bindings[index] = new GemReportBinding(vids[index],
GemDataSourceKind.Constant, 0, Own(ConstantId(constantIndices[index - 6])));
}
var data = new GemDataConfiguration(Options.Data, dataDefinitions,
Options.Bindings, bindings);
return await GemEquipmentWorker.StartAsync(address, Options.Transport(mode),
Options.Equipment, Options.Worker, status,
new GemReportConfiguration(Options.Reports, new uint[] { 100 }),
null, null, null, data,
new GemConstantConfiguration(Options.Constants, constants), token);
}
finally
{
foreach (SecsItem item in owned) item.Dispose();
}
}
internal static async Task RunHost(GemHostWorker host,
IAsyncEnumerator<GemEvent> events, CancellationToken token)
{
await WaitSelected(host, token);
HsmsToken pending = await host.BeginCommunicationAsync(token);
int step = -1;
int received = 0;
while (step < 3)
{
if (!await events.MoveNextAsync())
throw new InvalidOperationException("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 && info.Token == pending)
{
if (step < 0)
{
if (info.Outcome != GemOutcome.CommunicationEstablished)
throw new InvalidOperationException("Communication establishment failed.");
step = 0;
pending = await SendHostStep(host, step, token);
}
else if (step < 3)
{
CheckHostResponse(result, step);
if (step < 2)
pending = await SendHostStep(host, ++step, token);
}
}
else if (info.Outcome == GemOutcome.EventReceived)
{
CheckEvent(result, received);
byte acknowledgement = Acknowledgement(received);
Task response = host.AcknowledgeEventAsync(result, acknowledgement, token);
if (received == 0)
{
SecsItem retained = result.GetEventValue(0, 0);
current.Dispose();
disposed = true;
using (retained)
using (SecsItem expected = Expected(0, 0, 0))
Equal(retained, expected);
}
await response;
received++;
if (received == EventCount)
{
step = 3;
pending = await SendHostStep(host, step, token);
}
}
}
finally
{
if (!disposed) current.Dispose();
}
}
while (true)
{
if (!await events.MoveNextAsync())
throw new InvalidOperationException("Host status response was not received.");
using GemEvent current = events.Current;
GemResult? result = current.Result;
if (result is null || result.GetInfo().Token != pending) continue;
CheckHostResponse(result, 3);
break;
}
if (received != EventCount) throw new InvalidOperationException("Event count mismatch.");
await host.StopAsync(token);
}
internal static async Task RunEquipment(GemEquipmentWorker equipment,
IAsyncEnumerator<GemEvent> events, CancellationToken token)
{
int configured = 0;
int published = 0;
int acknowledged = 0;
HsmsToken publication = default;
while (acknowledged < EventCount)
{
if (!await events.MoveNextAsync())
throw new InvalidOperationException("Equipment event stream ended early.");
using GemEvent current = events.Current;
GemResult? result = current.Result;
if (result is null) continue;
GemResultInfo info = result.GetInfo();
if (info.Kind == GemResultKind.Primary && info.Outcome is
>= GemOutcome.DefineReportsAck and <= GemOutcome.EnableEventsAck)
{
configured++;
if (info.Acknowledgement != 0)
throw new InvalidOperationException("Host configuration was rejected.");
if (info.Outcome == GemOutcome.EnableEventsAck)
publication = await Publish(equipment, published++, token);
}
else if (info.Kind == GemResultKind.Response && info.Outcome == GemOutcome.EventAck)
{
if (info.Token != publication || info.Acknowledgement != Acknowledgement(acknowledged))
throw new InvalidOperationException("Event acknowledgement mismatch.");
acknowledged++;
if (published < EventCount)
publication = await Publish(equipment, published++, token);
}
}
if (configured != 3 || published != EventCount)
throw new InvalidOperationException("Equipment scenario count mismatch.");
while ((await equipment.GetTransportStateAsync(token)).State == HsmsConnectionState.Selected)
await Task.Delay(1, token);
await equipment.StopAsync(token);
}
private static async Task<HsmsToken> SendHostStep(
GemHostWorker host, int step, CancellationToken token) => step switch
{
0 => await host.DefineReportsAsync(1,
new GemReportSpec[]
{
new(10, new uint[] { 102, 201, 301, 202, 201, 203, 204, 101 }),
new(20, new uint[] { 211, 302, 303, 304, 305 }),
}, token),
1 => await host.LinkReportsAsync(2,
new GemLinkSpec[] { new(100, new uint[] { 10, 20 }) }, token),
2 => await host.EnableEventsAsync(true, new uint[] { 100 }, token),
3 => await host.RequestStatusValuesAsync(new uint[] { 1 }, token),
_ => throw new ArgumentOutOfRangeException(nameof(step)),
};
private static void CheckHostResponse(GemResult result, int step)
{
GemResultInfo info = result.GetInfo();
GemRequestKind[] kinds = [GemRequestKind.DefineReports, GemRequestKind.LinkReports,
GemRequestKind.EnableEvents, GemRequestKind.StatusValues];
GemOutcome[] outcomes = [GemOutcome.DefineReportsAck, GemOutcome.LinkReportsAck,
GemOutcome.EnableEventsAck, GemOutcome.ValuesReceived];
if (info.Kind != GemResultKind.Response || info.RequestKind != kinds[step] ||
info.Outcome != outcomes[step])
throw new InvalidOperationException("Host response mismatch.");
if (step < 3)
{
if (info.Acknowledgement != 0)
throw new InvalidOperationException("Configuration acknowledgement mismatch.");
}
else
{
using GemStatusValue value = result.GetStatusValue(0);
using SecsItem expected = SecsItem.FromF4([32f], Options.Limits);
Equal(value.Value, expected);
}
}
private static async Task<HsmsToken> Publish(
GemEquipmentWorker equipment, int ordinal, CancellationToken token)
{
var owned = new List<SecsItem>();
try
{
var samples = new List<GemDataSample>();
void Add(uint id, SecsItem value) { owned.Add(value); samples.Add(new GemDataSample(id, value)); }
if (ordinal is 0 or 5) Add(1, SecsItem.FromU4([ordinal == 0 ? 20u : 60u], Options.Limits));
if (ordinal is 0 or 2 or 3 or 5)
Add(2, SecsItem.FromAscii(ordinal == 0 ? "SHOT-A" : ordinal == 2 ? "SHOT-C" :
ordinal == 3 ? "" : "LAST", Options.Limits));
if (ordinal is 2 or 5) Add(3, ordinal == 2 ? DataTree(true) : List());
if (ordinal is 3 or 5) Add(4, SecsItem.FromBinary(ordinal == 3 ? [9, 8] : [], Options.Limits));
if (ordinal is 0 or 5) Add(5, SecsItem.FromAscii("UNUSED-SAMPLE", Options.Limits));
HsmsToken result = ordinal is 1 or 4
? await equipment.PublishEventAsync((uint)(700 + ordinal), 100, token)
: await equipment.PublishEventWithDataAsync((uint)(700 + ordinal), 100,
samples.ToArray(), token);
await AfterPublish(equipment, ordinal, token);
return result;
}
finally
{
foreach (SecsItem item in owned) item.Dispose();
}
}
private static async Task AfterPublish(
GemEquipmentWorker equipment, int ordinal, CancellationToken token)
{
async Task Update(uint id, SecsItem value)
{
using (value) await equipment.UpdateDataVariableAsync(id, value, token);
}
switch (ordinal)
{
case 0:
await Update(1, SecsItem.FromU4([30], Options.Limits));
await Update(2, SecsItem.FromAscii("CACHE-B", Options.Limits));
using (SecsItem temperature = SecsItem.FromF4([32f], Options.Limits))
await equipment.UpdateStatusAsync(1, temperature, token);
break;
case 1:
await equipment.ClearDataVariableAsync(1, token);
await equipment.ClearDataVariableAsync(2, token);
await Update(1, SecsItem.FromU4([40], Options.Limits));
break;
case 2:
await Update(1, SecsItem.FromU4([50], Options.Limits));
await Update(2, SecsItem.FromAscii("CACHE-D", Options.Limits));
break;
case 3:
await Update(2, SecsItem.FromAscii("CACHE-E", Options.Limits));
break;
case 4:
await equipment.ClearDataVariableAsync(2, token);
break;
case 5:
for (uint id = 1; id <= 4; id++)
await equipment.ClearDataVariableAsync(id, token);
break;
}
}
private static void CheckEvent(GemResult result, int ordinal)
{
GemEventInfo info = result.GetEventInfo();
if (info.DataId != (uint)(700 + ordinal) || info.EventId != 100 || info.ReportCount != 2)
throw new InvalidOperationException("Event metadata mismatch.");
for (nuint reportIndex = 0; reportIndex < 2; reportIndex++)
{
GemEventReport report = result.GetEventReport(reportIndex);
nuint expectedCount = reportIndex == 0 ? 8u : 5u;
if (report.ReportId != (reportIndex == 0 ? 10u : 20u) ||
report.ValueCount != expectedCount)
throw new InvalidOperationException("Event report mismatch.");
for (nuint valueIndex = 0; valueIndex < expectedCount; valueIndex++)
{
using SecsItem actual = result.GetEventValue(reportIndex, valueIndex);
using SecsItem expected = Expected(ordinal, (int)reportIndex, (int)valueIndex);
Equal(actual, expected);
}
}
}
private static SecsItem Expected(int ordinal, int report, int value)
{
if (report == 1 && value != 0)
return ConstantValue(new[] { 1, 2, 3, 7 }[value - 1]);
if (report == 1 || value is 1 or 4)
return SecsItem.FromU4([new uint[] { 20, 30, 40, 50, 50, 60 }[ordinal]], Options.Limits);
return value switch
{
0 => SecsItem.FromAscii("LOT-001", Options.Limits),
2 => ConstantValue(0),
3 => SecsItem.FromAscii(new[] { "SHOT-A", "CACHE-B", "SHOT-C", "", "CACHE-E", "LAST" }[ordinal], Options.Limits),
5 => ordinal == 5 ? List() : DataTree(ordinal == 2),
6 => SecsItem.FromBinary(ordinal == 3 ? [9, 8] : [], Options.Limits),
7 => SecsItem.FromF4([ordinal == 0 ? 21.5f : 32f], Options.Limits),
_ => throw new ArgumentOutOfRangeException(nameof(value)),
};
}
private static byte Acknowledgement(int ordinal) =>
new byte[] { 0, 7, 0, 63, 0, 0 }[ordinal];
private static async Task WaitSelected(GemWorker worker, CancellationToken token)
{
while ((await worker.GetTransportStateAsync(token)).State != HsmsConnectionState.Selected)
await Task.Delay(1, token);
}
private static readonly string[] ConstantNames =
["CycleLimit", "Label", "Gain", "Counter", "Tree", "Flags", "Bytes", "Empty"];
private static readonly string[] DataNames = ["Count", "Sample", "Tree", "Bytes", "Unused"];
private static readonly SecsFormat[] DataFormats =
[SecsFormat.U4, SecsFormat.Ascii, SecsFormat.List, SecsFormat.Binary, SecsFormat.Ascii];
private static SecsItem? InitialData(int index) => index switch
{
0 => SecsItem.FromU4([10], Options.Limits),
1 => null,
2 => DataTree(false),
3 => SecsItem.FromBinary([], Options.Limits),
4 => SecsItem.FromAscii("UNUSED", Options.Limits),
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
private static SecsItem DataTree(bool sampled) => List(
SecsItem.FromU4([sampled ? 8u : 7u], Options.Limits),
SecsItem.FromAscii(sampled ? "SAMPLED" : "NESTED", Options.Limits));
private static SecsItem ConstantId(int index) => index switch
{
0 => SecsItem.FromU1([1], Options.Limits),
1 => SecsItem.FromAscii("LABEL", Options.Limits),
2 => SecsItem.FromI8([-1], Options.Limits),
3 => SecsItem.FromU8([ulong.MaxValue], Options.Limits),
4 => SecsItem.FromAscii("TREE", Options.Limits),
5 => SecsItem.FromAscii("FLAGS", Options.Limits),
6 => SecsItem.FromAscii("BYTES", Options.Limits),
7 => SecsItem.FromAscii("", Options.Limits),
_ => throw new ArgumentOutOfRangeException(nameof(index)),
};
private static SecsItem ConstantValue(int index) => index switch
{
0 => SecsItem.FromU4([10], Options.Limits),
1 => SecsItem.FromAscii("IDLE", Options.Limits),
2 => SecsItem.FromF8([1.5], Options.Limits),
3 => SecsItem.FromU8([ulong.MaxValue], Options.Limits),
4 => List(SecsItem.FromU4([7], Options.Limits), SecsItem.FromAscii("OK", Options.Limits)),
5 => SecsItem.FromBoolean([false, true], Options.Limits),
6 => SecsItem.FromBinary([1, 2], Options.Limits),
7 => SecsItem.FromAscii("", Options.Limits),
_ => List(),
};
private static SecsItem ConstantMetadata(int index, GemConstantField field)
{
if (field == GemConstantField.Default) return ConstantValue(index);
if (index == 0) return SecsItem.FromU4([field == GemConstantField.Minimum ? 1u : 100u], Options.Limits);
if (index == 2) return SecsItem.FromF8([field == GemConstantField.Minimum ? -5d : 5d], Options.Limits);
if (index == 5) return SecsItem.FromBoolean([field != GemConstantField.Minimum], Options.Limits);
if (index == 6) return SecsItem.FromBinary([field == GemConstantField.Minimum ? (byte)0 : (byte)255], Options.Limits);
return SecsItem.FromAscii("", Options.Limits);
}
private static SecsItem List(params SecsItem[] children)
{
try { return SecsItem.CreateList(children, Options.Limits); }
finally { foreach (SecsItem child in children) child.Dispose(); }
}
private static void Equal(SecsItem actual, SecsItem expected)
{
if (!actual.Encode().AsSpan().SequenceEqual(expected.Encode()))
throw new InvalidOperationException("SECS-II value mismatch.");
}
}
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 GemReportOptions Reports = new()
{
MaxEvents = 4, MaxReports = 4, MaxVariablesPerReport = 8,
MaxReportsPerEvent = 4, MaxTotalVariables = 16, MaxTotalLinks = 16,
};
internal static readonly GemConstantOptions Constants = new()
{
MaxConstants = 8, MaxMetadataBytes = 1024, MaxValueBytes = 4096,
MaxQueryIds = 32, MaxChangesPerRequest = 16, MaxIdentifierBytes = 32,
ItemLimits = Limits,
};
internal static readonly GemDataOptions Data = new()
{
MaxVariables = 8, MaxMetadataBytes = 512, MaxValueBytes = 4096,
MaxSamplesPerEvent = 5, MaxSampleBytes = 4096, ItemLimits = Limits,
};
internal static readonly GemBindingOptions Bindings = new()
{
MaxBindings = 16, MaxIdentifierBytes = 32, MaxTotalIdentifierBytes = 64,
};
internal static readonly HsmsWorkerOptions Worker = new()
{
CommandCapacity = 64, EventCapacity = 64, MaxCommandBytes = 131072,
MaxEventBytes = 131072, PollIntervalMilliseconds = 1, ShutdownTimeoutMilliseconds = 2000,
};
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,
};
}

In C, use pdcomm_gem_equipment_configure_data_variables, pdcomm_gem_equipment_configure_report_bindings, pdcomm_gem_equipment_publish_event_with_data, pdcomm_gem_equipment_update_data_variable and pdcomm_gem_equipment_clear_data_variable.

An equipment constant (EC) is an equipment setting the host may change. The host reads metadata such as name, range, default and units with S2F29, reads current values with S2F13, and changes values with S2F15. The equipment reports the result as an EAC in S2F16:

EAC Meaning
0 Accepted and applied.
1 At least one ECID doesn’t exist.
2 The equipment can’t do it now (busy).
3 At least one value is out of range.

StartEquipment puts eight GemConstantDefinition values into a GemConstantConfiguration. An ECID can be an integer item (U1(1), I8(-1), U8) or an ASCII item ("LABEL", "TREE", an empty string). For example, CycleLimit (ECID U1(1)) holds U4 10 with minimum 1, maximum 100 and units count. Label (ECID "LABEL") is ASCII with at most 16 characters.

The host runs 24 steps. The important ones:

  • An empty list in S2F29 and S2F13 returns all eight constants. S2F14 carries no ECIDs, so the entries of an all-values result have no ECID (null). When you request specific IDs, the SDK pairs each value with the ID you asked for.
  • If you mix an unknown ECID into a query, its position holds metadata with empty fields and a zero-length list as the value. Order and duplicates follow your request.
  • S2F15 with CycleLimit=20 and Label=RUN returns EAC 0 and applies both.
  • An unknown ECID (EAC 1), 101 above the maximum, a value in another format (F8), NaN and a string longer than the maximum (all EAC 3) are rejected.
  • When a request passes validation and the equipment application picks CannotPerform, the EAC is 2.
터미널 창
# Terminal 1
dotnet "$Root\build\managed\bin\PDComm.GemConstantsDemo\release\PDComm.GemConstantsDemo.dll" $Native equipment passive 127.0.0.1:0
# Terminal 2
dotnet "$Root\build\managed\bin\PDComm.GemConstantsDemo\release\PDComm.GemConstantsDemo.dll" $Native host active 127.0.0.1:54463

The programs print PASS host active and PASS equipment passive and exit with code 0.

  • Host: RequestConstantMetadataAsync (S2F29), RequestConstantValuesAsync (S2F13) and ChangeConstantsAsync (S2F15).
  • Equipment: S2F15 arrives as a result whose Outcome is ConstantChangesReceived, with no automatic reply. Your application answers with RespondConstantsAsync and a GemConstantDecision (Apply, CannotPerform or OutOfRange).
  • Before it replies, the SDK checks that every ECID exists and that the format, range and count fit. If that check fails, the SDK sends EAC 1 or 3 and leaves the values unchanged, even when your application chose Apply. Read Applied in the reply to learn whether the values changed.
Full source: PDComm.GemConstantsDemo/Program.cs
PDComm.GemConstantsDemo/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.GemConstantsDemo <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;
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(25));
await using GemWorker worker = role == "host"
? await GemHostWorker.StartAsync(args[3], Options.Transport(mode), Options.Host, Options.Worker, deadline.Token)
: await StartEquipment(args[3], mode, 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, deadline.Token); else await RunEquipment((GemEquipmentWorker)worker, events, deadline.Token);
while (await events.MoveNextAsync()) events.Current.Dispose();
await worker.Completion.WaitAsync(deadline.Token);
Console.WriteLine($"PASS {role} {args[2]}");
return 0;
static async Task<GemEquipmentWorker> StartEquipment(string address, HsmsMode mode, CancellationToken token)
{
var owned = new List<SecsItem>();
try
{
SecsItem Item(int index, int phase = 0) { SecsItem item = Value(index, phase); owned.Add(item); return item; }
var definitions = new GemConstantDefinition[8];
for (int i = 0; i < 8; i++)
{
SecsItem id = Id(i); owned.Add(id);
SecsItem current = Item(i);
SecsItem? def = i < 4 || i == 5 ? Metadata(i, GemConstantField.Default) : null; if (def is not null) owned.Add(def);
SecsItem? min = i is 0 or 2 or 5 or 6 ? Metadata(i, GemConstantField.Minimum) : null; if (min is not null) owned.Add(min);
SecsItem? max = i is 0 or 2 or 5 or 6 ? Metadata(i, GemConstantField.Maximum) : null; if (max is not null) owned.Add(max);
definitions[i] = new(id, Names()[i], i is 0 or 3 ? "count" : "", current, def, min, max,
i is 0 or 2 or 3 ? 1u : i == 5 ? 2u : 0u, i == 1 ? 16u : i is 4 or 6 ? 4u : i == 7 ? 8u : i == 5 ? 2u : 1u);
}
return await GemEquipmentWorker.StartAsync(address, Options.Transport(mode), Options.Equipment, Options.Worker,
null, null, null, null, null, new GemConstantConfiguration(Options.Constants, definitions), token);
}
finally { foreach (SecsItem item in owned) item.Dispose(); }
}
static async Task RunHost(GemHostWorker host, IAsyncEnumerator<GemEvent> events, CancellationToken token)
{
bool began = false, established = false; int next = 0, completed = 0; HsmsToken pending = default;
while (completed < 24)
{
if (!began && (await host.GetTransportStateAsync(token)).State == HsmsConnectionState.Selected) { pending = await host.BeginCommunicationAsync(token); began = true; }
if (!await events.MoveNextAsync()) throw new InvalidOperationException("Host event stream ended early.");
using GemEvent current = events.Current; GemResult? result = current.Result; if (result is null) continue;
GemResultInfo info = result.GetInfo();
if (info.Kind != GemResultKind.Response) continue;
if (info.Token != pending) throw new InvalidOperationException("Token mismatch.");
if (!established) { if (info.Outcome != GemOutcome.CommunicationEstablished) throw new InvalidOperationException("Communication failed."); established = true; pending = await Send(host, next++, token); continue; }
Check(result, completed++);
if (next < 24) pending = await Send(host, next++, token);
}
await host.StopAsync(token);
}
static async Task<HsmsToken> Send(GemHostWorker host, int step, CancellationToken token)
{
if (IsQuery(step))
{
int[] indices = Query(step); var ids = new SecsItem[indices.Length];
try { for (int i = 0; i < ids.Length; i++) ids[i] = step == 3 && i == 3 ? SecsItem.FromI8([1], Options.Limits) : Id(indices[i]);
return Kind(step) == GemRequestKind.ConstantMetadata ? await host.RequestConstantMetadataAsync(ids, token) : await host.RequestConstantValuesAsync(ids, token); }
finally { foreach (SecsItem item in ids) item?.Dispose(); }
}
if (step == 11) return await host.ChangeConstantsAsync(Array.Empty<GemConstantChange>(), token);
(int id, SecsItem value)[] changes = Changes(step); var items = new List<SecsItem>();
try { var request = new GemConstantChange[changes.Length]; for (int i = 0; i < changes.Length; i++) { SecsItem id = step == 7 && i == 1 ? SecsItem.FromI8([1], Options.Limits) : Id(changes[i].id); items.Add(id); items.Add(changes[i].value); request[i] = new(id, changes[i].value); } return await host.ChangeConstantsAsync(request, token); }
finally { foreach (SecsItem item in items) item.Dispose(); }
}
static async Task RunEquipment(GemEquipmentWorker equipment, IAsyncEnumerator<GemEvent> events, CancellationToken token)
{
int primaries = 0, changes = 0, applied = 0;
while (primaries < 25 || changes < 16)
{
if (!await events.MoveNextAsync()) throw new InvalidOperationException("Equipment 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.Primary) primaries++;
if (info.Outcome != GemOutcome.ConstantChangesReceived) continue;
int step = ChangeSteps()[changes++]; GemConstantChangeInfo detail = result.GetConstantChangeInfo();
if (!detail.IsRequest || detail.Responded || detail.ChangeCount != (nuint)(step == 11 ? 0 : step is 4 or 6 or 7 or 17 ? 2 : 1)) throw new InvalidOperationException("Receipt metadata mismatch.");
for (nuint i = 0; i < detail.ChangeCount; i++) using (GemConstantChangeEntry entry = result.GetConstantChange(i)) { if (entry.Id is null || entry.Value is null) throw new InvalidOperationException("Receipt fields missing."); }
GemConstantDecision decision = (GemConstantDecision)Decision(step);
Task<GemConstantReply> response = equipment.RespondConstantsAsync(result, decision, token); current.Dispose(); disposed = true;
GemConstantReply reply = await response; if (reply.Acknowledgement != Ack(step) || reply.Applied != (step is 4 or 16 or 17 or 21)) throw new InvalidOperationException("Receipt response mismatch.");
if (reply.Applied) applied++;
}
finally { if (!disposed) current.Dispose(); }
}
if (primaries != 25 || applied != 4) throw new InvalidOperationException("Equipment counts mismatch.");
while ((await equipment.GetTransportStateAsync(token)).State == HsmsConnectionState.Selected)
await Task.Delay(1, token);
await equipment.StopAsync(token);
}
static void Check(GemResult result, int step)
{
GemResultInfo info = result.GetInfo();
if (!IsQuery(step)) { if (info.Outcome != GemOutcome.ConstantChangeAck || info.Acknowledgement != Ack(step)) throw new InvalidOperationException("Change ACK mismatch."); if (result.GetConstantChangeInfo().IsRequest) throw new InvalidOperationException("Change result direction mismatch."); return; }
int[] selected = Query(step); int count = selected.Length == 0 ? 8 : selected.Length; int phase = step <= 3 ? 0 : step == 5 ? 1 : step == 23 ? 3 : 2;
if ((int)info.EntryCount != count) throw new InvalidOperationException("Query count mismatch.");
for (nuint i = 0; i < (nuint)count; i++) { int index = selected.Length == 0 ? (int)i : selected[i]; if (Kind(step) == GemRequestKind.ConstantMetadata) { for (GemConstantField field = GemConstantField.Id; field <= GemConstantField.Units; field++) { using SecsItem? actual = result.GetConstantMetadata(i, field); using SecsItem expected = Metadata(index, field); Equal(actual, expected); } } else { using GemConstantValue actual = result.GetConstantValue(i); using SecsItem expected = index >= 8 ? List() : Value(index, phase); Equal(actual.Value, expected); if (selected.Length == 0) { if (actual.Id is not null) throw new InvalidOperationException("All-values ID must be null."); } else { using SecsItem expectedId = step == 3 && i == 3 ? SecsItem.FromI8([1], Options.Limits) : Id(index); Equal(actual.Id, expectedId); } } }
}
static bool IsQuery(int step) => step <= 3 || step is 5 or 18 or 22 or 23;
static GemRequestKind Kind(int step) => step is 0 or 2 or 22 ? GemRequestKind.ConstantMetadata : IsQuery(step) ? GemRequestKind.ConstantValues : GemRequestKind.ChangeConstants;
static int[] Query(int step) => step is 0 or 1 or 23 ? [] : step == 2 ? [1, 8, 0, 2, 3, 7, 1] : step == 3 ? [4, 8, 0, 0] : step == 5 ? [0, 1] : step == 18 ? [4, 2, 3] : [4, 1, 0];
static int[] ChangeSteps() => [4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20, 21];
static byte Ack(int step) => step is 6 or 19 ? (byte)1 : step is 7 or 8 or 9 or 10 or 14 or 20 ? (byte)3 : step == 12 ? (byte)2 : step == 13 ? (byte)4 : step == 15 ? (byte)63 : (byte)0;
static byte Decision(int step) => step == 12 ? (byte)2 : step == 13 ? (byte)4 : step == 14 ? (byte)3 : step == 15 ? (byte)63 : (byte)0;
static (int, SecsItem)[] Changes(int step) => step switch { 4 => [(0, Value(0, 1)), (1, Value(1, 1))], 6 => [(0, SecsItem.FromU4([101], Options.Limits)), (8, SecsItem.FromAscii("UNKNOWN", Options.Limits))], 7 => [(0, SecsItem.FromU4([30], Options.Limits)), (0, SecsItem.FromU4([40], Options.Limits))], 8 => [(0, SecsItem.FromU4([101], Options.Limits))], 9 => [(0, SecsItem.FromF8([20], Options.Limits))], 10 => [(2, SecsItem.FromF8([double.NaN], Options.Limits))], 12 => [(1, SecsItem.FromAscii("BUSY", Options.Limits))], 13 => [(1, SecsItem.FromAscii("SPEC", Options.Limits))], 14 => [(1, SecsItem.FromAscii("REJECT", Options.Limits))], 15 => [(1, SecsItem.FromAscii("MAX", Options.Limits))], 16 => [(4, Value(4, 2))], 17 => [(2, Value(2, 2)), (3, Value(3, 2))], 19 => [(9, SecsItem.FromAscii("x", Options.Limits))], 20 => [(1, SecsItem.FromAscii("01234567890123456", Options.Limits))], 21 => [(7, Value(7, 3))], _ => throw new InvalidOperationException("Invalid change step.") };
static string[] Names() => ["CycleLimit", "Label", "Gain", "Counter", "Tree", "Flags", "Bytes", "Empty"];
static SecsItem Id(int index) => index switch { 0 => SecsItem.FromU1([1], Options.Limits), 1 => SecsItem.FromAscii("LABEL", Options.Limits), 2 => SecsItem.FromI8([-1], Options.Limits), 3 => SecsItem.FromU8([ulong.MaxValue], Options.Limits), 4 => SecsItem.FromAscii("TREE", Options.Limits), 5 => SecsItem.FromAscii("FLAGS", Options.Limits), 6 => SecsItem.FromAscii("BYTES", Options.Limits), 7 => SecsItem.FromAscii("", Options.Limits), 8 => SecsItem.FromAscii("UNKNOWN", Options.Limits), 9 => SecsItem.FromAscii("label", Options.Limits), _ => throw new ArgumentOutOfRangeException(nameof(index)) };
static SecsItem Value(int index, int phase = 0) => index switch { 0 => SecsItem.FromU4([phase == 0 ? 10u : 20u], Options.Limits), 1 => SecsItem.FromAscii(phase == 0 ? "IDLE" : "RUN", Options.Limits), 2 => SecsItem.FromF8([phase < 2 ? 1.5 : 2.5], Options.Limits), 3 => SecsItem.FromU8([phase < 2 ? ulong.MaxValue : ulong.MaxValue - 1], Options.Limits), 4 => Tree(phase), 5 => SecsItem.FromBoolean([false, true], Options.Limits), 6 => SecsItem.FromBinary([1, 2], Options.Limits), 7 => SecsItem.FromAscii(phase < 3 ? "" : "EMPTY", Options.Limits), _ => throw new ArgumentOutOfRangeException(nameof(index)) };
static SecsItem Tree(int phase) => phase < 2 ? List(SecsItem.FromU4([7], Options.Limits), SecsItem.FromAscii("OK", Options.Limits)) : List(SecsItem.FromBoolean([true], Options.Limits), SecsItem.FromBinary([1, 2], Options.Limits), List(SecsItem.FromAscii("NESTED", Options.Limits)));
static SecsItem Metadata(int index, GemConstantField field) { if (field == GemConstantField.Id) return index == 0 ? SecsItem.FromU8([1], Options.Limits) : Id(index); if (index >= 8) return SecsItem.FromAscii("", Options.Limits); if (field == GemConstantField.Name) return SecsItem.FromAscii(Names()[index], Options.Limits); if (field == GemConstantField.Units) return SecsItem.FromAscii(index is 0 or 3 ? "count" : "", Options.Limits); if (field == GemConstantField.Default) return index < 4 || index == 5 ? Value(index) : SecsItem.FromAscii("", Options.Limits); if (index == 0) return SecsItem.FromU4([field == GemConstantField.Minimum ? 1u : 100u], Options.Limits); if (index == 2) return SecsItem.FromF8([field == GemConstantField.Minimum ? -5d : 5d], Options.Limits); if (index == 5) return SecsItem.FromBoolean([field != GemConstantField.Minimum], Options.Limits); if (index == 6) return SecsItem.FromBinary([field == GemConstantField.Minimum ? (byte)0 : (byte)255], Options.Limits); return SecsItem.FromAscii("", Options.Limits); }
static SecsItem List(params SecsItem[] children) { try { return SecsItem.CreateList(children, Options.Limits); } finally { foreach (SecsItem item in children) item.Dispose(); } }
static void Equal(SecsItem? actual, SecsItem expected) { if (actual is null || !actual.Encode().AsSpan().SequenceEqual(expected.Encode())) throw new InvalidOperationException("Fixture item mismatch."); }
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 GemConstantOptions Constants = new() { MaxConstants = 8, MaxMetadataBytes = 1024, MaxValueBytes = 4096, MaxQueryIds = 32, MaxChangesPerRequest = 16, MaxIdentifierBytes = 32, ItemLimits = Limits }; internal static readonly HsmsWorkerOptions Worker = new() { CommandCapacity = 64, EventCapacity = 64, MaxCommandBytes = 131072, MaxEventBytes = 131072, PollIntervalMilliseconds = 1, ShutdownTimeoutMilliseconds = 2000 }; 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 }; }

In C, use pdcomm_gem_host_constant_request, pdcomm_gem_host_change_constants and pdcomm_gem_equipment_respond_constants.

A directory returns labels, not values:

  • S1F21/S1F22 (data variable namelist) returns the name and units of each VID.
  • S1F23/S1F24 (collection event namelist) returns the name of each CEID and the VIDs you can report with it.

The equipment installs four data variables (Count, Sample, Tree, Bytes), bindings for VIDs 201–204 plus the alias 211, and one static event directory entry defined with a GemEventDirectoryDefinition: CEID 100, name DataSnapshot, VIDs [201, 202, 211].

Host request Equipment reply
S1F21 with an empty list VIDs 201, 202, 203, 204 and 211 with their names and units
S1F21 [211, 999, 101, 201, 301, 202, 211] Seven entries in the same order. VIDs 999, 101 and 301 aren’t bound to a data variable, so their name and units are empty strings.
S1F23 with an empty list CEID 100 DataSnapshot with VIDs [201, 202, 211]
S1F23 [999, 100, 100] Three entries in the same order. CEID 999 has an empty name and VID list.

After it answers the first S1F21, the equipment clears the Count cache. Directories don’t depend on the cache, so later replies stay the same.

터미널 창
# Terminal 1
dotnet "$Root\build\managed\bin\PDComm.GemDirectoriesDemo\release\PDComm.GemDirectoriesDemo.dll" $Native equipment passive 127.0.0.1:0
# Terminal 2
dotnet "$Root\build\managed\bin\PDComm.GemDirectoriesDemo\release\PDComm.GemDirectoriesDemo.dll" $Native host active 127.0.0.1:54468

The programs print PASS host active and PASS equipment passive.

The host passes a GemDirectoryKind (Data or Event) to RequestDirectoryAsync. The equipment replies automatically, so it needs no handling code. You install the event directory before the start with a GemDirectoryConfiguration.

Full source: PDComm.GemDirectoriesDemo/Program.cs
PDComm.GemDirectoriesDemo/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.GemDirectoriesDemo <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;
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(25));
await using GemWorker worker = role == "host"
? await GemHostWorker.StartAsync(args[3], Setup.Transport(mode), Setup.Host, Setup.Worker, cancellation.Token)
: await Setup.StartEquipment(args[3], mode, cancellation.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(cancellation.Token).GetAsyncEnumerator();
if (worker is GemHostWorker host) await RunHost(host, events, cancellation.Token);
else await RunEquipment((GemEquipmentWorker)worker, events, cancellation.Token);
while (await events.MoveNextAsync()) events.Current.Dispose();
await worker.Completion.WaitAsync(cancellation.Token);
Console.WriteLine($"PASS {role} {args[2]}");
return 0;
static async Task RunHost(GemHostWorker host, IAsyncEnumerator<GemEvent> events, CancellationToken token)
{
await WaitSelected(host, token);
HsmsToken pending = await host.BeginCommunicationAsync(token);
int step = -1;
while (step < 10)
{
if (!await events.MoveNextAsync()) throw new InvalidOperationException("Host event stream ended early.");
using GemEvent current = events.Current;
GemResult? result = current.Result;
if (result is null) continue;
GemResultInfo info = result.GetInfo();
if (info.Kind != GemResultKind.Response || info.Token != pending) continue;
if (step < 0)
{
if (info.Outcome != GemOutcome.CommunicationEstablished)
throw new InvalidOperationException("Communication establishment failed.");
step = 0;
pending = await Send(host, step, token);
continue;
}
Verify(result, step);
if (++step < 10) pending = await Send(host, step, token);
}
await host.StopAsync(token);
}
static async Task RunEquipment(GemEquipmentWorker equipment, IAsyncEnumerator<GemEvent> events,
CancellationToken token)
{
bool cleared = false;
await WaitSelected(equipment, token);
while (await events.MoveNextAsync())
{
using GemEvent current = events.Current;
GemResult? result = current.Result;
if (result is not null && result.GetInfo().Outcome == GemOutcome.DataDirectoryReplied && !cleared)
{
await equipment.ClearDataVariableAsync(1, token);
cleared = true;
}
if (result is not null && result.GetInfo().Outcome == GemOutcome.StatusReplied)
{
await equipment.StopAsync(token);
break;
}
}
if (!cleared) throw new InvalidOperationException("The data directory reply was not observed.");
}
static async Task<HsmsToken> Send(GemHostWorker host, int step, CancellationToken token) => step switch
{
0 or 7 => await host.RequestDirectoryAsync(GemDirectoryKind.Data, Array.Empty<uint>(), token),
1 => await host.RequestDirectoryAsync(GemDirectoryKind.Data, new uint[] { 211, 999, 101, 201, 301, 202, 211 }, token),
2 or 8 => await host.RequestDirectoryAsync(GemDirectoryKind.Event, Array.Empty<uint>(), token),
3 => await host.RequestDirectoryAsync(GemDirectoryKind.Event, new uint[] { 999, 100, 100 }, token),
4 => await host.DefineReportsAsync(1, new GemReportSpec[] { new(10, new uint[] { 201 }) }, token),
5 => await host.LinkReportsAsync(2, new GemLinkSpec[] { new(100, new uint[] { 10 }) }, token),
6 => await host.EnableEventsAsync(true, new uint[] { 100 }, token),
9 => await host.RequestStatusValuesAsync(new uint[] { 1 }, token),
_ => throw new ArgumentOutOfRangeException(nameof(step)),
};
static void Verify(GemResult result, int step)
{
GemResultInfo info = result.GetInfo();
if (step is >= 4 and <= 6)
{
GemRequestKind[] kinds = [GemRequestKind.DefineReports, GemRequestKind.LinkReports,
GemRequestKind.EnableEvents];
GemOutcome[] outcomes = [GemOutcome.DefineReportsAck, GemOutcome.LinkReportsAck,
GemOutcome.EnableEventsAck];
int index = step - 4;
if (info.Kind != GemResultKind.Response || info.RequestKind != kinds[index] ||
info.Outcome != outcomes[index] || info.Acknowledgement != 0 || info.ReplySent)
throw new InvalidOperationException("Unexpected configuration response.");
return;
}
if (step == 9)
{
if (info.Kind != GemResultKind.Response || info.RequestKind != GemRequestKind.StatusValues ||
info.Outcome != GemOutcome.ValuesReceived || info.EntryCount != 1 || info.ReplySent)
throw new InvalidOperationException("Unexpected status response metadata.");
using GemStatusValue value = result.GetStatusValue(0);
if (value.Id != 1 || value.Value is null || value.Value.GetF4()[0] != 21.5f)
throw new InvalidOperationException("Unexpected status response.");
return;
}
bool data = step is 0 or 1 or 7;
uint[] expected = data ? step == 1 ? [211, 999, 101, 201, 301, 202, 211] : [201, 202, 203, 204, 211]
: step == 3 ? [999, 100, 100] : [100];
if (info.Outcome != (data ? GemOutcome.DataDirectoryReceived : GemOutcome.EventDirectoryReceived) ||
info.EntryCount != (nuint)expected.Length) throw new InvalidOperationException("Unexpected directory response.");
for (nuint index = 0; index < (nuint)expected.Length; index++)
{
GemDirectoryEntryInfo entry = result.GetDirectoryAt(index);
if (entry.Id != expected[(int)index]) throw new InvalidOperationException("Directory order changed.");
using SecsItem name = result.GetDirectory(index, GemDirectoryField.Name);
if (data)
{
using SecsItem units = result.GetDirectory(index, GemDirectoryField.Units);
string expectedName = entry.Id is 201 or 211 ? "Count" : entry.Id == 202 ? "Sample" : entry.Id == 203 ? "Tree" : entry.Id == 204 ? "Bytes" : "";
string expectedUnits = entry.Id is 201 or 211 ? "count" : "";
if (name.GetAscii() != expectedName || units.GetAscii() != expectedUnits)
throw new InvalidOperationException("Data metadata mismatch.");
}
else
{
using SecsItem ids = result.GetDirectory(index, GemDirectoryField.VariableIds);
uint[] expectedIds = entry.Id == 100 ? [201, 202, 211] : [];
if (name.GetAscii() != (entry.Id == 100 ? "DataSnapshot" : "") ||
!ids.GetU4().SequenceEqual(expectedIds))
throw new InvalidOperationException("Event metadata mismatch.");
}
}
}
static async Task WaitSelected(GemWorker worker, CancellationToken token)
{
while ((await worker.GetTransportStateAsync(token)).State != HsmsConnectionState.Selected)
await Task.Delay(1, token);
}
internal static class Setup
{
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 GemDataOptions Data = new() { MaxVariables = 8, MaxMetadataBytes = 512, MaxValueBytes = 4096, MaxSamplesPerEvent = 5, MaxSampleBytes = 4096, ItemLimits = Limits };
internal static readonly GemBindingOptions Bindings = new() { MaxBindings = 16, MaxIdentifierBytes = 32, MaxTotalIdentifierBytes = 64 };
internal static readonly GemReportOptions Reports = new() { MaxEvents = 4, MaxReports = 4, MaxVariablesPerReport = 8, MaxReportsPerEvent = 4, MaxTotalVariables = 16, MaxTotalLinks = 16 };
internal static readonly GemDirectoryOptions Directories = new() { MaxEvents = 4, MaxVariablesPerEvent = 8, MaxTotalVariables = 16, MaxNameBytes = 64, MaxMetadataBytes = 128 };
internal static readonly HsmsWorkerOptions Worker = new() { CommandCapacity = 64, EventCapacity = 64, MaxCommandBytes = 131072, MaxEventBytes = 131072, PollIntervalMilliseconds = 1, ShutdownTimeoutMilliseconds = 2000 };
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 };
internal static async Task<GemEquipmentWorker> StartEquipment(string address, HsmsMode mode, CancellationToken token)
{
using SecsItem temperature = SecsItem.FromF4([21.5f], Limits);
using SecsItem count = SecsItem.FromU4([10], Limits);
using SecsItem sample = SecsItem.FromAscii("CACHE", Limits);
GemDataDefinition[] data = [new(1, "Count", "count", SecsFormat.U4, 1, 1, count), new(2, "Sample", "", SecsFormat.Ascii, 0, 16, sample), new(3, "Tree", "", SecsFormat.List, 0, 16, null), new(4, "Bytes", "", SecsFormat.Binary, 0, 16, null)];
GemReportBinding[] bindings = [new(201, GemDataSourceKind.Data, 1, null), new(202, GemDataSourceKind.Data, 2, null), new(203, GemDataSourceKind.Data, 3, null), new(204, GemDataSourceKind.Data, 4, null), new(211, GemDataSourceKind.Data, 1, null)];
return await GemEquipmentWorker.StartAsync(address, Transport(mode), Equipment, Worker,
new GemStatusVariable[] { new(1, "Temperature", "C", temperature) },
new GemReportConfiguration(Reports, new uint[] { 100 }), null, null, null,
new GemDataConfiguration(Data, data, Bindings, bindings), null,
new GemDirectoryConfiguration(Directories, new GemEventDirectoryDefinition[] { new(100, "DataSnapshot", new uint[] { 201, 202, 211 }) }), token);
}
}

In C, use pdcomm_gem_host_directory_request and pdcomm_gem_equipment_configure_event_directory.

When the host wants report values now instead of waiting for an event, it sends a report query:

Request GemReportQueryKind Target Reply
S6F15 Event CEID S6F16: DATAID, CEID and the linked reports
S6F19 Individual RPTID S6F20: the report values
S6F21 Annotated RPTID S6F22: VID and value pairs

The host defines RPTID 10 as VIDs [201, 202, 211, 201, 101, 301] and RPTID 20 as VIDs [203, 204, 201], and links CEID 100 to [20, 10] in that order. It then enables and disables the event, deletes reports and links, and queries in between: 22 steps in total.

  • S6F15 returns reports only while the event is enabled and has linked reports. Before you enable it, after you disable it and after you unlink it, the reply contains no reports. The PDComm equipment sends DATAID 1.
  • Querying an unknown RPTID (999) returns no values. An empty reply alone doesn’t tell you whether an ID is registered.
  • The equipment doesn’t answer queries automatically. When it gets a result whose Outcome is ReportQueryReceived, it replies with RespondReportQueryAsync. With samples it uses them; with an empty list it uses the cache. The example passes Count=77 and Sample=SHOT samples for some queries only.
터미널 창
# Terminal 1
dotnet "$Root\build\managed\bin\PDComm.GemQueriesDemo\release\PDComm.GemQueriesDemo.dll" $Native equipment passive 127.0.0.1:0
# Terminal 2
dotnet "$Root\build\managed\bin\PDComm.GemQueriesDemo\release\PDComm.GemQueriesDemo.dll" $Native host active 127.0.0.1:54473

The programs print PASS host active and PASS equipment passive.

The host queries with RequestReportQueryAsync and reads the result with GetQueryInfo, GetQueryReport and GetQueryValue. Only S6F22 values carry VIDs.

Full source: PDComm.GemQueriesDemo/Program.cs
PDComm.GemQueriesDemo/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.GemQueriesDemo <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;
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await using GemWorker worker = role == "host"
? await Setup.StartHost(args[3], mode, cancellation.Token)
: await Setup.StartEquipment(args[3], mode, cancellation.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(cancellation.Token).GetAsyncEnumerator();
if (worker is GemHostWorker host) await RunHost(host, events, cancellation.Token);
else await RunEquipment((GemEquipmentWorker)worker, events, cancellation.Token);
while (await events.MoveNextAsync()) events.Current.Dispose();
await worker.Completion.WaitAsync(cancellation.Token);
Console.WriteLine($"PASS {role} {args[2]}");
return 0;
static async Task RunHost(GemHostWorker host, IAsyncEnumerator<GemEvent> events,
CancellationToken cancellationToken)
{
await WaitSelected(host, cancellationToken);
HsmsToken pending = await host.BeginCommunicationAsync(cancellationToken);
using (GemEvent established = await Next(events,
current => current.Result?.GetInfo().Token == pending))
{
if (established.Result!.GetInfo().Outcome != GemOutcome.CommunicationEstablished)
throw new InvalidOperationException("Communication establishment failed.");
}
for (int step = 0; step < 22; step++)
{
pending = await Send(host, step, cancellationToken);
using GemEvent response = await Next(events,
current => current.Result?.GetInfo().Token == pending);
Verify(response.Result!, step);
}
await host.StopAsync(cancellationToken);
}
static async Task RunEquipment(GemEquipmentWorker equipment,
IAsyncEnumerator<GemEvent> events, CancellationToken cancellationToken)
{
await WaitSelected(equipment, cancellationToken);
int queryOrdinal = 0;
while (await events.MoveNextAsync())
{
using GemEvent current = events.Current;
GemResult? result = current.Result;
if (result is null) continue;
GemResultInfo info = result.GetInfo();
if (info.Outcome == GemOutcome.ReportQueryReceived)
{
if (queryOrdinal >= 14)
throw new InvalidOperationException("The equipment received an extra report query.");
int step = QueryStep(queryOrdinal);
GemQueryInfo query = result.GetQueryInfo();
if (!query.IsRequest || query.Responded || query.Kind != QueryKind(step) ||
query.Id != QueryId(step))
throw new InvalidOperationException("Unexpected equipment report query.");
GemDataSample[] samples = CreateSamples(queryOrdinal);
try
{
await equipment.RespondReportQueryAsync(result, samples, cancellationToken);
}
finally
{
foreach (GemDataSample sample in samples) sample.Value.Dispose();
}
queryOrdinal++;
}
else if (info.Outcome == GemOutcome.StatusReplied)
{
if (queryOrdinal != 14)
throw new InvalidOperationException("The equipment did not receive all report queries.");
await equipment.StopAsync(cancellationToken);
break;
}
}
}
static GemDataSample[] CreateSamples(int ordinal)
{
if (ordinal is not (1 or 3 or 4)) return [];
return
[
new GemDataSample(1, SecsItem.FromU4([77], Setup.Limits)),
new GemDataSample(2, SecsItem.FromAscii("SHOT", Setup.Limits)),
];
}
static int QueryStep(int ordinal)
{
int[] steps = [2, 4, 5, 6, 7, 8, 9, 11, 12, 14, 15, 17, 19, 20];
return steps[ordinal];
}
static Task<HsmsToken> Send(GemHostWorker host, int step,
CancellationToken cancellationToken) => step switch
{
0 => host.DefineReportsAsync(1,
new GemReportSpec[]
{
new(10, new uint[] { 201, 202, 211, 201, 101, 301 }),
new(20, new uint[] { 203, 204, 201 }),
}, cancellationToken),
1 => host.LinkReportsAsync(2,
new GemLinkSpec[] { new(100, new uint[] { 20, 10 }) }, cancellationToken),
3 or 10 or 16 => host.EnableEventsAsync(step != 10, new uint[] { 100 }, cancellationToken),
13 => host.DefineReportsAsync(3,
new GemReportSpec[] { new(20, Array.Empty<uint>()) }, cancellationToken),
18 => host.LinkReportsAsync(2,
new GemLinkSpec[] { new(100, Array.Empty<uint>()) }, cancellationToken),
21 => host.RequestStatusValuesAsync(new uint[] { 1 }, cancellationToken),
_ => host.RequestReportQueryAsync(QueryKind(step), QueryId(step), cancellationToken),
};
static GemReportQueryKind QueryKind(int step) => step switch
{
2 or 4 or 9 or 11 or 15 or 17 or 19 => GemReportQueryKind.Event,
5 or 7 or 12 or 14 => GemReportQueryKind.Individual,
6 or 8 or 20 => GemReportQueryKind.Annotated,
_ => throw new ArgumentOutOfRangeException(nameof(step)),
};
static uint QueryId(int step)
{
if (step is 7 or 8 or 9) return 999;
if (step is 12 or 14) return 20;
return QueryKind(step) == GemReportQueryKind.Event ? 100u : 10u;
}
static void Verify(GemResult result, int step)
{
GemResultInfo generic = result.GetInfo();
if (step is 0 or 1 or 3 or 10 or 13 or 16 or 18)
{
GemRequestKind expectedKind = step is 0 or 13 ? GemRequestKind.DefineReports :
step is 1 or 18 ? GemRequestKind.LinkReports : GemRequestKind.EnableEvents;
GemOutcome expectedOutcome = step is 0 or 13 ? GemOutcome.DefineReportsAck :
step is 1 or 18 ? GemOutcome.LinkReportsAck : GemOutcome.EnableEventsAck;
if (generic.Kind != GemResultKind.Response || generic.RequestKind != expectedKind ||
generic.Outcome != expectedOutcome || generic.Acknowledgement != 0)
throw new InvalidOperationException("Unexpected report configuration response.");
return;
}
if (step == 21)
{
if (generic.Kind != GemResultKind.Response ||
generic.RequestKind != GemRequestKind.StatusValues ||
generic.Outcome != GemOutcome.ValuesReceived || generic.EntryCount != 1)
throw new InvalidOperationException("Unexpected status response metadata.");
using GemStatusValue value = result.GetStatusValue(0);
if (value.Id != 1 || value.Value.GetF4()[0] != 21.5f)
throw new InvalidOperationException("Unexpected status response value.");
return;
}
VerifyQuery(result, generic, step);
}
static void VerifyQuery(GemResult result, GemResultInfo generic, int step)
{
GemReportQueryKind kind = QueryKind(step);
GemOutcome expectedOutcome = kind switch
{
GemReportQueryKind.Event => GemOutcome.EventQueryReceived,
GemReportQueryKind.Individual => GemOutcome.IndividualQueryReceived,
GemReportQueryKind.Annotated => GemOutcome.AnnotatedQueryReceived,
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};
GemQueryInfo query = result.GetQueryInfo();
if (generic.Kind != GemResultKind.Response || generic.Outcome != expectedOutcome ||
generic.RequestKind != (GemRequestKind)kind || generic.Acknowledgement is not null ||
query.IsRequest || query.Responded || query.Kind != kind || query.Id != QueryId(step))
throw new InvalidOperationException("Unexpected report query response metadata.");
if (kind == GemReportQueryKind.Event)
{
int reportCount = step == 4 ? 2 : step == 17 ? 1 : 0;
int valueCount = step == 4 ? 9 : step == 17 ? 6 : 0;
if (!query.DataIdPresent || query.DataId != 1 ||
query.ReportCount != (nuint)reportCount || query.ValueCount != (nuint)valueCount ||
generic.EntryCount != (nuint)reportCount)
throw new InvalidOperationException("Unexpected event query shape.");
for (nuint reportIndex = 0; reportIndex < query.ReportCount; reportIndex++)
{
uint reportId = step == 4 && reportIndex == 0 ? 20u : 10u;
GemQueryReportInfo report = result.GetQueryReport(reportIndex);
nuint expectedValues = reportId == 10 ? 6u : 3u;
if (report.Id != reportId || report.ValueCount != expectedValues)
throw new InvalidOperationException("Unexpected event query report.");
VerifyValues(result, reportIndex, reportId, report.ValueCount, step == 4, false);
}
return;
}
int count = step is 5 or 6 or 20 ? 6 : step == 12 ? 3 : 0;
if (query.DataIdPresent || query.ReportCount != 0 || query.ValueCount != (nuint)count ||
generic.EntryCount != (nuint)count)
throw new InvalidOperationException("Unexpected individual query shape.");
VerifyValues(result, 0, QueryId(step), query.ValueCount, step == 6,
kind == GemReportQueryKind.Annotated);
}
static void VerifyValues(GemResult result, nuint reportIndex, uint reportId, nuint count,
bool sampled, bool annotated)
{
for (nuint index = 0; index < count; index++)
{
uint variableId = VariableId(reportId, (int)index);
using GemQueryValue actual = result.GetQueryValue(reportIndex, index);
if (actual.VariableId != (annotated ? variableId : null))
throw new InvalidOperationException("Unexpected annotated variable identifier.");
using SecsItem expected = ExpectedValue(variableId, sampled);
if (!actual.Value.Encode().SequenceEqual(expected.Encode()))
throw new InvalidOperationException("Unexpected report query value.");
}
}
static uint VariableId(uint reportId, int index)
{
uint[] report10 = [201, 202, 211, 201, 101, 301];
uint[] report20 = [203, 204, 201];
return reportId == 10 ? report10[index] : report20[index];
}
static SecsItem ExpectedValue(uint variableId, bool sampled) => variableId switch
{
201 or 211 => SecsItem.FromU4([sampled ? 77u : 10u], Setup.Limits),
202 => SecsItem.FromAscii(sampled ? "SHOT" : "CACHED", Setup.Limits),
203 => Setup.CreateTree(),
204 => SecsItem.FromBinary([], Setup.Limits),
101 => SecsItem.FromF4([21.5f], Setup.Limits),
301 => SecsItem.FromU4([10], Setup.Limits),
_ => throw new ArgumentOutOfRangeException(nameof(variableId)),
};
static async Task<GemEvent> Next(IAsyncEnumerator<GemEvent> events,
Func<GemEvent, bool> predicate)
{
while (await events.MoveNextAsync())
{
if (predicate(events.Current)) return events.Current;
events.Current.Dispose();
}
throw new InvalidOperationException("Event stream ended early.");
}
static async Task WaitSelected(GemWorker worker, CancellationToken cancellationToken)
{
while ((await worker.GetTransportStateAsync(cancellationToken)).State !=
HsmsConnectionState.Selected)
await Task.Delay(1, cancellationToken);
}
internal static class Setup
{
internal static readonly SecsLimits Limits = new(4096, 4096, 256, 16);
private static readonly GemHostOptions HostOptions = new()
{
MaxPendingIds = 64,
ItemLimits = Limits,
};
private static readonly GemEquipmentOptions EquipmentOptions = new()
{
Model = "PDComm",
SoftwareRevision = "0.1.0",
InitialControl = GemControlState.HostOffline,
OnlineMode = GemOnlineMode.Remote,
MaxVariables = 8,
MaxMetadataBytes = 1024,
MaxValueBytes = 4096,
ItemLimits = Limits,
};
private static readonly GemReportOptions ReportOptions = new()
{
MaxEvents = 4,
MaxReports = 4,
MaxVariablesPerReport = 8,
MaxReportsPerEvent = 4,
MaxTotalVariables = 16,
MaxTotalLinks = 16,
};
private static readonly GemDataOptions DataOptions = new()
{
MaxVariables = 8,
MaxMetadataBytes = 512,
MaxValueBytes = 4096,
MaxSamplesPerEvent = 5,
MaxSampleBytes = 4096,
ItemLimits = Limits,
};
private static readonly GemBindingOptions BindingOptions = new()
{
MaxBindings = 16,
MaxIdentifierBytes = 32,
MaxTotalIdentifierBytes = 64,
};
private static readonly GemConstantOptions ConstantOptions = new()
{
MaxConstants = 8,
MaxMetadataBytes = 1024,
MaxValueBytes = 4096,
MaxQueryIds = 32,
MaxChangesPerRequest = 16,
MaxIdentifierBytes = 32,
ItemLimits = Limits,
};
private static readonly HsmsWorkerOptions WorkerOptions = new()
{
CommandCapacity = 64,
EventCapacity = 64,
MaxCommandBytes = 131072,
MaxEventBytes = 131072,
PollIntervalMilliseconds = 1,
ShutdownTimeoutMilliseconds = 2000,
};
internal static Task<GemHostWorker> StartHost(string address, HsmsMode mode,
CancellationToken cancellationToken) => GemHostWorker.StartAsync(
address, Transport(mode), HostOptions, WorkerOptions, cancellationToken);
internal static async Task<GemEquipmentWorker> StartEquipment(string address, HsmsMode mode,
CancellationToken cancellationToken)
{
using SecsItem temperature = SecsItem.FromF4([21.5f], Limits);
using SecsItem count = SecsItem.FromU4([10], Limits);
using SecsItem cached = SecsItem.FromAscii("CACHED", Limits);
using SecsItem tree = CreateTree();
using SecsItem bytes = SecsItem.FromBinary([], Limits);
using SecsItem constantId = SecsItem.FromU1([1], Limits);
using SecsItem constantValue = SecsItem.FromU4([10], Limits);
GemDataDefinition[] data =
[
new(1, "Count", "count", SecsFormat.U4, 1, 1, count),
new(2, "Sample", "", SecsFormat.Ascii, 0, 32, cached),
new(3, "Tree", "", SecsFormat.List, 0, 4, tree),
new(4, "Bytes", "", SecsFormat.Binary, 0, 32, bytes),
];
GemReportBinding[] bindings =
[
new(201, GemDataSourceKind.Data, 1, null),
new(202, GemDataSourceKind.Data, 2, null),
new(203, GemDataSourceKind.Data, 3, null),
new(204, GemDataSourceKind.Data, 4, null),
new(211, GemDataSourceKind.Data, 1, null),
new(101, GemDataSourceKind.Status, 1, null),
new(301, GemDataSourceKind.Constant, 0, constantId),
];
GemConstantDefinition[] constants =
[
new(constantId, "CycleLimit", "count", constantValue,
null, null, null, 1, 1),
];
return await GemEquipmentWorker.StartAsync(
address, Transport(mode), EquipmentOptions, WorkerOptions,
[new GemStatusVariable(1, "Temperature", "C", temperature)],
new GemReportConfiguration(ReportOptions, new uint[] { 100 }),
null, null, null,
new GemDataConfiguration(DataOptions, data, BindingOptions, bindings),
new GemConstantConfiguration(ConstantOptions, constants), cancellationToken);
}
internal static SecsItem CreateTree()
{
using SecsItem number = SecsItem.FromU4([7], Limits);
using SecsItem text = SecsItem.FromAscii("NESTED", Limits);
return SecsItem.CreateList([number, text], Limits);
}
private 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,
};
}

In C, use pdcomm_gem_host_report_query and pdcomm_gem_equipment_respond_report_query.

data.cpp, constants.cpp, directories.cpp and queries.cpp in examples/qt implement the same scenarios with Qt, but they share verification code with the PDComm product tests, so they don’t build from the examples folder alone. To learn how a Qt application drives a PDComm endpoint, use reports.cpp from Reports and events. The C++ API mirrors the .NET names, for example GemEquipment::configureDataVariables, GemEquipment::configureReportBindings, GemEquipment::configureConstants, GemEquipment::configureEventDirectory, GemHost::dataDirectory, GemHost::eventDirectory and GemHost::eventQuery.

Status and data variables

  1. Start a session in an Equipment window and a Host window and establish communication. Without a profile, the simulator equipment has SVID 1001 EquipmentState (ASCII IDLE) and SVID 1002 WaferCount (U4 0).
  2. In the Host window’s Status & Data workspace, enter 1001, 1002 in Status IDs, click Discover names, then click Read values. You need the names before you can read the values.
  3. In the Equipment window’s Status & Data workspace, select SVID 1002, type a new value in Value and click Apply value. Click Read values on the host again to see the change.
Host Status & Data workspace with SVID 1001 EquipmentState (IDLE) and SVID 1002 WaferCount (0) read from the equipment, marked Received.Host Status & Data workspace with SVID 1001 EquipmentState (IDLE) and SVID 1002 WaferCount (0) read from the equipment, marked Received.
Equipment Status & Data workspace with the local status variables 1001 and 1002, marked Local.Equipment Status & Data workspace with the local status variables 1001 and 1002, marked Local.

Equipment constants

  1. In the Equipment window, open the Configuration tab of the Equipment Constants workspace, click Load example, reserve it with Reserve for next start, and start the session. The example constant is ECID U1(1) CycleLimit (U4 10, range 1–100).
  2. In the Host window’s Host Requests tab, choose Values or Metadata and click Query. To change a value, enter ID=value in the field below and click Send change.
  3. In the Equipment window’s Equipment Traffic tab, select the request, choose Apply (0), Cannot perform (2), Out of range (3) or Reject (4), and click Respond.
Configuration tab of the Equipment Constants workspace with the constant catalog.Configuration tab of the Equipment Constants workspace with the constant catalog.
Host Requests tab of the Equipment Constants workspace with a completed Values query.Host Requests tab of the Equipment Constants workspace with a completed Values query.

Read the data directory with Read data directory in Status & Data. Run event directory reads and report queries from the Directories and Queries sub-tabs on the Traffic tab of the Reports & Events workspace.

Symptom Cause Fix
A request fails with PdcommException (InvalidState). You sent it before communication was established. Wait for CommunicationEstablished first.
An S1F4 value is a zero-length item. You asked for an SVID the equipment doesn’t have. Check the registered IDs with S1F11.
An event report fails and the data variable value is missing. You reported a DV without a cache and without a sample. Fill the cache with UpdateDataVariableAsync or pass a sample.
S2F34 rejects a report definition with DRACK 4. The VID doesn’t exist on the equipment. With bindings installed, that means a VID missing from the binding table. Use only VIDs from the binding table.
S2F16 returns EAC 3. The format, range or length doesn’t fit. Compare with the minimum and maximum from S2F29 and the format of the current value.
You changed a constant but the value stays the same. The equipment application chose a decision other than Apply, or validation rejected the change. Check the EAC and Applied in the reply.
The host never gets S2F16 or S6F16 and fails after T3. The equipment application didn’t call RespondConstantsAsync or RespondReportQueryAsync. Neither request is answered automatically; always reply.
  • The examples exit when the scenario ends. Stop a hung program with Ctrl+C.
  • Delete $PDCOMM_ROOT/build/managed when you no longer need it.