ミルク色の記録

やったこと、やってみたこと

はじめてOSSライブラリを育てた話

公開してるOSSライブラリが自分史上初めて(微々たるものではあるけれども)誰かに使われるようになったので振り返ってみる。

対象のライブラリはこれ。

github.com

 

開発のきっかけ

Raspberry Piを使ったデバイス開発をしていると、システムに明るくないエンドユーザー向けに何かとネットワーク設定を変更できるようにしたいユースケースが出てくる。

そんなときはWebやUART経由などでRaspberry PIの外からdhcpcd.confやwpa_supplicant.confを編集出来るようにUIを作って提供するのだけれど、設定ファイルのパーサーから書いたりしなければならず、これがなかなかに面倒くさい。

一方で、Linuxのネットワーク設定というとNetwork-Managerが主流ではあるけど、Raspberry PI OSではデフォルトになっておらず、またNetwork-ManagerというとGUIによる設定の印象が強かったので、どうにかならんかなとは思っていた。

ある日、CLIのnmcliがあるバージョンから結構な範囲で色々できることに気づき、これでできそうだなーとPythonからCLIを叩いて使えるライブラリを作成した。

 

開発当初のコンセンプト

実装的にはコマンド叩いてパースする程度なので、作り自体は単純なもの。

そんな中で、とにかく型は定義したい、dataclassなども使いたいということでPythonは3.7以降対象として、可能な限りテストも書いた。

APIインターフェイスはなるべく直感的にわかるようにnmcliのコマンド・サブコマンドから連想できるようにした(そのためにPythonで__call__とか初めて使った)。

エラーもnmcliのステータスコードに準拠して例外を定義した。

また、テストのときなどに都度モック作らずに良いように最初からdummyのモッククラスを搭載した(現在はおまけ要素程度になっている)。

 

公開当初とPyPIへの登録

最初はPyPIにも登録せず、ビルドしたwheelファイルをGithubのReleaseに添付する形で公開した。そのためライブラリ名を決める際に特に悩むことなくnmcliそのままにした(そのせいでCLIの方のnmcliへの質問がIssueで来たりしてちょっと困ったが)。

公開してしばらくしてからフォークして使ってくれている人が出てきて、自分はサポートしていなかったnmcliのコマンドに対応しようとしている事に気づいた。それならサポートの幅を広げてみるかと、一度nmcliの搭載しているコマンドをざっと洗い出して、コンパチビリティテーブルを作って対応した。また、このタイミングでPyPIに登録することにした。

 

コントリビューターがついた

PyPIに登録してから不具合修正や機能追加のPRをもらうようになり、DeepLで翻訳しながらコントリビューターとやり取りするようになった。未対応だったサブコマンドに対しても熱い要望をもらって対応した。

英語そこまで得意な方ではないので翻訳しながらではあったけど、自分のライブラリを好んで使ってくれているメッセージが入ると素直に嬉しかった。

2021年にはバージョンも0.3.1から0.9.0まで上がり、途中文字コード的な問題への対応などもあったけどそれなりに順調に来ている。

 

今後の課題

もらえるPRはテスト書いてくれていない人もいるので、強制まではするつもりはないけどGithub Actionとかでテスト壊したのが気付けるようにはしようかなーと考えている。

コマンドやサブコマンドのオプションパラメータはサポートしていたりしていなかったりするので、必要出てきたら足していかないとなと思っている。

 

所感

Webのフロントエンドに興味が全振りしているはずの自分が、初めて人に使ってもらえるところまで行ったのがデバイス用のライブラリか…と思わないこともないけど、微々たるものでも誰かの役に立てているというのが素直に嬉しい。

世界を大きく変えるようなものはできないけど、ちょっとしたお役立ちを目指して今後もコードを書いていきたい。

 

シリアル接続同士をプロキシするツールを作った

とある通信モジュールをM5Stack(UART 2)で使う必要があったのだけど、通信モジュールの評価ボードがUSBでシリアル接続するものなので直接繋げないという状態になった。

幸い、USBシリアル変換ケーブルのTTL-232R-3V3が手元にあったので、通信モジュール -> USBケーブル -> (なにか) -> TTL-232R-3V3 -> M5Stack とすれば繋げられるなと思ったので、次のPythonスクリプトを書いてしのいだ(socatでできそうな気がしたんだけど、うまくできなくて投げ出した)。

import serial
from threading import Thread
import time

com1 = serial.Serial("/dev/ttyUSB0", 115200, timeout=1)
com2 = serial.Serial("/dev/ttyUSB1", 115200, timeout=1)

def pipe1():
    while True:
        b = com1.read(com1.in_waiting or 1)
        if b:
            com2.write(b)

def pipe2():
    while True:
        b = com2.read(com2.in_waiting or 1)
        if b:
            com1.write(b)

th1 = Thread(target=pipe1, daemon=True)
th2 = Thread(target=pipe2, daemon=True)
th1.start()
th2.start()

while True:
    time.sleep(1)

そこそこ便利だったので、書き直してデバッグ用のコンソール表示機能つけた。

github.com

Raspbianのネットワーク管理をNetwork-Managerにした

Raspbianのネットワーク設定周り、大昔は/etc/network/interfacesだったのが、 いつの頃からか/etc/dhcpcd.confで設定するようになって、固定IPにするときとか /etc/dhcpcd.confを編集している。

加えて、Wifiを設定するのであれば/etc/wpa_supplicant/wpa_supplicant.confを編集し、 USBドングルを使ってモデムで繋ぐならばwvdialやpppconfigを...と言った感じで、 とにかく設定が散らばっていてめんどくさい。

以前から普通のデスクトップLinuxみたいにNetwork-Managerでやってしまったほうが楽なのでは... と思っていたけど、バージョン的にnmcli(Network-ManagerのCUIクライアント)でできることが少なくて見送っていた。

去年からRaspbianのBusterを使うようになり、 「そう言えばnmcliのバージョンってどうなったんだろ?」と思って試しに入れてみたら、 結構バージョン上がっていて色々とできるようになっていた。 この際なのでネットワークまわりをNetwork-Manager使うようにしてみた。

