MELSEC PLC Android スマホで I/O チェック 2019/03/24
 
 MX Cpmponent 不要で、GX Works2, (3) を使った I/O チェッカーはこちらです。(2019/04/01)


 ・2019/03/24 Android アプリで、手動操作時のブザー音を追加.
 ・2019/03/08 Android アプリで、一度接続されると、接続先を変更できないのを手直し.
 ・2019/03/06.2 グリッドの列幅を調整可能にした.
 ・2019/03/06.1 音声メッセージを修正.
 ・2019/03/06 FX で一部取得できないアドレスがあるのを手直し.コメントファイルの保存、Windows での音声メッセージを追加.
 ・2019/02/20.1 アイコンを作成しました.
 ・2019/02/20 PC名の入力方法を変更.アドレス表記を修正.
 ・2019/02/17A FX5U (Ethernet) に対応
 ・2019/02/17 FX3 / QCPU に対応
 ・2019/02/14 初版作成(FX3 用)

 これまでに iOS / Android 版 の PLC モニタを数種類試作してきましたが、COM ポートか、Ethernet ポートと接続に伴う設定が必要で、
 現場でちょっと使うには、無理がありました。
 今回は違います。
 PLC との通信に MX Component 4 を使っているため、PLC の USB / COM / Ethernet ポートに接続するだけで、
 Android 端末で PLC の I/O チェックが行えるようになります。
 MX Component 付属のツール「通信設定ユーティリティ」で接続可能な PLC であれば、使用可能です。GX Works 2 / 3 との同時使用も可能です。
 ↓の画像では、FX3G/S (USB接続) になっていますが、FX5CPU (Eternet接続)、QCPU (USB接続)、FX3U (COM接続) にも対応しています。
 
 キーエンス KV Com + 、オムロン CX Compolet を使って同様のことができるのではないかと思います。

 [ 弱点 ]
 ・MX Component が必要なこと。(実売価格は、そう高価でもない。)

 [ MX Component の恩恵 ]
 ・CPU 直結接続が可能。シリアル通信よりかなり速いです。PLC 側での設定が不要。
 ・「通信設定ユーティリティ」を使うと、プログラムで接続パラメータを書かなくて良いので非常に楽。

■仕組み
 PLC とパソコンは、MX Component USB または LAN ケーブルで直結接続し、250 msec 周期でポーリング。データを保持します。
 パソコン内蔵の Bluetooth と Android 端末で通信し、250 msec 周期でパソコンの格納データをポーリングします。
 (一度にモニタ出来るビットデバイス数は、128点。 表示は Q の場合: 128 点、FX の場合: 先頭 64 点のみ。)
 外付けの Bluetooth アダプタでも使えると思いますが、試してはいません。

 Android 端末から PLC と直接やりとりするよりかなり高速になります。
 パソコンから見ると、Android 端末は Bluetooth 経由の COM ポートで、普通のシリアル通信と何ら変わりなく、
 Bluetooth 用の特別なプログラムを書く必要はありません。

 ※あらかじめ、Bluetooth 経由の COM ポートの追加が必要です。
 ※Bluetooth は Classic Bluetooth で、iOS でも使用可能な Bluetooth LE とは異なります。
 ※パソコンと Andoroid 端末は最初の一度だけペアリングが必要です。

■パソコン側のアプリ
 ・単体で、I/O チェック (X, Y のみ)が可能です。
 ・ON / OFF の変化があったデバイスを大きく表示します。
  ※あらかじめコメントファイルを作っておくと、デバイスコメントが表示されます。
 ・SAPI (音声合成)がインストールされている場合、デバイス名とコメントを読み上げます。(2019/03/06)
  ※音声終了まで次の操作はできません。かなり作業効率が悪くなります。
  ※こちらの環境(Windows 10) では、SAPI 5 がインストールされていました。
 
 FXの場合、表示されるのは先頭 64 点ですが、以降の 64 点を含めて 計 128 点の ON/OFF の状態変化をモニタしています。
 例えば、先頭が 'X000 'の場合は、'X177' まで。これは、スマホ側も同様です。
 

 ・あらかじめ論理局番の説明を入力しておくと、接続先を選択しやすくなります。
 ・コメントファイルを作っておくと、変化にあったデバイスのコメントが表示されるようになります。
 

■スマホ側のアプリ
 ・パソコン側のアプリと連動して、周囲 10m 程度の範囲で、I/O チェック(X,Yのみ)が可能です。
 ・ON / OFF の変化があったデバイスを大きく表示します。
 ・ON / OFF の変化があったデバイスとコメントを音声で知らせます。(ポケットに入れたままで、I/O チェックができます。)

 ・入力チェック中のスクリーンショット
 

■Bluetooth 経由の COM ポートを使うには

 ・あらかじめ、「Bluetooth 設定」 で COM ポートを追加しておきます。
  

 こちらの環境 (Winodws10) では、タスクトレイの [ ^ ] をクリック。Bluetooh のアイコンを右クリック。
 ポップアップメニュから「設定を開く」を選択。「その他の Bluetooth オプション」をクリックすると「Bluetooth 設定」が出てきます。

    

 ・デバイスマネージャーから 「ポートの設定」を確認、変更できます。
 

■通信設定ユーティリティー

 ・あらかじめ、MX Component 付属の 「通信設定ユーティリティ」 で通信設定が必要です。
    

■ダウンロード

 MXC4_IO.zip (Winodws 側アプリ EXE のみ Ver. 2019.03.06.2)
  
MX Component 4 がインストールされている必要があります。
  ※高解像度環境で作成しているため、画面サイズが大きすぎる、配置が崩れることがあります。

  ・「通信設定ユーティリティ」で設定した論理局番、追加した「Bluetooth 経由 ComPort」 のポート番号を選択してください。
   あらかじめ、論理局番の説明を入力しておくと、分かりやすいです。
  ・コメントファイルは、"X000, コメント1" のような CSVファイルです。エクセルでも作成できます。
   一時的に使う場合、[アドレス作成] ボタンでアドレスを作成し、GX Works のコメントをクリップボードにコピーし、[ Ctrl ] + V キーでペーストできます。

 MXC4_IO.apk (Android 側アプリ APK のみ Ver. 2019.03.24 手動操作時のブザー音追加版)
 ・最初の1回だけペアリングが必要です。
 ・初回起動時は、接続エラーになります。一番上のコンボボックスで接続先の PC 名を選択し、再起動します。
 ・通信異常の時は、Android 側のアプリを終了し、PC 側で [PLC CLOSE] -> [PLC OPEN] し、再度 Android 側のアプリを起動してください。
  それでも通信異常になる時は、Android 側のアプリを終了し、PC 側のアプリを一度終了し、再度起動。その後 Android 側のアプリを起動してください。
 ・通信できない場合は、上記を数回繰り返してみてください。

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

