第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. 今回やること
| やること | 何のため | |
|---|---|---|
| 2 | app.py 333 行を通しで読む | 自分で直せるようになる |
| 3 | index.html と manage.html を読む | 表示を変えたくなったときの地図 |
| 4 | GitHub から取ってきて動かす | 他人が再現できる |
| 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–17 | docstring と import | 使う道具の宣言 |
| 19–29 | # ===== 設定 ===== | パス、シリアルポート、保持日数、Flask 本体の生成 |
| 31–44 | find_serial_port() | 第9回 3.2 で足した自動検出 |
| 46–86 | # ===== データベース ===== | db() と init_db()。表を 2 枚作る |
| 89–121 | save_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–333 | if __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_loop | USB シリアルを読み続ける | ESP-NOW 経路だけ死ぬ |
purge_loop | 1 日 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.js | 205,222 B | Chart.js 4.4.0 (MIT) |
date-fns.min.js | 98,562 B | date-fns 3.6.0 (MIT) |
chartjs-adapter-date-fns.bundle.min.js | 50,650 B | chartjs-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 が failed | Python の例外 | 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] 解釈できない JSON | Master のファームウェアが古い | 連載の 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 table | init_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 は実測ではない」と書いて残してある件に、そこで決着を付けます。合っていなければ、合っていないと書きます。


コメント