MELSOFT GX Works 2 / (3) で スマホ I/O チェック 2019/04/01

 ・2019/04/04 設定が保存されないのを手直し。尺度調整を1か所だけに変更。
 ・2019/04/01 初版作成

 MX Component、追加機器等 は不要です。
 GX Works 2, (3) がインストールされ、Bluetooth が使えるパソコンがあれば、 Android スマホで I/O チェックが出来ます。
 ※GX Works3 の場合 Q/L/FXシリーズ互換モードのみ使用可能です。今のところ FX5, RCPUには対応していません。
 ※GX Works2, (3) は画面上に見えている必要があります。 本ツールは画面上に見えていなくても動作します。
 ・対象デバイスは、X または Y のみ。FX: 80点、Q/L :160点 のモニタ、及び出力反転が可能です。
 ・状態が変化した最後のデバイス名を読み上げます。

 ■仕組み

 ・「デバイス/バッファメモリ一括モニタ」を表示させておき、そのウィンドウを 250 msec 周期でキャプチャします。
  その画像から、CPUタイプ、デバイス ON/OFF 状態を判断します。
  下 ↓ のスクリーンショットのマス内の赤い点。その位置の色が白であれば OFF、青であれば ON と判断。
  データ行の 1 行目の左から 9 番目のマスの色が、白または青でなければ、FXCPU (8 進数アドレス)と判断。
 ・Android スマホへは、Bluetooth 経由の COM ポートを使って、デバイスの状態を送信しています。
  あらかじめ、ペアリング後、PC 側に「Bluetooth リンク経由の標準シリアル」ポートの追加が必要です。
  方法については、「Bluetooth ペアリング」、「Bluetooth COMポート 追加」等でネットで検索してみて下さい。

 

 ■注意点

 ・正常に動くかどうかは、画面キャプチャの位置が合うかどうかによります。上 ↑ のスクリーンショットのようであれば、ほぼ使えます。
  環境によっては、微調整が必要になります。
 ・画面キャプチャのため、GX Works2 が最前面になり、操作がしにくくなります。作業できない場合は、キャプチャを「STOP」にしてください。
  「STOP」 するとスマホとの通信が出来なくなるため、スマホアプリの再起動が必要になります。
 ・なるべく存在しないアドレスを設定しないでください。(エラーダイアログは自動で消えるようになっています。)

 ■Android アプリ
 
 PC 側で、[START] ボタンをクリックし、画面キャプチャ、CPU 判断等 が出来ていることを確認してからアプリを起動してください。
 初回起動時は、エラーになります。一番上のコンボボックスから接続先のPC名を選択し、「保存」 をタップして PC 名を保存。再起動してください。
 PC と通信できない時は、PC 側で [STOP] をクリック、[START] をクリックしたあと、アプリを再度起動してみて下さい。

 ・セル(マス目)をタップすると、反転対象のデバイス番号が変わります。

 
 
 ■著作権、免責事項等
  
  本ツール、アプリの著作権は作者 f.izawa が所有し、これを主張します。
  本ツール、アプリをインストール、使用したことによる事故、損害等の一切について、作者はその責を負いません。

 ■作者連絡先

  e-mail : f.izawa@dream.com (@は小文字にしてください)
  URL : http://www.izawa-web.com/


 ■ダウンロード

  GXW2_IO.zip (Windows 側アプリ 本体のみ)
  ※高解像度環境にて作成しています。他の環境では、サイズ、配置が異なる可能性があります。
   設定タブにて、「全体の尺度」を 0.446 程度にすると、他の設定はそのままで使用できます。
   GX Works 2 (3 )にて、「デバイス/バッファメモリ一括モニタ」をのデータ行を10行以上表示させた状態で使ってください。

  GXW2_IO.apk (Android 側アプリ本体のみ)


// **********************************
// Windows 側
// **********************************

unit GXW2Unit4;unit GXW2Unit4;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls, Vcl.Buttons,
  OoMisc, AdPort, AdSelCom, AdPacket, IniFiles, Vcl.ComCtrls;
type
  // 16 点×10 行のデータを保持
  TBitAry = array [0..159] of ShortInt;
  TWordAry = array [0..9] of Word;

