PR

【第6回】ESP から Pi の AP へ、温度データを HTTP POST で送る

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

第5回までで、Pi は 2 系統の WiFi を持ち、ESP 専用の AP が立ち上がった状態になっています。今回はいよいよ ESP から Pi へ温度データを送ります。第1回で「シリアルモニタに温度が出た」ところから、ようやく無線でデータが飛ぶところまで来ます。

この連載で一番「動いた」と実感できる回だと思います。

今回のゴール

  • ESP が第4回で作った AP (192.168.4.x) に接続する
  • 温度データを JSON にして Pi へ HTTP POST する
  • Pi 側の画面に、届いたデータがそのまま表示される
  • そのうえで、電池運用のための DeepSleep 版に進む

前提の確認

先に進む前に、第4回で作った AP が生きているか確認してください。Pi の SSH で:

systemctl is-active hostapd
systemctl is-active dnsmasq
ip addr show wlan1 | grep 'inet '

active が 2 つと inet 192.168.4.1/24 が出れば準備 OK です。


hostapd・dnsmasq が active、wlan1 が 192.168.4.1 であることの事前確認

1. まず Pi 側に「受け皿」を作る

ここで順番の話をします。データを送る前に、受け取る側を先に作ります。受け皿がない状態で送っても、ESP 側には「送れなかった」としか出ず、原因が ESP なのかネットワークなのか Pi なのか分かりません。

本格的なサーバー (データベースに保存してグラフを描く Flask アプリ) は第8回で作ります。今回作るのは、届いた中身をそのまま画面に吐き出すだけの 10 行ほどのスクリプトです。これがあるだけで、切り分けが劇的に楽になります。

1.1 Flask を入れる

Pi の SSH で以下を実行します。

sudo apt install -y python3-flask

Flask は Python で Web サーバーを書くための道具です。第8回で本格的に使いますが、今回は「POST を受け取って表示する」だけに使います。

補足: ネットの記事では pip install flask と書かれていることが多いですが、Raspberry Pi OS Trixie でこれをやると externally-managed-environment というエラーで拒否されます。OS が管理している Python を壊さないための仕組みです。今回は apt で入れるのが一番簡単です。第8回では、この問題への正式な対処である仮想環境 (venv) を扱います。

1.2 受信スクリプトを作る

nano ~/recv_test.py

以下を貼り付けて保存します 。

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post("/api/temperature")
def receive():
    print("受信:", request.get_data(as_text=True), flush=True)
    return jsonify(status="ok")

app.run(host="0.0.0.0", port=5000)

中身はこれだけです。

nano エディタで作成した Flask 受信テストスクリプト

Ctrl + O → Enter → Ctrl + X で 上書き保存&nanoエディタ終了

  • /api/temperature という宛先に POST が来たら、
  • 受け取った中身をそのまま画面に出して、
  • {"status":"ok"} と返す

host="0.0.0.0" は「どのネットワークからの接続も受ける」という意味です。ESP は wlan1 側 (192.168.4.1) から来るので、この指定が必要になります。

1.3 起動して待ち受ける

python3 ~/recv_test.py

以下のような表示が出て、そのまま止まったように見えれば正常です。これは待ち受け状態です。

 * Serving Flask app 'recv_test'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://192.168.11.23:5000
Press CTRL+C to quit

WARNING は「これは開発用の簡易サーバーなので本番で使うな」という注意です。今回はテストなので気にしなくて構いません。

ここで戸惑いやすい点があります。表示されている URL に 192.168.4.1(ESP から見た Pi のアドレス)が出てきません。上の例では 127.0.0.1(Pi 自身)と 192.168.11.23(家の LAN 側 = wlan0)の 2 つだけです。

これで正常です。1 行上の Running on all addresses (0.0.0.0) が答えで、「このマシンが持っている全部のアドレスで待ち受けている」という意味です。Flask は代表的なものを 1〜2 個だけ表示する仕様なので、wlan1 側の 192.168.4.1 は省略されているだけです。ちゃんと待ち受けています。次の §1.4 で、実際に 192.168.4.1 宛に投げて確かめます。

受信テストスクリプトがポート 5000 で待ち受けている画面

止めるときは Ctrl + C です。この画面は開いたままにしておいてください。ここに ESP からのデータが流れてきます。

1.4 受け皿が動いているか、先に Pi 自身で試す

ESP を触る前に、受け皿が本当に動いているかを確認しておきます。SSH をもう 1 枚開いてログイン後、以下を実行します。

