2022年1月1日土曜日

LEGO MINDSTORMS Pythonで独自ライブラリ

 MINDSTORMSのPythonでは独自ライブりの利用ができないのである程度の規模のプログラムではソースが大きくなって見通しが悪くなるというお話をしました。ただ、MINDSTORMSのHUBでは0〜19までの20種類のプログラムをアップロードできて、LEGO本体(以下HUB)のキー操作で選択して実行することができるようになっています。この機能を使えばLEGOには20種類のPythonプログラム(LEGOアプリではプロジェクト:以下プロジェクト)をアップロードできることになります。

ということでアップロードしたプロジェクトはHUBに保存されているはずなのでライブラリとして作成したものをアップロードしてそれをimportすれば、なんちゃってライブラリとして使えるのではないかなと考えHUBの方を少し調べてみました。

MINDSTORMS HUBの調査


注意:以下の内容はMINDSTORMSのHUBを直接操作しますので、自己責任でお願いいたします。

先日MINDSTORMSのMicroPythonのバージョン調査した時は特に説明をしませんでしたがHUBとUSBないしはBluetoothで接続するとPCとシリアル接続ができます。このシリアルポートを使ってMicroPythonをキーボードから操作することができます。
シリアル通信が可能なソフトならなんでも良いのですが私はTeraTermを利用していますが、TeraTermを利用してHUBと接続する方法はこちらの説明が分かり易いと思います。尚、記事の「インストール」については作業する必要はなく「REPL(コンソール/コマンドライン)による実行」の部分だけ参考にすればHUBとの接続は可能です。

HUBとの接続ができると後はMicroPythonのインタプリタに接続した状態になりますので、適時プログラムを入力することでHUBの中身を調査していきます。

>>> import os
>>> os.listdir()
['boot.py', 'bt-lk1.dat', 'bt-lk2.dat', 'main.py', 'util', 'projects', 'runtime', 'system', '_api',
 'commands', 'event_loop', 'mindstorms', 'programrunner', 'protocol', 'sounds', 'spike', 'ui',
 'hub_runtime.mpy', 'version.py', '.runtime_hash', 'etc', 'extra_files', '.extra_files_hash',
 'runtime.log']
 注)見やすくする為、行を折り返しています。
いろいろなファイルやディレクトリがありますね。ファイル拡張子が.pyはPythonのソースファイル、.mpyはソースをコンパイルしMicroPythonのバイトコードに変換されたコードオブジェクトになります。
今後ファイルの中身を見る必要も何回かあると思うので、まずはテキストファイルの中身を表示する関数catを作成しておきます。
では早速main.pyの中を覗いてみます。
>>> def cat(name):
...        f = open(name, 'r')
...        src = f.read()
...        f.close()
...        print(src)
...
...
...
>>> cat('main.py')
import gc
import micropython

import hub_runtime

micropython.alloc_emergency_exception_buf(256)

