REX-BT60 を Delphi / Bluetooth で、KEYENCE KV 上位リンク通信 を試してみた (2017/ 1/ 5)

 2017/ 1/15 オプション - Bluetooth を追加
 2017/ 1/11 一部追加、スクリーンショットを更新
 2017/ 1/ 8  一部更新

■ラトックシステムの「Bluetooth - RS-232C 変換アダプター」 REX-BT60 を、Delphi 10.1 Berlin から使ってみました。

 Windows では、Bluetooth リンク経由の COM ポートを使うと Bluetooth を気にすることなく、通常のシリアル通信のプログラムで動きますが、
 Android での使用を考えて、Bluetooth 通信を試してみました。

 ※ iOS(iPhone, iPad)も SPP (Serial Port Profile) を搭載していますが、OS にてその機能が特殊な方法でしか利用できないようになっているため、通信できません。
 ※ Bluetoopth LE (Low Energy) との、互換性はありません。

 REX-BT60 は、+5V のACアダプターが必要ですが、D-Sub9pin コネクタのの 9 番ピンに供給することも可能です。(DIP スイッチの4 をON)
 KEYENCE KV nano / KV-3000 の内蔵シリアルポート(モジュラーコネクタ 6 ピン)の 1 、2 番ピンに、+5V が出力されています。
 これらを接続すると、ACアダプターは不要になります。

 デバイスマネージャーで 「Bluetooth」 デバイスを見ると、「RN42-****」 (後ろ4桁は、製品個体により異なる)があります。
 これが、通信先になる REX-BT60 です。

 
 
 [ 大まかな手順 ]
 ペアリングされたデバイス一覧の中から、「RN42-」で始まるデバイスを探しだします。
 SPP(Serial Port Profile) による通信のUUID : 00001101-0000-1000-8000-00805F9B34FB を指定し、ソケットを生成、接続します。
 あとは、KV nanoに対して、通信開始コマンド 'CR' を送信。レスポンスを受信。操作するコマンドを送信-受信。
 最後に、通信終了コマンド 'CQ' を送信して、KV nano との通信を終了します。

 ※Windows で Bluetooth リンク経由の COM ポートを使う場合は、こちら↓

 

■スクリーンショット

 設計時のフォーム
 Android でも使えるように FMX を使っていますが、Windows のフォームでも、サンプルコードは動きます


 
 
 実行時の画面
 REX-WF60 (Wi-Fi - RS232C) と違って、接続までに若干時間 (1, 2 秒程度) がかかります。
 実用では、REX-BT60, RS-232C を占有して良い場合は、
 フォーム生成時に、別スレッドで、通信開始(KV へ 'CR' 送信)まで行い、終了時に通信終了(KV へ 'CQ' 送信)する。
 つまり、接続しっぱなしにすると良いと思います。

 

 KV STUDIO のトレースモニタ画面

 
 
 Android では、マニフェストファイル (AndroidManifest.xml) でパーミッションの設定を忘れるとエラー↓になります。

 

 メニュー - [プロジェクト] - [オプション] - 「使用する権限」で 「Bluetooth」 を True にすると、AndroidManifest.xml に反映されます。
 マニフェストファイルの編集は、不要です。


 

 Delphi の場合、AndroidManifest.xml は、その元になる AndroidManifest.template.xml から自動生成されるので、
 編集しても書き換えられてしまいます。手動で追加する場合は、AndroidManifest.template.xml を編集します。
 (AndroidManifest.template.xml は、C:\Users\<ユーザー名>\Documents\Embarcadero\Studio\Projects フォルダにあります。)
 
  [オプション] - 「使用する権限」 で、Bluetooth を True にした時、
  <!-- This is the platform API where NativeActivity was introduced. -->
  の下に、「Bluetooth = True」の場合、
  <uses-permission android:name="android.permission.BLUETOOTH" />
  「Bluetooth 管理 = True」の場合
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  が、自動で追加されるようです。

■ほぼ実用版 Android (Nexus7) のスクリーンショット

 対象は、KV nano ですが、KV-3000 でも動くと思います。

  ・常時、デバイス 16 点の値取得、表示形式を変えての表示 (100msec 周期)
  ・デバイスコメントの取得(自動/手動)
  ・ビットデバイスの ON/OFF 反転
  ・ワードデバイスの値変更
  ・再接続(手動)

  ・・・ が、出来ます。デザインは別にして、デバッグ用として何とか使えるレベルです。

 

 


■サンプルコード (Android では、パーミッションの設定を忘れずに!)

// 2017/ 1/ 9 ASocketReceiveData() コメント取得時、2バイト文字が分断されるのを対策
// 2017/ 1/ 8 2バイト文字が取得できないため、TEncoding.UTF8.GetString() を、TEncoding.ANSI.GetString() に変更