curl -X POST http://192.168.4.1:5000/api/temperature \
     -H "Content-Type: application/json" \
     -d '{"device_id":"TEST","temperature":25.5}'

{"status":"ok"} が返り、さっきの待ち受け画面に以下が出れば成功です。

Raspberry Pi 自身から curl でテスト POST を送る
受信: {"device_id":"TEST","temperature":25.5}
テスト POST を受け取った待ち受け側の表示

ここまで来れば、あとは「ESP から同じことをする」だけになります。もしここで失敗するなら、原因は ESP ではなく Pi 側だと確定できます。これが受け皿を先に作る理由です。


2. ESP 側 — 最小のスケッチで送ってみる

配線は第1回のままです。DS18B20 の DATA を GPIO4、4.7kΩ プルアップ、それだけ。新しく足す部品はありません。

2.1 スケッチ

Arduino IDE で新規スケッチを開いて貼り付けてください。

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <ArduinoJson.h>

#define ONE_WIRE_BUS 4              // DS18B20 の DATA を繋いだピン

// ★ここ 2 行だけ、第4回で決めた自分の値に書換える
const char* apSSID     = "YOUR_AP_SSID_HERE";
const char* apPassword = "YOUR_AP_PASSWORD_HERE";

const char* serverURL  = "http://192.168.4.1:5000/api/temperature";

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
WiFiClient wifiClient;

void setup() {
    Serial.begin(115200);
    delay(500);
    Serial.println();
    Serial.println("=== ESP -> Pi 送信テスト ===");

    sensors.begin();

    // ----- WiFi 接続 -----
    WiFi.mode(WIFI_STA);
    WiFi.begin(apSSID, apPassword);
    Serial.print("[WiFi] 接続中");

    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }
    Serial.println();

    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("[WiFi] 接続できませんでした。SSID とパスワードを確認してください");
        return;
    }
    Serial.print("[WiFi] 接続成功 IP=");
    Serial.print(WiFi.localIP());
    Serial.print("  電波強度=");
    Serial.print(WiFi.RSSI());
    Serial.println(" dBm");
}

void loop() {
    if (WiFi.status() != WL_CONNECTED) {
        delay(5000);
        return;
    }

    // ----- 温度を測る -----
    sensors.requestTemperatures();
    delay(800);
    float temp = sensors.getTempCByIndex(0);

    if (temp == DEVICE_DISCONNECTED_C) {
        Serial.println("[DS18B20] センサーが読めません");
        delay(10000);
        return;
    }

    // ----- JSON を組み立てる -----
    StaticJsonDocument<256> doc;
    doc["device_id"]   = "ESP-TEST01";
    doc["temperature"] = round(temp * 100) / 100.0;

    String payload;
    serializeJson(doc, payload);
    Serial.print("[POST] 送信: ");
    Serial.println(payload);

    // ----- HTTP POST -----
    HTTPClient http;
    http.setTimeout(5000);
    http.begin(wifiClient, serverURL);
    http.addHeader("Content-Type", "application/json");
    int httpCode = http.POST(payload);

    if (httpCode > 0) {
        Serial.printf("[POST] HTTP %d  応答: %s\n",
                      httpCode, http.getString().c_str());
    } else {
        Serial.printf("[POST] 失敗: %s\n",
                      http.errorToString(httpCode).c_str());
    }
    http.end();

    delay(10000);   // 10 秒ごとに送る
}

2.2 書換えるのは 2 行だけ

apSSID と apPassword を、第4回の hostapd.conf に書いた値に合わせてください。ここが一致していないと接続できません。

プレースホルダをそのまま書込まないでください。私は第4回の記事の再検証で自分自身でこれをやって、うまくいかない事をAIに指摘を受けAP の名前が「(実際の SSID)」でスマホに表示されてAIに指摘を受けました。

2.3 ライブラリを 1 つ追加

第1回・第2回で入れた OneWire と DallasTemperature に加えて、ArduinoJson が必要です。ツール → ライブラリを管理 で「ArduinoJson」を検索し、Benoit Blanchon のものをインストールしてください。

Arduino IDE のライブラリマネージャで ArduinoJson をインストール

注意: バージョン 6 系と 7 系で書き方が変わっています。上のスケッチは 6 系の書き方です。7 系を入れると StaticJsonDocument が非推奨の警告を出します (動きはします)。迷ったら 6 系の最新版を選んでください。

