TWELITE ワイヤレス I/O 2019/07/08

「モノをつなぐ無線マイコン TWELITE(トワイライト)」を使ってみました。

使用したのは、10mW 出力の RED タイプ+外部アンテナと USB タイプのMONOSTICK (RED) です。
出荷時に書き込まれている「標準アプリ」で簡単に双方向のワイヤレス I/O が実現できます。
USB タイプの MONOSTICK を使うと、WIndows パソコン、OTG(USB ホスト機能)対応の Android 端末からでも使用できます。



■入出力(「標準アプリ」の場合)
 ・デジタル入力 4 点
 ・デジタル出力 4 点
 ・アナログ入力 4 点
 ・PWM 出力 4 点
 ・UART 通信
 ・I2C 通信
 DO と DI を短絡するとアンサーバックとして使え、出力信号が確実に届いたことが分かります。
 すべてが同時に使えるので、使い勝手が良いです。
 「中継機」に設定すると、「遮断物の回避」、「通信距離の延長」ができるようです。
 USBタイプのMONOSTICK と USBバッテリーを持っておくと簡単に設置でき、通信範囲が広がりそうです。

 非常に多機能で、「標準アプリ」以外にも用途ごとのアプリが用意されています。
 「シリアル通信アプリ」に書き換え「透過モード」を使うと送信バイト数の制限はありますが、シリアル (UART) 通信の無線化も実現できそうです。
 「リモコンアプリ」の場合は、ポート数が増加し、最大12 ポート(DIO) が使用できるようです。
 「無線タグアプリ」の場合は、センサーのデータ収集ができるようです。

■通信距離
 ・10mW のレッドタイプ+外部アンテナ × 2台の組み合わせでは、見通し130m程度
 ・10mW のレッドタイプ+外部アンテナ + MONOSTICK (RED) の組み合わせでは、見通し100m
 あくまでこちらの環境で試した見通し距離です。樹木の影だと通信できなくなったりします。
 同じテスト環境で、同じ 2.4GH z帯の Bluetooth Class1 (10mW) と Android スマホ 通信とほぼ同じ結果でした。

■Windows、Android から使う
 Widnows の場合、MONOSTICK をパソコンの USB に接続すると、COM ポートが作成されます。
 Androidの場合は、ポートは関係ありません。OTG対応であれば、つながると思います。
 「標準アプリ」では、51 文字の文字列を送り続ける仕様になっており、これを受信します。
 その文字列から端末情報、電波状況、デジタル入力、アナログ入力、電源電圧等の値を取得します。
 また、特定のコマンドを送信することで、デジタル出力、PWN出力の値を変えることができます。
 ※TWELITE DIP の場合は、UART に シリアル-USB コンバータ を付けると、USB 経由で接続できます。
 

■Android サンプルアプリ

 

 ・ダウンロード
  TweAnd.apk (apk 本体のみ)
  動作確認:Nexus 7 のみ
  開発環境;Delphi 10.2.3 Community Edition
   ※通信は、有限会社 シー・エス・ディー の Uni232C コンポーネントを使用しています。

 ・著作権、免責事項
  本アプリの著作権は、作者 f.izawa が所有し、これを主張します。
  本アプリをインストール、使用したことによる事故、損害等の一切について、作者はその責を負いません。

■参考文献
 ・「TWE-Lite ではじめる簡単電子工作」 大澤 文孝著(工学社)


// Delphi 10.2.3
// 要: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;
    Button4: TButton;
    Uni232C1: TUni232C;
    Button6: TButton;
    StringGrid1: TStringGrid;
    StringColumn1: TStringColumn;
    StringColumn2: TStringColumn;
    StringColumn3: TStringColumn;
    RoundRect1: TRoundRect;
    RoundRect2: TRoundRect;
    RoundRect3: TRoundRect;
    RoundRect4: TRoundRect;
    Rectangle1: TRectangle;
    Rectangle2: TRectangle;
    Rectangle3: TRectangle;
    Rectangle4: TRectangle;
    Label1: TLabel;
    Label2: TLabel;
    Label5: TLabel;
    Label6: TLabel;
    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;
    Rectangle13: TRectangle;
    Rectangle14: TRectangle;
    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;

  RecBuf : string;
  RecData : array [0..1023] of Byte;
  RecIndex : integer;
  LoopFlag : boolean;
  ThComRead : TComReadThread;

  ToneGenerator: JToneGenerator;

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 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
            RecBuf := '';
            for j := 0 to RecIndex - 1 do RecBuf := RecBuf + Char(RecData[j]);
            // 受信結果を表示
            Synchronize(procedure
              begin
                Form2.DispRecData;
              end
            );
            RecIndex := 0;
          end
          else begin
            RecData[RecIndex] := AData[i];
            Inc(RecIndex);
          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 := 115200;
    ByteSize := Bit8;
    StopBits := StopBit1;
    ParityBits := ParityNone;
    Uni232C1.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
var
  btn : TCornerButton;
  s, sum, msk, bit : string;
  i, isum : integer;
  ret : integer;
  AData : TBytes;
  len : integer;
