Skip to content
Start free evaluation

Alarms

Alarms tell the host that something is wrong on the equipment. In this tutorial the host enables alarm reporting and lists alarms, while the equipment observes alarms being set and cleared and reports them. You also send the collection events that are linked to an alarm.

  • How to run S5F1/S5F2 (alarm report), S5F3/S5F4 (enable alarm), S5F5/S5F6 (list alarms) and S5F7/S5F8 (list enabled alarms).
  • Why PDComm separates changing an alarm’s state (observe) from telling the host about it (publish).
  • How alarm reports with and without the W-bit differ.
  • 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. For the Qt example you also need Qt 6 (Core), CMake 3.20 or later and a build tool such as Ninja.
  • Read Reports and events first if you want to follow the alarm-linked events.

An alarm consists of an ALID (alarm ID), an ALCD (alarm code) and an ALTX (alarm text). The high bit of the ALCD (0x80) means the alarm is set; the lower seven bits hold the category. The example equipment defines two alarms:

ALID Category ALTX CEID when set CEID when cleared
10 2 Vacuum lost 100 200
20 3 Door open none none

Both alarms start cleared, with reporting disabled. The equipment also has SVID 1 Temperature (F4 21.5) and CEIDs 100 and 200.

After establishing communication, the host in PDComm.GemAlarmsDemo runs sixteen steps:

Host Equipment
0 S5F5 all ALIDs -> S5F6 [10 Vacuum lost, 20 Door open]
1 S5F5 [20, 10, 20] -> S5F6 3 entries in the requested order
2 S5F7 -> S5F8 [] (no alarm enabled yet)
3 S5F3 enable ALID 10, W1 -> S5F4 ACKC5 0
4 S5F3 enable ALID 99, W1 -> S5F4 ACKC5 1 (unknown ALID)
5 S2F33 RPTID 10 = [VID 1] -> DRACK 0
6 S2F35 CEID 100->[10], 200->[10] -> LRACK 0
7 S2F37 enable all events -> ERACK 0
8 S5F3 enable ALID 20, W0 (no reply)
9 S5F7 -> S5F8 [10, 20]
equipment: observe ALID 10 set, then publish
<- S5F1 W1 ALID 10 ALCD 0x82 "Vacuum lost" S5F2 ACKC5 0 ->
<- S6F11 DATAID 61 CEID 100 RPT 10=[21.5] S6F12 ACKC6 0 ->
10 S5F5 [10] -> S5F6 ALID 10 active
11 S5F3 disable ALID 10, W0 (no reply)
12 S5F7 -> S5F8 [20]
equipment: observe ALID 10 clear; S5F1 is refused (reporting off)
<- S6F11 DATAID 62 CEID 200 RPT 10=[21.5] S6F12 ACKC6 7 ->
13 S5F3 enable ALID 10, W1 -> S5F4 ACKC5 0
<- S5F1 W0 ALID 10 ALCD 0x02 (cleared, no reply expected)
14 S5F5 all ALIDs -> S5F6 2 entries
15 S5F3 disable ALID 20, W1 -> S5F4 ACKC5 0
  • An empty S5F5 list returns every alarm. A list of ALIDs returns them in the requested order, duplicates included.
  • Steps 8 and 11 send S5F3 without the W-bit. The equipment doesn’t reply, so the host moves on immediately.
  • Changing an alarm’s state sends nothing by itself. The equipment application observes the change and then sends S5F1 and any linked S6F11 explicitly.
  • In step 12 the equipment observes the clear, but the host has disabled reporting for ALID 10, so S5F1 can’t be sent. The clear event CEID 200 is an independent event report, so it still goes out.
  • The S5F1 after step 13 has no W-bit. If the host tries to answer it with S5F2, the SDK refuses with InvalidState.
  • At the end the equipment holds two alarms, none set, and one with reporting enabled (ALID 10).
터미널 창
dotnet build "$Examples\dotnet\PDComm.GemAlarmsDemo\PDComm.GemAlarmsDemo.csproj" -c Release "-p:PDCommSdk=$Sdk" --artifacts-path "$Root\build\managed"

The build prints Build succeeded. and the path of PDComm.GemAlarmsDemo.dll.

터미널 창
dotnet "$Root\build\managed\bin\PDComm.GemAlarmsDemo\release\PDComm.GemAlarmsDemo.dll" $Native equipment passive 127.0.0.1:0
LISTEN 127.0.0.1:54477

In another terminal, pass the address the equipment printed:

터미널 창
dotnet "$Root\build\managed\bin\PDComm.GemAlarmsDemo\release\PDComm.GemAlarmsDemo.dll" $Native host active 127.0.0.1:54477

The host and the equipment print these lines and exit with code 0:

PASS host active: managed GEM alarms worker completed scenario 16 and shut down
PASS equipment passive: managed GEM alarms worker completed scenario 16 and shut down

StartEquipmentAsync starts the equipment with a GemAlarmConfiguration that holds two GemAlarmDefinition values. The fourth and fifth values of a definition are the CEIDs for set and clear. Those CEIDs must exist in the GemReportConfiguration you pass alongside.

The SDK answers S5F3, S5F5 and S5F7 from the alarm catalog. RunEquipmentAsync watches the result Outcome to drive the scenario:

  • ObserveAlarmAsync changes only the local alarm state and returns a GemAlarmChange: whether the state changed, whether the host has enabled reporting, and which CEID the transition is linked to. It sends nothing.
  • PublishAlarmAsync sends S5F1 with the current state. The second argument is the W-bit. If the host has disabled reporting for that alarm, it throws PdcommException with InvalidState.
  • Alarm-linked events go out separately: read GemAlarmChange.EventId and call PublishEventAsync.
  • At the end, GetAlarmStateAsync reports the number of alarms, how many are set, how many have reporting enabled and how many text bytes they use.

SendStepAsync builds the requests with these APIs:

For list replies, GetAlarmInfo returns the count and GetAlarmAt returns each alarm’s ALID, category, set flag and ALTX. A result whose Outcome is AlarmReceived is an S5F1 from the equipment. Answer only W1 reports, with AcknowledgeAlarmAsync. ACKC5 0 means accepted.

