원격 명령
원격 명령(remote command)은 호스트가 장비에 작업을 지시하는 GEM 기능입니다. 호스트는 S2F41(Host Command Send)이나 S2F49(Enhanced Remote Command)를 보냅니다. 장비는 명령을 검증하고, 수행 여부를 정해 S2F42나 S2F50으로 답합니다. 실제 작업이 끝났다는 사실은 보통 별도의 수집 이벤트(collection event)로 알립니다.
이 튜토리얼에서는 PDComm 예제 두 개로 이 흐름 전체를 실행합니다. 이어서 같은 흐름을 Qt 예제와 시뮬레이터 GUI에서 확인합니다.
- S2F41 명령의 RCMD, CPNAME/CPVAL, HCACK, CPACK이 어떻게 오가는지 확인합니다.
- 장비의 처리 순서인 수신 시 검증 → 애플리케이션 결정 → 응답을 코드로 따라갑니다.
- 수락(HCACK 0, 4)과 작업 완료를 구분하고, 완료를 이벤트 보고(S6F11)로 알리는 방법을 익힙니다.
- S2F49 확장 명령의 대상 객체(object)와 중첩 파라미터, 재귀 CEPACK 응답을 확인합니다.
준비물
섹션 제목: “준비물”- 개발 환경 준비에서 정한 변수
PDCOMM_ROOT,PDCOMM_SDK,PDCOMM_NATIVE,PDCOMM_EXAMPLES(PowerShell은$Root,$Sdk,$Native,$Examples) - .NET 10 SDK
- Qt 예제를 빌드하려면 Qt 6 Core, CMake 3.20 이상, C++17 컴파일러
- 완료 이벤트를 이해하려면 보고서와 이벤트를 먼저 보면 좋습니다.
원격 명령의 흐름
섹션 제목: “원격 명령의 흐름”Host Equipment | S2F41 RCMD + [CPNAME, CPVAL]... | |-------------------------------------->| 1. validation on receipt | | (command, online remote, parameters) | | 2. application decision (HCACK) | S2F42 HCACK + [CPNAME, CPACK]... |<--------------------------------------| 3. reply | | 4. work (HCACK 0: before the reply, | | HCACK 4: after the reply) | S6F11 completion event | |<--------------------------------------| 5. completion report | S6F12 ACKC6 | |-------------------------------------->|PDComm 장비는 명령 카탈로그를 미리 등록합니다(GemCommandDefinition). 카탈로그에는 명령 이름, 파라미터 정의(GemCommandParameterDefinition), 온라인 원격(online remote) 상태 요구 여부, 완료 이벤트 CEID가 들어갑니다. S2F41을 받으면 PDComm이 다음을 먼저 검사합니다.
| 검사 | 실패 시 HCACK | 실패 시 CPACK |
|---|---|---|
| 등록된 명령인가 | 1 | - |
| 온라인 원격 상태를 요구하는 명령이면 지금 그 상태인가 | 2 | - |
| 필수 파라미터가 있는가, 개수와 정수 범위가 맞는가 | 3 | 2 |
| 값의 형식(format)이 정의와 같은가 | 3 | 3 |
| 모르는 파라미터 이름인가 | 3 | 1 |
검사 결과는 수신 결과의 GemCommandResultInfo로 읽습니다. ValidationAccepted가 false이면 응답에는 검증이 정한 HCACK와 CPACK이 그대로 실리고, 애플리케이션의 결정은 무시됩니다. 검증을 통과한 명령만 애플리케이션이 HCACK를 고릅니다(GemCommandDecision).
| HCACK | GemCommandDecision |
의미 |
|---|---|---|
| 0 | Completed |
명령을 수행했습니다. |
| 1 | UnknownCommand |
명령이 없습니다. |
| 2 | CannotPerformNow |
지금은 수행할 수 없습니다. |
| 3 | ParameterErrors |
파라미터가 잘못됐습니다. CPACK 목록을 함께 보낼 수 있는 유일한 값입니다. |
| 4 | AcceptedForLater |
수락했고 나중에 수행합니다. 완료는 이벤트로 알립니다. |
| 5 | AlreadyInCondition |
이미 요청한 상태입니다. |
| 6 | NoSuchObject |
대상 객체가 없습니다. |
1. 예제 빌드
섹션 제목: “1. 예제 빌드”.NET 예제 두 개를 SDK에 맞춰 빌드합니다.
foreach ($Demo in 'PDComm.GemCommandsDemo', 'PDComm.GemEnhancedCommandsDemo') { dotnet build "$Examples\dotnet\$Demo\$Demo.csproj" -c Release "-p:PDCommSdk=$Sdk" --artifacts-path "$Root\build\managed"}for demo in PDComm.GemCommandsDemo PDComm.GemEnhancedCommandsDemo; do dotnet build "$PDCOMM_EXAMPLES/dotnet/$demo/$demo.csproj" -c Release "-p:PDCommSdk=$PDCOMM_SDK" --artifacts-path "$PDCOMM_ROOT/build/managed"donefor demo in PDComm.GemCommandsDemo PDComm.GemEnhancedCommandsDemo; do dotnet build "$PDCOMM_EXAMPLES/dotnet/$demo/$demo.csproj" -c Release "-p:PDCommSdk=$PDCOMM_SDK" --artifacts-path "$PDCOMM_ROOT/build/managed"done기대 결과: 각 프로젝트마다 Build succeeded.와 0 Error(s)가 출력됩니다. DLL은 build/managed/bin/<예제 이름>/release/<예제 이름>.dll에 생깁니다.
자주 생기는 오류
error CS0246: The type or namespace name 'PDComm' could not be found:-p:PDCommSdk값이sdk폴더를 가리키는지 확인합니다. 그 아래에managed/PDComm.dll이 있어야 합니다.- .NET SDK가 없거나 버전이 낮다는 오류:
dotnet --info로 .NET 10 SDK가 설치됐는지 확인합니다.
2. 기본 명령(S2F41) 예제 실행
섹션 제목: “2. 기본 명령(S2F41) 예제 실행”터미널 두 개를 엽니다. 장비를 Passive로 먼저 시작합니다. 포트 0은 운영체제가 빈 포트를 고르게 합니다.
# 터미널 A: 장비dotnet "$Root\build\managed\bin\PDComm.GemCommandsDemo\release\PDComm.GemCommandsDemo.dll" $Native equipment passive 127.0.0.1:0# 터미널 B: 호스트, 터미널 A가 출력한 주소를 씁니다dotnet "$Root\build\managed\bin\PDComm.GemCommandsDemo\release\PDComm.GemCommandsDemo.dll" $Native host active 127.0.0.1:<port># 터미널 A: 장비dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemCommandsDemo/release/PDComm.GemCommandsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0# 터미널 B: 호스트, 터미널 A가 출력한 주소를 씁니다dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemCommandsDemo/release/PDComm.GemCommandsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port># 터미널 A: 장비dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemCommandsDemo/release/PDComm.GemCommandsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0# 터미널 B: 호스트, 터미널 A가 출력한 주소를 씁니다dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemCommandsDemo/release/PDComm.GemCommandsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port>기대 결과: 장비는 먼저 LISTEN 127.0.0.1:<port>를 출력합니다. 1초 안팎으로 두 프로그램이 모두 다음 줄을 출력하고 종료 코드 0으로 끝납니다.
PASS host active: managed GEM commands worker completed 16 steps, simulated 3 executions, and shut downLISTEN 127.0.0.1:54615PASS equipment passive: managed GEM commands worker completed 16 steps, simulated 3 executions, and shut down예제는 정해진 16단계를 끝까지 검사합니다. 기대와 다른 응답이 오면 Command acknowledgement mismatch. 같은 예외로 멈춥니다. 역할과 모드를 바꿔 호스트를 Passive, 장비를 Active로 실행해도 같은 결과가 나옵니다.
3. 시나리오 따라 읽기
섹션 제목: “3. 시나리오 따라 읽기”장비는 시작할 때 명령 두 개를 등록합니다.
SET_TEMP: 필수 파라미터VALUE(I4, 0~100)와 선택 파라미터TAG(ASCII, 최대 32바이트)를 받습니다. 온라인 원격 상태를 요구하고, 완료 이벤트는 CEID 100입니다.- U1
7: 이름이 숫자인 명령입니다. 파라미터가 없고 온라인 원격 상태를 요구하지 않습니다.
호스트(RunHost, SendStep)는 다음 순서로 요청합니다. 장비(RunEquipment)는 각 요청을 받아 결정하고 GemEquipmentWorker.RespondCommandAsync로 답합니다.
| 단계 | 호스트 요청 | HCACK | 이유 |
|---|---|---|---|
| 0 | SET_TEMP VALUE=10 |
2 | 아직 호스트 오프라인입니다. 검증이 거부하므로 장비의 결정은 쓰이지 않습니다. |
| 1–4 | S1F17 온라인 요청, S2F33 보고서 정의, S2F35 이벤트 연결, S2F37 이벤트 활성화 | 0 | 온라인 원격 상태로 바꾸고 CEID 100 보고를 준비합니다. |
| 5 | MISSING |
1 | 등록되지 않은 명령입니다. |
| 6 | SET_TEMP(파라미터 없음) |
3 | 필수 파라미터 VALUE가 없습니다. |
| 7 | SET_TEMP VALUE=101 |
3 | 범위 0~100을 벗어났습니다. |
| 8 | SET_TEMP VALUE=20 TAG=NOW |
0 | 장비가 상태 변수 3을 20으로 바꾼 뒤 응답하고, 완료 이벤트(DATAID 71)를 보고합니다. |
| 9 | SET_TEMP VALUE=30 TAG=LATER |
4 | 장비가 먼저 응답하고, 그다음 값을 30으로 바꿔 완료 이벤트(DATAID 72)를 보고합니다. |
| 10–13 | U1 7 네 번 |
5, 6, 2, 1 | 검증은 통과했고, 장비 애플리케이션이 HCACK를 차례로 고릅니다. |
| 14 | SET_TEMP VALUE=40 TAG=BAD |
3 | 장비가 ParameterErrors와 CPACK 두 개(VALUE 2, TAG 3)를 보냅니다. |
| 15 | SET_TEMP VALUE=50 TAG=LAST |
0 | 값을 50으로 바꾸고 완료 이벤트(DATAID 73)를 보고합니다. |
“simulated 3 executions”는 8, 9, 15단계에서 장비가 상태 변수 3을 바꾼 일을 가리킵니다. 실제 장비 동작 대신 값 변경으로 작업을 흉내 냅니다. 호스트는 완료 이벤트마다 값(20, 30, 50)을 확인하고 S6F12로 답합니다. 두 번째 이벤트에는 일부러 ACKC6 7을 보내, 호스트가 0이 아닌 확인 코드도 보낼 수 있음을 보여 줍니다.
장비 쪽 핵심은 RunEquipment의 이 순서입니다.
GetCommandInfo()로ValidationAccepted를 확인합니다.- 검증을 통과했으면 명령 이름과 파라미터를 읽고 결정을 고릅니다.
- HCACK 0이면 작업을 먼저 하고,
RespondCommandAsync로 응답합니다. - HCACK 4면 응답을 보낸 다음 작업을 시작합니다.
- 응답의
CompletionEvent가 있으면 작업이 끝난 뒤GemEquipmentWorker.PublishEventAsync로 보고합니다.
호스트는 GemHostWorker.RemoteCommandAsync로 요청하고, 응답의 Acknowledgement(HCACK)와 GetCommandIssue(CPACK)를 읽습니다.
PDComm.GemCommandsDemo/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.GemCommandsDemo <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 limits = new(4096, 4096, 256, 16);using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(25));await using GemWorker worker = role == "host" ? await GemHostWorker.StartAsync(args[3], DemoOptions.Transport(mode, limits), DemoOptions.Host(limits), DemoOptions.Worker(), deadline.Token) : await StartEquipment(args[3], mode, limits, deadline.Token);if (mode == HsmsMode.Passive){ Console.WriteLine($"LISTEN {worker.LocalAddress ?? throw new InvalidOperationException("Missing passive address.")}"); Console.Out.Flush();}await using IAsyncEnumerator<GemEvent> events = worker.ReadEventsAsync(deadline.Token).GetAsyncEnumerator();if (worker is GemHostWorker host) await RunHost(host, events, limits, deadline.Token);else await RunEquipment((GemEquipmentWorker)worker, events, limits, deadline.Token);while (await events.MoveNextAsync()) events.Current.Dispose();await worker.Completion.WaitAsync(deadline.Token);Console.WriteLine($"PASS {role} {args[2]}: managed GEM commands worker completed 16 steps, simulated 3 executions, and shut down");return 0;
static async Task<GemEquipmentWorker> StartEquipment(string address, HsmsMode mode, SecsLimits limits, CancellationToken token){ using SecsItem initial = SecsItem.FromI4([0], limits); using SecsItem command = SecsItem.FromAscii("SET_TEMP", limits); using SecsItem value = SecsItem.FromAscii("VALUE", limits); using SecsItem tag = SecsItem.FromAscii("TAG", limits); using SecsItem numeric = SecsItem.FromU1([7], limits); GemCommandParameterDefinition[] parameters = [ new(value, true, SecsFormat.I4, 1, 1, 0, 100), new(tag, false, SecsFormat.Ascii, 0, 32), ]; var commands = new GemCommandConfiguration( DemoOptions.Commands(), new GemCommandDefinition[] { new(command, parameters, true, 100), new(numeric, Array.Empty<GemCommandParameterDefinition>(), false), }); return await GemEquipmentWorker.StartAsync( address, DemoOptions.Transport(mode, limits), DemoOptions.Equipment(limits), DemoOptions.Worker(), [new GemStatusVariable(3, "CommandValue", "C", initial)], new GemReportConfiguration(DemoOptions.Reports(), new uint[] { 100, 200 }), null, commands, token);}
static async Task RunHost(GemHostWorker host, IAsyncEnumerator<GemEvent> events, SecsLimits limits, CancellationToken token){ bool began = false; bool established = false; bool responseReceived = false; bool eventReceived = false; int step = 0; int eventOrdinal = 0; HsmsToken pending = default; while (step < 16) { if (!began && (await host.GetTransportStateAsync(token)).State == HsmsConnectionState.Selected) { pending = await host.BeginCommunicationAsync(token); began = true; } if (!await events.MoveNextAsync()) throw new InvalidOperationException("Command host event stream ended early."); GemEvent current = events.Current; bool disposed = false; try { GemResult? result = current.Result; if (result is null) continue; GemResultInfo info = result.GetInfo(); if (info.Kind == GemResultKind.Response) { if (info.Token != pending) throw new InvalidOperationException("Command response token mismatch."); if (!established) { if (info.Outcome != GemOutcome.CommunicationEstablished) throw new InvalidOperationException("Communication failed."); established = true; pending = await SendStep(host, step, limits, token); } else { CheckResponse(result, step); responseReceived = true; } } else if (info.Outcome == GemOutcome.EventReceived) { if (step is not (8 or 9 or 15)) throw new InvalidOperationException("Unexpected completion event."); GemEventInfo eventInfo = result.GetEventInfo(); if (eventInfo.DataId != (uint)(71 + eventOrdinal) || eventInfo.EventId != 100 || eventInfo.ReportCount != 1) throw new InvalidOperationException("Completion event metadata mismatch."); using SecsItem value = result.GetEventValue(0, 0); int expected = eventOrdinal == 0 ? 20 : eventOrdinal == 1 ? 30 : 50; if (value.GetI4()[0] != expected) throw new InvalidOperationException("Completion event value mismatch."); Task acknowledgement = host.AcknowledgeEventAsync(result, eventOrdinal == 1 ? (byte)7 : (byte)0, token); current.Dispose(); disposed = true; await acknowledgement; eventReceived = true; eventOrdinal++; } if (responseReceived && (step is not (8 or 9 or 15) || eventReceived)) { responseReceived = false; eventReceived = false; step++; if (step < 16) pending = await SendStep(host, step, limits, token); } } finally { if (!disposed) current.Dispose(); } } if (eventOrdinal != 3) throw new InvalidOperationException("Missing completion events."); await host.StopAsync(token);}
static async Task<HsmsToken> SendStep(GemHostWorker host, int step, SecsLimits limits, CancellationToken token){ if (step == 1) return await host.RequestOnlineAsync(token); if (step == 2) return await host.DefineReportsAsync(1, new GemReportSpec[] { new(10, new uint[] { 3 }) }, token); if (step == 3) return await host.LinkReportsAsync(2, new GemLinkSpec[] { new(100, new uint[] { 10 }) }, token); if (step == 4) return await host.EnableEventsAsync(true, new uint[] { 100 }, token); using SecsItem name = step is >= 10 and <= 13 ? SecsItem.FromU1([7], limits) : SecsItem.FromAscii(step == 5 ? "MISSING" : "SET_TEMP", limits); if (step is 5 or 6 or >= 10 and <= 13) return await host.RemoteCommandAsync(name, Array.Empty<GemCommandParameter>(), token); int commandValue = step switch { 0 => 10, 7 => 101, 8 => 20, 9 => 30, 14 => 40, _ => 50 }; string? tag = step switch { 8 => "NOW", 9 => "LATER", 14 => "BAD", 15 => "LAST", _ => null }; using SecsItem key = SecsItem.FromAscii("VALUE", limits); using SecsItem value = SecsItem.FromI4([commandValue], limits); if (tag is null) return await host.RemoteCommandAsync(name, new GemCommandParameter[] { new(key, value) }, token); using SecsItem tagName = SecsItem.FromAscii("TAG", limits); using SecsItem tagValue = SecsItem.FromAscii(tag, limits); return await host.RemoteCommandAsync(name, new GemCommandParameter[] { new(key, value), new(tagName, tagValue) }, token);}
static void CheckResponse(GemResult result, int step){ byte[] expected = [2, 0, 0, 0, 0, 1, 3, 3, 0, 4, 5, 6, 2, 1, 3, 0]; if (result.GetInfo().Acknowledgement != expected[step]) throw new InvalidOperationException("Command acknowledgement mismatch."); if (step == 14) { if (result.GetCommandInfo().IssueCount != 2) throw new InvalidOperationException("Command issue count mismatch."); using GemCommandError first = result.GetCommandIssue(0); using GemCommandError second = result.GetCommandIssue(1); if (first.Name.GetAscii() != "VALUE" || first.Code != 2 || second.Name.GetAscii() != "TAG" || second.Code != 3) throw new InvalidOperationException("Command issues mismatch."); }}
static async Task RunEquipment(GemEquipmentWorker equipment, IAsyncEnumerator<GemEvent> events, SecsLimits limits, CancellationToken token){ int primaryCount = 0; int commandCount = 0; int numeric = 0; int executions = 0; int eventAcknowledgements = 0; HsmsToken[] eventTokens = new HsmsToken[3]; while (eventAcknowledgements < 3) { if (!await events.MoveNextAsync()) throw new InvalidOperationException("Command equipment event stream ended early."); GemEvent current = events.Current; bool disposed = false; try { GemResult? result = current.Result; if (result is null) continue; GemResultInfo generic = result.GetInfo(); if (generic.Kind == GemResultKind.Primary) primaryCount++; if (generic.Outcome == GemOutcome.CommandReceived) { commandCount++; GemCommandResultInfo info = result.GetCommandInfo(); GemCommandDecision decision = GemCommandDecision.Completed; int value = 0; if (info.ValidationAccepted) { using SecsItem name = result.GetCommandName(); if (name.GetInfo().Format != SecsFormat.Ascii) { GemCommandDecision[] choices = [GemCommandDecision.AlreadyInCondition, GemCommandDecision.NoSuchObject, GemCommandDecision.CannotPerformNow, GemCommandDecision.UnknownCommand]; decision = choices[numeric++]; } else { using GemCommandArgument argument = result.GetCommandParameter(0); value = argument.Value.GetI4()[0]; decision = value == 30 ? GemCommandDecision.AcceptedForLater : value == 40 ? GemCommandDecision.ParameterErrors : GemCommandDecision.Completed; } } if (info.ValidationAccepted && value is 20 or 50) { using SecsItem updated = SecsItem.FromI4([value], limits); await equipment.UpdateStatusAsync(3, updated, token); executions++; } using SecsItem valueName = SecsItem.FromAscii("VALUE", limits); using SecsItem tagName = SecsItem.FromAscii("TAG", limits); GemCommandIssue[] issues = decision == GemCommandDecision.ParameterErrors ? [new(valueName, 2), new(tagName, 3)] : []; Task<GemCommandReply> response = equipment.RespondCommandAsync(result, decision, issues, token); current.Dispose(); disposed = true; GemCommandReply reply = await response; if (info.ValidationAccepted && value == 30) { using SecsItem updated = SecsItem.FromI4([value], limits); await equipment.UpdateStatusAsync(3, updated, token); executions++; } if (reply.CompletionEvent.HasValue) eventTokens[executions - 1] = await equipment.PublishEventAsync((uint)(70 + executions), reply.CompletionEvent.Value, token); } else if (generic.Kind == GemResultKind.Response && generic.RequestKind == GemRequestKind.EventReport) { byte expected = eventAcknowledgements == 1 ? (byte)7 : (byte)0; if (generic.Token != eventTokens[eventAcknowledgements] || generic.Acknowledgement != expected) throw new InvalidOperationException("Completion acknowledgement mismatch."); eventAcknowledgements++; } } finally { if (!disposed) current.Dispose(); } } if (primaryCount != 17 || commandCount != 12 || numeric != 4 || executions != 3) throw new InvalidOperationException("Command scenario counts mismatch."); await equipment.StopAsync(token);}
static class DemoOptions{ internal static GemCommandOptions Commands() => new() { MaxCommands = 8, MaxParametersPerCommand = 8, MaxTotalParameters = 32, MaxIdentifierBytes = 64, MaxTotalIdentifierBytes = 1024, MaxErrors = 8 }; internal static GemReportOptions Reports() => new() { MaxEvents = 4, MaxReports = 4, MaxVariablesPerReport = 8, MaxReportsPerEvent = 4, MaxTotalVariables = 16, MaxTotalLinks = 16 }; 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 HsmsWorkerOptions Worker() => new() { CommandCapacity = 64, EventCapacity = 64, MaxCommandBytes = 131072, MaxEventBytes = 131072, PollIntervalMilliseconds = 1, ShutdownTimeoutMilliseconds = 2000 }; 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 = 65536, MaxWriteFrames = 64, MaxWriteBytes = 65536, MaxIoChunk = 3, MaxApplicationEvents = 128, MaxApplicationBytes = 65536, PollBudget = 32, ItemLimits = limits };}4. 확장 명령(S2F49) 예제 실행
섹션 제목: “4. 확장 명령(S2F49) 예제 실행”S2F49는 S2F41을 넓힌 메시지입니다. DATAID, 대상 객체(OBJSPEC), 명령 이름, 그리고 리스트를 포함할 수 있는 파라미터(CEPVAL)를 보냅니다. 장비는 S2F50으로 HCACK와 파라미터별 확인(CEPACK)을 돌려줍니다. CEPACK은 파라미터 값의 구조를 따라 중첩될 수 있습니다.
2단계와 같은 방법으로 PDComm.GemEnhancedCommandsDemo를 실행합니다.
dotnet "$Root\build\managed\bin\PDComm.GemEnhancedCommandsDemo\release\PDComm.GemEnhancedCommandsDemo.dll" $Native equipment passive 127.0.0.1:0dotnet "$Root\build\managed\bin\PDComm.GemEnhancedCommandsDemo\release\PDComm.GemEnhancedCommandsDemo.dll" $Native host active 127.0.0.1:<port>dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemEnhancedCommandsDemo/release/PDComm.GemEnhancedCommandsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemEnhancedCommandsDemo/release/PDComm.GemEnhancedCommandsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port>dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemEnhancedCommandsDemo/release/PDComm.GemEnhancedCommandsDemo.dll" "$PDCOMM_NATIVE" equipment passive 127.0.0.1:0dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.GemEnhancedCommandsDemo/release/PDComm.GemEnhancedCommandsDemo.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:<port>첫 줄은 장비 터미널, 둘째 줄은 호스트 터미널에서 실행합니다.
기대 결과:
PASS host active: managed GEM commands worker completed 24 steps, simulated 3 executions, and shut down장비는 객체 세 개에 명령을 등록합니다. CHAMBER/1의 APPLY_RECIPE는 필수 LIST 파라미터 RECIPE(TEMP와 STEPS)를 받고 온라인 원격 상태를 요구합니다. CHAMBER/2의 APPLY_RECIPE는 F8 값 하나를 받습니다. 빈 객체 이름 아래에는 숫자 명령 U1 7이 있습니다. 확장 명령 카탈로그는 기본 명령 카탈로그와 따로 관리됩니다.
| 단계 | 호스트 요청 | HCACK | 확인할 점 |
|---|---|---|---|
| 5 | 객체 chamber/1 |
6 | 객체 이름은 대소문자를 구분합니다. |
| 7 | RECIPE 없음 |
3 | CEPACK RECIPE=2가 돌아옵니다. |
| 8 | RECIPE가 ASCII |
3 | 형식 오류로 CEPACK RECIPE=3이 돌아옵니다. |
| 9 | CHAMBER/2, RECIPE=F8 42 |
0 | 같은 명령 이름이라도 객체마다 정의가 다릅니다. |
| 11 | TEMP 30 | 4 | 응답 뒤에 작업과 완료 이벤트가 이어집니다. |
| 12 | TEMP 40 | 3 | 장비가 RECIPE → TEMP 0, STEPS [0, 4, 0] 형태의 재귀 CEPACK 트리를 보냅니다. |
| 19 | 같은 이름을 S2F41로 보냄 | 1 | 기본 카탈로그에 없는 이름이므로 거부됩니다. |
| 20 | 기본 명령 SET_TEMP를 S2F49로 보냄 |
1 | 확장 카탈로그에 없는 이름이므로 거부됩니다. |
| 21, 22 | RECIPE 중복, 모르는 EXTRA |
3 | CEPACK 2와 1이 돌아옵니다. |
호스트는 GemHostWorker.EnhancedRemoteCommandAsync로 보내고, 장비는 GemEquipmentWorker.RespondEnhancedCommandAsync로 답합니다. ParameterErrors일 때만 CEPACK 트리를 직접 넘깁니다.
PDComm.GemEnhancedCommandsDemo/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.GemEnhancedCommandsDemo <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 limits = new(4096, 4096, 256, 16);using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(25));await using GemWorker worker = role == "host" ? await GemHostWorker.StartAsync(args[3], DemoOptions.Transport(mode, limits), DemoOptions.Host(limits), DemoOptions.Worker(), deadline.Token) : await StartEquipment(args[3], mode, limits, deadline.Token);if (mode == HsmsMode.Passive){ Console.WriteLine($"LISTEN {worker.LocalAddress ?? throw new InvalidOperationException("Missing passive address.")}"); Console.Out.Flush();}await using IAsyncEnumerator<GemEvent> events = worker.ReadEventsAsync(deadline.Token).GetAsyncEnumerator();if (worker is GemHostWorker host) await RunHost(host, events, limits, deadline.Token);else await RunEquipment((GemEquipmentWorker)worker, events, limits, deadline.Token);while (await events.MoveNextAsync()) events.Current.Dispose();await worker.Completion.WaitAsync(deadline.Token);Console.WriteLine($"PASS {role} {args[2]}: managed GEM commands worker completed 24 steps, simulated 3 executions, and shut down");return 0;
static async Task<GemEquipmentWorker> StartEquipment(string address, HsmsMode mode, SecsLimits limits, CancellationToken token){ using SecsItem initial = SecsItem.FromI4([0], limits); using SecsItem command = SecsItem.FromAscii("SET_TEMP", limits); using SecsItem value = SecsItem.FromAscii("VALUE", limits); using SecsItem tag = SecsItem.FromAscii("TAG", limits); using SecsItem numeric = SecsItem.FromU1([7], limits); GemCommandParameterDefinition[] parameters = [ new(value, true, SecsFormat.I4, 1, 1, 0, 100), new(tag, false, SecsFormat.Ascii, 0, 32), ]; var commands = new GemCommandConfiguration( DemoOptions.Commands(), new GemCommandDefinition[] { new(command, parameters, true, 100), new(numeric, Array.Empty<GemCommandParameterDefinition>(), false), }); using SecsItem first = SecsItem.FromAscii("CHAMBER/1", limits); using SecsItem second = SecsItem.FromAscii("CHAMBER/2", limits); using SecsItem empty = SecsItem.FromAscii("", limits); using SecsItem apply = SecsItem.FromAscii("APPLY_RECIPE", limits); using SecsItem recipe = SecsItem.FromAscii("RECIPE", limits); GemCommandParameterDefinition[] nested = [new(recipe, true, SecsFormat.List, 2, 2), new(tag, false, SecsFormat.Ascii, 0, 32)]; GemCommandParameterDefinition[] floating = [new(recipe, true, SecsFormat.F8, 1, 1)]; var enhanced = new GemEnhancedCommandConfiguration(DemoOptions.Enhanced(limits), new GemEnhancedObjectDefinition[] { new(first, new GemCommandDefinition[] { new(apply, nested, true, 100) }), new(second, new GemCommandDefinition[] { new(apply, floating, false) }), new(empty, new GemCommandDefinition[] { new(numeric, Array.Empty<GemCommandParameterDefinition>(), false) }), }); return await GemEquipmentWorker.StartAsync( address, DemoOptions.Transport(mode, limits), DemoOptions.Equipment(limits), DemoOptions.Worker(), [new GemStatusVariable(3, "CommandValue", "C", initial)], new GemReportConfiguration(DemoOptions.Reports(), new uint[] { 100, 200 }), null, commands, enhanced, token);}
static async Task RunHost(GemHostWorker host, IAsyncEnumerator<GemEvent> events, SecsLimits limits, CancellationToken token){ bool began = false; bool established = false; bool responseReceived = false; bool eventReceived = false; int step = 0; int eventOrdinal = 0; HsmsToken pending = default; while (step < 24) { if (!began && (await host.GetTransportStateAsync(token)).State == HsmsConnectionState.Selected) { pending = await host.BeginCommunicationAsync(token); began = true; } if (!await events.MoveNextAsync()) throw new InvalidOperationException("Command host event stream ended early."); GemEvent current = events.Current; bool disposed = false; try { GemResult? result = current.Result; if (result is null) continue; GemResultInfo info = result.GetInfo(); if (info.Kind == GemResultKind.Response) { if (info.Token != pending) throw new InvalidOperationException("Command response token mismatch."); if (!established) { if (info.Outcome != GemOutcome.CommunicationEstablished) throw new InvalidOperationException("Communication failed."); established = true; pending = await SendStep(host, step, limits, token); } else { CheckResponse(result, step); responseReceived = true; } } else if (info.Outcome == GemOutcome.EventReceived) { if (step is not (10 or 11 or 23)) throw new InvalidOperationException("Unexpected completion event."); GemEventInfo eventInfo = result.GetEventInfo(); if (eventInfo.DataId != (uint)(71 + eventOrdinal) || eventInfo.EventId != 100 || eventInfo.ReportCount != 1) throw new InvalidOperationException("Completion event metadata mismatch."); using SecsItem value = result.GetEventValue(0, 0); int expected = eventOrdinal == 0 ? 20 : eventOrdinal == 1 ? 30 : 50; if (value.GetI4()[0] != expected) throw new InvalidOperationException("Completion event value mismatch."); Task acknowledgement = host.AcknowledgeEventAsync(result, eventOrdinal == 1 ? (byte)7 : (byte)0, token); current.Dispose(); disposed = true; await acknowledgement; eventReceived = true; eventOrdinal++; } if (responseReceived && (step is not (10 or 11 or 23) || eventReceived)) { responseReceived = false; eventReceived = false; step++; if (step < 24) pending = await SendStep(host, step, limits, token); } } finally { if (!disposed) current.Dispose(); } } if (eventOrdinal != 3) throw new InvalidOperationException("Missing completion events."); await host.StopAsync(token);}
static async Task<HsmsToken> SendStep(GemHostWorker host, int step, SecsLimits limits, CancellationToken token){ if (step == 1) return await host.RequestOnlineAsync(token); if (step == 2) return await host.DefineReportsAsync(1, new GemReportSpec[] { new(10, new uint[] { 3 }) }, token); if (step == 3) return await host.LinkReportsAsync(2, new GemLinkSpec[] { new(100, new uint[] { 10 }) }, token); if (step == 4) return await host.EnableEventsAsync(true, new uint[] { 100 }, token); using SecsItem name = step is >= 13 and <= 18 ? SecsItem.FromU1([7], limits) : SecsItem.FromAscii(step == 6 ? "MISSING" : step == 20 ? "SET_TEMP" : "APPLY_RECIPE", limits); if (step == 19) return await host.RemoteCommandAsync(name, Array.Empty<GemCommandParameter>(), token); using SecsItem scope = SecsItem.FromAscii(step == 5 ? "chamber/1" : step == 9 ? "CHAMBER/2" : step is >= 13 and <= 17 ? "" : "CHAMBER/1", limits); using SecsItem key = SecsItem.FromAscii("RECIPE", limits); int temperature = step switch { 10 => 20, 11 => 30, 12 => 40, 23 => 50, _ => 10 }; using SecsItem value = step == 8 ? SecsItem.FromAscii("invalid", limits) : step == 9 ? SecsItem.FromF8([42], limits) : Recipe(temperature, limits); using SecsItem tag = SecsItem.FromAscii(step == 22 ? "EXTRA" : "TAG", limits); using SecsItem tagValue = SecsItem.FromAscii(step == 10 ? "NOW" : step == 11 ? "LATER" : step == 12 ? "BAD" : "LAST", limits); GemCommandParameter[] parameters = step is 6 or 7 or >= 13 and <= 20 ? [] : step is 10 or 11 or 12 or >= 21 ? [new(key, value), new(step == 21 ? key : tag, step == 21 ? value : tagValue)] : [new(key, value)]; return await host.EnhancedRemoteCommandAsync((uint)(1000 + step), scope, name, parameters, token);}
static void CheckResponse(GemResult result, int step){ byte[] expected = [2, 0, 0, 0, 0, 6, 1, 3, 3, 0, 0, 4, 3, 5, 6, 2, 1, 0, 1, 1, 1, 3, 3, 0]; GemResultInfo info = result.GetInfo(); if (info.Acknowledgement != expected[step]) throw new InvalidOperationException("Enhanced acknowledgement mismatch."); if (step is >= 1 and <= 4) return; if (step == 19) { if (info.RequestKind != GemRequestKind.RemoteCommand || info.Outcome != GemOutcome.CommandAck) throw new InvalidOperationException("Basic command fallback detected."); return; } if (info.RequestKind != GemRequestKind.EnhancedRemoteCommand || info.Outcome != GemOutcome.EnhancedCommandAck) throw new InvalidOperationException("Enhanced response kind mismatch."); GemEnhancedCommandResultInfo command = result.GetEnhancedCommandInfo(); nuint count = step is 7 or 8 or 12 or 21 or 22 ? 1u : 0u; if (command.IsRequest || command.Responded || command.DataId != 0 || command.ParameterCount != 0 || command.IssueCount != count || info.EntryCount != count) throw new InvalidOperationException("Enhanced response metadata mismatch."); using SecsItem actual = result.GetEnhancedCommandAcknowledgements(); using SecsItem expectedItem = step == 12 ? RecursiveAck(DemoOptions.Limits) : count != 0 ? OwnedList(DemoOptions.Limits, Pair(step == 22 ? "EXTRA" : "RECIPE", SecsItem.FromU1([step == 8 ? (byte)3 : step == 22 ? (byte)1 : (byte)2], DemoOptions.Limits), DemoOptions.Limits)) : OwnedList(DemoOptions.Limits); if (!actual.Encode().AsSpan().SequenceEqual(expectedItem.Encode())) throw new InvalidOperationException("Enhanced ACK tree mismatch.");}
static SecsItem OwnedList(SecsLimits limits, params SecsItem[] children){ try { return SecsItem.CreateList(children, limits); } finally { foreach (SecsItem child in children) child.Dispose(); }}static SecsItem Pair(string name, SecsItem value, SecsLimits limits) => OwnedList(limits, SecsItem.FromAscii(name, limits), value);static SecsItem Recipe(int value, SecsLimits limits) => OwnedList(limits, Pair("TEMP", SecsItem.FromI4([value], limits), limits), Pair("STEPS", OwnedList(limits, SecsItem.FromI4([1], limits), SecsItem.FromI4([2], limits), SecsItem.FromI4([3], limits)), limits));static SecsItem RecursiveAck(SecsLimits limits) => OwnedList(limits, Pair("RECIPE", OwnedList(limits, Pair("TEMP", SecsItem.FromU1([0], limits), limits), Pair("STEPS", OwnedList(limits, SecsItem.FromU1([0], limits), SecsItem.FromU1([4], limits), SecsItem.FromU1([0], limits)), limits)), limits));
static async Task RunEquipment(GemEquipmentWorker equipment, IAsyncEnumerator<GemEvent> events, SecsLimits limits, CancellationToken token){ int primaryCount = 0; int commandCount = 0; int numeric = 0; int executions = 0; int eventAcknowledgements = 0; HsmsToken[] eventTokens = new HsmsToken[3]; while (eventAcknowledgements < 3) { if (!await events.MoveNextAsync()) throw new InvalidOperationException("Command equipment event stream ended early."); GemEvent current = events.Current; bool disposed = false; try { GemResult? result = current.Result; if (result is null) continue; GemResultInfo generic = result.GetInfo(); if (generic.Kind == GemResultKind.Primary) primaryCount++; if (generic.Outcome == GemOutcome.EnhancedCommandReceived) { commandCount++; GemEnhancedCommandResultInfo info = result.GetEnhancedCommandInfo(); if (!info.IsRequest || info.Responded || info.DataId < 1000 || info.DataId >= 1024) throw new InvalidOperationException("Enhanced request metadata mismatch."); GemCommandDecision decision = GemCommandDecision.Completed; int value = 0; if (info.ValidationAccepted) { using SecsItem name = result.GetEnhancedCommandName(); if (name.GetInfo().Format != SecsFormat.Ascii) { GemCommandDecision[] choices = [GemCommandDecision.AlreadyInCondition, GemCommandDecision.NoSuchObject, GemCommandDecision.CannotPerformNow, GemCommandDecision.UnknownCommand, GemCommandDecision.Completed]; decision = choices[numeric++]; } else { using SecsItem scope = result.GetEnhancedCommandObject(); using GemCommandArgument argument = result.GetEnhancedCommandParameter(0); if (name.GetAscii() != "APPLY_RECIPE" || argument.Name.GetAscii() != "RECIPE") throw new InvalidOperationException("Command identifier mismatch."); if (info.DataId == 1009) { if (scope.GetAscii() != "CHAMBER/2" || info.ParameterCount != 1 || argument.Value.GetF8()[0] != 42) throw new InvalidOperationException("Floating command mismatch."); } else { if (scope.GetAscii() != "CHAMBER/1" || info.ParameterCount != 2) throw new InvalidOperationException("Recipe object mismatch."); using SecsItem pair = argument.Value.GetChild(0); using SecsItem temperature = pair.GetChild(1); value = temperature.GetI4()[0]; using SecsItem expectedRecipe = Recipe(value, limits); if (!argument.Value.Encode().AsSpan().SequenceEqual(expectedRecipe.Encode())) throw new InvalidOperationException("Recipe mismatch."); decision = value == 30 ? GemCommandDecision.AcceptedForLater : value == 40 ? GemCommandDecision.ParameterErrors : GemCommandDecision.Completed; } } } if (info.ValidationAccepted && value is 20 or 50) { using SecsItem updated = SecsItem.FromI4([value], limits); await equipment.UpdateStatusAsync(3, updated, token); executions++; } using SecsItem? acknowledgements = decision == GemCommandDecision.ParameterErrors ? RecursiveAck(limits) : null; Task<GemCommandReply> response = equipment.RespondEnhancedCommandAsync(result, decision, acknowledgements, token); current.Dispose(); disposed = true; GemCommandReply reply = await response; if (info.ValidationAccepted && value == 30) { using SecsItem updated = SecsItem.FromI4([value], limits); await equipment.UpdateStatusAsync(3, updated, token); executions++; } if (reply.CompletionEvent.HasValue) eventTokens[executions - 1] = await equipment.PublishEventAsync((uint)(70 + executions), reply.CompletionEvent.Value, token); } else if (generic.Outcome == GemOutcome.CommandReceived) { if (result.GetCommandInfo().ValidationAccepted) throw new InvalidOperationException("Enhanced command fell back to basic registry."); await equipment.RespondCommandAsync(result, GemCommandDecision.Completed, ReadOnlyMemory<GemCommandIssue>.Empty, token); } else if (generic.Kind == GemResultKind.Response && generic.RequestKind == GemRequestKind.EventReport) { byte expected = eventAcknowledgements == 1 ? (byte)7 : (byte)0; if (generic.Token != eventTokens[eventAcknowledgements] || generic.Acknowledgement != expected) throw new InvalidOperationException("Completion acknowledgement mismatch."); eventAcknowledgements++; } } finally { if (!disposed) current.Dispose(); } } if (primaryCount != 25 || commandCount != 19 || numeric != 5 || executions != 3) throw new InvalidOperationException("Command scenario counts mismatch."); await equipment.StopAsync(token);}
static class DemoOptions{ internal static readonly SecsLimits Limits = new(4096, 4096, 256, 16); internal static GemEnhancedCommandOptions Enhanced(SecsLimits limits) => new() { MaxObjects = 8, MaxObjectBytes = 64, MaxTotalObjectBytes = 256, MaxCommandsPerObject = 8, MaxTotalCommands = 16, MaxParametersPerCommand = 8, MaxTotalParameters = 32, MaxIdentifierBytes = 64, MaxTotalIdentifierBytes = 1024, MaxErrors = 8, MaxRequestValueBytes = 2048, ItemLimits = limits, }; internal static GemCommandOptions Commands() => new() { MaxCommands = 8, MaxParametersPerCommand = 8, MaxTotalParameters = 32, MaxIdentifierBytes = 64, MaxTotalIdentifierBytes = 1024, MaxErrors = 8 }; internal static GemReportOptions Reports() => new() { MaxEvents = 4, MaxReports = 4, MaxVariablesPerReport = 8, MaxReportsPerEvent = 4, MaxTotalVariables = 16, MaxTotalLinks = 16 }; 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 HsmsWorkerOptions Worker() => new() { CommandCapacity = 64, EventCapacity = 64, MaxCommandBytes = 131072, MaxEventBytes = 131072, PollIntervalMilliseconds = 1, ShutdownTimeoutMilliseconds = 2000 }; 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 = 65536, MaxWriteFrames = 64, MaxWriteBytes = 65536, MaxIoChunk = 3, MaxApplicationEvents = 128, MaxApplicationBytes = 65536, PollBudget = 32, ItemLimits = limits };}5. Qt 예제로 실행
섹션 제목: “5. Qt 예제로 실행”Qt 예제는 같은 16단계(확장 명령은 24단계) 시나리오를 C++ API로 실행합니다. GEM 끝점은 QThread 작업 스레드에서만 동작합니다. 장비가 명령을 받으면 작업 스레드가 결정 요청을 Qt 메인 스레드로 넘기고(invokeQueued), 메인 스레드가 고른 HCACK를 받아 GemEquipment::respondCommand로 응답합니다. UI가 끝점을 직접 만지지 않으므로 화면이 멈추지 않습니다.
예제 파일 commands.cpp, enhanced_commands.cpp, queued_handoff.hpp를 한 폴더에 두고, 같은 폴더에 다음 CMakeLists.txt를 만듭니다.
cmake_minimum_required(VERSION 3.20)project(pdcomm_qt_examples 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)foreach(example commands enhanced_commands) add_executable(pdcomm_qt_${example} ${example}.cpp) target_include_directories(pdcomm_qt_${example} PRIVATE "${PDCOMM_SDK}/include") target_link_libraries(pdcomm_qt_${example} PRIVATE Qt6::Core "${PDCOMM_FFI}") set_target_properties(pdcomm_qt_${example} PROPERTIES BUILD_RPATH "${PDCOMM_SDK}/lib")endforeach()빌드하고 실행합니다. Qt가 기본 경로에 없으면 -DCMAKE_PREFIX_PATH=<Qt 설치 경로>를 더합니다.
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DPDCOMM_SDK="$PDCOMM_SDK"cmake --build build./build/pdcomm_qt_commands equipment passive 127.0.0.1:0./build/pdcomm_qt_commands host active 127.0.0.1:<port>마지막 두 명령은 서로 다른 터미널에서 실행합니다. NAMES의 pdcomm_ffi.dll은 Windows SDK의 가져오기 라이브러리(import library) pdcomm_ffi.dll.lib를 찾기 위한 이름입니다. Windows에서는 실행 전에 sdk\lib 폴더를 PATH에 추가합니다.
기대 결과: 양쪽 모두 PASS QT COMMANDS를 출력합니다. pdcomm_qt_enhanced_commands는 PASS QT ENHANCED PEER를 출력합니다. Qt 장비와 .NET 호스트를 섞어도 같은 시나리오가 통과합니다. 예를 들어 pdcomm_qt_commands equipment passive 127.0.0.1:0에 .NET PDComm.GemCommandsDemo를 호스트로 연결하면 양쪽이 각자의 PASS 줄을 출력합니다.
인수를 빼고 실행하면 사용법이 출력되고 종료 코드 2로 끝납니다.
usage: pdcomm_qt_commands ROLE MODE ADDRESS [--cancel]--cancel을 붙이면 20밀리초 뒤 작업 스레드를 중단시켜 취소 경로를 확인합니다.
commands.cpp 전체 코드
#include "pdcomm_gem.hpp"#include "queued_handoff.hpp"
#include <QCoreApplication>#include <QThread>#include <QTimer>
#include <atomic>#include <chrono>#include <cstdint>#include <cstdio>#include <memory>#include <stdexcept>#include <string>#include <thread>#include <utility>#include <vector>
namespace {
void require(bool condition, const char *message) { if (!condition) throw std::runtime_error(message);}
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 = mode == PDCOMM_TCP_ACTIVE ? 100 : 500; 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 primitive(uint8_t format, const std::vector<uint8_t> &bytes) { return pdcomm::SecsItem::primitive(format, 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());}
pdcomm::SecsItem u1(uint8_t value) { return primitive(PDCOMM_SECS_U1, {value}); }
pdcomm::SecsItem i4(int32_t value) { const auto bits = static_cast<uint32_t>(value); return primitive(PDCOMM_SECS_I4, {static_cast<uint8_t>(bits >> 24), static_cast<uint8_t>(bits >> 16), static_cast<uint8_t>(bits >> 8), static_cast<uint8_t>(bits)});}
int32_t i4Value(const pdcomm::SecsItem &item) { require(item.info().format == PDCOMM_SECS_I4, "expected I4 item"); const auto bytes = item.payload(); require(bytes.size() == 4, "invalid I4 payload"); const uint32_t value = (static_cast<uint32_t>(bytes[0]) << 24) | (static_cast<uint32_t>(bytes[1]) << 16) | (static_cast<uint32_t>(bytes[2]) << 8) | bytes[3]; return static_cast<int32_t>(value);}
std::string asciiValue(const pdcomm::SecsItem &item) { require(item.info().format == PDCOMM_SECS_ASCII, "expected ASCII item"); const auto bytes = item.payload(); return std::string(bytes.begin(), bytes.end());}
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 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)); }}
PdcommTcpToken sendCommand(pdcomm::GemHost &host, size_t step) { if (step == 5) { auto name = ascii("MISSING"); return host.remoteCommand(name); } if (step >= 10 && step <= 13) { auto name = u1(7); return host.remoteCommand(name); } auto command = ascii("SET_TEMP"); if (step == 6) return host.remoteCommand(command); auto valueName = ascii("VALUE"); auto value = i4(step == 0 ? 10 : step == 7 ? 101 : step == 8 ? 20 : step == 9 ? 30 : step == 14 ? 40 : 50); if (step == 0 || step == 7) { return host.remoteCommand(command, {{&valueName, &value}}); } auto tagName = ascii("TAG"); auto tag = ascii(step == 8 ? "NOW" : step == 9 ? "LATER" : step == 14 ? "BAD" : "LAST"); return host.remoteCommand( command, {{&valueName, &value}, {&tagName, &tag}});}
PdcommTcpToken sendStep(pdcomm::GemHost &host, size_t step) { if (step == 1) return host.request(pdcomm::GemRequest::Online); if (step == 2) return host.defineReports(1, {{10, {3}}}); if (step == 3) return host.linkReports(2, {{100, {10}}}); if (step == 4) return host.enableEvents(true, {100}); return sendCommand(host, step);}
uint8_t expectedAck(size_t step) { static const uint8_t values[] = {2, 1, 3, 3, 0, 4, 5, 6, 2, 1, 3, 0}; require(step == 0 || step >= 5, "invalid command step"); return values[step == 0 ? 0 : step - 4];}
pdcomm::GemCommandAcknowledgement decisionFor(size_t step) { using Ack = pdcomm::GemCommandAcknowledgement; if (step == 9) return Ack::AcceptedForLater; if (step == 10) return Ack::AlreadyInCondition; if (step == 11) return Ack::NoSuchObject; if (step == 12) return Ack::CannotPerform; if (step == 13) return Ack::Unknown; if (step == 14) return Ack::ParameterErrors; return Ack::Completed;}
void validateRequest(const pdcomm::GemResult &result, size_t step) { const auto info = result.info(); require(info.kind == pdcomm::GemResultKind::Primary && info.outcome == pdcomm::GemOutcome::CommandReceived, "command request classification mismatch"); const auto command = result.commandInfo(); require(command.isRequest && !command.responded, "command request flags mismatch"); require(command.parameterCount == info.entryCount, "command parameter count mismatch"); if (step == 5) { require(asciiValue(result.commandName()) == "MISSING" && !command.validationAccepted && static_cast<uint8_t>(command.validationAcknowledgement) == 1, "unknown command validation mismatch"); return; } if (step >= 10 && step <= 13) { require(result.commandName().payload() == std::vector<uint8_t>{7} && command.validationAccepted && command.parameterCount == 0, "numeric command snapshot mismatch"); return; } require(asciiValue(result.commandName()) == "SET_TEMP", "command name mismatch"); const bool rejected = step == 0 || step == 6 || step == 7; require(command.validationAccepted != rejected, "command validation acceptance mismatch"); require(command.completionEventPresent == !rejected, "command completion metadata mismatch"); if (!rejected) require(command.completionEvent == 100, "completion CEID mismatch"); if (step != 6) { auto parameter = result.commandParameter(0); require(asciiValue(parameter.name) == "VALUE", "VALUE name mismatch"); const int expected = step == 0 ? 10 : step == 7 ? 101 : step == 8 ? 20 : step == 9 ? 30 : step == 14 ? 40 : 50; require(i4Value(parameter.value) == expected, "VALUE snapshot mismatch"); } if (step == 8 || step == 9 || step == 14 || step == 15) { auto parameter = result.commandParameter(1); require(asciiValue(parameter.name) == "TAG", "TAG name mismatch"); }}
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 CommandsWorker final : public QThread {public: CommandsWorker(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 command 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 requestCommandDecision( const std::shared_ptr<pdcomm::GemResult> &result, size_t step) { 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, step] { try { require(QThread::currentThread() == application->thread(), "command inspection missed the UI thread"); validateRequest(*result, step); const auto choice = decisionFor(step); shared->uiDeliveries.fetch_add(1); decision->store(static_cast<int>(choice), std::memory_order_release); } catch (...) { decision->store(-2, std::memory_order_release); } }), "failed to queue command 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 command decision failed"); return value; }
int requestEventDecision( const std::shared_ptr<pdcomm::GemResult> &result, size_t index) { 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, index] { try { require(QThread::currentThread() == application->thread(), "event inspection missed the UI thread"); const auto info = result->eventInfo(); const auto report = result->eventReport(0); const int values[] = {20, 30, 50}; require(info.dataId == 71 + index && info.eventId == 100 && info.reportCount == 1 && !info.acknowledged && report.id == 10 && report.valueCount == 1 && i4Value(result->value(0, 0)) == values[index], "command completion event mismatch"); shared->uiDeliveries.fetch_add(1); decision->store(index == 1 ? 7 : 0, std::memory_order_release); } catch (...) { decision->store(-2, std::memory_order_release); } }), "failed to queue command event 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 event 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 stepSent = false; PdcommTcpToken communicationToken = {}; PdcommTcpToken requested[16] = {}; size_t step = 0; size_t commandAcks = 0; size_t eventReports = 0; bool commandDone = false; bool eventDone = false; bool peerStopped = false; const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(15); while (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> 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 (!stepSent) { requested[0] = sendStep(host, 0); stepSent = true; } } else if (info.outcome == pdcomm::GemOutcome::EventReceived) { eventReport = std::make_shared<pdcomm::GemResult>( std::move(result)); } else if (info.kind == pdcomm::GemResultKind::Response) { require(stepSent && step < 16 && sameToken(info.token, requested[step]), "command response token mismatch"); if (step == 1) { require(info.request == pdcomm::GemRequest::Online && info.outcome == pdcomm::GemOutcome::OnlineAck, "online response mismatch"); } else if (step >= 2 && step <= 4) { const auto expected = step == 2 ? pdcomm::GemOutcome::DefineReportsAck : step == 3 ? pdcomm::GemOutcome::LinkReportsAck : pdcomm::GemOutcome::EnableEventsAck; require(info.outcome == expected && info.acknowledgement == 0, "configuration response mismatch"); } else { require(info.request == pdcomm::GemRequest::RemoteCommand && info.outcome == pdcomm::GemOutcome::CommandAck && info.acknowledgement == expectedAck(step), "command ACK mismatch"); const auto detail = result.commandInfo(); const size_t expectedIssues = step == 6 || step == 7 ? 1 : step == 14 ? 2 : 0; require(!detail.isRequest && detail.issueCount == expectedIssues, "command response issue count mismatch"); for (size_t index = 0; index < detail.issueCount; ++index) { const auto issue = result.commandIssue(index); require(issue.code >= 1 && issue.code <= 3, "command response issue mismatch"); } ++commandAcks; } commandDone = true; } } if (eventReport) { const int ack = requestEventDecision(eventReport, eventReports); 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; eventDone = true; } if (stepSent && commandDone && (!(step == 8 || step == 9 || step == 15) || eventDone)) { ++step; commandDone = false; eventDone = false; if (step < 16) requested[step] = sendStep(host, step); } if (step == 16 && host.tcp().state().state != PDCOMM_TCP_SELECTED) { peerStopped = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } if (isInterruptionRequested()) { payload.cancelled = true; } else { require(step == 16 && commandAcks == 12 && eventReports == 3 && peerStopped, "host command 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.registerStatus(3, "CommandValue", "C", i4(0)); auto setTemp = ascii("SET_TEMP"); auto valueName = ascii("VALUE"); auto tagName = ascii("TAG"); auto numeric = u1(7); const auto zero = pdcomm::GemInt128::fromSigned(0); const auto hundred = pdcomm::GemInt128::fromSigned(100); equipment.configureCommands( pdcomm::gemCommandOptions(4, 4, 8, 32, 128, 8), {{&setTemp, {{&valueName, 1, 1, zero, hundred, true, true, PDCOMM_SECS_I4, true, true}, {&tagName, 0, 32, zero, zero, false, true, PDCOMM_SECS_ASCII, false, false}}, true, true, 100}, {&numeric, {}, false, false, 0}}); require(equipment.commandState().commandCount == 2, "command catalog mismatch"); equipment.start(); size_t step = 0; size_t commands = 0; size_t executions = 0; size_t eventAcks = 0; PdcommTcpToken eventTokens[3] = {}; 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(); std::shared_ptr<pdcomm::GemResult> command; 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) { if (info.outcome == pdcomm::GemOutcome::CommandReceived) { command = std::make_shared<pdcomm::GemResult>( std::move(result)); } else if (step >= 1 && step <= 4) { const auto expected = step == 1 ? pdcomm::GemOutcome::OnlineAccepted : step == 2 ? pdcomm::GemOutcome::DefineReportsAck : step == 3 ? pdcomm::GemOutcome::LinkReportsAck : pdcomm::GemOutcome::EnableEventsAck; if (info.outcome == expected) ++step; } } else if (info.kind == pdcomm::GemResultKind::Response && info.outcome == pdcomm::GemOutcome::EventAck) { require(eventAcks < 3 && sameToken(info.token, eventTokens[eventAcks]) && info.acknowledgement == (eventAcks == 1 ? 7 : 0), "event acknowledgement mismatch"); ++eventAcks; } } if (command) { const size_t commandStep = step; const auto decision = static_cast<pdcomm::GemCommandAcknowledgement>( requestCommandDecision(command, commandStep)); auto issueValue = ascii("VALUE"); auto issueTag = ascii("TAG"); std::vector<pdcomm::GemCommandIssue> issues; if (commandStep == 14) { issues = {{&issueValue, 2}, {&issueTag, 3}}; } if (commandStep == 8 || commandStep == 15) { equipment.updateStatus(3, i4(commandStep == 8 ? 20 : 50)); ++executions; } const auto reply = equipment.respondCommand( *command, decision, issues); require(static_cast<uint8_t>(reply.acknowledgement) == expectedAck(commandStep), "actual command acknowledgement mismatch"); if (commandStep == 9) { equipment.updateStatus(3, i4(30)); ++executions; } if (commandStep == 8 || commandStep == 9 || commandStep == 15) { const size_t index = commandStep == 8 ? 0 : commandStep == 9 ? 1 : 2; eventTokens[index] = equipment.publishEvent( static_cast<uint32_t>(71 + index), 100); } if (commandStep == 15) payload.results.push_back(command); ++commands; ++step; } if (step == 16 && commands == 12 && eventAcks == 3) { equipment.stop(); peerStopped = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } if (isInterruptionRequested()) { payload.cancelled = true; } else { if (!(step == 16 && commands == 12 && executions == 3 && eventAcks == 3 && peerStopped && i4Value(equipment.status(3)) == 50)) { throw std::runtime_error( "equipment command exchange timed out: step=" + std::to_string(step) + " commands=" + std::to_string(commands) + " executions=" + std::to_string(executions) + " eventAcks=" + std::to_string(eventAcks) + " peerStopped=" + std::to_string(peerStopped ? 1 : 0)); } } 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() == 3 && payload.values.size() == 3, "retained command events are missing"); const int expected[] = {20, 30, 50}; for (size_t index = 0; index < 3; ++index) { require(i4Value(*payload.values[index]) == expected[index] && payload.results[index] ->eventInfo().acknowledged, "retained command event changed"); } } if (!payload.cancelled && !payload.host) { require(payload.results.size() == 1 && payload.results[0] ->commandInfo().responded && asciiValue(payload.results[0] ->commandName()) == "SET_TEMP", "retained command result changed"); } shared->uiDeliveries.fetch_add(1); completion->store(1); } catch (...) { completion->store(-1); } }), "failed to queue command 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_commands ROLE MODE ADDRESS [--cancel]\n"); return 2; }
const bool cancel = argc == 5; const auto shared = std::make_shared<SharedState>(); CommandsWorker 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 COMMANDS: %s\n", shared->failure.empty() ? "UI/worker validation failed" : shared->failure.c_str()); application.exit(1); return; } std::fprintf(stdout, "PASS QT COMMANDS\n"); std::fflush(stdout); application.quit(); }); });
worker.start(); const int result = application.exec(); worker.requestInterruption(); worker.wait(); return result;}enhanced_commands.cpp 전체 코드
#include "pdcomm_gem.hpp"#include "queued_handoff.hpp"
#include <QCoreApplication>#include <QMetaObject>#include <QThread>#include <QTimer>
#include <atomic>#include <chrono>#include <cstdint>#include <cstdio>#include <iostream>#include <memory>#include <stdexcept>#include <string>#include <thread>#include <utility>#include <vector>
namespace {void require(bool value, const char *message) { if (!value) throw std::runtime_error(message);}PdcommSecsLimits limits() { return pdcomm::secsLimits(4096, 4096, 128, 8); }PdcommTcpOptions tcpOptions(uint32_t mode) { PdcommTcpOptions v = {}; v.struct_size = sizeof(v); v.version = PDCOMM_TCP_OPTIONS_VERSION; v.mode = mode; v.session_id = 7; v.initial_system_bytes = mode == PDCOMM_TCP_ACTIVE ? 100 : 500; v.t3_ms = 2000; v.t5_ms = 50; v.t6_ms = 2000; v.t7_ms = 2000; v.t8_ms = 1000; v.connect_timeout_ms = 20; v.shutdown_timeout_ms = 200; v.write_timeout_ms = 2000; v.max_frame_bytes = 4110; v.max_events = 128; v.max_queued_bytes = 32768; v.max_write_frames = 8; v.max_write_bytes = 16384; v.max_io_chunk = 3; v.max_application_events = 128; v.max_application_bytes = 32768; v.poll_budget = 16; v.item_limits = limits(); return v;}pdcomm::SecsItem primitive(uint8_t f, const std::vector<uint8_t> &b) { return pdcomm::SecsItem::primitive(f, b, limits());}pdcomm::SecsItem ascii(const std::string &s) { return pdcomm::SecsItem::primitive( PDCOMM_SECS_ASCII, reinterpret_cast<const uint8_t *>(s.data()), s.size(), limits());}pdcomm::SecsItem u1(uint8_t v) { return primitive(PDCOMM_SECS_U1, {v}); }pdcomm::SecsItem i4(int32_t v) { const uint32_t b = static_cast<uint32_t>(v); return primitive(PDCOMM_SECS_I4, {static_cast<uint8_t>(b >> 24), static_cast<uint8_t>(b >> 16), static_cast<uint8_t>(b >> 8), static_cast<uint8_t>(b)});}pdcomm::SecsItem f8() { return primitive(PDCOMM_SECS_F8, {0x40, 0x45, 0, 0, 0, 0, 0, 0});}pdcomm::SecsItem list(const std::vector<const pdcomm::SecsItem *> &items) { return pdcomm::SecsItem::list(items, limits());}pdcomm::SecsItem pair(const std::string &name, const pdcomm::SecsItem &value) { auto n = ascii(name); return list({&n, &value});}pdcomm::SecsItem recipe(int32_t value) { auto temperature = i4(value); auto tempPair = pair("TEMP", temperature); auto one = i4(1); auto two = i4(2); auto three = i4(3); auto steps = list({&one, &two, &three}); auto stepsPair = pair("STEPS", steps); return list({&tempPair, &stepsPair});}pdcomm::SecsItem recursiveAck() { auto zero1 = u1(0), four = u1(4), zero2 = u1(0); auto codes = list({&zero1, &four, &zero2}); auto tempZero = u1(0); auto temp = pair("TEMP", tempZero); auto steps = pair("STEPS", codes); auto fields = list({&temp, &steps}); auto outer = pair("RECIPE", fields); return list({&outer});}pdcomm::SecsItem singleAck(const std::string &name, uint8_t code) { auto c = u1(code); auto p = pair(name, c); return list({&p});}std::string text(const pdcomm::SecsItem &item) { const auto b = item.payload(); return std::string(b.begin(), b.end());}int32_t integer(const pdcomm::SecsItem &item) { const auto b = item.payload(); require(b.size() == 4, "I4 width"); return static_cast<int32_t>((uint32_t(b[0]) << 24) | (uint32_t(b[1]) << 16) | (uint32_t(b[2]) << 8) | b[3]);}bool tokenEqual(const PdcommTcpToken &a, const PdcommTcpToken &b) { return a.endpoint_id == b.endpoint_id && a.generation == b.generation && a.system_bytes == b.system_bytes && a.reserved == b.reserved;}void stopAndDrain(pdcomm::GemEndpoint &e) { if (!e.tcp().state().closed) e.stop(); const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (true) { if (!e.tcp().state().closed) e.poll(); for (auto v = e.nextEvent(); v; v = e.nextEvent()) (void)e.handle(v); if (e.tcp().state().closed || std::chrono::steady_clock::now() >= d) break; std::this_thread::sleep_for(std::chrono::milliseconds(1)); }}
void configure(pdcomm::GemEquipment &e) { e.configureReports(pdcomm::gemReportOptions(4, 4, 8, 4, 16, 16), {100, 200}); e.registerStatus(3, "CommandValue", "C", i4(0)); auto set = ascii("SET_TEMP"), value = ascii("VALUE"), tag = ascii("TAG"), seven = u1(7); const auto z = pdcomm::GemInt128::fromSigned(0), h = pdcomm::GemInt128::fromSigned(100); e.configureCommands( pdcomm::gemCommandOptions(8, 8, 32, 64, 1024, 8), {{&set, {{&value, 1, 1, z, h, true, true, PDCOMM_SECS_I4, true, true}, {&tag, 0, 32, z, z, false, true, PDCOMM_SECS_ASCII, false, false}}, true, true, 100}, {&seven, {}, false, false, 0}}); auto chamber1 = ascii("CHAMBER/1"), chamber2 = ascii("CHAMBER/2"), empty = ascii(""), apply = ascii("APPLY_RECIPE"), recipeName = ascii("RECIPE"); e.configureEnhancedCommands( pdcomm::gemEnhancedCommandOptions(8, 64, 256, 8, 16, 8, 32, 64, 1024, 8, 2048, limits()), {{&chamber1, {{&apply, {{&recipeName, 2, 2, z, z, true, true, PDCOMM_SECS_LIST, false, false}, {&tag, 0, 32, z, z, false, true, PDCOMM_SECS_ASCII, false, false}}, true, true, 100}}}, {&chamber2, {{&apply, {{&recipeName, 1, 1, z, z, true, true, PDCOMM_SECS_F8, false, false}}, false, false, 0}}}, {&empty, {{&seven, {}, false, false, 0}}}});}
PdcommTcpToken sendStep(pdcomm::GemHost &h, size_t step) { if (step == 1) return h.request(pdcomm::GemRequest::Online); if (step == 2) return h.defineReports(1, {{10, {3}}}); if (step == 3) return h.linkReports(2, {{100, {10}}}); if (step == 4) return h.enableEvents(true, {100}); auto name = step >= 13 && step <= 18 ? u1(7) : ascii(step == 6 ? "MISSING" : step == 20 ? "SET_TEMP" : "APPLY_RECIPE"); if (step == 19) return h.remoteCommand(name); auto object = ascii(step == 5 ? "chamber/1" : step == 9 ? "CHAMBER/2" : step >= 13 && step <= 17 ? "" : "CHAMBER/1"); auto key = ascii("RECIPE"), value = step == 8 ? ascii("invalid") : step == 9 ? f8() : recipe(step == 10 ? 20 : step == 11 ? 30 : step == 12 ? 40 : step == 23 ? 50 : 10); auto tag = ascii(step == 22 ? "EXTRA" : "TAG"), tagValue = ascii(step == 10 ? "NOW" : step == 11 ? "LATER" : step == 12 ? "BAD" : "LAST"); std::vector<pdcomm::GemCommandParameter> p; if (!(step == 7 || step == 6 || (step >= 13 && step <= 20))) { p.push_back({&key, &value}); if (step == 10 || step == 11 || step == 12 || step >= 21) p.push_back( {step == 21 ? &key : &tag, step == 21 ? &value : &tagValue}); } return h.enhancedRemoteCommand(static_cast<uint32_t>(1000 + step), object, name, p);}uint8_t expectedAck(size_t s) { static const uint8_t a[24] = {2, 0, 0, 0, 0, 6, 1, 3, 3, 0, 0, 4, 3, 5, 6, 2, 1, 0, 1, 1, 1, 3, 3, 0}; return a[s];}bool eventStep(size_t s) { return s == 10 || s == 11 || s == 23; }bool accepted(size_t s) { return s == 9 || s == 10 || s == 11 || s == 12 || (s >= 13 && s <= 17) || s == 23;}pdcomm::GemCommandAcknowledgement decision(size_t s) { using A = pdcomm::GemCommandAcknowledgement; if (s == 11) return A::AcceptedForLater; if (s == 12) return A::ParameterErrors; if (s == 13) return A::AlreadyInCondition; if (s == 14) return A::NoSuchObject; if (s == 15) return A::CannotPerform; if (s == 16) return A::Unknown; return A::Completed;}void checkRequest(const pdcomm::GemResult &r, size_t step) { const auto i = r.enhancedCommandInfo(); require(i.isRequest && i.dataId == 1000 + step && !i.responded, "enhanced request info"); require(i.validationAccepted == accepted(step), "enhanced admission"); auto object = r.enhancedCommandObject(), name = r.enhancedCommandName(); if (step >= 13 && step <= 18) require(name.payload() == std::vector<uint8_t>{7}, "numeric name"); else require(text(name) == (step == 6 ? "MISSING" : step == 20 ? "SET_TEMP" : "APPLY_RECIPE"), "enhanced name"); if (i.validationAccepted && i.parameterCount) { auto p = r.enhancedCommandParameter(0); require(text(p.name) == "RECIPE", "recipe name"); }}void checkResponse(const pdcomm::GemResult &r, size_t step) { const auto i = r.info(); require(i.acknowledgement == expectedAck(step), "enhanced ACK"); if (step == 19) { require(i.outcome == pdcomm::GemOutcome::CommandAck, "basic separation"); return; } require(i.outcome == pdcomm::GemOutcome::EnhancedCommandAck, "enhanced outcome"); const auto d = r.enhancedCommandInfo(); const size_t count = step == 7 || step == 8 || step == 12 || step == 21 || step == 22 ? 1 : 0; require(!d.isRequest && d.issueCount == count, "enhanced issue count"); auto a = r.enhancedCommandAcknowledgements(); pdcomm::SecsItem expected; if (step == 12) expected = recursiveAck(); else if (count) expected = singleAck(step == 22 ? "EXTRA" : "RECIPE", step == 8 ? 3 : step == 22 ? 1 : 2); else expected = list({}); require(a.bytes() == expected.bytes(), "recursive acknowledgement");}} // namespace
struct Payload { bool cancelled = false, host = false; std::vector<std::shared_ptr<pdcomm::GemResult>> results; std::vector<std::shared_ptr<pdcomm::SecsItem>> values; std::shared_ptr<pdcomm::GemResult> recursive;};struct Shared { std::atomic<bool> active{false}, success{false}, cancelled{false}; std::atomic<int> deliveries{0}, heartbeats{0}; std::string failure;};
class Worker final : public QThread { public: Worker(QCoreApplication *app, std::string role, std::string mode, std::string address, std::shared_ptr<Shared> s) : app_(app), role_(std::move(role)), mode_(std::move(mode)), address_(std::move(address)), shared_(std::move(s)) {}
protected: void run() override { shared_->active.store(true); try { require(QThread::currentThread() != app_->thread(), "endpoint on UI thread"); auto p = role_ == "host" ? runHost() : runEquipment(); post(std::move(p)); } catch (const pdcomm::Error &e) { shared_->failure = "PDComm status " + std::to_string(e.status()); } catch (const std::exception &e) { shared_->failure = e.what(); } shared_->active.store(false); }
private: void listen(const std::string &a) { auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, a] { if (QThread::currentThread() != app->thread()) return; s->deliveries.fetch_add(1); std::fprintf(stdout, "LISTEN %s\n", a.c_str()); std::fflush(stdout); }), "queue listen notification"); } pdcomm::TcpEndpoint endpoint() { bool a = mode_ == "active"; pdcomm::TcpEndpoint e( address_, tcpOptions(a ? PDCOMM_TCP_ACTIVE : PDCOMM_TCP_PASSIVE)); if (!a) listen(e.localAddress()); return e; } int uiCommand(const std::shared_ptr<pdcomm::GemResult> &r, size_t step) { auto answer = std::make_shared<std::atomic<int>>(-1); auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, r, answer, step] { try { require(QThread::currentThread() == app->thread(), "command not on UI"); checkRequest(*r, step); s->deliveries.fetch_add(1); answer->store(static_cast<int>(decision(step)), std::memory_order_release); } catch (...) { answer->store(-2, std::memory_order_release); } }), "queue command decision"); return await(answer, "command UI decision"); } void uiResponse(const std::shared_ptr<pdcomm::GemResult> &r, size_t step) { auto answer = std::make_shared<std::atomic<int>>(-1); auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, r, answer, step] { try { require(QThread::currentThread() == app->thread(), "response not on UI"); checkResponse(*r, step); s->deliveries.fetch_add(1); answer->store(0, std::memory_order_release); } catch (...) { answer->store(-2, std::memory_order_release); } }), "queue response inspection"); (void)await(answer, "response UI inspection"); } int uiEvent(const std::shared_ptr<pdcomm::GemResult> &r, size_t index) { auto answer = std::make_shared<std::atomic<int>>(-1); auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, r, answer, index] { try { require(QThread::currentThread() == app->thread(), "event not on UI"); require( r->eventInfo().dataId == 71 + index && integer(r->value(0, 0)) == (index == 0 ? 20 : index == 1 ? 30 : 50), "event UI snapshot"); s->deliveries.fetch_add(1); answer->store(index == 1 ? 7 : 0, std::memory_order_release); } catch (...) { answer->store(-2, std::memory_order_release); } }), "queue event decision"); return await(answer, "event UI decision"); } int await(const std::shared_ptr<std::atomic<int>> &a, const char *m) { const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(2); int v = -1; while ((v = a->load(std::memory_order_acquire)) == -1 && std::chrono::steady_clock::now() < d && !isInterruptionRequested()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); require(v >= 0, m); return v; }
Payload runHost() { Payload p; p.host = true; { auto t = endpoint(); pdcomm::GemHost h(std::move(t), pdcomm::gemHostOptions(limits(), 64)); h.start(); bool began = false, sent = false, cmd = false, event = false, peer = false; size_t step = 0, events = 0; PdcommTcpToken comm = {}, tokens[24] = {}; const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(15); while (std::chrono::steady_clock::now() < d && !isInterruptionRequested()) { h.poll(); if (!began && h.tcp().state().state == PDCOMM_TCP_SELECTED) { comm = h.beginCommunication(); began = true; } std::shared_ptr<pdcomm::GemResult> report; for (auto ev = h.nextEvent(); ev; ev = h.nextEvent()) { auto value = h.handle(ev); auto i = value.info(); if (i.outcome == pdcomm::GemOutcome::CommunicationEstablished) { require(i.kind == pdcomm::GemResultKind::Response && i.request == pdcomm::GemRequest::Establish && tokenEqual(i.token, comm), "communication token"); tokens[0] = sendStep(h, 0); sent = true; } else if (i.outcome == pdcomm::GemOutcome::EventReceived) report = std::make_shared<pdcomm::GemResult>( std::move(value)); else if (i.kind == pdcomm::GemResultKind::Response) { require(sent && tokenEqual(i.token, tokens[step]), "response token"); if (step >= 1 && step <= 4) require(i.acknowledgement == 0, "configuration ACK"); else { auto r = std::make_shared<pdcomm::GemResult>( std::move(value)); uiResponse(r, step); if (step == 12) p.recursive = r; } cmd = true; } } if (report) { int ack = uiEvent(report, events); p.values.push_back(std::make_shared<pdcomm::SecsItem>( report->value(0, 0))); h.acknowledgeEvent(*report, static_cast<uint8_t>(ack)); p.results.push_back(report); ++events; event = true; } if (sent && cmd && (!eventStep(step) || event)) { ++step; cmd = false; event = false; if (step < 24) tokens[step] = sendStep(h, step); } if (step == 24 && h.tcp().state().state != PDCOMM_TCP_SELECTED) { peer = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } if (isInterruptionRequested()) p.cancelled = true; else require(step == 24 && events == 3 && peer, "host enhanced timeout"); stopAndDrain(h); } return p; }
Payload runEquipment() { Payload p; { auto t = endpoint(); pdcomm::GemEquipment e(std::move(t), limits(), pdcomm::GemControl::HostOffline, pdcomm::GemOnlineMode::Remote, "PDComm", "0.1.0", 8, 1024, 4096); configure(e); e.start(); size_t step = 0, primaries = 0, enhanced = 0, numeric = 0, executions = 0, eventAcks = 0; PdcommTcpToken eventTokens[3] = {}; bool stopped = false; const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(15); while (std::chrono::steady_clock::now() < d && !isInterruptionRequested()) { e.poll(); std::shared_ptr<pdcomm::GemResult> command; for (auto ev = e.nextEvent(); ev; ev = e.nextEvent()) { auto value = e.handle(ev); auto i = value.info(); if (i.kind == pdcomm::GemResultKind::Primary) { ++primaries; if (i.outcome == pdcomm::GemOutcome::EnhancedCommandReceived) { command = std::make_shared<pdcomm::GemResult>( std::move(value)); ++enhanced; } else if (i.outcome == pdcomm::GemOutcome::CommandReceived) { require(step == 19 && !value.commandInfo().validationAccepted, "basic separation"); e.respondCommand( value, pdcomm::GemCommandAcknowledgement::Completed); ++step; } else if (step >= 1 && step <= 4) { auto x = step == 1 ? pdcomm::GemOutcome::OnlineAccepted : step == 2 ? pdcomm::GemOutcome::DefineReportsAck : step == 3 ? pdcomm::GemOutcome::LinkReportsAck : pdcomm::GemOutcome::EnableEventsAck; if (i.outcome == x) ++step; } } else if (i.kind == pdcomm::GemResultKind::Response && i.outcome == pdcomm::GemOutcome::EventAck) { require( eventAcks < 3 && tokenEqual(i.token, eventTokens[eventAcks]) && i.acknowledgement == (eventAcks == 1 ? 7 : 0), "event ACK"); ++eventAcks; } } if (command) { size_t s = step; auto choice = static_cast<pdcomm::GemCommandAcknowledgement>( uiCommand(command, s)); if (s >= 13 && s <= 17) ++numeric; if (s == 10 || s == 23) { e.updateStatus(3, i4(s == 10 ? 20 : 50)); ++executions; } pdcomm::SecsItem ack; if (s == 12) ack = recursiveAck(); auto reply = e.respondEnhancedCommand( *command, choice, s == 12 ? &ack : nullptr); require(static_cast<uint8_t>(reply.acknowledgement) == expectedAck(s), "actual ACK"); if (s == 11) { e.updateStatus(3, i4(30)); ++executions; } if (eventStep(s)) eventTokens[s == 10 ? 0 : s == 11 ? 1 : 2] = e.publishEvent( static_cast<uint32_t>(70 + executions), 100); if (s == 23) p.results.push_back(command); ++step; } if (step == 24 && eventAcks == 3) { e.stop(); stopped = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } if (isInterruptionRequested()) p.cancelled = true; else require(step == 24 && primaries == 25 && enhanced == 19 && numeric == 5 && executions == 3 && eventAcks == 3 && stopped, "equipment enhanced timeout"); stopAndDrain(e); } return p; }
void post(Payload p) { auto done = std::make_shared<std::atomic<int>>(0); auto app = app_; auto s = shared_; require(pdcomm_qt_example::invokeQueued( app, [app, s, p, done] { try { require(QThread::currentThread() == app->thread(), "completion not on UI"); if (!p.cancelled && p.host) { require(p.recursive && p.results.size() == 3 && p.values.size() == 3, "host retained payload"); auto ack = p.recursive ->enhancedCommandAcknowledgements(); require(ack.bytes() == recursiveAck().bytes(), "delayed recursive ACK"); const int v[3] = {20, 30, 50}; for (size_t i = 0; i < 3; ++i) require( integer(*p.values[i]) == v[i] && p.results[i] ->eventInfo() .acknowledged, "delayed event"); } if (!p.cancelled && !p.host) require( p.results.size() == 1 && p.results[0] ->enhancedCommandInfo() .responded && p.results[0] ->enhancedCommandInfo() .dataId == 1023, "delayed request"); s->cancelled.store(p.cancelled); s->deliveries.fetch_add(1); done->store(1); } catch (...) { done->store(-1); } }), "queue completion"); const auto d = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (done->load() == 0 && std::chrono::steady_clock::now() < d) std::this_thread::sleep_for(std::chrono::milliseconds(1)); require(done->load() == 1, "completion UI"); require(shared_->heartbeats.load() > 0, "UI heartbeat"); shared_->success.store(true); } QCoreApplication *app_; std::string role_, mode_, address_; std::shared_ptr<Shared> shared_;};
int main(int argc, char **argv) { QCoreApplication app(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_enhanced_commands ROLE MODE " "ADDRESS [--cancel]\n"); return 2; } bool cancel = argc == 5; auto shared = std::make_shared<Shared>(); Worker worker(&app, argv[1], argv[2], argv[3], shared); QTimer heartbeat; heartbeat.setInterval(1); QObject::connect(&heartbeat, &QTimer::timeout, &app, [&] { if (shared->active.load()) { shared->heartbeats.fetch_add(1); } }); heartbeat.start(); QTimer cancelTimer; cancelTimer.setSingleShot(true); QObject::connect(&cancelTimer, &QTimer::timeout, &app, [&] { worker.requestInterruption(); }); QTimer watchdog; watchdog.setSingleShot(true); QObject::connect(&watchdog, &QTimer::timeout, &app, [&] { worker.requestInterruption(); }); QObject::connect(&worker, &QThread::started, &app, [&] { watchdog.start(18000); if (cancel) cancelTimer.start(20); }); QObject::connect(&worker, &QThread::finished, &app, [&] { worker.wait(); QTimer::singleShot(0, &app, [&] { if (!shared->success.load() || shared->deliveries.load() == 0 || shared->heartbeats.load() == 0) { std::fprintf(stderr, "FAIL QT ENHANCED: %s\n", shared->failure.empty() ? "validation failed" : shared->failure.c_str()); app.exit(1); return; } std::fprintf(stdout, shared->cancelled.load() ? "PASS QT ENHANCED CANCEL\n" : "PASS QT ENHANCED PEER\n"); std::fflush(stdout); app.quit(); }); }); worker.start(); int result = app.exec(); worker.requestInterruption(); worker.wait(); return result;}6. 시뮬레이터 GUI에서 확인
섹션 제목: “6. 시뮬레이터 GUI에서 확인”시뮬레이터 창 두 개로 같은 흐름을 손으로 실행할 수 있습니다. 화면 구성은 시뮬레이터 GUI 가이드에 자세히 나옵니다.


-
장비 창의 Remote Commands → Catalog 탭에서 Load example을 누르고 Use on next start를 누릅니다. 예제는 ASCII 필수 파라미터
LOT(1~32자)를 받는 기본 명령START와 확장 명령PORT-1/MOVE입니다. -
Overview에서 GEM role을 Equipment, TCP mode를 Passive - listen, Session ID를
7로 정하고 Start session을 누릅니다. -
호스트 창은 Host, Active - connect, 같은 주소와 Session ID로 시작한 뒤 Establish communication을 누릅니다.
-
호스트 창의 Remote Commands → Traffic에서 Basic name에
START를 입력합니다. Basic parameters에서 Add parameter를 누르고 NameLOT, RepresentationASCII, ValueLOT-0042를 넣은 뒤 Submit basic을 누릅니다. -
장비 창의 Traffic 탭에서 받은 요청을 고르고, 결정 목록에서 Decision 0을 고른 뒤 Respond를 누릅니다. 장비 창에서는 Host submissions 영역이 비활성이고, 아래쪽의 받은 요청 목록과 Respond만 씁니다.


-
호스트 창의 결과 목록에서 State가
Completed, ACK가0인지 확인하고 Acknowledge selected result로 결과를 정리합니다. 화면 아래 문구처럼, 이 결과는 명령 제출의 결과일 뿐 장비 작업 완료를 뜻하지 않습니다.

LOT 없이 보내면 장비 화면에서 검증이 거부한 요청(HCACK 3)으로 보입니다. 이 요청도 Respond를 눌러야 응답이 나갑니다. 이때 고른 결정은 무시되고 HCACK 3이 그대로 전송됩니다.
자주 생기는 오류
섹션 제목: “자주 생기는 오류”| 증상 | 원인 | 해결 |
|---|---|---|
예제가 Usage: ...를 출력하고 종료 코드 2로 끝남 |
네이티브 라이브러리 경로가 절대 경로가 아니거나 인수가 빠졌습니다. | $PDCOMM_NATIVE(Windows는 $Native)처럼 절대 경로를 넘깁니다. |
호스트 요청이 PdcommException(InvalidState)로 실패 |
GEM 통신이 수립되기 전에 명령을 보냈습니다. | 통신 수립(S1F13) 응답을 받은 뒤 보냅니다. |
| 모든 명령이 HCACK 2 | 명령이 온라인 원격 상태를 요구하는데 장비가 오프라인이거나 로컬입니다. | 호스트에서 온라인 요청(S1F17)을 먼저 보내고 장비의 온라인 모드가 원격인지 확인합니다. |
| HCACK 1인데 이름은 맞아 보임 | 기본 명령을 S2F49로 보냈거나, 확장 명령을 S2F41로 보냈습니다. 숫자 이름의 형식이 다를 수도 있습니다. | 명령이 등록된 카탈로그와 메시지 종류를 맞춥니다. |
| HCACK 3과 CPACK 2 또는 3 | 필수 파라미터 누락, 범위 초과, 형식 불일치입니다. | 카탈로그의 파라미터 정의와 값을 비교합니다. |
RespondCommandAsync가 InvalidArgument로 실패 |
ParameterErrors가 아닌 결정에 CPACK 목록을 넘겼습니다. |
CPACK은 HCACK 3일 때만 보냅니다. |
RespondCommandAsync가 StaleToken으로 실패 |
이미 응답한 요청이거나 연결이 바뀐 뒤의 요청입니다. | 같은 결과에 두 번 응답하지 않습니다. 재연결 뒤에는 새 요청을 기다립니다. |
Backpressure |
작업자(worker)의 명령 큐나 바이트 예산이 가득 찼습니다. | 이벤트를 계속 읽어 큐를 비우고, 응답 작업이 끝난 뒤 다시 시도합니다. |
| 호스트가 완료 이벤트를 받지 못함 | 이벤트를 켜지 않았거나 장비가 이벤트를 보고하지 않았습니다. 보고서를 정의·연결하지 않은 채 켠 이벤트는 값 없이 빈 보고서 목록으로 옵니다. | 1–4단계와 같은 준비를 하고, 장비가 작업 뒤 이벤트를 보내는지 확인합니다. |
- 예제는 시나리오를 마치면 스스로 끝납니다. 중간에 멈추면 각 터미널에서 Ctrl+C를 누릅니다.
- 시뮬레이터에서는 양쪽 창에서 Stop을 누른 뒤 창을 닫습니다.
- 빌드 결과를 지우려면
build/managed폴더와 Qt 예제의build폴더를 삭제합니다.
다음 단계
섹션 제목: “다음 단계”- 공정 프로그램에서 레시피를 주고받는 S7 흐름을 실습합니다.
- API 세부 사항은 SECS/GEM API 레퍼런스를 봅니다.