環境設定

使ったraspbian OSのバージョンはRaspbian Buster Liteの2020-02-13バージョン。

aptでnetwork-managerをインストールして、dhcpcdは止めて再起動する。

$ sudo apt-get update
$ sudo apt-get install -y network-manager
$ sudo systemctl disable dhcpcd.service
$ sudo reboot

これで準備は完了。

Wifiや有線の設定

まずはnmcli connectionで接続設定を一覧取得する。

$ sudo nmcli connection
NAME                UUID                                  TYPE      DEVICE
Wired connection 1  00000000-0000-0000-0000-000000000000  ethernet  eth0

それからnmcli deviceでネットワークインターフェイスの状態を取得する。

$ sudo nmcli device
DEVICE  TYPE      STATE         CONNECTION
eth0    ethernet  connected     Wired connection 1
wlan0   wifi      disconnected  --
lo      loopback  unmanaged     --

とりあえずWifi繋ぐためにnmcli device wifiWifiネットワークを探す(実際の値は????????????ではない)。

$ sudo nmcli device wifi
IN-USE  SSID             MODE   CHAN  RATE        SIGNAL  BARS  SECURITY
        ????????????  Infra  1     130 Mbit/s  89      ▂▄▆█  WPA1 WPA2
        ????????????  Infra  11    195 Mbit/s  70      ▂▄▆_  WPA1 WPA2
        ????????????  Infra  36    270 Mbit/s  57      ▂▄▆_  WPA1 WPA2
        ????????????  Infra  5     260 Mbit/s  54      ▂▄__  WPA2
        ????????????  Infra  11    195 Mbit/s  49      ▂▄__  WPA2
        ????????????  Infra  11    195 Mbit/s  49      ▂▄__  WPA2
        ????????????  Infra  108   405 Mbit/s  44      ▂▄__  WPA1 WPA2
        ????????????  Infra  100   540 Mbit/s  40      ▂▄__  WPA2
        ????????????  Infra  6     54 Mbit/s   39      ▂▄__  WEP
        ????????????  Infra  6     270 Mbit/s  37      ▂▄__  WPA2
        ????????????  Infra  6     135 Mbit/s  25      ▂___  WPA2
        ????????????  Infra  116   405 Mbit/s  25      ▂___  WPA2
        ????????????  Infra  3     130 Mbit/s  22      ▂___  WPA1 WPA2
        ????????????  Infra  52    405 Mbit/s  22      ▂___  WPA2

nmcli device wifi connectWifiを設定する。

$ nmcli device wifi connect <SSID> password <password>
Device 'wlan0' successfully activated with '00000000-0000-0000-0000-000000000000'.

これでWifiは繋がる。

次にnmcli connection addで有線接続を固定IPにする。

$ sudo nmcli connection add con-name Home \
type ethernet \
ifname eth0 \
ipv4.addresses 192.168.1.99/24 \
ipv4.gateway 192.168.1.1 \
ipv4.dns 8.8.8.8 ipv4.method manual
Connection 'Home' (00000000-0000-0000-0000-000000000000) successfully added.

作った接続を使うためにnmcli connection downでデフォルトの有線接続を止める(sshでログイン中の場合、切断されるので注意)。

$ sudo nmcli connection down Wired\ connection\ 1

改めて接続設定を確認する。

$ sudo nmcli connection 
NAME                UUID                                  TYPE      DEVICE 
??????????????      00000000-0000-0000-0000-000000000000  wifi      wlan0  
Home                00000000-0000-0000-0000-000000000000  ethernet  eth0   
Wired connection 1  00000000-0000-0000-0000-000000000000  ethernet  --

作った接続を使うので、nmcli connection deleteでデフォルトの有線接続は削除する。

$ sudo nmcli connection delete Wired\ connection\ 1 
Connection 'Wired connection 1' (00000000-0000-0000-0000-000000000000) successfully deleted.

コマンドで操作した設定ファイルは/etc/NetworkManager/system-connections配下に保存される。 設定ファイルを手で触ってもいいけど、接続の追加、編集、削除が全部コマンドでできて、かなり楽。

USBドングルでアクセスポイントに繋ぐ

wvdialをやめるために、USBドングルの設定も試す。手持ちのL-02Cを使った。 そういえばいつの間にかRaspbianにusb_modeswitchって標準搭載されてる気がする。 L-02Cはudevのルール書かなくても自動でスイッチされてモデムとして認識される。

USBポートに挿してネットワークインターフェイスの状態を見る。

$ sudo nmcli device 
DEVICE   TYPE      STATE         CONNECTION     
eth0     ethernet  connected     Home           
wlan0    wifi      connected     ???????????? 
ttyUSB2  gsm       disconnected  --             
lo       loopback  unmanaged     --

iijmioのアクセスポイントで設定してみる。

$ sudo nmcli connection add autoconnect true \
con-name iijmio \
type gsm \
apn iijmio.jp \
user mio@iij \
password iij \
gsm.number *99***1# \
ppp.refuse-eap true \
ppp.refuse-mschap true \
ppp.refuse-mschapv2 true \
ifname "*"
Connection 'iijmio' (00000000-0000-0000-0000-000000000000) successfully added.

設定完了してPPPが繋がると接続状態は次のようになる。

$ sudo nmcli connection 
NAME            UUID                                  TYPE      DEVICE  
Buffalo-G-5209  00000000-0000-0000-0000-000000000000  wifi      wlan0   
Home            00000000-0000-0000-0000-000000000000  ethernet  eth0    
iijmio          00000000-0000-0000-0000-000000000000  gsm       ttyUSB2

PPP接続は時々ちゃんと繋がらないことがあり、syslogみると次みたいなエラーになっている。

Feb 29 17:13:20 raspberrypi NetworkManager[343]: <warn>  [1582964000.7050] modem-broadband[ttyUSB2]: failed to connect modem: 34

こういう場合はModemManagerを再起動して、それからコネクションを開始する。

