PybricksでMove Hubを動かそう

プログラミング入門ゼミ補助教材 2022年度

LEGO社のBoostキット には Move Hub と呼ばれるコントローラが含まれています。 このコントローラには左右2個のモータとIMUが装備され、 さらに外部端子を使えば合計2個のモータやセンサを 接続することも可能です。

以下は信州大学全学教育機構で開講している共通教育の授業 『プログラミング入門ゼミ』の補助教材で、 PybricksというMicroPythonを使ってこのMove Hubを活用するための チュートリアルです。

プログラミングのための準備

Move Hubを動かすために必要な環境

Pybricksを使ってMove Hubを動かすためには、以下の環境が必要です。

Web Bluetoothについて

Web BluetoothはウェブブラウザからBluetooth対応の機器を操作する仕組みです。 ただし、現在ではまだ対応していないブラウザも多く、セキュリティの問題も含め、 現時点では実験段階の技術です。

参考ページ: https://webbluetoothcg.github.io/web-bluetooth/

2022年10月現在、FirefoxやSafariはWeb Bluetoothに対応していません。 この授業では、Web Bluetoothの開発状況を注視しながら、 Web Bluetoothに対応しているChromeブラウザを使ってプログラミング作業を行います。

Chromeの設定変更

Chromeの設定を変更するには、通常のURLを入力するところに以下を入力し、 [experimental-web-platform-features] を [Disabled] から [Enabled] に変更してください。ただし、すでに [Enabled] になっている場合は、そのままにしておいてください。

chrome://flags/#enable-experimental-web-platform-features

ファームウェアのインストール

Pybricksを使用するには、Move Hubにもともと入っているLEGO社純正のファームウェアではなく Pybricksのファームウェアを使用します。 このファームウェアのインストール方法については、 Pybricksの公式ドキュメント https://pybricks.com/install/technic-boost-city/ やその中の動画を参考にしてください。 基本的には以下のような手順です。

  1. https://code.pybricks.com/にアクセスする。
  2. MoveHubの電源を切る (緑のボタンを長押し)。
  3. 緑のボタンを押したまま待ち、ピンクのランプが点滅したら、 ボタンを離さずに右端の「firmware update」(ブロックの上に下向き矢印が描かれているアイコン)をクリックする。
  4. 「code.pybricks.com wants to pair」というウィンドウが表示されるので、 その中の「LEGO Bootloader」を選び「Pair」ボタンをクリックする。
  5. ピンク色のランプの点滅が終わり、赤・緑・青の点滅が始まったらすぐに緑のボタンを離す。
  6. ファームウェアのインストールが終了するまで待つ。

とりあえず動かしてみる

Pybricksの公式ドキュメント https://docs.pybricks.com/en/latest/hubs/movehub.html を参考にしてください。

Bluetoothで接続

まずMove HubとパソコンをBluetoothで接続します。

  1. https://code.pybricks.com/ にアクセスする。
  2. 本体の緑のボタンを押したのち、Bluetoothのアイコンをクリック。
  3. 「Pybricks hub」を選び「Pair」をクリック。Bluetoothのアイコンが薄青から青に変わればOK。

モータAを3秒間回転させる

簡単な例から始めましょう。 まず次のコードを入力してください。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port
from pybricks.tools import wait

これら行は「import文」と呼ばれている命令で、 必要な機能をライブラリ(モジュール)から読み込むためのものです。 この例では、モータを動かすために、 Motor クラス、 Port クラス、 wait 関数をそれぞれのモジュールから読み込んでいます。 「クラス」については後ほど説明します。

入力したら、三角のアイコンをクリックして、スクリプトを実行してみましょう。 もちろんこれらの命令は単にクラスや関数を読み込む(インポートする)だけなので モータは動かず、プログラムはすぐに終了するはずです。 しかし、何かエラーがあった場合は、下部のコンソールにエラーメッセージが表示されます。 エラーがあった場合は、特に綴りなどを確認して修正しましょう。

では上記のインポート文のあとにモータを回転させる命令を追加しましょう。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port
from pybricks.tools import wait

motor_a = Motor(Port.A)
motor_a.run(360)
wait(3000)
motor_a.stop()

入力したら、再度三角のアイコンをクリックして、スクリプトを実行してみましょう。 モータAが3秒間回ればOKです。

このスクリプトについて簡単に説明しておきましょう。

まず、「 motor_a = Motor(Port.A)」 は、 Motor というクラス(雛形)から motor_a という名前でインスタンス(実体)を生成します。 クラス名のあとの括弧にはモータの接続されている端子を指定しますが、 Move Hubの場合は内蔵モータがAとBに割り当てられています。 ここで「クラス」とはオブジェクトが持ついろいろなプロパティや機能をまとめた設計図(雛形)のようなもので、 実際に使う際には、この雛形からから「インスタンス(実体)」を生成する必要があります。 一つのクラスから複数のインスタンスを生成することができますが、 それぞれのインスタンスのプロパティは、それぞれ別々の値を保持することができます。

Pythonの場合、クラスを変数(この場合は motor_a )に代入するだけで、 インスタンスが生成されます。 そういう意味で「変数」と「インスタンス」は同じだと考えてかまいません。 ちなみに「 = 」は代入演算子と呼ばれるもので、右辺の値を左辺の変数に代入します。

クラス名も変数名も英数字とアンダースコアー「 _ 」を使用して構成できますが、 最初の文字に数字を使うことは許されていません。 また、PEP8という文書では、クラス名は CapWords方式 (先頭だけ大文字の単語を繋げる、アンダースコアは使わない) が推奨されています。 一方、変数名は小文字とアンダースコアで構成することが推奨されています。 ちなみに、「 x = Motor(Port.A)」 のように簡単な変数名を 使うことももちろん可能ですが、なるべく分かりやすい変数名を使いましょう。

次の命令、「 motor_a.run(360) 」は、 生成したインスタンス( motor_a )の run() というメソッド(関数)を実行します。 run()Motor クラスに 用意されているモータを回転する機能(関数,メソッド)で、 括弧の中にスピードなどを指定することができます。 スピードの単位は deg/s (度/秒) で、この例では毎秒360度なので、1秒間で1回転します。 また、スピードに正の値を指定すると時計回りに、負の値を指定すると反時計周りに回転します。 ただし、 Motor インスタンスを生成するときに、 motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE) とすると、正回転の方向を逆にしておくことも可能です (あらかじめparameterモジュールから Direction をインポートしておく必要あり)。

ここで注意すべき点は、メソッド(関数)を呼び出す時は、 インスタンス名のあとに「 . 」に 続けてメソッド名を指定すること、そして、 メソッド名の後には必ず ( ) が必要だということです。 この ( ) の中には、関数に渡す値(「引数」と呼びます)を入れますが、 引数がない場合でも空の () が必要である、ということに注意してください。

wait(3000) 」は、次の命令に進むまで3秒間待つ、という命令です。 引数の単位は1/1000秒です。 つまり、モータが動いている状態で3秒間待ちます。

そして、3秒間待ったあと、 stop() メソッドを実行して、 モータを停止し、プログラムが終了します。

上記の例と同じスピードで、モータAを逆に回転させましょう。

上記の例でモータAの変わりにモータBを回転させましょう。

端子Cにモータを接続し、モータCを5回転させましょう。

モータAとBを3秒間回転させる

モータBも回転させてみましょう。 今回は、モータAとモータBの回転の正方向が同じになるように、 インスタンス生成に、 positive_direction 引数も 指定しておきましょう。 ロボットの進む向きにあわせて、モータAかモータBのどちらかに 指定してください。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction  # Directionもインポート
from pybricks.tools import wait

motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)  # 正回転方向の指定がないと時計回りが正回転になる

motor_a.run(360)
motor_b.run(360)
wait(3000)
motor_a.stop()
motor_b.stop()

ここで 「 # 」以下は、コードの説明などを書いておく「コメント文」と呼ばれるもので、 スクリプトの実行とは関係ないので、入力する必要はありません。

正しく前進したでしょうか? もし後進したなら、 positive_direction の指定を AとBで逆にしてください。

3秒間後進させましょう。

モータAとモータBを逆に回転させて、ロボットをその場で360度旋回させましょう。

モータの回転角とスピードの取得

モータの回転角を表示してみましょう。 モータの現在の回転角は angle() メソッドで得られます。 また値をコンソールに表示するには print() 関数を使います。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait

motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

print(motor_a.angle())  # 回転前の角度
  
motor_a.run(360)
motor_b.run(360)
wait(3000)
motor_a.stop()
motor_b.stop()

print(motor_a.angle())  # 回転後の角度

毎秒360度のスピードで3秒間回転したので、1080あたりの値になっていればOKです (慣性があるので正確に1080にはならないでしょう)。

モータの回転スピードは、 speed() メソッドで取得できます。 上記の例で、回転している途中のスピードを表示させてみましょう。

回転角をリセットするには reset_angle() メソッドを使います。 例えば、 motor_a の角度を0にリセットするには、 motor_a.reset_angle(0) とします。 3秒間前進したのち、モータAの角度を0にリセットし、 さらに3秒間後進させたのち、モータAの角度を表示させてみましょう。

時間や角度を指定してモータを回転させる

これまでの例では、 wait 関数を使って モータの回転時間を指定しました。 しかし、 Motor クラスには、時間や角度を指定して 回転させるメソッドが用意されています。 それらを使ってみましょう。

まずモータを1個だけ回転させます。 以下は、最初のプログラムと同じく、 モータAを毎秒360度のスピードで3秒間回転させる例です。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port
from pybricks.tools import wait

motor_a = Motor(Port.A)
motor_a.run_time(360, 3000)  # 360deg/sのスピードで3000ms回転

同様に角度を指定して回転させるには run_angle() を 使います。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port
from pybricks.tools import wait

motor_a = Motor(Port.A)
motor_a.run_angle(360, 1080)  # 360deg/sのスピードで1080度回転

この例は、1080度を毎秒360度のスピードで回転するので、 3秒間かかるはずです。

この例で、モータBも1080度回転させてみましょう。

実は、 run_time() メソッドや run_angle() は、 標準の設定ではモータの回転が終了するまで、次の処理に移行しません。 そのため、これらのメソッドを使ってAとBのモータを同時に回転させるには、 次のように、 wait オプションで False を指定する必要があります。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port
from pybricks.tools import wait

motor_a = Motor(Port.A)
motor_b = Motor(Port.B)
  
motor_a.run_angle(360, 1080, wait=False)
motor_b.run_angle(360, 1080)

