マイクロビットを使ってみる 〜「ESPr Developer」でWi-Fi通信するための「カスタムブロック」をつくる

マイクロビットにはWi-Fi通信機能がありません。


これに対し、「Grove UART WiFi V2」という製品があります。
Groveシールドをつかってマイクロビットと「Grove UART WiFi V2」をつなぐことで、マイクロビットでWi-Fi通信ができるようになります。「Grove UART WiFi V2」用の拡張機能ブロック群(「grove」拡張機能)も用意されており、ブロックエディタでWi-Fi通信するプログラムをつくることができます(記事は こちら)。

また、この「Grove UART WiFi V2」は現在、秋月電子やスイッチサイエンスでも販売されておらず、入手するのがなかなか大変なのですが、代わりにスイッチサイエンス製のマイコンボード「ESPr Developer」を使って同等の処理を行えることを確認しました。
マイクロビットと「ESPr Developer」をつなぎ、「grove」拡張機能をそのまま使うことで、ブロックエディタでWi-Fi通信するプログラムをつくることができました(記事は こちら)。

さて、この調査を行う際には、「1分毎にIFTTTにイベントを送信する」という処理を行っていたのですが、「grove」拡張機能の「Send Data to your IFTTT Event」ブロックを「1回」実行すると、IFTTTにイベントが「2回」送信されてしまうという現象が生じました。
「ESPr Developer」と「Grove UART WiFi V2」のどちらを使っても同じ問題が生じているため、デバイスの問題ではなく、「Send Data to your IFTTT Event」ブロックの問題のようです。

ブロックの内容を確認してみます。
MakeCodeエディタの「拡張機能」で「grove」を検索し、「Learn Mode」>「GitHub」>「main.ts」をクリックします。
「Send Data to your IFTTT Event」ブロックの内容は以下のとおりです。

    export function sendToIFTTT(event: string, key: string, value1: string, value2: string, value3: string) {
        let result = 0
        let retry = 2

        // close the previous TCP connection
        if (isWifiConnected) {
            sendAtCmd("AT+CIPCLOSE")
            waitAtResponse("OK", "ERROR", "None", 2000)
        }

        while (isWifiConnected && retry > 0) {
            retry = retry - 1;
            // establish TCP connection
            sendAtCmd("AT+CIPSTART=\"TCP\",\"maker.ifttt.com\",80")
            result = waitAtResponse("OK", "ALREADY CONNECTED", "ERROR", 2000)
            if (result == 3) continue

            let data = "GET /trigger/" + event + "/with/key/" + key
            data = data + "?value1=" + value1
            data = data + "&value2=" + value2
            data = data + "&value3=" + value3
            data = data + " HTTP/1.1"
            data = data + "\u000D\u000A"
            data = data + "User-Agent: curl/7.58.0"
            data = data + "\u000D\u000A"
            data = data + "Host: maker.ifttt.com"
            data = data + "\u000D\u000A"
            data = data + "Accept: */*"
            data = data + "\u000D\u000A"

            sendAtCmd("AT+CIPSEND=" + (data.length + 2))
            result = waitAtResponse(">", "OK", "ERROR", 2000)
            if (result == 3) continue
            sendAtCmd(data)
            result = waitAtResponse("SEND OK", "SEND FAIL", "ERROR", 5000)

            // // close the TCP connection
            // sendAtCmd("AT+CIPCLOSE")
            // waitAtResponse("OK", "ERROR", "None", 2000)

            if (result == 1) break
        }
    }

「retry」という変数を使い、正常にイベント送信ができなかった場合は、処理を2回繰り返しています。また、正常にイベント送信できたかどうかは、サーバーから返ってきた文字列で判断しています。
この処理で、実際には正常にイベント送信できているにも関わらず、イベント送信できなかったと誤判断されるため、1回の処理でイベントが2回送信されてしまうようです。

このプログラムを、イベント送信が正常にできたかどうかに関わらず、繰り返しは行わず、1回の処理だけで終了するように変更してみます。
今回は、このプログラムを「ブロックプログラミング」でつくってみたいと思います。

ブロックでつくっていますが、先ほどの「grove」拡張機能の「main.ts」の内容から繰り返し処理をなくしただけです。

このプログラムの中で使われている「sendAtCmd」「waitAtResponse」関数もブロックでつくりました。
先ほど見た「grove」拡張機能の「main.ts」では、これらの関数は以下のようになっています。

    function sendAtCmd(cmd: string) {
        serial.writeString(cmd + "\u000D\u000A")
    }

    function waitAtResponse(target1: string, target2: string, target3: string, timeout: number) {
        let buffer = ""
        let start = input.runningTime()

        while ((input.runningTime() - start) < timeout) {
            buffer += serial.readString()

            if (buffer.includes(target1)) return 1
            if (buffer.includes(target2)) return 2
            if (buffer.includes(target3)) return 3

            basic.pause(100)
        }

        return 0
    }

