Pythonによる高水準なstuduino制御ライブラリ設計日記#1

# studuino(スタディーノ)とは
**studuino**は株式会社アーテックから発売されている、各種センサ・LED・ブザー・DCモータ・サーボモータをコネクタの抜き差しだけで接続できる**arduino互換基盤**です。studuinoと書いて**スタディーノ**と読みます。
一般的には、このarduino互換基盤に各種パーツおよびパーツを自由に配置できる組み立てブロック(アーテックブロック)がセットになった、子供向けのお手軽ロボット制作＆プログラミング環境を指します。**アーテックロボ**とも呼ばれています。
接続するパーツの組み合わせについて幾つかの制約はあるものの、最大８個のサーボモータを同時に接続できるため、複雑な動作をするロボットも比較的容易に構築することのできるすぐれものです。
なお、デフォルトのプログラミング環境として、ブロックプログラミングで有名なMITのscratchをベースにした無料のプログラミング環境が用意されています。
もちろんarduino互換基盤ですので、C/C++のサブセットのようなarduino言語で低水準の制御プログラムをゴリゴリ書き下すことも可能ですが、その仕事は上記のscratchベースのブロックプログラミング環境にうまく隠蔽されおり、子供たちは裏方の言語トランスレータの仕事を意識することなくロボットを制御するプログラムをマウス操作だけで組むことができます。

## ブロックプログラミング環境
scratchを始めとするブロックプログラミング環境には、プログラミングという行為に初めて触れる子供の興味を惹きつける多くの視覚的直感的魅力があるものの、汎用のプログラミング言語と比べた場合、抽象化能力が低いため処理が複雑になればなるほど冗長にならざるを得ず、同時に視認性が急激に悪化するという抜き差しならない欠点を抱えています。
そもそもが子供向けにデザインされたものですから当然といえば当然ではありますが、一定のレベルを超えた複雑なプログラムをブロックプログラミングによって記述するのは大昔のBASICのように力技に頼らざるを得ず、現実的な選択肢とはいえません。

## pystuduino
幸いなことにアーテックさんがPython用のstuduino制御ライブラリ**pystuduino**をオープンソースとして公開してくれています。
https://github.com/artec-kk/pystuduino
このライブラリを用いれば、studuinoとリアルタイムに通信することができ、すべてのパーツをPythonから制御することができるようになります。

### pystuduinoの導入
以下、Windows環境での運用を前提に、公開されている最新バージョンであるpystuduino-1.0.0について(あくまでもいちユーザの視点から)解説します。随所に間違いがあるかも知れませんので、詳しい方がおられましたらご指摘いただけると幸いです。

pystuduinoのインストールはpipでもできますが、後述するファームウェアの問題もありアーテックさんの上記リポジトリを引っ張ってくる方法が確実と思われます。なお、依存ライブラリとして**pyserial**と**numpy**と**matplotlib**が必要です。

基盤とのシリアル通信が必要となるため、アーテックさんの下記ページから事前にUSBドライバをインストールしておきましょう(すでにブロックプログラミング環境がインストールされている場合には不要です)。
https://www.artec-kk.co.jp/studuino/ja/studuino.php

ドライバをインストールし、PCと基盤をUSBで繋げてやると、Windowsの**デバイスとプリンター**の項目にCOMポートが表示されるはずです。COM3やCOM4などと表示されていると思いますが、このポート番号はUSBケーブルを挿すUSBポートによって変わってくるようなので、面倒くさいですが基盤を繋ぐたびにポート番号を確認しておきましょう。

pystuduinoを使ってstuduinoを制御する場合、制御対象の基盤にファームウェアを事前に書き込んでおく必要があるようです。
おそらく、`python -m studuino.upload COM3`のような感じで出来るはずですが、当方の環境では何度やってもファームウェアのイメージと思われるdata/firmware.hexが見つからないというエラーが出ますので、そういう場合には上記リポジトリから引っ張ってきたソースツリーのuploaderというディレクトリから、`python upload.py COM3`などとして基盤にファームウェアを書き込んでやりましょう。

以上でstuduinoをPythonから操る手筈が整いました。

### pystuduinoの基本的な構造
ソースを読む限り、簡潔にまとめられたライブラリという印象を受けます。
基盤に対する接続の開始と終了は、start()とstop()という２つの関数で行います。
LEDやセンサ類やモータ類などstuduinoに接続するすべてのパーツはPartクラスを継承するオブジェクトとして表現されます。
センサパーツはすべてSensorクラスを継承しています。センサには大きくアナログセンサ(光センサ等)とデジタルセンサ(プッシュスイッチ等)の２種類があり、それぞれAnalogSensorクラスとDigitalSensorクラスを直接の親として持ちます。なお、Accelerometer(加速度センサ)はSensorクラスを直接の親として持ちます。
シンプルでわかりやすい、自明なクラス構造といえます。

