リモータブル対応Arduinoプログラム

まずはコード全体

// --- 必要なライブラリ ---
#include <WiFi.h>
#include <Firebase_ESP_Client.h>
#include <ArduinoJson.h>
#include <SPIFFS.h>
#include <vector>

// --- Wi-Fi設定 ---
const char* ssidList[] = {"abc-g-defghi"}; //IDの真ん中に「-g]が入った方を入力
const char* passwordList[] = {"abcdefghijkl"};
//const char* ssidList[] = {"abc-g-defghi", "abc-g-123456"}; //←Wi-Fiが複数ある時に使用
//const char* passwordList[] = {"abcdefghijkl", "123456789012"}; // ←Wi-Fiが複数ある時に使用

const int ssidCount = sizeof(ssidList) / sizeof(ssidList[0]);

// --- Firestore情報 ---
#define USER_EMAIL "sample@lunchtown.jp" //アプリに登録時のアドレス
#define USER_PASSWORD "abc123" //アプリ登録時のパスワード
#define DEVICE_ID "XYZ123" // 6ケタのデバイスID

#define API_KEY "AIzaSyDZbbWJ44XJRtqOJjWo7lCfNUs2I_thcys"
#define DATABASE_ID "(default)"
#define PROJECT_ID "controlingwatersupply"
#define LED_PIN 2

// --- グローバル変数群 ---
std::vector<String> valveIdList;
FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
String uid, apiKey, deviceId;
unsigned long lastCheck = 0;
unsigned long lastValveCheck = 0;
unsigned long lastUpdate = 0;
unsigned long lastRebootCheck = 0;
const unsigned long interval = 10000;
const unsigned long valveCheckInterval = 10000;
const unsigned long updateInterval = 20000;
const unsigned long rebootCheckInterval = 30000;
int pinNum = 14; // デフォルト値

// --- 関数プロトタイプ宣言 ---
void connectToWiFi();
void getDeviceConfigFromFirestore();
void saveConfigToSPIFFS(const String& uid, const String& apiKey, const String& deviceId);
bool loadConfigFromSPIFFS();
void readValvesFromFirestore();
void loadPinNumFromFirestore(const String& vid);
void updateValveFromFirestore();
void checkValveState();
void checkShouldReboot();

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("🚀 Firestore最小構成テスト");

  initializePins(); 

  if (!SPIFFS.begin(true)) {
    Serial.println("❌ SPIFFSマウントに失敗");
    return;
  } 
  Serial.println("✅ SPIFFS マウント成功");

  if (deviceId != DEVICE_ID) {
  Serial.println("⚡️ デバイスIDが異なるためSPIFFSを更新します");
  deviceId = DEVICE_ID;
  saveConfigToSPIFFS(uid, apiKey, deviceId);
}

  connectToWiFi();
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("❌ Wi-Fi 接続失敗");
    return;
  }
  Serial.println("✅ Wi-Fi接続完了");

  // ピンのモードを設定
  pinMode(LED_PIN, OUTPUT);

  if (!loadConfigFromSPIFFS()) {
    Serial.println("⚠️ /config.json なし または JSONエラー → 手動でセット");
    apiKey = API_KEY;
    deviceId = DEVICE_ID;
  }

  config.api_key = API_KEY;
  config.database_url = "https://controlingwatersupply.firebaseio.com";
  auth.user.email = USER_EMAIL; 
  auth.user.password = USER_PASSWORD;
  
  Firebase.begin(&config, &auth);
  Firebase.reconnectWiFi(true);
  Serial.println("✅ Firebase初期化完了");

  // 4. UIDが取得できるまで待つ
  while (auth.token.uid.c_str() == "") {
    delay(100);
  }
  
  uid = String(auth.token.uid.c_str());
  Serial.println("✅ 現在のUID: " + uid);
  
  // ✅ 空チェックを明示(安全ガード)
  if (deviceId == "") {
    Serial.println("❌ deviceIdが未設定 → 処理停止");
    while (true); // フリーズ
  }

  // Firestoreからデータ取得
  getDeviceConfigFromFirestore();
  readValvesFromFirestore();

  for (String vid : valveIdList) {
    //loadPinNumFromFirestore(vid);  
    if (vid.length() > 0) {  // 🔵 vidが空でないかを確認
    loadPinNumFromFirestore(vid);
    }
  }

  pinMode(pinNum, OUTPUT);
  digitalWrite(pinNum, LOW);
  Serial.println("✅ 使用ピン:" + String(pinNum));
}

