IM920 ワイヤレス I/O (その3) 2019/08/13

(その2)では、USB 接続で Windows PC/ Android 端末から操作していましたが、
仕事用のスマホが OTG (USBホスト)機能に対応していないので、
RN42 Bluetooth モジュールを使って Android スマホで使えるようにしてみました。

920MHz 無線モジュール IM920 シリーズ のうち、外部アンテナタイプの IM920XS を使っています。

全体の構成は下図↓のようになります。接続図(PDF)はこちらです。

 ■概要
  500 ミリ秒周期で、DI 4 点の状態、AI 4 点の値を読み込んでいます。
  DI の状態が前回と異なる場合は、"SW 1 ON" のように音声合成が発生されます。
  AI (アナログ入力)は 0V~1.1V に対応し、そのままの値が表示されます。

  DO は 2 点。ON/OFF と 500 ミリ秒の 1 パルス動作が行えます。
  操作が受信側に届いたときは、アンサーバックとしてピピッ音が、失敗の時は、ブー音が鳴ります。
  AO(アナログ出力)は、PWM出力です。0~5.0 の入力値がほぼ出力電圧値になります。

 ■ Android スマホ画面

 

 ■送信側(RN42 + IM920)
  Android から通信を受けて、それを受信側の IM920 に飛ばします。
  ユニバーサル基板は、IM315-UNB。RN42は、秋月電子の Bluetooth 無線モジュール評価キット を使っています。

 

 ■受信側(IM920 + Arduino UNO)
  送信側の信号を受けて、Arduino に渡します。
  IM920 シールド IM315-SHLD-RX-V2 は使わず、ユニバーサル基板 IM315-UNB を使っています。
  出力リレーは、SainSMART の 2 チャンネルリレーモジュールを HIGH で ON になるようにプチ改造しています。(接続図 PDF を参照)
  ※共に Amazon にて購入可能です。
  
  

 ■サンプルコード
  Android スマホ側 : Delphi + IM920 子機側 : Arduino
  ※Arduino スケッチは、かなり下の方になります。
  


// Delphi 10.3 Community Edition
unit Unit4;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  System.Bluetooth, System.Bluetooth.Components, FMX.ScrollBox, FMX.Memo,
  FMX.Controls.Presentation, FMX.StdCtrls, FMX.Layouts, FMX.Edit, System.Rtti,
  FMX.Grid.Style, FMX.Grid,{ Math,} FMX.Objects, System.UIConsts, FMX.ListBox,
  System.IOUtils, System.IniFiles,
  // for TTS
  Androidapi.JNI.TTS,AndroidAPI.JNIBridge,
  AndroidApi.JNI.Media;

type
  TBtThread = class(TThread)
  private
    { Private 宣言 }
    procedure BtOpen;
  protected
    procedure Execute; override;
  public
    constructor Create; virtual;
  end;
