IM920 シリアル I/O (その2) 2019/07/25, 26

・2019/07/26 PWM x 2 点を追加。コードを変更。


920MHz 無線モジュール IM920 の「データモード」(シリアル通信)を使っています。
送信側から送られる文字列を受信側の Arduino UNO で受け、その内容により、デジタル入力 4 点 + デジタル出力 2 点 + アナログ入力 4 点を使います。
※ IM920 と Arduino の接続には、レベルシフタが必要です。面倒なので、Arduino 用 シールド (IM315-SHLD-RX-V2) を使いました。
 送信側は、USB インターフェイスモジュール (IM315-USB-RX) を使っています。
 
下記、サンプルでは、500 msec 周期で、デジタル、アナログの入力状態をポーリングしています。
実用の時は、送信側には Android 端末を使う予定です。
OTG (USBホスト)機能 対応であれば 「USBインターフェイスモジュール」 がそのまま使えます。

 

■サンプルコード
 送信側 : Delphi 10.2.3、受信側 : Arduino UNO R3
 ※データ送信は、"TXDA"コマンドを使用し、入出力設定は、送受信側とも規定値の "DCIO" (HEX 入出力設定) です。
  "ECIO" (キャラクタ入出力設定) の場合は、コードの変更が必要です。
  詳しくは、「IM920 取扱説明書(ソフトウェア編)」を参照してください。


// 送信側 : Delphi 10.2.3
// 要:ApdComPort コンポーネント
{
■コマンド

・SWRD : デジタル入力 4 点読み込み(LOW で ON)
  >レスポンス : SWRDx (x は 入力の値を 16進表記)
  SW1 = 1, SW2 = 2, SW3 = 4, SW4 = 8 の合計値。4 点すべて ON では 'F'

・ADRD : アナログ入力 4 点読み込み (基準電圧 1.1V)
  >レスポンス : ADRDh1h2h3h4 (h1~h4 は A0~A3 の入力の値を 16進表記)

・SARD : デジタル入力 4 点 + アナログ入力 4 点読み込み
 SWRD + ADRD を 1 つにしたもの
  >レスポンス : SARDxh1h2h3h4 (x, h1~h4 は 入力の値を 16進表記)

・RY1ON, RY2ON, RY1OF, RY2OF : デジタル出力 2 点 ON または OFF
  >レスポンス : RYOK01, RYOK02

・RY1PLS, RY2PLS : デジタル出力 2 点 ON/OFF (500msec ワンパルス)
  >レスポンス : RYOK01, RYOK02

・PWM1hh, PWM2hh : PWM 出力 2 点 (hh は 0~5V に対して 0~255 を16進表記)
  >レスポンス : PWMOK01, RWMOK02
}


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, Vcl.Buttons;

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;
    ComboBox1: TComboBox;
    Edit1: TEdit;
    Edit2: TEdit;
    Edit3: TEdit;
    Edit4: TEdit;
    Shape5: TShape;
    Shape6: TShape;
    CheckBox1: TCheckBox;
    v: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label1: TLabel;
    Label5: TLabel;
    Label6: TLabel;
    Label7: TLabel;
    Label8: TLabel;
    Label9: TLabel;
    Edit5: TEdit;
    Label10: TLabel;
    Label11: TLabel;
    Edit6: TEdit;
    Shape7: TShape;
    Shape8: TShape;
    SpeedButton1: TSpeedButton;
    SpeedButton2: TSpeedButton;
    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);
    procedure Timer1Timer(Sender: TObject);
    procedure CheckBox1Click(Sender: TObject);
    procedure SpeedButton1Click(Sender: TObject);
    procedure Edit5KeyPress(Sender: TObject; var Key: Char);
    procedure Edit6KeyPress(Sender: TObject; var Key: Char);
  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);