パーツを接続するコネクタはConnectorクラスとして表現され、このConnectorのインスタンスをPartクラスを継承するオブジェクトのattachメソッドに放り込んでやることでパーツとの接続が確立される仕様です。これらConnectorインスタンスは、A0,A1,D2,M4などといった実機のコネクタ名と同一名称の定数データとして事前に定義されています。


### pystuduinoの使用例
```python
# A0コネクタに接続されたLEDをブリンクさせ、A3コネクタに接続されたブザーも連動して鳴らすスクリプト
from studuino import *
from time import sleep

# COMポートを指定して接続
start('COM3') 

# パーツのインスタンスを生成
led, buzzer = LED(), Buzzer() 

# パーツをコネクタにアタッチする
led.attach(A0)
buzzer.attach(A3)

for i in range(10):
    if i: sleep(0.2)
    led.on()
    buzzer.on(0, 5)
    sleep(0.2)
    led.off()
    buzzer.off()

# 接続終了
stop()    
```

# 高水準wrapperの必要性
このようにお手軽な感じで記述することができるpystuduinoですが、提供されている機能は(当たり前のことですが)ハードに寄り添う低水準なものです。
そして、これも当たり前のことですが、studuinoの実機がなければプログラムの動作確認はできません。

より高水準な制御機構を提供し、かつ、実機がなくても視覚的に動作イメージを確認できるようなGUIのシミュレータを備えたstuduino制御ライブラリがあれば便利です。
探しても無いものは自前で作ればいいだけですので、pystuduinoの高水準wrapperとして**pgkstuduino**の開発が始まりました。
https://github.com/PGkids/pgkstuduino

現在ベータ版のため仕様は流動的ですが、pgkstuduinoの提供する機能はおおよそ以下のようなものです。

- 表向きはpystuduinoを完全に隠蔽
- 実機モードと非実機(シミュレート)モード
- クロージャとスレッドを用いた、自由に結合できる制御ジョブオブジェクト
- プログラムされたパーツの動作を視認できるGUIのシミュレータ/モニタ
- 実機モードであっても非実機モードのパーツとして扱われる仮想コネクタ

## 適用例

```python:led-blink.py
# A0,A1,A2に接続された3つのLEDを異なる発光パターンで同時にブリンクさせる並列処理の例
import pgkstuduino as ps
from sys import argv

ps.st_set_real('-devel' not in argv) #実機モードの設定

# studuinoとの接続
ps.connect(3) ## もしくは ps.connect('COM3')

# パーツの生成と接続
led1,led2,led3 = ps.mkpart('LED:A0/A1/A2')

# 並列ジョブの生成
job = ps.par(led1.job_blink(n=10, on=0.4, off=0.1),
             led2.job_blink(n=10, on=0.2, off=0.3),
             led3.job_blink(n=5, on=1, off=0.2))

# ジョブの開始と終了の待機
job.start().join()

# 接続終了
ps.disconnect()
```

`python led-blink.py` とすると次のようなパネルが開きます。
※ 正確には、connect()関数が呼ばれた時点でGUIパネルが生成されます。

もしも実行中に無限ループやスレッドのロック等に陥った場合は、このパネルのABORTボタンを押すことで強制的にプロセスを終了させることができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/340758/1499395d-aad0-a4e6-7206-55e146cde167.png)

実機に接続していない場合は`python led-blink.py -devel`とすると非実機モードで動作します。
もしくは、

```python
led1,led2,led3 = ps.mkpart('LED:A0*/A1*/A2*')
```
のようにコネクタ名の直後にアスタリスクをつけることで、仮想コネクタに接続された非実機パーツとして認識させることもできます。

```python
job.start().join()
ps.disconnect()
```
ジョブが別スレッドで実行され実行完了するまでjoinメソッドがブロックされた後、接続は終了となります。
接続が終了すると、GUIのパネルは次のように変化します。
ABORTボタンがEXITボタンに変化すれば正しくdisconnect()まで到達した証拠です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/340758/c9076a42-ff2a-118e-849b-2e9422797f5d.png)

接続終了と同時にこのパネルも終了させたい場合は、次のようにdisconnect()に真を渡してください。

```python
# 接続終了と同時にGUIパネルを終了させる場合
ps.disconnect(True)
```

## 今回はこのへんで…
長くなってきましたので、今回はこれで終わりとし、細かな仕様や具体例などは順を追って書いていきたいと思います。
最後に、アーテックロボをゲームパッドで操作する実験動画と、そのソースコードを載せておきます。
https://www.youtube.com/watch?v=PrxFf71gqIk

