PythonでETロボコンシミュレータ（大会用）を動かしてみた。


https://github.com/YoshitakaAtarashi/ETroboSimController

# ETロボコンシミュレータ（大会用）について

ETロボコンは参加者向けに[環境構築のためのリポジトリ](https://github.com/ETrobocon/etrobo/)を公開しており、ここからETロボコンシミュレータ（大会用）をインストールすることができます。実は、参加者でない方でもサンプルコースだけが動くETロボコンシミュレータを体験することができます。作りかけの[WEB版ETロボコンシミュレータ](https://qiita.com/YoshitakaAtarashi/items/877d1a0cc5ba94a39755)と異なり、大会に必要な機能をすべて備えていますので、こちらのシミュレータをもっと活用できればいいのになぁ、と思う方もいらっしゃると思います。

# 今時C言語でロボコン？

などと言われるのですが、以前はJavaで参加するチームもいましたし、ETロボコン2021では実は[mruby](https://qiita.com/yamanekko/items/095a65f507b67a210e27)が公式に使えます。やる気になればどうにでもなる大会なのです。mrubyは実機でも動作することを意識し、TOPPERS、Athrillの環境をベースにしているため、これはこれでとても興味深い環境だと思っています。

# MiniScriptとか知らんし。

[WEB版ETロボコンシミュレータ](https://qiita.com/YoshitakaAtarashi/items/877d1a0cc5ba94a39755)ではMiniScriptを採用しましたが、これを勉強したところで役に立たないよね？という意見も多いかと思います。まぁ、私もそうかなと思いつつも、今のところこれ以上いい案が思いついていないだけなのです。sessionStorageとWebAssemblyを使えば何でもできそうな気がしなくはないですが、まだモチベーションが上がっていません。

# 実はETロボコンシミュレータ（大会用）はPythonでも動かせる。

ここからが本題です。ETロボコンシミュレータは組込みCPUシミュレータAthrillとUDPで通信しています。ここの通信フォーマットを理解しているならば、Athrillでなくとも制御ができてしまいます。（もちろん、今大会では利用できませんけど！）

ということで理解している私がPythonクライアントを作りました。ETロボコンシミュレータを起動し適当なディレクトリで

```
$ git clone https://github.com/YoshitakaAtarashi/ETroboSimController 
$ cd ETroboSimController
$ python test_Motor.py
```
とすると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1804036/a0b8b256-d480-b4ed-6740-92aa08db92bf.png)

というような感じで走行体を動かすことができます。前提環境はPythonだけなのでインストール地獄にはならないはずです。Pythonは3.7.6以上で確認しています。ETroboSimServerがIO処理でブロックされないようasyncioを使っているため、古いPythonだと動かない可能性が高いです。

# test_Motor.pyの解説

まず、[EV3 C++ API](https://www.toppers.jp/ev3pf/EV3RT_CXX_API_Reference/)に似せたetrobosim.ev3api(as ev3)と、Athrillの代わりにETロボコンシミュレータを制御する etrobosim(as ets)をインポートします。
次に、各モータの初期化をします。この辺は[WEB版ETロボコンシミュレータ](https://qiita.com/YoshitakaAtarashi/items/877d1a0cc5ba94a39755)と違って、大会環境に似せようという過去の私の努力が見て取れます。
ets.Controllerで左コースか右コースかを選択できます。Python実装を見るとわかりますが、ここでUDPポートを切り替えています。controller.addHandlersは作成したデバイス(motor*)をここで追加することで、controllerの管理下に置くことができます。あとはお決まりのPWM値の設定です。
Ctrl+Cで停止できるように、KeyboardInterruptをキャッチしています。（これがなかなかうまく実装できなかった）


``` python
import etrobosim.ev3api as ev3
import etrobosim as ets
import time

motorR=ev3.Motor(ev3.ePortM.PORT_B,True,ev3.MotorType.LARGE_MOTOR)
motorL=ev3.Motor(ev3.ePortM.PORT_C,True,ev3.MotorType.LARGE_MOTOR)
motorARM=ev3.Motor(ev3.ePortM.PORT_A,True,ev3.MotorType.MEDIUM_MOTOR)
motorTAIL=ev3.Motor(ev3.ePortM.PORT_D,True,ev3.MotorType.LARGE_MOTOR)
motorR.reset()
motorL.reset()

# 1秒毎に直進と回転を切り替える。
walker=[(50,50),(50,0)]
wid=0

try:
    controller=ets.Controller(ets.Course.LEFT)
    controller.addHandlers([motorR,motorL,motorARM,motorTAIL])
    controller.start(debug=False)
    while controller.isAlive():
        motorR.setPWM(walker[wid][0])
        motorL.setPWM(walker[wid][1])
        wid=(wid+1)%len(walker)
        print("MotorR={},MotorL={}".format(motorR.getCount(),motorL.getCount()))
        time.sleep(1)
    controller.exit_process()
except KeyboardInterrupt:
    controller.exit_process()
    raise
```

# test_LineTrace.pyによるライントレース

もちろんライントレースもできます。colorSensorを追加している点と、pidControlを周期的に動かすためにcontroller.runCyclic(pidControl)を実行している点が異なります。組込みOSと違って精度よく周期実行させることはできませんが、まぁまぁいい感じに動くのでぜひ試してみてください。


``` python
import etrobosim.ev3api as ev3
import etrobosim as ets

# ColorSensorのReflectを使ってP制御でライントレースする。

def calcPID(r, target=20, power=70,P=1.8):
    p=r-target
    left=power-P*p
    right=power+P*p
    return (int(left),int(right))

def pidControl(initARM_count=-50,initTAIL_count=0):
    left,right=calcPID(colorSensor.getBrightness())
    motorL.setPWM(left)
    motorR.setPWM(right)
    motorARM.setPWM(initARM_count-motorARM.getCount())
    motorTAIL.setPWM(initTAIL_count-motorTAIL.getCount())
    #print("MotorR={},MotorL={},MotorARM={},Color={}".format(motorR.getCount(),motorL.getCount(),motorARM.getCount(),colorSensor.getBrightness()))


motorR=ev3.Motor(ev3.ePortM.PORT_B,True,ev3.MotorType.LARGE_MOTOR)
motorL=ev3.Motor(ev3.ePortM.PORT_C,True,ev3.MotorType.LARGE_MOTOR)
motorARM=ev3.Motor(ev3.ePortM.PORT_A,True,ev3.MotorType.MEDIUM_MOTOR)
motorTAIL=ev3.Motor(ev3.ePortM.PORT_D,True,ev3.MotorType.LARGE_MOTOR)
motorR.reset()
motorL.reset()
colorSensor=ev3.ColorSensor(ev3.ePortS.PORT_2)

try:
    controller=ets.Controller(ets.Course.LEFT)
    controller.addHandlers([motorR,motorL,motorARM,motorTAIL,colorSensor])
    controller.start(debug=True)
    controller.runCyclic(pidControl)
    controller.exit_process()
except KeyboardInterrupt:
    controller.exit_process()
    pass
```

# 最後に

UDPの通信仕様だけ知ってればいいんだからPythonで簡単に作れると思いきや、同期周りで苦労して、組込みOS的な機能がやはり欲しくなりました。asyncioは今回初めて使ったのですが、ちゃんと動くようになるまではイライラしっぱなしでした。非同期処理ってホント難しいですね。
これの応用ですが、大会で採用するとしたら機械学習とか強化学習とかに使えるんじゃないですかね。学習データをPythonで取っておいて、大会用プログラムはAthrill対応の言語(C、C++、mrubyなど)とするのも面白そうです。来年はさすがにシミュレータ大会オンリーじゃなくなっててほしいですけど。