Scratch × Firmata ×Python 自作ブロック ~ Arduino制御

Scratch × Firmata ×Python 自作ブロック ~ Arduino制御

 今回はScrach 3.0の拡張機能を作ることで、自作のブロックを用意できるようなので試していきたいと思います。

FirmataやPythonの詳細な使い方に関しては今回の記事では説明しないので以前の記事1・以前の記事2を合わせて読んで頂けるとよりわかりやすいと思います。

最初はScratch側から「Jony-Five」というパッケージを使ってFirmataで通信したかったのですが、パッケージをインストールしてインポートすると、依存関係でScratchが起動しなくなってしまい、どうしても解決できませんでした。なのでScratchからは簡単にURLをたたくだけにして、Pythonサーバを挟んでArduinoを操作する少し遠回りな方法で解決することにしました。

記事内容がとても長くなっていますが、ブロックの作成までのプロセスを知りたい方は、第4項ブロックの説明まで読んで頂ければ十分です。

環境

  • OS 名:Microsoft Windows 10 Home
  • モデル:Surface Laptop 3
  • システム:x64-ベース PC
  • Node.JS バージョン:14.06.0 LTS (3/28時点の推奨版)

準備&動作確認

私の場合は公式サイトに載っている方法のままで動作したので、その手順に則って必要な操作を載せていきます。

細かい説明が必要な場合は公式サイト↓を確認してください。本記事では途中で名前の付け方やブロックの動作などオリジナルな部分が出てくるので両方を見比べるとわかりやすいかもしれません。

ja.scratch-wiki.info

ダウンロード&起動

まずはコマンドプロンプトでGit Hubからコードをもらってきて、そのあとnpm linkします。

私は知らなかったのですがnpm linkした後に、npm installとするとリンクが削除されてしまうようで、Jony-Five導入テストの時にこれで躓いてしまいました。

git clone --depth 1 https://github.com/llk/scratch-vm.git
git clone --depth 1 https://github.com/llk/scratch-gui.git

cd scratch-vm
npm i
npm link
cd ../scratch-gui
npm i
npm link scratch-vm

ここで

npm start

を実行するとScratch 3.0が、使っているPC上で起動されるので

http://localhost:8601/

↑にアクセスするとScratch 3.0の画面がでてくると思います。

この時点で起動しなければ前の操作がうまくいっていないので公式サイトなどをよく読んでやりなおしてください。

画像の用意

このような800x37280x80の2つの画像を用意します。

公式でサイズが指定されていたのでそれに倣って画像を用意しました。

左側が拡張機能一覧(下に画像あり)でサムネイルになる大きい方の画像と右側が小さいアイコンになります。

 

それぞれにHaLake_blocks.pngHaLake_blocks_small.pngのように名前を付けておきます。

ファイルの配置

拡張機能を作るにはフォルダの追加やソースコードの追加・追記が必要なので以下のような構成にする操作をしていきます。

フォルダやファイルを全て表示するととんでもない量になるので操作が不必要な部分は省略して見やすくしています。

  • 注意:以下の説明ではscratch-guiscratch-vmの大きく2つのフォルダの中身を追加・追記していきます。 その際、それぞれのフォルダの中にも同じ名前のフォルダが存在しているので、編集する方を間違えないように注意してください。

最後に以下のような構成になっていればソースの配置は問題ないと思います。

scratch-gui
├─src
│  ├─lib
│  │  ├─libraries
│  │  │  └─extensions <-①
           ├HaLake_blocks.png       *追加
           ├HaLake_blocks_small.png *追加
           ├index.js                *追記

scratch-vm
├─src
│  ├─extension-support <-②
│  │ ├extension-manager.js        *追記
│  │
│  ├─extensions <-③
│  │ ├─scratch3_halakeblocks      *フォルダ作成
│  │   ├index.js                  *追加

操作を説明します。

①extensions(scratch-gui)

パスはscratch-gui\src\lib\libraries\extensionsとなっているので、このextensionsフォルダに先ほど用意した2つの画像を追加します。

その後、extensionsフォルダ内にあるindex.jsに追記していきます。

公式の手順では19行目に以下を追記と書かれているので追記していきます。

scratch-gui\src\lib\libraries\extensions

import halakeblocksImage from './HaLake_blocks.png'
import halakeblocksInsetImage from './HaLake_blocks_small.png'

