Raspberry Pi Pico Wを使用してステッピングモータをAndroidスマホ(kotlin使用)からWi-Fiソケット通信で制御する実験(Arduino IDE使用)


Raspberry Pi Pico WのWi-Fi機能を使用して、Androidスマホと接続してステッピングモータをソケット通信で制御する実験を行いましたので紹介します。

Raspberry Pi Pico W側のプログラミングツールとしてはArduino IDEを使用しています。
プログラムはArduino IDEでスケッチ例として見ることが出来るWiFiServerを参考にしています。Raspberry Pi Pico W側をサーバーとし、スマホ側をクライアントとしてIPアドレスとポート番号を指定してソケット通信で接続し、スマホからRaspberry Pi Pico Wにステッピングモータ制御コマンドを送信してモータを制御しています。

ステッピングモータの駆動はArduino言語のStepperライブラリを使用しています。

Androidスマホのアプリの開発言語はKotlinを使用しています。

1.動作動画

以下動画は、今回作成した実験装置を動作させる様子です。
Raspberry Pi Pico Wにステッピングモータを接続して、スマホとWi-Fi接続してステッピングモータをソケット通信で操作しています。

2.実験装置

ステッピングモータは28BYJ-48とそのドライバーボードのセットを使用しました。このステッピングモータとドライバーボードは複数のネット通販から購入することが出来ます。
コントローラとしてはRaspberry Pi Pico Wを使用して実験装置を組みました。(ステッピングモータ駆動の詳細に関してはこちらを参照して下さい)

28BYJ-48ステッピングモータとドライバ
28BYJ-48ステッピングモータ裏面
実験装置

今回使用したステッピングモータの主な仕様は表1の通りです。このモータは1/64のギアで減速されていて、1-2相励磁の場合にステップ角は5.625°/64なので4096ステップでモータ軸が1回転します。最大自起動周波数が500ppsなので、500パルス/sec以下で起動する必要があります。起動後は徐々に加速を行った場合に、最大で1000パルス/secまで仕様上は可能と思われます。
ただし、Arduino IDEのStepperライブラリーのstep()関数を使用した場合の励磁方式は2相励磁のようです。そのため2048ステップでモータ1回転となります。

項目仕様
相数4相
励磁方式1-2相励磁方式
ステップ角5.625°/64   (減速比1/64)
電圧5VDC
相抵抗22Ω ± 7% 25°C
最大応答周波数1000pps
最大自起動周波数500pps
引き込みトルク800gf.cm / 5VDC 400pps
表1.28BYJ-48ステッピングモータの主な仕様

3.実験装置回路図

図1に実験装置の回路図を示します。1個のステッピングモータをRaspberry Pi Pico Wに接続しています。

28BYJ-48ステッピングモータのドライバーボードはテキサスインスツルメンツのULN2003ANと言うダーリントントランジスタアレイが使用されています。ドライバーボード自体の資料が無かったので、テスタと基板のパターンを見て回路を調べました。

ステッピングモータのドライバーボードの入力信号IN1~IN4をRaspberry Pi Pico WのGP0~GP3に接続しました。

Wi-Fiルータを介してスマホと接続します。パソコンとUSBケーブルで接続して、このパソコンでArduino IDEを動作させています。

図1.実験装置の回路図

4.Raspberry Pi Pico W側プログラム(スケッチ)に関して

スマホからRaspberry Pi Pico WへWi-Fiを介したソケット通信で表2に示した様なコマンドテキストを送ることによってステッピングモータが動作するプログラム(スケッチ)を作成しました。Raspberry Pi Pico W側はサーバーとして動作します。
プログラミングツールとしてはArduino IDEを使用しています。

コマンドテキスト (スマホ→Poco W)ステッピングモータ動作
“CW\n”時計方向に回転する
“CCW\n”反時計方向に回転する
“STOP\n”回転を停止する
表2.ステッピングモータ制御コマンド
Pico W→スマホ内容
“X:「ステッピングモータ現在回転位置」\r\n”ステッピングモータの現在回転位置データ
表3.Raspberry Pi Pico Wから送信されるデータ

4-1.Wi-Fi関係の関数の説明

最初に今回作成したプログラム(スケッチ)で使用したWi-Fi関係の関数の概要を表4に示します。
作成したプログラムの説明用として記載したので、各関数のクラス部分はプログラム中で使用しているインスタンス名のまま記載しています。
関数の引数のバリエーションもプログラムで使用したものに限り記載しています。全てのバリエーションを記載した厳密なものではありませんので、ご注意お願い致します。