これをブロックでつくると以下のようになります。処理内容は全く同じです。

動作確認してみます。
「grove」拡張機能の「Setup WiFi」ブロックでWi-Fi接続した後、今回つくった「sendToIFTTT」関数でIFTTTにイベントを送信してみます。

ボタンAを押すと、所望のとおり、IFTTTにイベントが1回だけ送信されました。

ついでに、「grove」拡張機能のその他のブロック(「setupWiFi」と「wifiOK」)も、ブロックプログラミングでつくっておきます。
「main.ts」では、これらの関数は以下のように記述されています。

    export function setupWifi(txPin: SerialPin, rxPin: SerialPin, baudRate: BaudRate, ssid: string, passwd: string) {
        let result = 0

        isWifiConnected = false

        serial.redirect(
            txPin,
            rxPin,
            baudRate
        )

        sendAtCmd("AT")
        result = waitAtResponse("OK", "ERROR", "None", 1000)

        sendAtCmd("AT+CWMODE=1")
        result = waitAtResponse("OK", "ERROR", "None", 1000)

        sendAtCmd(`AT+CWJAP="${ssid}","${passwd}"`)
        result = waitAtResponse("WIFI GOT IP", "ERROR", "None", 20000)

        if (result == 1) {
            isWifiConnected = true
        }
    }

    export function wifiOK() {
        return isWifiConnected
    }

これをブロックでつくると以下のようになります。

「grove」拡張機能の「setupWiFi」ブロックでは、ブロック内でシリアル通信の初期設定を行っていますが、その処理を関数の外に出しました。他の処理内容は全く同じです。

MakeCodeエディタで、画面上部のエディタ選択ボタンを「JavaScript」モードに切り替えると、ブロックでつくったプログラムがJavaScriptで表示されます。
内容は以下のとおりです(関数の順番がデタラメになっていたため、順番だけを修正しました)。

function setupWiFi (ssid: string, passwd: string) {
    result = 0
    isWiFiConnected = false
    sendAtCmd("AT")
    result = waitAtResponse("OK", "ERROR", "None", 1000)
    sendAtCmd("AT+CWMODE=1")
    result = waitAtResponse("OK", "ERROR", "None", 1000)
    sendAtCmd("AT+CWJAP=\"" + ssid + "\",\"" + passwd + "\"")
    result = waitAtResponse("WIFI GOT IP", "ERROR", "None", 20000)
    if (result == 1) {
        isWiFiConnected = true
    }
}
function wifiOK () {
    return isWiFiConnected
}
function sendToIFTTT (event: string, key: string, value1: string, value2: string, value3: string) {
    result = 0
    if (isWiFiConnected) {
        sendAtCmd("AT+CIPCLOSE")
        waitAtResponse("OK", "ERROR", "None", 2000)
        sendAtCmd("AT+CIPSTART=\"TCP\",\"maker.ifttt.com\",80")
        result = waitAtResponse("OK", "ALREADY CONNECTED", "ERROR", 2000)
        if (result == 3) {
            return
        }
        data = "GET /trigger/" + event + "/with/key/" + key + "?value1=" + value1 + "&value2=" + value2 + "&value3=" + value3 + " HTTP/1.1" + String.fromCharCode(13) + String.fromCharCode(10) + "User-Agent: curl/7.58.0" + String.fromCharCode(13) + String.fromCharCode(10) + "Host: maker.ifttt.com" + String.fromCharCode(13) + String.fromCharCode(10) + "Accept: */*" + String.fromCharCode(13) + String.fromCharCode(10)
        sendAtCmd("AT+CIPSEND=" + convertToText(data.length + 2))
        result = waitAtResponse(">", "OK", "ERROR", 2000)
        if (result == 3) {
            return
        }
        sendAtCmd(data)
        result = waitAtResponse("SEND OK", "SEND FAIL", "ERROR", 5000)
        if (result == 1) {
            return
        }
    }
}
function waitAtResponse (target1: string, target2: string, target3: string, timeout: number) {
    buffer = ""
    start = input.runningTime()
    while (input.runningTime() - start < timeout) {
        buffer = "" + buffer + serial.readString()
        if (buffer.includes(target1)) {
            return 1
        }
        if (buffer.includes(target2)) {
            return 2
        }
        if (buffer.includes(target3)) {
            return 3
        }
        basic.pause(100)
    }
    return 0
}
function sendAtCmd (cmd: string) {
    serial.writeString("" + cmd + String.fromCharCode(13) + String.fromCharCode(10))
}

let data = ""
let result = 0
let start = 0
let buffer = ""
let isWiFiConnected = false

serial.redirect(
SerialPin.P15,
SerialPin.P1,
BaudRate.BaudRate115200
)

