2022年1月3日月曜日

MINDSTORMS Python まとめ

 MINDSTORMS Python 全部入り

今まで実装してきた非同期処理、ステートマシン、独自ライブラリについて全てまとめたソースについてGiHubへ公開しておきました。
非同期処理で距離センサーを使用しながら、ステートマシンで直進します。距離センサーが20cm以内に物体を検知した場合にイベントで通知するので停止するという簡単なプログラムです。
メイン処理、ステートマシンでの処理、非同期処理するプログラムと3つのプロジェクトに分けて作成してあります。
MINDSTORMSのモーターやセンサーオブジェクトはそれぞれでimportしても動くとは思いますが、接続ポートの変更などを考慮してメインで全て定義して引数で渡す様にしています。
独自ライブラリの方ではできるだけ汎用性が出る様にプログラムを作成する様に心がけるなどすると将来的にも役立つと思います。

クラスの継承問題

サンプルプログラムではclassを使ってオブジェクト思考風に設計していますが、ここで1つ問題が出てきます。オブジェクト指向で設計していくと継承を使いたくなりますがプロジェクトを分割した場合、親クラスが自分のプロジェクト内に定義されているか、importする必要があるということです。(クラスだけでなく関数でも同じです。)

サンプルではステートマシン用のプロジェクト内にStateクラスとGoStraightクラス(Stateクラスを継承)が定義されていますが、プログラムが大きるなくと各ステート毎にプロジェクトを分割したくなると思います。(私だとステート毎にプロジェクトに分割すると思います)
StateクラスとStateMachineクラス(やその他共通で使用するクラスや関数)を共通ライブラリ化して1つのプロジェクトとして各ステートではStateクラスなどをimportして使うというやり方です。
GoStraightクラスを別プロジェクトにした例が以下です。(Project3.pyとします)

import json

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, 3, 'lib1')
from .lib1 import State

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

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

    def exit(self):
        self.__wheels.stop()

if __file__.split('/')[3] == '__init__.mpy':
    print(__file__)

Stateのimportを行わないとプロジェクトをアップロードする時にStateが無いというエラーが出てアップロードすることが出来ません。またStateをimportするのはアップロードする時だけて良さそうですが(ライブラリとしてimportされる場合は、事前にStateをimportしておく)、GoStraightをimportする時もStateをimportしていないとエラーとなります。

CopyProjectクラスだけはimportして使うことが出来ないので、今回の例の様にStateクラスの提供する機能が殆どない様なケースではGoStraightクラスではStateクラスを継承せずに必要なメソッドは自力でコーディングしておく方がソースがスッキリする様です。

共通ライブラリ化の場合は参照関係を整理して助長性とのトレードオフを考える必要がありそうです。


2022年1月2日日曜日

MINDSTORMS Pythonで if __name__ == "__main__"

 if __name__ == "__main__"

Pythonのドキュメントによると

モジュールは、自身の __name__ をチェックすることでメインスコープで実行されているかどうかを確認できます。これはモジュールがスクリプトとして、あるいはインポートでなく python -m で起動されたときに実行するコードの条件として使用できる一般的なイディオムです:

とあります。
ライブラリを作成している時にテストコードを書く場合に良く使用されますが、今回のLEGOでのライブラリ化では使用できませんでした。

色々試した結果以下の方法で実現できました。

LEGOでif __name__ == "__main__"

プロジェクトとして実行された場合にHUBのメモリやディスクの使用状況を表示しています。