$ sudo systemctl restart ModemManager.service
$ sudo nmcli connection up iijmio 
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/10)

この辺りのワークアラウンドはwvdial使っていても遭遇したことがあって、プロセス監視してだめだなと検知したら落としてもう一度みたいにやってたのに近いかも。

とりあえず設定を一本化できるのでやはり楽。

Wifiアクセスポイント化

Raspberry PI自身をWifiアクセスポイント化して接続させるやつ。 前回はhostapdでやったけど、これもNetwork-Managerだけでできる。

wifiタイプの接続を作るときにモードをapにすればいい。

$ sudo nmcli connection add con-name RPi-AP \
type wifi \
ifname wlan0 \
wifi.ssid RPi-AP \
wifi.mode ap \
wifi-sec.psk raspberry \
ipv4.method manual \
ipv4.addresses 192.168.100.1/24 \
ipv4.gateway 192.168.100.1 \
wifi-sec.key-mgmt wpa-psk
Connection 'RPi-AP' (00000000-0000-0000-0000-000000000000) successfully added.

ログの調整

デフォルトだとsyslogにinfoレベルでログが出過ぎるので、/etc/NetworkManager/NetworkManager.confにログレベルを設定して調整する。

[main]
plugins=ifupdown,keyfile

[ifupdown]
managed=false

[logging]
level=WARN

感想

設定を一本化できて楽。しばらく動かしてみたが、dhcpcdをやめて駄目になった部分もみられない。 もう標準でNetwork-Managerでいいんじゃないかな...という印象。

ついでにプログラムから設定変更できるようにしたいのでpython用のライブラリ作った。

github.com

とりあえず必要最低限程度に実装。この前やったWifi設定のをもっといい感じにしてみたい。

Raspberry PIをWifiアクセスポイントにしてWifiネットワーク設定をできるようにした

タイトルが変なので何言ってるんだコイツ状態になるけど。

時々見かける、最初はデバイス自身がWifiのアクセスポイントになって、PCなどから接続して実際に使うネットワーク設定を行う...というやつをRaspberry PIでやってみるという話。

作ったものはこれ。

github.com

環境準備

まずはhostpadをインストールする。

$ sudo apt-get update
$ sudo apt-get install -y hostapd

hostapd.serviceはmaskされているのでインストール時点では起動に失敗する。

設定ファイルをexampleからコピーしてきて/etc/hostapd/hostapd.confを編集する。

$ sudo cp /usr/share/doc/hostapd/examples/hostapd.conf /etc/hostapd/hostapd.conf

とりあえず、デフォルトから変更した項目は次の通り。

ssid=RasPi-AP
ieee80211n=1
ht_capab=[HT40][SHORT-GI-20][DSSS_CCK-40]
auth_algs=1
wpa=2
wpa_passphrase=raspberry
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP

続いて、dhcpサーバをインストールする。

$  sudo apt-get install -y isc-dhcp-server

こちらもインストール時点では起動に失敗する。

/etc/dhcp/dhcpd.confを編集して末尾に次の設定を追加する。

subnet 192.168.100.0 netmask 255.255.255.0 {
  range 192.168.100.100 192.168.100.105;
  option routers 192.168.100.1;
  option broadcast-address 192.168.100.255;
  default-lease-time 600;
  max-lease-time 7200;
}

/etc/default/isc-dhcp-serverを編集してwlan0インターフェイスとして有効にする。

INTERFACESv4="wlan0"

isc-dhcp-serverは自動起動せずに使うので自動起動を無効化しておく。

$ sudo update-rc.d isc-dhcp-server disable

とりあえず動作確認のために簡易起動スクリプトをざっと書いて実行してみる。

#!/bin/sh

sudo hostapd /etc/hostapd/hostapd.conf &
sudo ip addr flush dev wlan0
sudo ip addr replace 192.168.100.1/24 dev wlan0
sudo /etc/init.d/isc-dhcp-server start

スクリプトを実行したら、wlan0192.168.100.1が割り当てられるので、PCなどからアクセスポイントに接続してIPアドレスを貰えればOK。

ブラウザからWifi設定するためのWebサーバを用意

アクセスポイントに接続してPC側がクライアントになることはできるようになったので、アクセスポイントのIPアドレスにブラウザでアクセスしてWifi設定を行えるようにWebサーバを作る。

とりあえず前に作ったtornado使ったソースを流用してざっくり作成。

import os
import subprocess
import tornado.ioloop
import tornado.web

class SystemControl(object):

    def read_wpa_supplicant_conf(self, config_path):
        with open(config_path) as f:
            return f.read()

    def write_wpa_supplicant_conf(self, config_path, data):
        subprocess.check_call('sudo sh -c "echo -n \'%s\' > %s"' % (data, config_path) , shell=True)

    def auto_start_off(self):
        subprocess.run('sudo sed -i --follow-symlinks s/AUTO_START=ON/AUTO_START=OFF/g /etc/default/pi-wifi-admin', shell=True)

    def reboot(self):
        subprocess.run('sudo reboot', shell=True)


class MainHandler(tornado.web.RequestHandler):

    def initialize(self, sys_ctrl, wpa_supplicant_conf_path):
        self._sys_ctrl = sys_ctrl
        self._wpa_supplicant_conf_path = wpa_supplicant_conf_path

    def get(self):
        html = """<html>
  <head><title>Wifi Setting</title></head>
  <body>
    <form method='POST' action='/'>
      <textarea name='config' style='width:800px;height:600px;'>%s</textarea>
      <input type='submit' value='Save' />
    </form>
  </body>
</html>
"""
        conf = self._sys_ctrl.read_wpa_supplicant_conf(self._wpa_supplicant_conf_path)
        self.write(html % conf)
        self.set_status(200)

    def post(self):
        conf = self.get_body_argument('config')
        self._sys_ctrl.write_wpa_supplicant_conf(self._wpa_supplicant_conf_path, conf.replace('\r', '').replace('"', '\\"'))
        self._sys_ctrl.auto_start_off()
        self._sys_ctrl.reboot()
        self.redirect('/', permanent=True)

