httpによるev3devモータ駆動サーバ

#0.何のメモか

モータ駆動実験の目標：
先週までの実験で、加速度センサが置かれた状態、あるいは加速度センサにより計測できる「動き」を検出するための準備調査を行った。
今週は、より高度な状態や「動き」を検出し、ユニークな出力に繋げるIoTシステムの設計・実装を行うため、Arduinoに接続されたLEDではなく、PCを介してモータサーバを制御することを狙う。このページはモータサーバの構築内容についてのメモである。

実験者の準備としては、ev3devのイメージをダウンロードしてSDカードに焼いたものを持参することを仮定する。

※情報工学実験の3週目の内容は3.3の内容からスタートする。
https://qiita.com/takelab/items/ed4435f0278fd58fd286#33-%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%AB%E9%80%9A%E4%BF%A1%E3%81%A8%E3%81%AE%E9%80%A3%E6%90%BA%E3%81%99%E3%82%8B%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0

#1. ev3dev-lang-pythonの動作確認
##1.1 インストール済のパッケージの確認
宿題にしてあったソフトのインストールと設定を前提に作業を行います。
https://qiita.com/takelab/items/ebde398893d05ca7a56a
OpenDHCPサーバでクロスケーブルにより有線接続されたev3がIPを自動取得。
その後、TeraTermでSSHアクセスを行う（ユーザ名:robot パスワード:maker）

リスト表示`dpkg -l |grep ev3dev`

```bash

ii  python3-ev3dev                             1.2.0                             all          Python language bindings for ev3dev
ii  python3-ev3dev2                            2.0.0~beta3                       all          Python language bindings for ev3dev
```
こんな結果が出るはず。
基本的に、最初にインストールされているpython3-ev3dev2の2.0.0~beta3を基準に実験を進める。つまり、'apt-get update'等はしない。

一応、何をセレクトしているか表示`dpkg --get-selections`

```bash

robot@ev3dev:~$ dpkg --get-selections |grep ev3
ev3-config                                      install
ev3-systemd                                     install
ev3dev-adduser-config                           install
ev3dev-base-files                               install
ev3dev-bluez-config                             install
ev3dev-connman-config                           install
ev3dev-media                                    install
ev3dev-rules                                    install
ev3dev-tools                                    install
ev3devkit-data                                  install
firmware-ev3                                    install
gir1.2-ev3devkit-0.5                            install
jri-11-ev3:armel                                install
libev3devkit-0.5-0                              install
linux-image-4.14.96-ev3dev-2.3.2-ev3            install
linux-image-ev3dev-ev3                          install
micropython-ev3dev2                             install
python3-ev3dev                                  install
python3-ev3dev2                                 install
rtl8188eu-modules-4.14.96-ev3dev-2.3.2-ev3      install
rtl8812au-modules-4.14.96-ev3dev-2.3.2-ev3      install
```

##1.2 ev3devでのpython動作確認
公式資料の内容で確認する。

https://github.com/ev3dev/ev3dev-lang-python/blob/ev3dev-stretch/README.rst

コマンドラインから`python3`で起動。
先頭の「>>>」は対話入力をしている印。
その後ろに以下の各行を一行づつ入力し、Enterキーを押す。
pythonの対話モード終了の際は「>>>」の後に`exit()`でEnterキーを押す。


```python
>>> from ev3dev2.sound import Sound
>>> sound = Sound()
>>> sound.speak('Hello World')
```

##1.3 ev3devでのモータ動作確認

同様にモータの動作も確認する。AポートとBポートにモータを接続する。
ここで、レポートのために、モータの接続図を作成するためのメモを取っておくこと。

```python

>>> from ev3dev2.motor import LargeMotor, OUTPUT_A, OUTPUT_B, SpeedPercent
>>> m1 = LargeMotor(OUTPUT_A)
>>> m2 = LargeMotor(OUTPUT_B)
>>> m1.on(SpeedPercent(-10),brake=False)
>>> m1.on(SpeedPercent(0),brake=False)
>>> m2.on(SpeedPercent(10),brake=False)
>>> m1.on(SpeedPercent(0),brake=False)
>>> m1.on(SpeedPercent(-10),brake=False)
>>> m2.on(SpeedPercent(0),brake=False)
>>> m1.on(SpeedPercent(0),brake=False)

```


