気の向くままに辿るIT/ICT/IoT
IoT・電子工作

ESP8266/ESP32 SPIFFSとLittleFS

ホーム前へ次へ
ESP8266って?

ESP8266/ESP32 SPIFFSとLittleFS

ESP8266/ESP32 SPIFFSとLittleFS

2022/08/13

 ESP8266で非推奨扱いとなったらしきSPIFFS使ってできていたはずのスケッチのコンパイルやアップロードに詰まったらLittleFSなど他のファイルシステムに移行するのが吉!?将来ESP32でも起こり得る!?という話。

 ESP8266やESP32のスケッチをコンパイルやアップロードしようとしたら、PCがフリーズ、時に再起動しないといけないほどCPUやRAMを消費しまくる事象に遭遇。

 えー、そら、メインと言う名のサブであるノートPCのCPUはCeleronで、かつデュアルコア、RAM4GBだよ...、じゃ、というわけでサブという名のメインRaspberry Pi 400(CPUクアッドコア/RAM4GB)で試しても同じ...もっとハイスペックなものを前提にしているわけじゃないよね...。

 一発めは、変わらないものの、これはこれでありかとccacheも導入、後に素晴らしい威力で爆速っぷりを発揮してくれ、超ウルトラスーパー思いっきり快適になったわけですが、一発めはcacheを有効にしようもなく、当然、フリーズの解決にはならない。

 ちなみにccacheの導入は、入っていなければ、sudo apt install -y ccache、ESP8266なら、/path/to/.arduino15/packages/esp8266/hardware/esp8266/3.0.2/platform.txtの[## Compile c files]/[## Compile c++ files]各行recipe.c.o.pattern=の右辺先頭にccache (スペース)を追記。

 現時点、そもそもArduino IDEが再利用できるものがあれば、見繕って良きに計らってくれているのでビルドパスは固定してもしなくても変わらないっぽい。

 それにしても何れも以前は何事もなくコンパイル、アップロード、SPIFFSへのdataフォルダ転送できており、実用していたもの。

 ESP8266はともかく、ESP32はごく最近もやっていたことなのになぜ?と思ったら、Arduino IDEのコンパイラ精度が向上したようで以前スルーしてくれていた怪しいコードをより厳格に対処するようになった模様でESP32については、怪しい箇所を修正(後掲スケッチのようにリモコン信号発信各関数のserver.send()行をコメントアウト)したところ改善。

 が、ESP8266の方は、同じ対策をとっても相変わらず。

 糸口を見つけようと任意にこれまでのスケッチをコンパイルしまくってみたところ、自作IoT化家電・スマート化家電を作り始めたばかりの頃に作った赤外線リモコン用のスマートリモコン化でIRRemoteESP8266.hとIRsend.hを使っているところをコメントアウトしまくれば起きないところまで絞り込め、これはライブラリではなく、自身の浅はかさ満載のスケッチでしょとハナから疑って修正した次第。

 いろいろやっているうち、フリーズ直前にArduino IDEの下部の窓をクリック、そこに表示されたメッセージ類を[Crtl]+[a]、[Crtl]+[c]でコピると、いつからかParceliteなるクリップボードアプリが入っていたおかげで復旧後、確認、すると警告の模様ながら以下を含むメッセージのコピー履歴が残っていました。

[SPIFFS has been deprecated. Please consider moving to LittleFS or other filesystems.]

 SPIFFSは非推奨になってるよ、そろそろ、LittleFSとか他のファイルシステム使うことも考えといてよ...。

 って一体いつの間に...。

 いろいろ情報を探してみるとSPIFFSは将来バージョンでなくなる可能性が高いよって話もある一方でメモリの少ないボードを使ってるなら今まで通りSPIFFSを、メモリたくさんあるならLittleFSほかを使っていこうって話もある。

 手持ちのボードのメモリ量、しっかり確認したことないけど、目の前のトラブル解消の唯一とも言える手がかかりは、この警告だし、新たな動きが出てきたなら時流に乗らないとねというのも半分、当初、ESP32には未対応だった模様も、ここに来てIDFコンポーネントベースのLittleFS_esp32なるライブラリもできてるみたいだし、ESP8266でやっとけばESP32に移行するのもより楽でしょ。

 ということでhttps://arduino-esp8266.readthedocs.io/en/latest/filesystem.htmlにたどり着き、ざっと美味しそうなところだけ超抜粋して読んでみました。

 そしたら、基本、最新アップローダをダウンロードしてArduinoのライブラリパスのtoolsディレクトリ(path/to/Arduino/tools/)以下にESP8266LittleFS/tool/esp8266littlefs.jarを展開してからArduino IDE開いて(既に起動済みなら再起動)、LittleFS.hをincludeしてSPIFFS.begin()とSPIFFS.open()のSPIFFSをLittleFSにしたスケッチと[ESP8266 LittleFS Data Upload]でdataフォルダをボードにアップロードすれば良いんだよーとある。

 なんだ!超簡単じゃーん!と思ったら、それだけじゃなかったじゃん...ということで意外とハマった話。

 ってか、結果オーライだったわけですが、このESP8266におけるSPIFFSから他への移行を促すメッセージについては、警告じゃなくてエラーだったの!?それとも対処している内、知らず知らずにバグ潰してた!?

 ちなみにLittleFSにするとファイルごとのファイルアロケーションユニットの単位は大きくはなるものの、SPIFFSより高速に読み書きできるし、SPIFFSと違ってディレクトリも作成できちゃったりするんだって。

ESP8266ボードとLittleFS

 まず、LittleFSで静的なHTMLファイルを表示する方法としてESPAsyncWebServerを使う方法ならあるものの、ESP8266WebServerを使う方法が見当たらない。

 そこでSPIFFSと同じ要領でよいんでしょと思い、[ESP8266 LittleFS Data Upload]してmDNS.localにアクセスしてもindex.htmlが表示されない...。

 mDNS.local/index.htmlってすれば表示される?と思いきや、そんなコンテンツないと怒られる。

 ちなみにIPも固定してあるのでIPアドレスでも同様、もちろん、dataフォルダには、相応の内容のindex.html、style.css、script.jsを入れてある。

 あれ?アップロードされてないの!?と思って古いバージョンのアップローダにしてみたら、esptool.pyにも影響のあるアップデートだったみたいで機能しない...。

 とりあえず、最新のアップローダに戻して再考...。

 .binファイルのイメージの中身を確認できるというlittlefs disk image viewer(littlefs-project / littlefs)を発見するも他は自動ながらブロックサイズは指定必須、ブロックサイズ...、そこ調べるのめんどくさい...。

 そこで素直にLittleFSの標準サンプルスケッチLittleFS_TimestampやSpeedTest、Read/Write/Deleteしてみたスケッチを試すと機能する...のでLittleFSにアップロードできていること、ボードが壊れておらず、正常であるらしきことは確認できた...。

 なのに、なぜ、index.htmlが表示されないのか...。

 もしかしてアップロード済みの静的ファイル名渡して読んで閉じてハイっ!じゃなく、昔ながらに一文字ずつ読んでいって、もしくは、行ごとに取得しつつ、最後にまとめてから、ゴソッと投げないとHTMLもCSSもJavaScriptも表示できないし、効かない!?

#include <ESP8266WiFi.h>
#include <ArduinoOTA.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <Arduino.h>
#include <FS.h>
#include <LittleFS.h>
 
const char *path_root = "/index.html";
const char *path_css = "/css.css";
const char *path_js = "/script.js";
 
const char *ssid     = "SSID";
const char *password = "PASSPHRASE";
 
IPAddress local_IP(192, 168, 0, 250);
IPAddress gateway(192, 168, 0, 1);
IPAddress subnet(255, 255, 255, 0);
//IPAddress primaryDNS(8, 8, 8, 8); //optional
//IPAddress secondaryDNS(8, 8, 4, 4); //optional
 
const char common_name[40] = "esptv";
const char *OTAName = common_name;
const char *mdnsName = common_name;
 
#define BUFFER_SIZE 16384
uint8_t buf[BUFFER_SIZE];
 
ESP8266WebServer server ( 80 );
IRsend irsend(14);
 
boolean readHTML() {
  //File htmlFile = SPIFFS.open(path_root, "r");
  File htmlFile = LittleFS.open(path_root, "r");
  if (!htmlFile) {
    Serial.println("Failed to open index.html");
    return false;
  }
  size_t size = htmlFile.size();
  if (size >= BUFFER_SIZE) {
    Serial.print("File Size Error:");
    Serial.println((int)size);
  } else {
    Serial.print("File Size OK:");
    Serial.println((int)size);
  }
 
  htmlFile.read(buf, size);
  htmlFile.close();
  return true;
}
 
void on_off() {
  //Serial.println("Power");
  irsend.sendPanasonic(0x***, 0x***0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***0x***);
  delay(2000);
  //server.send(200, "text/html", "Power ON/OFF");
}
void ch1() {
  //Serial.println("1ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "1ch");
}
void ch2() {
  //Serial.println("2ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "2ch");
}
void ch3() {
  //Serial.println("3ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "3ch");
}
void ch4() {
  //Serial.println("4ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "4ch");
}
void ch5() {
  //Serial.println("5ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "5ch");
}
void ch6() {
  //Serial.println("6ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "6ch");
}
void ch7() {
  //Serial.println("7ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "7ch");
}
void ch8() {
  //Serial.println("8ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "8ch");
}
void ch9() {
  //Serial.println("9ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "9ch");
}
void ch10() {
  //Serial.println("10ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "10ch");
}
void ch11() {
  //Serial.println("11ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "11ch");
}
void ch12() {
  //Serial.println("12ch");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "12ch");
}
void vol_up() {
  //Serial.println("Volume Up");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "Volume Up");
}
void vol_down() {
  //Serial.println("Volume Down");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "Volume Down");
}
void prog_list() {
  //Serial.println("Program List");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "Program List");
}
void prog_info() {
  //Serial.println("Program Info");
  irsend.sendPanasonic(0x***, 0x***);
  delay(10);
  irsend.sendPanasonic(0x***, 0x***);
  delay(2000);
  //server.send(200, "text/html", "Program Info");
}
 
void loadFile (String path) {
  String contents = "";
  String line = "";
  char c;
  File target_file = LittleFS.open(path, "r");
  if (target_file) {
    Serial.println("file open succeeded!");
    while (target_file.available()) {
      c = target_file.read();
      if (c == '\n' || c == '\r') {
        if (line.length() > 0) {
          contents = contents + line + '\n';
         }
        line = "";
      } else {
        line = line + String(c);
      }
    }
    if (line.length() > 0) {
      contents = contents + line;
    }
    target_file.close();
  } else {
    Serial.println("file open failed!");
  }
  if (path == path_root) {
    server.send(200, "text/html", contents);
  } else if (path == path_css) {
    server.send(200, "text/css", contents);
  } else if (path == path_js) {
    server.send(200, "text/javascript", contents);
  }
}
 
void handleRoot() {
  Serial.println("Access");
 
  loadFile("index.html");
 
  char message[20];
  String(server.arg(0)).toCharArray(message, 20);
  server.send(200, "text/html", (char *)buf);
 
  if (server.arg(0).indexOf("電源") != -1) {
    on_off();
  }
  else if (server.arg(0).indexOf("1ch") != -1) {
    //    Serial.println("1ch");
    ch1();
  }
  else if (server.arg(0).indexOf("2ch") != -1) {
    //    Serial.println("2ch");
    ch2();
  }
  else if (server.arg(0).indexOf("3ch") != -1) {
    //    Serial.println("3ch");
    ch3();
  }
  else if (server.arg(0).indexOf("4ch") != -1) {
    //    Serial.println("4ch");
    ch4();
  }
  else if (server.arg(0).indexOf("5ch") != -1) {
    //    Serial.println("5ch");
    ch5();
  }
  else if (server.arg(0).indexOf("6ch") != -1) {
    //    Serial.println("6ch");
    ch6();
  }
  else if (server.arg(0).indexOf("7ch") != -1) {
    //    Serial.println("7ch");
    ch7();
  }
  else if (server.arg(0).indexOf("8ch") != -1) {
    //    Serial.println("8ch");
    ch8();
  }
  else if (server.arg(0).indexOf("9ch") != -1) {
    //    Serial.println("9ch");
    ch9();
  }
  else if (server.arg(0).indexOf("ch10") != -1) {
    //    Serial.println("ch10");
    ch10();
  }
  else if (server.arg(0).indexOf("ch11") != -1) {
    //    Serial.println("11ch");
    ch11();
  }
  else if (server.arg(0).indexOf("ch12") != -1) {
    //    Serial.println("12ch");
    ch12();
  }
  else if (server.arg(0).indexOf("音量⇑") != -1) {
    //    Serial.println("vol_up");
    vol_up();
  }
  else if (server.arg(0).indexOf("音量⇓") != -1) {
    //    Serial.println("vol_down");
    vol_down();
  }
  else if (server.arg(0).indexOf("番組表") != -1) {
    //    Serial.println("prog_list");
    prog_list();
  }
  else if (server.arg(0).indexOf("番組情報") != -1) {
    //    Serial.println("prog_info");
    prog_info();
  }
}
 
void handleNotFound() {
 
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += ( server.method() == HTTP_GET ) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
 
  for ( uint8_t i = 0; i < server.args(); i++ ) {
    message += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n";
  }
  server.send ( 404, "text/plain", message );
}
 
void startOTA() { // Start the OTA service
  ArduinoOTA.setHostname(OTAName);
  //  ArduinoOTA.setPassword(OTAPassword);
 
  ArduinoOTA.onStart([]() {
    Serial.println("Start");
    // turn off the LEDs
    for (int i = 0; i < 6; i++) {
      //      digitalWrite(LED_BUILTIN, HIGH);
      //      digitalWrite(LED_BUILTIN, LOW);
    }
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\r\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.begin();
  Serial.println("OTA ready\r\n");
}
 
void startMDNS() { // Start the mDNS responder
  MDNS.begin(mdnsName);                        // start the multicast domain name server
  Serial.print("mDNS responder started: http://");
  Serial.print(mdnsName);
  Serial.println(".local");
}
 
void setup() {
  Serial.begin(115200);
  irsend.begin();
 
  //  if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
  if (!WiFi.config(local_IP, gateway, subnet)) {
    Serial.println("STA Failed to configure");
  }
 
  WiFi.begin(ssid, password);
  Serial.println("");
  // AP+STAモードの設定
  //  WiFi.mode(WIFI_AP_STA);
  WiFi.mode(WIFI_STA);
  /*
    // APとして振る舞うためのSSIDとPW情報
    WiFi.softAP(SOFTAP_SSID, SOFTAP_PW);
    Serial.print("Connecting to ");
    Serial.println(SOFTAP_SSID);
    Serial.println("----------");
  */
  //wait for connection
  while ( WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
 
  startOTA();
 
  /*
    SPIFFS.begin();
    if (!readHTML()) {
      Serial.println("Read HTML error!!");
    }
  */
  if (!LittleFS.begin())
  {
    Serial.println("Failed to mount file system");
    return;
  }
 
  startMDNS();                 // Start the mDNS responder
 
  server.on("/", handleRoot);
  server.on("/css.css", [](){
    loadFile(path_css);
  });
  server.on("/script.js", [](){
    loadFile(path_js);
  });
  server.on("/Power", on_off);
  server.on("/1ch", ch1);
  server.on("/2ch", ch2);
  server.on("/3ch", ch3);
  server.on("/4ch", ch4);
  server.on("/5ch", ch5);
  server.on("/6ch", ch6);
  server.on("/7ch", ch7);
  server.on("/8ch", ch8);
  server.on("/9ch", ch9);
  server.on("/10ch", ch10);
  server.on("/11ch", ch11);
  server.on("/12ch", ch12);
  server.on("/vol_up", vol_up);
  server.on("/vol_down", vol_down);
  server.on("/prog_list", prog_list);
  server.on("/prog_info", prog_info);
  server.onNotFound(handleNotFound);
 
  server.begin();
  Serial.println("HTTP server started");
 
  // Add service to MDNS-SD
  MDNS.addService("http", "tcp", 80);
}
 
void loop() {
  server.handleClient();
  ArduinoOTA.handle();                        // listen for OTA events
}

 というわけで古来のその方法でコンテンツ全体(contents)を取得、server.send(200, "text/html", contents);としたらいけました...。

 と思いきや、自身の理解不足だっただけでSPIFFSでも基本同じようです...。

 で、setup(){}内でserver.on("/style.css", [](){ loadContents("/style.css") });などとすることで外部JavaScriptや外部CSSも当該パスへのアクセス時に中身を表示させることもできました。

 が、JavaScriptの機能は効くものの、外部CSSについては、装飾が一切、反映されない...。

 あ、スケッチ側でMIMEタイプ別にしておらず、全てserver.send(200, text/html, contents)にしてたからじゃん...。

 そのままでソースを表示させるとJavaScriptもCSSも改行がなくなったりして、なんで?と思ってましたが、原因は、これでした。

 というわけでHTMLソース上は、CSS、JavaScript共にtype属性は不要もスケッチのloadContents(){}内でHTMLならtext/html、CSSならtext/css、JavaScriptは変えなくてもいけましたが、念のためtext/javascriptとすることで外部CSSも効くようになり、それぞれソース表示上もオリジナルソース通りに改行されるようになりました。

 ちなみに、そのままだとmDNS.local/にアクセス、ブラウザ機能の[ソースを表示]などとすれば、index.htmlファイルの内容が表示される一方、mDNS.local/index.htmlとしても、そんなファイルないよと言われます。

 もし、mDNS.local/index.htmlにアクセスした際にもソースを表示させたい場合は、setup(){}内でserver.on("/index.html", [](){ loadContents("/index.html") });のようにすればOK。

 尚、LittleFSにおいては、HTMLファイル内のscriptタグのsrc属性値やlinkタグのhref属性値の先頭やloadContents()の引数の先頭にルートを表わすスラッシュ(/)がなくてもあるものと見做すとされており、実際機能しますが、紛らわしいのでスラッシュは付けておいた方が賢明でしょう。

 ファイルはともかく、信号送信用のGET部分は要らないよね、まぁいいか。

 あと、未検証ではありますが、LittleFSの場合、FS.hは要らないかも。

 ESP8266ボードはWeMOSでしたが、該当するボードもなく、Generic ESP8266だとOTAとの兼ね合いで設定値の選択に困り、NodeMCU 1.0(ESP-12E Module)、Flash Sizeを["4MB (FS:2MB OTA:~1019KB)"]としました。

 これでCeleronデュアルコアCPU RAM4GBのノートPCでもCPUやRAMを専有されることも、フリーズすることもなく、コンパイル、アップロードできました。

ESP32ボードとLittleFS

 前述の通り、Ardruino Coreにも将来的に組み込まれる模様ながら、現時点では、Arduino IDE標準ライブラリにもそれまでのつなぎ?を謳うIDFコンポーネントベースのLittleFS_esp32 Arduino Reference / lorol/LITTLEFS githubがあります。

ホーム前へ次へ