type
  TForm4 = class(TForm)
    PageControl1: TPageControl;
    TabSheet1: TTabSheet;
    TabSheet2: TTabSheet;
    Image1: TImage;
    SpeedButton1: TSpeedButton;
    SpeedButton2: TSpeedButton;
    SpeedButton3: TSpeedButton;
    SpeedButton4: TSpeedButton;
    SpeedButton5: TSpeedButton;
    SpeedButton6: TSpeedButton;
    SpeedButton7: TSpeedButton;
    SpeedButton8: TSpeedButton;
    SpeedButton9: TSpeedButton;
    SpeedButton10: TSpeedButton;
    Edit1: TEdit;
    Button3: TButton;
    Edit2: TEdit;
    Edit3: TEdit;
    Edit4: TEdit;
    Button4: TButton;
    Edit5: TEdit;
    CheckBox1: TCheckBox;
    ComboBox1: TComboBox;
    Timer1: TTimer;
    ApdComPort1: TApdComPort;
    ApdDataPacket1: TApdDataPacket;
    Label3: TLabel;
    Label4: TLabel;
    Button1: TButton;
    GroupBox1: TGroupBox;
    Label5: TLabel;
    Edit8: TEdit;
    Label6: TLabel;
    Edit9: TEdit;
    Label7: TLabel;
    Edit10: TEdit;
    Label8: TLabel;
    Edit11: TEdit;
    Label9: TLabel;
    Edit12: TEdit;
    Button2: TButton;
    Button5: TButton;
    Label1: TLabel;
    Edit6: TEdit;
    procedure Timer1Timer(Sender: TObject);
    procedure SpeedButton1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure SpeedButton2Click(Sender: TObject);
    procedure SpeedButton5Click(Sender: TObject);
    procedure SpeedButton7Click(Sender: TObject);
    procedure SpeedButton9Click(Sender: TObject);
    procedure SpeedButton10Click(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure CheckBox1Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure ApdDataPacket1StringPacket(Sender: TObject; Data: AnsiString);
    procedure Button2Click(Sender: TObject);
    procedure Button5Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form4: TForm4;


implementation

{$R *.dfm}

var
  // SpreadSheet ウィンドウ
  SpreadSheetHwnd : HWND;
  // SpreadSheet ウィンドウの左上座標
  shtCaptLeft, shtCaptTop : integer;
  // 座標補正の係数
  //shtXScale, shtYScale : double;
  // 列幅、行高
  shtColWidth, shtRowHeight : double;
  // マウスクリック、ピクセル取得の左上基点からのオフセット
  pickXOff, pickYOff : integer;
  // 先頭デバイス番号の Edit ウィンドウ
  DeviceEditHwnd : HWND;
  // 先頭デバイス番号の ComboBox ウィンドウ
  DeviceComboHwnd : HWND;
  // メインウィンドウ
  GXW2FrameHwnd : HWND;
  captL, captW, captH: integer;

  // 全体の尺度
  shtScale : double;

  // 比較用内部データ(ShortInt)
  // 今回値
  BitAryNew : TBitAry;
  // 前回値
  BitAryOld : TBitAry;

  // 常時監視の先頭デバイスが変わる判断
  devHeadOld : string;
  // 先頭デバイスが変わった時は比較を1回パス
  passFlag : boolean;
  // 先頭デバイスの変更が失敗した時に戻す
  devChgOld : string;

  // スマホへのデータ送信用
  WordAryNew : TWordAry;
  WordAryOld : TWordAry;


//******************************************
// n の k 乗 (Math ユニット不要)
//******************************************
function IntPower(n, k : integer):integer;
var
  i : integer;
begin
  result := 1;
  for i := 1 to k do result := result * n;
end;

//******************************************
// 8 進数表記 -> 整数
//******************************************
function OctToIntDef(const Value: string; Def :integer): integer;
var
  i, len, n : integer;
begin
  result := 0;
  len :=  Length(Value);
  for i := 1 to len do begin
    n := StrToIntDef(Value[i], -1);
    if (n >= 0 ) and (n < 8) then
      Inc(result, n * IntPower(8, len - i))
    else begin
      result := Def;
      break;
    end;
  end;
end;

//******************************************
// 整数 -> 8 進数表記
//******************************************
function IntToOct(Value: integer; digits: Integer): string;
var
  rest: Longint;
  oct: string;
  i: Integer;
begin
  oct := '';
  while Value <> 0 do begin
    rest  := Value mod 8;
    Value := Value div 8;
    oct := IntToStr(rest) + oct;
  end;
  if Length(oct) < digits then
    for i := Length(oct) + 1 to digits do oct := '0' + oct;
  result := oct;
end;
//****************************************
// 画面の指定位置をBitmapに変換
//****************************************
procedure CaptureToBmp(Lf, Tp, W, H: Integer; bmp: TBitmap);
const
  CAPTUREBLT = $40000000;
var
  hdcScreen : HDC;
begin
  bmp.Width := W;
  bmp.Height := H;
  hdcScreen := CreateDC('DISPLAY', nil, nil, nil);
  try
    BitBlt( bmp.Canvas.Handle, 0, 0, W, H, hdcScreen, Lf, Tp, SRCCOPY or CAPTUREBLT);
  finally
    DeleteDC(hdcScreen);
  end;
end;
//******************************************
// ウィンドウのタイトル(キャプション)を得る
//******************************************
function GetWindowCaption(h : HWND) : string;
var
  Title : array [0..255] of char;
begin
  result := '';
  if GetWindowText(h, Title, 255) <> 0 then
    result := Title;
end;
//******************************************
// 他のプロセス内のコントロールの文字列を得る
//******************************************
function GetWindowString(h : HWND) : string;
var
  p : PChar;
  len : LongInt;
begin
  result := '';
  //ウィンドウの文字列のバイト数を取得
  //終端のNULL文字を含まない文字列の長さ(バイト数)
  len := SendMessage(h, WM_GETTEXTLENGTH, 0, 0);

  if len > 0 then begin
    //終端のNULL文字を含むサイズを確保
    GetMem(p, (len + 1) * 2);
    //格納するバッファの最大サイズ(終端のNULL文字を含む長さ)
    //文字列バッファ
    SendMessage(h, WM_GETTEXT, (len+1)*2, LongInt(p));
    //文字列がバッファサイズより長いとき、後部がカットされる
    result := string(p);
    FreeMem(p);
  end;
end;
//******************************************
// クラス名取得
//******************************************
function GetHwndClassName(h : HWND):string;
var
  PC  : PChar;
  Len : Integer;
  Classname : string;
begin
  ClassName := '';
  if not IsWindow(h) then exit;
  GetMem(PC, 100);
  try
    Len := GetClassName(h, PC, 100);
    SetString(Classname, PC, Len);
  finally
    FreeMem(PC);
  end;
  result := Classname;
end;

// 先頭デバイスのコンボボックスを探す
function EnumCWinProc_DeviceCombo(h: HWND; lparam: Integer): Bool; stdcall;
var
  Title : array [0..255] of char;
begin
  result := true;
  if GetWindowText(h, Title, 255) <> 0 then begin
    if Pos('デバイス/バッファメモリ一括モニタ', Title) = 1 then begin
      DeviceComboHwnd := h;
      Result := False;
    end;
  end;
end;

// デバイス一括モニタのスプレッドシートを探す
function EnumCWinProc_SpreadSheet(h: HWND; lparam: Integer): Bool; stdcall;
var
  ClassName : string;
begin
  result := true;
  ClassName := GetHwndClassName(h);
  if 'SPR32AU70_SpreadSheet' = ClassName then begin
    SpreadSheetHwnd := h;
    Result := False;
  end;
end;

//******************************************
// Window に文字列を送る
//******************************************
function SendCharHwnd(h: HWND; const s: string):boolean;
var
  i : integer;
begin
  result := False;
  if h <> 0 then begin
    for i := 1 to Length(s) do
      SendMessage(h, WM_CHAR, Word(s[i]), 0);
    result := true;
  end;
end;

//******************************************
// Window に文字列を送る
//******************************************
function SendTextHwnd(h: HWND; const s : string):boolean;
begin
  result := False;
  if h <> 0 then begin
    SendMessage(h, WM_SETTEXT, 0, LPARAM(PChar(s)));
    result := true;
  end;
end;

procedure TForm4.ApdDataPacket1StringPacket(Sender: TObject; Data: AnsiString);
var
  cmd, res, s, s0, s1 : string;
  i, j, k0 : integer;
begin
  cmd := Trim(string(Data));
  //Memo1.Lines.Add(cmd);
  if cmd = 'CPU' then begin
    if CheckBox1.Checked then res := 'FX'
    else res := 'QL';
    ApdComPort1.PutString(res + #13#10);
  end
  else if cmd = 'READ' then begin
    s0 := ''; s1 := '';
    for i := 0 to 9 do begin
      k0 := 0;// k1 := 0;
      for j := 0 to 15 do begin
        if BitAryNew[i * 16 + j] = 1 then
          k0 := k0 + IntPower(2, j);
      end;
      WordAryNew[i] := k0;
      //k1 := WordAryOld[i];
      s0 := s0 + IntToHex(k0, 4);
      s1 := s1 + IntToHex(WordAryOld[i], 4);
    end;
    if CheckBox1.Checked then res := 'FX'
    else res := 'QR';
    res := res + Copy(Edit1.Text + '      ', 1, 6) ;  // 先頭デバイス
    res := res + s0 + s1;   // 合計 88 文字
    ApdComPort1.PutString(res + #13#10);
    WordAryOld := WordAryNew;
  end
  // ビット反転
  else if Pos('BTRV', cmd) = 1 then begin
    s := Copy(cmd, 6);
    Edit4.Text := s;
    Button4Click(self);
    ApdComPort1.PutString('OK' + #13#10);
  end
  // 先頭デバイス変更
  else if Pos('DEVN', cmd) = 1 then begin
    s := Copy(cmd, 6);
    Edit5.Text := s;
    Button1Click(self);
    ApdComPort1.PutString('OK' + #13#10);
  end
  else
    ApdComPort1.PutString('??' + #13#10);

end;

procedure TForm4.Button1Click(Sender: TObject);
// 先頭デバイス変更
var
  x, y : integer;
  pt, pt0 : TPoint;
  h : HWND;
  tmFlag : boolean;
begin
  tmFlag := Timer1.Enabled;
  if tmFlag then Timer1.Enabled := False;

  if IsWindowVisible(DeviceEditHwnd) then begin
    GetCursorPos(pt0);

    SendTextHwnd(DeviceEditHwnd, Trim(Edit5.Text));
    SetForegroundWindow(DeviceEditHwnd);
    // [Enter]
    keybd_event(VK_RETURN, 0, 0, 0);
    keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);
    // セル 0,0 に移動
    x := shtCaptLeft + Trunc(pickXOff * shtScale);
    y := shtCaptTop  + Trunc(shtRowHeight +  pickYOff * shtScale);
    SetCursorPos(x, y);

    pt.X := x; pt.Y := y;
    // マウス直下の Window を取得
    if WindowFromPoint(pt) =  SpreadSheetHwnd then begin
      // マウスクリックで Active に
      mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
      mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
    end;
    // カーソルを元に戻す
    SetCursorPos(pt0.X, pt0.Y);

    Sleep(200);
    // 変更できない時、ダイアログを探す
    h := FindWindow(nil, 'MELSOFTシリーズ GX Works2');
    if h = 0 then
      h := FindWindow(nil, 'MELSOFT GX Works3 Q/L/FXシリーズ互換モード');
    // ダイアログを閉じる
    if (h <> 0) and IsWindowVisible(h) then begin
      if '#32770' = GetHwndClassName(h) then begin
        SetForegroundWindow(h);
        keybd_event(VK_RETURN, 0, 0, 0);
        keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);
        Sleep(200);
        // 先頭番号を戻す
        if devChgOld <> '' then begin
          Edit5.Text := devChgOld;
        end
        else begin
          Edit5.Text := Copy(Edit5.Text, 1, 1) + '000';
          devChgOld := Edit5.Text;
        end;

        SendTextHwnd(DeviceEditHwnd, Trim(Edit5.Text));
        SetForegroundWindow(DeviceEditHwnd);
        // [Enter]
        keybd_event(VK_RETURN, 0, 0, 0);
        keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);
        Sleep(200);
        // セル 0,0 に移動
        x := shtCaptLeft + Trunc(pickXOff * shtScale);
        y := shtCaptTop  + Trunc(shtRowHeight +  pickYOff * shtScale);
        SetCursorPos(x, y);
        pt.X := x; pt.Y := y;
        // マウス直下の Window を取得
        if WindowFromPoint(pt) =  SpreadSheetHwnd then begin
          // マウスクリックで Active に
          mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
          mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
        end;
        // カーソル位置を戻す
        SetCursorPos(pt0.X, pt0.Y);
        Sleep(200);
      end;
    end;
    // ダイアログを閉じる
    h := FindWindow(nil, '現在値変更');
    if (h <> 0) and IsWindowVisible(h) and IsWindowEnabled(h) then begin
      if '#32770' = GetHwndClassName(h) then begin
        SetForegroundWindow(h);
        keybd_event(VK_ESCAPE, 0, 0, 0);
        keybd_event(VK_ESCAPE, 0, KEYEVENTF_KEYUP, 0);
        Sleep(200);
      end;
    end;
  end;
  if tmFlag then Timer1.Enabled := True;
end;

procedure TForm4.Button2Click(Sender: TObject);
begin
  shtScale := StrToFloatDef(Edit6.Text, 1);
  Edit6.Text := Format('%.4f', [shtScale]);

  //shtYScale := StrToFloatDef(Edit7.Text, 1);
  //Edit7.Text := Format('%.3f', [shtYScale]);

  captW := StrToIntDef(Edit8.Text, 504);
  Edit8.Text := IntToStr(captW);
  captH := StrToIntDef(Edit9.Text, 372);
  Edit9.Text := IntToStr(captH);
  captL := StrToIntDef(Edit10.Text, 243);
  Edit10.Text := IntToStr(captL);
  pickXOff := StrToIntDef(Edit11.Text, 10);
  Edit11.Text := IntToStr(pickXOff);
  pickYOff := StrToIntDef(Edit12.Text, 8);
  Edit12.Text := IntToStr(pickYOff);
end;

procedure TForm4.Button3Click(Sender: TObject);
var
  s : string;
begin
  if Button3.Caption = 'START' then begin
    Button3.Caption := 'STOP';
    with ApdComPort1 do begin
      s := Copy(ComboBox1.Text, 4);
      s := Copy(s, 1, Length(s) -1);
      ComNumber := StrToIntDef(s, 4);
      Baud := 9600;
      StopBits := 1;
      DataBits := 8;
      Parity := TParity.pNone;
      SWFlowOptions := TSWFlowOptions.swfNone;
    end;
    with  ApdDataPacket1 do begin
      Enabled := False;
      EndCond := [ecString];
      EndString := #13#10;
      StartCond := scAnyData;
      TimeOut := 500;
    end;
    try
      ApdComPort1.Open := True;
      if  ApdComPort1.Open then begin
        ApdDataPacket1.Enabled := True;
        ComboBox1.Enabled := False;
      end;
    except
      ShowMessage('ComPort Open Error');
    end;
    Timer1.Enabled := True;
  end
  else begin
    Timer1.Enabled := False;
    Button3.Caption := 'START';
    if ApdComPort1.Open then begin
    ApdComPort1.Open := False;
    ComboBox1.Enabled := True;
  end;
  end;
end;

procedure TForm4.Button4Click(Sender: TObject);
// デバイス ON/OFF 反転
var
  h, k, n, m : integer;
  x, y : integer;
  pt, pt0 : TPoint;
  hnd : HWND;
  tmFlag : boolean;
begin
  tmFlag := Timer1.Enabled;
  if tmFlag then Timer1.Enabled := False;

  x := 0; y := 0;
  GetCursorPos(pt0);

  if CheckBox1.Checked then begin
    // 先頭デバイス
    h := OctToIntDef(Copy(Edit1.Text, 2), 0);
    // 反転対象のデバイス
    k := OctToIntDef(Copy(Edit4.Text, 2), -1);
    if k >= 0 then begin
      k := k - h;
      n := k div 8;
      m := k mod 8;
      if BitAryNew[n * 16 + m] >= 0 then begin
        x := shtCaptLeft + Trunc((7 - m) * shtColWidth  + pickXOff * shtScale);
        y := shtCaptTop  + Trunc((n + 1) * shtRowHeight + pickYOff * shtScale);
      end;
    end;
  end
  else begin
    // 先頭デバイス
    h := StrToIntDef('$' + Copy(Edit1.Text, 2), 0);
    // 反転対象のデバイス
    k := StrToIntDef('$' + Copy(Edit4.Text, 2), -1);
    if k >= 0 then begin
      k := k - h;
      n := k div 16;
      m := k mod 16;
      if BitAryNew[n * 16 + m] >= 0 then begin
        x := shtCaptLeft + Trunc((15 - m) * shtColWidth  + pickXOff * shtScale);
        y := shtCaptTop  + Trunc((n + 1)  * shtRowHeight + pickYOff * shtScale);
      end;
    end;
  end;
  if (x > 0) and (y > 0) then begin
    SetCursorPos(x, y);
    pt.X := x; pt.Y := y;
    // マウス直下の Window を取得
    if WindowFromPoint(pt) =  SpreadSheetHwnd then begin
      // マウスクリックで Active に
      mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
      mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
      Sleep(100);
      // [Shift] + [Enter] キーで反転
      keybd_event(VK_SHIFT, 0, 0, 0);
      keybd_event(VK_RETURN, 0, 0, 0);
      keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);
      keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);
    end;
    Sleep(200);
    hnd := FindWindow(nil, '現在値変更');
    if (hnd <> 0) and IsWindowVisible(hnd) and IsWindowEnabled(hnd) then begin
      if '#32770' = GetHwndClassName(hnd) then begin
        SetForegroundWindow(hnd);
        keybd_event(VK_ESCAPE, 0, 0, 0);
        keybd_event(VK_ESCAPE, 0, KEYEVENTF_KEYUP, 0);
        Sleep(200);
      end;
    end;
  end;
  // マウス位置を戻す
  SetCursorPos(pt0.X, pt0.Y);
  if tmFlag then Timer1.Enabled := True;
end;

procedure TForm4.Button5Click(Sender: TObject);
begin
  Edit6.Text := '1.0000';
  //Edit7.Text := '1.000';
  Edit8.Text := '504';
  Edit9.Text := '372';
  Edit10.Text := '243';
  Edit11.Text := '10';
  Edit12.Text := '8';
end;

procedure TForm4.CheckBox1Click(Sender: TObject);
begin
  if CheckBox1.Checked then begin
    SpeedButton5.Caption := '+40';
    SpeedButton7.Caption := '-40';
  end
  else begin
    SpeedButton5.Caption := '+80';
    SpeedButton7.Caption := '-80';
  end;
end;

procedure TForm4.FormCreate(Sender: TObject);
var
  i : integer;
  ini : TIniFile;
begin
  shtScale := 1.0;
  //shtYScale := 1.0;
  // デバイス SpreadSheet のセルをアクティブにする左上からオフセット
  // ピクセルの色取得の位置
  pickXOff := 8;
  pickYOff := 6;
  captL := 243;
  captW := 504;
  captH := 372;

  // 使用可能な COM ポートを列挙
  AdSelCom.ShowPortsInUse := False;
  for i := 0 to 32 do begin
    if AdSelCom.IsPortAvailable(i) then
      ComboBox1.Items.Add (AdPort.ComName(i) + '.');
  end;
  ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  try
    Left := ini.ReadInteger('Form', 'Left', (Screen.Width - Width) div 2);
    Top  := ini.ReadInteger('Form', 'Top' , (Screen.Height - Height) div 2);
    i := ini.ReadInteger('COM', 'PortIndex', 0);
    if ComboBox1.Items.Count > i then  ComboBox1.ItemIndex := i;
    shtScale := ini.ReadFloat('Capt', 'Scale', 1);
    //shtYScale := ini.ReadFloat('Capt', 'YScale', 1);
    captW := ini.ReadInteger('Capt', 'captW', captW);
    captH := ini.ReadInteger('Capt', 'captH', captH);
    captL := ini.ReadInteger('Capt', 'captL', captL);
    pickXOff := ini.ReadInteger('Capt', 'pickXOff', pickXOff);
    pickYOff := ini.ReadInteger('Capt', 'pickYOff', pickYOff);
    Edit6.Text := Format('%.4f', [shtScale]);
    //Edit7.Text := Format('%.3f', [shtYScale]);
    Edit8.Text := IntToStr(captW);
    Edit9.Text := IntToStr(captH);
    Edit10.Text := IntToStr(captL);
    Edit11.Text := IntToStr(pickXOff);
    Edit12.Text := IntToStr(pickYOff);
  finally
    ini.Free;
  end;
  ApdDataPacket1.Enabled := False;

end;

procedure TForm4.FormDestroy(Sender: TObject);
var
  ini: TIniFile;
begin
  if ApdComPort1.Open then ApdComPort1.Open := False;
  ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  try
    ini.WriteInteger('Form', 'Left', Left);
    ini.WriteInteger('Form', 'Top' , Top);
    ini.WriteInteger('COM', 'PortIndex', ComboBox1.ItemIndex);
    ini.WriteFloat('Capt', 'Scale', shtScale);
    //ini.WriteFloat('Capt', 'YScale', shtYScale);
    ini.WriteInteger('Capt', 'captW', captW);
    ini.WriteInteger('Capt', 'captH', captH);
    ini.WriteInteger('Capt', 'captL', captL);
    ini.WriteInteger('Capt', 'pickXOff', pickXOff);
    ini.WriteInteger('Capt', 'pickYOff', pickYOff);
  finally
    ini.Free;
  end;
end;

procedure TForm4.SpeedButton10Click(Sender: TObject);
// デバイス変更
begin
  if Uppercase(Copy(Edit5.Text, 1, 1)) <> 'Y' then begin
    Edit5.Text := 'Y' + Copy(Edit5.Text, 2);
    Button1Click(self);
  end;
end;

procedure TForm4.SpeedButton1Click(Sender: TObject);
// [+10],[+1]
var
  m, n, h, i, j : integer;
begin
  if CheckBox1.Checked then begin
    // 先頭デバイス(基準)
    n := OctToIntDef(Copy(Edit1.Text, 2), 0);
    // 現在の番号
    m := OctToIntDef(Copy(Edit4.Text, 2), 0);
    if Sender as TSpeedButton = SpeedButton1 then Inc(m)
    else m := m + 8;
    if  m - n < 80 then begin
      h := m - n;
      i := h div 8;
      j := h mod 8;
      if BitAryNew[i * 16 + j] >= 0 then
        Edit4.Text := Copy(Edit4.Text, 1, 1) + IntToOct(m, 3);
    end;
  end
  else begin
    n := StrToIntDef('$' + Copy(Edit1.Text, 2), 0);
    m := StrToIntDef('$' + Copy(Edit4.Text, 2), 0);
    if Sender as TSpeedButton = SpeedButton1 then Inc(m)
    else m := m + 16;
    if  m - n < 160 then begin
      h := m - n;
      i := h div 16;
      j := h mod 16;
      if BitAryNew[i * 16 + j] >= 0 then
        Edit4.Text := Copy(Edit4.Text, 1, 1) + IntToHex(m, 3);
    end;
  end;
end;

procedure TForm4.SpeedButton2Click(Sender: TObject);
// [-10],[-1]
var
  n, m : integer;
begin
  if CheckBox1.Checked then begin
    n := OctToIntDef(Copy(Edit1.Text, 2), 0);
    m := OctToIntDef(Copy(Edit4.Text, 2), 0);
    if Sender as TSpeedButton = SpeedButton2 then Dec(m)
    else m := m - 8;
    if  m - n >= 0 then
      Edit4.Text := Copy(Edit4.Text, 1, 1) + IntToOct(m, 3);
  end
  else begin
    n := StrToIntDef('$' + Copy(Edit1.Text, 2), 0);
    m := StrToIntDef('$' + Copy(Edit4.Text, 2), 0);
    if Sender as TSpeedButton = SpeedButton2 then Dec(m)
    else m := m - 16;
    if  m - n >= 0 then
      Edit4.Text := Copy(Edit4.Text, 1, 1) + IntToHex(m, 3);
  end;
end;

procedure TForm4.SpeedButton5Click(Sender: TObject);
// 先頭アドレス変更 [+80],[+100]
var
  m : integer;
begin
  devChgOld := Edit5.Text;

  if CheckBox1.Checked then begin
    m := OctToIntDef(Copy(Edit5.Text, 2), 0);
    if Sender as TSpeedButton = SpeedButton5 then
      m := m + 32
    else
      m := m + 64;
    Edit5.Text := Copy(Edit5.Text, 1, 1) + IntToOct(m, 1);
    Button1Click(self);
  end
  else begin
    m := StrToIntDef('$' + Copy(Edit5.Text, 2), 0);
    if Sender as TSpeedButton = SpeedButton5 then
      m := m + $80
    else
      m := m + $100;
    Edit5.Text := Copy(Edit5.Text, 1, 1) + IntToHex(m, 1);
    Button1Click(self);
  end;
end;

procedure TForm4.SpeedButton7Click(Sender: TObject);
// 先頭アドレス変更 [-80],[-100]
var
  m : integer;
begin
  if CheckBox1.Checked then begin
    m := OctToIntDef(Copy(Edit5.Text, 2), 0);
    if Sender as TSpeedButton = SpeedButton7 then
      m := m - 32
    else
      m := m - 64;
    if  m >= 0 then begin
      Edit5.Text := Copy(Edit5.Text, 1, 1) + IntToOct(m, 1);
      Button1Click(self);
    end;
  end
  else begin
    m := StrToIntDef('$' + Copy(Edit5.Text, 2), 0);
    if Sender as TSpeedButton = SpeedButton7 then
      m := m - $80
    else
      m := m - $100;
    if  m >= 0 then begin
      Edit5.Text := Copy(Edit5.Text, 1, 1) + IntToHex(m, 1);
      Button1Click(self);
    end;
  end;
end;

procedure TForm4.SpeedButton9Click(Sender: TObject);
// デバイス変更
begin
  if Uppercase(Copy(Edit5.Text, 1, 1)) <> 'X' then begin
    Edit5.Text := 'X' + Copy(Edit5.Text, 2);
    Button1Click(self);
  end;
end;

procedure TForm4.Timer1Timer(Sender: TObject);
var
  h : HWND;
  i, j, n : integer;
  ARect : TRect;
  bmp : TBitmap;
  Pnt : PByteArray;
  R, G, B : Byte;
  x, y : integer;
  pt : TPoint;
  hBound : integer;
begin
  // メインウィンドウを探す
  GXW2FrameHwnd := FindWindow('GXW2FrameWnd', nil);

  if IsWindowVisible(GXW2FrameHwnd) and not IsIconic(GXW2FrameHwnd) then begin
    // メインウィンドウのキャプション
    //Edit6.Text := GetWindowCaption(GXW2FrameHwnd);
    // 「デバイス/バッファメモリ一括モニタ」Window を探す
    EnumChildWindows(GXW2FrameHwnd, @EnumCwinProc_DeviceCombo, 0);
    h := GetWindow(DeviceComboHwnd, GW_CHILD);
    h := GetWindow(h, GW_CHILD);
    for i := 0 to 2 do
      h := GetWindow(h, GW_HWNDNEXT);
    DeviceEditHwnd := GetWindow(h, GW_CHILD);

    // デバイス先頭
    Edit1.Text := GetWindowString(DeviceEditHwnd);
    if Edit1.Text = '' then begin
      SendTextHwnd(DeviceEditHwnd, 'X000');
      SetForegroundWindow(DeviceEditHwnd);
      // [Enter]
      keybd_event(VK_RETURN, 0, 0, 0);
      keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);
      Edit1.Text := GetWindowString(DeviceEditHwnd);
    end;
    if Edit4.Text = '' then Edit4.Text := Edit1.Text;
    if Edit5.Text = '' then Edit5.Text := Edit1.Text;

    // 先頭デバイスが変わったときは、1回パス
    if devHeadOld <> Edit1.Text then begin
      passFlag := True;
      devHeadOld := Edit1.Text;
      // 反転デバイスを変更
      Edit4.Text := Edit1.Text;
      // 先頭デバイスのアドレスを変更
      Edit5.Text := Edit1.Text;
    end
    else
      passFlag := False;

    // クラス名「SPR32AU70_SpreadSheet」 を探す
    EnumChildWindows(GXW2FrameHwnd, @EnumCwinProc_SpreadSheet, 0);
    GetWindowRect(SpreadSheetHwnd, ARect);

    // 左上座標を保持
    shtCaptLeft := Trunc(ARect.Left + captL * shtScale);
    shtCaptTop  := ARect.Top;
    shtColWidth := (captW * shtScale) / 16 ;  // 16 列
    shtRowHeight := (captH * shtScale) / 11 ; // 11 行 (タイトルを含む行数)

    // アクティブにする
    pt.X := shtCaptLeft; pt.Y := shtCaptTop;
    if WindowFromPoint(pt) <> SpreadSheetHwnd then begin
      SetForegroundWindow(SpreadSheetHwnd);
      Sleep(100);
    end;
    if WindowFromPoint(pt) = SpreadSheetHwnd then begin
      // キャプチャ
      bmp := TBitmap.Create;
      try
        bmp.PixelFormat:= TPixelFormat.pf24bit; // 重要
        CaptureToBmp(shtCaptLeft, shtCaptTop, Trunc(shtColWidth*16), Trunc(shtRowHeight * 11), bmp);
        Image1.Picture.Assign(bmp);
      finally
        bmp.Free;
      end;
      // ON/OFF をセルの色でチェック
      for i := 0 to 9 do begin
        y := Trunc(shtRowHeight * (i+1) + pickYOff * shtScale);
        Pnt := Image1.Picture.Bitmap.ScanLine[y];
        // FXCPU であるか1行目で判断
        if i = 0 then begin
          // 9 個めのセルの色で判断
          x := Trunc(shtColWidth * 8 + pickXOff * shtScale);
          R := Pnt[x * 3 + 2]; G := Pnt[x * 3 + 1]; B := Pnt[x * 3];
          // 白または青色
          if ((R = 0) and (G = 0) and (B = $FF)) or ((R = $FF) and (G = $FF) and (B = $FF)) then begin
            if CheckBox1.Checked then begin
              CheckBox1.Checked := False;
              CheckBox1Click(self);
            end;
          end
          else begin
            // FXCPU である
            if not CheckBox1.Checked then begin
              CheckBox1.Checked := True;
              CheckBox1Click(self);
            end;
          end;
        end;

        if CheckBox1.Checked then hBound := 7
        else hBound := 15;
        for j := 0 to hBound do begin
          x := Trunc(shtColWidth * j + pickXOff *  shtScale);
          R := Pnt[x * 3 + 2]; G := Pnt[x * 3 + 1]; B := Pnt[x * 3];
          if (R = 0) and (G = 0) and (B = $FF) then // 青
            BitAryNew[i * 16 + hBound - j] := 1
          else
          if (R = $FF) and (G = $FF) and (B = $FF) then // 白
            BitAryNew[i * 16 + hBound - j] := 0
          else // その他(グレイ)
            BitAryNew[i * 16 + hBound - j] := -1;
          with Image1.Picture.Bitmap.Canvas do begin
            Pen.Color := clRed;
            Brush.Style := bsSolid;
            Brush.Color := clRed;
            Ellipse(x-2, y-2, x + 2,y + 2);
          end;
        end;
      end;
      if not passFlag then begin
        // 結果表示
        for i:= 0 to 159 do begin
          if (BitAryNew[i] >= 0) and (BitAryOld[i] >= 0)  and (BitAryNew[i] <> BitAryOld[i]) then begin
            if CheckBox1.Checked then begin
              n := OctToIntDef(Copy(Edit1.Text, 2), 0);
              Edit2.Text := Copy(Edit1.Text, 1, 1) + IntToOct(n + (i div 16) * 8 + i mod 16, 3);
            end
            else begin
              n := StrToIntDef('$'+ Copy(Edit1.Text, 2), 0);
              Edit2.Text := Copy(Edit1.Text, 1, 1) + IntToHex(n + (i div 16) * 16 + i mod 16, 3);
            end;
            if BitAryNew[i] = 1 then Edit3.Text := 'ON'
            else Edit3.Text := 'OFF';
          end;
        end;
      end
      else begin
        Edit2.Text := '';
        Edit3.Text := '';
        for i := 0 to 159 do BitAryNew[i] := 0;
      end;
      // 前回値を更新
      BitAryOld := BitAryNew;
    end;
  end;