関数説明
#include <WiFi.h>Wi-Fi関係の関数のヘッダファイルのinclude
WiFiServer server(int port)指定されたポートで着信接続をリッスンするサーバーを作成する
引数: int
port: ポート番号を指定
WiFi.mode(WiFiMode_t m)Wi-Fiのモードを設定する
引数: WiFiMode_t
WIFI_OFF: (0) WiFi off
WIFI_STA: (1) WiFi stationモード
WIFI_AP: (2) WiFi APモード
WIFI_AP_STA: (3) WiFi stationモードかつAPIモード
戻り値: void
WiFi.setHostname(const char *name)DHCP 要求に使用するホスト名を設定する
引数:
const char *name ホスト名の文字列のポインタ
戻り値: void
WiFi.begin(const char *ssid, const char *passphrase)Wi-Fi接続を開始する
引数:
const char *ssid: SSIDの文字列のポインタ
const char *passphrase: パスワードの文字列のポインタ
戻り値: int
WL_CONNECTED: (3) ネットワークに接続されている
WL_IDLE_STATUS: (0) 電源入っているがネットワークに接続されていない
WiFi.status()現在のWi-Fiの接続状態をリターン
戻り値: uint8_t (wl_status_tで定義)
WL_NO_SHIELD: (255)
WL_STOPPED: (254)
WL_IDLE_STATUS: (0)
WL_NO_SSID_AVAIL: (1)
WL_SCAN_COMPLETED: (2)
WL_CONNECTED: (3)
WL_CONNECT_FAILED: (4)
WL_CONNECTION_LOST: (5)
WL_DISCONNECTED: (6)
WiFi.localIP()Wi-Fiルータから現在割り当てられているIPアドレスを読み出す
戻り値: IPAddress
server.begin();Wi-Fiサーバを開始する
戻り値: void
server.accept()Wi-Fiクライアントからの接続を取得する
戻り値: WiFiClient
(以後WiFiClient client = server.accept()で得たclientに対してデータの送受信等を行う)
client.println(const char[])引数で指定した文字列の末尾に’\r’と’\n’を付けてクライアントに送信する
引数:const char[] 文字列の配列
戻り値: 送信されたバイト数 (size_t)
(clientはWiFiClient client =server.accept()で得られたもの)
client.connected()クライアントと接続されているか確認する
戻り値: 接続中 true、切断中 false
(clientはWiFiClient client =server.accept()で得られたもの)
client.abailable()クライアントから受信したデータのバイト数をリターンする
戻り値: int
(clientはWiFiClient client =server.accept()で得られたもの)
client.readStringUntil(char terminator)クライアントから受信したデータを先頭からターミネータの文字まで読み込む
引数: char
ターミネータ文字
戻り値: String
読み込んだ文字列
(clientはWiFiClient client =server.accept()で得られたもの)
表4.WiFi関係関数説明

4-2.プログラム(スケッチ)の説明

今回作成したプログラム(スケッチ)に関して2つの部分に分割して説明します。

①初期処理 共通変数とsetup()関数

初期処理を行うsetup()関数等のソースコードを以下に示します。

// Raspberry Pi Pico W Wi_Fi通信ステッピングモータ動作テスト-----
#include <WiFi.h>
#include <Stepper.h>

//---ステッピングモータ関係変数-----
#define RPM_SPEED 10      //モータ回転速度(rpm)
#define STEPS 2048        //モータの1回転のステップ数

//軸の現在位置記憶用データ(符号付きステップ位置)
long current_pos = 0;

//経過時間判断用データ
unsigned long last_time = 0;  //前回の時間記憶
unsigned long delay_time = 0; //処理待ち時間(次の処理をするまでの時間)

//回転指令フラグ
bool cw_mov_flag = false;   //CW回転指令フラグ
bool ccw_mov_flag = false;  //CCW回転指令フラグ

//ステッピングモータ初期設定
Stepper stepper(STEPS, 0, 2, 1, 3);//ステッピングモータ設定(1回転2048ステップ)

//------Wi_Fi関係---------------
const char* ssid = "your-ssid";   //SSID
const char* password = "your-password"; //パスワード
int port = 4242;                        //ポート番号
WiFiClient client;                      //WiFiClient
WiFiServer server(port);                //WiFiServer
bool connectFlag = false;               //クライアント接続フラグ

void setup() {
  Serial.begin(115200); //シリアルモニタ用シリアル通信開始
  
  //----ステッピングモータ関係セットアップ-----
  pinMode(0, OUTPUT);       //ステッピングモータ(青線)
  pinMode(1, OUTPUT);       //ステッピングモータ(桃線)
  pinMode(2, OUTPUT);       //ステッピングモータ(黄線)
  pinMode(3, OUTPUT);       //ステッピングモータ(橙線)

  //ステッピングモータ回転速度設定
  stepper.setSpeed(500);    //ステッピングモータの回転速度500rpm

  //処理待ち時間の設定
  delay_time = 60 * 1000 * 1000 / STEPS / RPM_SPEED;

  //-----WiFi関係セットアップ----------------------------
  WiFi.mode(WIFI_STA);        //WiFi Stationモード
  WiFi.setHostname("PicoW2");
  WiFi.begin(ssid, password); //WiFi接続実施
  while (WiFi.status() != WL_CONNECTED) { //WiFi接続待ち
    Serial.print(".");
    delay(100);
  }
  server.begin();     //WiFiサーバ開始

  //WiFiルータから割り当てられたIPアドレスとポート番号をシリアルモニタで表示
  Serial.printf("\nConnected to WiFi\n\nConnect to server at %s:%d\n", WiFi.localIP().toString().c_str(), port);
}

