برمجة المتحكّمات بلغة مايكروبايثون · الفصل 14 — شبكة WiFi وخادم HTTPרשת WiFi ושרת HTTP · תכנות מיקרו-בקרים בשפת MicroPython⌂📚🏠
الفصل 14

شبكة WiFi وخادم HTTP

רשת WiFi ושרת HTTP – WiFi Network and HTTP Server
4 ساعات تعليمية

حتى الآن كانت اللوحة تكلّمنا عبر سلك. في هذا الفصل تدخل الشبكة اللاسلكية نفسها التي يدخلها هاتفنا، ونفتح في المتصفّح صفحة تسكن داخل اللوحة: نقرأ منها حسّاسا ونضبط ثنائيا باعثا للضوء دون أي سلك. نبني ذلك بأدوات صغيرة نفهم كل سطر فيها، ونعرف أيضا حدودها، لأن هذه الصفحة مفتوحة لكل من في الشبكة.

في هذا الفصل:
  • نشرح عنوان IP والمنفذ والفرق بين العميل والخادم.
  • نربط اللوحة بالشبكة بمهلة انتظار، ونعالج فشل الاتصال، ونبقي كلمة المرور خارج البرنامج.
  • نشغّل اللوحة محطة في شبكة قائمة، ونقطة وصول تنشئ شبكتها الخاصة.
  • نكتب خادم HTTP بالمقبس: نقرأ الطلب والمسار ونبني الاستجابة.
  • نتحكّم في ثنائي من صفحة، ونعرض قراءة، ونستقبل قيمة من نموذج ونتحقّق منها.
  • نحدّد ما يجوز وصله بخادم لا تشفير فيه ولا مصادقة.

14.1 العنوان والمنفذ والعميل والخادم – כתובת IP, פורט ומודל לקוח-שרת (IP, Port, Client-Server)

قبل سطر الشيفرة الأول نحتاج إلى أربع كلمات. لكل جهاز في الشبكة عنوان، وفي كل جهاز عدة برامج، ولذلك يحتاج الوصول إلى برنامج معيّن إلى عنوان ورقم معا. وفي كل محادثة طرف يطلب وطرف يجيب.

عنوان IP (כתובת IP): أربعة أعداد بين 0 و255 تحدّد الجهاز داخل الشبكة، مثل 192.168.1.50. يعطيه عادة الموجّه (נתב) عند الانضمام.
المنفذ (פורט): عدد يحدّد البرنامج داخل الجهاز. خوادم الصفحات تستمع عادة إلى المنفذ 80.
الخادم (שרת): برنامج ينتظر الطلبات ويجيب عنها. لا يبدأ المحادثة أبدا.
العميل (לקוח): برنامج يبدأ المحادثة ويرسل طلبا، مثل المتصفّح (דפדפן).
الهاتف: العميل192.168.1.20الموجّه192.168.1.1اللوحة: الخادم192.168.1.50 : 80طلباستجابةطلباستجابة
الهاتف يطلب، والموجّه يوصل، واللوحة تجيب من المنفذ 80.

في هذا الفصل اللوحة هي الخادم. وفي الفصل التالي نعكس الأدوار فتصبح عميلا.

مثال 14.1
قناع الشبكة (מסכת רשת) في بيوت كثيرة هو 255.255.255.0، أي أن الأعداد الثلاثة الأولى ثابتة والعدد الرابع وحده يتغيّر. كم جهازا يمكن أن يأخذ عنوانا في هذه الشبكة، إذا كان عنوانان محجوزين، الأول والأخير؟
الحل:
N = 28 − 2
N = 256 − 2
N = 254
N = 254
سؤال 14.1: على اللوحة نفسها يعمل خادمان. هل يمكن أن يستمعا معا إلى المنفذ 80؟ وكيف يصل المتصفّح إلى الثاني؟

14.2 الانضمام إلى الشبكة – הצטרפות לרשת (Joining a Network)

