🌐 Level 6 – Wi-Fi Robotics

Project 6.9: "Wireless Sensor Network"

 

What you’ll learn

  • ✅ Multi‑node networking: Connect several R32 boards and exchange small, labeled packets reliably.
  • ✅ Distributed sensing: Collect sensor readings from many nodes and aggregate them.
  • ✅ Fault tolerance: Detect missing nodes and keep the system running with graceful fallbacks.
  • ✅ Energy optimization: Reduce power usage with smart duty cycles and sleep intervals.
  • ✅ Central visualization: Show live, combined data from all nodes on a simple hub page or PC log.

Blocks glossary (used in this project)

  • WiFi STA: Join the same network with SSID/PASSWORD.
  • UDP send/recv: Lightweight datagrams for node messages and sensor reports.
  • Node IDs: Short labels (N1, N2, N3) for addressing and grouping.
  • Heartbeats: Periodic “I’m alive” signals with timestamps to detect failures.
  • Duty cycle: Schedule active sensing and network sleeps to save energy.
  • Serial println: Print “HB:…”, “SENS:…”, “AGG:…”, “NODE:…”, “PWR:…”, “FAULT:…”.

What you need

PartHow many?Pin connection / Notes
D1 R32 (ESP32)3–6Each with USB; same WiFi network
Sensors (demo: ADC)3–6One per node (e.g., Pin 34 analog)
Optional PC1Same network; receives UDP or opens hub page

Notes

  • Assign each node a unique ID (N1, N2, …) and static or known IP if possible.
  • Keep prints short and consistent across nodes to simplify classroom debugging.

Before you start

  • All nodes flashed and labeled with unique IDs
  • All nodes on the same WiFi network
  • Serial monitors open; quick test:
print("Ready!")  # Confirm serial is working so you can see messages

Microprojects 1–5

Microproject 6.9.1 – Communication between multiple R32

Goal: Each node sends a heartbeat; the hub receives and prints IDs and timestamps.
Blocks used:

  • UDP sockets: sendto and recvfrom.
  • Serial println: HB:SENT and HB:RECV.

MicroPython code (Node side):

import network  # Import network to manage WiFi
import socket  # Import socket to send UDP datagrams
import time  # Import time to build timestamps

SSID = "YourSSID"  # Set WiFi SSID
PASSWORD = "YourPassword"  # Set WiFi password
NODE_ID = "N1"  # Set this node's ID (change per board: N1/N2/N3...)
HUB_IP = "192.168.1.100"  # Set hub IP (PC or hub R32)
HUB_PORT = 6300  # Set hub UDP port

wlan = network.WLAN(network.STA_IF)  # Create WiFi station interface
wlan.active(True)  # Activate WiFi hardware
wlan.connect(SSID, PASSWORD)  # Connect to the WiFi network
print("NET:CONNECTING", SSID)  # Print connecting SSID
while not wlan.isconnected():  # Wait until connected
    time.sleep(0.2)  # Short wait
print("NET:CONNECTED", wlan.ifconfig()[0])  # Print local IP

u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket
print("UDP:READY", HUB_IP, HUB_PORT)  # Print hub target info

