GAミント至上主義

Web Monomaniacal Developer.

Raspberry PiでPython3を使ってFirestoreにクエリする

Raspberry PiでFirebaseのFirestoreを介して外部と通信するために、最初はNode.jsのライブラリでやろうとがんばってたけど、1日やっても下記エラーが出てダメそうなので、LED部分と同じくPythonでやる。

Node.jsでのエラー

Node.jsはaptで入れて、nでv10を入れたもの、公式からLinux Binaries (ARM) ARMv6を入れて動かしたけど同じだった。認証部分でコケてしまうがまったく同一のファイルでPixelbookのDebian上で動いた。

(node:873) UnhandledPromiseRejectionWarning: FetchError: request to https://www.googleapis.com/oauth2/v4/token failed, reason: connect ENETUNREACH 172.217.161.42:443 - Local (0.0.0.0:0)
    at ClientRequest.<anonymous> (/home/pi/led8/node_modules/node-fetch/lib/index.js:1455:11)
    at ClientRequest.emit (events.js:198:13)
    at TLSSocket.socketErrorListener (_http_client.js:392:9)
    at TLSSocket.emit (events.js:198:13)
    at emitErrorNT (internal/streams/destroy.js:91:8)
    at emitErrorAndCloseNT (internal/streams/destroy.js:59:3)
    at process._tickCallback (internal/process/next_tick.js:63:19)
(node:873) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:873) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Pythonの準備

pipのインストールは省略。
公式通り入れて
firebase.google.com

サンプルコードを動かす。キーファイルはjsの時と同じものを利用した。

import firebase_admin
from firebase_admin import credentials

cred = credentials.Certificate("path/to/serviceAccountKey.json")
firebase_admin.initialize_app(cred)

PythonでFirestoreにクエリする

今回は特定のコレクションの最新のドキュメントを1件取得し続けたいので下記のようなコードとなった。

import firebase_admin
from time import sleep 
from firebase_admin import credentials 
from firebase_admin import firestore 
 
cred = credentials.Certificate("accountKey.json")
firebase_admin.initialize_app(cred)
 
client = firestore.client()

# 監視したいコレクション 今回はドキュメントがtimestampを持ってるので並び替えしとく
collection_ref = client.collection('path', 'to', 'parent_document')\
    .order_by('timestamp', direction='DESCENDING')\
    .limit(1)

# 更新があったときのコールバック用関数
def update_callback(docs, changes, read_time): 
    print('changes', changes)
    print('read_time', read_time)
    for doc in docs: 
        print(doc.id, doc.to_dict()) 

# 監視の開始、サブプロセスで動く
watcher = collection_ref.on_snapshot(update_callback)

print(watcher) 

# メインプロセスはとりあえずWhileしとく。sleep()をかませないとCPU利用率が高くなるので適度につけとく。
while True:
    sleep(1) 

実行して、Firebaseのコンソールでドキュメントを追加したところ、ほぼリアルタイムで反応したのを確認できた

pi@raspberrypi:~/led8 $ python3 firestore.py 
<google.cloud.firestore_v1.watch.Watch object at 0xb5fe5610>
changes [<google.cloud.firestore_v1.watch.DocumentChange object at 0xb5674350>]
read_time 2019-07-01 02:40:33+00:00
aXPGdePvZFzc5nA6S4RE {'timestamp': DatetimeWithNanoseconds(2019, 7, 2, 15, 0, tzinfo=<UTC>)}
changes [<google.cloud.firestore_v1.watch.DocumentChange object at 0xb56742f0>, <google.cloud.firestore_v1.watch.DocumentChange object at 0xb5674350>]
read_time 2019-07-01 02:41:05+00:00
yWwK3RmSt49EhZazqICW {'timestamp': DatetimeWithNanoseconds(2019, 7, 3, 15, 0, tzinfo=<UTC>)}

家のRaspberry Pi へのSSH接続を楽にする設定

家のローカルネットワーク経由でSSH経由でラズパイに入ることが増えてきたので、手間を減らすためにいろいろ設定する。
特にラズパイに限ったことではない気がする。

最終的には

ssh pi

だけでログインできるようになる。

使ったのはRaspberry Pi Zero WH

OSは

pi@raspberrypi:~ $ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 9.9 (stretch)
Release:        9.9
Codename:       stretch

IPを固定する(DHCP内)

今までラズパイを再起動すると、前後してしまうので固定したかった。

下記を参考にした
Raspberry Pi に固定IPアドレスを割り当てる方法(Raspbian Jessie) - Qiita

# 現在のIPを確認する
# 作業前のログを残すの忘れたので、これは作業後のもの。
#  inet 192.168.0.81/24 で現在のIPと範囲を確認する。ルーター等によって違う。
# 今回は inet 192.168.0.6-9ぐらいで変わっていたので、
# /24なら一番右のところは変えて大丈夫そうという雑なネットワーク知識のもと81にした

