· 5 months ago · Apr 24, 2025, 11:20 PM
1#include <ESP8266WiFi.h>
2#include <ESP8266WebServer.h>
3#include <EEPROM.h>
4#include <ArduinoJson.h>
5#include <ESP8266HTTPClient.h>
6#include <ESP8266httpUpdate.h>
7#include <Ticker.h>
8#include <DNSServer.h>
9#include <functional>
10#include <WiFiClientSecure.h>
11
12const char* CURRENT_FIRMWARE_VERSION = "v1.0.0";
13const char* API_PASSWORD = "mohansakthi";
14
15const int ledPins[] = {5, 4, 14, 12};
16const int numStrips = sizeof(ledPins) / sizeof(ledPins[0]);
17const int pirPin = 13;
18
19const char* hardcoded_ssid = "TP-Link_6525";
20const char* hardcoded_password = "reaperox2.4g";
21
22const char* apSSID = "ESP-LED-Controller-Setup";
23const char* apPassword = "1234567890";
24const IPAddress apIP(192, 168, 4, 1);
25const IPAddress apNetmask(255, 255, 255, 0);
26unsigned long wifiConnectTimeout = 20000;
27bool apMode = false;
28
29const char* gh_repo_user = "freak-head";
30const char* gh_repo_name = "espledlocalfirm";
31String latestVersionTag = "";
32String firmwareUrl = "";
33bool updateAvailable = false;
34bool updateInProgress = false;
35
36enum EffectMode {
37 EFFECT_NONE, EFFECT_BREATHE, EFFECT_SEQUENCE_ON, EFFECT_SEQUENCE_OFF,
38 EFFECT_CHASE, EFFECT_SWELL, EFFECT_RANDOM_SOFT, EFFECT_CYCLE,
39 EFFECT_CANDLE, EFFECT_WAVE, EFFECT_RAMP_UP, EFFECT_RAMP_DOWN
40};
41const char* effectNames[] = {
42 "none", "breathe", "sequence_on", "sequence_off", "chase",
43 "swell", "random_soft", "cycle", "candle", "wave",
44 "ramp_up", "ramp_down"
45};
46const int numEffects = sizeof(effectNames) / sizeof(effectNames[0]);
47EffectMode currentEffect = EFFECT_NONE;
48unsigned long effectStartTime = 0;
49int effectStep = 0;
50int effectLedBrightness[numStrips];
51
52struct DeviceState {
53 char wifiSSID[33];
54 char wifiPassword[65];
55 int ledBrightness[numStrips];
56 int masterBrightness;
57 EffectMode activeEffect;
58 bool pirEnabled;
59 uint32_t magic;
60};
61DeviceState currentState;
62const uint32_t EEPROM_MAGIC_NUMBER = 0xCAFEBABE;
63
64const int EEPROM_ADDRESS = 0;
65const int EEPROM_SIZE = sizeof(DeviceState);
66
67ESP8266WebServer server(80);
68DNSServer dnsServer;
69Ticker effectTicker;
70Ticker pirTicker;
71Ticker updateCheckTicker;
72bool pirState = false;
73bool lastPirReading = false;
74unsigned long lastPirChangeTime = 0;
75const unsigned long pirDebounceDelay = 50;
76
77void saveStateToEEPROM(bool saveCredentials = true);
78void loadStateFromEEPROM();
79void applyLedState(bool forceMasterBrightness = false);
80void setLedBrightness(int stripIndex, int brightness, bool persist = true);
81void setMultipleLeds(const char* stripIndicesStr, int brightness, bool stateOn, bool persist = true);
82void setMasterBrightness(int brightness, bool persist);
83void setActiveEffect(EffectMode newEffect, bool persist);
84void setPirEnabled(bool enabled, bool persist);
85String getStatusJson();
86void updateEffects();
87void readPIR();
88void checkForUpdates(bool forceCheck = false);
89int compareVersions(const String& v1, const String& v2);
90void startAPMode();
91void startSTAMode();
92bool attemptWifiConnection(const char* attemptSSID, const char* attemptPassword);
93bool isAuthenticated();
94bool handleCaptivePortal();
95
96bool isAuthenticated() {
97 if (!server.hasArg("password") || server.arg("password") != API_PASSWORD) {
98 server.send(401, "application/json", "{\"error\":\"Unauthorized\",\"message\":\"Missing or incorrect password parameter\"}");
99 return false;
100 }
101 return true;
102}
103
104void saveStateToEEPROM(bool saveCredentials) {
105 DeviceState stateToSave = currentState;
106 stateToSave.magic = EEPROM_MAGIC_NUMBER;
107 if(!saveCredentials) {
108 DeviceState tempCreds;
109 EEPROM.get(EEPROM_ADDRESS, tempCreds);
110 if(tempCreds.magic == EEPROM_MAGIC_NUMBER) {
111 strncpy(stateToSave.wifiSSID, tempCreds.wifiSSID, sizeof(stateToSave.wifiSSID)-1);
112 strncpy(stateToSave.wifiPassword, tempCreds.wifiPassword, sizeof(stateToSave.wifiPassword)-1);
113 stateToSave.wifiSSID[sizeof(stateToSave.wifiSSID)-1] = '\0';
114 stateToSave.wifiPassword[sizeof(stateToSave.wifiPassword)-1] = '\0';
115 } else {
116 Serial.println("Warning: Could not preserve WiFi creds during save, EEPROM might be uninitialized.");
117 stateToSave.wifiSSID[0] = '\0';
118 stateToSave.wifiPassword[0] = '\0';
119 }
120 }
121 EEPROM.put(EEPROM_ADDRESS, stateToSave);
122 if (!EEPROM.commit()) {
123 Serial.println("ERROR! EEPROM commit failed");
124 } else {
125 Serial.println("State saved to EEPROM.");
126 }
127}
128
129void loadStateFromEEPROM() {
130 Serial.println("Loading state from EEPROM...");
131 EEPROM.get(EEPROM_ADDRESS, currentState);
132 if (currentState.magic != EEPROM_MAGIC_NUMBER) {
133 Serial.println("EEPROM magic number mismatch or uninitialized. Loading defaults.");
134 memset(currentState.wifiSSID, 0, sizeof(currentState.wifiSSID));
135 memset(currentState.wifiPassword, 0, sizeof(currentState.wifiPassword));
136 currentState.masterBrightness = 1023;
137 for (int i = 0; i < numStrips; i++) currentState.ledBrightness[i] = 0;
138 currentState.activeEffect = EFFECT_NONE;
139 currentState.pirEnabled = false;
140 } else {
141 Serial.println("EEPROM data loaded successfully.");
142 currentState.wifiSSID[sizeof(currentState.wifiSSID)-1] = '\0';
143 currentState.wifiPassword[sizeof(currentState.wifiPassword)-1] = '\0';
144 Serial.print(" Loaded SSID from EEPROM: "); Serial.println(currentState.wifiSSID);
145 if (currentState.activeEffect < EFFECT_NONE || currentState.activeEffect >= numEffects) {
146 Serial.println("Invalid effect loaded, defaulting to NONE.");
147 currentState.activeEffect = EFFECT_NONE;
148 }
149 currentState.masterBrightness = constrain(currentState.masterBrightness, 0, 1023);
150 for(int i = 0; i < numStrips; i++) {
151 currentState.ledBrightness[i] = constrain(currentState.ledBrightness[i], 0, 1023);
152 }
153 }
154}
155
156void applyLedState(bool forceMasterBrightness) {
157 if (currentState.activeEffect != EFFECT_NONE) {
158 if (!effectTicker.active()) {
159 effectStartTime = millis();
160 effectStep = 0;
161 for(int i=0; i < numStrips; i++) { effectLedBrightness[i] = 0; }
162 effectTicker.attach_ms(50, updateEffects);
163 Serial.println("Effect ticker started.");
164 }
165 } else {
166 if (effectTicker.active()) {
167 effectTicker.detach();
168 Serial.println("Effect ticker stopped.");
169 }
170 for (int i = 0; i < numStrips; i++) {
171 int brightnessToSet;
172 if (forceMasterBrightness) {
173 brightnessToSet = (currentState.ledBrightness[i] > 0) ? currentState.masterBrightness : 0;
174 } else {
175 brightnessToSet = constrain(currentState.ledBrightness[i], 0, currentState.masterBrightness);
176 }
177 brightnessToSet = constrain(brightnessToSet, 0, 1023);
178 analogWrite(ledPins[i], brightnessToSet);
179 }
180 }
181}
182
183void setLedBrightness(int stripIndex, int brightness, bool persist) {
184 if (stripIndex >= 0 && stripIndex < numStrips) {
185 brightness = constrain(brightness, 0, 1023);
186 if (currentState.ledBrightness[stripIndex] != brightness) {
187 currentState.ledBrightness[stripIndex] = brightness;
188 Serial.printf("Set Strip %d direct brightness: %d\n", stripIndex + 1, brightness);
189 if (currentState.activeEffect == EFFECT_NONE) applyLedState(false);
190 if (persist) saveStateToEEPROM(false);
191 }
192 }
193}
194
195void setMultipleLeds(const char* stripIndicesStr, int brightness, bool stateOn, bool persist) {
196 bool changed = false;
197 brightness = constrain(brightness, 0, 1023);
198 int targetBrightness = stateOn ? brightness : 0;
199 String indicesStr = String(stripIndicesStr);
200 int currentPos = 0;
201 int nextPos = 0;
202 while (nextPos != -1) {
203 nextPos = indicesStr.indexOf(',', currentPos);
204 String stripNumStr = (nextPos == -1) ? indicesStr.substring(currentPos) : indicesStr.substring(currentPos, nextPos);
205 stripNumStr.trim();
206 if (stripNumStr.length() > 0) {
207 int stripNum = stripNumStr.toInt();
208 if (stripNum >= 1 && stripNum <= numStrips) {
209 int stripIndex = stripNum - 1;
210 if (currentState.ledBrightness[stripIndex] != targetBrightness) {
211 currentState.ledBrightness[stripIndex] = targetBrightness;
212 changed = true;
213 Serial.printf("Set Strip %d direct brightness: %d\n", stripNum, targetBrightness);
214 }
215 } else { Serial.printf("Invalid strip number in list: %s\n", stripNumStr.c_str()); }
216 }
217 if (nextPos != -1) currentPos = nextPos + 1;
218 }
219 if (changed) {
220 if (currentState.activeEffect == EFFECT_NONE) applyLedState(false);
221 if (persist) saveStateToEEPROM(false);
222 }
223}
224
225void setMasterBrightness(int brightness, bool persist) {
226 brightness = constrain(brightness, 0, 1023);
227 if (currentState.masterBrightness != brightness) {
228 currentState.masterBrightness = brightness;
229 Serial.printf("Set Master Brightness: %d\n", brightness);
230 applyLedState(false);
231 if (persist) saveStateToEEPROM(false);
232 }
233}
234
235void setActiveEffect(EffectMode newEffect, bool persist) {
236 if (newEffect < EFFECT_NONE || newEffect >= numEffects) return;
237 if (currentState.activeEffect != newEffect) {
238 currentState.activeEffect = newEffect;
239 Serial.printf("Set Active Effect: %s\n", effectNames[currentState.activeEffect]);
240 applyLedState(false);
241 if (persist) saveStateToEEPROM(false);
242 }
243}
244
245void setPirEnabled(bool enabled, bool persist) {
246 if (currentState.pirEnabled != enabled) {
247 currentState.pirEnabled = enabled;
248 Serial.printf("PIR Sensor %s\n", enabled ? "Enabled" : "Disabled");
249 if (!enabled) pirState = false;
250 if (persist) saveStateToEEPROM(false);
251 }
252}
253
254String getStatusJson() {
255 StaticJsonDocument<1024> doc;
256 doc["firmware_version"] = CURRENT_FIRMWARE_VERSION;
257 doc["update_available"] = updateAvailable;
258 doc["latest_version"] = latestVersionTag;
259 doc["update_in_progress"] = updateInProgress;
260 doc["wifi_mode"] = apMode ? "AP" : "STA";
261 doc["ip_address"] = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
262 doc["ap_ssid"] = apMode ? String(apSSID) : "";
263 doc["sta_ssid"] = apMode ? "" : WiFi.SSID();
264 doc["configured_sta_ssid"] = String(currentState.wifiSSID);
265 doc["using_hardcoded_wifi"] = (!apMode && WiFi.SSID() == hardcoded_ssid);
266 doc["wifi_connected"] = (WiFi.status() == WL_CONNECTED);
267 doc["master_brightness"] = currentState.masterBrightness;
268 doc["active_effect"] = effectNames[currentState.activeEffect];
269 JsonObject pirStatus = doc.createNestedObject("pir");
270 pirStatus["enabled"] = currentState.pirEnabled;
271 pirStatus["motion_detected"] = pirState;
272 JsonArray ledStates = doc.createNestedArray("leds");
273 for (int i = 0; i < numStrips; i++) {
274 JsonObject strip = ledStates.createNestedObject();
275 strip["strip"] = i + 1; strip["pin"] = ledPins[i];
276 int reportedBrightness = (currentState.activeEffect == EFFECT_NONE) ? constrain(currentState.ledBrightness[i], 0, currentState.masterBrightness) : effectLedBrightness[i];
277 strip["brightness"] = reportedBrightness;
278 strip["state"] = (reportedBrightness > 0) ? "on" : "off";
279 strip["base_brightness"] = currentState.ledBrightness[i];
280 }
281 String output; serializeJson(doc, output); return output;
282}
283
284void handleRoot() {
285 String html = "<html><head><title>ESP LED Control"; if (apMode) html += " - WiFi Setup"; html += "</title>";
286 html += "<style>body{font-family: sans-serif; max-width: 600px; margin: auto; padding: 15px;}";
287 html += "label{display: block; margin-top: 10px;} input{width: 100%; padding: 8px; margin-top: 5px; box-sizing: border-box;}";
288 html += "button{padding: 10px 15px; margin-top: 15px; cursor: pointer;} .status{background-color: #f0f0f0; border: 1px solid #ccc; padding: 10px; margin-top: 20px; white-space: pre-wrap; word-wrap: break-word;}";
289 html += ".error{color: red; font-weight: bold;}</style>";
290 html += "</head><body><h1>ESP8266 LED Controller</h1>";
291 if (apMode) {
292 html += "<h2>WiFi Setup Mode</h2><p>Connect this device to your WiFi network.</p>";
293 html += "<p class='error'>You are currently connected to the device's setup network (<strong>"; html += apSSID; html += "</strong>).</p>";
294 html += "<form method='POST' action='/wifi'>";
295 html += "<label for='ssid'>WiFi Network Name (SSID):</label><input type='text' id='ssid' name='ssid' required>";
296 html += "<label for='pass'>WiFi Password:</label><input type='password' id='pass' name='pass'>";
297 html += "<label for='pwd'>Device API Password (for this action):</label><input type='password' id='pwd' name='password' required>";
298 html += "<button type='submit'>Save & Connect</button></form>";
299 } else {
300 html += "<h2>Device Control Panel</h2><p>Control LEDs, effects, and check for updates.</p>";
301 html += "<p><a href='/wifi_setup?password=" + String(API_PASSWORD) + "'>Switch to WiFi Setup Mode</a> (Requires password & reboot)</p>";
302 html += "<h2>API Password Note</h2><p>Protected actions require the parameter <code>?password=" + String(API_PASSWORD) + "</code> appended to the URL.</p>";
303 }
304 html += "<h2>Status</h2><div class='status' id='status'>Loading...</div>";
305 html += "<script>function fetchStatus() { fetch('/status').then(response => response.ok ? response.json() : Promise.reject('Failed to fetch'))";
306 html += ".then(data => { document.getElementById('status').textContent = JSON.stringify(data, null, 2); })";
307 html += ".catch(error => { console.error('Error fetching status:', error); document.getElementById('status').textContent = 'Error loading status.'; }); }";
308 html += "setInterval(fetchStatus, 5000); fetchStatus();</script>";
309 html += "</body></html>"; server.send(200, "text/html", html);
310}
311
312void handleStatus() { server.send(200, "application/json", getStatusJson()); }
313
314void handleWifiSave() {
315 if (!isAuthenticated()) return;
316 if (!server.hasArg("ssid") || server.arg("ssid") == "") { server.send(400, "text/plain", "Missing SSID"); return; }
317 String ssid = server.arg("ssid"); String pass = server.arg("pass");
318 strncpy(currentState.wifiSSID, ssid.c_str(), sizeof(currentState.wifiSSID) - 1); currentState.wifiSSID[sizeof(currentState.wifiSSID) - 1] = '\0';
319 strncpy(currentState.wifiPassword, pass.c_str(), sizeof(currentState.wifiPassword) - 1); currentState.wifiPassword[sizeof(currentState.wifiPassword) - 1] = '\0';
320 Serial.println("New WiFi credentials received via API:"); Serial.print(" SSID: "); Serial.println(currentState.wifiSSID);
321 saveStateToEEPROM(true);
322 String html = "<html><head><title>WiFi Saved</title><meta http-equiv='refresh' content='10;url=/'></head><body><h1>WiFi Settings Saved!</h1><p>Device will now attempt to connect to '<strong>";
323 html += ssid; html += "</strong>'.</p><p>Rebooting in 5 seconds...</p></body></html>";
324 server.send(200, "text/html", html); delay(5000); ESP.restart();
325}
326
327void handleWifiSetup() {
328 if (!isAuthenticated()) return;
329 String html = "<html><head><title>Entering WiFi Setup</title></head><body><h1>Entering WiFi Setup Mode</h1><p>Device will reboot into Access Point mode.</p>";
330 html += "<p>Connect to '<strong>"; html += apSSID; html += "</strong>' (password: <strong>"; html += apPassword; html += "</strong>) and browse to <a href='http://192.168.4.1'>http://192.168.4.1</a>.</p>";
331 html += "<p>Rebooting now...</p></body></html>"; server.send(200, "text/html", html);
332 Serial.println("Clearing WiFi credentials to force AP mode on reboot.");
333 memset(currentState.wifiSSID, 0, sizeof(currentState.wifiSSID)); memset(currentState.wifiPassword, 0, sizeof(currentState.wifiPassword));
334 saveStateToEEPROM(true); delay(2000); ESP.restart();
335}
336
337void handleLedControl() {
338 if (!isAuthenticated()) return;
339 bool stateOn = false; int brightness = -1; bool stateParam = false; bool brightnessParam = false; bool persist = true; String stripsToSet = "";
340 if (server.hasArg("brightness")) { brightness = server.arg("brightness").toInt(); brightness = constrain(brightness, 0, 1023); brightnessParam = true; stateOn = (brightness > 0); stateParam = true; }
341 else if (server.hasArg("state")) { stateOn = server.arg("state").equalsIgnoreCase("on"); stateParam = true; brightness = stateOn ? currentState.masterBrightness : 0; brightnessParam = true; }
342 if (!stateParam && !brightnessParam) { server.send(400, "application/json", "{\"error\":\"Missing state or brightness parameter\"}"); return; }
343 if (server.hasArg("all") && server.arg("all").equalsIgnoreCase("true")) { for (int i = 1; i <= numStrips; ++i) stripsToSet += String(i) + (i < numStrips ? "," : ""); }
344 else if (server.hasArg("strip")) { stripsToSet = server.arg("strip"); } else if (server.hasArg("strips")) { stripsToSet = server.arg("strips"); }
345 else { server.send(400, "application/json", "{\"error\":\"Missing strip, strips, or all parameter\"}"); return; }
346 if(currentState.activeEffect != EFFECT_NONE) setActiveEffect(EFFECT_NONE, persist);
347 setMultipleLeds(stripsToSet.c_str(), brightness, stateOn, persist); handleStatus();
348}
349
350void handleEffectControl() {
351 if (!isAuthenticated()) return; bool persist = true; bool changed = false;
352 if (server.hasArg("master_brightness")) { int mb = server.arg("master_brightness").toInt(); setMasterBrightness(mb, persist); changed = true; }
353 if (server.hasArg("mode")) { String modeStr = server.arg("mode"); modeStr.toLowerCase(); EffectMode requestedEffect = EFFECT_NONE; bool found = false;
354 for (int i = 0; i < numEffects; i++) { if (modeStr == effectNames[i]) { requestedEffect = (EffectMode)i; found = true; break; } }
355 if (found) { setActiveEffect(requestedEffect, persist); changed = true; } else { server.send(400, "application/json", "{\"error\":\"Invalid effect mode\"}"); return; } }
356 if (!changed) { server.send(400, "application/json", "{\"error\":\"Missing mode or master_brightness parameter\"}"); return; } handleStatus();
357}
358
359void handlePirControl() {
360 if (!isAuthenticated()) return;
361 if (server.hasArg("enabled")) { bool enable = server.arg("enabled").equalsIgnoreCase("true"); setPirEnabled(enable, true); handleStatus(); }
362 else { server.send(400, "application/json", "{\"error\":\"Missing enabled parameter (true/false)\"}"); }
363}
364
365void handleUpdateCheck() {
366 if (updateInProgress) { server.send(503, "application/json", "{\"error\":\"Update already in progress\"}"); return; }
367 checkForUpdates(true); handleStatus();
368}
369
370void handleUpdateTrigger() {
371 if (!isAuthenticated()) return;
372 if (updateInProgress) {
373 server.send(503, "application/json", "{\"error\":\"Update already in progress\"}");
374 return;
375 }
376 checkForUpdates(true);
377 if (!updateAvailable || firmwareUrl.isEmpty()) {
378 server.send(400, "application/json", "{\"error\":\"No update available or firmware URL missing\", \"latest_version\":\"" + latestVersionTag + "\"}");
379 return;
380 }
381
382 Serial.println("Starting OTA update via API request...");
383 updateInProgress = true;
384 server.send(202, "application/json", "{\"message\":\"Update started. Device will reboot if successful.\", \"url\":\"" + firmwareUrl + "\"}");
385 delay(100);
386
387 WiFiClientSecure clientSecure;
388 clientSecure.setInsecure(); // Necessary for GitHub without certificate validation
389
390 ESPhttpUpdate.setLedPin(LED_BUILTIN, LOW);
391 ESPhttpUpdate.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); // Fix: Enable following redirects
392
393 t_httpUpdate_return ret = ESPhttpUpdate.update(clientSecure, firmwareUrl);
394
395 updateInProgress = false;
396 ESPhttpUpdate.setLedPin(LED_BUILTIN, HIGH);
397
398 switch (ret) {
399 case HTTP_UPDATE_FAILED:
400 Serial.printf("HTTP_UPDATE_FAILED Error (%d): %s\n", ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
401 // Optionally send error back to client if possible (might reboot before this)
402 break;
403 case HTTP_UPDATE_NO_UPDATES:
404 Serial.println("HTTP_UPDATE_NO_UPDATES"); // Should not happen here as we checked version before
405 break;
406 case HTTP_UPDATE_OK:
407 Serial.println("HTTP_UPDATE_OK - Rebooting...");
408 // Will reboot automatically
409 break;
410 default:
411 Serial.printf("HTTP Update Unknown return code: %d\n", ret);
412 break;
413 }
414}
415
416
417void handleReboot() { if (!isAuthenticated()) return; server.send(200, "application/json", "{\"message\":\"Rebooting device...\"}"); delay(1000); ESP.restart(); }
418
419void handleNotFound() { if (apMode && handleCaptivePortal()) { /* Redirected */ } else { server.send(404, apMode ? "text/plain" : "application/json", apMode ? "AP Mode: Not Found. Go to http://192.168.4.1" : "{\"error\":\"Not Found\"}"); } }
420
421bool handleCaptivePortal() {
422 if (!apMode) return false;
423 String host = server.hostHeader();
424 IPAddress currentIP = WiFi.softAPIP();
425
426 // Check if the host header is not the AP's IP or assigned hostname
427 if (!host.equals(currentIP.toString()) && !host.equals(apSSID)) {
428 Serial.print("Captive portal redirect: Host="); Serial.print(host); Serial.print(" -> http://"); Serial.println(currentIP);
429 server.sendHeader("Location", String("http://") + currentIP.toString(), true);
430 server.send(302, "text/plain", ""); // 302 Found / Redirect
431 return true;
432 }
433 return false;
434}
435
436
437void updateEffects() {
438 if (currentState.activeEffect == EFFECT_NONE) return;
439 unsigned long currentTime = millis(); unsigned long elapsedTime = currentTime - effectStartTime; float effectProgress; int calculatedBrightness;
440 auto applyMaster = [&](int base) { return constrain((long)base * currentState.masterBrightness / 1023, 0, 1023); };
441 switch (currentState.activeEffect) {
442 case EFFECT_BREATHE: { const int duration = 3000; effectProgress = fmod((float)elapsedTime, duration) / duration; calculatedBrightness = applyMaster((int)((sin(effectProgress * 2.0 * PI - PI / 2.0) + 1.0) / 2.0 * 1023)); for (int i = 0; i < numStrips; i++) { effectLedBrightness[i] = calculatedBrightness; analogWrite(ledPins[i], calculatedBrightness); } break; }
443 case EFFECT_SEQUENCE_ON: { const int stepDuration = 500; int currentStep = (elapsedTime / stepDuration) % numStrips; calculatedBrightness = applyMaster(1023); for (int i = 0; i < numStrips; i++) { int b = (i <= currentStep) ? calculatedBrightness : 0; effectLedBrightness[i] = b; analogWrite(ledPins[i], b); } break; }
444 case EFFECT_SEQUENCE_OFF: { const int stepDuration = 500; int currentStep = (elapsedTime / stepDuration) % numStrips; calculatedBrightness = applyMaster(1023); for(int i=0; i<numStrips; i++) { int b = (i <= currentStep) ? 0 : calculatedBrightness; effectLedBrightness[i] = b; analogWrite(ledPins[i], b); } break; }
445 case EFFECT_CHASE: { const int stepDuration = 250; int currentStep = (elapsedTime / stepDuration) % numStrips; calculatedBrightness = applyMaster(1023); for(int i=0; i<numStrips; i++) { int b = (i == currentStep) ? calculatedBrightness : 0; effectLedBrightness[i] = b; analogWrite(ledPins[i], b); } break; }
446 case EFFECT_SWELL: { const int duration = 2000; effectProgress = fmod((float)elapsedTime, duration) / duration; float sineVal = (sin(effectProgress * 2.0 * PI - PI / 2.0) + 1.0) / 2.0; int brightCenter = applyMaster((int)(sineVal * 1023)); int brightOuter = applyMaster((int)((1.0 - sineVal) * 1023)); if (numStrips == 4) { effectLedBrightness[0] = brightOuter; analogWrite(ledPins[0], brightOuter); effectLedBrightness[1] = brightCenter; analogWrite(ledPins[1], brightCenter); effectLedBrightness[2] = brightCenter; analogWrite(ledPins[2], brightCenter); effectLedBrightness[3] = brightOuter; analogWrite(ledPins[3], brightOuter); } else { for(int i=0; i<numStrips; i++) { effectLedBrightness[i] = brightCenter; analogWrite(ledPins[i], brightCenter); } } break; }
447 case EFFECT_RANDOM_SOFT: { const int changeInterval = 1500; if (elapsedTime / changeInterval != effectStep) { effectStep = elapsedTime / changeInterval; for(int i=0; i<numStrips; i++) { int targetBrightness = applyMaster(random(100, 700)); effectLedBrightness[i] = targetBrightness; analogWrite(ledPins[i], targetBrightness); } } break; }
448 case EFFECT_CYCLE: { const int cycleDurationPerLed = 2000; int currentLedIndex = (elapsedTime / cycleDurationPerLed) % numStrips; float ledProgress = fmod((float)elapsedTime, cycleDurationPerLed) / cycleDurationPerLed; calculatedBrightness = applyMaster((int)(sin(ledProgress * PI) * 1023)); for (int i = 0; i < numStrips; i++) { int b = (i == currentLedIndex) ? calculatedBrightness : 0; effectLedBrightness[i] = b; analogWrite(ledPins[i], b); } break; }
449 case EFFECT_CANDLE: { const int changeInterval = 150; if (elapsedTime / changeInterval != effectStep) { effectStep = elapsedTime / changeInterval; for (int i = 0; i < numStrips; i++) { int baseBright = applyMaster(800); int flicker = applyMaster(random(-150, 150)); calculatedBrightness = constrain(baseBright + flicker, 0, applyMaster(1023)); effectLedBrightness[i] = calculatedBrightness; analogWrite(ledPins[i], calculatedBrightness); } } break; }
450 case EFFECT_WAVE: { const int waveDuration = 5000; const float waveLength = (float)numStrips * 1.5; effectProgress = fmod((float)elapsedTime, waveDuration) / waveDuration; for (int i = 0; i < numStrips; i++) { float posOffset = (float)i / waveLength; float sineVal = (sin((effectProgress - posOffset) * 2.0 * PI) + 1.0) / 2.0; calculatedBrightness = applyMaster((int)(sineVal * 1023)); effectLedBrightness[i] = calculatedBrightness; analogWrite(ledPins[i], calculatedBrightness); } break; }
451 case EFFECT_RAMP_UP: { const int duration = 5000; effectProgress = min(1.0f, (float)elapsedTime / duration); calculatedBrightness = applyMaster((int)(effectProgress * 1023)); for(int i=0; i<numStrips; i++) { effectLedBrightness[i] = calculatedBrightness; analogWrite(ledPins[i], calculatedBrightness); } break; }
452 case EFFECT_RAMP_DOWN: { const int duration = 5000; effectProgress = min(1.0f, (float)elapsedTime / duration); calculatedBrightness = applyMaster((int)((1.0 - effectProgress) * 1023)); for(int i=0; i<numStrips; i++) { effectLedBrightness[i] = calculatedBrightness; analogWrite(ledPins[i], calculatedBrightness); } break; }
453 default: setActiveEffect(EFFECT_NONE, true); break;
454 }
455}
456
457void readPIR() {
458 bool currentReading = digitalRead(pirPin); if (currentReading != lastPirReading) { lastPirChangeTime = millis(); } if ((millis() - lastPirChangeTime) > pirDebounceDelay) { if (currentReading != pirState) { if (currentState.pirEnabled) { pirState = currentReading; Serial.printf("PIR Motion: %s\n", pirState ? "Yes" : "No"); } else if (pirState) { pirState = false; Serial.println("PIR Motion Cleared (Sensor Disabled)"); } } } lastPirReading = currentReading;
459}
460
461int compareVersions(const String& v1, const String& v2) {
462 String v1_num = v1; v1_num.replace("v", "");
463 String v2_num = v2; v2_num.replace("v", "");
464 return v1_num.compareTo(v2_num);
465}
466
467void checkForUpdates(bool forceCheck) {
468 static unsigned long lastUpdateCheck = 0;
469 if (!forceCheck && millis() - lastUpdateCheck < 3600000) return;
470 if (WiFi.status() != WL_CONNECTED) return;
471
472 lastUpdateCheck = millis();
473 Serial.println("Checking for firmware updates...");
474 String releaseUrl = "https://api.github.com/repos/";
475 releaseUrl += gh_repo_user; releaseUrl += "/";
476 releaseUrl += gh_repo_name; releaseUrl += "/releases/latest";
477
478 firmwareUrl = "";
479 latestVersionTag = "";
480 updateAvailable = false;
481
482 WiFiClientSecure client;
483 client.setInsecure();
484 HTTPClient http;
485
486 Serial.print("Requesting URL: "); Serial.println(releaseUrl);
487
488 if (http.begin(client, releaseUrl)) {
489 http.setUserAgent("ESP8266-http-Update"); // Good practice to set User Agent
490 int httpCode = http.GET();
491 if (httpCode == HTTP_CODE_OK) {
492 DynamicJsonDocument doc(2048);
493 DeserializationError error = deserializeJson(doc, http.getStream());
494 if (!error) {
495 latestVersionTag = doc["tag_name"].as<String>();
496 if (!latestVersionTag.isEmpty() && compareVersions(latestVersionTag, CURRENT_FIRMWARE_VERSION) > 0) {
497 Serial.printf("Newer version available: %s (Current: %s)\n", latestVersionTag.c_str(), CURRENT_FIRMWARE_VERSION);
498 JsonArray assets = doc["assets"];
499 for (JsonObject asset : assets) {
500 if (asset["name"].as<String>() == "firmware.bin") {
501 firmwareUrl = asset["browser_download_url"].as<String>();
502 updateAvailable = true;
503 Serial.print("Firmware URL: "); Serial.println(firmwareUrl);
504 break;
505 }
506 }
507 if (!updateAvailable) Serial.println("ERROR: firmware.bin not found in latest release assets!");
508 } else {
509 Serial.print("Current version "); Serial.print(CURRENT_FIRMWARE_VERSION);
510 Serial.print(", Latest fetched version "); Serial.print(latestVersionTag);
511 Serial.println(". No new update available or version is not newer.");
512 }
513 } else {
514 Serial.printf("deserializeJson() failed: %s\n", error.c_str());
515 }
516 } else {
517 Serial.printf("HTTP GET failed, error: %s (Code: %d)\n", http.errorToString(httpCode).c_str(), httpCode);
518 }
519 http.end();
520 } else {
521 Serial.println("Unable to connect to GitHub API");
522 }
523}
524
525
526bool attemptWifiConnection(const char* attemptSSID, const char* attemptPassword) {
527 if (attemptSSID == nullptr || strlen(attemptSSID) == 0) return false;
528 Serial.printf("Attempting to connect to SSID: %s\n", attemptSSID);
529 if (attemptPassword == nullptr) {
530 WiFi.begin(attemptSSID);
531 } else {
532 WiFi.begin(attemptSSID, attemptPassword);
533 }
534
535 unsigned long startAttemptTime = millis();
536 while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < wifiConnectTimeout) {
537 delay(500); Serial.print(".");
538 }
539 Serial.println();
540 return (WiFi.status() == WL_CONNECTED);
541}
542
543void startAPMode() {
544 Serial.println("Starting AP Mode..."); apMode = true;
545 WiFi.disconnect(true); // Ensure STA is off
546 WiFi.mode(WIFI_AP);
547 WiFi.softAPConfig(apIP, apIP, apNetmask);
548 bool result = WiFi.softAP(apSSID, apPassword);
549
550 if(result) {
551 Serial.println("AP Started");
552 Serial.print("AP IP address: "); Serial.println(WiFi.softAPIP());
553 Serial.print("AP SSID: "); Serial.println(apSSID);
554 dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
555 dnsServer.start(53, "*", apIP);
556 } else {
557 Serial.println("AP Failed to start");
558 // Maybe reboot?
559 delay(1000);
560 ESP.restart();
561 }
562
563 if (currentState.magic != EEPROM_MAGIC_NUMBER) { Serial.println("Saving default state to EEPROM in AP mode.");
564 currentState.masterBrightness = 1023; for (int i = 0; i < numStrips; i++) currentState.ledBrightness[i] = 0;
565 currentState.activeEffect = EFFECT_NONE; currentState.pirEnabled = false; currentState.magic = EEPROM_MAGIC_NUMBER; saveStateToEEPROM(true); }
566}
567
568void startSTAMode() {
569 Serial.println("Starting STA Mode..."); apMode = false;
570 WiFi.softAPdisconnect(true); // Ensure AP is off
571 WiFi.mode(WIFI_STA);
572 bool connected = false;
573
574 if (strlen(currentState.wifiSSID) > 0) {
575 Serial.println("Trying connection with EEPROM credentials...");
576 connected = attemptWifiConnection(currentState.wifiSSID, currentState.wifiPassword);
577 } else { Serial.println("No WiFi credentials stored in EEPROM."); }
578
579 if (!connected && strlen(hardcoded_ssid) > 0) {
580 Serial.println("EEPROM connection failed or skipped. Trying hardcoded credentials...");
581 connected = attemptWifiConnection(hardcoded_ssid, hardcoded_password);
582 } else if (!connected) { Serial.println("No hardcoded SSID defined."); }
583
584 if (connected) {
585 Serial.println("WiFi connected successfully!"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); Serial.print("Connected to SSID: "); Serial.println(WiFi.SSID());
586 updateCheckTicker.attach(3600, [](){ checkForUpdates(false); });
587 checkForUpdates(true);
588 } else {
589 Serial.println("Failed to connect using EEPROM and Hardcoded credentials. Falling back to AP Mode.");
590 WiFi.disconnect(true); // Disconnect from any failed attempts
591 startAPMode();
592 }
593}
594
595void setup() {
596 pinMode(LED_BUILTIN, OUTPUT);
597 digitalWrite(LED_BUILTIN, HIGH);
598 Serial.begin(115200); Serial.println("\n\nESP8266 Multi-Strip LED Controller (AP+OTA+Fallback)"); Serial.print("Firmware Version: "); Serial.println(CURRENT_FIRMWARE_VERSION);
599 EEPROM.begin(EEPROM_SIZE); loadStateFromEEPROM(); pinMode(pirPin, INPUT); for (int i = 0; i < numStrips; i++) pinMode(ledPins[i], OUTPUT);
600 analogWriteRange(1023); applyLedState(false);
601 WiFi.persistent(false); // Prevent SDK from saving WiFi config automatically
602
603 startSTAMode();
604
605 server.on("/", HTTP_GET, handleRoot);
606 server.on("/status", HTTP_GET, handleStatus);
607 server.on("/led", HTTP_GET, handleLedControl);
608 server.on("/effect", HTTP_GET, handleEffectControl);
609 server.on("/pir", HTTP_GET, handlePirControl);
610 server.on("/wifi", HTTP_POST, handleWifiSave);
611 server.on("/wifi_setup", HTTP_GET, handleWifiSetup);
612 server.on("/update/check", HTTP_GET, handleUpdateCheck);
613 server.on("/update/trigger", HTTP_GET, handleUpdateTrigger);
614 server.on("/reboot", HTTP_GET, handleReboot);
615
616 server.onNotFound(handleNotFound);
617 server.begin(); Serial.println("HTTP server started");
618 pirTicker.attach_ms(100, readPIR); Serial.println("Setup complete.");
619}
620
621void loop() {
622 if (apMode) { dnsServer.processNextRequest(); }
623 server.handleClient();
624 yield(); // Give time to background processes
625}