def make_app(sys_ctrl, wpa_supplicant_conf_path):
    return tornado.web.Application([
        (r'/', MainHandler, {
            'sys_ctrl': sys_ctrl,
            'wpa_supplicant_conf_path': wpa_supplicant_conf_path
            })
    ])

if __name__ == '__main__':
    try:
        port = int(os.getenv('PORT', '8080'))
        sys_ctrl = SystemControl()
        app = make_app(sys_ctrl, '/etc/wpa_supplicant/wpa_supplicant.conf')
        app.listen(port, '0.0.0.0')
        tornado.ioloop.IOLoop.instance().start()
    except KeyboardInterrupt:
        pass

やってることは/にアクセスがあったらwpa_supplicant.confの中身を書きだしたtextareaのあるフォームを表示する。フォームのPOSTでwpasupplicant.confを上書きして設定アプリケーションの自動起動停止、OSを再起動するだけ。ちゃんとAPI作る気力が今回はなかったので超ざっくりに。

これを/usr/local/lib/pi-wifi-adminディレクトリを作って設置し、venvでtornadoを入れて使えるようにしておく。

アクセスポイント化とWebサーバの起動をまとめてサービス化

環境準備で用意したhostapdやisc-dhcp-serverの起動と設定用Webサーバを起動するスクリプトを次の通り用意して/usr/local/sbin/pi-wifi-adminとして設置する。

毎回起動されても困るので、環境変数AUTO_STARTを見て完全に起動を行うかを判定している。

#!/bin/bash

AUTO_START=${AUTO_START:-OFF}
WIFI_IFNAME=${WIFI_IFNAME:-wlan0}
IP_ADDR=${IP_ADDR:-192.168.100.1/24}

APP_HOME_DIR=/usr/local/lib/pi-wifi-admin

if [ "$AUTO_START" == "OFF" ]; then
    echo "auto start disable"
    exit 0
fi

systemctl stop wpa_supplicant
systemctl stop dhcpcd
hostapd -B -P /tmp/hostapd.pid  /etc/hostapd/hostapd.conf
ip addr flush dev $WIFI_IFNAME
ip addr replace $IP_ADDR dev $WIFI_IFNAME
systemctl start isc-dhcp-server
$APP_HOME_DIR/venv/bin/python $APP_HOME_DIR/app.py &

使用する環境変数/etc/default/pi-wifi-adminに設定として置いておく。

AUTO_START=ON
WIFI_IFNAME=wlan0
IP_ADDR=192.168.100.1/24

これらをsystemd用に次の/etc/systemd/wifi-pi-admin.serviceとして設定を用意してサービス化する。

[Unit]
Description = PI Wifi Admin
After=network.target

[Service]
ExecStart = /usr/local/sbin/pi-wifi-admin
EnvironmentFile = /etc/default/pi-wifi-admin
Type = oneshot
RemainAfterExit = yes

[Install]
WantedBy = multi-user.target

あとはサービスを有効化して終わり。

$ sudo systemctl enable pi-wifi-admin.service

使い方

PCでRasPI-APWifi接続して、ブラウザでhttp://192.168.100.1:8080を開く。

f:id:ushiboy:20200125114153p:plain

wpa_supplicant.confの設定フォーマットで設定してSaveを押す。

レスポンスは返る前にOSが再起動してしまうのでタイムアウトすると思う。

課題

スマホWifi接続したら、外部に通信送れないせいか「このWifi駄目だ」と判断されて自動で切り替わってしまった。PCでももしかしたらそういうことあるかも。

ファイル直接触るのやめてちゃんとしたAPIで設定できるようにしたい。

設定保存した後にOSを再起動せずに変更を反映できるようにしたい。

Webプログラマが業務でIoTデバイス開発をして思ったこと

自分は本来Webプログラマのつもりなのだけれど、会社のやんごとなき事情により、今年はRaspberry PIやESP32を使ったいわゆるIoTデバイスアプリの開発をすることが多かった(あと、Kotlin使ってAndroidモバイルアプリ開発もやった)。

もともと、時々調査手伝ったり、プロトタイプ的なデバイスアプリ作ったりはしていたので、全く未経験ってわけではなかったけれど、自分一人で全部やって納品まで持っていったのは初めてだった。

少ないハードウェアリソースの中で専門的にやっている人たちに比べれば、自分のしたことはママゴトみたいなものなのだろうけど、開発を通して知ったこと・気づいたことがあったので記録として残しておく。

知ったこと・気づいたこと

ボード上で直接コードを変更しない

Raspberry PIやBeagle BoneでPython使ってアプリ作る時の話。ボードのOSがLinuxなので書こうと思えばSSHで入って直接書くことはできる。特にGPIOとかSPIとかI2Cみたいなボードの入出力使う必要がある場合、PCだとできない動作確認をする都合もあって、ついついソースコードを直接編集したくなる。

でも、ボードの方で編集しているとPCの方に持ってくるのを忘れてたり、うっかりボードのOS固有のファイルパスとかをハードコーディングしてしまって移植性が落ちるみたいなことになるので、「PCでコード書いて、ボードに転送して動作確認」を徹底したほうが良い。

自分はlsyncdを非デーモンモードで動かしてファイル変更したら自動転送して作っていた。

PythonPyPIモジュールはAPTで入れない

GPIO触るやつとか、OpenCVまわりとか、APTで直接OSに入れられるものもあるけど、requirements.txtに載っていない依存になったりして後々混乱するはめになるので、pipでインストールするほうが良い。最近はpiwheelsでも配布されていたりして、意外と大丈夫だったりする。

requirements.txtに載ってないけど、コード上はimportしているみたいなのはOSへのインストール忘れてたりして危なっかしくなる。

バイスOSの環境セットアップはスクリプトにしておく

Python3のvenvであったりPyPIモジュールが依存するOSの共有ライブラリなどのインストール、ちょっとした環境設定などはシェルスクリプトでセットアップスクリプトにしておくと良い。

ただし、どんなケースでも動くセットアップスクリプトを目指すと大変になるので、実行できる設定ドキュメントぐらいのノリで書いておくと良い。