$ pi@raspberrypi:~ $ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether b8:27:eb:40:53:0b brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.81/24 brd 192.168.0.255 scope global wlan0
       valid_lft forever preferred_lft forever
    inet6 240f:65:8a29:1:2bbd:361e:3a6c:9a0d/64 scope global mngtmpaddr noprefixroute dynamic 
       valid_lft 263sec preferred_lft 263sec
    inet6 fe80::636a:e31b:a209:3831/64 scope link 
       valid_lft forever preferred_lft forever
# 設定ファイルを開く
pi@raspberrypi:~ $ sudo vim /etc/dhcpcd.conf

# It is possible to fall back to a static IP if DHCP fails:
# define static profile
#profile static_eth0
static ip_address=192.168.0.81/24
#static routers=192.168.1.1
static domain_name_servers=192.168.0.1

static ip_addressのところだけ#をはずし、さっき決めたIPに変更する。
他のはそのままだったが今回は大丈夫だった。
static domain_name_serversを家のルーターのIPにしないと繋がらない場所が出てきた。

# 終わったら再起動して接続を確認する
pi@raspberrypi:~ $ sudo reboot

秘密鍵でログインする

SSHのたびに毎回パスワード打つの面倒なので、設定する。

公開鍵の作り方とかはありふれた情報なのでググる
SSH公開鍵認証で接続するまで - Qiita

今回はパスフレーズすら打ちたくないので、ラズパイ専用に新しくパスフレーズが空のものを作って設定した。
終わったら一度ログアウトして、サラッと入れるか試す。

.ssh/configで入力を省略する

詳しくは下記の記事。
~/.ssh/configについて - Qiita

# ラズパイではなく、ノートPCなどホスト側の作業
~$ vim .ssh/config

# 今回は下記3行だけ。
Host pi
HostName 192.168.0.81
User pi

確認する

~$ ssh pi
Linux raspberrypi 4.19.42+ #1219 Tue May 14 21:16:38 BST 2019 armv6l

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Jun 29 00:00:30 2019 from 192.168.0.6
pi@raspberrypi:~ $ 

たった6文字入力で入れるようになった

またsshを使う、scpなどでも有効なので

scp ./filename pi:~/

だけでできるようになる。

おまけ

さらに.bashrcや.bash_profileなどにエイリアスを登録すれば

.bashrc

alias pi="ssh pi"
pi

だけでおうちのラズパイにログインできるようになる

8つのLEDでLチカして2進数で256まで数えさせる【Raspberry Pi Zero WH】

前回、Raspberry Pi Zero WHで8つのLEDをチカチカさせたが、せっかく8つあるので2進数として扱い、みんな大好き8bitで256までカウントしてみようと思った。
uyamazak.hatenablog.com

計算機科学の教材として2進数を体感する教材としても利用価値があるかもしれない。

動画


コード

前回のコードからgenerate_demo_pattern()部分を差し替えるだけ。

今回も無駄にジェネレータを使い、さらに無理にでもリスト内包表記で表現することでコメントを入れてもびっくりするほど読みづらいコードとなった。 メモリが少ないラズパイでメモリ効率大事だとしても職場で書いたら場合によっては命に関わるレベル。

def generate_binary_list():
    for i in range(0, 256):
        print(i)
        yield [index + 1 for index, value in enumerate([int(b) for b in reversed(format(i, 'b').rjust(8, '0'))]) if value == 1]

書いた本人もすでに解読不能となっている。

出力はこんな感じで、要求通り。

0
[]
1
[1]
2
[2]
3
[1, 2]
4
[3]
5
[1, 3]
# 省略
248
[4, 5, 6, 7, 8]
249
[1, 4, 5, 6, 7, 8]
250
[2, 4, 5, 6, 7, 8]
251
[1, 2, 4, 5, 6, 7, 8]
252
[3, 4, 5, 6, 7, 8]
253
[1, 3, 4, 5, 6, 7, 8]
254
[2, 3, 4, 5, 6, 7, 8]
255
[1, 2, 3, 4, 5, 6, 7, 8]

せっかく無駄にがんばったので、途中までの過程もがんばってGoogle Colaboratoryで書き、GitHub Gistで公開してみた。ジェネレータにはしてないけどほぼ同じ。

良いことないけどリスト内包表記でここまで出来るって感じてもらえたら嬉しい。良いことはないっていうのが分かるのは良いことかも。
なお一度書いたら解読や変更は困難な模様で、Colaboratoryでは0から書き直している。

f:id:uyamazak:20190625232625p:plain

使ったラズパイは下記。

妙に安いカメラも届いたので次の土日につなげてみて、LEDの光具合を撮影できるようにしたい。