ちなみに、各要素は次のような結果を返す。


```bash

>>> print(LargeMotor)
<class 'ev3dev2.motor.LargeMotor'>
>>> print(OUTPUT_A)
ev3-ports:outA

```

モータ動作の仕様は、以下の通り（今回はサーバ経由で制御するので直接制御は行わない）
https://python-ev3dev.readthedocs.io/en/ev3dev-stretch/motors.html#ev3dev.motor.Motor.on_for_degrees
https://sites.google.com/site/ev3devpython/learn_ev3_python/using-motors


##1.4 ev3devでhttpサーバ

`python3 -m http.server 8000`
が使える。

```bash
robot@ev3dev:~/dist-pack-ev3dev$ cd
robot@ev3dev:~$ python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 ...
192.168.137.5 - - [04/Mar/2019 09:11:57] "GET / HTTP/1.1" 200 -
192.168.137.5 - - [04/Mar/2019 09:11:57] code 404, message File not found
192.168.137.5 - - [04/Mar/2019 09:11:57] "GET /favicon.ico HTTP/1.1" 404 -
^C
Keyboard interrupt received, exiting.
```

#2. HTTPサーバの部分

1.4のようにpythonの標準ライブラリである、http.serverを使うことができることまで確認した。

##2.1 デモにあるEV3D4のwebコントロール

デモ中のEV3D4WebControl.pyの先頭部分
WebControlledTankをimportして、それをEV3D4WebControlledに継承させて使っている。

```EV3D4WebControl.py
#!/usr/bin/env python3

import logging
import sys
from ev3dev2.motor import OUTPUT_A, OUTPUT_B, OUTPUT_C, MediumMotor
from ev3dev2.control.webserver import WebControlledTank

class EV3D4WebControlled(WebControlledTank):
    def __init__(self, medium_motor=OUTPUT_A, left_motor=OUTPUT_C, right_motor=OUTPUT_B):
        WebControlledTank.__init__(self, left_motor, right_motor)
        self.medium_motor = MediumMotor(medium_motor)
        self.medium_motor.reset()
    #この後まだ数行続く
```

importされているWebControlledTankがある。ev3dev2.control.webserver.pyの先頭部分を見る。
`from http.server import BaseHTTPRequestHandler, HTTPServer`の行を見ると分かるように、やはりhttp.serverからimportをしているので、python3の標準的な実装を行っている様子。具体的には、/usr/lib/python3.5/の下のhttp/server.pyを見てやると具体的なことは分かる。
Webを検索すると、`from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer`とやっている例があるが、python2とpython3の違いか？

```webserver.py
#!/usr/bin/env python3

import logging
import os
import re
from ev3dev2.motor import MoveJoystick, list_motors, LargeMotor
from http.server import BaseHTTPRequestHandler, HTTPServer
```

###継承しているWebControlledTankの確認
具体的には、MoveJoyStickの継承をして、TankWebHandlerをハンドラにして、RobotWebServerを使ったサーバ起動を行う。具体的なソース。

```WebControlledTank.py

class WebControlledTank(MoveJoystick):
    """
    A tank that is controlled via a web browser
    """

    def __init__(self, left_motor, right_motor, port_number=8000, desc=None, motor_class=LargeMotor):
        MoveJoystick.__init__(self, left_motor, right_motor, desc, motor_class)
        self.www = RobotWebServer(self, TankWebHandler, port_number)

    def main(self):
        # start the web server
        self.www.run()

```

###サーバ部分のRobotWebServer

サーバは`self.content_server = HTTPServer(('', self.port_number), self.handler_class)`でハンドラを分岐する。
具体的には、’’は自分のIPアドレス、ポート番号は8000, self.handler_classはTankWebHandlerになる。