unit KV_BT60_BlueToothUnit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  System.Bluetooth, FMX.Controls.Presentation, FMX.StdCtrls,
  System.Bluetooth.Components, FMX.ScrollBox, FMX.Memo;

type
  TForm1 = class(TForm)
    Bluetooth1: TBluetooth;
    Button1: TButton;
    Memo1: TMemo;
    Button2: TButton;
    Panel1: TPanel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { private 宣言 }
  public
    { public 宣言 }
  end;

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

implementation

{$R *.fmx}
{$R *.LgXhdpiTb.fmx ANDROID}
{$R *.Windows.fmx MSWINDOWS}
{$R *.LgXhdpiPh.fmx ANDROID}

// 2017/ 1/ 8 更新
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 TForm1.Button1Click(Sender: TObject);
var
  ABluetoothManager : TBluetoothManager;
  APairedDevices : TBluetoothDeviceList;
  AData : TBytes;
  ASocket : TBluetoothSocket;
  ADevice : TBluetoothDevice;
  AServices : TBluetoothServiceList;

  i,idx : integer;
  res : string;
  //UUID : TBluetoothUUID;
  //os: TOSVersion;
  ATimeout: Cardinal;
  Ticks : Cardinal;
begin
  ATimeout := 1000;
  Ticks := TThread.GetTickCount;

  ABluetoothManager := Bluetooth1.CurrentManager.Current;
  try

    if ABluetoothManager.ConnectionState = TBluetoothConnectionState.Connected then begin
      // 'IZAWA-PC-201411', 'Nexus 7' ... PC名
      Memo1.Lines.Add('CurrentAdapterName : ' + ABluetoothManager.CurrentAdapter.AdapterName);

      // 過去にペアリングされたデバイスの一覧から、REX-BT60 を探す
      APairedDevices := ABluetoothManager.GetPairedDevices;
      Memo1.Lines.Add('PairedDevicesCount : '+IntToStr(APairedDevices.Count));

      if APairedDevices.Count > 0 then begin
        idx := -1;
        for i := 0 to APairedDevices.Count -1 do begin
          // 'RN42-****' が、REX-BT60
          Memo1.Lines.Add('PairedDevicesName : ' + APairedDevices[i].DeviceName);

          if Pos('RN42-', APairedDevices[i].DeviceName) = 1 then begin
            idx := i;
            break;
          end;
        end;
        if idx >= 0 then begin
          ADevice := APairedDevices[idx];
          if ADevice <> nil then begin
            AServices := Device.GetServices;
            if AServices.Count > 0 then begin
              ASocket := ADevice.CreateClientSocket(StringToGUID(ServiceUUID), False);
              if ASocket <> nil then begin
                try
                  // 接続
                  ASocket.Connect;
                  if ASocket.Connected then begin
                    Memo1.Lines.Add('SocketConnect : '+ IntToStr(TThread.GetTickCount - Ticks));
                    Ticks := TThread.GetTickCount;

                    // 通信開始コマンドを送信
                    AData := TEncoding.ANSI.GetBytes('CR'+#13);
                    // 送信
                    ASocket.SendData(AData);
                    // 受信
                    res := ASocketReceiveData(ASocket, ATimeout);
                    Memo1.Lines.Add('CR -> ' + res);
                    Memo1.Lines.Add('Command : '+ IntToStr(TThread.GetTickCount - Ticks));

                    // 出力リレー R500 を ON
                    AData := TEncoding.ANSI.GetBytes('WR R500 1'+#13);
                    ASocket.SendData(AData);
                    res := ASocketReceiveData(ASocket, ATimeout);
                    Memo1.Lines.Add('WR R500 1 -> ' + res);

                    Sleep(500);

                    // 出力リレー R500 を OFF
                    AData := TEncoding.ANSI.GetBytes('WR R500 0'+#13);
                    ASocket.SendData(AData);
                    res := ASocketReceiveData(ASocket, ATimeout);
                    Memo1.Lines.Add('WR R500 0 -> ' + res);

                    // 通信終了コマンドを送信
                    AData := TEncoding.ANSI.GetBytes('CQ'+#13);
                    ASocket.SendData(AData);

                    res := ASocketReceiveData(ASocket, ATimeout);
                    Memo1.Lines.Add('CQ -> ' + res);
                  end;
                finally
                  ASocket.Close;
                  ASocket.Free;
                end;
              end;
            end;
          end;
        end;
      end;
    end;
    Memo1.GoToTextEnd;
  except
    on E : Exception do
      ShowMessage(E.Message);
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  Close;
end;

end.