PaPeRo i をScratch3.0で制御する-その2-LEDと頭の制御

 PaPeRo i をScratch3.0で制御する-その1-接続・発話・ボタンの続きです。今回はLEDと頭移動を制御するブロックを追加しました。

シーケンスをScratchのブロックで表現する方法

 PaPeRo i の額・口・耳・頬・胸のLED制御および頭移動のAPIは、可変長の点灯/移動シーケンスデータを受け取り、それの1回実行または繰り返し実行を指定できる様になっています。例えば額のLEDを「緑1秒→赤0.5秒→オレンジ2秒→消灯0.5秒」で繰り返し点灯させる、などです。こういったシーケンスをScratchのブロックで表現できる様にする方法として思いついたのは、

(1) 条件分岐や繰り返し実行の様なC型ブロックをシーケンス実行ブロックとし、内部に「緑1秒」の様なシーケンスデータブロックを複数入れる様にする。
(2) 「緑1秒」の様な個々のシーケンスデータブロックに、「シーケンスを次に続ける」「以上のシーケンスを実行する」の選択肢を設ける

でした。本質的には(1)が近いと思うのですが、最低2個のブロックを正しく組み合わせないと正しく動作させられないのに対し、(2)であればブロック1個で(デフォルトの選択肢を変えなければ)正しく動作させられるようにできるので、(2)を採用することにしました。
 なおいずれも、例えば(1)では「額LEDシーケンス実行ブロック内に口LEDシーケンスデータを入れる」、(2)では「異なるLEDのシーケンスブロックを続ける」といった間違ったプログラムが作れてしまう欠陥がありますが、この点で(1)と(2)に優劣は無いように思われます。

シーケンスブロックの実装

 ブロックプログラム実行時には前回作成したScratch3PaperoiBlocksクラス (scratch-vm\src\extensions\scratch3_paperoi\index.js) のメソッドが、ブロック定義に従い呼び出されます。LEDと頭移動では次に続ける指定ブロック(「にしたら次に」)の複数のシーケンスデータをまとめて、実行するタイミング(「にする命令を送る」/「にするくり返し命令を送る」)でAPIに渡す必要がありますので、「次に続ける」データを保持しておく必要があります。このためにメンバ変数_ledSeqを追加しました。以下は額LEDにかかわる部分を抜き出したものです。

class Scratch3PaperoiBlocks {

    ...中略...

    constructor (runtime) {
        this.runtime = runtime;
        this._paperoDic = {};
        this._currentLed = '';
        this._ledSeq = [];
        this._LED_SEQ_MAX = 26*2;
        this._headSeq = [];
        this._HEAD_SEQ_MAX = 64;
    }

    ...中略...

    getInfo () {
        return {
            id: 'paperoi',
            name: 'PaPeRo i',
            blockIconURI: blockIconURI,
            blocks: [

                ...中略...

                {
                    opcode: 'setForeheadLed',
                    text: formatMessage({
                        id: 'paperoi.setForeheadLed',
                        default: 'パペロ [ID] のおでこを [SEC] 秒間 [VAL] [SET_TYPE]',
                        description: '指定パペロの額LEDセット'
                    }),
                    blockType: BlockType.COMMAND,
                    arguments: {
                        ID: {
                            type: ArgumentType.STRING,
                            defaultValue: '1'
                        },
                        SEC: {
                            type: ArgumentType.NUMBER,
                            menu: 'sec',
                            defaultValue: '1'
                        },
                        VAL: {
                            type: ArgumentType.STRING,
                            menu: 'rgyLed',
                            defaultValue: 'G3'
                        },
                        SET_TYPE: {
                            type: ArgumentType.STRING,
                            menu: 'setTyp',
                            defaultValue: 'once'
                        },
                    }
                },

               ...中略...

            ],
            menus: {
                buttons: this.BUTTONS_MENU,
                rgyLed: this.RGY_LED_MENU,
                redLed: this.RED_LED_MENU,
                whiteLed: this.WHITE_LED_MENU,
                chestLed: this.CHEST_LED_MENU,
                num321: this.NUM321_MENU,
                setTyp: this.SET_TYP_MENU,
                moveAbsXpos: this.MOVE_ABS_XPOS_MENU,
                moveAbsYpos: this.MOVE_ABS_YPOS_MENU,
                moveRelXpos: this.MOVE_REL_XPOS_MENU,
                moveRelYpos: this.MOVE_REL_YPOS_MENU,
                moveSpeed: this.MOVE_SPEED_MENU,
                sec: this.SEC_MENU,
                moveTyp: this.MOVE_TYP_MENU,
                pauseTyp: this.PAUSE_TYP_MENU,
                event: this.EVENT_MENU,
            }
        };
    }

    ...中略...

