みつきんのメモ

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

raspberrypi3 model b+ USBブート

ラズベリーパイ3 Model B+は、初期状態でUSBブートが可能になっている。

もともとのラズベリーパイ3(Model B)では、 OTPという一度だけ書き込みができる特殊なメモリの設定を変更することでUSBブートができるらしいのだが、 間違った設定をすると文鎮化するということもあって、なかなか手を出せないでいた。

個人的にはそんなに需要もないし。

やりかた

USBブートの方法は簡単。

今まで、EtcherやWin32DiskImagerでSDに書き込んでいたイメージを、USBストレージに書き込んで、 BOOT用のパーティションにある「cmdline.txt」の「root=/dev/mmcblk0p2」を「root=/dev/sda2」に変更するだけで良い。

そして、ラズベリーパイにSDカードをささずに、USBストレージを挿した状態で電源を投入すると、きちんとLinuxが起動する。

yocto

yocto環境でも簡単。local.confに下記の内容を追加すれば、USBストレージからブートできる。

CMDLINE_remove = "root=/dev/mmcblk0p2"
CMDLINE_append = " root=/dev/sda2"

まとめ

USBブートができるようになると、USBメモリはもちろんUSB接続のハードディスクなどでラズベリーパイを起動できるようになる。

システムのストレージの大容量化や、書き込み回数に制限のあるSDカードでは都合が悪い場合など、いままで使いづらかった用途でも、活用範囲が広がるかもしれない。

raspberrypi3 model b+を購入した

ラズベリーパイ3をパワーアップしたModel B+が届いた。

f:id:mickey_happygolucky:20180602074124j:plain

主には

  • WiFiが5GHz対応になった!
  • CPUのクロックが1.2GHzから1.4GHzになった!
  • Etherがギガビットになった!

とか。気になるところはこんなところ。Bluetoothはあまり気にならないかな。

概ね使いやすいんだけどちょっとずつ気に入らなかったところが解消した感じ。 NASを作りたいけどギガビットEtherじゃないんで、わざわざRock64買ったりとしてたし。

ちゃんと技適も取ってるし。

f:id:mickey_happygolucky:20180602074401j:plain

とりあえず、yoctoは動いた。

root@raspberrypi3:~# cat /proc/device-tree/model && echo                        
Raspberry Pi 3 Model B Plus Rev 1.3

マルチスレッド環境でのtar

最近ではCPUはマルチコアであることが多く、プログラムがマルチスレッドに対応している場合、 処理時間を大幅に短縮することができる。

意外と機会の多いtarコマンドによる圧縮伸長処理はデフォルトではシングルスレッドで動作するため、 マルチコアCPUの恩恵が受けられない。

tarコマンドがマルチコアCPUの恩恵を受けるための方法を調べた。

従来の方法

tar使用時の圧縮形式とオプションについては次の表のようになる。

圧縮形式 拡張子 圧縮 伸長 圧縮率 処理時間 備考
GZIP tar.gz cfz xf
BZIP2 tar.bz2 cfj xf
XZ(LZMA2) tar.xz cfJ xfJ 1.22からサポート

大体、vオプションを含んで実行することが多いため、コマンドの実行例は次のようになる。

$ tar cvfz HOGE.tar.gz ./HOGE

マルチスレッド対応の圧縮コマンド

tarで指定できる形式で、マルチスレッド対応の圧縮伸張コマンドが存在する。

圧縮形式 圧縮伸張コマンド
GZIP pigz
BZIP2 pbzip2
XZ(LZMA2) pxz/pixz

pxzとpixz

pixzはxzを圧縮する際にインデックス情報を保持させることができる。 インデックスを参照ことで、アーカイブから一部のファイルだけ取り出す場合に全体を展開する必要がなくなる。

pixzで作成したアーカイブもxzやpxzで伸張することができるが、その場合インデックスは意味をなさない。

インストール方法

Ubuntuであればaptでインストールできる

$ sudo apt install pigz pzip2 pxz

tarのオプション

tarでは-Iもしくは--use-compress-programで圧縮伸張に使用するコマンドを指定する事ができる。

tarで指定する場合は次のようなオプションとなる。

圧縮形式 拡張子 圧縮オプション 伸張
GZIP tar.gz -I pigz -cf -I pigz -xf
BZIP2 tar.bz2 -I pbzip2 -cf -I pbzip2 -xf
XZ(pxz) tar.xz -I pxz -cf -I pxz -xf
XZ(pixz) tar.xz -I pixz -cf -I pixz -xf