يعمل الاتصال اللاسلكي في مايكروبايثون عبر الصنف network.WLAN، وله وضعان: في وضع المحطة (תחנה) تنضم اللوحة إلى شبكة قائمة كما يفعل الهاتف، وفي وضع نقطة الوصول (נקודת גישה) تنشئ هي شبكتها الخاصة.

وضع المحطةوضع نقطة الوصولالهاتفالموجّهاللوحة192.168.1.50الهاتفاللوحة192.168.4.1
في وضع المحطة يمر الطريق عبر الموجّه، وفي وضع نقطة الوصول يتصل الهاتف باللوحة مباشرة.

نبدأ بالسؤال: ماذا ترى اللوحة حولها؟ كل نتيجة في المسح (סריקה) تحمل الاسم والقناة وقوة الإشارة. قوة الإشارة rssi عدد سالب، وكلما اقترب من الصفر كانت الإشارة أقوى.

مسح الشبكات. الأسماء والأعداد من العالم المُحاكى.
import network

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
for ssid, bssid, channel, rssi, sec, hidden in wlan.scan():
    print(ssid.decode(), channel, rssi)
ESP32 + Pico W · MicroPython 1.29 · simulated board
Home 6 -50
Lab 11 -71
Cafe 1 -85
ملاحظة: لا تُرى شبكة 5 GHz من لوحات هذا الكتاب: ESP32 وPico W يعملان على نطاق 2.4 GHz فقط.

الأمر connect لا ينتظر: يبدأ الاتصال ويعود فورا. لذلك نسأل isconnected() في حلقة، ونحدّد لها مهلة (פסק זמן) حتى لا يعلق البرنامج إلى الأبد إذا تغيّرت كلمة المرور أو انطفأ الموجّه.

الانضمام بمهلة انتظار.
import network
import time
from secrets import WIFI_SSID, WIFI_PASS

WAIT_S = 4

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.disconnect()
wlan.connect(WIFI_SSID, WIFI_PASS)

waited = 0
while not wlan.isconnected() and waited < WAIT_S:
    print("waiting", waited)
    time.sleep(1)
    waited += 1

if wlan.isconnected():
    print("IP:", wlan.ifconfig()[0])
else:
    print("failed, status", wlan.status())
    wlan.active(False)
ESP32 + Pico W · MicroPython 1.29 · simulated board
waiting 0
IP: 192.168.1.50

وقبل connect نستدعي disconnect(): إذا كانت محاولة سابقة ما زالت جارية من تشغيل سابق، يعطي ESP32 الخطأ Wifi Internal Error. ظهر هذا الخطأ فعلا عند تجربة البرنامج على لوحة حقيقية.

والبرنامج نفسه حين تبقى الشبكة بعيدة المنال:

ESP32 · MicroPython 1.29 · simulated board
waiting 0
waiting 1
waiting 2
waiting 3
failed, status 201

العدد الذي يعيده status() يفسّره ثابت من الوحدة network، مثل STAT_WRONG_PASSWORD أو STAT_NO_AP_FOUND. والقيم العددية قد تختلف بين اللوحات، فنقارن دائما بالثابت لا بالعدد.

خطأ شائع: حلقة while not wlan.isconnected(): pass دون مهلة هي أشهر سبب للوحة "ميتة": لا رسالة ولا خطأ، والبرنامج كله واقف عند السطر نفسه.
سؤال 14.2: لماذا نوقف البطاقة بـwlan.active(False) بعد الفشل، بدل أن نتركها مفعّلة؟

14.3 ملف الأسرار ومكتبة wifi.py – קובץ הסודות וספריית wifi.py

لاحظوا أن كلمة المرور لم تظهر في أي برنامج. فهي في ملف واحد نرفعه إلى اللوحة ولا نرفعه إلى أي مكان آخر:

الملف secrets.py: نبدّل القيم باسم شبكتنا وكلمة مرورها، وبكلمة مرور جديدة لنقطة الوصول
# secrets.py - your network. Keep it off GitHub and shared folders.
WIFI_SSID = "my-network"
WIFI_PASS = "my-password"
AP_PASS = "pick-a-new-one"
قاعدة ثابتة: البرامج تُنشر: نرسلها إلى زميل، أو نرفعها إلى GitHub، أو ننسخها في تقرير. وكلمة مرور في سطر من البرنامج تسافر معه، وتبقى في تاريخ المستودع حتى بعد حذفها. لذلك لا نضع في شيفرة مشتركة كلمة مرور ولا مفتاحا، ونضيف secrets.py إلى ملف .gitignore.

