Bluetooth ワイヤレス I/O (その1) 2019/07/02

 エレファイン (ELEFINE) で販売されている 「BluetoothRelay2」 を使ってみました。
 「BluetoothRelay2」 は、Bluetooth モジュール (RN42) と PIC (PIC18F14K50)、DC12V リレー駆動用のドライバ IC (ULN2003) で構成されています。
 設定でいろいろな用途に使える入出力 GPIO (General Purpose Input/Output) 8 点+リレー出力 2 点が使えます。
 GPIO は、あらかじめ割り付けするのではなく、送信したコマンドに応じて、DI (デジタル入力)、DO (デジタル出力)、ADC (アナログ入力) に変わります。
 Bluetooth Classic なので、iOS での使用はできません。こちらでは、Android スマホで使用しています。


 GPIO は 10kΩ 程度でプルアップしておく必要があります。USB 給電の時は、DC12Vリレーは使用できません。
 動作電流は、DC12V 給電の場合で 通常 50mA 程度 + リレー駆動 35mA/1 個で、リレー 2 個 ON の場合は最大 110mA でした。
 市販の USB 5V->12V ケーブルでも使えそうです。
 通信距離は、見通し ギリ 10m 程度。アンテナ廻りの基板、USB コネクタが影響するのか、他の RN42 使用のものより通信距離が若干も短い印象です。

■ Windows で使う
 Windows でペアリングを行うと、仮想COMポートが2つ作成されますが、名前に 'SPP' がある発信側を使います。
 ↓の例では、名前が「RNBT-???? 'RNI-SPP'」の 「COM20:発信」がそれです。

 

 デバイスマネージャーだと分かりにくいですね。
 

 通信プロパティーを確認(変更)しておきます。
 
 
 通信はごく一般的なシリアル通信です。コマンドを送信すると、必ず返信があります。
 ↓の例では、GPIO 1番めのデジタル入力 (DI 0) の状態を読み込んでいます。
 'gpio read 0' + CR を送ると、'gpio read 0' + LF + CR + '0' + LF + CR + '>' が返ってきます。 '0' が 値 (LOW)、 '>' が終端です。
    

■ Android で使う 
 ペアリング済の Bluetooth 端末をリストアップします。名前が 「RNBT-」 で始まる RN42 を見つけて接続します。
 あとは、SPP(Serial Port Profile) を使った通信で、コマンド送信、返信を受信します。
 下記アプリでは、約 1 秒周期で DI 4 点、ADC 2 点、リレー 2 点の状態を取得しています。

 

■ダウンロード

 BTGPIO.apk (Android サンプルアプリケーション apk 本体のみ)
 ※本アプリの著作権は作者 f.izawa が所有し、これを主張します。
 ※本アプリをインストール、使用したことによる事故、損害等の一切について作者はその責を負いません。ご自身の責任において使用してください。

■作者連絡先

 e-mail : f.izawa@dream.com (@は小文字に)
 URL : http://www.izawa-web.com/


// Android サンプル Delphi 10.2.3

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;
    Label7: TLabel;
    ComboBox3: TComboBox;
    Button2: TButton;
    Switch1: TSwitch;
    Rectangle8: TRectangle;
    Label6: 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;
    Label29: TLabel;
    Rectangle1: TRectangle;
    CornerButton1: TCornerButton;
    CornerButton2: TCornerButton;
    CornerButton3: TCornerButton;
    CornerButton4: TCornerButton;
    Rectangle2: TRectangle;
    Label1: TLabel;
    Rectangle3: TRectangle;
    Label2: TLabel;
    CornerButton6: TCornerButton;
    CornerButton7: TCornerButton;
    CornerButton8: TCornerButton;
    Rectangle5: TRectangle;
    Rectangle6: TRectangle;
    Rectangle4: TRectangle;
    CornerButton5: TCornerButton;
    Rectangle7: TRectangle;
    Rectangle9: TRectangle;
    Rectangle10: TRectangle;
    Rectangle11: TRectangle;
    Rectangle12: TRectangle;
    Rectangle13: TRectangle;
    Rectangle23: TRectangle;
    Label3: TLabel;
    Rectangle24: TRectangle;
    Rectangle25: TRectangle;
    CornerButton9: TCornerButton;
    CornerButton10: TCornerButton;
    Rectangle26: TRectangle;
    Rectangle27: TRectangle;
    CornerButton11: TCornerButton;
    CornerButton12: TCornerButton;
    procedure Timer1Timer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure CornerButton1Click(Sender: TObject);
    procedure CornerButton5Click(Sender: TObject);
    procedure CornerButton9Click(Sender: TObject);
    procedure CornerButton11Click(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 宣言 }

    cmdMode, ioIndex : integer;
    decScale : Double;
    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;

  CRLF = #$0D#$0A;