実行例(GZIP)

圧縮

$ tar -I pigz -cf HOGE.tar.gz ./HOGE

伸張

$ tar -I pigz -xf HOGE.tar.gz

実行例(BZIP2)

圧縮

$ tar -I pbzip2 -cf HOGE.tar.bz2 ./HOGE

伸張

$ tar -I pbzip2 -xf HOGE.tar.bz2

実行例(XZ/pxz)

圧縮

$ tar -I pxz -cf HOGE.tar.xz ./HOGE

伸張

$ tar -I pxz -xf HOGE.tar.xz

実行例(XZ/pixz)

圧縮

$ tar -I pixz -cf HOGE.tar.xz ./HOGE

伸張

$ tar -I pixz -xf HOGE.tar.xz

性能比較

timeコマンドによって時間を計測した。

timeコマンドではrealusersysの3つの時間が表示される。 これらの値は次のような意味を持っている。

概要 備考
real コマンドを開始してから終了するまでの時間
user ユーザープログラムのCPU使用時間 アプリやライブラリ
sys システムのCPU使用時間 カーネル

処理が正しく並列に実行されている場合は、実際に処理にかかる時間(real)よりもプログラムのCPU使用時間(user)の方が大きくなる。

圧縮性能

シングル マルチ シングル(msec) マルチ(msec)
GZIP real 0m37.454s 0m4.495s 37454 4495
user 0m24.996s 0m31.620s 24996 31620
sys 0m1.676s 0m1.104s 1676 1104
BZIP2 real 1m39.269s 0m31.026s 99269 31026
user 1m37.900s 3m44.148s 97900 224148
sys 0m1.384s 0m3.836s 1380 3836
XZ(pxz) real 4m34.652s 0m49.782s 274652 49782
user 4m33.948s 5m48.872s 273948 348872
sys 0m2.600s 0m3.392s 2600 3392
XZ(pixz) real - 0m46.958s - 46958
user - 5m42.244s - 342244
sys - 0m2.120s - 2120

マルチスレッド版はどのコマンドでもreal < userとなり、並列に処理されていることがわかる。

realの時間を比較しても、高速に処理されていることがわかる。

圧縮に関しては、pxzとpixzで大きな差は見られなかった。

伸張性能

シングル マルチ シングル(msec) マルチ(msec)
GZIP real 0m4.498s 0m2.272s 4498 2272
user 0m4.268s 0m2.740s 4268 2740
sys 0m0.936s 0m1.056s 936 1056
BZIP2 real 0m42.598s 0m11.781s 42598 11781
user 0m41.976s 1m29.540s 41976 89540
sys 0m1.612s 0m1.144s 1612 1144
XZ(pxz) real 0m25.418s 0m25.817s 25418 25817
user 0m25.140s 0m24.984s 25140 24984
sys 0m1.988s 0m1.892s 1988 1892
XZ(pixz) real - 0m5.744s - 5744
user - 0m32.068s - 32068
sys - 0m1.352s - 1352

圧縮と比較するとシングルスレッド版でもそれほど時間がかからないので、差は小さくみえる。

pigzとpbzip2に関しては並列に処理されているが、pxzに関しては何故かシングルスレッドで動作しているようだった。 pixzでは狙い通りマルチスレッドで動作されているため、高速に処理されていることがわかる。

このためUbuntu16.04では、xz形式のファイルで並列処理したい場合は、pxzよりもpixzの方がよさそう。

結論

pxzコマンド以外は、マルチスレッド対応のコマンドを使用することで大幅に処理時間を短縮できることが解った。 意外とtarでの圧縮伸張はする機会が多いため、これらを有効に活用することで作業時間を短縮できる。

pyconnmanで遊んでみる。

pyconnmanを使用するとconnmanの機能にpythonからアクセスできるようになる。

クラス

pyconnmanモジュールには次のクラスが定義されている。

クラス 概要
ConnTechnology tecnology
ConnManager connman本体
ConnCanceledException
ConnInterface ネットワークインターフェース
ConnLaunchBrowseException
ConnService サービス
ConnSignalNameNotRecognizedException
ConnSimpleInterface シンプルインターフェース
GenericAgent エージェント
SimpleWifiAgent Wifiエージェント

technologyのリストを取得

最初にtechnologyのリストを取得してみる。

technologyのリストを取得は次のような流れになる。

f:id:mickey_happygolucky:20180411062122p:plain

このようにConnManagerクラスからリストを取得する。

