Arduino+ラズパイ3で金魚の自動餌やり機をリモート操作できるようにした
こんにちは、koheiです。
以前、Arduinoを使って金魚の自動餌やり機を作成しましたが、今回は、ラズパイ3を組み合わせて、スマホ(Android)からリモートで操作できるようにしました。
Aruduino単体では、Wifiモジュールがついていないため、無線通信ができません。ラズパイ3はWifiがついているので、無線通信できます。多分ラズパイだけでも金魚の餌やり機が作れると思いますが、せっかくArduinoで餌やり機を作っていたので、今回はラズパイ+Arduinoでリモート操作を実現してみました。
備忘録としてまとめておきます。
目次
用意したもの
以前の金魚餌やり機から追加したものは、ラズパイ3B+、Android端末です。
・ラズパイ3B+
・Androidスマホ
回路図
回路図は前回の金魚餌やり機の回路にUSBケーブルでArduinoとラズパイをつなぐだけです。(Arduino側の回路図は前回の記事を参照ください)
動作仕様
スマホで、ONボタンを押すと、サーボモーターが動き、OFFを押すとサーボモートが止まるシンプルな仕様です。
システム構成
さて、スマホ(Android端末)からのリモート操作でどうやってサーボを動かすのか?
ざっくり図に書くと以下になります。
・スマホとラズパイ間の通信は、「MQTT」というプロトコルを使っています。
・ラズパイとArduino間はシリアル通信でやり取りします。
1.Arduinoとラズパイ間のシリアル通信を構築
まずは、Arduinoとラズパイ間のシリアル通信を実装していきます。
シリアル通信の仕様は、シンプルに[on]or[off]を通知してモーターを動かす、止めるという仕様にしました。
・off:モーター止める(ラズパイ→Arduino)
1-1. Arduino側プログラム
Arduino側のプログラムはシリアル通信でデータを受信し、データがon/offだったらモーターを動かす、止めるというプログラムです。
以下のように実装しました。
■kingyo_esa_program(ver2.0)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
#include <MsTimer2.h> /* Sweep by BARRAGAN <http://barraganstudio.com> This example code is in the public domain. modified 8 Nov 2013 by Scott Fitzgerald http://www.arduino.cc/en/Tutorial/Sweep */ #include <Servo.h> #include <pt.h> Servo myservo; // create servo object to control a servo // twelve servo objects can be created on most boards int pos = 0; // variable to store the servo position char data[10]; // 文字列格納用 int i = 0; // 文字数カウンタ volatile boolean sw=false; int ct=0; int pt=0; volatile boolean timer_mode=false; volatile boolean tsw=false; int long_key_t=0; //タイマー関連 int sec_cnt=0; int min_cnt=0; int hour_cnt=0; static struct pt pt1, pt2; // pos init int p = 180; int kaiten = 0; #define PT_WAIT(pt, timestamp, usec) PT_WAIT_UNTIL(pt, millis() - *timestamp > usec);*timestamp = millis(); void setup() { myservo.attach(9); // attaches the servo on pin 9 to the servo object pinMode(2, INPUT_PULLUP); //タクトスイッチの設定 pinMode(13,OUTPUT); pinMode(12,OUTPUT); attachInterrupt(0, chgsw, CHANGE); Serial.begin(9600); myservo.write(180); // tell servo to go to position in variable 'pos' MsTimer2::set(1000, timecnt); MsTimer2::start(); PT_INIT(&pt1); PT_INIT(&pt2); } void timecnt() { if(!timer_mode){ return; } //1000ms->1s毎に呼び出される sec_cnt++; Serial.print(hour_cnt); Serial.print("h"); Serial.print(min_cnt); Serial.print("min"); Serial.print(sec_cnt); Serial.println("sec"); if(sec_cnt > 59) { //tsw=true; min_cnt++; sec_cnt=0; } if(min_cnt > 59) { hour_cnt++; min_cnt=0; } if( hour_cnt > 8 ) { tsw=true; sec_cnt=0; min_cnt=0; hour_cnt=0; } } void loop() { thread1(&pt1); // Serial Command Servo action //key switch action /* if( sw ) { motor_action(); } */ // timer action if( tsw ){ motor_action(); tsw=0; } } void motor_action() { for (pos = 180; pos >=80; pos -= 1) { //for (pos = 0; pos <= 120; pos += 1) { // goes from 0 degrees to 180 degrees // in steps of 1 degree myservo.write(pos); // tell servo to go to position in variable 'pos' delay(100); // waits 15ms for the servo to reach the position } // for (pos = 120; pos >= 0; pos -= 1) { // goes from 180 degrees to 0 degrees for (pos = 80; pos <= 180; pos += 1) { myservo.write(pos); // tell servo to go to position in variable 'pos' delay(100); // waits 15ms for the servo to reach the position } } void chgsw() { ct = millis(); if((ct-pt)>300) //チャタリング対応 { //最初のプレス if(digitalRead(2)==LOW) { //長押しONタイマー保存 long_key_t = ct; } //リリース else{ //長押しタイマー判定 if( (ct-long_key_t) > 3000 ){ timer_mode = true; sec_cnt=0; min_cnt=0; hour_cnt=0; Serial.println("TIMER MODE"); digitalWrite(12,HIGH); } else{ //タイマーモード中は、短押しでタイマーモード解除 if(timer_mode){ timer_mode=false; digitalWrite(12,LOW); return; } Serial.println("SHORTKEY"); if(!sw) { sw = true; digitalWrite(13,HIGH); Serial.println("HIGH"); } else { sw = false; digitalWrite(13,LOW); Serial.println("LOW"); } } } } pt = ct; } // データ受信 String serialRead() { // データ受信したとき if(Serial.available() > 0) { // 1文字ずつ読み込み data[i] = Serial.read(); // 文字数が10以上 or 終端文字なら終了 if (i > 10 || data[i] == '\0') { //Serial.println("end"); i = 0; memset(&data[0],0x00,10); // バッファクリア } else { i++; } } return String(data); } // Serial Thread(シリアルコマンドで、サーボを動作させる) static int thread1(struct pt *pt) { static unsigned long timestamp = 0; PT_BEGIN(pt); while(true) { // thread1の中身 PT_WAIT(pt, ×tamp, 100); // 100ms wait //Serial.println("Thread1"); PT_WAIT_THREAD(pt, thread2(&pt2)); // thread2 wait //入力を待つ //Serial.println(sw); if( sw ) { if( kaiten == 0) { myservo.write(p); //Serial.println("servo_write"); p -= 1; if( p == 80 ) { kaiten = 1; } } else if( kaiten == 1) { myservo.write(p); //Serial.println("servo_write"); p += 1; if( p == 180) { kaiten = 0; } } } } PT_END(pt); } // Serial check thread static int thread2(struct pt *pt) { static unsigned long timestamp = 0; PT_BEGIN(pt); //Serial.println("thread2"); String cmd = serialRead(); //Serial.println(cmd); if(cmd =="on") { sw = true; i = 0; memset(&data[0],0x00,10); // バッファクリア digitalWrite(13,HIGH); Serial.println("HIGH"); } if(cmd =="off") { sw = false; i = 0; memset(&data[0],0x00,10); // バッファクリア digitalWrite(13,LOW); Serial.println("LOW"); } PT_END(pt); } |
・前回作成したArduinoだけで動く金魚餌やり機のプログラムをベースに作っています。
・シリアルデータを読むこむ関数は、175行目のserialRead()です。
・193行目のthread1関数が、シリアルコマンドを受けて、サーボモーターを動作させている処理です。
・228行目のthread2関数でシリアルデータで読み込んだデータの判定を行っています。
1-2. ラズパイ側のシリアルテストプログラム
ラズパイ側は、on/offを通知するプログラムになります。
とりあえず、テスト的に、コマンド画面から入力した文字をシリアルで通知するプログラムを組みます。
以下のように実装しました。
■arduino_serial_test.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# -*- coding: utf-8 -*- import serial def main(): ser = serial.Serial('/dev/ttyACM0', 9600 , timeout=1) while True: val=ser.readline() print(val) flag = raw_input() print("input="+flag) ser.write(flag+'\0') if(flag == 'q'): # qが入力されたら通信終了 break; ser.close() if __name__ == '__main__': main() |
・6行目は、シリアル通信するための設定です。
→/dev/ttyACM0は、自分が接続したUSBポートの名前を設定します。「dmesg」コマンドで確認できます。
・12行目でシリアルデータに書き込んでいます。
1-3. 動作確認
さて、早速シリアル通信がうまくいっているか動作確認してみましょう。
ラズパイ側で、作成したPythonテストプログラム「arduino_serial_test.py」を実行してください。
python arduino_serial_test.py
実行したら、onと入力してモーターが駆動、offでモーターが止まればOKです。
2. ラズパイとスマホ間のMQTT通信を実装
次に、ラズパイースマホ間のMQTTプロトコルを実装していきます。
通信仕様は、以下のようにしました。
トピック | メッセージ | 説明 |
mqtt/test | motor_on | サーボモーターを動かす |
mqtt/test | motor_off | サーボモーターを止める |
2-1. ラズパイ側
まずは、ラズパイ側にMQTTブローカーをインストールします。
今回は、mosquittoというソフトを使います。以下の記事を参考に早速ラズパイにインストールします。
mosquittoをインストールしたら、ラズパイ側にSubscribe処理を入れます。
Pahoというライブラリを使えば、簡単に実装できます。
以下の記事を参考にさせていただきました。
以下のような処理をPythonで実装します。
■arduino_serial_mqtt.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# -*- coding: utf-8 -*- import serial import threading import paho.mqtt.client as mqtt host = '127.0.0.1' port = 1883 keepalive = 60 topic = 'mqtt/test' ser = serial.Serial('/dev/ttyACM0', 9600 , timeout=1) def main(): #ser = serial.Serial('/dev/ttyACM0', 9600 , timeout=1) # ser = serial.Serial('/dev/ttyACM0', 9600) while True: val=ser.readline() print(val) #flag = input() #flag = flag + "\0" #print flag #ser.write(flag.encode()) # python2 ver flag = raw_input() print("input="+flag) # if flag == 'on': # ser.write("on\n") # elif flag == 'off': # ser.write("off\n") ser.write(flag+'\0') if(flag == 'q'): # qが入力されたら通信終了 break; ser.close() def mqtt_func(): # MQTT SETTINGS client = mqtt.Client() client.on_connect = on_connect client.on_message = on_message client.connect(host, port, keepalive) client.loop_forever() def on_connect(client, userdata, flags, rc): print('Connected with result code ' + str(rc)) client.subscribe(topic) def on_message(client, userdata, msg): global ser print(msg.topic + ' ' + str(msg.payload)) if str(msg.payload) == 'motor_on': print("serial motor on") ser.write('on'+'\0') elif str(msg.payload) == 'motor_off': print("serial motor off") ser.write('off'+'\0') if __name__ == '__main__': thread_1 = threading.Thread(target=main) thread_2 = threading.Thread(target=mqtt_func) thread_1.start() thread_2.start() |
・46行目でMQTTブローカーに対して、subscribeしています。
・48行目のon_message関数で、受信したメッセージの内容を判断し、シリアル通信(on/off)を通知しています。
2-2. スマホ側処理
スマホ側では、パブリッシャー処理を実装します。
こちらも、Pahoというライブラリを使って実装します。
以下の記事を参考にさせていただきました。
MQTT クライアント を Android に実装する
まずは、ライブラリファイルを以下のサイトからダウンロードし、Androidプロジェクトファイル内のlibsフォルダ内に格納します。
org.eclipse.paho.android.service-1.1.0.jar
org.eclipse.paho.client.mqttv3-1.1.0.jar
build.gradleに以下を追加しライブラリを読み込みます。
1 2 3 4 5 6 7 8 9 10 |
dependencies { //implementation fileTree(dir: 'libs', include: ['*.jar']) compile files('libs/org.eclipse.paho.android.service-1.1.0.jar') compile files('libs/org.eclipse.paho.client.mqttv3-1.1.0.jar') implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' } |
AndroidManifest.xmlファイルに以下を追加します。
■AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.inomacreate.kingyo"> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name="org.eclipse.paho.android.service.MqttService" > </service> </application> </manifest> |
MainActivity.javaとactivity_main.xmlは以下のように実装しました。
■MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
package com.inomacreate.kingyo; import android.content.Context; import android.content.Intent; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import org.eclipse.paho.android.service.MqttAndroidClient; import org.eclipse.paho.client.mqttv3.IMqttActionListener; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.IMqttToken; import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttPersistenceException; public class MainActivity extends AppCompatActivity { private final String TAG = "Debug"; private MqttAndroidClient mqttAndroidClient; private String ID = "hoge@github"; private String PASS = "sango"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mqttAndroidClient = new MqttAndroidClient(this, "tcp://xxx.xxx.xx.xx:1883", "d:lite:test:"){ @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); Bundle data = intent.getExtras(); String action = data.getString("MqttService.callbackAction"); Object parcel = data.get("MqttService.PARCEL"); String destinationName = data.getString("MqttService.destinationName"); if(action.equals("messageArrived")) { Log.d(TAG,destinationName + " " + parcel.toString()); } } }; try { MqttConnectOptions options = new MqttConnectOptions(); options.setUserName(ID); options.setPassword(PASS.toCharArray()); mqttAndroidClient.connect(options, null, new IMqttActionListener() { @Override public void onSuccess(IMqttToken iMqttToken) { Log.d(TAG, "onSuccess"); // try { // mqttAndroidClient.subscribe("hoge@github/#", 0); // Log.d(TAG, "subscribe"); // } catch (MqttException e) { // Log.d(TAG, e.toString()); // } } @Override public void onFailure(IMqttToken iMqttToken, Throwable throwable) { Log.d(TAG, "onFailure"); } }); } catch (MqttException e) { Log.d(TAG, e.toString()); } findViewById(R.id.button_on).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(mqttAndroidClient == null) return; try { if(mqttAndroidClient.isConnected()) { IMqttDeliveryToken token = mqttAndroidClient.publish("mqtt/test", "motor_on".getBytes(), 0, true); } } catch (MqttPersistenceException e) { Log.d(TAG,e.toString()); } catch (MqttException e) { Log.d(TAG,e.toString()); } } }); findViewById(R.id.button_off).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(mqttAndroidClient == null) return; try { if(mqttAndroidClient.isConnected()) { IMqttDeliveryToken token = mqttAndroidClient.publish("mqtt/test", "motor_off".getBytes(), 0, true); } } catch (MqttPersistenceException e) { Log.d(TAG,e.toString()); } catch (MqttException e) { Log.d(TAG,e.toString()); } } }); } @Override protected void onPause() { super.onPause(); try { if(mqttAndroidClient.isConnected()) { mqttAndroidClient.disconnect(); Log.d(TAG,"disconnect"); } mqttAndroidClient.unregisterResources(); } catch (MqttException e) { Log.d(TAG,e.toString()); } } } |
・33行目は、ブローカーのIPアドレスを設定します。(今回は、ラズパイのIPアドレスになります)
・92行目、110行目でトピック、メッセージをpublishしています。
■activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.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="com.inomacreate.kingyo_remote.MainActivity"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="30dp" android:text="きんちゃんエサやりモード" android:textSize="24sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/button_on" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ON" app:layout_constraintBaseline_toBaselineOf="@+id/button_off" app:layout_constraintStart_toStartOf="@+id/textView" /> <Button android:id="@+id/button_off" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="12dp" android:layout_marginTop="19dp" android:text="OFF" app:layout_constraintStart_toEndOf="@+id/button_on" app:layout_constraintTop_toBottomOf="@+id/textView" /> </android.support.constraint.ConstraintLayout> |
3. 動作確認
さて、早速、スマホ→ラズパイ→Arduino→サーボモーターの流れで、動作するかどうかを検証してみました。
まずは、ラズパイで以下のコマンドを実行し、作成したPythonプログラムを動かしておきます。
python arduino_serial_mqtt.py
次にスマホアプリを起動、ON/OFFボタンでサーボモーターを動かします。
やった!うまく動きました!!
4.ラズパイ起動時に自動でプログラムを起動する
【追記】2018年8月26日
水槽の横で動かすために、ラズパイ起動時に自動でプログラムを起動するようにします。
以下サイトによると、ラズパイ起動時にプログラムを動かす方法としては、5種類あるようですが、systemsとrc.localのやり方はうまくいかずハマりそうだったので、autorunというやり方を採用しました。
まずは、今回作ったプログラム「arduino_serial_mqtt.py」をシェルスクリプトで実行できるようにします。
テキストエディタで、以下のような「kingyo.sh」というスクリプトファイルを作成します。
1 |
/usr/bin/python /home/pi/arduino/serial_test/arduino_serial_mqtt.py |
作成したシェルスクリプトに実行権限を与えます。
・chmod +x kingyo.sh
シェルスクリプト「kingyo.sh」を以下へコピーします。
・/usr/local/bin
次に、autostartのファイルを編集します。
テキストエディタで「autostart」を開きます。(以下はテキストエディタviを使用)
・vi /home/pi/.config/lxsession/LXDE-pi/autostart
最後の行に以下を追記します。
@kingyo.sh
これで、ラズパイ起動時に「kingyo.sh」を自動で実行してくれます。
最後に
スマホからリモート操作できるようになったので、あとは実際に金魚の水槽の横で動かしてみたいと思います。
そのためには、ラズパイを起動したときに作成したPythonプログラムを自動で動かすこと、あとはラズパイの電源OFFをどうするかなど。
もう少し課題がありそうです。また随時対応したら記事を更新します!
【更新情報】2018年8月26日 ラズパイ起動時に自動でプログラムを起動するようにしました。これで、水槽の横で動かせるようになりました。(詳細は、4章を参照ください)
それでは!
スポンサーリンク