PR

【第10回】出来上がったものを読み直す — app.py 333 行の地図と、GitHub から動かす手順

環境センサーデータサーバーシステム
記事内に広告が含まれています。

第9回で、電源を入れれば勝手に上がって、落ちても戻ってきて、90 日で古い行が消えるところまで来ました。置いておける状態です。

ただ、ここまでの作り方は「第8回で骨格を書いて、第9回で足す」でした。通しで読んだことが一度もありません。 足した順にコードが積み上がっているだけです。今回はまずそれを読み直します。

後半は、他人が同じものを動かせるようにする話です。GitHub に置いたものを取ってきて、自分の Pi で最初の 1 行が入るまで。ここまでの記事を読んでいない人でも、この回だけで動かせるように書きます。


この記事の構成 — 何を検証したか

今回は新しく作るものがありません。すでに動いているものを読むだけです。したがって本文に出てくるコードは、全部いま私の Pi で動いている実物そのものです。

部分出どころ状態
2 節の app.py 解説Pi で動いている実物 333 行2026-09-10 時点の実物
行番号・節の並びgrep -n の出力をそのまま転記実測
SERIAL_BAUD の修正今回読み直して見つけた不整合実機で反映・再起動確認済み
4 節の導入手順GitHub の tempserver/README.mdまっさらな場所での起動は検証済み (5 節)。git clone の行のみ公開後に確認
3.1 の CDN 廃止この記事を書きながら AP 経由で開いて発覚実機で修正・表示確認済み
3 節のテンプレート解説Pi の templates/ の実物実物

「読むだけ」の回ですが、読み直してバグが 1 つ見つかりました。 2.7 に書きます。動いているものを読み直す価値はここにあります。


1. 今回やること

やること何のため
2app.py 333 行を通しで読む自分で直せるようになる
3index.html と manage.html を読む表示を変えたくなったときの地図
4GitHub から取ってきて動かす他人が再現できる
5その手順を実際に通す書いた手順が本当に通るか確かめる

触るファイルはありません。2 節と 3 節は読むだけです。4 節で別ディレクトリに clone しますが、いま動いているサーバーには触りません。


2. app.py を通しで読む

2.1 まず、自分のファイルの地図を出す

解説を読む前に、自分の手元で地図を出してください。私の環境の行番号と一致しているはずです。

cd ~/tempserver
grep -n "^# =====\|^def \|^@app\.\|^if __name__" app.py

私の Pi ではこう出ました。

19:# ===== 設定 =====
31:def find_serial_port():
46:# ===== データベース =====
48:def db():
61:def init_db():
89:def save_reading(data, conn_type):
124:# ===== 経路 1: HTTP POST (第6回の WiFi 子機) =====
125:@app.post("/api/temperature")
126:def api_temperature():
136:# ===== 経路 2: USB シリアル (第7回の ESP-NOW Master) =====
137:def serial_loop():
204:# ===== 表示用 API =====
205:@app.get("/api/latest")
206:def api_latest():
221:@app.get("/api/history")
222:def api_history():
246:@app.get("/")
247:def dashboard():
251:# ===== 起動 =====
252:# ===== 表示名 (nickname) の API =====
253:@app.get("/api/nicknames")
254:def api_nicknames():
265:@app.put("/api/nicknames/<device_id>")
266:def api_nickname_set(device_id):
291:@app.delete("/api/nicknames/<device_id>")
292:def api_nickname_delete(device_id):
302:@app.get("/manage")
303:def manage():
306:# ===== 古いデータの掃除 =====
307:def purge_old_rows(days=None):
320:def purge_loop():
329:if __name__ == "__main__":

この出力の中に、すでに 1 つおかしなものがあります。 251 行と 252 行です。気づいた方は 2.6 まで飛ばしても構いません。

2.2 節の地図

行節中身
1–17docstring と import使う道具の宣言
19–29# ===== 設定 =====パス、シリアルポート、保持日数、Flask 本体の生成
31–44find_serial_port()第9回 3.2 で足した自動検出
46–86# ===== データベース =====db() と init_db()。表を 2 枚作る
89–121save_reading()2 経路の受信を 1 か所で正規化して保存する
124–133# ===== 経路 1 =====HTTP POST の受信口
136–201# ===== 経路 2 =====USB シリアルの受信ループ (別スレッド)
204–248# ===== 表示用 API =====/api/latest /api/history /
251# ===== 起動 =====中身が無い。迷子のコメント (2.6)
252–304# ===== 表示名の API =====/api/nicknames 3 本と /manage
306–326# ===== 古いデータの掃除 =====purge_old_rows() と purge_loop()
329–333if __name__ == "__main__":ここが本当の入口

