unit uOPCClient; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, System.Win.ComObj; const ITEMMAX = 8; VAL_CTRL_SPACE = 8; VAL_CTRL_TOP = 192; VAL_CTRL_LEFT = 16; VAL_ITEMNAME_HEIGHT = 24; VAL_ITEMNAME_WIDTH = 88; VAL_VALUE_HEIGHT = 24; VAL_VALUE_WIDTH = 48; VAL_TIME_HEIGHT = 24; VAL_TIME_WIDTH = 128; VAL_QUALITY_HEIGHT = 24; VAL_QUALITY_WIDTH = 40; VAL_TYPE_HEIGHT = 24; VAL_TYPE_WIDTH = 56; // VarType 표시 칸 type TfOPCClient = class(TForm) Label5: TLabel; Label1: TLabel; Label2: TLabel; Label3: TLabel; Label4: TLabel; txtSvrName: TEdit; btnConnect: TButton; btnRead: TButton; btnWrite: TButton; btnAdvise: TButton; btnAsyncRead: TButton; btnAsyncWrite: TButton; txtUpdateRate: TEdit; Label6: TLabel; procedure FormCreate(Sender: TObject); procedure btnConnectClick(Sender: TObject); procedure btnReadClick(Sender: TObject); procedure btnWriteClick(Sender: TObject); procedure btnAdviseClick(Sender: TObject); procedure btnAsyncReadClick(Sender: TObject); procedure btnAsyncWriteClick(Sender: TObject); procedure FormDestroy(Sender: TObject); private txtItemName: array[0..ITEMMAX-1] of TEdit; txtValue: array[0..ITEMMAX-1] of TEdit; txtTime: array[0..ITEMMAX-1] of TEdit; txtQuality: array[0..ITEMMAX-1] of TEdit; txtType: array[0..ITEMMAX-1] of TEdit; // VarType 표시 칸 OPCServer: OLEVariant; OPCGroup: OLEVariant; OPCItemObjects: array[1..ITEMMAX] of OLEVariant; sH: array[1..ITEMMAX] of Integer; FItemVarTypes: array[1..ITEMMAX] of Integer; // Read 시 저장한 각 아이템의 실제 VarType bConnect: Boolean; wTransID: Integer; procedure SetupUI; procedure SyncDataToUI(ItemValues, Qualities, TimeStamps: OLEVariant); public end; var fOPCClient: TfOPCClient; implementation {$R *.dfm} procedure TfOPCClient.FormCreate(Sender: TObject); begin SetupUI; bConnect := False; wTransID := 1000; end; procedure TfOPCClient.FormDestroy(Sender: TObject); begin if bConnect then btnConnectClick(Self); end; procedure TfOPCClient.SetupUI; var i: Integer; begin for i := 0 to ITEMMAX - 1 do begin txtItemName[i] := TEdit.Create(Self); txtItemName[i].Parent := Self; txtItemName[i].Top := VAL_CTRL_TOP + (VAL_ITEMNAME_HEIGHT + VAL_CTRL_SPACE) * i; txtItemName[i].Left := VAL_CTRL_LEFT; txtItemName[i].Width := VAL_ITEMNAME_WIDTH; txtItemName[i].Height := VAL_ITEMNAME_HEIGHT; txtItemName[i].Text := 'SYSTEM.Tag00'+IntToStr(i+2); txtValue[i] := TEdit.Create(Self); txtValue[i].Parent := Self; txtValue[i].Top := VAL_CTRL_TOP + (VAL_VALUE_HEIGHT + VAL_CTRL_SPACE) * i; txtValue[i].Left := VAL_CTRL_LEFT + VAL_ITEMNAME_WIDTH + VAL_CTRL_SPACE; txtValue[i].Width := VAL_VALUE_WIDTH; txtValue[i].Height := VAL_VALUE_HEIGHT; txtTime[i] := TEdit.Create(Self); txtTime[i].Parent := Self; txtTime[i].Top := VAL_CTRL_TOP + (VAL_TIME_HEIGHT + VAL_CTRL_SPACE) * i; txtTime[i].Left := txtValue[i].Left + VAL_VALUE_WIDTH + VAL_CTRL_SPACE; txtTime[i].Width := VAL_TIME_WIDTH; txtTime[i].Height := VAL_TIME_HEIGHT; txtQuality[i] := TEdit.Create(Self); txtQuality[i].Parent := Self; txtQuality[i].Top := VAL_CTRL_TOP + (VAL_QUALITY_HEIGHT + VAL_CTRL_SPACE) * i; txtQuality[i].Left := txtTime[i].Left + VAL_TIME_WIDTH + VAL_CTRL_SPACE; txtQuality[i].Width := VAL_QUALITY_WIDTH; txtQuality[i].Height := VAL_QUALITY_HEIGHT; txtQuality[i].ReadOnly := True; txtType[i] := TEdit.Create(Self); txtType[i].Parent := Self; txtType[i].Top := VAL_CTRL_TOP + (VAL_TYPE_HEIGHT + VAL_CTRL_SPACE) * i; txtType[i].Left := txtQuality[i].Left + VAL_QUALITY_WIDTH + VAL_CTRL_SPACE; txtType[i].Width := VAL_TYPE_WIDTH; txtType[i].Height := VAL_TYPE_HEIGHT; txtType[i].ReadOnly := True; txtType[i].Color := $00F0F0F0; // 연회색 배경 end; end; procedure TfOPCClient.btnConnectClick(Sender: TObject); var i: Integer; StepMsg: string; AddedItem: OLEVariant; begin if Trim(txtSvrName.Text) = '' then begin ShowMessage('Server Name is not registered.'); Exit; end; if not bConnect then begin try StepMsg := 'CreateOleObject'; // Create OPC Server using Late Binding OPCServer := CreateOleObject('OPC.Automation.1'); StepMsg := 'OPCServer.Connect'; OPCServer.Connect(txtSvrName.Text, EmptyParam); StepMsg := 'OPCGroups.Add'; OPCGroup := OPCServer.OPCGroups.Add('Group1'); StepMsg := 'Set UpdateRate'; OPCGroup.UpdateRate := StrToIntDef(txtUpdateRate.Text, 1000); StepMsg := 'Set IsActive'; OPCGroup.IsActive := True; StepMsg := 'Set IsSubscribed'; OPCGroup.IsSubscribed := False; StepMsg := 'AddItem (Loop)'; // 태그 이름이 입력된 슬롯만 AddItem 등록 (빈 슬롯은 스킵) for i := 1 to ITEMMAX do begin if Trim(txtItemName[i - 1].Text) = '' then begin OPCItemObjects[i] := Unassigned; sH[i] := 0; txtItemName[i - 1].Enabled := False; Continue; end; AddedItem := OPCGroup.OPCItems.AddItem(txtItemName[i - 1].Text, i); OPCItemObjects[i] := AddedItem; sH[i] := AddedItem.ServerHandle; txtItemName[i - 1].Enabled := False; end; bConnect := True; btnConnect.Caption := 'Disconnect'; btnRead.Enabled := True; btnWrite.Enabled := True; btnAdvise.Enabled := True; txtSvrName.Enabled := False; except on E: Exception do begin ShowMessage('Connection failed at [' + StepMsg + ']: ' + E.Message); Exit; end; end; end else begin try if not VarIsEmpty(OPCGroup) then OPCServer.OPCGroups.RemoveAll; if not VarIsEmpty(OPCServer) then OPCServer.Disconnect; except end; for i := 1 to ITEMMAX do OPCItemObjects[i] := Unassigned; OPCGroup := Unassigned; OPCServer := Unassigned; bConnect := False; btnConnect.Caption := 'Connect'; btnRead.Enabled := False; btnWrite.Enabled := False; btnAdvise.Enabled := False; btnAsyncRead.Enabled := False; btnAsyncWrite.Enabled := False; txtSvrName.Enabled := True; for i := 1 to ITEMMAX do txtItemName[i - 1].Enabled := True; end; end; procedure TfOPCClient.btnReadClick(Sender: TObject); var i: Integer; ItemValue: OLEVariant; Quality: OLEVariant; TimeStamp: OLEVariant; begin if not bConnect then Exit; try for i := 1 to ITEMMAX do begin // 빈 슬롯 스킵 if VarIsEmpty(OPCItemObjects[i]) or VarIsNull(OPCItemObjects[i]) then Continue; OPCItemObjects[i].Read(Integer(1), ItemValue, Quality, TimeStamp); // CanonicalDataType: 서버가 선언한 태그의 원본 타입 (VarType(ItemValue)와 다를 수 있음) // Write 시 반드시 이 타입을 사용해야 E_INVALIDARG를 피할 수 있음 FItemVarTypes[i] := OPCItemObjects[i].CanonicalDataType; case FItemVarTypes[i] of varSmallInt: txtType[i-1].Text := 'Short'; varInteger: txtType[i-1].Text := 'Int'; varSingle: txtType[i-1].Text := 'Float'; varDouble: txtType[i-1].Text := 'Double'; varBoolean: txtType[i-1].Text := 'Bool'; varOleStr: txtType[i-1].Text := 'String'; varByte: txtType[i-1].Text := 'Byte'; varWord: txtType[i-1].Text := 'Word'; varLongWord: txtType[i-1].Text := 'DWord'; varInt64: txtType[i-1].Text := 'Int64'; else txtType[i-1].Text := 'CDT' + IntToStr(FItemVarTypes[i]); end; txtValue[i - 1].Text := VarToStr(ItemValue); if not VarIsEmpty(TimeStamp) and not VarIsNull(TimeStamp) then txtTime[i - 1].Text := DateTimeToStr(VarToDateTime(TimeStamp)); if not VarIsEmpty(Quality) and not VarIsNull(Quality) then txtQuality[i - 1].Text := VarToStr(Quality); end; except on E: Exception do ShowMessage('Read Error: ' + E.Message); end; end; procedure TfOPCClient.btnWriteClick(Sender: TObject); var i, cnt: Integer; ServerHandles: OLEVariant; ItemValues: OLEVariant; Errors: OLEVariant; targetVT: Integer; inputText: string; ItemValue: OLEVariant; Quality: OLEVariant; TimeStamp: OLEVariant; begin if not bConnect then Exit; // 유효한 아이템 수 집계 cnt := 0; for i := 1 to ITEMMAX do if not VarIsEmpty(OPCItemObjects[i]) and not VarIsNull(OPCItemObjects[i]) then Inc(cnt); if cnt = 0 then Exit; // OPCGroup.SyncWrite() 사용: OPCItem.Write()는 IDispatch 경유 시 // VT_I2(Short) 마샬링 문제로 0x80070057(E_INVALIDARG)이 발생하는 경우가 있음 ServerHandles := VarArrayCreate([1, cnt], varInteger); ItemValues := VarArrayCreate([1, cnt], varVariant); // Errors는 반드시 미리 배열로 초기화해야 함 (Unassigned 전달 시 DISP_E_TYPEMISMATCH 발생) Errors := VarArrayCreate([1, cnt], varInteger); cnt := 0; for i := 1 to ITEMMAX do begin if VarIsEmpty(OPCItemObjects[i]) or VarIsNull(OPCItemObjects[i]) then Continue; inputText := Trim(txtValue[i - 1].Text); if inputText = '' then Continue; Inc(cnt); ServerHandles[cnt] := sH[i]; // CanonicalDataType 기반으로 올바른 타입의 Variant 생성 // ※ VarAsType() 대신 Delphi 직접 캐스팅 사용: // varVariant 배열 요소에 VarAsType 결과를 넣으면 VT_VARIANT|VT_I2 로 이중 래핑되어 // SyncWrite에서 DISP_E_TYPEMISMATCH가 발생할 수 있음 targetVT := FItemVarTypes[i]; if targetVT = 0 then targetVT := varSmallInt; case targetVT of varSmallInt: ItemValues[cnt] := SmallInt(StrToIntDef(inputText, 0)); varInteger: ItemValues[cnt] := Integer(StrToIntDef(inputText, 0)); varByte: ItemValues[cnt] := Byte(StrToIntDef(inputText, 0)); varWord: ItemValues[cnt] := Word(StrToIntDef(inputText, 0)); varLongWord: ItemValues[cnt] := LongWord(StrToInt64Def(inputText, 0)); varInt64: ItemValues[cnt] := Int64(StrToInt64Def(inputText, 0)); varSingle: ItemValues[cnt] := Single(StrToFloatDef(inputText, 0.0)); varDouble: ItemValues[cnt] := Double(StrToFloatDef(inputText, 0.0)); varBoolean: ItemValues[cnt] := (inputText <> '0') and (inputText <> ''); varOleStr: ItemValues[cnt] := inputText; else ItemValues[cnt] := inputText; // 알 수 없는 타입 → 문자열로 전송 end; end; if cnt = 0 then Exit; try // OPCGroup.SyncWrite(Integer(cnt), ServerHandles, ItemValues, Errors); // OPCItemObjects[1].Read(Integer(1), ItemValue, Quality, TimeStamp); OPCItemObjects[1].write(Integer(1), ServerHandles, Integer(1), Errors); ShowMessage('Write 완료!'); except on E: EOleException do ShowMessage('Write Error [0x' + IntToHex(E.ErrorCode, 8) + ']: ' + E.Message + #13#10 + 'CanonicalDataType: ' + IntToStr(targetVT) + #13#10 + '입력값: ' + inputText); on E: Exception do ShowMessage('Write Error [' + E.ClassName + ']: ' + E.Message); end; end; procedure TfOPCClient.btnAdviseClick(Sender: TObject); begin try OPCGroup.IsActive := not OPCGroup.IsActive; OPCGroup.IsSubscribed := OPCGroup.IsActive; if OPCGroup.IsActive then begin btnAdvise.Caption := 'Unadvise'; btnAsyncRead.Enabled := True; btnAsyncWrite.Enabled := True; btnReadClick(Self); end else begin btnAdvise.Caption := 'Advise'; btnAsyncRead.Enabled := False; btnAsyncWrite.Enabled := False; end; except on E: Exception do ShowMessage('Advise Error: ' + E.Message); end; end; procedure TfOPCClient.btnAsyncReadClick(Sender: TObject); var i: Integer; ServerHandles: OLEVariant; Errors: OLEVariant; CancelID: Integer; begin Inc(wTransID); ServerHandles := VarArrayCreate([1, ITEMMAX], varInteger); for i := 1 to ITEMMAX do ServerHandles[i] := sH[i]; // OPCItemObjects[i].Read(Integer(1), ItemValue, Quality, TimeStamp); try Errors := Unassigned; OPCGroup.AsyncRead(Integer(ITEMMAX), ServerHandles, Errors, Integer(wTransID), CancelID); except on E: Exception do ShowMessage('AsyncRead Error: ' + E.Message); end; end; procedure TfOPCClient.btnAsyncWriteClick(Sender: TObject); var i: Integer; ServerHandles: OLEVariant; ItemValues: OLEVariant; Errors: OLEVariant; CancelID: Integer; begin Inc(wTransID); ServerHandles := VarArrayCreate([1, ITEMMAX], varInteger); ItemValues := VarArrayCreate([1, ITEMMAX], varVariant); for i := 1 to ITEMMAX do begin ServerHandles[i] := sH[i]; ItemValues[i] := txtValue[i - 1].Text; end; try Errors := Unassigned; OPCGroup.AsyncWrite(Integer(ITEMMAX), ServerHandles, ItemValues, Errors, Integer(wTransID), CancelID); except on E: Exception do ShowMessage('AsyncWrite Error: ' + E.Message); end; end; procedure TfOPCClient.SyncDataToUI(ItemValues, Qualities, TimeStamps: OLEVariant); var i: Integer; begin for i := 1 to ITEMMAX do begin if not VarIsEmpty(ItemValues[i]) then txtValue[i - 1].Text := VarToStr(ItemValues[i]); if not VarIsEmpty(TimeStamps[i]) then txtTime[i - 1].Text := DateTimeToStr(VarToDateTime(TimeStamps[i])); if not VarIsEmpty(Qualities[i]) then txtQuality[i - 1].Text := VarToStr(Qualities[i]); end; end; end.