ESP32 で取得したセンサーデータを AWS に送って可視化する、という個人プロジェクトを始めました。
この記事はその第1回です。開発環境を構築して、ビルドが通るところまでを扱います。まだセンサーも AWS も出てきません。地味な回ですが、ここでハマると先に進めないので丁寧に書きます。
目次
使用するボード
Freenove の ESP32-WROOM Board(型番 FNK0090) を使います。2枚入りのパックです。

搭載チップは無印の ESP32(ESP32-WROOM-32)で、Wi-Fi と Bluetooth を内蔵しています。AWS にデータを送るのに追加のモジュールが要らないのが選定理由です。
公式チュートリアルは docs.freenove.com/projects/fnk0090 にあり、C 言語版と Python 言語版の2系統が用意されています。
開発環境に PlatformIO を選んだ理由
ESP32 の開発環境にはいくつか選択肢があります。今回は PlatformIO + Arduino framework を選びました。
| 選択肢 | 特徴 |
|---|---|
| PlatformIO + Arduino | VS Code 統合。設定ファイルで依存を固定でき、CLI で完結する |
| MicroPython + Thonny | REPL で即座に試せる。センサーの動作確認が速い |
| Arduino IDE | GUI 中心で初心者向け。設定がリポジトリに残らない |
| ESP-IDF | Espressif 公式 SDK。低レベルだが学習コストが高い |
決め手は再現性でした。
Arduino IDE はボード選択もライブラリ導入も GUI 操作なので、その設定がリポジトリに残りません。数ヶ月後に別のマシンで開いたとき、何のライブラリをどのバージョンで入れたか分からなくなります。PlatformIO は platformio.ini という設定ファイルにボード定義もライブラリのバージョンも書けるので、git で管理できます。
もうひとつは、後で AWS IoT Core に MQTT over TLS で接続する予定があることです。この用途は Arduino のライブラリ(PubSubClient + WiFiClientSecure)の事例が圧倒的に多く、詰まったときに情報を探しやすいと判断しました。
PlatformIO Core の導入
macOS なので Homebrew で入れます。
brew install platformio
これで pio コマンドが使えるようになります。
$ pio --version
PlatformIO Core, version 6.2.0
プロジェクトの作成
このプロジェクトは最終的に AWS 側の Terraform コードも持つことになります。そのため、リポジトリ直下をいきなり PlatformIO プロジェクトにせず、firmware/ という階層を切りました。
esp32/
├── firmware/ ESP32 ファームウェア(PlatformIO)
├── infra/ Terraform(AWS 側。今後作成)
└── docs/ 作業記録
--board esp32dev が ESP32-WROOM-32 の標準的なボード定義です。Freenove 専用の定義は PlatformIO に無いので、この汎用定義を使います。
mkdir -p firmware
pio project init --project-dir firmware --board esp32dev
初回はツールチェーンとフレームワークを丸ごとダウンロードするので時間がかかります。最終的に ~/.platformio が 1.5GB ほどになりました。 ディスクの空きに注意してください。
導入されたのは以下の4つです。
toolchain-xtensa-esp32— ESP32 向けのコンパイラframework-arduinoespressif32— Arduino フレームワーク本体tool-esptoolpy— 書き込みツールtool-scons— ビルドシステム
platformio.ini の設定
生成された platformio.ini は最低限の内容なので、いくつか追記しました。
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
; シリアル速度。src/main.cpp の Serial.begin() と一致させること
monitor_speed = 115200
; 書き込み速度。不安定な場合は 460800 や 115200 に下げる
upload_speed = 921600
; 例外発生時のスタックトレースをデコードして表示する
monitor_filters = esp32_exception_decoder
特に monitor_filters = esp32_exception_decoder は入れておくべきです。これが無いと、プログラムがクラッシュしたときにシリアルモニタには16進数のアドレスが並ぶだけで、何が起きたのか分かりません。この設定を入れると、アドレスをソースコードの行番号に変換して表示してくれます。
動作確認用のスケッチ
まずはボードが生きているかを確認するだけのコードを書きます。LED を点滅させつつ、チップの情報をシリアルに出力します。
#include <Arduino.h>
// オンボード LED。点灯しない場合は基板のシルク印刷を確認して調整する
constexpr uint8_t LED_PIN = 2;
void setup() {
Serial.begin(115200);
delay(1000); // シリアルモニタの接続待ち
pinMode(LED_PIN, OUTPUT);
Serial.println();
Serial.println("=== ESP32 起動 ===");
Serial.printf("チップモデル : %s (リビジョン %d)\n", ESP.getChipModel(), ESP.getChipRevision());
Serial.printf("CPU コア数 : %d\n", ESP.getChipCores());
Serial.printf("Flash サイズ : %d MB\n", ESP.getFlashChipSize() / (1024 * 1024));
Serial.println("==================");
}
void loop() {
digitalWrite(LED_PIN, HIGH);
Serial.println("LED: ON");
delay(1000);
digitalWrite(LED_PIN, LOW);
Serial.println("LED: OFF");
delay(1000);
}
setup() の冒頭に delay(1000) を入れているのがポイントです。ESP32 は起動が速いので、これが無いとシリアルモニタが接続する前に起動メッセージが流れてしまい、何も表示されないように見えます。
ビルド
pio run -d firmware
通りました。
RAM: [= ] 6.6% (used 21464 bytes from 327680 bytes)
Flash: [== ] 20.7% (used 271045 bytes from 1310720 bytes)
Successfully created esp32 image.
========================= [SUCCESS] Took 4.06 seconds =========================
LED を光らせるだけのコードで Flash を 20.7% 使っています。Arduino フレームワークと Wi-Fi スタックが丸ごと含まれるためで、これは想定内です。
ハマったところ
コンテナの中から書き込みはできない
私は普段、開発用のスクリプトはすべてコンテナ(podman)の中で実行するルールにしています。環境依存を排除するためです。今回もそうしようとして、これは無理だと分かりました。
macOS の podman は仮想マシン(applehv)の上で動いているため、USB シリアルデバイスをコンテナに渡せません。 ESP32 への書き込みもシリアルモニタも、USB デバイスへのアクセスが前提なので、原理的にホストで実行するしかありません。
ビルドだけコンテナ化して、生成された .bin をホストから書き込む、という折衷案も考えました。ただ、開発中は「コードを直す→書き込む→シリアルを見る」を何十回も繰り返します。ここに手順が1段増えるのは地味に効いてきます。
結局、ファームウェアの領域についてはホスト実行を許容することにしました。代わりに再現性は platformio.ini でのバージョン固定によって担保します。AWS 側の Terraform や Lambda は従来どおりコンテナで実行します。
この判断とその理由はプロジェクトの CLAUDE.md に書き残しました。あとで自分がなぜこうしたか忘れるので。
.gitignore でファイルが黙って消えていた
VS Code の推奨拡張を設定する .vscode/extensions.json はコミットしたいけれど、他の VS Code 設定は除外したい。そこでこう書きました。
.vscode/
!.vscode/extensions.json
これは動きません。 git はディレクトリ自体を除外した場合、その配下のファイルを否定パターンで再包含できない仕様です。ディレクトリを走査しないので、中のファイルに到達しないわけです。
正しくはこう書きます。
.vscode/*
!.vscode/extensions.json
.vscode/* なら「ディレクトリの中身」を除外するので、否定パターンが効きます。
厄介なのは、これがエラーにならないことです。git add しても何も言われず、ただ静かに無視されます。気づいたのは git check-ignore -v で確認したときでした。
$ git check-ignore -v .vscode/extensions.json
.gitignore:7:.vscode/ .vscode/extensions.json
意図せず除外されているファイルが無いか不安なときは、このコマンドが役に立ちます。
認証情報の置き場所を最初に決めておく
Wi-Fi のパスワードは、いずれ必ずコードに書くことになります。あとから対処すると履歴に残ってしまうので、最初に決めておきました。
実体は firmware/include/secrets.h に置き、.gitignore で除外します。代わりにテンプレートの secrets.h.example をコミットしておきます。
// secrets.h.example
#pragma once
#define WIFI_SSID "your-ssid"
#define WIFI_PASSWORD "your-password"
使うときはコピーして書き換えるだけです。
cp firmware/include/secrets.h.example firmware/include/secrets.h
現時点の構成
esp32/
├── CLAUDE.md プロジェクトの設計判断メモ
├── .gitignore
├── .vscode/extensions.json
└── firmware/
├── platformio.ini ボード定義とバージョン固定
├── src/main.cpp 動作確認用スケッチ
└── include/secrets.h.example 認証情報テンプレート
次回
ビルドは通りましたが、まだ実機に書き込んでいません。 ボードを開封していないためです。
次回は実際に USB で接続して、書き込みとシリアル出力の確認をします。ここでも pio device list にボードが出てこない、といった定番のトラブルが待っていそうです。
その先はセンサーを繋いでデータを取り、AWS へ送る部分に進みます。AWS 側の構成(IoT Core を使うか、API Gateway + Lambda にするか)はまだ決めていないので、そこも含めて記録していく予定です。