見て分かるとおり、足した順にそのまま並んでいます。 第8回で書いた部分 (設定・DB・2 経路・表示用 API) の後ろに、第9回で足した部分 (表示名・掃除) がくっついています。読む順としては自然ではありません。

ただ、Python も Flask もこの並びを気にしません。関数は定義されていれば呼べますし、Flask のルートは @app.get が読み込まれた時点で登録されます。定義の順序ではなく、URL の具体度で照合されます。だから動きます。

2.3 動いているのは 3 つのスレッド

最後の 5 行が全体を決めています。

if __name__ == "__main__":
    init_db()
    threading.Thread(target=serial_loop, daemon=True).start()
    threading.Thread(target=purge_loop,  daemon=True).start()
    app.run(host="0.0.0.0", port=5000, debug=False, use_reloader=False)
スレッドやっていること止まると
メインFlask。HTTP を待ち受ける全部止まる
serial_loopUSB シリアルを読み続けるESP-NOW 経路だけ死ぬ
purge_loop1 日 1 回、古い行を消すDB が太り続ける

daemon=True は「メインが終わったら道連れで終わってよい」という印です。これが無いと、Ctrl+C で Flask を止めてもプロセスが終わりません。

並びを気にする必要があるのは 1 か所だけです。 init_db() が最初にあること。表が無い状態でスレッドが動き出すと、第9回で私が踏んだ no such table になります。

2.4 入口は 2 つ、保存は 1 か所

このサーバーの設計で一番効いているのがここです。データの入口は 2 つあります。

ESP8266 (WiFi)  ──HTTP POST──→  api_temperature()   124-133 行
XIAO (ESP-NOW)  ──→ Master ──USB──→  serial_loop()    136-201 行
                                          │
                                          ↓
                                   save_reading()      89-121 行
                                          ↓
                                     temperature.db

入口は形が違います。片方は JSON の本文が 1 件ぶん、もう片方は Master が配列でまとめて送ってきます。それでも保存する処理は 1 つしか書いていません。 save_reading() です。

この関数がやっているのは 4 つです。

内容
キー名の吸収temperature でも temp でも受ける。rssi と signal_strength も同じ
必須項目の確認device_id が無ければ拒否。温度が数値でなければ拒否
範囲の確認-55〜+125℃ の外は捨てる。DS18B20 の測定範囲
保存と記録INSERT して、標準出力に 1 行出す

範囲チェックがあるおかげで、第1回で見た -127 (CRC 失敗) や 85.0 (変換未実行) のうち、-127 はデータベースに入りません。 グラフが谷底まで落ちるのを防いでいます。

なお 85.0 は通ります。 範囲内の値だからです。電源不足の定番の値なので、グラフに 85 度の平らな線が出たら疑ってください。ここは弾こうと思えば弾けますが、真夏の屋外で 85 度が絶対に無いとは言い切れないので、私は入れていません。

2.5 シリアル側だけ長い理由

serial_loop() が 65 行と、この中では飛び抜けて長いです。中身の大半は異常系です。

while True:                      # 外側: ポートを探して開くループ
    port = find_serial_port()    #   毎回探し直す
    if port is None: 5 秒待って continue
    try: ser = serial.Serial(...)
    except: 5 秒待って continue

    try:
        while True:              # 内側: 1 行ずつ読むループ
            raw = ser.readline()
            空行        → 捨てる
            "#" で始まる → Master の人間向けメッセージ。表示だけ
            "{" 以外     → 捨てる
            JSON 不正   → 表示して捨てる
            "sensors" 配列 → 中の 1 件ずつ save_reading()
    except:                      # 抜線・切断はここに来る
        表示して外側へ戻る

二重ループなのは、抜き差しに耐えるためです。 内側で例外が出ると外側に戻り、ポートを探し直してから開き直します。第9回で find_serial_port() を try: の前に置いたのはこのためです。中に入れると、開けなかったときに port が未定義のまま except に飛んで別のエラーになります。

もう 1 か所、読み飛ばしやすい注意書きがあります。

