IM920 ワイヤレス I/O 2019/07/13

920MHz帯 無線モジュール IM920 を使ってみました。

16点入力、16点出力、アナログ入力とシリアル通信が使えます。
TWELITE DIP のようにすべてが同時に使えるわけではなく、いずれかのモードを選択する必要があります。
8 点のリモコンとして使う場合は、アンサーバック機能が使え、相手に確実に届いたことが確認できます。
また、シリアル (UART) から相手機の接点入力状態の取得、アナログ入力値の取得のほか、接点出力も行えます。



シリアル通信 (UART - UART) は、独自のヘッダーが付加されます。
TWELITEの透過モード、双葉電子 FEP-01 のヘッダーレス通信のような使い方はできないようです。

通信距離(送受とも外部アンテナタイプ)
 
 IM920 は、920MHz 帯を使っています。同じ 10mW の 2.4GHz帯の Bluetooth、Wi-Fi、TWELITE ... と比べて飛び具合が違います。
 樹木の影、ある程度の建物の影でも通信でき、距離としては、見通し 300 m 以上、ビル影でも 200~250m でした。
 マンションの扉を超えたのは、IM920 だけです。

リモコンとして使う
 
 最初に相手機器の登録が必要です。→ ペアリングの方法の「4. REG 端子を用いる方法」
 あとは、モード(MODE1, 2, 3) を設定するだけで使えます。→ 取扱説明書(ハードウェア編) の「7-6.動作モードの設定」
 ※モード変更後は、電源の OFF → ON が必要です。
 
パソコン / Android から使う

 パソコンに接続するには、USB シリアル変換(FT231 等)が必要ですが、作成するのが面倒なので、IM315-USB-RX を使いました。
 実装する場合は、送信側、受信側とも USB シリアル変換を付けておくと、モジュールの抜き差しをしなくて良いので使いやすいと思います。

 パソコンに USB コネクタを接続すると自動で COM ポートが作成されます。(Windows 10 環境です)
 Android の場合、OTG(USBホスト)機能に対応していれば、同じように使えると思います。
 IM920 とは、この COM ポートを使って通信します。19200bps、8 DataBits、1 StopBits、ParityNone、FlowcontrolNone でつながります。
 
 "RDVR" + <CR><LF> を送信してみます。バージョン番号が返ってくれば成功です。
 
 コマンドは大文字、小文字を混在できます。バイト境界のカンマ、スペースは無視されます。
 行末に、<CR><LF> (0Dh, 0Ah) が必要です。

 相手機器の登録(コマンドを使わず、REG スイッチを使ってハード的に登録することも可能です)
  ENWR コマンドで、パラメータ書き込みを許可にします。
  SRID コマンドで、お互いに相手機のシリアル番号を 16 進数で受信 ID に登録してます。
  16 進変換が面倒な時は、RDID コマンドで、自機のシリアル番号の 16 進数表記が分かります。これを相手機器の受信 ID に登録します。
  必要な場合は、DIOR コマンドで、パラメータ書き込みを禁止にします。そのままでも良いと思います。 

パソコン / Adroid から接点入力の状態を取得

 入力側のモードは、スイッチ入力モード(MODE1=H、MODE2=H、MODE3=L)
 パソコン側のモードは、データモード(MODE1=H、MODE2=L、MODE3=L)
 ※モード変更後は、電源の OFF → ON が必要です。

 入力側、パソコン側とも、アンサーバックは無効(デフォルト)です。有効の場合は、結果が異なります。

 ・アンサーバック無効(デフォルト)のとき

 ↓のスクリーンショットは、IO9 を押して離した時の受信文字列(16 文字+CrLf)です。 押している間は連続、離した後は数回出力されます。
 ノード番号(00),送信側のモジュール ID(3CBC), RSSI値(入力信号レベル B4) : IO1~8の値(00), IO9~16の値(01) + CrLf
 接点情報は、":"以降の","を無視した 4 文字です。
 最初の 1 文字目が IO5~8、2 文字目が IO1~4、3 文字目が IO13~16、4 文字目が IO9~12 の状態情報になります。
 IO 番号の若い順からそれぞれ1, 2, 4, 8 の値があり、ON の時は、その値を合計した値になります。
 例えば、1 文字目が 4 の時は IO7、2 文字目が 3 の時は、IO1, IO2 が ON であることが分かります。
 離した後は 0000 になり、すべて IO が OFF であることが分かります。
 

 
 ・アンサーバック有効のとき
  「パソコンから接点出力を操作」のレスポンスと同じです。
  どのスイッチが押されたかが分かるだけで、他の入力を含めて状態は取得できません。
  また、アンサーバックを送信している間は次の操作ができません。STATUS LED が消灯する(3 秒ほど)のを待って次の操作を行います。
  接点入力は、IO1~IO8 までが使用できます。IO9~16 は IO1~8 のアンサーバックの出力になります。


