Open8

LEGOのモーターやセンサーをSwiftで制御したい

Shinichiro ObaShinichiro Oba

SPIKEアプリのワードブロックでモーターの制御。毎回ポートを指定する。指定ポートにモーターが繋がっていない場合、命令は無視され、センサー値は0になる。

Shinichiro ObaShinichiro Oba

SPIKEアプリからMicroPythonでモーターの制御。ポート指定でMotorのインスタンスを生成し、それに対して操作を行う。インスタンス生成時にモーターが接続されていないとRuntimeError

from spike import Motor

motor = Motor('A')
motor.run_for_rotations(1)
print(motor.get_position())
Shinichiro ObaShinichiro Oba

PybrickのMicroPythonでモーターの制御。ポート指定でMotorのインスタンスを生成し、それに対して操作を行う。インスタンス生成時にモーターが接続されていないとOSError

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

motor = Motor(Port.A)
motor.run_angle(500, 360) # 500 deg/sで1回転
print(motor.angle()) # 絶対位置は取得できない
Shinichiro ObaShinichiro Oba

基本的にはSPIKEアプリでプログラミングできる公式MicroPythonと同じ形がよさそう。時間がかかる命令の待機にはasync/awaitを使う。

let motor = Motor(Port.A)
await motor.runForRotations(1)
print(motor.position)

ただし、MicroPythonのプログラムはハブに書き込むことを前提としているため、Swiftの場合はBLEで遠隔制御することを考慮する必要がある。ハブの接続や切断、モーターやセンサーの接続などのイベントをとれるようにした方がよさそう。

Shinichiro ObaShinichiro Oba

Swift PlaygroundsでLEGO MINDSTORMS EV3のプログラミングをするときは、あらかじめ用意されているev3というインスタンスに対して毎回ポート指定で制御する。ハブやモーターを接続していないときに実行するとどうなる?

ev3.motorOn(forDegrees: 360, on: .a, withPower: 75)
print(ev3.measureMotorDegrees(on: .a))
Shinichiro ObaShinichiro Oba

よく考えたら上記の設計だといくつか問題がある。

  1. Motorインスタンスからどうやって現在接続しているハブにBLEのコマンドを送るのか
  2. ハブの複数接続に対応できない

モーターやセンサーへは、ハブを管理するクラスを経由してアクセスするほうがよさそう。

Shinichiro ObaShinichiro Oba

node-poweredupを使うと、ハブとのBLE通信をNode.jsでプログラムすることができる。コードはハブのスキャンから始まり、async/awaitやイベントを使ってモーターの制御やセンサー値の取得をしていくスタイル。ハブの複数接続も可能。

const PoweredUP = require("node-poweredup");
const poweredUP = new PoweredUP.PoweredUP();

let latest_degrees = null;

poweredUP.on("discover", async (hub) => {
    await hub.connect();
    const motor = await hub.waitForDeviceAtPort("A");

    hub.on("rotate", (device, { degrees }) => {
        latest_degrees = degrees;
    });

    await motor.rotateByDegrees(360);
    console.log(latest_degrees);
});

poweredUP.scan();

センサー値はイベントでしか受け取れない模様。ハブではなくモーターからのイベントとして受け取ることもできる。

    motor.on("rotate", ({ degrees }) => {
        latest_degrees = degrees;
    });

このスタイルは理にかなっている気がする。もしセンサー値のプロパティを用意するならオプショナルのアンラップかtryが必要になりそう。