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 494D414E53424B00000006<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.