/*
 * ESP32 Multi-Zone Controller — E25
 * ESP32 & ESP8266 Connectivity
 *
 * Board   : ESP32 DevKit (ESP32-WROOM-32, 30 or 38 pin)
 * MCU     : ESP32 dual core @ 240 MHz, 3.3 V logic
 * Compile : arduino-cli compile --fqbn esp32:esp32:esp32
 *
 * Pin map (also in the wiring notes and on the project page):
 *   OUT_1              -> 4
 *   OUT_2              -> 5
 *   OUT_3              -> 16
 *   OUT_4              -> 17
 *   IN_1               -> 34
 *   IN_2               -> 35
 *   IN_3               -> 32
 *   IN_4               -> 33
 *
 * GPIO34–35 are input only and have no pull-ups. Use them for sensors, never for LEDs.
 *
 * Project page : https://codeproven.pages.dev/project/E25/
 * Wiring       : https://codeproven.pages.dev/project-art/E25-connections.svg
 * Level        : Intermediate
 * 
 * An ESP32 serving four output channels and four inputs, with the whole state available as JSON for a script.
 * 
 * Safety       : Four channels switching at once can pull a supply down. Size the supply for the worst case, not the average.
 *
 * This sketch is the one the project page offers for download, and it is
 * compile-checked for esp32:esp32:esp32 before publication. It is bench code: read it,
 * change the constants for your own hardware, and test with the load
 * disconnected first.
 */

#include <WiFi.h>
#include <WebServer.h>

/* ------------------------------------------------------------ pin map -----
   Written by the generator from the board's own pin pools. The same numbers
   are in the connection diagram and the project page. */
#define PIN_OUT_1            4
#define PIN_OUT_2            5
#define PIN_OUT_3            16
#define PIN_OUT_4            17
#define PIN_IN_1             34
#define PIN_IN_2             35
#define PIN_IN_3             32
#define PIN_IN_4             33
/* What this build calls itself on the wire and in its log. */
#define PROJECT_ID        "E25"
#define FIRMWARE_VERSION  "1.0"
#define BOARD_LABEL       "ESP32 DevKit (ESP32-WROOM-32, 30 or 38 pin)"


/* ------------------------------------------------------------- self-check --
   Runs once at boot. If a wiring fault is present, it is reported here rather
   than discovered later as a mystery reset. */
void startupReport() {
  Serial.println();
  Serial.println(F("--- build self-check ---"));
  Serial.print(F("project="));   Serial.println(F(PROJECT_ID));
  Serial.print(F("firmware="));  Serial.println(F(FIRMWARE_VERSION));
  Serial.print(F("board="));     Serial.println(F(BOARD_LABEL));
  Serial.print(F("pin OUT_1="));  Serial.println(PIN_OUT_1);
  Serial.print(F("pin OUT_2="));  Serial.println(PIN_OUT_2);
  Serial.print(F("pin OUT_3="));  Serial.println(PIN_OUT_3);
  Serial.print(F("pin OUT_4="));  Serial.println(PIN_OUT_4);
  Serial.print(F("pin IN_1="));  Serial.println(PIN_IN_1);
  Serial.print(F("pin IN_2="));  Serial.println(PIN_IN_2);
  Serial.print(F("pin IN_3="));  Serial.println(PIN_IN_3);
  Serial.print(F("pin IN_4="));  Serial.println(PIN_IN_4);
  Serial.println(F("--- end self-check ---"));
  Serial.println();
}
/* ---------------------------------------------------------------- serial ----
   One telemetry shape across every connected build: key=value pairs, comma
   separated, so any log from any project reads the same way. */
void logKey(const char *key, float value, int places = 2) {
  Serial.print(key); Serial.print('='); Serial.print(value, places); Serial.print(',');
}
void logStr(const char *key, const char *value) {
  Serial.print(key); Serial.print('='); Serial.print(value); Serial.print(',');
}


/* ------------------------------------------------------------------ setup --
   Leave WIFI_SSID empty to get a config portal on first boot: the board comes
   up as its own access point, you enter your network on the page, and the
   credentials are stored in flash. No credentials are compiled into the sketch,
   so a downloaded file never carries someone else's network. */
const char *WIFI_SSID = "";
const char *WIFI_PASS = "";
const char *DEVICE_NAME = "e25-esp32-multi-zone-controller";


const uint8_t PIN_OUT1 = PIN_OUT_1;
const uint8_t PIN_OUT2 = PIN_OUT_2;
const uint8_t PIN_OUT3 = PIN_OUT_3;
const uint8_t PIN_OUT4 = PIN_OUT_4;
const uint8_t PIN_IN1  = PIN_IN_1;
const uint8_t PIN_IN2  = PIN_IN_2;
const uint8_t PIN_IN3  = PIN_IN_3;
const uint8_t PIN_IN4  = PIN_IN_4;
bool OutputState[4] = { false, false, false, false };
const int PWM_MAX = 255;
const uint32_t WIFI_RETRY_MS = 15000;

WebServer server(80);