【説明】
2行目:
include <WiFi.h>でWi-Fi機能のヘッダファイルをインクルードしています。この処理によってWi-Fi関係関数の使用が出来ます。

3行目:
include <Stepper.h>でステッピングモータ関係ライブラリのヘッダファイルをインクルードしています。この処理によってステッピングモータ駆動関係関数の使用が出来ます。

24行目、25行目:
const char* ssidとconst char* passwordに使用するWi-FiルータのSSIDとパスワードをセットします。

35行目~44行目:
ステッピングモータ関係の設定部分です。

35行目~38行目:
pinMode(0, OUTPUT);~pinMode(3, OUTPUT);の処理で、I/OのGP0~GP3を出力ピンとしてステッピングモータ線に割り当てています。

41行目:
stepper.setSpeed(500)でステッピングモータの回転速度を500rpmに設定しています。但し実際には1ステップずつの駆動しかしないので、setSpeed()関数では回転速度は決まりません。他の処理を行うためにstep()関数からは出来るだけ速く抜け出るようにsetSpeed(rpms)のrpmsの値は大きめに設定しています。

44行目:
delay_time = 60 * 1000 * 1000 / STEPS / RPM_SPEED;
はステッピングモータを1ステップ駆動した後に次に1ステップ駆動するまでの待ち時間をモータの回転速度(rpm)とモータの1回転のステップ数から計算しています。時間の単位はμsecです。

47行目~54行目:
WiFi接続に到るまでの処理です。
49行目のWiFi.begin()関数でWiFiルータのSSIDとパスワードを指定してWiFi接続を行います。50行目のWiFi.status()関数でWi-Fi接続の状態を確認してWi-Fiが接続されるまで待ちます。

②ループ処理 loop()関数

ループで繰り返し処理を行うloop()関数のソースコードを以下に示します。この部分でスマホからコマンドデータを受け取り、ステッピングモータの回転制御を行っています。

void loop() {
  if (connectFlag == false){        //クライアント切断中?
    client = server.accept();       //クライアントからの接続を取得
    if (!client) {
       return;
    }
    connectFlag = true;              //クライアント接続フラグセット
    Serial.println("Client connect");//シリアルモニタに接続メッセージ
  }

  if (client.connected() == false){ //クライアント切断?
    connectFlag = false;            //クライアント接続フラグリセット
    Serial.println("Client disconnect");  //シリアルモニタに切断メッセージ
  }

  if (client.available() > 0){      //受信データ有り?
    String data = client.readStringUntil('\n');     //"\n"までデータ読込
   
    //----受信データ判断処理------------------
    if (data.startsWith("CW") == true){             //"CW"?
      ccw_mov_flag = false;                         //ccw回転フラグリセット
      cw_mov_flag = true;                           //CW回転フラグセット
    }else if (data.startsWith("CCW") == true){      //"CCW"?
      cw_mov_flag = false;                          //cw回転フラグリセット
      ccw_mov_flag = true;                          //CCW回転フラグセット
    }else if (data.startsWith("STOP") == true){     //"STOP"?
      cw_mov_flag = false;                          //cw回転フラグリセット
      ccw_mov_flag = false;                         //ccw回転フラグリセット
    }
  }

  //-----モータ回転処理----------------------
  //前回処理からの経過時間計算
  unsigned long pass_time;
  unsigned long now_time = micros();    //現在時刻読出し(usec単位)
  if (now_time >= last_time){
    pass_time = now_time - last_time;   //経過時間計算
  }else{
    //オーバフローを加味して経過時間計算
    pass_time = now_time - (0xffffffffUL - last_time + 1);
  }
  
  //モータ回転処理
  if (pass_time >= delay_time){     //時間到達?
    last_time = now_time;
    if (cw_mov_flag == true){       //CW回転?
      stepper.step(-1);             //-1ステップ回転
      current_pos--;                //現在位置マイナス
      //現在位置をクライアントに送信
      send_pos();
    }
    else if (ccw_mov_flag == true){ //CCW回転?
      stepper.step(1);              //1ステップ回転
      current_pos++;                //現在位置プラス
      //現在位置をクライアントに送信
      send_pos();
    }
  }
}

//現在位置をクライアントに送信
void send_pos(){
  String text_x = String(current_pos);
  String text = String("X:" + text_x);
  client.println(text);     //現在位置をPCへ送信
}

【説明】
3行目:client = server.accept();
クライアント(スマホ)からの接続を取得します。

7行目:connectFlag = true;
クライアント(スマホ)との接続が行われた時にconnectFlagをtrueにしています。