パソコン / Android から接点出力を操作

 出力側のモードは、接点 16 出力モード、プッシュ動作 (MODE1=H、MODE2=L、MODE3=H)
 パソコン側のモードは、データモード (MODE1=H、MODE2=L、MODE3=L)
 ※モード変更後は、電源の OFF → ON が必要です。
 
 プッシュ動作(スイッチの場合は押している間だけ ON なる)なので、コマンドを送ると 0.5 秒程度 ON になります。

 ・アンサーバック無効(デフォルト)のとき
  IO1 と 2 だけを ON にする : TXDT 0300<CR><LF>
  IO9 と 14 だけを ON にする : TXDT 0021<CR><LF>
  正常に送信できた場合、"OK" が返ってきます。相手機に届いたかどうかの確認はできません。
  パラメータは受信と同じく最初の 2 文字が IO1~8、次の 2 文字が IO9~16で、それぞれ IO 番号の若い順から 1, 2, 4, 8, 10, 20, 40, 80 の和の値になります。
  こちらの環境では、1.5 秒程度の周期で送信可能でした。

 ・アンサーバック有効のとき
  あらかじめ、送信側、受信側ともアンサーバックを有効にします。
  ENWR<CR><LF> (パラメータ書き込み許可)
  EABK<CR><LF> (アンサーバック有効)

  出力は、IO1~IO8 までが使用できます。
  アンサーバック無効のときとは、コマンド、パラメータの指定方法が異なります。
  IO2 と 3 だけを ON にする : TXDA 494D414E53424B000000
06<CR><LF>
  パラメータ "494D414E53424B0000" は固定値で、最後の 2 文字が 出力値です。"06" を "FF" に変えると、IO1~8 まですべて ON になります。
  IO1 ~8 は若い順に 1, 2, 4 ,8, 16, 32, 64, 128 値 (2 の (n-1) 乗) を持ち、ON にする IO の値の和を 2 桁の 16 進表記に変えた文字列で指定します。
  06 の時は、2 (IO2 ) + 4 (IO3) = 6 で、IO2 と IO3 のみが ON になります。

  正常に送信できた場合、"OK"が返ってきます。
  相手機に届いたときは、その数ミリ秒後、アンサーが返ってきます。最後の 2 文字が 出力の情報です。
  相手機に届かなかったときは、何も返ってきません。
  こちらの環境では、3 秒程度の周期で送信可能でした。

  Windows 用サンプルアプリでのアンサーバック
 

  Android 用サンプルアプリでのアンサーバック
 

まとめ

 ・パソコン / Android 端末から接点入力の状態を監視する場合は、アンサーバック無効(デフォルト)。出力操作の場合は、アンサーバック有効で使う。
 ・パソコン / Android 端末から出力を操作する場合、プッシュ動作が分かりやすい。
 ・リモコンで使う(人が操作する)場合は、アンサーバック有効で使う。
 ・標準では、入力 4 点 + 出力4 点のような使い方はできない。シリアル通信+マイコン / PLC で実現できそう。
 ・920MHz 帯の飛び方は素晴らしい。感動した。

ダウンロード

 アンサーバック有効でのパソコン -> 8 点出力(プッシュ動作)のテストに作ったものです。
 アンサーバック有効での 8 点入力 -> パソコンの確認にも使えます。

 IM920DO..zip (Winodws 用 IM920DO.exe 本体のみ)

 IM920DO..apk (Android 用 IM920DO.apk 本体)(OPEN -> READ 後、OUT ボタンをタップしてください)
 ※ OTG(USB ホスト機能)対応の機種でないと使えません。


■ ソースコード Windows 版 (要:ApdComPort コンポーネント)

unit Unit4;

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, System.UITypes, AdSelCom, IniFiles;