get_technologies()メソッドが返すデータは次のようなペアの配列となっている。

データ 概要
path パス文字列
technology プロパティの辞書

technologyの辞書には次のようなデータが入っている。

キー データ データ型
Name tecnology名称 dbus.String
Type タイプ文字列 dbus.String
Powered 有効/無効 dbus.Boolean
Connected 接続/未接続 dbus.Boolean
Tethering テザリング dbus.Boolean
import pyconnman

# Get manager class object
manager = pyconnman.ConnManager()

# Get list of technologies
technologies = manager.get_technologies()

# Iterate and print data in the list
for (path, technology) in technologies:
    print path
    print '======================='
    for prop in technology.keys():
        print prop + ' : ' + str(technology[prop])
    print '======================='

technologyのプロパティを設定する

プロパティを設定するには、ConnTechnologyクラスのオブジェクトを生成する必要がある。 ConnTechnologyオブジェクトの生成にはpathが必要となるため、 一度ConnManagerオブジェクトからget_technologies()で情報を取得する必要がある。

WiFi technologyのPoweredプロパティをTrueに設定するスクリプトを作成する。

全体の流れは次のようになる。

f:id:mickey_happygolucky:20180411062214p:plain

スクリプトを次に示す。

import pyconnman

# Get manager object
manager = pyconnman.ConnManager()

# Get list of technologies
technologies = manager.get_technologies()

# Get path of Wifi technology
wifi_path = ""
for (path, technology) in technologies:
    if technology['Name'] == 'WiFi':
        wifi_path = path

# Get Technology object from Wifi path
wifi_technology = pyconnman.ConnTechnology(wifi_path)

# Set Powered property to True
if not wifi_technology.get_property(name='Powered'):
    wifi_technology.set_property(name='Powered', value=True)
else:
    print 'Powered already set to True.'

ここまでがtechnology関連の基本操作となる。

任意のtechnologyのpathを取得できるようにしておく

pathを取得するためにいちいちget_technologies()を実行するのも手間なので、 get_technologies()メソッドからtechnologyのリストを取得した時に technology名でpathが引けるように辞書を作成しておくと便利になる。

...(省略)...

# Create the dictionary that contains path and names.
tech_dict = {}
for (path, technology) in technologies:
    tech_dict[technology['Name']] = path

# Get Technology object from Bluetooth path
bt_technology = pyconnman.ConnTechnology(tech_dict['Bluetooth'])

...(省略)..

yoctoのbitbakeで作ったLinuxでPyCharmのリモートデバッグ環境をつくる

これがしたかったために、有償版を買った。

PyCharmを使った普通のリモートデバッグ環境の構築はたくさんあると思うので、 yoctoで作ったラズベリーパイ3向けの環境でリモートデバッグする環境を作る。

その際にハマったところピックアップする。

確実に名前でアクセスできるようにする

local.confに下記を追加して、確実に「raspberrypi3.local」でアクセスできるようにする。

詳細についてはavahi-autoipdでDHCPサーバが無い場所でもIP通信を参照。

# avahi
IMAGE_INSTALL_append = " \
             avahi-daemon \
             avahi-autoipd \
"

リモートデバッグの設定

通常の手順でプロジェクトを作成した後に「File」->「Settings...」で設定ダイアログを出し、 左側のツリーの検索フィールドで「Project Interpreter」の項目を選択する

f:id:mickey_happygolucky:20180411060441p:plain

ダイアログ右端の歯車アイコンをクリックし「Add...」を選択する。

「Add Python Interpreter」ダイアログが表示されるので、画面左のリストから「SSH Interpreter」を選択する。

f:id:mickey_happygolucky:20180411060510p:plain

次の内容を入力し「Next」ボタンをクリックする。

項目
Host raspberrypi3.local
Username root
Port 22

次の画面でそのまま「Finish」ボタンをクリックする。

java.io.IOException: inputstream is closed」のエラー

SSHインタプリタの追加画面で「Finish」ボタンをクリックした時に次のような画面が表示された場合は、 ターゲット側でSFTPサーバが動作していないことが疑われる。

f:id:mickey_happygolucky:20180411060519p:plain

回避するためには、次のようにlocal.confに追加する。

# ssh/sftp
EXTRA_IMAGE_FEATURES += "ssh-server-openssh"
IMAGE_INSTALL_append = " openssh-sftp-server"

PyCharmではターゲット側へのファイルの転送にSFTPを使用する。 dropbearのSFTPサーバでも良いかもしれないが、opensshのほうがおそらく無難。