// --- loop() ---
void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectToWiFi();
    digitalWrite(LED_PIN, LOW);  // Wi-Fiが切断された場合、LEDを消灯
  } else {
    digitalWrite(LED_PIN, HIGH);  // Wi-Fiが接続された場合、LEDを点灯
  }

  if (millis() - lastCheck > interval) {
    lastCheck = millis();
    checkShouldReboot();
  }
  // バルブ状態チェック
  if (millis() - lastValveCheck > valveCheckInterval) {
    lastValveCheck = millis();
    checkValveState();
  }
  if (millis() - lastUpdate > updateInterval) {
    lastUpdate = millis();
    updateValveFromFirestore(); // ← この呼び出しに10秒制限を追加
  }
}

void connectToWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.disconnect(true);  // WiFiリセット
  delay(1000);

  for (int i = 0; i < ssidCount; i++) {
    Serial.print("Connecting to ");
    Serial.println(ssidList[i]);
    
    WiFi.begin(ssidList[i], passwordList[i]);

    unsigned long startAttemptTime = millis();

    while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 10000) {
      delay(500);
      Serial.print(".");
      digitalWrite(LED_PIN, !digitalRead(LED_PIN));  // LEDを点滅
    }

    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("\nConnected to WiFi!");
      Serial.print("IP address: ");
      Serial.println(WiFi.localIP());
      digitalWrite(LED_PIN, HIGH);  // 接続が確立したらLEDを点灯
      return;
    } else {
      Serial.println("\nFailed to connect to WiFi.");
      WiFi.disconnect();
      delay(1000);
    }
  }

  Serial.println("Could not connect to any Wi-Fi network.");
  digitalWrite(LED_PIN, LOW);  // 接続に失敗したらLEDを消灯
}

void getDeviceConfigFromFirestore() {
  String path = "users/" + uid + "/devices/" + String(DEVICE_ID);
  bool success = Firebase.Firestore.getDocument(&fbdo, PROJECT_ID, "", path.c_str());

  if (success) {
    Serial.println("✅ Firestore ドキュメント取得成功");
    Serial.println(fbdo.payload());
    FirebaseJson json;
    json.setJsonData(fbdo.payload());
    FirebaseJsonData resultUid, resultApiKey, resultValveId;
    //FirebaseJsonData result;

    if (json.get(resultUid, "fields/uid/stringValue")) {
    uid = resultUid.stringValue;
    Serial.println("✅ uid: " + uid);
    } else {
    Serial.println("❌ uid の取得に失敗しました");
    }

    if (json.get(resultApiKey, "fields/apikey/stringValue")) {
    apiKey = resultApiKey.stringValue;
    Serial.println("✅ apiKey取得成功");
    } else {
      Serial.println("❌ apiKey の取得に失敗しました");
    }

    String deviceId = DEVICE_ID;

    saveConfigToSPIFFS(uid, apiKey, deviceId);
  } else {
    Serial.println("❌ Firestore ドキュメント取得失敗:");
    Serial.println(fbdo.errorReason());
  }
}

void saveConfigToSPIFFS(const String& uid, const String& apiKey, const String& deviceId) {
  StaticJsonDocument<512> doc;
  doc["uid"] = uid;
  doc["apiKey"] = apiKey;
  doc["deviceId"] = deviceId;

  File file = SPIFFS.open("/config.json", FILE_WRITE);
  if (!file) {
    Serial.println("❌ SPIFFS 書き込みに失敗しました");
    return;
  }

  serializeJson(doc, file);
  file.close();
  Serial.println("✅ 構成情報を SPIFFS に保存しました:");
  serializeJsonPretty(doc, Serial);
  Serial.println();
}

bool loadConfigFromSPIFFS() {
  if (!SPIFFS.begin(true)) {
    Serial.println("❌ SPIFFS開始失敗");
    return false;
  }

  File file = SPIFFS.open("/config.json", FILE_READ);
  if (!file) {
    Serial.println("❌ SPIFFS 構成ファイルが見つかりません");
    file.close();
    return false;
  }

  StaticJsonDocument<512> doc;
  DeserializationError error = deserializeJson(doc, file);
  if (error) {
    Serial.print("❌ JSON パースエラー: ");
    Serial.println(error.c_str());
    return false;
  }

  uid = doc["uid"].as<String>();
  apiKey = doc["apiKey"].as<String>();
  deviceId = doc["deviceId"].as<String>();

  Serial.println("✅ SPIFFS 構成読み込み成功:");
  serializeJsonPretty(doc, Serial);
  Serial.println();
   file.close();

  return true;
}