```python:test-js.py
# -*- coding: utf-8 -*-

from pgkstuduino import *
import pygame as pgm
from time import sleep
from sys import argv

JS_QUITBTN  = 7  #終了ボタン
JS_ACCELBTN = 3 #アクセルボタン
JS_RACCELBTN = 1 #反転アクセルボタン

pgm.mixer.init(44100, -16, 2, 2048)
sound_up   = pgm.mixer.Sound('js-up.wav')
sound_down = pgm.mixer.Sound('js-down.wav')

pgm.init()
pgm.joystick.init()
try:
    js = pgm.joystick.Joystick(0)
    js.init()
except pgm.error:
    raise Exception('Joystick not found')

st_set_debug('-debug' in argv)
st_set_real('-devel' not in argv)
connect(4)

led_left,led_accel,led_right = mkpart('LED:A0/A1/A2')
dc_left,dc_right,buzzer = mkpart('DCMotor:M1/M2,Buzzer:A3')
direction = None # 'left' or 'right'
back_job  = None

powers = [(50,30),(60,30),(70,40),(80,40),(90,50),(100,50)]
power_index = len(powers) - 1
dc_left.power = dc_right.power = powers[power_index][0]

# J.S.Bachの教会カンタータの有名な旋律(BWV147)
SONG_DATA = (0,2,4,7,5,5,9,7,7,12,11,12,7,4,0,2,4,5,7,9,7,5,4,2,4,0, -1,0,2,-5,-1,2,5,4,2,4,
            0,2,4,7,5,5,9,7,7,12,11,12,7,4,0,2,4, 2,7,5,4,2,0,-5,0,-1,0,4,7,12,7,4,0,4, 6,
            7,-5,-3,-1,2,1,1,4,2,2,5,4,5,2,-3,-7,-5,-3,-2,7,5,7,4,1,-3,-1,1,
            2,5,4,5,9,7,7,10,9,9,14,13,14,9,5,2,4,5, 10,9,7,5,4,2,-3,2,1,2,5,9,14,9,5,2)

def mk_back_job():
    cnt,length = 0,len(SONG_DATA)
    def perform_song():
        nonlocal cnt
        current_note = 55 + SONG_DATA[cnt%length] #カンタータ原曲どおりト長調で
        cnt += 1
        buzzer.noteon(current_note)
        sleep(0.15)
        buzzer.noteoff()
    return par(led_accel.job_blink(on=0.2,off=0.3),
               rep(mkjob(perform_song), interval=0.05))

status = create_label('初期状態です')
power_status = create_label('')
def set_power_status():
    power_status(f'DCモーター出力: {powers[power_index][0]}%')

set_power_status()

while True:
    e = pgm.event.wait()
    if e.type==pgm.JOYBUTTONDOWN and e.button==JS_QUITBTN:
        status('ロボットとの通信を終了します')
        break
    elif e.type==pgm.JOYAXISMOTION and e.axis==0:
        # 十字ボタンX軸 (方向制御)
        hi,lo = powers[power_index]
        if e.value > 0.5: #############  右折   ################
            dc_left.power,dc_right.power = hi,lo
            led_right.on()
            direction = 'right'
        elif e.value < -0.5: ##########  左折   ################
            dc_left.power,dc_right.power = lo,hi
            led_left.on()
            direction = 'left'
        else:                ##########  直進   #################
            dc_left.power,dc_right.power = hi,hi
            if direction   is 'left':  led_left.off()
            elif direction is 'right': led_right.off()
            direction = None

    elif e.type==pgm.JOYAXISMOTION and e.axis==1:
        # 十字ボタンY軸 (パワー制御)
        if e.value > 0.5:
            if power_index > 0: power_index -= 1; sound_down.play()
            else: continue
        elif e.value < -0.5:
            if power_index < len(powers)-1: power_index += 1; sound_up.play()
            else: continue
        else:
            continue
        hi,lo = powers[power_index]
        if direction   is 'left':  dc_left.power,dc_right.power = lo,hi
        elif direction is 'right': dc_left.power,dc_right.power = hi,lo
        else:                      dc_left.power = dc_right.power = hi
        set_power_status()
            
    elif e.type==pgm.JOYBUTTONDOWN and (e.button==JS_ACCELBTN or
                                        e.button==JS_RACCELBTN):
        fwdp = e.button==JS_ACCELBTN
        dc_left.state = dc_right.state = 'fwd' if fwdp else 'back'
        if fwdp:
            led_accel.on()
            status('出発進行～！ THE VOICE OF ROCK! GLENN HUGHES!!')
            pgm.mixer.music.load('js-drive.wav')
        else:
            back_job = mk_back_job().start()
            status('バックします。ご注意ください！')
            pgm.mixer.music.load('js-back.wav')
        pgm.mixer.music.set_volume(1.0)
        pgm.mixer.music.play(-1)
    elif e.type==pgm.JOYBUTTONUP and (e.button==JS_ACCELBTN or
                                      e.button==JS_RACCELBTN):
        dc_left.state = dc_right.state = 'brake'
        status('停車')
        if back_job: 
            back_job.cancel().join()
            back_job = None
        else:
            led_accel.off()
        pgm.mixer.music.stop()

disconnect()
```