2.4 書込みとシリアルモニタ

ボード設定は第1回のままで構いません。書込んだら ツール → シリアルモニタ、ボーレート 115200 にします。

ESP 側の画面にこう出れば成功です。

=== ESP -> Pi 送信テスト ===
[WiFi] 接続中......
[WiFi] 接続成功 IP=192.168.4.231  電波強度=-47 dBm
[POST] 送信: {"device_id":"ESP-TEST01","temperature":26.75}
[POST] HTTP 200  応答: {"status":"ok"}
ESP8266 のシリアルモニタに出た HTTP POST 成功(HTTP 200)の表示

同時に、Pi の待ち受け画面にはこう出ます。

受信: {"device_id":"ESP-TEST01","temperature":26.75}
192.168.4.231 - - [01/Sep/2026 12:34:56] "POST /api/temperature HTTP/1.1" 200 -
ESP-WROOM-02 から届いた温度データを Raspberry Pi 側で受信したログ

IPアドレス192.168.4.228のESP-WROOM02からPOSTがPiに届きました

2 つの画面が同時に動いた瞬間が、この連載で達成感の出るところだと思います。センサープローブを握ると、10 秒後に Pi 側の数字が上がります。

この時点で ESP が持っている IP (192.168.4.231 など) は、第4回で設定した dnsmasq の動的プール .228〜.254 から割当てられたものです。


3. いま何が起きたのか

コードの流れを追っておきます。ここを理解しておくと、後で詰まったときに自力で切り分けられます。

コードやっていること
WiFi.mode(WIFI_STA)ESP を「子機」として動かす宣言。AP (親機) ではなくクライアントになる
WiFi.begin(SSID, PW)接続を開始するだけ。ここで待ってはくれない
while (WiFi.status() != WL_CONNECTED)だから自分で待つ。0.5 秒 × 20 回 = 最大 10 秒
WiFi.localIP()割当てられた自分の IP。ここが 0.0.0.0 なら繋がっていない
WiFi.RSSI()電波の強さ。-50 前後なら良好、-80 を下回ると不安定
serializeJson(doc, payload)C++ のデータを JSON の文字列に変換
http.POST(payload)送信。戻り値が 200 なら成功

WiFi.begin() が「接続を開始するだけ」という点が、初めてだと分かりにくいところです。関数を呼んだ時点では、まだ繋がっていません。

JSON の中身と、Pi 側が読む項目

今回送ったのは device_id と temperature の 2 つだけですが、第8回で作る本番の Flask サーバーは、もっと多くの項目を受け取れるように作ってあります。

項目意味必須
device_idどのチップから来たか必須
temperature (または temp)温度 (℃)必須
name表示名のフォールバック任意
ip_addressチップが名乗る自分の IP任意
voltage電源電圧 (V)任意
battery_percent電池残量 (%)任意
battery_mode電池駆動なら 1、USB 給電なら 0任意
signal_strength電波強度 (dBm)任意

必須は 2 つだけです。まず 2 つで通してから、必要な項目を足していくのが確実な進め方です。


4. 本番版へ — 電池で動かすために足りないもの

ここまでのスケッチは USB 給電でずっと起きている前提です。このまま電池を繋ぐと、単三 3 本で 1 日ももちません。ESP は WiFi を使っている間、70〜170mA を食い続けるからです。

本番機にするために足りないものが 4 つあります。

4.1 DeepSleep — 普段は寝かせる

温度を 2 分に 1 回送るなら、残りの 1 分 55 秒は寝かせておけばいいという発想です。ESP8266 の DeepSleep 中の消費電流は 20µA 前後。起きているときの 1/5000 以下です。

コードとしては最後の 1 行です。

ESP.deepSleep(120e6, WAKE_RF_DEFAULT);   // 120 秒 = 2 分

単位はマイクロ秒で、e6 は「×100 万」を意味します。

で⚠️ ここに配線が 1 本必要です。DeepSleep から自力で起きるために、スケッチを書き込んだ後GPIO2をGNDから抜いてRSTをGNDに落としてリセット完了後からGPIO16 と RST をジャンパー線で繋いでください。これがないと、寝たまま二度と起きません。ESP8266 の内部タイマーは GPIO16 にしか信号を出せないためです。

DeepSleep 用に GPIO16 と RST をジャンパー線で繋いだ ESP8266 のブレッドボード
GPIO16 と RST を繋ぐジャンパー線の拡大

