unit MainUnit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls,
  AIOAIO;

const
  AM_DEPTH = WM_USER + $98;
  AM_MESSAGE = WM_USER + $99;

type
  TMainForm = class(TForm)
    ControlPanel: TPanel;
    GroupBox1: TGroupBox;
    RangeRadios: TRadioGroup;
    LogMemo: TMemo;
    DeviceCombo: TComboBox;
    Panel3: TPanel;
    DifferentialCheck: TCheckBox;
    LogFormatRadios: TRadioGroup;
    ChEdit: TEdit;
    Label3: TLabel;
    Label1: TLabel;
    HzEdit: TEdit;
    Panel2: TPanel;
    GoButton: TButton;
    StopButton: TButton;
    DepthLabel: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure GoButtonClick(Sender: TObject);
    procedure StopButtonClick(Sender: TObject);
    procedure DifferentialCheckClick(Sender: TObject);
    procedure DeviceComboChange(Sender: TObject);
  protected
    bPollStarts: Boolean;
    RunLength: Single;
    RunsKnown: Integer;

    procedure EnableGo(bEnable: Boolean);
    procedure EnableStop(bEnable: Boolean);
    procedure DoCycleRun(NewHz: Double; ThisRunLength: Int64);
    procedure ProcessPause;
    procedure AMMessage(var Msg: TMessage); message AM_MESSAGE;
    procedure AMDepth(var Msg: TMessage); message AM_DEPTH;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

uses
  Math, ShellAPI;

const
  ERROR_TRANSLATION_COMPLETE = 757;

function LocalAlloc(uFlags, uBytes: UINT): Pointer; stdcall; external kernel32;
function LocalFree(pMem: Pointer): Pointer; stdcall; external kernel32;
function PIO_DMAPiongControl(iBoard: TBoardIndex; pControlReply: TPUInt8; ControlSize: TUInt32; ReplySize: TPUInt32 = nil): TWinErrCode; cdecl; external 'AIOAIO.dll';
function PIO_DMAPiongIn(iBoard: TBoardIndex; pData: TPUInt8; DataSize: TUInt32; pOverlap: POverlapped): TWinErrCode; cdecl; external 'AIOAIO.dll';
function PIO_GetDeviceHandle(iBoard: TBoardIndex; var hDevice: THandle): TWinErrCode; cdecl; external 'AIOAIO.dll';
function DoDMAPiongControlReply(iBoard: TBoardIndex; var ControlReply: AnsiString): TWinErrCode;
    var
      Size: TUInt32;
begin
  Size := Length(ControlReply);
  Result := PIO_DMAPiongControl(iBoard, Pointer(@ControlReply[1]), Size,Pointer(@Size));
  SetLength(ControlReply, Size);
end;
function DoDMAPiongControl(iBoard: TBoardIndex; Control: AnsiString): TWinErrCode;
begin
  Result := DoDMAPiongControlReply(iBoard, Control);
end;


const
	IRQEnableOffset = $01;
	DACOffset = $04;
	BaseClockOffset = $0C;
	DivisorOffset = $10;
	ADCRangeOffset = $18;
	AFLevelOffset = $20;
	ADCDepthOffset = $28;
	ADCStartStopOffset = $38;
	ADCImmediateOffset = $3A;
	ADCDataOffset: array[0..1] of TUInt32 = ($4000, $8000);
	dSequencer = $4;

	BaseImmediateCmd = $FCCE;
	BaseStartStopCmd = $FCEE;

var
        Version: TUInt32;

function NybbleToHex(N: Byte): Char;
begin
  N := N and $F;
  if N < 10 then
    Result := Char(Byte('0') + N)
  else
    Result := Char(Byte('A') - 10 + N)
  ;
end;

function HexToNybble(Hex: Char): Byte;
begin
  case Hex of
  '0'..'9': Result := Byte(Hex) - Byte('0');
  'A'..'F': Result := Byte(Hex) - Byte('A') + 10;
  'a'..'f': Result := Byte(Hex) - Byte('a') + 10;
  else      raise Exception.CreateFmt('Invalid hex character "%s"', [ Hex ]);
  end;
end;

function StrToHex(Text: String): String;
var
  I: Integer;