وبما أن كل برنامج في بقية الكتاب يبدأ بالانضمام إلى الشبكة، ننقل الحلقة إلى مكتبة. المكتبة لا تطبع شيئا: إن فشلت أطلقت خطأ، والبرنامج الذي يستدعيها يقرّر ماذا يفعل.

المكتبة wifi.py، نفسها على اللوحتين
# wifi.py - join the network named in secrets.py, or say why not.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import network
import time
from secrets import WIFI_SSID, WIFI_PASS


def connect(timeout_s=15):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if wlan.isconnected():
        return wlan
    wlan.disconnect()
    wlan.connect(WIFI_SSID, WIFI_PASS)
    start = time.ticks_ms()
    while not wlan.isconnected():
        if time.ticks_diff(time.ticks_ms(), start) > timeout_s * 1000:
            status = wlan.status()
            wlan.active(False)
            raise OSError("WiFi failed, status %d" % status)
        time.sleep_ms(200)
    return wlan
مثال 14.2
تفحص المكتبة الاتصال كل 0.2 s، والمهلة 15 s. كم مرة على الأكثر تفحص قبل أن تعلن الفشل؟
الحل:
N = Tt
N = 150.2
N = 75
N = 75
استعمال المكتبة: الحلقة كلها صارت استدعاء واحدا.
import wifi

try:
    wlan = wifi.connect(timeout_s=5)
    print("IP:", wlan.ifconfig()[0])
except OSError as e:
    print("no network:", e)
ESP32 + Pico W · MicroPython 1.29 · simulated board
IP: 192.168.1.50

وحين يفشل الاتصال يصل الخطأ إلى except:

ESP32 · MicroPython 1.29 · simulated board
no network: WiFi failed, status 201
سؤال 14.3: فتحنا المكتبة فوجدنا السطر if wlan.isconnected(): return wlan قبل connect. ما فائدته عند إعادة تشغيل البرنامج دون فصل التغذية؟

14.4 اللوحة نقطة وصول – הלוח כנקודת גישה (Access Point Mode)

في الميدان أو في الورشة لا يوجد دائما موجّه. عندها تنشئ اللوحة شبكتها، فنختارها من قائمة الشبكات في الهاتف، ونفتح العنوان الذي تعطيه لنفسها.

شبكة باسم lab-board. والعنوان هنا هو العنوان الذي تعطيه المُحاكاة.
import network
from secrets import AP_PASS

ap = network.WLAN(network.AP_IF)
ap.active(True)
ap.config(ssid="lab-board", key=AP_PASS)
ap.active(False)
ap.active(True)
print("network:", ap.config("ssid"))
print("IP:", ap.ifconfig()[0])
ESP32 + Pico W · MicroPython 1.29 · simulated board
network: lab-board
IP: 192.168.4.1

نشغّل الواجهة أولا ثم نضبطها ثم نعيد تشغيلها، لأن هذا الترتيب مقبول في كل الإصدارات: في إصدار قديم من ESP32 يعطي الضبط قبل التشغيل خطأ. ولم نجرّب هذا الترتيب على Pico W حقيقية.

تحذير: لنقطة الوصول كلمة مرور خاصة بها، ولا نستعمل فيها كلمة مرور شبكة البيت. ونتأكد من الهاتف أن الشبكة الجديدة تطلب كلمة مرور فعلا. طريقة تحديد نوع الأمان عبر المعامل security وثوابته تتعلّق باللوحة، فنرجع إلى توثيق اللوحة التي بين أيدينا. وكلمة مرور نقطة الوصول يُفضّل ألا تكون كلمة مرور البيت.

كل ما سيأتي في الفصل من خادم وصفحات يعمل في الوضعين. فالخادم لا يهمّه كيف وصل العميل، بل يهمّه أن يصل.