def hhmmss():  # Build simple time text from ticks
    t = time.ticks_ms() // 1000  # Seconds since boot
    h = (t // 3600) % 24  # Hours modulo 24
    m = (t % 3600) // 60  # Minutes
    s = t % 60  # Seconds
    return "{:02d}:{:02d}:{:02d}".format(h, m, s)  # Format hh:mm:ss

while True:  # Heartbeat loop
    msg = ("HB:" + NODE_ID + " TIME=" + hhmmss()).encode()  # Build heartbeat bytes
    u.sendto(msg, (HUB_IP, HUB_PORT))  # Send heartbeat to hub
    print("HB:SENT", NODE_ID)  # Print heartbeat sent
    time.sleep(1.0)  # Wait 1 second before next heartbeat

MicroPython code (Hub side):

import socket  # Import socket to receive UDP
import time  # Import time for timeouts

PORT = 6300  # Hub listen port
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket
u.bind(("0.0.0.0", PORT))  # Bind to all interfaces
u.settimeout(0.5)  # Short timeout for responsiveness
print("HUB:LISTEN", PORT)  # Print listening port

while True:  # Receive loop
    try:  # Try receive data
        data, addr = u.recvfrom(256)  # Receive up to 256 bytes
        text = data.decode()  # Decode to text
        print("HB:RECV", addr[0], text)  # Print sender IP and heartbeat text
    except OSError:  # Timeout occurred
        pass  # Continue loop
    time.sleep(0.05)  # Small delay

Reflection: A steady heartbeat builds trust—everyone knows who’s online.
Challenge:

  • Easy: Add the node’s local IP in each heartbeat.
  • Harder: Track last heartbeat per node and print “NODE:STALE” after 5 seconds of silence.

Microproject 6.9.2 – Collection of distributed data

Goal: Nodes send sensor readings; hub aggregates and prints per‑node values.
Blocks used:

  • ADC read: Simple analog value per node.
  • UDP reports: SENS:Nx V=… to hub.

MicroPython code (Node side):

import machine  # Import machine to read ADC
import socket  # Import socket to send UDP
import time  # Import time for pacing

NODE_ID = "N1"  # Set unique node ID
HUB_IP = "192.168.1.100"  # Set hub IP
HUB_PORT = 6301  # Set hub port for sensor data
adc = machine.ADC(machine.Pin(34))  # Create ADC on Pin 34 for sensor reading
adc.atten(machine.ADC.ATTN_11DB)  # Set attenuation for full-scale range
adc.width(machine.ADC.WIDTH_12BIT)  # Set 12-bit resolution (0–4095)
print("SENS:ADC READY 34")  # Confirm sensor ADC ready

u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket
print("UDP:READY", HUB_IP, HUB_PORT)  # Print hub target for data

while True:  # Reporting loop
    v = adc.read()  # Read sensor value
    msg = ("SENS:" + NODE_ID + " V=" + str(v)).encode()  # Build sensor report bytes
    u.sendto(msg, (HUB_IP, HUB_PORT))  # Send report to hub
    print("SENS:SENT", v)  # Print sent value
    time.sleep(1.5)  # Wait 1.5 seconds between reports

MicroPython code (Hub side):

import socket  # Import socket for UDP receive
import time  # Import time for timeouts

PORT = 6301  # Sensor hub port
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket
u.bind(("0.0.0.0", PORT))  # Bind to port
u.settimeout(0.5)  # Short timeout
print("AGG:LISTEN", PORT)  # Print listen port

values = {}  # Create dict to hold latest per-node values

while True:  # Aggregation loop
    try:  # Try receive
        data, addr = u.recvfrom(256)  # Receive bytes
        text = data.decode()  # Decode to text
        print("AGG:RECV", text)  # Print received text
        parts = text.split()  # Split by spaces
        node = parts[0].split(":")[1] if ":" in parts[0] else "?"  # Extract node ID
        val = int(parts[1].split("=")[1]) if "=" in parts[1] else 0  # Extract value
        values[node] = val  # Store latest value for node
        print("AGG:STATE", values)  # Print current aggregation state
    except OSError:  # Timeout
        pass  # Continue loop
    time.sleep(0.05)  # Small delay

Reflection: A simple aggregator turns many readings into one coherent picture.
Challenge:

  • Easy: Add a moving average per node before printing.
  • Harder: Add a “UNIT:” field so nodes can report different sensors consistently.

Microproject 6.9.3 – Fault tolerance of a node

Goal: Hub detects missing nodes and marks them as FAULT; nodes retry sending on failure.
Blocks used:

  • Last‑seen map: node → timestamp.
  • Retry logic: Node resends if send fails.

MicroPython code (Hub side):

import socket  # Import socket for UDP receive
import time  # Import time to track timestamps

PORT = 6302  # Fault-detection hub port
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket
u.bind(("0.0.0.0", PORT))  # Bind port for hearing nodes
u.settimeout(0.5)  # Short timeout
print("FAULT:LISTEN", PORT)  # Print listening port

last_seen = {}  # Dict: node ID -> last timestamp (ms)
timeout_ms = 5000  # Consider node missing if >5 s since last heartbeat

while True:  # Detection loop
    now = time.ticks_ms()  # Current time in ms
    try:  # Try receive
        data, addr = u.recvfrom(256)  # Receive bytes
        text = data.decode()  # Decode to text
        print("FAULT:RECV", text)  # Print message
        node = text.split(":")[1].split()[0] if ":" in text else "?"  # Extract node ID
        last_seen[node] = now  # Update last seen timestamp
        print("NODE:SEEN", node)  # Print node seen
    except OSError:  # Timeout
        pass  # Continue loop

    for node, ts in list(last_seen.items()):  # Iterate nodes
        if time.ticks_diff(now, ts) > timeout_ms:  # If timeout exceeded
            print("FAULT:MISSING", node)  # Print missing node
    time.sleep(0.2)  # Small delay

MicroPython code (Node side):

import socket  # Import socket for UDP send
import time  # Import time for retries

NODE_ID = "N1"  # Set node ID
HUB_IP = "192.168.1.100"  # Set hub IP
HUB_PORT = 6302  # Set hub fault-detection port

u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket
print("FAULT:SOCK_READY", HUB_IP, HUB_PORT)  # Print hub info

def send_hb():  # Send heartbeat with simple retry
    msg = ("HB:" + NODE_ID).encode()  # Build heartbeat bytes
    try:  # Try to send
        u.sendto(msg, (HUB_IP, HUB_PORT))  # Send message
        print("HB:SENT", NODE_ID)  # Print success
    except OSError:  # On send error
        print("HB:RETRY", NODE_ID)  # Print retry notice
        time.sleep(0.2)  # Short wait
        try:  # Try again
            u.sendto(msg, (HUB_IP, HUB_PORT))  # Send again
            print("HB:SENT2", NODE_ID)  # Print success
        except OSError:  # If still failing
            print("HB:FAIL", NODE_ID)  # Print failure

while True:  # Heartbeat loop
    send_hb()  # Send heartbeat
    time.sleep(1.0)  # Wait 1 second

Reflection: Knowing who’s missing lets the system adapt instead of stall.
Challenge:

  • Easy: Add “FAULT:RECOVERED” when a missing node returns.
  • Harder: Promote a backup node to “temp hub” if the main hub fails.

Microproject 6.9.4 – Energy optimization

Goal: Nodes duty‑cycle sensing and networking to save energy while staying responsive.
Blocks used:

  • Intervals: ACTIVE window and SLEEP window.
  • Reduced frequency: Longer gaps when stable.

MicroPython code (Node side):

import machine  # Import machine to read ADC
import socket  # Import socket to send UDP
import time  # Import time to schedule duty cycles

NODE_ID = "N1"  # Set node ID
HUB_IP = "192.168.1.100"  # Set hub IP
HUB_PORT = 6303  # Set hub port for energy-aware data
adc = machine.ADC(machine.Pin(34))  # Create ADC on Pin 34
adc.atten(machine.ADC.ATTN_11DB)  # Set attenuation
adc.width(machine.ADC.WIDTH_12BIT)  # Set resolution
print("PWR:ADC READY 34")  # Confirm ADC

u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket
print("PWR:UDP READY", HUB_IP, HUB_PORT)  # Confirm UDP

ACTIVE_MS = 3000  # Active window duration in ms
SLEEP_MS = 2000  # Sleep window duration in ms
stable_count = 0  # Count consecutive stable readings
last_val = -1  # Store last reading value

def send_val(v):  # Send sensor value with power tag
    msg = ("SENS:" + NODE_ID + " V=" + str(v) + " PWR=DC").encode()  # Build message bytes
    u.sendto(msg, (HUB_IP, HUB_PORT))  # Send to hub
    print("PWR:SENT", v)  # Print sent value

while True:  # Duty-cycle loop
    start = time.ticks_ms()  # Record start time
    while time.ticks_diff(time.ticks_ms(), start) < ACTIVE_MS:  # During active window
        v = adc.read()  # Read sensor
        send_val(v)  # Send sensor value
        if last_val != -1 and abs(v - last_val) < 20:  # If stable within small delta
            stable_count += 1  # Increase stability count
        else:  # If not stable
            stable_count = 0  # Reset stability
        last_val = v  # Update last value
        gap = 1000 if stable_count > 3 else 400  # Choose longer gap if stable
        print("PWR:GAP", gap)  # Print chosen gap
        time.sleep(gap / 1000.0)  # Sleep for gap
    print("PWR:SLEEP", SLEEP_MS)  # Print sleep window
    time.sleep(SLEEP_MS / 1000.0)  # Sleep window to save energy

Reflection: Smarter timing cuts energy use without losing important changes.
Challenge:

  • Easy: Add “PWR:LEVEL=LOW/MED/HIGH” based on gap length.
  • Harder: Pause WiFi during long sleeps and reconnect on ACTIVE start.

Microproject 6.9.5 – Centralized data visualization

Goal: Hub serves a simple web page showing latest values per node.
Blocks used:

  • TCP server: Port 80 with minimal HTML.
  • Shared dict: Node → latest value.

MicroPython code (Hub side):

import socket  # Import socket for TCP server
import time  # Import time for pacing

values = {"N1": 0, "N2": 0, "N3": 0}  # Initialize latest values per node

def page_html():  # Build HTML showing values
    body = "<html><body><h1>Network Sensors</h1>"  # Start HTML body
    for k, v in values.items():  # Iterate node values
        body += "<p>" + k + ": " + str(v) + "</p>"  # Add node line
    body += "</body></html>"  # Close HTML body
    return "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n" + body  # Build response

addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]  # Resolve address for port 80
srv = socket.socket()  # Create TCP socket
srv.bind(addr)  # Bind to port 80
srv.listen(1)  # Listen for one client at a time
srv.settimeout(0.2)  # Short timeout for responsiveness
print("WEB:LISTENING", addr)  # Print listening address

udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket for data updates
udp.bind(("0.0.0.0", 6303))  # Bind UDP port for energy-aware data
udp.settimeout(0.2)  # Short timeout
print("AGG:LISTEN 6303")  # Print UDP listen port

while True:  # Main loop
    try:  # Try accept a web client
        cl, remote = srv.accept()  # Accept client
        req = cl.recv(256)  # Read request bytes
        line = req.decode().split('\r\n')[0]  # Get request line
        print("WEB:REQ", line)  # Print request line
        cl.send(page_html().encode())  # Send HTML page
        cl.close()  # Close client
    except OSError:  # Accept timeout
        pass  # Continue loop

    try:  # Try receive UDP sensor update
        data, addr = udp.recvfrom(256)  # Receive bytes
        text = data.decode()  # Decode to text
        print("AGG:RECV", text)  # Print received text
        parts = text.split()  # Split by spaces
        node = parts[0].split(":")[1] if ":" in parts[0] else "?"  # Extract node ID
        val = int(parts[1].split("=")[1]) if "=" in parts[1] else 0  # Extract value
        values[node] = val  # Update dict
    except OSError:  # Timeout
        pass  # Continue loop

    time.sleep(0.05)  # Small delay

Reflection: A simple page makes the invisible visible—every student can see the network breathe.
Challenge:

  • Easy: Add a timestamp line per node.
  • Harder: Color values green/yellow/red based on thresholds with inline CSS.

