#include <WiFi.h>
#include <WebServer.h>
#include <PZEM004Tv30.h>
// --- إعدادات المنافذ ---
#define RELAY_PIN 23
#define PZEM_RX_PIN 16
#define PZEM_TX_PIN 17
// --- إعدادات الواي فاي ---
const char* ssid = "Smart-Energy-Meter";
const char* password = "password123";
WebServer server(80);
HardwareSerial pzemSerial(2);
PZEM004Tv30 pzem(pzemSerial, PZEM_RX_PIN, PZEM_TX_PIN);
// --- المتغيرات الرئيسية ---
float remainingEnergy = 0.0;
float totalCapacity = 0.0;
float consumedEnergy = 0.0;
float voltage = 0.0, current = 0.0, power = 0.0;
unsigned long lastUpdateTime = 0;
bool currentRelayState = false;
// --- دالة التحكم الذكي بالريلاي (تعتمد العزل التام للـ 3.3V) ---
void setRelayState(bool turnOn) {
currentRelayState = turnOn;
if (turnOn) {
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // سحب الإشارة للـ GND (تشغيل)
} else {
pinMode(RELAY_PIN, INPUT); // عزل المنفذ تماماً لمنع تسريب الـ 1.7V (إطفاء قطعي)
}
}
// --- واجهة الموقع ---
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>منظومة الطاقة - وضع الفحص</title>
<style>
:root {
--bg-color: #0f172a; --card-bg: #1e293b; --accent-green: #10b981;
--accent-blue: #3b82f6; --accent-red: #ef4444; --accent-amber: #f59e0b;
--text-main: #f8fafc; --text-sub: #94a3b8;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', sans-serif; }
body { background-color: var(--bg-color); color: var(--text-main); padding: 20px; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.container { width: 100%; max-width: 480px; display: flex; flex-direction: column; gap: 20px; }
.header { text-align: center; padding: 10px 0; }
.header h1 { font-size: 1.6rem; color: var(--text-main); }
.card { background: var(--card-bg); border-radius: 16px; padding: 20px; border: 1px solid rgba(255,255,255,0.05); box-shadow: 0 10px 25px rgba(0,0,0,0.3); }
.balance-box { text-align: center; }
.balance-title { font-size: 0.9rem; color: var(--text-sub); margin-bottom: 8px; }
.balance-value { font-size: 2.2rem; font-weight: 800; color: var(--accent-amber); }
.balance-unit { font-size: 1rem; color: var(--text-sub); }
.usage-section { margin-top: 15px; }
.usage-header { display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--text-sub); margin-bottom: 6px; }
.progress-bar-bg { width: 100%; height: 12px; background: #334155; border-radius: 6px; overflow: hidden; }
.progress-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent-green), var(--accent-amber), var(--accent-red)); transition: width 0.5s ease; }
.metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px; }
.metric-card { background: rgba(15, 23, 42, 0.6); padding: 12px 8px; border-radius: 12px; text-align: center; border: 1px solid rgba(255,255,255,0.03); }
.metric-value { font-size: 1.1rem; font-weight: 700; margin-top: 4px; }
.metric-label { font-size: 0.75rem; color: var(--text-sub); }
.status-badge { width: 100%; padding: 14px; border-radius: 12px; font-size: 1.1rem; font-weight: 700; text-align: center; }
.status-badge.on { background: rgba(16, 185, 129, 0.15); color: var(--accent-green); border: 1px solid var(--accent-green); }
.status-badge.off { background: rgba(239, 68, 68, 0.15); color: var(--accent-red); border: 1px solid var(--accent-red); }
.recharge-box h3 { font-size: 1rem; margin-bottom: 12px; }
.input-group { display: flex; gap: 8px; }
.code-input { flex: 1; padding: 12px; border-radius: 10px; border: 1px solid #475569; background: #0f172a; color: #fff; font-size: 1rem; text-align: center; letter-spacing: 2px; }
.submit-btn { padding: 12px 20px; border-radius: 10px; border: none; background: var(--accent-blue); color: white; font-weight: 600; cursor: pointer; }
.code-hint { font-size: 0.75rem; color: var(--text-sub); margin-top: 10px; line-height: 1.5; background: rgba(255,255,255,0.02); padding: 8px; border-radius: 8px; }
.badge { background: #334155; padding: 2px 6px; border-radius: 4px; color: var(--accent-amber); font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚡ منظومة التغذية التلقائية</h1>
</div>
<div class="card">
<div class="balance-box">
<div class="balance-title">الرصيد المتبقي</div>
<div class="balance-value" id="remEnergy">0.00000 <span class="balance-unit">kWh</span></div>
</div>
<div class="usage-section">
<div class="usage-header">
<span>مستوى الاستهلاك المتراكم</span>
<span id="usagePercent">0%</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progressFill"></div>
</div>
</div>
</div>
<div class="status-badge off" id="statusBadge">
🔴 التغذية متوقفة (أدخل كود للشحن)
</div>
<div class="card">
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">الفولتية</div>
<div class="metric-value" id="valVolts">0 V</div>
</div>
<div class="metric-card">
<div class="metric-label">التيار</div>
<div class="metric-value" id="valAmps">0.0 A</div>
</div>
<div class="metric-card">
<div class="metric-label">القدرة</div>
<div class="metric-value" id="valWatts">0 W</div>
</div>
</div>
</div>
<div class="card recharge-box">
<h3>💳 تعبئة كارت الفحص</h3>
<div class="input-group">
<input type="number" id="rechargeCode" class="code-input" placeholder="أدخل الكود" maxlength="4">
<button class="submit-btn" onclick="submitCode()">تأكيد</button>
</div>
<div class="code-hint">
📌 <b>أكواد الفحص السريع (لحمل 26W):</b><br>
• <span class="badge">1010</span> : تشغيل لمدة <b>دقيقة واحدة</b><br>
• <span class="badge">2020</span> : تشغيل لمدة <b>دقيقتين</b><br>
• <span class="badge">3020</span> : تشغيل لمدة <b>3 دقائق</b>
</div>
</div>
</div>
<script>
setInterval(fetchData, 1000);
function fetchData() {
fetch('/data').then(res => res.json()).then(data => {
document.getElementById('remEnergy').innerText = data.remEnergy.toFixed(5) + " kWh";
document.getElementById('valVolts').innerText = data.voltage.toFixed(1) + " V";
document.getElementById('valAmps').innerText = data.current.toFixed(2) + " A";
document.getElementById('valWatts').innerText = data.power.toFixed(0) + " W";
let percent = data.totalCap > 0 ? ((data.consumed / data.totalCap) * 100) : 0;
if(percent > 100) percent = 100;
document.getElementById('progressFill').style.width = percent.toFixed(1) + "%";
document.getElementById('usagePercent').innerText = percent.toFixed(1) + "%";
let badge = document.getElementById('statusBadge');
if (data.relayState) {
badge.className = "status-badge on";
badge.innerHTML = "🟢 التغذية شغالـة (جاري الاستهلاك)";
} else {
badge.className = "status-badge off";
badge.innerHTML = "🔴 التغذية متوقفة (انتهى الرصيد)";
}
});
}
function submitCode() {
let code = document.getElementById('rechargeCode').value;
if(!code) return alert("أدخل الكود أولاً!");
fetch('/recharge?code=' + code, { method: 'POST' }).then(res => res.text()).then(msg => {
alert(msg);
document.getElementById('rechargeCode').value = '';
});
}
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
void handleData() {
String json = "{";
json += "\"remEnergy\":" + String(remainingEnergy, 5) + ",";
json += "\"totalCap\":" + String(totalCapacity, 5) + ",";
json += "\"consumed\":" + String(consumedEnergy, 5) + ",";
json += "\"voltage\":" + String(voltage, 1) + ",";
json += "\"current\":" + String(current, 2) + ",";
json += "\"power\":" + String(power, 1) + ",";
json += "\"relayState\":" + String(currentRelayState ? "true" : "false");
json += "}";
server.send(200, "application/json", json);
}
void handleRecharge() {
if (server.hasArg("code")) {
String code = server.arg("code");
float addEnergy = 0.0;
if (code == "1010") addEnergy = 0.000433; // 1 دقيقة
else if (code == "2020") addEnergy = 0.000867; // 2 دقيقة
else if (code == "3020") addEnergy = 0.001300; // 3 دقائق
if (addEnergy > 0) {
remainingEnergy += addEnergy;
totalCapacity += addEnergy;
server.send(200, "text/plain", "تم التفعيل بنجاح! الرصيد كافٍ للفحص.");
} else {
server.send(400, "text/plain", "⚠️ كود غير صحيح!");
}
}
}
void setup() {
Serial.begin(115200);
// إطفاء الريلاي تماماً في البداية
setRelayState(false);
pzemSerial.begin(9600, SERIAL_8N1, PZEM_RX_PIN, PZEM_TX_PIN);
WiFi.softAP(ssid, password);
Serial.println("WiFi Started!");
server.on("/", handleRoot);
server.on("/data", handleData);
server.on("/recharge", HTTP_POST, handleRecharge);
server.begin();
}
void loop() {
server.handleClient();
if (millis() - lastUpdateTime >= 1000) {
lastUpdateTime = millis();
voltage = pzem.voltage();
current = pzem.current();
power = pzem.power();
if (isnan(voltage)) voltage = 0.0;
if (isnan(current)) current = 0.0;
if (isnan(power)) power = 0.0;
// تشغيل التغذية طالما هناك رصيد
if (remainingEnergy > 0) {
setRelayState(true);
if (power > 0) {
float consumedThisSecond = (power / 1000.0) / 3600.0;
remainingEnergy -= consumedThisSecond;
consumedEnergy += consumedThisSecond;
if (remainingEnergy <= 0) {
remainingEnergy = 0;
}
}
} else {
setRelayState(false); // قطع التغذية عادلاً وفوراً
}
}
}
رابط فديو الشرح