unit IOIRMain;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  Forms, Dialogs, Buttons, StdCtrls, ComCtrls,
  JvHidControllerClass;

const
  cCodeMercenariesVID = $07C0;
  cIOWarriorPID1      = $1500;
  cIOWarriorPID2      = $1501;
  cIRCommand          = $0C;
  cIRReportID         = $0C;
  cIROff              = $00;
  cIROn               = $01;

  // translation of RC5-Address to device names
  AddressNames: array [0..31] of PChar =
   (
    'TV1',
    'TV2',
    'Teletext',
    'Video',
    'LV1',
    'VCR1',
    'VCR2',
    'Experimental',
    'Sat1',
    'Camera',
    'Sat2',
    '11',
    'CDV',
    'Camcorder',
    '14',
    '15',
    'Pre-amp1',
    'Tuner',
    'Recorder1',
    'Pre-amp2',
    'CD Player',
    'Phono',
    'SatA',
    'Recorder2',
    '24',
    '25',
    'CDR',
    '27',
    '28',
    'Lighting1',
    'Lighting2',
    'Phone'
   );

type
  // data structure received from IO-Warrior 24
  PIOWarriorIRInputReport = ^TIOWarriorIRInputReport;
  TIOWarriorIRInputReport = packed record
    Command: Byte;  // data part 0..63 of IR RC5 code
    Address: Byte;  // device ID part 0..31 of IR RC5 code
    Empty: array [1..5] of Byte;  // 5 bytes containing 0
  end;

  // data structure to send to IO-Warrior 24
  PIOWarriorIROutputReport = ^TIOWarriorIROutputReport;
  TIOWarriorIROutputReport = packed record
    ReportID: Byte;  // needed for output
    IOData: array [0..6] of Byte;  // 7 Bytes for IO-Warrior
  end;

  // RC5 data and assigned key combination
  PKeyData = ^TKeyData;
  TKeyData = record
    Command: Byte;        // decoded RC5-Command
    Address: Byte;        // decoded RC5-Address
    Toggle: Boolean;      // toggles with each RC5 keypress
    Key: Word;            // assigned key
    Shift: TShiftState;   // assigned modifier keys
    ExtendedKey: Boolean; // numpad key or not
  end;

  TMainForm = class(TForm)
    HidCtl: TJvHidDeviceController;
    IOWarriorDetected: TLabel;
    CodeList: TListView;
    Description: TLabel;
    Clear: TButton;
    procedure HidCtlDeviceChange(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure HidCtlDeviceData(HidDev: TJvHidDevice;
      ReportID: Byte; const Data: Pointer; Size: Word);
    procedure CodeListDblClick(Sender: TObject);
    procedure ClearClick(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  public
    IOWarrior: TJvHidDevice;
    procedure UpdateControls;
  end;

var
  MainForm: TMainForm;

implementation

uses
  KeyEdit;

{$R *.dfm}

// untangle the RC5 data

procedure DecodeRC5Data(const Report: TIOWarriorIRInputReport;
  var KeyData: TKeyData);
begin
  // only 6 bits meaningful
  KeyData.Command := Report.Command and $3F;
  // move the inverted /C6 bit from Address to Command
  KeyData.Command := KeyData.Command or
    ((not Report.Address) and $40);
  // only 5 bits are address
  KeyData.Address := Report.Address and $1F;
  KeyData.Toggle  := (Report.Address and $20) <> 0;
end;

// send the assigned key combination as if
// coming from keyboard

procedure SendKeycodes(KeyData: PKeyData);
var
  ExtKey: DWORD;
begin
  if KeyData.Key = 0 then
    Exit;

  // the modifier keys go down
  if ssShift in KeyData^.Shift then
    keybd_event(VK_SHIFT, 0, 0, 0);
  if ssCtrl in KeyData^.Shift then
    keybd_event(VK_CONTROL, 0, 0, 0);
  if ssAlt in KeyData^.Shift then
    keybd_event(VK_MENU, 0, 0, 0);

  // add the extended bit as flag
  if KeyData^.ExtendedKey then
    ExtKey := KEYEVENTF_EXTENDEDKEY
  else
    ExtKey := 0;
  // the key goes down and up
  keybd_event(KeyData^.Key, 0, ExtKey, 0);
  keybd_event(KeyData^.Key, 0, ExtKey or KEYEVENTF_KEYUP, 0);

  // the modifier keys go up (reverse order)
  if ssAlt in KeyData^.Shift then
    keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0);
  if ssCtrl in KeyData^.Shift then
    keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
  if ssShift in KeyData^.Shift then
    keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);
end;

procedure TMainForm.FormActivate(Sender: TObject);
begin
  // the first OnDeviceChange could not access
  // the controls so it is repeated here
  UpdateControls;
end;

procedure TMainForm.UpdateControls;
const
  cIOWMessage = 'IO-Warrior with IR is plugged in';
