みつきんのメモ

組み込みエンジニアです。Interface誌で「Yocto Projectではじめる 組み込みLinux開発入門」連載中

Flask-Bootstrap + Ajax(jQuery) + RPi.GPIOでLEDを制御してみる

ここ何回かにわたって、pythonでFlask-BootstrapやAjaxのお話をしてきた。

なぜ急にFlaskでAjaxだったのか。実は今回のこれがやりたかった。 yoctoで作ったOSをより組み込み機器っぽくするために、Webインターフェースによる設定画面やハードウェア制御をできるようにしたかったのだ。

そのためyoctoでパッケージが提供されていて、使い方も比較的シンプルなものを探したところ、Flask-Bootstrapが見つかった。

作成するもの

こんな感じで、ブラウザからラズベリーパイに接続したLEDを点灯/消灯できるようにする。

f:id:mickey_happygolucky:20180402233900p:plain

サーバとPCはEthernetで接続し、データの送受信はAjaxで行なう。 Ajaxを使うのは、LED制御時にいちいち画面を遷移させたくないため。

ディレクトリ構成

ディレクトリ構成はFlaskの時と同じ。

.
├── led.py
└── templates
    └── led.html

JSONデータ

Ajaxでやり取りするのはJSON形式のデータで次のような構造を持っている。

例としてここでは例としてledの値に13、statusの値に0を指定している。

{
  "led": 13,
  "status": 0
}
キー 概要
led 13 LEDのGPIO番号(BCM)
status 0 0=消灯/1=点灯

led.html(htmlファイル)

led.htmlの内容を次に示す。

分割して掲載する。

先頭〜contentブロック

{% extends "bootstrap/base.html" %}

{% block title %}LED{% endblock title %}

{% block content %}
<div class="container">
    <div class="page-header">
        {% for led in leds %}
        <p>
            <label for="led{{led}}">led{{led}} = off</label>
            <button id="led{{led}}" value="1">on</button>
        </p>
        {% endfor %}
    </div>
</div>
{% endblock content %}

ラベルとボタンの組み合わせを表示している。 ボタンの表示及びvalueは「押された時にどの状態にするか」を保持している。

ledsはLEDの番号が入ったリストをFlaskから渡される。

scriptsブロック

led.htmlの残りの部分を次に示す。

{{ super() }}は忘れずに。

setStatus()関数と、各ボタンのclick()関数に分けることができる。

{% block scripts %}
{{ super() }}
<script>

function setStatus(label, button, data) {
  if (data.status == 0) {
    label.text('led'+data.led+' = off')
    button.val(1+'');
    button.text('on');
  } else {
    label.text('led'+data.led+' = on')
    button.val(0+'');
    button.text('off');
  }
}

{% for led in leds %}
$(function() {
  $('#led{{led}}').click(function() {
    let status = parseInt($('#led{{led}}').val());
    let jsonData = JSON.stringify({"led":{{led}}, "status":status});
    console.log(jsonData);
    $.ajax({
      url: '{{ url_for('led_change') }}',
      data: jsonData,
      contentType: 'application/json;charset=UTF-8',
      type: 'POST'
    }).done(function(data){
      let label =  $('label[for=led'+data.led+']');
      let button = $('#led'+data.led);
      setStatus(label, button, data);
   }).fail(function(){
      console.log('fail');
    });
  })
});
{% endfor %}
</script>
{% endblock scripts %}

setStatus()はボタンの状態を変更する関数でajaxのdone()から呼び出される。 ボタンに設定するvalueは文字列型である必要があるため1+''0+''で変換している。

dataはajaxでサーバから返されたJSON形式の文字列。 内容はサーバに送信したものと同じになる。

各ボタンのclick()関数はボタンをクリックされた時に、対応するボタンのclick()関数が呼び出される。 これらの関数はledsのリストに入っている分だけ生成される。

処理内容は、JSON形式の文字列を生成し、ajaxでサーバに送信。 帰ってきた内容をdone()で処理する。

