デルタオーム社(イタリア) 温湿度トランスミッター HD9817T2R (RS-232C タイプ) を使ってみました (2018/02/24)
・2018/03/02 Android Bluetooth ソースコードを追加
下の画像で、2番目が RS-232C タイプです。D-sub9 ピンのプラグが付いています。
電源は不要で、RS-232C から供給されます。(RTS を ON にする必要があるようです)
RS-232C の他、USB、RS-485 (Modbus) タイプもあります。メーカーへのリンクトは、こちらです。
精度は、温度:±0.2℃±測定値の1.5%、湿度:±1.5%RH (0~90%RH) と高精度です。
同じようにプローブ単体でシリアル出力できる製品にロトロニック社の HC2A-S プローブがあります。
こちらの精度(@23℃)は、 温度:±0.1℃(0~30℃)、湿度:±0.8%RH(0~100%RH)と、より高精度です。
※H2A-Sプローブは、UART 出力のため、RS232C レベルに変換する必要があります。(例えば、秋月電子のこちら を使います。アマゾンだとこちら。)
また、接続用のHC2 丸7pin ソケットは市販されていないため、専用のケーブルを切断してハンダ付けすることになります。
ちなみに定価は、デルタオーム社トランスミッター HD9817T2R \67,400.。ロトロニック社プローブ HC2A-S \50,000.
+ 接続ケーブル \20,000.
■ デルタオームのサイトから、読み込みツール↓がダウンロードできます。
■ 通信テスト用にDelphiで読み込みツールを作ってみました。
■ Delphi 開発中のスクリーンショットです。
■ ソースコード
※ApdComPort を使っています。メニュー 「ツール」-「GetIt パッケージマネージャのインストール」からインストール可能です。
unit Unit2; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, OoMisc, AdPort, Vcl.ExtCtrls, AdStatLt, AdSelCom, AdPacket; type TForm2 = class(TForm) ApdComPort1: TApdComPort; Button1: TButton; Edit1: TEdit; Timer1: TTimer; Button2: TButton; ApdStatusLight1: TApdStatusLight; ApdSLController1: TApdSLController; ApdStatusLight2: TApdStatusLight; Label1: TLabel; Label2: TLabel; ComboBox1: TComboBox; ApdDataPacket1: TApdDataPacket; Edit2: TEdit; procedure Button1Click(Sender: TObject); procedure ApdComPort1TriggerAvail(CP: TObject; Count: Word); procedure Timer1Timer(Sender: TObject); procedure Button2Click(Sender: TObject); procedure FormCreate(Sender: TObject); procedure ApdDataPacket1StringPacket(Sender: TObject; Data: AnsiString); private { Private 宣言 } public { Public 宣言 } buf : string; end; var Form2: TForm2; implementation {$R *.dfm} // 受信 (ApdComPortコンポーネントのみを使用した時) procedure TForm2.ApdComPort1TriggerAvail(CP: TObject; Count: Word); var i: Word; c: AnsiChar; begin // 受信文字列の取得 for i := 1 to Count do begin c := ApdComPort1.GetChar; // 終端に#13 (CR) が付加されている if c = #13 then Edit2.Text := buf else buf := buf + string(c); end; end; // 受信 (ApdDataPacketコンポーネントを使用した時) procedure TForm2.ApdDataPacket1StringPacket(Sender: TObject; Data: AnsiString); begin Edit1.Text := String(Data); end; // OPEN procedure TForm2.Button1Click(Sender: TObject); var comNo : integer; begin with ComboBox1 do begin if ItemIndex >= 0 then comNo := StrToIntDef(Items[ItemIndex].Substring (4), -1) else comNo := -1; end; if comNo >= 0 then begin with ApdComport1 do begin ComNumber := comNo; Baud := 2400; StopBits := 1; DataBits := 8; Parity := TParity.pNone; RTS := True; // デフォルト try Open := True; // ステータスモニタを ON ApdSLController1.Monitoring := True; except ShowMessage('OPEN 失敗'); end; end end else ShowMessage('有効な COM ポートが見つかりません.');; end; // CLOSE procedure TForm2.Button2Click(Sender: TObject); begin ApdComPort1.Open := False; Edit1.Text := ''; Edit2.Text := ''; end; // FormCreate procedure TForm2.FormCreate(Sender: TObject); var i :integer; begin // 有効な COM ポートを列挙 for i := 1 to 32 do if IsPortAvailable(i) then ComboBox1.Items.Add ( 'COM ' + IntToStr (i)); if ComboBox1.Items.Count > 0 then ComboBox1.ItemIndex := 0; Edit1.Text := ''; Edit2.Text := ''; with ApdDataPacket1 do begin // 終端文字をセット EndCond := [AdPacket.ecString]; EndString := #13; // 開始文字を無視 StartCond := AdPacket.scAnyData; end; end; // 1秒周期 procedure TForm2.Timer1Timer(Sender: TObject); begin if ApdComPort1.Open then begin buf := ''; // 送信 // 型番を取得 //ApdComPort1.Output := 'G0'+#13; // 温度、湿度を取得 //ApdComPort1.Output := 'S0'+#13; ApdComPort1.PutString('S0'+#13); end; end; end.
■ REX-BT60 (RS232C-Bluetooth 変換アダプタ) を使って Android 端末に計測データを表示してみました。
・Parani-SD1000-00 でも使用可能。(アマゾンへのリンク)(マイクロテクニカへのリンク)(マイクロテクニカ説明書)
・露点温度計算
・音声読み上げ
30 秒に 1 回、計測値を読みあげます。結構便利です。
・トレンドグラフ表示
計測値が安定しているかの確認用で、最新 12 分間のみの表示です。
CSV ファイルに保存する機能を追加予定です。
返信文字列数が少ないので、Bluetooth LE でも使えるかもしれません。であれば、iOS 端末でも使えますね。
■ソースコード
コード unit Unit2; 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.ScrollBox, FMX.Memo, FMX.Controls.Presentation, FMX.StdCtrls, System.Bluetooth.Components, FMX.Edit, FMX.Objects, UIConsts, Math, FMX.Colors, // for TTS Androidapi.JNI.TTS,AndroidAPI.JNIBridge; type TBtThread = class(TThread) private { Private 宣言 } procedure BtOpen; protected procedure Execute; override; public constructor Create; virtual; end; type TForm2 = class(TForm) Bluetooth1: TBluetooth; Timer1: TTimer; Label1: TLabel; Label2: TLabel; PaintBox1: TPaintBox; Label3: TLabel; Label4: TLabel; Panel1: TPanel; Rectangle1: TRectangle; Rectangle2: TRectangle; Rectangle3: TRectangle; Rectangle5: TRectangle; Rectangle6: TRectangle; Label5: TLabel; Label6: TLabel; Label7: TLabel; Label8: TLabel; Switch1: TSwitch; Label9: TLabel; Switch2: TSwitch; Label10: TLabel; procedure Timer1Timer(Sender: TObject); procedure FormCreate(Sender: TObject); procedure PaintBox1Paint(Sender: TObject; Canvas: TCanvas); procedure FormDestroy(Sender: TObject); // TTS type TttsOnInitListener = class(TJavaLocal, JTextToSpeech_OnInitListener) private [weak] FParent : TForm2; public constructor Create(AParent : TForm2); procedure onInit(status: Integer); cdecl; end; private { private 宣言 } ttsListener : TttsOnInitListener; tts : JTextToSpeech; procedure SpeakOut(const s :string); procedure InitTTS; public { public 宣言 } constructor Create(AOwner : TComponent); override; destructor Destroy; override; end; type TRecData = record Temp : double; Humi : double; Dp : double; RecTime : TDateTime; end; const RECDATA_MAX = 720; var GThdMode : integer; GCmdMode : integer; GSocket:TBluetoothSocket; ThBt : TBtThread; OpenNGcnt : integer; OpenMsecCnt : integer; Counter : integer; BtDeviceHead : string; const // SPP(Serial Port Profile) による通信のUUID ServiceUUID = '{00001101-0000-1000-8000-00805F9B34FB}'; BTDeviceHead1 = 'RN42-'; BTDeviceHead2 = 'SD1000v2'; thdTHSTART = 1000; thdTHTERM = 2000; cmdSCCREATE = 200; cmdSCCONNECT = 201; cmdSCNG = 202; var Form2: TForm2; ASocket : TBluetoothSocket; FlagBTConnect : boolean; RecDataAry : array [0..RECDATA_MAX-1] of TRecData; DispIndex : integer; implementation {$R *.fmx} uses Androidapi.JNI.JavaTypes, FMX.Helpers.Android {$IF CompilerVersion >= 27.0} , Androidapi.Helpers {$ENDIF} ; function CalcPws(Td : double):double; // uses ... Math; var S, Pws, T : double; begin // 絶対温度 [K] T := Td + 273.15; // 飽和水蒸気圧 [hPa] // Vaisala -------------------------------------------------------------- S := T - ( -0.12743214E-7 * Power(T, 3) + 0.13746454E-4 * Power(T, 2) + -0.46094296E-2 * Power(T, 1) + 0.49313580 * Power(T, 0) ); Pws := Exp( -0.14452093E-7 * Power(S, 3) + 0.41764768E-4 * Power(S, 2) + -0.48640239E-1 * Power(S, 1) + 0.13914993E01 * Power(S, 0) + -0.58002206E04 * Power(S, -1)+ 6.5459673 * Ln(S) ) / 100; result := Pws; end; function CalcDewPoint(Td, Hu : double): double; var Pw, Pws, Tdp : double; begin Pws := CalcPws(td); // 水蒸気圧 [hPa] Pw := Pws * Hu / 100; // 露点温度 [℃DP] Tdp := 237.30 / (7.5000 / Log10(Pw / 6.1078) - 1); result := Tdp; 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 Form2.Label1.Text := '[' + ABluetoothManager.CurrentAdapter.AdapterName + ']' end); // 過去にペアリングされたデバイスの一覧から、REX-BT60 を探す APairedDevices := ABluetoothManager.GetPairedDevices; if APairedDevices.Count > 0 then begin idx := -1; for i := 0 to APairedDevices.Count -1 do begin // 'RN42-****' が、REX-BT60 if (Pos(BTDeviceHead, APairedDevices[i].DeviceName) = 1) then begin Synchronize(procedure() begin Form2.Label1.Text := Form2.Label1.Text + ' - ' + '[' + APairedDevices[i].DeviceName + ']'; end); idx := i; break; end; end; if idx >= 0 then begin ADevice := APairedDevices[idx]; if ADevice <> nil then begin GSocket := ADevice.CreateClientSocket(StringToGUID(ServiceUUID), False); if GSocket <> nil then begin GCMDMODE := cmdSCCREATE; // 接続 GSocket.Connect; if GSocket.Connected then begin GCMDMODE := cmdSCCONNECT; end; 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 TForm2.InitTTS; begin tts := TJTextToSpeech.JavaClass.init(TAndroidHelper.Context, ttsListener); end; procedure TForm2.SpeakOut(const s : string); var text : JString; begin text := StringToJString(s); tts.speak(text, TJTextToSpeech.JavaClass.QUEUE_FLUSH, nil); end; { TForm1.TttsOnInitListener } constructor TForm2.TttsOnInitListener.Create(AParent: TForm2); begin inherited Create; FParent := AParent end; procedure TForm2.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') else begin //FParent.Button1.Enabled := true; // SayMe //FParent.button2.Enabled := false; // Init TTS end; end else ShowMessage('Initilization Failed!'); end; // ----------------------------------------------------------------------------- function ASocketReceiveData(ASocket: TBluetoothSocket; term: Char; 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); // CR であれば、抜ける if (AData[i] = Ord(term)) 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; // ----------------------------------------------------------------------------- constructor TForm2.Create(AOwner: TComponent); begin inherited; ttsListener := TttsOnInitListener.Create(self); end; destructor TForm2.Destroy; begin if Assigned(tts) then begin tts.stop; tts.shutdown; tts := nil; end; end; procedure TForm2.FormCreate(Sender: TObject); var i : integer; begin Timer1.Interval := 10; ThBt := TBtThread.Create; Label1.Text := ''; Label2.Text := ''; Label3.Text := ''; Label5.Text := ''; for i := 0 to RECDATA_MAX -1 do begin RecDataAry[i].Temp := -99; RecDataAry[i].Humi := -99; RecDataAry[i].Dp := -99; RecDataAry[i].RecTime := 0; end; DispIndex := -1; BtDeviceHead := BTDeviceHead1; // TTS InitTTS; end; procedure TForm2.FormDestroy(Sender: TObject); begin // ソケット破棄 if GSocket <> nil then begin if GSocket.Connected then GSocket.Close; FreeAndNil(GSocket); end; end; procedure TForm2.PaintBox1Paint(Sender: TObject; Canvas: TCanvas); var xsc , ysc, ybase : double; i : integer; stX, stY, edX, edY : single; aLeft, aTop, aRight, aBottom : single; aStep : integer; pbox : TPaintBox; begin pBox := Sender as TPaintBox; with Canvas do begin BeginScene; xsc := pbox.Width / RECDATA_MAX; ysc := pbox.Height/ 100; ybase := 0.0; aLeft := 0; aTop := 0; aRight := aLeft + pbox.Width; aBottom := aTop + pbox.Height; aStep := RECDATA_MAX div 6; Fill.Kind := TBrushKind.Solid ; Fill.Color := TAlphaColors.Black;//claBlack; FillRect(RectF(aLeft, aTop, aRight + 1, aBottom), 0, 0, AllCorners, 1.0); Stroke.Color := claBlue; // 線色 Stroke.Dash := TStrokeDash.Dash; // 線種 Stroke.Kind := TBrushKind.Solid; Stroke.Thickness := 1 ; // 線幅 // 横目盛り for i := 1 to 9 do begin //if i * 10 = 50 then // Stroke.Color := claRed //else // Stroke.Color := claBlue; // 横線 DrawLine( PointF(aLeft + 1, aBottom - i * 10 * ysc), PointF(aRight, aBottom - i * 10 * ysc), 1.0); Fill.Color := claWhite; Font.Size := 10; // 左側の目盛り FillText( RectF(aLeft, aBottom - i * 10 * ysc - 6, aLeft + 20, aBottom - i * 10 * ysc - 6 + 16), IntToStr(i*10), True, 1.0, [], TTextAlign.Leading, TTextAlign.Leading); // 右側の目盛り FillText( RectF(aRight - 20, aBottom - i * 10 * ysc - 6, aRight, aBottom - i * 10 * ysc - 6 + 16), IntToStr(i*5), True, 1.0, [], TTextAlign.Trailing, TTextAlign.Leading); end; Fill.Color := claWhite; FillText(RectF(aLeft + 1, aTop, aLeft + 40, aTop + 16), '[%RH]', True, 1.0, [], TTextAlign.Leading, TTextAlign.Leading); FillText(RectF(aRight - 20, aTop, aRight, aTop + 16), '[℃]', True, 1.0, [], TTextAlign.Trailing, TTextAlign.Leading); // 縦目盛り for i := 1 to 5 do begin DrawLine( PointF(aLeft + i * aStep * xsc, aBottom), PointF(aLeft + i * aStep * xsc, aTop), 0.75); end; // 時刻 FillText( RectF(aLeft, aBottom - 16, aLeft + 40, aBottom), FormatDateTime('hh:mm', RecDataAry[0].RecTime), True, 1.0, [], TTextAlign.Leading, TTextAlign.Leading); for i := 1 to 6 do begin if RecDataAry[i * aStep-1].RecTime > 0 then begin FillText( RectF(aLeft + i * aStep * xsc, aBottom - 16, aLeft + i * aStep * xsc + 40, aBottom), FormatDateTime('hh:mm', RecDataAry[i * aStep - 1].RecTime), True, 1.0, [], TTextAlign.Leading, TTextAlign.Leading); end; end; // 現在の値 FillText( RectF(aLeft + 40, aTop + 1, aLeft + 40 + 400, aTop + 17), FormatDateTime('[ yyyy/mm/dd hh:mm:ss ] ', Now) + Format('%.1f %%RH: %.1f ℃: %.1f ℃DP', [RecDataAry[DispIndex].Humi, RecDataAry[DispIndex].Temp, RecDataAry[DispIndex].Dp]), True, 1.0, [], TTextAlign.Leading, TTextAlign.Leading); // 計測データ Stroke.Dash := TStrokeDash.Solid; Stroke.Thickness := 1 ; // 線幅 // 相対湿度 Stroke.Color := claYellow; stX := 0; stY := 0; edX := 0; edY := 0; for i := 0 to RECDATA_MAX - 2 do begin if RecDataAry[i].Humi >= 0 then begin stX := aLeft + i * xsc; stY := aBottom - (RecDataAry[i].Humi + ybase) * ysc; end; if RecDataAry[i+1].Humi >= 0 then begin edX := aLeft + (i + 1) * xsc; edY := aBottom - (RecDataAry[i + 1].Humi + ybase) * ysc; end; if (stX > 0) and (stY > 0) and (edX > 0) and (edY > 0) then DrawLine(PointF(stX + 1, stY), PointF(edX + 1, edY), 1.0); end; // 温度 ysc := pbox.Height / 50; Stroke.Color := claLime; stX := 0; stY := 0; edX := 0; edY := 0; for i := 0 to RECDATA_MAX - 2 do begin if RecDataAry[i].Temp >= 0 then begin stX := aLeft + i * xsc; stY := aBottom - (RecDataAry[i].Temp + ybase) * ysc; end; if RecDataAry[i+1].Temp >= 0 then begin edX := aLeft + (i + 1) * xsc; edY := aBottom - (RecDataAry[i + 1].Temp + ybase) * ysc; end; if (stX > 0) and (stY > 0) and (edX > 0) and (edY > 0) then DrawLine(PointF(stX + 1, stY), PointF(edX + 1, edY), 1.0); end; // 露点温度 if Switch1.IsChecked then begin Stroke.Color := claAqua; stX := 0; stY := 0; edX := 0; edY := 0; for i := 0 to RECDATA_MAX - 2 do begin if RecDataAry[i].Dp >= 0 then begin stX := aLeft + i * xsc; stY := aBottom - (RecDataAry[i].Dp + ybase) * ysc; end; if RecDataAry[i+1].Dp >= 0 then begin edX := aLeft + (i + 1) * xsc; edY := aBottom - (RecDataAry[i + 1].Dp + ybase) * ysc; end; if (stX > 0) and (stY > 0) and (edX > 0) and (edY > 0) then DrawLine(PointF(stX + 1, stY), PointF(edX + 1, edY), 1.0); end; end; EndScene; end; end; procedure TForm2.Timer1Timer(Sender: TObject); var res : string; T, H, Tdp : double; i : integer; TxData : TBytes; begin if not ((GCMDMODE = cmdSCCONNECT) and GSocket.Connected) then begin Inc(OpenMsecCnt); Label4.Text := IntToStr(OpenMsecCnt * 10) + 'msec'; if GCMDMODE = cmdSCNG then begin Inc(OpenNgCnt); if OpenNgCnt > 4 then begin Timer1.Enabled := False; ShowMessage('接続できません.'); end else begin // 再接続 // ターゲットを変えてみる if BtDeviceHead = BTDeviceHead1 then BtDeviceHead := BTDeviceHead2 else BtDeviceHead := BTDeviceHead1; Sleep(500); OpenMsecCnt := 0; GCMDMODE := 0; GTHDMODE := 0; ThBt := TBtThread.Create; end; end; end; if (GCMDMODE = cmdSCCONNECT) and GSocket.Connected then begin Timer1.Interval := 500; try // 送信 TxData := TEncoding.ANSI.GetBytes('S0' + #13); GSocket.SendData(TxData); // 受信 res := ASocketReceiveData(GSocket, #13, 500); T := StrToFloatDef(res.Substring(0, 6), -20); H := StrToFloatDef(res.Substring(11), 0); Label2.Text := Format('%.1f', [T]); Label3.Text := Format('%.1f', [H]); if (T > -20) and (H > 0) then begin Tdp := CalcDewPoint(T,H); Label5.Text := Format('%.1f', [Tdp]); end else begin Tdp := 0; Label5.Text := ''; end; Inc(DispIndex); if DispIndex >= RECDATA_MAX - 1 then begin DispIndex := RECDATA_MAX - 121; for i := 0 to RECDATA_MAX - 121 do RecDataAry[i] := RecDataAry[i + 121]; for i := RECDATA_MAX - 120 to RECDATA_MAX - 1 do begin RecDataAry[i].Temp := -99; RecDataAry[i].Humi := -99; RecDataAry[i].Dp := -99; RecDataAry[i].RecTime := 0; end; end; RecDataAry[DispIndex].Humi := H; RecDataAry[DispIndex].Temp := T; RecDataAry[DispIndex].Dp := Tdp; RecDataAry[DispIndex].RecTime := Now; // 描画 PaintBox1.Repaint; // テキストスピーチ Inc(Counter); if Switch2.IsChecked then begin if Counter = 10 then SpeakOut('温度:' + Label2.Text) else if Counter = 20 then SpeakOut('湿度:' + Label3.Text) else if Counter = 30 then SpeakOut('露点温度:' + Label5.Text); end; if Counter >= 30 then Counter := 0; except ; end; end; end; end.