begin
  SetString(Result, nil, Length(Text) * 2);
  for I := 1 to Length(Text) do begin
    Result[I * 2 - 1] := NybbleToHex(Byte(Text[I]) shr 4);
    Result[I * 2]     := NybbleToHex(Byte(Text[I])      );
  end;
end;

function HexToStr(Hex: String): String;
var
  I: Integer;

begin
  SetString(Result, nil, (Length(Hex) + 1) div 2);
  for I := 1 to Length(Result) do
    Result[I] := Char(HexToNybble(Hex[I * 2]) + HexToNybble(Hex[I * 2 - 1]) shl 4)
  ;
end;

function StrToSpacedHex(Text: String): String;
var
  I: Integer;

begin
  Result := StringOfChar(' ', Length(Text) * 3-1);
  for I := 1 to Length(Text) do begin
    Result[I * 3 - 2] := NybbleToHex(Byte(Text[I]) shr 4);
    Result[I * 3 - 1] := NybbleToHex(Byte(Text[I])      );
  end;
end;

function StrToSpacedWords(Text: String): String;
var
  TI, RI, L: Integer;

begin
  L := Length(Text);
  Result := StringOfChar(' ', (L shr 1)*5 + (L and 1)*3 - 1);
  TI := 1;
  RI := 1;
  while L >= 2 do begin
    Inc(TI);
    Result[RI] := NybbleToHex(Byte(Text[TI]) shr 4);
    Inc(RI);
    Result[RI] := NybbleToHex(Byte(Text[TI])      );
    Inc(RI); Dec(TI);
    Result[RI] := NybbleToHex(Byte(Text[TI]) shr 4);
    Inc(RI);
    Result[RI] := NybbleToHex(Byte(Text[TI])      );
    Inc(RI, 2); Inc(TI, 2); Dec(L, 2);
  end;

  if L > 0 then begin
    Result[RI] := NybbleToHex(Byte(Text[TI]) shr 4);
    Inc(RI);
    Result[RI] := NybbleToHex(Byte(Text[TI])      );
    //Inc(RI, 2); Inc(TI); Dec(L);
  end;
end;

procedure DumpStringToFile(S, Path: String);
var
  Fil: TFileStream;

begin
  Fil := TFileStream.Create(Path, fmCreate or fmShareDenyWrite);
  try
    Fil.Write(S[1], Length(S));
  finally
    Fil.Free;
  end;
end;

function DumpFileToString(Path: String): String;
var
  Fil: TFileStream;
  L: Integer;

begin
  Fil := TFileStream.Create(Path, fmOpenRead or fmShareDenyNone);
  try
    L := Fil.Size;
    SetString(Result, nil, L);
    Fil.Read(Result[1], L);
  finally
    Fil.Free;
  end;
end;

function BStatus(bSucc: Boolean): LongWord;
begin
	if bSucc then
		Result := ERROR_SUCCESS
	else begin
		Result := GetLastError;
		if Result = ERROR_SUCCESS then Result := ERROR_GEN_FAILURE;
	end;
end;



type
  TLogThread = class;

  TReadData = record
    pData: Pointer;
    Ovl: TOverlapped;
  end;
  TPReadData = ^TReadData;

  TDataThread = class(TThread)
  public
    constructor Create; virtual;
    destructor Destroy; override;
  public
    hDataWnd: THandle;
    FNSuffix: String;
  protected
    function GetDone: Boolean; virtual;
  public property bDone: Boolean read GetDone;
  protected
    hDoneEvent: THandle;
    LogThread: TLogThread;

    procedure Execute; override;
  end;

  TLogThread = class(TThread)
  public
    hDepthWnd: THandle;
    //CachedDepth: Integer;

    constructor Create; virtual;
    destructor Destroy; override;

    procedure OpenFN(Suffix: String); virtual;
    procedure PutItem(NewItem: Pointer); virtual;
    function GetItem: Pointer; virtual;
  protected
    hItemMutex, hItemSem: THandle;
    Items: TList;
    LogFil: TFileStream;

    procedure Execute; override;
  end;

var
  TicksPerSec: Int64;
  DataThread: TDataThread;
  iBoard: TBoardIndex;
  DMADataSize: TUInt32;
  LogFormat: (lfNone, lfBinary, lfSingleSample, lfAllChannels, lfClearAll);

