azbil SDC/R を Android スマホで動作チェック (2019/03/20)

 azbil 調節計 SDC/R 35 シリーズの現場チェックを Android スマホで行います。
 
 SDC のローダーコネクタに Bluetooth アダプターを接続し、Bluetooth 経由で Android と CPL 通信をしています。
 ・PV、SP、OUT、MFB の表示のほか、設定変更、手動出力が行えます。
 ・PV、MFB の値を音声合成で読み上げます。

  

■Bluetooth アダプタ
 ・自作の場合
  マイクロテクニカの「Bluetooth - シリアルモジュール RBT-001」 + 「電圧レベル変換アダプタ 80FG990」 (合計¥4,630)を使います。
  レベル確保のため、SDC へはバッファ IC 74HC4050 を通して接続ます。接続図はこちらです。
  SDC への接続は、富士パーツ商会の3.5mm -> 2.5mm 変換プラグ AD-SPS-06 または、AD-SPS-12 を使っています。(ゴムキャップ切れなし)

  RBT-001 のツールで、通信設定を 19200BPS、Even に変更。 No events reported, no UART break に変更します。
  接続先の名前は、「EasyBT」です。

  
  ↑+5V は USB 給電です。

  ※スイッチサイエンスでも同様のものが販売されています。
   「Bluetooth モジュール RBT-001」+「RBT-001用シリアルレベルコンバータ Rev.3」 (合計¥7,044.)

 ・市販品の場合
  ラトックシステムの「RS232C-Bluetooth 変換アダプタ REX-BT60」と azbil 純正のローダーケーブル (RS232C) を使います。
  REX-BT60 のツールで、通信設定を 19200BPS、8Bits、OneStopBit、Even にします。
  Android アプリ 「BtSerialUtility」 で設定変更がうまくいかない場合は、Windows の「BtSerialUtility_com.exe」 を使います。
  接続先の名前は、「RN42-」 または、「RNBT-」 で始まります。
  REX-BT60 とローダーケーブル (RS232C) を直結するだけでつながります。
  REX-BT60 の電源ケーブルは、USB 給電タイプのものを使うとモバイルバッテリーで使用できます。

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

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

■ダウンロード
 ・andC35.apk (apk 本体のみ)
 起動前にペアリングしておいてください。
 初回起動時は、エラーになります。一覧から接続先を指定し、「保存」ボタンをタップし、終了して下さい。
 接続できない場合は、一度終了し再度起動してみて下さい。


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,
  AndroidApi.JNI.Media;

type
  TBtThread = class(TThread)
  private
    { Private 宣言 }
    procedure BtOpen;
  protected
    procedure Execute; override;
  public
    constructor Create; virtual;
  end;