type
  TForm4 = class(TForm)
    ApdComPort1: TApdComPort;
    Button1: TButton;
    Button2: TButton;
    Memo1: TMemo;
    Timer1: TTimer;
    GroupBox1: TGroupBox;
    Button5: TButton;
    Button6: TButton;
    Button7: TButton;
    Button8: TButton;
    Shape1: TShape;
    Shape2: TShape;
    Shape3: TShape;
    Shape4: TShape;
    Button9: TButton;
    Button10: TButton;
    Button11: TButton;
    Button12: TButton;
    Shape5: TShape;
    Shape6: TShape;
    Shape7: TShape;
    Shape8: TShape;
    ComboBox1: TComboBox;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure ApdComPort1TriggerAvail(CP: TObject; Count: Word);
    procedure Button5Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    resBuf : string;
  end;

var
  Form4: TForm4;

implementation

{$R *.dfm}

// 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;

procedure TForm4.ApdComPort1TriggerAvail(CP: TObject; Count: Word);
// アンサーバック有効の時、接点出力の操作状態を取得
// (アンサーバック有効の時、接点入力のアンサーバック確認にも使える)
var
  i : Word;
  ch : AnsiChar;
  shp : TShape;
  resNo : integer;
  j, k : integer;
  coOn, coOff : TColor;
begin
  coOn := clLime;
  coOff := clWhite;

  for i := 1 to Count do begin
    ch := ApdComPort1.GetChar;
    resBuf := resBuf + string(ch);
    if ch = #10 then begin
      resBuf := Trim(resBuf); // CrLfを削除
      Memo1.Lines.Add(resBuf);
      resBuf := StringReplace(resBuf, ',', '', [rfReplaceAll]);
      if Pos('494D414E53424B', resBuf) > 0 then begin
        // 出力の状態
        resNo := StrToIntDef(Copy(resBuf, Length(resBuf)- 1), 0);
        if resNo > 0 then begin
          for j := 1 to 8 do begin
            // 出力の状態
            if j <= 4 then begin
              resNo := StrToIntDef(Copy(resBuf, Length(resBuf)), 0);
              k := j;
            end
            else begin
              resNo := StrToIntDef(Copy(resBuf, Length(resBuf) -1, 1), 0);
              k := j - 4;
            end;
            if resNo and intPower(2, k - 1) > 0 then begin
              shp := FindComponent('Shape' + j.ToString) as TShape;
              if shp <> nil then begin
                // '00'から始まるシーケンス番号の最後
                if Copy(resBuf, Length(resBuf) - 3, 2) = '04' then
                  shp.Brush.Color := coOff
                else if shp.Brush.Color <> coOn then
                  shp.Brush.Color := coOn;
              end;
            end;
          end;
        end;
      end;
      resBuf := '';
    end;
  end;
end;

procedure TForm4.Button1Click(Sender: TObject);
// COM ポートオープン
var
  comNo : integer;
begin
  comNo := - 1;
  with ComboBox1 do begin
    if ItemIndex >= 0 then begin
      comNo := StrToIntDef(Copy(Items[ItemIndex], 4), -1);
    end;
  end;
  if comNo >= 0 then begin
    with ApdComport1 do begin
      ComNumber := comNo;
      Baud := 19200;
      DataBits := 8;
      StopBits := 1;
      Parity := TParity.pNone;
      try
        Open:= True;
        if Open then Memo1.Lines.Add('COMPORT OPEN OK')
        else Memo1.Lines.Add('COMPORT OPEN NG');
      except
        Memo1.Lines.Add('COMPORT ERROR');
      end;
    end;
  end;
end;

procedure TForm4.Button2Click(Sender: TObject);
// COM ポートクローズ
begin
  with ApdComport1 do begin
    if Open then begin
      Open := False;
      Memo1.Lines.Add('COMPORT CLOSE');
    end;
  end;
end;

procedure TForm4.Button5Click(Sender: TObject);
// OUT 1~8
// MODE = H, L, H
// プッシュ動作+アンサーバック有効
var
  btn : TButton;
  outNo : integer;
  cmd : string;