11行目:if (client.connected() == false){
クライアント(スマホ)との接続が切れたかどうかの判断をしています。接続が切断されている場合はconnectFlag をfalseにしています。

16、17行目:
if (client.available() > 0)で受信データが有るか判断しています。client.available()関数は受信データのバイト数を返します。
受信データがある場合は、String data = client.readStringUntil(‘\n’)で受信データを先頭から改行コード(‘\n’)まで読み込みます。
受信データが有るか判断した後にclient.readStringUntil(‘\n’)で受信データを読み込むことによって、プログラム実行がデータ受信までの間client.readStringUntil(‘\n’)の部分で停止してしまうのを防ぎます。
プログラムの停止を防ぐことによって、その下方の行のステッピングモータの駆動動作を連続に行うことが出来ます。

20~29行目:
受信データの判断処理をしています。受信データによって以下の処理をしています。
受信データ ”CW” の時 cw_mov_flagをtureにする。
受信データ “CCW” の時 ccw_mov_flagをtureにする。
受信データ ”STOP” の時 cw_mov_flagとccw_mov_flagを共にfalseにする。

34~41行目:
今回のプログラムでは、一定時間間隔毎にステッピングモータを1ステップずつ駆動させています。このステップ駆動間の時間を取るのに、現在時間を読み込むmicros()関数を使用します。
micros()関数はプログラム実行を開始した時から現在までの時間をマイクロ秒単位で返す関数です。この関数で現在の時間を得て、前回処理した時の時間との差が予定した一定時間になった時にステッピングモータを1ステップ駆動することによって一定速度の回転が可能となります。

但し、注意が必要なのは、micros()のリターン値はunsigned longの32bitでオーバーフローすると零に戻ります。実際には0xffffffff (4294967295)を超えると零に戻ります。これは71.6分に相当します。
よって、micros()関数で得た現在時間から、前回の処理時にmicros()関数で得た時間を引いて経過時間を計算すると、前回処理した時の時間が71.6分近くの場合に、次の時間読込が0付近の値となってしまい経過時間が正しく計算出来ません。

今回作成したプログラムでは、現在時間と前回の時間を比較して、
現在時間 < 前回の時間
の場合は途中でオーバフローが発生したと判断して、次の様に経過時間を計算しています。
経過時間 = 現在時間 – (0xffffffff – 前回の時間 + 1)

44~57行目:
if (pass_time >= delay_time)で経過時間が予め予定した時間以上になっているか判断します。
経過時間が予定時間以上の場合に、cw_mov_flagがtureの場合はCW方向に1ステップ、ccw_mov_flagがtureの場合はCCW方向に1ステップステッピングモータを駆動しています。
ステッピングモータの駆動は、stepper.step(steps)関数をしています。これはStepperライブラリの関数でステッピングモータをstepsで指定したステップ数駆動します。

ステッピングモータの現在回転位置の変数current_posをモータが+1ステップ動作した場合は、+1して、-1ステップ動作した場合は、-1しています。send_pos()関数でcurrent_posの値を”X:「現在位置」\r\n”の形式でクライアント(スマホ)に送信します。

5.Androidスマホ側のアプリのプログラム

スマホ側のアプリはAndroid IDEを使用してKotlin言語で作成しました。今回使用したスマホのAndroidのバージョンは14です。
アプリはステッピングモータを手動操作する操作パネルです。以下で作成アプリに関して説明します。

5-1.ManifestファイルにPermissonを追加する

Wi-Fiのソケット通信機能を使用するアプリはmanifestsに”android.permission.INTERNET”と”android.permission.ACCESS_WIFI_STATE”の二つのパミッションが必要です。
app→manifests→AndroidManifest.xmlに以下の様にPermissonを追加します。(6行目~7行目)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!--ソケット通信を行うためのパーミッション-->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"

5-2.アプリ画面のレイアウトの作成

アプリ画面のレイアウトは押し釦スイッチ4個とEditTextを2個とTextViewを3個配置しました。以下に画面レイアウトと各Viewのidと役割に関して記します。

名称種類id役割
CWボタンButtoncwButtonモータをCW方向に回転させるボタン
CCWボタンButtonccwButtonモータをCCW方向に回転させるボタン
接続ボタンButtonconnectButtonイーサネット(ソケット通信)の接続を行うボタン
切断ボタンButtondisconnectButtonイーサネット(ソケット通信)を切断するボタン
IPアドレス入力欄EditTextIPAddEditText接続相手(Pico W)のIPアドレスを入力する
ポート番号入力欄EditTextPortEditText接続相手(Pico W)のポート番号を入力する
現在位置表示TextViewtextViewPosモータの回転位置を表示する
“IP Address” 文字列TextViewIP_AddLabelTextView“IP Address”と言う文字列を表示する
“Port” 文字列TextViewPortLabelTextView“Port”と言う文字列を表示する
表5.各Viewの役割とid

画面レイアウトに対応したXMLのコード(app→res→layout→activity_main.xml)を以下に示します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textViewPos"
        android:layout_width="224dp"
        android:layout_height="68dp"
        android:layout_marginStart="32dp"
        android:layout_marginTop="32dp"
        android:text="X 0"
        android:textSize="48sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/cwButton"
        android:layout_width="147dp"
        android:layout_height="74dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="24dp"
        android:text="CW"
        android:textSize="34sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textViewPos" />

    <Button
        android:id="@+id/ccwButton"
        android:layout_width="147dp"
        android:layout_height="74dp"
        android:layout_marginEnd="24dp"
        android:layout_marginTop="24dp"
        android:text="CCW"
        android:textSize="34sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textViewPos" />

    <TextView
        android:id="@+id/IP_AddLabelTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="63dp"
        android:text="IP Address"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/cwButton" />

    <EditText
        android:id="@+id/IPAddEditText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="23dp"
        android:layout_marginTop="52dp"
        android:ems="10"
        android:inputType="text"
        android:text="Name"
        android:textSize="20sp"
        app:layout_constraintStart_toEndOf="@+id/IP_AddLabelTextView"
        app:layout_constraintTop_toBottomOf="@+id/ccwButton" />

    <TextView
        android:id="@+id/PortLabelTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="75dp"
        android:layout_marginTop="32dp"
        android:text="Port"
        android:textSize="20sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/IP_AddLabelTextView" />

    <EditText
        android:id="@+id/PortEditText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="23dp"
        android:layout_marginTop="12dp"
        android:ems="10"
        android:inputType="text"
        android:text="Name"
        android:textSize="20sp"
        app:layout_constraintStart_toEndOf="@+id/PortLabelTextView"
        app:layout_constraintTop_toBottomOf="@+id/IPAddEditText" />

    <Button
        android:id="@+id/connectButton"
        android:layout_width="147dp"
        android:layout_height="74dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="44dp"
        android:text="接続"
        android:textSize="34sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/PortLabelTextView" />

    <Button
        android:id="@+id/disconnectButton"
        android:layout_width="147dp"
        android:layout_height="74dp"
        android:layout_marginTop="33dp"
        android:layout_marginEnd="24dp"
        android:text="切断"
        android:textSize="34sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/PortEditText" />

</androidx.constraintlayout.widget.ConstraintLayout>

5-3.Kotlinプログラムコードの主要要素の説明

今回作成したアプリのKotlinプログラム(MainActivity.kt)のソースコード主要要素の部分を抜き出して説明します。

①onCleate()メソッド処理

アプリが起動された時に最初に実行されるonCleate()メソッドのソースコードを以下に示します。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //接続ボタンのonClickListenerを設定
        val con_button = findViewById<Button>(R.id.connectButton)
        con_button.setOnClickListener(con_buttonOnClickListener())

        //切断ボタンのonClickListenerを設定
        val discon_button = findViewById<Button>(R.id.disconnectButton)
        discon_button.setOnClickListener(discon_buttonOnClickListener())

        //CWボタンのonTouchListenerを設定
        val cw_button = findViewById<Button>(R.id.cwButton)
        cw_button.setOnTouchListener(cw_buttonOnTouchListener())

        //CCWボタンのonTouchListenerを設定
        val ccw_button = findViewById<Button>(R.id.ccwButton)
        ccw_button.setOnTouchListener(ccw_buttonOnTouchListener())
    }

