Raspberry Piとpyenv

当然Pythonを入れねばならない。Pythonならpyenvである。

git clone https://github.com/pyenv/pyenv.git $HOME/.pyenv

して.zshrcとかに

export PYENV_ROOT=${HOME}/.pyenv
if [ -d ${PYENV_ROOT} ]; then
    export PATH=${PYENV_ROOT}/bin:$PATH
    export PYTHONPATH=./pyenv/python:$PYTHONPATH
    eval "$(pyenv init -)"
fi

書いて

source ~/.zshrc

して

pyenv install 3.6.5

して終わり。とか思っていたが最後のインストールで躓いた。なぜだ。どうやらopenSSLがないかららしい。
必要なもの諸々を入れる。

sudo apt-get install -y git openssl libssl-dev libbz2-dev libreadline-dev libsqlite3-dev

これで問題なくインストールできる。
ちなみにbz2とreadlineがないとインストール後にWarningを食らう。こんな感じ↓

$ pyenv install 3.6.5
Downloading Python-3.6.5.tar.xz...
-> https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tar.xz
Installing Python-3.6.5...
WARNING: The Python bz2 extension was not compiled. Missing the bzip2 lib?
WARNING: The Python readline extension was not compiled. Missing the GNU readline lib?

sqliteがないとipythonとかのhistoryが保存できない。ひとまずこれでPythonの環境が作れた。

Raspberry Piの設定の備忘録

Rapberry Piを買った。単純にRaspberry Pi が欲しかったのとSwitch Botのためだ。ninhydrin.hatenablog.com

デスクトップ用途ではなくサーバー用途なのでそのために幾つか設定を行った。思っていた以上に良かったので今後も購入する可能性がある。そのときのために設定を書き残しておく。

ユーザーの追加

sudo adduser hoge
sudo gpasswd -a hoge sudo
sudo gpasswd -d pi sudo
rm /etc/sudoers.d/010_pi-nopasswd # パスワード無しでのsudoをさせない

sudo passwd pi # pi ユーザーのパスワードを変更
# su hoge ログイン

piユーザーのパスワードを変更しているがpi ユーザーの名前を変更したほうがいいかもしれない。削除はしない。
rootのパスワードについては設定しなければログインそのものができないそうなのであえて設定はしていない。

公開鍵認証でSSH接続

まずは鍵を作成。

ssh-keygen -t rsa
chmod 600 id_rsa

パーミッションは忘れてはいけない。
次に公開鍵(pubの方)をRasPiに渡す。

cat id_rsa.pub >> authorized_keys
chmod 600 authorized_keys

ここでもパーミッションを忘れない。
次に何らかのエディタでsshd_configを編集

sudo emacs /etc/ssh/sshd_config

幾つか変更

Port 11011 # 22以外に適当に
PermitRootLogin no
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no # パスワードログイン禁止

sshサーバーを再起動

sudo /etc/init.d/ssh restart

起動時にSSHを有効にする

sudo touch /boot/ssh

VNCで接続できるようにする

こちらを参照
Raspberry Piの設定【VNCサーバ(tightVNC)の設定】 - Aldebaranな人のブログ
ありがとうございました。
ただこちらの方法だと起動時にvncサーバーが立ち上がらなかった。シバンの

#! /bin/sh

をファイルの一番上に持ってくることで起動するようになった。

HDMIをオフにする

VNCで接続できるようになったのでHDMI出力はいらない

tvservice -o # HDMIオフ
tvservice -p # HDMIオン

このオフの設定を/etc/rc.localに書き込む

sudo emacs /etc/rc.local # root権限が必要

linuxの起動時に自動的に実行するコマンドを書き込むファイルらしい。

デフォルトのログインユーザーを変更

sudo emacs /etc/lightdm/lightdm.conf

autologin-user=piとなっているところを任意のユーザーに変更する。


とりあえずこんなところかな。何かあったら追記していく。

Raspberry PiとGoogle HomeとSwitch Botと

Switch Botに興味があったので買ってしまった。2個で7000円なので一つあたり3500円。これぐらいの値段なら買ってもいいかなと思える。