実際にどのような動きになるか確認してみましょう。

上記の例で、モータBの回転にも wait=False を指定すると どのような動きになるでしょうか? (ヒント) 一番最後に、 print('Bye!') という行を追加してみましょう。

一辺が40cmの正三角形を描いて動くロボットのプログラムを作成しましょう。

一辺が30cmの正方形を描いて動くロボットのプログラムを作成しましょう。

一辺が24cmの星型を描いて動くロボットのプログラムを作成しましょう。

roboticsモジュールを使ってみよう

roboticsモジュールに含まれる DriveBase クラスを使えば、 左右のモータの回転を同時に制御できます。

DriveBaseクラスのインポートとインスタンス生成

まず、 robotics モジュールから DriveBase クラスをインポートしておきましょう。

DriveBase クラスからインスタンスを生成するためには、 左右のモータオブジェクト(インスタンス)に加え、 タイヤ径とトレッド幅(左右のタイヤの幅)の指定が必要です。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait
from pybricks.robotics import DriveBase  # DriveBaseをインポート
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)  # 左モータ
motor_b = Motor(Port.B)  # 右モータ

# DriveBaseのインスタンスを生成 (タイヤ直径が30mm, トレッド幅が58mmの場合)
robot = DriveBase(motor_a, motor_b, 30, 58)

ここで、タイヤの直径やトレッド幅は整数で入力する必要があるので、 実測の直径が30.4mmのタイヤの場合はとりあえず30mmにしておきましょう。 もし、この1%ほどの誤差が重要になる場合は、回転角やスピードなどを 補正すればよいでしょう。

ここまでのところで、間違いがないか、プログラムを実行して確認しておきましょう (ロボットは動きません)。

driveメソッドを使おう

では、 DriveBase クラスに用意されている drive メソッドを実際に使ってロボットを動かしてみましょう。 drive メソッドでは、前進速度(mm/s)と 単位時間あたりの旋回角度(deg/s)を指定します。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait
from pybricks.robotics import DriveBase
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

robot = DriveBase(motor_a, motor_b, 30, 58)

robot.drive(50, 0)  # 前進速度 50 mm/s, 旋回角速度 0 deg/s
wait(3000)
robot.stop()  # 停止

この例では、 秒速50mmで3秒間進むので、15cm進んで止まればOKです。

次に、前進速度を0にして、その場で旋回させてみましょう。

'... 以上省略 ...'
  
robot.drive(0, 120)  # 前進速度 0, 旋回角速度 120 deg/s
wait(3000)
robot.stop()  # 停止

ちょうど360度ロボットが旋回したでしょうか? もし、360度から大きくずれている場合は、トレッド幅を調整しておきましょう。

旋回角速度を毎秒45度にして、8秒間動かしてみましょう。 前進速度を毎秒0, 50mm, 100mmにして変化を観察しましょう。

上記の練習問題で旋回角速度を毎秒マイナス45度にしてみましょう。

前進角速度を毎秒80mmにして、旋回速度を毎秒45度、60度、90度、180度、360度と 変化させて、ちょうど360度旋回するようにしてみましょう。

straightメソッドとturnメソッドを使おう

Drivebase クラスには距離を指定してロボットを直進させる straight メソッドと旋回角度を指定してロボットをその場で 旋回させる turn メソッドも用意されています。 ただし、これらのメソッドを使うにはあらかじめ setting メソッドで前進速度(mm/s)・加速度(mm/s/s/)、旋回角速度(deg/s)・ 旋回角加速度(deg/s/s)を指定しておく必要があります。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait
from pybricks.robotics import DriveBase
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

robot = DriveBase(motor_a, motor_b, 30, 58)

# 速度 100mm/s, 加速度 400m/s/s, 角速度 180 deg/s, 角加速度 720 deg/s/s
robot.settings(100, 400, 180, 720)

robot.turn(360)      # 360度旋回
robot.straight(150)  # 150mm前進

360度旋回したのち、150mm前進したでしょうか?

200mm進んだのち、180度旋回して、もとの位置に戻ってくるプログラムを作成しましょう。

上記の練習問題で、前進の加速度(mm/s/s)を、100, 200, 400 と変更して試してみましょう。

1辺が20cmの三角形を描いて動くプログラムを作成しましょう。

1辺が15cmの正方形を描いて動くプログラムを作成しましょう。

前進距離と旋回角度を表示しよう

前進した距離と旋回した角度は、それぞれ distance メソッドと angle メソッドで取得できます。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait
from pybricks.robotics import DriveBase
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

robot = DriveBase(motor_a, motor_b, 30, 58)
robot.settings(100, 400, 180, 720)

robot.reset()            # 距離と角度をリセット

print(robot.distance())  # リセット直後の距離
robot.straight(150) 
print(robot.distance())  # 前進後の距離

コンソールに 0150 が表示されればOKです。

1辺が40cmの正三角形描いて動くプログラムを実行して、 最終的な距離と角度を表示させてみましょう。 旋回角度は angle メソッドで取得できます。

動作の繰り返し

同じような動作を繰り返す方法について説明します。 基本的には、 ある条件が満たされている間だけ動作を繰り返すwhile構文を使った方法と、 リストやタプルなどから要素を一つひとつ選んで動作を実行するfor構文を使った方法があります。

while構文を使った繰り返し

まず、 while 構文の簡単な例として、 コンソールに0から9までの数字を表示するスクリプトを作ってみましょう。

i = 0  # 変数i の初期値を0に設定

while i < 10:  # iが10未満の間だけ繰り返す
    print(i)      # 字下げに注意(4文字の空白を前に入れる)
    i += 1        # iの値を1増やす (i = i + 1 と同じ)

ここで重要な点がいくつかあります。

まず、C言語などと違ってPythonでは変数を使うときにあらかじめ宣言しておく必要はありません。 ある値を変数に代入した時点でその変数が定義され使えるようになります。 「 = 」は代入演算子とよばれ右辺の値を左辺の変数に代入するためのものです。 Pythonでは「変数」=「インスタンス」と考えてもかまいません。 以前説明したように、変数名には英数字や「 _ 」(アンダースコア)が使えますが、 最初の文字に数字を使うことはできません。 例えば、 xabc_xyzg13 のような 変数名は許されますが、 9xya-b のような変数名は使えません。

次に、while構文の最初の行の終わりの「 : 」に注意しましょう。 このコロンは、この行からwhile構文が続くと意味なので、忘れてはいけません。 iが10未満という条件については詳しい説明の必要はないでしょう。 そして、2行目以降は繰り返しをする実際の処理を記述しますが、 これらの行は字下げ(インデント)して書く必要があります。 Pythonでは、それぞれのブロックをC言語のように中括弧 { } で囲むことはせず、 この字下げで指定します。

ちなみにPEP8というPythonの標準ライブラリのコーディング規約によると、 4文字の半角スペースで字下げが推奨されています。 この授業でも、この方法を採用します(タブは使わないでください)。

最後の i += 1 は変数 i = i + 1 と同じ意味で 現在の i の値に1足した値を新たに i に代入する、 つまり i の値を1増やすという処理です。 これを繰り返して、 i が10になると、条件を満たさなくなるんので 繰り返し(ループ)が終了します。

上記のスクリプトを実行して、次のように0から9までの値が表示されればOKです。

0
1  
2
3
4
5
6
7
8
9

0から50までの3の倍数を順番に表示しましょう。

では、正方形を描いて動くプログラムを while を使って 書いてみましょう。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait
from pybricks.robotics import DriveBase
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

robot = DriveBase(motor_a, motor_b, 30, 58)
robot.settings(100, 400, 180, 720)

i = 0

while i < 4:
    robot.straight(150)   # 150mm前進
    robot.turn(90)        # 90度旋回
    i += 1

上記の例を1辺が5cmの正12角形を描いて動くプログラムに変更しましょう。

ここで、一周の合計の距離から正n角形の一辺の長さを計算できるように、 新たに ntotal_lengthedge_length という3個の 変数を導入しましょう。

'... 以上省略 ...'

n = 12                           # 正12角形の場合
total_length = 600               # 1周の長さ
edge_length = total_length // n  # 1辺の長さ (整数割り算「//」に注意)

i = 0

while i < 4:
    robot.straight(edge_length)
    robot.turn(30)
    i += 1

ここで注意してほしいのは、「 // 」という演算子です。 この演算子は割り算の商の整数部分を取り出す演算子です。 Pythonでは、通常の割り算は「 / 」という演算子で行いますが、 この演算は整数同士の割り算であっても結果は小数になります。 ところが、 straight メソッドの引数は整数(mm)で 指定する必要があります。 さらに、Movehubでは小数そのものがまだサポートされておらず、 通常の割り算「 / 」だけでなく小数を整数に変換する int 関数などが使えないので注意しましょう。

上記の例に、さらに turn_angle という変数を導入して、 n が与えられれば旋回角度も自動で計算するように しましょう。

上記の練習問題で n が3, 4, 5の場合も正しく動くか どうか確認しましょう。

for構文を使った繰り返し

次に for 構文を使った繰り返しについて説明しましょう。

for 構文では while 構文と違い、リストやタプルなどの いわゆる「反復可能オブジェクト(イテラブル)」の中から 要素を一つずつ取り出して、順次処理を実行していきます。

リストは0個以上のオブジェクトをコンマで区切って並べて [ ] で囲んだものです。 オブジェクトを追加・削除したり変更したりできます。 一方、タプルは0個以上のオブジェクトをコンマで区切って並べて ( ) で囲んだもので、大雑把に言えば、その要素となっている オブジェクトを変更したり追加・削除したりできません。 リストもタプルも違うタイプのオブジェクトを並べることが可能です。 例えば [0, 1, 'two', (3, 'gomashio')] のように 整数タイプ、文字列、タプル、リスト、その他どんなオブジェクトを 並べることリストやタプルを作ることも可能です。

リストやタプルのn番目の要素は、リストやタプルのあとに [n] をつければ取得できます。 例えば seasons = ['spring', 'summer', 'autumn', 'winter'] で定義した seasons というリストの(0番目から数えて)2番目の要素は seasons[2] で得られます(中身は 'autumn' )。

まずリストを使った例を試してみましょう。

for i in [0, 1, 2, 3]:   # i に 0, 1, 2, 3 を代入して繰り返す
    print(i)

ここでも最初の行の終わりにある「 : 」と 2行目の字下げに注意しましょう。

実行すると、

0
1
2
3

と表示されるはずです。

文字列を要素にもつリストについても試しておきましょう。

for i in ['apple', 'orange', 'banana', 'strawberry']:
    print(i)