【説明】
3行目:
setContentView()メソッドでactivity_main画面を表示します。

6行目~7行目:
接続ボタンがクリックされた時に呼び出されるリスナを設定します。

10行目~11行目:
切断ボタンがクリックされた時に呼び出されるリスナを設定します。

14行目~15行目:
CWボタンがタッチされた時に呼び出されるリスナを設定します。CWボタンに関しては押し釦に触れた瞬間と離した瞬間の動作を捉えたいので、setOnClickListerではなくてsetOnTouchListenerを使用しています。

18行目~19行目:
CCWボタンがタッチされた時に呼び出されるリスナを設定します。CCWボタンに関しては押し釦に触れた瞬間と離した瞬間の動作を捉えたいので、setOnClickListerではなくてsetOnTouchListenerを使用しています。

②接続ボタン押された時のonClickリスナ

接続ボタンを押された時に、EditTextに設定されたIPアドレスとPort番号のサーバー(Raspberry Pi Pico W)にイーサネット接続(ソケット通信接続)します。
接続ボタンを押された時に呼び出されるリスナのソースコードを以下に示します。

    //接続ボタン押された時のonClickListener
    private inner class con_buttonOnClickListener : View.OnClickListener {
        override fun onClick(v: View?) {
            //IPアドレスをEditTextから得る
            val ipText = findViewById<EditText>(R.id.IPAddEditText)
            val ipAdd: String = ipText.text.toString()
            //ポート番号をEditTextから得る
            val portText = findViewById<EditText>(R.id.PortEditText)
            val port: String = portText.text.toString()
            val portInt: Int = port.toInt()

            if (connectFlag == false) { //接続中?
                //接続と受信スレッドスタート
                val connectThread = ConnectThread(ipAdd, portInt)
                connectThread.start()
            }
        }
    }

【説明】
4行目~10行目:
EditTextに設定された接続先(Raspberry Pi Pico W)のIPアドレスとポート番号を取得します。

14行目~15行目:
ソケット通信接続を行うためのスレッドを起動します。ソケット通信接続処理は時間を要するので、別スレッドで行う必要があります。(スレッド使用しないと異常となります)

③イーサネット接続スレッド