// IM920 から受信文字
var
  i : Word;
  ch : AnsiChar;
  shp : TShape;
  j : integer;
  coOn, coOff : TColor;
  n, v, m : integer;
  s, cmd, sv : string;
  edt : TEdit;
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); // 前後の制御文字、空白を削除
      Memo1.Lines.Add(resBuf);
      resBuf := StringReplace(resBuf, ',', '', [rfReplaceAll]);
      n := Pos(':', resBuf);
      if n > 0 then begin
        cmd := Copy(resBuf, n + 1, 8);
        s := Copy(resBuf, n + 8 + 1);
        Memo1.Lines.Add(cmd +'=' + s);
        // スイッチ入力(4 点)
        if cmd = '53575244' then begin  // 'SWRD'
          Memo1.Lines.Add('SWRD');
          v := StrToInt('$' + s);
          for j := 0 to 3 do begin
            shp := FindComponent('Shape' + IntToStr(j + 1)) as TShape;
            if shp <> nil then begin
              if v and IntPower(2, j) > 0 then begin
                if shp.Brush.Color <> coOn then shp.Brush.Color := coOn;
              end
              else begin
                if shp.Brush.Color <> coOff then shp.Brush.Color := coOff;
              end;
            end;
          end;
        end
        // アナログ入力(4 点)
        else if cmd = '41445244' then begin // 'ADRD'
          Memo1.Lines.Add('ADRD');
          m := Length(s) div 4;
          v := 0;
          for j := 0 to m - 1 do begin
            sv := Copy(s, 1 + j * 4, 4);
            if sv <> '' then begin
              v := StrToInt('$' + sv);
              Memo1.Lines.Add('AD' + IntToStr(j+1) +' = ' + IntToStr(v));
            end;
            edt := FindComponent('Edit' + IntToStr(j + 1)) as TEdit;
            if edt <> nil then
              edt.Text := Format('%.2f', [(v / 1023) * 1.1]);
          end;
        end
        // スイッチ入力(4 点) +  アナログ入力(4 点)
        else if cmd = '53415244' then begin  // 'SARD'
          v := StrToInt('$' + Copy(s, 1, 2));
          Memo1.Lines.Add(intToStr(v));
          for j := 0 to 3 do begin
            shp := FindComponent('Shape' + IntToStr(j + 1)) as TShape;
            if shp <> nil then begin
              if v and IntPower(2, j) > 0 then begin
                if shp.Brush.Color <> coOn then shp.Brush.Color := coOn;
              end
              else begin
                if shp.Brush.Color <> coOff then shp.Brush.Color := coOff;
              end;
            end;
          end;
          m := (Length(s) - 2) div 4;
          v := 0;
          for j := 0 to m - 1 do begin
            sv := Copy(s, 3 + j * 4, 4);
            if sv <> '' then v := StrToInt('$' + sv);
            edt := FindComponent('Edit' + IntToStr(j + 1)) as TEdit;
            if edt <> nil then
              edt.Text := Format('%.2f', [(v / 1023) * 1.1]);
          end;
        end
        // リレー x 2 個
        else if cmd = '5259514B' then begin  // 'RYOK'
          v := StrToIntDef(s, 0);
          if (v = 1) or (v = 2) then begin
            // アンサーバック
            if v = 1 then shp := Shape5
            else shp := Shape6;
            with shp do begin
              Brush.Color := coOn;
              Repaint;
              Sleep(500);
              Brush.Color := coOff;
              Repaint;
            end;
          end;
        end
        // PWM x 2 個
        else if cmd = '50574D4F' then begin  // 'PWMO'
          // s には 'K' が含まれる
          v := StrToIntDef(Copy(s, 3), 0);
          if (v = 1) or (v = 2) then begin
            // アンサーバック
            if v = 1 then shp := Shape7
            else shp := Shape8;
            with shp do begin
              Brush.Color := coOn;
              Repaint;
              Sleep(500);
              Brush.Color := coOff;
              Repaint;
            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);
// RY1,2 On/Off
var
  btn : TButton;
  cmd : string;
  s : string;
  i : integer;
begin
  btn := Sender as TButton;
  s := btn.Caption;
  s := StringReplace(s, ' ', '', [rfReplaceAll]);
  with ApdComport1 do begin
    if Open then begin
      Timer1.Enabled := False;
      Sleep(400); // 400 ~ 500 msec
      resBuf := '';
      cmd := 'TXDA';
      for i := 1 to Length(s) do
        cmd := cmd + IntToHex(Ord(s[i]), 2);
      PutString(cmd + #13#10);
      Memo1.Lines.Add('>'+cmd);
      Timer1.Enabled := CheckBox1.Checked;
    end;
  end;
end;

procedure TForm4.CheckBox1Click(Sender: TObject);
begin
  Timer1.Enabled := CheckBox1.Checked;
end;

procedure TForm4.Edit5KeyPress(Sender: TObject; var Key: Char);
// PWN キー入力 {Enter] でコマンド送信
begin
  if Key = #13 then begin
    Key := #00;
    SpeedButton1Click(SpeedButton1);
  end;
end;

procedure TForm4.Edit6KeyPress(Sender: TObject; var Key: Char);
begin
  if Key = #13 then begin
    SpeedButton1Click(SpeedButton2);
  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', '');
    CheckBox1.Checked := ini.ReadBool('CheckBox', 'Auto', True);

    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);
    ini.WriteBool('CheckBox', 'Auto', CheckBox1.Checked);
  finally
    ini.Free;
  end;