done()では、ボタンとラベルを検索し、setStatus()関数を呼び出している。

led.py(pythonスクリプト)

python側のスクリプトを次に示す。

# coding: utf-8
from flask import Flask, render_template, request, jsonify
from flask_bootstrap import Bootstrap
#import RPi.GPIO as GPIO


# for Flask-Bootstrap
app = Flask(__name__)
bootstrap = Bootstrap(app)

# GPIO pins for LED
leds = [13, 14, 15]


@app.route('/')
def root():
    return render_template('led.html', leds=leds)


@app.route('/ledChange', methods=['POST'])
def led_change():
    id = request.json['led']
    status = request.json['status']
    GPIO.output(id, status)
    return jsonify({'led': id, 'status': status})


if __name__ == '__main__':
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(leds, GPIO.OUT)
    app.run(host='0.0.0.0')
    GPIO.cleanup()

サイトのルートにアクセスされると、led.htmlの内容を返す。

ブラウザでボタンをクリックされると「/ledChange」にアクセスされ、led_change()関数が呼び出される。 led_change()ではGPIO.output()により、GPIOの状態を設定している。

GPIO.cleanup()はおそらく到達することは無いとおもう。

yocto環境

今回はrockoブランチを使用している。

local.conf

このスクリプトが動作する環境をyoctoで作るにはlocal.confを次のようにする。

MACHINE = "raspberrypi3"
DL_DIR = "${TOPDIR}/../downloads"

IMAGE_INSTALL_append = " \
             rpi-gpio \
             python-flask \
             python-flask-bootstrap \
"

bblayers.conf

必要なレイヤを追加するには次のレイヤを追加する。

  • meta-oe
  • meta-python
  • meta-raspberrypi

Flask-BootstrapでAJAX(jQuery)

これらの続き

前回Flask-BootstrapでjQueryを使用する方法を紹介したが、ブラウザ側でボタンのラベルを書き換えるだけだった。

今回はAJAXを使用して、ブラウザとサーバ(Flask)で通信させる。

ディレクトリ構成

毎度おなじみのディレクトリ構成

.
├── ajax.py
└── templates
    └── ajax.html

ajax.html(htmlファイル)

ブロックごとに解説するため、2つに分ける。

contentブロック

先頭からcontentブロックまでを示す。

{% extends "bootstrap/base.html" %}

{% block title %}Hello{% endblock title %}

{% block content %}
<div class="container">
    <div class="page-header">
        <h1>Hello World!</h1>
        <label for="name">name:</label>
        <input id="name" type="text" size="20" />
        <button id="btn">send</button>
    </div>
</div>
{% endblock content %}

contentブロックに「label」と「input」を追加。 inputには"name"というidを振っている。

今回これらはformタグでくくっていない。

scriptsブロック

「{{ super() }}」は忘れずに。

{% block scripts %}
{{ super() }}
<script>
$(function() {
  $('#btn').click(function() {
    var textData = JSON.stringify({"name":$('#name').val()});

    $.ajax({
      url: '{{ url_for('getData') }}',
      data: textData,
      contentType: 'application/json;charset=UTF-8',
      type: 'POST'
    }).done(function(data){
      console.log(data);
    }).fail(function(){
      console.log('fail');
    });
  })
});
</script>
{% endblock scripts %}

ボタンをクリックすると、テキストの内容を取得し、JSON文字列を作成する。 ajaxオブジェクトで、サーバにJSONをPOSTする。

ajaxオブジェクトに設定した値を次に示す。

