Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #include <ESP8266WiFi.h>
- #include <ESP8266WebServer.h>
- #include <DHT.h>
- #include <EEPROM.h>
- // —— Calibration ——
- // Adjust after testing your motor + microstepping
- const int FULL_STEPS = 800;
- // —— Wi-Fi & HTTP Server ——
- const char* ssid = "******";
- const char* password = "*****";
- ESP8266WebServer server(80);
- // —— Static IP Config ——
- IPAddress local_IP(192,168,11,101); // pick an unused address on your LAN
- IPAddress gateway(192,168,11,1); // your router
- IPAddress subnet(255,255,255,0); // standard mask
- // —— Sensors & Stepper ——
- #define DHTPIN D5
- #define DHTTYPE DHT11
- #define RAIN_PIN A0
- #define RAIN_THRESH 500
- DHT dht(DHTPIN, DHTTYPE);
- // Stepper driver pins (HW-135 / A4988 / DRV8825)
- #define STEP_PIN D1
- #define DIR_PIN D2
- // Motion state (non-blocking)
- volatile bool motorActive = false;
- volatile int motorDir = 0; // 0 = close, 1 = open
- volatile long motorStepsRemaining = 0;
- unsigned long lastStepMicros = 0;
- const int stepDelay = 800; // µs between steps
- // —— App State ——
- bool autoMode = false;
- bool windowLocked = true;
- float thresholdTemp = 25.0;
- float currTemp = NAN;
- bool isRaining = false;
- int windowPos = 0; // 0–100%
- const String unlockPin = "1972";
- // —— EEPROM Addresses ——
- const int ADDR_THRESHOLD = 0;
- const int ADDR_WINDOWPOS = ADDR_THRESHOLD + sizeof(float);
- const int ADDR_AUTOMODE = ADDR_WINDOWPOS + sizeof(int);
- void setup() {
- Serial.begin(115200);
- delay(100);if (!WiFi.config(local_IP, gateway, subnet)) {
- Serial.println("⚠️ Failed to configure static IP");
- }
- WiFi.begin(ssid, password);
- Serial.print("Connecting");
- while (WiFi.status() != WL_CONNECTED) {
- delay(500);
- Serial.print('.');
- }
- Serial.println("\nIP = " + WiFi.localIP().toString());
- dht.begin();
- EEPROM.begin(512);
- // Load threshold
- EEPROM.get(ADDR_THRESHOLD, thresholdTemp);
- if (isnan(thresholdTemp) || thresholdTemp < 10 || thresholdTemp > 50) {
- thresholdTemp = 25.0;
- }
- // Load saved window position if valid
- EEPROM.get(ADDR_WINDOWPOS, windowPos);
- if (windowPos < 0 || windowPos > 100) {
- windowPos = 0;
- }
- // Load autoMode
- byte modeByte = 0;
- EEPROM.get(ADDR_AUTOMODE, modeByte);
- autoMode = (modeByte == 1);
- // Stepper pins
- pinMode(STEP_PIN, OUTPUT);
- pinMode(DIR_PIN, OUTPUT);
- // HTTP endpoints
- server.on("/", HTTP_GET, handleRoot);
- server.on("/status", HTTP_GET, handleStatus);
- server.on("/open", HTTP_GET, handleOpen);
- server.on("/close", HTTP_GET, handleClose);
- server.on("/vent", HTTP_GET, handleVent);
- server.on("/toggleAuto", HTTP_GET, handleToggleAuto);
- server.on("/setThreshold", HTTP_GET, handleSetThreshold);
- server.on("/toggleLock", HTTP_GET, handleToggleLock);
- server.begin();
- }
- void loop() {
- server.handleClient();
- handleMotor(); // keep motor moving in background
- static unsigned long last = 0;
- if (millis() - last < 2000) return;
- last = millis();
- currTemp = dht.readTemperature();
- isRaining = (analogRead(RAIN_PIN) < RAIN_THRESH);
- // Force close on rain
- if (isRaining && !windowLocked && windowPos > 0) {
- startMoveByPercent(0, windowPos);
- windowPos = 0;
- saveWindowPos();
- Serial.println("Rain detected → auto-closing window");
- }
- // auto-mode window control
- if (autoMode && !windowLocked) {
- if (currTemp > thresholdTemp && !isRaining && windowPos < 100) {
- startMoveByPercent(1, 100 - windowPos);
- windowPos = 100;
- saveWindowPos();
- }
- else if ((currTemp < thresholdTemp || isRaining) && windowPos > 0) {
- startMoveByPercent(0, windowPos);
- windowPos = 0;
- saveWindowPos();
- }
- }
- }
- // Start a move by percent (non-blocking)
- void startMoveByPercent(int direction, int percentDelta) {
- percentDelta = constrain(percentDelta, 0, 100);
- long steps = (FULL_STEPS * percentDelta) / 100;
- if (steps <= 0) return;
- motorDir = direction;
- digitalWrite(DIR_PIN, motorDir ? HIGH : LOW);
- motorStepsRemaining = steps;
- motorActive = true;
- lastStepMicros = micros();
- }
- // Called in loop() to advance motor
- void handleMotor() {
- if (!motorActive) return;
- unsigned long now = micros();
- if (now - lastStepMicros >= stepDelay) {
- digitalWrite(STEP_PIN, HIGH);
- delayMicroseconds(2);
- digitalWrite(STEP_PIN, LOW);
- motorStepsRemaining--;
- lastStepMicros = now;
- if (motorStepsRemaining <= 0) {
- motorActive = false;
- }
- }
- }
- // EEPROM helper
- void saveWindowPos() {
- static int lastPos = -1;
- if (windowPos != lastPos) {
- EEPROM.put(ADDR_WINDOWPOS, windowPos);
- EEPROM.commit();
- lastPos = windowPos;
- }
- }
- // ——— Handlers ———
- void handleStatus() {
- String j = "{";
- j += "\"temp\":" + String(isnan(currTemp) ? -1 : currTemp, 1) + ",";
- j += "\"threshold\":" + String(thresholdTemp, 1) + ",";
- j += "\"rain\":"; j += (isRaining ? "true" : "false"); j += ",";
- j += "\"windowPos\":" + String(windowPos) + ",";
- j += "\"auto\":"; j += (autoMode ? "true" : "false"); j += ",";
- j += "\"locked\":"; j += (windowLocked ? "true" : "false");
- j += "}";
- server.sendHeader("Content-Type", "application/json; charset=utf-8");
- server.sendHeader("Cache-Control", "no-store, must-revalidate");
- server.sendHeader("X-Content-Type-Options", "nosniff");
- server.send(200, "application/json", j);
- }
- void handleOpen() {
- if (windowLocked) { server.send(403, "text/plain", "Locked"); return; }
- int delta = 100 - windowPos;
- if (delta > 0) {
- startMoveByPercent(1, delta);
- windowPos = 100;
- saveWindowPos();
- }
- server.send(200, "text/plain", "Open");
- }
- void handleClose() {
- if (windowLocked) { server.send(403, "text/plain", "Locked"); return; }
- int delta = windowPos;
- if (delta > 0) {
- startMoveByPercent(0, delta);
- windowPos = 0;
- saveWindowPos();
- }
- server.send(200, "text/plain", "Close");
- }
- void handleVent() {
- if (windowLocked) { server.send(403, "text/plain", "Locked"); return; }
- int newPos = min(windowPos + 25, 100);
- int delta = newPos - windowPos;
- if (delta > 0) {
- startMoveByPercent(1, delta);
- windowPos = newPos;
- saveWindowPos();
- }
- server.send(200, "text/plain", "Vent");
- }
- void handleToggleAuto() {
- autoMode = !autoMode;
- byte modeByte = autoMode ? 1 : 0;
- EEPROM.put(ADDR_AUTOMODE, modeByte);
- EEPROM.commit();
- Serial.println(autoMode ? "Auto mode enabled" : "Manual mode enabled");
- server.send(200, "text/plain", autoMode ? "Auto" : "Manual");
- }
- void handleSetThreshold() {
- if (!server.hasArg("value")) { server.send(400, "text/plain", "No value"); return; }
- float v = server.arg("value").toFloat();
- if (v < 10 || v > 50) { server.send(400, "text/plain", "Out of range"); return; }
- thresholdTemp = v;
- EEPROM.put(ADDR_THRESHOLD, thresholdTemp);
- EEPROM.commit();
- server.send(200, "text/plain", "Threshold set");
- }
- void handleToggleLock() {
- if (!server.hasArg("pin")) { server.send(400, "text/plain", "No PIN"); return; }
- if (server.arg("pin") == unlockPin) {
- windowLocked = !windowLocked;
- server.send(200, "text/plain", windowLocked ? "Locked" : "Unlocked");
- } else {
- server.send(403, "text/plain", "Bad PIN");
- }
- }
- void handleRoot() {
- // … your existing HTML/JS payload here …
- // (unchanged from your original sketch)
- String html = R"rawliteral(
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>LCARS Window Ctrl</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- html, body { margin:0; padding:0; height:100%; overflow:hidden;
- background:#000; color:#0FA; font-family:Segoe UI, sans-serif; }
- #starfield { position:fixed; top:0; left:0; width:100%; height:100%; z-index:0; }
- #clock { position:absolute; top:8px; right:12px; color:#0FA; font-size:0.9em; z-index:2; }
- #weatherCurrent {
- position:absolute; top:8px; left:12px; display:flex; align-items:center;
- color:#0FA; font-size:0.9em; z-index:2;
- }
- #weatherCurrent img { width:24px; height:24px; margin-right:6px; }
- #panel {
- position:absolute; top:40px; left:50%; transform:translateX(-50%);
- display:flex; flex-direction:column; align-items:center; z-index:1;
- }
- #dashboard {
- display:flex; justify-content:space-around;
- width:100%; max-width:540px; margin-bottom:16px;
- }
- .gauge-container {
- position:relative; width:140px; border:2px solid #0FA;
- border-radius:6px; padding:8px; background:rgba(0,0,0,0.5);
- cursor:pointer;
- }
- .gauge-container .threshold-text {
- margin-top:4px; font-size:0.85em; color:#0FA; text-align:center;
- }
- .lock-text {
- position:absolute; top:50%; left:50%;
- transform:translate(-50%,-50%);
- color:rgba(255,0,0,0.8); font-size:14px;
- font-weight:bold; display:none;
- }
- #controls {
- display:flex; flex-wrap:wrap; justify-content:center;
- gap:8px; background:rgba(0,0,0,0.5); padding:8px 0;
- }
- #controls button, #controls input {
- margin:4px; padding:6px 12px; border:none; border-radius:4px;
- background:#0FA; color:#000; font-size:0.9em; cursor:pointer;
- transition: box-shadow 0.2s, transform 0.1s;
- }
- #controls button:hover { background:#0CF; }
- #controls button:active {
- box-shadow:0 0 12px #0FA, 0 0 24px #0FA; transform:scale(0.95);
- }
- #modal {
- display:none; position:fixed; inset:0;
- background:rgba(0,0,0,0.8); justify-content:center; align-items:center;
- z-index:2;
- }
- #modal.show { display:flex; }
- #modal-content {
- background:#111; border:2px solid #0FA; border-radius:8px;
- padding:16px; max-height:80vh; width:90%; max-width:400px;
- overflow-y:auto; position:relative;
- }
- #modal-close {
- position:absolute; top:8px; right:8px; background:#0FA; color:#000;
- border:none; width:28px; height:28px; border-radius:50%; cursor:pointer;
- font-weight:bold;
- }
- #historyChart { width:100%!important; height:200px!important; }
- </style>
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
- </head>
- <body>
- <canvas id="starfield"></canvas>
- <div id="clock"></div>
- <div id="weatherCurrent">
- <img id="weatherIcon" src="" alt="">
- <span id="weatherText"></span>
- </div>
- <div id="panel">
- <div id="dashboard">
- <div class="gauge-container">
- <canvas id="gaugeTemp" width="120" height="120"></canvas>
- <div>Temp</div>
- <div id="tempThresholdDisplay" class="threshold-text"></div>
- </div>
- <div class="gauge-container">
- <canvas id="gaugeRain" width="120" height="120"></canvas>
- <div>Rain</div>
- <div id="rainForecast" class="threshold-text"></div>
- </div>
- <div class="gauge-container">
- <canvas id="gaugeWin" width="120" height="120"></canvas>
- <div>Window</div>
- <div id="lockOverlay" class="lock-text">LOCKED</div>
- </div>
- </div>
- <div id="controls">
- <button onclick="openWindow()">Open</button>
- <button onclick="closeWindow()">Close</button>
- <button onclick="ventWindow()">Vent</button>
- <button onclick="toggleAuto()">Mode: <span id="modeBtn">–</span></button>
- <input id="thresholdInput" type="number" step="0.1" min="10" max="50" placeholder="Temp Threshold">
- <button onclick="updateThreshold()">Set</button>
- <input id="pinInput" type="password" placeholder="PIN">
- <button onclick="toggleLock()">Lock/Unlock</button>
- </div>
- </div>
- <div id="modal">
- <div id="modal-content">
- <button id="modal-close">×</button>
- <h3 id="modal-title">History</h3>
- <canvas id="historyChart"></canvas>
- </div>
- </div>
- <script>
- // Starfield & dynamic star color
- const sf = document.getElementById('starfield'),
- sfc = sf.getContext('2d');
- let stars = [], starColor = '#FFF';
- function resizeSF(){ sf.width = innerWidth; sf.height = innerHeight; }
- window.addEventListener('resize', resizeSF);
- resizeSF();
- class Star {
- constructor(){ this.reset(); }
- reset(){
- this.x = Math.random()*sf.width;
- this.y = Math.random()*sf.height;
- this.z = Math.random()*sf.width;
- }
- update(){
- this.z -= 4;
- if (this.z <= 0) this.reset();
- }
- draw(){
- const x = (this.x - sf.width/2)*(sf.width/this.z) + sf.width/2,
- y = (this.y - sf.height/2)*(sf.width/this.z) + sf.height/2,
- sz= 1.5*(sf.width/this.z);
- sfc.fillStyle = starColor;
- sfc.beginPath(); sfc.arc(x,y,sz,0,2*Math.PI); sfc.fill();
- }
- }
- for(let i=0; i<150; i++) stars.push(new Star());
- (function anim(){
- sfc.fillStyle = 'rgba(0,0,0,0.3)';
- sfc.fillRect(0,0,sf.width,sf.height);
- stars.forEach(s=>{ s.update(); s.draw(); });
- requestAnimationFrame(anim);
- })();
- // Gauge class with bottom gap
- class Gauge {
- constructor(id, min, max, yellowThreshold, unit) {
- this.c = document.getElementById(id);
- this.ctx = this.c.getContext('2d');
- this.min = min;
- this.max = max;
- this.yellow = yellowThreshold;
- this.unit = unit;
- this.val = min;
- }
- draw() {
- const ctx = this.ctx,
- w = this.c.width,
- h = this.c.height,
- gap = Math.PI/6,
- start = Math.PI*1.5 + gap/2,
- range = 2*Math.PI - gap,
- pct = (this.val - this.min)/(this.max - this.min),
- theta = start + range*pct;
- ctx.clearRect(0,0,w,h);
- // background
- ctx.lineWidth = 6;
- ctx.strokeStyle = '#333';
- ctx.beginPath(); ctx.arc(w/2,h/2,w/2-8,start,start+range,false); ctx.stroke();
- // value arc
- ctx.strokeStyle = this.val > this.yellow ? '#F00' : '#0FA';
- ctx.beginPath(); ctx.arc(w/2,h/2,w/2-8,start,theta,false); ctx.stroke();
- // center text
- ctx.fillStyle = '#FFF';
- ctx.font = '18px Segoe UI';
- ctx.textAlign = 'center';
- ctx.fillText(this.val.toFixed(1) + this.unit, w/2, h/2 + 8);
- }
- update(v) {
- this.val = v;
- this.draw();
- }
- }
- // Instantiate & initial draw
- const gT = new Gauge('gaugeTemp', 0, 50, 30, '°C'),
- gR = new Gauge('gaugeRain', 0,100, 50, '%'),
- gW = new Gauge('gaugeWin', 0,100, 50, '%');
- gT.draw(); gR.draw(); gW.draw();
- // Element references
- const clockEl = document.getElementById('clock'),
- rainForecastEl = document.getElementById('rainForecast'),
- tempThresholdEl = document.getElementById('tempThresholdDisplay'),
- lockOverlay = document.getElementById('lockOverlay'),
- modeBtn = document.getElementById('modeBtn'),
- weatherIconEl = document.getElementById('weatherIcon'),
- weatherTextEl = document.getElementById('weatherText');
- // History buffers
- const hx = [], hT = [], hR = [], hW = [], MAX = 20;
- async function refreshStatus() {
- try {
- const res = await fetch('/status'),
- st = await res.json();
- // update gauges & UI
- gT.update(Math.max(0, st.temp));
- gR.update(st.rain ? 100 : 0);
- gW.update(st.windowPos);
- modeBtn.textContent = st.auto ? 'Auto' : 'Manual';
- tempThresholdEl.textContent = thresholdTemp.toFixed(1) + '°C';
- lockOverlay.style.display = st.locked ? 'block' : 'none';
- // dynamic star color from blue→red over 0–40°C
- let ratio = Math.min(Math.max(st.temp/40,0),1);
- let r = Math.round(255*ratio), b = Math.round(255*(1-ratio));
- starColor = `rgb(${r},0,${b})`;
- // history
- const t = new Date().toLocaleTimeString();
- hx.push(t); hT.push(Math.round(st.temp));
- hR.push(st.rain?100:0); hW.push(st.windowPos);
- if (hx.length > MAX) { hx.shift(); hT.shift(); hR.shift(); hW.shift(); }
- } catch(e) {
- console.warn('refreshStatus error', e);
- }
- }
- setInterval(refreshStatus, 2000);
- refreshStatus();
- // Chart.js stepped-bar history
- const modal = document.getElementById('modal'),
- modalClose = document.getElementById('modal-close'),
- titleEl = document.getElementById('modal-title'),
- ctxChart = document.getElementById('historyChart').getContext('2d');
- const historyChart = new Chart(ctxChart, {
- type:'bar',
- data:{ labels:[], datasets:[{ label:'', data:[], backgroundColor:'#0FA' }] },
- options:{
- responsive:true,
- maintainAspectRatio:false,
- scales:{ y:{ beginAtZero:true, max:100 } }
- }
- });
- document.querySelectorAll('.gauge-container').forEach(box => {
- box.addEventListener('click', () => {
- const cid = box.querySelector('canvas').id;
- let dataArr, label;
- if (cid === 'gaugeTemp') { dataArr = hT; label = 'Temp (°C)'; }
- else if (cid === 'gaugeRain'){ dataArr = hR; label = 'Rain (%)'; }
- else { dataArr = hW; label = 'Window (%)'; }
- historyChart.data.labels = hx;
- historyChart.data.datasets[0].data = dataArr;
- historyChart.data.datasets[0].label = label;
- historyChart.update();
- titleEl.textContent = label + ' History';
- modal.classList.add('show');
- });
- });
- modalClose.addEventListener('click', () => modal.classList.remove('show'));
- // WeatherAPI.com integration
- async function fetchWeather() {
- try {
- const key = '934ffa93bb054c47a97121247251109'; // ← your WeatherAPI.com key
- const lat = 51.307, lon = 0.26;
- const url = `https://api.weatherapi.com/v1/forecast.json`
- + `?key=${key}&q=${lat},${lon}&days=2&aqi=no&alerts=no`;
- const res = await fetch(url);
- const data = await res.json();
- // tomorrow’s rain chance
- const pop = data.forecast.forecastday[1].day.daily_chance_of_rain;
- rainForecastEl.textContent = `${pop}% Chance Of Rain`;
- // current weather
- const curr = data.current;
- weatherIconEl.src = 'https:' + curr.condition.icon;
- weatherIconEl.alt = curr.condition.text;
- weatherTextEl.textContent = `${Math.round(curr.temp_c)}°C – ${curr.condition.text}`;
- } catch(e) {
- console.warn('fetchWeather error', e);
- rainForecastEl.textContent = 'Forecast error';
- weatherIconEl.style.display = 'none';
- weatherTextEl.textContent = 'No weather';
- }
- }
- fetchWeather();
- setInterval(fetchWeather, 60*60*1000);
- // Real-time clock
- function updateClock() {
- clockEl.textContent = new Date().toLocaleTimeString();
- }
- setInterval(updateClock, 1000);
- updateClock();
- // Control handlers
- function openWindow() { fetch('/open'); }
- function closeWindow() { fetch('/close'); }
- function ventWindow() { fetch('/vent'); }
- function toggleAuto() { fetch('/toggleAuto'); }
- function updateThreshold(){
- const v = parseFloat(document.getElementById('thresholdInput').value);
- if (isNaN(v)) return;
- fetch('/setThreshold?value=' + v).then(_=>{
- thresholdTemp = v;
- tempThresholdEl.textContent = v.toFixed(1) + '°C';
- });
- }
- function toggleLock() {
- fetch('/toggleLock?pin=' + document.getElementById('pinInput').value);
- }
- setInterval(refresh, 2000);
- </script>
- </body>
- </html>
- )rawliteral";
- server.sendHeader("Content-Type", "text/html; charset=utf-8");
- server.sendHeader("Cache-Control", "no-store, must-revalidate");
- server.sendHeader("X-Content-Type-Options", "nosniff");
- server.send(200, "text/html; charset=utf-8", html);
- }
Advertisement
Add Comment
Please, Sign In to add comment