Android ComRead RS232C モニタ  (2018/03/19)

2018/03/25
 ・受信データのリアルタイム表示をとりやめ、受信1回 最大 1024 バイトまでに変更.

2018/03/23
 ・送信文字列に制御コードを追加。16進数値は、'<00>' .. '<ff> '(小文字) で対応
2018/03/20
 ・送信・受信を 64 バイトごとに変更
 ・制御文字の表示を追加 0D -> 'CR'、0A -> 'LF' ..

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 のみ。自己責任でお試し下さい。
スマホの画面サイズにも対応しています。

時間があれば、AKI-RS232C ラインモニタ用ツールの Android 版を作ってみたいですね。


■ソースコード
 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.