type
  TForm4 = class(TForm)
    Timer1: TTimer;
    ScaledLayout1: TScaledLayout;
    MOUT: TButton;
    Rectangle1: TRectangle;
    Label4: TLabel;
    Rectangle2: TRectangle;
    Label5: TLabel;
    Rectangle3: TRectangle;
    Label3: TLabel;
    Rectangle4: TRectangle;
    Label1: TLabel;
    Rectangle5: TRectangle;
    Label2: TLabel;
    Label7: TLabel;
    Rectangle9: TRectangle;
    ComboBox3: TComboBox;
    Button2: TButton;
    Switch1: TSwitch;
    Rectangle6: TRectangle;
    Label8: TLabel;
    Rectangle7: TRectangle;
    Label9: TLabel;
    Rectangle8: TRectangle;
    Label6: TLabel;
    Rectangle10: TRectangle;
    Label10: TLabel;
    Rectangle11: TRectangle;
    Label12: TLabel;
    SP: TButton;
    Label14: TLabel;
    Label15: TLabel;
    Label16: TLabel;
    Label17: TLabel;
    Label11: TLabel;
    Rectangle12: TRectangle;
    Label19: TLabel;
    Button1: TButton;
    Rectangle13: TRectangle;
    Label18: TLabel;
    Rectangle14: TRectangle;
    Label13: TLabel;
    Rectangle15: TRectangle;
    Label20: TLabel;
    Label21: TLabel;
    Rectangle16: TRectangle;
    Rectangle17: TRectangle;
    Rectangle18: TRectangle;
    Rectangle19: TRectangle;
    Rectangle20: TRectangle;
    Rectangle21: TRectangle;
    Rectangle22: TRectangle;
    Label22: TLabel;
    Label23: TLabel;
    Label24: TLabel;
    Label25: TLabel;
    Label26: TLabel;
    Label27: TLabel;
    Label28: TLabel;
    Switch2: TSwitch;
    Label29: TLabel;
    Label30: TLabel;
    procedure MOUTClick(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Rectangle7Click(Sender: TObject);
    procedure SPClick(Sender: TObject);
    procedure Rectangle14Click(Sender: TObject);
    procedure Button1Click(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 宣言 }
    decScale : Double;
    decPt : integer;
    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;
  TTScnt : integer;

  BtDeviceHead : string;
  // uses ... Androidapi.JNIBridge, AndroidApi.JNI.Media;
  ToneGenerator: JToneGenerator;
const
  // SPP(Serial Port Profile) による通信のUUID
  ServiceUUID = '{00001101-0000-1000-8000-00805F9B34FB}';

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

  STX  = #$02;
  ETX  = #$03;
  CRLF = #$0D#$0A;

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;

// CPL 通信の文字列を作成
function MakeCPLCommandStr(const cmd : string): string;
var
  i : integer;
  s : string;
  sum : integer;
  chksum : string;
  AData : TBytes;
begin
  s := '';
  s := STX;      // 電文先頭
  s := s + '01'; // 機器アドレス
  s := s + '00'; // サブアドレス(未使用)
  s := s + 'X' ; // デバイス区別コード 'X' または、'x'
  s := s + cmd ; // コマンド
  s := s + ETX ; // アプリケーション層の終了位置

  // チェックサムの作成
  // STX ~ ETX までを1バイトずつ加算
  sum := 0;
  AData := TEncoding.ANSI.GetBytes(s);
  for i := 0 to Length(AData) -1 do
    sum := sum + Adata[i];
  // 加算結果の下位1バイト
  sum := sum and $000000FF;
  // 2の補数
  sum := (- sum and $000000FF);
  // それを2バイトの ASCII コードに変換
  chksum := IntToHex(sum, 2);

  s := s + chksum; // チェックサム
  s := s + CRLF;   // 電文の最後
  result := s;
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
        // 過去にペアリングされたデバイスの一覧から、ターゲット を探す
        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;
// SDC からの応答
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 (AData[i] = $03)} 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.SPClick(Sender: TObject);
// SP 変更
var
  AData : TBytes;
  res, cmd, s : string;
  ATimeout: Cardinal;
  d : double;
begin
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    ATimeout := 250;
    d := StrToFloatDef(Label8.Text, 0);
    cmd := 'WD238E' + IntToHex(Trunc(d * IntPower(10, decPt)), 4);
    s := MakeCPLCommandStr(cmd);
    AData := TEncoding.ANSI.GetBytes(s);
    // 送信
    ASocket.SendData(AData);
    // 受信
    res := ASocketReceiveData(ASocket, ATimeout);
    if Copy(res, 6, 2) = '00' then
      ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
    else
      ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
    Timer1.Enabled := True;
  end;
end;

procedure TForm4.Button1Click(Sender: TObject);
// Auto / Manual 反転
var
  AData : TBytes;
  res, cmd, s : string;
  ATimeout: Cardinal;
begin
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    ATimeout := 250;
    if Pos('AUTO', Label5.Text) > 0 then begin
      cmd := 'WU00'+ '3904' +  '0001' + '3902' + IntToHex(Trunc(StrToFloatDef(Label19.Text, 0) * 10), 4);
    end
    else cmd := 'WD3904' + '0000';
    s := MakeCPLCommandStr(cmd);
    AData := TEncoding.ANSI.GetBytes(s);
    // 送信
    ASocket.SendData(AData);
    // 受信
    res := ASocketReceiveData(ASocket, ATimeout);
    if Copy(res, 6, 2) = '00' then
      ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
    else
      ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
    Timer1.Enabled := True;
  end;
end;

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

procedure TForm4.MOUTClick(Sender: TObject);
// Manual OUT
var
  AData : TBytes;
  res, cmd, s : string;
  ATimeout: Cardinal;
  d : double;