hub_runtime.start()
hub_runtimeについてhelpを見てみます。
>>> help(hub_runtime)
object <module 'hub_runtime' from 'hub_runtime.mpy'> is of type module
  HubUI -- <class 'HubUI'>
  LinegraphMonitorMethods -- <class 'LinegraphMonitorMethods'>
  SoundMethods -- <class 'SoundMethods'>
  BT_VCP -- BT_VCP(0)
  ProgramRunner -- <class 'ProgramRunner'>
  __connection_changed -- <function __connection_changed at 0x20038b70>
  error_handler -- <ErrorHandler object at 20020e00>
  init -- <function init at 0x20038b40>
  TIMER_PACE_HIGH -- 16
  __file__ -- hub_runtime.mpy
  start -- <function start at 0x20038b60>
  LightMethods -- <class 'LightMethods'>
  ProgramMethods -- <class 'ProgramMethods'>
  initialize_ports -- <function initialize_ports at 0x200197b0>
  pop_force_reset -- <function pop_force_reset at 0x2001d200>
  cleanup_slot_list -- <function cleanup_slot_list at 0x2001d070>
  notify_orientation_event -- <function notify_orientation_event at 0x2001d390>
  USB_VCP -- USB_VCP(0)
  __name__ -- hub_runtime
  HubMethods -- <class 'HubMethods'>
  MotorMethods -- <class 'MotorMethods'>
  hub -- <module 'hub'>
  RPCProtocol -- <class 'RPCProtocol'>
  system -- <System object at 200296b0>
  notify_button_event -- <function notify_button_event at 0x2001d290>
  get_event_loop -- <function get_event_loop at 0x200155c0>
  notify_gesture_event -- <function notify_gesture_event at 0x2001d380>
  WaitMethods -- <class 'WaitMethods'>
  RTTimer -- <class 'RTTimer'>
  RadioBroadcastMethods -- <class 'RadioBroadcastMethods'>
  Timer -- <class 'Timer'>
  DeviceMethods -- <class 'DeviceMethods'>
  runtime -- <module 'runtime' from 'runtime/__init__.mpy'>
  MoveMethods -- <class 'MoveMethods'>
>>>
いろいろな関数やクラスが定義されいます。ProgramRunnerクラスが実際に作成したプロジェクトを実行する為のクラスかなと想像できます。時間ができたらそれぞれについてもう少し調べてみたいと思います。
次にフォルダの中身を見ていきたいと思いますがprojectsフォルダはその名前から作成したプロジェクトを保存していると思われます。
>>> os.listdir('projects')
['standalone_', 'standalone.mpy', '.slots', '15015', '13191', '21493', '7000']
>>>
現在4つのプロジェクトをアップロードしてあるので数字の部分がプロジェクトのようです。LEGOアプリからアップロードを繰り返すと同じプロジェクトをアップロードしてもこの数字は変更されるようです。プロジェクトを修正してアップロードする度に名前が変わってしまうのではimportする時にその都度importする名前を変更する必要があるので使い勝手が悪くなってしまいます。
そこで「.slots」の中身を覗いてみましょう。
>>> cat('projects/.slots')
{
 0: {'name': 'UHJvamVjdCAw', 'id': 13191, 'modified': 1641021587911, 'type': 'python', 'project_id': 'E7DDg6MLrjmB', 'created': 1640675556147},
 1: {'name': 'UHJvamVjdCAx', 'id': 15015, 'modified': 1641019129550, 'type': 'python', 'project_id': '53cym18n7Gxv', 'created': 1640932342297},
 2: {'name': 'UHJvamVjdCAy', 'project_id': 'PTGPVlM9kK14', 'modified': 1641026738726, 'created': 1640934681576, 'id': 21493, 'type': 'python'},
 3: {'name': 'UHJvamVjdCAz', 'type': 'scratch', 'modified': 1641028117920, 'id': 7000, 'project_id': 'objY1Z0dw-Ep', 'created': 1641028058971}}
>>>
注)見やすくする為、一部改行しています。
このファイルでアップロードしたプロジェクトを管理しているようです。プロジェクトをアップロードする0〜19の番号のことはスロットと呼んでいるんですね。またprojectsフォルダ内に作成されている数字のidは調べたところファイルではなくフォルダでした。
そこでこのフォルダの中を見てみると
>>> os.listdir('projects/15015')
['__init__.mpy']
>>>
となっていて、作成したプロジェクトはコンパイルされて「__init__.mpy」というファイル名でHUBへアップロードされていました。
この「__init__.mpy」または「__init__.py」というファイル名はPythonを少し勉強した方ならご存知のように特別なファイル名です。(知りたい方はこちらを参照すると纏まっています)
また、LEGOのMicroPythonの制限なのか色々やってみましたがimportすることが出来ませんでした。(私の力不足なのかもしれませんが・・・)