```RobotWebServer.py

class RobotWebServer(object):
    """
    A Web server so that 'robot' can be controlled via 'handler_class'
    """

    def __init__(self, robot, handler_class, port_number=8000):
        self.content_server = None
        self.handler_class = handler_class
        self.handler_class.robot = robot
        self.port_number = port_number

    def run(self):

        try:
            log.info("Started HTTP server (content) on port %d" % self.port_number)
            self.content_server = HTTPServer(('', self.port_number), self.handler_class)
            self.content_server.serve_forever()

        # Exit cleanly, stop both web servers and all motors
        except (KeyboardInterrupt, Exception) as e:
            log.exception(e)

            if self.content_server:
                self.content_server.socket.close()
                self.content_server = None

            for motor in list_motors():
                motor.stop()
```


##2.2 HTTPメソッドハンドラ

2.1で説明したようにWebサーバのハンドラ制御部分が中心的になる。
###TankWebHandlerはRobotWebHandlerを継承している。
RobotWebHandlerは後述のソースように標準的なBaseHTTPRequestHandlerを継承している。

```TankWebHandler.py

class TankWebHandler(RobotWebHandler):
    def __str__(self):
        return "%s-TankWebHandler" % self.robot
    def do_GET(self):
        """
        Returns True if the requested URL is supported
        """
        if RobotWebHandler.do_GET(self):
            return True
```


実際のRobotWebHandlerは以下の通り。

```RobotWebHandler.py

log = logging.getLogger(__name__)

# ==================
# Web Server classes
# ==================
class RobotWebHandler(BaseHTTPRequestHandler):
    """
    Base WebHandler class for various types of robots.
    RobotWebHandler's do_GET() will serve files, it is up to the child class to handle REST APIish GETs via their do_GET()
    self.robot is populated in RobotWebServer.__init__()
    """

    # File extension to mimetype
    mimetype = {
        'css'  : 'text/css',
        'gif'  : 'image/gif',
        'html' : 'text/html',
        'ico'  : 'image/x-icon',
        'jpg'  : 'image/jpg',
        'js'   : 'application/javascript',
        'png'  : 'image/png'
    }

    def do_GET(self):
        """
        If the request is for a known file type serve the file (or send a 404) and return True
        """

        if self.path == "/":
            self.path = "/index.html"

        # Serve a file (image, css, html, etc)
        if '.' in self.path:
            extension = self.path.split('.')[-1]
            mt = self.mimetype.get(extension)

            if mt:
                filename = os.curdir + os.sep + self.path

                # Open the static file requested and send it
                if os.path.exists(filename):
                    self.send_response(200)
                    self.send_header('Content-type', mt)
                    self.end_headers()

                    if extension in ('gif', 'ico', 'jpg', 'png'):
                        # Open in binary mode, do not encode
                        with open(filename, mode='rb') as fh:
                            self.wfile.write(fh.read())
                    else:
                        # Open as plain text and encode
                        with open(filename, mode='r') as fh:
                            self.wfile.write(fh.read().encode())
                else:
                    log.error("404: %s not found" % self.path)
                    self.send_error(404, 'File Not Found: %s' % self.path)
                return True

        return False

    def log_message(self, format, *args):
        """
        log using our own handler instead of BaseHTTPServer's
        """
        # log.debug(format % args)
        pass
```

##2.3 簡易do_GET実装
以上までのソースを参考に、プログラム的には褒められないが、馬鹿正直なハンドラ部分を作ると次のようになる。
もちろんテストはブラウザからテストできる。

```test.py
from http.server import HTTPServer, BaseHTTPRequestHandler

class MyHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        print(self.path)
        if self.path == "/":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("OK".encode('utf-8'))
            return

        elif self.path == "/command":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("OK".encode('utf-8'))
            return

        else:
            self.send_response(200)
            self.end_headers()
            self.wfile.write("NG".encode('utf-8'))
            return


if __name__ == "__main__":
    host = ''
    port = 8181

    server = HTTPServer((host, port), MyHandler)
    server.serve_forever()

```