DeepSleep から復帰すると、ESP は電源を入れ直したのと同じ状態で起動します。つまり setup() が頭から実行されます。だから本番スケッチは、処理を全部 setup() に書いて loop() を空にしてあります。

4.2 IP をどう決めるか — 何回かひっくり返った話

ここはAIとセッションして何度かひっくり返ったところでした。現場出動したESPには個別でスケッチないにIPアドレスをハードコーディングしていました。しかしブログ用に整理しているうちにAIから初心者を対象にするにはそれはやめた方が良いと繰り返し提案があり、MACアドレス由来を選択しています。

最初は DHCP にしていました。Pi 側の dnsmasq に MAC アドレスを登録しておけば、チップごとに決まった IP が降ってくる。スケッチは全チップ共通のままでいい。きれいな設計に見えました。

AI に「DHCP と静的 IP で電池の持ちはどれくらい違うか」と聞くと、「起動時間の差は 100ms 程度、誤差レベル」という答えでした。私もそうなのか思って DHCP のまま進めました。

結果、電池が数日でなくなりました。

方式電池寿命 (単三 3 本、2 分間隔)
静的 IP1 か月以上
DHCP2 週間も持たない

原因は DHCP の折衝でした。DISCOVER → OFFER → REQUEST → ACK の 4 往復に、1 回あたり 3〜5 秒かかります。2 分間隔なら 1 日 720 回。1 日あたり 60mAh 近い上乗せです。単三 3 本の実効容量が 2000〜2500mAh ですから、月に 1800mAh。総容量の 7〜9 割を DHCP の待ち時間だけで食っていました。

「誤差レベル」という推測が、実機の前で完全に間違っていたわけです。この連載で AI を使い倒しておいてなんですが、電池の持ちのような物理的な話は、推測ではなく測るしかありません。

4.3 それでも per-chip 書換えはしたくない

静的 IP に戻すとして、チップごとにスケッチの IP を書換えるのは避けたい。10 台作ったら 10 種類のスケッチを管理することになり、必ず取り違えます。

そこで採った方法が、MAC アドレスから IP を計算するというものです。

uint8_t mac[6];
WiFi.macAddress(mac);

// MAC の下位 1 バイトから IP の第 4 オクテットを作る
// (0x7F でマスクして 0〜127、+100 で 100〜227)
IPAddress fixedIP(192, 168, 4, 100 + (mac[5] & 0x7F));
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);
WiFi.config(fixedIP, gateway, subnet);

MAC アドレスはチップごとに物理的に違うので、同じスケッチを何台に焼いても、それぞれ違う IP を名乗ります。しかも DHCP の折衝は発生しません。

同じ考えで device_id も MAC から作ります。

char deviceId[16];
snprintf(deviceId, sizeof(deviceId), "ESP-%02X%02X%02X",
         mac[3], mac[4], mac[5]);
// MAC が 84:CC:A8:A1:B2:C3 なら → "ESP-A1B2C3"

これで スケッチは全チップ完全に同じもの、コピペで焼くだけになりました。

注意: この方法は「MAC の下位 1 バイトが 128 通りしかない」ことに依存しています。理屈のうえでは、下位バイトの下 7 ビットが偶然一致する 2 個体を掴むと IP がぶつかります。数台規模なら実用上問題ありませんが、台数が増えたら Pi 側のログで IP の重複を確認してください。

4.4 電池の残量を知る — 実測で分かった「測定上限」

実測しました。そして「新品の単三 3 本は測れない」ことが分かりました。 分圧回路を組んでテスターと突き合わせた結果です。

テスターで測った電池電圧4.6 V
ダッシュボードの表示4.30 V
この回路の測定上限A0 の 1.0V × 分圧比 4.3 = 4.3 V

ADC が 1023 で振り切れています。 4.3 V を超えた入力はすべて 4.30 と表示されます。電池が 4.6 V でも 5.0 V でも同じです。

この記事を最初に書いたとき、私は「新品単三 3 本なら 4.6V 位出るのでアナログ入力の範囲を超えてしまう」と書きました。それが実測で裏付けられたことになります。推測で書いた懸念が、そのまま観測結果になりました。

対処は 3 通りあります。

やり方測れる範囲向き不向き
そのまま使う4.3 V 以下電池が減って 4.3 V を割ってからは正しく読めます。「そろそろ切れる」を知るのが目的なら実害は小さい
分圧比を上げる (470kΩ + 100kΩ)5.7 V まで新品から測れます。ただし分解能は粗くなります
電池構成を変える (単三 2 本 + 昇圧)3.0 V 前後回路が増えます。ここでは扱いません

