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.