end;

end.

// **********************************
// Android 側
// **********************************

unit Unit4;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  System.Bluetooth, System.Bluetooth.Components, FMX.ScrollBox, FMX.Memo,
  FMX.Controls.Presentation, FMX.StdCtrls, FMX.Layouts, FMX.Edit, System.Rtti,
  FMX.Grid.Style, FMX.Grid,{ Math,} FMX.Objects, System.UIConsts, FMX.ListBox,
  System.IOUtils, System.IniFiles,
  // for TTS
  Androidapi.JNI.TTS,AndroidAPI.JNIBridge,
  // for ToneGenerator;
  AndroidApi.JNI.Media;
type
  TBitAry = array [0..159] of Boolean;
type
  TBtThread = class(TThread)
  private
    { Private 宣言 }
    procedure BtOpen;

  protected
    procedure Execute; override;
  public
    constructor Create; virtual;
  end;
type
  TForm4 = class(TForm)
    ScaledLayout1: TScaledLayout;
    Bluetooth1: TBluetooth;
    Button6: TButton;
    Timer1: TTimer;
    StringGrid1: TStringGrid;
    StringColumn1: TStringColumn;
    StringColumn2: TStringColumn;
    StringColumn3: TStringColumn;
    StringColumn4: TStringColumn;
    StringColumn5: TStringColumn;
    StringColumn6: TStringColumn;
    StringColumn7: TStringColumn;
    StringColumn8: TStringColumn;
    StringColumn9: TStringColumn;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Rectangle1: TRectangle;
    Label4: TLabel;
    Label5: TLabel;
    Rectangle2: TRectangle;
    Rectangle3: TRectangle;
    Rectangle4: TRectangle;
    Rectangle5: TRectangle;
    ComboBox1: TComboBox;
    ComboBox2: TComboBox;
    Label7: TLabel;
    StringColumn10: TStringColumn;
    StringColumn11: TStringColumn;
    StringColumn12: TStringColumn;
    StringColumn13: TStringColumn;
    StringColumn14: TStringColumn;
    StringColumn15: TStringColumn;
    StringColumn16: TStringColumn;
    StringColumn17: TStringColumn;
    CheckBox1: TCheckBox;
    ComboBox3: TComboBox;
    Button2: TButton;
    Switch1: TSwitch;
    procedure Button6Click(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Rectangle1Click(Sender: TObject);
    procedure Rectangle2Click(Sender: TObject);
    procedure StringGrid1DrawColumnCell(Sender: TObject; const Canvas: TCanvas;
      const Column: TColumn; const Bounds: TRectF; const Row: Integer;
      const Value: TValue; const State: TGridDrawStates);
    procedure StringGrid1DrawColumnHeader(Sender: TObject;
      const Canvas: TCanvas; const Column: TColumn; const Bounds: TRectF);
    procedure ComboBox1Change(Sender: TObject);
    procedure StringGrid1CellClick(const Column: TColumn; const Row: Integer);
    procedure CheckBox1Change(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button2Click(Sender: TObject);
   // TTS
    type
      TttsOnInitListener = class(TJavaLocal, JTextToSpeech_OnInitListener)
      private
        [weak] FParent : TForm4;
      public
        constructor Create(AParent : TForm4);
        procedure onInit(status: Integer); cdecl;
      end;
  private
    { private 宣言 }
    ttsListener : TttsOnInitListener;
    tts : JTextToSpeech;
    procedure SpeakOut(const s :string);
    procedure InitTTS;
  public
    { public 宣言 }
    BitAryOld : TBitAry;
    BitAryNew : TBitAry;
    GB_DeviceName : string;
    GB_DeviceStartIndex : integer;
    GB_fxFlag : boolean;
    //GB_Busy : boolean;

    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;
  end;

var
  Form4: TForm4;

  ADevice : TBluetoothDevice;
  ASocket : TBluetoothSocket;

  GThdMode : integer;
  GCmdMode : integer;

  ThBt : TBtThread;
  OpenNGcnt : integer;
  OpenMsecCnt : integer;
  Counter : integer;
  BtDeviceHead : string;

  ToneGenerator: JToneGenerator;
const
  // SPP(Serial Port Profile) による通信のUUID
  ServiceUUID = '{00001101-0000-1000-8000-00805F9B34FB}';

  thdTHSTART   = 1000;
  thdTHTERM    = 2000;
  cmdSCCREATE  = 200;
  cmdSCCONNECT = 201;
  cmdSCNG = 202;

implementation

uses Androidapi.JNI.JavaTypes, FMX.Helpers.Android
{$IF CompilerVersion >= 27.0}
, Androidapi.Helpers
{$ENDIF}
;

{$R *.fmx}

// n の k 乗 (Math ユニット不要)
function IntPower(n, k : integer):integer;
var
  i : integer;
begin
  result := 1;
  for i := 1 to k do result := result * n;
end;

// 8 進数表記 -> 整数
function OctToIntDef(const Value: string; Def :integer): integer;
var
  i, len, n : integer;
begin
  result := 0;
  len :=  Length(Value);
  for i := 1 to len do begin
    n := StrToIntDef(Copy(Value, i, 1), -1);
    if (n >= 0 ) and (n < 8) then
      Inc(result, n * IntPower(8, len - i))
    else begin
      result := Def;
      break;
    end;
  end;
end;

// 整数 -> 8 進数表記
function IntToOct(Value: integer; digits: Integer): string;
var
  rest: Longint;
  oct: string;
  i: Integer;
begin
  oct := '';
  while Value <> 0 do begin
    rest  := Value mod 8;
    Value := Value div 8;
    oct := IntToStr(rest) + oct;
  end;
  if Length(oct) < digits then
    for i := Length(oct) + 1 to digits do oct := '0' + oct;
  result := oct;
end;

// -----------------------------------------------------------------------------
// Bluetooth を Open し、接続する
procedure TBtThread.BtOpen;
var
  ABluetoothManager : TBluetoothManager;
  APairedDevices : TBluetoothDeviceList;
  ADevice : TBluetoothDevice;
  idx, i : integer;
begin
  GThdMODE := thdTHSTART;
  try
    try
      ABluetoothManager := TBluetoothManager.Current;
      if ABluetoothManager.ConnectionState = TBluetoothConnectionState.Connected then begin
        // PC名
        //Synchronize(procedure() begin
        //    Form4.Label6.Text :=
        //      '[' + ABluetoothManager.CurrentAdapter.AdapterName + ']'
        //end);
        // 過去にペアリングされたデバイスの一覧から、ターゲット を探す
        APairedDevices := ABluetoothManager.GetPairedDevices;
        if APairedDevices.Count > 0 then begin
          idx := -1;
          for i := 0 to APairedDevices.Count -1 do begin
            Synchronize(procedure() begin
                with Form4.ComboBox3 do begin
                  BeginUpdate;
                  Items.Add(APairedDevices[i].DeviceName );
                  EndUpdate;
                end;
            end);
            if (BTDeviceHead = APairedDevices[i].DeviceName) then begin
              Synchronize(procedure() begin
                  with Form4.ComboBox3 do begin
                    ItemIndex := i;
                  end;
              end);
              idx := i;
              //break;
            end;
          end;
          if idx >= 0 then begin
            ADevice := APairedDevices[idx];
            if ADevice <> nil then begin
              ASocket := ADevice.CreateClientSocket(StringToGUID(ServiceUUID), False);
              if ASocket <> nil then begin
                GCMDMODE := cmdSCCREATE;
                // 接続
                ASocket.Connect;
                if ASocket.Connected then begin
                  Sleep(250);
                  GCMDMODE := cmdSCCONNECT;
                end;
              end;
            end;
          end;
        end;
      end;
    except
      on E : Exception do begin
        GCMDMODE := cmdSCNG;
      end;
    end;
  finally
    // 明示的にスレッドを終了(破棄される)
    // スレッド実行中にアプリを終了した時エラーになるため
    Terminate;
    WaitFor;
    FreeAndNil(ThBt);
    GThdMODE := thdTHTERM;
  end;
end;

constructor TBtThread.Create;
begin
  // スレッドを生成、直ちに実行
  inherited Create(False);
  // スレッド終了時、スレッドオブジェクトを破棄
  FreeOnTerminate := True;
end;

procedure TBtThread.Execute;
begin
  BtOpen;
end;

// -----------------------------------------------------------------------------
procedure TForm4.InitTTS;
begin
  tts := TJTextToSpeech.JavaClass.init(TAndroidHelper.Context, ttsListener);
end;
procedure TForm4.SpeakOut(const s : string);
var
  text : JString;
begin
  text := StringToJString(s);
  tts.speak(text, TJTextToSpeech.JavaClass.QUEUE_FLUSH, nil);
end;

{ TForm4.TttsOnInitListener }
constructor TForm4.TttsOnInitListener.Create(AParent: TForm4);
begin
  inherited Create;
  FParent := AParent
end;

procedure TForm4.TttsOnInitListener.onInit(status: Integer);
var
  Result : Integer;
begin
  if (status = TJTextToSpeech.JavaClass.SUCCESS) then
  begin
   //result := FParent.tts.setLanguage(TJLocale.JavaClass.US);
   result := FParent.tts.setLanguage(TJLocale.JavaClass.JAPAN);
   if (result = TJTextToSpeech.JavaClass.LANG_MISSING_DATA) or
      (result = TJTextToSpeech.JavaClass.LANG_NOT_SUPPORTED) then
     ShowMessage('This Language is not supported');
  end
  else
    ShowMessage('Initilization Failed!');
end;
constructor TForm4.Create(AOwner: TComponent);
begin
  inherited;
  ttsListener := TttsOnInitListener.Create(self);
end;

destructor TForm4.Destroy;
begin
  if Assigned(tts) then begin
    tts.stop;
    tts.shutdown;
    tts := nil;
  end;
end;
// -----------------------------------------------------------------------------

function ASocketReceiveData(ASocket: TBluetoothSocket; ATimeout: Cardinal): string;
var
  AData : TBytes;
  ReadData : TBytes;
  i : integer;
  res : string;
  Ticks : Cardinal;
  idx : integer;
  loop : boolean;
  cnt : integer;
begin
  res := '';
  cnt := 0;
  SetLength(ReadData, 1024);
  idx := 0;
  Ticks := TThread.GetTickCount;
  loop := True;
  while loop and (cnt < 500) do begin
    Sleep(1);
    AData := ASocket.ReceiveData;
    if Length(AData) > 0 then begin
      for i := 0 to Length(AData) - 1 do begin
        ReadData[idx] := AData[i];
        Inc(idx);
        if (AData[i] = $0A) or (idx >= 1024) then begin
          loop := False;
          break;
        end;
      end;
    end;
    Inc(cnt);
    if loop then
      loop := TThread.GetTickCount - Ticks < ATimeout;
  end;
  SetLength(ReadData, idx);
  res := TEncoding.ANSI.GetString(ReadData);
  result := Trim(res); // 制御コードを含まない
end;

procedure TForm4.Button2Click(Sender: TObject);
// 接続先保存
var
  IniFile: TMemIniFile;
begin
  IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine(
    System.IOUtils.TPath.GetDocumentsPath, 'GXW2_IO.ini'), TEncoding.UTF8);
  with IniFile do begin
    try
      with ComboBox3 do begin
        if ItemIndex >= 0 then begin
          WriteString('Target', 'PCName', Items[ItemIndex]);
          ShowMessage('接続先: ' + Items[ItemIndex] + 'を保存しました.' + #13#10 +
            '次回起動時から有効になります.' + #13#10 + 'このアプリを再起動して下さい.');
        end
        else
          ShowMessage('接続先が選択されていません.');
      end;
      IniFile.UpdateFile;
    finally
      Free;
    end;
  end;
end;

procedure TForm4.Button6Click(Sender: TObject);
// デバイスの値を反転
var
  AData : TBytes;
  res : string;
  ATimeout: Cardinal;
begin
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    ATimeout := 250;
    AData := TEncoding.ANSI.GetBytes('BTRV ' + Label3.Text + #13#10);
    // 送信
    ASocket.SendData(AData);
    // 受信
    res := ASocketReceiveData(ASocket, ATimeout);

    with Label3.TextSettings do begin
      if res = 'ON' then FontColor := TAlphaColorRec.Red
      else if res = 'OFF' then FontColor := TAlphaColorRec.Lime
      else FontColor := TAlphaColorRec.White;
    end;
    // ブザー
    if Switch1.IsChecked then begin
      if (res = 'ON') or (res = 'OFF') or (res = 'OK') then
        ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
      else
        ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
    end;
    Timer1.Enabled := True;
  end;
end;

procedure TForm4.CheckBox1Change(Sender: TObject);
var
  i :integer;
begin
  GB_fxFlag := CheckBox1.IsChecked;

  // 初期に戻す
  with StringGrid1 do begin
    if not GB_fxFlag then
      for i := 0 to 9 do Cells[0, i] := (i * 16).ToHexString(3)
    else
      for i := 0 to 9 do Cells[0, i] := IntToOct(i * 8, 3);
    Row := 0;
    Col := 1;
  end;

  ComboBox1.OnChange := nil;
  ComboBox2.OnChange := nil;
  with ComboBox2 do begin
    BeginUpdate;
    Items.Clear;
    if not GB_fxFlag then
      for i := 0 to 255 do Items.Add(IntToHex(i * $80, 3))
    else
      for i := 0 to 255 do Items.Add(IntToOct(i * 32, 3));
    EndUpdate;
    ItemIndex := 0;
  end;
  // X に戻す
  with ComboBox1 do begin
    ItemIndex := 0;
  end;
  ComboBox1.OnChange := ComboBox1Change;
  ComboBox2.OnChange := ComboBox1Change;
  Label3.Text := 'X000';
end;

procedure TForm4.ComboBox1Change(Sender: TObject);
var
  AData : TBytes;
  s2, s1, res : string;
  ATimeout: Cardinal;
  i : integer;
begin
  // ここでは、StringGrid のデバイス番号を変更しない
  // PC 側へ先頭アドレスを送信するだけ
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    // 初期化
    Label1.Text := '';
    Label2.Text := '';
    for i := 0 to 159 do BitAryNew[i] := False;
    BitAryOld := BitAryNew;

    // PC の値を変更
    ATimeout := 250;
    // デバイス名
    with ComboBox1 do begin
      if ItemIndex < 0 then ItemIndex := 0;
      s1 := ListBox.Items[ItemIndex];
    end;
    with ComboBox2 do begin
      if ItemIndex < 0 then ItemIndex := 0;
      if ItemIndex < 0 then s2 := '000'
      else begin
        if not GB_fxFlag then s2 := IntToHex(ItemIndex * $80, 3)
        else s2 := IntToOct(ItemIndex * 32, 3);
      end;
    end;

    AData := TEncoding.ANSI.GetBytes('DEVN ' + s1 + s2 + #13#10);
    // 送信
    ASocket.SendData(AData);
    res := ASocketReceiveData(ASocket, ATimeout);

    Rectangle4.Fill.Color := TAlphaColorRec.Black;
    Rectangle5.Fill.Color := TAlphaColorRec.Black;
    Timer1.Enabled := True;
  end;
end;

procedure TForm4.FormCreate(Sender: TObject);
var
  IniFile: TMemIniFile;   // uses .... System.IniFiles;
begin
  GB_DeviceName := 'X';
  GB_DeviceStartIndex := 0;
  GB_fxFlag := True;

  StringColumn1.Header := 'X';
  StringColumn2.Header := '0';
  StringColumn3.Header := '1';
  StringColumn4.Header := '2';
  StringColumn5.Header := '3';
  StringColumn6.Header := '4';
  StringColumn7.Header := '5';
  StringColumn8.Header := '6';
  StringColumn9.Header := '7';
  StringColumn10.Header := '8';
  StringColumn11.Header := '9';
  StringColumn12.Header := 'A';
  StringColumn13.Header := 'B';
  StringColumn14.Header := 'C';
  StringColumn15.Header := 'D';
  StringColumn16.Header := 'E';
  StringColumn17.Header := 'F';

  // 縦画面に固定
  Application.FormFactor.Orientations :=
    [TFormOrientation.Portrait, TFormOrientation.InvertedPortrait];

  // use ..... System.IOUtils;
  IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine(
    System.IOUtils.TPath.GetDocumentsPath, 'GXW2_IO.ini'), TEncoding.UTF8);
  with IniFile do begin
    try
      BtDeviceHead := ReadString('Target', 'PCName', '');
    finally
      Free;
    end;
  end;
  // TTS
  InitTTS;

  // ブザー
  ToneGenerator := TJToneGenerator.JavaClass.init(
    TJAudioManager.JavaClass.STREAM_ALARM,
    TJToneGenerator.JavaClass.MAX_VOLUME);

  // Bruetooth スレッド
  Timer1.Interval := 10;
  Timer1.Enabled := True;
  ThBt := TBtThread.Create;

  // FX モード で起動
  CheckBox1.IsChecked := True;
  CheckBox1Change(self);
end;

procedure TForm4.FormDestroy(Sender: TObject);
begin
  if ASocket <> nil then begin
    ASocket.Close;
    ASocket.Free;
    ASocket := nil;
  end;
end;

procedure TForm4.Rectangle1Click(Sender: TObject);
// [ + ]
var
  n, m : integer;
begin
  if not GB_fxFlag then begin
    n := StrToIntDef('$' + Copy(Label3.Text, 2), 0);
    Inc(n);
    m := n - GB_DeviceStartIndex;
    if m >= 0 then begin
      if  m div 16 < 10 then begin
        with Label3 do begin
          Text := Copy(Text, 1, 1) + n.ToHexString(3);
          TextSettings.FontColor := TAlphaColorRec.Orange;
        end;
        with StringGrid1 do begin
          OnCellClick := nil;
          Row :=  m div 16;
          Col := m mod 16 + 1;
          OnCellClick := StringGrid1CellClick;
          SetFocus;
        end;
      end;
    end;
  end
  else begin
    n := OctToIntDef(Copy(Label3.Text, 2), 0);
    Inc(n);
    m := n - GB_DeviceStartIndex;
    if m >= 0 then begin
      if m div 8 < 10 then begin
        with Label3 do begin
          Text := Copy(Text, 1, 1) + IntToOct(n, 3);
          TextSettings.FontColor := TAlphaColorRec.Orange;
        end;
        with StringGrid1 do begin
          OnCellClick := nil;
          Row := m div 8;
          Col := m mod 8 + 1;
          OnCellClick := StringGrid1CellClick;
          SetFocus;
        end;
      end;
    end;
  end;
end;

procedure TForm4.Rectangle2Click(Sender: TObject);
// [ - ]
var
  n : integer;
begin
  if not GB_fxFlag then begin
    n := StrToIntDef('$' + Copy(Label3.Text, 2), 0);
    Dec(n);
    if n < 0 then n := 0;
    with Label3 do begin
      Text := Copy(Text, 1, 1) + n.ToHexString(3);
      TextSettings.FontColor := TAlphaColorRec.Orange;
    end;
    n := n - GB_DeviceStartIndex;
    if n >= 0 then begin
      with StringGrid1 do begin
        OnCellClick := nil;
        Row := n div 16;
        Col := n mod 16 + 1;
        OnCellClick := StringGrid1CellClick;
        SetFocus;
      end;
    end;
  end
  else begin
    n := OctToIntDef(Copy(Label3.Text, 2), 0);
    Dec(n);
    if n < 0 then n := 0;
    with Label3 do begin
      Text := Copy(Text, 1, 1) + IntToOct(n, 3);
      TextSettings.FontColor := TAlphaColorRec.Orange;
    end;
    n := n - GB_DeviceStartIndex;
    if n >= 0 then begin
      with StringGrid1 do begin
        OnCellClick := nil;
        Row := n div 8;
        Col := n mod 8 + 1;
        OnCellClick := StringGrid1CellClick;
        SetFocus;
      end;
    end;
  end;
end;

procedure TForm4.StringGrid1CellClick(const Column: TColumn;
  const Row: Integer);
var
  n : integer;
begin
  // 出力反転の対象
  if not GB_fxFlag or (GB_fxFlag and (Column.Index <= 8)) then begin
    n := StrToIntDef('$' + StringGrid1.Cells[0, Row], 0) + StrToIntDef('$' + Column.Header, 0);
    with Label3 do begin
      Text := GB_DeviceName + n.ToHexString(3);
      TextSettings.FontColor := TAlphaColorRec.Orange;
    end;
  end;
end;

procedure TForm4.StringGrid1DrawColumnCell(Sender: TObject;
  const Canvas: TCanvas; const Column: TColumn; const Bounds: TRectF;
  const Row: Integer; const Value: TValue; const State: TGridDrawStates);
// AlphaColor uses ... System.UIConsts;
var
  s : string;
  n : integer;
  flag : boolean;
begin
  if not Value.IsEmpty then s := Value.ToString
  else s := '';
  with Canvas do begin
    if Column.Index = 0 then begin
      if s <> '' then begin
        Fill.Color := claSilver;//claAqua;//claSilver;//Yellow;
        FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round );
        Fill.Color := claBlack;
        Font.Size := 15;
        FillText(Bounds, s, False, 1.0, [], TTextAlign.Center);
      end;
    end
    else begin
      flag := False;
      if (Label2.Text = 'OFF') or  (Label2.Text = 'ON') then begin
        if not GB_fxFlag then n := StrToIntDef('$' + Copy(Label1.Text, 2), -1)
        else n := OctToIntDef(Copy(Label1.Text, 2), -1);

        if (n >= GB_DeviceStartIndex) then begin
          n := n - GB_DeviceStartIndex;
          if (not GB_fxFlag and (Row = n div 16) and (Column.Index = n mod 16 + 1)) or
            (GB_fxFlag and (Row = n div 8) and (Column.Index = n mod 8 + 1)) then begin
            if Label2.Text = 'OFF' then begin
              Fill.Color := claGray;//Black;
              FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round );
              Fill.Color := claLime;
            end;
            if Label2.Text = 'ON' then begin
              Fill.Color := claRed;
              FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round );
              Fill.Color := claWhite;
            end;
            if not GB_fxFlag then s := IntToHex(n mod 16, 1)
            else s := IntToHex(n mod 8, 1);
            Font.Size := 16;
            FillText(Bounds, s, False, 1.0, [], TTextAlign.Center);
            flag := true;
          end;
        end;
      end;
      if not flag and (s <> '') then begin
        Fill.Color := claOrange;//Red;
        FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round );
        Fill.Color := claWhite;
        Font.Size := 16;
        FillText(Bounds, s, False, 1.0, [], TTextAlign.Center);
      end;
    end;
  end;
