Raspberry Pi 3 Model BとMH-Z19でCO2測定

きっかけ

ほぼ自宅勤務になったので部屋の換気が気になり始める

そんな中で CO2 計測してみたいが、市販の計測器高いと感じていたがふとラズパイ (Raspberry Pi 3 Model B) が転がってることを思い出す。
センサーとかないのかなと調べてみたら、MH-Z19 というセンサーで計測できることを知る。

色々購入してみる

アマゾンで注文してみると 2 週間かかった。中国から送ってくるみたいなのでそんなもんなんだろう。 電子工作っぽいものは初めてなのでハンダゴテすら持ってなかったので USB はんだごてを買う。
(はんだの種類すら知らなかったので調べる)

さらにこのセンサーを取り付けるためには、どうやらピンヘッダ、ジャンプワイヤーってのが必要らしい。
→ 買う

届いたMH-Z19にもケーブルが付属していたが、これは使わない。
f:id:flick-flick:20200422224324j:plain

はんだ付けの練習

Youtube で勉強して何度か練習してはんだ付けをしてみる。
ちっちゃくてうまく見えないがうまくいったっぽい

ラズパイのインターフェースを調べてみる

どうやらこの 2 つのピンがキモらしい。

  • VIN = 電力を外部に出力(供給)するピンらしい。(乾電池で言うプラス極)
  • GND = 乾電池で言うマイナス極的なピンらしい

あとこの用語もよく出てきたのメモしておく

UART = Universal Asynchronous Receiver/Transmitter (ユーアートと読むらしい)
シリアル通信上の規格、信号線は TX(送信)を相手の RX(受信)に接続する。
- 各デバイスは対等
- 1 対 1 の通信
- 信号線は送受信の 2 本
- 9600bps と 115200bps が標準(最高 1Mbps 程度)

https://manual.atmark-techno.com/armadillo-guide-std/armadillo-guide-std-hardware-expansion_ja-1.0.1/ch04.html

MH-Z19 をラズパイにはんだ付けする

MH-Z19 でググると出てくる通りにはんだ付けする。
ピンヘッダというのを、MH−Z19 にぶっ刺してはんだ付けした後に、ジャンプワイヤーでラズパイと接続する。
この時点ではうまくいっているのかはわからない。

本体に雑に輪ゴムでくくりつけておくことにする。
f:id:flick-flick:20200509123457j:plain

MH-Z19 にコマンドを送ることができるユーティリティツールをラズパイにインストール

Python 環境整えて、mh_z19 ライブラリをインストール

MH-Z19 の Python 用ライブラリ
github.com

MH-Z19 の仕様書
https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf

動かす

# sudo python3 -m mh_z19
{"co2": 726}