begin
  if Uni232C1.Connect then begin
    btn := Sender as TCornerButton;
    ret := -1;
    for i := 1 to 8 do begin
      if btn = FindComponent('CornerButton' + i.ToString) then begin
        ret := i;
        break;
      end;
    end;
    if ret > 0 then begin
      // 1~4 = ON
      if ret <= 4 then begin
        ret := 1 shl (ret -1);
        bit := IntToHex(ret, 2);
        msk := bit;
      end
      // 5~8 = OFF
      else begin
        bit := '00';
        ret := ret - 4;
        ret := 1 shl (ret -1);
        msk := IntToHex(ret, 2);
      end;

      s := '78'; // 宛先:78 = 子機
      s := s + '8001';  // 固定
      s := s + bit;  // DO ON
      s := s + msk;  // マスク
      s := s + 'FFFFFFFFFFFFFFFF'; // PWM 出力 4ch 分
      // チェックサム(Ver.1.6.5 以降は'X'に差し替え可)
      isum := 0;
      for i := 0 to s.Length div 2 -1 do
        isum := isum + StrToInt('$'+s.Substring(i * 2, 2));
      isum := isum and $FF;
      isum := $100 - isum;
      sum := IntToHex(isum, 2);
      // 送信文字列
      s := ':' + s + sum + #13#10;
      AData := TEncoding.ANSI.GetBytes(s);
      len := Length(AData);
      Uni232C1.Write(len, @AData[0]);
      Beep(0);
    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
  s, s1 : string;
  i : integer;
  d : double;
  ai, ef, mv : array [0..3] of integer;
  efn : integer;
begin
  s := RecBuf;
  if (s.Length >= 49) and (s.Substring(0, 1) = ':') then begin
    with StringGrid1 do begin
      Cells[1, 0] := s.Substring(1, 2);  //' 送信元デバイスID';
      Cells[1, 1] := s.Substring(3, 2);  // 'コマンド番号';
      Cells[1, 2] := s.Substring(5, 2);  // 'パケット識別子';
      Cells[1, 3] := s.Substring(7, 2);  // 'プロトコルバージョン';

      Cells[1, 4] := s.Substring(9, 2); // '受信電波品質';
      i := StrToInt('$' + Cells[1, 4]);  // LQI
      d := (i * 7 - 1970) / 20; // LQI -> dBm
      Cells[2, 4] := i.ToString +  Format(' (%.0f dBm)', [d]);
      with Rectangle14 do begin
        Width := Trunc(i * 345 / 255);
        if i< 50 then Fill.Color := claLightslategray
        else if i < 100 then Fill.Color := claLightsteelblue
        else if i < 150 then Fill.Color := claLightskyblue
        else Fill.Color := claAquamarine;
      end;

      Cells[1, 5] := s.Substring(11, 8); // '個体識別番号';
      Cells[1, 6] := s.Substring(19, 2); // '端末デバイスID';

      Cells[1, 7] := s.Substring(21, 4); // 'タイムスタンプ';
      i := StrToInt('$' + Cells[1, 7]);
      d := i / 64; // sec
      Cells[2, 7] := Format('%.1f sec', [d]);

      Cells[1, 8] := s.Substring(25, 2); // '中継フラグ';

      Cells[1, 9] := s.Substring(27, 4); // '電源電圧';
      i := StrToInt('$' + Cells[1, 9]);
      d := i / 1000; // volt
      Cells[2, 9] := Format('%.3f V', [d]);

      Cells[1, 10] := s.Substring(33, 2); // 'デジタル入力値';
      i := StrToInt('$' + Cells[1, 10]);
      Cells[2, 10] := Format('%d', [i]);

      with RoundRect1 do begin
        if i and 1 > 0 then Fill.Color := claRed
        else Fill.Color := claLime;
      end;
      with RoundRect2 do begin
        if i and 2 > 0 then Fill.Color := claRed
        else Fill.Color := claLime;
      end;
      with RoundRect3 do begin
        if i and 4 > 0 then Fill.Color := claRed
        else Fill.Color := claLime;
      end;
      with RoundRect4 do begin
        if i and 8 > 0 then Fill.Color := claRed
        else Fill.Color := claLime;
      end;

      Cells[1, 11] := s.Substring(35, 2); // 'デジタル入力変更状態';
      i := StrToInt('$' + Cells[1, 11]);
      Cells[2, 11] := Format('%d', [i]);

      s1 := s.Substring(37, 10); // 'アナログ入力値';
      Cells[1, 12] := s1;

      // 補正値
      efn := StrToInt('$' + s1.Substring(8, 2));
      for i := 0 to 3 do begin
        // AI 値
        ai[i] := StrToInt('$' + s1.Substring(i * 2, 2));
        // ch 毎の補正値
        ef[i] := efn and 3; // 下位 2 ビット
        mv[i] := (ai[i] * 4 + ef[i]) * 4;
        efn := efn shr 2;
      end;

      // 電圧(有効範囲:0 ~ 2000 mV)
      // 2V を超えると 4092
      Label1.Text := Format('%4d', [mv[0]]);
      Label2.Text := Format('%4d', [mv[1]]);
      Label5.Text := Format('%4d', [mv[2]]);
      Label6.Text := Format('%4d', [mv[3]]);

    end;
  end;
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);

  with StringGrid1 do begin
    Cells[0, 0] := '送信元デバイスID';
    Cells[0, 1] := 'コマンド番号';
    Cells[0, 2] := 'パケット識別子';
    Cells[0, 3] := 'プロトコルバージョン';
    Cells[0, 4] := '受信電波品質';
    Cells[0, 5] := '個体識別番号';
    Cells[0, 6] := '端末デバイスID';
    Cells[0, 7] := 'タイムスタンプ';
    Cells[0, 8] := '中継フラグ';
    Cells[0, 9] := '電源電圧';
    Cells[0, 10] := 'デジタル入力値';
    Cells[0, 11] := 'デジタル入力変更状態';
    Cells[0, 12] := 'アナログ入力値';
  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;
    Beep(1);
  end;
end;

end.