ボード上のLEDをスマホから操作(PYNQ-Z2シリーズ)

# 最終形

https://twitter.com/tt41357393/status/1738793333735207006

# やりたいこと
1. PYNQ-Z2上でWebAPIを動かそう
1. Androidでアプリを作ってWebAPIにアクセスしよう
1. Androidから指示を受け取ってボード上のLEDを動かそう

# ハードとソフト

- ハード
    - PYNQ-Z2
    - AQUOS wish 2
- ソフト
    - Vivado 2022.2
    - Android Studio 2.0

# 本題に入る前に
PYNQ-Z2上で動いてるOSがV2.3とか(うろ覚え)だったのでV3.0.1にアップグレードしました．
OS自体をアップデートする方法がなさそう(もしあるなら教えてください)だったことに加え，SDカードの容量が16GBしかなくパンパンになっていたこともあり，新しいSDカードを購入してそこにV3.0.1のimageファイルをインストールしました．
imageファイルは8GB弱ありました．[PYNQ: PYTHON PRODUCTIVITY](http://www.pynq.io/board.html)からダウンロードできます．

# WebAPIについて
今回作成したWebAPIの仕様は以下の通り．AndroidがクライアントでPYNQ-Z2がサーバー．
`set_led_value`で，AndroidからPYNQ-Z2に対し「この番号のLEDの状態を反転して」と命令し，`get_led_value`でPYNQ-Z2はAndroidにLEDの状態を返す．
端末(Android)側でLEDの状態を保持するのは違うよなーと思ったのでサーバー(PYNQ-Z2)側で保持する設定．

|命令|メソッド|リクエストボディ|レスポンスボディ|備考|
|:-|:-|:-|:-|:-|
|set_led_value|POST|{<br>"led_index": 0<br>}|{<br>"func": "set_led_value",<br>"result": "success"<br>}|リクエストボディで，点灯状態を反転させたいLEDの番号を指定<br>リクエストボディに問題がなく，処理に成功すれば"result": "success"が返る|
|get_led_value|GET|なし|{<br>"func": "get_led_value",<br>"result": "success",<br>"led_value": [0, 0, 0, 0]<br>}|ステータス200である限り"result": "success"を返す．<br>LEDの点灯状態を0か1かで返す(LEDは四つ並んでるので配列)．|

# PYNQ-Z2上でWebAPIを動かそう
以下のコードはJupyter Notebook上で動かしてます．
そんなに解説することもないのでどばーっと載せます．
PL側(回路)については[Androidから指示を受け取ってボード上のLEDを動かそう](#Androidから指示を受け取ってボード上のLEDを動かそう)に記述．

```py
# Flask関連
from flask import Flask
from flask import jsonify, request

LED_NUM = 4

app = Flask(__name__)

led = [0 for _ in range(LED_NUM)]

# ---- セル境界 ----

# Overlay関連
from pynq import Overlay

ol = Overlay("./bit/server.bit")

gpio = ol.axi_gpio_0

# ---- セル境界 ----

# LEDの状態をボード上のLEDに出力する
def led_output(led_value):
    global gpio
    if len(led_value) != 4:
        return
    
    value = 0
    for i, v in enumerate(led_value):
        value |= v << i
    
    gpio.write(0, value)

# ---- セル境界 ----

@app.route("/set_led_value", methods=["POST"])
def set_led_value():
    global led
    rtn = {"func": "set_led_value"}

    if request.headers["Content-Type"] != "application/json"\
        and request.headers["Content-Type"] != "application/json; charset=utf-8":
        rtn["result"] = "error"
        rtn["error_msg"] = "fail content type"
    else:
        for k, v in request.json.items():
            if k == "led_index":
                i = int(v)
                # 見た目似てるけどforじゃなくてif
                if i in range(4):
                    led[i] = int(not led[i])
                    led_output(led)
                    rtn["result"] = "success"
                    break
        else:
            rtn["result"] = "error"
            rtn["error_msg"] = "fail led data"

    return jsonify(rtn)

# ---- セル境界 ----

@app.route("/get_led_value")
def get_led_value():
    global led
    rtn = {
        "func": "get_led_value",
        "result": "success",
        "led_value": led
    }
    return jsonify(rtn)

# ---- セル境界 ----

if __name__ == "__main__":
    led_output(led)
    app.run(debug=False, host="0.0.0.0")
```

# Androidでアプリを作ってWebAPIにアクセスしよう
AndroidアプリはLEDに見立てたボタンを四つ並べただけのシンプルなデザインにします．
押したボタンに対応したLEDの状態を反転させるようAPIを投げます．
また，そのたびにPYNQ-Z2から点灯状態を取得して，画面上のボタンの色を変え点灯状態を再現します．

レイアウトはこんな感じ．

```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/led0"
        android:layout_width="50dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:text="@string/led_btn_value"

        app:layout_constraintBottom_toBottomOf="@+id/led1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintStart_toEndOf="@+id/led1" />

    <Button
        android:id="@+id/led3"
        android:layout_width="50dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:text="@string/led_btn_value"

        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/led2"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/led2"
        android:layout_width="50dp"
        android:layout_height="wrap_content"
        android:text="@string/led_btn_value"

        app:layout_constraintBottom_toBottomOf="@+id/led3"
        app:layout_constraintEnd_toStartOf="@+id/led1"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/led3" />

    <Button
        android:id="@+id/led1"
        android:layout_width="50dp"
        android:layout_height="wrap_content"
        android:text="@string/led_btn_value"

        app:layout_constraintBottom_toBottomOf="@+id/led2"
        app:layout_constraintEnd_toStartOf="@+id/led0"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/led2" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

コードはこんな感じ．
ボタンの色の初期化もPYNQ-Z2から点灯状態を取得して行いたかったんですが，メインスレッドでのHTTP通信ができなかったので断念しました．PYNQ-Z2上のLEDがどんな状態であろうとAndroid側では全部消灯スタートとします．

```kotlin
package com.example.ledcommander

import android.content.res.ColorStateList
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.Button
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.HandlerCompat
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {
    companion object {
        // ルートURL
        private const val ROOT_URL = "http://XXX.XXX.XXX.XXX:5000"

        // LEDの状態を取得するURL
        private const val GET_URL = "${ROOT_URL}/get_led_value"
        // LEDの状態をセットするURL
        private const val SET_URL = "${ROOT_URL}/set_led_value"

        // タイムアウト
        private const val TIMEOUT = 1000

        // ボタンのID
        private val ledButtons = intArrayOf(R.id.led0, R.id.led1, R.id.led2, R.id.led3)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

//        // 四つのボタンの色の初期化
//        val ledValue = this.getLedValue()
//        this.setLedButtons(ledValue)
        setLedButtons(IntArray(4))

        // 四つのボタンにリスナをセット
        ledButtons.forEach{
            val button = findViewById<Button>(it)
            val listener = LedButtonListener()
            button.setOnClickListener(listener)
        }
    }

    // ボタンの色をLEDの点灯状態に合わせて変更する
    private fun setLedButtons(ledValue: IntArray) {
        if (ledValue.size != 4) {
            return
        }

        var i = 0
        ledButtons.forEach {
            val button = findViewById<Button>(it)

            if (ledValue.get(i) == 1) {
                button.setBackgroundColor(Color.rgb(0xff, 0xff, 0x00))
            }
            else {
                button.setBackgroundColor(Color.rgb(0xcc, 0xcc, 0xcc))
            }

            i++
        }
    }

    // LEDの点灯状態を取得する
    private fun getLedValue(): IntArray {
        var ledValue = intArrayOf(0, 0, 0, 0)

        var url = URL(GET_URL)
        val con = url.openConnection() as? HttpURLConnection

        con?.let {
            try {
                // タイムアウト設定
                it.connectTimeout = TIMEOUT
                it.readTimeout = TIMEOUT

                // リクエストボディの送信を許可しない
                it.doOutput = false
                // レスポンスボディの受信を許可
                it.doInput = true

                // GETメソッドを指定
                it.requestMethod = "GET"

                // 通信
                it.connect()

                // レスポンス取得
                val stream = it.inputStream
                ledValue = this.getLedValueFromStream(stream)

                // 解放
                stream.close()
            } catch (e: SocketTimeoutException) {
                // 何もしない
            } finally {
                // 解放
                it.disconnect()
            }
        }

        return ledValue
    }

    // レスポンスボディからLEDの点灯状態を取得する
    private fun getLedValueFromStream(stream: InputStream): IntArray {
        // InputStreamを文字列に変換
        val sb = StringBuilder()
        val reader = BufferedReader(InputStreamReader(stream, "UTF-8"))
        var line = reader.readLine()
        while (line != null) {
            sb.append(line)
            line = reader.readLine()
        }
        reader.close()

        // 文字列からデータ取得
        val json = JSONObject(sb.toString())
        if (json.getString("result") == "success") {
            val ledValue = json.getJSONArray("led_value")
            return IntArray(4){ ledValue.get(it) as Int }
        }
        return intArrayOf(0, 0, 0, 0)
    }

    // ボタンタップ時の処理
    private inner class LedButtonListener: View.OnClickListener {
        override fun onClick(view: View) {
            var ledIndex = 0

            when (view.id) {
                R.id.led0 -> ledIndex = 0
                R.id.led1 -> ledIndex = 1
                R.id.led2 -> ledIndex = 2
                R.id.led3 -> ledIndex = 3
                else      -> ledIndex = 0
            }

            // 別スレッドで通信処理を行う準備
            val handler = HandlerCompat.createAsync(mainLooper)
            val sender = LedValueSender(handler, ledIndex)
            val executor = Executors.newSingleThreadExecutor()

            // 処理実行
            executor.submit(sender)
        }
    }

    // PYNQ-Z2アクセスクラス
    private inner class LedValueSender(handler: Handler, ledIndex: Int): Runnable {
        private val handler = handler
        private val ledIndex = ledIndex

        @WorkerThread
        override fun run() {
            // 送信
            this.send()

            // 取得
            val ledValue = getLedValue()

            // 元スレッドに処理を戻す
            val executor = LedValueGetter(ledValue)
            this.handler.post(executor)
        }

        @WorkerThread
        private fun send() {
            val url = URL(SET_URL)
            val con = url.openConnection() as? HttpURLConnection

            con?.let {
                try {
                    // 送信データ
                    val json = "{\"led_index\": ${this.ledIndex}}".toByteArray()

                    // タイムアウト設定
                    it.connectTimeout = TIMEOUT
                    it.readTimeout = TIMEOUT

                    // リクエストボディの送信を許可
                    it.doOutput = true
                    // レスポンスボディの受信を許可
                    it.doInput = true

                    // リクエストボディのストリーミングを有効化
                    it.setFixedLengthStreamingMode(json.size)

                    // POST通信準備
                    it.setRequestProperty("Content-Type", "application/json")
                    it.requestMethod = "POST"
                    it.doOutput = true

                    // 接続
                    it.connect()

                    // POST通信
                    val stream = con.outputStream
                    stream.write(json)

                    // 解放
                    stream.flush()
                    stream.close()
                }
                catch (e: SocketTimeoutException) {
                    Log.w("user info", "接続タイムアウト", e)
                }
                finally {
                    // 解放
                    it.disconnect()
                }
            }
        }
    }

    // ボタンの状態を更新するクラス
    private inner class LedValueGetter(ledValue: IntArray): Runnable {
        private val ledValue = ledValue

        @UiThread
        override fun run() {
            setLedButtons(ledValue)
        }
    }
}
```

# Androidから指示を受け取ってボード上のLEDを動かそう
大したものじゃないんですが，今回PYNQ-Z2上で動いてる回路です．
HDL書いてないです．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/db69b580-0cb7-1817-bfc2-26d17b5c2ba4.png)

# 実際に動いている様子
[最終形](#最終形)と同じ投稿です．

https://twitter.com/tt41357393/status/1738793333735207006