最終形
やりたいこと
- PYNQ-Z2上でWebAPIを動かそう
- Androidでアプリを作ってWebAPIにアクセスしよう
- 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からダウンロードできます.
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 | { "led_index": 0 } |
{ "func": "set_led_value", "result": "success" } |
リクエストボディで,点灯状態を反転させたいLEDの番号を指定 リクエストボディに問題がなく,処理に成功すれば"result": "success"が返る |
get_led_value | GET | なし | { "func": "get_led_value", "result": "success", "led_value": [0, 0, 0, 0] } |
ステータス200である限り"result": "success"を返す. LEDの点灯状態を0か1かで返す(LEDは四つ並んでるので配列). |
PYNQ-Z2上でWebAPIを動かそう
以下のコードはJupyter Notebook上で動かしてます.
そんなに解説することもないのでどばーっと載せます.
PL側(回路)についてはAndroidから指示を受け取ってボード上のLEDを動かそうに記述.
# 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 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側では全部消灯スタートとします.
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書いてないです.
実際に動いている様子
最終形と同じ投稿です.