첫 통신
- 장비와 호스트를 대화형으로 실행하고 HSMS 연결 상태와 GEM 통신 상태가 바뀌는 순서를 확인합니다.
- 상태 변수의 이름과 값을 요청하고 SECS-II 본문을 읽습니다.
- 온라인(online)·오프라인(offline) 요청과 응답 코드를 확인합니다.
- 장비가 처리하지 않는 요청이 어떻게 끝나는지 확인합니다.
- 같은 호스트를 시뮬레이터 GUI의 장비에 연결합니다.
준비물
섹션 제목: “준비물”- 개발 환경 준비를 마친 터미널 두 개. 두 터미널 모두 환경 변수를 정해 둡니다.
- 빌드한 C++ 예제
build/pdcomm-tutorial과 C# 예제build/managed/bin/PDComm.Tutorial/release/PDComm.Tutorial.dll
이 튜토리얼의 명령은 Linux와 macOS 셸 기준입니다. Windows에서는 "$PDCOMM_ROOT/build/pdcomm-tutorial"을 & "$Root\build\pdcomm-tutorial.exe"로, "$PDCOMM_NATIVE"를 "$Native"로 바꿉니다.
1. C++ 장비를 시작합니다
섹션 제목: “1. C++ 장비를 시작합니다”첫 번째 터미널에서 장비를 Passive 모드로 시작합니다.
"$PDCOMM_ROOT/build/pdcomm-tutorial" equipment passive 127.0.0.1:5000LISTEN 127.0.0.1:5000Commands: state, establish, identity, values, names, online, offline, linktest, quit장비는 SVID 1(Temperature, F4 21.5)과 SVID 2(Lot, ASCII LOT-001)를 등록하고, 제어 상태 HostOffline으로 시작합니다.
2. C# 호스트를 연결합니다
섹션 제목: “2. C# 호스트를 연결합니다”두 번째 터미널에서 C# 호스트를 Active 모드로 시작합니다. C# 예제는 C++ 예제의 명령에 더해 디렉터리, 알람 목록, 정형 공정 프로그램 명령을 제공합니다.
dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.Tutorial/release/PDComm.Tutorial.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:5000Commands: 줄이 보이면 연결과 Select가 끝났습니다. 아래 단계의 명령을 한 줄씩 입력하고, 결과가 나온 뒤 다음 명령을 입력합니다.
3. GEM 통신을 수립합니다
섹션 제목: “3. GEM 통신을 수립합니다”stateestablishstateSTATE tcp=Selected gem=NotCommunicatingRESULT kind=2 request=1 outcome=2 status=CommunicationEstablishedSECS-II 0102210100010241065044436F6D6D4105302E312E30STATE tcp=Selected gem=Communicating- TCP 연결과 HSMS Select가 끝나도 GEM 통신 상태는 NotCommunicating입니다. 호스트가 S1F13을 보내고 COMMACK 0이 담긴 S1F14를 받아야 Communicating이 됩니다.
- C# 예제는 상태를 이름으로, C++ 예제는 숫자로 출력합니다.
tcp=4는 Selected,gem=1은 NotCommunicating,gem=3은 Communicating입니다.
장비 터미널에는 받은 S1F13의 결과가 나옵니다. kind=1은 받은 요청(primary), outcome=2는 통신 수립입니다.
RESULT kind=1 request=0 outcome=2SECS-II 01 004. 상태 변수의 이름과 값을 읽습니다
섹션 제목: “4. 상태 변수의 이름과 값을 읽습니다”names 1 2values 2 1values 9RESULT kind=2 request=4 outcome=6 status=NamesReceivedSECS-II 01020103B10400000001410B54656D70657261747572654101430103B1040000000241034C6F744100RESULT kind=2 request=3 outcome=5 status=ValuesReceivedSECS-II 010241074C4F542D303031910441AC0000RESULT kind=2 request=3 outcome=5 status=ValuesReceivedSECS-II 01010100names 1 2는 S1F11을 보냅니다. S1F12 본문은 SVID마다L[3] { U4 SVID, A 이름, A 단위 }입니다.Temperature의 단위는C,Lot의 단위는 빈 문자열입니다.values 2 1은 S1F3을 보냅니다. S1F4의 값은 요청한 순서대로 옵니다. 여기서는A "LOT-001"이 먼저,F4 21.5가 나중입니다.values 9처럼 등록되지 않은 SVID를 요청하면 장비는 그 자리에 길이 0인 리스트L[0](01 00)를 돌려줍니다. 오류가 아니므로 값의 형식과 길이를 함께 확인합니다.
5. 온라인과 오프라인을 요청합니다
섹션 제목: “5. 온라인과 오프라인을 요청합니다”onlineofflineRESULT kind=2 request=6 outcome=12 status=OnlineAckSECS-II 210100RESULT kind=2 request=5 outcome=11 status=OfflineAckSECS-II 210100online은 S1F17, offline은 S1F15를 보냅니다. 응답 본문 21 01 00은 B 0x00이며 ONLACK 0(수락), OFLACK 0(확인)입니다. 장비 터미널의 outcome=8(OnlineAccepted)과 outcome=7(OfflineAccepted)은 장비의 제어 상태가 실제로 바뀌었다는 뜻입니다.
통신 상태와 제어 상태는 별개입니다. Communicating이어도 장비가 오프라인일 수 있고, 원격 명령은 OnlineRemote 같은 제어 상태를 추가로 요구할 수 있습니다. 자세한 내용은 GEM 상태 모델을 참고합니다.
6. Linktest를 보냅니다
섹션 제목: “6. Linktest를 보냅니다”linkteststatelinktest는 HSMS 제어 메시지 Linktest.req를 보냅니다. GEM 결과가 아니므로 RESULT 줄이 나오지 않습니다. 응답이 오지 않으면 T6 뒤에 연결이 끊어지므로, 이어서 입력한 state가 tcp=Selected이면 Linktest가 성공한 것입니다. 시뮬레이터의 Message Trace & Diagnostics에서는 이 왕복을 직접 볼 수 있습니다.
7. 장비가 처리하지 않는 요청을 보냅니다
섹션 제목: “7. 장비가 처리하지 않는 요청을 보냅니다”튜토리얼 장비는 알람 카탈로그와 이벤트 디렉터리를 설정하지 않았습니다. 호스트에서 다음을 입력합니다.
alarmsdirectories호스트 출력:
RESULT kind=3 request=13 outcome=0 status=NoneRESULT kind=2 request=21 outcome=36 status=DataDirectoryReceivedSECS-II 0100RESULT kind=3 request=22 outcome=0 status=None장비 출력:
RESULT kind=5 request=0 outcome=0SECS-II 01 00ABORT S5F0 for S5F5RESULT kind=1 request=0 outcome=38SECS-II 01 00RESULT kind=5 request=0 outcome=0SECS-II 01 00ABORT S1F0 for S1F23alarms는 S5F5(알람 목록 요청)를 보냅니다. 장비의 GEM 처리 결과는kind=5(Unhandled)입니다. SDK는 처리하지 않은 요청에 자동으로 응답하지 않습니다. 예제는 응답을 기다리는 요청(W 비트 1)에 트랜잭션 중단 메시지 S5F0으로 답하고ABORT줄을 출력합니다.- 호스트는 S5F0을 받고 바로
kind=3(Failed)으로 요청을 끝냅니다. 장비가 아무 응답도 하지 않으면 호스트는 T3가 지날 때까지 기다려야 합니다. directories는 S1F21(데이터 디렉터리)과 S1F23(이벤트 디렉터리)을 보냅니다. S1F21은 처리되어 빈 목록이 돌아오고, S1F23은 중단됩니다.
C# 예제의 해당 부분은 다음과 같습니다.
using System.Collections.Concurrent;using PDComm;
if (args.Length is < 4 or > 5 || args[1] is not ("host" or "equipment") || args[2] is not ("active" or "passive") || args.Length == 5 && args[4] != "--auto"){ Console.Error.WriteLine("Usage: PDComm.Tutorial <absolute-library-path> <host|equipment> <active|passive> <numeric-IP:port> [--auto]"); return 2;}try{ PdcommRuntime.Initialize(Path.GetFullPath(args[0])); bool automatic=args.Length==5; var mode=args[2]=="active"?HsmsMode.Active:HsmsMode.Passive; using GemEndpoint endpoint=args[1]=="host"? GemHost.Create(args[3],TutorialOptions.Transport(mode),TutorialOptions.HostOptions()): GemEquipment.Create(args[3],TutorialOptions.Transport(mode),TutorialOptions.EquipmentOptions()); var api=endpoint.FormattedPrograms; TutorialOptions.Configuration().Configure(endpoint); using var id=SecsItem.FromAscii("PP-DEMO",TutorialOptions.Limits); using var code=SecsItem.FromAscii("RUN",TutorialOptions.Limits); var payload=new GemFormattedProgramPayload(id,"PDComm","0.1.0",[new(code,[])]); if(endpoint is GemEquipment equipment) { api.SeedQuery(payload); using var temperature=SecsItem.FromF4([21.5f],TutorialOptions.Limits); using var lot=SecsItem.FromAscii("LOT-001",TutorialOptions.Limits); equipment.RegisterStatus(1,"Temperature","C",temperature); equipment.RegisterStatus(2,"Lot","",lot); api.ReplaceCapabilities("PDComm","0.1.0",4,4096,[TutorialOptions.Capability()]); } endpoint.Start(); if(mode==HsmsMode.Passive) Console.WriteLine($"LISTEN {endpoint.GetLocalAddress()}"); var input=new ConcurrentQueue<string>(); if(!automatic) { Console.WriteLine("Commands: state, establish, identity, values, names, online, offline, linktest, directories, alarms, transfer, query, verify-request, verify-report, capability, negotiate, store, dryrun, execute, quit"); _=Task.Run(()=> { string? line; while((line=Console.ReadLine()) is not null) { input.Enqueue(line); if(line=="quit") return; } input.Enqueue("quit"); }); } int step=0,received=0; DateTime deadline=DateTime.UtcNow.AddSeconds(20),finish=deadline; bool finishing=false,quit=false; while(!quit) { endpoint.Poll(); HsmsEvent? current; while((current=endpoint.NextEvent()) is not null) { using(current) using(var result=endpoint.HandleEvent(current)) { var info=result.GetInfo(); if(info.Kind!=GemResultKind.Ignored) { Console.WriteLine($"RESULT kind={(uint)info.Kind} request={(uint)info.RequestKind} outcome={(uint)info.Outcome} status={info.Outcome}"); using var body=result.GetBody(); if(body is not null) Console.WriteLine("SECS-II "+Convert.ToHexString(body.Encode())); } if(info.Kind==GemResultKind.Unhandled && current.GetInfo().Header is { ReplyExpected: true } primary) { // No GEM handler took this primary. Abort it with SxF0 so the peer does not // wait for T3 and the primary does not keep holding an incoming slot. try { endpoint.ReplyRaw(current,primary.Stream,0,false,ReadOnlySpan<byte>.Empty); Console.WriteLine($"ABORT S{primary.Stream}F0 for S{primary.Stream}F{primary.Function}"); } catch(PdcommException error) { Console.Error.WriteLine($"REPLY ERROR status={error.Status}"); } } if(info.Kind==GemResultKind.Primary) { switch(info.RequestKind) { case GemRequestKind.FormattedTransfer: api.RespondTransferAndStore(result,0,GemFormattedProgramStoreAction.Upsert); break; case GemRequestKind.FormattedQuery: api.RespondQuery(result); break; case GemRequestKind.FormattedVerificationRequest: api.RespondVerificationRequest(result,0); break; case GemRequestKind.FormattedVerificationReport: api.RespondVerificationReport(result); break; case GemRequestKind.FormattedCapability: api.RespondCapabilityQuery(result); break; case GemRequestKind.FormattedVerificationNegotiation: api.RespondVerification(result,0); break; } if(automatic && endpoint is GemEquipment && ++received==3) { finishing=true; finish=DateTime.UtcNow.AddMilliseconds(500); } } if(automatic && endpoint is GemHost host && info.Kind==GemResultKind.Response) { if(step==1 && info.Outcome==GemOutcome.CommunicationEstablished) { host.AreYouOnline(); step=2; } else if(step==2 && info.Outcome==GemOutcome.EquipmentIdentity) { host.RequestStatusValues([1,2]); step=3; } else if(step==3 && info.Outcome==GemOutcome.ValuesReceived) { finishing=true; finish=DateTime.UtcNow.AddMilliseconds(500); } } } } if(automatic && endpoint is GemHost autoHost && step==0 && endpoint.GetTransportState().State==HsmsConnectionState.Selected) { autoHost.BeginCommunication(); step=1; } if(automatic && finishing && DateTime.UtcNow>=finish) break; if(automatic && DateTime.UtcNow>deadline) throw new TimeoutException("Tutorial peer response deadline expired"); if(input.TryDequeue(out string? line)) { try { uint[] ids=[1,2]; if(line.StartsWith("values ",StringComparison.Ordinal) || line.StartsWith("names ",StringComparison.Ordinal)) { string[] fields=line.Split(' ',StringSplitOptions.RemoveEmptyEntries); ids=fields.Skip(1).Select(uint.Parse).ToArray(); line=fields[0]; } if(line=="quit") quit=true; else if(line=="state") Console.WriteLine($"STATE tcp={endpoint.GetTransportState().State} gem={endpoint.GetState().Communication}"); else if(line=="linktest") endpoint.Linktest(); else if(line=="transfer") api.SubmitTransfer(payload); else if(line=="query") api.SubmitQuery(id); else if(line=="verify-request") api.SubmitVerificationRequest(payload); else if(line=="verify-report") api.SubmitVerificationReport(id,[new(1,1,"demo issue")]); else if(line=="capability") api.SubmitCapabilityQuery(); else if(line=="negotiate") { using var prepared=api.PrepareVerification(id,[]); api.SubmitPreparedVerification(prepared); } else if(line=="store") { api.PutProgram(payload,GemFormattedProgramStoreAction.Upsert); Console.WriteLine($"STORE entries={api.GetStoreInfo().EntryCount}"); } else if(line is "dryrun" or "execute") { var selection=api.SelectProgram(id); using var report=api.DryRun(selection,new(1,32,8192)); Console.WriteLine($"DRYRUN outcome={(GemFormattedProgramDryRunOutcome)report.GetInfo().Outcome} issues={report.GetInfo().IssueCount}"); if(line=="execute") { using var start=api.StartExecution(selection,report); api.SubmitActionResult(GemFormattedPrograms.GetActionToken(start.Action),GemFormattedProgramActionResult.Accepted); using var action=api.StepExecution(start.Token); Console.WriteLine("APPLICATION DEMO: acknowledge RUN without controlling equipment"); api.SubmitActionResult(GemFormattedPrograms.GetActionToken(action),GemFormattedProgramActionResult.Applied); api.CompleteExecution(start.Token,1,GemFormattedProgramTerminalOutcome.Complete,ReadOnlyMemory<byte>.Empty); using var snapshot=api.GetExecutionSnapshot(); Console.WriteLine($"EXECUTION state={(GemFormattedProgramExecutionState)snapshot.GetInfo().State}"); } } else if(endpoint is GemHost host) { switch(line) { case "establish": host.BeginCommunication(); break; case "identity": host.AreYouOnline(); break; case "values": host.RequestStatusValues(ids); break; case "names": host.RequestStatusNames(ids); break; case "online": host.RequestOnline(); break; case "offline": host.RequestOffline(); break; case "directories": host.RequestDataDirectory([]); host.RequestEventDirectory([]); break; case "alarms": host.RequestAlarmList([]); break; default: throw new ArgumentException("Unknown command"); } } else throw new ArgumentException("This command requires the Host role"); } catch(Exception error) { Console.Error.WriteLine("COMMAND ERROR "+error.Message); } } Thread.Sleep(2); } endpoint.Stop(); if(automatic) Console.WriteLine($"PASS tutorial {args[1]} {args[2]}"); return 0;}catch(Exception error) { Console.Error.WriteLine(error); return 1; }두 터미널에서 quit을 입력해 끝냅니다.
8. 역할과 연결 방향을 바꿉니다
섹션 제목: “8. 역할과 연결 방향을 바꿉니다”역할(호스트·장비)과 연결 모드(Active·Passive)는 서로 독립입니다. 이번에는 C# 호스트가 기다리고 C++ 장비가 연결합니다.
dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.Tutorial/release/PDComm.Tutorial.dll" "$PDCOMM_NATIVE" host passive 127.0.0.1:5000"$PDCOMM_ROOT/build/pdcomm-tutorial" equipment active 127.0.0.1:5000호스트 터미널에서 establish와 values 1 2를 입력하면 앞과 같은 결과가 나옵니다. 장비 쪽 state는 STATE tcp=4 gem=1에서 시작합니다. 둘 다 Active이거나 둘 다 Passive이면 연결을 기다리는 쪽이나 시작하는 쪽이 없으므로 통신하지 않습니다.
9. 시뮬레이터 GUI의 장비에 연결합니다
섹션 제목: “9. 시뮬레이터 GUI의 장비에 연결합니다”시뮬레이터를 장비로 실행하고 C++ 호스트를 연결합니다.
- 시뮬레이터를 실행합니다. Windows는
& "$Simulator\pdcomm-simulator.exe", Linux는 데스크톱 세션에서"$PDCOMM_SIMULATOR/pdcomm-simulator", macOS는pdcomm-simulator.app입니다. - Overview의 Endpoint configuration에서 GEM role을 Equipment, TCP mode를 Passive - listen, Address를
127.0.0.1:5000, Session ID를7로 정합니다. - Start session을 누릅니다. TCP TRANSPORT 카드가 Listening이 됩니다.
- 두 번째 터미널에서 C++ 호스트를 시작하고
establish,values 1001 1002를 입력합니다.
"$PDCOMM_ROOT/build/pdcomm-tutorial" host active 127.0.0.1:5000RESULT kind=2 request=1 outcome=2SECS-II 01 02 21 01 00 01 02 41 10 50 44 43 6f 6d 6d 20 53 69 6d 75 6c 61 74 6f 72 41 01 31RESULT kind=2 request=3 outcome=5SECS-II 01 02 41 04 49 44 4c 45 b1 04 00 00 00 00시뮬레이터 장비의 모델명은 PDComm Simulator, 소프트웨어 버전은 1입니다. 기본 상태 변수는 SVID 1001 EquipmentState(ASCII IDLE)와 1002 WaferCount(U4 0)입니다. 튜토리얼 장비의 SVID 1, 2와 다르므로 values 1001 1002를 입력합니다.


- 시뮬레이터의 Status & Data에서 SVID 1002를 선택하고 Value에
5를 넣은 뒤 Apply value를 누릅니다. - 호스트에서
values 1001 1002를 다시 입력합니다. 두 번째 값이b1 04 00 00 00 05(U4 5)로 바뀝니다.


- 호스트에서
quit을 입력하고, 시뮬레이터에서 Stop을 누릅니다.
시뮬레이터 화면 전체는 시뮬레이터 GUI 가이드에서 설명합니다.
자주 생기는 오류
섹션 제목: “자주 생기는 오류”| 증상 | 원인과 해결 방법 |
|---|---|
COMMAND ERROR PDComm native operation failed with status InvalidState. |
아직 Selected가 아니거나 GEM 통신이 수립되지 않았습니다. state로 상태를 확인하고 establish부터 실행합니다. |
COMMAND ERROR This command requires the Host role |
장비 터미널에 호스트 명령을 입력했습니다. |
COMMAND ERROR Unknown command |
명령 이름이 틀렸습니다. C++ 예제는 directories, alarms, 정형 프로그램 명령을 지원하지 않습니다. |
values의 값이 모두 01 00 |
상대 장비에 없는 SVID입니다. 튜토리얼 장비는 1, 2, 시뮬레이터는 1001, 1002입니다. |
시뮬레이터와 연결되지만 establish 뒤 5초(T3)가 지나서야 RESULT kind=3 request=1이 나옵니다. |
시뮬레이터의 Session ID가 7이 아닙니다. HSMS 세션 ID가 다르면 상대는 데이터 메시지를 처리하지 않습니다. 양쪽을 같은 값으로 맞춥니다. |
요청마다 RESULT kind=3이 T3 뒤에 나옵니다. |
상대가 요청을 처리하지 않고 응답도 하지 않습니다. PDComm 시뮬레이터 장비는 처리하지 않은 요청을 SxF0으로 바로 중단하므로 이 증상이 나오지 않습니다. 연결과 타임아웃 문제를 참고합니다. |
모든 터미널에서 quit을 입력하고, 시뮬레이터는 Stop을 누른 뒤 닫습니다.
다음 단계
섹션 제목: “다음 단계”- 데이터 수집에서 데이터 변수, 장비 상수, 디렉터리를 다룹니다.
- 보고서와 이벤트에서 장비가 먼저 보내는 이벤트 보고를 받습니다.
- 결과 코드의 전체 목록은 상태 코드와 결과 읽기에 있습니다.