Android ComRead RS232C モニタ (2018/03/25)
2019/05/01
・使い方を追加.
2018/03/25
・受信データのリアルタイム表示をとりやめ、受信1回 最大 1024 バイトまでに変更.
2018/03/23
・送信文字列に制御コードを追加。16進数値は、'<00>' .. '<ff> '(小文字) で対応
2018/03/20
・送信・受信を 64 バイトごとに変更
・制御文字の表示を追加 0D -> 'CR'、0A -> 'LF' ..
2018/03/19
・初版作成
Android で使える Delphi 用 シリアルコンポーネントを見つけたので、受信モニタを作ってみました。
(使用している Uni232C シリアル通信用コンポーネントのリンクは、こちらです。)
すこし大きいですが、7 インチタブレット (Nexus 7) + OTGケーブル + USB / RS232C変換ケーブル で使用しています。
USB に接続すると、自動でアプリが立ち上がります(これはOSの機能)。USBの抜き差しにも対応(これはUni232Cの機能)。かなり快適です。
残念ながら、いつも現場で使っているテスト用スマホ (HUAWEI P9 Lite) は、OTG に対応していませんでした。
D-sub9 ピンのうち、GND と RXD の2本で、送信側または受信側をひたすらモニタするだけの使い方を想定していましたが、
表示が追い付かないため、最大 1024 バイトの受信1回に変更しました。受信データ表示は、リアルタイムではありません。
受信データが 1024 バイト を超えるか、[Stop] ボタンをタップすると、結果が表示されます。
受信中にテスト用の文字列の送信も行えます。16進数値は、<00> .. <ff> '(小文字) のように"<"、">"で囲んで下さい。
APK を公開しました。ダウンロードは、こちらです。動作確認は、Xexus7 のみ。自己責任でお試し下さい。
スマホの画面サイズにも対応しています。
USB/シリアル変換モジュールを使うと、UARTのモニタも行えると思います。例えば、これ。入力5Vトレラントです。
時間があれば、AKI-RS232C ラインモニタ用ツールの Android 版を作ってみたいですね。←作成しました。
■使い方
受信のみ
・通信パラメータを合わせます。
・[Open] ボタンで COM ポートをオープンします。ブッブッとブザーが2回鳴ります。
・[Read] ボタンで受信を開始します。受信バイト数は、Read Byres = に表示されます。
・受信バイト数が適当なところで [Stop] ボタンをタップすると、受信データが表示されます。
受信バイト数が 1024 を超えると自動で Stop し、受信データが表示されます。
・最後に [Close] ボタンでポートを閉じます。
送信/受信 (ループバック)
・通信パラメータを合わせます。
・送信文字列を作成します。
・[Open] ボタンで COM ポートをオープンします。ブッブッとブザーが2回鳴ります。
・[Read] ボタンで受信を開始します。受信バイト数は、Read Byres = に表示されます。
・[Write] ボタンで文字列を送信します。
・受信バイト数が適当なところで、[Stop] ボタンをタップすると、受信データが表示されます。
・最後に [Close] ボタンでポートを閉じます。
■ソースコード
Delphi 10.2 Tokyo + Uni232C コンポーネント
unit ComReadUnit; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, Uni232C, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Edit, FMX.ScrollBox, FMX.ListBox, System.Rtti, FMX.Grid.Style, FMX.Grid, System.UIConsts, System.IOUtils, System.IniFiles, FMX.Objects, FMX.Layouts, Androidapi.JNIBridge, AndroidApi.JNI.Media; type TComReadThread = class(TThread) private { Private 宣言 } procedure ComRead; protected procedure Execute; override; public constructor Create; virtual; end; TForm2 = class(TForm) ScaledLayout1: TScaledLayout; Button1: TButton; Button2: TButton; Button3: TButton; Button4: TButton; ComboBox1: TComboBox; Edit2: TEdit; ComboBox2: TComboBox; ComboBox3: TComboBox; Edit1: TEdit; CheckBox1: TCheckBox; CheckBox2: TCheckBox; ComboBox4: TComboBox; CheckBox3: TCheckBox; CheckBox4: TCheckBox; Label1: TLabel; ComboBox5: TComboBox; Button5: TButton; Label2: TLabel; Uni232C1: TUni232C; StringGrid1: TStringGrid; StringColumn1: TStringColumn; StringColumn2: TStringColumn; StringColumn3: TStringColumn; StringColumn4: TStringColumn; StringColumn5: TStringColumn; StringColumn6: TStringColumn; StringColumn7: TStringColumn; StringColumn8: TStringColumn; StringColumn9: TStringColumn; StringColumn10: TStringColumn; StringColumn11: TStringColumn; StringColumn12: TStringColumn; StringColumn13: TStringColumn; StringColumn14: TStringColumn; StringColumn15: TStringColumn; StringColumn16: TStringColumn; StringColumn17: TStringColumn; StringColumn18: TStringColumn; StringColumn19: TStringColumn; StringColumn20: TStringColumn; StringColumn21: TStringColumn; StringColumn22: TStringColumn; StringColumn23: TStringColumn; StringColumn24: TStringColumn; StringColumn25: TStringColumn; StringColumn26: TStringColumn; StringColumn27: TStringColumn; StringColumn28: TStringColumn; StringColumn29: TStringColumn; StringColumn30: TStringColumn; StringColumn31: TStringColumn; StringColumn32: TStringColumn; StringColumn33: TStringColumn; SpeedButton1: TSpeedButton; SpeedButton2: TSpeedButton; Label3: TLabel; Button6: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); procedure Button3Click(Sender: TObject); procedure Button4Click(Sender: TObject); //procedure Timer1Timer(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure ComboBox1Change(Sender: TObject); procedure StringGrid1DrawColumnCell(Sender: TObject; const Canvas: TCanvas; const Column: TColumn; const Bounds: TRectF; const Row: Integer; const Value: TValue; const State: TGridDrawStates); procedure CheckBox1Change(Sender: TObject); procedure CheckBox2Change(Sender: TObject); procedure Uni232C1UsbDettach(Sender: TObject); procedure Button5Click(Sender: TObject); procedure Edit1Change(Sender: TObject); procedure SpeedButton1Click(Sender: TObject); procedure SpeedButton2Click(Sender: TObject); procedure Button6Click(Sender: TObject); private { private 宣言 } public { public 宣言 } procedure EnableComParams(flag : boolean); function MakeSendStr(const src: string): string; procedure ClearSg; procedure DispRecData; end; var Form2: TForm2; RecData : array [0..1023] of Byte; RecIndex : integer; LoopFlag : boolean; ThComRead : TComReadThread; ControlCode : array of string = [ 'NUL', 'SOH', 'STX', 'ETX', 'EOT', 'ENQ', 'ACK', 'BEL', 'BS', 'HT', 'LF', 'VT', 'FF', 'CR', 'SO', 'SI', 'DLE', 'DC1', 'DC2', 'DC3', 'DC4', 'NAK', 'SYN', 'ETB', 'CAN', 'EM', 'SUB', 'ESC', 'FS', 'GS', 'RS', 'US', 'DEL']; //DEL = $7E implementation {$R *.fmx} procedure Beep(typ : integer); // ブザー音 // uses ... Androidapi.JNIBridge, AndroidApi.JNI.Media; var ToneGenerator: JToneGenerator; begin ToneGenerator := TJToneGenerator.JavaClass.init( TJAudioManager.JavaClass.STREAM_ALARM, TJToneGenerator.JavaClass.MAX_VOLUME); //https://developer.android.com/reference/android/media/ToneGenerator.html if typ = 2 then // TONE_PROP_BEEP2 = 400Hz+1200Hz, 35ms ON, 200ms OFF, 35ms ON ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_BEEP2) else // TONE_PROP_BEEP = 400Hz+1200Hz, 35ms ON ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_BEEP); end; procedure TForm2.DispRecData; // 受信データを表示 var k : integer; SgColIndex, SgRowIndex : integer; begin if RecIndex > 0 then begin SgColIndex := 1; SgRowIndex := 0; with StringGrid1 do begin BeginUpdate; for k := 0 to RecIndex - 1 do begin Cells[SgColIndex, SgRowIndex] := IntToHex(RecData[k], 2); Cells[SgColIndex, SgRowIndex + 1] := Char(RecData[k]); Inc(SgColIndex); if SgColIndex > 32 then begin SgColIndex := 1; SgRowIndex := SgRowIndex + 2; if SgRowIndex >= 63 then begin break; end; end; end; EndUpdate; end; Beep(1); end; end; procedure TForm2.ClearSg; // クリア var i, j: integer; begin Label3.Text := ''; with StringGrid1 do begin BeginUpdate; for i := 0 to RowCount-1 do begin for j := 1 to ColumnCount - 1 do begin if Cells[j, i * 2] <> '' then Cells[j, i * 2] := ''; if Cells[j, i * 2 + 1] <> '' then Cells[j, i * 2 + 1] := ''; end; end; TopRow := 0; EndUpdate; end; end; // ----------------------------------------------------------------------------- procedure TComReadThread.ComRead; var ret : integer; AData : TBytes; i:integer; begin loopFlag := True; while not Terminated and loopFlag do begin if Form2.Uni232C1.Connect then begin SetLength(AData, 64); ret := Form2.Uni232C1.Read(64, @AData[0]); if( ret > 0 ) then begin for i := 0 to ret - 1 do begin RecData[RecIndex] := AData[i]; Inc(RecIndex); if RecIndex >= 1024 then begin loopFlag := False; Synchronize(procedure begin Form2.DispRecData; end); Break; end; end; Synchronize(procedure begin Form2.Label3.Text := 'Read Bytes = ' + RecIndex.ToString; end); end; end else TThread.Sleep(1); end; end; constructor TComReadThread.Create; begin // スレッドを生成、直ちに実行 inherited Create(False); // スレッド終了時、スレッドオブジェクトを破棄 FreeOnTerminate := True; end; procedure TComReadThread.Execute; begin ComRead; end; // ----------------------------------------------------------------------------- function TForm2.MakeSendStr(const src : string): string; // 送信文字列を作成 var i : integer; begin result := src; if CheckBox1.IsChecked then result := result + #13 else if CheckBox2.IsChecked then result := result + #13#10; // <> 表記の制御コードを数値に置き換え for i := 0 to $19 do result := StringReplace(result, '<' + ControlCode[i] + '>', string(Char(i)), [rfReplaceAll]); result := StringReplace(result, '<DEL>', string(Char($7E)), [rfReplaceAll]); // <> 表記の16進コードを数値に置き換え for i := 0 to $FF do result := StringReplace(result, '<' + LowerCase(IntToHex(i, 2)) + '>', string(Char(i)), [rfReplaceAll]); end; procedure TForm2.Edit1Change(Sender: TObject); // バイト数表示 var Encoding: TEncoding; s : string; begin s := MakeSendStr(Edit1.Text); //Shift_JIS Encoding := TEncoding.GetEncoding(932); Label2.Text := IntToStr(Encoding.GetByteCount(s)) + ' Bytes'; Encoding.Free; end; procedure TForm2.EnableComParams(flag : boolean); begin ComboBox1.Enabled := Flag; ComboBox2.Enabled := Flag; ComboBox3.Enabled := Flag; ComboBox4.Enabled := Flag; end; procedure TForm2.Button1Click(Sender: TObject); // OPEN var ret : integer; begin ret := Uni232C1.Open; if ret < 0 then ShowMessage('Cannot OPEN' + Uni232C1.Error2Str(ret)) else begin EnableComParams(false); Beep(2); end; end; procedure TForm2.Button2Click(Sender: TObject); // CLOSE begin if Assigned(ThComRead) then begin ThComRead.TerminatedSet ; ThComRead := nil; end; if Form2.Label1.Text <> '' then Form2.Label1.Text := ''; Uni232C1.Close; // パラメータの変更を許可 EnableComParams(True); Beep(1); end; procedure TForm2.Button3Click(Sender: TObject); // WRITE var AData : TBytes; s : string; k, nth, len, wlen :integer; begin if Uni232C1.Connect then begin s := MakeSendStr(Edit1.Text); AData := TEncoding.ANSI.GetBytes(s); len := Length(AData); // 64 Byte ごとに送信 nth := len div 64; if len mod 64 > 0 then nth := nth + 1; for k := 0 to nth - 1 do begin if k < nth - 1 then wlen := 64 else wlen := len - k * 64; Uni232C1.Write(wlen, @AData[k * 64]); end; Beep(2); end; end; procedure TForm2.Button4Click(Sender: TObject); // READ begin if Assigned(ThComRead) then begin ThComRead.TerminatedSet; ThComRead := nil; end; RecIndex := 0; ClearSg; if Uni232C1.Connect then begin ThComRead := TComReadThread.Create; Beep(2); end; end; procedure TForm2.Button5Click(Sender: TObject); // 制御コードを挿入 var s : string; begin with ComboBox5 do begin if ItemIndex >= 0 then begin if ItemIndex < 32 then s :='<' + ControlCode[ItemIndex] + '>' else s := '<DEL>'; Edit1.Text := Edit1.Text.Insert(Edit1.SelStart, s); Edit1.SelStart := Edit1.SelStart + s.Length; end; end; end; procedure TForm2.Button6Click(Sender: TObject); // READ STOP begin if Assigned(ThComRead) then begin loopFlag := False; ThComRead.TerminatedSet; ThComRead := nil; DispRecData; end; end; procedure TForm2.CheckBox1Change(Sender: TObject); // CR 付加 begin if CheckBox1.IsChecked then CheckBox2.IsChecked := False; Edit1Change(self); end; procedure TForm2.CheckBox2Change(Sender: TObject); // CR + LF 付加 begin if CheckBox2.IsChecked then CheckBox1.IsChecked := False; Edit1Change(self); end; procedure TForm2.ComboBox1Change(Sender: TObject); // 通信パラメータ設定 var AData : Word; begin with Uni232C1 do begin {$ifdef MSWINDOWS} Port := Edit2.Text.ToInteger; {$endif} BaudRate := ComboBox1.Items[ComboBox1.ItemIndex].ToInteger; if ComboBox2.ItemIndex = 0 then ByteSize := Bit7 else ByteSize := Bit8; if ComboBox3.ItemIndex = 1 then StopBits := StopBit2 else StopBits := StopBit1; case ComboBox4.ItemIndex of 1: ParityBits := ParityOdd; 2: ParityBits := ParityEven; 3: ParityBits := ParityMark; 4: ParityBits := ParitySpace; else ParityBits := ParityNone; end; AData := $0300; // DTS if CheckBox3.IsChecked then AData := AData or $01; // RTS if CheckBox4.IsChecked then AData := AData or $02; SetModemStatus(AData); end; end; procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction); var IniFile: TMemIniFile; begin if Assigned(ThComRead) then begin ThComRead.TerminatedSet; ThComRead := nil; end; Uni232C1.Close; IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine( System.IOUtils.TPath.GetDocumentsPath, 'ComRead.ini'), TEncoding.UTF8); with IniFile do begin try WriteInteger('ComboBox', 'BaudRate', ComboBox1.ItemIndex); WriteInteger('ComboBox', 'ByteSize', ComboBox2.ItemIndex); WriteInteger('ComboBox', 'StopBits', ComboBox3.ItemIndex); WriteInteger('ComboBox', 'Parity', ComboBox4.ItemIndex); WriteBool('CheckBox', 'DTS', CheckBox3.IsChecked); WriteBool('CheckBox', 'RTS', CheckBox4.IsChecked); WriteBool('CheckBox', 'CR', CheckBox1.IsChecked); WriteBool('CheckBox', 'CRLF', CheckBox2.IsChecked); WriteString('EditBox', 'WriteString', Edit1.Text); WriteString('EditBox', 'PortNo', Edit2.Text); UpdateFile; finally IniFile.Free; end; end; end; procedure TForm2.FormCreate(Sender: TObject); var i :integer; IniFile: TMemIniFile; begin // Port 番号は、Windows のみ {$ifdef MSWINDOWS} Edit2.Visible := True; {$else} Edit2.Visible := False; {$endif} Label1.Text := ''; Label2.Text := ''; Label3.Text := ''; // 横画面に固定 Application.FormFactor.Orientations := [TFormOrientation.Landscape, TFormOrientation.InvertedLandscape]; with StringGrid1 do begin for i := 1 to ColumnCount - 1 do Columns[i].Header := i.ToString; for i := 0 to RowCount div 2 -1 do begin Cells[0, i*2] := (i+1).ToString; end; end; IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine( System.IOUtils.TPath.GetDocumentsPath, 'ComRead.ini'), TEncoding.UTF8); with IniFile do begin try ComboBox1.ItemIndex := ReadInteger('ComboBox', 'BaudRate', 3); ComboBox2.ItemIndex := ReadInteger('ComboBox', 'ByteSize', 1); ComboBox3.ItemIndex := ReadInteger('ComboBox', 'StopBits', 0); ComboBox4.ItemIndex := ReadInteger('ComboBox', 'Parity', 0); CheckBox3.IsChecked := ReadBool('CheckBox', 'DTS', False); CheckBox4.IsChecked := ReadBool('CheckBox', 'RTS', False); CheckBox1.IsChecked := ReadBool('CheckBox', 'CR', False); CheckBox2.IsChecked := ReadBool('CheckBox', 'CRLF', False); Edit1.Text := ReadString('EditBox', 'WriteString', ''); Edit2.Text := ReadString('EditBox', 'PortNo', '15'); finally Free; end; end; ComboBox1Change(self); end; procedure TForm2.SpeedButton1Click(Sender: TObject); // UP begin with StringGrid1 do begin if TopRow mod 2 > 0 then TopRow := TopRow - 1 else TopRow := TopRow - 2; end; end; procedure TForm2.SpeedButton2Click(Sender: TObject); // DOWN begin with StringGrid1 do begin if TopRow mod 2 > 0 then TopRow := TopRow + 1 else TopRow := TopRow + 2; end; end; procedure TForm2.StringGrid1DrawColumnCell(Sender: TObject; const Canvas: TCanvas; const Column: TColumn; const Bounds: TRectF; const Row: Integer; const Value: TValue; const State: TGridDrawStates); var s: string; c: Cardinal; begin if Column.Index = 0 then begin end else begin // 16進表示を色分け if Row mod 2 = 0 then begin if not Value.IsEmpty then s := Value.ToString else s := ''; if s <> '' then begin c := StrToInt('$'+s); if (c < $20) or (c = $7E) then Canvas.Fill.Color := claRed else Canvas.Fill.Color := claCyan;//claSkyBlue;//Cyan;//Lime; // 透過で塗りつぶし Canvas.FillRect( Bounds, 0, 0, AllCorners, 0.4, TCornerType.Round ); end; end // 制御コードを表示 else begin s := StringGrid1.Cells[Column.Index, Row - 1]; if s <> '' then begin c := StrToInt('$' + s); if (c < $20) or (c = $7E) then begin Canvas.Fill.Color := claYellow; Canvas.FillRect(Bounds, 0, 0, AllCorners, 1, TCornerType.Round ); Canvas.Fill.Color := claBlack; if c < $20 then s := ControlCode[c] else s := ControlCode[$20]; Canvas.Font.Size := 11; Canvas.FillText(Bounds, s, False, 1.0, [], TTextAlign.Center); end; end; end; end; end; procedure TForm2.Uni232C1UsbDettach(Sender: TObject); // USB ケーブル抜け begin if Uni232C1.Connect then begin if Assigned(ThComRead) then begin ThComRead.TerminatedSet; ThComRead := nil; end; Uni232C1.Close; EnableComParams(True); Beep(1); end; if Label1.Text <> '' then Label1.Text := ''; end; end.