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

첫 통신

  • 장비와 호스트를 대화형으로 실행하고 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"로 바꿉니다.

첫 번째 터미널에서 장비를 Passive 모드로 시작합니다.

터미널 창
"$PDCOMM_ROOT/build/pdcomm-tutorial" equipment passive 127.0.0.1:5000
LISTEN 127.0.0.1:5000
Commands: state, establish, identity, values, names, online, offline, linktest, quit

장비는 SVID 1(Temperature, F4 21.5)과 SVID 2(Lot, ASCII LOT-001)를 등록하고, 제어 상태 HostOffline으로 시작합니다.

두 번째 터미널에서 C# 호스트를 Active 모드로 시작합니다. C# 예제는 C++ 예제의 명령에 더해 디렉터리, 알람 목록, 정형 공정 프로그램 명령을 제공합니다.

터미널 창
dotnet "$PDCOMM_ROOT/build/managed/bin/PDComm.Tutorial/release/PDComm.Tutorial.dll" "$PDCOMM_NATIVE" host active 127.0.0.1:5000

Commands: 줄이 보이면 연결과 Select가 끝났습니다. 아래 단계의 명령을 한 줄씩 입력하고, 결과가 나온 뒤 다음 명령을 입력합니다.

state
establish
state
STATE tcp=Selected gem=NotCommunicating
RESULT kind=2 request=1 outcome=2 status=CommunicationEstablished
SECS-II 0102210100010241065044436F6D6D4105302E312E30
STATE 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=2
SECS-II 01 00

4. 상태 변수의 이름과 값을 읽습니다

섹션 제목: “4. 상태 변수의 이름과 값을 읽습니다”
names 1 2
values 2 1
values 9
RESULT kind=2 request=4 outcome=6 status=NamesReceived
SECS-II 01020103B10400000001410B54656D70657261747572654101430103B1040000000241034C6F744100
RESULT kind=2 request=3 outcome=5 status=ValuesReceived
SECS-II 010241074C4F542D303031910441AC0000
RESULT kind=2 request=3 outcome=5 status=ValuesReceived
SECS-II 01010100
  • names 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. 온라인과 오프라인을 요청합니다”
online
offline
RESULT kind=2 request=6 outcome=12 status=OnlineAck
SECS-II 210100
RESULT kind=2 request=5 outcome=11 status=OfflineAck
SECS-II 210100

online은 S1F17, offline은 S1F15를 보냅니다. 응답 본문 21 01 00은 B 0x00이며 ONLACK 0(수락), OFLACK 0(확인)입니다. 장비 터미널의 outcome=8(OnlineAccepted)과 outcome=7(OfflineAccepted)은 장비의 제어 상태가 실제로 바뀌었다는 뜻입니다.

통신 상태와 제어 상태는 별개입니다. Communicating이어도 장비가 오프라인일 수 있고, 원격 명령은 OnlineRemote 같은 제어 상태를 추가로 요구할 수 있습니다. 자세한 내용은 GEM 상태 모델을 참고합니다.

linktest
state

linktest는 HSMS 제어 메시지 Linktest.req를 보냅니다. GEM 결과가 아니므로 RESULT 줄이 나오지 않습니다. 응답이 오지 않으면 T6 뒤에 연결이 끊어지므로, 이어서 입력한 state가 tcp=Selected이면 Linktest가 성공한 것입니다. 시뮬레이터의 Message Trace & Diagnostics에서는 이 왕복을 직접 볼 수 있습니다.

7. 장비가 처리하지 않는 요청을 보냅니다

섹션 제목: “7. 장비가 처리하지 않는 요청을 보냅니다”

튜토리얼 장비는 알람 카탈로그와 이벤트 디렉터리를 설정하지 않았습니다. 호스트에서 다음을 입력합니다.

alarms
directories

호스트 출력:

RESULT kind=3 request=13 outcome=0 status=None
RESULT kind=2 request=21 outcome=36 status=DataDirectoryReceived
SECS-II 0100
RESULT kind=3 request=22 outcome=0 status=None

장비 출력:

RESULT kind=5 request=0 outcome=0
SECS-II 01 00
ABORT S5F0 for S5F5
RESULT kind=1 request=0 outcome=38
SECS-II 01 00
RESULT kind=5 request=0 outcome=0
SECS-II 01 00
ABORT S1F0 for S1F23
  • alarms는 S5F5(알람 목록 요청)를 보냅니다. 장비의 GEM 처리 결과는 kind=5(Unhandled)입니다. SDK는 처리하지 않은 요청에 자동으로 응답하지 않습니다. 예제는 응답을 기다리는 요청(W 비트 1)에 트랜잭션 중단 메시지 S5F0으로 답하고 ABORT 줄을 출력합니다.
  • 호스트는 S5F0을 받고 바로 kind=3(Failed)으로 요청을 끝냅니다. 장비가 아무 응답도 하지 않으면 호스트는 T3가 지날 때까지 기다려야 합니다.
  • directories는 S1F21(데이터 디렉터리)과 S1F23(이벤트 디렉터리)을 보냅니다. S1F21은 처리되어 빈 목록이 돌아오고, S1F23은 중단됩니다.

C# 예제의 해당 부분은 다음과 같습니다.

examples/tutorial/PDComm.Tutorial/Program.cs
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++ 호스트를 연결합니다.

  1. 시뮬레이터를 실행합니다. Windows는 & "$Simulator\pdcomm-simulator.exe", Linux는 데스크톱 세션에서 "$PDCOMM_SIMULATOR/pdcomm-simulator", macOS는 pdcomm-simulator.app입니다.
  2. Overview의 Endpoint configuration에서 GEM role을 Equipment, TCP mode를 Passive - listen, Address를 127.0.0.1:5000, Session ID를 7로 정합니다.
  3. Start session을 누릅니다. TCP TRANSPORT 카드가 Listening이 됩니다.
  4. 두 번째 터미널에서 C++ 호스트를 시작하고 establish, values 1001 1002를 입력합니다.
터미널 창
"$PDCOMM_ROOT/build/pdcomm-tutorial" host active 127.0.0.1:5000
RESULT kind=2 request=1 outcome=2
SECS-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 31
RESULT kind=2 request=3 outcome=5
SECS-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를 입력합니다.

Equipment 역할로 Listening 후 Communicating 상태가 된 시뮬레이터 Overview 화면Equipment 역할로 Listening 후 Communicating 상태가 된 시뮬레이터 Overview 화면
  1. 시뮬레이터의 Status & Data에서 SVID 1002를 선택하고 Value에 5를 넣은 뒤 Apply value를 누릅니다.
  2. 호스트에서 values 1001 1002를 다시 입력합니다. 두 번째 값이 b1 04 00 00 00 05(U4 5)로 바뀝니다.
장비 상태 변수 1001과 1002가 보이는 Status & Data 화면장비 상태 변수 1001과 1002가 보이는 Status & Data 화면
  1. 호스트에서 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을 누른 뒤 닫습니다.