3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-05-18

#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


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


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動作確認
公式資料の内容で確認する。

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

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

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

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


>>> 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)

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


>>> 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
が使える。

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);
  }
}
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?