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.