■連絡先
 e-mail : f.izawa@dream.com (@を小文字に変えてください)
 URL: http://www.izawa-web.com/


// ---------------------------------------------------------------
// Windows 側
// ---------------------------------------------------------------
unit Unit4;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.OleCtrls, ActProgTypeLib_TLB,
  Vcl.StdCtrls, ActUtlTypeLib_TLB, System.Bluetooth, System.Bluetooth.Components,
  AdPacket, OoMisc, AdPort, Math, Vcl.ComCtrls, Vcl.ExtCtrls, Vcl.Grids, AdSelCom,
  Vcl.Buttons,
  IniFiles, System.UITypes, ClipBrd, Vcl.ExtDlgs,
  SpeechLib_TLB, ComObj;
type
  TBitAry  = array [0..127] of Boolean;
  TWordAry = array [0..7]   of SmallInt;
type
  TForm4 = class(TForm)
    ActProgType1: TActProgType;
    ActUtlType1: TActUtlType;
    PageControl1: TPageControl;
    TabSheet1: TTabSheet;
    TabSheet2: TTabSheet;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    SpeedButton1: TSpeedButton;
    SpeedButton2: TSpeedButton;
    SpeedButton3: TSpeedButton;
    SpeedButton4: TSpeedButton;
    Button5: TButton;
    Button6: TButton;
    Edit4: TEdit;
    Edit7: TEdit;
    Edit8: TEdit;
    Button3: TButton;
    Edit9: TEdit;
    ComboBox1: TComboBox;
    ComboBox2: TComboBox;
    Edit5: TEdit;
    Edit6: TEdit;
    Button4: TButton;
    ApdComPort1: TApdComPort;
    ApdDataPacket1: TApdDataPacket;
    Timer1: TTimer;
    Edit3: TEdit;
    StringGrid1: TStringGrid;
    StringGrid2: TStringGrid;
    StringGrid3: TStringGrid;
    Button1: TButton;
    Edit10: TEdit;
    Edit11: TEdit;
    SpeedButton5: TSpeedButton;
    CheckBox1: TCheckBox;
    StringGrid4: TStringGrid;
    ComboBox3: TComboBox;
    ComboBox4: TComboBox;
    Edit1: TEdit;
    Label6: TLabel;
    OpenTextFileDialog1: TOpenTextFileDialog;
    SaveTextFileDialog1: TSaveTextFileDialog;
    Button2: TButton;
    CheckBox2: TCheckBox;
    CheckBox3: TCheckBox;
    procedure ApdDataPacket1StringPacket(Sender: TObject; Data: AnsiString);
    procedure Timer1Timer(Sender: TObject);
    procedure Button5Click(Sender: TObject);
    procedure Button6Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure SpeedButton1Click(Sender: TObject);
    procedure SpeedButton2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
      Rect: TRect; State: TGridDrawState);
    procedure StringGrid1Click(Sender: TObject);
    procedure ComboBox1Change(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure StringGrid2KeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure SpeedButton5Click(Sender: TObject);
    procedure CheckBox1Click(Sender: TObject);
    procedure Edit1Change(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    // PLC からの受信データ保持
    WordAryOld : TWordAry;
    WordAryNew : TWordAry;
    // 内部用データ保持
    BitAryOld : TBitAry;
    BitAryNew : TBitAry;
    // デバイス名
    GB_DeviceHead : string;
    // 先頭番号
    GB_DeviceStartNo : integer;
    // 「通信設定ユーティリティ」での論理局番
    Gb_PLCStationNo : integer;

    GB_fxFlag : boolean;
    GB_SgScale : double;
    function GetDeviceComment(const devStr : string): string;
    procedure ReadCommentFile(const FileName : TFileName);
    procedure SaveCommentFile(const FileName : TFileName);

  end;

var
  Form4: TForm4;
  SpVoice: OleVariant;
  TTSFlag : boolean;

function OctToIntDef(const Value: string; def : integer): integer;
function IntToOct(Value: integer; digits: Integer): string;
function IntPower(n, k : integer):integer;

implementation

{$R *.dfm}

uses Unit1;

// 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 進数 -> 10進数
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;

// 10 進数 -> 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;

// *****************************
// StringGrid でのキー操作
// *****************************
procedure SgKeyDown(SG: TSTringGrid; var Key: Word; Shift: TShiftState);
var
  i, j, k, n : integer;
  sl : TStringList;
  s, s1 : string;
  xflag : boolean;
begin
  if Key = VK_DELETE then begin
    with SG do begin
      if (Selection.Top <> Selection.Bottom) or
         (Selection.Left <> Selection.Right) then begin
        Key := 0;
        for i := Selection.Top to Selection.Bottom do
          for j := Selection.Left to Selection.Right do
            Cells[j, i] := '';
      end;
    end;
  end;
  if ssCtrl in Shift then begin
    if true then begin
      xflag := (Key = Ord('X')) or (Key = Ord('x'));
      if (Key = Ord('C')) or (Key = Ord('c')) or xflag then begin
        Key := 0;
        Clipboard.AsText := '';
        with SG do begin
          for i := Selection.Top to Selection.Bottom do begin
            for j := Selection.Left to Selection.Right do begin
              Clipboard.AsText := Clipboard.AsText + Cells[j, i];
              if j < Selection.Right then
                Clipboard.AsText := Clipboard.AsText + #9
              else Clipboard.AsText :=
                Clipboard.AsText + #13#10;
            end;
          end;
          if xflag then begin
            for i := Selection.Top to Selection.Bottom do
              for j := Selection.Left to Selection.Right do
                Cells[j, i] := '';
          end;
        end;
      end
      else if (Key = Ord('V')) or (Key = Ord('v')) then begin
        //with SG do
        //  if EditorMode then EditorMode := false;
        Key := 0;
        with SG do begin
          sl := TStringList.Create;
          try
            s := Clipboard.AsText;
            while true do begin
              k := Pos(#13#10, s);
              if k = 0 then break
              else begin
                sl.Add(Copy(s, 1, k - 1));
                Delete(s, 1, k + 1);
              end;
            end;
            for i := 0 to sl.Count-1 do begin
              s := SL[i];
              j := 0;
              while true do begin
                k := Pos(#9, s);
                if k = 0 then
                  s1 := Copy(s, 1, Length(s))
                else begin
                  s1 := Copy(s, 1, k - 1);
                  Delete(s, 1, k);
                end;
                Cells[Selection.Left + j, Selection.Top + i] := s1;
                n := 1;
                while true do begin
                  if Selection.Bottom < Selection.Top + i + (sl.Count * n) then
                    break
                  else
                    Cells[Selection.Left + j, Selection.Top + i + (sl.Count * n)] := s1;
                  Inc(n);
                end;
                if k = 0 then break;
                Inc(j);
              end;
            end;
          finally
            sl.Free;
          end;
        end;
      end;
    end;
  end;
end;
// --------------------------------------------

procedure TForm4.ReadCommentFile(const FileName : TFileName);
// コメントファイル読み込み
var
  sl : TStringList;
  cnt1, cnt2 : integer;
  i, n : integer;
  s, s1, s2 :string;
begin
  cnt1 := 0; cnt2 := 0;
  sl := TStringList.Create;
  try
    sl.LoadFromFile(FileName);
    for i := 0 to sl.Count - 1 do begin
      n := Pos(',', sl[i]);
      s1 := Copy(sl[i], 1, n- 1);
      s2 := Copy(sl[i], n + 1);
      s := Uppercase(Copy(s1, 1, 1));
      if s = 'X' then begin
        with StringGrid2 do begin
          Inc(cnt1);
          if RowCount <= cnt1 then
            RowCount := RowCount + 1;
          Cells[0, cnt1] := s1;
          Cells[1, cnt1] := s2;
        end;
      end
      else if s = 'Y' then begin
        with StringGrid3 do begin
          Inc(cnt2);
          if RowCount <= cnt2 then
            RowCount := RowCount + 1;
          Cells[0, cnt2] := s1;
          Cells[1, cnt2] := s2;
        end;
      end;
    end;
  finally
    sl.Free;
  end;
  with StringGrid2 do begin
    if cnt1 > 0 then begin
      if cnt1 < RowCount then
        RowCount := cnt1;
    end
    else begin
      RowCount := 2;
      Cells[0, 1] := '';
      Cells[1, 1] := '';
    end;
  end;
  with StringGrid3 do begin
    if cnt2 > 0 then
      if cnt2 < RowCount then RowCount := cnt2
    else begin
      RowCount := 2;
      Cells[0, 1] := '';
      Cells[1, 1] := '';
    end;
  end;
end;
procedure TForm4.SaveCommentFile(const FileName : TFileName);
// コメントファイル保存
var
  sl : TStringList;
  i : integer;
begin
  sl := TStringList.Create;
  try
    with StringGrid2 do begin
      for i := 1 to RowCount -1 do
        sl.Add(Cells[0, i] + ',' + Cells[1, i]);
    end;
    with StringGrid3 do begin
      for i := 1 to RowCount -1 do
        sl.Add(Cells[0, i] + ',' + Cells[1, i]);
    end;
    sl.SaveToFile(FileName);
  finally
    sl.Free;
  end;
end;

function TForm4.GetDeviceComment(const devStr : string): string;
var
  i, n : integer;
  sg : TStringGrid;
  s : string;
begin
  result := '';
  n := StrToInt('$' + Copy(devStr, 2));
  if Copy(devStr, 1, 1) = 'X' then
    sg := StringGrid2
  else
    sg := StringGrid3;
  with sg do begin
    for i := 1 to RowCount - 1 do begin
      s :=  Copy(Cells[0, i], 2);
      if (s <> '') and (n = StrToInt('$' + s)) then begin
        result := Cells[1, i];
        break;
      end;
    end;
  end;
end;

procedure TForm4.ApdDataPacket1StringPacket(Sender: TObject; Data: AnsiString);
// Bluetooth 仮想 COM ポートから、Android 端末からのコマンドを受信
var
  s, s0 : string;
  szDevice : WideString;
  lData : integer;
  n : integer;
  i : integer;
  res, res1, res2 : string;
begin
  s := Trim(string(Data));
  // Android からのコマンド
  Edit7.Text := s;
  s0 := Trim(Copy(s, 1, 2));
  // CPU Type
  if s = 'CPU' then begin
    Timer1.Enabled := False;
    res := Edit9.Text;
    ApdComPort1.PutString(res + #13#10);
    Edit5.Text := res;
    Timer1.Enabled := True;
  end
  // 一括読み出し
  else if s0 = 'RD' then begin
    Timer1.Enabled := False;

    // 内部データを返信
    res := '';
    res1 := '';
    res2 := '';
    // 16進2桁×4 左から若い順
    // 今回値
    for i := 0 to 7 do
      res1 := res1 + WordAryNew[i].ToHexString(4);
    // 前回値
    for i := 0 to 7 do
      res2 := res2 + WordAryOld[i].ToHexString(4);
    // デバイス名と開始番号(16進表示)
    res := res1 + res2 + ' ' + GB_DeviceHead + ' ' + GB_DeviceStartNo.ToHexString(4);
    // コメント
    if Edit3.Text <> '' then begin
      s := GetDeviceComment(Edit3.Text);
      if s <> '' then res := res + ' ' + s;
    end;

    ApdComPort1.PutString(res + #13#10);
    Edit5.Text := res;

    // 前回値を更新
    WordAryOld := WordAryNew;
    Timer1.Enabled := True;
  end
  else if s0 = 'WR' then begin
    // 'WR Y0 1'
    Timer1.Enabled := False;
    with ActUtlType1 do begin
      // PLC に 書き込み
      s0 := Copy(s, 4);
      n := Pos(' ', s0);
      if n > 0 then begin
        // デバイス名
        szDevice := Copy(s0, 1, n -1);
        // 無視
        // lData := StrToIntDef(Copy(s0, n + 1), 0);
        // 反転
        GetDevice(szDevice, lData);
        lData := abs(lData - 1);

        if SetDevice(szDevice, lData) = 0 then
          if lData = 1 then res := 'ON'
          else res := 'OFF'
        else res := 'NG';
      end
      else res := 'NG';
      ApdComPort1.PutString(res + #13#10);
      Edit5.Text := res;
    end;
    Timer1.Enabled := True;
  end
  // デバイス番号をセット
  // 'CF X 0', CF X 16'...
  else if s0 = 'CF' then begin
    Timer1.Enabled := False;
    // デバイス名
    s0 := Copy(s, 4);
    n := Pos(' ', s0);
    if n > 0 then begin
      res := 'OK';
      Edit5.Text := res;
      ApdComPort1.PutString(res + #13#10);

      GB_DeviceHead := Trim(Copy(s0, 1, n - 1));
      // デバイス名
      if GB_DeviceHead = 'Y' then ComboBox1.ItemIndex := 1
      else ComboBox1.ItemIndex := 0;
      // 開始アドレス
      GB_DeviceStartNo := StrToIntDef('$' + Copy(s0, n + 1), 1);
      if not Gb_fxFlag then
        ComboBox2.ItemIndex := GB_DeviceStartNo div 32
      else
        ComboBox2.ItemIndex := GB_DeviceStartNo div 16;

      // 変更を反映
      ComboBox1Change(self);
    end;
    Timer1.Enabled := True;
  end
  else
    ApdComPort1.PutString('??' + #13#10);
end;

procedure TForm4.Button1Click(Sender: TObject);
// コメントファイル読み込み
begin
  OpenTextFileDialog1.InitialDir := ExtractFileDir(Edit10.Text);
  if OpenTextFileDialog1.Execute then begin
    Edit10.Text := OpenTextFileDialog1.FileName;
    ReadCommentFile(Edit10.Text);
  end;
end;

procedure TForm4.Button2Click(Sender: TObject);
// コメントファイル保存
var
  fname : TFileName;
  flag : boolean;
begin
  if (StringGrid2.Cells[0, 1] <> '') or (StringGrid3.Cells[0, 1] <> '') then begin
    SaveTextFileDialog1.InitialDir := ExtractFileDir(Edit10.Text);
    if SaveTextFileDialog1.Execute then begin
      fname := SaveTextFileDialog1.FileName;
      if ExtractFileExt(fname) = '' then fname := fname + '.csv';
      flag := True;
      if FileExists(fname) then
        flag := MessageDlg('すでにファイルが存在します.上書きしますか?', mtInformation, [mbYes, mbNo], 0) = mrYes;
      if flag then begin
        SaveCommentFile(fname);
        Edit10.Text := fname;
      end;
    end;
  end;
end;

procedure TForm4.Button3Click(Sender: TObject);
//  出力反転
var
  szDevice : string;
  lData : integer;
  edt : TEdit;
begin
  if Sender as TButton = Button3 then edt := Edit8
  else edt := Edit6;

  with ActUtlType1 do begin
    Timer1.Enabled := False;
    with edt do begin
      // デバイス名
      szDevice := UpperCase(Text);
      // 反転
      if GetDevice(szDevice, lData) = 0 then begin
        Font.Color := clWhite;
        lData := abs(lData - 1);
        if SetDevice(szDevice, lData) = 0 then
          if lData = 1 then Font.Color := clRed
          else Font.Color := clLime
        else Font.Color := clYellow;
        Timer1.Enabled := True;
      end
      else Font.Color := clYellow;
    end;
  end;
end;

procedure TForm4.Button5Click(Sender: TObject);
// Bluetooth SPP 通信(仮想 COM ポート)接続
var
  s : string;
begin
  Edit9.Text := '';  // CPU Name
  Edit7.Text := '';
  Edit5.Text := '';

  with ApdComPort1 do begin
    s := Copy(ComboBox4.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;
     end;
  except
    ShowMessage('ComPort Open Error');
  end;

  with ActUtlType1 do begin
    // 対象デバイス
    if ComboBox1.ItemIndex = 1 then GB_DeviceHead := 'Y'
    else GB_DeviceHead := 'X';

    // 「通信設定ユーティリティ」での論理局番
    Gb_PLCStationNo := ComboBox3.ItemIndex;

    ActLogicalStationNumber := Gb_PLCStationNo;
    Timer1.Enabled := Open = 0;
  end;
end;

procedure TForm4.Button6Click(Sender: TObject);
// PLC、Android 通信終了
begin
  with ActUtlType1 do begin
    Close;
    Timer1.Enabled := False;
  end;
  // Bluetooth SPP 通信(仮想 COM ポート)切断
  if  ApdComPort1.Open then
    ApdComPort1.Open := False;
  Label5.Caption := '';
end;

procedure TForm4.CheckBox1Click(Sender: TObject);
// CPU Type = FX / Other
var
  i :integer;
begin
  GB_fxFlag := CheckBox1.Checked;
  with StringGrid1 do begin
    for i := 0 to 15 do Cells[i + 1, 0] := i.ToHexString(1);
    for i := 0 to 15 do Cells[0, i + 1] := (i * 16).ToHexString(3);
    Cells[0, 0] := 'X';
    Row := 1;
    Col := 1;
  end;
  with ComboBox2 do begin
    Items.Clear;
    if not GB_fxFlag then for i := 0 to 255 do Items.Add(IntToHex(i * 32, 3))
    else for i := 0 to 255 do Items.Add(IntToOct(i * 16, 3));
    ItemIndex := 0;
  end;
  with ComboBox1 do begin
    ItemIndex := 0;
  end;
  Edit6.Text := 'X000';
  Edit8.Text := 'Y000';
end;

procedure TForm4.ComboBox1Change(Sender: TObject);
// デバイス X or Y
var
  i : integer;
begin
  if ComboBox1.ItemIndex = 0 then GB_DeviceHead := 'X'
  else GB_DeviceHead := 'Y';
  if GB_fxFlag then begin
    GB_DeviceStartNo := ComboBox2.ItemIndex * 16;
    Edit6.Text := 'X' + IntToOct(GB_DeviceStartNo, 3);
    Edit8.Text := 'Y' + IntToOct(GB_DeviceStartNo, 3);
  end
  else begin
    GB_DeviceStartNo := ComboBox2.ItemIndex * 32;
    Edit6.Text := 'X' + IntToHex(GB_DeviceStartNo, 3);
    Edit8.Text := 'Y' + IntToHex(GB_DeviceStartNo, 3);
  end;
  with StringGrid1 do begin
    Cells[0, 0] := GB_DeviceHead;
    if GB_fxFlag then
      for i := 0 to 15 do Cells[0, i + 1] := IntToOct(GB_DeviceStartNo + i * 8, 3)
    else
      for i := 0 to 15 do Cells[0, i + 1] := IntToHex(GB_DeviceStartNo + i * 16, 3);
    Row := 1;
    Col := 1;
  end;
  for i := 0 to 127 do BitAryOld[i] := False;
  for i := 0 to 7 do WordAryOld[i] := 0;

  Edit3.Text := '';
  Edit4.Text := '';
end;

procedure TForm4.Edit1Change(Sender: TObject);
begin
  GB_SgScale := StrToFloatDef(Edit1.Text, 1.0);
  StringGrid1.Repaint;
end;

procedure TForm4.FormCreate(Sender: TObject);
var
  ini : TInifile;
  i : integer;
begin
  GB_DeviceHead := 'X';
  GB_DeviceStartNo := 0;
  GB_fxFlag := True;

  AdSelCom.ShowPortsInUse := False;
  for i := 0 to 32 do
    if AdSelCom.IsPortAvailable(i) then
      ComboBox4.Items.Add (AdPort.ComName(i) + '.');

  with StringGrid4 do begin
    RowCount := 101;
    ColWidths[0] := 150;
    ColWidths[1] := 550;
    Cells[0, 0] := ' 論理番号';
    Cells[1, 0] := ' コメント';
    for i := 0 to 99 do begin
      Cells[0, i + 1] := i.ToString;
    end;
  end;

  ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  with ini do begin
    try
      with StringGrid4 do begin
        for i := 0 to 9 do
          Cells[1, i + 1] := ReadString('MXCompo', 'StationNo' + i.ToString, '');
      end;
      with ComboBox3 do begin
        with StringGrid4 do
          for i := 0 to 99 do
            Items.Add(' '+ i.ToString + ' : ' + Cells[1, i + 1]);
        //ItemIndex := 0;
      end;
      ComboBox3.ItemIndex := ReadInteger('PLC', 'StationNo', 0);
      with ComboBox4 do
        ItemIndex := Items.IndexOf(ReadString('COM', 'PortNo', ''));
      Edit10.Text := ReadString('Device', 'CommentFileName', '');
      GB_SgScale := StrToFloatDef(ReadString('Grid', 'TextScale', '2.0'), 1.0);
      Edit1.Text := Format('%.2f', [GB_SgScale]);
    finally
      Free;
    end;
  end;

  Edit3.Text := '';
  Edit4.Text := '';
  Edit5.Text := '';
  Edit7.Text := '';
  Label5.Caption := '';

  with StringGrid2 do begin
    Cells[0, 0]:= ' デバイス';
    Cells[1, 0]:= '  コメント';
    ColWidths[1] := 450;
  end;
  with StringGrid3 do begin
    Cells[0, 0]:= ' デバイス';
    Cells[1, 0]:= '  コメント';
    ColWidths[1] := 450;
  end;
  Edit11.Text := '';
  PageControl1.ActivePageIndex := 0;
  CheckBox1Click(self);

  TTSFlag := False;
  try
    SpVoice := CreateOleObject('SAPI.SpVoice');
    TTSFlag := True;
    CheckBox2.Enabled := True;
    CheckBox3.Enabled := True;
  except
    ;
  end;
end;

procedure TForm4.FormDestroy(Sender: TObject);
var
  ini : TIniFile;
  i : integer;
begin
  with ActUtlType1 do Close;
  if  ApdComPort1.Open then ApdComPort1.Open := False;
  ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  with ini do begin
    try
      WriteInteger('PLC', 'StationNo', ComboBox3.ItemIndex);
      WriteString('COM', 'PortNo', ComboBox4.Text);
      WriteString('Device', 'CommentFileName', Edit10.Text);
      with StringGrid4 do begin
        for i := 0 to 99 do
          WriteString('MXCompo', 'StationNo' + i.ToString, Cells[1, i + 1]);
      end;
      WriteString('Grid', 'TextScale', GB_SgScale.ToString);
    finally
      Free;
    end;
  end;
end;

procedure TForm4.FormShow(Sender: TObject);
begin
  if (Edit10.Text <> '') and FileExists(Edit10.Text) then begin
    if MessageDlg(
      '前回終了時のコメントファイル' + #13 +
         Edit10.Text + #13 + 'を、読み込みますか?',
         mtInformation, [mbYes, mbNo], 0) = mrYes then
       ReadCommentFile(Edit10.Text)
    else if MessageDlg(
      '前回終了時のコメントファイル名' + #13 +
         Edit10.Text + #13 + 'を、削除しますか?',
         mtInformation, [mbYes, mbNo], 0) = mrYes then
      Edit10.Text := '';
  end;
end;

procedure TForm4.SpeedButton1Click(Sender: TObject);
// [ + ] ボタン
var
  n, r, c : integer;
  edt : TEdit;
begin
  if Sender as TSpeedButton = SpeedButton1 then edt := Edit8
  else edt := Edit6;
  if not GB_fxFlag then begin
    with edt do begin
      n := StrToIntDef('$' + Copy(Text, 2), 0);
      Inc(n);
      Text := Copy(Text, 1, 1) + n.ToHexString(3);
      Font.Color := RGB($FF, $A5, $00);
    end;
    n := n - GB_DeviceStartNo;
    if n >= 0 then begin
      with StringGrid1 do begin
        r := n div 16 + 1;
        c := n mod 16 + 1;
        if (r < RowCount) and (c < ColCount)  then begin
          OnClick := nil;
          Row := r;
          Col := c;
          OnClick := StringGrid1Click;
        end;
      end;
    end;
  end
  else begin
    with edt do begin
      n := OctToIntDef(Copy(Text, 2), 0);
      Inc(n);
      Text := Copy(Text, 1, 1) + IntToOct(n, 3);
      Font.Color := RGB($FF, $A5, $00);
    end;
    n := n - GB_DeviceStartNo;
    if n >= 0 then begin
      with StringGrid1 do begin
        r := n div 8 + 1;
        c := n mod 8 + 1;
        if (r < RowCount) and (c < ColCount)  then begin

          OnClick := nil;
          Row := r;
          Col := c;
          OnClick := StringGrid1Click;
        end;
      end;
    end;
  end;
end;

procedure TForm4.SpeedButton2Click(Sender: TObject);
// [ - ] ボタン
var
  n, r, c : integer;
  edt : TEdit;
begin
  if Sender as TSpeedButton = SpeedButton2 then edt := Edit8
  else edt := Edit6;
  if not GB_fxFlag then begin
    with edt do begin
      n := StrToIntDef('$' + Copy(Text, 2), 0);
      Dec(n);
      if n < 0 then n := 0;
      Text := Copy(Text, 1, 1) + n.ToHexString(3);
      Font.Color := RGB($FF, $A5, $00);
    end;
    n := n - GB_DeviceStartNo;
    if n >= 0 then begin
      with StringGrid1 do begin
        r := n div 16 + 1;
        c := n mod 16 + 1;
        if (r > 0) and (c > 0) and (r < RowCount) and (c < ColCount) then begin
          OnClick := nil;
          Row := r;
          Col := c;
          OnClick := StringGrid1Click;
        end;
      end;
    end;
  end
  else begin
    with edt do begin
      n := OctToIntDef(Copy(Text, 2), 0);
      Dec(n);
      if n < 0 then n := 0;
      Text := Copy(Text, 1, 1) + IntToOct(n, 3);
      Font.Color := RGB($FF, $A5, $00);
    end;
    n := n - GB_DeviceStartNo;
    if n >= 0 then begin
      with StringGrid1 do begin
        r := n div 8 + 1;
        c := n mod 8 + 1;
        if (r > 0) and (c > 0) and (r < RowCount) and (c < ColCount) then begin
          OnClick := nil;
          Row := r;
          Col := c;
          OnClick := StringGrid1Click;
        end;
      end;
    end;
  end;
end;

procedure TForm4.SpeedButton5Click(Sender: TObject);
begin
  Form1.ShowModal;
end;

procedure TForm4.StringGrid1Click(Sender: TObject);
// 出力先
var
  s : string;
begin
  with StringGrid1 do begin
    if not GB_fxFlag or (GB_fxFlag and (Col <= 8)) then begin
      s := IntToHex(StrToIntDef('$'+Cells[0, Row], 0) + StrToIntDef('$'+Cells[Col, 0], 0), 3);
      Edit11.Text := GetDeviceComment(Cells[0, 0] + s);
      with Edit8 do begin
        Text := 'Y' + s;
        Font.Color := RGB($FF, $A5, $00);
      end;
      with Edit6 do begin
        Text := 'X' + s;
        Font.Color := RGB($FF, $A5, $00);
      end;
    end;
  end;
end;

procedure TForm4.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  ARect : TRect;
  s : string;
  scale : double;
  flag : boolean;
  n : integer;
begin
  flag := False;
  scale := GB_SgScale;

  ARect := Rect;

  ARect.Top := Rect.Top + 1;
  ARect.Bottom := Rect.Bottom - 1;
  ARect.Left := Rect.Left + 1;
  ARect.Right := Rect.Right - 1;

  with StringGrid1 do begin
    s := Cells[ACol, ARow];
    if (ARow = 0) or (ACol = 0) then begin
      if (ARow = 0) and (ACol = 0) then begin
        Canvas.Brush.Color := clLime;
        Canvas.FillRect(Rect);
        Canvas.Font.Height := Trunc(20 * scale);
        Canvas.Font.Color := clBlack;
      end
      else begin
        Canvas.Brush.Color := clSilver;
        Canvas.FillRect(Rect);
        Canvas.Font.Height := Trunc(20 * scale);
        Canvas.Font.Color := clGray;
      end;
      DrawText(Canvas.Handle, PChar(s), Length(s), ARect, DT_CENTER);
    end
    else begin
      if (Edit4.Text = 'ON') or (Edit4.Text = 'OFF') then begin
        if not GB_fxFlag then
          n := StrToIntDef('$' + Copy(Edit3.Text, 2), -1)
        else
          n := OctToIntDef(Copy(Edit3.Text, 2), -1);

        if n >= GB_DeviceStartNo then begin
          n := n - GB_DeviceStartNo;
          if (not GB_fxFlag and (ARow = n div 16 + 1) and (ACol = n mod 16 + 1))  or
            (GB_fxFlag and (ARow = n div 8 + 1) and (ACol = n mod 8 + 1)) then begin
            flag := True;
            if Edit4.Text = 'ON' then begin
              Canvas.Brush.Color := clRed;
              Canvas.FillRect(ARect);
              Canvas.Font.Height := Trunc(20 * scale);
              Canvas.Font.Color := clWhite;
            end
            else begin
              Canvas.Brush.Color := clLime;
              Canvas.FillRect(ARect);
              Canvas.Font.Height := Trunc(20 * scale);
              Canvas.Font.Color := clBlack;
              s := Copy(Edit3.Text, 2);
            end;
          end;
        end;
      end;
      if not flag and (s <> '') then begin
        Canvas.Brush.Color := RGB($FF, $A5, $00);
        Canvas.FillRect(ARect);
        Canvas.Font.Height := Trunc(20 * scale);
        Canvas.Font.Color := clWhite;
      end;
      if s <> '' then
        DrawText(Canvas.Handle, PChar(s), Length(s), ARect, DT_CENTER);
    end;
  end;
end;

procedure TForm4.StringGrid2KeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  SgKeyDown(Sender as TStringGrid, Key, Shift);
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
  i, j, devN : integer;
  lSize : integer;
  szDevice : WideString;
  s : string;
  flag : boolean;
  SpeechFlag : boolean;
begin
  SpeechFlag := False;
  Timer1.Enabled := False;
  with ActUtlType1 do begin
    // CPU Type
    if Edit9.Text = '' then begin
      GetCpuType(szDevice, lSize);
      Edit9.Text := szDevice;
      // FX であるか
      flag := Pos('FX', Edit9.Text) = 1;
      if flag <> GB_fxFlag then begin
        CheckBox1.Checked := flag;
        CheckBox1Click(self);
      end;
    end;
    if GB_fxFlag then devN := 8
    else devN := 16;

    lSize := 1; // 1 ワード(ビットデバイスでは16点)
    for i := 0 to 7 do begin
      WordAryNew[i] := 0;
      if not GB_fxFlag then
        szDevice := GB_DeviceHead + (GB_DeviceStartNo + i * 16).ToHexString
      else
        szDevice := GB_DeviceHead + IntToOct(GB_DeviceStartNo + i * 16, 4);
      if ReadDeviceBlock2(szDevice, lSize, WordAryNew[i]) <> 0 then begin
        WordAryNew[i] := 0;
        break;
      end;
    end;
    // 内部データに格納  8 x 16 = 128 個
    // FX の時グリッド表示は、 8 x 8 = 64 個
    for i := 0 to 7 do begin
      for j := 0 to 15 do
        BitAryNew[i * 16 + j] := WordAryNew[i] and IntPower(2, j) > 0;
    end;
    for i := 0 to 128 - 1 do begin
      if not GB_fxFlag then
        s := IntToHex(GB_DeviceStartNo + i, 3)
      else
        s := IntToOct(GB_DeviceStartNo + i, 3);

      if BitAryNew[i] and not BitAryOld[i] then begin
        Edit3.Text := GB_DeviceHead + s;
        Edit3.Font.Color := clRed;
        Edit4.Text := 'ON';
        Edit4.Font.Color := clRed;
        StringGrid1.Repaint;
        Edit11.Text := GetDeviceComment(Edit3.Text);
        SpeechFlag := True;
      end
      else if not BitAryNew[i] and BitAryOld[i] then begin
        Edit3.Text := GB_DeviceHead + s;
        Edit3.Font.Color := clLime;
        Edit4.Text := 'OFF';
        Edit4.Font.Color := clLime;
        StringGrid1.Repaint;
        Edit11.Text := GetDeviceComment(Edit3.Text);
        SpeechFlag := True;
      end;
      with StringGrid1 do begin
        if  BitAryNew[i] then begin
          if Cells[i mod devN + 1, i div devN + 1] <> s then
            Cells[i mod devN + 1, i div devN + 1] := s ;
        end
        else Cells[i mod devN + 1, i div devN + 1] := '';
      end;
    end;
    BitAryOld := BitAryNew;
    if Label5.Caption <> '■' then Label5.Caption := '■'
    else Label5.Caption := '□';
    // テキストスピーチ
    if CheckBox2.Checked and SpeechFlag and TTSFlag then begin
      Application.ProcessMessages;
      s := Copy(Edit3.Text, 1,1) {+ #13} + NumToSpeechText(Copy(Edit3.Text, 2));
      if CheckBox3.Checked then s := s + Edit11.Text + #13;
      if Edit4.Text = 'ON' then s := s + 'オンン'
      else s := s + 'オフ';
      SpVoice.Speak(s, SVSFDefault);
    end;

  end;
  Timer1.Enabled := True;
end;

end.

// ---------------------------------------------------------------
// Android 側
// 
// TextToSpeech :
// Androidapi.JNI.TTS, AndroidAPI.JNIBridge は、GitHub よりダウンロード
// https://github.com/jimmckeeth/FireMonkey-Android-Voice/tree/master/JNIBridge
// ---------------------------------------------------------------
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;
type
  TBitAry = array [0..127] 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;
    Rectangle6: TRectangle;
    Label8: TLabel;
    Rectangle7: TRectangle;
    Label9: TLabel;
    Rectangle8: TRectangle;
    Label10: TLabel;
    Button1: TButton;
    Label11: TLabel;
    Rectangle9: TRectangle;
    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;

    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;

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 進数 -> 10進数
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;

// 10 進数 -> 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 GCMDMODE := cmdSCCONNECT;
              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] = Ord(#10)) 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, 'MXC4_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;
  lbl : TLabel;
begin
  if Sender as TButton = Button6 then
    lbl := Label3
  else
    lbl := Label8;

  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    ATimeout := 250;
    AData := TEncoding.ANSI.GetBytes('WR ' + lbl.Text + ' 1' + #13#10);
    // 送信
    ASocket.SendData(AData);
    // 受信
    res := ASocketReceiveData(ASocket, ATimeout);
    with lbl.TextSettings do begin
      if res = 'ON' then FontColor := TAlphaColorRec.Red
      else if res = 'OFF' then FontColor := TAlphaColorRec.Lime
      else FontColor := TAlphaColorRec.White;
    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 7 do Cells[0, i] := (i * 16).ToHexString(3)
    else
      for i := 0 to 7 do Cells[0, i] := IntToOct(i * 8, 3);
    Row := 0;
    Col := 1;
  end;
  with ComboBox2 do begin
    BeginUpdate;
    Items.Clear;
    if not GB_fxFlag then
      for i := 0 to 255 do Items.Add(IntToHex(i * 32, 3))
    else
      for i := 0 to 255 do Items.Add(IntToOct(i * 16, 3));
    EndUpdate;
    ItemIndex := 0;
  end;
  // X に戻す
  with ComboBox1 do begin
    ItemIndex := 0;
  end;
  Label8.Text := 'X000';
  Label3.Text := 'Y000';
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 127 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 * 32, 4)
        else
          s2 := IntToHex(ItemIndex * 16, 4);
      end;
    end;

    AData := TEncoding.ANSI.GetBytes('CF ' + 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, 'MXC4_IO.ini'), TEncoding.UTF8);
  with IniFile do begin
    try
      BtDeviceHead := ReadString('Target', 'PCName', '');
    finally
      Free;
    end;
  end;
  // TTS
  InitTTS;

  // 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 : integer;
  lbl : TLabel;
begin
  if Sender as TRectangle = Rectangle1 then lbl := Label3
  else lbl := Label8;
  if not GB_fxFlag then begin
    n := StrToIntDef('$' + Copy(lbl.Text, 2), 0);
    Inc(n);
    with lbl 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(lbl.Text, 2), 0);
    Inc(n);
    with lbl 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.Rectangle2Click(Sender: TObject);
// [ - ]
var
  n : integer;
  lbl : TLabel;
begin
  if Sender as TRectangle = Rectangle2 then lbl := Label3
  else lbl := Label8;
  if not GB_fxFlag then begin
    n := StrToIntDef('$' + Copy(lbl.Text, 2), 0);
    Dec(n);
    if n < 0 then n := 0;
    with lbl 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(lbl.Text, 2), 0);
    Dec(n);
    if n < 0 then n := 0;
    with lbl 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 := 'Y'+ n.ToHexString(3);
      TextSettings.FontColor := TAlphaColorRec.Orange;
    end;
    with Label8 do begin
      Text := 'X'+ 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 : integer;
  Ticks : Cardinal;
  j : integer;
  s, s1 : string;
  n, idx : integer;
  flag : boolean;
  fxFlag : boolean;
  devN, devM : integer;
  ttsFlag : boolean;
begin
  ttsFlag := False;
  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);
        Label7.Text := res;
        flag := res <> '';
        fxFlag := (Pos('FX', Label7.Text) = 1) or (res = '');
        if GB_fxFlag <> fxFlag then begin
          CheckBox1.IsChecked := fxFlag;
          CheckBox1Change(self);
          // GB_fxFlag := fxFlag; // CheckBox1Chenge イベントに含まれる
        end;
      end
      else begin
        if GB_fxFlag then begin
          devN := 8;
          devM := 64;
        end
        else begin
          devN := 16;
          devM := 128;
        end;

        if Flag then begin
          // デバイス一括読み出しコマンド
          AData := TEncoding.ANSI.GetBytes('RD' + #13#10);
          // 送信
          ASocket.SendData(AData);
          // 受信
          res := ASocketReceiveData(ASocket, ATimeout);
          flag := res <> '';
          // データ格納
          if res.Length >= 64 then begin
            for i := 0 to 7 do begin
              s := Copy(res, i * 4 + 1, 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 + 1 + 32, 4);
              n := StrToIntDef('$' + s, 0);
              for j := 0 to 15 do
                BitAryOld[i * 16 + j] := n and IntPower(2, j) > 0;
            end;
            s := Copy(res, 66); // スペース1個ある
            if s <> '' then begin
              n := Pos(' ', s);
              if n > 0 then begin
                // デバイス番号
                s1 := Copy(s, 1, n - 1);
                s := Copy(s, n + 1);
                n := Pos(' ', s);
                if n = 0 then begin
                  idx := StrToIntDef('$' + s, 0);
                  Label11.Text := ''; // コメント
                end
                else begin
                  // 先頭デバイス番号(PC からの応答は 16 進表記)
                  idx := StrToIntDef('$' + Copy(s, 1, n - 1), 0);
                  // コメント
                  Label11.Text := Copy(s, n + 1);
                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 32
                      else
                        ItemIndex := GB_DeviceStartIndex div 16;
                    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 7 do
                        Cells[0, i] := (GB_DeviceStartIndex + i * 16).ToHexString(3);
                    end
                    else begin
                      for i := 0 to 7 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
                    Label8.Text := 'X' + IntToHex(GB_DeviceStartIndex, 3);
                    Label3.Text := 'Y' + IntToHex(GB_DeviceStartIndex, 3);
                  end
                  else begin
                    Label8.Text := 'X' + IntToOct(GB_DeviceStartIndex, 3);
                    Label3.Text := 'Y' + IntToOct(GB_DeviceStartIndex, 3);
                  end;
                end;
              end;
            end;
          end;
        end;
        // 表示
        with StringGrid1 do begin
          for i := 0 to devM -1 do begin
            if BitAryNew[i] then begin
              s := (i mod devN).ToHexString(1);
              if Cells[i mod devN + 1, i div devN] <> s then
                Cells[i mod devN + 1, i div devN] := s ;
            end
            else begin
              if Cells[i mod devN + 1, i div devN] <> '' then
                Cells[i mod devN + 1, i div devN] := '';
            end;
          end;
        end;
        // 比較
        // 内部データ数 = 128, FX は先頭 64 データのみ表示される
        for i := 0 to 128 -1 do begin
          idx := i + GB_DeviceStartIndex;
          if BitAryNew[i] and not BitAryOld[i] then begin
            Rectangle4.Fill.Color := TAlphaColorRec.Red;
            with Label1 do begin
              if not GB_fxFlag then
                Text := GB_DeviceName + idx.ToHexstring(3)
              else
                Text := GB_DeviceName + IntToOct(idx, 3);
              TextSettings.FontColor := TAlphaColorRec.White;
            end;
            Rectangle5.Fill.Color := TAlphaColorRec.Red;
            with Label2 do begin
              Text := 'ON';
              TextSettings.FontColor := TAlphaColorRec.White;
            end;
            ttsFlag := True;
          end
          else if not BitAryNew[i] and BitAryOld[i] then begin
            Rectangle4.Fill.Color := TAlphaColorRec.Black;
            with Label1 do begin
              if not GB_fxFlag then
                Text := GB_DeviceName + idx.ToHexstring(3)
              else
                Text := GB_DeviceName + IntToOct(idx, 3);
              TextSettings.FontColor := TAlphaColorRec.Lime;
            end;
            Rectangle5.Fill.Color := TAlphaColorRec.Black;
            with Label2 do begin
              Text := 'OFF';
              TextSettings.FontColor := TAlphaColorRec.Lime;
            end;
            ttsFlag := True;
          end;
          if ttsFlag then begin
            s :=Copy(Label1.Text, 1, 1) + #13 + NumToSpeechText(Copy(Label1.Text, 2));
            if Switch1.IsChecked then
              s := s + '。' + Label11.Text;
            if Label2.Text = 'ON' then s := s + '。' + 'オン'
            else s := s + '。' + 'オフ';
            SpeakOut(s);
          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.