#3. モータ制御との連携
#3.1 Python(ev3dev側）のサーバプログラム
2.3のプログラムに単にモータ管理用オブジェクトをグローバル変数化してif-elifで制御する単純プログラム。
まずはここから。これから良いプログラムにしてください。

実行は、`python3 motorHttp.py`でOK。
終了はCtrl+C。

各if文の分岐先にある
`m2.on(SpeedPercent(10),brake=False)`がev3devに対する制御部分。

```motorHttp.py
#!/usr/bin/env python3
#
#

import logging
import sys

from ev3dev2.motor import LargeMotor, OUTPUT_A, OUTPUT_B, SpeedPercent
from http.server import HTTPServer, BaseHTTPRequestHandler


class MyHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        global m1
        global m2
        print(self.path)
        if self.path == "/":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA: OFF B: OFF".encode('utf-8'))
            m1.on(SpeedPercent(0),brake=False)
            m2.on(SpeedPercent(0),brake=False)
            return

        elif self.path == "/a0":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA: OFF B:x".encode('utf-8'))
            m1.on(SpeedPercent(0),brake=False)
            return

        elif self.path == "/a1":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA: ON B:x".encode('utf-8'))
            m1.on(SpeedPercent(10),brake=False)
            return

        elif self.path == "/a-1":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA: RV B:x".encode('utf-8'))
            m1.on(SpeedPercent(-10),brake=False)
            return

        elif self.path == "/b0":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA:x B:OFF".encode('utf-8'))
            m2.on(SpeedPercent(0),brake=False)
            return

        elif self.path == "/b1":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA:x B:ON".encode('utf-8'))
            m2.on(SpeedPercent(10),brake=False)
            return

        elif self.path == "/b-1":
            self.send_response(200)
            self.end_headers()
            self.wfile.write("MotorA:x B:RV".encode('utf-8'))
            m2.on(SpeedPercent(-10),brake=False)
            return

        else:
            self.send_response(200)
            self.end_headers()
            self.wfile.write("OK".encode('utf-8'))
            return

if __name__ == "__main__":
    host = ''
    port = 8181
    m1 = LargeMotor(OUTPUT_A)
    m2 = LargeMotor(OUTPUT_B)
    m1.on(SpeedPercent(0),brake=False)
    m2.on(SpeedPercent(0),brake=False)

    server = HTTPServer((host, port), MyHandler)
    server.serve_forever()
```

##3.2 Windows側のクライアントプログラム

キーボード入力の先頭文字列を使い、3.1のサーバプログラムと連携する。
IPアドレスを固定で書いてあるので、IPアドレスで置換をかけて対処する。

```key2HTTP.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.IO.Ports;
using System.IO;

namespace key2HTTP
{
    class Program
    {
        static void Main(string[] args)
        {
            ConsoleKeyInfo k;

            for (int i = 0; i < 100; i++)
            {
                //Console.WriteLine("InputInt Number");
                k = Console.ReadKey(true);
                string s = k.KeyChar.ToString();
                //この段階でiは文字列sが数字なら数字に、0とそれ以外は0になっている。
                Console.WriteLine("KEYBOARD INPUT: {0}", s);
                if (!string.IsNullOrEmpty(s))
                {
                    WebClient wc = new WebClient();
                    Stream st;
                    StreamReader sr;
                    //受信データに関する処理
                    if (s.StartsWith("0"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("a"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/a1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("b"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/a-1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("c"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/a0");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("e"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/b1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("f"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/b-1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else if (s.StartsWith("g"))
                    {
                        st = wc.OpenRead("http://192.168.137.7:8181/b0");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else
                    {
                        Console.WriteLine("We don't define the command");
                    }
                    wc.Dispose();
                }
            }
            Console.WriteLine("Press Enter Key");
            Console.ReadLine();
        }
    }
}
```