begin
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    ATimeout := 250;
    d := StrToFloatDef(Label19.Text, 0);
    cmd := 'WD3902' + IntToHex(Trunc(d * 10), 4);
    s := MakeCPLCommandStr(cmd);
    AData := TEncoding.ANSI.GetBytes(s);
    // 送信
    ASocket.SendData(AData);
    // 受信
    res := ASocketReceiveData(ASocket, ATimeout);
    if Copy(res, 6, 2) = '00' then
      ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
    else
      ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
    Timer1.Enabled := True;
  end;
end;

procedure TForm4.FormCreate(Sender: TObject);
var
  IniFile: TMemIniFile;   // uses .... System.IniFiles;
begin
  Label1.Text := '';
  Label2.Text := '';
  Label3.Text := '';
  Label4.Text := '';
  Label5.Text := '';
  Label6.Text := '';
  Label7.Text := '';
  Label8.Text := '';
  Label19.Text := '';

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

  // use ..... System.IOUtils;

  IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine(
    System.IOUtils.TPath.GetDocumentsPath, 'azbilSDC.ini'), TEncoding.UTF8);
  with IniFile do begin
    try
      BtDeviceHead := ReadString('Target', 'DeviceName', 'EasyBT');
    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;

end;

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

procedure TForm4.Rectangle14Click(Sender: TObject);
// Manual OUT UP / DOWN
var
  mout : double;
  d : double;
begin
  mout := StrToFloatDef(Label19.Text, 0);
  if Sender as TRectangle = Rectangle14 then
    d := StrToFloatDef(Label13.Text, 0)
  else
    d := StrToFloatDef(Label20.Text, 0);
  mout := mout + d;
  if mout < 0 then mout := 0
  else if mout > 100 then mout := 100;
  Label19.Text := Format('%.1f', [mout]);
end;

procedure TForm4.Rectangle7Click(Sender: TObject);
// SP UP / DOWN
var
  sp : double;
  d : double;
  fmt : string;
begin
  fmt := '%.' + decPt.ToString + 'f';
  sp := StrToFloatDef(Label8.Text, 25);

  if Sender as TRectangle = Rectangle7 then
    d := StrToFloatDef(Label9.Text, 0)
  else if Sender as TRectangle = Rectangle10 then
    d := StrToFloatDef(Label10.Text, 0)
  else if Sender as TRectangle = Rectangle11 then
    d := StrToFloatDef(Label12.Text, 0)
  else
    d := StrToFloatDef(Label18.Text, 0);
  sp := sp + d;
  Label8.Text := Format(fmt, [sp]);
end;

procedure TForm4.Timer1Timer(Sender: TObject);
var
  ATimeout : Cardinal;
  AData : TBytes;
  i : integer;
  s, cmd, rcv : string;
  fmt : string;
  Ticks : Cardinal;
  sw : integer;
