SungjuNewPrime/OPC Server Setting/OPC Client sample(DotNet to Delphi13)/Delphi13_VCL/__history/uOPCClient.pas.~13~
jhw 6516e4a8f0 first upload
OPC test program upload
2026-09-04 11:41:21 +09:00

449 lines
13 KiB
Plaintext

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.