implementation

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

{$R *.fmx}

// -----------------------------------------------------------------------------
// 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;
// BT からの返信データ
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);
        // 終端まで読む #$3E = '>'
        if (AData[i] = $3E) {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.CornerButton11Click(Sender: TObject);
// GPIO 4, 5 ON-OFF (1 パルス)
// GPIO PIN No. 5, 6 が対象
var
  ATimeout : Cardinal;
  AData : TBytes;
  btn : TCornerButton;
  cmd, res : string;
begin
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    ATimeout := 250;
    btn := Sender as TCornerButton;
    if btn = CornerButton11 then cmd := 'gpio set 4'
    else cmd := 'gpio set 5';
    AData := TEncoding.ANSI.GetBytes(cmd + #13);
    // 送信
    ASocket.SendData(AData);
    // 受信
    res := ASocketReceiveData(ASocket, ATimeout);
    if Pos(cmd, res) = 1 then begin
      Sleep(250);
      if btn = CornerButton11 then cmd := 'gpio clear 4'
      else cmd := 'gpio clear 5';
      AData := TEncoding.ANSI.GetBytes(cmd + #13);
      // 送信
      ASocket.SendData(AData);
      // 受信
      res := ASocketReceiveData(ASocket, ATimeout);
      if Switch1.IsChecked then begin
        // ブザー
        if Pos(cmd, res) = 1 then
          ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
        else
          ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
      end;
    end
    else if Switch1.IsChecked then
      ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
    Timer1.Enabled := True;
  end;
end;

procedure TForm4.CornerButton1Click(Sender: TObject);
// RELAY ON/OFF
var
  AData : TBytes;
  res, cmd : string;
  ATimeout: Cardinal;
  btn : TCornerButton;
begin
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    ATimeout := 250;
    btn := Sender as TCornerButton;
    if btn = CornerButton1 then cmd := 'relay on 0'
    else if btn = CornerButton2 then cmd := 'relay off 0'
    else if btn = CornerButton3 then cmd := 'relay on 1'
    else  cmd := 'relay off 1';
    AData := TEncoding.ANSI.GetBytes(cmd + #13);
    // 送信
    ASocket.SendData(AData);
    // 受信
    res := ASocketReceiveData(ASocket, ATimeout);
    if Switch1.IsChecked then begin
      // ブザー
      if Pos(cmd, res) = 1 then
        ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
      else
        ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
    end;
    Timer1.Enabled := True;
  end;
end;


procedure TForm4.CornerButton5Click(Sender: TObject);
// GPIO 4, 5 ON-OFF
// GPIO PIN No. 5, 6 が対象
var
  ATimeout : Cardinal;
  AData : TBytes;
  btn : TCornerButton;
  cmd, res : string;
begin
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    ATimeout := 250;
    btn := Sender as TCornerButton;
    if btn = CornerButton5 then cmd := 'gpio set 4'
    else if btn = CornerButton6 then cmd := 'gpio clear 4'
    else if btn = CornerButton7 then cmd := 'gpio set 5'
    else  cmd := 'gpio clear 5';
    AData := TEncoding.ANSI.GetBytes(cmd + #13);
    // 送信
    ASocket.SendData(AData);
    // 受信
    res := ASocketReceiveData(ASocket, ATimeout);
    if Switch1.IsChecked then begin
      // ブザー
      if Pos(cmd, res) = 1 then
        ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
      else
        ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
    end;
    Timer1.Enabled := True;
  end;
end;

procedure TForm4.CornerButton9Click(Sender: TObject);
// Relay ON/OFF(1 パルス)
var
  AData : TBytes;
  res, cmd : string;
  ATimeout: Cardinal;
  btn : TCornerButton;
begin
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    ATimeout := 250;
    btn := Sender as TCornerButton;
    if btn = CornerButton9 then cmd := 'relay on 0'
    else cmd := 'relay on 1';
    AData := TEncoding.ANSI.GetBytes(cmd + #13);
    // 送信
    ASocket.SendData(AData);
    // 受信
    res := ASocketReceiveData(ASocket, ATimeout);
    if Pos(cmd, res) = 1 then begin
      Sleep(250);
      if btn = CornerButton9 then cmd := 'relay off 0'
      else cmd := 'relay off 1';
      AData := TEncoding.ANSI.GetBytes(cmd + #13);
      // 送信
      ASocket.SendData(AData);
      // 受信
      res := ASocketReceiveData(ASocket, ATimeout);
      if Switch1.IsChecked then begin
        // ブザー
        if Pos(cmd, res) = 1 then
          ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
        else
          ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
      end;
    end
    else if Switch1.IsChecked then
      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, 'btgpio.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.FormCreate(Sender: TObject);
var
  IniFile: TMemIniFile;   // uses .... System.IniFiles;
begin
  // 縦画面に固定
  Application.FormFactor.Orientations :=
    [TFormOrientation.Portrait, TFormOrientation.InvertedPortrait];
  // use ..... System.IOUtils;
  IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine(
    System.IOUtils.TPath.GetDocumentsPath, 'btgpio.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.Timer1Timer(Sender: TObject);
var
  ATimeout : Cardinal;
  AData : TBytes;
  i : integer;
  cmd, rcv : string;
  Ticks : Cardinal;
  comp : TComponent;
  lbl : TLabel;
  n, m : integer;
  v : integer;
begin
  // BT 接続処理
  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 > 200 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;
    // RELAY 2 点の状態を取得
    for i := 0 to 1 do begin
      cmd := 'relay read ' + i.ToString;
      AData := TEncoding.ANSI.GetBytes(cmd + #13);
      // 送信
      ASocket.SendData(AData);
      // 受信
      rcv := ASocketReceiveData(ASocket, ATimeout);
      rcv := StringReplace(rcv, #$0A#$0D, ';', [rfReplaceAll]);

      comp := FindComponent('Label'+IntToStr(28 + i));
      if comp <> nil then begin
        lbl := TLabel(comp);
        if Pos(cmd, rcv) = 1 then begin
          if Pos('on', rcv) > 0 then lbl.TextSettings.FontColor := claRed
          else if Pos('off', rcv) > 0 then lbl.TextSettings.FontColor := claLime
          else lbl.TextSettings.FontColor := claYellow;
        end
        else
          lbl.TextSettings.FontColor := claWhite;
      end;
    end;

    // GPIO 0~3 の 4 点の状態を取得
    // GPIO PIN No. の 1~4
    // GPIO のピン番号 1~8 が、DI 0~7 に対応
    for i := 0 to 3 do begin
      cmd := 'gpio read ' + i.ToString;
      AData := TEncoding.ANSI.GetBytes(cmd + #13);
      // 送信
      ASocket.SendData(AData);
      // 受信
      rcv := ASocketReceiveData(ASocket, ATimeout);
      rcv := StringReplace(rcv, #$0A#$0D, ';', [rfReplaceAll]);
      comp := FindComponent('Label'+IntToStr(22 + i));
      if comp <> nil then begin
        lbl := TLabel(comp);
        if Pos(cmd, rcv) = 1 then begin
          // GPIO はプルアップしているため、Low(0) で ON
          if Copy(rcv, Length(rcv) - 2, 1) = '0' then lbl.TextSettings.FontColor := claRed
          else if Copy(rcv, Length(rcv) - 2, 1) = '1' then lbl.TextSettings.FontColor := claLime
          else lbl.TextSettings.FontColor := claYellow;
        end
        else
          lbl.TextSettings.FontColor := claWhite;
      end;
    end;

    // ADC 5,6 (= GPIO 6, 7)
    // GPIO PIN No. の 7, 8
    // GPIO のピン番号 2~8 が、ADC 0~6 に対応
    for i := 0 to 1 do begin
      cmd := 'adc read ' + (i + 5).ToString;
      AData := TEncoding.ANSI.GetBytes(cmd + #13);
      // 送信
      ASocket.SendData(AData);
      // 受信
      rcv := ASocketReceiveData(ASocket, ATimeout);
      rcv := StringReplace(rcv, #$0A#$0D, ';', [rfReplaceAll]);
      if Pos(cmd, rcv) = 1 then begin
        n := Pos(';', rcv);
        m := LastDelimiter(';', rcv);
        if (n > 0) and (m > n) then begin
          // 値
          v := StrToIntDef(Copy(rcv, n+1, m-n-1), 0);
          // バーグラフ
          if i = 0 then begin
            Label20.Text := v.ToString;
            RectAngle5.Width := Trunc(205 * (v / 1023));
          end
          else begin
            Label13.Text := v.ToString;
            RectAngle6.Width := Trunc(205 * (v / 1023));
          end;
        end;
      end
      // 受信文字列が正常でない
      else begin
        if i = 0 then begin
          Label20.Text := '';
          RectAngle5.Width := 0;
        end
        else begin
          Label13.Text := '';
          RectAngle6.Width := 0;
        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.