begin
  if not ((GCMDMODE = cmdSCCONNECT) and ASocket.Connected) then begin
    Inc(OpenMsecCnt);
    Label7.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;
    Timer1.Enabled := False;
    Ticks := TThread.GetTickCount;
    ATimeout := 250;

    cmd := 'RU00' +'1454'+ '3814' + '3815' + '3816' + '3851' + '3811' + '3902';
    s := MakeCPLCommandStr(cmd);
    AData := TEncoding.ANSI.GetBytes(s);
    // 送信
    ASocket.SendData(AData);
    // 受信
    rcv := ASocketReceiveData(ASocket, ATimeout);
    if (Copy(rcv, 6, 2) = '00') and (Length(Copy(rcv, 8)) >= 28) then begin
      for i := 0 to 6 do begin
        s := Copy(rcv, 8 + i * 4, 4);
        if i = 0 then begin
          decPt := StrToIntDef('$' + s, 0);
          decScale := 1 / IntPower(10, decPt);
          if decPt > 0 then begin
            Label18.Text := '+0.5';
            Label9.Text := '-0.5';
          end
          else begin
            Label18.Text := '+1';
            Label9.Text := '-1';
          end;
        end
        else begin
          fmt := '%.' + decPt.ToString + 'f ';
          case i of
            // PV
            1: Label1.Text := Format(fmt, [StrToInt('$' + s) * decScale]);
            // SP
            2: begin
                 s := Format(fmt, [StrToInt('$' + s) * decScale]);
                 if (Label2.Text = '') and (Label8.Text = '') then Label8.Text := Trim(s);
                 Label2.Text := s;
               end;
            // OUT
            3: Label3.Text := Format('%.1f ', [StrToInt('$' + s) * 0.1]);
            // MFB
            4: Label4.Text := Format('%.1f ', [StrToInt('$' + s) * 0.1]);
            // Auto / Man
            5: if StrToInt('$' + s) = 1 then begin
                 if Label5.Text <> 'MANUAL ' then begin
                   Label5.Text := 'MANUAL ';
                   Label5.TextSettings.FontColor := claRed;
                   Label6.TextSettings.FontColor := claRed;
                   Label3.TextSettings.FontColor := claRed;
                 end;
               end
               else begin
                 if Label5.Text <> 'AUTO ' then begin
                   Label5.Text := 'AUTO ';
                   Label5.TextSettings.FontColor := claSpringgreen;
                   Label6.TextSettings.FontColor := claSpringgreen;
                   Label3.TextSettings.FontColor := claSpringgreen;
                 end;
               end;
            // MAN Out
            6: begin
                 s := Format('%.1f ', [StrToInt('$' + s) * 0.1]);
                 if Label19.Text = '' then Label19.Text := Trim(s);
                 Label6.Text := s;
               end;
          end;
        end;
      end;
    end;

    cmd := 'RD' +'23F4'+ '0002';
    s := MakeCPLCommandStr(cmd);
    AData := TEncoding.ANSI.GetBytes(s);
    // 送信
    ASocket.SendData(AData);
    // 受信
    rcv := ASocketReceiveData(ASocket, ATimeout);
    if (Copy(rcv, 6, 2) = '00') and (Length(Copy(rcv, 8)) >= 8) then begin
      for i := 0 to 1 do begin
        sw := StrToIntDef('$' + Copy(rcv, 8 + i * 4, 4), 0);
        if i = 0 then begin
          // MANUAL mode
          with Label22.TextSettings do begin
            if sw and 1 = 1 then begin
              if FontColor <> claSpringgreen then FontColor := claSpringgreen;
            end
            else begin
              if FontColor <> claGray then FontColor := claGray;
            end;
          end;
          // RSP mode
          with Label23.TextSettings do begin
            if sw and 4 = 4 then begin
              if FontColor <> claSpringgreen then FontColor := claSpringgreen;
            end
            else begin
              if FontColor <> claGray then FontColor := claGray;
            end;
          end;
        end
        else begin
          // OUT 1
          with Label27.TextSettings do begin
            if sw and 1 = 1 then begin
              if FontColor <> claSpringgreen then FontColor := claSpringgreen;
            end
            else begin
              if FontColor <> claGray then FontColor := claGray;
            end;
          end;
          // OUT 2
          with Label28.TextSettings do begin
            if sw and 2 = 2 then begin
              if FontColor <> claSpringgreen then FontColor := claSpringgreen;
            end
            else begin
              if FontColor <> claGray then FontColor := claGray;
            end;
          end;
          // EV1
          with Label24.TextSettings do begin
            if sw and 4 = 4 then begin
              if FontColor <> claRed then FontColor := claRed;
            end
            else begin
              if FontColor <> claGray then FontColor := claGray;
            end;
          end;
          // EV2
          with Label25.TextSettings do begin
            if sw and 8 = 8 then begin
              if FontColor <> claRed then FontColor := claRed;
            end
            else begin
              if FontColor <> claGray then FontColor := claGray;
            end;
          end;
          // EV3
          with Label26.TextSettings do begin
            if sw and 16 = 16 then begin
              if FontColor <> claRed then FontColor := claRed;
            end
            else begin
              if FontColor <> claGray then FontColor := claGray;
            end;
          end;
        end;
      end;
    end;
    Label7.Text := IntToStr(TThread.GetTickCount - Ticks) + ' msec';

    // テキストスピーチ
    Inc(TTScnt);
    if TTScnt > 10 then begin
      TTScnt := 0;
      s := '';
      if Switch1.IsChecked then s := s + 'PV ' + Label1.Text + #13;
      if Switch2.IsChecked then s := s + 'MFB ' + Label4.Text + ' ';
      if s <> '' then SpeakOut(s);
      end;
    Timer1.Enabled := True;
  end;
end;

end.