Google Homeを持っておりGoogle HomeからSwitch Botを動かすのが本命。Google Homeと連携しようと思うとSwitch Linkが必要になるがこれはいい値段するので買うのを躊躇してしいた。色々調べてみるとRaspberry Piから操作できるようなのでRaspberry Piで操作する(汎用性も高いしね)。エンジニアなのに恥ずかしながらRaspberry Piを触ったことがないのでいい機会だ。個人的なRasPiの設定も後日書く予定


Raspberry PiGoogle Homeの連携について参考にしたのは以下
IFTTTとBeebotteを使ってGoogleHomeからRaspberryPiを操作する - Qiita

おかげさまでGoogle Homeに話しかけて何かしらのレスポンスを受け取れるようになった(Beebottle便利ですね)。ここまでくればこっちのものであとはSwitch Bot にかぎらずpythonでなんでもできる。今後も何かやっていこう。
Switch Botpythonで動かすコードはこちら。このSwitch Botを動かす処理をon_messageの中に書けば終わり。これでGoogle HomeからSwitchLinkを使わずにSwitch Botを動かせるようになった。

ただ困ったところが2つある。
一つ目は反応が少し遅い。p = Peripheral("ff:ff:ff:ff:ff:ff", "random")で接続した時に失敗することがある(失敗しても1回ぐらい)。これはfor文tryすることでなんとかなる。
二つ目の問題は重大でセキュリティに関して。SwitchBotは専用アプリがあって一つ一つのSwitch Botにパスワードを設定できる。パスワードを設定することで他人がSwitch Bot動かせないようになっており安心。なのだがパスワードを設定すると上記pythonスクリプトが動かない。つまり現状Google HomeからセキュアにSwitch Botを動かすにはSwitchLinkしか選択肢がない。うーむ、どうしたもんか。
なおソースコードはこちら。まあ殆ど上記の記事のまんまである。

import json
import logging
import time

import paho.mqtt.client as mqtt
import binascii
from bluepy.btle import Peripheral


TOKEN = "token_***********"  # beebottleのチャンネルトークン
HOSTNAME = "mqtt.beebotte.com"
PORT = 8883
TOPIC = "channel_name/resource_name"
CACERT = "mqtt.beebotte.com.pem" # 証明書 https://beebotte.com/certs/mqtt.beebotte.com.pem

SWITCH_MAC = "ff:ff:ff:ff:ff:ff"
SWITCH_NAME = "switch_name"

_LOGGER = logging.getLogger(__name__)

class SwitchBot:
    def __init__(self, name, mac):
        self._mac = mac
        self._name = name

    def turn_on(self):
        for connection in range(1,6):
            try:
                p = Peripheral(self._mac, "random")
            except:
                _LOGGER.error(f'Connection attempt failed after {connection} tries')
                time.sleep(1)
                continue
            break
        else:
            _LOGGER.error('Connection to Switchbot failed')

        try:
            hand_service = p.getServiceByUUID("cba20d00-224d-11e6-9fb8-0002a5d5c51b")
            hand = hand_service.getCharacteristics("cba20002-224d-11e6-9fb8-0002a5d5c51b")[0]
            hand.write(binascii.a2b_hex("570100"))
            p.disconnect()
        except:
            _LOGGER.error("Cannot connect to switchbot.")

switch_bot = SwitchBot(SWITCH_NAME, SWITCH_MAC)


def on_connect(client, userdata, flags, respons_code):
    print('status {0}'.format(respons_code))
    client.subscribe(TOPIC)

def on_message(client, userdata, msg):
    data = json.loads(msg.payload.decode("utf-8"))["data"][0]
    data = {key:value.strip() for key, value in data.items()}
    #  この辺で条件分岐してスイッチを動かす
    swich_bot.turn_on()

client = mqtt.Client()
client.username_pw_set(f"token:{TOKEN}")
client.on_connect = on_connect
client.on_message = on_message
client.tls_set(CACERT)
client.connect(HOSTNAME, port=PORT, keepalive=60)
client.loop_forever()