procedure DisposeRead(pRead: TPReadData);
begin
  if pRead.pData <> nil then begin
    LocalFree(pRead.pData);
    pRead.pData := nil;
  end;
  if pRead.Ovl.hEvent <> 0 then begin
    CloseHandle(pRead.Ovl.hEvent);
    pRead.Ovl.hEvent := 0;
  end;
  Dispose(pRead);
end;

{ TDataThread }

constructor TDataThread.Create;
begin
  inherited Create(True);

  hDoneEvent := CreateEvent(nil, True, False, nil);
end;

destructor TDataThread.Destroy;
var
  H: THandle;

begin
  H := hDoneEvent;
  hDoneEvent := 0;
  CloseHandle(H);

  inherited;
end;

procedure TDataThread.Execute;
var
  Status: TWinErrCode;
  Overlaps, UnusedOverlaps: TList;
  pThisRead: TPReadData;
  iRead: Integer;
  hDevice: THandle;
  DummyBytesTransferred: Longword;

  procedure PostMessageStr(Msg: String);
  var
    pMsg: PString;

  begin
    if hDataWnd = INVALID_HANDLE_VALUE then Exit;

    New(pMsg);
    pMsg^ := Msg;
    PostMessage(hDataWnd, AM_MESSAGE, 0, LPARAM(pMsg));
  end;

begin
  ReturnValue := PIO_GetDeviceHandle(iBoard, hDevice);
  if ReturnValue <> ERROR_SUCCESS then begin
    PostMessageStr('Error ' + IntToStr(ReturnValue) + ' getting device handle.');
    Exit;
  end;

  LogThread := TLogThread.Create;
  LogThread.hDepthWnd := hDataWnd;
  LogThread.OpenFN(FNSuffix);
  LogThread.Resume;
  Overlaps := TList.Create;
  UnusedOverlaps := TList.Create;

  SetThreadPriority(GetCurrentThread, THREAD_PRIORITY_TIME_CRITICAL);
  repeat
    if Overlaps.Count < 4 then begin
      if UnusedOverlaps.Count > 0 then begin
        pThisRead := UnusedOverlaps[0];
        UnusedOverlaps.Delete(0);
      end
      else begin
        New(pThisRead);
        ZeroMemory(pThisRead, SizeOf(pThisRead^));
        pThisRead.pData := LocalAlloc(LMEM_FIXED or LMEM_ZEROINIT, DMADataSize);
        pThisRead.Ovl.hEvent := CreateEvent(nil, False, False, nil);
      end;
      Status := PIO_DMAPiongIn(iBoard, pThisRead.pData, DMADataSize, @pThisRead.Ovl);
      case Status of
        ERROR_SUCCESS: ;
        ERROR_IO_PENDING: ;
        else Break;
      end;

      Overlaps.Add(pThisRead);
      pThisRead := nil;
    end
    else begin
      pThisRead := Overlaps[0];
      Overlaps.Delete(0);

      Status := BStatus(GetOverlappedResult(hDevice, pThisRead.Ovl, DummyBytesTransferred, True));
      case Status of
        ERROR_NO_MORE_FILES,
        ERROR_TRANSLATION_COMPLETE: Status := ERROR_SUCCESS;
      end;
      if Status <> ERROR_SUCCESS then Break;

      LogThread.PutItem(pThisRead.pData);
      pThisRead.pData := LocalAlloc(LMEM_FIXED or LMEM_ZEROINIT, DMADataSize);

      UnusedOverlaps.Add(pThisRead);
      pThisRead := nil;
    end;

    if Terminated then begin
      Status := ERROR_OPERATION_ABORTED;
      Break;
    end;
  until False;
  SetThreadPriority(GetCurrentThread, THREAD_PRIORITY_NORMAL);

  if pThisRead <> nil then begin
    DisposeRead(pThisRead);
    //pThisRead := nil;
  end;

  for iRead := Overlaps.Count-1 downto 0 do DisposeRead(Overlaps[iRead]);
  Overlaps.Free;
  for iRead := UnusedOverlaps.Count-1 downto 0 do DisposeRead(UnusedOverlaps[iRead]);
  UnusedOverlaps.Free;

  LogThread.Free;
  Sleep(1);
  PostMessage(hDataWnd, AM_DEPTH, 0, 0);

  ReturnValue := Status;
  if Status = ERROR_OPERATION_ABORTED then                                     
    PostMessageStr('Worker thread was stopped.')
  else
    PostMessageStr('Worker thread exiting with ' + IntToStr(ReturnValue) + '.')
  ;
  SetEvent(hDoneEvent);
