콘텐츠로 이동
무료 평가 시작

알람

알람(alarm)은 장비(equipment)의 이상 상태를 호스트(host)에 알립니다. 이 튜토리얼에서는 호스트가 알람 보고를 켜고 목록을 조회하며, 장비가 알람 발생과 해제를 관찰하고 보고하는 흐름을 직접 실행합니다. 알람에 연결한 수집 이벤트(collection event)도 함께 다룹니다.

  • S5F1/S5F2(알람 보고), S5F3/S5F4(알람 활성화), S5F5/S5F6(알람 목록), S5F7/S5F8(켜진 알람 목록)을 실행합니다.
  • 알람 상태를 바꾸는 일(관찰)과 호스트에 알리는 일(보고)이 PDComm에서 분리되어 있다는 점을 이해합니다.
  • W 비트(W-bit)를 켠 알람 보고와 끈 알람 보고의 차이를 확인합니다.
  • 환경 준비를 마친 SDK와 셸입니다. 이 페이지는 PDCOMM_ROOT, PDCOMM_SDK, PDCOMM_NATIVE, PDCOMM_EXAMPLES(Windows는 $Root, $Sdk, $Native, $Examples)를 씁니다.
  • .NET 10 SDK입니다. Qt 예제에는 Qt 6(Core 모듈), CMake 3.20 이상, Ninja 같은 빌드 도구가 필요합니다.
  • 보고서와 이벤트를 먼저 읽으면 알람 연동 이벤트를 이해하기 쉽습니다.

알람 하나는 ALID(알람 ID), ALCD(알람 코드), ALTX(알람 문자열)로 이뤄집니다. ALCD의 최상위 비트(0x80)는 발생(set), 아래 7비트는 분류(category)입니다. 예제 장비는 다음 알람을 정의합니다.

ALID 분류 ALTX 발생 시 CEID 해제 시 CEID
10 2 Vacuum lost 100 200
20 3 Door open 없음 없음

처음에는 두 알람 모두 보고가 꺼져 있고 발생 상태도 아닙니다. 장비에는 SVID 1 Temperature(F4 21.5)와 CEID 100, 200도 있습니다.

PDComm.GemAlarmsDemo의 호스트는 통신 수립 뒤 16단계를 보냅니다.

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
  • S5F5에 빈 목록을 보내면 모든 알람을, ALID 목록을 보내면 요청한 순서대로(중복 포함) 돌려줍니다.
  • 8단계와 11단계는 W 비트를 끈 S5F3입니다. 장비는 응답하지 않으므로 호스트는 바로 다음 요청으로 넘어갑니다.
  • 알람 상태가 바뀌어도 장비는 저절로 메시지를 보내지 않습니다. 장비 애플리케이션이 상태를 관찰(observe)한 뒤 직접 S5F1과 연결된 S6F11을 보냅니다.
  • 12단계에서 장비는 해제를 관찰하지만, 호스트가 ALID 10의 보고를 꺼 두었으므로 S5F1을 보내지 못합니다. 해제 이벤트 CEID 200은 별개의 이벤트 보고이므로 보냅니다.
  • 13단계 뒤의 S5F1은 W 비트를 끈 보고입니다. 호스트가 여기에 S5F2를 보내려고 하면 SDK가 InvalidState로 거부합니다.
  • 시나리오가 끝나면 장비의 알람 상태는 알람 2개, 발생 0개, 보고 켜짐 1개(ALID 10)입니다.
터미널 창
dotnet build "$Examples\dotnet\PDComm.GemAlarmsDemo\PDComm.GemAlarmsDemo.csproj" -c Release "-p:PDCommSdk=$Sdk" --artifacts-path "$Root\build\managed"

빌드가 끝나면 Build succeeded.와 함께 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

다른 터미널에서 장비가 출력한 주소를 넣어 실행합니다.

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

호스트와 장비가 각각 다음 줄을 출력하고 종료 코드 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는 GemAlarmConfiguration에 GemAlarmDefinition 두 개를 담아 장비를 시작합니다. 정의의 네 번째와 다섯 번째 값이 발생 CEID와 해제 CEID입니다. 이 CEID는 함께 넘긴 GemReportConfiguration에 있어야 합니다.

S5F3, S5F5, S5F7은 SDK가 알람 카탈로그로 자동 응답합니다. RunEquipmentAsync는 결과의 Outcome을 보고 시나리오를 진행합니다.

  • ObserveAlarmAsync는 로컬 알람 상태만 바꾸고 GemAlarmChange를 돌려줍니다. 여기에는 상태가 바뀌었는지, 호스트가 보고를 켰는지, 이 변화에 연결된 CEID가 무엇인지가 들어 있습니다. 메시지는 보내지 않습니다.
  • PublishAlarmAsync가 현재 상태로 S5F1을 보냅니다. 두 번째 인수가 W 비트입니다. 호스트가 그 알람의 보고를 꺼 두었으면 PdcommException(InvalidState)을 던집니다.
  • 알람 연동 이벤트는 GemAlarmChange.EventId를 보고 PublishEventAsync로 따로 보냅니다.
  • 마지막에 GetAlarmStateAsync로 알람 수, 발생 수, 보고가 켜진 수, 문자열 바이트 수를 확인합니다.