سؤال 14.4: في وضع نقطة الوصول، هل يستطيع برنامج على اللوحة أن يطلب صفحة من الإنترنت؟ ولماذا؟

14.5 خادم HTTP بالمقبس – שרת HTTP ב-socket (An HTTP Server on a Socket)

المقبس (שקע, socket): الكائن الذي يمثّل طرف اتصال في الشبكة: نكتب فيه فتصل الكتابة إلى الطرف الآخر، ونقرأ منه ما أرسله.
HTTP (פרוטוקול HTTP): اتفاق نصّي بين العميل والخادم: طلب (בקשה) واحد يتبعه استجابة (תגובה) واحدة.
المتصفّح: العميلاللوحة: الخادمGET /on HTTP/1.1HTTP/1.0 200 OK + HTMLclose()
محادثة كاملة: طلب، فاستجابة، فإغلاق.

أول سطر في الطلب يقول كل شيء: الطريقة GET، ثم المسار (נתיב) /on، ثم النسخة. والاستجابة أيضا نص بترتيب ثابت: سطر الحالة، ثم الترويسات (כותרות)، ثم سطر فارغ، ثم الصفحة نفسها.

خادم كامل. الطلب هنا من متصفّح مُحاكى، وفي آخر المُخرَج ما أرسلته اللوحة على المقبس.
import socket
import wifi

wlan = wifi.connect()
print("open http://%s/" % wlan.ifconfig()[0])

addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1]
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(addr)
server.listen(2)

while True:
    conn, peer = server.accept()
    request = conn.recv(1024)
    print(peer[0], request.split(b"\r\n")[0].decode())
    conn.sendall(b"HTTP/1.0 200 OK\r\n"
                 b"Content-Type: text/html\r\n\r\n"
                 b"<h1>Hello from the board</h1>")
    conn.close()
ESP32 + Pico W · MicroPython 1.29 · simulated board
open http://192.168.1.50/
192.168.1.20 GET / HTTP/1.1
[http_sent]
HTTP/1.0 200 OK
Content-Type: text/html

<h1>Hello from the board</h1>
...
الأمرعمله
getaddrinfo("0.0.0.0", 80)عنوان يعني: استمع على كل واجهات اللوحة، في المنفذ 80.
setsockopt(... SO_REUSEADDR, 1)يسمح بإعادة تشغيل البرنامج على المنفذ نفسه فورا.
server.listen(2)يبدأ الاستماع، ويُبقي عميلين في الانتظار.
server.accept()ينتظر عميلا، ويعيد مقبسا خاصا به مع عنوانه.
conn.recv(1024)يقرأ حتى 1024 بايتا من الطلب.
conn.sendall(...)يرسل الاستجابة كاملة.
conn.close()يغلق الاتصال، فيعرف المتصفّح أن الصفحة انتهت.
خطأ شائع: السطر الفارغ بين الترويسات والصفحة ليس زينة: \r\n\r\n هو ما يفصل بينهما. من دونه تُفهم الصفحة كلها على أنها ترويسات، ولا يظهر شيء في المتصفّح.
سؤال 14.5: نسينا conn.close() في الحلقة. ماذا يرى المستخدم؟ وماذا يحدث للوحة بعد عدة طلبات؟

14.6 صفحة تتحكّم في الثنائي – דף ששולט בנורית (A Page that Drives the LED)

لنبدأ بما سيتكرر: فتح المقبس، وتفكيك السطر الأول، وبناء الاستجابة. نجمع ذلك في مكتبة صغيرة، لتبقى في البرنامج الأسطر التي تقول ماذا يفعل الجهاز.

المكتبة miniweb.py
# miniweb.py - a tiny web server on plain sockets, one client at a time.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import socket


def listen(port=80):
    addr = socket.getaddrinfo("0.0.0.0", port)[0][-1]
    server = socket.socket()
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(addr)
    server.listen(2)
    return server