# 注意: この階層の device_id は Master 自身の ID。
#       子機の ID は sensors[].sensor_id のほう。

Master が送る JSON には device_id が 2 か所に出てきます。外側は Master 自身 (MST-)、配列の中が子機 (NOW-) です。ここを取り違えると、全部の温度が Master の名前で入ります。私は一度やりました。

2.6 継ぎ足しの跡 — 迷子になったコメント

2.1 の出力の 251 行と 252 行です。

251:# ===== 起動 =====
252:# ===== 表示名 (nickname) の API =====

「起動」の見出しの下に、起動のコードがありません。 すぐ次の行から表示名の API が始まります。本物の起動処理は 329 行、ファイルの一番下です。

原因ははっきりしています。第8回の時点では、このファイルの末尾は「表示用 API → 起動」でした。第9回で表示名の API を「起動の手前」に挿し、さらに掃除の処理を挿した結果、見出しだけが元の位置に取り残されました。

直していません。 直すこと自体は 1 行の移動ですが、この記事の目的は「出来上がったものを読む」ことです。継ぎ足しで育てたコードには必ずこういう跡が残るという実例として、そのまま置いておきます。

気になる方は、251 行を切り取って 328 行の直前に貼れば直ります。nano なら Ctrl+K で切り取り、Ctrl+U で貼り付けです。動作は変わりません。

2.7 読み直して見つけた不整合 — 設定が効いていなかった

こちらは実害があったので直しました。19〜29 行の設定部です。修正前はこうでした。

SERIAL_PORT    = os.environ.get("SERIAL_PORT", "auto")
SERIAL_BAUD = 115200
RETENTION_DAYS = int(os.environ.get("RETENTION_DAYS", "90"))

上と下は .env から読んでいます。真ん中だけ直接書いてあります。 ところが私の .env には、こう書いてありました。

SERIAL_BAUD=115200
RETENTION_DAYS=90

この SERIAL_BAUD は何の効果もありません。 たまたま値が同じなので誰も気づきません。もし 9600 に変えたい人がいたら、.env を書き換えて再起動して、それでも 115200 のままで、原因が分からず何時間か溶かすことになります。

直しました。1 行です。

SERIAL_BAUD = int(os.environ.get("SERIAL_BAUD", "115200"))

手順はこうです。sed で書き換えて、Python に構文だけ見てもらってから再起動します。

cd ~/tempserver
cp app.py app.py.bak-$(date +%Y%m%d-%H%M%S)
sed -i 's/^SERIAL_BAUD = 115200$/SERIAL_BAUD = int(os.environ.get("SERIAL_BAUD", "115200"))/' app.py
grep -n "SERIAL_BAUD" app.py
python3 -c "import ast; ast.parse(open('/home/pi/tempserver/app.py').read()); print('syntax OK')" 

syntax OK を見てから再起動します。この 1 行は必ず単独で貼ってください。 他の行と一緒に貼ると、パスワードの入力待ちが次の行を食べます。

sudo systemctl restart tempserver
systemctl is-active tempserver
journalctl -u tempserver -n 15 --no-pager

ast.parse は安上がりな保険です。 書き換えた Python を再起動する前に、構文だけでも通るか見ておく。これだけで「サービスが上がってこない」の何割かは事前に潰せます。


3. 画面側を読む

3.1 index.html — 表とグラフ

122 行です。半分が CSS で、残りが JavaScript です。やっていることは 2 つだけです。

関数やること
loadLatest()/api/latest を叩いて表を作り直す
loadChart()/api/history を叩いて Chart.js に渡す

この 2 つを refresh() が呼び、setInterval(refresh, 60000) で 1 分ごとに繰り返します。ページを開きっぱなしにしておけば勝手に更新されます。

読むうえで大事なのは、時刻の扱いです。

function toDate(s) { return new Date(s.replace(' ', 'T') + 'Z'); }

SQLite の CURRENT_TIMESTAMP は 2026-09-10 09:45:00 という形のUTCです。そのまま new Date() に渡すとブラウザが「地方時」と解釈して、9 時間ずれます。空白を T にして末尾に Z を足す、これだけで正しく解釈されます。

もう 1 つ、script の読み込み先です。ここは、この記事を書きながら直しました。 書き換える前と後を両方載せます。

もともとはこう書いていました。

<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/date-fns@3.6.0/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>

CDN から取ってくる形です。Pi 側にインターネットは要らない、必要なのはダッシュボードを開く端末側だけ。それで足りると思っていました。