begin
  btn := Sender as TButton;
  outNo := StrToIntDef(Copy(btn.Caption, Length(btn.Caption)), 0);
  if outNo > 0 then begin
    with ApdComport1 do begin
      if Open then begin
        resBuf := '';
        cmd := 'TXDA494D414E53424B000000';
        cmd := cmd + IntToHex(intPower(2, (outNo - 1)), 2);
        PutString(cmd + #13#10);
        Memo1.Lines.Add('>'+cmd);
      end;
    end;
  end;
end;

procedure TForm4.FormCreate(Sender: TObject);
// フォーム生成
var
  i : integer;
  ini : TIniFile;
  s : string;
begin
  Memo1.Lines.Clear;

  // COM ポートの列挙
  AdSelCom.ShowPortsInUse := False;
  for i := 0 to 32 do if AdSelCom.IsPortAvailable(i) then
    ComboBox1.Items.Add (AdPort.ComName(i));

  // 前回選択した COM ポート名を読み込み
  ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  try
    s := ini.ReadString('Comport', 'ComName', '');
    with ComboBox1 do begin
      if (s <> '') and (Items.Count > 0) then begin
        for i := 0 to Items.Count - 1 do begin
          if Items[i] = s then begin
            ItemIndex:= i;
            break;
          end;
        end;
      end;
    end;
  finally
    ini.Free;
  end;
end;

procedure TForm4.FormDestroy(Sender: TObject);
// フォーム破棄
var
  ini : TIniFile;
begin
  // 選択した COM ポート名を保存
  ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini'));
  try
    ini.WriteString('Comport', 'ComName', ComboBox1.Text);
  finally
    ini.Free;
  end;
end;

end.

■ ソースコード Android (要: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, FMX.Memo;

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;
    Button4: TButton;
    Uni232C1: TUni232C;
    Button6: TButton;
    RoundRect1: TRoundRect;
    RoundRect2: TRoundRect;
    RoundRect3: TRoundRect;
    RoundRect4: TRoundRect;
    Rectangle5: TRectangle;
    Rectangle6: TRectangle;
    Rectangle7: TRectangle;
    Rectangle8: TRectangle;
    CornerButton1: TCornerButton;
    CornerButton2: TCornerButton;
    CornerButton3: TCornerButton;
    CornerButton4: TCornerButton;
    Rectangle9: TRectangle;
    CornerButton5: TCornerButton;
    Rectangle10: TRectangle;
    CornerButton6: TCornerButton;
    Rectangle11: TRectangle;
    CornerButton7: TCornerButton;
    Rectangle12: TRectangle;
    CornerButton8: TCornerButton;
    RoundRect5: TRoundRect;
    RoundRect6: TRoundRect;
    RoundRect7: TRoundRect;
    RoundRect8: TRoundRect;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    //procedure Timer1Timer(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure Uni232C1UsbDettach(Sender: TObject);
    procedure Button6Click(Sender: TObject);
    procedure CornerButton1Click(Sender: TObject);
  private
    { private 宣言 }
  public
    { public 宣言 }
    procedure DispRecData;
  end;

var
  Form2: TForm2;

  ResBuf : string;
  ResData : array [0..1023] of Byte;
  ResIndex : integer;
  LoopFlag : boolean;
  ThComRead : TComReadThread;

  ToneGenerator: JToneGenerator;

implementation

{$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;

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 if typ = 1 then
    // TONE_PROP_BEEP  = 400Hz+1200Hz, 35ms ON
    ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_BEEP)
  else
    // TONE_PROP_ACK  = 1200Hz, 100ms ON, 100ms OFF 2 bursts
    ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK);

end;
// -----------------------------------------------------------------------------
procedure TComReadThread.ComRead;
var
  ret : integer;
  AData : TBytes;
  i, j : 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
          if AData[i]= $0A then begin // <LF>
            resBuf := '';
            for j := 0 to resIndex - 1 do resBuf := resBuf + Char(resData[j]);
            // 受信結果を表示
            Synchronize(procedure
              begin
                Form2.DispRecData;
              end
            );
            resIndex := 0;
          end
          else begin
            resData[resIndex] := AData[i];
            Inc(resIndex);
          end;
        end;
      end;
    end
    else
      loopFlag := False;
  end;
end;

constructor TComReadThread.Create;
begin
  // スレッドを生成、直ちに実行
  inherited Create(False);
  // スレッド終了時、スレッドオブジェクトを破棄
  FreeOnTerminate := True;
end;

procedure TComReadThread.Execute;
begin
  ComRead;
end;

// -----------------------------------------------------------------------------
procedure TForm2.Button1Click(Sender: TObject);
// OPEN
var
  ret : integer;
begin
  with Uni232C1 do begin
    BaudRate := 19200;
    ByteSize := Bit8;
    StopBits := StopBit1;
    ParityBits := ParityNone;
    FlowControls := CtrlNone;
    SetModemStatus($0300);

    ret := Open;
    if ret < 0 then
      ShowMessage('Cannot OPEN' + Error2Str(ret))
    else begin
      Beep(2);
    end;
  end;
end;

procedure TForm2.Button2Click(Sender: TObject);
// CLOSE
begin
  if Assigned(ThComRead) then loopFlag := False;
  Uni232C1.Close;
  Beep(1);
end;

procedure TForm2.Button4Click(Sender: TObject);
// READ
begin
  if Assigned(ThComRead) then begin
    ThComRead.TerminatedSet;
    ThComRead := nil;
  end;

  if Uni232C1.Connect then begin
    ThComRead := TComReadThread.Create;
    Beep(2);
  end;
end;

procedure TForm2.Button6Click(Sender: TObject);
// READ STOP
begin
  if Assigned(ThComRead) then begin
    loopFlag := False;
    Beep(1);
  end;
end;

procedure TForm2.CornerButton1Click(Sender: TObject);
// DO 1~8
var
  btn : TCornerButton;
  AData : TBytes;
  len : integer;
  outNo : integer;
  cmd : string;
begin
  if Uni232C1.Connect then begin
    btn := Sender as TCornerButton;
    // 押された IO の番号
    outNo := StrToIntDef(Copy(btn.Text, 4), -1);
    if outNo >= 0 then begin
      resBuf := '';
      cmd := 'TXDA494D414E53424B000000';
      cmd := cmd + IntToHex(intPower(2, (outNo - 1)), 2);
      AData := TEncoding.ANSI.GetBytes(cmd + #13#10);
      len := Length(AData);
      Uni232C1.Write(len, @AData[0]);
      Memo1.Lines.Clear;
      Memo1.Lines.Add('>' + cmd);
      Beep(1); // プッ
    end;
  end;
end;

procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if Assigned(ThComRead) then begin
    ThComRead.TerminatedSet;
    ThComRead := nil;
  end;

  Uni232C1.Close;
  ToneGenerator.release;
end;

procedure TForm2.DispRecData;
var
  resNo : Integer;
  rect  : TRoundRect;
  j, k : integer;
  coOn, coOff : Cardinal;
begin
  coOn := claLime;
  coOff := claWhite;
  resBuf := Trim(resBuf); // 制御文字を削除
  Memo1.Lines.Add(resBuf);

  resBuf := StringReplace(resBuf, ',', '', [rfReplaceAll]);
  if Pos('494D414E53424B', resBuf) > 0 then begin
    // 出力の状態(最後の 2 文字)
    resNo := StrToIntDef(Copy(resBuf, Length(resBuf) - 1), 0);
    //Memo1.Lines.Add('resNo = ' + resNo.ToString);
    if resNo > 0 then begin
      for j := 1 to 8 do begin
        // 出力の状態
        if j <= 4 then begin
          // 最後の 1 文字
          resNo := StrToIntDef(Copy(resBuf, Length(resBuf)), 0);
          k := j;
        end
        else begin
          // 最後から 2 文字目の 1 文字
          resNo := StrToIntDef(Copy(resBuf, Length(resBuf) -1, 1), 0);
          k := j - 4;
        end;

        if resNo and intPower(2, k - 1) > 0 then begin
          rect := FindComponent('RoundRect' + j.ToString) as TRoundRect;
          if rect <> nil then begin

            // '00'から始まるシーケンス番号の最後
            if Copy(resBuf, Length(resBuf) - 3, 2) = '04' then begin
              rect.Fill.Color := coOff;
            end
            else if rect.Fill.Color <> coOn then
              rect.Fill.Color := coOn;
            if Copy(resBuf, Length(resBuf) - 3, 2) = '00' then begin
              Beep(0); // ピピッ
            end;
          end;
        end;
      end;
    end;
  end;
  resBuf := '';
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
  // 縦画面に固定
  Application.FormFactor.Orientations :=
    [TFormOrientation.Portrait, TFormOrientation.InvertedPortrait];

  ToneGenerator := TJToneGenerator.JavaClass.init(
    TJAudioManager.JavaClass.STREAM_ALARM,
    TJToneGenerator.JavaClass.MAX_VOLUME);

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;
    Beep(1);
  end;
end;

end.