Full source: PDComm.GemAlarmsDemo/Program.cs
PDComm.GemAlarmsDemo/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.GemAlarmsDemo <absolute-native-library-path> <host|equipment> <active|passive> <numeric-address>");
return 2;
}
PdcommRuntime.Initialize(args[0]);
string role = args[1];
HsmsMode mode = args[2] == "active" ? HsmsMode.Active : HsmsMode.Passive;
SecsLimits itemLimits = new(4096, 4096, 256, 16);
HsmsOptions transportOptions = DemoOptions.Transport(mode, itemLimits);
HsmsWorkerOptions workerOptions = DemoOptions.Worker();
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(20));
await using GemWorker worker = role == "host"
? await GemHostWorker.StartAsync(
args[3], transportOptions, DemoOptions.Host(itemLimits), workerOptions, deadline.Token)
: await StartEquipmentAsync(args[3], transportOptions, itemLimits, workerOptions, deadline.Token);
if (mode == HsmsMode.Passive)
{
string address = worker.LocalAddress ??
throw new InvalidOperationException("The passive endpoint did not publish its listening address.");
Console.WriteLine($"LISTEN {address}");
Console.Out.Flush();
}
await using IAsyncEnumerator<GemEvent> events =
worker.ReadEventsAsync(deadline.Token).GetAsyncEnumerator();
if (worker is GemHostWorker host)
await RunHostAsync(host, events, deadline.Token);
else if (worker is GemEquipmentWorker equipment)
await RunEquipmentAsync(equipment, events, deadline.Token);
while (await events.MoveNextAsync()) events.Current.Dispose();
await worker.Completion.WaitAsync(deadline.Token);
Console.WriteLine($"PASS {role} {args[2]}: managed GEM alarms worker completed scenario 16 and shut down");
return 0;
static async Task<GemEquipmentWorker> StartEquipmentAsync(
string address,
HsmsOptions transport,
SecsLimits limits,
HsmsWorkerOptions worker,
CancellationToken cancellationToken)
{
using SecsItem temperature = SecsItem.FromF4([21.5f], limits);
using SecsItem lot = SecsItem.FromAscii("LOT-001", limits);
GemStatusVariable[] variables =
[
new(1, "Temperature", "C", temperature),
new(2, "Lot", "", lot),
];
var reports = new GemReportConfiguration(DemoOptions.Reports(), new uint[] { 100, 200 });
var alarms = new GemAlarmConfiguration(
DemoOptions.Alarms(),
new GemAlarmDefinition[]
{
new(10, 2, "Vacuum lost", 100, 200),
new(20, 3, "Door open"),
});
return await GemEquipmentWorker.StartAsync(
address, transport, DemoOptions.Equipment(limits), worker, variables, reports, alarms, cancellationToken);
}
static async Task RunHostAsync(
GemHostWorker host,
IAsyncEnumerator<GemEvent> events,
CancellationToken cancellationToken)
{
bool began = false;
bool established = false;
int step = 0;
int alarmCount = 0;
int eventCount = 0;
HsmsToken pending = default;
while (step < 16)
{
if (!began && (await host.GetTransportStateAsync(cancellationToken)).State == HsmsConnectionState.Selected)
{
pending = await host.BeginCommunicationAsync(cancellationToken);
began = true;
}
if (!await events.MoveNextAsync())
throw new InvalidOperationException("The GEM event stream ended before the alarms scenario completed.");
GemEvent current = events.Current;
bool disposed = false;
try
{
_ = ValidateEvent(current);
GemResult? result = current.Result;
if (result is null) continue;
GemResultInfo info = result.GetInfo();
if (info.Kind == GemResultKind.Response)
{
if (info.Token != pending)
throw new InvalidOperationException("The GEM response token did not match the current request.");
if (!established)
{
if (info.RequestKind != GemRequestKind.Establish ||
info.Outcome != GemOutcome.CommunicationEstablished)
throw new InvalidOperationException("GEM communication establishment failed.");
established = true;
pending = await SendStepAsync(host, step, cancellationToken);
step = EffectiveStep(step);
}
else
{
CheckResponse(result, step);
if (step is not (9 or 12 or 13))
{
step++;
if (step < 16)
{
pending = await SendStepAsync(host, step, cancellationToken);
step = EffectiveStep(step);
}
}
}
}
else if (info.Outcome == GemOutcome.AlarmReceived)
{
int expectedStep = alarmCount == 0 ? 9 : 13;
if (step != expectedStep || info.Kind != GemResultKind.Primary)
throw new InvalidOperationException("An alarm report arrived at an unexpected scenario step.");
CheckAlarmReport(result, alarmCount == 0, alarmCount == 0);
if (alarmCount == 0)
{
Task acknowledgement = host.AcknowledgeAlarmAsync(result, 0, cancellationToken);
current.Dispose();
disposed = true;
await acknowledgement;
}
else
{
try
{
await host.AcknowledgeAlarmAsync(result, 0, cancellationToken);
throw new InvalidOperationException("A W0 alarm report unexpectedly accepted an acknowledgement.");
}
catch (PdcommException error) when (error.Status == PdcommStatus.InvalidState) { }
}
alarmCount++;
}
else if (info.Outcome == GemOutcome.EventReceived)
{
int expectedStep = eventCount == 0 ? 9 : 12;
if (step != expectedStep || info.Kind != GemResultKind.Primary)
throw new InvalidOperationException("An event report arrived at an unexpected scenario step.");
CheckEvent(result, eventCount);
Task acknowledgement = host.AcknowledgeEventAsync(
result, eventCount == 0 ? (byte)0 : (byte)7, cancellationToken);
current.Dispose();
disposed = true;
await acknowledgement;
eventCount++;
}
if ((step == 9 && alarmCount == 1 && eventCount == 1) ||
(step == 12 && eventCount == 2) ||
(step == 13 && alarmCount == 2))
{
step++;
pending = await SendStepAsync(host, step, cancellationToken);
step = EffectiveStep(step);
}
}
finally
{
if (!disposed) current.Dispose();
}
}
if (alarmCount != 2 || eventCount != 2)
throw new InvalidOperationException("The host did not receive all alarm and event reports.");
await host.StopAsync(cancellationToken);
}
static async Task RunEquipmentAsync(
GemEquipmentWorker equipment,
IAsyncEnumerator<GemEvent> events,
CancellationToken cancellationToken)
{
int primaryCount = 0;
int enabledLists = 0;
int alarmAcknowledgements = 0;
int eventAcknowledgements = 0;
int phase = 0;
bool communicated = false;
HsmsToken alarmToken = default;
HsmsToken[] eventTokens = new HsmsToken[2];
while (true)
{
if (!await events.MoveNextAsync())
throw new InvalidOperationException("The GEM event stream ended before the alarms scenario completed.");
using GemEvent current = events.Current;
HsmsEventInfo transportEvent = ValidateEvent(current);
GemResult? result = current.Result;
if (result is not null)
{
GemResultInfo info = result.GetInfo();
if (info.State.Communication == GemCommunicationState.Communicating) communicated = true;
if (info.Kind == GemResultKind.Primary &&
info.Outcome != GemOutcome.CommunicationEstablished)
{
primaryCount++;
if (info.Outcome == GemOutcome.EnabledAlarmListReplied)
{
enabledLists++;
if (enabledLists == 2)
{
if (phase != 0) throw new InvalidOperationException("The first alarm phase was repeated.");
GemAlarmChange change = await equipment.ObserveAlarmAsync(10, true, cancellationToken);
CheckChange(change, true, true, 100);
alarmToken = await equipment.PublishAlarmAsync(10, true, cancellationToken);
eventTokens[0] = await equipment.PublishEventAsync(61, 100, cancellationToken);
phase = 1;
}
else if (enabledLists == 3)
{
if (phase != 1) throw new InvalidOperationException("The clear alarm phase was out of order.");
GemAlarmChange change = await equipment.ObserveAlarmAsync(10, false, cancellationToken);
CheckChange(change, false, false, 200);
try
{
await equipment.PublishAlarmAsync(10, true, cancellationToken);
throw new InvalidOperationException("A disabled alarm unexpectedly published a report.");
}
catch (PdcommException error) when (error.Status == PdcommStatus.InvalidState) { }
eventTokens[1] = await equipment.PublishEventAsync(62, 200, cancellationToken);
phase = 2;
}
}
else if (info.Outcome == GemOutcome.AlarmEnableAck && phase == 2 &&
(await equipment.GetAlarmStateAsync(cancellationToken)).EnabledCount == 2)
{
_ = await equipment.PublishAlarmAsync(10, false, cancellationToken);
phase = 3;
}
}
else if (info.Kind == GemResultKind.Response)
{
if (info.RequestKind == GemRequestKind.AlarmReport)
{
if (alarmAcknowledgements != 0 || info.Outcome != GemOutcome.AlarmAck ||
info.Token != alarmToken || info.Acknowledgement != 0)
throw new InvalidOperationException("The alarm acknowledgement response is incorrect.");
alarmAcknowledgements++;
}
else
{
if (eventAcknowledgements >= 2 || info.RequestKind != GemRequestKind.EventReport ||
info.Outcome != GemOutcome.EventAck || info.Token != eventTokens[eventAcknowledgements] ||
info.Acknowledgement != (eventAcknowledgements == 0 ? 0 : 7))
throw new InvalidOperationException("The event acknowledgement response is incorrect.");
eventAcknowledgements++;
}
}
}
if (communicated && transportEvent.Kind == HsmsEventKind.StateChanged &&
transportEvent.FromState == HsmsConnectionState.Selected &&
transportEvent.ToState != HsmsConnectionState.Selected)
{
GemAlarmState state = await equipment.GetAlarmStateAsync(cancellationToken);
if (primaryCount != 16 || enabledLists != 3 || phase != 3 || alarmAcknowledgements != 1 ||
eventAcknowledgements != 2 || state != new GemAlarmState(2, 0, 1, 20))
throw new InvalidOperationException("The final equipment alarm state is incorrect.");
await equipment.StopAsync(cancellationToken);
return;
}
}
}
static async Task<HsmsToken> SendStepAsync(
GemHostWorker host,
int step,
CancellationToken cancellationToken)
{
HsmsToken token = step switch
{
0 or 14 => await host.RequestAlarmListAsync(Array.Empty<uint>(), cancellationToken),
1 => await host.RequestAlarmListAsync(new uint[] { 20, 10, 20 }, cancellationToken),
2 or 9 or 12 => await host.RequestEnabledAlarmListAsync(cancellationToken),
3 or 13 => await host.EnableAlarmAsync(10, true, true, cancellationToken),
4 => await host.EnableAlarmAsync(99, true, true, cancellationToken),
5 => await host.DefineReportsAsync(
1, new GemReportSpec[] { new(10, new uint[] { 1 }) }, cancellationToken),
6 => await host.LinkReportsAsync(
2, new GemLinkSpec[] { new(100, new uint[] { 10 }), new(200, new uint[] { 10 }) }, cancellationToken),
7 => await host.EnableEventsAsync(true, Array.Empty<uint>(), cancellationToken),
8 => await host.EnableAlarmAsync(20, true, false, cancellationToken),
10 => await host.RequestAlarmListAsync(new uint[] { 10 }, cancellationToken),
11 => await host.EnableAlarmAsync(10, false, false, cancellationToken),
15 => await host.EnableAlarmAsync(20, false, true, cancellationToken),
_ => throw new ArgumentOutOfRangeException(nameof(step)),
};
if (step is 8 or 11)
return await SendStepAsync(host, step + 1, cancellationToken);
return token;
}
static int EffectiveStep(int step) => step is 8 or 11 ? step + 1 : step;
static void CheckResponse(GemResult result, int step)
{
GemResultInfo info = result.GetInfo();
GemRequestKind kind = step switch
{
0 or 1 or 10 or 14 => GemRequestKind.AlarmList,
2 or 9 or 12 => GemRequestKind.EnabledAlarmList,
5 => GemRequestKind.DefineReports,
6 => GemRequestKind.LinkReports,
7 => GemRequestKind.EnableEvents,
_ => GemRequestKind.EnableAlarm,
};
GemOutcome outcome = step switch
{
0 or 1 or 10 or 14 => GemOutcome.AlarmListReceived,
2 or 9 or 12 => GemOutcome.EnabledAlarmListReceived,
5 => GemOutcome.DefineReportsAck,
6 => GemOutcome.LinkReportsAck,
7 => GemOutcome.EnableEventsAck,
_ => GemOutcome.AlarmEnableAck,
};
if (info.Kind != GemResultKind.Response || info.RequestKind != kind || info.Outcome != outcome || info.ReplySent)
throw new InvalidOperationException($"Unexpected alarms response at scenario step {step}.");
if (kind is GemRequestKind.AlarmList or GemRequestKind.EnabledAlarmList)
CheckAlarmList(result, step);
else if (info.Acknowledgement != (step == 4 ? (byte)1 : (byte)0))
throw new InvalidOperationException("The alarm configuration acknowledgement is incorrect.");
}
static void CheckAlarmList(GemResult result, int step)
{
GemAlarmResultInfo alarm = result.GetAlarmInfo();
nuint count = step == 1 ? 3u : step == 2 ? 0u : step is 10 or 12 ? 1u : 2u;
if (alarm != new GemAlarmResultInfo(count, false, false, false))
throw new InvalidOperationException("The alarm list metadata is incorrect.");
for (nuint index = 0; index < count; index++)
{
uint id = step == 1 ? (index == 1 ? 10u : 20u) : step == 12 ? 20u : index == 0 ? 10u : 20u;
CheckAlarmValue(result.GetAlarmAt(index), id, step == 10);
}
}
static void CheckAlarmReport(GemResult result, bool active, bool replyExpected)
{
GemAlarmResultInfo alarm = result.GetAlarmInfo();
if (alarm != new GemAlarmResultInfo(1, true, replyExpected, false))
throw new InvalidOperationException("The alarm report metadata is incorrect.");
CheckAlarmValue(result.GetAlarmAt(0), 10, active);
}
static void CheckAlarmValue(GemAlarmValue value, uint id, bool active)
{
byte category = id == 10 ? (byte)2 : (byte)3;
string text = id == 10 ? "Vacuum lost" : "Door open";
if (value != new GemAlarmValue(new GemAlarmValueInfo(id, category, active), text))
throw new InvalidOperationException("The alarm value is incorrect.");
}
static void CheckChange(GemAlarmChange change, bool active, bool reportingEnabled, uint eventId)
{
if (change != new GemAlarmChange(10, true, active, reportingEnabled, eventId))
throw new InvalidOperationException("The observed alarm change is incorrect.");
}
static void CheckEvent(GemResult result, int ordinal)
{
GemEventInfo info = result.GetEventInfo();
if (info != new GemEventInfo((uint)(61 + ordinal), ordinal == 0 ? 100u : 200u, 1, false))
throw new InvalidOperationException("The alarm event metadata is incorrect.");
if (result.GetEventReport(0) != new GemEventReport(10, 1))
throw new InvalidOperationException("The alarm event report is incorrect.");
using SecsItem value = result.GetEventValue(0, 0);
float[] payload = value.GetF4();
if (payload.Length != 1 || BitConverter.SingleToInt32Bits(payload[0]) != BitConverter.SingleToInt32Bits(21.5f))
throw new InvalidOperationException("The alarm event value is incorrect.");
}
static HsmsEventInfo ValidateEvent(GemEvent current)
{
if (current.ProcessingStatus != PdcommStatus.Ok)
throw new PdcommException(current.ProcessingStatus);
HsmsEventInfo raw = current.TransportEvent.GetInfo();
if (raw.Kind == HsmsEventKind.Failed)
throw new InvalidOperationException($"HSMS transaction failed with reason {raw.Code}.");
if (raw.Kind == HsmsEventKind.ProtocolError)
throw new InvalidOperationException($"HSMS protocol error {raw.Code}, detail {raw.Detail}.");
if (raw.Kind == HsmsEventKind.TransportError)
throw new InvalidOperationException($"HSMS transport error {raw.Code}, detail {raw.Detail}.");
return raw;
}
static class DemoOptions
{
internal static HsmsOptions Transport(HsmsMode mode, SecsLimits limits) => new()
{
Mode = mode, SessionId = 7, InitialSystemBytes = 1,
T3Milliseconds = 2000, T5Milliseconds = 20, T6Milliseconds = 200,
T7Milliseconds = 1000, T8Milliseconds = 500, ConnectTimeoutMilliseconds = 50,
ShutdownTimeoutMilliseconds = 100, WriteTimeoutMilliseconds = 1000,
MaxFrameBytes = 4110, MaxEvents = 128, MaxQueuedBytes = 32768,
MaxWriteFrames = 64, MaxWriteBytes = 16384, MaxIoChunk = 3,
MaxApplicationEvents = 128, MaxApplicationBytes = 32768, PollBudget = 32,
ItemLimits = limits,
};
internal static HsmsWorkerOptions Worker() => new()
{
CommandCapacity = 64, MaxCommandBytes = 65536, EventCapacity = 128,
MaxEventBytes = 65536, PollIntervalMilliseconds = 1, ShutdownTimeoutMilliseconds = 5000,
};
internal static GemHostOptions Host(SecsLimits limits) => new()
{
MaxPendingIds = 64, ItemLimits = limits,
};
internal static GemEquipmentOptions Equipment(SecsLimits limits) => new()
{
Model = "PDComm", SoftwareRevision = "0.1.0",
InitialControl = GemControlState.HostOffline, OnlineMode = GemOnlineMode.Remote,
MaxVariables = 8, MaxMetadataBytes = 1024, MaxValueBytes = 4096, ItemLimits = limits,
};
internal static GemReportOptions Reports() => new()
{
MaxEvents = 4, MaxReports = 4, MaxVariablesPerReport = 8, MaxReportsPerEvent = 4,
MaxTotalVariables = 16, MaxTotalLinks = 16,
};
internal static GemAlarmOptions Alarms() => new()
{
MaxAlarms = 4, MaxTotalTextBytes = 128, MaxTextBytes = 120,
};
}