3.1.1 現場の AP には、外に出る道が無い

ノート PC を Pi の AP に繋いでダッシュボードを開いてみました。表は出るのに、グラフだけ出ません。 しかも表示がひどく遅い。

ブラウザの開発者ツールを見ると、答えは 1 行で出ていました。

GET https://cdn.jsdelivr.net/npm/chart.js@4.4.0/...
net::ERR_NAME_NOT_RESOLVED

考えれば当たり前でした。AP に繋いだ PC のデフォルトゲートウェイは Pi です。そしてこの Pi は、wlan1 から wlan0 へ通す設定をしていません。 名前解決すらできないので、ブラウザは 3 つの CDN を順にタイムアウトさせてから、ようやく表の描画に入る。「グラフが出ない」と「表示が遅い」は、同じ原因の両面でした。

なぜ最初から気づかなかったのか。作り方が違ったからです。

先に現地へ出した旧版は、Pi を wlan0 で WiFi に繋いで、VNC で Pi の画面を共有しながら作っていました。手元の PC も Pi も、作っている間ずっとインターネットの中にいた。CDN が届かない状況が、最後まで一度も起きなかったんです。

このブログ版は「Pi の AP に直接繋いで見る」ことを前提に組み直しました。そこで初めて表に出ました。同じものを作り直したつもりでも、見る経路の前提が変われば、CDN のような一見無関係な箇所が効いてきます。

3.1.2 ライブラリを Pi の中に置く

現場に置く機械で、外に出ないと絵が描けないのは筋が悪い。 ライブラリの実体を Pi の中に置くことにしました。

mkdir -p ~/tempserver/static/vendor
cd ~/tempserver/static/vendor
curl -fsSL -o chart.umd.min.js https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js
curl -fsSL -o date-fns.min.js https://cdn.jsdelivr.net/npm/date-fns@3.6.0/cdn.min.js
curl -fsSL -o chartjs-adapter-date-fns.bundle.min.js https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js

index.html の読み込み先を、その 3 つに向けます。

<script src="/static/vendor/chart.umd.min.js"></script>
<script src="/static/vendor/date-fns.min.js"></script>
<script src="/static/vendor/chartjs-adapter-date-fns.bundle.min.js"></script>

Flask は static/ を最初から配信します。 app.py には 1 行も足していません。置いて、参照先を変えて、再起動しただけです。

sudo systemctl restart tempserver
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/static/vendor/chart.umd.min.js

200 が返れば配れています。この状態で AP に繋いだ PC から開き直すと、グラフまで出て、表示も速くなりました。 待つ相手がいなくなったからです。

同梱したのは次の 3 つです。いずれも配布元の最小化済みファイルそのままで、ライセンス表記もファイル先頭に残してあります。

ファイルサイズ出どころ
chart.umd.min.js205,222 BChart.js 4.4.0 (MIT)
date-fns.min.js98,562 Bdate-fns 3.6.0 (MIT)
chartjs-adapter-date-fns.bundle.min.js50,650 Bchartjs-adapter-date-fns 3.0.0 (MIT)

合計 350 KB ほどリポジトリが太りますが、clone しただけで完結するほうを取りました。

3.2 manage.html — 表示名の編集

147 行。第9回で作った管理画面です。読みどころは 1 か所だけです。

const ids = new Set();
for (const r of latest) { ids.add(r.device_id); ... }
for (const id of Object.keys(nickOf)) ids.add(id);

温度が来ているデバイスと、名前だけ登録済みのデバイスを合わせて並べています。片方だけだと Master が一覧に出ません。Master は温度を送らないからです。名前を付けたいのに一覧に出ない、という状態になります。

それと、この画面は innerHTML ではなく createElement と textContent で組み立てています。device_id に引用符が混ざったときに、組み立てた HTML が壊れないようにするためです。第9回 8 節に、この書き換えを AI に提案されたときのやりとりを載せてあります。


4. GitHub から取ってきて動かす

ここからは、この連載を読んでいない人向けの手順です。すでに動かしている方も、手順が本当に通るかの確認として 4.5 まで目を通してもらえると助かります。

4.1 リポジトリには温度サーバーが 4 つ入っています

先に断っておきます。clone すると、それらしい名前のディレクトリが 4 つ出てきます。