shやbashで書くのが辛くなったら、いっそのことpython3で書いてしまえばいい。標準ライブラリの範囲内であればOSのプリインストール範囲内で結構できる。

国際化設定はちゃんと設定しておく

言語設定、タイムゾーン設定、キーボードレイアウト設定はデフォルトではen_GB環境になっているので、ちゃんと設定変えておいたほうが良い(en_GBな環境が対象ならばそのままでいいけど)。Wifi使うならWifiの地域設定も同様。後からキーボード繋いでちょっと調査...みたいなときに日本語キーボード繋いだらレイアウトあってなくて大混乱とかマジ大変。

いちいちraspi-configで設定するのが面倒ならば一通り設定するスクリプトを作っておけばいい。

raspi-configのコード見ればどのファイル編集すればいいか・コマンド叩けばいいかは判断できる。

OS(Raspbian)のリリース情報はこまめにチェックする

Raspbian、結構な頻度でバージョンアップするので公式のリリースはチェックして、チェンジログとか見ておいたほうが良い。rootじゃなくても実行できる権限増えてたり、ハードウェア乱数生成ツールがプリインストール対象にされていたりするし、OverlayFS利用したファイルシステムの読み込み専用化みたいな「コレもう標準でできるんじゃん」が結構出てきたりする。

ボード自体のアップデート(Raspberry PI 4のリリースとか)があると、旧バージョンのディストリだともう動かないとか出てくるので、最新リリースのディストリ(現在はBuster)と一つ前の(現在はStretch)は抑えて置くくらいの感覚でいたほうが良い。

ただし、ディストリの新バージョンが出た直後だとまだちょっと安定してない...?みたいな動作をする時があるので、先は見すえつつ採用は慎重にみたいな感じ。

ネットで見つかる環境設定方法などは鮮度に注意する

バイス開発に限った話じゃないけれど。

たとえばRTCの設定方法とか、「それもうdtoverlayでできるよ」みたいなのが見つかったり、自分でコマンド実装して叩いているやつがサービスとしてすでに入っていたりするので、ネットで見つけた情報は参考にしつつも公式の情報にないか調べてみると良い。

雑な見分け方として/etc/rc.localに実行コマンド足して何かしている系は、より良いやり方見つけられることが多い。

udevやsystemdなど活用してOSでできることはOSでやる

USBカメラをつないだら/dev/cameraシンボリックリンクができるようにudevルールを設定しておいて、/dev/video[0-9]+を直接探さなくても良いようにするとか、アプリケーションが動く前に確実にNTP同期が取れているようにsystemdの依存関係をうまく組み合わせて動かすとか。

アプリケーションに何でも仕事を持たせすぎずにOSでできることはOSに任せたほうが良い。

時計はマジで重要

HTTPSの接続とかWifiの証明書つかった認証とか、システムクロックがあっていないと途端にうまくいかなくなることがたくさんあるので、時刻があっていることを重視したほうが良い。

NTP頼みならば同期が確実に取れるまで待機させるとか、可能であればRTCの搭載を積極的に行うとか。とにかく時計は大事。

工夫すればテストは意外と書ける

以前はデバイスアプリの開発はどうしてもハードウェアへの依存が多くなるので、単体テストとか書きにくそうだなーと思っていた。 ただ、Webアプリケーションを使う場合でも、テストしにくいもの(データベースとか、ネットワーク越しの外部APIとか)を使うことになるモジュールをテストしやすくする方法はあるので、結局同じことかなーと思って工夫してみた。

UARTを使うpyserialや、SPI用のspidev、I2C用のsmbus-cffiとか、ビジネスロジックの中でなるべく直接使わずに、薄くラップしてインターフェイスを意識して利用することで分離し、ビジネスロジックをハードウェアの異存なしにテストできるようにした。

簡単な例で書くと説明しにくいのだけれど、例えば次のようにAppクラスのコンストラクタでシリアルコネクションを生成するとテストし難いコードになる。

import serial

class App(object):

    def __init__(self, serial_port):
        self._serial_conn = serial.Serial(serial_port, 115200, timeout=1)
        # その他初期化処理...

    def run(self):
        b = self._serial_conn.read(self._serial_conn.in_waiting or 1)
        if b:
            # 読み取りバッファを使った何らかの処理...

そこで、コンストラクタではシリアルコネクションを受け取って使うようにして、依存を外に追い出す。これだけでシリアルコネクションをモックに差し替えてテスト可能になる(実際はpyserialそのまま使わずに、もっと抽象化したものを使う)。

import serial

class App(object):

    def __init__(self, serial_conn):
        self._serial_conn = serial_conn
        # その他の初期化処理...

                                                                                                                                                                    
    def run(self):
        b = self._serial_conn.read(self._serial_conn.in_waiting or 1)
        if b:
            # 読み取りバッファを使った何らかの処理...

この辺りはRobert C.Martin先生のClean CodeとかClean Architectureとか読んだことが活かせた気がする(そういえばハードウェアを抽象化した話もあったような気がする)。

Pythonの場合だけではなくて、ESP32用にC++で書いていた時もAuniterでテストするにあたって同じようにやれた。

SDカードからイメージを作るときはサイズに注意

同じ8GBや16GBのSDカードだからといって、実際の細かいサイズまで同じとは限らない(メーカーの違いとかで変わる)。 8GBのSDカードに焼いたOSをddコマンドで取り出して、別のSDカードに焼こうとして容量不足で書けないとかザラにある。

SDカードの容量いっぱいまで使わないサイズにパーティションを意識して作成して、ddで取り出す時もcountオプションでパーティションの終わりまで切って取り出したほうが良い。

モバイルバッテリーで動かすときは消費電力に注意が必要

電源取れない場所で使うデバイスでモバイルバッテリー使うことになってわかったけど、一般のモバイルバッテリーにはオートパワーオフ機能がついていて、一定以上の電力消費が無いと供給をやめてしまう機能が付いている。