パスワードを設定すると動かないのは私だけなのか?でもSwitch Botを動かすスクリプトを見た感じパスワードを送信するようなところはないのでMacアドレスさえわかれば近くのSwitch Botを動かせることになる。多分公開してないパスワード関連のAPIがあるのだろう。早く公開してほしいものである。もしもパスワード設定してもpythonから動かせたよという人がいたらぜひ教えて頂きたい。

pythonでターミナル上のカーソルを上に移動する

プログレスバーなどを作りたいときにsys.stdout.write("~\r") のような感じでキャリッジリターンを使っていたがこれだとカーソルを行頭には戻せるがn行上には戻せない。なので全ての情報を1行に記述するしかなく表示する情報に限界がある。カーソルの移動くらい方法があるだろうと調べてエスケープシーケンスにたどり着いた。ターミナル上のカーソルの移動や文字の削除などができる特殊文字列のようだ。キャリッジリターンもそうだったんだな。知らんかった。

使い方はESC[**といった感じでこれをディスプレイに送る。ESCは\033もしくは\x1bのどっちでもいい。やりたいことに応じて**の部分を変更する。例えばカーソルをn行上に移動するには\033[nAと書く。なのでカーソル3行上に移動させるには

print("\033[3A")

とすればいい。カーソルを上の行に移動できるのでprintでも問題ない。他にも\aでアラート音を出したり、\033[y;xHでコンソールのy行x列にカーソルを移動できたり、print("hoge\033[33mhoge\033[0mhoge")で真ん中のhogeだけ黄色くしたりできる。詳しくはエスケープシーケンス一覧などで検索すればいくらでも出てくるだろう。量が多すぎるのでここには書かない。まだまだ知らないことが多い

ちなみにプログレスバーtqdmを使っている。

numpyのRandomStateとmultiprocessing使用時のseed値の罠

numpyとmultiprocessingでシミュレーション的なことをしていた。思ったより良い感じにならなかったしなんとなくおかしいなと思って調査。
numpy.randomとmultiprocessingを使って並列に乱数生成する。適当にその乱数を出力してみる。

コード

import os
import time
from multiprocessing import Pool

import numpy as np


def func(x: int) -> None:
    print(f"x={x}, np.random={np.random.rand()}, pid={os.getpid()}")

if __name__ == '__main__':
    with Pool(10) as p:
        p.map(func, range(10))

結果

python test.py
x=0, np.random=0.5962941676890798, pid=29550
x=1, np.random=0.5962941676890798, pid=29551
x=2, np.random=0.5962941676890798, pid=29552
x=3, np.random=0.5962941676890798, pid=29553
x=4, np.random=0.5962941676890798, pid=29554
x=5, np.random=0.5962941676890798, pid=29555
x=6, np.random=0.5962941676890798, pid=29556
x=9, np.random=0.030654881568257686, pid=29550
x=7, np.random=0.5962941676890798, pid=29557
x=8, np.random=0.5962941676890798, pid=29558

ほとんど同じやんけ。
調べてみるとnumpyのRandomStateは生成時に現在時刻からseed値を決定するらしい(しっかり調べてない)。なんとなくわかった。
回避策としては親プロセスが子プロセスにseed値を与えるぐらい?

変更後

def func(y: tuple) -> None:
    x, seed = y
    np.random.seed(seed)
    print(f"x={x}, np.random={np.random.rand()}, pid={os.getpid()}")

if __name__ == '__main__':
    with Pool(10) as p:
        p.map(func, zip(range(10), np.random.randint(0, 2 ** 32 -1, 10)))

変更後結果

python test.py
x=0, np.random=0.9123107234323904, pid=30126
x=1, np.random=0.01700757843088274, pid=30127
x=2, np.random=0.15402676571119933, pid=30128
x=3, np.random=0.8384796480668341, pid=30129
x=4, np.random=0.03180545379613742, pid=30130
x=5, np.random=0.9923826065352217, pid=30131
x=6, np.random=0.8970540102911799, pid=30132
x=7, np.random=0.9861043353661219, pid=30133
x=8, np.random=0.6360555488868728, pid=30134
x=9, np.random=0.9287125485831335, pid=30135

もっとエレガントな方法はないのか。