end;

function TDataThread.GetDone: Boolean;
var
  H: THandle;

begin
  Result := False;

  if Self = nil then Exit;

  H := hDoneEvent;
  if H = 0 then Exit;

  if WaitForSingleObject(hDoneEvent, 0) <> WAIT_OBJECT_0 then Exit;

  Result := True;
end;

{ TLogThread }

procedure TLogThread.OpenFN(Suffix: String);
var
  ST: TSystemTime;
  FN: String;
  HeadLine: String;

begin
  GetLocalTime(ST);
  with ST do FN := Format('%.4d%.2d%.2d_%.2d%.2d%.2d ', [ wYear, wMonth, wDay, wHour, wMinute, wSecond ]) + Suffix;
  case LogFormat of
    lfNone: begin
      LogFil := nil;
      Exit;
    end;
    lfBinary: begin
      FN := FN + ' Raw.bin';
      HeadLine := '';
    end;
    lfSingleSample: begin
      FN := FN + ' Single.csv';
      HeadLine := '"Ch OK", "Raw","Channel","Gain","Top Byte","Counts"'#13#10;
    end;
    lfAllChannels: begin
      FN := FN + ' All.csv';
      HeadLine := '"Row#","Ch0","Ch1","Ch2","Ch3","Ch4","Ch5","Ch6","Ch7","Ch8","Ch9","ChA","ChB","ChC","ChD","ChE","ChF"'#13#10;
    end;
    lfClearAll: begin
      FN := FN + ' Clear All.txt';
      HeadLine := 'Sample#'#9' 0000 1111 2222 3333 4444 5555 6666 7777 8888 9999 AAAA BBBB CCCC DDDD EEEE FFFF'#13#10;
    end;
  end;
  LogFil := TFileStream.Create(FN, fmCreate);
  LogFil.Free;
  LogFil := TFileStream.Create(FN, fmOpenWrite or fmShareDenyWrite);

  if HeadLine <> '' then begin
    LogFil.WriteBuffer(HeadLine[1], Length(HeadLine));
    HeadLine := '';
  end;
end;

constructor TLogThread.Create;
begin
  inherited Create(True);

  hItemMutex := CreateMutex(nil, False, nil);
  hItemSem := CreateSemaphore(nil, 0, $0FFFFFFF, nil);

  Items := TList.Create;
end;

destructor TLogThread.Destroy;
begin
  //With no item, this is a "kill release".
  ReleaseSemaphore(hItemSem, 1, nil);

  inherited;

  WaitForSingleObject(hItemMutex, 0);
  FreeAndNil(Items);
  CloseHandle(hItemMutex);
  CloseHandle(hItemSem);

  FreeAndNil(LogFil);
end;