イーサネット(ソケット)を接続するスレッド部分のソースコードを以下にします。イーサネット(ソケット)接続後にデータ受信処理も本スレッドで行います。

    //イーサネット接続と受信スレッド
    private inner class ConnectThread(val address: String, val port: Int) : Thread() {

        private val RCV_BUF_SIZE: Int = 1024                        //受信バッファサイズ
        private val rcvBuffer: ByteArray = ByteArray(RCV_BUF_SIZE)  //受信バッファ
        private var numBytes: Int = 0                               //受信バイト数
        private var rcvString: String? = null                       //受信データ文字列

        public override fun run() {
            //イーサネット接続
            try {
                mmSocket = Socket(address, port)   //ソケット設定
                inStream = mmSocket.getInputStream()    //インプットストリーム取得
                outStream = mmSocket.getOutputStream()  //アウトプットストリーム取得
            } catch (e: IOException) {
                runOnUiThread {
                    Toast.makeText(this@MainActivity, "イーサネット接続出来ません", Toast.LENGTH_LONG).show()
                }
                return
            }
            connectFlag = true      //接続中フラグセット
            runOnUiThread {         //UIスレッド起動
                Toast.makeText(this@MainActivity, "イーサネット接続しました", Toast.LENGTH_LONG).show()
            }

            //データ受信処理
            while (true) {
                var res = receiveData()  //データ受信処理
                if (res == false) {
                    break;
                }
            }

            //イーサネット切断処理
            inStream.close()
            outStream.close()
            mmSocket.close()
            runOnUiThread {         //UIスレッド起動
                Toast.makeText(this@MainActivity, "イーサネット切断しました3", Toast.LENGTH_LONG).show()
            }
        }

【説明】
12行目~14行目:
mmSocket = Socket(address, port)で接続先のRaspberry Pi Pico WのIPアドレスとポート番号を指定してソケットを作成します。(スマホ側がクライアントでRaspberry Pi Pico W側はサーバーです)
インプットストリームとアウトプットストリームを取得します。


27行目~32行目:
while (true){}の中で繰り返しvar res = reciveData()を実行してRaspberry Pi Pico Wから送られて来るデータを受信します。データ受信でエラーが発生した場合はresがfalseとなり、その場合は受信処理から抜け出して、ソケットのクローズ処理等を行ってインサーネットを切断し、スレッドを終了します。

④データ受信関数 receiveData()

Raspberry Pi Pico Wから送信されるデータを受信する処理関数です。以下にソースコードを示します。

        fun receiveData(): Boolean {
            try{
                //受信データ読出し
                numBytes = inStream.read(rcvBuffer, 0, RCV_BUF_SIZE)
            }catch (e: IOException){
                connectFlag = false //接続中フラグクリア
                return false;
            }

            //受信データを文字列に変換する
            var rcvData: String = String(rcvBuffer, 0, numBytes)

            //前回までに受信したデータと結合
            if (rcvString == null){
                rcvString = rcvData
            }else{
                rcvString = rcvString + rcvData
            }

            //ターミネータ"\r\n"検索
            val termPos: Int = rcvString?.lastIndexOf("\r\n")!!
            if (termPos != -1) {  //"\r\n"存在?
                //"X:"を検索する
                val x_pos:Int = rcvString?.lastIndexOf("X:")!!

                //"X:"が存在して"\r\n"より前に存在
                if ((x_pos != -1) && (x_pos < termPos)){
                    //数値の部分を抜き出す
                    val posData: String = rcvString?.substring(x_pos + 2, termPos)!!
                    runOnUiThread{      //UIスレッド起動
                        val posTextView = findViewById<TextView>(R.id.textViewPos)
                        posTextView.text = "X " + posData
                    }
                }
                //受信データストリングクリア
                rcvString = null
            }

            if (connectFlag == false){  //接続中フラグクリア?
                return false
            }
            return true
        }

【説明】
4行目:
numBytes = inStream.read(rcvBuffer, 0, RCV_BUF_SIZE)
ソケット通信で接続されたRaspberry Pi Pico Wから送られて来たデータをrcvBufferに読み込みます。RCV_BUF_SIZEは受信バッファサイズで最大に読み込むバイト数です。

11行目:
受信データはバイトアレイデータなので、var rcvData: String = String(rcvBuffer, 0, numBytes)でStringに変換します。

14行目~18行目:
データが切れ切れに送られて来ることも有りうるので、ターミネータ”\r\n”が来るまで、受信データを結合し続けます。

21行目~37行目:
受信データは”X:「現在位置」\r\n”の形で送られてくるので、受信データから”X:”と”\r\n”を検出して、その間にあるのが現在位置を表す文字列なので、それを現在位置を表示するTextViewに設定しています。
但し、TestViewへの操作は、UIスレッドで行わなければならないので、runOnUiThread{}を使用して、UIスレッドでTextViewへの文字列設定を行っています。

⑤CWボタンonTouchリスナ

CWボタンにタッチした時に呼び出されるリスナのソースコードを以下に示します。CCWボタンにタッチした時に呼び出されるリスナもほぼ同様です。

    //CWボタンのonTouchListener
    private inner class cw_buttonOnTouchListener : View.OnTouchListener {
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            if (connectFlag == true) {   //接続中?
                if (event?.action == MotionEvent.ACTION_DOWN) {         //ボタン押された
                    val data: String = "CW\n"
                    val byteData = data.toByteArray()   //バイト配列にする

                    Thread{                             //スレッドとする
                        outStream.write(byteData)       //データ送信
                    }.start()
                } else if (event?.action == MotionEvent.ACTION_UP) {    //ボタン離された
                    val data: String = "STOP\n"
                    val byteData = data.toByteArray()   //バイト配列にする

                    Thread{                             //スレッドとする
                        outStream.write(byteData)       //データ送信
                    }.start()
                }
            }
            return true
        }
    }