画像のインポートなので、画像の名前を変更している方は、それに合わせて書き換える必要があります。

次に52行当たりにexport default [とあるのでその中に以下のように追記してきます。

省略
export default [
//##############追記(ここから)
    {
        name: (
            "HaLake Blocks"
                description="Name for the 'HaLake Blocks' extension"
                id="gui.extension.halakeblocks.name"
            />
        ),
        extensionId: 'halakeblocks',
        iconURL: halakeblocksImage,
        insetIconURL: halakeblocksInsetImage,
        description: (
            "HaLake extension"
                description="Description for the 'HaLake Blocks' extension"
                id="gui.extension.halakeblocks.description"
            />
        ),
        featured: true
    },
//##############追記(ここまで)
    {
        name: (
省略

ここではブロックをしまっておくための箱を作るようなイメージですね。

用意した画像2つは、他の箱と区別するための装飾、その説明をソースコードで追記していったことになります。

追加・追記したフォルダの大元をたどるとscratch-guiとなっているので、見た目(GUI)の設定をしているとわかります。

実際に設定されるとこのような見た目になります。(全ての操作を終えた後の写真ですが。)

とりあえず、これで①は完了です。

②extension-support(scratch-vm)

今度はscratch-vmで作業をしていきます。

パスはscratch-vm\src\extension-support\extension-manager.jsこんな感じになっているのでextension-manager.jsに少しだけ追記しておきます。

scratch-vm\src\extension-support\extension-manager.js

ソースの20行目あたりに

halakeblocks: () => require('../extensions/scratch3_halakeblocks'),

これを追記します。

次に作成する拡張機能の心臓部を読み込んでいるようです。

これでだけで②は完了です。

③extensions(scratch-vm)

scratch-vmの中のextensionsフォルダを編集していきます。

パスはscratch-vm\src\extensionsになるので、その中に新しくscratch3_halakeblocksというフォルダを作成します。

この名前はで追記したパスの名前と合わせる必要があるので注意してください。

画像の赤下線の部分がそれにあたります。

その中に新しくindex.jsを作成して以下を全コピペします。

scratch-vm\src\extensions\scratch3_halakeblocks\index.js
const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const log = require('../../util/log');
const nets = require('nets');

/**
 * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI.
 * @type {string}
 */
// eslint-disable-next-line max-len
const blockIconURI = 'data:image/png;base64,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';

/**
 * Icon svg to be displayed in the category menu, encoded as a data URI.
 * @type {string}
 */
// eslint-disable-next-line max-len
const menuIconURI = 'data:image/png;base64,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';


/**
 * Class for the new blocks in Scratch 3.0
 * @param {Runtime} runtime - the runtime instantiating this block package.
 * @constructor
 */
class Scratch3HaLakeBlocks {
    constructor (runtime) {
        /**
         * The runtime instantiating this block package.
         * @type {Runtime}
         */
        this.runtime = runtime;

        //this._onTargetCreated = this._onTargetCreated.bind(this);
        //this.runtime.on('targetWasCreated', this._onTargetCreated);
    }


    /**
     * @returns {object} metadata for this extension and its blocks.
     */
    getInfo () {
        return {
            id: 'halakeblocks',
            name: 'HaLake Blocks',
            menuIconURI: menuIconURI,
            blockIconURI: blockIconURI,
            blocks: [
                {
                    opcode: 'fetchURL',
                    blockType: BlockType.COMMAND,
                    text: 'URL [TEXT]',
                    arguments: {
                        TEXT: {
                            type: ArgumentType.STRING,
                            defaultValue: "http://192.168.x.xx:8000/?ON=0"
                        }
                    }
                },
                {
                    opcode: 'reqURL',
                    blockType: BlockType.REPORTER,
                    text: 'URL [URL]',
                    arguments: {
                        URL: {
                            type: ArgumentType.STRING,
                            defaultValue: "http://192.168.x.xx:8000/?READ=0"
                        }
                    }
                }
            ],
            menus: {
            }
        };
    }

    /**
     * Fetch URL.
     * @param {object} args - the block arguments.
     * @property {number} TEXT - the text.
     */
    fetchURL (args) {
        const text = Cast.toString(args.TEXT);
        fetch(text,
        {
            method: 'GET',
            mode: 'no-cors'
        });
    }

    /**
     * Request URL
     * @property {number} URL
     * @return {number}
     */
    reqURL (args){
        const ajaxPromise = new Promise(resolve => {
            nets({
                url: Cast.toString(args.URL)
            }, function(err, res, body){
                resolve(body);
               return body;
            });
        });
        return ajaxPromise;
    }
}

module.exports = Scratch3HaLakeBlocks;

公式の説明では、ソースコードのコピペ前に40x40の画像を用意して、指定の操作でbase64形式に変換したものを用意すると書かれているので、自前の画像を使いたい方はそちらを読んで所定の場所に書き込んでください。その際は下記URLを参照してください。

ja.scratch-wiki.info

  • 12行目のblockIconURIはブロック左のアイコン
  • 19行目のmenuIconURIは画面左端に出てくるアイコン

とりあえずこちらで用意したので面倒な方は何もしなくて大丈夫です。

これでやっと完成したので起動してみましょう。

コマンドプロンプトで必ずscratch-guiに移動して起動コマンドを打ち込んでください。

cd scratch-gui
npm start

これで実行できます。コマンドプロンプトCompiled successfully.と出ればひとまずは安心です。

http://localhost:8601/

↑にアクセスするといつものスクラッチの画面が出てくると思うので拡張機能を有効にしていきます。

ここまでできれば完璧です。

自分で操作してうまく動作しなかった方の場合は、私の動作したものをGit Hubにあげておくのでそちらを落として実行してみてください。

リポジトリ

github.com

github.com

↓のリンクを開くと、お試しでこちらが用意した拡張機能を使えるScratchが起動するようになっています。

m-shinoda.github.io

上記3つは次の記事で追加するブロックやコードが既に入っていますが、ここで追加したコードはそのままになっているので、無視して頂いて大丈夫です。

ブロックの説明

ブロック自体の動作は、前の項ので載せたソースコード

scratch-vm\src\extensions\scratch3_halakeblocks\index.jsの中身で決めます。

43行目あたりのgetInfo(){省略}で大まかな挙動や形(ブロックの種類や入力欄の作成)などを決めています。

そのblocksの中で2つのブロックが定義されていてopcodeで個別の名前が付けられています。

これで各ブロックが定義されるので既存、又は自作するほかのブロックと名前がかぶってはいけない、など公式で説明があるので以下を参照してください。

ja.scratch-wiki.info

77行目あたりより下では、ブロックの細かい挙動が記述されています。

それぞれopcode名を関数名にすることでブロックに適用するようです。

 /**
     * Fetch URL.
     * @param {object} args - the block arguments.
     * @property {number} TEXT - the text.
     */
    fetchURL (args) {
        const text = Cast.toString(args.TEXT);
        fetch(text,
        {
            method: 'GET',
            mode: 'no-cors'
        });
    }

    /**
     * Request URL
     * @property {number} URL
     * @return {number}
     */
    reqURL (args){
        const ajaxPromise = new Promise(resolve => {
            nets({
                url: Cast.toString(args.URL)
            }, function(err, res, body){
                resolve(body);
               return body;
            });
        });
        return ajaxPromise;
    }

これら二つのブロックは、次にPythonサーバを立てるのでその通信のために用意されています。

Fetch URLでは、ブロックのテキスト入力欄に書かれたURLに対してGETリクエストを送っているだけのスタックブロックです。

主にArduinoピンのON・OFFなどのアウトプットに使われます。

Request URLでも、ブロックのテキスト入力欄に書かれたURLに対してGETリクエスト送信していますが、レスポンスのbodyを返す値ブロックになっています。

主にArduinoピンのアナログ値の読み取りに使われます。

これら二つのブロックを使ってArduinoを制御していきます。

Firmata動作確認Lチカなど

pyfirmataを使うので事前にpipなどでインストールしておいてください。

  • 注意:以下で説明するコードはArduino UNO(一部の互換機も含む)でのみ動作確認と主にピン配列の最適化がされています。他のボードでの動作確認はとっていないので動作しない可能性があります。試す場合は自己責任でおねがいします。

またArduino UNOが接続されているポートも使うので事前に調べておく必要があります。

わからない方はarduino ポート 調べ方などとネットで調べると出てきます。

OSによって違う場合もあるので検索ワードにOS名も入れると確実かと思われます。

まずはコードを載せておきます。

# -*- encoding: utf-8 -*-

import json
from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse as p
import pyfirmata
import time

port = 'COM9'#各環境で書き換え必要
board = pyfirmata.Arduino(port)

#Digital
DPins_info = ['d:' + str(i)+ ':o' for i in [2, 3, 4, 5, 12]]#Digital使用の準備
DPins = [board.get_pin(i) for i in DPins_info]#Digital 2~5, 13 Pin

#Analog Read
it = pyfirmata.util.Iterator(board)#Analog Read 使用の準備
it.start()#Analog Read 使用の準備
[board.analog[i].enable_reporting() for i in range(0, 6)]#Analog Read A0~5 Pin

#Servo
SPins_info = ['d:' + str(i)+ ':s' for i in [6, 7, 8, 13]]#Servo使用の準備
SPins = [board.get_pin(i) for i in SPins_info]#Servo 6, 7, 8, 13 Pin 

#PWM*9, 10きかない?故障か確認
PPins_info = ['d:' + str(i)+ ':p' for i in [9,10,11]]#PWM使用の準備
PPins = [board.get_pin(i) for i in PPins_info]#PWM 9, 10, 11 Pin

def param_check(server, param):
    if 'ON' in param:
        pin_n = int(param['ON'][0])
        DPins[pin_n].write(1)

    elif 'OFF' in param:
        #/エラー箇所
        # p_info = 'd:' + param['OFF'][0] + ':o'
        # pin = board.get_pin(p_info)
        # pin.write(0)
        #/
        pin_n = int(param['OFF'][0])
        DPins[pin_n].write(0)


    elif 'READ' in param:
        pin_n = int(param['READ'][0])
        val = board.analog[pin_n].read()
        print(val)
        server.wfile.write(str(val).encode('utf-8'))

    elif 'SERVO' in param:
        ang = int(param['SERVO'][0])
        pin = int(param['PIN'][0])
        SPins[pin].write(ang)

    elif 'PWM' in param:
        pwm = float(param['PWM'][0])
        pin = int(param['PIN'][0])
        PPins[pin].write(pwm)
    
class Server(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            # #///Consolのエラー回避必要であればコメント外す
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.send_header('Access-Control-Allow-Origin', '*')#これがないとリクエストに答えられない。js側エラー内容( No 'Access-Control-Allow-Origin' header is present on the requested resource.)
            self.end_headers()
            #///

            print(self.path)                #例)/?ON=1
            q_path = p.urlparse(self.path)  #例)ON=1
            q_par = p.parse_qs(q_path.query)#例)1

            param_check(self, q_par)
            print()

        except Exception as e:  
            print("An error occured")
            print("The information of error is as following")
            print(type(e))
            print(e.args)
            print(e)
            print()
            
def run(server_class=HTTPServer, handler_class=Server, server_name='', port=8000):

    server = server_class((server_name, port), handler_class)
    server.serve_forever()

print('start')
run(server_name='', port=8000)

プログラムの一部書き換えが必要になります。

序盤の

port = 'COM9'#各環境で書き換え必要

私の場合はCOM9だったのでそのようになっていますが、各環境に応じてArduinoが接続されたポートに書き換えてください。

テスト用に回路を組んで動作するか確認できるようにします。

使っている抵抗は22kΩですが、330~10kΩかそれ以上であればたいていの場合大丈夫だと思います。抵抗値が高すぎるとLEDの光が暗めになってしまうので、光っているか確認しずらくなってしまうので注意しましょう。

細かい説明は後回しにして、実際に動かしてみましょう。

Pythonのコードをコピペしてserver.pyなどの名前で保存して、コマンドプロンプトで実行します。

python server.py

正常に動作すれば少し経つと、startと表示されて制御待機状態になります。

後は別のコマンドプロンプトでスクラッチの方も起動させてブロックを動作させます。

この時サーバを立ち上げているPCのIPアドレスも今後使うので調べておきましょう。

同じPC内でScratchとPythonサーバを起動している場合はIPアドレスlocalhostと指定しても大丈夫です。

ipconfig

わからなければ以下を参照してください。

パソコンのIPアドレスを確認する方法(Standard TCP/IP)

ディジタル出力

クラッチが起動したら拡張機能を読み込んで凹凸があるスタックブロックを作業スペースにドラッグ&ドロップしてテキストを書き換えましょう。

デフォルトでは

http://192.168.x.xx:8000/?ON=0

ローカルホストの場合(以下省略)

http://localhost:8000/?ON=0

と記述されているので192.168.x.xxの部分を先ほど調べたIPアドレスに書き換えましょう。

そして適当にブロックを組んで1秒おきにLチカをしていきます。

LEDが1秒おきに点滅していれば完璧です。

URL最後の数字でピンのしていができます。

0を指定することによって2番ピンのON・OFFが出来ます。

対応表は以下になります。

ONパラメータピン番号
02
13
24
35
412

アナログ

次に、アナログ値を読んでいきます。

アナログ値を読むピンはA0~A5で、pythonのプログラムでpyfirmataからすべて使えるように初期化してあります。

今回は角が丸い値ブロックを使っていきますので、以下のようにブロックを組みます。

http://l192.168.x.xx:8000/?READ=0

最後のパラメータは0~4を指定できます。

指定した数+1のアナログピンの値が取れます。

READ=0だとA1ピンのアナログ値が返ってきます。

タクトスイッチを押した状態で値をとると0.653あたりが返ってくると思います。

最大5V測定できて割合で値が返ってくるので、計算すると3.3/5=0.66となるので誤差の範囲内ですね。

サーボ

今回は凸凹のスタックブロックを使っていきます。

今回のパラメータは2つ使います。

1つ目は、SERVO=90で、数字はサーボモータの角度を指定できます。

今回のブロック構成では90°→2秒待機→0°→2秒待機を10回繰り返すようになっています。

2つ目は、PIN=0で、名前の通りピンの指定です。

対応表を以下に示しましす。

PINパラメータピン番号
06
17
28
313

PWM

次はPWMです。

http://192.168.x.xx:8000/?PWM=0.3&PIN=2

これもURLのパラメータは2つでPWMとPINがあります。

PWMのパラメータに渡す値は0~1の浮動小数で渡します。つまり割合ですね。

これを0に近づければLEDは光らなくなり、1に近づけることによって光は強くなっていきます。

PWMの出力は3.3Vが上限で、パラメータの割合で電圧が変わるようになっています。

PINには0~3のピン番号を指定してあげます。

対応表を以下に示します。

PINパラメータピン番号
211
  • 注意:Pythonプログラムでは11ピン以外にも指定できるようにしてありますがそのほかのピンでは正常には動作しないことが分かったのでPWMを使うときは必ずPINパラメータには2を指定してください。

発展

次は少しだけ汎用性の高いコードを用意したのでそちらを紹介します。

halake_ex_lib.py

# -*- encoding: utf-8 -*-

import json
from http.server import BaseHTTPRequestHandler, HTTPServer
import urllib.parse as p

def default():
    print()
    print('################################')
    print('NO SET FUNCTION \nPlease et the function with set_func(func: function)')
    print('################################')
    print()

function = default

def set_func(func):
    global function
    function = func

param = {}

class Server(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            # #///Consolのエラー回避必要であればコメント外す
            self.send_response(200)
            self.send_header('Content-type', 'text/html')
            self.send_header('Access-Control-Allow-Origin', '*')#これがないとリクエストに答えられない。js側エラー内容( No 'Access-Control-Allow-Origin' header is present on the requested resource.)
            self.end_headers()
            #///

            print(self.path)                #例)/?ON=1
            q_path = p.urlparse(self.path)  #例)ON=1
            q_par = p.parse_qs(q_path.query)#例)1

            # param_check(self, q_par)
            global param
            global function
            param = q_par
            function(self)
            print()

        except Exception as e:  
            print("An error occured")
            print("The information of error is as following")
            print(type(e))
            print(e.args)
            print(e)
            print()
            
def run(server_class=HTTPServer, handler_class=Server, server_name='', port=8000):

    server = server_class((server_name, port), handler_class)
    server.serve_forever()

def server():
    print('start')
    run(server_name='', port=8000)

def retunr_param():
    global param
    return param

main.py

import halake_ex_lib as halake
import pyfirmata
import time

#///定型文
port = 'COM9'#各環境で書き換え必要
board = pyfirmata.Arduino(port)
#///

 # ///自由
def main(self):
    print('hello world')
# ///自由

#///定型文
halake.set_func(main)
halake.server()
#///
  • 注意:main.pyの6行目あたりのport = 'COM9'の書き換えを忘れないようにしてください

この二つとそれ以前のプログラムは、halake_ex_lib.pymain.pyはHaLakeで行われている小学生向けプログラミング教室用教材の参考として作成しました。実際の教材に使う前段階ですので、あくまで出来るかなどの試しになります。

サーバを立てたり、どんなリクエストが来たか、リクエストされたら実行する関数の設定は教室で教えるには時間などの関係から無理があると思います。

教わる側も情報量が多いと、学校と同じで嫌になってしまいますので、こちら側である程度隠すことによってほんの少しでも直感的にわかりやすくなって頂きたいと考えています。

既に相当な情報量になってきていますが、今回の記事だけでなくいろいろブラッシュアップして更新していく予定です。

今回は特に、回りくどいことをしているのでまずはhalake_ex_lib.pyを簡易的なライブラリとして作ってみました。

halake_ex_lib.pyで用意されている外部で使える関数

関数説明
server()サーバの起動
set_function()server()で起動させたサーバにリクエストされた時、起動する関数の設定。引数に関数を渡す必要があり、最初は強制的にdefault()が設定されている
return_param()最後のリクエストのクエリパラメータをdict型で返す

main.pyではリクエストされたときに起動する関数の内容を記述します。

set_function関数の引数にmain関数を設定しているので、main()にArduinoの操作を記述することが出来ます。

例えば、 # ///自由で挟まれた部分を以下のように書き換えてリクエストすると、

LEDが1秒光って消えるようになります。デジタル出力ですね。

Dpin = board.get_pin('d:2:o')
def main(self):
    Dpin.write(1)
    time.sleep(1)
    Dpin.write(0)
    print(Dpin_i.read())

次は、デジタル入力です。

it = pyfirmata.util.Iterator(board)
it.start()
Dpin_i = board.get_pin('d:3:i')
def main(self):
    io = Dpin_i.read()
    print(io)
    self.wfile.write(str(io).encode('utf-8'))

次は、アナログ入力です。

it = pyfirmata.util.Iterator(board)
it.start()
board.analog[0].enable_reporting()
def main(self):
    time.sleep(0.5)
    val = board.analog[0].read()
    print(val)
    self.wfile.write(str(val).encode('utf-8'))

サーボ出力

Spin = board.get_pin('d:6:s')
def main(self):
    Spin.write(90)

PWM出力

Ppin = board.get_pin('d:11:p')
def main(self):
    for i in range(100):
        Ppin.write(float(0.001*i))
        time.sleep(0.1)

これもArduinoのピン操作はpyfirmataライブラリを使っているので、それに沿って記述しています。

pyfirmataは癖が強いのか、使い勝手がいまいちで注意点がいくつかあります。

  • 同じピン番号を指定してboard.get_pin関数を複数回呼ぶとエラーが出てしまう。
  • 設定済みのピンを別の用途に再設定する方法がわからない。(調査不足の場合もあるが、時間をかけても見つからなかった)
  • exit関数が用意されているがシリアルポートごと解放されてしまう。
  • 9、10番ピンのPWMが使えなかった
  • デジタル・アナログの入力を使う場合には以下の2行が必須
it = pyfirmata.util.Iterator(board)
it.start()

それぞれ使えるピンと機能が決まっているので以前の記事で紹介した確認用のテストプログラムが公式から出ているので確認してみてください。

以前の記事でおまけの項の一番上に記述されています。

アナログ入力以外ではピンの設定をboard.get_pin()で設定します。

この引数はstr型の文字列を渡す必要があるようで

-デジタル/アナログ-番号-入出力設定
d:2:o
-d(digital)---o(Output)
-a(analog)---i(Input)
-----s(Servo)
-----p(PWM)

3つの項目をコロン(:)2つでしきるように記述します。

まとめ

  • Node.jsからFirmataが使えなかったのでかなり遠回りになってしまった。
  • コードが無駄に難しくなってしまった。

今回は全体的にややこしくなってしまった印象があります。

いきなりFiramataを使ってしまうと情報が多すぎて無駄にわかり辛くなってしまうので次回は、Firmataを使わずにPythonとScratchを併用して何かできないか試してみたいと思います。

良ければ次の記事も読んで頂くと嬉しいです。