    _setLed(ledtyp, val, args) {
        const id = Cast.toString(args.ID);
        var papero = this._paperoDic[id]
        if (!papero) {
            return false;
        }
        var sec = args.SEC;
        var settyp = args.SET_TYPE;
        if ((0 >= sec)||(10 < sec)) {
            sec = 0.1;
        }
        if (this._currentLed != ledtyp) {
            log.log('ERROR: led type changed. dicard:' + this._currentLed + ' seq:' + this._ledSeq);
            this._currentLed = ledtyp;
            this._ledSeq = [];
        }
        else if (this._ledSeq.length >= this._LED_SEQ_MAX) {
            log.log('ERROR: led sequence too long. dicard:' + this._currentLed + ' seq:' + this._ledSeq);
            this._currentLed = ledtyp;
            this._ledSeq = [];
        }
        this._ledSeq.push(val);
        this._ledSeq.push(String(Math.floor(sec*10)));
        if (('once' == settyp) || ('repeat' == settyp)) {
            var repeat = ('repeat' == settyp);
            papero.send_turn_led_on(ledtyp, this._ledSeq, repeat);
            this._currentLed = '';
            this._ledSeq = [];
        }
    }

    setForeheadLed (args) {
        var val = args.VAL;
        var res = this._setLed('forehead', val, args);
        return res;
    }

    ...中略...

}

module.exports = Scratch3PaperoiBlocks;

他のLEDおよび頭移動についても同じ様に実装しました。

選択メニューについて

 本来であればLEDの色はカラーパレットを表示して選択できる様にしたいのですが、少なくとも現状scratch-vmから使用できるブロックにはカラーパレットは無いようです。scratch-blocksにまで手を入れれば可能だと思われますが、ひとまずそれは今後の課題として、「緑3」「赤3」の様な文字列を選択する方法で実装することにしました。この選択メニューは

    get BUTTONS_MENU () {
        return [
            {
                text: formatMessage({
                    id: 'paperoi.buttonsMenu.L',
                    default: '左',
                    description: 'PaPeRo iの左ボタン'
                }),
                value: 'R'
            },
            {
                text: formatMessage({
                    id: 'paperoi.buttonsMenu.C',
                    default: '中',
                    description: 'PaPeRo iの中ボタン'
                }),
                value: 'C'
            },
            {
                text: formatMessage({
                    id: 'paperoi.buttonsMenu.R',
                    default: '右',
                    description: 'PaPeRo iの右ボタン'
                }),
                value: 'L'
            },
            {
                text: formatMessage({
                    id: 'paperoi.buttonsMenu.any',
                    default: 'どれか',
                    description: 'PaPeRo iのボタンどれか'
                }),
                value: 'any'
            }
        ];
    }

(ボタン選択、前回の再掲)
の様に記述するのですが、LED、頭位置、時間など種類が増えてくるとコードが大変冗長になります。そこで以下の様にstaticメソッド_menuDic()を追加してコンパクトに記述する様修正しました。

class Scratch3PaperoiBlocks {

    static _menuDic (info, idpre) {
        return {
            text: formatMessage({
                id: idpre + info[0],
                default: info[1],
                description: info[2]
            }),
            value: info[0]
        };
    }

    get BUTTONS_MENU () {
        const pre = 'paperoi.buttonsMenu.';
        const lst = [
            ['R', '左', 'PaPeRo iの左ボタン'],
            ['C', '中', 'PaPeRo iの中ボタン'],
            ['L', '右', 'PaPeRo iの右ボタン'],
            ['any', 'どれかの', 'PaPeRo iのボタンどれか'],
        ];
        const res = lst.map(info => 
            Scratch3PaperoiBlocks._menuDic(info, pre));
        return res;
    }

    // menu for forehead and mouth led (G3,G2,G1,R3,R2,R1,Y3,Y2,Y1,N)
    get RGY_LED_MENU () {
        const pre = 'paperoi.rgyLedMenu.';
        const lst = [
            ['G3', '緑3', '緑3'],
            ['G2', '緑2', '緑2'],
            ['G1', '緑1', '緑1'],
            ['R3', '赤3', '赤3'],
            ['R2', '赤2', '赤2'],
            ['R1', '赤1', '赤1'],
            ['Y3', 'オレンジ3', 'オレンジ3'],
            ['Y2', 'オレンジ2', 'オレンジ2'],
            ['Y1', 'オレンジ1', 'オレンジ1'],
            ['N', '消灯', '消灯'],
        ];
        const res = lst.map(info => 
            Scratch3PaperoiBlocks._menuDic(info, pre));
        return res;
    }

イベントの真偽ブロック

 Scratch3.0には「<>まで待つ」というブロックがあり、「マウスが押された」「キーが押された」といったイベントの真偽ブロックと組み合わせてイベントを順に待つようなプログラムを作ることが出来ます。
 前回イベントはハットブロックのみ実装しましたが、今回このイベントの真偽ブロックを追加しました。マウスやキーボードのキーの場合には、ハットブロックではOFF->ONになったタイミングのみtrueを返し、真偽ブロックではキーが押されている間trueを返すという動作になりますが、今のところPaPeRo i のイベントはすべて元がイベントなのでハットブロックと真偽ブロックは動作が同じです。但し、ハットブロックと真偽ブロックが同時に使われた場合に備え、イベントを保持しておく変数はそれぞれ必要になります。

動作確認

 IBM Cloudのアプリを更新しましたのでお試しください。保存したブロックがLoadできなかった不具合は修正しました。保存ブロックをLoadする前にPaPeRo i 拡張機能を追加しておく必要がありますのでご注意ください。
 順次機能追加を行う予定なので、もし記事内容と異なる場合にはご容赦ください。