#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
// ESP32-C3 + relay module + simulated bulb (white LED)
// Wokwi relay module default behavior is ACTIVE-LOW:
// LOW -> relay energized -> COM connected to NO
// HIGH -> relay idle -> COM connected to NC
//
// Hardware:
// - GPIO4 -> relay IN
// - GPIO3 -> pushbutton to GND (INPUT_PULLUP)
//
// Behavior:
// - Short button press toggles the bulb
// - Web UI shows current state and toggles the bulb
// - WiFi credentials can be provisioned from the web page and saved in NVS
// - A fallback AP is started for real hardware recovery provisioning
constexpr int RELAY_PIN = 4;
constexpr int BUTTON_PIN = 3;
constexpr int RELAY_ON = LOW;
constexpr int RELAY_OFF = HIGH;
//const char* DEFAULT_WOKWI_SSID = "RelayBulb";
const char* DEFAULT_WOKWI_SSID = "Wokwi-GUEST";
const char* DEFAULT_WOKWI_PASS = "";
const char* AP_SSID = "SONNYNET";
const char* AP_PASS = "sunnyairplane264";
constexpr unsigned long WIFI_TIMEOUT_MS = 12000;
constexpr unsigned long DEBOUNCE_MS = 50;
WebServer server(80);
Preferences prefs;
bool bulbOn = false;
bool apStarted = false;
String configuredSsid;
String configuredPass;
bool lastButtonReading = HIGH;
bool buttonState = HIGH;
unsigned long lastDebounceTime = 0;
String htmlEscape(const String& value) {
String out = value;
out.replace("&", "&");
out.replace("<", "<");
out.replace(">", ">");
out.replace("\"", """);
out.replace("'", "'");
return out;
}
String jsonEscape(const String& value) {
String out = value;
out.replace("\\", "\\\\");
out.replace("\"", "\\\"");
out.replace("\n", "\\n");
out.replace("\r", "\\r");
out.replace("\t", "\\t");
return out;
}
void setBulb(bool on) {
bulbOn = on;
digitalWrite(RELAY_PIN, on ? RELAY_ON : RELAY_OFF);
Serial.printf("Bulb is now %s\n", bulbOn ? "ON" : "OFF");
}
void toggleBulb() {
setBulb(!bulbOn);
}
void loadWifiConfig() {
prefs.begin("wifi", false);
configuredSsid = prefs.getString("ssid", "");
configuredPass = prefs.getString("pass", "");
}
void saveWifiConfig(const String& ssid, const String& pass) {
configuredSsid = ssid;
configuredPass = pass;
prefs.putString("ssid", configuredSsid);
prefs.putString("pass", configuredPass);
}
void clearWifiConfig() {
configuredSsid = "";
configuredPass = "";
prefs.remove("ssid");
prefs.remove("pass");
}
bool connectToWifi(const String& ssid, const String& pass, unsigned long timeoutMs = WIFI_TIMEOUT_MS) {
if (ssid.isEmpty()) {
return false;
}
Serial.printf("Connecting to WiFi SSID '%s'", ssid.c_str());
WiFi.mode(WIFI_AP_STA);
WiFi.disconnect();
delay(200);
if (ssid == DEFAULT_WOKWI_SSID) {
WiFi.begin(ssid.c_str(), pass.c_str(), 6);
} else {
WiFi.begin(ssid.c_str(), pass.c_str());
}
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < timeoutMs) {
delay(250);
Serial.print('.');
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("WiFi connected. IP: %s\n", WiFi.localIP().toString().c_str());
return true;
}
Serial.println("WiFi connection failed");
return false;
}
void startProvisioningAp() {
if (apStarted) {
return;
}
WiFi.mode(WIFI_AP_STA);
apStarted = WiFi.softAP(AP_SSID, AP_PASS);
if (apStarted) {
Serial.printf("Provisioning AP started: %s\n", AP_SSID);
Serial.printf("Provisioning AP IP: %s\n", WiFi.softAPIP().toString().c_str());
} else {
Serial.println("Failed to start provisioning AP");
}
}
String wifiStatusText() {
switch (WiFi.status()) {
case WL_CONNECTED:
return "Connected";
case WL_NO_SSID_AVAIL:
return "SSID not available";
case WL_CONNECT_FAILED:
return "Connect failed";
case WL_CONNECTION_LOST:
return "Connection lost";
case WL_DISCONNECTED:
default:
return "Disconnected";
}
}
String networkSummary() {
String staIp = WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : String("-");
String staSsid = WiFi.status() == WL_CONNECTED ? WiFi.SSID() : String("-");
String apIp = apStarted ? WiFi.softAPIP().toString() : String("-");
String apName = apStarted ? String(AP_SSID) : String("-");
String summary;
summary += "<ul>";
summary += "<li><strong>Bulb state:</strong> <span id='bulbState'>" + String(bulbOn ? "ON" : "OFF") + "</span></li>";
summary += "<li><strong>WiFi status:</strong> <span id='wifiStatus'>" + htmlEscape(wifiStatusText()) + "</span></li>";
summary += "<li><strong>Connected SSID:</strong> <span id='connectedSsid'>" + htmlEscape(staSsid) + "</span></li>";
summary += "<li><strong>STA IP:</strong> <span id='staIp'>" + htmlEscape(staIp) + "</span></li>";
summary += "<li><strong>Provisioning AP:</strong> " + htmlEscape(apName) + "</li>";
summary += "<li><strong>AP IP:</strong> " + htmlEscape(apIp) + "</li>";
summary += "</ul>";
return summary;
}
String buildPage() {
String page;
page.reserve(6000);
page += F("<!doctype html><html><head><meta charset='utf-8'>");
page += F("<meta name='viewport' content='width=device-width,initial-scale=1'>");
page += F("<title>ESP32-C3 Relay Bulb</title>");
page += F("<style>");
page += F("body{font-family:Arial,sans-serif;max-width:760px;margin:24px auto;padding:0 16px;background:#f5f5f5;color:#222}");
page += F(".card{background:#fff;border-radius:14px;padding:18px 20px;margin:16px 0;box-shadow:0 2px 10px rgba(0,0,0,.08)}");
page += F("button,input{font-size:16px;padding:10px 12px;border-radius:10px;border:1px solid #bbb}");
page += F("button{cursor:pointer}");
page += F("input{width:100%;box-sizing:border-box;margin:6px 0 12px}");
page += F(".row{display:flex;gap:12px;flex-wrap:wrap}");
page += F(".row > *{flex:1}");
page += F(".state{font-size:28px;font-weight:700;margin:8px 0 12px}");
page += F(".hint{color:#555;font-size:14px}");
page += F("code{background:#eee;padding:2px 6px;border-radius:6px}");
page += F("</style></head><body>");
page += F("<h1>ESP32-C3 Relay + Bulb</h1>");
page += F("<div class='card'>");
page += F("<h2>Bulb control</h2>");
page += F("<div class='state' id='stateHero'>");
page += (bulbOn ? "ON" : "OFF");
page += F("</div>");
page += F("<form method='POST' action='/toggle'><button type='submit'>Toggle bulb</button></form>");
page += F("<p class='hint'>Physical button on GPIO3 also toggles the relay. In Wokwi you can press the green button or use keyboard shortcut <code>T</code>.</p>");
page += networkSummary();
page += F("</div>");
page += F("<div class='card'>");
page += F("<h2>WiFi provisioning</h2>");
page += F("<form method='POST' action='/wifi'>");
page += F("<label>SSID</label>");
page += "<input name='ssid' value='" + htmlEscape(configuredSsid) + "' placeholder='Wokwi-GUEST'>";
page += F("<label>Password</label>");
page += "<input name='password' type='password' value='" + htmlEscape(configuredPass) + "' placeholder='leave empty for Wokwi-GUEST'>";
page += F("<div class='row'><button type='submit'>Save and connect</button><button type='button' onclick=\"location.href='/wifi/default'\">Use Wokwi defaults</button></div>");
page += F("</form>");
page += F("<form method='POST' action='/wifi/clear' style='margin-top:12px'><button type='submit'>Clear saved WiFi</button></form>");
page += F("<p class='hint'>For Wokwi simulation, the default SSID is <code>Wokwi-GUEST</code> with an empty password. For browser access to the ESP32 web server in Wokwi, run the Private IoT Gateway and open <code>http://localhost:9080/</code>.</p>");
page += F("</div>");
page += F("<script>");
page += F("async function refreshState(){try{const r=await fetch('/state');const s=await r.json();document.getElementById('bulbState').textContent=s.bulbOn?'ON':'OFF';document.getElementById('stateHero').textContent=s.bulbOn?'ON':'OFF';document.getElementById('wifiStatus').textContent=s.wifiStatus;document.getElementById('connectedSsid').textContent=s.connectedSsid;document.getElementById('staIp').textContent=s.staIp;}catch(e){}}setInterval(refreshState,1000);refreshState();");
page += F("</script>");
page += F("</body></html>");
return page;
}
void handleRoot() {
server.send(200, "text/html", buildPage());
}
void handleToggle() {
toggleBulb();
server.sendHeader("Location", "/");
server.send(303);
}
void handleState() {
String staIp = WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : String("-");
String connectedSsid = WiFi.status() == WL_CONNECTED ? WiFi.SSID() : String("-");
String json = "{";
json += "\"bulbOn\":" + String(bulbOn ? "true" : "false");
json += ",\"wifiStatus\":\"" + jsonEscape(wifiStatusText()) + "\"";
json += ",\"connectedSsid\":\"" + jsonEscape(connectedSsid) + "\"";
json += ",\"staIp\":\"" + jsonEscape(staIp) + "\"";
json += "}";
server.send(200, "application/json", json);
}
void handleWifiSave() {
String ssid = server.arg("ssid");
String password = server.arg("password");
ssid.trim();
if (ssid.isEmpty()) {
server.send(400, "text/plain", "SSID cannot be empty");
return;
}
saveWifiConfig(ssid, password);
bool connected = connectToWifi(configuredSsid, configuredPass);
if (!connected) {
startProvisioningAp();
}
server.sendHeader("Location", "/");
server.send(303);
}
void handleWifiDefault() {
saveWifiConfig(DEFAULT_WOKWI_SSID, DEFAULT_WOKWI_PASS);
connectToWifi(configuredSsid, configuredPass);
server.sendHeader("Location", "/");
server.send(303);
}
void handleWifiClear() {
clearWifiConfig();
WiFi.disconnect();
startProvisioningAp();
server.sendHeader("Location", "/");
server.send(303);
}
void handleNotFound() {
server.send(404, "text/plain", "Not found");
}
void setupServer() {
server.on("/", HTTP_GET, handleRoot);
server.on("/toggle", HTTP_GET, handleToggle);
server.on("/toggle", HTTP_POST, handleToggle);
server.on("/state", HTTP_GET, handleState);
server.on("/wifi", HTTP_POST, handleWifiSave);
server.on("/wifi/default", HTTP_GET, handleWifiDefault);
server.on("/wifi/clear", HTTP_POST, handleWifiClear);
server.onNotFound(handleNotFound);
server.begin();
Serial.println("HTTP server started on port 80");
}
void handleButton() {
bool reading = digitalRead(BUTTON_PIN);
if (reading != lastButtonReading) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > DEBOUNCE_MS) {
if (reading != buttonState) {
buttonState = reading;
if (buttonState == LOW) {
toggleBulb();
}
}
}
lastButtonReading = reading;
}
void setup() {
Serial.begin(115200);
delay(300);
Serial.println("\nESP32-C3 relay + bulb + button + web UI starting...");
pinMode(RELAY_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
setBulb(false);
loadWifiConfig();
startProvisioningAp();
bool connected = false;
if (!configuredSsid.isEmpty()) {
connected = connectToWifi(configuredSsid, configuredPass);
}
if (!connected) {
saveWifiConfig(DEFAULT_WOKWI_SSID, DEFAULT_WOKWI_PASS);
connected = connectToWifi(configuredSsid, configuredPass);
}
if (!connected) {
Serial.println("Running with provisioning AP only");
}
setupServer();
Serial.println("Open the web UI:");
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("- Device IP: http://%s/\n", WiFi.localIP().toString().c_str());
Serial.println("- In Wokwi with Private IoT Gateway: http://localhost:9080/");
}
if (apStarted) {
Serial.printf("- Provisioning AP IP: http://%s/\n", WiFi.softAPIP().toString().c_str());
}
}
void loop() {
handleButton();
server.handleClient();
delay(5);
}
AC - N
DC - GND
AC - L
DC - +5/12V
Toggle