Wi-FiでNeoPixel LEDストリップを制御:ESP8266とAndroidアプリで炎エフェクト
コンポーネントと用品
| ElectroPeak ESP8266 Serial WiFi Witty Cloud Board | ×1 | |
| NeoPixel スマートLEDストリップ (60LED/m) | ×3 | |
| ElectroPeak ロジックレベルコンバーター | ×1 | |
| ElectroPeak 21cm 40P オス-メス ジャンパーワイヤー | ×1 | |
| PVCパイプ 60cm サイズ2インチ | ×1 | |
| トレーシングペーパー | ×1 | |
| ガラスシリンダー | ×1 | |
| 5V電源 (9A以上) | ×1 | |
![]() |
Arduino Nano R3 | ×1 |
必要な工具と機械
![]() |
ホットグルーガン(汎用) |
![]() |
はんだごて(汎用) |
アプリとオンラインサービス
![]() |
Arduino IDE |
このプロジェクトについて
このチュートリアルや他の素晴らしいチュートリアルは、ElectroPeakの公式サイトで読めます。
Wi-Fi無線制御でクールな炎シミュレーション効果を作ります。見た目の良いインターフェースを持つAndroidスマホ用モバイルアプリがインストール済みで、あなたの作品で遊べます!ArduinoとESP8266を使って炎を制御します。このプロジェクトの最後に以下を学べます:
- NeoPixelの仕組み
- ESP8266のプログラミングとWi-Fi経由での変数制御
- NeoPixelでクールな炎エフェクトを作る方法
NeoPixelの紹介
個別にアドレス指定可能なLED、通称NeoPixelはしばらく前から存在しています。ご存知ない方のために説明すると、通常のRGB LEDのようですが、名前の通り各LEDの色を個別に制御できるため、無限にクールなパターンやアニメーションが作れます。WS2812Bなら電源2本とデータ1本の計3本のワイヤーだけで、Arduinoの空きピン1本で大量のLEDを制御できます!
このプロジェクトでは、これらのスマートLEDで炎エフェクトを作ります。LED制御には素晴らしいFastLEDライブラリを使います。Mark Kriegsman氏作のライブラリ例「Fire2012」スケッチを使用します。30個のLEDを持つ6本のストリップ(合計180個)を使い、PVCパイプに貼り付けてガラスシリンダー(通常は花瓶として使われる)に入れます。LEDの光を拡散して連続的に見せるため、光を通して拡散するトレーシングペーパーを使いました。

構築
まず適切なガラスシリンダーを用意します。 oursは長さ60cm、直径12cmです。
フロストガラスのシリンダーが見つかればベストですが、透明ガラスの場合はトレーシングペーパーでシリンダー表面(内側でも外側でも)を覆えば、光をうまく拡散して良い結果が得られます。
ガラスシリンダーの内部長さを測り、PVCパイプをそれに合わせてカットします。 oursは高さ60cm(底を除く内部長さ59cm)なので、PVCパイプを59cmにカットしました。このパイプにLEDストリップを貼り付けます。直径4cmのパイプが最適です。
次にLEDストリップを6等分にカットします。ここでは60LED/m密度のストリップを使用(より高密度ならさらに良い効果)し、50cmを6本使うので合計3メートル必要です。6本をPVCパイプに等間隔で配置し貼り付けます。見た目はこんな感じです。



LEDストリップ同士を接続するには、以下の図のようにストリップに直接ワイヤーをはんだ付けするか、まずピンヘッダーをストリップにはんだ付けしてからブレッドボードワイヤーで接続します。