begin
  if Assigned(IOWarriorDetected) then
  begin
    Description.Enabled := Assigned(IOWarrior);
    if Assigned(IOWarrior) then
      IOWarriorDetected.Caption := cIOWMessage
    else
      IOWarriorDetected.Caption := 'No ' + cIOWMessage;
  end;
end;

// callback function to search for the IO-Warrior

function FindIOWarrior(HidDev: TJvHidDevice): Boolean; stdcall;
begin
  // the IO-Warrior shows up as two devices
  // we want access to the IO-Warrior device for the IR command
  // the other one with a InputReportByteLength of 2 is for
  // access to the IO pins
  Result :=
    (HidDev.Attributes.VendorID = cCodeMercenariesVID) and
    (HidDev.Attributes.ProductID = cIOWarriorPID2) and
    (HidDev.Caps.InputReportByteLength = 8);
end;

// check if the IO-Warrior was plugged or unplugged

procedure TMainForm.HidCtlDeviceChange(Sender: TObject);
var
  BytesWritten: Cardinal;
  IOWarriorOutputReport: TIOWarriorIROutputReport;
begin
  // Free the device object if it has been unplugged
  if Assigned(IOWarrior) and not IOWarrior.IsPluggedIn then
    FreeAndNil(IOWarrior);

  // if no IO-Warrior in use yet then search for one
  if not Assigned(IOWarrior) then
    if HidCtl.CheckOutByCallback(IOWarrior, FindIOWarrior) then
    begin
      // enable IR mode
      FillChar(IOWarriorOutputReport,
        SizeOf(IOWarriorOutputReport), #0);
      IOWarriorOutputReport.ReportID  := cIRCommand;
      IOWarriorOutputReport.IOData[0] := cIROn;
      IOWarrior.WriteFile(IOWarriorOutputReport,
        SizeOf(IOWarriorOutputReport), BytesWritten);
    end;
  UpdateControls;
end;

// IR data arrived from the device

procedure TMainForm.HidCtlDeviceData(HidDev: TJvHidDevice;
  ReportID: Byte; const Data: Pointer; Size: Word);
var
  I: Integer;
  KeyData: TKeyData;
  PData: PKeyData;
  FoundAt: Integer;
  Item: TListItem;
  IOWarriorInputReport: TIOWarriorIRInputReport;
begin
  IOWarriorInputReport := PIOWarriorIRInputReport(Data)^;
  if ReportID = cIRReportID then
  begin
    // extract data payload from report
    FillChar(KeyData, SizeOf(KeyData), #0);
    DecodeRC5Data(IOWarriorInputReport, KeyData);

    // search if RC5 data is already in list
    FoundAt := -1;
    for I := 0 to CodeList.Items.Count-1 do
    begin
      PData := PKeyData(CodeList.Items[I].Data);
      if (KeyData.Command = PData^.Command) and
        (KeyData.Address = PData^.Address) then
      begin
        FoundAt := I;
        Break;
      end;
    end;

    if FoundAt = -1 then
    begin
      // RC5 data not found so add it to list
      Item := CodeList.Items.Add;
      Item.Caption := AddressNames[KeyData.Address];
      Item.SubItems.Add(Format('%d', [KeyData.Command]));
      Item.SubItems.Add('--');  // no name yet
      Item.SubItems.Add('');    // no key assigned yet
      // store KeyData in Data of Item
      GetMem(PData, SizeOf(TKeyData));
      PData^ := KeyData;
      Item.Data := PData;
    end
    else
    begin
      // code found so send the assigned key combination
      Item := CodeList.Items[FoundAt];
      CodeList.ItemIndex := FoundAt;
      SendKeycodes(Item.Data);
    end;
  end;
end;

// edit an entry of the CodeList

procedure TMainForm.CodeListDblClick(Sender: TObject);
var
  Item: TListItem;
  PData: PKeyData;
begin
  // see if an item is selected
  Item := nil;
  if CodeList.ItemIndex >= 0 then
    Item := CodeList.Items[CodeList.ItemIndex];
  if Assigned(Item) then
    // create edit form
    with TKeyEditForm.Create(Self) do
    begin
      // initialize controls
      PData := Item.Data;
      KeyData := PData^;
      Address.Caption := Item.Caption;
      Value.Caption   := Item.SubItems[0];
      Name.Text       := Item.SubItems[1];
      Keys.Text       := Item.SubItems[2];
      ExtKey.Checked  := KeyData.ExtendedKey;
      if ShowModal = mrOk then
      begin
         // get back edited values
         Item.SubItems[1] := Name.Text;
         Item.SubItems[2] := Keys.Text;
         PData^ := KeyData;
      end;
      Free;
    end;
end;

// clear all assignments

procedure TMainForm.ClearClick(Sender: TObject);
var
  I: Integer;
begin
  CodeList.Items.BeginUpdate;
  for I := CodeList.Items.Count-1 downto 0 do
  begin
    FreeMem(CodeList.Items[I].Data);
    CodeList.Items.Delete(I);
  end;
  CodeList.Items.EndUpdate;
end;

// always clean up properly

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  ClearClick(Self);
end;

end.