SendStepAsync는 다음 API로 요청을 만듭니다.

목록 응답은 GetAlarmInfo로 개수를, GetAlarmAt로 각 알람의 ALID, 분류, 발생 여부, ALTX를 읽습니다. Outcome이 AlarmReceived인 결과는 장비의 S5F1입니다. W 비트가 켜진 보고에만 AcknowledgeAlarmAsync로 S5F2를 보냅니다. ACKC5 0은 수락입니다.

전체 코드: 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,
};
}

C API에서는 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, pdcomm_gem_host_acknowledge_alarm가 같은 역할을 합니다.

examples/qt/alarms.cpp는 같은 16단계를 C++ 래퍼로 구현합니다. 장비는 GemEquipment::configureAlarms로 알람을 설치하고 GemEquipment::observeAlarm, GemEquipment::publishAlarm을 호출합니다. 호스트는 GemHost::enableAlarm, GemHost::requestAlarmList, GemHost::requestEnabledAlarmList, GemHost::acknowledgeAlarm을 씁니다. 끝점은 작업 스레드에서만 다루고, 받은 알람은 invokeQueued로 UI 스레드에 넘겨 검사합니다.

빈 폴더에 alarms.cpp와 queued_handoff.hpp를 복사하고 다음 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")

빌드하고 장비와 호스트를 차례로 실행합니다. 호스트에는 장비가 출력한 주소를 넣습니다.

터미널 창
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

양쪽 모두 PASS QT ALARMS를 출력하고 0으로 끝납니다. Qt를 찾지 못하면 -DCMAKE_PREFIX_PATH=<Qt 설치 경로>를 추가합니다. Windows에서는 sdk\lib\pdcomm_ffi.dll.lib에 링크하고 실행 파일 옆에 pdcomm_ffi.dll을 둡니다.

전체 코드: 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. 장비 역할 창에서 먼저 Reports & Events 작업공간의 예제 카탈로그를 예약합니다(Load example, Use on next start). 예제 알람 1은 이 카탈로그의 CEID 10을 발생 이벤트로 씁니다. 이어서 Alarms 작업공간의 Catalog 탭에서 Load example과 Use on next start를 누르고 세션을 시작합니다.
  2. 호스트 창에서 통신을 수립하고 Traffic 탭의 Enable ALID에 알람 ID를 넣어 Enable을 누릅니다. Request list와 Request enabled list로 목록을 확인합니다.
  3. 장비 창의 Traffic 탭에서 Observe ALID에 같은 ID를 넣고 Set active를 고른 뒤 Observe를 누릅니다. 이어서 Publish ALID에 ID를 넣고 W1을 골라 Publish를 누릅니다.
  4. 호스트 창에서 받은 알람을 고르고 ACKC5 값을 정한 뒤 Acknowledge를 누릅니다.
장비의 Alarms Catalog 탭. 예제 알람 두 개가 초안과 실제 세션 카탈로그에 있고, 알람 1이 Active 상태입니다.장비의 Alarms Catalog 탭. 예제 알람 두 개가 초안과 실제 세션 카탈로그에 있고, 알람 1이 Active 상태입니다.
호스트의 Alarms Traffic 탭. 알람 1의 W1 보고가 확인을 기다리는 Pending 상태입니다.호스트의 Alarms Traffic 탭. 알람 1의 W1 보고가 확인을 기다리는 Pending 상태입니다.

Observe만 누르면 메시지가 나가지 않습니다. 호스트에 알리려면 Publish를 눌러야 합니다.

증상 원인 해결
PublishAlarmAsync가 InvalidState로 실패합니다. 호스트가 그 알람의 보고를 켜지 않았거나 통신 중이 아닙니다. 호스트가 S5F3로 보고를 켰는지 확인합니다.
S5F4 ACKC5 1 장비에 없는 ALID입니다. S5F5로 장비의 알람 목록을 먼저 확인합니다.
알람 상태를 바꿨는데 호스트가 아무것도 받지 못합니다. ObserveAlarmAsync는 상태만 바꿉니다. 이어서 PublishAlarmAsync를 호출합니다.
AcknowledgeAlarmAsync가 InvalidState로 실패합니다. W 비트가 꺼진 보고(W0)에는 응답할 수 없습니다. 같은 결과에 두 번 응답하면 StaleToken으로 실패합니다. 결과의 ReplyExpected를 확인하고 W1 보고에만 한 번 응답합니다.
W1 S5F1을 보낸 장비가 T3 뒤에 실패합니다. 호스트가 S5F2로 응답하지 않았습니다. 호스트에서 AcknowledgeAlarmAsync를 호출합니다.
알람 연동 이벤트가 오지 않습니다. 그 CEID가 꺼져 있거나, 장비 애플리케이션이 이벤트를 보내지 않았습니다. 켜져 있지만 보고서가 연결되지 않은 CEID는 보고서 목록이 빈 S6F11로 옵니다. 보고서와 이벤트의 순서로 설정합니다.
  • 예제는 시나리오를 마치면 스스로 끝납니다. 멈춘 프로그램은 Ctrl+C로 끝냅니다.
  • 필요 없으면 $PDCOMM_ROOT/build/managed와 Qt 예제 폴더의 build를 지웁니다.