ディレクトリ位置づけ
tempserver/この連載で作るもの。まずこれ。 1 ファイル完結
temperature_server/旧実装。app/ にパッケージ分割した版
temperature_server_full/旧実装。実運用版。テストや CLI まで入っている大きいもの
temperature_server_deploy/旧実装への配布スクリプト

後ろの 3 つは、会社で動かしていたものを引き上げてきたものです。参考にはなりますが、この連載を再現するなら tempserver/ だけ見てください。 リポジトリのルート README にも同じ案内を書いてあります。

4.2 必要なものを入れる

sudo apt update
sudo apt install -y python3-flask python3-serial sqlite3

venv は使いません。 apt 版は OS が管理する Python にそのまま入るので、pip が出す externally-managed-environment を踏みません。私の Pi での組み合わせは Python 3.13.5 / Flask 3.1.1 です。

pip で入れたい方向けに requirements.txt も置いてありますが、Raspberry Pi OS では apt を勧めます。

4.3 取ってきて置く

cd ~
git clone https://github.com/whiskerpad/sensorserverwithESP.git
mkdir -p ~/tempserver
cp -r ~/sensorserverwithESP/tempserver/. ~/tempserver/
cd ~/tempserver
ls

app.py と templates、そして static が見えれば成功です。static/vendor/ にはグラフ用の JavaScript が 3 つ入っています。 CDN を使わないので、これが無いとグラフだけ出ません(3.1)。

置き場所は ~/tempserver に合わせてください。 tempserver.service の中身が /home/pi/tempserver を指しているためです。ユーザー名が pi 以外の場合は、サービスファイルの 3 か所 (User= WorkingDirectory= ExecStart=) を自分の名前に書き換えてください。

4.4 設定は、いじらなくても動く

cd ~/tempserver
cp .env.example .env
nano .env

既定のままで動きます。触るとしたら保持日数 (RETENTION_DAYS) くらいです。

1 つ落とし穴があります。 app.py は python-dotenv を使っていません。この .env を読むのは systemd の EnvironmentFile= だけです。つまり、

起動のしかた.env が効くか
sudo systemctl restart tempserver効く
python3 app.py と手で叩く効かない (既定値で動く)

設定を変えたのに反映されない、というときはここを疑ってください。

4.5 まず手で動かす

いきなり systemd に載せないでください。 動かなかったときに、原因がコードなのか systemd なのか切り分けられなくなります。

cd ~/tempserver
python3 app.py

Running on http://0.0.0.0:5000 が出たら、別のターミナルから 1 件放り込みます。

curl -X POST http://localhost:5000/api/temperature \
  -H "Content-Type: application/json" \
  -d '{"device_id":"TEST-0001","temperature":25.0,"voltage":4.1}'

{"status":"ok"} が返り、app.py を動かしている側に受信のログが 1 行出れば、受信から保存まで通っています。 データベースを直接見て確かめることもできます。

sqlite3 ~/tempserver/temperature.db \
  "SELECT * FROM temperatures WHERE device_id='TEST-0001';" 

確認できたら試験データは消します。残すとグラフに架空のデバイスが出ます。

sqlite3 ~/tempserver/temperature.db \
  "DELETE FROM temperatures WHERE device_id='TEST-0001';" 

Ctrl+C で app.py を止めます。

4.6 自動起動に載せる

sudo cp ~/tempserver/tempserver.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now tempserver
systemctl is-active tempserver
journalctl -u tempserver -n 30 --no-pager

active と出て、ログに [serial] ... を開きました か [serial] シリアルポートが見つかりません のどちらかが出ていれば動いています。後者は異常ではありません。 ESP-NOW の Master を挿していないだけです。

4.7 開く

http://<Pi の IP アドレス>:5000/

表示名の編集は /manage です。IP が分からなければ Pi 側で hostname -I を叩いてください。

開き方は 3 通りあります。localhost が使えるのは Pi 本体の中だけである点に注意してください。

どこからアドレス
Pi 本体(モニタとキーボードを付けて)http://localhost:5000/
同じ LAN の PC / スマートフォンhttp://(Pi の IP アドレス):5000/
Pi の AP に直接繋いだ端末http://192.168.4.1:5000/

3 番目が、インターネットの無い現場でそのまま使える形です。WiFi の繋がるノート PC でもタブレットでもスマートフォンでも、AP に入れば画面の確認もデータの吸い出しもできます。