end;

procedure TForm4.StringGrid1DrawColumnHeader(Sender: TObject;
  const Canvas: TCanvas; const Column: TColumn; const Bounds: TRectF);
var
  s: string;
begin
  s := Column.Header;
  if s <> '' then begin
    with Canvas do begin
      if Column.Index = 0 then begin
        Fill.Color := claLime;
        FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round );
        Fill.Color := claBlack;
        Font.Size := 18;
        FillText(Bounds, s, False, 1.0, [], TTextAlign.Center);
      end
      else begin
        Fill.Color := claSilver;
        FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round );
        Fill.Color := claBlack;
        Font.Size := 15;
        FillText(Bounds, s, False, 1.0, [], TTextAlign.Center);
      end;
    end;
  end;
end;

function NumToSpeechText(const hex : string): string;
var
  i : integer;
  s : string;
begin
  result := '';
  for i := 1 to hex.Length do begin
    s := Copy(hex, i, 1);
    if s = '0' then result := result + 'ゼロ'
    else if s = '1' then result := result + 'イチ'
    else if s = '2' then result := result + 'ニイ'
    else if s = '3' then result := result + 'サン'
    else if s = '4' then result := result + 'ヨン'
    else if s = '5' then result := result + 'ゴー'
    else if s = '6' then result := result + 'ロク'
    else if s = '7' then result := result + 'ナナ'
    else if s = '8' then result := result + 'ハチ'
    else if s = '9' then result := result + 'キュウ'

    else if s = 'A' then result := result + 'エイ'
    else if s = 'B' then result := result + 'ビイ'
    else if s = 'C' then result := result + 'シイ'
    else if s = 'D' then result := result + 'デー'
    else if s = 'E' then result := result + 'イイ'
    else if s = 'F' then result := result + 'エフ'
    else result := result + s;
    result := result + ' ';
  end;