で、困ったことにこの基準となる消費電力のスペックがあまり公開されていない(Ankerの場合、日本法人のWebサイトにはなく、本家のWebサイトに載っていた)ので、満たせるかどうか綱渡り感がある。

とくにESP32は消費電力を抑えようとしてディープスリープ入れたりすると供給止められちゃうので要注意。

ESP32のディープスリープ時の時計は当てにならない

ESP32のディープスリープで起床時間を指定して眠りに入る機能があるけど、ディープスリープ時の時計が結構ずれやすく、長く寝させれば寝させるほど予定の時間に起きなかったりする。

しょうが無いのでRTCのDS3231を積んで、DS3231に付いているアラーム機能でWake upの信号を入れるようにして正しく起床させた。やはり時計は重要。

Arduinoの開発するならVSCodeがおすすめ

ESP32の開発はArduino IDEでできるんだけど、VS CodeArduino用拡張当てて使うほうが断然開発効率が良い。 シリアルでの入力だけはArduino IDEのほうがよい。

Arch Wikiは役にたつ

何かと行き着くArch Wiki。困った時に読むと情報載ってたりして役に立つ。 Raspberry PI用のページもあって非常に良い。

まとめ

他にもあった気がするけど、ざっとこんな感じ。

本業の方のWebプログラミングはさっぱり進歩がなくて、会社の他のメンバーのほうがやってたんじゃないかなーくらいな状態で、正直なところ焦ってる。将来を考えた上で非常にまずい感じなので、今後は「デバイス開発も素人程度にはできるWebプログラマ」になれるように努めていきたい。

raspi-configのAdvanced OptionsでOverlay FSを使ってみた

きっかけ

先日、メモリの設定を変えようとraspi-configを起動して、Advanced Optionsを開いたらメニューにOverlay FSが増えていることに気がついた。

f:id:ushiboy:20191215130640p:plain
Advanced Options

説明にEnable/Disable read-only file systemとあるので、これって/パーティションを読み込み専用にするやつじゃね?と思い、素のRaspbian OSイメージをSDカードに焼いて試してみた。

試したイメージはRaspbian Buster Lite 2019-09-26でAPTでパッケージを最新に上げたもの。

Overlay FS の有効化

Overlay FSを選択すると、有効にするか聞かれるので、Yesを選択する。

f:id:ushiboy:20191215131141p:plain

すると、initramfsの生成が行われ、完了メッセージが表示される。

f:id:ushiboy:20191215131318p:plain

次に/bootパーティションを書き込み保護するか聞かれる。Yesを選択した場合は/bootパーティションが読み込み専用になり、Noにした場合/bootパーティションはそのままとなる。

f:id:ushiboy:20191215131426p:plain

とりあえずNoを選択して実行すると、/bootの扱いについて表示される。

f:id:ushiboy:20191215131657p:plain

このあとraspi-configを終了すると再起動を促されるので、OSを再起動してログインし直す。

ログイン後、mount状態を見ると次のようになる。

$ mount
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,relatime)
udev on /dev type devtmpfs (rw,nosuid,relatime,size=465128k,nr_inodes=116282,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
tmpfs on /run type tmpfs (rw,nosuid,noexec,relatime,size=94832k,mode=755)
overlay on / type overlay (rw,noatime,lowerdir=/lower,upperdir=/upper/data,workdir=/upper/work)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=29,pgrp=1,timeout=0,minproto=5,maxproto=5,direct)
mqueue on /dev/mqueue type mqueue (rw,relatime)
sunrpc on /run/rpc_pipefs type rpc_pipefs (rw,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
configfs on /sys/kernel/config type configfs (rw,relatime)
/dev/mmcblk0p1 on /boot type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=94828k,mode=700,uid=1000,gid=1000)

overlay on / type overlay (rw,noatime,lowerdir=/lower,upperdir=/upper/data,workdir=/upper/work)とあり、/パーティションがoverlayfsで保護されているのがわかる。

この時、/boot/cmdline.txtは次のようになっている。

boot=overlay console=serial0,115200 console=tty1 root=PARTUUID=6c586e13-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait

先頭にboot=overlayが追加されている。また、/boot/config.txtの末尾には次のようにinitramfs initrd.img-4.19.75-v7+-overlayが追加されている。

[all]
#dtoverlay=vc4-fkms-v3d
initramfs initrd.img-4.19.75-v7+-overlay

Overlay FS の無効化

raspi-configのAdvanced OptionsOverlay FSを選択し、disableにすればよい。再起動後にマウントの状態は次のように戻る。