def parse(raw):
    parts = raw.split(b"\r\n")[0].decode().split(" ")
    if len(parts) < 2:
        return "", {}
    target = parts[1].split("?", 1)
    args = {}
    if len(target) == 2:
        for pair in target[1].split("&"):
            kv = pair.split("=", 1)
            if len(kv) == 2:
                args[kv[0]] = kv[1]
    return target[0], args


def reply(conn, body, status="200 OK", kind="text/html"):
    head = "HTTP/1.0 %s\r\nContent-Type: %s\r\n\r\n" % (status, kind)
    conn.sendall(head.encode() + body.encode())
مثال 14.3
ماذا تعيد parse للطلب GET /set?level=60&mode=slow HTTP/1.1؟
الحل:

نقطع السطر عند الفراغات، فيكون الجزء الثاني هو الهدف. ثم نقطعه عند ?: قبلها المسار، وبعدها المعاملات (פרמטרים)، مفصولة بـ&. والقيم كلها نصوص.

miniweb.parse
('/set', {'mode': 'slow', 'level': '60'})
كل رابط طلب جديد. الترتيب: الصفحة، تشغيل، طلب الأيقونة، إطفاء.
from machine import Pin
import board
import miniweb
import wifi

PAGE = """<h1>LED: %s</h1>
<a href="/on">ON</a> <a href="/off">OFF</a>"""

led = Pin(board.LED, Pin.OUT)
wifi.connect()
server = miniweb.listen()

while True:
    conn, peer = server.accept()
    conn.settimeout(3)
    try:
        path, args = miniweb.parse(conn.recv(1024))
        print(peer[0], path)
        if path == "/on":
            led.value(1)
        elif path == "/off":
            led.value(0)
        if path in ("/", "/on", "/off"):
            state = "ON" if led.value() else "OFF"
            miniweb.reply(conn, PAGE % state)
        else:
            miniweb.reply(conn, "Not found", "404 Not Found")
    except OSError as e:
        print("client error:", e)
    finally:
        conn.close()
ESP32 · MicroPython 1.29 · simulated board
192.168.1.20 /
192.168.1.20 /on
192.168.1.20 /favicon.ico
192.168.1.20 /off
[pins]
2 1
2 0
...
Pico W · MicroPython 1.29 · simulated board
192.168.1.20 /
192.168.1.20 /on
192.168.1.20 /favicon.ico
192.168.1.20 /off
[pins]
100 1
100 0
...

انظروا إلى الطلب الثالث: لم نضغط على شيء يقود إلى /favicon.ico. المتصفّحات تطلب هذا الملف من تلقاء نفسها لتعرض أيقونة الموقع، ولذلك يحتاج كل خادم إلى إجابة 404 لما لا يعرفه. والمرور بأي مسار غير معروف لا يغيّر حالة الثنائي.

ولماذا conn.settimeout(3)؟ لأن هذا الخادم يخدم عميلا واحدا في كل مرة. عميل فتح اتصالا ولم يرسل شيئا يوقف الخادم كله عند recv. المهلة تطلق OSError، وfinally يغلق المقبس في كل الأحوال، ويعود الخادم إلى accept.

تحذير: الخادم هنا يحجز البرنامج كله: ما دام accept ينتظر، لا تُنفّذ حلقة قياس ولا مؤقّت في البرنامج الرئيسي. نحل ذلك في فصل البرمجة غير المتزامنة.
سؤال 14.6: المتصفّح يعيد فتح آخر رابط عند تحديث الصفحة. فما الذي يحدث عند التحديث بعد الضغط على /on؟ ولماذا لا يضر هنا، ومتى يضر؟

14.7 عرض قراءة واستقبال قيمة – הצגת קריאה וטופס (Showing a Reading, Receiving a Value)

نخطو خطوة أخيرة: صفحة تعرض جهد مقياس الجهد، وفيها نموذج (טופס) يضبط إضاءة ثنائي خارجي. النموذج في HTML لا يحتاج إلى أي شيفرة خاصة: عند الضغط على الزر يطلب المتصفّح /set?level=60، أي مسار ومعامل، وهذا ما نعرف قراءته.