The C API offers the same operations: pdcomm_gem_equipment_configure_alarms, pdcomm_gem_equipment_observe_alarm, pdcomm_gem_equipment_publish_alarm, pdcomm_gem_host_enable_alarm, pdcomm_gem_host_request_alarm_list, pdcomm_gem_host_request_enabled_alarm_list and pdcomm_gem_host_acknowledge_alarm.

examples/qt/alarms.cpp implements the same sixteen steps with the C++ wrappers. The equipment installs its alarms with GemEquipment::configureAlarms and calls GemEquipment::observeAlarm and GemEquipment::publishAlarm. The host uses GemHost::enableAlarm, GemHost::requestAlarmList, GemHost::requestEnabledAlarmList and GemHost::acknowledgeAlarm. Only the worker thread touches the endpoint; received alarms reach the UI thread through invokeQueued.

Copy alarms.cpp and queued_handoff.hpp into an empty folder and add this CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(pdcomm_qt_alarms LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PDCOMM_SDK "" CACHE PATH "Folder that contains include/ and lib/ of the PDComm SDK")
find_package(Qt6 REQUIRED COMPONENTS Core)
find_library(PDCOMM_FFI NAMES pdcomm_ffi pdcomm_ffi.dll PATHS "${PDCOMM_SDK}/lib" NO_DEFAULT_PATH REQUIRED)
add_executable(pdcomm_qt_alarms alarms.cpp)
target_include_directories(pdcomm_qt_alarms PRIVATE "${PDCOMM_SDK}/include")
target_link_libraries(pdcomm_qt_alarms PRIVATE Qt6::Core "${PDCOMM_FFI}")
set_target_properties(pdcomm_qt_alarms PROPERTIES BUILD_RPATH "${PDCOMM_SDK}/lib")