設定 概要
url {{ url_for(getData') }}' /getDataへアクセス
data textData 送信データ
contentType application/json;charset=UTF-8 JSONによるデータ受け渡し
type POST HTTPメソッド

contentTypeを忘れると、python側で「request.json」でデータにアクセスできなくなるので注意。

処理が成功すると「.done()」が実行され、失敗すると「.fail()」が実行される。

always(おまけ)

どちらの場合でも実行したい処理がある場合は、次のように「.always()」を記述するよい。

    $.ajax({
      data: textData,
      contentType: 'application/json;charset=UTF-8',
      type: 'POST'
    }).done(function(data){

    }).fail(function(){

    }).always(function({
        // ここに処理を記述
    }));

ajax.py

# coding: utf-8
from flask import Flask, render_template, request, jsonify
from flask_bootstrap import Bootstrap

app = Flask(__name__)
bootstrap = Bootstrap(app)


@app.route('/')
def index():
    return render_template('ajax.html')


@app.route('/getData', methods=['POST'])
def getData():
    name = request.json['name']
    print name
    return jsonify({'result': 'ok', 'value': name})


if __name__ == '__main__':
    app.run()

/getDataにポストされたデータを処理する。

「name = request.json['name']」でブラウザから受け取ったJSONデータを取得。

「return jsonify({'result': 'ok', 'value': name})」でデータをJSON形式に整形してブラウザに返却している。

これで、画面遷移を伴わずににサーバと通信することができる。

Flask-BootstrapでjQuery

これらの続き

Webアプリケーションでボタン操作などちょっと格好いいことをしようとするとjQueryは外せない。

じつは、Flask-Bootstrapで基本となっているbootstrap/base.htmlは次のようになっており、jQueryCDNを読み込むようになっている。

{% block doc -%}
<!DOCTYPE html>
<html{% block html_attribs %}{% endblock html_attribs %}>
{%- block html %}
...(snip)...

    {% block scripts %}
    <script src="{{bootstrap_find_resource('jquery.js', cdn='jquery')}}"></script>
    <script src="{{bootstrap_find_resource('js/bootstrap.js', cdn='bootstrap')}}"></script>
    {%- endblock scripts %}
    {%- endblock body %}
  </body>
{%- endblock html %}
</html>
{% endblock doc -%}

なので、これを参照するflask-bootstrapのhtmlではscriptsブロックを拡張することで、jQueryの機能が使用できる。

前回作成したbootstrap.htmlを次のように修正するだけで、jQueryでボタンのイベントを処理できる。

contentブロックに<button id="btn">Button</button>の行でボタンを追加し、scriptsブロックでclickの処理を行っている。

{% extends "bootstrap/base.html" %}

{% block title %}Hello{% endblock %}

{% block content %}
<div class="container">
    <div class="page-header">
    <h1>Hello World!</h1>
    <button id="btn">Button</button>
    </div>
</div>
{% endblock %}

{% block scripts %}
{{ super() }}
<script>
$(function() {
  $('#btn').click(function(e) {
    $(this).text("Clicked");
  });
});
</script>
{% endblock  %}

これで「Button」ボタンをクリックすると、文字列が「Clicked」に変化する。

{{super()}}を忘れると、せっかく読み込んだCDNの設定が上書きされてしまい、 意図したように動作しないので注意。

Flask-Bootstrapを使ってみる

前回のつづき

pythonでWebアプリケーションをつくる場合、Flaskが便利。

Webアプリケーションでは、デザインを定義するためにCSSが必要だが意外とめんどくさい。 そこで定義済みのCSSフレームワークを使用することで、手間を軽減することができる。

そのうちの一つにBootstrapがある。

Flaskの使い方を探してみると、htmlファイルの作成時にBootstrapを使用している例が多い。

Flask-Bootstrapを使用すると、更にhtmlファイルの記述量を減らすことができる。

インストールは例によってpipでできるので省略する。

使い方

使い方の例を探してみると、簡単に書けるせいかいきなり凝ったものが多く、 意外とこれと言ったものがなかった。

ここでは何も凝らずに、ものすごく単純なものを作成する。

ディレクトリ構成

ディレクトリ構成はFlaskの時と同じ。

.
├── bootstrap.py
└── templates
    └── bootstrap.html

bootstrap.py(pythonスクリプト)

# coding: utf-8
from flask import Flask, render_template
from flask_bootstrap import Bootstrap

app = Flask(__name__)
bootstrap = Bootstrap(app)


@app.route('/')
def index():
    return render_template('bootstrap.html')


if __name__ == '__main__':
    app.run()

基本はFlaskの時と同じで、Bootstrapオブジェクトの作成が追加されている。 サイトのルートにアクセスすると、「bootstrap.html」の内容を返す。

bootstrap.html(htmlファイル)

たったこれだけの記述で、Bootstrapを使用したWebページを作成することができる。

{% extends "bootstrap/base.html" %}

{% block title %}Hello{% endblock %}

{% block content %}
<div class="container">
    <div class="page-header">
    <h1>Hello World!</h1>
    </div>
</div>
{% endblock %}

実体は、{% block content %}〜{% endblock %}の間の部分だけ。

{% extends "bootstrap/base.html" %}の行で、Flask-Bootstrapが予め定義しているテンプレートを参照しており、 この中で、BootstrapのCSSの定義や基本的なレイアウトが定義されている。

共通のテンプレートを参照することで、デザインの統一もより簡単に行なうことができる。

Flaskの機能も健在なのでjinja2の記法によって、pythonスクリプトからのデータの受け渡しも行なうことができる。

Flaskでpythonのウェブアプリケーションを作る

pythonでWebアプリケーションを作成する方法はいくつかあるが、 Flaskを使用すると非常に簡単に作成することができる。

インストールはpipでできるので、詳細は省く。

使い方

ディレクトリ構成

作業ディレクトリのルートにpythonスクリプトを置き、「templates」ディレクトリ配下にhtmlファイルを置く。

次に例を示す。

.
├── hello.py
└── templates
    └── hello.html

hello.py(pythonスクリプト)

スクリプトの例を示す。

# coding: utf-8
from flask import Flask, render_template
app = Flask(__name__)


@app.route("/")
def hello():
  return "Hello World!"


@app.route("/hello")
def index():
   hello = 'hello'
   return render_template('hello.html', message=hello)


if __name__ == "__main__":
    app.run()

この例では、サイトのルートにアクセスされると、「Hello World!」とブラウザに返し、 「/hello」にアクセスされると、「hello.html」の内容をブラウザに返す。

この時、引数messageを介して「hello」という文字列をhtmlファイルへ渡している。

htmlファイル

「hello.html」の例を次に示す。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
    <title>Title</title>
</head>
<body>
<div class="container">
    <div class="page-header">
        <h1>{{message}}</h1>
    </div>
</div>
</body>
</html>

{{{message}}で、pythonスクリプトから渡された文字列を表示する。 スクリプトから渡されたデータはjanja2の記法によってhtmlから参照することができる。

Flaskを使用すると、pythonで計算した結果などを簡単にブラウザに表示できるようになるのが便利。

また、見栄えを良くするためにBootstrapを読み込んでいる。Flaskを使用するアプリを紹介しているサイトでは、Bootstrapと組み合わせているものが多かった。

ブラウザからアクセス

スクリプトを実行し、同じPC上でブラウザから「http://127.0.0.1:5000」にアクセスすると動作を確認することができる。

デフォルトでは他のマシンからのアクセスは許可されていないため、これを許可するには、pythonスクリプト側で次のようにする。

app.run(host='0.0.0.0')

ポートを変更する場合は次のようにする。

app.run(host='0.0.0.0', port=8080)

参考サイト

下記のサイトが非常に参考になった。

今回の例では、特にhtml側でデータの有無をチェックせずに参照しているが、このサイトではそれらのチェックの方法や、 String以外のデータの受け渡しなども紹介している。

上記のサンプルはほぼ丸パクリ。

yoctoでの使用方法

meta-pythonに次のパッケージがある。

Yocto raspberrypi3でbootchartを試す(systemd編)

Yoctoのbootchartのおはなし systemdを使用する場合は「systemd-bootchart」が使用できる。

本当は、systemdの時もbootchart2を使用して同じ条件で計測してみたいところだけど。 raspbianはsystemdなので、そちらとの比較の場合は有効な手段かもしれない。

local.confの修正

以下の設定でsystemdを有効化する。

# systemd
DISTRO_FEATURES_append = " systemd"
VIRTUAL-RUNTIME_init_manager = "systemd"
DISTRO_FEATURES_BACKFILL_CONSIDERED = "sysvinit"
VIRTUAL-RUNTIME_initscripts = ""

bootchart関連のためにさらに以下を追加する。

IMAGE_INSTALL_append = " systemd-bootchart"
CMDLINE_append = " initcall_debug printk.time=y init=/lib/systemd/systemd-bootchart"

追加パッケージ

systemdの場合は「systemd-bootchart」の追加だけでよい。

カーネルオプション

CMDLINE変数に次を追加しsystemd-bootchartが起動されるようにする。

initcall_debug printk.time=y init=/lib/systemd/systemd-bootchart

次のコマンドでsystemd-bootchartのログを見ると、「/run/log」以下にsvg形式の画像が出力されていることがわかる。

# systemctl status systemd-bootchart
● systemd-bootchart.service - Boot Process Profiler
   Loaded: loaded (/lib/systemd/system/systemd-bootchart.service; enabled; vendor preset: enabled)
   Active: inactive (dead) since Wed 2018-03-07 05:24:34 UTC; 3min 39s ago
     Docs: man:systemd-bootchart.service(1)
           man:bootchart.conf(5)
  Process: 113 ExecStart=/lib/systemd/systemd-bootchart -r (code=exited, status=0/SUCCESS)
 Main PID: 113 (code=exited, status=0/SUCCESS)

Mar 07 05:24:34 raspberrypi3 systemd-bootchart[113]: Error reading disk model for mmc: No such file or directory
Mar 07 05:24:34 raspberrypi3 systemd-bootchart[113]: systemd-bootchart wrote /run/log/bootchart-20180307-0524.svg
Mar 07 05:24:34 raspberrypi3 systemd-bootchart[113]: Bootchart created: /run/log/bootchart-20180307-0524.svg
Warning: Journal has been rotated since unit was started. Log output is incomplete or unavailable.

yocto raspberrypi3でbootchartを試す(sysvinit編)

bitbakeで作成したlinuxの起動時の状況を知るためにbootchartを使用したい。 pokyには「bootchart2」が収録されているので、これを使用する。

meta-raspberrypiの組み込みおよび、bitbake rpi-basic-imageまで作成できている事を前提とする。

local.confの修正

local.confに次の内容を追加する。

PACKAGE_INSTALL_append = " bootchart2 bootchartd-stop-initscript pybootchartgui"
CMDLINE_append = " initcall_debug printk.time=y init=/sbin/bootchartd"

bootchart2関連のパッケージと、OS起動時にbootchart2を実行するためのカーネルオプションの設定を追加している。

追加パッケージ

次のパッケージを追加する。

パッケージ 機能
bootchart2 bootchart2本体
bootchart-stop-initscript OS起動後にbootchart2の終了
pybootchartgui bootchart.pngの生成

bootchart2はサービスの終了時に、「/var/log/bootchart.tgz」を作成するが、 デフォルトでは、手動で終了させるまで実行され続けるため、 ブートプロセスの終了と同時に、bootchart2を終了させるために「bootchart-stop-initscript」をインストールする。

また、bootchart.tgzはそのままではただのログデータのアーカイブなので、 これをpng形式のチャートに変換するためにpybootchartguiをインストールする。

pybootchartguiをインストールしておくと、ブートプロセス終了時に実行され、 bootchart.tgzからpng画像が作成されるようになる。

カーネルオプション

meta-raspberrypiではカーネルへ渡すコマンドラインの文字列はCMDLINE変数で設定されるため、この変数に次を追加する。

initcall_debug printk.time=y init=/sbin/bootchartd

これで、起動時にbootchart2が実行されるようになる。