なお、AP に繋いだ端末で http://localhost:5000/ と打つと必ず接続を拒否されます。 localhost は「そのブラウザが動いている機械自身」を指す名前なので、その端末自身の 5000 番を探しに行くだけだからです。私は一度これをやって、AP 側からは見られないものだと思い込みました。


5. 書いた手順を、実際に通す

ここまでの手順は、私が記憶で書いたものです。 記憶で書いた手順は通りません。通してから記事にします。

1 つ断りがあります。この記事を書いている時点で、リポジトリはまだ非公開です。したがって git clone の行だけは、公開後に改めて確認します。 ここで確かめるのは残り全部 —「まっさらな場所に app.py と templates を置いただけで、本当に動き出すか」です。実質こちらが本題です。

まず本番を止めます。ポート 5000 がぶつかるためです。この行は単独で貼ってください。

sudo systemctl stop tempserver

次に、まっさらなディレクトリを作って必要なファイルだけ置きます。

mkdir -p ~/clone_test/tempserver/templates
cd ~/tempserver
cp app.py ~/clone_test/tempserver/
cp templates/index.html templates/manage.html ~/clone_test/tempserver/templates/
cd ~/clone_test/tempserver
ls -laR

temperature.db も .env もありません。この状態から起動します。

cd ~/clone_test/tempserver
python3 app.py

別のターミナルから 1 件放り込んで、その場で中身を見ます。

curl -X POST http://localhost:5000/api/temperature -H "Content-Type: application/json" -d '{"device_id":"TEST-0001","temperature":25.0,"voltage":4.1}'
ls -l ~/clone_test/tempserver/
sqlite3 ~/clone_test/tempserver/temperature.db "SELECT * FROM temperatures;"

返ってきたのがこれです。

total 40
-rw-rw-r-- 1 pi pi 12176 Sep 10 13:43 app.py
-rw-r--r-- 1 pi pi 24576 Sep 10 13:44 temperature.db
drwxrwxr-x 2 pi pi  4096 Sep 10 13:43 templates

1|ESP-FA8BC4|29.38|4.3|-35|0|wifi|2026-09-10 04:43:19
2|TEST-0001|25.0|4.1||0|wifi|2026-09-10 04:44:52

見たかったことは 3 つとも確認できました。

temperature.db が新しく出来ているinit_db() が効いている
{"status":"ok"} が返る受信口が生きている
TEST-0001 の行がある保存まで通っている

おまけが 1 行目に写っています。 ESP-FA8BC4 — こちらは試験データではなく、本物の子機です。テスト中も 2 分おきに送り続けているので、試験用のデータベースのほうに入りました。

裏を返すと、本番のデータベースにはこの数分ぶんの穴が空きます。 グラフを見ると、その時間だけ線が途切れているはずです。止めて試すというのはそういうことです。短時間で済ませるか、子機を止めてからやるか、どちらかです。

後始末をして本番を戻します。

cd ~
rm -rf ~/clone_test
sudo systemctl start tempserver
systemctl is-active tempserver
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/
active
200

元どおりです。


6. 詰まりやすいところ

症状原因対処
systemctl status が failedPython の例外journalctl -u tempserver -n 50 --no-pager に本文が出ている
ブラウザは開くが表が空まだ 1 件も届いていない4.5 の curl で試験データを入れて切り分ける
表は出るがグラフが出ない端末側から CDN に届いていない3.1 参照。ブラウザの開発者ツールでエラーを見る
時刻が 9 時間ずれるtoDate() を通していないSQLite の時刻は UTC。3.1 参照
[serial] シリアルポートが見つかりません が続くMaster が挿さっていないls /dev/ttyUSB* /dev/ttyACM* で見えるか確認
[serial] 解釈できない JSONMaster のファームウェアが古い連載の Master スケッチに更新する
.env を変えたのに効かない手で python3 app.py している4.4 参照。systemd 経由で再起動する
Address already in useサービスが動いたまま手起動したsudo systemctl stop tempserver してから
全部の温度が Master の名前で入るsensor_id ではなく外側の device_id を拾っている2.5 参照
no such tableinit_db() より先にスレッドが動いた2.3 参照。起動順を確認

7. AI との会話例

コツ: 「読み直して」と頼む

今回いちばん役に立った頼み方はこれです。新しく作らせるのではなく、すでに動いているものを読ませる。