10 kΩ3V334 · GP26 (POT)13 · GP15 (LED_EXT)GND330 Ω
التوصيل: الرقم الأول لـESP32، والثاني لـPico W. والبرنامج لا يذكر أيا منهما بفضل board.py.
خمسة طلبات: الصفحة، وقيمة سليمة، ونص، وعدد خارج المجال، وطلب بلا معامل.
import board
import miniweb
import wifi
from convert import to_volts

PAGE = """<h1>%.2f V</h1><p>LED: %d%%</p>
<form action="/set">
<input name="level" type="number" min="0" max="100">
<button>Set</button></form>"""

pot = board.analog_in(board.POT)
led = board.pwm(board.LED_EXT, 1000)
led.duty_u16(0)
level = 0
wifi.connect()
server = miniweb.listen()


def handle(path, args):
    global level
    if path == "/set":
        value = int(args["level"])
        if not 0 <= value <= 100:
            raise ValueError("range")
        level = value
        led.duty_u16(level * 65535 // 100)
    elif path != "/":
        return "Not found", "404 Not Found"
    return PAGE % (to_volts(pot.read_u16()), level), "200 OK"


while True:
    conn, peer = server.accept()
    conn.settimeout(3)
    try:
        path, args = miniweb.parse(conn.recv(1024))
        try:
            body, status = handle(path, args)
        except (KeyError, ValueError):
            body, status = "Bad value", "400 Bad Request"
        print(path, args, status, "level", level)
        miniweb.reply(conn, body, status)
    except OSError as e:
        print("client error:", e)
    finally:
        conn.close()
ESP32 · MicroPython 1.29 · simulated board
/ {} 200 OK level 0
/set {'level': '60'} 200 OK level 60
/set {'level': 'abc'} 400 Bad Request level 60
/set {'level': '150'} 400 Bad Request level 60
/set {} 400 Bad Request level 60
[pwm]
13 0
13 39321
...
Pico W · MicroPython 1.29 · simulated board
/ {} 200 OK level 0
/set {'level': '60'} 200 OK level 60
/set {'level': 'abc'} 400 Bad Request level 60
/set {'level': '150'} 400 Bad Request level 60
/set {} 400 Bad Request level 60
[pwm]
15 0
15 39321
...
مثال 14.4
طلب النموذج level=60. ما دورة العمل التي يضعها البرنامج في duty_u16؟
الحل:
D = level · 65535100
D = 60 · 65535100
D = 39321
D = 39321
قاعدة ثابتة: الخاصيتان min وmax في النموذج لا تحميان شيئا: يكفي أن يكتب أحد العنوان مباشرة في المتصفّح ليتجاوز النموذج. كل قيمة تأتي من الشبكة نتحقق منها في اللوحة نفسها، ولا نثق بها أبدا. لذلك يرد البرنامج 400 ولا يغيّر الحالة.
سؤال 14.7: حرف عربي أو فراغ في قيمة نموذج يصل إلى اللوحة مرمّزا، مثل a%20b للنص a b. هل تفكّ المكتبة miniweb هذا الترميز؟ وماذا نفعل إن احتجنا إليه؟

14.8 حدود الأمان – אבטחה: מה מותר לחבר (Security Limits)

الخادم الذي كتبناه يعمل، لكن علينا أن نقول بوضوح ما الذي ينقصه:

ما نصله بهذا الخادمالحكمالسبب
ثنائي، قراءة حرارة أو ضوء، عرض في المختبرمناسبأسوأ نتيجة مزعجة وليست خطرة.
محرّك صغير، طنّان، مضخة صغيرة بجهد منخفضبحذرنحدّد زمن التشغيل في البرنامج، ولا نتركها تعمل دون مراقبة.
قفل باب، سخّان، أي حمل بجهد الشبكة الكهربائيةلاأي شخص في الشبكة يشغّله، وتكرار طلب واحد قد يسبب حريقا أو ضررا.
فتح منفذ في الموجّه نحو الإنترنتلايصبح الخادم مكشوفا للعالم كله بلا أي حماية.

القاعدة التي نعمل بها: هذا الخادم لشبكة مختبر نعرف كل من فيها. وكل مشغّل نصله به نختار له حالة لا تضر عند انقطاع الشبكة، وهذا يقرّره البرنامج وليس الشبكة.

سؤال 14.8: طالب يريد التحكم في مروحة 230 V من هاتفه عبر برنامج هذا الفصل. ماذا نقول له؟

14.9 فروق اللوحات والنشر – הבדלים בין הלוחות ופריסה (Board Differences, Deployment)

نعود إلى ما وعدنا به في فصل القراءة التماثلية. على ESP32 كتلة المحوّلات الثانية يستعملها WiFi، فالقراءة من طرف فيها تفشل ما دامت البطاقة مفعّلة:

الطرف 25 في الكتلة الثانية، والخطأ يظهر فقط بعد تفعيل البطاقة.
from machine import ADC, Pin
import network

wlan = network.WLAN(network.STA_IF)
wlan.active(True)
pot = ADC(Pin(25), atten=ADC.ATTN_11DB)
print(pot.read_u16())
ESP32 · MicroPython 1.29 · simulated board
OSError: ADC2 is in use by WiFi

لهذا كان board.POT على ESP32 الطرف 34، وهو من المجال 32–39. أما على Pico W فلا يوجد هذا القيد، والأطراف GP26–GP28 تعمل مع الشبكة.

كل برامج الفصل عملت على اللوحتين دون تعديل سطر، وهذا ما عرضته الشاشات أعلاه. والعمل الوحيد هو رفع الملفات التي تستوردها إلى اللوحة، ثم تسمية البرنامج main.py ليعمل عند التشغيل:

mpremote cp secrets.py wifi.py miniweb.py board.py convert.py :
mpremote cp http_panel.py :main.py
mpremote reset
ملاحظة: على Raspberry Pi 4/5 لا يوجد network.WLAN: نظام لينكس هو الذي ينضم إلى الشبكة، ويكفي البرنامج أن يفتح المقبس. وللباي فصل خاص به في الجزء السادس.

أهمّ أفكار الفصل:

أوامر الفصل:

الأمرعمله
network.WLAN(network.STA_IF)بطاقة في وضع المحطة. AP_IF لنقطة الوصول.
wlan.active(True)تفعيل البطاقة.
wlan.scan()قائمة الشبكات حول اللوحة.
wlan.connect(ssid, key)بدء الانضمام، دون انتظار.
wlan.isconnected()هل تم الانضمام؟
wlan.status()حالة الاتصال، تقارَن بثوابت network.STAT_*.
wlan.ifconfig()[0]عنوان اللوحة.
ap.config(ssid=..., key=...)اسم نقطة الوصول وكلمة مرورها.
wifi.connect(timeout_s)مكتبتنا: انضمام بمهلة، أو OSError.
miniweb.listen(port)مكتبتنا: مقبس خادم جاهز.
miniweb.parse(raw)مكتبتنا: المسار والمعاملات من الطلب.
miniweb.reply(conn, body, status)مكتبتنا: إرسال استجابة.

قاموس المصطلحات – מילון מונחים:

بالعربيةبالعبريةEnglish
عنوان IPכתובת IPIP Address
المنفذפורטPort
الموجّهנתבRouter
الخادمשרתServer
العميلלקוחClient
المتصفّحדפדפןBrowser
المحطةתחנהStation
نقطة الوصولנקודת גישהAccess Point
المهلةפסק זמןTimeout
المقبسשקעSocket
الطلبבקשהRequest
الاستجابةתגובהResponse
المسارנתיבPath
الترويسةכותרתHeader
النموذجטופסForm
التشفيرהצפנהEncryption
المصادقةאימותAuthentication
الإجابات النهائية لأسئلة الفصل
14.1 لا: لكل منفذ برنامج واحد. نشغّل الثاني على منفذ آخر، مثل 8080، ونكتب في المتصفّح http://192.168.1.50:8080/.
14.2 حتى لا تبقى تستهلك الطاقة وتحاول في الخلفية دون فائدة، وتعود المحاولة التالية من حالة نظيفة.
14.3 البطاقة قد تكون متصلة من التشغيل السابق، فنستعمل الاتصال القائم بدل أن نقطعه ونبدأ من جديد.
14.4 لا: الشبكة التي أنشأتها اللوحة لا تتصل بالموجّه، فلا طريق منها إلى الإنترنت.
14.5 يبقى المتصفّح في انتظار نهاية الصفحة. وتبقى المقابس مفتوحة في الذاكرة حتى تنفد فتتوقف اللوحة عن قبول عملاء جدد.
14.6 يُرسل /on من جديد. لا يضر لأن النتيجة واحدة في كل مرة. يضر حين يغيّر الطلب شيئا في كل تكرار، مثل /toggle أو تشغيل مضخة لمدة محددة.
14.7 لا تفكّه: القيمة تصل كما هي. نكتب دالة صغيرة تحوّل كل %XX إلى بايت، أو نقتصر على قيم عددية كما فعلنا هنا.
14.8 لا نفعل ذلك: لا مصادقة ولا تشفير، والحمل بجهد خطر. نبقى على جهد منخفض في المختبر، ونفصل الحمل الخطر عن أي تحكم غير محمي.

أسئلة للتمرين:

المستوى الأول — أساسي
1.
في وضع المحطة وفي وضع نقطة الوصول: أي الوضعين يحتاج إلى موجّه؟ وأيهما يوصل اللوحة بالإنترنت؟
الإجابات: وضع المحطة في الحالتين، إذا كان الموجّه متصلا بالإنترنت.
2.
رتّبوا أجزاء استجابة HTTP: الصفحة، سطر فارغ، سطر الحالة، الترويسات.
الإجابات: سطر الحالة، الترويسات، سطر فارغ، الصفحة.
3.
ما الذي تعيده parse للطلب GET /on HTTP/1.1؟
الإجابات: المسار /on وقاموس معاملات فارغ.
المستوى الثاني — تطبيق
4.
أضيفوا إلى برنامج الثنائي مسارا /toggle يقلب حالته. وهل يكفي هذا السطر وحده؟
الإجابات:
if path == "/toggle":
    led.value(1 - led.value())
لا: يجب إضافة المسار أيضا إلى قائمة المسارات المعروفة، وإلا كانت الاستجابة 404 رغم تغيّر الثنائي.
5.
طلب النموذج level=25. ما قيمة duty_u16؟
الإجابات: D = 16383.
6.
أضيفوا إلى برنامج اللوحة مسارا /api يعيد القراءة بصيغة JSON بدل صفحة.
الإجابات:
import json

if path == "/api":
    volts = to_volts(pot.read_u16())
    text = json.dumps({"volts": volts, "level": level})
    miniweb.reply(conn, text, "200 OK", "application/json")
المستوى الثالث — تعمّق
7.
أضيفوا إلى برنامج الانضمام محاولة ثانية بعد الفشل، على ألا تتجاوز المدة الكلية 30 s. كيف تُكتب باستعمال wifi.py؟
الإجابات: نستدعي wifi.connect(15) داخل try مرتين في حلقة for، ونخرج عند أول نجاح. بعد المرة الثانية نترك الخطأ يصل إلى البرنامج، أو ننتقل إلى وضع نقطة الوصول.
8.
فتح زميل اتصالا بالمنفذ 80 ولم يرسل شيئا، وليس في البرنامج settimeout. ماذا يحدث لبقية المستخدمين؟ ولماذا لا يحدث ذلك في برنامجنا؟
الإجابات: يبقى الخادم عند recv ولا يخدم أحدا. في برنامجنا تنتهي المهلة بعد 3 ثوانٍ، ويغلق finally المقبس، ويعود الخادم إلى accept.
9.
صمّموا خادما لمختبر فيه عشرة طلاب، يتحكم فيه طالب واحد فقط بالثنائي. هل تكفي كلمة في الرابط مثل /on?pw=1234؟
الإجابات: تمنع الخطأ غير المقصود فقط، ولا تحمي: الكلمة تمر نصا مكشوفا، وتبقى في سجل المتصفّح. الحل الأبسط شبكة نقطة وصول خاصة بهذا الطالب.