type
  TForm4 = class(TForm)
    Timer1: TTimer;
    ScaledLayout1: TScaledLayout;
    Pwm2: TButton;
    Rectangle4: TRectangle;
    Label7: TLabel;
    ComboBox3: TComboBox;
    Button2: TButton;
    Rectangle6: TRectangle;
    Label8: TLabel;
    updn2: TRectangle;
    Label9: TLabel;
    Rectangle8: TRectangle;
    Label6: TLabel;
    updn3: TRectangle;
    Label10: TLabel;
    updn4: TRectangle;
    Label12: TLabel;
    Pwm1: TButton;
    Rectangle12: TRectangle;
    Label19: TLabel;
    Label18: TLabel;
    updn5: TRectangle;
    Label13: TLabel;
    updn6: TRectangle;
    Label20: TLabel;
    Label21: TLabel;
    Rectangle16: TRectangle;
    Rectangle17: TRectangle;
    Rectangle18: TRectangle;
    Rectangle19: TRectangle;
    Rectangle20: TRectangle;
    Rectangle21: TRectangle;
    Rectangle22: TRectangle;
    Label22: TLabel;
    Label23: TLabel;
    Label24: TLabel;
    Label25: TLabel;
    Label26: TLabel;
    Label27: TLabel;
    Label28: TLabel;
    Label29: TLabel;
    Button3: TButton;
    Button4: TButton;
    Button5: TButton;
    Button6: TButton;
    Button7: TButton;
    Button8: TButton;
    Label3: TLabel;
    updn7: TRectangle;
    Label4: TLabel;
    updn8: TRectangle;
    Label5: TLabel;
    updn1: TRectangle;
    Rectangle1: TRectangle;
    Rectangle2: TRectangle;
    Rectangle3: TRectangle;
    Rectangle5: TRectangle;
    Rectangle7: TRectangle;
    Rectangle10: TRectangle;
    Rectangle11: TRectangle;
    Rectangle13: TRectangle;
    Label1: TLabel;
    procedure Timer1Timer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Pwm1Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure updn1Click(Sender: TObject);
    // TTS
    type
      TttsOnInitListener = class(TJavaLocal, JTextToSpeech_OnInitListener)
      private
        [weak] FParent : TForm4;
      public
        constructor Create(AParent : TForm4);
        procedure onInit(status: Integer); cdecl;
      end;
  private
    { private 宣言 }
    ttsListener : TttsOnInitListener;
    tts : JTextToSpeech;
    procedure SpeakOut(const s :string);
    procedure InitTTS;
  public
    { public 宣言 }
    constructor Create(AOwner : TComponent); override;
    destructor Destroy; override;
  end;
type
  TSwAry = array[0..3] of boolean;
var
  Form4: TForm4;

  // スイッチ入力の比較
  swAryNew : TSwAry;
  swAryOld : TSwAry;

  ADevice : TBluetoothDevice;
  ASocket : TBluetoothSocket;

  GThdMode : integer;
  GCmdMode : integer;

  ThBt : TBtThread;
  OpenNGcnt : integer;
  OpenMsecCnt : integer;
  Counter : integer;
  TTScnt : integer;

  BtDeviceHead : string;
  // uses ... Androidapi.JNIBridge, AndroidApi.JNI.Media;
  ToneGenerator: JToneGenerator;
const
  // SPP(Serial Port Profile) による通信のUUID
  ServiceUUID = '{00001101-0000-1000-8000-00805F9B34FB}';

  thdTHSTART   = 1000;
  thdTHTERM    = 2000;
  cmdSCCREATE  = 200;
  cmdSCCONNECT = 201;
  cmdSCNG = 202;

  CO_ON = claLime;
  CO_OFF = claGray;
  CO_NG = claRed;

implementation

uses Androidapi.JNI.JavaTypes, FMX.Helpers.Android
{$IF CompilerVersion >= 27.0}
, Androidapi.Helpers
{$ENDIF}
;