実行してみてください。 それぞれの要素が表示されたでしょうか?

これは、次のように書いても同じ結果になります。

fruits =  ['apple', 'orange', 'banana', 'strawberry']  # リストを定義
  
for fruit in fruits:  # ループ変数はiでなくてもよい
    print(fruit)

もう一つ、便利な方法として range 関数を使った方法も 紹介しておきましょう。

for i in range(0, 10):  # i  0, 1, 2,.., 9 を代入して繰り返す
    print(i)

range(10) と指定したとき10は含まれないことに注意してください。 また、この例では最初の0を省略して range(10) と書いても 同じ結果が得られます。

上記の例で、 range(0, 20, 2) に変更してみると結果はどうなるでしょうか。

さて、 range() 関数で得られる数値の並びは、そのままでは print() 関数で表示されませんが、リストやタプルに変換すると表示できます。

  print(list(range(10)))  # list関数でlist型に変換

とすると [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] が表示されるはずです(試してください)。

上記の list() のかわりに tupple() を使い、 range型をタプルに変換して表示してみましょう。

では、上記のwhile構文で作ったプログラムをfor構文に書き直してみましょう。 最初から省略せずに書いてみます。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait
from pybricks.robotics import DriveBase
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

robot = DriveBase(motor_a, motor_b, 30, 58)
robot.settings(100, 400, 180, 720)

n = 4
total_length = 600

edge_length = total_length // n
turn_angle = 360 // n             # 頂点での旋回角度

for i in range(n):
    robot.straight(edge_length)
    robot.turn(turn_angle)

プログラムを実行してみて、while構文と同じ結果が得られたらOKです。

上記の例で、 range(n)['apple', 'orange', 'banana', 'strawberry'] というリストに変更して、 プログラムを実行してみましょう。

関数を使おう

一連の動作を「関数」として定義しておくと便利です。 これまでもいくつかの関数を紹介しましたが、 ここでは関数を自作します。 最終的には、 n を与えたときに、どのような n の場合でも正n角形を描いて動いてくれるような プログラムの作成が目標です。

簡単な例

簡単な例から始めましょう。 まずは、「Hello」と表示するだけの関数です。

def greeting():       # greetingとういう名前の関数を定義
    print('Hello!')   # 実際の処理


greeting()            # 関数の呼び出し(括弧を忘れないように)

実行して、「 Hello 」と表示されればOKです。

上記の例で関数を呼び出している最後の行 greeting() を 3行コピーして実行してみましょう。

次に引数(ひきすう)を取る関数を定義してみましょう。 この例では name という名前でパラメータ(仮の引数)を 1個用意しています。

def greeting(name):   # nameというパラメータを用意
    print('Hello,' + name + '!')   # 文字列をつなぎ合わせて表示


greeting('Taro')

実行してみて「Hello, Taro!」と表示されればOKです。

上記の例で 'Taro' を自分の名前に変更して実行してみましょう。 文字列については日本語(ただしUTF-8)でも大丈夫です。

これまでの例では、文字列を表示するだけで、値を返さない関数でしたが、 何らかの値を返す関数の例も紹介しておきましょう。

def square(x):      # ある整数の2乗を返す関数
    return x * x    # 「*」は乗算の記号


print(square(2))
print(square(3))
print(square(5))

この例では引数として渡した数の2乗を return 文で返します。 つまり、 square(2) というふうに関数を呼び出すと 4 という結果が得られます。

上記の例を変更して、1から10までの整数の2乗を計算して表示するようにしましょう。

正方形を描いて動く関数を定義しよう

では、実際にロボットを動かす関数を作りましょう。 ロボットを動かすことに関してはすでに慣れているはずですので、 特に難しくはないでしょう。

まずは「正方形」を描いて動くプログラムから作成します。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait
from pybricks.robotics import DriveBase
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

robot = DriveBase(motor_a, motor_b, 30, 58)
robot.settings(100, 400, 180, 720)


def draw_polygon():      # 関数の宣言

    n = 4                # 頂点の数(正方形の場合)
    total_length = 400   # 辺の合計の長さ
    edge_length = total_length // n  # 1辺の長さ
    turn_angle = 360 // n            # 頂点での旋回角

    for i in range(n):
        robot.straight(edge_length)
        robot.turn(turn_angle)


draw_polygon()  # 関数の呼び出し(括弧を忘れないように)

上記の例で n = 8 の場合も確認しましょう。

正n角形を描く関数を定義しよう

正方形の場合の動作が確認できれば、 一般の n の場合でも使えるように 関数を改良しましょう。 これは、 n = 4 と代入する代わりに、 関数定義のときに n をパラメータとして 宣言しておくだけです。

'... 以上省略 ...'

def draw_polygon(n):  # パラメータ n を用意

    total_length = 400
    edge_length = total_length // n
    turn_angle = 360 // n

    for i in range(n):
        robot.straight(edge_length)
        robot.turn(turn_angle)

draw_polygon(4)       # 引数で関数に値を渡す

正方形を描いて1秒停止、さらに続けて正六角形描いて1秒停止、さらに続けて正八角形を続けて描くように上記のプログラムを変更しましょう。

パラメータのデフォルト値の設定とキーワード引数

上記の例で、 total_length も変更できるようにしましょう。

'... 以上省略 ...'

def draw_polygon(n, total_length):  # total_lengthもパラメータに

    edge_length = total_length // n
    turn_angle = 360 // n

    for i in range(n):
        robot.straight(edge_length)
        robot.turn(turn_angle)

draw_polygon(4, 400)

上記の例で1周の長さを600mmにして実行してみましょう。

さて、この例では2個の引数を指定して draw_polygon を呼び出しましたが、 total_lenght を省略した場合は、デフォルト値として 400 が が設定されるようにしてみましょう。 これは単にパラメータ宣言のときに total_length の代わりに total_length=400 と書いておくだけです。 関数内部の変更は必要ありません。

'... 以上省略 ...'

def draw_polygon(n, total_length=400):  # total_lengthのデフォルト値を400に

    '... 途中省略 ...'


draw_polygon(4, 300)   # total_length=300
wait(1000)
draw_polygon(4)        # total_lengthを省略するとデフォルト値の400が使われる

実行して、プログラムを確認しましょう。

ここで注意すべき点は、デフォルト値のあるパラメータは、 デフォルト値のないパラメータよりも後ろで 宣言する必要がある、ということです。

上記の例で、 n にもデフォルト値として4を設定し、 引数なしで関数を呼び出してみましょう。

上記の例で、 def draw_polygon(n=4, total_length) のように n だけにデフォルト値を設定すると、どのようなエラーが表示されるか 確認しましょう。またエラーにならないようにするにはどうすればよいでしょうか。

ところで、上記の例では関数を呼び出すときに、 4300 という 2つの引数を指定しましたが、関数で使われているパラメータ名を使って次のように 「キーワード引数」として指定することもできます。

'... 以上省略 ...'

draw_polygon(n=4, total_length=300)

キーワード引数は、次のように順序を入れ替えて使うこともできます。

'... 以上省略 ...'

draw_polygon(total_length=300, n=4)

実際にプログラムを実行して、同じ動きになるか確認しましょう。

このようにキーワード引数は、順序を入れ替えることもできます。 これに対し、キーワードなしの引数は「位置引数」と呼ばれ、順序が大切です。 では、位置引数とキーワード引数が混在した場合はどうでしょうか? この場合は、位置引数を必ずキーワード引数の前に指定する必要があります。

draw_polygon(4, total_length=300)  # OK
draw_polygon(total_length=300, 4)  # エラー

上記の例を実行してみてどのようなエラーが表示されるか確認しましょう。

時間を測ろう

toolsモジュールに含まれるストップウォッチ機能を使ってみましょう。 簡単な例として一定の距離を進むのにかかった時間を計測してみましょう。 StopWatch クラスには、時間をリセットするための reset() メソッドやその時の時刻を取得するための time() メソッドが用意されています。 StopWatch クラスを利用するには Motor クラスなどと同様に インスタンスをまず生成します。

一定の距離を進むのにかかる時間を測定しよう

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.robotics import DriveBase
from pybricks.tools import wait, StopWatch  # StopWatchをインポート
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

robot = DriveBase(motor_a, motor_b, 30, 58)
robot.settings(100, 50, 180, 720)  # 速度100mm/s, 加速度50mm/s/s
watch = StopWatch()  # StopWatchクラスのインスタンスを生成

watch.reset()        # ストップウォッチをリセット
robot.straight(200)  # 200mm前進
print(watch.time())  # 止まった直後の時刻を表示

この例では、 速度が100mm/sになるまで加速度で50mm/s/sで加速し、 止まるときは加速度がマイナス50mm/s/sで減速するので、 200mm進むためには4秒かかるはずです。 実際に、プログラムを実行してストップウォッチで計測した時間と 比較してみましょう。

上記の例で、加速度はそのままにして速度を100mm/sから200mm/s に上げると200 mm進むのに何秒かかるでしょうか? 予想した上でプログラムを実行してみましょう。

上記の例で、速度は100mm/sのままsにして加速度を50mm/s/sから100mm/s/sに上げると、 200mm進むのに何秒かかるでしょうか? 予想した上でプログラムを実行してみましょう。

上記の例で、速度は100mm/sのままsにして加速度をさらに上げていくと、 所要時間は2秒に近づいていくことを確認しましょう。 ただし、実際の加速度は有限なので、2秒ちょうどにはなりません。

一定の時間だけロボットを動かそう

ロボットを前進させて、一定の時間が経ったらロボットを停止させるのに、 これまでは wait 関数を使ってきました。 しかし、 wait 関数は、一定の時間だけ処理を中断するための関数なので、 その間は他の処理を実行できません。 ストップウォッチを使うことで、処理を中断することなく同様の動作が可能です。

まず、以下の例は wait 関数と同じような機能を StopWatch を使って書いたものです。

from pybricks.pupdevices import Motor
from pybricks.parameters import Port, Direction
from pybricks.robotics import DriveBase
from pybricks.tools import wait, StopWatch

motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

robot = DriveBase(motor_a, motor_b, 30, 58)
watch = StopWatch()

watch.reset()
robot.drive(100, 0)  # 100mm/sの速度で前進

while watch.time() < 2000:  # 2秒未満のとき繰り返し
    pass  # 何も実行しないで素通り

robot.stop()
print(watch.time())  # 停止した時間を表示