私はそのまま使っています。 満充電付近の絶対値が知りたいわけではなく、切れる前に気づければ十分だからです。ただし 4.30 と出ている間は「4.3 V 以上」としか言えない、と分かった上で使う必要があります。

現地に行かずに「そろそろ電池が切れる」と分かるように、電源電圧も一緒に送る設計にしています。ESP8266 の A0 ピンは 0〜1.0V しか測れないので、抵抗 2 本で分圧して電圧を下げます。以下がその回路と換算式です。

電池+ ── 330kΩ ──┬── A0
                   │
                 100kΩ
                   │
                  GND
int adcValue = analogRead(A0);
float voltage = (adcValue / 1023.0) * 4.3;   // 分圧比 (330+100)/100 = 4.3
                                             // ↑ 4.3 が測定上限。電池 4.6V では 4.30 で頭打ち (実測)

実際では新品単三3本直列なら電池4.6V位の出力が有るのでアナログ入力の電圧の共用範囲を超えてしまうが、私の環境ではなんとか問題ない様です。そもそも必要な電圧電流を供給できる電圧を仮に4.0Vとして4.0V~4.6Vを分圧したら0.93~1.06Vなので誤差の範囲だ。電池の能力(容量)にもよるので思いついたけど、現在はおまけみたいな機能でしかないので誰か良いアイデアが有れば教えていただきたい。今回の記事ではブレッドボードには差していません。

5. 本番スケッチ

上の 4 つを全部入れたものが以下です。これが実際に現場で動いているスケッチそのものです。SSID とパスワードの 2 行以外、書換えるところはありません。

ただし 4.4 のとおり、電源電圧には測定上限があります。 分圧回路は結線して実測済みですが、電池が新品のうちは 4.30 で頭打ちになります。battery_mode は「残量が少ない」の意味で、BATTERY_LOW_V を下回ったときに 1 になります。この 3.3V は私が決めた運用値であって、実測ではありません。 電池を使い切ったときにどう出るかのデータをまだ取っていないので、正直なところどう出るかは分かりません。自分の電池構成で 1 度使い切って決め直してください。

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <ArduinoJson.h>

#define ONE_WIRE_BUS 4           // DS18B20 データピン
#define POWER_SOURCE_PIN A0      // 電源検出 (330kΩ + 100kΩ 分圧)

// 測定周期。単位マイクロ秒、e6 = ×100万
//   30 秒 → 30e6 / 1 分 → 60e6 / 2 分 → 120e6 / 5 分 → 300e6
//   ※ ESP8266 の DeepSleep 上限は約 71 分
#define DEEP_SLEEP_INTERVAL 120e6

// ★書換えるのはこの 2 行だけ
const char* apSSID = "YOUR_AP_SSID_HERE";
const char* apPassword = "YOUR_AP_PASSWORD_HERE";

const char* serverURL = "http://192.168.4.1:5000/api/temperature";
const int HTTP_TIMEOUT = 5000;

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
WiFiClient wifiClient;

