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.