ここでは、 pass という命令に注意しましょう。 C言語ではwhile構文の処理内容が空であっても文法的に間違いではないのですが、 Pythonでは空のブロックは許されていません。 つまり、何らか命令を書いておく必要があります。 そのときによく使われるのが、素通りするだけの命令 pass です。

実際に実行してみましょう。 2秒間前進して止まったでしょうか。

さて、上記の例では pass の代わりに continue という命令を使うこともできます。 continuepass とは違って、 continue 以下の処理は行わず while ループの最初に戻る、という命令です。 ただし、上の例の場合は pass と同じ動きになります。 実際に確認しておきましょう。

'... 以上省略 ...'
  
while watch.time() < 2000:
    continue  # ループの最初に戻る

'... 以下省略 ...'
  

上記の例で、while構文の中で、 continue 文に続いて wait(3000) という命令を追加すると、どのような動きになるでしょうか?

上記の例で、 continue ではなく pass のあとに wait(3000) という命令を追加すると、どのような動きになるでしょうか?

pauseとresume

ストップウォッチを一時停止するためには pause メソッドを、 再スタートさせるために resume メソッドを使います。

一辺が15cmの正方形を描くプログラムを実行し、 その際、前進した時間の合計を表示しましょう。 ただし、旋回している時間は除くものとします。

ところで pause をしている状態で reset メソッドを 実行すると、そのまま(つまり時刻0で) pause している状態になります。 一方 resume した状態で reset メソッドを 実行すると、時刻0から直ちに測定を開始します(ストップウォッチが動き出します)。

カラー距離センサを使おう

カラー距離センサを使うと、一つのセンサで、色、明るさ、距離が測定できます。 詳しくは公式ドキュメント https://docs.pybricks.com/en/stable/pupdevices/colordistancesensor.html を参考にしてください。

以下では、これらの値が正しく取得できるかどうか確認します。 まず、カラー距離センサをMove Hubに接続しましょう。 以下では、ポートDに接続した例で説明しますが、 ポートCに接続した場合も同様です。

色を調べよう (1)

色を調べる前に、観測できる色の一覧を表示してみましょう。

from pybricks.pupdevices import ColorDistanceSensor  # センサクラスのインポート
from pybricks.parameters import Port
from pybricks.tools import wait

sensor = ColorDistanceSensor(Port.D)  # ポートDに接続した場合

print(sensor.detectable_colors())     # 観測可能な色を表示

以下のようにタプルで色が表示されればOKです。

(Color.RED, Color.YELLOW, Color.GREEN, Color.BLUE, Color.WHITE, Color.NONE)

次に、いろいろな物体にセンサを近づけ、その色を観測してみましょう。

from pybricks.pupdevices import ColorDistanceSensor
from pybricks.parameters import Port
from pybricks.tools import wait

sensor = ColorDistanceSensor(Port.D)

while True:
    print(sensor.color())  # 色を表示
    wait(500)    # 0.5秒おきに表示

上記の例を実行し、どのくらい正確に色を観測できるか確認しましょう。

色を調べよう (2)

上のように color メソッドは、カラー距離センサで 色を観測した色に近い登録された色を表示します。 しかし、数値的に色を表示する hsv メソッドも 用意されています。

色を数値的に表現する際にRGB空間がよく用いられますが、 これ以外にもいろいろな表し方があります。 HSV空間は色相(Hue)、彩度(Saturation)、明度(Value) を使った表現で、明るさがいろいろ変わっても色を判別しやすいのが 特徴です。 色相(Hue)は0〜360度で、0度(赤)、60度(黄)、120度(緑)、180度(シアン)、240度(青)、300度(マゼンタ)となっています。 (参考 wikipedia https://ja.wikipedia.org/wiki/%E8%89%B2%E7%9B%B8)

一方、彩度はその名の通り色の鮮やかさ、明度は明るさを示す量で、それぞれ0〜100の値が得られます。

HSVの値を表示するだけの簡単なプログラムを実行してみましょう。

from pybricks.pupdevices import ColorDistanceSensor
from pybricks.parameters import Port
from pybricks.tools import wait

sensor = ColorDistanceSensor(Port.D)

while True:
    print(sensor.hsv())  # 測定した色をHSVで表示
    wait(500)

上記の例を実行し、いろいろな物体の色を測定したときに、 それぞれの値がどのようなものになるのか調べてみましょう。

反射光の明るさを測ろう

カラー距離センサには三色のLEDが装備されています。 それらを光らせ、その反射光の明るさを測定しましょう。

from pybricks.pupdevices import ColorDistanceSensor
from pybricks.parameters import Port
from pybricks.tools import wait

sensor = ColorDistanceSensor(Port.D)

while True:
    print(sensor.reflection())  # 反射光の明るさを表示
    wait(500)

白や表面に光沢のある物体では反射光は明るくなり、 逆に黒い物体は暗くなります。 また、一般に物体に近づけば反射光は明るくなり、遠ざかれば反射光は弱くなります。

いろいろな色の反射光の明るさを調べてみましょう。 その際、表面とカラー距離センサの距離にも注意しましょう。

後のセクションで反射光を測定しながら黒い線に沿ってロボットを動かすライントレースを扱いますが、 その前に反射光の測定を使った簡単な例を試しておきましょう。

以下の例は、白い紙の上でロボット前進させ、黒い線に到達したら停止する、という 簡単なプログラム例です。センサは地面の方向に向けて装着します。 おそらく、明るい色の机の上でロボットを前進させて端まで来たら 停止させる、という動作もこのプログラムでできるでしょう。

from pybricks.pupdevices import Motor
from pybricks.robotics import DriveBase
from pybricks.parameters import Port, Direction
from pybricks.pupdevices import ColorDistanceSensor
from pybricks.tools import wait
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)
robot = DriveBase(motor_a, motor_b, 30, 58)

sensor = ColorDistanceSensor(Port.D)

robot.drive(30, 0)  # 毎秒30mmの速度で前進

while sensor.reflection() > 15:   # しきい値の15は適切に調整する
    pass

robot.stop()

上記の例で、動き始める前と停止したときの明るさを コンソールに表示させましょう。

上記の例を改良して、スタートしてから黒い線(あるいは机の端)に到達する までの時間を表示させてみましょう。

上記の練習問題を改良して、 黒い線あるいは机の端に到達したら、 1秒間停止して、そのままバックしてスタート位置まで 戻るようにプログラムを変更してみましょう。 (ヒント) スタートしてから黒い線に達するまでの 時間を測定し、それと同じ時間だけバックさせる。

上記の練習問題は、時間を測定してもとの位置に戻りましたが、 distance() メソッドを使ってロボットが進んだ距離を測り、 それと同じ距離だけバックさせる方法で、もとの位置に戻る プログラムを作ってみましょう。

黒い線あるいは机の端に到達したらUターンし、 その動作を繰り返すようなプログラムを作成しましょう。

環境光の明るさを測ろう

環境光(周囲の明るさ)を測定したい場合は ambient() メソッドを使います。

from pybricks.pupdevices import ColorDistanceSensor
from pybricks.parameters import Port
from pybricks.tools import wait

sensor = ColorDistanceSensor(Port.D)

while True:
    print(sensor.ambient())
    wait(500)

上記のプログラムを実行し、いろいろな方向の環境光の明るさを測定してみましょう。

距離を測ろう

NXTやEV3に含まれている超音波センサほどは精度良く測定できませんが、 カラー距離センサを使えば、おおまかな距離を測定することができます。 次のプログラムを実行してみましょう。

from pybricks.pupdevices import ColorDistanceSensor
from pybricks.parameters import Port
from pybricks.tools import wait

sensor = ColorDistanceSensor(Port.D)

while True:
    print(sensor.distance())   # 0〜100の間の値が表示される(10きざみ)
    wait(500)

物体との距離が離れれば値が大ききくなり、接近すれば値が小さくなります。 手元で試したところ、白い紙との距離の場合、 約13cmで50、約27cmで100になりました。 また、黒い紙の場合、約4cmで50、約9cmで100という値になりました。

上記のプログラムを実行し、物体との距離をいろいろと変えて、 測定値がどのように変化するか確認しましょう。 また、いろいろな物体までの距離を測定し、色や形状によって どのように値が変化するか観察しましょう。

物体の近くまで前進し、近づいたら停止するプログラムを作りましょう。

物体の近くまで前進し、近づいたら停止してもとの位置までバックするプログラムを作りましょう。

物体の近くまで前進し、近づいたらUターンして前進を繰り返すプログラムを作りましょう。

机の縁に沿って動くロボットを作ろう

カラー距離センサをロボットの左前方に装着し、机の上かそうでないかを 検知して、ロボットが机の縁に沿って動くようにしてみましょう。

何らかの条件によって違う処理を行いたい場合は、 if 構文を使用します。 以下では、 if ... else ... 構文を使用して、 反射光の値が10より大きい場合は反時計回りに旋回、 そうでない場合、つまり反射光の値が10以下のときは 時計回りに旋回させています。 つまり、ロボットは左折と右折を繰り返しながら前に進んでいきます。

from pybricks.pupdevices import Motor, ColorDistanceSensor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait
from pybricks.robotics import DriveBase
  
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)

robot = DriveBase(motor_a, motor_b, 30, 58)
sensor = ColorDistanceSensor(Port.D)

while True:
    intensity = sensor.reflection()   # 反射光の値を変数intensityに代入
    
    if intensity > 10:          # intensityが10より大きいとき
        robot.drive(40, -180)      # 毎秒40mmで前進、反時計回りに毎秒180度で旋回

    else:                       # それ以外のとき(つまりintensityが10以下のとき)
        robot.drive(40, 180)       # 毎秒40mmで前進、時計回りに毎秒180度で旋回
        
    wait(5)  # わずかな時間だけ処理を休止

しきい値の 10 という値は、 机の表面の色やセンサと距離によっても変わってきます。 とりあえず、机の上の値と机から外れたときの値を測定して、 その中間くらいにしておきましょう。

速度や旋回の速さも変更して試してみましょう。

上記の例では、反射光の明るさに応じて2段階でロボットの動きを変化させました。 これを少し改良して、明るさが一定の範囲にあるときは旋回せずに直進させるようにしましょう。 つまり、明るさを3段階で判断してみましょう。 この場合は、 if ... elif ... else 構文を使います。

'... 以上省略 ...'

while True:
    intensity = sensor.reflection()

    if intensity > 12:
        robot.drive(40, -180)

    elif intensity > 8:   # 場合分けの順序に注意
        robot.drive(40, 0)

    else:
        robot.drive(40, 180)

    wait(5)

境界の値は適宜調整してください。