全てのLEDストリップ接続が完了したら、パイプをシリンダー内に配置します。パイプをシリンダー内で中央に固定するには、ガラスシリンダーの内径と同じ外径、PVCパイプの外径と同じ内径の円形にカットした発泡スチロールを使います。これを両端用に2つ用意し、パイプの両端に取り付けてから優しくシリンダー内に入れます。
コード
Arduino IDEでコーディングし、ESP8266へアップロードします。SPIFFSにコントローラソフトウェアファイルをアップロードするには、3MBのSPIFFSを持つESP8266搭載ボードが必要です。SPIFFSは「Serial Peripheral Interface Flash File System」の略で、ここにコントローラファイルをアップロードしてそこからファイルを提供できます。これでブラウザ(スマホやノートPC)を開き、ESPのアドレス(デフォルトは192.168.4.1)にアクセスすると、アプリをインストールしなくてもブラウザでコントローラインターフェースが表示されます。iPhoneやiPadの場合はこれが唯一の選択肢になります。
以下のスケッチをESPボードにアップロードしてください。FastLEDライブラリが必要なので、まだ追加していない場合はArduino IDEに追加してください(こちらからダウンロード可能)。炎シミュレーションコードはMark Kriegsman氏のfire2012スケッチで、ライブラリの例にあります。この例は1本のLEDストリップ用ですが、ここでは可変本数のストリップに対応するようコードを修正しました。ストリップ/LEDの本数が多いほど効果は大きくなります。
炎シミュレーションのロジックは例ファイルで明確に説明されています。仕組みを知りたい方は例のソースコードを読んでみてください。
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include "FastLED.h"
#include "EEPROM.h"
#include "FS.h" // SPIFFS用
#define DATA_PIN 5
#define LED_TYPE WS2811
#define COLOR_ORDER GRB
#define NUM_LEDS 30
#define NUM_STRIPS 6
#define CHIPSET WS2812B
// EEPROMにデータを保存して炎シミュレーションの状態を保持するアドレス
#define cs0Adr 0
#define cs1Adr 3
#define cs2Adr 6
#define cs3Adr 9
#define BriAdr 15
#define FpsAdr 16
#define SparkingAdr 17
#define CoolingAdr 18
#define EEPROMCheckAdr 20 // この値が250なら以前にEEPROMに保存済みとみなし、そこからデータを読み込む
CRGB leds[NUM_STRIPS * NUM_LEDS];
String inData;
uint8_t FPS = 100;
uint8_t SPARKING = 150;
uint8_t COOLING = 90;
uint8_t BRIGHTNESS = 100;
uint8_t csRGB[4][3] = {{0, 0, 0}, {255, 0, 0}, {255, 127, 0}, {255, 255, 255}};
unsigned long previousMillis = 0;
bool change = false;
unsigned long changeMillis = 0;
bool initSetup = true;
CRGBPalette16 gPal;
ESP8266WebServer server(80);
void setup() {
EEPROM.begin(200);
cWiFi();
setupFastLED();
loadConfig();
gPal = CRGBPalette16(
CRGB(csRGB[0][0],csRGB[0][1],csRGB[0][2]),
CRGB(csRGB[1][0],csRGB[1][1],csRGB[1][2]),
CRGB(csRGB[2][0],csRGB[2][1],csRGB[2][2]),
CRGB(csRGB[3][0],csRGB[3][1],csRGB[3][2])
);
}
inline void setupFastLED() {
delay(1000);
FastLED.addLeds<CHIPSET, DATA_PIN, COLOR_ORDER>(leds, NUM_STRIPS * NUM_LEDS).setCorrection(TypicalLEDStrip);
FastLED.setBrightness(BRIGHTNESS);
}
void loop() {
server.handleClient();
if (change) {
if (millis() - changeMillis > 60000) {
change = false;
saveToEEPROM();
}
}
fire();
FastLED.show();
FastLED.delay(1000 / FPS);
}
void Fire2012WithPalette(int stripNo) {
static byte heat[NUM_STRIPS][NUM_LEDS];
// Step 1. 各セルを少し冷やす
for(int i = 0; i < NUM_LEDS; i++) {
heat[stripNo][i] = qsub8(heat[stripNo][i], random8(0, ((COOLING * 10) / NUM_LEDS) + 2));
}
// Step 2. 各セルから熱が「上」へ漂い、少し拡散
for(int k = NUM_LEDS - 1; k >= 2; k--) {
heat[stripNo][k] = (heat[stripNo][k-1] + heat[stripNo][k-2] + heat[stripNo][k-2]) / 3;
}
// Step 3. 底付近にランダムに新しい「火花」を発生
if(random8() < SPARKING) {
int y = random8(5);
heat[stripNo][y] = qadd8(heat[stripNo][y], random8(160,200));
}
// Step 4. 熱セルからLED色へマッピング
for(int j = 0; j < NUM_LEDS; j++) {
byte colorindex = scale8(heat[stripNo][j], 240);
CRGB color = ColorFromPalette(gPal, colorindex);
leds[j + stripNo * NUM_LEDS] = color;
}
}
void fire() {
for(int i = 0; i < NUM_STRIPS; i++) {
Fire2012WithPalette(i);
}
}
int str2int(String InputStr) {
return InputStr.toInt();
}
boolean EveryNSec(uint8_t period) {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= period * 1000) {
previousMillis = currentMillis;
return true;
}
return false;
}
void EEPROMupdate(byte address, byte value) {
if (EEPROM.read(address) != value) {
EEPROM.write(address, value);
EEPROM.commit();
}
}
void saveToEEPROM() {
EEPROMupdate(BriAdr, BRIGHTNESS);
EEPROMupdate(FpsAdr, FPS);
EEPROMupdate(SparkingAdr, SPARKING);
EEPROMupdate(CoolingAdr, COOLING);
for (uint8_t i = 0; i < 4; i++) {
for (uint8_t j = 0; j < 3; j++) {
EEPROMupdate((i * 3 + j), csRGB[i][j]);
}
}
}
void handleCS0Change() {
csRGB[0][0] = str2int(server.arg("R"));
csRGB[0][1] = str2int(server.arg("G"));
csRGB[0][2] = str2int(server.arg("B"));
gPal = CRGBPalette16(
CRGB(csRGB[0][0],csRGB[0][1],csRGB[0][2]),
CRGB(csRGB[1][0],csRGB[1][1],csRGB[1][2]),
CRGB(csRGB[2][0],csRGB[2][1],csRGB[2][2]),
CRGB(csRGB[3][0],csRGB[3][1],csRGB[3][2])
);
changeMillis = millis();
change = true;
}
void handleCS1Change() {
csRGB[1][0] = str2int(server.arg("R"));
csRGB[1][1] = str2int(server.arg("G"));
csRGB[1][2] = str2int(server.arg("B"));
gPal = CRGBPalette16(
CRGB(csRGB[0][0],csRGB[0][1],csRGB[0][2]),
CRGB(csRGB[1][0],csRGB[1][1],csRGB[1][2]),
CRGB(csRGB[2][0],csRGB[2][1],csRGB[2][2]),
CRGB(csRGB[3][0],csRGB[3][1],csRGB[3][2])
);
changeMillis = millis();
change = true;
}
void handleCS2Change() {
csRGB[2][0] = str2int(server.arg("R"));
csRGB[2][1] = str2int(server.arg("G"));
csRGB[2][2] = str2int(server.arg("B"));
gPal = CRGBPalette16(
CRGB(csRGB[0][0],csRGB[0][1],csRGB[0][2]),
CRGB(csRGB[1][0],csRGB[1][1],csRGB[1][2]),
CRGB(csRGB[2][0],csRGB[2][1],csRGB[2][2]),
CRGB(csRGB[3][0],csRGB[3][1],csRGB[3][2])
);
changeMillis = millis();
change = true;
}
void handleCS3Change() {
csRGB[3][0] = str2int(server.arg("R"));
csRGB[3][1] = str2int(server.arg("G"));
csRGB[3][2] = str2int(server.arg("B"));
gPal = CRGBPalette16(
CRGB(csRGB[0][0],csRGB[0][1],csRGB[0][2]),
CRGB(csRGB[1][0],csRGB[1][1],csRGB[1][2]),
CRGB(csRGB[2][0],csRGB[2][1],csRGB[2][2]),
CRGB(csRGB[3][0],csRGB[3][1],csRGB[3][2])
);
changeMillis = millis();
change = true;
}
void handleConf() {
if (server.arg("brightness") != "") {
BRIGHTNESS = str2int(server.arg("brightness"));
FastLED.setBrightness(BRIGHTNESS);
changeMillis = millis();
change = true;
}
if (server.arg("fps") != "") {
FPS = str2int(server.arg("fps"));
changeMillis = millis();
change = true;
}
if (server.arg("sparking") != "") {
SPARKING = str2int(server.arg("sparking"));
changeMillis = millis();
change = true;
}
if (server.arg("cooling") != "") {
COOLING = str2int(server.arg("cooling"));
changeMillis = millis();
change = true;
}
server.sendHeader("Connection", "close");
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, "text/plain", "");
}
void loadConfig() {
if (EEPROM.read(EEPROMCheckAdr) == 250) {
BRIGHTNESS = EEPROM.read(BriAdr);
SPARKING = EEPROM.read(SparkingAdr);
COOLING = EEPROM.read(CoolingAdr);
FPS = EEPROM.read(FpsAdr);
if (FPS == 0) FPS = 100;
for (uint8_t i = 0; i < 4; i++) {
for (uint8_t j = 0; j < 3; j++) {
csRGB[i][j] = EEPROM.read(i * 3 + j);
}
}
} else {
EEPROMupdate(BriAdr, BRIGHTNESS);
EEPROMupdate(FpsAdr, FPS);
EEPROMupdate(CoolingAdr, COOLING);
EEPROMupdate(SparkingAdr, SPARKING);
for (uint8_t i = 0; i < 4; i++) {
for (uint8_t j = 0; j < 3; j++) {
EEPROMupdate((i*3+j), csRGB[i][j]);
}
}
EEPROMupdate(EEPROMCheckAdr, 250);
}
}
void cWiFi() {
WiFi.softAP("ElectroPeak's Flame", "");
// パスワードを設定する場合: WiFi.softAP("ElectroPeak's Flame", "12345678");
IPAddress myIP = WiFi.softAPIP();
server.on("/cs0", handleCS0Change);
server.on("/cs1", handleCS1Change);
server.on("/cs2", handleCS2Change);
server.on("/cs3", handleCS3Change);
server.on("/conf", handleConf);
server.serveStatic("/", SPIFFS, "/", "max-age=86400");
server.begin();
}
炎の「見た目と感触」を制御するには、SPARKINGとCOOLINGという2つの変数を操作します。これらはSPIFFSにアップロードしたコントローラソフトウェアやダウンロード可能なAndroidアプリで動的に制御できます。FPSもここで制御可能です。