void setup() {
    Serial.begin(115200);
    delay(100);
    delay(500);

    // ===== device_id を MAC から自動生成 =====
    uint8_t mac[6];
    WiFi.macAddress(mac);
    char deviceId[16];
    snprintf(deviceId, sizeof(deviceId), "ESP-%02X%02X%02X",
             mac[3], mac[4], mac[5]);

    // ===== 温度取得 =====
    sensors.begin();
    sensors.setResolution(12);
    sensors.requestTemperatures();
    delay(800);

    float temp = sensors.getTempCByIndex(0);
    if (temp == DEVICE_DISCONNECTED_C || temp == -127.0 || temp == 85.0) {
        temp = -999.0;
    }

    // ===== 電源電圧 =====
    int adcValue = analogRead(POWER_SOURCE_PIN);
    float voltage = (adcValue / 1023.0) * 4.3;
    // 「残量が少ない」の判定。第7回の XIAO 版と意味を揃えています。
    // 3.3V は運用上の判断値で、実測ではありません。電池を 1 セット
    // 使い切って、実際に止まる電圧を見てから決め直すのが確実です。
    const float BATTERY_LOW_V = 3.3;
    bool isBatteryLow = (voltage < BATTERY_LOW_V);

    // ===== WiFi 接続 (MAC 由来の静的 IP) =====
    WiFi.persistent(false);              // flash 書込みを省略
    WiFi.mode(WIFI_STA);
    WiFi.setPhyMode(WIFI_PHY_MODE_11N);  // 接続を速くする

    IPAddress fixedIP(192, 168, 4, 100 + (mac[5] & 0x7F));
    IPAddress gateway(192, 168, 4, 1);
    IPAddress subnet(255, 255, 255, 0);
    WiFi.config(fixedIP, gateway, subnet);
    delay(100);
    WiFi.begin(apSSID, apPassword);

    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        attempts++;
    }

    // ===== 送信 =====
    if (WiFi.status() == WL_CONNECTED && temp != -999.0) {
        StaticJsonDocument<384> doc;
        doc["device_id"] = deviceId;
        doc["name"] = deviceId;
        doc["temperature"] = round(temp * 100) / 100.0;
        doc["temp"] = round(temp * 100) / 100.0;
        doc["ip_address"] = WiFi.localIP().toString();
        doc["voltage"] = round(voltage * 100) / 100.0;
        doc["battery_mode"] = isBatteryLow ? 1 : 0;
        doc["rssi"] = WiFi.RSSI();
        doc["signal_strength"] = WiFi.RSSI();

        String payload;
        serializeJson(doc, payload);

        HTTPClient http;
        http.setTimeout(HTTP_TIMEOUT);
        http.begin(wifiClient, serverURL);
        http.addHeader("Content-Type", "application/json");
        int httpCode = http.POST(payload);
        http.end();
    }

    // ===== DeepSleep =====
    delay(100);
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    delay(500);

    // GPIO16 → RST の配線が必須
    ESP.deepSleep(DEEP_SLEEP_INTERVAL, WAKE_RF_DEFAULT);
}

void loop() {
    // DeepSleep 環境では使いません
}

このスケッチはシリアルに何も出しません。省電力のためです。手元で動きを見たいときは、GitHub にある ESP8266_DeepSleep_FixedIP_Sensor_debug.ino を使ってください。各段階の値をシリアルに出す版です。

DeepSleep 版スケッチで約 2 分おきに POST が届いているログ

結果は約二分おきにPOSTできています。temperature と temp、rssi と signal_strength のように同じ値を 2 つの名前で送っているのは、サーバー側の古い実装との互換のためです。無駄に見えますが、片方だけにすると受け取れない組合せが出るので残してあります。


6. アドレス設計の答え合わせ

第4回で「.100〜.227 は ESP 用に空けておく、dnsmasq のプールは .228〜.254 だけ」と決めました。その理由がここで回収されます。

範囲誰が使うか決め方
192.168.4.1Pi 自身 (wlan1)固定
192.168.4.100〜227ESP センサーESP が MAC から自分で計算して名乗る
192.168.4.228〜254スマホ等の一時接続dnsmasq が配る

ESP は「Pi に IP をもらう」のではなく「自分で決めて名乗る」ので、Pi 側は ESP の存在を事前に知らなくて構いません。だから新しいチップを足すときも、Pi 側の設定変更が一切要らないのです。

今回のテストスケッチ (最小版) は WiFi.config を使っていないので、DHCP で .228〜.254 のどれかをもらいます。本番スケッチに切替えると .100〜227 に移ります。同じチップでも IP が変わるので、驚かないでください。


7. よくある失敗

症状原因対処
[WiFi] 接続できませんでしたSSID かパスワードが hostapd.conf と違うPi で sudo grep -E "^(ssid|wpa_passphrase)=" /etc/hostapd/hostapd.conf を実行して照合。プレースホルダのままになっていないか確認
接続はするが IP=0.0.0.0dnsmasq が動いていないPi で systemctl is-active dnsmasq。第4回 §5 を確認
[POST] 失敗: connection refusedPi 側の受信スクリプトが起動していないpython3 ~/recv_test.py の画面が待ち受け状態か確認
[POST] 失敗: connection lost / タイムアウト電波が弱い、または Pi が遠いWiFi.RSSI() の値を見る。-80 を下回るなら距離か遮蔽物の問題
HTTP 404宛先の綴り間違い/api/temperature か確認。/api/sensor は別のエンドポイント
HTTP 400 device_id requiredJSON に device_id が入っていない必須項目は device_id と temperature の 2 つ
コンパイルで ArduinoJson.h: No such fileライブラリ未導入ライブラリマネージャで ArduinoJson (Benoit Blanchon) を導入
DeepSleep から起きてこないGPIO16 と RST が繋がっていないジャンパー線 1 本で繋ぐ。本番版で最も多い詰まりどころ
起きるが毎回 WiFi 接続に失敗する電源の瞬間的な電圧降下第1回で扱った 100µF 電解コンデンサを電源ラインに。電池駆動では特に効く
検出数=1 なのに温度が -127センサーは見えているが、読み取ったデータの検査 (CRC) に失敗している。電源が最有力下の「補足」へ。まず 検出数 と 生の値 を分けて表示して切り分ける
温度が -999 で送られてくるセンサーが読めていない第1回のトラブルシューティングへ。4.7kΩ プルアップを確認