この if ... elif ... else 構文では、 最初の if 節の条件を判断して、Falseなら 次の elif 節の処理に移ります。 もし最初の if 節の条件がTrueなら、 elif 節や else 節は実行されないことに注意しましょう。

さらに上記の練習問題を改良して、右急旋回、右旋回、直進、左旋回、左急旋回の 5段階でロボットを動かしてみましょう。 5段階にすることで、その場で旋回(速度0)させることもできます (3段階でも可能ですが、そうすると前進するのが極端に遅くなります)。

明るさの目標値からのズレと比例した旋回の割合いにするにはどうすればよいでしょうか?

ライントレース・ロボットを作ろう (1)

簡単なクラスを作ろう

これまで MotorDriveBase などの いろいろなクラスを使ってきましたが、ここではクラスを自作してみましょう。

クラスはインスタンスを生成するための雛形になるものです。

まず、簡単な例として、 Hello, World! と表示するだけの hello という名の関数(メソッド)を もつクラスを MyClass という名前で作ってみましょう。 Pythonのクラス名は先頭だけ大文字にして単語をつなげたものが推奨されています。

ちなみに通常の関数であれば次のように def 構文を使って簡単に定義できます。

def hello():    # 関数の定義
    print('Hello, World!')


hello()         # 関数の呼び出し(Hello, World!)と表示される

実行して動作するか確認しておきましょう。

ではこの関数を MyClass クラスの関数(メソッド)として定義してみましょう。

class MyClass:            # classキーワードでクラスを定義
    
    def hello(self):      # クラス関数(メソッドを定義)、 selfというパラメータに注意
        print('Hello, World!')

入力したら実行してみましょう。何も(エラーも)表示されず終了すればOKです。

上のプログラムは、単にクラスを定義しただけなので、何も表示されません。 では、 MyClass からインスタンスを生成して、 hello メソッドを実行してみましょう。

class MyClass:
    
    def hello(self):
        print('Hello, World!')


x = MyClass()    # MyClassのインスタンスを生成
x.hello()        # helloメソッドを呼び出し

メソッドを呼び出すには、これまで学習してきたのと同様に インスタンス名のあとに「 . 」に続けてメソッド名を書きます。 もちろんメソッドを実行する(呼び出す)ので「 () 」を忘れてはいけません。

Hello, World! 」と表示されたでしょうか。

ここで、パラメータ self について説明しておきます。 このパラメータの名は nvar1 など 別のものでもかまわないのですが、慣習的に self を用います。 では、これは何を表すパラメータなのかというと、 実は「インスタンスそのもの」を表すパラメータです。

上の例では、インスタンス名を使って x.hello() のようにメソッドを呼び出しましたが、 次のように、クラス名そのものを使ってメソッドを呼び 出すことができます。

x = MyClass()      # xという名でインスタンスを生成
MyClass.hello(x)   # クラス名でメソッドを呼び出すときは
                   # インスタンスを引数として渡す

このように書くと、上の self というパラメータの 存在意味が理解できるのではないでしょうか。 このようなメソッドはインスタンス関数と呼ばれ、 定義する際の最初のパラメータにはインスタンスそのものが 入ります。

クラス名でメソッド呼び出して同じ結果が得られることを確認しましょう。

引数のあるメソッドも作ってみましょう。

class MyClass:
    
    def hello(self, name):
        print('Hello, ' + name + '!')


x = MyClass()
x.hello('Taro')   # 引数を指定してhelloメソッドを呼び出し

上記のプログラムを実行して動作を確認しましょう。

LineFollowerクラスを作ろう

では、ライントレースをするロボットのクラスを LineFollower という名前で作ってみましょう。 上の例で、 MyClassLineFollower に変更しましょう。

実際にロボットを動かす前に、バッテリーの残量を表示する機能 を実装してみましょう。 バッテリの電圧は MoveHub クラスの持つ battery オブジェクトの voltage() 関数で得られます。

from pybricks.hubs import MoveHub  # MoveHubクラスをインポート

hub = MoveHub()   # MoveHubのインスタンス生成

class LineFollower:

    def show_voltage(self):    # パラメータselfを忘れないように
        print(hub.battery.voltage())   # バッテリ残量を表示


robot = LineFollower()   # LineFollowerのインスタンスの生成
robot.show_voltage()     # show_voltageを呼び出し

実行してプログラムに間違いがないか確認しましょう。

このプログラムでは hub という変数(インスタンス)は クラス定義の外で定義されているので、グローバル変数(大域変数)と呼ばれます。 グローバル変数はどのようなクラスや関数からも参照できます。 ちなみに、変数が有効な範囲のことを「スコープ」と呼びますが、 プログラムの修正や改良が容易にできるように、 数スコープを狭くしておくことが推奨されています。 そこで、上のプログラムを修正して、hubという変数を クラス内で定義して、インスタンス変数に変更してみましょう。

from pybricks.hubs import MoveHub


class LineFollower:

    def show_voltage(self):
        self.hub = MoveHub()   # hubをインスタンス変数として定義する
        print(self.hub.battery.voltage())


robot = LineFollower()
robot.show_voltage()

コンソールに電圧が表示されることを確認しましょう。

ここでも、 show_voltage メソッドの中で定義した 変数 self.hub に注意しましょう。 これは、インスタンス変数と呼ばれ、上の例では生成した robot という名をインスタンスを使って、 robot.hub でアクセスできます。

上記の hub.battery.voltage() を書式付きの文字列 '{} mV'.format(self.hub.battery.voltage()) に変更して電圧を表示してみましょう。 この書式付きの文字列の {} には self.hub.battery.voltage() で得られた値(整数, int型)が入ります。 複数の値を指定したい場合は、 format の中に「 , 」 で区切って書き、「 {0} 」「 {1} 」「 {2} 」 のように {} の中に順番の数字を指定します。

さて、上の例では、電圧を表示するのに show_voltage というメソッドを 呼び出す必要がありました。 これを少し改良して、インスタンスを生成したときに自動的に 電圧を表示するようにしてみましょう。

これは、インスタンスの生成時に自動的に実行される __init__ という特殊な名前のメソッドを 定義しておけばよいだけです。 この特殊なメソッドのことを、初期化関数と言ったりコンストラクタと いうこともあります。

では上で定義した show_voltage というメソッド名前を __init__ に変更しましょう。

from pybricks.hubs import MoveHub


class LineFollower:

    def __init__(self):      # パラメータselfを忘れないように
        self.hub = MoveHub()
        print('{} mV'.format(self.hub.battery.voltage()))


robot = LineFollower()    # インスタンスの生成時に__init__メソッドが
                          # 自動的に実行される

この例では、 __init__ メソッドを明示的に呼び出して いませんが、インスタンスを生成した時点、つまり、 robot = LineFollower() を実行した時点で 自動的にこのメソッドが呼び出されることに注意しましょう。

ところで、次のように hubself を つけないとインスタンス変数にはなりません。 この場合は、 __init__ メソッドが呼び出されたとき だけ存在する、メソッド内の変数になります。 つまり、インスタンスからは直接アクセスできない変数となります。

'... 以上省略 ...'
  
class LineFollower:

    def __init__(self):
        hub = MoveHub()
        print('{} mV'.format(hub.battery.voltage()))

'... 以下省略 ...'

上の例を実行し、バッテリの残量が表示されるどうかか確認しましょう。

MoveHub と同様にカラー距離センサのインスタンスを __init__ メソッドの中で定義し、 LineFollower のインスタンス生成時に そのときの反射光の値を表示しましょう。

DriveBaseクラスの継承

さて、自作のクラス LineFollower にロボットを 前後に動かしたり旋回させたりする機能を自分で実装することも 可能ですが、 そのような機能はすでに DriveBase クラスに 用意されています。 そこで、 DriveBase クラスの機能を LineFollower クラスでも使えるようにしましょう。 そのためには、 LineFollower クラスを作るときに DriveBase クラスを「継承」します。

この場合、継承される側の DriveBase クラスを「親クラス」 継承する側の LineFollower クラスを「子クラス」とよびます。

必要となりそうなモジュールをインポートしておく

クラスを継承するには、基本的にはクラス宣言文のクラス名の あとの ( ) に親クラスを指定するだけです。

あらかじめ今後必要になるクラスなどもインポートして、 DriveBase クラスを継承した LineFollower クラスを定義しましょう。

from pybricks.hubs import MoveHub
from pybricks.pupdevices import Motor, ColorDistanceSensor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait, StopWatch
from pybricks.robotics import DriveBase


class LineFollower(DriveBase):   # DriveBaseクラスを継承

    def __init__():
        self.hub = MoveHub()
        self.sensor = ColorDistanceSensor(Port.D)  # センサのインスタンス生成
        print('{} mV'.format(self.hub.battery.voltage()))

親クラスの初期化メソッドの呼び出し

DriveBase クラスのインスタンスを生成する際には 左右のモータのインスタンスやタイヤ径、トレッド幅などを指定 する必要がありました。 つまり、

motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)
robot = DriveBase(motor_a, motor_b, 30, 58)

と同様に

motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)
robot = LineFollower(motor_a, motor_b, 30, 58)

のように指定できるようにするにはどうすればよいでしょうか?

実は、インタスタンスを生成する際に指定した (motor_a, motor_b, 30, 58) という引数は、初期化関数 __init__ にそのまま 渡されます。 つまり robot = DriveBase(motor_a, motor_b, 30, 58) を実行すると、 インスタンス生成時に __init__(motor_a, motor_b, 30, 58) が実行されます。

したがって、 robot = LineFollower(motor_a, motor_b, 30, 58) で指定された引数をそのまま親クラス DriveBase の初期化関数 __init__ に渡してあげればよい、 というのがわかります。

これらの引数を受け取るために、 __init__ 関数の定義でパラメータを用意しましょう。

'... 以上省略 ....'

class LineFollower(DriveBase):

    def __init__(self, left_motor, right_motor, wheel_diameter, axle_track)  # パラメータを用意

        # 親クラスの初期化関数を呼び出す(親クラスはsuper()で参照できる)
        super().__init__(left_motor, right_motor, wheel_diameter, axle_track)
        self.hub = MoveHub()
        self.sensor = ColorDistanceSensor(Port.D)
        print('{} mV'.format(self.hub.battery.voltage()))


motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)
robot = LineFollower(motor_a, motor_b, 30, 58)   # DriveBaseクラスと同じ引数でインスタンス生成

robot.drive(30, 0)   # DriveBaseクラスのdriveメソッドがそのまま使えるか確認
wait(3000)
robot.stop()         # DriveBaseクラスのstopメソッドもそのまま使える

