SpikePrimeをiPhoneから制御する方法

はじめまして、益田慶一朗です。現在株式会社アフレルのインターンに参加させていただいている大学院生です。今回はSpikePrimeをiPhoneから制御する方法を紹介させていただきます。

# 開発の経緯
SpikePrimeはBluetooth接続することで無線での操作が可能だが、iPhoneやiPadにおけるBluetoothで接続できる範囲は四方約10mと限りがあります。そこで、今回はRaspberry PiとSpikePrimeをシリアル通信し、Raspberry Pi をWi-Fiで接続し、Webアプリケーションの作成が可能なFlaskを用いてブラウザ上からSpikePrimeを操作しようと試みました。
これにより、SpikePrimeをタブレットやiPhoneから制御することが可能になります。
また、RaspberrypiからSpikePrimeを操作する際に専用のPortケーブルが必要でしたが、今回の手法ではSpikePrimeの電源用のmicroUSBケーブル接続のみで操作が可能です。

# 開発環境
### 使用するもの
・LEGO SpikePrime
・RaspberryPi3（OS:bullseye）
・iPhone or iPad
・Raspberry PiとiPhoneを接続するためのWi-Fi（もしくはデザリング可能な携帯)

### 環境
・SpikeLegacy (version2)　　※ダウングレードする必要あり
・Python3.8.3
・Flask3.0.0
##### 参考
https://qiita.com/Atomin/items/30136878e08d58cf0fd3

# ハードウェア
今回はこのような機体を設計しました。
　  ![機体1_resize.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070537/1f0ed5f1-154b-4d3f-0e90-ffd2958ab1dc.jpeg)　 　![機体2_resize.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070537/a634d5fc-480c-c293-ee71-d614a28f2fc5.jpeg)

Raspberry PiのUSBポートと電源を指すこと考慮しました。また、左右に曲がる機能を取り入れるため、モーターは左右に一つずつ設置しました。接続は以下のように行いました。
![コード接続.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070537/0a6fe908-a188-0d8c-322d-6568d93a9d25.jpeg)


