unit FMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, Vcl.StdCtrls, cl_Irinos, cl_MscCall, cl_Opcodes,
  Vcl.ExtCtrls, Vcl.Grids, Vcl.Samples.Spin, Vcl.Mask;

const
	cNStaticChannels = 24;    // Number of static channels displayed on the GUI
  cNBitIOs = 128;           // Number of digital inputs/outputs displayed on the GUI
  cNDynChannels = 48;       // Max. number of dynamic measurement channels

type
  TMainForm = class(TForm)
    lbHistory: TListBox;
    statusBarMain: TStatusBar;
    btnConnect: TButton;
    tbControlMain: TPageControl;
    tabPageReadme: TTabSheet;
    tbReadme: TMemo;
    tabPageStatic: TTabSheet;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Label6: TLabel;
    Label7: TLabel;
    Label8: TLabel;
    lbStaticT1: TLabel;
    lbStaticT2: TLabel;
    lbStaticT3: TLabel;
    lbStaticT4: TLabel;
    lbStaticT5: TLabel;
    lbStaticT6: TLabel;
    lbStaticT7: TLabel;
    lbStaticT8: TLabel;
    GroupBox1: TGroupBox;
    GroupBox2: TGroupBox;
    lbHardStat1: TLabel;
    lbHardStat2: TLabel;
    lbHardStat3: TLabel;
    lbHardStat4: TLabel;
    lbHardStat5: TLabel;
    lbHardStat6: TLabel;
    lbHardStat7: TLabel;
    lbHardStat8: TLabel;
    Label9: TLabel;
    Label10: TLabel;
    Label11: TLabel;
    Label12: TLabel;
    Label13: TLabel;
    Label14: TLabel;
    Label15: TLabel;
    Label16: TLabel;
    lbStaticT16: TLabel;
    lbStaticT15: TLabel;
    lbStaticT14: TLabel;
    lbStaticT13: TLabel;
    lbStaticT12: TLabel;
    lbStaticT11: TLabel;
    lbStaticT10: TLabel;
    lbStaticT9: TLabel;
    lbHardStat9: TLabel;
    lbHardStat10: TLabel;
    lbHardStat11: TLabel;
    lbHardStat12: TLabel;
    lbHardStat13: TLabel;
    lbHardStat14: TLabel;
    lbHardStat15: TLabel;
    lbHardStat16: TLabel;
    Label33: TLabel;
    Label34: TLabel;
    Label35: TLabel;
    Label36: TLabel;
    Label37: TLabel;
    Label38: TLabel;
    Label39: TLabel;
    Label40: TLabel;
    lbStaticT24: TLabel;
    lbStaticT23: TLabel;
    lbStaticT22: TLabel;
    lbStaticT21: TLabel;
    lbStaticT20: TLabel;
    lbStaticT19: TLabel;
    lbStaticT18: TLabel;
    lbStaticT17: TLabel;
    lbHardStat17: TLabel;
    lbHardStat18: TLabel;
    lbHardStat19: TLabel;
    lbHardStat20: TLabel;
    lbHardStat21: TLabel;
    lbHardStat22: TLabel;
    lbHardStat23: TLabel;
    lbHardStat24: TLabel;
    tmrUpdateStatic: TTimer;
    lbStaticUpdates: TLabel;
    lbHardStatUpdates: TLabel;
    tabPageBitIO: TTabSheet;
    gbDigInputs: TGroupBox;
    gbDigOutputs: TGroupBox;
    lbBitIOUpdates: TLabel;
    tabPageDynamic: TTabSheet;
    gbDynTimeControlled: TGroupBox;
    gbDynPosControlled: TGroupBox;
    gbDynMeasStatus: TGroupBox;
    gbDynMeasValues: TGroupBox;
    Label17: TLabel;
    Label18: TLabel;
    seDynTimeNValues: TSpinEdit;
    rbDynTime100us: TRadioButton;
    rbDynTime1ms: TRadioButton;
    rbDynTime10ms: TRadioButton;
    Label19: TLabel;
    Label20: TLabel;
    btnDynTimeStart: TButton;
    cbDynTrg1Active: TCheckBox;
    cbDynTrg1WasActive: TCheckBox;
    cbDynTrg1EventOccured: TCheckBox;
    cbDyn1Active: TCheckBox;
    cbDyn1WasActive: TCheckBox;
    cbDyn1ValueStored: TCheckBox;
    cbDyn1ReadActive: TCheckBox;
    cbDyn1BufferFull: TCheckBox;
    tmrUpdateDynMeas: TTimer;
    btnDynPosStart: TButton;
    lbDynValuesPerChannelReceived: TLabel;
    lbDynBytesPerChannelReceived: TLabel;
    lbDynValuesReceived: TLabel;
    lbDynBytesReceived: TLabel;
    Label21: TLabel;
    seDynPosNValues: TSpinEdit;
    cbDyn1PosTrgChannel: TComboBox;
    Label22: TLabel;
    Label23: TLabel;
    Label24: TLabel;
    Label25: TLabel;
    Label26: TLabel;
    editDyn1Divisor: TEdit;
    editDyn1PosTriggerStart: TEdit;
    editDyn1PosTriggerDistance: TEdit;
    editDyn1PosTriggerStop: TEdit;
    btnDyn1ValuesStore: TButton;
    btnDyn1Stop: TButton;
    lbSystemstring: TLabel;
    edSysString: TEdit;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure btnConnectClick(Sender: TObject);
    procedure tmrUpdateStaticTimer(Sender: TObject);
    procedure tmrUpdateDynMeasTimer(Sender: TObject);
    procedure btnDynTimeStartClick(Sender: TObject);
    procedure editDyn1DivisorChange(Sender: TObject);
    procedure editDyn1PosTriggerStartChange(Sender: TObject);
    procedure editDyn1PosTriggerDistanceChange(Sender: TObject);
    procedure editDyn1PosTriggerStopChange(Sender: TObject);
    procedure btnDynPosStartClick(Sender: TObject);
    procedure btnDyn1ValuesStoreClick(Sender: TObject);
    procedure btnDyn1StopClick(Sender: TObject);
  private
    { Private-Deklarationen }
    cIrinos : TIrinos;
    albStatic : array [1..cNStaticChannels] of TLabel;
    albHardStat : array[1..cNStaticChannels] of TLabel;
    acbDigInputs : array[1..cNBitIOs] of TCheckBox;
    acbDigOutputs : array[1..cNBitIOs] of TCheckBox;
    acbDynChUsed : array[1..cNDynChannels] of TCheckBox;
    abCurrentDynChUsed : array[1..cNDynChannels] of Boolean;
    bTimedOut : Boolean;
    bNewStaticMeasurementValues : Boolean;
    bNewBitIO : Boolean;
    bNewHardStatData : Boolean;
    ulNStaticMeasurementValueUpdates : Cardinal;
    ulNBitIOUpdates : Cardinal;
    ulNHardStatUpdates : Cardinal;
    ulDynChannels : Cardinal;
    bDyn1DivisorValid : Boolean;
    bDyn1StartValid : Boolean;
    bDyn1DistanceValid : Boolean;
    bDyn1StopValid : Boolean;
    function  FileVersion(const filename: TFileName): String;
    procedure MessageReadStatic( var Msg : TMessage );  message WM_MESSAGE_MSC_READSTATIC;
    procedure MessageBIO( var Msg : TMessage );  message WM_MESSAGE_MSC_BITIO;
    procedure MessageReadHardStat( var Msg : TMessage );  message WM_MESSAGE_MSC_HW_STATUS;
    procedure AddHistoryEntry(s: string; bError : Boolean);
    function  ConvertFloatString(s : string; var flVal : single) : Boolean;
  public
    { Public-Deklarationen }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}