上記の例では、すべてのパラメータを明示しましたが、 次のように可変長の位置パラメータとキーワードパラメータを 使って簡略に指定することもできます。

'... 以上省略 ....'

class LineFollower(DriveBase):

    def __init__(self, *args, **kwargs):    # すべての引数は *args と **kwargs に渡される
        super().__init__(*args, **kwargs)   # 親クラスの初期化関数を呼び出す
        self.hub = MoveHub()
        self.sensor = ColorDistanceSensor(Port.D)
        print('{} mV'.format(self.hub.battery.voltage()))

'... 以下省略 ....'

明るさを判断してスピードと旋回をコントロール

いよいよ本題です。 ライントレースをするメソッドを run という名前で作成しましょう。

以下作成中

from pybricks.hubs import MoveHub
from pybricks.pupdevices import Motor, ColorDistanceSensor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait, StopWatch
from pybricks.robotics import DriveBase

class LineFollower(DriveBase):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.hub = MoveHub()
        self.sensor = ColorDistanceSensor(Port.D)
        print('{} mV'.format(self.hub.battery.voltage()))

    def run(self):    # パラメータselfを忘れないように
        while True:
            intensity = self.sensor.reflection()
            if intensity > 10: 
                self.drive(40, -90)
            else:
                self.drive(40, 90)
            wait(2)


motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)
robot = LineFollower(motor_a, motor_b, 30, 58)

robot.run()   # ライントレースを開始

ライントレース・ロボットを作ろう (2)

交差点における一時停止や右左折、直進もできるように プログラムを改良しましょう。 以下、前回の復習も兼ねて最初からプログラムを作っていきます。

各ステップごとに確実にプログラムが動作するか確認してください。 また、確実に動いたのを確認したら、ステップごとに line1.py, line2.py, .... のように別のファイルに保存しておくと よいでしょう。

プログラムの骨格を作る

使用するモジュールのインポート

まず、あらかじめ使用するクラスや関数などをインポートしておきましょう。

# 必要なモジュールをインポート

from pybricks.hubs import MoveHub
from pybricks.pupdevices import Motor, ColorDistanceSensor
from pybricks.parameters import Port, Direction
from pybricks.robotics import DriveBase
from pybricks.tools import wait, StopWatch

綴りの間違いがないか、プログラムを走らせて確認しましょう。 間違いがなければ何も表示されずにすぐにプログラムが終了するはずです。

LineFollowerクラスを作る

次にDriveBaseクラスを継承したLineFollowerクラスを定義して、 正しくインスタンスが生成されるか確認します。

# 必要なモジュールをインポート

... 途中省略 ...


# LineFollowerクラスを定義

class LineFollower(DriveBase):    # DriveBaseクラスを継承

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)   # 親クラスの初期化関数の呼び出し
        self.hub = MoveHub()   # MoveHubのインスタンス生成(バッテリ電圧表示のため)
        self.sensor = ColorDistanceSensor(Port.D)  # センサのインスタンス生成
        print('{} mV'.format(self.hub.battery.voltage()))  # バッテリ電圧を表示する


# 以下mainプログラム

motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)
robot = LineFollower(motor_a, motor_b, 30, 58)   # LineFollowerクラスのインスタンス生成

この例では、 LineFollower クラスのインスタンス変数として 初期化メソッドの中で hub という名前でハブ本体の、 sensor という名前でカラー距離センサを定義しました。 つまり LineFollower クラスのインスタンスを例えば robot という名前で生成すれば robot.hubrobot.sensor でハブやカラー距離センサのインスタンスが参照できます。

プログラムを実行してエラーが出ないか確認しましょう。

上記の例で作成した LineFollower クラスの インスタンス robot を使って、 実際に、DriveBaseクラスのメソッドである drivestop が実行できるか確認しましょう。 例えば robot.drive(40, 0)robot.drive(0, 180)robot.stop() などのメソッドをmain部分に追加して実行してみましょう。

とりあえずメソッドを定義しておく

いろいろなコースを走行できるように、直近の交差点までライントレースを するメソッド( run )と交差点で右左折・直進するメソッド ( turn_right, turn_left, cross_line )を定義します。 ただし中身は後ほど実装するので、以下ではメソッドが正しく 呼ばれるかどうか print 関数を使って表示するだけに しておきます。

ライントレースのメソッドについては、 ライントレースをする最短時間( min_runtime )、 最長時間( max_runtime )、 さらに交差点と判断するために連続で黒になっても走行を継続する 限界の時間( max_duration )を引数に取れるようにし、 それぞれのデフォルト値用意しておきます。 ここでデフォルト値とは、メソッドの呼び出し時(実行時)に、 特に引数で値を指定しなければ使用される初期設定の値のことです。

'... 以上省略 ...'

class LineFollower(DriveBase):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.hub = MoveHub()
        self.sensor = ColorDistanceSensor(Port.D)
        print('{} mV'.format(self.hub.battery.voltage()))

    def run(self, min_runtime=1000, max_runtime=30000, max_duration=150):
        '''
        最初の交差点までライントレースをするメソッド

          min_runtime: スタート後、最低でもこの時間はライントレースを継続
          max_runtime: ライントレースをする最大時間(経過すれば停止する)
          max_duration: この時間継続して黒(一定値以下)なら交差点と判断する
        '''
        print('following the line ...')

    def turn_left(self):
        '''交差点を左折'''
        print('turning to the left ...')

    def turn_right(self):
        '''交差点を右折'''
        print('turning to the right ...')

    def cross_line(self):
        '''交差点を直進'''
        print('crossing to the line ...')


# mainプログラム

motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)
robot = LineFollower(motor_a, motor_b, 30, 58)

robot.run()         # メソッド呼び出して確認
robot.turn_left()
robot.turn_right()
robot.cross_line()

上記の run メソッドの宣言文の直後の """""" に囲まれた部分は docstringと呼ばれ、メソッドなどの説明を書いておくための 文字列です。 基本的にはコメントのような役割ですが、 通常のPythonでは、この文字列を __doc__ プロパティという特殊なプロパティで呼び出すことが できます。 ただし、Pybricksではこの機能は使えないようです。

プログラムを実行して、それぞれのメソッドが正しく呼び出されるかどうか 確認しましょう。

上記の例で、 run メソッドを呼び出す際に、 run(min_runtime=500) のように引数を渡し、 その値が正しく取得できているか、 run メソッドの中に print 関数を 追加して確認してみましょう。

ライントレースをするメソッドを実装する

5段階の明るさ判断でライントレース

では、 run メソッドから実装していきましょう。 以下の例では黒線の左側の境界をトレースしていきます。 その際、5段階で明るさを判定し、右急旋回、右旋回、直進、左旋回、左急旋回、 の5種類の動作をさせています。 明るさの境界値やスピード、旋回スピードなどは各自のロボットに適切な 値を選んでください。

走行時間の制限を課す前に無限ループで試しておきましょう。

'... 以上省略 ...'

class LineFollower(DriveBase):

    '... 途中省略 ...'

    def run(self, min_runtime=1000, max_runtime=30000, max_duration=150):
        '''
        最初の交差点までライントレースをするメソッド

          min_runtime: スタート後、最低でもこの時間はライントレースを継続
          max_runtime: ライントレースをする最大時間(経過すれば停止する)
          max_duration: この時間継続して黒(一定値以下)なら交差点と判断する
        '''
        
        WHITE, LIGHTGRAY, DARKGRAY, BLACK = 50, 40, 20, 10  # 5段階で明るさを判別
        
        while True:
            
            intensity = self.sensor.reflection()
            
            if intensity > WHITE:
                self.drive(0, 180)       # 右急旋回
            elif intensity > LIGHTGRAY:
                self.drive(30, 90)       # 右旋回
            elif intensity > DARKGRAY:
                self.drive(40, 0)        # 直進
            elif intensity > BLACK:
                self.drive(30, -90)      # 左旋回
            elif intensity > BLACK:
                self.drive(0, -180)      # 左急旋回

            wait(1)


... 以下省略 ...

急なカーブでもロボットがラインに追従できているか確認しましょう。

最長走行時間を設定

run メソッドを改良して、走行時間を設定できるようにしましょう。 基本的には while 文の条件として、スタートからの経過時間の条件を 設定するだけです。 ストップウォッチの使い方はすでに学習したはずなので、特に問題はないとと思います。

'... 以上省略 ...'

class LineFollower(DriveBase):

    '... 途中省略 ...'

    def run(self, min_runtime=1000, max_runtime=30000, max_duration=150):

        '... 途中省略 ...'
        
        WHITE, LIGHTGRAY, DARKGRAY, BLACK = 50, 40, 20, 10  # 明るさを5段階で判別
        
        watch_runtime = StopWatch()     # 走行時間測定用のストップウォッチ
        watch_runtime.reset()           # 測定開始

        while watch_runtime.time() < max_runtime:  # Trueを最大時間の条件に変更
            
            '... 途中省略 ...'


        self.stop()  # whileループを抜けたらロボットを停止

    '... 途中省略 ...'

robot.run(max_runtime=10000)   # 10秒間ライントレース

実際にプログラムを実行して、ロボットが10秒間ライントレースをした後、停止するか確認しましょう。

run メソッドを呼び出す際に、 robot.run() のように引数を指定しなければ デフォルト値(上の例では30秒)が適用されることを確認しましょう。

交差点で一時停止

さて、いよいよ本題の交差点を判断する機能を実装しましょう。

交差点の認識にはいろいろな方法が考えられますが、 以下の例では、 「ある一定の明るさ以下の明るさが一定時間続けば、そこは交差点」 と判断してみます。 黒の上にずっといる時間を測定するという原理はとても簡単です。 つまり、黒以外なら(一定の明るさよりも明るければ)毎回ストップ ウォッチをリセットすればよいだけです。 黒が続けば、ストップウォッチがリセットされずに、そのうち 制限値を超えて、「交差点」と判断できるはずです。

もちろん時間の代わりにループを繰り返した回数をカウントして それを条件にすることもできます。

以下の例では、 連続して黒になる時間を測定するために、新たなストップウォッチを watch_online という名前のインスタンスで用意しました。 その上で、連続して黒の時間が max_duration を超えたら break 文でライントレースの while ループから 強制的に抜け出るようにしてあります。

'... 以上省略 ...'