void connectToFirebase(const String &apiKey, const String &uid) {
  config.api_key = apiKey;
  config.database_url = "https://controlingwatersupply.firebaseio.com"; // ← Firestore使う場合でも一応必要

  auth.user.email.clear();
  auth.user.password.clear();
  
  Firebase.begin(&config, &auth);
  Firebase.reconnectWiFi(true);
  Serial.println("✅ Firebase 初期化完了");
}

void readValvesFromFirestore() {
  valveIdList.clear();  // 毎回クリアして再取得
  String path = "users/" + uid + "/devices/" + deviceId + "/valves";
  int pageSize = 100;

  if (Firebase.Firestore.listDocuments(&fbdo, PROJECT_ID, DATABASE_ID, path.c_str(),  pageSize, "", "", "", false)) {
    Serial.println("✅ Firestoreからvalvesを取得しました:");
    Serial.println(fbdo.payload());

    FirebaseJson json;
    json.setJsonData(fbdo.payload());

    FirebaseJsonData docData;
    if (json.get(docData, "documents")) {
      FirebaseJsonArray docArray;
      docArray.setJsonArrayData(docData.stringValue);

      for (size_t i = 0; i < docArray.size(); ++i) {
        FirebaseJsonData item;
        if (docArray.get(item, i)) {
          FirebaseJson docJson(item.stringValue); //
          FirebaseJsonData nameField;
           if (docJson.get(nameField, "name")) {
            String fullPath = nameField.stringValue;
            int lastSlash = fullPath.lastIndexOf('/');
            if (lastSlash != -1) {
              String valveId = fullPath.substring(lastSlash + 1);
              valveIdList.push_back(valveId);
              Serial.println("✅ valveId 抽出: " + valveId);
            }
          }
        }
      }
    }
  } else {
    Serial.println("❌ Firestoreからvalvesの取得に失敗: " + fbdo.errorReason());
  }
}

void updateValveFromFirestore() {
  String path = "users/" + uid + "/devices/" + deviceId + "/valves";
  Serial.println("📡 FirestoreからvalveStateを取得します(最終修正版)...");

  if (Firebase.Firestore.listDocuments(&fbdo,
                                       PROJECT_ID,
                                       DATABASE_ID,
                                       path.c_str(),
                                       10,
                                       "", "", "", false)) {

    Serial.println("✅ 取得成功:");
    Serial.println(fbdo.payload());

    FirebaseJson root(fbdo.payload());
    FirebaseJsonData result;

    // ✅ documents 配列全体を取り出す
    if (root.get(result, "documents")) {
      FirebaseJsonArray docArray;
      docArray.setJsonArrayData(result.stringValue);  // ← ここで documents を配列として再構築

      FirebaseJsonData docItem;
      if (docArray.get(docItem, 0)) {
        FirebaseJson docJson(docItem.stringValue);  // ← ドキュメント1件分をJSONに

        FirebaseJsonData isOnValue;
        if (docJson.get(isOnValue, "fields/isOn/booleanValue")) {
          bool valveState = isOnValue.boolValue;
          Serial.printf("🆕 isOn: %s\n", valveState ? "ON" : "OFF");
          //digitalWrite(pinNum, valveState ? HIGH : LOW);
        } else {
          Serial.println("⚠️ fields/isOn/booleanValue が取得できません");
        }

      } else {
        Serial.println("❌ documents 配列の 0番要素が取得できませんでした");
      }
    } else {
      Serial.println("❌ documents フィールドが取得できませんでした");
    }

  } else {
    Serial.println("❌ Firestore 取得失敗:");
    Serial.println(fbdo.errorReason());
  }
}

void onFirestoreUpdate(FirebaseStream data) {
  Serial.println("📥 Firestore ドキュメント変更検知");

  FirebaseJson payload;
  FirebaseJsonData result;

  payload.setJsonData(data.payload());

  if (payload.get(result, "fields/isOn/booleanValue")) {
    bool valveState = result.boolValue;
    Serial.printf("🆕 受信 valveState: %s\n", valveState ? "ON" : "OFF");
    digitalWrite(pinNum, valveState ? HIGH : LOW);
  } else {
    Serial.println("⚠️ isOn が見つかりません");
  }
}