procedure TLogThread.Execute;
var
  pItem, pData: PLongWord;
  iCh, iCh2, iChBack1, iChBack2, iChExpected, dCh2, iData, iSample: Integer;
  LogBlock: TStringList;
  LogStr: AnsiString;
  LogCh: array of Integer;

  procedure ParseCh;
  begin
    iCh := (pData^ shr 20) and $7;
    if (iData and 1) <> 0 then iCh := iCh or $8;
    if iChBack2 < 0 then begin
      dCh2 := 0;
    end
    else begin
      dCh2 := Cardinal(iCh - iChBack2) mod 8;
      if dCh2 <> 1 then dCh2 := 0;
    end;
  end;

  function bAnyChannels: Boolean;
  var
    iCh: Integer;
  
  begin
    Result := True;
    for iCh := 0 to High(LogCh) do
      if LogCh[iCh] <> MAXINT then
        Exit
    ;
    Result := False;
  end;

  procedure LogInvalid(Sample: LongWord);
  begin
    LogBlock.Add(IntToStr(iSample) + ',INVALID,' + IntToHex(Sample and $7FFFFFFF, 1));

    Inc(iSample);
  end;

  function CountsToVolts(counts: smallint): real;
  var fraction : double;
  begin
    Result := 666.0;
    fraction := counts / 32768.0; // fraction is now �1.0
    case MainForm.RangeRadios.ItemIndex of
      0: Result := fraction * 12.0;
      1: Result := fraction * 5.0;
      2: Result := fraction * 2.5;
      3: Result := fraction * 1.25;
      4: Result := fraction * 0.625;
      5: Result := fraction * 0.3125;
      7: Result := fraction * 10.0;
    end;
  end;

  procedure LogAllChannels;
  var
    iCh: Integer;
    Buf: String;

  begin
    Buf := IntToStr(iSample);
    for iCh := 0 to High(LogCh) do
      if LogCh[iCh] = MAXINT then
        Buf := Buf + ','
      else
        Buf := Buf + (',' + Format('%6.3f',[CountsToVolts(LogCh[iCh])]))
    ;
    LogBlock.Add(Buf);

    Inc(iSample);
    for iCh := 0 to High(LogCh) do LogCh[iCh] := MAXINT;
  end;


  procedure PostMessageStr(Msg: String);
  var
    pMsg: PString;

  begin
    New(pMsg);
    pMsg^ := Msg;
    PostMessage(MainForm.Handle, AM_MESSAGE, 0, Integer(pMsg));
  end;

begin
  LogBlock := TStringList.Create;
  if LogFormat = lfAllChannels then begin
    SetLength(LogCh, 16);
    for iCh := 0 to High(LogCh) do LogCh[iCh] := MAXINT;
  end;
  iSample := 0;
  LogStr := '';

  iChBack2 := -2;
  iChBack1 := -2;
  repeat
    pItem := GetItem;
    if pItem = nil then Break;

    if LogFormat = lfNone then
    else begin
      LogBlock.Clear;
      case LogFormat of
        lfBinary: begin
          SetString(LogStr, PAnsiChar(pItem), DMADataSize);
        end;
        lfSingleSample: begin
          pData := pItem;
          for iData := 0 to (DMADataSize div 4)-1 do begin
            if (pData^ and $80000000) <> 0 then begin
              iCh := -2;
              LogBlock.Add('0,"' + IntToHex(pData^, 8) + '",-1');
            end
            else begin
              ParseCh;
              LogBlock.Add('' + NybbleToHex(dCh2) + ',"' + IntToHex(pData^, 8) + '",' + IntToStr(iCh) + ',' + IntToStr((pData^ shr 16) and $F) + ',"' + IntToHex(pData^ shr 24, 2) + '",' + IntToStr(SmallInt(pData^ and $FFFF)));
            end;
            iChBack2 := iChBack1;
            iChBack1 := iCh;
            Inc(pData);
          end;
          LogStr := LogBlock.Text;
          LogBlock.Clear;
        end;
        lfAllChannels: begin
          pData := pItem;
          for iData := 0 to (DMADataSize div 4)-1 do begin
            if (pData^ and $80000000) <> 0 then begin
              if bAnyChannels then LogAllChannels;
              LogInvalid(pData^);
            end
            else begin
              ParseCh;
              if LogCh[iCh] <> MAXINT then LogAllChannels;
              LogCh[iCh] := SmallInt(pData^ and $FFFF);
            end;
            Inc(pData);
          end;
          if bAnyChannels then LogAllChannels;
          LogStr := LogBlock.Text;
          LogBlock.Clear;
        end;
        lfClearAll: begin
          pData := pItem;
          for iData := 0 to (DMADataSize div 4)-1 do begin
            LogStr := IntToHex(iSample, 1) + #9;
            if (pData^ and $80000000) <> 0 then begin
              iCh := -2;
            end
            else begin
              ParseCh;
              if iChBack2 < 0 then
                iChExpected := -1
              else
                iChExpected := (iChBack2 and $8) or ((iChBack2+1) and $7)
              ;
              for iCh2 := $0 to $F do
                if iCh2 = iCh then begin
                  if (pData^ and $8000) <> 0 then
                    LogStr := LogStr + ('-' + IntToHex($10000 - (pData^ and $FFFF), 4))
                  else
                    LogStr := LogStr + (' ' + IntToHex(pData^ and $FFFF, 4))
                  ;
                end
                else if iCh2 = iChExpected then
                  LogStr := LogStr + ' ????'
                else
                  LogStr := LogStr + ' ————'
              ;
            end;
            LogBlock.Add(LogStr);
            Inc(iSample);
            iChBack2 := iChBack1;
            iChBack1 := iCh;
            Inc(pData);
          end;
          LogStr := LogBlock.Text;
          LogBlock.Clear;
        end;
        else begin
          PostMessageStr('Invalid log format #' + IntToStr(Ord(LogFormat)) + '.');
          Terminate;
          LocalFree(pItem);
          Break;
        end;
      end;
    end;
    LocalFree(pItem);

    if LogStr <> '' then begin
      LogFil.WriteBuffer(LogStr[1], Length(LogStr));
      LogStr := '';
    end;
  until False;

  FreeAndNil(LogBlock);
