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.