##3.3 シリアル通信との連携するプログラム
前節の3.2節でkey2HTTP.csでモータ制御のコツをつかんだら、キーボード入力に応じてモータ駆動するのではなく、シリアル通信の内容に応じてモータ駆動するプログラムに改造する。
前提としては、ミニLinux Boxで、
```python3 motorHttp.py```
でモータサーバが起動している必要がある。
また、Arduinoでシリアルポートで接続され、COMポート番号が振られていることを確認すること。

###3.3.1 C#のプログラム
シリアル通信の基礎プログラムである以下のプログラムを、改造し、より自分たちの目的に適合したプログラムを作成せよ。
プログラム中のCOMポート番号とIPアドレスは、それぞれ自分たちのArduinoとモータサーバに変更して使うこと。

```ComReceive.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.IO.Ports;
using System.IO;
using System.Threading;

namespace ComReceive
{
    class Program
    {
        static void Main(string[] args)
        {
            SerialPort sp = new SerialPort();
            sp.BaudRate = 9600;
            //sp.BaudRate = 115200;
            sp.PortName = "COM4"; //自分たちのCOMポート番号にすること
            // extra
            sp.Parity = Parity.None;
            sp.StopBits = StopBits.One;
            sp.DataBits = 8;
            //sp.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
            // フロー制御はしません。
            // port.DtrEnable = false;
            //port.RtsEnable = false;

            //Start Send Charctor
            Console.Write("Press EnterKey to start!");
            Console.ReadLine();
            string req = "s";

            try
            {
                sp.Open();//シリアルポートのオープン
                sp.WriteLine(req);
                Console.WriteLine("send 　 :" + req);
            }
            catch (Exception e)
            {
                Console.WriteLine("Unexpected exception: ", e.ToString());
            }


            //while(true)
            for (int i = 0; i < 50; i++)
            {
                //シリアルポートからの受信
                string s = sp.ReadLine();
                if (!string.IsNullOrEmpty(s))
                {
                    WebClient wc = new WebClient();
                    Stream st;
                    StreamReader sr;
                    //受信データに関する処理
                    if (s.StartsWith("a"))
                    {
                        Console.WriteLine("Receive command: a");
                        st = wc.OpenRead("http://192.168.137.8:8181/a1");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                        //引数の値をミリ秒単位でみて時間待ちをする。
                        Thread.Sleep(1000);
                        st = wc.OpenRead("http://192.168.137.8:8181/a0");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    else
                    {
                        Console.WriteLine("Receive undefined command !");
                        st = wc.OpenRead("http://192.168.137.8:8181/");
                        sr = new StreamReader(st, Encoding.GetEncoding("UTF-8"));
                        Console.WriteLine(sr.ReadToEnd());
                    }
                    wc.Dispose();
                }
            }
            Console.WriteLine("loop was over");
            sp.Close();//シリアルポートのクローズ
        }
    }
}
```

###3.3.2 Arduino側のプログラム

Arduinoの加速度センサのx軸の計測値変化だけを見て、シリアルポートに文字出力を行うプログラム例。
連携がうまく行かないときは、一旦は連携を辞め、モータ側ならkey2HTTP.csで動作確認する。
Arduino側のデバッグはC#のプログラムを止め、      ```Serial.println("DEBUG");```
関数を使って、Arduinoのシリアルモニタを使って、動作確認すると良い。
Serialは先頭が大文字なので注意すること。


```sendCommand.c
void setup() {
  pinMode(8,OUTPUT);
  pinMode(12,OUTPUT);
  Serial.begin(9600);
}

void loop() {
  int i,f;
  int x, y, z;
  while(Serial.available() == 0);
  char c = Serial.read(); // 一文字分データを取り出す。

  f=0;
  while(1){
    x = analogRead(A0);
    y = analogRead(A1);
    z = analogRead(A2);
    if(f != 1 && x >600){
      Serial.println("a");
      f=1;
    }
    else if(f ==1 && x <550){
      f =0;
    }
    delay(100);
  }
}
```