String buildPage() {
  String html = F("<!doctype html><meta charset='utf-8'>"
                  "<meta name='viewport' content='width=device-width,initial-scale=1'>"
                  "<title>E25 Multi-Zone Controller</title>"
                  "<body style='font-family:monospace;background:#1c1a1a;color:#e8e6e3;padding:1.5rem;max-width:38rem;margin:auto'>"
                  "<h1 style='font-size:1.3rem'>Multi-Zone Controller</h1>");
  html += "<p>rssi=" + String(WiFi.RSSI()) + " dBm · uptime=" + String(millis() / 1000) + " s</p>";
    html += "<p>input 1=" + String(analogRead(PIN_IN1)) + "</p>";
    html += "<p>input 2=" + String(analogRead(PIN_IN2)) + "</p>";
    html += "<p>input 3=" + String(analogRead(PIN_IN3)) + "</p>";
    html += "<p>input 4=" + String(analogRead(PIN_IN4)) + "</p>";

  for (uint8_t i = 0; i < 4; i++) {
    html += "<p>output " + String(i + 1) + ": ";
    html += OutputState[i] ? "ON " : "OFF ";
    html += "<a href='/out" + String(i + 1) + "?on=1'>on</a> ";
    html += "<a href='/out" + String(i + 1) + "?on=0'>off</a></p>";
  }
  html += "<p><a href='/data'>json</a></p>";
  html += "<p style='color:#9aa0a6;font-size:.85rem'>CodeProven E25</p></body>";
  return html;
}

void handleRoot() { server.send(200, "text/html", buildPage()); }

void handleData() {
  String json = "{\"id\":\"E25\",\"uptime\":" + String(millis() / 1000) + ",\"rssi\":" + String(WiFi.RSSI());
  json += ",\"in1\":" + String(analogRead(PIN_IN1));
  json += ",\"in2\":" + String(analogRead(PIN_IN2));
  json += ",\"in3\":" + String(analogRead(PIN_IN3));
  json += ",\"in4\":" + String(analogRead(PIN_IN4));
  json += "}";
  server.send(200, "application/json", json);
}

void connectWifi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.print(F("# connecting"));
  for (uint8_t attempt = 0; attempt < 30 && WiFi.status() != WL_CONNECTED; attempt++) {
    delay(300);
    Serial.print('.');
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print(F("# http://")); Serial.println(WiFi.localIP());
  } else {
    Serial.println(F("# no network — set WIFI_SSID and WIFI_PASS at the top"));
  }
}

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println();
  Serial.println(F("=== E25 Multi-Zone Controller — control page ==="));
  for (uint8_t i = 0; i < 4; i++) {
    pinMode((&PIN_OUT1)[i], OUTPUT);
    digitalWrite((&PIN_OUT1)[i], OutputState[i] ? HIGH : LOW);
  }
  pinMode(PIN_IN1, INPUT);
  pinMode(PIN_IN2, INPUT);
  pinMode(PIN_IN3, INPUT);
  pinMode(PIN_IN4, INPUT);

  server.on("/", handleRoot);
  server.on("/data", handleData);
  server.on("/out1", []() {
    const bool on = server.arg("on") == "1";
    digitalWrite(PIN_OUT1, on ? HIGH : LOW);
    OutputState[0] = on;
    server.send(200, "text/plain", on ? "on" : "off");
  });
  server.on("/out2", []() {
    const bool on = server.arg("on") == "1";
    digitalWrite(PIN_OUT2, on ? HIGH : LOW);
    OutputState[1] = on;
    server.send(200, "text/plain", on ? "on" : "off");
  });
  server.on("/out3", []() {
    const bool on = server.arg("on") == "1";
    digitalWrite(PIN_OUT3, on ? HIGH : LOW);
    OutputState[2] = on;
    server.send(200, "text/plain", on ? "on" : "off");
  });
  server.on("/out4", []() {
    const bool on = server.arg("on") == "1";
    digitalWrite(PIN_OUT4, on ? HIGH : LOW);
    OutputState[3] = on;
    server.send(200, "text/plain", on ? "on" : "off");
  });
  server.onNotFound([]() { server.send(404, "text/plain", "not found"); });

  connectWifi();
  server.begin();
  Serial.println(F("# server listening on port 80"));
}

void loop() {
  server.handleClient();

  /* Reconnect rather than going quiet: a controller that drops off the network
     is indistinguishable from a controller that has crashed. */
  static uint32_t lastCheck = 0;
  if (millis() - lastCheck > WIFI_RETRY_MS) {
    lastCheck = millis();
    if (WiFi.status() != WL_CONNECTED) {
      Serial.println(F("# link lost, reconnecting"));
      connectWifi();
    }
  }

  static uint32_t lastLog = 0;
  if (millis() - lastLog >= 5000) {
    lastLog = millis();
    Serial.print(F("NODE,"));
    logKey("rssi", WiFi.RSSI(), 0);
    logKey("heap", ESP.getFreeHeap() / 1024.0f, 1);
    logKey("uptime", millis() / 1000.0f, 0);
    Serial.println();
  }
}