Build it, then start the equipment and the host. Give the host the address the equipment printed:

터미널 창
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPDCOMM_SDK="$PDCOMM_SDK"
cmake --build build
./build/pdcomm_qt_alarms equipment passive 127.0.0.1:0
./build/pdcomm_qt_alarms host active 127.0.0.1:54559

Both sides print PASS QT ALARMS and exit with code 0. If CMake can’t find Qt, add -DCMAKE_PREFIX_PATH=<Qt installation prefix>. On Windows, link against sdk\lib\pdcomm_ffi.dll.lib and keep pdcomm_ffi.dll next to the executable.

Full source: qt/alarms.cpp
qt/alarms.cpp
#include "pdcomm_gem.hpp"
#include "queued_handoff.hpp"
#include <QCoreApplication>
#include <QThread>
#include <QTimer>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <stdexcept>
#include <string>
#include <thread>
#include <utility>
#include <vector>
namespace {
PdcommSecsLimits itemLimits() {
return pdcomm::secsLimits(4096, 4096, 128, 8);
}
PdcommTcpOptions tcpOptions(uint32_t mode) {
PdcommTcpOptions value = {};
value.struct_size = sizeof(value);
value.version = PDCOMM_TCP_OPTIONS_VERSION;
value.mode = mode;
value.session_id = 7;
value.initial_system_bytes = 1000;
value.t3_ms = 2000;
value.t5_ms = 50;
value.t6_ms = 2000;
value.t7_ms = 2000;
value.t8_ms = 1000;
value.connect_timeout_ms = 20;
value.shutdown_timeout_ms = 200;
value.write_timeout_ms = 2000;
value.max_frame_bytes = 4110;
value.max_events = 128;
value.max_queued_bytes = 32768;
value.max_write_frames = 8;
value.max_write_bytes = 16384;
value.max_io_chunk = 3;
value.max_application_events = 128;
value.max_application_bytes = 32768;
value.poll_budget = 16;
value.item_limits = itemLimits();
return value;
}
pdcomm::SecsItem f4(float value) {
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
const std::vector<uint8_t> bytes = {
static_cast<uint8_t>(bits >> 24),
static_cast<uint8_t>(bits >> 16),
static_cast<uint8_t>(bits >> 8),
static_cast<uint8_t>(bits)};
return pdcomm::SecsItem::primitive(PDCOMM_SECS_F4, bytes, itemLimits());
}
pdcomm::SecsItem ascii(const std::string &value) {
return pdcomm::SecsItem::primitive(
PDCOMM_SECS_ASCII,
reinterpret_cast<const uint8_t *>(value.data()), value.size(),
itemLimits());
}
bool sameToken(const PdcommTcpToken &left, const PdcommTcpToken &right) {
return left.endpoint_id == right.endpoint_id &&
left.generation == right.generation &&
left.system_bytes == right.system_bytes &&
left.reserved == right.reserved;
}
void require(bool condition, const char *message) {
if (!condition) throw std::runtime_error(message);
}
template <typename Operation>
bool throwsStatus(Operation operation, PdcommStatus expected) {
try {
operation();
} catch (const pdcomm::Error &error) {
return error.status() == expected;
}
return false;
}
void stopAndDrain(pdcomm::GemEndpoint &endpoint) {
if (!endpoint.tcp().state().closed) endpoint.stop();
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
while (true) {
if (!endpoint.tcp().state().closed) endpoint.poll();
for (auto event = endpoint.nextEvent(); event;
event = endpoint.nextEvent()) {
(void)endpoint.handle(event);
}
if (endpoint.tcp().state().closed ||
std::chrono::steady_clock::now() >= deadline) break;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
require(endpoint.tcp().state().closed, "GEM shutdown timed out");
}
PdcommTcpToken sendStep(pdcomm::GemHost &host, size_t step) {
switch (step) {
case 0: return host.requestAlarmList();
case 1: return host.requestAlarmList({20, 10, 20});
case 2: return host.requestEnabledAlarmList();
case 3: return host.enableAlarm(10, true, true);
case 4: return host.enableAlarm(99, true, true);
case 5: return host.defineReports(1, {{10, {1}}});
case 6: return host.linkReports(2, {{100, {10}}, {200, {10}}});
case 7: return host.enableEvents(true);
case 8: return host.enableAlarm(20, true, false);
case 9: return host.requestEnabledAlarmList();
case 10: return host.requestAlarmList({10});
case 11: return host.enableAlarm(10, false, false);
case 12: return host.requestEnabledAlarmList();
case 13: return host.enableAlarm(10, true, true);
case 14: return host.requestAlarmList();
default: return host.enableAlarm(20, false, true);
}
}
bool stepHasAck(size_t step) {
return step == 3 || step == 4 || step == 5 || step == 6 || step == 7 ||
step == 13 || step == 15;
}
uint8_t expectedAck(size_t step) {
return step == 4 ? 1 : 0;
}
pdcomm::GemRequest requestForStep(size_t step) {
if (step == 0 || step == 1 || step == 10 || step == 14) {
return pdcomm::GemRequest::AlarmList;
}
if (step == 2 || step == 9 || step == 12) {
return pdcomm::GemRequest::EnabledAlarmList;
}
if (step == 5) return pdcomm::GemRequest::DefineReports;
if (step == 6) return pdcomm::GemRequest::LinkReports;
if (step == 7) return pdcomm::GemRequest::EnableEvents;
return pdcomm::GemRequest::EnableAlarm;
}
pdcomm::GemOutcome outcomeForPrimary(size_t step) {
if (step == 0 || step == 1 || step == 10 || step == 14) {
return pdcomm::GemOutcome::AlarmListReplied;
}
if (step == 2 || step == 9 || step == 12) {
return pdcomm::GemOutcome::EnabledAlarmListReplied;
}
if (step == 5) return pdcomm::GemOutcome::DefineReportsAck;
if (step == 6) return pdcomm::GemOutcome::LinkReportsAck;
if (step == 7) return pdcomm::GemOutcome::EnableEventsAck;
return pdcomm::GemOutcome::AlarmEnableAck;
}
pdcomm::GemOutcome outcomeForResponse(size_t step) {
if (step == 0 || step == 1 || step == 10 || step == 14) {
return pdcomm::GemOutcome::AlarmListReceived;
}
if (step == 2 || step == 9 || step == 12) {
return pdcomm::GemOutcome::EnabledAlarmListReceived;
}
if (step == 5) return pdcomm::GemOutcome::DefineReportsAck;
if (step == 6) return pdcomm::GemOutcome::LinkReportsAck;
if (step == 7) return pdcomm::GemOutcome::EnableEventsAck;
return pdcomm::GemOutcome::AlarmEnableAck;
}
void validateList(const pdcomm::GemResult &result, size_t step) {
const auto detail = result.alarmInfo();
require(!detail.isReport && !detail.replyExpected &&
!detail.acknowledged,
"alarm list result flags mismatch");
std::vector<uint32_t> ids;
std::vector<bool> active;
if (step == 0 || step == 14) {
ids = {10, 20}; active = {false, false};
} else if (step == 1) {
ids = {20, 10, 20}; active = {false, false, false};
} else if (step == 2) {
ids = {};
} else if (step == 9) {
ids = {10, 20}; active = {false, false};
} else if (step == 10) {
ids = {10}; active = {true};
} else {
ids = {20}; active = {false};
}
require(detail.count == ids.size(), "alarm list count mismatch");
for (size_t index = 0; index < ids.size(); ++index) {
const auto value = result.alarmAt(index);
require(value.id == ids[index] && value.active == active[index],
"alarm list value mismatch");
require((value.id == 10 && value.category == 2 &&
value.text == "Vacuum lost") ||
(value.id == 20 && value.category == 3 &&
value.text == "Door open"),
"alarm list metadata mismatch");
}
}
enum class UiKind { AlarmSet, EventSet, EventClear, AlarmClearW0 };
struct UiPayload {
bool cancelled = false;
bool host = false;
std::vector<std::shared_ptr<pdcomm::GemResult>> results;
std::vector<std::shared_ptr<pdcomm::SecsItem>> values;
};
struct SharedState {
std::atomic<bool> active{false};
std::atomic<bool> succeeded{false};
std::atomic<int> uiDeliveries{0};
std::atomic<int> activeHeartbeats{0};
std::string failure;
};
class AlarmsWorker final : public QThread {
public:
AlarmsWorker(QCoreApplication *application, std::string role,
std::string mode, std::string address,
std::shared_ptr<SharedState> shared)
: application_(application), role_(std::move(role)),
mode_(std::move(mode)), address_(std::move(address)),
shared_(std::move(shared)) {}
protected:
void run() override {
shared_->active.store(true);
try {
require(QThread::currentThread() != application_->thread(),
"GEM endpoint ran on the UI thread");
auto payload = role_ == "host" ? runHost() : runEquipment();
postCompletion(std::move(payload));
} catch (const pdcomm::Error &error) {
shared_->failure = "PDComm status " +
std::to_string(error.status());
} catch (const std::exception &error) {
shared_->failure = error.what();
}
shared_->active.store(false);
}
private:
void postListen(const std::string &address) {
auto *application = application_;
const auto shared = shared_;
require(pdcomm_qt_example::invokeQueued(
application,
[application, shared, address] {
if (QThread::currentThread() != application->thread()) return;
shared->uiDeliveries.fetch_add(1);
std::fprintf(stdout, "LISTEN %s\n", address.c_str());
std::fflush(stdout);
}), "failed to queue alarm listen notification");
}
pdcomm::TcpEndpoint makeEndpoint() {
const bool activeMode = mode_ == "active";
pdcomm::TcpEndpoint endpoint(
address_, tcpOptions(activeMode ? PDCOMM_TCP_ACTIVE
: PDCOMM_TCP_PASSIVE));
if (!activeMode) postListen(endpoint.localAddress());
return endpoint;
}
int requestUiDecision(const std::shared_ptr<pdcomm::GemResult> &result,
UiKind kind) {
const auto decision = std::make_shared<std::atomic<int>>(-1);
auto *application = application_;
const auto shared = shared_;
require(pdcomm_qt_example::invokeQueued(
application,
[application, shared, result, decision, kind] {
try {
require(QThread::currentThread() == application->thread(),
"alarm inspection missed the UI thread");
if (kind == UiKind::AlarmSet ||
kind == UiKind::AlarmClearW0) {
const auto info = result->alarmInfo();
const auto value = result->alarmAt(0);
const bool active = kind == UiKind::AlarmSet;
require(info.isReport && info.count == 1 &&
info.replyExpected == active &&
!info.acknowledged && value.id == 10 &&
value.category == 2 &&
value.text == "Vacuum lost" &&
value.active == active,
"alarm report payload mismatch");
} else {
const size_t index = kind == UiKind::EventSet ? 0 : 1;
const auto info = result->eventInfo();
const auto report = result->eventReport(0);
require(info.dataId == (index == 0 ? 61U : 62U) &&
info.eventId ==
(index == 0 ? 100U : 200U) &&
info.reportCount == 1 &&
!info.acknowledged && report.id == 10 &&
report.valueCount == 1 &&
result->value(0, 0).payload() ==
f4(21.5F).payload(),
"alarm event payload mismatch");
}
shared->uiDeliveries.fetch_add(1);
const int response = kind == UiKind::EventClear ? 7 :
(kind == UiKind::AlarmClearW0 ? 64 : 0);
decision->store(response, std::memory_order_release);
} catch (...) {
decision->store(-2, std::memory_order_release);
}
}), "failed to queue alarm UI decision");
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
int value = -1;
while ((value = decision->load(std::memory_order_acquire)) == -1 &&
std::chrono::steady_clock::now() < deadline &&
!isInterruptionRequested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
require(value >= 0, "queued alarm decision failed");
return value;
}
UiPayload runHost() {
UiPayload payload;
payload.host = true;
{
auto endpoint = makeEndpoint();
pdcomm::GemHost host(
std::move(endpoint), pdcomm::gemHostOptions(itemLimits(), 64));
host.start();
bool beginSent = false;
bool firstSent = false;
size_t step = 0;
size_t alarmReports = 0;
size_t eventReports = 0;
bool waitPhaseOne = false;
bool waitPhaseTwo = false;
PdcommTcpToken requested[16] = {};
PdcommTcpToken communicationToken = {};
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(15);
while (step < 16 && std::chrono::steady_clock::now() < deadline &&
!isInterruptionRequested()) {
host.poll();
if (!beginSent &&
host.tcp().state().state == PDCOMM_TCP_SELECTED) {
communicationToken = host.beginCommunication();
beginSent = true;
}
std::shared_ptr<pdcomm::GemResult> alarm;
std::shared_ptr<pdcomm::GemResult> eventReport;
for (auto event = host.nextEvent(); event;
event = host.nextEvent()) {
auto result = host.handle(event);
const auto info = result.info();
if (info.outcome ==
pdcomm::GemOutcome::CommunicationEstablished) {
require(info.kind == pdcomm::GemResultKind::Response &&
info.request ==
pdcomm::GemRequest::Establish &&
sameToken(info.token,
communicationToken),
"communication response token mismatch");
if (!firstSent) {
requested[0] = sendStep(host, 0);
firstSent = true;
}
} else if (info.outcome ==
pdcomm::GemOutcome::AlarmReceived) {
alarm = std::make_shared<pdcomm::GemResult>(
std::move(result));
} else if (info.outcome ==
pdcomm::GemOutcome::EventReceived) {
eventReport = std::make_shared<pdcomm::GemResult>(
std::move(result));
} else if (info.kind ==
pdcomm::GemResultKind::Response) {
require(step < 16 &&
info.request == requestForStep(step) &&
info.outcome == outcomeForResponse(step) &&
sameToken(info.token, requested[step]),
"alarm response token mismatch");
if (stepHasAck(step)) {
require(info.acknowledgementPresent &&
info.acknowledgement ==
expectedAck(step),
"alarm response ACK mismatch");
} else {
validateList(result, step);
}
++step;
if (step == 8) {
requested[8] = sendStep(host, 8);
++step;
requested[9] = sendStep(host, 9);
} else if (step == 10) {
waitPhaseOne = true;
} else if (step == 11) {
requested[11] = sendStep(host, 11);
++step;
requested[12] = sendStep(host, 12);
} else if (step == 13) {
waitPhaseTwo = true;
} else if (step == 14) {
// The W0 alarm report starts the next step.
} else if (step < 16) {
requested[step] = sendStep(host, step);
}
}
}
if (alarm) {
const bool w1 = alarm->alarmInfo().replyExpected;
const UiKind kind = w1 ? UiKind::AlarmSet
: UiKind::AlarmClearW0;
const int ack = requestUiDecision(alarm, kind);
if (w1) {
require(ack == 0, "alarm UI ACK mismatch");
host.acknowledgeAlarm(*alarm,
static_cast<uint8_t>(ack));
} else {
require(ack == 64,
"W0 alarm unexpectedly requested an ACK");
requested[14] = sendStep(host, 14);
}
payload.results.push_back(alarm);
++alarmReports;
}
if (eventReport) {
const bool first = eventReports == 0;
const int ack = requestUiDecision(
eventReport, first ? UiKind::EventSet
: UiKind::EventClear);
payload.values.push_back(
std::make_shared<pdcomm::SecsItem>(
eventReport->value(0, 0)));
host.acknowledgeEvent(*eventReport,
static_cast<uint8_t>(ack));
payload.results.push_back(eventReport);
++eventReports;
}
if (waitPhaseOne && alarmReports >= 1 && eventReports >= 1) {
waitPhaseOne = false;
requested[10] = sendStep(host, 10);
}
if (waitPhaseTwo && eventReports >= 2) {
waitPhaseTwo = false;
requested[13] = sendStep(host, 13);
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (isInterruptionRequested()) {
payload.cancelled = true;
} else {
require(step == 16 && alarmReports == 2 && eventReports == 2,
"host alarm exchange timed out");
}
stopAndDrain(host);
}
return payload;
}
UiPayload runEquipment() {
UiPayload payload;
{
auto endpoint = makeEndpoint();
pdcomm::GemEquipment equipment(
std::move(endpoint), itemLimits(),
pdcomm::GemControl::HostOffline,
pdcomm::GemOnlineMode::Remote, "PDComm", "0.1.0", 8,
1024, 4096);
equipment.configureReports(
pdcomm::gemReportOptions(4, 4, 8, 4, 16, 16), {100, 200});
equipment.configureAlarms(
pdcomm::gemAlarmOptions(4, 128, 120),
{{10, 2, "Vacuum lost", true, 100, true, 200},
{20, 3, "Door open", false, 0, false, 0}});
equipment.registerStatus(1, "Temperature", "C", f4(21.5F));
equipment.registerStatus(2, "Lot", "", ascii("LOT-001"));
equipment.start();
size_t step = 0;
size_t f7Replies = 0;
size_t alarmAcks = 0;
size_t eventAcks = 0;
PdcommTcpToken alarmToken = {};
PdcommTcpToken eventTokens[2] = {};
bool peerStopped = false;
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(15);
while (std::chrono::steady_clock::now() < deadline &&
!isInterruptionRequested()) {
equipment.poll();
for (auto event = equipment.nextEvent(); event;
event = equipment.nextEvent()) {
auto result = equipment.handle(event);
const auto info = result.info();
if (info.kind == pdcomm::GemResultKind::Primary &&
step < 16 && info.outcome == outcomeForPrimary(step)) {
if (stepHasAck(step)) {
require(info.acknowledgementPresent &&
info.acknowledgement ==
expectedAck(step),
"equipment alarm ACK mismatch");
}
if (info.outcome ==
pdcomm::GemOutcome::EnabledAlarmListReplied) {
++f7Replies;
if (f7Replies == 2) {
const auto change =
equipment.observeAlarm(10, true);
require(change.changed && change.active &&
change.reportingEnabled &&
change.eventPresent &&
change.eventId == 100,
"set observation mismatch");
alarmToken = equipment.publishAlarm(10, true);
eventTokens[0] =
equipment.publishEvent(61, 100);
} else if (f7Replies == 3) {
const auto change =
equipment.observeAlarm(10, false);
require(change.changed && !change.active &&
!change.reportingEnabled &&
change.eventPresent &&
change.eventId == 200,
"clear observation mismatch");
require(throwsStatus(
[&] { (void)equipment.publishAlarm(
10, true); },
PDCOMM_INVALID_STATE),
"disabled alarm publish was accepted");
eventTokens[1] =
equipment.publishEvent(62, 200);
}
}
if (step == 13) {
require(equipment.alarmState().enabledCount == 2,
"alarm enable state mismatch");
const auto before = equipment.state().pendingCount;
(void)equipment.publishAlarm(10, false);
require(equipment.state().pendingCount == before,
"W0 alarm occupied native pending");
}
++step;
} else if (info.kind ==
pdcomm::GemResultKind::Response &&
info.outcome == pdcomm::GemOutcome::AlarmAck) {
require(alarmAcks == 0 &&
info.acknowledgementPresent &&
info.acknowledgement == 0 &&
sameToken(info.token, alarmToken),
"alarm report ACK mismatch");
++alarmAcks;
} else if (info.kind ==
pdcomm::GemResultKind::Response &&
info.outcome == pdcomm::GemOutcome::EventAck) {
require(eventAcks < 2 &&
info.acknowledgementPresent &&
info.acknowledgement ==
(eventAcks == 0 ? 0 : 7) &&
sameToken(info.token,
eventTokens[eventAcks]),
"alarm event ACK mismatch");
++eventAcks;
}
}
if (step == 16 && alarmAcks == 1 && eventAcks == 2 &&
equipment.tcp().state().state != PDCOMM_TCP_SELECTED) {
peerStopped = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (isInterruptionRequested()) {
payload.cancelled = true;
} else {
const auto state = equipment.alarmState();
require(step == 16 && alarmAcks == 1 && eventAcks == 2 &&
peerStopped && state.activeCount == 0 &&
state.enabledCount == 1 &&
equipment.alarmAt(0).enabled &&
!equipment.alarmAt(1).enabled,
"equipment alarm exchange timed out");
}
stopAndDrain(equipment);
}
return payload;
}
void postCompletion(UiPayload payload) {
const auto completion = std::make_shared<std::atomic<int>>(0);
auto *application = application_;
const auto shared = shared_;
require(pdcomm_qt_example::invokeQueued(
application,
[application, shared, payload, completion] {
try {
require(QThread::currentThread() == application->thread(),
"completion missed the UI thread");
if (!payload.cancelled && payload.host) {
require(payload.results.size() == 4 &&
payload.values.size() == 2,
"retained alarm results are missing");
require(payload.values[0]->payload() ==
f4(21.5F).payload() &&
payload.values[1]->payload() ==
f4(21.5F).payload(),
"retained alarm event values changed");
size_t acknowledged = 0;
for (const auto &result : payload.results) {
const auto info = result->info();
if (info.outcome ==
pdcomm::GemOutcome::AlarmReceived) {
acknowledged +=
result->alarmInfo().acknowledged ? 1 : 0;
} else {
require(result->eventInfo().acknowledged,
"retained event was not acknowledged");
}
}
require(acknowledged == 1,
"retained alarm ACK count mismatch");
}
shared->uiDeliveries.fetch_add(1);
completion->store(1);
} catch (...) {
completion->store(-1);
}
}), "failed to queue alarm completion");
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
while (completion->load() == 0 &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
require(completion->load() == 1,
"queued completion validation failed");
require(shared_->activeHeartbeats.load() > 0,
"UI heartbeat did not run while worker was active");
shared_->succeeded.store(true);
}
QCoreApplication *application_;
std::string role_;
std::string mode_;
std::string address_;
std::shared_ptr<SharedState> shared_;
};
} // namespace
int main(int argc, char **argv) {
QCoreApplication application(argc, argv);
if (argc < 4 || argc > 5 ||
(std::string(argv[1]) != "host" &&
std::string(argv[1]) != "equipment") ||
(std::string(argv[2]) != "active" &&
std::string(argv[2]) != "passive") ||
(argc == 5 && std::string(argv[4]) != "--cancel")) {
std::fprintf(stderr,
"usage: pdcomm_qt_alarms ROLE MODE ADDRESS [--cancel]\n");
return 2;
}
const bool cancel = argc == 5;
const auto shared = std::make_shared<SharedState>();
AlarmsWorker worker(&application, argv[1], argv[2], argv[3], shared);
QTimer heartbeat;
heartbeat.setInterval(1);
QObject::connect(&heartbeat, &QTimer::timeout, &application,
[shared, &application] {
if (QThread::currentThread() == application.thread() &&
shared->active.load()) {
shared->activeHeartbeats.fetch_add(1);
}
});
heartbeat.start();
QTimer cancelTimer;
cancelTimer.setSingleShot(true);
QObject::connect(&cancelTimer, &QTimer::timeout, &application,
[&worker] { worker.requestInterruption(); });
QTimer watchdog;
watchdog.setSingleShot(true);
QObject::connect(&watchdog, &QTimer::timeout, &application,
[&worker] { worker.requestInterruption(); });
QObject::connect(&worker, &QThread::started, &application,
[&] {
watchdog.start(18000);
if (cancel) cancelTimer.start(20);
});
QObject::connect(&worker, &QThread::finished, &application,
[&] {
worker.wait();
QTimer::singleShot(0, &application, [&] {
if (!shared->succeeded.load() ||
shared->uiDeliveries.load() == 0 ||
shared->activeHeartbeats.load() == 0) {
std::fprintf(stderr, "FAIL QT ALARMS: %s\n",
shared->failure.empty()
? "UI/worker validation failed"
: shared->failure.c_str());
application.exit(1);
return;
}
std::fprintf(stdout, "PASS QT ALARMS\n");
std::fflush(stdout);
application.quit();
});
});
worker.start();
const int result = application.exec();
worker.requestInterruption();
worker.wait();
return result;
}
  1. In an Equipment window, first reserve the example catalog of the Reports & Events workspace (Load example, Use on next start). Example alarm 1 uses that catalog’s CEID 10 as its set event. Then open the Catalog tab of the Alarms workspace, click Load example and Use on next start, and start the session.
  2. In a Host window, establish communication, open the Traffic tab, enter an alarm ID in Enable ALID and click Enable. Check the lists with Request list and Request enabled list.
  3. In the Equipment window’s Traffic tab, enter the same ID in Observe ALID, choose Set active and click Observe. Then enter the ID in Publish ALID, choose W1 and click Publish.
  4. In the Host window, select the received alarm, choose an ACKC5 value and click Acknowledge.
Equipment Catalog tab of Alarms with the two example alarms in the draft and in the actual session catalog, alarm 1 set.Equipment Catalog tab of Alarms with the two example alarms in the draft and in the actual session catalog, alarm 1 set.
Host Traffic tab of Alarms with the W1 report of alarm 1 pending acknowledgement.Host Traffic tab of Alarms with the W1 report of alarm 1 pending acknowledgement.

Observe alone sends nothing. Click Publish to tell the host.

Symptom Cause Fix
PublishAlarmAsync fails with InvalidState. The host hasn’t enabled reporting for that alarm, or the session isn’t communicating. Check that the host enabled the alarm with S5F3.
S5F4 returns ACKC5 1. The ALID doesn’t exist on the equipment. List the equipment’s alarms with S5F5 first.
You changed an alarm but the host receives nothing. ObserveAlarmAsync only changes the state. Call PublishAlarmAsync afterwards.
AcknowledgeAlarmAsync fails with InvalidState. You can’t answer a report sent without the W-bit (W0). A second answer to the same result fails with StaleToken. Check ReplyExpected and answer each W1 report once.
The equipment’s W1 S5F1 fails after T3. The host didn’t reply with S5F2. Call AcknowledgeAlarmAsync on the host.
Alarm-linked events don’t arrive. The CEID is disabled, or the equipment application never published the event. An enabled CEID without linked reports still arrives, as an S6F11 with an empty report list. Configure it as shown in Reports and events.
  • The examples exit when the scenario ends. Stop a hung program with Ctrl+C.
  • Delete $PDCOMM_ROOT/build/managed and the build folder of your Qt project when you no longer need them.