end;

procedure TForm4.Timer1Timer(Sender: TObject);
var
  ATimeout : Cardinal;
  AData : TBytes;
  res : string;
  i, j, k : integer;
  Ticks : Cardinal;
  s, s1 : string;
  n, idx : integer;
  flag : boolean;
  fxFlag : boolean;
begin
  if not ((GCMDMODE = cmdSCCONNECT) and ASocket.Connected) then begin
    Inc(OpenMsecCnt);
    CheckBox1.Text := IntToStr(OpenMsecCnt * 10) + 'msec';
    if GCMDMODE = cmdSCNG then begin
      Inc(OpenNgCnt);
      if OpenNgCnt > 4 then begin
        Timer1.Enabled := False;
        ShowMessage(BTDeviceHead + ' に、接続できません.');
      end;
    end;
    if OpenMsecCnt > 100 then begin
      Timer1.Enabled := False;
      ShowMessage('接続先が無効です.');
    end;
  end;

  if (GCMDMODE = cmdSCCONNECT) and ASocket.Connected then begin
    Timer1.Interval := 250;
    //flag := True;
    Timer1.Enabled := False;
    try
      Ticks := TThread.GetTickCount;
      ATimeout := 250;
      // 初回は CPU TYPE 取得のみ
      if Label7.Text = '' then begin
        AData := TEncoding.ANSI.GetBytes('CPU' + #13#10);
        // 送信
        ASocket.SendData(AData);
        // 受信
        res := ASocketReceiveData(ASocket, ATimeout);
        flag := res <> '';
        fxFlag := (Pos('FX', res) = 1);
        if fxFlag then Label7.Text := 'FXCPU'
        else Label7.Text := 'Q/LCPU';
        if GB_fxFlag <> fxFlag then begin
          CheckBox1.IsChecked := fxFlag;
          CheckBox1Change(self);
        end;
      end
      else begin
        // デバイス一括読み出しコマンド
        AData := TEncoding.ANSI.GetBytes('READ' + #13#10);
        // 送信
        ASocket.SendData(AData);
        // 受信
        res := ASocketReceiveData(ASocket, ATimeout);
        flag := res <> '';
        // データ格納
        if res.Length >= 88 then begin
          // FX FLAG
          //GB_FxFlag := Copy(res, 1,2) = 'FX'; // 未使用
          // 先頭デバイス
          s1 := Copy(res, 3, 1);
          if not GB_fxFlag then
            idx := StrToIntDef('$' + Trim(Copy(res, 4, 5)), 0)
          else
            idx := OctToIntDef(Trim(Copy(res, 4, 5)), 0);

          for i := 0 to 9 do begin
            s := Copy(res, i * 4 + 9, 4);
            n := StrToIntDef('$' + s, 0);
            for j := 0 to 15 do
              BitAryNew[i * 16 + j] := n and IntPower(2, j) > 0;

            s := Copy(res, i * 4 + 49, 4);
            n := StrToIntDef('$' + s, 0);
            for j := 0 to 15 do
              BitAryOld[i * 16 + j] := n and IntPower(2, j) > 0;
          end;
          // 先頭デバイスが変わった
          if (GB_DeviceName <> s1) or (GB_DeviceStartIndex <> idx) then begin
            GB_DeviceName := s1;
            GB_DeviceStartIndex := idx;

            // イベント無効 (PC へ送り返すため)
            ComboBox1.OnChange := nil;
            ComboBox2.OnChange := nil;

            with ComboBox1 do begin
              if GB_DeviceName = 'X' then ItemIndex := 0
              else ItemIndex := 1;
            end;

            // 先頭デバイス番号
            with ComboBox2 do begin
              if Items.Count > 0 then begin
                if not GB_fxFlag then
                  ItemIndex := GB_DeviceStartIndex div $80
                else
                  ItemIndex := GB_DeviceStartIndex div 32;
              end;
            end;
            // イベントを戻す
            ComboBox1.OnChange := ComboBox1Change;
            ComboBox2.OnChange := ComboBox1Change;

            // X or Y
            StringColumn1.Header := GB_DeviceName;
            // アドレス番号を変える
            with StringGrid1 do begin
              if not GB_fxFlag then begin
                for i := 0 to 9 do
                  Cells[0, i] := (GB_DeviceStartIndex + i * 16).ToHexString(3);
              end
              else begin
                for i := 0 to 9 do
                  Cells[0, i] := IntToOct(GB_DeviceStartIndex + i * 8, 3);
              end;
              Row := 0;
              Col := 1;
            end;
            // デバイス ON/OFF の表示を初期化
            Label1.Text := '';
            Label2.Text := '';
            Rectangle4.Fill.Color := TAlphaColorRec.Black;
            Rectangle5.Fill.Color := TAlphaColorRec.Black;
            // 反転デバイス番号を更新
            if not GB_fxFlag then begin
              Label3.Text := GB_DeviceName + IntToHex(GB_DeviceStartIndex, 3);
            end
            else begin
              Label3.Text := GB_DeviceName + IntToOct(GB_DeviceStartIndex, 3);
            end;
            for i := 0 to 159 do BitAryOld[i] := False;
          end;
        end;
        // 表示
        with StringGrid1 do begin
          for i := 0 to 9 do begin
            for j := 0 to 15 do begin
              if BitAryNew[i * 16 + j] then s := j.ToHexString(1)
              else s := '';
              if Cells[j + 1, i] <> s then Cells[j + 1, i] := s;
              if GB_fxFlag and (j = 7) then break;
            end;
          end;
        end;
        // 比較
        // 内部データ数 = 160
        for i := 0 to 9 do begin
          for j := 0 to 15 do begin
            k := i * 16 + j;
            if BitAryNew[k] <> BitAryOld[k] then begin
              with Label1 do begin
                if not GB_fxFlag then begin
                  idx := k + GB_DeviceStartIndex;
                  Text := GB_DeviceName + idx.ToHexstring(3);
                end
                else begin
                  idx := i * 8 + j + GB_DeviceStartIndex;
                  Text := GB_DeviceName + IntToOct(idx, 3);
                end;
              end;
              if BitAryNew[k] then begin
                Rectangle4.Fill.Color := TAlphaColorRec.Red;
                Label1.TextSettings.FontColor := TAlphaColorRec.White;
                Rectangle5.Fill.Color := TAlphaColorRec.Red;
                with Label2 do begin
                  Text := 'ON';
                  TextSettings.FontColor := TAlphaColorRec.White;
                end;
              end
              else begin
                Rectangle4.Fill.Color := TAlphaColorRec.Black;
                Label1.TextSettings.FontColor := TAlphaColorRec.Lime;
                Rectangle5.Fill.Color := TAlphaColorRec.Black;
                with Label2 do begin
                  Text := 'OFF';
                  TextSettings.FontColor := TAlphaColorRec.Lime;
                end;
              end;
              if Switch1.IsChecked then begin
                s := Copy(Label1.Text, 1, 1) + #13 + NumToSpeechText(Copy(Label1.Text, 2));
                if Label2.Text = 'ON' then s := s + '。' + 'オン'
                else s := s + '。' + 'オフ';
                SpeakOut(s);
              end;
            end;
            if GB_fxFlag and (j = 7) then break;
          end;
        end;
      end;
      if flag then
        CheckBox1.Text := (TThread.GetTickCount - Ticks).ToString
      else
        CheckBox1.Text := 'PC 接続失敗';

      if flag then
        Timer1.Enabled := True;
    except
      CheckBox1.Text := 'PC 応答なし';
      Timer1.Enabled := True;
    end;
  end;
end;

end.