unit Sealtest;
{ ==================================================
  WinWCP - Patch clamp pipette seal test module
  (c) J. Dempster, University of Strathclyde 1996-97
  25/6/97 V1.6c ... Modified to support 3 test pulse settings
              3rd option has pulse amplitude=0
  27/6/97 V1.6d ... Seal test baseline now fixed when AutoScale is unchecked
  8/7/97 V1.6e ... Amplifier Gain factor now taken into account in scaling
  16/7/97 V1.6f ... Autoscale always runs first time Seal Test is used
  20/1/98 V1.8b ... Free run mode added, sweeps without test pulses
  30/5/98 V2.0c ... Test pulse width can now be set within range 0.1 ms - 1000ms
                    General re-write and tidy up of code
                    MemoryToDAC now started in single-sweep mode for Digidata 1200
                    and cyclic auto-repeat mode for all others. (Needed to
                    fix intermittent corruption of D/A waveform with Digidata

  ==================================================}

interface

uses WinTypes, WinProcs, Classes, Graphics, Forms, Controls, Buttons,
  StdCtrls, ExtCtrls, global, shared, SysUtils, LAbio, Spin, vds, maths ;

type
  TState = ( Idle, StartSweep, SweepInProgress, EndofSweep ) ;
  TSealTestFrm = class(TForm)
    pbDisplay: TPaintBox;
    ChannelsGrp: TGroupBox;
    cbCurrentChannel: TComboBox;
    Label1: TLabel;
    cbVoltageChannel: TComboBox;
    Label2: TLabel;
    bClose: TButton;
    VoltsGrp: TGroupBox;
    edVHold: TEdit;
    Label5: TLabel;
    edVPulse: TEdit;
    Label6: TLabel;
    CurrentGrp: TGroupBox;
    Label7: TLabel;
    Label8: TLabel;
    edIHold: TEdit;
    EdIPulse: TEdit;
    CellGrp: TGroupBox;
    Label9: TLabel;
    Label10: TLabel;
    EdResistance: TEdit;
    EdCapacity: TEdit;
    lbTMin: TLabel;
    lbTMax: TLabel;
    Timer1: TTimer;
    DisplayGrp: TGroupBox;
    ckAutoScale: TCheckBox;
    DACGrp: TGroupBox;
    edHoldingVoltage1: TEdit;
    rbUseHoldingVoltage1: TRadioButton;
    edHoldingVoltage2: TEdit;
    rbUseHoldingVoltage2: TRadioButton;
    DivideGrp: TGroupBox;
    edVDivide: TEdit;
    edPulseHeight1: TEdit;
    Label3: TLabel;
    Label11: TLabel;
    Label12: TLabel;
    edPulseheight2: TEdit;
    Label14: TLabel;
    edPulseWidth: TEdit;
    Label4: TLabel;
    EdScale: TEdit;
    SpScale: TSpinButton;
    rbUseHoldingVoltage3: TRadioButton;
    Label13: TLabel;
    EdHoldingVoltage3: TEdit;
    Shape1: TShape;
    ckFreeRun: TCheckBox;
    procedure Timer1Timer(Sender: TObject);
    procedure bCloseClick(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure EdVDivideKeyPress(Sender: TObject; var Key: Char);
    procedure edHoldingVoltage1KeyPress(Sender: TObject; var Key: Char);
    procedure FormDeactivate(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure edHoldingVoltage2KeyPress(Sender: TObject; var Key: Char);
    procedure rbUseHoldingVoltage1Click(Sender: TObject);
    procedure rbUseHoldingVoltage2Click(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure FormResize(Sender: TObject);
    procedure edPulseHeight1KeyPress(Sender: TObject; var Key: Char);
    procedure edPulseheight2KeyPress(Sender: TObject; var Key: Char);
    procedure edPulseWidthKeyPress(Sender: TObject; var Key: Char);
    procedure SpScaleUpClick(Sender: TObject);
    procedure SpScaleDownClick(Sender: TObject);
    procedure EdScaleKeyPress(Sender: TObject; var Key: Char);
    procedure rbUseHoldingVoltage3Click(Sender: TObject);
    procedure EdHoldingVoltage3KeyPress(Sender: TObject; var Key: Char);
    procedure ckAutoScaleClick(Sender: TObject);
    procedure ckFreeRunClick(Sender: TObject);
  private
    { Private declarations }

    procedure HeapBuffers( Operation : THeapBufferOp ) ;
    procedure CreateTestPulse ;
    procedure AnalyseTestPulse ;
    procedure UpdateAllParameters ;
  public
    { Public declarations }
    State : TState ;
    procedure StopADCandDAC ;
  end;

var
  SealTestFrm: TSealTestFrm;

implementation

{$R *.DFM}

uses Mdiform ;

const
     EmptyFlag = 32767 ;
     StimDACChannel = 0 ;
     SyncDACChannel = 1 ;
     NumDACChannels = 2 ;
     NumTestSamples = 512 ;
     MinDACInterval = 0.0002 ;
     MinPulseWidth = 0.0001 ;
     MaxPulseWidth = 1.0 ;
type
    TTestPulse = record
               Duration : single ;
               TStart : single ;
               TEnd : Single ;
               RecordLength : single ;
               RepeatPeriod : single ;
               end ;
var
  ADC : ^TIntArray ;
  ADCHnd : THandle ;
  DAC : ^TIntArray ;
  DACHnd : THandle ;
  x,dx : Single ;
  NextSample,EndOfBuf : LongInt ;
  yLast : Array[0..ChannelLimit] of LongInt ;
  xyPixOld : Array[0..ChannelLimit] of TPoint ;
  xyPix : TPoint ;
  RH : ^TRecHeader ;
  MinFormHeight : LongInt ; { Minimum height allowed for form }
  MinFormWidth : LongInt ;  { Minimum width allowed for form }
  ADCActive : Boolean ; { Indicates A/D sub-system active }
  DACActive : Boolean ; { Indicates D/A sub-system active }
  ChangeDisplayScaling : Boolean ;
  TestChannel : array[0..ChannelLimit] of TChannel ;
  TestPulse : TTestPulse ;
  PulseHeight, DACScale, SyncScale : single ;
  ADCdt : single ; { A/D sampling interval }
  DACdt : single ; { D/A update interval }
  MaxDACVolts : single ;
  TimerBusy : boolean ;
  Resistance : single ;
  Capacity : single ;
  BuffersAllocated : boolean ;{ Indicates if memory buffers have been allocated }
  ADCPointer : LongInt ;
  nDACValues : Integer ;

procedure TSealTestFrm.HeapBuffers( Operation : THeapBufferOp ) ;
{ -----------------------------------------------
  Allocate/deallocation dynamic buffers from heap
  -----------------------------------------------}
var
   Err : Word ;
begin
     case Operation of
          Allocate : begin
             if not BuffersAllocated then begin
                { Create and lock A/D buffer into RAM because it
                  will be updated by an interrupt service routine }
                ADCHnd := GlobalAlloc( GMEM_FIXED, Sizeof(TIntArray) ) ;
                GlobalFix( ADCHnd ) ;
                ADC := GlobalLock( ADCHnd ) ;
                { Create and lock D/A buffer }
                DACHnd := GlobalAlloc( GMEM_FIXED, Sizeof(TIntArray) ) ;
                GlobalFix( DACHnd ) ;
                DAC := GlobalLock( DACHnd ) ;
                { Create record header buffer }
                NEW(RH) ;
                BuffersAllocated := True ;
                end ;
             end ;
          Deallocate : begin
             if BuffersAllocated then begin
                GlobalUnlock( ADCHnd ) ;
                GlobalUnFix( ADCHnd ) ;
                GlobalFree( ADCHnd ) ;
                GlobalUnlock( DACHnd ) ;
                GlobalUnFix( DACHnd ) ;
                GlobalFree( DACHnd ) ;
                Dispose(RH) ;
                BuffersAllocated := False ;
                end ;
             end ;
          end ;
     end ;


procedure TSealTestFrm.FormCreate(Sender: TObject);
{ ----------------------------------- -
  Procedures when form is first created
  ------------------------------------}
begin
     { Disable "Seal test" option in "Record" menu }
     Main.SealTest.enabled := false ;
     { Enable "Seal Test" option in "Windows" menu }
     Main.ShowSealTest.Visible := True ;

     { Allocate dynamic buffers }
     BuffersAllocated := False ;
     HeapBuffers( Allocate ) ;

     { Select which kind of lab. interface hardware is to be used }
     SelectLaboratoryInterface( Settings.LaboratoryInterface ) ;

     MinFormHeight := Height ;
     MinFormWidth := Width ;
     Resistance := 0. ;
     Capacity := 0. ;

    end;


procedure TSealTestFrm.FormShow(Sender: TObject);
{ ----------------
  Initialise form
  ---------------}
var
   ch : Integer ;
   Supplier,Model : string ;
begin
     { Set tick in Windows menu }
     Main.ClearTicksInWindowsMenu ;
     Main.ShowSealTest.checked := True ;

     { Set current and voltage channel selection combo boxes }
     cbCurrentChannel.Items := ChannelNames ;
     if Settings.SealTest.CurrentChannel <= cbCurrentChannel.Items.Count-1 then
        cbCurrentChannel.ItemIndex := Settings.SealTest.CurrentChannel
     else begin
          cbCurrentChannel.ItemIndex := 0 ;
          Settings.SealTest.CurrentChannel := 0 ;
          end ;

     cbVoltageChannel.Items := ChannelNames ;
     if Settings.SealTest.VoltageChannel <= cbVoltageChannel.Items.Count-1 then
        cbVoltageChannel.ItemIndex := Settings.SealTest.VoltageChannel
     else begin
        cbVoltageChannel.ItemIndex := 0 ;
        Settings.SealTest.VoltageChannel := 0 ;
        end ;

     for ch := 0 to fH.NumChannels-1 do begin
         TestChannel[ch] := Channel[ch] ;
         TestChannel[ch].xMin := 0. ;
         TestChannel[ch].xMax := NumTestSamples ;
         TestChannel[ch].yMin := Settings.SealTest.yMin[ch] ;
         TestChannel[ch].yMax := Settings.SealTest.yMax[ch] ;
         TestChannel[ch].InUse := True ;
         end ;

     { Test pulse amplitude & width }
     edPulseHeight1.text := format('%6.1f mV',
                           [Settings.SealTest.PulseHeight1*VoltsTomV] );
     edPulseHeight2.text := format('%6.1f mV',
                           [Settings.SealTest.PulseHeight2*VoltsTomV] );
     edPulseWidth.text := format('%6.1f ms',
                           [Settings.SealTest.PulseWidth*SecsToMs]);

     { Update the holding potential of selected test pulse
       to the current holding potential }
     case Settings.SealTest.Use of
          2 : Settings.SealTest.HoldingVoltage2 := Settings.VCommand.HoldingVoltage ;
          3 : Settings.SealTest.HoldingVoltage3 := Settings.VCommand.HoldingVoltage ;
          else Settings.SealTest.HoldingVoltage1 := Settings.VCommand.HoldingVoltage ;
          end ;

     { Update holding potential text boxes }
     edHoldingVoltage1.text := format( '%6.1f mV',
                              [Settings.SealTest.HoldingVoltage1*VoltsTomV] ) ;
     edHoldingVoltage2.text := format( '%6.1f mV',
                              [Settings.SealTest.HoldingVoltage2*VoltsTomV] ) ;
     edHoldingVoltage3.text := format( '%6.1f mV',
                              [Settings.SealTest.HoldingVoltage3*VoltsTomV] ) ;

     { Update command voltage divide factor box }
     edVDivide.text := format(' %6.1f ',[Settings.VCommand.DivideFactor]) ;

     { Update all parameters }
     UpdateAllParameters ;

     lbTMin.Top := pbDisplay.top + pbDisplay.Height + 2 ;
     lbTMin.Left := pbDisplay.left ;
     lbTMax.Top := lbTMin.Top ;
     lbTMax.Left := pbDisplay.left + pbDisplay.Width - lbTMax.Width ;

     { Select test pulse to use }
     case Settings.SealTest.Use of
          2 : rbUseHoldingVoltage2.checked := True ; {Executes click procedure}
          3 : rbUseHoldingVoltage3.checked := True ; {Executes click procedure}
          else rbUseHoldingVoltage1.checked := True ; {Executes click procedure}
          end ;

     DACActive := False ;
     ADCActive := False ;

     ckAutoScale.checked := Settings.SealTest.AutoScale ;
     edScale.text := format( ' X%d', [Settings.SealTest.DisplayScale] ) ;

     ckFreeRun.Checked := Settings.SealTest.FreeRun ;

     ChangeDisplayScaling := False ;

     { Call GetLabInterfaceInfo to force initialisation of lab. interface
       (this can take 3-4 secs for National Instruments interfaces)
       Note timer turned off to prevent premature execution of A/D routines }
     Timer1.Enabled := False ;
     GetLabInterfaceInfo( Supplier, Model ) ;

     Timer1.Enabled := True ;
     TimerBusy := False ;
      State := StartSweep ;
     end ;


procedure TSealTestFrm.Timer1Timer(Sender: TObject);
{ ---------------------
  Timed Event scheduler
  ---------------------}

var
   i,j,ch,iSample,ChOffset,NextCh : Integer ;
   NumSamplesIn : LongInt ;
   xPix,yPix : Integer ;
   y,MinADCdt,MaxADCdt : single ;
   Offsets : Array[0..ChannelLimit] of LongInt ;
   OK : Boolean ;
   Supplier,Model : string ;
begin
     if not TimerBusy then begin
          TimerBusy := True ;
          case State of


               StartSweep : Begin

                   { Start recording sweep(s) }

                   if cbCurrentChannel.ItemIndex >= 0 then
                      Settings.SealTest.CurrentChannel := cbCurrentChannel.ItemIndex ;
                   if cbVoltageChannel.ItemIndex >= 0 then
                      Settings.SealTest.VoltageChannel := cbVoltageChannel.ItemIndex ;

                   if Settings.VCommand.DivideFactor <= 0. then
                                           Settings.VCommand.DivideFactor := 1. ;

                   CreateTestPulse ;

                   { A/D sampling interval }
                   ADCdt := TestPulse.RecordLength / NumTestSamples ;
                   GetSamplingIntervalRange( MinADCdt, MaxADCdt ) ;
                   ADCdt := MaxFlt( [ADCdt,MinADCdt*RawfH.NumChannels] ) ;
                   dx := 1. ;
                   x := -dx ;
                   NextSample := 1 ;
                   NextCh := RawfH.NumChannels - 1;
                   EndofBuf := (RawFH.NumChannels*NumTestSamples) - 1;
                   { Start A/D converter sampling (Note. No waiting for
                     Ext. Trigger when in Free run mode 20/1/98 V1.8b )}
                   if ADCActive then StopADC ;
                   ADCToMemory( ADC^, RawfH.NumChannels,
                                NumTestSamples,ADCdt,RawfH.ADCVoltageRange,
                                not ckFreeRun.Checked,False) ;
                   ADCPointer := 0 ;
                   ADCActive := True ;

                   { Start D/A conversion }
                   if Settings.LaboratoryInterface = Integer(DD) then
                      { Digidata 1200 must have single sweep mode to
                        avoid intermittent glitches when in
                        cyclic autorepeat mode }
                      MemoryToDAC( DAC^,NumDACChannels,nDACValues,DACdt,1,DACActive)
                   else
                      { All others in auto-repeat mode }
                      MemoryToDAC( DAC^,NumDACChannels,nDACValues,DACdt,0,DACActive) ;

                   { Erase display & Draw horizontal zero level cursor(s) }
                   EraseDisplay( pbDisplay ) ;
                   rH^.dt := ADCdt ;

                   { Get Channel offsets }
                   GetChannelOffsets( Offsets, RawFH.NumChannels ) ;
                   for ch := 0 to RawFH.NumChannels-1 do
                       Channel[ch].ChannelOffset := Offsets[ch] ;

                   InitializeDisplay(TestChannel,RawfH.NumChannels,RH^,
                                     lbTMin,lbTMax,pbDisplay) ;

                   for ch := 0 to RawfH.NumChannels-1 do if Channel[ch].InUse then
                      DrawHorizontalCursor( pbDisplay, TestChannel[ch],
                                            Channel[ch].ADCZero ) ;
                   State := SweepInProgress ;
                   End ;

               SweepInProgress : Begin

                    { Get A/D samples from interface
                      (only does something for interfaces which
                       do not have DMA }
                    GetADCSamples( @ADC^[ADCPointer], NumSamplesIn ) ;
                    ADCPointer := ADCPointer + NumSamplesIn ;

                    { Display A/D samples on screen and save to file as they are acquired }

                    While (ADC^[NextSample] <> EmptyFlag)
                         and (State = SweepInProgress) do Begin

                         { Calculate pixel location on screen for next data point }

                         iSample := NextSample - 1 ;
                         ConvertADCValue(ADC^[iSample],iSample,
                                         NextCh,RawFH.NumChannels,x,dx) ;
                         y := ADC^[iSample] ;
                         yLast[NextCh] := ADC^[iSample] ;

                         if y < TestChannel[NextCh].yMin then
                            y := TestChannel[NextCh].yMin ;
                         if y > TestChannel[NextCh].yMax then
                            y := TestChannel[NextCh].yMax ;

                         xPix := Trunc(TestChannel[NextCh].xScale
                                 * (x - TestChannel[NextCh].xMin))
                                 + TestChannel[NextCh].Left  ;
                         yPix := TestChannel[NextCh].Bottom -
                                 Trunc(TestChannel[NextCh].yScale
                                 * (y - TestChannel[NextCh].yMin));
                         xyPix := Point(xPix,yPix) ;

                         if x = 0. then xyPixOld[NextCh] := xyPix ;

                         pbdisplay.canvas.polyline( [xyPixOld[NextCh],xyPix]);
                         xyPixOld[NextCh] := xyPix ;

                         if NextSample > EndOfBuf then State := EndOfSweep ;

                         { Increment sample counter & channel counter }
                         Inc(NextSample) ;
                         End ;
                    End ;

               EndOfSweep : Begin

                   { Procedure to be done when recording sweep completes }

                   { Analyse and display test pulse results }
                   AnalyseTestPulse ;

                  { Update output ports if a request has been made }
                  if Settings.UpdateOutputs then begin
                     { Stop D/A output }
                     StopDAC ;
                     DACActive := False ;
                     WriteOutputPorts( Settings.VCommand.HoldingVoltage *
                                       Settings.VCommand.DivideFactor,
                                       Settings.DigitalPort.Value ) ;
                     Settings.UpdateOutputs := False ;
                     { Request a new pulse to be generated }
                     end ;

                  { Do another sweep }
                  State := StartSweep ;
                  end ;

               Idle : begin
                    { Procedures when recording is in idle mode }
                    end ;
               end ;
          TimerBusy := False ;
          end ;
     end ;


procedure TSealTestFrm.CreateTestPulse ;
{ ----------------------------
  Create test pulse waveform
  ----------------------------}
var
   i,j,iStart,iEnd,iOffLevel,iOnLevel : Integer ;
   TrgOnVolts, TrgOffVolts : single ;

begin

     if DACActive then StopDAC ;
     DACActive := False ;

     { Get max. positive D/A output voltage }
     MaxDACVolts := GetMaxDACVolts ;

     { D/A channel voltage -> bits scaling factors }
     DACScale := (MaxDACValue*Settings.VCommand.DivideFactor)/MaxDACVolts ;
     SyncScale := MaxDACValue / MaxDACVolts ;

     { Test pulse duration and recording sweep length }
     TestPulse.Duration := Settings.SealTest.PulseWidth ;
     TestPulse.TStart := MaxFlt([TestPulse.Duration/6.0,0.005]) ;
     TestPulse.TEnd := TestPulse.TStart + TestPulse.Duration ;
     TestPulse.RecordLength := TestPulse.TEnd + TestPulse.TStart ;

     { Test pulse repeat period }
     TestPulse.RepeatPeriod := MaxFlt([TestPulse.RecordLength,0.1]) ;

     { D/A update interval }

     DACdt := MaxFlt( [TestPulse.RepeatPeriod/NumTestSamples,MinDACInterval] ) ;

     { No. of D/A values in buffer }
     nDacValues := Trunc( TestPulse.RepeatPeriod / DACdt ) ;

     { Create test pulse waveform }
     iStart := MaxInt([Trunc(TestPulse.TStart/DACdt),1 ]) ;
     iEnd := MaxInt([Trunc(TestPulse.TEnd/DACdt),1 ]) ;
     iOffLevel := Trunc(Settings.VCommand.HoldingVoltage*DACScale ) ;
     iOnLevel :=  Trunc( (Settings.SealTest.PulseHeight +
                          Settings.VCommand.HoldingVoltage)*DACScale ) ;

     { Get ON/OFF trigger levels for sync. pulse channel }
     GetSyncPulseLevels( TrgOnVolts, TrgOffVolts ) ;
     j := 0 ;
     for i := 0 to nDacValues-1 do begin
         { VCommand channel }
         if (i >= iStart) and (i<=iEnd) then DAC^[j] := iOnLevel
                                        else DAC^[j] := iOffLevel ;
         Inc(j) ;
         { Sync. channel }
         if i = 1 then DAC^[j] := Trunc( TrgOnVolts*SyncScale )
                  else DAC^[j] := Trunc( TrgOffVolts*SyncScale ) ;
         Inc(j) ;
         end ;

     { Convert D/A waveform buffer to DAC codes }
     ConvertToDACCodes(DAC^,NumDACChannels,nDACValues) ;

     end ;


procedure TSealTestFrm.AnalyseTestPulse ;
{ --------------------------------------------------------------------
  Calculate and display seal test pulse amplitudes and seal properties
  --------------------------------------------------------------------}
var
   i,j,ch,iStart,iEnd : Integer ;
   iAvgStart,iAvgEnd,nAvg,EndofVHold,NumSamplesIn : Integer ;
   NewDisplayScale,NewDisplayRange,HalfRange,Mid : Integer ;
   ChIm,ChVm,ChOffset,yADC : Integer ;
   HoldLevel,VThreshold,iOffLevel,iOnLevel : Integer ;
   Min,Max : Array[0..ChannelLimit] of Integer ;
   VHold,VPulse,IHold,IPulse,Avg,Sum : single ;
   Voltage,Current,VScale,IScale : single ;
   Charge,SteadyStateLevel : single ;
   Offsets : Array[0..ChannelLimit] of LongInt ;

begin
     ChIm := Settings.SealTest.CurrentChannel ;
     ChVm := Settings.SealTest.VoltageChannel ;

     for ch := 0 to RawFH.NumChannels-1 do
         TestChannel[ch].ADCScale := RawFH.ADCVoltageRange /
                                  ( TestChannel[ch].ADCCalibrationFactor *
                          TestChannel[ch].ADCAmplifierGain * (MaxADCValue+1) ) ;

     { Find Min./Max. limits of voltage and current }
     for ch := 0 to RawFH.NumChannels-1 do begin
         ChOffset := Channel[ch].ChannelOffset ;
         Max[ch] := -MaxADCValue ;
         Min[ch] := MaxADCValue ;
         for i := 0 to NumTestSamples-1 do begin
             j := i*RawfH.NumChannels + ChOffset ;
             yADC := ADC^[j] ;
             if Max[ch] <= yADC then Max[ch] := yADC ;
             if Min[ch] >= yADC then Min[ch] := yADC ;
             end ;
         end ;

     { Calculate holding voltage }

     Sum := 0. ;
     EndofVHold := MaxInt( [Trunc(TestPulse.TStart/(2.*ADCdt)),1] ) ;
     ChOffset := Channel[chVm].ChannelOffset ;
     for i := 0 to EndofVHold do begin
         j := i*RawfH.NumChannels + ChOffset ;
         Sum := Sum + ADC^[j] ;
         end ;
     Avg := Sum/(EndofVHold+1) ;
     HoldLevel := Trunc(Avg) ;
     VHold := (Avg - TestChannel[ChVm].ADCZero)*TestChannel[ChVm].ADCScale ;

     { Calculate holding current }
     Sum := 0. ;
     ChOffset := Channel[chIm].ChannelOffset ;
     for i := 0 to EndofVHold do begin
         j := i*RawfH.NumChannels + ChOffset ;
         Sum := Sum + ADC^[j] ;
         end ;
     Avg := Sum/(EndofVHold+1) ;
     IHold := (Avg - TestChannel[ChIm].ADCZero)*TestChannel[ChIm].ADCScale ;

     { Find start / end of test pulse }
     VThreshold := Abs( Max[ChVm] - Min[ChVm] ) div 2 ;
     iStart := 0 ;
     iEnd := 0 ;
     ChOffset := Channel[chVm].ChannelOffset ;
     for i := 0 to NumTestSamples-1 do begin
         j := i*RawfH.NumChannels + ChOffset ;
         yADC := Abs(ADC^[j] - HoldLevel) ;
         if (yADC >= VThreshold) and (iStart = 0) then iStart := i ;
         if (iStart <> 0) and (iEnd=0) and (yADC < VThreshold) then iEnd := i ;
         end ;

     { Calculate amplitude of voltage pulse (from last half of pulse) }
     nAvg := MaxInt( [(iEnd - iStart + 1) div 2, 1] ) ;
     iAvgStart := MaxInt( [iEnd - nAvg + 1,iStart] ) ;
     iAvgEnd := MaxInt( [iEnd - 2,iAvgStart] ) ;
     Sum := 0. ;
     ChOffset := Channel[chVm].ChannelOffset ;
     for i := iAvgStart to iAvgEnd do begin
         j := i*RawfH.NumChannels + ChOffset ;
         Sum := Sum + ADC^[j] ;
         end ;
     Avg := Sum / (iAvgEnd-iAvgStart+1) ;

     VPulse := (Avg - TestChannel[ChVm].ADCZero)*TestChannel[ChVm].ADCScale
                - VHold ;

     { Calculate steady-state amplitude of current pulse (from last half of pulse) }
     Sum := 0. ;
     ChOffset := Channel[chIm].ChannelOffset ;
     for i := iAvgStart to iAvgEnd do begin
         j := i*RawfH.NumChannels + ChOffset ;
         Sum := Sum + ADC^[j] ;
         end ;
     Avg := Sum / (iAvgEnd-iAvgStart+1) ;
     SteadyStateLevel := Avg ;
     IPulse := (Avg - TestChannel[ChIm].ADCZero)*TestChannel[ChIm].ADCScale
                - IHold ;

     EdVHold.text := format( '%5.1f %s',[VHold,TestChannel[ChVm].ADCUnits] ) ;
     EdVPulse.text := format( '%5.1f %s',[VPulse,TestChannel[ChVm].ADCUnits] ) ;
     EdIHold.text := format( '%5.1f %s',[IHold,TestChannel[ChIm].ADCUnits] ) ;
     EdIPulse.text := format( '%5.1f %s',[IPulse,TestChannel[ChIm].ADCUnits] ) ;

     { Set scaling factors to convert current and voltage from user's
                     units to Volts and Amps }
     if Contains('mV',TestChannel[ChVm].ADCUnits) then VScale := 1E-3
                                                  else VScale := 1. ;
     if Contains('nA',TestChannel[ChIm].ADCUnits) then IScale := 1E-9
     else if Contains('pA',TestChannel[ChIm].ADCUnits) then IScale := 1E-12
     else if Contains('uA',TestChannel[ChIm].ADCUnits) then IScale := 1E-6
     else if Contains('mA',TestChannel[ChIm].ADCUnits) then IScale := 1E-3
     else IScale := 1. ;

     { Calculate seal resistance }
     Voltage := VPulse*VScale ;
     Current := IPulse*IScale ;
     if Abs(Current) > 1E-14 then
        Resistance := 0.9*(Voltage/Current) + 0.1*Resistance ;
        { Note the use of low pass filter to smooth reading }

     EdResistance.text := format( '%6.4f GOhm', [Resistance*1E-9] ) ;

     { Calculate capacity }
     Sum := 0. ;
     ChOffset := Channel[chIm].ChannelOffset ;
     for i := iStart to iAvgStart-1 do begin
         j := i*RawfH.NumChannels + ChOffset ;
         Sum := Sum + ADC^[j] - SteadyStateLevel ;
         end ;
     Charge := Sum*Channel[chIm].ADCScale*IScale * ADCdt ;
     if Abs(Voltage) > 1E-5 then
        Capacity := 0.9*(Charge/Voltage) + 0.1*Capacity ;
        { Note the use of low pass filter to smooth reading }
     EdCapacity.text := format( '%6.1f pF', [Capacity*1E12] ) ;

     { Get current display magnification setting }
     Settings.SealTest.DisplayScale :=
                       MinInt([MaxInt([Settings.SealTest.DisplayScale,1]),100]);

     { If in auto-scale mode ... determine whether a new magnification is needed }
     if ckAutoScale.checked or Settings.SealTest.FirstSweep then begin
        Settings.SealTest.FirstSweep := False ;
        NewDisplayRange := MaxInt([Max[ChVm] - Min[ChVm],
                                   Max[ChIm] - Min[ChIm],50]) ;
        NewDisplayScale := (MaxADCValue - MinADCValue) div NewDisplayRange ;
        NewDisplayScale := MaxInt([NewDisplayScale,1]) ;
        if (NewDisplayScale < Settings.SealTest.DisplayScale)
           or (NewDisplayScale >= (8*Settings.SealTest.DisplayScale)) then begin
              Settings.SealTest.DisplayScale := NewDisplayScale ;
              edScale.text := format( ' X%d', [Settings.SealTest.DisplayScale] ) ;
              end ;
        ChangeDisplayScaling := True ;
        end ;

     { Update display scaling if either the user has manually
                    change the scale OR auto-scale is in use }
     if  ChangeDisplayScaling = True then begin

         HalfRange := (MaxADCValue - MinADCValue) div
                       Settings.SealTest.DisplayScale ;
         Mid := (Max[ChIm] + Min[ChIm]) div 2 ;
         Settings.SealTest.yMax[ChIm] := MinInt([Mid+HalfRange,MaxADCValue]) ;
         Settings.SealTest.yMin[ChIm]  := MaxInt([Mid-HalfRange,MinADCValue]) ;
         Mid := (Max[ChVm] + Min[ChVm]) div 2 ;
         Settings.SealTest.yMax[ChVm] := MinInt([Mid+HalfRange,MaxADCValue]) ;
         Settings.SealTest.yMin[ChVm] := MaxInt([Mid-HalfRange,MinADCValue]) ;
         ChangeDisplayScaling := False ;
         end ;

     for ch := 0 to fH.NumChannels-1 do begin
         TestChannel[ch].yMin := Settings.SealTest.yMin[ch] ;
         TestChannel[ch].yMax := Settings.SealTest.yMax[ch] ;
         end ;

     end ;


procedure TSealTestFrm.bCloseClick(Sender: TObject);
{ ----------------------------
  Close button - closes window
  ----------------------------}
begin
     { Shut down A/D and D/A sub-systems }
     close ;
     end;


procedure TSealTestFrm.StopADCandDAC ;
{ ---------------------------------
  Shut down A/D and D/A sub-systems
  ---------------------------------}
begin
    if ADCActive then begin
       StopADC ;
       ADCActive := False ;
       end ;
    if DACActive then begin
       { Stop any D/A sweep }
       StopDAC ;
       { Return voltage command to holding voltage and Sync. O/P to OFF }
{       WriteOutputPorts( Settings.VCommand.HoldingVoltage *
                         Settings.VCommand.DivideFactor,
                         Settings.DigitalPort.Value ) ;}
       DACActive := False ;
       end ;
    end ;


procedure TSealTestFrm.EdVDivideKeyPress(Sender: TObject; var Key: Char);
begin
     if key = chr(13) then begin
        UpdateAllParameters ;
        end ;
     end;


procedure TSealTestFrm.edHoldingVoltage1KeyPress(Sender: TObject; var Key: Char);
{ ------------------------------------
  Set Voltage clamp holding voltage #1
  ------------------------------------}
begin
     if key = char(13) then begin
        UpdateAllParameters ;
        if rbUseHoldingVoltage1.checked then begin
           Settings.VCommand.HoldingVoltage := Settings.SealTest.HoldingVoltage1 ;
           end ;
        end ;

     end;


procedure TSealTestFrm.edHoldingVoltage2KeyPress(Sender: TObject; var Key: Char);
{ ------------------------------------
  Set Voltage clamp holding voltage #2
  ------------------------------------}
begin
     if key = char(13) then begin
        UpdateAllParameters ;
        if rbUseHoldingVoltage2.checked then begin
           Settings.VCommand.HoldingVoltage := Settings.SealTest.HoldingVoltage2 ;
           end ;
        end ;

     end;


procedure TSealTestFrm.EdHoldingVoltage3KeyPress(Sender: TObject;
  var Key: Char);
{ ------------------------------------
  Set Voltage clamp holding voltage #3
  ------------------------------------}
begin
     if key = char(13) then begin
        UpdateAllParameters ;
        if rbUseHoldingVoltage3.checked then begin
           Settings.VCommand.HoldingVoltage := Settings.SealTest.HoldingVoltage3 ;
           end ;
        end ;
     end ;


procedure TSealTestFrm.rbUseHoldingVoltage1Click(Sender: TObject);
{ ---------------------------------
  Set holding voltage to voltage #1
  ---------------------------------}
begin
     { Set holding voltage and pulse height to group #1 }
     Settings.SealTest.Use := 1 ;
     UpdateAllParameters ;
     Settings.VCommand.HoldingVoltage := Settings.SealTest.HoldingVoltage1 ;
     Settings.SealTest.PulseHeight := Settings.SealTest.PulseHeight1 ;
     end;


procedure TSealTestFrm.rbUseHoldingVoltage2Click(Sender: TObject);
{ ---------------------------------
  Set holding voltage to voltage #2
  ---------------------------------}
begin
     { Set holding voltage and pulse height to group #2 }
     Settings.SealTest.Use := 2 ;
     UpdateAllParameters ;
     Settings.VCommand.HoldingVoltage := Settings.SealTest.HoldingVoltage2 ;
     Settings.SealTest.PulseHeight := Settings.SealTest.PulseHeight2 ;
     end;


procedure TSealTestFrm.rbUseHoldingVoltage3Click(Sender: TObject);
{ ---------------------------------
  Set holding voltage to voltage #3
  Note. No pulse with this option
  ---------------------------------}
begin
     { Set holding voltage and pulse height to group #3 }
     Settings.SealTest.Use := 3 ;
     UpdateAllParameters ;
     Settings.VCommand.HoldingVoltage := Settings.SealTest.HoldingVoltage3 ;
     Settings.SealTest.PulseHeight := 0.0 ;
     end;

procedure TSealTestFrm.edPulseHeight1KeyPress(Sender: TObject;
  var Key: Char);
{ -------------------------------
  Set test pulse amplitude #1
  -------------------------------}
begin
     if key = char(13) then begin
        UpdateAllParameters ;
        if rbUseHoldingVoltage1.checked then begin
           Settings.SealTest.PulseHeight := Settings.SealTest.PulseHeight1 ;
           end ;
        end ;
     end;


procedure TSealTestFrm.edPulseheight2KeyPress(Sender: TObject;
  var Key: Char);
  { -----------------------------
    Set test pulse amplitude #2
    -----------------------------}
begin
     if key = char(13) then begin
        UpdateAllParameters ;
        if rbUseHoldingVoltage2.checked then begin
           Settings.SealTest.PulseHeight := Settings.SealTest.PulseHeight2 ;
           end ;
        end ;
     end;


procedure TSealTestFrm.edPulseWidthKeyPress(Sender: TObject; var Key: Char);
{ ----------------------------
  Set width of seal test pulse
  ----------------------------}
begin
     if key = chr(13) then begin
        UpdateAllParameters ;
        { Note. Force a re-start of D/A waveform so that D/A cycle
          time is updated }
        if DACActive then StopADC ;
        State := StartSweep ;
        end ;
     end ;


procedure TSealTestFrm.UpdateAllParameters ;
{ -------------------------------------------------------
  Update all test pulse parameters and their text boxes
  -------------------------------------------------------}
begin
     { Holding voltage #1 }
     Settings.SealTest.HoldingVoltage1 := ExtractFloat(edHoldingVoltage1.text,
                                          Settings.SealTest.HoldingVoltage1
                                             *VoltsTomV)*mVToVolts ;
     edHoldingVoltage1.text := format( ' %6.1f mV',
                               [Settings.SealTest.HoldingVoltage1*VoltsTomV]) ;

     { Test pulse amplitude #1 }
     Settings.SealTest.PulseHeight1 := ExtractFloat(edPulseHeight1.text,
                          Settings.SealTest.PulseHeight1*VoltsTomV)*mVToVolts ;
     edPulseHeight1.text := format( ' %6.1f mV',
                               [Settings.SealTest.PulseHeight1*VoltsTomV]) ;


     { Holding voltage #2 }
     Settings.SealTest.HoldingVoltage2 := ExtractFloat(edHoldingVoltage2.text,
                                          Settings.SealTest.HoldingVoltage2
                                             *VoltsTomV)*mVToVolts ;
     edHoldingVoltage2.text := format( ' %6.1f mV',
                               [Settings.SealTest.HoldingVoltage2*VoltsTomV]) ;

     { Test pulse amplitude #2 }
     Settings.SealTest.PulseHeight2 := ExtractFloat(edPulseHeight2.text,
                          Settings.SealTest.PulseHeight2*VoltsTomV)*mVToVolts ;
     edPulseHeight2.text := format( ' %6.1f mV',
                               [Settings.SealTest.PulseHeight2*VoltsTomV]) ;

     { Holding voltage #3 }
     Settings.SealTest.HoldingVoltage3 := ExtractFloat(edHoldingVoltage3.text,
                                          Settings.SealTest.HoldingVoltage3
                                             *VoltsTomV)*mVToVolts ;
     edHoldingVoltage3.text := format( ' %6.1f mV',
                                  [Settings.SealTest.HoldingVoltage3*VoltsTomV]) ;

     { Test pulse width }
     Settings.SealTest.PulseWidth := MsToSecs*ExtractFloat(edPulseWidth.text,
                                     Settings.SealTest.PulseWidth*SecsToMs) ;
     Settings.SealTest.PulseWidth := MaxFlt( [MinFlt([
                                     Settings.SealTest.PulseWidth,MaxPulseWidth]),
                                     MinPulseWidth]) ;
     edPulseWidth.text := format(' %5.1f ms',[Settings.SealTest.PulseWidth*SecsToMs]) ;

     { Command voltage divide factor }
     Settings.VCommand.DivideFactor := ExtractFloat(edVDivide.text,
                                       Settings.VCommand.DivideFactor) ;
     if Settings.VCommand.DivideFactor <= 0. then
           Settings.VCommand.DivideFactor := 1. ;
      edVDivide.text := format( ' %f ',[Settings.VCommand.DivideFactor]) ;

     end ;


procedure TSealTestFrm.FormDeactivate(Sender: TObject);
{ -----------------------------------------
  Called when focus moves to another window
  -----------------------------------------}
begin
     { Terminate seal test pulses if form loses focus }
     Timer1.enabled := false ;
     StopADCandDAC ;
     State := Idle ;
     end;


procedure TSealTestFrm.FormActivate(Sender: TObject);
{ -------------------------------------------
  Called when focus moves to seal test window
  -------------------------------------------}
begin
     { Set tick in Windows menu }
     Main.ClearTicksInWindowsMenu ;
     Main.ShowSealTest.checked := True ;
     { Start seal test pulses when form gains focus }
     Timer1.enabled := True ;
     TimerBusy := False ;

     State := StartSweep ;
     end;


procedure TSealTestFrm.FormClose(Sender: TObject;
  var Action: TCloseAction);
{ -----------------------------
  Called when window is closed
  -----------------------------}
begin

     Main.SealTest.enabled := True ;
     Main.ShowSealTest.Visible := False ;

     { Shut down A/D and D/A sub-systems }
     StopADCandDAC ;
     { Note. If A/D and D/A sub-systems are still active when the
       form is closed the program will crash. }

     { Release memory used by objects created in FORMCREATE}
     Timer1.enabled := false ;
     { Close the form }
     Action := caFree ;

     end;


procedure TSealTestFrm.FormDestroy(Sender: TObject);
{ -------------------------------
  Called when window is destroyed
  -------------------------------}
begin
     { Free data buffers }
     HeapBuffers( Deallocate ) ;
     end;


procedure TSealTestFrm.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
{ --------------
  Function keys
  -------------}
begin
     case Key of
          { F3 selects holding voltage #1 }
          VK_F3 : begin
               Settings.SealTest.Use := 1 ;
               UpdateAllParameters ;
               Settings.VCommand.HoldingVoltage := Settings.SealTest.HoldingVoltage1 ;
               Settings.SealTest.PulseHeight := Settings.SealTest.PulseHeight1 ;
               rbUseHoldingVoltage1.checked := True ;
               end ;
          { F4 selects holding voltage #2 }
          VK_F4 : begin
               Settings.SealTest.Use := 2 ;
               UpdateAllParameters ;
               Settings.VCommand.HoldingVoltage := Settings.SealTest.HoldingVoltage2 ;
               Settings.SealTest.PulseHeight := Settings.SealTest.PulseHeight2 ;
               rbUseHoldingVoltage2.checked := True ;
               end ;
          { F5 selects holding voltage #3 }
          VK_F5 : begin
               rbUseHoldingVoltage3.checked := True ;
               Settings.SealTest.Use := 3 ;
               UpdateAllParameters ;
               Settings.VCommand.HoldingVoltage := Settings.SealTest.HoldingVoltage3 ;
               Settings.SealTest.PulseHeight := 0.0 ;
               end ;
          end ;
     end;


procedure TSealTestFrm.FormResize(Sender: TObject);
{ ----------------------------------
  Called when window size is changed
  ----------------------------------}
begin
     Height := MaxInt( [Height,MinFormHeight] ) ;
     Width := MaxInt( [Width,MinFormWidth] ) ;
     DivideGrp.Top := Height - DivideGrp.Height - 25 ;
     DACGrp.Top := DivideGrp.Top - DACGrp.Height - 5 ;
     VoltsGrp.Top := DivideGrp.Top + DivideGrp.Height - VoltsGrp.Height ;
     CurrentGrp.Top := VoltsGrp.Top ;
     CellGrp.Top := VoltsGrp.Top ;
     DisplayGrp.Top := VoltsGrp.Top ;
     bClose.Top := DisplayGrp.Top + DisplayGrp.Height + 10 ;
     pbDisplay.Width := Width - pbDisplay.Left - 15 ;
     pbDisplay.Height := VoltsGrp.Top - lbTMin.Height*2 - pbDisplay.Top ;
     lbTMin.Top := pbDisplay.top + pbDisplay.Height + 2 ;
     lbTMin.Left := pbDisplay.left ;
     lbTMax.Top := lbTMin.Top ;
     lbTMax.Left := pbDisplay.left + pbDisplay.Width - lbTMax.Width ;
     end;


procedure TSealTestFrm.SpScaleUpClick(Sender: TObject);
{ -------------------------------
  Increase display magnification
  ------------------------------}
begin
     Settings.SealTest.DisplayScale :=
                      MinInt([MaxInt([ExtractInt(edScale.text) + 1,1]),100]) ;
     edScale.text := format( ' X%d', [Settings.SealTest.DisplayScale] ) ;
     { Request that display range is updated on next sweep }
     ChangeDisplayScaling := True ;
     end;


procedure TSealTestFrm.SpScaleDownClick(Sender: TObject);
{ -------------------------------
  Decrease display magnification
  ------------------------------}
begin
     Settings.SealTest.DisplayScale :=
                       MinInt([MaxInt([ExtractInt(edScale.text) - 1,1]),100]) ;
     edScale.text := format( ' X%d', [Settings.SealTest.DisplayScale] ) ;
     { Request that display range is updated on next sweep }
     ChangeDisplayScaling := True ;
     end;


procedure TSealTestFrm.EdScaleKeyPress(Sender: TObject; var Key: Char);
  {------------------------------
  Change display magnification
  ------------------------------}
begin
     if key = chr(13) then begin
        Settings.SealTest.DisplayScale :=
                           MinInt([MaxInt([ExtractInt(edScale.text),1]),100]) ;
        edScale.text := format( ' X%d', [Settings.SealTest.DisplayScale] ) ;
        { Request that display range is updated on next sweep }
        ChangeDisplayScaling := True ;
        end;
     end ;


procedure TSealTestFrm.ckAutoScaleClick(Sender: TObject);
{ ------------------------------
  Enable display auto-scale mode
  ------------------------------}
begin
     Settings.SealTest.AutoScale := ckAutoScale.checked ;
     end;


procedure TSealTestFrm.ckFreeRunClick(Sender: TObject);
{ ------------------------------------------------
  Force a re-start when Free run check box changed
  ------------------------------------------------}
begin
     Settings.SealTest.FreeRun := ckFreeRun.Checked ;
     StopADCandDAC ;
     State := StartSweep ;
     end;



end.