//*****************************************************************************
// FUNCTION: TMainForm.FormCreate
//-----------------------------------------------------------------------------
// - Initialize various data
// - Assign GUI objects to arrays for easier access in the rest of the code
// - Load MscDll
//*****************************************************************************/
procedure TMainForm.FormCreate(Sender: TObject);
var
  s : string;
  brk : string;
	apiv, dllv: Cardinal;
  ulCol, ulRow, ulCtr, ulI : Cardinal;
  slLeft, slTop, slLeftInc, slTopInc : Integer;
begin
  // Initialize various data
  bTimedOut := false;
  bNewStaticMeasurementValues := false;
  bNewBitIO := false;
  bNewHardStatData := false;
  ulNStaticMeasurementValueUpdates := 0;
  ulNHardStatUpdates := 0;
  ulNBitIOUpdates := 0;
  ulDynChannels := 0;
  for ulI := 1 to cNDynChannels do begin
    abCurrentDynChUsed[ulI] := false;
  end;
  bDyn1DivisorValid := false;
  bDyn1StartValid := false;
  bDyn1DistanceValid := false;
  bDyn1StopValid := false;

  // Set Readme text
  brk := AnsiString(#13#10);
  s  := 'This is a demo for the Irinos MscDll. Please note:' + brk + brk +
        'The purpose of this demo is helping software developers to integrate the Irinos-System into their software.' + brk +
        'Focus has been put on demonstrating how to use the MscDll.dll. User interface design an error handling are very basic. ' +
        'Integration into your own software probably requires a more sophisticated user interface and extended error handling.' + brk + brk +
        'THIS DEMO COMES WITHOUT ANY WARRANTY OR LIABILITY.' + brk + brk +
        'We strongly recomment to separate the GUI-update and the DLL into different Windows-threads. ' +
        'One way to do this is using Windows messages in combination with a timer, that runs in the GUI thread. ' +
        'In this example this is demonstrated by the combination of T_MSC_SetNotificationMessage, ' +
        'MessageReadStatic and tmrUpdateStaticTimer.' + brk +
        '(The MscDll also supports notification by callback or by event. However, we do not recommend these ' +
        'techniques. Therefore they are not shown here. If you need to use them, please contact us.)' + brk +
        'In this demo the MscDll.dll file must be placed in the same directory as the Exe. Further the Msc.cfg ' +
        'file is required in the same directory. The IP-settings in the Msc.cfg file must fit to the IP-Settings ' +
        'of the Irinos-System.' + brk + brk +
        'Messtechnik Sachs GmbH' + brk + 'www.messtechnik-sachs.de' + brk + 'Phone: +49 7181 99960 0';
  tbReadme.Text := s;
  tbControlMain.ActivePageIndex := 0;


  // Setup static measurement GUI
  albStatic[1] := lbStaticT1;
  albStatic[2] := lbStaticT2;
  albStatic[3] := lbStaticT3;
  albStatic[4] := lbStaticT4;
  albStatic[5] := lbStaticT5;
  albStatic[6] := lbStaticT6;
  albStatic[7] := lbStaticT7;
  albStatic[8] := lbStaticT8;
  albStatic[9] := lbStaticT9;
  albStatic[10] := lbStaticT10;
  albStatic[11] := lbStaticT11;
  albStatic[12] := lbStaticT12;
  albStatic[13] := lbStaticT13;
  albStatic[14] := lbStaticT14;
  albStatic[15] := lbStaticT15;
  albStatic[16] := lbStaticT16;
  albStatic[17] := lbStaticT17;
  albStatic[18] := lbStaticT18;
  albStatic[19] := lbStaticT19;
  albStatic[20] := lbStaticT20;
  albStatic[21] := lbStaticT21;
  albStatic[22] := lbStaticT22;
  albStatic[23] := lbStaticT23;
  albStatic[24] := lbStaticT24;
  albHardStat[1] := lbHardStat1;
  albHardStat[2] := lbHardStat2;
  albHardStat[3] := lbHardStat3;
  albHardStat[4] := lbHardStat4;
  albHardStat[5] := lbHardStat5;
  albHardStat[6] := lbHardStat6;
  albHardStat[7] := lbHardStat7;
  albHardStat[8] := lbHardStat8;
  albHardStat[9] := lbHardStat9;
  albHardStat[10] := lbHardStat10;
  albHardStat[11] := lbHardStat11;
  albHardStat[12] := lbHardStat12;
  albHardStat[13] := lbHardStat13;
  albHardStat[14] := lbHardStat14;
  albHardStat[15] := lbHardStat15;
  albHardStat[16] := lbHardStat16;
  albHardStat[17] := lbHardStat17;
  albHardStat[18] := lbHardStat18;
  albHardStat[19] := lbHardStat19;
  albHardStat[20] := lbHardStat20;
  albHardStat[21] := lbHardStat21;
  albHardStat[22] := lbHardStat22;
  albHardStat[23] := lbHardStat23;
  albHardStat[24] := lbHardStat24;

  // Setup Bit I/O GUI
  ulCtr := 1;
  slLeft := 7;
  slLeftInc := (gbDigInputs.Width - 7) div 8;
  slTopInc := (gbDigInputs.Height - 18) div 16;
  for ulCol := 1 to 8 do begin
    slTop := 18;
    for ulRow := 1 to 16 do begin
      acbDigInputs[ulCtr] := TCheckBox.Create(self);
      acbDigInputs[ulCtr].Caption := 'T' + IntToStr(ulCtr);
      acbDigInputs[ulCtr].Parent := gbDigInputs;
      acbDigInputs[ulCtr].Left := slLeft;
      acbDigInputs[ulCtr].Top := slTop;
      slTop := slTop + slTopInc;
      Inc(ulCtr);
    end;
    slLeft := slLeft + slLeftInc;
  end;

  ulCtr := 1;
  slLeft := 7;
  slLeftInc := (gbDigOutputs.Width - 7) div 8;
  slTopInc := (gbDigOutputs.Height - 18) div 16;
  for ulCol := 1 to 8 do begin
    slTop := 18;
    for ulRow := 1 to 16 do begin
      acbDigOutputs[ulCtr] := TCheckBox.Create(self);
      acbDigOutputs[ulCtr].Caption := 'T' + IntToStr(ulCtr);
      acbDigOutputs[ulCtr].Parent := gbDigOutputs;
      acbDigOutputs[ulCtr].Left := slLeft;
      acbDigOutputs[ulCtr].Top := slTop;
      slTop := slTop + slTopInc;
      Inc(ulCtr);
    end;
    slLeft := slLeft + slLeftInc;
  end;


  // Setup dynamic measurement GUI
  ulCtr := 1;
  slLeft := 7;
  slLeftInc := 60;
  slTopInc := (tabPageDynamic.Height - 30) div 16;
  for ulCol := 1 to 3 do begin
    slTop := 30;
    for ulRow := 1 to 16 do begin
      acbDynChUsed[ulCtr] := TCheckBox.Create(self);
      acbDynChUsed[ulCtr].Caption := 'T' + IntToStr(ulCtr);
      acbDynChUsed[ulCtr].Parent := tabPageDynamic;
      acbDynChUsed[ulCtr].Left := slLeft;
      acbDynChUsed[ulCtr].Top := slTop;
      acbDynChUsed[ulCtr].Width := 55;
      slTop := slTop + slTopInc;
      Inc(ulCtr);
    end;
    slLeft := slLeft + slLeftInc;
  end;
  for ulCtr := 1 to 3 do begin
    acbDynChUsed[ulCtr].Checked := true;
  end;
  for ulCtr := 1 to 256 do begin
    cbDyn1PosTrgChannel.Items.Add('T' + IntToStr(ulCtr));
  end;
  cbDyn1PosTrgChannel.ItemIndex := 0;
  editDyn1DivisorChange(Sender);
  editDyn1PosTriggerStartChange(Sender);
  editDyn1PosTriggerDistanceChange(Sender);
  editDyn1PosTriggerStopChange(Sender);


  // Try to load the Irinos Msc-DLL
	cIrinos := TIrinos.Create;
	if cIrinos.Load('.\MscDll.dll') then begin
		cIrinos.F_MSC_GetVersion(apiv, dllv);
		s := Format('API Version %d.%d, DLL Version %d.%d',
			[apiv shr 16, apiv and 65535, dllv shr 16, dllv and 65535]);

    AddHistoryEntry('MscDll loaded successfully', false);
	end
	else begin
    s := '';
    AddHistoryEntry('Cannot load MscDll', false);
	end;

  // Show version info in form title
	Caption := ExtractFileName(Application.ExeName) + '  ' + FileVersion(Application.ExeName) + ' ### ' + s;

end;



//*****************************************************************************
// FUNCTION: TMainForm.FormClose
//-----------------------------------------------------------------------------
// - Stop DLL communication (if active)
//*****************************************************************************/
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  cIrinos.StopAndClose;
end;


//*****************************************************************************
// FUNCTION: TMainForm.FileVersion
//-----------------------------------------------------------------------------
// Read file version and build a string with version information
//-----------------------------------------------------------------------------
// PARAMETERS:
// fileName: file version will be read from this file
//-----------------------------------------------------------------------------
// Return: file version
//*****************************************************************************/
function TMainForm.FileVersion(const filename: TFileName): String;
var
	VerInfoSize: Cardinal;
	VerValueSize: Cardinal;
	Dummy: Cardinal;
	PVerInfo: Pointer;
	PVerValue: PVSFixedFileInfo;
	iLastError: DWord;
   fvMajor, fvMinor, fvRelease, fvBuild: Word;
begin
	Result := '';
	VerInfoSize := GetFileVersionInfoSize(PChar(filename), Dummy);
	if VerInfoSize > 0 then begin
		GetMem(PVerInfo, VerInfoSize);
		try
			if GetFileVersionInfo(PChar(filename), 0, VerInfoSize, PVerInfo) then begin
				if VerQueryValue(PVerInfo, '\', Pointer(PVerValue), VerValueSize) then
					with PVerValue^ do begin
						fvMajor := HiWord(dwFileVersionMS);
						fvMinor := LoWord(dwFileVersionMS);
						fvRelease := HiWord(dwFileVersionLS);
						fvBuild := LoWord(dwFileVersionLS);
						Result := Format('V%d.%d.%d Build %d', [fvMajor, fvMinor, fvRelease, fvBuild]);
					end;
			end
			else begin
				iLastError := GetLastError;
				Result := Format('GetFileVersionInfo failed: (%d) %s', [iLastError, SysErrorMessage(iLastError)]);
			end;
		finally
			FreeMem(PVerInfo, VerInfoSize);
		end;
	end
	else begin
		iLastError := GetLastError;
		Result := Format('GetFileVersionInfo failed: (%d) %s', [iLastError, SysErrorMessage(iLastError)]);
	end;
end;



//*****************************************************************************
// FUNCTION: TMainForm.AddHistoryEntry
//-----------------------------------------------------------------------------
// Add a new line with information for the user in the history-Listbox.
//-----------------------------------------------------------------------------
// PARAMETERS:
// s: String to display in the new line
// bError: false -> "only" information
//         true -> an error occured
//         So far this parameter is not used. It is intended future
//         extension of this function. However, when calling this function,
//         this parameter should be used as intended.
//-----------------------------------------------------------------------------
// Return: none
//*****************************************************************************/
procedure TMainForm.AddHistoryEntry(s: string; bError : Boolean);
var
   sType : string;
begin
    if bError = false then begin
       sType := 'Info';
    end
    else begin
       sType := 'Error';
    end;

    s := FormatDateTime('hh:nn:ss : zzz', Now) + ' -- ' + sType + ' -- ' + s;
    lbHistory.Items.Insert(0, s);
end;


//*****************************************************************************
// FUNCTION: TMainForm.btnConnectClick
//-----------------------------------------------------------------------------
// - "Click" event of button "Connect".
// - Try to connect to the Irinos system vai the MscDll.
// - If connection has been established successfully, start reading
//   "static measurement values", "Bit I/O" and "hardware status" continuously.
//   If new data is available for one of these three, a windows message will
//   be send to this form.
//*****************************************************************************/
procedure TMainForm.btnConnectClick(Sender: TObject);
var
  s : string;
  anStr : AnsiString;
begin
  if (cIrinos.eConnectStat = cscConnectionClosed) OR (cIrinos.eConnectStat = cscUndefined) then begin
    AddHistoryEntry('Trying to establish connection.', false);
    Application.ProcessMessages;

    if cIrinos.ConnectIrinos <> cscCommStarted then begin
      s := 'Failed to establish connection (' + cIrinos.GetConnectionStatusText + ')';
      AddHistoryEntry(s, true);
    end
    else begin
      AddHistoryEntry('Connection established successfully.', false);

      // Set absolute date/time. This is not required, but if the Irinos system
      // knows the current time, diagnosis memory entries will show the
      // absolute date/time.
      anStr := '#1;' + FormatDateTime('yyyy;m;d;h;m;s;z', Now) + '#';
      cIrinos.WriteDownCommandS(opcSAbstT, anStr);

      // ----> It is recommended to read the system string from the Irinos system
      //       here and to compare it with a reference string.
      //       Since this is a demo program for various systems,
      //       we cannot use a reference string here.
      anStr := '#1#';
      cIrinos.WriteDownCommandS(opcRSS, anStr);
      // if anStr <> ReferenceString then begin
      //    Do some error handling, e.g. show a message box
      // end;
      AddHistoryEntry('System string: ' + anStr, false);
      edSysString.Text := anStr;
      lbSystemString.Visible := true;
      edSysString.Visible := true;

      // Start reading static measurement values
      cIrinos.SetupStaticChannel(opcRS, 1, MainForm.Handle);

      // Start exchanging Bit I/Os
      cIrinos.SetupStaticChannel(opcBIO, (128 div 8), MainForm.Handle);

      // Start reading hardware status
      cIrinos.SetupStaticChannel(opcRHS, 1, MainForm.Handle);
    end;
  end
  else begin
    if cIrinos.StopAndClose then begin
      AddHistoryEntry('Connection closed', false);
    end;
  end;

end;


//*****************************************************************************
// FUNCTION: TMainForm.MessageReadStatic
//-----------------------------------------------------------------------------
// Message WM_MESSAGE_MSC_READSTATIC has been received. Proceed it.
//*****************************************************************************/
procedure TMainForm.MessageReadStatic( var Msg : TMessage );
begin
	if cIrinos.eConnectStat = cscCommStarted then begin

    if Msg.WParam = 10 then begin
      // Set flag "new measurement values available".
      // This flag will be checked and reset in a timer event routine
      // (tmrUpdateStaticTimer). If it is set, GUI data is update.
      // This is thread-safe. Do not update GUI data here.
      bNewStaticMeasurementValues := true;

      Inc(ulNStaticMeasurementValueUpdates);

      bTimedOut := false;
    end
    else if ( Msg.WParam = 20 ) then begin
      bTimedOut := true;
    end;

	end;
end;


//*****************************************************************************
// FUNCTION: TMainForm.MessageBIO
//-----------------------------------------------------------------------------
// Message WM_MESSAGE_MSC_HW_STATUS has been received. Proceed it.
//*****************************************************************************/
procedure TMainForm.MessageBIO(var Msg: TMessage);
begin
	if cIrinos.eConnectStat = cscCommStarted then begin

    if Msg.WParam = 10 then begin
      // Set flag "new measurement values available".
      // This flag will be checked and reset in a timer event routine
      // (tmrUpdateStaticTimer). If it is set, GUI data is update.
      // This is thread-safe. Do not update GUI data here.
      bNewBitIO := true;

      Inc(ulNBitIOUpdates);
    end;
	end;
end;


//*****************************************************************************
// FUNCTION: TMainForm.MessageReadHardStat
//-----------------------------------------------------------------------------
// Message WM_MESSAGE_MSC_HW_STATUS has been received. Proceed it.
//*****************************************************************************/
procedure TMainForm.MessageReadHardStat( var Msg : TMessage );
begin
	if cIrinos.eConnectStat = cscCommStarted then begin

    if Msg.WParam = 10 then begin
      // Set flag "new hardware status available".
      // This flag will be checked and reset in a timer event routine
      // (tmrUpdateStaticTimer). If it is set, GUI data is update.
      // This is thread-safe. Do not update GUI data here.
      bNewHardStatData := true;

      Inc(ulNHardStatUpdates);
    end;
	end;
end;



//*****************************************************************************
// FUNCTION: TMainForm.tmrUpdateStaticTimer
//-----------------------------------------------------------------------------
// Update GUI, if new data has been received via the MscDll.
//*****************************************************************************/
procedure TMainForm.tmrUpdateStaticTimer(Sender: TObject);
var
  ulI, ulByteNrIn, ulByteNrOut, ulBitNr : Cardinal;
  ucBitMask : byte;
begin
  // Update connection state in status bar
  if bTimedOut then begin
    statusBarMain.SimpleText := '----- Timeout';
  end
  else begin
    statusBarMain.SimpleText := cIrinos.GetConnectionStatusText;
  end;


  // Update text of button "Connect" according to connection state
  if cIrinos.eConnectStat = cscCommStarted then begin
    btnConnect.Caption := 'Disconnect';
  end
  else begin
    btnConnect.Caption := 'Connect';
  end;


  // ## Static measurement values
  if bNewStaticMeasurementValues then begin
    bNewStaticMeasurementValues := false;

    // Get static measurement values from DLL
    cIrinos.ReadStaticMeasurementValues;

    // Update GUI data
    for ulI := 1 to cNStaticChannels do begin
      albStatic[ulI].Caption := IntToStr(cIrinos.staticMeasVals[ulI]);
    end;
    lbStaticUpdates.Caption := IntToStr(ulNStaticMeasurementValueUpdates) +
                               ' measurement value updates';
  end;


  // ## Bit I/O
  if bNewBitIO then begin
    bNewBitIO := false;

    // Read digital inputs from DLL
    cIrinos.ReadDigitalInputs;

    // Update GUI data and dll output data
    for ulI := 1 to cNBitIOs do begin
      ulByteNrIn := (cNBitIOs div 8) + ((ulI-1) div 8) + 1;
      ulByteNrOut := ((ulI-1) div 8) + 1;
      ulBitNr := (ulI-1) mod 8;
      ucBitMask := 1 Shl ulBitNr;

      if (cIrinos.digInputBytes[ulByteNrIn] And ucBitMask) = ucBitMask then begin
        acbDigInputs[ulI].Checked := true;
      end
      else begin
        acbDigInputs[ulI].Checked := false;
      end;

      if acbDigOutputs[ulI].Checked then begin
        cIrinos.digOutputBytes[ulByteNrOut] := cIrinos.digOutputBytes[ulByteNrOut] Or ucBitMask;
      end
      else begin
        cIrinos.digOutputBytes[ulByteNrOut] := cIrinos.digOutputBytes[ulByteNrOut] And (Not ucBitMask);
      end;
    end;

    // Tell the DLL that digital output data has been updated
    cIrinos.PostDigitalOutputs;

    lbBitIOUpdates.Caption := IntToStr(ulNBitIOUpdates) +
                              ' Bit I/O updates';
  end;

  // ## Hardware status
  if bNewHardStatData then begin
    bNewHardStatData := false;

    // Get hardware status from DLL
    cIrinos.ReadHardwareStatus;

    // Update GUI data
    // If the hardware status shows a fault, color it red
    for ulI := 1 to cNStaticChannels do begin
      albHardStat[ulI].Caption := Format('0x%.*X', [2, cIrinos.hardStat[ulI]]);
      if cIrinos.hardStat[ulI] = 0 then begin
        albHardStat[ulI].Font.Color := clWindowText;
      end
      else begin
        albHardStat[ulI].Font.Color := clRed;
      end;
    end;
    lbHardStatUpdates.Caption := IntToStr(ulNHardStatUpdates) +
                                 ' hardware status updates';
  end;
end;



//*****************************************************************************
// FUNCTION: TMainForm.tmrUpdateDynMeasTimer
//-----------------------------------------------------------------------------
// Update the following GUI information:
// - Amount of dynamic measurement data received.
// - Dynamic measurement status
//*****************************************************************************/
procedure TMainForm.tmrUpdateDynMeasTimer(Sender: TObject);
var
  pul : PCardinal;
  ulPosition : Cardinal;
  ulValPerChannel, ulValTotal, ulBytesTotal : Cardinal;
begin
  ulPosition := 0;

  // Update the number of values/bytes received for the dynamic measurement
  if cIrinos.GetDynBytesReceived(opcRDM1, ulPosition) then begin
    ulValPerChannel := ulPosition div 4;
    ulValTotal := ulValPerChannel * ulDynChannels;
    ulBytesTotal := ulPosition * ulDynChannels;
    lbDynValuesPerChannelReceived.Caption := IntToStr(ulValPerChannel) + ' values per channel received';
    lbDynBytesPerChannelReceived.Caption := IntToStr(ulPosition) + ' bytes per channel received';
    lbDynValuesReceived.Caption := IntToStr(ulValTotal) + ' values received';
    lbDynBytesReceived.Caption := IntToStr(ulBytesTotal) + ' bytes received';
  end;

  // Update dynamic measurement status
  if cIrinos.WriteDownCommandCMD(opcRSW, 0) then begin
    pul := @cIrinos.rcvBufferCMD;
    cbDynTrg1Active.Checked := ((pul^ And $00000001) = $00000001);
    cbDynTrg1WasActive.Checked := ((pul^ And $00000002) = $00000002);
    cbDynTrg1EventOccured.Checked := ((pul^ And $00000004) = $00000004);
    cbDyn1Active.Checked := ((pul^ And $00000010) = $00000010);
    cbDyn1WasActive.Checked := ((pul^ And $00000020) = $00000020);
    cbDyn1ValueStored.Checked := ((pul^ And $00000040) = $00000040);
    cbDyn1ReadActive.Checked := ((pul^ And $00000080) = $00000080);
    cbDyn1BufferFull.Checked := ((pul^ And $00000100) = $00000100);
  end;

  btnDynTimeStart.Enabled := (cIrinos.eConnectStat = cscCommStarted) And
                             (bTimedOut = false);
  btnDynPosStart.Enabled := btnDynTimeStart.Enabled;
  btnDyn1Stop.Enabled := (cbDyn1Active.Checked And
                          (cIrinos.eConnectStat = cscCommStarted) And
                          (bTimedOut = false));
  btnDyn1ValuesStore.Enabled := (ulPosition > 0);
end;


//*****************************************************************************
// FUNCTION: TMainForm.btnDynTimeStartClick
//-----------------------------------------------------------------------------
// Start a time-based dynamic measurement with channel list 1, Trigger 1 and
// dynamic measurement 1.
// The following steps are performed:
// 1. Create a channel list of the used channels and send it to the Irinos
//    system. (Use only the required channels to reduce memory and
//    communication bandwidth consumption.)
// 2. Make sure trigger 1 is disabled.
// 3. Define / configure trigger 1 for time-based measurement.
// 4. Define the dynamic measurement.
// 5. Assign a buffer for the measurement values. The DLL will write received
//    values directly into this buffer.
// 6. Activate Trigger 1 to start the measurement.
// NOTE: Time- and position-based measurement differ only in
//       trigger configuration (step 3).
//*****************************************************************************/
procedure TMainForm.btnDynTimeStartClick(Sender: TObject);
var
  ansiStr : AnsiString;
  ulI : Cardinal;
  bStatus : Boolean;
begin
  bStatus := true;
  ulDynChannels := 0;
  tmrUpdateDynMeas.Enabled := false;

  // ###
  // Create channel list for dynamic measurement and send it to the Irinos system.
  // In this example channel list 1 is used. Any of the other channel lists (2..10)
	// could also be used.
  ansiStr := '#1';
  for ulI := 1 to cNDynChannels do begin
    abCurrentDynChUsed[ulI] := acbDynChUsed[ulI].Checked;
    if abCurrentDynChUsed[ulI] then begin
      ansiStr := ansiStr + ';T' + IntToStr(ulI);
      Inc(ulDynChannels);
    end;
  end;
  ansiStr := ansiStr + '#';

  if ulDynChannels = 0 then begin
    AddHistoryEntry('Dynamic measurement: no measurement channel selected', true);
    bStatus := false;
  end
  else begin
    // Send channel list to Irinos system and check response string.
    cIrinos.WriteDownCommandS(opcWCL, ansiStr);
    if ansiStr = '#0#' then begin
      AddHistoryEntry('Dynamic measurement: channel list 1 written successfully.', false);
    end
    else begin
      bStatus := false;
      AddHistoryEntry('Dynamic measurement: failed to write channel list 1 (' + ansiStr + ').', true);
    end;
  end;

  // ###
  // Inactivate Trigger 1 (in case it is already running)
  if bStatus then begin
    ansiStr := '#1#';
    cIrinos.WriteDownCommandS(opcIT, ansiStr);
    if ansiStr <> '#0#' then begin
      bStatus := false;
      AddHistoryEntry('Dynamic measurement: failed to inactivate trigger 1 (' + ansiStr + ').', true);
    end;
  end;

  // ###
  // Define / configure trigger.
  // In this example trigger 1 is used. Alternatively trigger 2 could be used as well.
  if bStatus then begin
    if rbDynTime100us.Checked then begin
      ansiStr := '#1;T;*;1;0.1;*;*#';
    end
    else if rbDynTime1ms.Checked then begin
      ansiStr := '#1;T;*;1;1;*;*#';
    end
    else begin
      ansiStr := '#1;T;*;1;10;*;*#';
    end;
    cIrinos.WriteDownCommandS(opcDT, ansiStr);

    if ansiStr = '#0#' then begin
      AddHistoryEntry('Dynamic measurement: Trigger 1 defined successfully', false);
    end
    else begin
      bStatus := false;
      AddHistoryEntry('Dynamic measurement: Failed to define Trigger 1 (' + ansiStr + ').', true);
    end;
  end;

  // ###
  // Define dynamic measurement
  // In this example dynamic measurement 1 is used. Alternatively dynamic measurement 2 could be used.
  // They can further be used/active in parallel.
  if bStatus then begin
    ansiStr := '#1;1;1;' + IntToStr(seDynTimeNValues.Value) + '#';
    cIrinos.WriteDownCommandS(opcDDM1, ansiStr);

    if ansiStr = '#0#' then begin
      AddHistoryEntry('Dynamic measurement 1 defined successfully.', false);
    end
    else begin
      bStatus := false;
      AddHistoryEntry('Dynamic measurement 1: Failed to define dynamic measurement (' + ansiStr + ').', true);
    end;
  end;

  // ###
  // Create buffer for measurement values
  if bStatus then begin
    bStatus := cIrinos.SetupDynamicChannel(opcRDM1, seDynTimeNValues.Value, ulDynChannels);
    if bStatus = false then begin
      AddHistoryEntry('Dynamic measurement: Failed to create receive buffer', true);
    end;
  end;

  // ###
  // Activate trigger 1. This will start the dynamic measurement.
  if bStatus then begin
    ansiStr := '#1#';
    cIrinos.WriteDownCommandS(opcAT, ansiStr);
    if ansiStr = '#0#' then begin
      AddHistoryEntry('Dynamic measurement: Trigger 1 activated successfully', false);
    end
    else begin
      bStatus := false;
      AddHistoryEntry('Dynamic measurement: Failed to activate trigger 1 (' + ansiStr + ').', true);
    end;
  end;

  // ###
  // Start timer for updating status and number of values received.
  tmrUpdateDynMeas.Enabled := true;
end;


//*****************************************************************************
// FUNCTION: TMainForm.btnDynPosStartClick
//-----------------------------------------------------------------------------
// Start a position-based dynamic measurement with channel list 1, Trigger 1 and
// dynamic measurement 1.
// The following steps are performed:
// 1. Create a channel list of the used channels and send it to the Irinos
//    system. (Use only the required channels to reduce memory and
//    communication bandwidth consumption.)
// 2. Make sure trigger 1 is disabled.
// 3. Define / configure trigger 1 for position-based measurement.
// 4. Define the dynamic measurement.
// 5. Assign a buffer for the measurement values. The DLL will write received
//    values directly into this buffer.
// 6. Activate Trigger 1 to start the measurement.
// NOTE: Time- and position-based measurement differ only in
//       trigger configuration (step 3).
//*****************************************************************************/
procedure TMainForm.btnDynPosStartClick(Sender: TObject);
var
  ansiStr : AnsiString;
  ulI : Cardinal;
  bStatus : Boolean;
begin
  bStatus := true;
  ulDynChannels := 0;
  tmrUpdateDynMeas.Enabled := false;

  if bDyn1DivisorValid = false then begin
    bStatus := false;
    AddHistoryEntry('Dynamic measurement: Divisor is invalid. Cannot start.', true);
  end
  else if bDyn1StartValid = false then begin
    bStatus := false;
    AddHistoryEntry('Dynamic measurement: Start value is invalid. Cannot start.', true);
  end
  else if bDyn1DistanceValid = false then begin
    bStatus := false;
    AddHistoryEntry('Dynamic measurement: Distance value is invalid. Cannot start.', true);
  end
  else if bDyn1StopValid = false then begin
    bStatus := false;
    AddHistoryEntry('Dynamic measurement: Stop value is invalid. Cannot start.', true);
  end;

  // ###
  // Create channel list for dynamic measurement and send it to the Irinos system.
  // In this example channel list 1 is used. Any of the other channel lists (2..10)
	// could also be used.
  if bStatus then begin
    ansiStr := '#1';
    for ulI := 1 to cNDynChannels do begin
      abCurrentDynChUsed[ulI] := acbDynChUsed[ulI].Checked;
      if abCurrentDynChUsed[ulI] then begin
        ansiStr := ansiStr + ';T' + IntToStr(ulI);
        Inc(ulDynChannels);
      end;
    end;
    ansiStr := ansiStr + '#';

    if ulDynChannels = 0 then begin
      AddHistoryEntry('Dynamic measurement: no measurement channel selected', true);
      bStatus := false;
    end
    else begin
      // Send channel list to Irinos system and check response string.
      cIrinos.WriteDownCommandS(opcWCL, ansiStr);
      if ansiStr = '#0#' then begin
        AddHistoryEntry('Dynamic measurement: channel list 1 written successfully.', false);
      end
      else begin
        bStatus := false;
        AddHistoryEntry('Dynamic measurement: failed to write channel list 1 (' + ansiStr + ').', true);
      end;
    end;
  end;

  // ###
  // Inactivate Trigger 1 (in case it is already running)
  if bStatus then begin
    ansiStr := '#1#';
    cIrinos.WriteDownCommandS(opcIT, ansiStr);
    if ansiStr <> '#0#' then begin
      bStatus := false;
      AddHistoryEntry('Dynamic measurement: failed to inactivate trigger 1 (' + ansiStr + ').', true);
    end;
  end;

  // ###
  // Define / configure trigger.
  // In this example trigger 1 is used. Alternatively trigger 2 could be used as well.
  if bStatus then begin
    ansiStr := '#1;P;' + cbDyn1PosTrgChannel.Text + ';' +
               editDyn1Divisor.Text + ';' +
               editDyn1PosTriggerDistance.Text + ';' +
               editDyn1PosTriggerStart.Text + ';' +
               editDyn1PosTriggerStop.Text + '#';
    cIrinos.WriteDownCommandS(opcDT, ansiStr);

    if ansiStr = '#0#' then begin
      AddHistoryEntry('Dynamic measurement: Trigger 1 defined successfully', false);
    end
    else begin
      bStatus := false;
      AddHistoryEntry('Dynamic measurement: Failed to define Trigger 1 (' + ansiStr + ').', true);
    end;
  end;

  // ###
  // Define dynamic measurement
  // In this example dynamic measurement 1 is used. Alternatively dynamic measurement 2 could be used.
  // They can further be used/active in parallel.
  if bStatus then begin
    ansiStr := '#1;1;1;' + IntToStr(seDynPosNValues.Value) + '#';
    cIrinos.WriteDownCommandS(opcDDM1, ansiStr);

    if ansiStr = '#0#' then begin
      AddHistoryEntry('Dynamic measurement 1 defined successfully.', false);
    end
    else begin
      bStatus := false;
      AddHistoryEntry('Dynamic measurement 1: Failed to define dynamic measurement (' + ansiStr + ').', true);
    end;
  end;

  // ###
  // Create buffer for measurement values
  if bStatus then begin
    bStatus := cIrinos.SetupDynamicChannel(opcRDM1, seDynPosNValues.Value, ulDynChannels);
    if bStatus = false then begin
      AddHistoryEntry('Dynamic measurement: Failed to create receive buffer', true);
    end;
  end;

  // ###
  // Activate trigger 1. This will start the dynamic measurement.
  if bStatus then begin
    ansiStr := '#1#';
    cIrinos.WriteDownCommandS(opcAT, ansiStr);
    if ansiStr = '#0#' then begin
      AddHistoryEntry('Dynamic measurement: Trigger 1 activated successfully', false);
    end
    else begin
      bStatus := false;
      AddHistoryEntry('Dynamic measurement: Failed to activate trigger 1 (' + ansiStr + ').', true);
    end;
  end;

  // ###
  // Start timer for updating status and number of values received.
  tmrUpdateDynMeas.Enabled := true;
end;


//*****************************************************************************
// FUNCTION: TMainForm.editDyn1DivisorChange
//-----------------------------------------------------------------------------
// Used for position controlled dynamic measurement.
// Check if the divisor is a valid value.
//*****************************************************************************/
procedure TMainForm.editDyn1DivisorChange(Sender: TObject);
var
  flVal : Single;
begin
  if ConvertFloatString(editDyn1Divisor.Text, flVal) And (flVal <> 0.0) then begin
    editDyn1Divisor.Font.Color := clWindowText;
    bDyn1DivisorValid := true;
  end
  else begin
    editDyn1Divisor.Font.Color := clRed;
    bDyn1DivisorValid := false;
  end;
end;


//*****************************************************************************
// FUNCTION: TMainForm.editDyn1PosTriggerStartChange
//-----------------------------------------------------------------------------
// Used for position controlled dynamic measurement.
// Check if the start value is a valid value.
//*****************************************************************************/
procedure TMainForm.editDyn1PosTriggerStartChange(Sender: TObject);
var
  flVal : Single;
begin
  if ConvertFloatString(editDyn1PosTriggerStart.Text, flVal) then begin
    editDyn1PosTriggerStart.Font.Color := clWindowText;
    bDyn1StartValid := true;
  end
  else begin
    editDyn1PosTriggerStart.Font.Color := clRed;
    bDyn1StartValid := false;
  end;
end;


//*****************************************************************************
// FUNCTION: TMainForm.editDyn1PosTriggerDistanceChange
//-----------------------------------------------------------------------------
// Used for position controlled dynamic measurement.
// Check if the trigger distance is a valid value.
//*****************************************************************************/
procedure TMainForm.editDyn1PosTriggerDistanceChange(Sender: TObject);
var
  flVal : Single;
begin
  if ConvertFloatString(editDyn1PosTriggerDistance.Text, flVal) And (flVal <> 0.0) then begin
    editDyn1PosTriggerDistance.Font.Color := clWindowText;
    bDyn1DistanceValid := true;
  end
  else begin
    editDyn1PosTriggerDistance.Font.Color := clRed;
    bDyn1DistanceValid := false;
  end;
end;


//*****************************************************************************
// FUNCTION: TMainForm.editDyn1PosTriggerStopChange
//-----------------------------------------------------------------------------
// Used for position controlled dynamic measurement.
// Check if the trigger stop is a valid value.
//*****************************************************************************/
procedure TMainForm.editDyn1PosTriggerStopChange(Sender: TObject);
var
  flVal : Single;
begin
  if ConvertFloatString(editDyn1PosTriggerStop.Text, flVal) Or (editDyn1PosTriggerStop.Text = '*') then begin
    editDyn1PosTriggerStop.Font.Color := clWindowText;
    bDyn1StopValid := true;
  end
  else begin
    editDyn1PosTriggerStop.Font.Color := clRed;
    bDyn1StopValid := false;
  end;
end;


//*****************************************************************************
// FUNCTION: TMainForm.ConvertFloatString
//-----------------------------------------------------------------------------
// Try to convert a string to a float (single) value while allowing
// a . or a , as decimal separator.
//-----------------------------------------------------------------------------
// PARAMETERS:
// s: string which contains the float value
// flVal: converted float value
//-----------------------------------------------------------------------------
// Return: true if string was converted successfully
//*****************************************************************************/
function TMainForm.ConvertFloatString(s : string; var flVal : single) : Boolean;
begin
  s := s.Replace(',', FormatSettings.DecimalSeparator);
  s := s.Replace('.', FormatSettings.DecimalSeparator);

  Result := TryStrToFloat(s, flVal);
end;


//*****************************************************************************
// FUNCTION: TMainForm.btnDyn1ValuesStoreClick
//-----------------------------------------------------------------------------
// Read dynamic measurement values from the buffer and store them into a
// ";" separated *.csv-file.
// The *.csv-file can be opened with MS Excel.
//*****************************************************************************/
procedure TMainForm.btnDyn1ValuesStoreClick(Sender: TObject);
var
  csvData : TStringList;
  s : string;
  sFilePath : string;
  ulI, ulCtr, ulChan : Cardinal;
  ulPosition : Cardinal;
begin
  csvData := TStringList.Create;

  sFilePath := ExtractFilePath(Application.ExeName) + 'DynMeasValues.csv';

  // Create header line
  s := '';
  ulCtr := 0;
  for ulI := 1 to cNDynChannels do begin
    if abCurrentDynChUsed[ulI] then begin
      if ulCtr > 0 then begin
        s := s + ';';
      end;
      s := s + 'T' + IntToStr(ulI);
      Inc(ulCtr);
    end;
  end;
  csvData.Add(s);

  // Create lines with measurement values
  ulPosition := 0;
  cIrinos.GetDynBytesReceived(opcRDM1, ulPosition);
  ulPosition := ulPosition div 4;
  for ulI := 1 to ulPosition do begin
    s := '';
    for ulChan := 1 to ulDynChannels do begin
      if ulChan > 1 then begin
        s := s + ';';
      end;
      s := s + IntToStr(cIrinos.rcvBufferDyn[ulChan-1].measData[ulI-1]);
    end;
    csvData.Add(s);
  end;

  try
    csvData.SaveToFile(sFilePath);
    s := 'Dynamic measurement values successfully written to ' +
         sFilePath + '.';
    AddHistoryEntry(s, false);
  except
    s := 'Dynamic measurement: cannot write DynValues.csv (' +
         sFilePath + ').';
    AddHistoryEntry(s, true);
  end;

  ShowMessage(s);
end;


//*****************************************************************************
// FUNCTION: TMainForm.btnDyn1StopClick
//-----------------------------------------------------------------------------
// Stop an active dynamic measurement.
// In this example the dynamic measurement is stopped via opcDDM1.
// It can also be stopped by inactivating the trigger, which is used for
// the dynamic measurement.
// In case the same trigger is used for both dynamic measurements (which is
// seldomly used), this would stop both dynamic measurements concurrently.
//*****************************************************************************/
procedure TMainForm.btnDyn1StopClick(Sender: TObject);
var
   ansiStr : AnsiString;
begin
   ansiStr := '#1;1;0;*#';
   cIrinos.WriteDownCommandS(opcDDM1, ansiStr);
   if ansiStr <> '#0#' then begin
      AddHistoryEntry('Dynamic measurement 1: cannot stop' + ansiStr, true);
   end
   else begin
      AddHistoryEntry('Dynamic measurement 1 successfully stopped.', false);
   end;
end;



end.