void loadPinNumFromFirestore(const String& vid) {
  //String valveId = VALVE_ID;
  if (vid.length() == 0) {
    Serial.println("❌ vidが空です。loadPinNum中断。");
    return;
  }
  String path = "users/" + uid + "/devices/" + deviceId + "/valves/" + vid;
  Serial.println("🔍 pinNum 取得用 Firestoreパス:" + path);

  if (Firebase.Firestore.getDocument(&fbdo, PROJECT_ID, "", path.c_str())) {
    FirebaseJson json;
    json.setJsonData(fbdo.payload());

    FirebaseJsonData pinData;
    if (json.get(pinData, "fields/pinNum/integerValue")) {
      int dynamicPin = pinData.intValue;
      pinNum = dynamicPin;
      Serial.println("✅ pinNum 読み込み成功:" + String(dynamicPin));

      pinMode(pinNum, OUTPUT);
      digitalWrite(pinNum, LOW);
      Serial.println("ピン番号" + pinNum);
    } else {
      Serial.println("pinNum フィールドが見つかりません");
      pinNum = 14;
    }
  } else {
    Serial.println("❌ FirestoreからpinNum取得失敗: " + fbdo.errorReason());
    pinNum = 14;
  }
}

void checkValveState() {
  for (String vid : valveIdList) {
    String path = "users/" + uid + "/devices/" + deviceId + "/valves/" + vid;
    Serial.println("📥 isOn 取得パス: " + path);

    if (Firebase.Firestore.getDocument(&fbdo, PROJECT_ID, DATABASE_ID, path.c_str())) {
      FirebaseJson json;
      json.setJsonData(fbdo.payload());

      FirebaseJsonData onData;
      FirebaseJsonData pinData;

    if (json.get(pinData, "fields/pinNum/integerValue")) {
        int pin = pinData.intValue;

        if (json.get(onData, "fields/isOn/booleanValue")) {
          bool isOn = onData.boolValue;
          pinMode(pin, OUTPUT);  // 念のため毎回指定(冗長でも安全)
          digitalWrite(pin, isOn ? HIGH : LOW);

          Serial.printf("🔁 pin %d%s\n", pin, isOn ? "ON" : "OFF");

        } else {
          Serial.println("⚠️ isOn フィールドが存在しません");
        }

      } else {
        Serial.println("⚠️ pinNum フィールドが存在しません");
      }

    } else {
      Serial.println("❌ Firestore取得失敗: " + fbdo.errorReason());
    }
  }
}

void checkShouldReboot() {
  String path = "users/" + uid + "/devices/" + deviceId;
  if (Firebase.Firestore.getDocument(&fbdo, PROJECT_ID, "", path.c_str())) {
    FirebaseJson json;
    json.setJsonData(fbdo.payload());
    
    FirebaseJsonData shouldRebootData;
    if (json.get(shouldRebootData, "fields/shouldReboot/booleanValue")) {
      bool shouldReboot = shouldRebootData.boolValue;
      Serial.println("📝 shouldReboot = "+ String(shouldReboot ? "true" : "false"));
      //Serial.println(shouldReboot ? "true" : "false");

      if (shouldReboot) {
        // shouldReboot を false に戻す
        FirebaseJson updateJson;
        updateJson.set("fields/shouldReboot/booleanValue", false);

        bool result = Firebase.Firestore.patchDocument(
          &fbdo,
          PROJECT_ID,
          "",
          path.c_str(),
          updateJson.raw(),
          "shouldReboot"
          );

          if (result) {
          Serial.println("📦 shouldReboot: false 書き込み完了");
          delay(1000);
          ESP.restart();
        } else {
          Serial.println("❌ shouldReboot書き戻し失敗: " + fbdo.errorReason());
        }
      }
     } else {
      Serial.println("⚠️ shouldRebootフィールドが取得できません");
    }
  } else {
    Serial.println("❌ CheckReboot Firestore取得失敗: " + fbdo.errorReason());
  }
}

// --- 使用するすべてのピンを明示的に初期化する ---
void initializePins() {
  const int pinsToInitialize[] = {25, 32};  // 必要なピンをここに列挙

  for (int i = 0; i < sizeof(pinsToInitialize) / sizeof(pinsToInitialize[0]); i++) {
    int pin = pinsToInitialize[i];
    pinMode(pin, OUTPUT);
    digitalWrite(pin, LOW);  // 起動直後は必ずLOWにしておく
    Serial.println("✅ 初期化ピン: " + String(pin) + " → LOW");
  }
}