end;

function TLogThread.GetItem: Pointer;
var
  CachedDepth: Integer;

begin
  Result := nil;
  CachedDepth := -1;

  if WaitForSingleObject(hItemSem, INFINITE) <> WAIT_OBJECT_0 then Exit;

  WaitForSingleObject(hItemMutex, INFINITE);
    if Items.Count <> 0 then begin
      Result := Items[0];
      Items.Delete(0);
      CachedDepth := Items.Count;
    end;
  ReleaseMutex(hItemMutex);

  if CachedDepth >= 0 then PostMessage(hDepthWnd, AM_DEPTH, 0, CachedDepth);
end;

procedure TLogThread.PutItem(NewItem: Pointer);
var
  CachedDepth: Integer;

begin
  WaitForSingleObject(hItemMutex, INFINITE);
    Items.Add(NewItem);
    CachedDepth := Items.Count;
  ReleaseMutex(hItemMutex);

  PostMessage(hDepthWnd, AM_DEPTH, 0, CachedDepth);

  ReleaseSemaphore(hItemSem, 1, nil);
end;

{ TMainForm }

procedure TMainForm.EnableGo(bEnable: Boolean);
begin
  DeviceCombo.Enabled := bEnable;
  RangeRadios.Enabled := bEnable;
  DifferentialCheck.Enabled := bEnable;
  HzEdit.Enabled := bEnable;
  GoButton.Enabled := bEnable;
end;

procedure TMainForm.EnableStop(bEnable: Boolean);
begin
  StopButton.Enabled := bEnable;
end;

procedure TMainForm.FormCreate(Sender: TObject);
var
  I: Integer;
  DeviceStr: AnsiString;
  DeviceStrSize: TUInt32;
  Status: TWinErrCode;