end;

procedure TForm4.SpeedButton1Click(Sender: TObject);
// PWN
// 送信文字列 ex: 'PWM1FF'
var
  pwmNo : integer;
  cmd, s : string;
  i : integer;
  v : Double;
  m : integer;
begin
  if Sender as TSpeedButton = SpeedButton1 then begin
    pwmNo := 1;
    v := StrToFloatDef(Edit5.Text, 0);
  end
  else begin
    pwmNo := 2;
    v := StrToFloatDef(Edit6.Text, 0);
  end;
  m := Trunc(v / 5.0 * 255);
  if m < 0 then m := 0;
  if m > 255 then m := 255;

  with ApdComport1 do begin
    if Open then begin
      Timer1.Enabled := False;
      Sleep(400);
      s := 'PWM' + pwmNo.ToString + IntToHex(m, 2);
      resBuf := '';
      cmd := 'TXDA';
      for i := 1 to Length(s) do
        cmd := cmd + IntToHex(Ord(s[i]), 2);
      PutString(cmd + #13#10);
      Memo1.Lines.Add('>'+cmd);
      Timer1.Enabled := CheckBox1.Checked;
    end;
  end;
end;

procedure TForm4.Timer1Timer(Sender: TObject);
// Interval = 500 msec
var
  s, cmd : string;
  i : integer;
begin
  with ApdComport1 do begin
    if Open then begin
      Timer1.Enabled := False;
      // DI x 4 点 + AI x 4 点の値を取得
      s := 'SARD';
      resBuf := '';
      cmd := 'TXDA';
      for i := 1 to Length(s) do
        cmd := cmd + IntToHex(Ord(s[i]), 2);
      PutString(cmd + #13#10);
      Timer1.Enabled := CheckBox1.Checked;
    end;
  end;
end;

end.

// Arduino UNO R3 + Arduino IDE 1.8.10
//IM920 で Serial I/O
// 2019/07/27 by f.izawa

#include <SoftwareSerial.h>

// Pin 8 ~ 11 は、IM920 にて使用
#define rxPin 8
#define txPin 9
#define busyPin 10

// DI x 4 点
int swPins[] = {2, 3, 4, 7};
// DO x 2 点
int ryPins[] = {12,13};
// PWM x 2 点
int pwmPins[] = {5, 6};

String rcvStr;
String cmdStr;
int busy;


// set up a new serial port
SoftwareSerial IM920Serial =  SoftwareSerial(rxPin, txPin);

//
// n の k 乗
int intPow(int n, int k){
  int res = 1;
  for (int i = 0; i < k; i++) res *= n;
  return res;
}
//
// 桁そろえ(文字列の前に"0"を追加)
void strDigits(String &str, int digits){
  int len = str.length();
  if (len < digits){
    for (int i = len; i < digits; i++){
      str = "0" + str;
    }
  }  
}
//
int hexToInt(String hex){
  char buf[5];
  hex.getBytes(buf, 5); // char[] に
  return (int)strtol(buf, NULL, 16); // HexToInt
}
//
void setup() {
  Serial.begin(9600);
  while (!Serial) {
  ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println("IM920 Serial!");
  // ピンモード設定
  pinMode(busyPin, INPUT);
  for (int i = 0; i < sizeof(swPins); i++) pinMode(swPins[i], INPUT_PULLUP); // プルアップ
  for (int i = 0; i < sizeof(ryPins); i++) pinMode(ryPins[i], OUTPUT);
  // アナログ入力基準電圧(内部 1.1V) 
  analogReference(INTERNAL);
  for (int i = 0; i < 4; i++) analogRead(i);
  // PWM 初期化
  for (int i = 0; i < sizeof(pwmPins); i++) analogWrite(pwmPins[i], 0); 
  // シリアル初期化
  IM920Serial.begin(19200);
}
//
// 
void loop() {
  // 受信
  if (IM920Serial.available()) {
    rcvStr = IM920Serial.readStringUntil(0x0A);
    rcvStr.trim();
    Serial.println(rcvStr); // 受信文字列をそのまま表示
    rcvStr.replace(",", ""); // "," を削除
    int idx = rcvStr.indexOf(":");
    if (idx >= 0){
      rcvStr = rcvStr.substring(idx + 1); // ":"以降の文字列
      int n = int(rcvStr.length() / 2); //繰り返し回数
      String s, sval;
      long m;
      cmdStr = "";
      for (int i = 0; i < n; i++){
        // 2文字ずつ取り出し数値に変換
        m = hexToInt(rcvStr.substring(i * 2, i * 2 + 2));
        cmdStr += char(m); // ascii コードを文字に変換
      }
      // 送信側での 16 進変換前のコマンド文字列
      Serial.println(cmdStr); // コマンドを表示
      if (cmdStr == "SWRD"){  // デジタル 4 点
        // busy 解除待ち
        do {
          busy = digitalRead(busyPin); 
        } while (busy != 0);
        // SW1 ~ 4 の状態を集計
        int sw = 0;
        for (int i = 0; i < sizeof(swPins); i++){
          if (digitalRead(swPins[i]) == LOW) sw = sw + intPow(2, i);
        }
        sval = String(sw, HEX);
        strDigits(sval, 2); // 桁合わせ
        s = "TXDA53575244" + sval + "\r\n"; // "SWRD"
        IM920Serial.print(s);
        delay(30);
      }
      else if (cmdStr == "ADRD"){ // アナログ 4 点
        // busy 解除待ち
        do {
          busy = digitalRead(busyPin); 
        } while (busy != 0);
        s = "TXDA41445244"; // "ADRD"
        // A0 ~ A1 の状態を集計
        for (int i = 0; i < 4; i++){
          sval = String(analogRead(i), HEX);
          strDigits(sval, 4);
          s += sval;
        }
        s += "\r\n";
        IM920Serial.print(s);
        delay(30);          
      }
      else if (cmdStr == "SARD"){ // デジタル 4 点 + アナログ 4 点
        // busy 解除待ち
        do {
          busy = digitalRead(busyPin); 
        } while (busy != 0);
        s = "TXDA53415244"; // "SARD"
        // SW1 ~ 4 の状態を集計
        int sw = 0;
        for (int i = 0; i < sizeof(swPins); i++){
          if (digitalRead(swPins[i]) == LOW) sw += intPow(2, i);
        }
        sval = String(sw, HEX);
        strDigits(sval, 2);
        s += sval;
        //sval = "";
        // A0 ~ A3 の状態を集計
        for (int i = 0; i < 4; i++){
          sval = String(analogRead(i), HEX);
          strDigits(sval, 4);
          s += sval;
        }
        s += "\r\n";
        IM920Serial.print(s);
        delay(30);          
      }
      else { // RY ON/OFF
        s = cmdStr.substring(0,2);
        if (s == "RY"){ // ex : "RY1ON", "RY2PLS", "RY2OFF"
          s = cmdStr.charAt(2);
          int ryNo = s.toInt();
          if (ryNo >= 1 && ryNo <= 2){
            if (cmdStr.indexOf("PLS") > 0){
              digitalWrite(ryPins[ryNo - 1], HIGH);
              delay(500);
              digitalWrite(ryPins[ryNo - 1], LOW);
            }
            else {
              int val = LOW;
              if (cmdStr.indexOf("ON") > 0) val = HIGH;
              digitalWrite(ryPins[ryNo - 1], val);
            }
            // busy 解除待ち
            do {
              busy = digitalRead(busyPin); 
            } while (busy != 0);
            s = "TXDA5259514B"; // "RYOK";
            if (ryNo == 1) s += "01";
            else s += "02";
            s += "\r\n";
            IM920Serial.print(s);
            delay(30);
          }
        }
        else if (s == "PW"){ // ex "PWM1FF"
          s = cmdStr.charAt(3);
          int pwmNo = s.toInt(); // 1 or 2
          if (pwmNo == 1 || pwmNo == 2){
            m = hexToInt(cmdStr.substring(4));
            if (m > 255) m = 255;
            analogWrite(pwmPins[pwmNo - 1], m);
            // busy 解除待ち
            do {
              busy = digitalRead(busyPin); 
            } while (busy != 0);
            s = "TXDA50574D4F4B"; // "PWMOK";
            if (pwmNo == 1) s += "01";
            else s += "02";
            s += "\r\n";
            IM920Serial.print(s);
            delay(30);             
          }
        }
      }
    }       
  }
  delay(1);
}