イメージをSDに書きなおした後、再度SSHインタプリタの追加を実施する。

リモート側のインストール済みパッケージが表示されない

ターゲット側にpipがインストールされていないとこのようになる。

f:id:mickey_happygolucky:20180411061059p:plain

local.confで次を追加する。

# pip
IMAGE_INSTALL_append = " python-pip"

「Couldn't refresh skeletons for remote interpreter」のエラー

リモートとの接続時に次のようなエラーが発生する。

f:id:mickey_happygolucky:20180411061146p:plain

Couldn't refresh skeletons for remote interpreter
            failed to run generator3.py for sftp://root@raspberrypi3.local:22/usr/bin/python, exit code 2, stderr: 
            -----
            /usr/bin/python: can't open file '/home/root/.pycharm_helpers/generator3.py': [Errno 2] No such file or directory
            -----

これは、SDにOSイメージを書き直した場合などに発生することが多く、その原因はPyCharmのリモートヘルパーパッケージがターゲット上で展開されていないためである。

リモートヘルパが正しく機能していないと、FlyCheckの処理でターゲットにインストールされているモジュールの解決がうまく行かないなどの症状が発生する。

このエラーを解消するためには、ターゲット側でリモートヘルパーパッケージを展開する必要がある。 一度SSHでターゲットにログインし、リモートヘルパーが転送済みか確認する。

ターゲット側で次のコマンドを実行する。

# cd ~/
# ls -la

この時点でホームディレクトリに「.pycharm_helpers」が存在しない場合は、PyCharmで一度Pythonスクリプトを実行してみる。 すると、ヘルパーパッケージがデプロイされるので、再度ホームディレクトリを確認する。 「.pycharm_helpers」の存在を確認した後で次のコマンドを実行する。

# cd ~/.pycharm_helpers
# tar xvf ./helpers.tar.gz

tarの展開が終わったらPyCharmでメニューの「File」->「Invalidate Caches / Restart...」を選択する。

f:id:mickey_happygolucky:20180411061308p:plain

ダイアログの「Invalidate and Restart」ボタンを押しキャッシュをクリアとPyCharmの再起動を行う。

f:id:mickey_happygolucky:20180411061326p:plain

すると、再起動にリモートインタプリタの更新が開始され、これらの処理が終了すると正しくリモートと接続されるようになる。

avahi-autoipdでDHCPサーバが無い場所でもIP通信

USB-Ethernet変換を使って、ラズパイなどのボードと1対1で接続するようなケースでは、 DHCPサーバをいちいち用意するのが面倒。

そういう場合でも、SSHでログインなどしたかったりするのでTCP/IPで通信したい。 そこでavahi-autoipdを使うとIPアドレスを解決できるようにしてくれる。

これで何がしたいかというと、組み込み機器として作成したラズベリーパイにWebインターフェースで設定画面を用意したとして、 ブラウザでアクセスするためにはHTTP(TCP/IP)での通信が必要となる。 その時のネットワークに確実にDHCPサーバが存在するわけではないため、 DHCPサーバがなくても、OSインストール後に設定なしの状態で確実にWebインターフェースにアクセスできるようにしたい。

yoctoで追加

local.confで次のパッケージを追加する。

IMAGE_INSTALL_append = " avahi-daemon \
                 avahi-autoipd \
"

マシンがラズベリーパイ3だった場合、

avahi-daemonraspberrypi3.localの名前でアクセスできるようになる。

avahi-autoipdで、DHCPサーバがなかった場合IPv4LLにフォールバックされ169.264.XXX.XXXのようなIPアドレスが割り振られるようになる。

PC側のネットワークインターフェースの設定

有線LANがひとつしかない場合などは特に設定はいらない(と思う)。

USB-Ethernet変換を使う場合は、そのNICを有効化するために適当にIPアドレスを割り当てる。

NICがそれぞれ有効になったら、次のようにpingを打ってみる。

$ ping raspberrypi3.local
PING raspberrypi3.local (169.254.13.XXX) 56(84) bytes of data.
^C
--- raspberrypi3.local ping statistics ---
23 packets transmitted, 0 received, 100% packet loss, time 22102ms

名前に対してIPアドレスが振られているのに、通信ができない場合はルーティングを設定してみる。

$ sudo route add -net 169.254.0.0 netmask 255.255.0.0 dev eth0 metric 99

eth0のところは適宜NICの名前に変更する。

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