補足: 「検出数=1 なのに -127」

ここは実録です。読み飛ばしても本題には影響しません。ただ、同じ穴に落ちる人が必ずいると思うので残します。

最小スケッチを動かしたとき、こういう出方をしました。

[DS18B20] 検出数=1  生の値=32.75      ← 1 回目は成功
[POST] HTTP 200
[DS18B20] 検出数=1  生の値=-127.00     ← 2 回目から、ずっとこれ
[DS18B20] 検出数=1  生の値=-127.00
[DS18B20] 検出数=1  生の値=-127.00

最初は「loop関数内で最後にOneWireの利用宣言がリセットされてる?」と思いました。違いました。

-127 は「センサーが無い」ではない

-127 は DEVICE_DISCONNECTED_C という定数の値で、名前のせいで「切断」と読めますが、実際の意味は「読み取ったデータの検査 (CRC) に失敗した」です。

この 2 つは全然違います。前者ならセンサーや配線を疑いますが、後者は電源・接触・通信タイミングのどれでも起こります。症状だけからは絞れません。

しかも 検出数=1 は出ています。センサーの ID 探索は成功しているのに、温度の読み取りだけ失敗している。この非対称が手がかりでした。

やったこと

  1. 検出数 と 生の値 を分けて表示するようにした。ここが出発点です。元のスケッチは「センサーが読めません」としか出しておらず、それでは何を疑えばいいかすら決まりません
  2. WiFi を完全に切って、センサーだけを回した → 13 回連続で成功
  3. WiFi は繋いだまま、POST だけ止めた → 成功。POST は無罪
  4. 失敗したときと同一のコードに戻した → 11 回連続で成功してしまった

4 で再現しなくなりました。コードは無罪、POST も無罪。残った差分は電源だけです。切り分けの途中で、電源ボードを繋いでいたのでした。

結論と、書けないこと

電源が原因だった可能性が高い、というのが妥当な読みです。第1回で「USB シリアル変換の 3.3V は数十 mA しか出せない」と書きましたが、WiFi の送信中はさらに厳しくなります。DS18B20 の温度変換は 1.5mA を要求し、そこで電圧が落ちれば読み取りが化けます。

ただし断定はしません。最初に失敗したときの電源構成を記録していなかったので、証明できないからです。事実として言えるのは「電源を補強したら再現しなくなり、コードを元に戻しても再現しない」ここまでです。

教訓は 2 つ

① 「読めない」ときは、まず情報を分けて出す。

検出数(ID 探索は通ったか)と 生の値(何が返ってきたか)を分けるだけで、疑う場所が半分になります。エラーメッセージを 1 行にまとめてしまうと、この分岐が消えます。

② 何を変えたか記録する。

私は電源ボードを繋いだことを忘れて、コードの側を疑い続けました。切り分けの途中で 2 つ以上を同時に変えると、その回のデータは価値を失います。「電源を変えた」「配線を触った」「コードを直した」を、変えるたびにメモしてください。

AI に相談するときも同じです。「動きません」だけでは何も返ってきません。「昨日は動いていた。今日は動かない。その間に変えたのは A と B」まで言えて、はじめて相手も考えられます。


AI との会話例

今回の範囲で AI に聞くなら、こういう聞き方が通ります。

ESP-WROOM-02 (ESP8266) から、192.168.4.1:5000 の Flask に JSON を HTTP POST したい。DS18B20 の温度を {"device_id":"...","temperature":25.5} の形で送る。ライブラリは ArduinoJson 6 系。WiFi 接続のタイムアウトも入れた最小のスケッチを書いて。