if __file__.split('/')[3] == '__init__.mpy':
    import os
    import gc
    import micropython
    print('-----------------------------')
    stat = os.statvfs('/')
    total_size = stat[0] * stat[2]
    free_size = stat[0] * stat[4]
    print('each block is {} bytes big'.format(stat[0]))
    print("in bytes, that's {} bytes free out of a total of {}".format(free_size, total_size))
    print('-----------------------------')
    gc.collect()
    micropython.mem_info()
    print('-----------------------------')
    print('Initial free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
    print('-----------------------------')    
実行すると
-----------------------------
each block is 4096 bytes big
in bytes, that's 29052928 bytes free out of a total of 32505856
-----------------------------
mem: total=595907, current=402437, peak=404485
stack: 1052 out of 7168
GC: total: 242368, used: 193024, free: 49344
 No. of 1-blocks: 3072, 2-blocks: 892, max blk sz: 321, max free sz: 869
-----------------------------
Initial free: 49344 allocated: 193024
-----------------------------
ストレージとしては32Mバイトの殆どが空いていることがわかります。
また、MicroPythonの場合、実行中のプログラムがオブジェクトをインスタンス化すると、必要なメモリがヒープから割り当てられますが、その空き容量が49Kバイト程あることもわかります。

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

2021年12月31日金曜日

LEGO MINDSTORMS でステートマシン

なんちゃってスレッドができるようになりましたが、プログラムを勉強していくとまずはモジュール化という考え方が必要になってきます。

Scratchではスプライトというオブジェクト毎にプログラムを作成するということで自然とモジュール化されるようになっていますし、ブロック化やメッセージ送信・受信ということで意識的にモジュール化することも可能です。

私がプログラミングを始めた頃は構造化プログラミングという言葉が流行っていましたがその後、オブジェクト指向、イベント駆動、データ駆動、ドメイン駆動にテスト駆動とプログラミング界隈でもいろいろな言葉が流行りましたが、基本は構造化プログラミングかなと個人的には思っています。

おそらくなんでもそうなんでしょうがある程度のところまでは才能だけで行くことが出来ますが、その上に行くにはやはり基本が大切なのかなと思います。

以下にMINDSTORMSのMicroPythonでのステートマシンのプログラム例を示しますが、ステートマシンは複雑な振る舞いの制御によく使われていて、ROS(Robot Operating System)ではsmachパッケージとしてPythonで利用できるようになっています。

MINDSTORMS用ステートマシン

必要に応じてuasyncioを利用した非同期処理を組み込めば複雑な処理も可能になりますが、その場合はイベント(uasyncuo.Event)によるタスク同期なども考える必要が出てきます。

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

# オブジェクトの定義
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 State(object):
    def __init__(self, name: String):
        self.__name = name
        self.__action = None

    @property
    def name(self):
        return self.__name

    def entry(self):
        pass

    def do(self) -> String:
        return None

    def exit(self):
        pass

# ステートマシンクラス
class StateMachine(object):
    def __init__(self):
        self.__stateList = {}
        self.__now = None

    @property
    def currentState(self):
        return self.__now.name if self.__now else ""

    def add(self, state: State):
        self.__stateList[state.name] = state

    def changeTo(self, tag: String):
        if self.__now:
            self.__now.exit()
        self.__now = self.__stateList[tag]
        if self.__now:
            self.__now.entry()

    def run(self) -> String:
        if self.__now:
            return self.__now.do()
        return None

# 直進
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

2021年12月30日木曜日

LEGO MINDSTORMS Pythonでなんちゃってスレッド2

 先日はジェネレータベースのコールチンを実装してみましたが、Python 3.10からはこのジェネレータベースのコールチンは廃止が予定されているようです。
MINDSTORMSのMicroPythonは3.4.0をベースとしたもののようです。

MicroPython v1.14-893-gbae6ff2ee on 2021-11-04; LEGO Technic Large Hub with STM32F413xx
Type "help()" for more information.
>>> import sys
>>> sys.version
'3.4.0'
>>>
現在のPythonの最新バージョンが3.10なのでMINDSTORMSのMicroPythonが3.10になることはまずないとは思いますが、プログラマを目指す子供たちはasyncioベースのコールチンに慣れておくべきかなと思います。(あまり使用することはないとは思いますがw)

コールチンをしっかりと理解しておくとスレッドやタスクといった概念についても理解しやすくなるでしょう。最近ではAndroidアプリの開発でもHTTPアクセス処理などでコールチンを利用した非同期処理が推奨されているようです。

uasyncioと一緒に使うコールチン


基本的なプログラムの作り方はジェネレータベースのコールチンとほぼ同じです。
async defで定義した関数の中でawaitでコールチンの処理を待つようにプログラミングするのが基本になります。実例を見た方が早いですね。
「yield True」を「await uasyncio.sleep_ms(1)」に書き換えた感じです。尚awaitするのはコールチンであれば良いので「uasyncio.sleep_ms(1)」でなく独自のメソッドを指定することも可能です。

コールチンの処理を終了させる方法はジェネレータベースのコールチンと同じ方法(コメントにしてあります)とuasyncio.Taskクラスが提供するcancel()メソッドを利用する方法があります。

uasyncioと一緒に使う方法ではメイン処理もコールチンになっているので適時awaitで処理を切り替えることが可能です。

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 uasyncio

# オブジェクトの定義
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_START = 1
STATE_EXIT = -1

# コールチンクラス
class Coroutine1:
    def __init__(self):
        self.__distance = -1
        self.__loop = True

    @property
    def distance(self):
        return self.__distance

    def stop(self):
        self.__loop = False

    async def run(self):
        try:
            while self.__loop:
                await self.__done()
        finally:
            print("Coroutine1 end!")

    async def __done(self):
        self.__distance = distance_sensor.get_distance_cm()
        print(self.__distance)
        await uasyncio.sleep_ms(1)
        if self.__distance and self.__distance < 15:
            print("The wall is near!")
            await uasyncio.sleep_ms(1)

# メイン処理
class Main:
    def __init__(self):
        self.state = STATE_START
        self.setup()
        uasyncio.run(self.run())

    def setup(self):
        self.__coroutines = []
        self.__coroutines.append(Coroutine1())

    async def run(self):
        self.__tasks = []
        for c in self.__coroutines:
            self.__tasks.append(uasyncio.create_task(c.run()))
        while self.state != STATE_EXIT:
            if hub.right_button.is_pressed():
                print("right button is pressed !")
                #for task in self.__coroutines:
                #    task.stop()
                for task in self.__tasks:
                    task.cancel()
                await uasyncio.sleep_ms(30)
                self.state = STATE_EXIT
            else:
                self.__done()
                await uasyncio.sleep_ms(1) # コールチンに処理を譲る

    def __done(self):
        print("Main keep alive!")

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

2021年12月29日水曜日

LEGO MINDSTORMS Pythonでなんちゃってスレッド処理

LEGO MINDSTORMS Robot Inventor


こちらに紹介ページはあるのですが「今すぐ購入」ボタンを押すとページは存在せず公式サイトで購入することはできません。日本では教育用のSPIKEプライムセットしか購入できません。SPIKEは教育用なのでパーツを入れるプラスチックケース入りなのに対してInventorはホビー向けなのでケースは紙製となっています。ただ、標準でモータが4つついていたりLEGOパーツの数が多かったりと個人で遊ぶのにはInventorの方が良いので、米国Amazonで購入することにしました。

本体の色がSPIKEは黄色なの対してInventorは青系の色となっています。またプログラム作成用ツールもSPIKE用とInventor用は別になっています。SPIKE用は日本語化されているのですがInventor用は日本で発売されていないこともあってか日本語化はされていません。
最近は小学校でも英語教育が必須化されたりしていますが、遊びの中で英語に触れる(LEGOなので殆ど言葉を利用することはありませんがw)ことが自然と英語を学ぶ良い方法なのかと思ったりします。

プログラミング用言語としてはScratchベースのブロックプログラミングとPythonが利用できます。PythonはMicroPythonなのでthreadingは入っていませんし、ローレベルのAPIである_threadも利用することはできません。利用できるのは非同期 I/O スケジューラuasyncio(非同期処理を扱うための標準ライブラリasyncioのサブセット)となるのでコールチンを利用した非同期処理のコードを書くことは可能となります。

ただ、ここで大切なのはマルチタスクやマルチスレッド、非同期処理といった概念ではなく複数の処理があたかも同時に処理されているように見えることが子供達にとっては大切だということだと思います。

MINDSTORMSでPythonは必要か?


Scratchでプログラムを作成する場合、まずはゲーム的なプログラムを作ることが多いと思いますが、その場合それぞれのスプライト用にプログラムを作成する考え方は直感的ですし、それを動かす場合に非同期処理(ScratchはNode.jsをベースに開発されているのでシングルスレッド・シングルプロセスが基本)を意識せずにマルチスレッド的な動作が自然と実践できるScratchの考え方は非常に優れていると思います。

さてそれではMINDSTORMSをPythonでプログラムする場合マルチスレッドは必要になるのかな?という疑問が出てきます。
ロボット工学は畑違いですが、MINDSTORMSのプログラムを作っていて感じるのはマルチスレッドよりもステートマシンでプログラムした方がすっきりとできるかなという感じです。

MINDSTORMS用のScratchは必要なブロックもきめ細かく用意されているのであえてPythonでプログラムを作成する必要はないかなと思います。Pythonを使ってプログラムしたい方はプログラミング言語を勉強したい方が取り組むべきかなと感じました。

Pythonジェネレータベースのコールチン


ということでまずはジェネレータベースの非同期処理を作ってみました。

Scratchの場合はスプライトという単位でプログラムが分けられますが、MINDSTORMS用のPythonではファイルの分割は出来なそうです。ある程度の規模のプログラムになるとできればファイル分割できた方が管理がやりやすいのですがね。

ノンプリエンティブなマルチタスクになりますので適時「yield True」でCPUを解放するのがポイントになります。またMainの__done()は非同期処理の対象外になっているので、ここで長時間CPUを占有すると非同期処理に影響を与えるので極力小さい処理にする必要があります。

LEGOでの注意事項としてはモータ制御はstart、stopで行うようにしてmoveは(同期処理になるので、短い距離では問題ないと思いますが)極力使用しないように、またセンサーのwait系も使用しないようにします。
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

# オブジェクトの定義
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_START = 1
STATE_EXIT = -1

# ジェネレータクラス
class Generator1:
    def __init__(self):
        self.__distance = -1
        self.__loop = True

    @property
    def distance(self):
        return self.__distance

    def stop(self):
        self.__loop = False

    # ジェネレータ関数本体
    def loop(self):
        while self.__loop:
            yield from self.__done()
        yield False

    def __done(self):
        self.__distance = distance_sensor.get_distance_cm()
        print(self.__distance)
        yield True
        if self.__distance and self.__distance < 15:
            print("The wall is near!")
            yield True

# メイン処理
class Main:
    def __init__(self):
        self.setup()
        self.loop()

    def setup(self):
        # ジェネレータの登録
        self.generators = []
        self.distance_sensor = Generator1()
        self.generators.append(self.distance_sensor)
        # ジェネレータ関数の登録
        self.generatorLoops = []
        for g in self.generators:
            self.generatorLoops.append(g.loop())

    def loop(self):
        while self.state != STATE_EXIT:
            if hub.right_button.is_pressed():
                for i, g in enumerate(self.generators):
                    g.stop()
                    while (next(self.generatorLoops[i])):
                        pass
                self.state = STATE_EXIT
            else:
                # ジェネレータの次の要素を取得(処理の再開)
                for loop in self.generatorLoops:
                    next(loop)
                # 自身の処理
                self.__done()
        
    def __done(self):
        print("Main keep alive!")

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

2021年9月23日木曜日

Raspberry Pi ZeroでHeadphonesが無くなった

最近こんなの

Adeept Robotic Arm Kit for Arduino

こんなの

「Petoi Bittle」(ペトイ・ビトル)

を購入して遊んでいました。








両方共にArduino互換ボードでサーボモーターを駆動する物ですが基本的なプログラムは提供されるのでとりあえず組み立てるだけで動かすことができます。

Arm Kitの方は仕組みは簡単なので工夫すればいろいろとプログラムをして遊べそうです。ただバッテリーが18650を2本とちょっと特殊ですが、バッテリーがなくてもUSBからの給電でも動くので無理してバッテリーを購入する必要はないとおもいます。なをバッテリーを購入するときは充電器も専用なものが必要となるのでご注意下さいね。

Bittleの方は赤外線リモコンもついていて組み立てればすぐに遊ぶことができます。
提供されるプログラムですぐに動かすことができますが、スムーズに歩くようにさせるには調整が必要になるようです。ビデオ等ではスムーズに動いているようですが、この写真のように顔をつけていない物がほとんどです。
ちょっと調整をしてみましたが思ったようにうまく動かすことができません。想像ですが提供されるプログラムは顔の部分がついていない状態で調整されているのではないかなと思われます。顔がつくことで重量バランスが違ってくるので、単純なキャリブレーションで調整できずにプログラム側の調整も必要になるのではないかなと。

ということでBittleのプログラムをざっと眺めてみましたが、各サーボモータの角度を持つフレームを複数用意してループさせることで動作をさせるという形式でした。
うまく歩かない場合はこの角度を微調整していくことでよりスムーズに動作させることが可能になります。とはいえ、なかなか時間がかかる作業になりますので今回は見送り、よたよたと動くことで満足することにしました。

といったことをやっていて、そういえばRAPIROをしばらく動かしていないということに気が付きました。

引っ張り出して来て1年ぶり程に動かしたら、どうも調子が悪いようです。

MicroSDカードでエラーが発生


原因を調べてみると組み込んだRaspberry Pi ZeroのMicroSDカードでエラーが発生しているようです。あちこちのデモで使用した時にきちんとShutdownせずに電源を切っていたのが原因かもしれません。せっかくShutdownスイッチを付けたのですが撤収時にバタバタしてついつい電源をプチンと切ることが多かったのかもしれません。
fsckで修復してみましたがやはりファイルシステムが破損しているようです、ログインがパスワードエラーで弾かれてしまいます。

出来る範囲で必要なファイルをサルベージしてからMicroSDカードをフォーマットしなおしてRaspberry Pi OSからインストールです。

bcm2835 Headphonesがなくなった


Raspberry Pi Zeroにはヘッドフォン用ジャックは付いていませんがHDMIで音声出力も可能にはなっています。また、RAPIROに組み込んで使うときはPWM Audioを利用してスピーカーから音声を出力していました。
ということでOSインストール後音声の出力先を3.5mm Jackに変更しようとraspi-configを実行してみたのですがHDMIしか表示されません。どうやらOSがStretchからBusterに変わったのが原因のようです。ハードウエア的についていない物は表示しないということでしょうか?

config.txtの修正で解決


ネットを検索すると同じような現象でこまっていた方もいるようで、こちらのサイトに解決策が上がっていました。
以下の2行を/boot/config.txtの最後に追加することでGPIO 12と13でPWM Audioが利用できます。(一般的にはGPIO 13と18のペアが利用されますがGPIO 18がRAPIROとの接続ケーブルで使用しているのでペアを変更して利用しています)
dtoverlay=audremap,enable_jack=on
dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4

Google Blogerにコピーボタン(execCommand不使用)を追加しました。

 日本ではBlogerが不人気なようです。 完全無料、広告表示なし、https標準対応、25年以上継続的に運用されてきたサービスなんですが、日本では使っている人が少ないようです。 とりあえず始めるのは簡単で、カスタマイズしようと思えば知識は必要ですが可能という強力なサービスだと思...