Main project – Wireless sensor network

Blocks steps (with glossary)

  • Multi‑node heartbeats: Each node announces presence; hub confirms receipt.
  • Distributed sensing: Nodes send ADC readings; hub aggregates latest values.
  • Fault detection: Hub marks missing nodes; nodes retry sending if errors occur.
  • Energy optimization: Nodes duty‑cycle sensing and reporting based on stability.
  • Visualization: Hub serves a tiny HTML page showing the latest network state.

MicroPython code (mirroring blocks)

# Project 6.9 – Wireless Sensor Network (Nodes + Hub)

import network  # Import network for WiFi STA mode
import socket  # Import socket for UDP and TCP
import machine  # Import machine for ADC
import time  # Import time for pacing and timestamps

# Hub configuration (run this on the HUB R32 or PC)
HUB_RX_HB = 6300  # Heartbeat port
HUB_RX_SENS = 6301  # Sensor aggregation port
HUB_RX_PWR = 6303  # Energy-aware data port
WEB_PORT = 80  # Web visualization port

# Node configuration (set per node when used in node mode)
SSID = "YourSSID"  # WiFi SSID
PASSWORD = "YourPassword"  # WiFi password
NODE_ID = "N1"  # Unique node ID (N1/N2/N3...)
HUB_IP = "192.168.1.100"  # Hub IP address

