449 lines
13 KiB
Plaintext
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), Quality, 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.
|