Python-LEGO Mindstormsでソケット通信

Python環境で[LEGOMindsotrmsEV3](https://afrel.co.jp/product/ev3-introduction)(以降EV3)を利用してロボットプログラミンなどを開発していると当然機械学習などの分析を応用して制御をしたいと思うことがある。そんな時まず考えるのがEV3のセンサー値をPCに送って分析に利用したいと考えるだろう。
方法としてはいくつかあるかもしれないが、今回はソケット通信を用いてセンサーの値をPC側に送ったり、またPC側からメッセージをEV3に送ったりする方法を書いていく。  

## 参考
本記事は以下の記事を参考にしている。
[pythonでsocket通信を勉強しよう](https://qiita.com/__init__/items/5c89fa5b37b8c5ed32a4)

また、以下の書籍を参考にしている。
Pythonを使ったEV3の基本的な制御等については以下に網羅されている。
[ロボットではじめるAI入門](https://ai.afrel.co.jp/pythonai/)


## ソケット通信とは  
ソケット(Socket)とは通信するアプリケーション(今回でいうと2つのpythonプログラム)同士の出入り口のことで、IPアドレスとポート番号の組み合わせで表現することができる。
ソケット通信にはTCP（Transmission Control Protocol）とUDP（User Diagram Protocol）の通信規格があり今回はTCPを用いる。2つの違いはTCPが信頼性を重視するのに対して、UDPは高速性を重視する通信規格になっている。  

## 今回の通信関係  
今回は以下の2通りの通信を行う。それぞれがサーバー・クライアントを行う方法を記載していく。
その1
<img width="500" alt="1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/d387fb30-ac3f-f35f-5fc2-6f71ea45c62f.png">
<img width="500" alt=".png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/f123b09d-78cd-0805-4e0e-d81559f6c02f.png">

その2
<img width="500" alt="2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/e61ca692-4c91-7c4c-f847-78ed348ed0b6.png">
<img width="500" alt="4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/61cb35c9-700f-7d9f-743c-cd2f29bab05f.png">


## 動作環境  
* PC
Windows10
Python 3.7.3

* EV3
ev3dev2-python
[APIリファレンス](https://ev3dev-lang.readthedocs.io/projects/python-ev3dev/en/stable/spec.html)
Python 3.5.3

## PC:サーバー・EV3：クライアント  
まずはPCがサーバー側となりEV3がクライアントとなる方法。といってもサーバー、クライアント共にデータの送受信ができるためできることに大きな違いはない。通信を確立させる際にサーバーが接続待ち状態からクライアントを受け付けるためクライアントが接続要求する際にサーバー側は接続待機状態である必要がある。  

### 準備
EV3の環境や構築については以下の記事を参照してほしい。
[EV3で機械学習その1　環境構築編](https://qiita.com/Hiroki-Fujimoto/items/6ce278411ca151fee750)

EV3をPCとBluetooth接続したらPCでコマンドプロンプトを開きPC-EV3間のBluetooth接続に利用しているIPアドレスを調べる。
PCとEV3をBluetooth接続すると`169.254.XXX.YYY`のリンクローカルアドレスが割り当てられる。以下の手順でIPアドレスを調べる。  

1. コマンドプロンプトを開く
2. `ipconfig`コマンドを実行する
3. 表示される`イーサネットアダプター Bluetooth ネットワークアドレス`に表示されるIPアドレスをメモする
<img width="500" alt="6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/43903202-31ae-c0f7-a650-766ed7402be8.png">



###　プログラム
以下がPC側サーバープログラム  

~~~ PC_server.py
import socket
import sys
import time

ev3_massage = None

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(('169.254.85.105', 50010))  # your PC's Bluetooth IP & PORT
    s.listen(1)
    print('Start program...')
    while True:
        conn, addr = s.accept()
        with conn:
            while True:
                ev3_massage = conn.recv(1024)
                if ev3_massage is not None:
                    ev3_massage = ev3_massage.decode()
                    print('get' + ev3_massage)
                    time.sleep(1.0)
                    if ev3_massage == 'BACKSPACE':
                        break
                    ev3_massage = None
            print('End program')
            sys.exit()
~~~
前述した`イーサネットアダプター Bluetooth ネットワークアドレス`のIPアドレスを`s.bind`のカッコ内に記述する。  
  
  

以下がEV3側クライアントプログラム

~~~ ev3_client.py
import socket
import sys
import time
from ev3dev2.button import Button
from ev3dev2.display import Display
import ev3dev2.fonts as fonts

# definition
button = Button()
screen = Display()
font = fonts.load('luBS12')
ev3_message = None


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(('169.254.85.105', 50010))    # your PC's Bluetooth IP & PORT
    print('connected')
    while not(button.backspace):
        if button.up:
            screen.draw.text((10,10), 'UP Button Pressed', font=font)
            ev3_message = 'UP'
            s.send(ev3_message.encode())
            time.sleep(0.5)
        if button.down:
            screen.draw.text((10,10), 'DOWN Button Pressed', font=font)
            ev3_message = 'DOWN'
            s.send(ev3_message.encode())
            time.sleep(0.5)
        if button.left:
            screen.draw.text((10,10), 'LEFT Button Pressed', font=font)
            ev3_message = 'LEFT'
            s.send(ev3_message.encode())
            time.sleep(0.5)
        if button.right:
            screen.draw.text((10,10), 'RIGHT Button Pressed', font=font)
            ev3_message = 'RIGHT'
            s.send(ev3_message.encode())
            time.sleep(0.5)
        screen.update()
        screen.clear()
        
    screen.draw.text((10,10), 'ENTER Button Pressed', font=font)
    ev3_message = 'BACKSPACE'
    s.send(ev3_message.encode())
screen.clear()
print('End program')
sys.exit()

~~~
前述した`イーサネットアダプター Bluetooth ネットワークアドレス`のIPアドレスを`s.connect`のカッコ内に記述する。

### 実行
プログラムを実行してみる。
サーバープログラムであるPC側から実行する。

コマンドプロンプトからPC側プログラムが格納されているフォルダにcdコマンドで移動する。
以下ではデスクトップに`test`というフォルダを作成し、その中にPC_server.pyを作成している。
<img width="700" alt="5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/ca31ee20-103e-c714-c366-5a014e0843c2.png">
サーバープログラムを実行すると`Start program...`と表示され接続待機状態になる。  


次に、クライアントプログラムであるEV3側を実行する。VSCCodeでのEV3のプログラム実行については[機械学習その1　環境構築編](https://qiita.com/Hiroki-Fujimoto/items/6ce278411ca151fee750)を参照してほしい。
<img width="600" alt="6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/caf525f1-07ad-9846-8d39-38f7bbea2362.png">
実行後、ソケット通信が確立すると`connected`が表示される。  
その後EV3の上下左右のボタンを押すと対応したメッセージをPC側プログラムに送信し、受信するとコマンドプロンプトに表示される。  
<img width="200" alt="17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/659fd76b-ce75-761f-6467-836581273cd8.png">　　　<img width="650" alt="7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/39acae7f-b8ea-5924-cd22-11e1cb0d492c.png">
左上のボタンを押すと、PC側、EV3側両方のプログラムが終了する。
PC側のプログラムにソケット通信を介してUPやRIGHTといった文字列が送信できているのがわかる。
正確にはソケット通信で送信できるデータはバイト型なので、文字列を`encode`して変換してから送り、受け取ったら`decode`して文字列に変換し直してからprint出力している。

## Appendix 
### ソケット通信で受け取った値をCSVファイルに記録
サーバー側のプログラムを書き換えることで、受け取った値をCSVファイルに蓄積することができる。  

~~~ PC_server_csv.py
import socket
import sys
import time
import os.path
import csv

ev3_massage = None

if os.path.exists('button.csv') == False:
    writedata = ['button']
    f = open('button.csv', 'w', newline='')
    writer = csv.writer(f)
    writer.writerow(writedata)
    f.close()

def write(data):
    f = open('button.csv', 'a', newline='')
    writer = csv.writer(f)
    writer.writerow([data])
    f.close()



with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(('169.254.85.105', 50010))  # your PC's Bluetooth IP & PORT
    s.listen(1)
    print('Start program...')
    while True:
        conn, addr = s.accept()
        with conn:
            while True:
                ev3_massage = conn.recv(1024)
                if ev3_massage is not None:
                    ev3_massage = ev3_massage.decode()
                    print('get' + ev3_massage)
                    write(ev3_massage)
                    time.sleep(1.0)
                    if ev3_massage == 'BACKSPACE':
                        break
                    ev3_massage = None
                    
            print('End program')
            sys.exit()

~~~

モジュールには`csv`、`os.path`を新たに利用している。 
プログラムを実行するとCSVファイルが生成され各セルに受け取った文字列が記録されているのがわかる。
<img width="300" alt="13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440646/c3a859b7-3249-81a9-5315-0145f596606c.png">