#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に継承させて使っている。
#!/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の違いか?
#!/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を使ったサーバ起動を行う。具体的なソース。
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になる。
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を継承している。
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は以下の通り。
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実装
以上までのソースを参考に、プログラム的には褒められないが、馬鹿正直なハンドラ部分を作ると次のようになる。
もちろんテストはブラウザからテストできる。
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に対する制御部分。
#!/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アドレスで置換をかけて対処する。
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とモータサーバに変更して使うこと。
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は先頭が大文字なので注意すること。
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);
}
}