結論として以下の方法でimportすることができるようになりました。

  1. 別のスロットの「__init__.mpy」を自スロットへ「xxx.mpy」(xxxは任意)としてコピーする。
  2. 「from .xxx import <識別子名>」で必要なクラスや関数をimportする。

ということで前回のステートマシンのサンプルを元にState, StateMachineクラスをライブラリとして分離したものを以下に示します。

別スロットのプロジェクトをimportする


スロット1にState, StateMachineクラスを定義したプロジェクトを、スロット0に以下のプログラムをアップロードしてテストしています。

注)今後LEGOによる仕様変更で以下の方法が利用できなる可能性もあります。

from mindstorms import MSHub, Motor, MotorPair, ColorSensor, DistanceSensor, App
from mindstorms.control import wait_for_seconds, wait_until, Timer
from mindstorms.operator import greater_than, greater_than_or_equal_to, less_than, less_than_or_equal_to, equal_to, not_equal_to
import math
import json

# 別スロットからのimport用コピー
class CopyProject:
    def __init__(self):
        f = open('projects/.slots', 'r')
        data = f.read()
        f.close()
        self.slots = json.loads(data.replace('\'', '"'))

    def getID(self, no):
        return self.slots[no]['id']

    def copy(self, no_from, no_to, name):
        from_path = 'projects/{}/__init__.mpy'.format(self.slots[no_from]['id'])
        to_path = 'projects/{}/{}.mpy'.format(self.slots[no_to]['id'], name)
        f = open(from_path, 'rb')
        data = f.read()
        f.close()
        f = open(to_path, 'wb')
        f.write(data)
        f.close()

cp = CopyProject()
cp.copy(1, 0, 'lib1')

from .lib1 import State, StateMachine

# オブジェクトの定義
app = App()
hub = MSHub()
wheels = MotorPair(MSHub.PORT_B, MSHub.PORT_A)
arm = Motor(MSHub.PORT_C)
distance_sensor = DistanceSensor(MSHub.PORT_D)
color_sensor = ColorSensor(MSHub.PORT_E)

# ステートの定義
STATE_EXIT = "exit"
STATE_GO_STRAIGHT = "GoStraight"

# 直進
class GoStraight(State):
    def __init__(self, name: String = STATE_GO_STRAIGHT):
        super().__init__(name)

    def entry(self):
        wheels.start(0, -20)

    def do(self) -> String:
        distance = distance_sensor.get_distance_cm()
        if distance and distance < 20: # 測定不能の場合None
            return STATE_EXIT
        return None

    def exit(self):
        wheels.stop()

# メイン処理
class Main:
    def __init__(self):
        self.__stateMachine = StateMachine()
        self.__stateMachine.add(State(STATE_EXIT))
        self.setup()
        self.loop()

    def setup(self):
        # ステートの登録
        self.__stateMachine.add(GoStraight())
        # 開始ステートへ切り替える
        self.__stateMachine.changeTo(STATE_GO_STRAIGHT)
        
    def loop(self):
        while self.__stateMachine.currentState != STATE_EXIT:
            nextState = self.__stateMachine.run()
            if nextState:
                self.__stateMachine.changeTo(nextState)
            # 外部よりのステートマシン終了
            if hub.right_button.is_pressed():
                print("right button is pressed !")
                self.__stateMachine.changeTo(STATE_EXIT)

# プログラム開始指示を待つ
hub.left_button.wait_until_pressed()
hub.speaker.beep()
# プログラム開始
Main()
# プログラム終了(メニューに戻る)
raise SystemExit

0 件のコメント:

コメントを投稿

Edisonが故障した一件

 最近またEdisonで遊んでいます。 教育用のデバイスとしてよく出来ているなと思っている Edison ですが、現在3台を所有しています。 最近このうちの1台が調子が悪くなってしましました。具体的にはBeep用のデバイスが壊れたようです。購入時にチェックをしたつもりですが今回久...