送りたい JSON の形を実例で示すのがコツです。「温度を送りたい」だけだと、AI は勝手に項目名を決めます。そのスケッチをサーバー側に繋ぐと 400 Bad Request になって、原因探しに時間を使うことになります。

もう 1 つ、今回の DHCP の件で学んだことがあります。

AI は「どちらが速いか」は答えられますが、「1 か月後に電池がどうなっているか」は答えられません。あのとき返ってきた「誤差レベル」という言葉は、論理としては間違っていませんでした。起動 1 回あたりの差は確かに小さい。しかしそれが 1 日 720 回、30 日分積み上がった結果までは、聞かれた範囲の外だったわけです。

聞き方を変えるべきでした。「DHCP と静的 IP どちらが速い?」ではなく、「2 分間隔で 1 か月動かしたとき、消費電力量の差は何 mAh になるか。計算過程も見せて」と聞いていれば、たぶん答えは違っていました。

AI にコードを頼むときの 2 つのコツ

今回のデバッグを通して、はっきり効いたことが 2 つあります。どちらも頼み方の問題です。

① 「変更箇所だけ」を受け取らない

AI に「ここを直して」と頼むと、気を利かせて差分だけ返してきます。「setup() の中の、この行の直前に以下を追加してください」という形です。これが曲者でした。

受け取った側は、その断片がどの階層に入るのかを自分で判断しなければなりません。私は関数の定義を setup() の中に貼ってしまい、こんなエラーを出しました。

error: a function-definition is not allowed here before '{' token

C++ は関数の中に関数を書けません。しかし断片だけ見ていると、そんなことは分かりません。括弧の対応もインデントも、目で追って合わせることになります。

最初にこう頼んでください。

変更箇所だけでなく、その関数(このケースではsetup関数…void setup() {}でくくられている所)を丸ごと出してください。長くなっても構いません。

スケッチ全体が 200 行以下なら、いっそ「ファイル全体を出して」で構いません。コピペは全消し → 全貼りが一番安全です。実際、私は断片で受け取った回はコンパイルエラーになり、ファイル丸ごとで受け取った回は一発で通って結果まで出ました。

トークンがもったいない気がしますが、エラー 1 回のやり取りの方がずっと高くつきます。

② 改造する前に「名前を付けて保存」

Arduino IDE は、開いているファイルをそのまま上書きします。動いているスケッチを試しに改造すると、元が消えます。

WINDOWSでARDUINO IDEを起動すると前回使って終了していたスケッチが起動するので、新しいコードをうっかりコピペしてファイル名が前回のまま上書き、という事を数知れず繰り返しました。

私は本番用のスケッチをテストコードで潰しました。DeepSleep も静的 IP も電池電圧の処理も、全部消えました。たまたまバックアップがあったので戻せましたが、無ければ作り直しです。

改造する前に ファイル → 名前を付けて保存 で別名にしてください。Arduino IDE は「スケッチ名 = フォルダ名」なので、名前を変えれば別フォルダになり、元は無傷で残ります。

本来は Git を使う場面です。連載の最終回で GitHub 公開を扱いますが、バックアップの手段としての Git は、もっと早く覚えた方がいいかもしれません。私のように青くなる前に。


この時点でできていること

  • ESP が Pi の AP に接続し、温度データを HTTP POST で送れる
  • Pi 側で受信内容を目視できる
  • DeepSleep で電池運用できる本番スケッチが手元にある
  • チップを増やしても、スケッチは書換え不要でコピペできる

まだできていないこと:

  • 受け取ったデータを保存する (第8回)
  • ブラウザでグラフとして見る (第9回)
  • デバイスに人間が読める名前を付ける (第8回)

今の受信スクリプトは画面に出すだけなので、Ctrl + C で止めればデータは消えます。ここから先が第8回の仕事です。


次回予告

第7回は もう 1 つの選択肢、ESP-NOW を扱います。今回使った WiFi + HTTP は、接続の確立に毎回 2〜3 秒かかります。ESP-NOW は WiFi の接続手順を省いて、数十ミリ秒で送りっぱなしにする方式です。電池はさらに長く持ちますが、受け側に中継用の ESP がもう 1 台必要になります。普通に考えればAP用USBWifiドングルよりESPモジュールの方が安価なので最初からこれにすればと考える方もいると思います。しかしArcher T2U Plusはアンテナが長く、実用的な電波の到達距離を考えて選択肢を残せるように作っています。


← 第5回 | 連載目次 | 第7回 →

コメント

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