$ mount
/dev/mmcblk0p2 on / type ext4 (rw,noatime)
devtmpfs on /dev type devtmpfs (rw,relatime,size=469544k,nr_inodes=117386,mode=755)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,relatime)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
tmpfs on /run type tmpfs (rw,nosuid,nodev,mode=755)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=33,pgrp=1,timeout=0,minproto=5,maxproto=5,direct)
sunrpc on /run/rpc_pipefs type rpc_pipefs (rw,relatime)
mqueue on /dev/mqueue type mqueue (rw,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
configfs on /sys/kernel/config type configfs (rw,relatime)
/dev/mmcblk0p1 on /boot type vfat (rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=94828k,mode=700,uid=1000,gid=1000)

/bootパーティションの書き込み保護

Overlay FSを有効化するときに/bootパーティションの書き込み保護も有効にすると、再起動後にマウント状態は次のようになる。

$ mount
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,relatime)
udev on /dev type devtmpfs (rw,nosuid,relatime,size=465128k,nr_inodes=116282,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
tmpfs on /run type tmpfs (rw,nosuid,noexec,relatime,size=94832k,mode=755)
overlay on / type overlay (rw,noatime,lowerdir=/lower,upperdir=/upper/data,workdir=/upper/work)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
sunrpc on /run/rpc_pipefs type rpc_pipefs (rw,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=35,pgrp=1,timeout=0,minproto=5,maxproto=5,direct)
mqueue on /dev/mqueue type mqueue (rw,relatime)
configfs on /sys/kernel/config type configfs (rw,relatime)
/dev/mmcblk0p1 on /boot type vfat (ro,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=94828k,mode=700,uid=1000,gid=1000)

/dev/mmcblk0p1 on /boot type vfat (ro,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,errors=remount-ro)となっており、/bootパーティションが読み込み専用(ro)になっていることがわかる。

これは/etc/fstabの設定によって行っているようで、/etc/fstabを見ると次のようになっている。

$ cat /etc/fstab 
proc            /proc           proc    defaults          0       0
PARTUUID=6c586e13-01  /boot           vfat    defaults,ro          0       2
PARTUUID=6c586e13-02  /               ext4    defaults,noatime  0       1
# a swapfile is not a swap partition, no line here
#   use  dphys-swapfile swap[on|off]  for that

PARTUUID=6c586e13-01 /boot vfat defaults,ro 0 2とあり、roが設定されていることがわかる。

ちなみに/bootの書き込み保護も有効にすると、Overlay FSの解除はraspi-configの1度の実行だけでは駄目で、2度行う必要がある。1度目の解除実行時に下のようなメッセージがでる。

f:id:ushiboy:20191215132927p:plain

想像だけど、一度目の解除実行では/bootのマウントを書き込み可で再マウントして、/boot/cmdline.txt/boot/config.txtを修正してOverlay FSを無効化するが、/etc/fstabOverlay FSの解除後でないと変更できないからとかじゃないかと思う。

気になること

initramfsのファイル名をinitrd.img-4.19.75-v7+-overlayという感じで-overlay付きで作っているけど、カーネルバージョンアップデート時の自動的なinitramfsの作り直しに対応できているのかな?というのはちょっと気になったりもする。

まとめ

Raspberry PIでなにも対策せずに動かしているとSDカード壊れるのは昔からの課題だったので、公式にOverlay FSで読み込み専用にできるようになったのは素直に嬉しい。

/は読み込み専用にしつつ特定のディレクトリは書き込み可にしておきたいということで、パーティションを分割する需要は今後も増えそう。

Raspberry PIでRTC(DS3231)を使った

Raspberry PIでなんやかんややっていると、HTTPSWifiの証明書あり認証とかで時計の重要性に気がつくことが多い。

で、NTPで確実に同期取れるの待つsystemd用サービスとか作って対処したりしていたけど、やっぱリアルタイムクロックあったほうが良いよなーと思ったので使い方を覚えてみることにした。

使ったRTCモジュール

Amazonでも売ってたこれを使った。モジュールはDS3231。I2Cで接続するもの。

試したOS

Raspbian Buster Liteの2019-09-26バージョンでパッケージを最新まで上げたやつ。

設定方法

まずraspi-configでI2Cを有効にする。

続いて、fake-hwclockを無効化する。

$ sudo systemctl disable fake-hwclock
$ sudo apt-get -y remove fake-hwclock

RTCへの時刻設定とRTCからシステムクロックへの反映については、ざっと調べた感じudevの/lib/udev/rules.d/85-hwclock.rulesが次のようになっていて、rtcデバイスを見つけたら/lib/udev/hwclock-setを実行してくれるみたい。

# Set the System Time from the Hardware Clock and set the kernel's timezone
# value to the local timezone when the kernel clock module is loaded.

KERNEL=="rtc0", RUN+="/lib/udev/hwclock-set $root/$name"

ただし、/lib/udev/hwclock-setはデフォルトだと/run/systemd/systemを見つけて処理せずに中断してしまう様子。

#!/bin/sh
# Reset the System Clock to UTC if the hardware clock from which it
# was copied by the kernel was in localtime.

dev=$1

if [ -e /run/systemd/system ] ; then
    exit 0
fi

if [ -e /run/udev/hwclock-set ]; then
    exit 0
fi

if [ -f /etc/default/rcS ] ; then
    . /etc/default/rcS
fi

# These defaults are user-overridable in /etc/default/hwclock
BADYEAR=no
HWCLOCKACCESS=yes
HWCLOCKPARS=
HCTOSYS_DEVICE=rtc0
if [ -f /etc/default/hwclock ] ; then
    . /etc/default/hwclock
fi

if [ yes = "$BADYEAR" ] ; then
    /sbin/hwclock --rtc=$dev --systz --badyear
    /sbin/hwclock --rtc=$dev --hctosys --badyear
else
    /sbin/hwclock --rtc=$dev --systz
    /sbin/hwclock --rtc=$dev --hctosys
fi

# Note 'touch' may not be available in initramfs
> /run/udev/hwclock-set

なので次のように/run/systemd/systemのチェックをコメントアウトした。

#if [ -e /run/systemd/system ] ; then
#    exit 0
#fi

で、あとはRTCをOS起動時にI2Cで有効化するだけなのだけど、検索してみると/etc/rc.localやsystemdなどでコマンドを実行するやり方を見かける。 例えば/etc/rc.localで行う場合は次みたいなやつ。

#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.

echo ds3231 0x68 > /sys/class/i2c-adapter/i2c-1/new_device

# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi

exit 0

echo ds3231 0x68 > /sys/class/i2c-adapter/i2c-1/new_deviceを追加して、DS3231をRTCとして認識させるわけだけど、これでやるとログが次のようになる。

Dec  7 16:48:03 raspberrypi systemd-modules-load[169]: Inserted module 'i2c_dev'
Dec  7 16:48:03 raspberrypi systemd[1]: Starting Flush Journal to Persistent Storage...
Dec  7 16:48:03 raspberrypi systemd[1]: Started Create Static Device Nodes in /dev.
Dec  7 16:48:03 raspberrypi systemd[1]: Starting udev Kernel Device Manager...
Dec  7 16:48:03 raspberrypi systemd[1]: Started Set the console keyboard layout.
Dec  7 16:48:03 raspberrypi systemd[1]: Started Flush Journal to Persistent Storage.
Dec  7 16:48:03 raspberrypi systemd[1]: Reached target Local File Systems (Pre).
Dec  7 16:48:03 raspberrypi systemd[1]: Started udev Coldplug all Devices.

****** 長いのでちょっと省略 ******

Dec  7 16:48:11 raspberrypi systemd[1]: Starting /etc/rc.local Compatibility...
Dec  7 16:48:11 raspberrypi systemd[1]: Condition check resulted in fast remote file copy program daemon being skipped.
Dec  7 16:48:11 raspberrypi kernel: [   40.700114] i2c i2c-1: new_device: Instantiated device ds3231 at 0x68
Dec  7 16:48:11 raspberrypi systemd[1]: Starting Permit User Sessions...
Dec  7 16:48:11 raspberrypi kernel: [   40.838077] rtc-ds1307 1-0068: registered as rtc0
Dec  7 16:48:11 raspberrypi systemd[1]: Starting OpenBSD Secure Shell server...
Dec  7 16:48:11 raspberrypi systemd[1]: Started /etc/rc.local Compatibility.
Dec  7 16:48:12 raspberrypi systemd[1]: Started Permit User Sessions.
Dec  7 16:48:12 raspberrypi systemd[1]: Started Serial Getty on ttyAMA0.
Dec  7 16:48:12 raspberrypi systemd[1]: Started Getty on tty1.
Dec  7 16:48:12 raspberrypi systemd[1]: Reached target Login Prompts.
Dec  7 22:20:06 raspberrypi systemd[1]: Starting Daily apt download activities...
Dec  7 22:20:06 raspberrypi systemd[1]: Started OpenBSD Secure Shell server.
Dec  7 22:20:06 raspberrypi avahi-daemon[304]: Server startup complete. Host name is raspberrypi-3.local. Local service cookie is 410738562.

OSが起動してから、ディスクに保存されていた最終日時でログが記録されていき、/etc/rc.localが実行されてからようやく正しい日時で記録されるようになる。なので、/etc/rc.localが動く前の日時は当てにならない。

そこで、/boot/overlays/READMEに次のようにRTCの例が書いてあるので、これに従って設定する。今回はds1307をds3231に置き換えるだけで良い。

Using Overlays
==============

Overlays are loaded using the "dtoverlay" config.txt setting. As an example,
consider I2C Real Time Clock drivers. In the pre-DT world these would be loaded
by writing a magic string comprising a device identifier and an I2C address to
a special file in /sys/class/i2c-adapter, having first loaded the driver for
the I2C interface and the RTC device - something like this:

    modprobe i2c-bcm2835
    modprobe rtc-ds1307
    echo ds1307 0x68 > /sys/class/i2c-adapter/i2c-1/new_device

With DT enabled, this becomes a line in config.txt:

    dtoverlay=i2c-rtc,ds1307

This causes the file /boot/overlays/i2c-rtc.dtbo to be loaded and a "node"
describing the DS1307 I2C device to be added to the Device Tree for the Pi. By
default it usees address 0x68, but this can be modified with an additional DT
parameter:

    dtoverlay=i2c-rtc,ds1307,addr=0x68

Parameters usually have default values, although certain parameters are
mandatory. See the list of overlays below for a description of the parameters
and their defaults.

/boot/config.txtdtoverlay=i2c-rtc,ds3231を追加して保存する。

これでOSを起動すると、ログは次のようになる。OSの起動の頭からRTCが効いて正確に日時が扱えているのがわかる。

Dec  7 22:25:47 raspberrypi systemd-modules-load[157]: Inserted module 'i2c_dev'
Dec  7 22:25:47 raspberrypi systemd[1]: Starting Flush Journal to Persistent Storage...
Dec  7 22:25:47 raspberrypi systemd[1]: Started Apply Kernel Variables.
Dec  7 22:25:47 raspberrypi systemd[1]: Started Flush Journal to Persistent Storage.
Dec  7 22:25:47 raspberrypi systemd[1]: Started Create Static Device Nodes in /dev.
Dec  7 22:25:47 raspberrypi systemd[1]: Starting udev Kernel Device Manager...
Dec  7 22:25:47 raspberrypi systemd[1]: Started Set the console keyboard layout.
Dec  7 22:25:47 raspberrypi systemd[1]: Reached target Local File Systems (Pre).
Dec  7 22:25:47 raspberrypi systemd[1]: Started udev Coldplug all Devices.
Dec  7 22:25:47 raspberrypi systemd[1]: Starting Helper to synchronize boot up for ifupdown...
Dec  7 22:25:47 raspberrypi systemd[1]: Started Helper to synchronize boot up for ifupdown.
Dec  7 22:25:47 raspberrypi systemd[1]: Started udev Kernel Device Manager.

****** あとは省略 ******

/boot/overlays/READMEにはI2CのRTCについて次のように記述されている。ここに載っているモジュールであれば同様のやり方で設定できると思う。

Name:   i2c-rtc
Info:   Adds support for a number of I2C Real Time Clock devices
Load:   dtoverlay=i2c-rtc,<param>=<val>
Params: abx80x                  Select one of the ABx80x family:
                                  AB0801, AB0803, AB0804, AB0805,
                                  AB1801, AB1803, AB1804, AB1805

        ds1307                  Select the DS1307 device

        ds1339                  Select the DS1339 device

        ds3231                  Select the DS3231 device

        m41t62                  Select the M41T62 device

        mcp7940x                Select the MCP7940x device

        mcp7941x                Select the MCP7941x device

        pcf2127                 Select the PCF2127 device

        pcf2129                 Select the PCF2129 device

        pcf8523                 Select the PCF8523 device

        pcf8563                 Select the PCF8563 device

        rv3028                  Select the Micro Crystal RV3028 device

        addr                    Sets the address for the RTC. Note that the
                                device must be configured to use the specified
                                address.

        trickle-diode-type      Diode type for trickle charge - "standard" or
                                "schottky" (ABx80x only)

        trickle-resistor-ohms   Resistor value for trickle charge (DS1339,
                                ABx80x, RV3028)

        wakeup-source           Specify that the RTC can be used as a wakeup
                                source

        backup-switchover-mode  Backup power supply switch mode. Must be 0 for
                                off or 1 for Vdd < VBackup (RV3028 only)