class LineFollower(DriveBase):

    '... 途中省略 ...'

    def run(self, min_runtime=1000, max_runtime=30000, max_duration=150):

        '... 途中省略 ...'

        watch_runtime = StopWatch()
        watch_runtime.reset()

        watch_online = StopWatch()   # 継続して黒くなる時間の測定用
        watch_online.reset()         # 測定開始

        while watch_runtime.time() < max_runtime:
            
            intensity = self.sensor.reflection()
            
            '... 途中省略 (明るさのifブロック) ...'

            if intensity > BLACK:      # 黒でなければ (適切な値ならBLACK以外でもOK)
                watch_online.reset()   # ストップウォッチをリセット (0から計測し直す)
            elif watch_online.time() > max_duration:   # 黒の継続がmax_durationを超えれば
                break                                  # whileループから抜ける

        self.stop()             # whileループを抜けたらロボットを停止

'... 以下省略 ...'

while が終了したら、ロボットを停止するために、 stop メソッドを呼び出すのを忘れないようにしましょう。

実際にプログラムを動かして、ロボットが交差点で停止するか確認しましょう。

上記の max_duration というパラメータを調整して 交差点(直角カーブ)で停止するが緩やかなカーブでは停止しないようにしましょう。

最短走行時間を設定

交差点で一時停止した後、 そのまま左折して、ライントレースを続けようとすると、 ライントレース再開直後に黒が続き、また交差点と判断してしまう 可能性があります。 そこで、スタートしてからある一定の時間内は、 たとえ黒が続いても無視してライントレースを続けるように してみましょう。

この機能を追加することで、すぐ近くにある急カーブなども 停止せずに通過することができます。

以下の例では、 先ほど作成した、 連続して黒になる時間が max_duration 以上であるという条件に、 ライントレースをしている時間 watch.run_time.time()min_runtime を超えているという条件を加えました。

'... 以上省略 ...'

class LineFollower(DriveBase):

    '... 途中省略 ...'

    def run(self, min_runtime=1000, max_runtime=30000, max_duration=150):

        '... 途中省略 ...'

            
        while watch_runtime.time() < max_runtime:
            
            '... 途中省略 ...'

            if intensity > BLACK:
                watch_online.reset()
            elif (watch_runtime.time() > min_runtime and   # 最低走行時間以上でかつ
                  watch_online.time() > max_duration):     # 黒の継続がmax_durationを超えれば
                break

        self.stop()

    '... 途中省略 ...'


# mainプログラム

'... 途中省略 ...'

robot.run()   # 交差点までライントレース
wait(1000)    # 1秒停止
robot.run(min_runtime=2000)   # そのまま左折してライントレースを再開するはず

上の例では、 and 演算子を使って2個の条件の論理積を取りました。 この論理積は次のように1行で書いてもかまいません。

  watch_runtime.time() > min_runtime and watch_online.time() > max_duration

しかしながら、これだと1行が少し長くなってしまうので、上の例ではあえて2行に分けて書きました。 ちなみにPythonでは、通常1行に一つの処理を記述しますが、 括弧「 ( ) 」や引用符「 " " 」が閉じていない場合は、 自動的に次の行を継続行とみなしてくれます。 そして、そのように継続行とみなしてもらうために、上の例ではあえて条件式を ( ) で囲みました。

右左折と直進のメソッドを追加する

turn_leftメソッドの実装

交差点で一時停止した後、再度 run メソッドを呼び出せば、 特に何もしなくても左折してライントレースを続けるはずです。

しかし、ライントレースをする本来の位置まで左旋回をさせて ロボットの方向変換を完了させておきましょう。

'... 以上省略 ...'

class LineFollower(DriveBase):

    '... 途中省略 ..'

    def turn_left(self):
        print('turning to the left ...')

        while self.sensor.reflection() < WHITE:  // 白くなるまで
            self.drive(0, 90)                   // 左旋回
                                         
        self.stop()

        '... 途中省略 ..'


# mainプログラム

'... 途中省略 ..'

robot.run()         # 直近の交差点までライントレース
wait(1000)
robot.turn_left()   # 左折
wait(1000)
robot.run()         # ライントレースを再開

実際にプログラムを実行して、 ろボットが左折するか確認しましょう。

正方形あるいは長方形のコースで、黒い線の内側の境界を続けてライントレースさせてみましょう。 その際、頂点で1秒間停止させましょう。

cross_lineメソッドの実装

交差点を直進するには、一旦黒線を横切る必要があります。 多くの場合、交差点で一時停止した場合は、ロボット自体が 少し左側を向いているので、 そのまま直進させれば、横方向の黒線を横切ることができます。 そして、一旦黒線を横切ったら本来の直進コースに戻るまで、 つまり黒線に達するまで右旋回させればOKです。

'... 以上省略 ...'

class LineFollower(DriveBase):

   '... 途中省略 ...'

    def cross_line(self):
        '''交差点を直進'''
        print('crossing to the line ...')

        while self.sensor.reflection() < WHITE:    # 白くなるまで直進
            self.drive(15, 0)
        
        while self.sensor.reflection() > BLACK:    # さらに黒くなるまで左旋回
            self.drive(15, 90)
        
        self.stop()    # 停止

    '... 途中省略 ...'


# mainプログラム

'... 途中省略 ...'

robot.run()
wait(1000)
robot.cross_line()   # 交差点を渡る
wait(1000)
robot.run()          # ライントレースを再開

実際にロボットを動かしてプログラムを確認しましょう。

実際のコースで、交差点を渡るのに失敗しないように、 上記のプログラムの明るさの境界値や旋回スピードなどののパラメータを調整しましょう。

turn_rightメソッドの実装

右折は、上で作った cross_line を 呼び出せば簡単に実装できます。

'... 以上省略 ...'

class LineFollower(DriveBase):

    '... 途中省略 ...'


    def turn_right(self):
        print('turning to the right ...')

        self.cross_line()    # 一旦横線を横切る(selfを忘れずに)
        'さらに正面の黒線を横切って白くなるまで右(急)旋回'  # 自分で実装しましょう
        'さらに黒くなるまで右(急)旋回'
        '停止'

    '... 途中省略 ...'        

# mainプログラム

'... 途中省略 ...'

robot.run()
wait(1000)
robot.turn_right()    # 右折
wait(1000)
robot.run()

上記のプログラムを完成させましょう。

交差点が複数あるコースに挑戦しましょう。

物体との距離に応じて動きを変化させよう

手と一定の距離を保つロボット

いつものように、まず、使う予定のモジュールをインポートして、 プログラムスタート時にバッテリ残量を表示するだけの プログラムから始めましょう。 以前作成したプログラムの一部をコピーして編集を始めれば簡単です。

# 必要なモジュールをインポート

from pybricks.hubs import MoveHub
from pybricks.pupdevices import Motor, ColorDistanceSensor
from pybricks.parameters import Port, Direction
from pybricks.robotics import DriveBase
from pybricks.tools import wait, StopWatch

# バッテリ残量を表示
hub = MoveHub()
print('{} mV'.format(hub.battery.voltage()))

モジュール名の綴り間違いなどがないか、 プログラムを走らせて確認しましょう。

では、上記のプログラムに少しずつ追加していきます。 以前の復習となりますが、カラーセンサのインスタンスを生成し、 0.2秒おきにセンサの値を表示するようにしてみましょう。

'... 以上省略 ...'
  
sensor = ColorDistanceSensor(Port.D)   # センサのインスタンス生成

while True:
    d = sensor.distance()   # 測定した距離を変数dに代入
    print(d)
    wait(200)

センサの前に手などをかざして、センサからの距離をいろいろ変えてみましょう。 距離に応じて値が適切に変わるか確認してください。

では、 DriveBase のインスタンスを生成して 障害物との距離に応じてロボットの動きを変えてみましょう。 簡単な例として、以下では距離が50%を超えれば前進、50%以下なら後進、としています。

'... 以上省略 ...'

sensor = ColorDistanceSensor(Port.D)   # センサのインスタンスを生成

# DriveBaseのインスタンスを生成
motor_a = Motor(Port.A, positive_direction=Direction.COUNTERCLOCKWISE)
motor_b = Motor(Port.B)
robot = DriveBase(motor_a, motor_b, 30, 58)

while True:
    d = sensor.distance()
    if d > 50:
        robot.drive(50, 0)   # 毎秒50mmで前進
    else:
        robot.drive(-50, 0)  # 毎秒50mmで後進

    wait(2)

実際にセンサの前に手などをかざして、距離に応じてロボットの動きが変わるかどうか 確認しましょう。

上記のプログラムを修正して、距離が45〜55%のときは ロボットを静止させましょう。

では、少し滑らかな動きになるように、 距離の目標値を50とし、実際の距離とこの目標値との差に 比例したスピードで前後させてみましょう。 例えば、距離が100%のときは毎秒150mmのスピードで前進、 距離が0%のときは毎秒150mmのスピードで後進するようにしてみましょう。

'... 以上省略 ...'

while True:
    d = sensor.distance()
    speed = 3 * (d - 50)   # 距離からスピードを計算

    robot.drive(speed, 0)  # 計算したスピードで前進
    wait(2)

上記の例の計算式で、距離が100のときスピードが150、距離が0のときスピードがマイナス150 になることを確かめましょう。

上記の例では比例定数を3としました。 この比例定数を1〜4まで変化させてロボットの動きの違いを観察しましょう。

障害物を回避するロボット

次の簡単な例として、前方に物体がある場合には右旋回して物体を避けて 動き続けるロボットのプログラムを作ってみましょう。

'... 以上省略 ...'

while True:
    d = sensor.distance()

    if d > 50:
        robot.drive(50, 0)    # 毎秒50mmで前進
    else:
        robot.drive(50, 180)  # 毎秒180度で右旋回

    wait(2)

スピードや旋回の角速度は適宜調整してください。 特に難しい箇所はないでしょう。

上記の例で、距離が近くなればなるほど旋回の角速度を速くしてみましょう。 例えば、距離が100で角速度は0、距離が0で角速度は毎秒300度となるように してみましょう。

最も近い物体を検知するロボット

周囲にある物体のうち、もっとも近くにある物体を探す ロボットを作ってみましょう。

まず、ロボットを360度旋回させて、物体までの距離が正しく 取得できているか確認しましょう。

'... 以上省略 ...'

robot.reset()       # ロボットの前進距離と旋回角度を0にリセット
robot.drive(0, 90)  # ロボットをその場で旋回(毎秒90度)

while robot.angle() < 360:   # 360度旋回する間
    d = sensor.distance()
    print(d)
    wait(10)

else:
    robot.stop()                # 360度以上になれば停止