炎の色はカラーパレットで制御され、これもコントローラソフトウェア(4つのカラーストップ)で変更可能です。各カラーストップを表す色の円をクリック/タップして色を設定し、閉じるを押すと変更が反映されます。

SPIFFSへのアップロード方法
Arduino IDEでファイルをSPIFFSメモリにアップロードするには、まずスケッチのフォルダ内に「data」というフォルダを作成し、アップロードしたい全てのファイルをそこに置きます。ここにアップロードされているファイルにはスケッチとこのフォルダの両方が含まれています。
次に、Arduino ESP8266 filesystem uploaderプラグインが必要です。GitHubページの手順に従ってプラグインをインストールしてください。インストールすると、ツールメニューの下にESP8266 Sketch Data Uploadが表示されます。ESPをプログラミングモードにしてそれをクリックしてください。ファイルのアップロードには少し時間がかかるので辛抱強く待ってください。注:「アップロード速度」を921600に設定すると速くなります。

仕組み
ESP8266ボードにアップロードされたスケッチはウェブサーバーを作成し、アプリから送信されるリクエストに応答します。アプリは単純にESP8266へGETリクエストを送信します。カラーパレット作成用の色データはGETリクエストの引数として送られ、SparkingやCoolingなどの他のパラメータも同様です。
例えば、輝度を設定するにはアプリから以下のリクエストが送られます:
https://192.168.4.1/conf?brightness=224
スケッチにはこのリクエストを処理するハンドラがあり、輝度を設定します。詳細はコードを見てください。
Androidアプリ
AndroidアプリはPhonegapで作成されています。HTML、CSS、JavaScriptなどのWeb技術を使ってクロスプラットフォームモバイルアプリを作れる技術です。ソースコードはこちらのページで入手できます。
このチュートリアルや他の素晴らしいチュートリアルは、ElectroPeakの公式サイトで読めます。
コード
- 炎エフェクトコード
- スケッチとDataフォルダ
炎エフェクトコード Arduino
// 上記と同じコード(省略)
スケッチとDataフォルダ Arduino
このzipファイルにはスケッチファイルとdataフォルダ(SPIFFSアップロード用)の両方が含まれています。
No preview (download only).
カスタムパーツとエンクロージャ
eps_flame_android_u5Zy5Bksvp.apk
製造プロセス
- Samsung SAMI、Arduino UNO、Raspberry Piで高速・信頼性の高い火災検知システムを構築
- 予算に優しいRaspberry Pi赤ちゃん泣き声検知器の自作 - DIYガイド
- スマート人数カウンターを作ろう:Androidアプリで制御
- DFRobot静電容量式指紋センサーをArduino/ESP8266で使う – 登録・認証・削除の完全ガイド
- ArduinoとAndroidでRoomba Create 2をリモコン操作 ─ Bluetooth経由のステップバイステップガイド
- Axiom Equipment Groupと地域スポンサー、Oxbow消防署に11,000ドルを寄付
- IoTを活用した早期山火事検知と予防
- 消防の革命:IoTが安全性、対応、救助をどう高めるか
- iOSとAndroid:エンタープライズ・アプリのセキュリティで優れるのはどちら?
- 火災被害と予防:命と財産を守る専門戦略
- Python strip()メソッド完全ガイド:実践例付き