begin
  QueryPerformanceFrequency(TicksPerSec);
  IsMultiThread := True;

  DeviceCombo.Align := alTop;
  DifferentialCheck.Align := alTop;
  DeviceCombo.Items.Clear;
  for I := 0 to BRD_GetHighestBoardIndex do begin
    DeviceStrSize := MAX_PATH;
    DeviceStr := StringOfChar(#0, DeviceStrSize);
    Status := BRD_GetName(I, DeviceStrSize, TPAnsiChar(DeviceStr), @DeviceStrSize);
    if Status <> ERROR_SUCCESS then
      DeviceStr := '<Error ' + IntToStr(Status) + ' from BRD_GetName>'
    else
      SetLength(DeviceStr, DeviceStrSize)
    ;
    DeviceCombo.Items.Add('[' + IntToStr(I) + '] ' + DeviceStr);
  end;
  if DeviceCombo.Items.Count <> 0 then DeviceCombo.ItemIndex := 0;
  DeviceComboChange(nil);
end;

procedure TMainForm.GoButtonClick(Sender: TObject);
var
  iRange: TRangeCode;
  Status: TWinErrCode;
  Hz: Double;
  StartStopCmd: TUInt16;
  bDifferential: Boolean;
  ChCt, HighCh: TUInt32;

begin
  EnableGo(False);

  if Sender <> nil then begin
    RunLength := 60 * TicksPerSec;
    RunsKnown := 0;
  end;


  iBoard := DeviceCombo.ItemIndex;
  AIOAIO.BRD_Reset(iBoard);

  iRange := RangeRadios.ItemIndex;
  bDifferential := DifferentialCheck.Checked;
  
  ChCt := StrToInt(ChEdit.Text);
  if (ChCt and 1) <> 0 then raise Exception.Create('Can''t acquire odd number of channels.');
  if ChCt < 2 then raise Exception.Create('Can''t acquire ' + IntToStr(ChCt) + ' channels.');
  if bDifferential then begin
    if ChCt > 8 then raise Exception.Create('Can''t acquire ' + IntToStr(ChCt) + ' channels.');
  end
  else begin
    if ChCt > 16 then raise Exception.Create('Can''t acquire ' + IntToStr(ChCt) + ' channels.');
  end;
  ChCt := ChCt div 2;
  HighCh := ChCt - 1;

  Hz := StrToFloat(HzEdit.Text);
  case LogFormatRadios.ItemIndex of
    0: LogFormat := lfBinary;
    1: LogFormat := lfAllChannels;
    else raise Exception.Create('Invalid log format #' + IntToStr(LogFormatRadios.ItemIndex) + '.');
  end;

  Status := DoDMAPiongControl(iBoard, #$10);
  case Status of
    ERROR_SUCCESS: ;
    ERROR_CONNECTION_INVALID: ;
    else begin
      LogMemo.Lines.Add('Error ' + IntToStr(Status) + ' stopping.');
      EnableGo(True);
      Exit;
    end;
  end;

  if DataThread <> nil then begin
    FreeAndNil(DataThread);
  end;

  Sleep(1);

  BRD_Reset(iBoard);

  Sleep(1);

  PIO_Out32(iBoard, AFLevelOffset, $F00);
  DMADataSize := PIO_In32(iBoard, AFLevelOffset) * 8;

  LogMemo.Lines.Add('DMADataSize = ' + IntToHex(DMADataSize, 1));
  if DMADataSize < $800 then begin
    LogMemo.Lines.Add('DMADataSize is too small.');
    EnableGo(True);
    Exit;
  end;

  //Acquire all channels at selected range.
  ADC_SetRange(iBoard, 0, ciAll, iRange);
  ADC_SetRange(iBoard, 1, ciAll, iRange);
  //These are the same as the defaults:
  //ADC_SetChannelCount(iBoard, 0, ciAll);
  //ADC_SetChannelCount(iBoard, 1, ciAll);
  //ADC_ConfigureAcquire(iBoard, 0, ctAuto, 0);
  //ADC_ConfigureAcquire(iBoard, 1, ctAuto, 0);
  ADC_SetRate(iBoard, 0, Hz);

  StartStopCmd := BaseStartStopCmd and not ($7 shl 12); //Mask out channel bits for now.
  if bDifferential then begin
    StartStopCmd := StartStopCmd and not (1 shl 11); //AND out single-ended bit.
    //Get differential, but only even channels. Odd channels are the low side, IN-, instead of using the common IN-.
    StartStopCmd := StartStopCmd or (HighCh shl 13);
  end
  else begin
    StartStopCmd := StartStopCmd or (1 shl 11); //OR in single-ended bit.
    StartStopCmd := StartStopCmd or (HighCh shl 12);
  end;

  Status := DoDMAPiongControl(iBoard, #$11);
  if Status <> ERROR_SUCCESS then begin
    LogMemo.Lines.Add('Error ' + IntToStr(Status) + ' starting.');
  end;

  DataThread := TDataThread.Create;
  DataThread.hDataWnd := Handle;
  DataThread.FNSuffix := FloatToStr(Hz) + ' Hz @ ' + IntToStr(iRange);
  if bDifferential then DataThread.FNSuffix := DataThread.FNSuffix + 'D';
  DataThread.Resume;
  Sleep(1);

  LogMemo.Lines.Add('Start/Stop = ' + IntToHex(StartStopCmd or $1, 4) + '/' + IntToHex(StartStopCmd and not $1, 4));

  PIO_Out32(iBoard, $40, $10); // enable IRQ
  PIO_Out32(iBoard, ADCStartStopOffset + dSequencer*1, StartStopCmd or $20000);
  PIO_Out32(iBoard, ADCStartStopOffset + dSequencer*0, StartStopCmd or $30000);
  EnableStop(True);
end;

procedure TMainForm.StopButtonClick(Sender: TObject);
var
  Status: TWinErrCode;

begin
  EnableStop(False);

  if Version and $FFFF0000 = $10000 then begin
    // disable IRQ on Release 1.x
    PIO_Out32(iBoard, $40, $0);
    // Stop ADC on Release 1.x
    PIO_Out32(iBoard, ADCStartStopOffset + dSequencer*0, BaseStartStopCmd);
  end
  else begin
    // disable IRQ on < Release 1.x
    PIO_Out8(iBoard, IRQEnableOffset, $00);
    // Stop ADC on < Release 1.x
    PIO_Out16(iBoard, ADCStartStopOffset + dSequencer*0, BaseStartStopCmd and not $1 );
  end;

  PIO_PollForIRQ(iBoard);

  Status := DoDMAPiongControl(iBoard, #$10);
  if Status <> ERROR_SUCCESS then begin
    LogMemo.Lines.Add('Error ' + IntToStr(Status) + ' stopping.');
    EnableGo(True);
    Exit;
  end;

  DataThread.Terminate;
  repeat
    Application.ProcessMessages;
  until DataThread.bDone;
  FreeAndNil(DataThread);

  if Sender <> nil then begin
    RunLength := 60 * TicksPerSec;
    RunsKnown := 0;
  end;

  EnableGo(True);

  LogMemo.Lines.Add('Done.');
end;

procedure TMainForm.DoCycleRun(NewHz: Double; ThisRunLength: Int64);
var
  EndTime, dT: Int64;

begin
  StopButtonClick(nil);

  RunLength := RunLength * RunsKnown;
  RunLength := RunLength + ThisRunLength;
  Inc(RunsKnown);
  RunLength := Min(60 * TicksPerSec, RunLength / RunsKnown);

  NewHz := Min(1000000, NewHz);
  NewHz := 1/100 * Round(100 * NewHz);
  HzEdit.Text := FloatToStr(NewHz);

  QueryPerformanceCounter(EndTime);
  EndTime := EndTime + TicksPerSec;
  repeat
    Application.ProcessMessages;
    QueryPerformanceCounter(dT);
    dT := dT - EndTime;
  until dT >= 0;

  GoButtonClick(nil);
end;

procedure TMainForm.ProcessPause;
var
  EndTime, dT: Int64;

begin
  QueryPerformanceCounter(EndTime);
  EndTime := EndTime + TicksPerSec;

  repeat
    Application.ProcessMessages;
    QueryPerformanceCounter(dT);
    dT := dT - EndTime;
  until dT > 0;
end;

procedure TMainForm.AMMessage(var Msg: TMessage);
var
  pMsg: PString;
begin
  Msg.Result := ERROR_SUCCESS;
  pMsg := PString(Msg.LParam);

  // Ensure pMsg is not nil before accessing it
  if Assigned(pMsg) then
  begin
    try
      LogMemo.Lines.Add(pMsg^);
    finally
      Dispose(pMsg);
    end;
  end
  else
  begin
    LogMemo.Lines.Add('Received null message pointer');
  end;
end;

procedure TMainForm.AMDepth(var Msg: TMessage);
const
  StopDepth = 256;

begin
  Msg.Result := ERROR_SUCCESS;
  if Msg.LParam = 0 then
    DepthLabel.Caption := ''
  else
    DepthLabel.Caption := 'Backed Up ' + IntToStr(Msg.LParam)
  ;
  if (Msg.LParam >= StopDepth) and StopButton.Enabled then begin
    LogMemo.Lines.Add('Logging too backed up, risk of memory overflow. Halting.');
    LogMemo.Lines.Add('May need to use a smaller log format(raw is smallest), or log to a faster drive.');
    StopButton.Click;
  end;
end;

procedure TMainForm.DifferentialCheckClick(Sender: TObject);
var
  ChCt: TUInt32;

begin
  try
    ChCt := StrToInt(ChEdit.Text);
  except
    Exit;
  end;

  if DifferentialCheck.Checked then begin
    if ChCt > 8 then ChEdit.Text := '8';
  end
  else begin
    if ChCt = 8 then ChEdit.Text := '16';
  end;
end;

procedure TMainForm.DeviceComboChange(Sender: TObject);
begin
    Version := PIO_In32(iBoard, $68);
end;

end.