【説明】
5行目:
onTouch()関数は、CWボタンに触れた瞬間と指を離した瞬間に、それぞれ呼び出されます。
if (event?.action == MotionEvent.ACTION_DOWN) {
の判断でCWボタンに指を触れた瞬間か、指が離れた瞬間かを判断しています。

6行目:
CWボタンに指を触れた瞬間は、”CW\n”をRaspberry Pi Pico Wに送信します。

9行目~11行目:
ソケットストリームに対するwrite処理は、一定の処理時間がかかるので、UIスレッドとな異なるスレッドで行わないとエラーとなります。ここでは、Thread{}.start()の{}内でoutStream.write()を行うことで、新たなスレッドでwrite処理を行っています。

12行目:
else if (event?.action == MotionEvent.ACTION_UP) {
の判断でCWボタンから指を離した瞬間かを判断しています。
CWボタンから指を離した瞬間は、”STOP\n”をRaspberry Pi Pico Wに送信します。

5-4.MainActivity.ktの全プログラムコード

以下に作成したKotlinプログラム(MainActivity.kt)の全プログラムコードを示します。

class MainActivity : AppCompatActivity() {
    private var connectFlag: Boolean = false //接続中フラグ
    private lateinit var mmSocket: Socket   //ソケット
    private lateinit var inStream: InputStream  //入力ストリーム
    private lateinit var outStream: OutputStream    //出力ストリーム

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //接続ボタンのonClickListenerを設定
        val con_button = findViewById<Button>(R.id.connectButton)
        con_button.setOnClickListener(con_buttonOnClickListener())

        //切断ボタンのonClickListenerを設定
        val discon_button = findViewById<Button>(R.id.disconnectButton)
        discon_button.setOnClickListener(discon_buttonOnClickListener())

        //CWボタンのonTouchListenerを設定
        val cw_button = findViewById<Button>(R.id.cwButton)
        cw_button.setOnTouchListener(cw_buttonOnTouchListener())

        //CCWボタンのonTouchListenerを設定
        val ccw_button = findViewById<Button>(R.id.ccwButton)
        ccw_button.setOnTouchListener(ccw_buttonOnTouchListener())
    }

    //接続ボタン押された時のonClickListener
    private inner class con_buttonOnClickListener : View.OnClickListener {
        override fun onClick(v: View?) {
            //IPアドレスをEditTextから得る
            val ipText = findViewById<EditText>(R.id.IPAddEditText)
            val ipAdd: String = ipText.text.toString()
            //ポート番号をEditTextから得る
            val portText = findViewById<EditText>(R.id.PortEditText)
            val port: String = portText.text.toString()
            val portInt: Int = port.toInt()

            if (connectFlag == false) { //接続中?
                //接続と受信スレッドスタート
                val connectThread = ConnectThread(ipAdd, portInt)
                connectThread.start()
            }
        }
    }

    //切断ボタン押された時のonClickListener
    private inner class discon_buttonOnClickListener : View.OnClickListener {
        override fun onClick(v: View?) {
            if (connectFlag == true) { //接続中?)
                Thread {                    //スレッドとする
                    try {
                        inStream.close()
                        outStream.close()
                        mmSocket.close()
                    } catch (e: IOException) {}

                    connectFlag = false     //接続中フラグクリア

                }.start()
            }
        }
    }

    //CWボタンのonTouchListener
    private inner class cw_buttonOnTouchListener : View.OnTouchListener {
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            if (connectFlag == true) {   //接続中?
                if (event?.action == MotionEvent.ACTION_DOWN) {         //ボタン押された
                    val data: String = "CW\n"
                    val byteData = data.toByteArray()   //バイト配列にする

                    Thread{                             //スレッドとする
                        outStream.write(byteData)       //データ送信
                    }.start()
                } else if (event?.action == MotionEvent.ACTION_UP) {    //ボタン離された
                    val data: String = "STOP\n"
                    val byteData = data.toByteArray()   //バイト配列にする

                    Thread{                             //スレッドとする
                        outStream.write(byteData)       //データ送信
                    }.start()
                }
            }
            return true
        }
    }

    //CCWボタンのonTouchLitener
    private inner class ccw_buttonOnTouchListener : View.OnTouchListener {
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            if (connectFlag == true) {   //接続中?
                if (event?.action == MotionEvent.ACTION_DOWN) {         //ボタン押された
                    val data: String = "CCW\n"
                    val byteData = data.toByteArray()   //バイト配列にする

                    Thread{                             //スレッドとする
                        outStream.write(byteData)       //データ送信
                    }.start()
                } else if (event?.action == MotionEvent.ACTION_UP) {    //ボタン離された
                    val data: String = "STOP\n"
                    val byteData = data.toByteArray()   //バイト配列にする

                    Thread{                             //スレッドとする
                        outStream.write(byteData)       //データ送信
                    }.start()
                }
            }
            return true
        }
    }

    //イーサネット接続と受信スレッド
    private inner class ConnectThread(val address: String, val port: Int) : Thread() {

        private val RCV_BUF_SIZE: Int = 1024                        //受信バッファサイズ
        private val rcvBuffer: ByteArray = ByteArray(RCV_BUF_SIZE)  //受信バッファ
        private var numBytes: Int = 0                               //受信バイト数
        private var rcvString: String? = null                       //受信データ文字列

        public override fun run() {
            //イーサネット接続
            try {
                mmSocket = Socket(address, port)   //ソケット設定
                inStream = mmSocket.getInputStream()    //インプットストリーム取得
                outStream = mmSocket.getOutputStream()  //アウトプットストリーム取得
            } catch (e: IOException) {
                runOnUiThread {
                    Toast.makeText(this@MainActivity, "イーサネット接続出来ません", Toast.LENGTH_LONG).show()
                }
                return
            }
            connectFlag = true      //接続中フラグセット
            runOnUiThread {         //UIスレッド起動
                Toast.makeText(this@MainActivity, "イーサネット接続しました", Toast.LENGTH_LONG).show()
            }

            //データ受信処理
            while (true) {
                var res = receiveData()  //データ受信処理
                if (res == false) {
                    break;
                }
            }

            //イーサネット切断処理
            inStream.close()
            outStream.close()
            mmSocket.close()
            runOnUiThread {         //UIスレッド起動
                Toast.makeText(this@MainActivity, "イーサネット切断しました", Toast.LENGTH_LONG).show()
            }
        }

        //データ受信処理関数
        fun receiveData(): Boolean {
            try{
                //受信データ読出し
                numBytes = inStream.read(rcvBuffer, 0, RCV_BUF_SIZE)
            }catch (e: IOException){
                connectFlag = false //接続中フラグクリア
                return false;
            }

            //受信データを文字列に変換する
            var rcvData: String = String(rcvBuffer, 0, numBytes)

            //前回までに受信したデータと結合
            if (rcvString == null){
                rcvString = rcvData
            }else{
                rcvString = rcvString + rcvData
            }

            //ターミネータ"\r\n"検索
            val termPos: Int = rcvString?.lastIndexOf("\r\n")!!
            if (termPos != -1) {  //"\r\n"存在?
                //"X:"を検索する
                val x_pos:Int = rcvString?.lastIndexOf("X:")!!

                //"X:"が存在して"\r\n"より前に存在
                if ((x_pos != -1) && (x_pos < termPos)){
                    //数値の部分を抜き出す
                    val posData: String = rcvString?.substring(x_pos + 2, termPos)!!
                    runOnUiThread{      //UIスレッド起動
                        val posTextView = findViewById<TextView>(R.id.textViewPos)
                        posTextView.text = "X " + posData
                    }
                }
                //受信データストリングクリア
                rcvString = null
            }

            if (connectFlag == false){  //接続中フラグクリア?
                return false
            }
            return true
        }
    }
}

