أساسيات تطوير الويب للعرض والتحكم
Web Development Basics for Display & Control
HTML للهيكل، CSS للتصميم المتجاوب الذي يعمل على هاتف المشرف في الحقل، و JavaScript لجلب البيانات ديناميكياً بـ Fetch API ورسمها بـ Chart.js — ونبني أول صفحة حية تعرض حرارة ورطوبة الصوبة.
نظرة عامة على السيشن
بنهاية السيشن ستكون قادراً على
- تبني هيكل صفحة بـ HTML دلالي (header / main / section).
- تصمم واجهة Responsive بـ CSS Grid و Media queries تعمل على الهاتف والشاشة.
- تكتب JavaScript يعدل الصفحة (DOM) ويستجيب للأحداث.
- تجلب بيانات JSON بـ
fetch()وasync/awaitوتتعامل مع الأخطاء. - ترسم سلسلة زمنية بـ Chart.js وتحدثها تلقائياً.
التطبيق في مزرعة الكلية
صفحة ويب بسيطة تعرض الحرارة والرطوبة الحالية للصوبة التجريبية ورسماً لرطوبة التربة آخر 6 ساعات، تتحدث كل 5 ثوانٍ من قاعدة بيانات المزرعة، ويفتحها المشرف من هاتفه داخل الصوبة.
المُخرج (Deliverable)
صفحة «الصوبة الحية» (index.html) متجاوبة تعرض القراءات الحقيقية وتنبيهاً عند جفاف التربة، مع رسم بياني يتحدث تلقائياً.
خطة السيشن (180 دقيقة)
- 0:00 مراجعة مراجعة: خط البيانات وقاعدة farm.db 10 د
- 0:10 شرح HTML + CSS: الهيكل والتصميم المتجاوب 30 د
- 0:40 شرح JavaScript: DOM، الأحداث، JSON، fetch و async 30 د
- 1:10 تطبيق Lab: صفحة الصوبة الحية من الصفر 30 د
- 1:40 استراحة استراحة 10 د
- 1:50 تطبيق Chart.js + التحديث التلقائي + DevTools 30 د
- 2:20 ميداني ميداني: الصفحة على هاتف المشرف داخل الصوبة 30 د
- 2:50 تقييم اختبار سريع + الواجب 10 د
الأدوات والمكونات
قاموس المصطلحات (Glossary)
تحدد عناصر الصفحة وهيكلها.
الألوان والخطوط والتخطيط.
الصفحة تتكيف مع عرض الشاشة.
@media (max-width:700px) — قواعد CSS للشاشات الصغيرة.
تقسيم الصفحة لأعمدة وصفوف.
تمثيل الصفحة الذي يعدله JavaScript.
نقرة، تحميل، مؤقت — يُستجاب له بدالة.
نص منظم {"key": value} تتبادله الأنظمة.
طلب بيانات من سيرفر داخل الصفحة بدون إعادة تحميلها.
انتظار الشبكة بدون تجميد الصفحة.
تحميل مكتبات مثل Chart.js من رابط عام.
F12 — Console و Network لتشخيص الأخطاء.
HTML و CSS المتجاوب
30 دقيقةالمشرف في المزرعة سيفتح صفحتنا من هاتف تحت الشمس، والباحث من لابتوب في المكتب. لذلك نصمم «للهاتف أولاً» (Mobile-first): أرقام كبيرة واضحة، تباين عالٍ، وتنبيه ملون عند الخطر.
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <!-- essential for phones -->
<title>الصوبة التجريبية</title>
</head>
<body>
<header><h1>🌱 الصوبة التجريبية</h1></header>
<main>
<div class="card">حرارة الهواء <b id="temp">—</b> °C</div>
<div class="card">رطوبة الهواء <b id="hum">—</b> %</div>
<div class="card">رطوبة التربة <b id="soil">—</b> %</div>
</main>
</body>
</html>
main {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3 cards side by side on a laptop */
gap: 1rem;
}
.card b { display: block; font-size: 2.2rem; color: #13787b; }
.alert { color: #c0392b; font-weight: 700; }
@media (max-width: 700px) { /* phone: one card per row */
main { grid-template-columns: 1fr; }
}
جرّب الآن
في Chrome اضغط F12 ثم أيقونة الهاتف (Toggle device toolbar) واختر Galaxy S20 — شاهد البطاقات تنتقل لعمود واحد.
JavaScript و Fetch API
30 دقيقةJavaScript هو «المخ» داخل الصفحة: يطلب أحدث القراءات من السيرفر كل بضع ثوانٍ ويضعها في العناصر بدون إعادة تحميل الصفحة.
async function refresh() {
try {
const res = await fetch('data.json?t=' + Date.now()); // ?t= avoids the browser cache
if (!res.ok) throw new Error('HTTP ' + res.status);
const d = await res.json(); // text -> JS object
document.getElementById('temp').textContent = d.latest.air_temp_c.toFixed(1);
document.getElementById('soil').textContent = d.latest.soil_moisture_pct.toFixed(0);
if (d.latest.soil_moisture_pct < 30) {
document.body.classList.add('dry'); // turn the page red-ish
}
} catch (err) {
console.error(err); // see it in DevTools -> Console
document.getElementById('updated').textContent = 'تعذر جلب البيانات';
}
}
refresh(); // run once now
setInterval(refresh, 5000); // then every 5 seconds
| المفهوم | في الكود | لماذا يهمنا في المزرعة |
|---|---|---|
| DOM | getElementById().textContent | تحديث الرقم فقط بدل تحميل الصفحة كلها (أقل بيانات على شبكة المزرعة) |
| async/await | await fetch() | الصفحة لا تتجمد لو الشبكة بطيئة |
| try/catch | رسالة «تعذر جلب البيانات» | المشرف يعرف أن الرقم المعروض قديم |
| Cache busting | ?t=Date.now() | بدونه قد يعرض المتصفح قراءة قديمة محفوظة |
لماذا Live Server؟
فتح الملف بالنقر المزدوج (file://) يجعل المتصفح يمنع fetch لأسباب أمنية. Live Server يشغّل سيرفر محلياً (http://127.0.0.1:5500) فيعمل كل شيء.
Lab: صفحة الصوبة الحية
30 دقيقةمن بيانات farm.db إلى صفحة حية
- افتح المشروع
افتح مجلد
web-basicsفي VS Code. - البيانات
شغّل
python export_json.py --node greenhouse_01: يقرأ آخر 24 قراءة من farm.db ويكتب data.json كل 10 ثوانٍ (بكتابة ذرية Atomic حتى لا يقرأ المتصفح ملفاً نصف مكتوب). - تشغيل الصفحة
كليك يمين على index.html → Open with Live Server.
- التحقق
F12 → Network: ترى طلب data.json كل 5 ثوانٍ بحالة 200.
- لا يوجد farm.db؟
عدّل data.json يدوياً واحفظه — الصفحة تتحدث خلال 5 ثوانٍ. هذه أسرع طريقة لاختبار التنبيه (اجعل soil_moisture_pct = 20).
النتيجة المتوقعة: الأرقام تتغير وحدها، والرسالة الحمراء «التربة جافة» تظهر عندما تقل الرطوبة عن 30%.
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Session 9 — Greenhouse live page</title>
<style>
/* Responsive design: CSS Grid + media query */
:root { --teal:#13787b; --amber:#f5a524; --navy:#12163a; }
body { margin:0; font-family:system-ui,Tahoma,sans-serif; background:#f6f5f1; color:var(--navy); }
header { background:var(--navy); color:#fff; padding:1rem 1.5rem; }
main { max-width:960px; margin:auto; padding:1.5rem; display:grid; gap:1rem; grid-template-columns:repeat(3,1fr); }
.card { background:#fff; border-radius:14px; padding:1.25rem; border:1px solid #e4e2da; }
.card b { display:block; font-size:2.2rem; color:var(--teal); }
.card.wide { grid-column:1/-1; }
.alert { color:#c0392b; font-weight:700; }
@media (max-width:700px){ main { grid-template-columns:1fr; } }
</style>
</head>
<body>
<header><h1>🌱 الصوبة التجريبية — مزرعة كلية الزراعة</h1><small id="updated">—</small></header>
<main>
<div class="card">حرارة الهواء <b id="temp">—</b> °C</div>
<div class="card">رطوبة الهواء <b id="hum">—</b> %</div>
<div class="card">رطوبة التربة <b id="soil">—</b> %<span id="soilMsg"></span></div>
<div class="card wide"><canvas id="chart" height="110"></canvas></div>
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<script>
// Data source: a JSON file next to this page (data.json) OR the Flask API of session 10.
const SOURCE = 'data.json';
const chart = new Chart(document.getElementById('chart'), {
type: 'line',
data: { labels: [], datasets: [{ label: 'رطوبة التربة %', data: [], borderColor: '#13787b', tension: .3 }] },
options: { animation: false }
});
async function refresh() {
try {
const res = await fetch(SOURCE + '?t=' + Date.now()); // Fetch API (no cache)
const d = await res.json();
document.getElementById('temp').textContent = d.latest.air_temp_c.toFixed(1);
document.getElementById('hum').textContent = d.latest.air_humidity_pct.toFixed(0);
document.getElementById('soil').textContent = d.latest.soil_moisture_pct.toFixed(0);
document.getElementById('soilMsg').innerHTML = d.latest.soil_moisture_pct < 30 ? '<p class="alert">التربة جافة — يلزم الري</p>' : '';
document.getElementById('updated').textContent = 'آخر تحديث: ' + new Date().toLocaleTimeString('ar-EG');
chart.data.labels = d.history.map(p => p.t);
chart.data.datasets[0].data = d.history.map(p => p.soil);
chart.update();
} catch (e) {
document.getElementById('updated').textContent = 'تعذر جلب البيانات: ' + e.message;
}
}
refresh();
setInterval(refresh, 5000);
</script>
</body>
</html>
"""Session 9 - refresh data.json from the farm database every 10 s so index.html shows live readings.
python export_json.py --db ../smart-farm/farm.db --node greenhouse_01
Then open index.html with VS Code "Live Server" (it must be served over http://, not file://).
"""
import argparse
import json
import sqlite3
import time
from pathlib import Path
ap = argparse.ArgumentParser()
ap.add_argument("--db", default=str(Path(__file__).resolve().parent.parent / "smart-farm" / "farm.db"))
ap.add_argument("--node", default="greenhouse_01")
ap.add_argument("--once", action="store_true")
a = ap.parse_args()
out = Path(__file__).resolve().parent / "data.json"
while True:
con = sqlite3.connect(a.db)
con.row_factory = sqlite3.Row
rows = con.execute("SELECT ts, air_temp_c, air_humidity_pct, soil_moisture_pct FROM readings "
"WHERE node_id=? AND air_temp_c IS NOT NULL ORDER BY ts DESC LIMIT 24", (a.node,)).fetchall()
con.close()
if rows:
latest = dict(rows[0])
data = {"latest": {"node_id": a.node, **{k: latest[k] for k in ("air_temp_c", "air_humidity_pct", "soil_moisture_pct")}},
"history": [{"t": r["ts"][11:16], "soil": r["soil_moisture_pct"]} for r in reversed(rows)]}
tmp = out.with_suffix(".tmp")
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=1), encoding="utf-8")
tmp.replace(out) # atomic: the browser never reads a half-written file
print(time.strftime("%H:%M:%S"), "updated", data["latest"])
else:
print("no readings yet for", a.node)
if a.once:
break
time.sleep(10)
export_json.py على قاعدة بيانات تجريبية
المخرجات الفعلية عند تشغيل السكربت مرة واحدة:
python export_json.py --once
المخرجات الفعلية عند التشغيل (Real output)
21:31:07 updated {'node_id': 'greenhouse_01', 'air_temp_c': 27.9, 'air_humidity_pct': 64.2, 'soil_moisture_pct': 35.5}Chart.js والتحديث التلقائي
30 دقيقةconst chart = new Chart(document.getElementById('chart'), {
type: 'line',
data: { labels: [], datasets: [{ label: 'رطوبة التربة %', data: [], borderColor: '#13787b', tension: .3 }] },
options: { animation: false } // no animation on every refresh (less CPU on phones)
});
// inside refresh(), after reading d:
chart.data.labels = d.history.map(p => p.t); // ["06:00", "06:15", ...]
chart.data.datasets[0].data = d.history.map(p => p.soil);
chart.update(); // redraw with the new data
خط العتبة + لون حسب الحالة
أضف للرسم خطاً أفقياً متقطعاً أحمر عند 30% (عتبة الري) كمجموعة بيانات ثانية، واجعل لون خط الرطوبة أحمر إذا كانت آخر قيمة أقل من 30.
الحل (Solution)
chart.data.datasets[1] = {
label: 'عتبة الري', data: d.history.map(() => 30),
borderColor: '#c0392b', borderDash: [6, 4], pointRadius: 0
};
const last = d.history[d.history.length - 1].soil;
chart.data.datasets[0].borderColor = last < 30 ? '#c0392b' : '#13787b';
chart.update();
الخطوة القادمة
ملف data.json حل مؤقت. في السيشن 10 نستبدله بـ REST API حقيقي (/api/v1/readings) — والكود في الصفحة لن يتغير تقريباً، فقط الرابط في fetch!
ميداني: الصفحة على هاتف المشرف
30 دقيقةافتح صفحة الصوبة من داخل الصوبة
الصوبة التجريبية — هاتف متصل بشبكة KFS-Farm-IoT
- الوصول من الهاتف
Live Server يعمل على اللابتوب. من الهاتف افتح
http://<IP-اللابتوب>:5500(فعّل في إعدادات Live Server خيار host = 0.0.0.0 أو استخدمpython -m http.server 5500). - اختبار القراءة
هل الأرقام مقروءة تحت الشمس؟ هل حجم الخط كافٍ؟ دوّن ملاحظات المشرف.
- اختبار التنبيه
ارفع حساس التربة من الأصيص في الهواء دقيقة: هل تظهر رسالة الجفاف على الهاتف خلال 30 ثانية؟
- زمن التأخير
قس الزمن من تغير القراءة على العقدة حتى ظهورها على الهاتف. أين يضيع الوقت؟ (فترة العقدة، export كل 10 ث، الصفحة كل 5 ث).
| الاختبار | النتيجة | زمن التأخير (ث) | ملاحظة المشرف |
|---|---|---|---|
- الهاتف في جيب مغلق أثناء الري.
فكّر وناقش
ما أهم 3 معلومات يريدها المشرف أول ما يفتح الصفحة؟ هل صفحتنا تعطيها؟
كيف نقلل زمن التأخير بين العقدة والهاتف؟ (تمهيد لـ WebSocket/MQTT over Web في السيشن 11)
اختبر فهمك
5 أسئلة · نتيجة فوريةالواجب والتسليم
الواجب الأسبوعي (Weekly Assignment)
أكمل تحدي خط العتبة واللون.
أضف بطاقة رابعة للإضاءة، وزر «تحديث الآن» يستدعي refresh() عند النقر.
اكتب ملاحظات المشرف من الاختبار الميداني و 3 تحسينات ستنفذها.
قائمة التسليم (Submission Checklist)
استعد للسيشن القادمة
السيشن القادمة نبني الـ Backend: Flask + REST API. تأكد أن pip install flask يعمل، وثبّت Postman (أو استخدم curl).