私: 「これがいま動いている app.py の全文。新しい機能はいらない。通しで読んで、おかしいところがあれば指摘してほしい」

返ってきたのが 2.6 の迷子コメントと、2.7 の SERIAL_BAUD です。片方は実害あり。動いているから正しい、ではないということです。テストが通ることと、書いたつもりのものが書けていることは別です。

コツ: 行番号を推測させない

2.2 の地図を作るとき、AI は最初「だいたいこの辺」で行番号を書こうとしました。以前これで痛い目を見ているので、止めました。

私: 「行番号を推測で書かないこと。grep -n のコマンドを出して、私が実行した出力をそのまま使ってほしい」

前に一度、構造体のバイト数を AI が計算で「68」と書き、私がそのまま記事に載せてしまったことがあります。実際は 72 でした。計算できるものと、実物を見ないと分からないものは違います。 行番号は後者です。

コツ: 手順は書かせたあと必ず通す

4 節の手順も、最初は AI が私の環境の記憶から書いたものでした。そのまま載せず、5 節のとおり clone から通しました。手順書は、書いた本人が通すまで下書きです。


8. ここまでで出来上がったもの

[ESP8266 子機] ──HTTP POST──┐
                             ├→ [Pi: Flask + SQLite] → ブラウザ
[XIAO 子機] ─ESP-NOW→ [Master] ──USB シリアル──┘

  ・電源を入れれば勝手に上がる
  ・落ちても 10 秒後に戻ってくる
  ・90 日より古いデータは自動で消える
  ・device_id ではなく「冷却塔1」で読める
  ・インターネットが無くても、AP に繋いだ端末だけで全部見られる
  ・GitHub から clone すれば、他人の Pi でも同じものが動く

序章で「しまい込んでいた部品を形にする」と書きました。ここまでで、形にしたものを他人に渡せるところまで来ました。

9. これは完成品ではありません

git clone して動いたら、そこがゴールではなく出発点です。最後にそれだけ書かせてください。

この連載で公開しているコードは、私の現場で必要だったものだけが入っています。温度を集めて、貯めて、表とグラフにする。それだけです。先に現地へ出した旧版のほうは、何度も作り直したので継ぎ足しの跡だらけで、見せられる形になっていません。連載版はそれを整理し直したものですが、整理し直したこの版ですら、この回を書きながら 2 か所変わりました。

変わったところきっかけ
SERIAL_BAUD が環境変数を読んでいなかった(2.7)通しで読み直したら見つかった
Chart.js を CDN から static/vendor/ へ(3.1)AP に繋いで開いたら、グラフだけ出なかった

どちらも「読み直した」「実際に試した」から出てきたもので、書く前には見えていませんでした。コードは書いた時点で完成しません。

だから、これを試す方にお願いがあります。動いたところから先は、自分の現場に必要な機能を、AI と話しながら自分で足していってください。

思いつくままに挙げれば、こういうものです。

足したくなるもの取っかかり
しきい値を超えたら知らせるsave_reading() の中で判定を 1 つ足すだけ
CSV で書き出す/api/history と同じ SQL に csv モジュールを噛ませる
Pi のそばに小さな画面を付ける番外編の I2C キャラクタ液晶がその例です
センサーを増やす・別の種類にするdevice_id の接頭辞を 1 つ増やす
表示を自分の見やすい形に変えるindex.html は 122 行しかありません

どれも大掛かりな話ではありません。難しいのは、最初に動くものを 1 つ手に入れるところで、そこさえ越えれば、あとは「こうしたい」を言葉にして相談する作業になります。この連載でやってきたのは、まさにそれだけです。

そして、うまくいかなかったら現物を見てください。 AI はコードを読めますが、抜けたジャンパー線も、切れかけたリード線も、届かない CDN も見られません。見て、出力をそのまま貼る。 要約しない。推測させない。手順は自分で通す。この 3 つだけで、独学より遥かに速く進めます。


次回予告

最終回は、ブレッドボードのままだったものをブレッドボード基板にはんだ付けして「現地出動盤」にします。 挿しているだけの配線を固定し、電池の分圧回路もそこで本実装です。

第6回で測った 4.30 V の飽和と、第7回・第9回で「しきい値 3.3 V は実測ではない」と書いて残してある件に、そこで決着を付けます。合っていなければ、合っていないと書きます。


← 第9回 | 連載目次 | 最終回 →

コメント

タイトルとURLをコピーしました