6.動作テスト

作成した実験装置の動作をテストをする手順を以下に示します。

6-1.テスト手順

①パソコンとRaspberry Pi Pico WをUSBケーブルで接続してArduino IDEを起動します。今回作成したプログラム(スケッチ)をArduino IDEで開きます。

②プログラム(スケッチ)のSSIDとパスワードの部分を、使用するWi-FiルータのSSIDとパスワードに変えます。

③ツール→シリアルモニタをクリックすると画面の下側にシリアルモニタ画面が表示されます。

④画面左上の→をクリックするとプログラム(スケッチ)のコンパイルが行われ、その後プログラム(スケッチ)がRaspberry Pi Pico Wに書き込まれます。

⑤Raspberry Pi Pico Wへのプログラム(スケッチ)の書き込みが終了すると、プログラムの実行が開始されます。
プログラムはWi-Fiルータへの接続を行い、接続完了後にシリアルモニタにConnect to server at—-と表示されます。

本例では、atの後に表示される192.168.1.4がWi-Fiルータから割り当てられたRaspberry Pi Pico WのIPアドレスです。4242はポート番号です。
同じWi-Fiルータに接続されたスマホからこのアドレスを指定してソケット接続することで、Raspberry Pi Pico Wと通信することが出来ます。

IPアドレスとポート番号を確認したらArduino IDEは閉じてしまっても良いです。

⑥スマホの今回作成したアプリを起動します。
IP Addressに⑤で確認したRaspberry Pi Pico WのIPアドレス192.168.1.4とPort番号に4242を設定します。
・接続ボタンを押すとRaspberry Pi Pico Wとイーサネット接続します。
・CWボタンを押している間ステッピングモータがCW方向に回転します。
・CCWボタンを押している間ステッピングモータがCCW方向に回転します。
・ステッピングモータの現在の回転位置が上方に表示されます。
・切断ボタンを押すとイーサネット接続が切断されます。

7.最後に

スマホとRaspberry Pi Pico WとをWi-Fiルータを介したイーサネットソケット通信で接続してステッピングモータを制御することが出来ました。スマホを制御装置の操作パネルとして使用する様々なシステムに応用することが出来ると思います。


PAGE TOP