# 実行手順
### 1.	RaspberryPiの環境構築
ブラウザ上でSpikePrimeを制御できるようにするためにFlaskを使用します。Flaskはpythonに依存しており、Webアプリケーションの作成に使われるアプリで、Web上でマイコンを操作するときに使用されます。
必要なツールを以下の方法でターミナルからインストールします。
```
sudo apt update
sudo apt upgrade
pip install --upgrade pip setuptools
pip install Flask pyserial
```
### 2.	モーター制御のプログラム
シリアル通信を介してモーターの制御を行うため、SpikeLegacyにて以下のプログラムを作成しました。
```Spike.py
#必要ライブラリをインポート
import hub
from hub import USB_VCP
from spike import PrimeHub, MotorPair, Motor
from spike.control import wait_for_seconds

#PCへメッセージを送信する関数
def send_message(serial, data_list1):
    wait_for_seconds(0.1)
    recv_msg = "+++" + str(data_list1) + "+++@"
    serial.write(recv_msg.encode())
    #Hub.speaker.beep(70, 0.5)

#PCからメッセージを受信する関数
def receive_message(serial,feed_flag):
    message = ""
    feed_val = 0
    while True:
        msg = serial.readline()
        if msg == None:
            continue

        if feed_flag == True:
            Hub.speaker.beep(50, 1)
            msg = msg.decode("utf-8")
            message = message + msg
            if message[-1:] == "+":
                message = message.rstrip("+")
                feed_val = round(float(message))
                break
        else :
            msg = msg.decode("utf-8")
            message = message + msg
            help_flag = False
            if message[-1:] == "+":
                message = message.rstrip("+")
                break
    return message

#Spikeと変数を初期化
Hub = PrimeHub()
motor_pair = MotorPair('A','B')
motor_pair.set_default_speed(speed=30)
motor_pair.set_stop_action('coast')
ser = USB_VCP(0)

#プログラム実行時確認音
Hub.speaker.beep(56,0.8)

#Spikeの動きの条件分岐
while True:
    # PCからメッセージを受信してからのモーターの動きの処理
    if receive_message(ser,'1'):
        #後進
        motor_pair.move_tank(1, 'seconds', left_speed=-60, right_speed=-60)

    elif receive_message(ser,'2'):
        #前進
        motor_pair.move_tank(1, 'seconds', left_speed=60, right_speed=60)

    elif receive_message(ser,'3'):
        #右旋回
        motor_pair.move_tank(0.5, 'seconds', left_speed=40, right_speed=-40)

    elif receive_message(ser,'4'):
        #左旋回
        motor_pair.move_tank(0.5, 'seconds', left_speed=-40, right_speed=40)
```
### 3.	Webアプリケーション立ち上げのプログラム
シリアル通信によってRaspberryPiからSpikePrimeを制御するためにFlaskを用いたプログラムとラジコン操作をするためのUIを作成しました。
フォルダの構造は以下の通りです。
```
ラジコン操作.py
SPIKE_CONNECTION_MODULES.py
templates
 └ index.html
 ```
 プログラムは以下の通りです。
 ```SPIKE_CONNECTION_MODULES.py
 import time

def get_ref_data(serial):
    message = receive_message(serial)
    cnv_msg = convert_message(message)
    ref_list = convert_ref_data(cnv_msg)
    return ref_list
def get_gyro_data(serial):
    message = receive_message(serial)
    cnv_msg = convert_message(message)
    yaw_data = convert_yaw_data(cnv_msg)
    gyro_data = yaw2gyro(yaw_data)
    return gyro_data

def send_message(serial,message):
    time.sleep(1.0)
    cmd = str(message) + "+"
    serial.write(cmd.encode())
    print("send data : {}".format(message))

def receive_message(serial):
    print("wait Data from SPIKE  ....")
    message = ""
    message1 = ""
    while True:
        msg = serial.read_all()
        msg = msg.decode('utf-8')
        message = message + msg
        if message.find("@") != -1:
            message = message.split("+++")
            message1 = message[1]
            break
    return message1
```
```ラジコン操作.py 
from flask import Flask
from flask import render_template
import os
import serial
import time
import SPIKE_CONNECTION_MODULES as spike

ser = serial.Serial("/dev/ttyACM0", 115200, timeout=1000)
  
app = Flask(__name__)

@app.route("/")
def main():
    return render_template("index.html")
    
            
@app.route("/front")
def front1():
    spike.send_message(ser,'1')
    return render_template("index.html")
  
@app.route("/left")
def left1():
    spike.send_message(ser,'2')  
    return render_template("index.html")

@app.route("/stop")
def stop1():
    spike.send_message(ser,'s')
    return render_template("index.html")
    
@app.route("/right")
def right1():
    spike.send_message(ser,'3')
    return render_template("index.html")
    
@app.route("/back")
def back1():
    spike.send_message(ser,'4')
    return render_template("index.html")        

if __name__ == "__main__":
    app.run(debug=True, host= 'Wi-FiのIPアドレス' ,port=7000)
```
```index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" name="viewport" content="width=device-width, initial-scale=1">
    <style>
    /* ipad版スタイル */
    @media screen and (max-width: 1000px) and (min-width: 750px) {
            .title {
                text-align: center;
                font-size: 50px;
                line-height: 1;
                margin-top: 40px;
            }
            .subtitle {
                text-align: center;
                font-size: 35px;
                font-style: italic;
                color: #808000;
                margin-top: 0px;
            }
            .button {
            display: inline-block;
            width: 130px;
            height: 90px;
            margin: 10px;
            cursor: pointer;
            background-color: gray;
            font-size: 20px;
            margin-top:1px;
        }
        .mgr-100 {
            margin-left: 305px;
        }
        .mgr-10 {
            margin-left: 150px;
        }
        .mgr-pwm {
            margin-left: 150px
        }
        
        .function { 
            margin-left: 100px;
            font-size: 36px;
            line-height: 1;
            margin-top: -15px;
        }
    }
　/* iphone12版(横向き)スタイル */
    @media screen and (max-width: 900px) and (min-width: 750px) {
            .title {
                text-align: center;
                font-size: 37px;
                line-height: 0.4;
                margin-top: 30px;
            }
            .subtitle {
                text-align: center;
                font-size: 28px;
                font-style: italic;
                color: #808000;
                margin-top: -3px;
            }
            .button {
            display: inline-block;
            width: 100px;
            height: 58px;
            margin: 7px;
            cursor: pointer;
            background-color: gray;
            font-size: 17px;
            margin-top:red;
        }
        .mgr-100 {
            margin-left: 345px;
    
        }
        .mgr-10 {
            margin-left: 227px;
        }
        .mgr-pwm {
            margin-left: 160px
        }
        
        .function { 
            margin-left: 140px;
            font-size: 26px;
            line-height: 0.6;
            margin-top: -50px;
            margin-bottom: -1px;
        }
    }
   
     /* スマートフォン(iphone12)版スタイル */
     @media screen and (max-width: 550px) and (min-width: 350px){
        .title {
                text-align: center;
                font-size: 40px;
            }
            .subtitle {
                text-align: center;
                font-size: 30px;
                font-style: italic;
                color: #808000;
                margin-top: 0px;
            }
            .button {
            display: inline-block;
            width: 70px;
            height: 49px;
            margin: 7px;
            cursor: pointer;
            background-color: gray;
            margin-top:1px;
        }
        .mgr-100 {
            margin-left: 160px;
        }
        .mgr-10 {
            margin-left: 72px;
        }
        .mgr-pwm {
            margin-left: 72px
        }
        
        .function { 
            margin-left: 67px;
            font-size: 25px;
            margin-top: 0px;
            line-height: 1.2;
        }
    }
    </style>
</head>
<body>
    <h1 class="title">ラジコン 操作画面</h1>
    <p class="subtitle">Let's play Spike</p>
　　<p class="function"></span>Direction</p>
  <a href="/front">
    <span class="mgr-100"></span>

#方向のボタン
    <button class="button" name="front">
        <span class="button-label">front</span>
    </button>
    <br>
  <a href="/left">
    <span class="mgr-10"></span>
    <button class="button"  name="left">
        <span class="button-label">left</span>
    </button>
  <a href="/stop">
    <button class="button"  name="stop">
        <span class="button-label">stop</span>
    </button>
  <a href="/right">
    <button class="button"  name="right">
        <span class="button-label">right</span>
    </button>
    <br>
  <a href="/back">
    <span class="mgr-100"></span>
    <button class="button"  name="back">
        <span class="button-label">back</span>
    </button>
</body>
</html>
```
ラジコンの操作画面のデザインは以下の通りです。
  ![UI(縦).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070537/76c8341d-76a5-b728-af75-fabd285d461d.jpeg)      ![UI(横).JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3070537/c0c0deee-0ee2-5d82-f158-60e9c296952f.jpeg)

ラジコン操作.pyを実行することでwebサーバーが立ち上がり、接続しているWiFiのIPアドレスを入力することで操作画面が表示され、iPhoneやiPadなどからSpikePrimeを操作することが可能になります。
こちらがSpikePrimeをiPhoneから操作している動画です。

https://youtube.com/shorts/Pmpr88Z8g6I

今回は部屋の外(約15mほど離れたところ)から操作しましたが、ボタンを押してからのラグもなく操作することができました。Wi-Fiがつながっている場所であればどこからでも操作ができると思われます。

# 今後の展望
今回はブラウザを通してiPhoneからSpikePrimeを操作する方法を紹介しました。今後はラズパイを使用しているのでUSBカメラを用いて物体検知を行いながらのラジコン操作を行ってみたいと考えています。