{$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;

// -----------------------------------------------------------------------------
// Bluetooth を Open し、接続する
procedure TBtThread.BtOpen;
var
  ABluetoothManager : TBluetoothManager;
  APairedDevices : TBluetoothDeviceList;
  ADevice : TBluetoothDevice;
  idx, i : integer;
begin
  GThdMODE := thdTHSTART;
  try
    try
      ABluetoothManager := TBluetoothManager.Current;
      if ABluetoothManager.ConnectionState = TBluetoothConnectionState.Connected then begin
        // 過去にペアリングされたデバイスの一覧から、ターゲット を探す
        APairedDevices := ABluetoothManager.GetPairedDevices;
        if APairedDevices.Count > 0 then begin
          idx := -1;
          for i := 0 to APairedDevices.Count -1 do begin
            Synchronize(procedure() begin
                with Form4.ComboBox3 do begin
                  BeginUpdate;
                  Items.Add(APairedDevices[i].DeviceName );
                  EndUpdate;
                end;
            end);
            if (BTDeviceHead = APairedDevices[i].DeviceName) then begin
              Synchronize(procedure() begin
                  with Form4.ComboBox3 do begin
                    ItemIndex := i;
                  end;
              end);
              idx := i;
              //break;  // リストアップを続ける
            end;
          end;
          if idx >= 0 then begin
            ADevice := APairedDevices[idx];
            if ADevice <> nil then begin
              ASocket := ADevice.CreateClientSocket(StringToGUID(ServiceUUID), False);
              if ASocket <> nil then begin
                GCMDMODE := cmdSCCREATE;
                // 接続
                ASocket.Connect;
                if ASocket.Connected then GCMDMODE := cmdSCCONNECT;
              end;
            end;
          end;
        end;
      end;
    except
      on E : Exception do begin
        GCMDMODE := cmdSCNG;
      end;
    end;
  finally
    // 明示的にスレッドを終了(破棄される)
    // スレッド実行中にアプリを終了した時エラーになるため
    Terminate;
    WaitFor;
    FreeAndNil(ThBt);
    GThdMODE := thdTHTERM;
  end;
end;

constructor TBtThread.Create;
begin
  // スレッドを生成、直ちに実行
  inherited Create(False);
  // スレッド終了時、スレッドオブジェクトを破棄
  FreeOnTerminate := True;
end;

procedure TBtThread.Execute;
begin
  BtOpen;
end;

// -----------------------------------------------------------------------------
procedure TForm4.InitTTS;
begin
  tts := TJTextToSpeech.JavaClass.init(TAndroidHelper.Context, ttsListener);
end;
procedure TForm4.SpeakOut(const s : string);
var
  text : JString;
begin
  text := StringToJString(s);
  tts.speak(text, TJTextToSpeech.JavaClass.QUEUE_FLUSH, nil);
end;

{ TForm4.TttsOnInitListener }
constructor TForm4.TttsOnInitListener.Create(AParent: TForm4);
begin
  inherited Create;
  FParent := AParent
end;

procedure TForm4.TttsOnInitListener.onInit(status: Integer);
var
  Result : Integer;
begin
  if (status = TJTextToSpeech.JavaClass.SUCCESS) then
  begin
   //result := FParent.tts.setLanguage(TJLocale.JavaClass.US);
   result := FParent.tts.setLanguage(TJLocale.JavaClass.JAPAN);
   if (result = TJTextToSpeech.JavaClass.LANG_MISSING_DATA) or
      (result = TJTextToSpeech.JavaClass.LANG_NOT_SUPPORTED) then
     ShowMessage('This Language is not supported');
  end
  else
    ShowMessage('Initilization Failed!');
end;
constructor TForm4.Create(AOwner: TComponent);
begin
  inherited;
  ttsListener := TttsOnInitListener.Create(self);
end;

destructor TForm4.Destroy;
begin
  if Assigned(tts) then begin
    tts.stop;
    tts.shutdown;
    tts := nil;
  end;
end;
// -----------------------------------------------------------------------------

function ASocketReceiveData(ASocket: TBluetoothSocket; ATimeout: Cardinal): string;
// IM920 からの応答
var
  AData : TBytes;
  ReadData : TBytes;
  i : integer;
  res : string;
  Ticks : Cardinal;
  idx : integer;
  loop : boolean;
  cnt : integer;
begin
  res := '';
  cnt := 0;
  SetLength(ReadData, 1024);
  idx := 0;
  Ticks := TThread.GetTickCount;
  loop := True;
  while loop and (cnt < 500) do begin
    Sleep(1);
    AData := ASocket.ReceiveData;
    if Length(AData) > 0 then begin
      for i := 0 to Length(AData) - 1 do begin
        ReadData[idx] := AData[i];
        Inc(idx);
        // 終端 LF まで読む
        if (AData[i] = $0A) {or (AData[i] = $03)} or (idx >= 1024) then begin
          loop := False;
          break;
        end;
      end;
    end;
    Inc(cnt);
    if loop then
      loop := TThread.GetTickCount - Ticks < ATimeout;
  end;
  SetLength(ReadData, idx);
  res := TEncoding.ANSI.GetString(ReadData);
  result := Trim(res); // 制御コードを含まない
end;

procedure TForm4.Pwm1Click(Sender: TObject);
// Pwm1, 2 出力
var
  AData : TBytes;
  res, cmd, s : string;
  ATimeout: Cardinal;
  d : double;
  lbl : TLabel;
  btn : TButton;
  i, m, n : integer;
begin
  Label3.Text := '';
  if (ASocket <> nil) and ASocket.Connected then begin
    ATimeout := 250;
    Timer1.Enabled := False;
    Sleep(500);
    btn := Sender as TButton;
    if btn = Pwm1 then
      lbl := Label8
    else
      lbl := Label19;
    s := btn.Text;
    s := StringReplace(s, ' ', '', [rfReplaceAll]);
    d := StrToFloatDef(lbl.Text, 0);
    m := Trunc(d / 5.0 * 255);
    if m < 0 then m := 0;
    if m > 255 then m := 255;
    s := s + IntToHex(m, 2);
    cmd := 'TXDA';
    for i := 1 to Length(s) do
      cmd := cmd + IntToHex(Ord(s[i-1]), 2);  // Windows と異なる
    AData := TEncoding.ANSI.GetBytes(cmd + #13#10);
    // 送信
    ASocket.SendData(AData);
    // 受信
    for i := 0 to 2 do begin
      res := ASocketReceiveData(ASocket, ATimeout);
      if Length(res) > 2 then break;
    end;
    res := StringReplace(res, ',', '', [rfReplaceAll]);
    Label3.Text := res;
    n := Pos(':', res);
    if n > 0 then begin
      s := Copy(res, n + 1);
      if Pos('50574D4F4B', s) > 0 then begin // 'PWMOK'
        n := StrToIntDef(Copy(s, 11, 2), 0);
        if (n = 1) or (n = 2) then
          ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
        else
          ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
      end;
    end;
    Timer1.Enabled := True;
  end;
end;

procedure TForm4.Button2Click(Sender: TObject);
// 接続先保存
var
  IniFile: TMemIniFile;
begin
  IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine(
    System.IOUtils.TPath.GetDocumentsPath, 'IM920And.ini'), TEncoding.UTF8);
  with IniFile do begin
    try
      with ComboBox3 do begin
        if ItemIndex >= 0 then begin
          WriteString('Target', 'DeviceName', Items[ItemIndex]);
          ShowMessage('接続先: ' + Items[ItemIndex] + 'を保存しました.' + #13#10 +
            '次回起動時から有効になります.' + #13#10 + 'このアプリを再起動して下さい.');
        end
        else
          ShowMessage('接続先が選択されていません.');
      end;
      IniFile.UpdateFile;
    finally
      Free;
    end;
  end;
end;

procedure TForm4.Button3Click(Sender: TObject);
// RY 2 個 ON/ OFF/ PLS
var
  btn : TButton;
  cmd, s, rcv : string;
  i, n : integer;
  AData : TBytes;
  ATimeout : Cardinal;
  v : integer;
  lbl : TLabel;
begin
  Label3.Text := '';
  Atimeout := 250;
  if (ASocket <> nil) and ASocket.Connected then begin
    Timer1.Enabled := False;
    Sleep(500);
    btn := Sender as TButton;
    s := btn.Text;
    s := StringReplace(s, ' ', '', [rfReplaceAll]);
    cmd := 'TXDA';
    for i := 1 to Length(s) do
      cmd := cmd + IntToHex(Ord(s[i - 1]), 2); // Windows と異なる
    AData := TEncoding.ANSI.GetBytes(cmd + #13#10);
    // 送信
    ASocket.SendData(AData);
    // 受信
    for i := 0 to 2 do begin
      rcv := ASocketReceiveData(ASocket, ATimeout);
      if Length(rcv) > 2 then break;
    end;
    rcv := StringReplace(rcv, ',', '', [rfReplaceAll]);
    Label3.Text := rcv;
    n := Pos(':', rcv);
    if n > 0 then begin
      cmd := Copy(rcv, n + 1, 8);
      if cmd = '5259514B' then begin  // 'RYOK'
        s := Copy(rcv, n + 8 + 1);
        // スイッチの状態
        v := StrToIntDef(Copy(s, 3), -1);
        for i := 0 to 3 do begin
          lbl := FindComponent('Label' + IntToStr(i + 22)) as TLabel;
          if lbl <> nil then begin
            if v < 0 then begin
              lbl.FontColor := CO_NG;
            end
            else begin
              if v and IntPower(2, i) > 0 then begin
                if lbl.FontColor <> CO_ON then lbl.FontColor := CO_ON;
              end
              else begin
                if lbl.FontColor <> CO_OFF then lbl.FontColor := CO_OFF;
              end;
            end;
          end;
        end;
        // リレー操作のアンサーバック
        v := StrToIntDef(Copy(s, 1, 2), 0);
        if (v = 1) or (v = 2) then
          ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_ACK)
        else
          ToneGenerator.startTone(TJToneGenerator.JavaClass.TONE_PROP_NACK);
      end;
    end;
    Timer1.Enabled := True;
  end;
end;

procedure TForm4.FormCreate(Sender: TObject);
var
  IniFile: TMemIniFile;   // uses .... System.IniFiles;
begin
  Label6.Text := '';
  Label7.Text := '';
  Label3.Text := '';
  Label1.Text := '';

  // 縦画面に固定
  Application.FormFactor.Orientations :=
    [TFormOrientation.Portrait, TFormOrientation.InvertedPortrait];

  // use ..... System.IOUtils;
  IniFile := TMemIniFile.Create(System.IOUtils.TPath.Combine(
    System.IOUtils.TPath.GetDocumentsPath, 'IM920And.ini'), TEncoding.UTF8);
  with IniFile do begin
    try
      BtDeviceHead := ReadString('Target', 'DeviceName', 'RN42-C7CF');
    finally
      Free;
    end;
  end;

  // TTS
  InitTTS;

  // ブザー
  ToneGenerator := TJToneGenerator.JavaClass.init(
    TJAudioManager.JavaClass.STREAM_ALARM,
    TJToneGenerator.JavaClass.MAX_VOLUME);

  // Bruetooth スレッド
  Timer1.Interval := 10;
  Timer1.Enabled := True;
  ThBt := TBtThread.Create;
end;

procedure TForm4.FormDestroy(Sender: TObject);
// フォーム破棄
begin
  if ASocket <> nil then begin
    ASocket.Close;
    ASocket.Free;
    ASocket := nil;
  end;
end;

procedure TForm4.Timer1Timer(Sender: TObject);
// インターバルタイマー
var
  ATimeout : Cardinal;
  AData : TBytes;
  i, j, n, m : integer;
  s, cmd, rcv : string;
  Ticks : Cardinal;
  v : integer;
  sv : string;
  lbl : TLabel;
begin
  if not ((GCMDMODE = cmdSCCONNECT) and ASocket.Connected) then begin
    Inc(OpenMsecCnt);
    Label7.Text := IntToStr(OpenMsecCnt * 10) + ' msec';
    // エラー
    if GCMDMODE = cmdSCNG then begin
      Timer1.Enabled := False;
      ShowMessage(BTDeviceHead + ' 接続エラーです.');
    end;
    if OpenMsecCnt >= 200 then begin
      Timer1.Enabled := False;
      ShowMessage(BTDeviceHead + ' 接続できませんでした.');
    end;
  end;

  if (GCMDMODE = cmdSCCONNECT) and ASocket.Connected then begin
    Timer1.Interval := 500;
    Timer1.Enabled := False;
    Ticks := TThread.GetTickCount;
    ATimeout := 250;

    // DI x 4 点 + AI x 4 点の値を取得
    s := 'SARD';
    cmd := 'TXDA';
    for i := 1 to Length(s) do
      cmd := cmd + IntToHex(Ord(s[i - 1]), 2); // Windows と異なる
    AData := TEncoding.ANSI.GetBytes(cmd + #13#10);
    // 送信
    ASocket.SendData(AData);
    // 受信
    for i := 0 to 2 do begin
      rcv := ASocketReceiveData(ASocket, ATimeout);
      if Length(rcv) > 2 then break;
    end;
    rcv := StringReplace(rcv, ',', '', [rfReplaceAll]);
    Label1.Text := rcv;
    n := Pos(':', rcv);
    if n > 0 then begin
      cmd := Copy(rcv, n + 1, 8);
      s := Copy(rcv, n + 8 + 1);
      if cmd = '53415244' then begin  // 'SARD'
        v := StrToIntDef('$' + Copy(s, 1, 2), -1);
        // スイッチ入力(4 点) Label22 ~ 25 の色を変える
        for j := 0 to 3 do begin
          lbl := FindComponent('Label' + IntToStr(j + 22)) as TLabel;
          if lbl <> nil then begin
            if v < 0 then begin
              lbl.FontColor := CO_NG;
            end
            else begin
              if v and IntPower(2, j) > 0 then begin
                swAryNew[j] := True;
                if lbl.FontColor <> CO_ON then lbl.FontColor := CO_ON;
              end
              else begin
                swAryNew[j] := False;
                if lbl.FontColor <> CO_OFF then lbl.FontColor := CO_OFF;
              end;
            end;
          end;
        end;
        m := (Length(s) - 2) div 4;
        v := 0;
        // アナログ入力(4 点)Label26~29 の Text を変える
        for j := 0 to m - 1 do begin
          sv := Copy(s, 3 + j * 4, 4);
          if sv <> '' then v := StrToIntDef('$' + sv, 0);
          lbl := FindComponent('Label' + IntToStr(j + 26)) as TLabel;
          if lbl <> nil then
            lbl.Text := Format('%.2f', [(v / 1023) * 1.1]);
        end;
      end;
    end;
    Label7.Text := IntToStr(TThread.GetTickCount - Ticks) + ' msec';
    // テキストスピーチ
    for i := 0 to 3 do begin
      s := '';
      if not swAryOld[i] and swAryNew[i] then
        s := 'SW ' + IntToStr(i + 1) + ' オン' + #13
      else if swAryOld[i] and not swAryNew[i] then
        s := 'SW ' + IntToStr(i + 1) + ' オフ' + #13;
      if s <> '' then SpeakOut(s);
    end;
    swAryOld := swAryNew;
    Timer1.Enabled := True;
  end;
end;

procedure TForm4.updn1Click(Sender: TObject);
// PWM UP/DOWN
var
  rect : TRectangle;
  n : integer;
  s : string;
  lbl : TLabel;
  d : double;
begin
  rect := Sender as TRectangle;
  s := rect.Name;
  n := StrToIntDef(Copy(s, Length(s)), 0);
  if n > 0 then begin
    if n <= 4 then lbl := Label8
    else lbl := Label19;
    d := StrToFloatDef(lbl.Text, 0);
    case n of
      1, 5 : d := d + 0.1;
      2, 6 : d := d - 0.1;
      3, 7 : d := d + 1;
      4, 8 : d := d - 1;
   end;
   if d < 0 then d := 0;
   if d > 5 then d := 5;
   lbl.Text := Format('%.1f', [d]);
  end;
end;
end.

//IM920 で Serial I/O
// 2019/07/24 by f.izawa
// 2019/07/29 RY ON/OFF のアンサーに、SW 入力の状態監視を追加
// 2019/08/04 RY_PLS のアンサーを即座に返すに変更
// 2019/08/05 RY のピン番号を 11, 12 に変更

#include <SoftwareSerial.h>

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

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

String rcvStr;
String cmdStr;
byte 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);
  // 出力をLOWに
  for (int i = 0; i < sizeof(ryPins); i++) digitalWrite(ryPins[i], LOW);
  
  // アナログ入力基準電圧(内部 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);
              // 2019/08/04 コメントアウト
              //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";
             // 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 + "\r\n");
            IM920Serial.print(s);
            delay(30);
            // 2019/08/04 追加
            if (cmdStr.indexOf("PLS") > 0){
              delay(470);
              digitalWrite(ryPins[ryNo - 1], LOW);
            }
          }
        }
        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);
}