上記の例で、dの値の代わりに「*」の 個数で距離を表示させてみましょう。 「*」を10個並べて表示するには、 print(10 * '*') とします。 このとき、最初の「*」は掛け算の演算子、 後ろの「'*'」は1文字の文字列であることに注意しましょう。 また、整数と文字列の掛け算は、自動的に文字列をその整数の分だけ並べて 新しい文字列を生成することにも注意しましょう。

'... 以上省略 ...'

while robot.angle() < 360:
    d = sensor.distance()
    print(d * '*')  # '*'をd個表示
    wait(10)

'... 以下省略 ...'

上記の例だと、'*'の数が多すぎるので 半分の数(最大で50個)になるようにしてみましょう。

では、実際に最も近い物体を探すプログラムに取り掛かりましょう。

いろいろな方法がありますが、距離の仮の最小値を使う方法で 試してみましょう。 仮の最小値の初期値は十分大きくしておきましょう。 そして、測定するたびに現在の距離と仮の最小値を比較し、 もし、現在の距離のほうが小さければ、仮の最小値を現在の距離に 置き換える、という操作を行っていきます。 また、仮の最小値が更新されたときの旋回角も保持しておきます。 そのようにして、360度旋回したあと、最終的に残っている仮の最小値が 実際の最小値であり、その最小値を更新したときの旋回角が、もっとも 近い物体の方向だと考えます。

'... 以上省略 ...'

d_min = 200       # 仮の最小値の初期値(100を超えれていればよい)
ang_nearest = 0   # 仮の最小値を更新したときの旋回角

robot.reset()
robot.drive(0, 90)

while robot.angle() < 360:
    d = sensor.distance()

    if d < d_min:               # 現在の値が仮の最小値より小さい場合は
        d_min = d                     # 仮の最小値を更新
        ang_nearest = robot.angle()   # 仮の最小値を更新したときの旋回角

    wait(10)

else:
    robot.stop()

print(d_min, ang_nearest)    # 最小値とそのときの旋回角を表示

上記のプログラムを走らせ、エラーがないか確認しましょう。

最終的に得られたang_nearestの値を用いて、 ロボットが最も違い物体の方向を向くように、 上記のプログラムを改良しましょう。 (ヒント)上記の例題でロボットが停止した位置から (360 - ang_nearest)度だけ逆に旋回すれば、 ang_nearest の角度の方向にロボットは向くはず。
























加速度を測定しよう

加速度を取得する

Move Hubには、慣性計測ユニット(IMU, Inertial Measurement Unit)が搭載されています。 ただし、Pybricksでは、現時点で3軸の加速度のみのサポートのようで、角速度はまだサポートされていません (浮動小数点の扱いの問題か)。

まずいつものように必要なモジュールをインポートして、 バッテリ残量を表示するところまで準備しましょう (以下では将来的に使う可能性のあるカラー距離センサやストップウォッチなどもインポートしてあります)。 。

# 必要なモジュールをインポート

from pybricks.hubs import MoveHub
from pybricks.pupdevices import Motor, ColorDistanceSensor
from pybricks.parameters import Port, Direction
from pybricks.tools import wait, StopWatch


# バッテリ残量を表示
hub = MoveHub()
print('{} mV'.format(hub.battery.voltage()))

スペルの間違いなどがないか、実際にプログラムを動かして確認しておきましょう。

最初の簡単な例として、print関数を使って0.2秒おきに加速度の値を表示させましょう。 加速度の値は、imuというモーションクラスのacceleration というメソッドで取得できます。 このメソッドで得られるのは、x軸、y軸、z軸方向の加速度(単位m/s/s)を 要素とするタプルです。

'... 以上省略 ...'

while True:
    print(hub.imu.acceleration())
    wait(200)

実際に実行して、次のような出力が得られればOKです。

(-1, 0, 10)
(-1, 0, 10)
(-1, 0, 10)
(-1, 0, 10)
(-1, 0, 9)
(0, 0, 9)
(-1, 0, 10)
(0, 0, 10)
(0, 0, 9)

このようにaccelerationメソッドは3つの要素をもつタプルを返します。 タプルは、リストと同じようにオブジェクトを並べたものですが、 リストとは違いそれぞれの要素のオブジェクトを変更することはできません。

加速度の単位は m/s/s(メートル毎秒毎秒)ですが、 Move Hubは整数しか扱えないので、整数値に丸められた加速度が表示されます。

ちなみに、実際の重力加速度は、よく知られているように 9.8 m/s/s ですので、 hubが静止している場合は上の結果からこのhubの向きが推測できることに注意しましょう。

ハブをいろいろな方向に動かして、加速度の値がどう変化するか 観察しましょう。

タプルの要素を取り出すためには、リストと同じく[]を使います。 例えばx軸方向(長手の方向でABと印字してある面の方向)の加速度はタプルの 0番目の値なので、次のようにして取得できます。

'... 以上省略 ...'

while True:
    print(hub.imu.acceleration()[0])
    wait(500)

上記のプログラムを実行し、ハブをx軸方向に動かして加速度の値が どう変化するか確認しましょう。

上記のプログラムを実行し、ハブを立てたときにどのような値になるか 確認しましょう(重力加速度の測定)。

上記のプログラムを実行し、ハブをy軸方向(緑のボタンが上になるように おいたときに水平面内でx軸と垂直な方向)やz軸方向(上下方向) に動かして加速度の値があまり変化しないことを確認しましょう。

上記のプログラムを変更し、y軸方向の加速度を表示させましょう。 測定できる加速度の最大値と最小値はいくらでしょうか?

加速度の変化を簡易グラフで表示しよう

次にy軸方向の加速度の大きさを'*'の数で表してみましょう。 「整数 * 文字列」という演算は、文字列を整数個並べる、 ということに注意しましょう。

'... 以上省略 ...'
  
while True:
    print(hub.imu.acceleration()[1] * '*')
    wait(100)

ここで整数と文字列の演算子としての「*」と 単なる文字列の「'*'」とをを混乱しないようにしましょう。

この例では時間間隔を0.1秒にしました。 実際に横方向にハブを振って、'*'がいくつぐらい表示されるか 確認しましょう。

前の練習問題で確認したように、Move Hubの加速度センサは -20〜20の値を返します。 そのため、上の例題では加速度が負の値になったとき、'*'が 表示されません。 そこで加速度0の状態で '*' を20個、加速度が a なら '*' を (a+20) 個 表示するように、してみましょう。 そうすれば '*' の数が0〜40個まで変化します。

'... 以上省略 ...'
  
while True:
    print((hub.imu.acceleration()[1]+20) * '*')
    wait(100)

実際にプログラムを実行して、加速度が-20〜20の範囲でグラフに表示できるかどうか 確認しましょう。

時間間隔を0.02秒にし、少し速くハブを振って加速度の変化を確認しましょう。

常に水平方向を指し示すロボット

片方のモータに軸を装着し、ハブを傾けてもこの軸が常に水平になるような プログラムを考えてみましょう。

本体のLEDを点灯させよう

MoveHub本体のLEDを点灯させてみましょう。

定義済のカラーを指定

いくつかの色はあらかじめ定義されているので、 Colorクラスの属性として色の名前を指定するだけで使えます。

# 必要なモジュールをインポート
from pybricks.hubs import MoveHub
from pybricks.parameters import Color
from pybricks.tools import wait


hub = MoveHub()           # インスタンス生成

hub.light.on(Color.RED)   # 赤色点灯
wait(2000)
hub.light.off()           # 消灯

while True: を使って上記のプログラムを改良し、 0.5秒点灯、0.5秒消灯を繰り返すようにしてみましょう。

この練習問題ではwhile構文を使ってLEDを点滅 させましたが、blinkというメソッドも用意されています。 このメソッドを使えば、点灯時間と消灯時間をリストとして引数で渡すことで 次のように簡単に書けます。

from pybricks.hubs import MoveHub
from pybricks.parameters import Color
from pybricks.tools import wait


hub = MoveHub()

hub.light.blink(Color.RED, [500, 500])   # 点滅開始
wait(10000)
hub.light.off()    # 10秒後に消灯

他の色でも試してみましょう ( Color.ORANGE, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.VIOLET, Color.MAGENTA, Color.WHITE, Color.GRAY, Color.BLACK )。

独自の色を指定

Colorクラスを使えば独自の色を簡単に定義することができます。

'... 以上省略 ...'
  
my_color = Color(h=120, s=40, v=90)     # 独自の色を定義
hub.light.blink(my_color, [500, 500])   # 上で定義した色を使う

'... 以下省略 ...'

ところで、それぞれの色のHSVの値はhsvメソッドで取得できます。 また、HSVの個々の値はそれぞれh, s, v という属性で取得できます。

from pybricks.parameters import Color

print(Color.MAGENTA.hsv())   # magentaのHSVの値を表示

my_color = Color(h=120, s=40, v=90)
print(my_color.hsv())      # my_colorのHSVの値をタプルで表示
print(my_color.h)   # my_colorのHの値(Hue)を表示
print(my_color.s)   # my_colorのSの値(Saturation)を表示
print(my_color.v)   # my_colorのVの値(Value)を表示

次の色のHSVの値を調べてみましょう。 Color.ORANGE, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.VIOLET, Color.MAGENTA, Color.WHITE, Color.GRAY, Color.BLACK

カラー距離センサで測定した色でLEDを点灯させる

簡単な応用として、カラー距離センサで測定した色と 同じ色でLEDを点灯させてみましょう。

カラー距離センサでhsvメソッドを使うと 測定した色がHSV表示で得られます。 これをそのままlightonメソッドに渡します。

from pybricks.hubs import MoveHub
from pybricks.pupdevices import ColorDistanceSensor
from pybricks.parameters import Color
from pybricks.tools import wait


hub = MoveHub()
sensor = ColorDistanceSensor()

while True:
    color = sensor.hsv()   # カラーを取得して変数colorに代入
    print(color.hsv())     # コンソールにHSVの値を表示
    hub.light.on(color)    # 取得したカラーでLEDを点灯
    wait(100)

さて、上記の例でV(value)の値だけを、例えば100に変更した色で LEDを点灯させたい場合はどうすればよいでしょうか。 この場合、HSVの値を個別に指定して新たに色を定義し、 それをonメソッドの引数とすればよいでしょう。

'... 以上省略 ...'

while True:
    color = sensor.hsv()
    print(color.hsv())
    hub.light.on(Color(h=color.h, s=color.s, v=100))    # HとSはそのままでVだけを100に
    wait(100)

少し紛らわしいですが、colorは測定した色を代入した変数、 大文字で始まるColorはクラス名です。

測定した色と色相Hを180度ずらした色でLEDを点灯してみましょう。