せっかくなので、ここでつくった関数群をカスタムブロックにしたいと思います。
カスタムブロックにするために、先ほどのJavaScriptプログラムを以下のように加工します。

/**
 * ESP WiFi
 */
//% weight=50 color=#32c032 icon="\uf1eb" block="ESP WiFi"
namespace ESPWiFi {
    let isWiFiConnected = false
    //% block="Setup Wifi|SSID = %ssid|PASSWORD = %passwd"
    export function setupWiFi (ssid: string, passwd: string) {
        let result = 0
        isWiFiConnected = false
        sendAtCmd("AT")
        result = waitAtResponse("OK", "ERROR", "None", 1000)
        sendAtCmd("AT+CWMODE=1")
        result = waitAtResponse("OK", "ERROR", "None", 1000)
        sendAtCmd("AT+CWJAP=\"" + ssid + "\",\"" + passwd + "\"")
        result = waitAtResponse("WIFI GOT IP", "ERROR", "None", 20000)
        if (result == 1) {
            isWiFiConnected = true
        }
    }
    //% block="WiFi OK?"
    export function wifiOK () {
        return isWiFiConnected
    }
    //% block="Send Data to your IFTTT Event|Event %event|Key %key|value1 %value1|value2 %value2|value3 %value3"
    //% event.defl="your Event"
    //% key.defl="your Key"
    //% value1.defl="hello"
    //% value2.defl="micro"
    //% value3.defl="bit"
    export function sendToIFTTT (event: string, key: string, value1: string, value2: string, value3: string) {
        let result = 0
        if (isWiFiConnected) {
            sendAtCmd("AT+CIPCLOSE")
            waitAtResponse("OK", "ERROR", "None", 2000)
            sendAtCmd("AT+CIPSTART=\"TCP\",\"maker.ifttt.com\",80")
            result = waitAtResponse("OK", "ALREADY CONNECTED", "ERROR", 2000)
            if (result == 3) {
                return
            }
            let data = "GET /trigger/" + event + "/with/key/" + key + "?value1=" + value1 + "&value2=" + value2 + "&value3=" + value3 + " HTTP/1.1" + String.fromCharCode(13) + String.fromCharCode(10) + "User-Agent: curl/7.58.0" + String.fromCharCode(13) + String.fromCharCode(10) + "Host: maker.ifttt.com" + String.fromCharCode(13) + String.fromCharCode(10) + "Accept: */*" + String.fromCharCode(13) + String.fromCharCode(10)
            sendAtCmd("AT+CIPSEND=" + convertToText(data.length + 2))
            result = waitAtResponse(">", "OK", "ERROR", 2000)
            if (result == 3) {
                return
            }
            sendAtCmd(data)
            result = waitAtResponse("SEND OK", "SEND FAIL", "ERROR", 5000)
            if (result == 1) {
                return
            }
        }
    }
    function waitAtResponse (target1: string, target2: string, target3: string, timeout: number) {
        let buffer = ""
        let start = input.runningTime()
        while (input.runningTime() - start < timeout) {
            buffer = "" + buffer + serial.readString()
            if (buffer.includes(target1)) {
                return 1
            }
            if (buffer.includes(target2)) {
                return 2
            }
            if (buffer.includes(target3)) {
                return 3
            }
            basic.pause(100)
        }
        return 0
    }
    function sendAtCmd (cmd: string) {
        serial.writeString("" + cmd + String.fromCharCode(13) + String.fromCharCode(10))
    }
}

こちら の記事で記載している方法で、カスタムブロックを追加しました。

このカスタムブロックを使ったプログラムは以下のようになります。
緑色のブロック(「Setup Wifi」と「Send Data to your IFTTT Event」)が、今回つくったカスタムブロックです。

このプログラムでも、先ほどと同様に、所望のとおりIFTTTにイベント送信できました。

また、「Send Data to your IFTTT Event」内の内容(「AT+CIPSTART」や「AT+CIPSEND」)を変更することで、IFTTT以外(自分で準備したWebサーバ)にも問題なくデータ送信することができました。

このようなカスタムブロックをあらかじめ準備しておくことで、マイクロビット+「ESPr Developer」でのWi-Fi通信を気軽に実施できるようになりそうです。


なお、私がマイクロビットの使い方を習得するのにあたっては、以下の書籍を参考にさせていただきました。


初心者向けから、比較的高度なものまで、さまざまな情報が記載されているだけでなく、子供向けの作例も多数掲載されていますので、「プログラミング教育」のための題材さがしなどにもおすすめです。


このサイトで書いている、マイクロビットに関するブログ記事を、「さとやまノート」という別のブログページに、あらためて整理してまとめました。

他のマイクロビット記事にも興味のある方は「さとやまノート」をご覧ください。