# Shared functions
def hhmmss():  # Build hh:mm:ss string from ticks
    t = time.ticks_ms() // 1000  # Seconds since boot
    h = (t // 3600) % 24  # Hours modulo 24
    m = (t % 3600) // 60  # Minutes
    s = t % 60  # Seconds
    return "{:02d}:{:02d}:{:02d}".format(h, m, s)  # Format text

# =========================
# HUB PROGRAM (aggregation + web)
# =========================
def run_hub():  # Run hub services
    u_hb = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket for heartbeats
    u_hb.bind(("0.0.0.0", HUB_RX_HB))  # Bind heartbeat port
    u_hb.settimeout(0.2)  # Short timeout
    print("HUB:HB_LISTEN", HUB_RX_HB)  # Print heartbeat listen

    u_sens = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket for sensors
    u_sens.bind(("0.0.0.0", HUB_RX_SENS))  # Bind sensor port
    u_sens.settimeout(0.2)  # Short timeout
    print("HUB:SENS_LISTEN", HUB_RX_SENS)  # Print sensor listen

    u_pwr = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # Create UDP socket for energy-aware data
    u_pwr.bind(("0.0.0.0", HUB_RX_PWR))  # Bind power data port
    u_pwr.settimeout(0.2)  # Short timeout
    print("HUB:PWR_LISTEN", HUB_RX_PWR)  # Print power listen

    addr = socket.getaddrinfo('0.0.0.0', WEB_PORT)[0][-1]  # Resolve web address
    srv = socket.socket()  # Create TCP server socket
    srv.bind(addr)  # Bind web port
    srv.listen(1)  # Listen for one client at a time
    srv.settimeout(0.2)  # Short timeout
    print("WEB:LISTEN", addr)  # Print web listen

    last_seen = {}  # Dict: node -> last seen ms
    values = {}  # Dict: node -> latest value
    pwr_values = {}  # Dict: node -> latest energy-aware value

    def page_html():  # Build network status page
        body = "<html><body><h1>WSN Status</h1>"  # Start HTML
        now = hhmmss()  # Current time label
        body += "<p>Time: " + now + "</p>"  # Add time
        body += "<h2>Nodes</h2>"  # Nodes section
        for node, ts in last_seen.items():  # Iterate nodes
            age_ms = time.ticks_diff(time.ticks_ms(), ts)  # Compute age
            status = "OK" if age_ms < 5000 else "STALE"  # Determine status
            val = values.get(node, "-")  # Get sensor value
            pval = pwr_values.get(node, "-")  # Get power value
            body += "<p>" + node + " | status=" + status + " | val=" + str(val) + " | pwr=" + str(pval) + "</p>"  # Add node line
        body += "</body></html>"  # Close HTML
        return "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n" + body  # Build response

    while True:  # Hub main loop
        # Receive heartbeats
        try:  # Try receive heartbeat
            data, addr_hb = u_hb.recvfrom(256)  # Receive bytes
            text = data.decode()  # Decode text
            print("HB:RECV", text)  # Print heartbeat
            node = text.split(":")[1].split()[0] if ":" in text else "?"  # Extract node
            last_seen[node] = time.ticks_ms()  # Update last seen
        except OSError:  # Timeout
            pass  # Continue

        # Receive sensor values
        try:  # Try receive sensor data
            data, addr_s = u_sens.recvfrom(256)  # Receive bytes
            text = data.decode()  # Decode text
            print("SENS:RECV", text)  # Print sensor line
            parts = text.split()  # Split tokens
            node = parts[0].split(":")[1] if ":" in parts[0] else "?"  # Extract node
            val = int(parts[1].split("=")[1]) if "=" in parts[1] else 0  # Extract value
            values[node] = val  # Store latest value
        except OSError:  # Timeout
            pass  # Continue

        # Receive power-aware values
        try:  # Try receive power data
            data, addr_p = u_pwr.recvfrom(256)  # Receive bytes
            text = data.decode()  # Decode text
            print("PWR:RECV", text)  # Print power line
            parts = text.split()  # Split tokens
            node = parts[0].split(":")[1] if ":" in parts[0] else "?"  # Extract node
            val = int(parts[1].split("=")[1]) if "=" in parts[1] else 0  # Extract value
            pwr_values[node] = val  # Store latest power value
        except OSError:  # Timeout
            pass  # Continue

        # Serve web page
        try:  # Try accept web client
            cl, remote = srv.accept()  # Accept client
            req = cl.recv(256)  # Read request bytes
            line = req.decode().split('\r\n')[0]  # Get first line
            print("WEB:REQ", line)  # Print request line
            cl.send(page_html().encode())  # Send page
            cl.close()  # Close client
        except OSError:  # Timeout
            pass  # Continue

        time.sleep(0.05)  # Small delay for responsiveness

# =========================
# NODE PROGRAM (heartbeat + sensing + energy)
# =========================
def run_node():  # Run node sender
    wlan = network.WLAN(network.STA_IF)  # Create WiFi station
    wlan.active(True)  # Activate WiFi
    wlan.connect(SSID, PASSWORD)  # Connect credentials
    print("NET:CONNECTING", SSID)  # Print SSID
    while not wlan.isconnected():  # Wait until connected
        time.sleep(0.2)  # Short delay
    print("NET:CONNECTED", wlan.ifconfig()[0])  # Print local IP

    adc = machine.ADC(machine.Pin(34))  # Create ADC
    adc.atten(machine.ADC.ATTN_11DB)  # Set attenuation
    adc.width(machine.ADC.WIDTH_12BIT)  # Set resolution
    print("ADC:READY 34")  # Confirm ADC

    u_hb = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP heartbeat socket
    u_sens = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP sensor socket
    u_pwr = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP power data socket
    print("UDP:TARGET", HUB_IP, HUB_RX_HB, HUB_RX_SENS, HUB_RX_PWR)  # Print targets

    ACTIVE_MS = 3000  # Active window ms
    SLEEP_MS = 2000  # Sleep window ms
    stable_count = 0  # Stability counter
    last_val = -1  # Last sensor value

    while True:  # Node main loop
        # Send heartbeat
        hb = ("HB:" + NODE_ID + " TIME=" + hhmmss()).encode()  # Build heartbeat
        u_hb.sendto(hb, (HUB_IP, HUB_RX_HB))  # Send heartbeat
        print("HB:SENT", NODE_ID)  # Print sent

        # Active window
        start = time.ticks_ms()  # Record start
        while time.ticks_diff(time.ticks_ms(), start) < ACTIVE_MS:  # Active loop
            v = adc.read()  # Read sensor value
            msg_s = ("SENS:" + NODE_ID + " V=" + str(v)).encode()  # Build sensor report
            u_sens.sendto(msg_s, (HUB_IP, HUB_RX_SENS))  # Send sensor report
            print("SENS:SENT", v)  # Print sent
            # Energy-aware report
            msg_p = ("SENS:" + NODE_ID + " V=" + str(v) + " PWR=DC").encode()  # Build power-aware report
            u_pwr.sendto(msg_p, (HUB_IP, HUB_RX_PWR))  # Send power report
            print("PWR:SENT", v)  # Print power sent
            # Stability logic
            if last_val != -1 and abs(v - last_val) < 20:  # If stable
                stable_count += 1  # Increase stability
            else:  # If not stable
                stable_count = 0  # Reset
            last_val = v  # Update last value
            gap = 1000 if stable_count > 3 else 400  # Choose gap
            print("PWR:GAP", gap)  # Print gap
            time.sleep(gap / 1000.0)  # Sleep for gap

        # Sleep window
        print("PWR:SLEEP", SLEEP_MS)  # Print sleep notice
        time.sleep(SLEEP_MS / 1000.0)  # Sleep to save energy

# Choose one function to run per device:
# run_hub()  # Uncomment on the hub R32
# run_node()  # Uncomment on each node R32 (change NODE_ID and HUB_IP)

External explanation

  • What it teaches: You built a complete small network: nodes announce themselves, send sensor values with energy‑aware timing, the hub detects faults and serves a live page with the latest state.
  • Why it works: UDP makes tiny messages fast; heartbeats track presence; duty cycles save power; simple parsing keeps the hub readable; a minimal web server turns the data into a shared view.
  • Key concept: “Announce → report → detect → optimize → visualize.”

Story time

In the classroom, each robot whispers its status: “I’m here. Value: 213.” The hub listens, notices who’s quiet, and shows the network breathing on a little page. It feels like a constellation—lights flicker, but the pattern holds.


Debugging (2)

Debugging 6.9.1 – Nodes do not communicate

Problem: Hub prints nothing; nodes say HB:SENT.
Clues: Wrong hub IP or port mismatch.
Broken code:

u_hb.sendto(hb, ("192.168.1.200", 6300))  # Wrong hub IP

Fixed code:

HUB_IP = "192.168.1.100"  # Correct hub IP
HUB_RX_HB = 6300  # Match hub heartbeat port
print("DEBUG:TARGET", HUB_IP, HUB_RX_HB)  # Verify targets

Why it works: Matching IP and ports ensures packets reach the hub.
Avoid next time: Write targets on a card and test with a single node first.

Debugging 6.9.2 – Data loss on the network

Problem: Hub receives some values but many are missing.
Clues: Messages arrive in bursts; no pacing.
Broken code:

u_sens.sendto(msg_s, (HUB_IP, HUB_RX_SENS))  # Spammed with 0 ms delay

Fixed code:

time.sleep(gap / 1000.0)  # Add a short delay (400–1000 ms) between sends
print("DEBUG:PACED_SEND")  # Confirm pacing

Why it works: Small gaps reduce congestion and allow the hub to process packets.
Avoid next time: Pace sends and keep payloads short with clear tags.


Final checklist

  • All nodes connect to WiFi and send HB with IDs and time
  • Hub aggregates sensor values and prints per‑node state
  • Missing nodes are detected as FAULT after a timeout
  • Nodes duty‑cycle reports and show longer gaps when stable
  • Hub serves a web page that shows latest values and node status

Extras

  • 🧠 Student tip: Keep a simple schema: “HB:Nx TIME=hh:mm:ss” and “SENS:Nx V=####”. Consistency prevents parsing headaches.
  • 🧑‍🏫 Instructor tip: Assign NODE_IDs at the start and run a roll call: the hub should list everyone.
  • 📖 Glossary:
    • Heartbeat: A periodic “I’m alive” message.
    • Duty cycle: Alternating active and sleep windows to save energy.
    • Aggregation: Combining many readings into a single, coherent state.
  • 💡 Mini tips:
    • Prefer short tags and integers; avoid big JSON in classroom demos.
    • Use timeouts and small delays to keep loops responsive.
    • Start with 2–3 nodes before scaling to 6+ to tune pacing.
On this page