すんなり計測できた(ありがたい
しばらくバックグラウンドプロセスでログを出し続けてみる
楽しい

外の空気が 400ppm らしいので、それがこのセンサーの最低値になるらしい。最低値としてリセットするためにしばらく外に放置して下記コマンドでリセットする。(キャリブレーションというらしい)

# sudo python3 -m mh_z19 --zero_point_calibration

さらになんかいい感じにキャリブレーションを調整してくれるモードがあるらしいので、有効にしておく。

# sudo python3 -m mh_z19 --abc_on

CO2 値がめっちゃ高い部屋で暮らしていたことを知る。

狭い部屋で締め切っているので当たり前っちゃーあたり前だけど、部屋についてる通気口をなんとかしてもうちょっとまともな数値(せめて 1000ppm 以下)をキープできないか?

部屋に換気ファンを付けてみる

PC 用のファンを取り付けてテストしてみる。
ラズパイは 5V 出力までしかできないので、トランジスタというのを使って昇圧すればいけるっぽい。
が、回路図の読み方がわからず挫折。

USB(5V) から電源をとって、12V に昇圧して PC 用ファンの 4 ピンにできるパーツを発見したので購入。
ラズパイの USB ポートからその変換パーツを通じて、PC ファンに電源を供給した状態で通気孔にファンを貼り付けてみる。
排気(部屋から外に空気を逃がす)で設置してみると改善なし。吸気(外から部屋に空気を吸い込む)で設置してみたら改善して常に 500 ~ 800ppm を維持できた

計測をモニタリング、アラート通知をしてみる

ssh でラズパイにログインして CO2 計測ログを見るのが面倒なので、せっかくなので Fluent-Bit でどこかにログ転送してみる。
datadog は無料枠だとアラート通知できないみたいなので、mackerel を使ってみる。

実際は curl を cron なんかでスケジュール実行して、mackerel に送ればいいだけなんだけど Fluent-Bit を使ってみたかった。
無事計測できて、とりあえず 1200ppm を超えたら slack に通知するように設定した。
f:id:flick-flick:20200822143559p:plain
寝てる間も通気孔のファン回しっぱなしだと締め切っていても800ppm維持できてるっぽい

よくわらなかったところ

MH-Z19 にコマンドを送るライブラリめっちゃ便利で楽だったが、Fluent-Bit から python 呼び出すときに外部ライブラリ依存させたくなかったので、MH-Z19へのコマンド送信部分を抜き出してFluent-Bitからの呼び出し用にScriptにしたらなんかよくわからんエラーが出てかなりの時間ハマった。

どうやら、ラズパイからMH-Z19への接続(通信)が多重になると出るエラーっぽい?よくわからんが tty を開け閉めしてたら直った。
この辺シリアル通信の知識がまったくないので調べることが多すぎて挫折した。

調べたことだけメモしておく

tty = 端末(ターミナル)のこと。ssh ログインした状態で `tty` すると現在の端末デバイス ID がわかる。 echo 'Hello' > /dev/pts/2 とかやると/dev/pts/2 の端末にメッセージ送信できる
stty = tty の各種設定を表示・変更できるコマンド

エラー内容

Traceback (most recent call last):
  File "/usr/local/lib/python3.7/dist-packages/serial/serialposix.py", line 501, in read
    'device reports readiness to read but returned no data '
serial.serialutil.SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 6, in f
  File "/usr/local/lib/python3.7/dist-packages/serial/serialposix.py", line 509, in read
    raise SerialException('read failed: {}'.format(e))
serial.serialutil.SerialException: read failed: device reports readiness to read but returned no data (device disconnected or multiple access on port?)

どうやら getty というサービスが起動していて、指定の tty (今回は/dev/ttyS0) の接続を監視し続けているらしい。

# systemctl status serial-getty@ttyS0.service
● serial-getty@ttyS0.service - Serial Getty on ttyS0
   Loaded: loaded (/lib/systemd/system/serial-getty@.service; disabled; vendor preset: enabled)
   Active: active (running) since Wed 2020-05-06 18:46:11 JST; 2min 41s ago
     Docs: man:agetty(8)
           man:systemd-getty-generator(8)
           http://0pointer.de/blog/projects/serial-console.html
 Main PID: 15798 (agetty)
    Tasks: 1 (limit: 4915)
   CGroup: /system.slice/system-serial\x2dgetty.slice/serial-getty@ttyS0.service
           └─15798 /sbin/agetty --keep-baud 115200,38400,9600 ttyS0 vt220

May 06 18:46:11 raspberrypi systemd[1]: Started Serial Getty on ttyS0.

で確認すると active になっているので

sudo systemctl stop serial-getty@ttyS0.service

で停止させて、再度

sudo systemctl start serial-getty@ttyS0.service

で開始させたらエラー解消した。 よくわからん

[参考]

窓を開けて新鮮な空気をいれよう!Raspberry Pi でCO2 濃度を測ろう - Qiita

ラズパイのGPIOシリアル通信時のエラー:serial.serialutil.SerialException device reports readiness to read but returned no data - suzu6

mh_z19b 仕様書 https://www.winsen-sensor.com/d/files/infrared-gas-sensor/mh-z19b-co2-ver1_0.pdf

github.com

プライバシーポリシー

広告の配信について

当サイトは第三者配信の広告サービス「Googleアドセンス」を利用しています。
広告配信事業者は、ユーザーの興味に応じた広告を表示するために「Cookie(クッキー)」を使用することがあります。Cookieを無効にする設定およびGoogleアドセンスに関して、詳しくはこちらをクリックしてください。
三者がコンテンツおよび宣伝を提供し、訪問者から直接情報を収集し、訪問者のブラウザにCookie(クッキー)を設定したりこれを認識したりする場合があります。

アクセス解析ツールについて

当サイトでは、Googleによるアクセス解析ツール「Googleアナリティクス」を利用しています。このGoogleアナリティクスはトラフィックデータの収集のためにCookieを使用しています。このトラフィックデータは匿名で収集されており、個人を特定するものではありません。
この機能はCookieを無効にすることで収集を拒否することが出来ますので、お使いのブラウザの設定をご確認ください。この規約に関して、詳しくはこちらをクリックしてください。

当サイトへのコメントについて

当サイトでは、スパム・荒らしへの対応として、コメントの際に使用されたIPアドレスを記録しています。これはブログの標準機能としてサポートされている機能で、スパム・荒らしへの対応以外にこのIPアドレスを使用することはありません。
当サイトでは、次の各号に掲げる内容を含むコメントは管理人の裁量によって承認せず、削除する事があります。
- 特定の自然人または法人を誹謗し、中傷するもの。
- 極度にわいせつな内容を含むもの。
- 禁制品の取引に関するものや、他者を害する行為の依頼など、法律によって禁止されている物品、行為の依頼や斡旋などに関するもの。
- その他、公序良俗に反し、または管理人によって承認すべきでないと認められるもの。

免責事項

当サイトで掲載している画像の著作権・肖像権等は各権利所有者に帰属致します。権利を侵害する目的ではございません。記事の内容や掲載画像等に問題がございましたら、各権利所有者様本人が直接メールでご連絡下さい。確認後、対応させて頂きます。
当サイトからリンクやバナーなどによって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。
当サイトのコンテンツ・情報につきまして、可能な限り正確な情報を掲載するよう努めておりますが、誤情報が入り込んだり、情報が古くなっていることもございます。
当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。

運営者:waysaku
初出掲載:2020年8月30日

RSSの <content:encoded> の文字列が数値文字参照になっているので読める文字列に変換する

こんなやつ

www.vogue.co.jp

大昔にRSSでシステム間連携を実装したときにハマった気がしたけど一切覚えてない。

content:encoded にある &#x30E1;&#x30FC;&#x30AC;&#x30F3;&#x5983; のような文字列はどうやら 符号化文字集合の一つである Unicode におけるその文字のCode Point を16進数で表示する 数値文字参照 という形式で出力されてるらしい。

https://ja.wikipedia.org/wiki/%E6%96%87%E5%AD%97%E5%8F%82%E7%85%A7#%E6%95%B0%E5%80%A4%E6%96%87%E5%AD%97%E5%8F%82%E7%85%A7%EF%BC%88%E6%96%87%E5%AD%97%E5%8F%82%E7%85%A7%EF%BC%89

ISO/IEC 10646 - Wikipedia

この数値文字参照を実際の文字に戻すには

            文字符号化方式
         vvvvvvvvvvvvvvvvvvvvv
文字 <-> Code Point <-> byte列
^^^^^^^^^^^^^^^^^^^
   符号化文字集合

のうち Code Point > 文字 をすればよい。
Pythonでやる場合は unicodedata モジュールを使って

def unicode_test(codepoint):
    import unicodedata
    #コードポイントを対応する文字の名前に変換
    name = unicodedata.name(codepoint)
    #対応する名前に当てはまる文字を取得
    letter = unicodedata.lookup(name)
    result = """
        Unicode文字: {}
        対応する文字の名前: {}
             """.format(letter, name)
    print(result)

kanitamago5503.hatenablog.com

とすると変換できる。

試してみる

>>> import re
>>> pattern = '&#x([0-9A-F]{4});'
>>> res = re.findall(pattern, '&#x30E1;&#x30FC;&#x30AC;&#x30F3;&#x5983;')  # 数値文字参照からCode Pointの16進数文字列部分だけ抜き出す
>>> print(res)
['30E1', '30FC', '30AC', '30F3', '5983']

>>> m = map(lambda x: unicode_test(chr(int(x, 16))), res) #16進数文字列から16進数数値型に変換した上で、(Unicode)文字列に変換して上記の関数に渡してみる
>>> print(list(m))
        Unicode文字: メ
        対応する文字の名前: KATAKANA LETTER ME

        Unicode文字: ー
        対応する文字の名前: KATAKANA-HIRAGANA PROLONGED SOUND MARK

        Unicode文字: ガ
        対応する文字の名前: KATAKANA LETTER GA

        Unicode文字: ン
        対応する文字の名前: KATAKANA LETTER N

        Unicode文字: 妃
        対応する文字の名前: CJK UNIFIED IDEOGRAPH-5983

できた。

Pythonhtml.unescape() を使うと 数値文字参照からコードポイント16進数文字列への変換コードポイント16進数文字列から文字に変換 を一気にやってくれるから便利。

Presto/HiveにはUTF-16を操作する関数が用意されてないっぽい
あるにはあったが、数値文字参照からコードポイント16進数文字列への変換コードポイント16進数文字列から文字に変換SQLでやるのは相当気合がいりそう(できるかどうかわからない)

prestodb.io

Raspberry Piでmdadmを使ったraid1ディスクでTimeMachineサーバ構築のメモ

Raspberry Pi OSをインストールした後の作業メモ

GUIでの設定

ネットワーク設定

とりあえずwifiつなげる

その他

GUIから - sshの有効化 - CUIでのデフォルト起動に変更 - current user(piユーザー)でのデフォルトログインを無効化 - キーボード設定を日本語キーボードに変更

apt upgrade と vimインストール

pi@raspberrypi:~ $ sudo apt update
pi@raspberrypi:~ $ sudo apt upgrade
pi@raspberrypi:~ $ sudo apt install -y vim gnome-screenshot

ユーザーの追加とpiユーザのパスワード変更

FYI: https://qiita.com/R-STYLE/items/b481ba2d695ddf8bcee4

pi@raspberrypi:~ $ sudo adduser watanabe_yusaku
pi@raspberrypi:~ $ sudo gpasswd -a watanabe_yusaku sudo
pi@raspberrypi:~ $ sudo gpasswd -d pi sudo

piユーザーはデフォルトユーザとして何かしらOSのデフォルト設定に絡んでるかもしれないので削除せずにパスワード変更だけしておく

pi@raspberrypi:~ $ sudo passwd pi

固定IPの設定

pi@raspberrypi:~ $ sudo vim /etc/dhcpcd.conf
interface eth0
static ip_address=192.168.11.203/24
static routers=192.168.11.1
static domain_name_servers=8.8.8.8

interface wlan0
static ip_address=192.168.11.204/24
static routers=192.168.11.1
static domain_name_servers=8.8.8.8

再起動後にCUIでログイン

再起動してIP確認

watanabe_yusaku@raspberrypi:~ $ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.11.203  netmask 255.255.255.0  broadcast 192.168.11.255
        inet6 fe80::58d9:9e7:854:df74  prefixlen 64  scopeid 0x20<link>
        inet6 2400:4051:23c0:600:6c36:2388:c5:e8ab  prefixlen 64  scopeid 0x0<global>
        ether dc:a6:32:91:39:36  txqueuelen 1000  (Ethernet)
        RX packets 543  bytes 131236 (128.1 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 149  bytes 22984 (22.4 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wlan0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.11.204  netmask 255.255.255.0  broadcast 192.168.11.255
        inet6 fe80::fd63:d724:db0d:fbd5  prefixlen 64  scopeid 0x20<link>
        inet6 2400:4051:23c0:600:225b:bdb7:cad3:d443  prefixlen 64  scopeid 0x0<global>
        ether dc:a6:32:91:39:37  txqueuelen 1000  (Ethernet)
        RX packets 386  bytes 106463 (103.9 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 35  bytes 4952 (4.8 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

時刻合わせ

FYI: https://www.xn--tckk6a9dufrb.com/raspberry-piraspbian%E3%81%AEntp%E3%82%B5%E3%83%BC%E3%83%90%E8%A8%AD%E5%AE%9A/

watanabe_yusaku@raspberrypi:~ $ sudo vim /etc/systemd/timesyncd.conf
[Time]
NTP=ntp.jst.mfeed.ad.jp
FallbackNTP=ntp.nict.jp time.google.com

を追記してrestart

sudo systemctl restart systemd-timesyncd
sudo systemctl status systemd-timesyncd

で確認するとSyncronizedになっていたが、TimezoneがUSになっていたのでraspi-configから 4 Localisation Options -> I2 Change Timezone でTokyoに設定したら時間が正しくなった

mdadmインストールとraid1デバイスの復旧

/dev/sda1と/dev/sdb1は既にmdadmでraid1デバイスとして構築されているものを再度assembleする。 新たにmd0デバイスを構築する場合はこっちを参考に

watanabe_yusaku@raspberrypi:~ $ sudo apt install -y mdadm
watanabe_yusaku@raspberrypi:~ $ sudo mdadm --assemble /dev/md0 /dev/sda1 /dev/sdb1
mdadm: /dev/md0 has been started with 2 drives.

watanabe_yusaku@raspberrypi:~ $ cat /proc/mdstat
Personalities : [raid1]
md0 : active (auto-read-only) raid1 sda1[0] sdb1[1]
      976629440 blocks super 1.2 [2/2] [UU]
      bitmap: 0/8 pages [0KB], 65536KB chunk

unused devices: <none>


watanabe_yusaku@raspberrypi:~ $ sudo mdadm --detail /dev/md0
/dev/md0:
           Version : 1.2
     Creation Time : Tue May 19 05:30:38 2020
        Raid Level : raid1
        Array Size : 976629440 (931.39 GiB 1000.07 GB)
     Used Dev Size : 976629440 (931.39 GiB 1000.07 GB)
      Raid Devices : 2
     Total Devices : 2
       Persistence : Superblock is persistent

     Intent Bitmap : Internal

       Update Time : Fri May 22 18:45:33 2020
             State : clean
    Active Devices : 2
   Working Devices : 2
    Failed Devices : 0
     Spare Devices : 0

Consistency Policy : bitmap

              Name : raspberrypi:0  (local to host raspberrypi)
              UUID : 30e8a7d7:7fe4d381:3a337844:d762b48f
            Events : 10672

    Number   Major   Minor   RaidDevice State
       0       8        1        0      active sync   /dev/sda1
       1       8       17        1      active sync   /dev/sdb1

/dev/md0のマウント

watanabe_yusaku@raspberrypi:~ $ sudo mkdir /data
watanabe_yusaku@raspberrypi:~ $ sudo chmod 777 /data
watanabe_yusaku@raspberrypi:~ $ sudo mount -t ext4 /dev/md0 /data

TimeMachineサーバ設定

netatalkのインストール

watanabe_yusaku@raspberrypi:~ $ apt install -y netatalk
watanabe_yusaku@raspberrypi:~ $ sudo vim /etc/netatalk/afp.conf
;
; Netatalk 3.x configuration file
;

[Global]
; Global server settings
mimic model = TimeCapsule6,106
mac charset = MAC_JAPANESE
log file = /var/log/netatalk.log

; [Homes]
; basedir regex = /xxxx

; [My AFP Volume]
; path = /path/to/volume

[TimeMachine]
valid users = watanabe_yusaku
path = /data/timemachine
time machine = yes

time machine = yes を書くの忘れててTimemachine設定の バックアップ用デスク選択の一覧に表示されなくてだいぶ時間とられた

再起動

watanabe_yusaku@raspberrypi:~ $ sudo service netatalk restart

確認

MacからTimeMachineの設定からディスク選択を確認すると表示されていることを確認

Google Cloud SpannerでFORMAT_TIMESTAMP関数使うと時間がずれる

SpannerにUTCでTIMESTAMP型で保存されてるデータをJSTに戻そうとしたときにはまった。

SELECT 
  TIMESTAMP "2017-08-20 20:00:00 UTC"                                                                              AS ORIGIN_UTC,
  TIMESTAMP_ADD(TIMESTAMP "2017-08-20 20:00:00 UTC", INTERVAL 9 HOUR)                                              AS DateTime_9_Plus,
  FORMAT_TIMESTAMP('%Y-%m-%d %H:%M:%S', TIMESTAMP_ADD(TIMESTAMP "2017-08-20 20:00:00 UTC", INTERVAL 9 HOUR))       AS DateTime_WITH_TIME_9_Plus_FORMATED,
  FORMAT_TIMESTAMP('%Y-%m-%d %H:%M:%S', TIMESTAMP_ADD(TIMESTAMP "2017-08-20 20:00:00 UTC", INTERVAL (8 + 9) HOUR)) AS DateTime_8_Plus_9_FORMATED
ORIGIN_UTC DateTime_9_Plus DateTime_WITH_TIME_9_Plus_FORMATED DateTime_8_Plus_9_FORMATED
2017-08-20T20:00:00Z 2017-08-21T05:00:00Z 2017-08-20 22:00:00 (なぜかずれる) 2017-08-21 05:00:00 (8時間を補正分としていれてズレを戻す

よくわからんが、内部で太平洋標準時(たいへいようひょうじゅんじ、Pacific Standard Time: 略称PST)+8:00が影響してるとかしてないとか? www.en.advertisercommunity.com

docs.looker.com

FORMAT_TIMESTAMP関数の第三引数で"+9:00"のように指定しないとだめだった。

FORMAT_TIMESTAMP(format_string, timestamp[, time_zone])

でも、Data Studio使ったときに期間指定すると

(FORMAT_TIMESTAMP('%Y%m%d', t0.DateTime) >= '20181201' AND FORMAT_TIMESTAMP('%Y%m%d', t0.DateTime) <= '20181231')

みたいなクエリになっちゃうので、上記のように手動で8を足しこんだカラムにしないとだめだった

さくらVPSにSoftEtherサーバ構築

基本的にはラズパイに構築したときと同じ
waysaku.hatenablog.com


ただ、すっかり忘れていたのでメモ
基本的にはここの通りにセットアップすればOKだが、vpnserverとvpnclientの両方セットアップが必要
linuxconfig.org

あと、さくらのVPSはiptableが /etc/iptables/iptables.rules で明示的にセットされてしまっているのでデフォだとudp/500とudp/4500がつながらないっぽい。
/etc/iptables/iptables.rules を削除してrebootして解決

golangでbyte配列から整数型への変換メモ

メモ

package main

import (
    "fmt"
    "golang.org/x/crypto/scrypt"
    "encoding/hex"
    "encoding/binary"
    "bytes"
)


func main() {

    b := []byte{0x00, 0x00, 0x00, 0xFF}
    fmt.Println(b)
    fmt.Println(hex.EncodeToString(b))

    var i int32
    buf := bytes.NewReader(b)
    err := binary.Read(buf, binary.LittleEndian, &i)
    if err != nil {
        fmt.Println("binary.Read failded:", err)
    }

    fmt.Println(i)
}