Project 6.8: "WiFi Messenger Robot"
What you’ll learn
- ✅ WiFi messaging: Receive short text messages over WiFi and parse them into actions.
- ✅ Navigation to destination: Move to predefined waypoints using simple steps.
- ✅ Visual and audio delivery: Show messages on OLED and beep/LED when delivered.
- ✅ Delivery confirmation: Send a reply back to the sender with status and time stamp.
- ✅ Multi‑message queue: Handle multiple incoming messages in order without losing any.
Blocks glossary (used in this project)
- WiFi STA + UDP/TCP: Connect to WiFi; use UDP for messages and TCP for optional status page.
- Message parser: Read “TO:ROOM_A|MSG:Hello|ID:42” and split into fields.
- Destinations table: Map labels like ROOM_A to coordinates or steps.
- OLED display: Show sender, destination, and message lines.
- LED/buzzer: Acknowledge delivery with a short pattern.
- Serial println: Print “RX:…”, “NAV:…”, “DELIVER:…”, “CONFIRM:…”, and “QUEUE:…” lines.
What you need
| Part | How many? | Pin connection / Notes |
|---|---|---|
| D1 R32 (ESP32) | 1 | USB cable (30 cm) |
| WiFi network (2.4 GHz) | 1 | SSID and PASSWORD ready |
| OLED SSD1306 128×64 | 1 | I2C: SCL→22, SDA→21, VCC, GND |
| LED module | 1 | Signal → Pin 13, VCC, GND |
| Buzzer module | 1 | Signal → Pin 23, VCC, GND |
| Motors + L298N | 1 set | Left IN1→18, IN2→19; Right IN3→5, IN4→23 |
Notes
- Share ground across R32, OLED, LED, buzzer, and L298N.
- Keep OLED wires short; confirm I2C address (default 0x3C).
- Use UDP for fast, simple messaging; reserve a fixed port for class.
Before you start
- WiFi SSID/PASSWORD are correct
- OLED wired to 22/21 and powers on
- Serial monitor open and shows:
print("Ready!") # Confirm serial is working so you can see messages
Microprojects 1–5
Microproject 6.8.1 – Receiving messages via WiFi
Goal: Join WiFi, open a UDP socket, receive a text message, and print it.
Blocks used:
- WiFi STA: Connect and show IP.
- UDP recvfrom: Read bytes and decode.
MicroPython code:
import network # Import network to manage WiFi
import socket # Import socket to handle UDP
import time # Import time for short delays
SSID = "YourSSID" # Put your WiFi SSID here
PASSWORD = "YourPassword" # Put your WiFi password here
PORT_RX = 6200 # Choose a UDP port to receive messages
wlan = network.WLAN(network.STA_IF) # Create a WiFi station interface
wlan.active(True) # Activate WiFi hardware
wlan.connect(SSID, PASSWORD) # Start connecting to WiFi
print("NET:CONNECTING", SSID) # Print which network we are connecting to
while not wlan.isconnected(): # Wait until connected
time.sleep(0.2) # Small delay between checks
print("NET:CONNECTED", wlan.ifconfig()[0]) # Print assigned IP
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Create a UDP socket
u.bind(("0.0.0.0", PORT_RX)) # Bind to all interfaces on chosen port
u.settimeout(1.0) # Set timeout so code does not block forever
print("UDP:LISTEN", PORT_RX) # Print listening port
try: # Try to receive one message
data, addr = u.recvfrom(256) # Receive up to 256 bytes
text = data.decode() # Decode bytes to text
print("RX:FROM", addr[0]) # Print sender IP
print("RX:TEXT", text) # Print received text
except OSError: # If timeout happened
print("RX:NONE") # No message arrived
u.close() # Close UDP socket
Reflection: One message proves your robot can “hear” over WiFi.
Challenge:
- Easy: Print message length (“LEN:n”).
- Harder: Keep the socket open and receive up to 3 messages in a loop.
Microproject 6.8.2 – Navigation to a predefined destination
Goal: Map destination labels (ROOM_A/B) to simple movement steps and print progress.
Blocks used:
- Dict table: label → sequence.
- Motor helpers: forward, left, right, stop.
MicroPython code:
import machine # Import machine to control motor pins
import time # Import time for movement pulses
# Motor pins for L298N
L_IN1 = machine.Pin(18, machine.Pin.OUT) # Left IN1
L_IN2 = machine.Pin(19, machine.Pin.OUT) # Left IN2
R_IN3 = machine.Pin(5, machine.Pin.OUT) # Right IN3
R_IN4 = machine.Pin(23, machine.Pin.OUT) # Right IN4
print("MOTORS:READY 18/19 5/23") # Confirm motor pins
def motors_stop(): # Stop both motors
L_IN1.value(0) # Left IN1 LOW
L_IN2.value(0) # Left IN2 LOW
R_IN3.value(0) # Right IN3 LOW
R_IN4.value(0) # Right IN4 LOW
print("MOVE:STOP") # Print stop
def forward(pulse=0.25): # Move forward
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:FWD", pulse) # Print action
time.sleep(pulse) # Run duration
motors_stop() # Stop after pulse
def left(pulse=0.18): # Turn left
L_IN1.value(0) # Left backward LOW
L_IN2.value(1) # Left backward HIGH
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:LEFT", pulse) # Print action
time.sleep(pulse) # Run duration
motors_stop() # Stop after pulse
def right(pulse=0.18): # Turn right
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(0) # Right backward LOW
R_IN4.value(1) # Right backward HIGH
print("MOVE:RIGHT", pulse) # Print action
time.sleep(pulse) # Run duration
motors_stop() # Stop after pulse
routes = { # Define simple routes for destinations
"ROOM_A": ["FWD", "LEFT", "FWD"], # Path to ROOM_A
"ROOM_B": ["FWD", "RIGHT", "FWD"] # Path to ROOM_B
}
print("NAV:ROUTES", list(routes.keys())) # Print available routes
def run_route(dest): # Execute route steps for a destination
seq = routes.get(dest, []) # Get step sequence or empty
print("NAV:DEST", dest) # Print destination
for step in seq: # Iterate over steps
if step == "FWD": # If forward step
forward(0.25) # Move forward pulse
elif step == "LEFT": # If left step
left(0.18) # Turn left pulse
elif step == "RIGHT": # If right step
right(0.18) # Turn right pulse
time.sleep(0.05) # Small spacing between steps
run_route("ROOM_A") # Demo route to ROOM_A
Reflection: Destinations feel real when steps are clear and labeled.
Challenge:
- Easy: Add “ROOM_C” with two turns.
- Harder: Insert “PAUSE” steps to simulate doors or obstacles.
Microproject 6.8.3 – Message delivery (visual/auditory)
Goal: Show message on OLED and play a short LED/buzzer pattern on delivery.
Blocks used:
- OLED show: title + two lines.
- LED/buzzer pattern: 3 short blinks/beeps.
MicroPython code:
import machine # Import machine to access I2C and pins
import time # Import time for pattern timing
import oled128x64 # Import OLED driver for SSD1306 128x64
# OLED I2C setup
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000) # Create I2C bus
oled = oled128x64.OLED(i2c, address=0x3c, font_address=0x3A0000, types=0) # Initialize OLED
print("OLED:READY 0x3C") # Confirm OLED ready
# LED and buzzer pins
led = machine.Pin(13, machine.Pin.OUT) # LED on Pin 13
buzzer = machine.Pin(23, machine.Pin.OUT) # Buzzer on Pin 23
print("ALERT:LED=13 BUZZER=23") # Confirm alert pins
def show_delivery(to_label, msg_text): # Display delivery on OLED
oled.clear() # Clear screen
oled.shows('WiFi Messenger', x=0, y=0, size=1, space=0, center=False) # Title line
oled.shows('TO:'+to_label, x=0, y=12, size=1, space=0, center=False) # Destination line
oled.shows('MSG:'+msg_text, x=0, y=24, size=1, space=0, center=False) # Message line
oled.show() # Refresh OLED
print("DELIVER:OLED", to_label) # Print deliver OLED
def celebrate(): # LED/buzzer pattern for delivery
for i in range(3): # Repeat 3 times
led.value(1) # LED ON
buzzer.value(1) # Buzzer ON
time.sleep(0.1) # On duration
led.value(0) # LED OFF
buzzer.value(0) # Buzzer OFF
time.sleep(0.1) # Off duration
print("DELIVER:CELEBRATE") # Print celebration done
show_delivery("ROOM_A", "Hello!") # Demo display
celebrate() # Demo alert pattern
Reflection: A tiny show + celebrate moment turns robots into friendly couriers.
Challenge:
- Easy: Add a quiet mode (LED only).
- Harder: Show a 2‑line scroll when the message is longer than screen width.
Microproject 6.8.4 – Delivery confirmation
Goal: Send “CONFIRM:ID=42 STATUS=DELIVERED TIME=hh:mm:ss” back to sender via UDP.
Blocks used:
- UDP sendto: Send confirmation to sender IP:PORT.
- Time stamp: Compose a simple hh:mm:ss from ticks.
MicroPython code:
import socket # Import socket to send UDP confirmation
import time # Import time for timestamp
SENDER_IP = "192.168.1.100" # Put the sender's IP here
SENDER_PORT = 6200 # Use the same port the sender listens on
msg_id = "42" # Demo message ID
def hhmmss(): # Create a simple hh:mm:ss from ticks
t = time.ticks_ms() // 1000 # Get seconds since boot
h = (t // 3600) % 24 # Compute hours modulo 24
m = (t % 3600) // 60 # Compute minutes
s = t % 60 # Compute seconds
return "{:02d}:{:02d}:{:02d}".format(h, m, s) # Format hh:mm:ss
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Create UDP socket
confirm_text = "CONFIRM:ID=" + msg_id + " STATUS=DELIVERED TIME=" + hhmmss() # Build confirm line
u.sendto(confirm_text.encode(), (SENDER_IP, SENDER_PORT)) # Send to sender IP:PORT
print("CONFIRM:SENT", confirm_text) # Print confirmation text
u.close() # Close UDP socket
Reflection: Confirmation closes the loop—sender knows the job is done.
Challenge:
- Easy: Add “STATUS=FAILED” when route not found.
- Harder: Include “TO:ROOM_A” and “MSG_LEN:n” in confirmation.
Microproject 6.8.5 – Multiple message system
Goal: Keep a small queue; process messages one by one with timeouts and clear prints.
Blocks used:
- List queue: append on receive; pop from front when processing.
- State print: show queue length and active ID.
MicroPython code:
import network # Import network for WiFi (assume already connected)
import socket # Import socket for UDP messaging
import time # Import time for pacing
PORT_RX = 6200 # Port to receive messages
queue = [] # Initialize an empty message queue
active_id = "" # Track current message ID
print("QUEUE:INIT") # Print init line
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Create UDP socket
u.bind(("0.0.0.0", PORT_RX)) # Bind local receive port
u.settimeout(0.2) # Short timeout for responsiveness
print("UDP:LISTEN", PORT_RX) # Print listening port
def enqueue(text, addr): # Add a message to the queue
queue.append((text, addr)) # Append tuple (text, sender addr)
print("QUEUE:ADD", len(queue)) # Print new queue length
def dequeue(): # Take next message from queue
if queue: # If queue not empty
print("QUEUE:NEXT", len(queue)) # Print queue length
return queue.pop(0) # Pop front item
return None # Otherwise return None
# Try receiving messages for a short period
start = time.ticks_ms() # Record start time
while time.ticks_diff(time.ticks_ms(), start) < 1500: # Run ~1.5 s
try: # Try receive
data, addr = u.recvfrom(256) # Receive bytes
text = data.decode() # Decode to text
print("RX:TEXT", text) # Print text
enqueue(text, addr) # Add to queue
except OSError: # On timeout
pass # Continue loop
item = dequeue() # Get next queued message
if item: # If there is an item
text, addr = item # Unpack text and sender address
# Extract simple ID field
parts = text.split("|") # Split text by '|'
id_part = next((p for p in parts if p.startswith("ID:")), "ID:?") # Find ID
active_id = id_part.split(":", 1)[1] if ":" in id_part else "?" # Extract ID value
print("QUEUE:ACTIVE_ID", active_id) # Print active message ID
else: # If queue empty
print("QUEUE:EMPTY") # Print empty queue
u.close() # Close UDP socket
Reflection: Queues keep things calm—no message is forgotten, and each gets full attention.
Challenge:
- Easy: Limit queue size to 5 and drop oldest on overflow.
- Harder: Add a retry counter per message for robust delivery.
Main project – WiFi messenger robot
Blocks steps (with glossary)
- WiFi + UDP: Connect, listen on a port, and receive tagged messages.
- Parse fields: TO, MSG, ID; validate destination exists.
- Navigate: Run route steps to destination with prints and safe pauses.
- Deliver: Show on OLED and celebrate with LED/buzzer pattern.
- Confirm: Send confirmation back to sender with ID and time.
- Queue: Process multiple messages in order.
MicroPython code (mirroring blocks)
# Project 6.8 – WiFi Messenger Robot
import network # Import network to manage WiFi STA mode
import socket # Import socket to send/receive UDP
import machine # Import machine for OLED, pins, and motors
import time # Import time for delays and timestamps
import oled128x64 # Import OLED driver for SSD1306 128x64
# WiFi credentials and ports
SSID = "YourSSID" # Put your WiFi SSID here
PASSWORD = "YourPassword" # Put your WiFi password here
PORT_RX = 6200 # UDP port to receive messages
SENDER_PORT = 6200 # Port to send confirmation (same as sender's listen)
print("NET:CONFIG RX_PORT", PORT_RX) # Print network config
# OLED setup
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000) # Create I2C bus on pins 22/21
oled = oled128x64.OLED(i2c, address=0x3c, font_address=0x3A0000, types=0) # Initialize OLED object
print("OLED:READY 0x3C") # Confirm OLED setup
# LED and buzzer for delivery alerts
led = machine.Pin(13, machine.Pin.OUT) # LED output on Pin 13
buzzer = machine.Pin(23, machine.Pin.OUT) # Buzzer output on Pin 23
print("ALERT:PINS LED=13 BUZZER=23") # Confirm alert pins
# Motors: L298N pins
L_IN1 = machine.Pin(18, machine.Pin.OUT) # Left IN1
L_IN2 = machine.Pin(19, machine.Pin.OUT) # Left IN2
R_IN3 = machine.Pin(5, machine.Pin.OUT) # Right IN3
R_IN4 = machine.Pin(23, machine.Pin.OUT) # Right IN4
print("MOTORS:PINS 18/19 5/23") # Confirm motor pins
def motors_stop(): # Stop both motors
L_IN1.value(0) # Left IN1 LOW
L_IN2.value(0) # Left IN2 LOW
R_IN3.value(0) # Right IN3 LOW
R_IN4.value(0) # Right IN4 LOW
print("MOVE:STOP") # Print stop
def forward(pulse=0.25): # Move forward for a short pulse
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:FWD", pulse) # Print forward pulse
time.sleep(pulse) # Run motors for pulse
motors_stop() # Stop motors
def left(pulse=0.18): # Turn left in place
L_IN1.value(0) # Left backward LOW
L_IN2.value(1) # Left backward HIGH
R_IN3.value(1) # Right forward HIGH
R_IN4.value(0) # Right forward LOW
print("MOVE:LEFT", pulse) # Print left turn pulse
time.sleep(pulse) # Run motors for pulse
motors_stop() # Stop motors
def right(pulse=0.18): # Turn right in place
L_IN1.value(1) # Left forward HIGH
L_IN2.value(0) # Left forward LOW
R_IN3.value(0) # Right backward LOW
R_IN4.value(1) # Right backward HIGH
print("MOVE:RIGHT", pulse) # Print right turn pulse
time.sleep(pulse) # Run motors for pulse
motors_stop() # Stop motors
def show_delivery(to_label, msg_text): # Display message on OLED
oled.clear() # Clear display
oled.shows('WiFi Messenger', x=0, y=0, size=1, space=0, center=False) # Title text
oled.shows('TO:'+to_label, x=0, y=12, size=1, space=0, center=False) # Destination text
oled.shows('MSG:'+msg_text, x=0, y=24, size=1, space=0, center=False) # Message text
oled.show() # Refresh display
print("DELIVER:OLED", to_label) # Print OLED delivery
def celebrate(): # LED/buzzer pattern to acknowledge delivery
for i in range(3): # Repeat 3 blinks/beeps
led.value(1) # LED ON
buzzer.value(1) # Buzzer ON
time.sleep(0.1) # On duration
led.value(0) # LED OFF
buzzer.value(0) # Buzzer OFF
time.sleep(0.1) # Off duration
print("DELIVER:CELEBRATE") # Print completion of celebration
routes = { # Predefined routes to destinations
"ROOM_A": ["FWD", "LEFT", "FWD"], # Route steps to ROOM_A
"ROOM_B": ["FWD", "RIGHT", "FWD"], # Route steps to ROOM_B
"HOME": ["RIGHT", "FWD"] # Route steps back to HOME
}
print("NAV:ROUTES", list(routes.keys())) # Print available destinations
def run_route(dest): # Execute movement sequence to a destination
seq = routes.get(dest, []) # Get sequence or empty list
print("NAV:DEST", dest) # Print chosen destination
for step in seq: # Loop through each step
if step == "FWD": # If forward
forward(0.25) # Forward pulse
elif step == "LEFT": # If left
left(0.18) # Left pulse
elif step == "RIGHT": # If right
right(0.18) # Right pulse
time.sleep(0.05) # Small pause between steps
def hhmmss(): # Build a simple hh:mm:ss timestamp
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
def parse_fields(text): # Parse TO, MSG, ID fields from message
fields = {"TO": "", "MSG": "", "ID": ""} # Initialize fields dictionary
for part in text.split("|"): # Split by '|'
if ":" in part: # If key:value format
k, v = part.split(":", 1) # Split into key and value
if k in fields: # If known field
fields[k] = v # Store value
print("PARSE:", fields) # Print parsed fields
return fields # Return fields dict
# WiFi connect
wlan = network.WLAN(network.STA_IF) # Create station interface
wlan.active(True) # Activate WiFi
wlan.connect(SSID, PASSWORD) # Connect to WiFi
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
# UDP socket for receiving and confirming
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Create UDP socket
u.bind(("0.0.0.0", PORT_RX)) # Bind receive port
u.settimeout(0.2) # Short timeout to keep loop responsive
print("UDP:LISTEN", PORT_RX) # Print listening port
queue = [] # Initialize message queue
def enqueue(text, addr): # Add message to queue
queue.append((text, addr)) # Append tuple (text, sender addr)
print("QUEUE:ADD", len(queue)) # Print queue length
def dequeue(): # Remove the first message from queue
if queue: # If queue not empty
print("QUEUE:NEXT", len(queue)) # Print current queue length
return queue.pop(0) # Pop front
return None # Otherwise, no item
print("RUN:Messenger") # Announce start
while True: # Main messenger loop
# Receive phase
try: # Try receive a message
data, addr = u.recvfrom(256) # Read up to 256 bytes
text = data.decode() # Decode bytes to text
print("RX:FROM", addr[0]) # Print sender IP
print("RX:TEXT", text) # Print raw message
enqueue(text, addr) # Add to processing queue
except OSError: # Timeout means no message
pass # Continue loop
# Process one queued message per cycle
item = dequeue() # Get next message
if item: # If there is a message
text, addr = item # Unpack message and sender
fields = parse_fields(text) # Parse TO, MSG, ID
to_label = fields["TO"] # Extract destination
msg_text = fields["MSG"] # Extract message text
msg_id = fields["ID"] # Extract message ID
if to_label in routes: # If destination known
run_route(to_label) # Navigate to destination
show_delivery(to_label, msg_text) # Show message on OLED
celebrate() # Blink/beep to acknowledge delivery
confirm_text = "CONFIRM:ID=" + msg_id + " TO=" + to_label + " STATUS=DELIVERED TIME=" + hhmmss() # Build confirmation line
u.sendto(confirm_text.encode(), (addr[0], SENDER_PORT)) # Send confirmation to sender IP
print("CONFIRM:SENT", confirm_text) # Print confirmation
else: # Unknown destination
err_text = "CONFIRM:ID=" + (msg_id or "?") + " STATUS=FAILED REASON=UNKNOWN_DEST TIME=" + hhmmss() # Build failure confirm
u.sendto(err_text.encode(), (addr[0], SENDER_PORT)) # Send failure confirmation
print("CONFIRM:SENT", err_text) # Print failure
time.sleep(0.05) # Short delay to keep loop responsive
External explanation
- What it teaches: How to turn WiFi text messages into real actions: parse fields, move to a destination, show the message, celebrate, and confirm. You also kept a queue so multiple messages are handled calmly.
- Why it works: UDP keeps messages light; clear tags make parsing easy; routes stay simple and reliable; OLED + alerts make delivery obvious; confirmations close the loop.
- Key concept: “Receive → parse → go → show → confirm.”
Story time
A ping arrives: “TO:ROOM_A | MSG:You’ve got mail.” Your robot glides down the hall, lights up the OLED, beeps three times, and replies, “Delivered at 09:12:03.” It’s a tiny courier with manners.
Debugging (2)
Debugging 6.8.1 – Message not delivered
Problem: Robot receives text but doesn’t move or show.
Clues: “REASON=UNKNOWN_DEST” appears; routes missing the label.
Broken code:
routes = {"ROOM_A": ["FWD"], "ROOMB": ["FWD"]} # Typo in ROOM_B
Fixed code:
routes = {"ROOM_A": ["FWD"], "ROOM_B": ["FWD"]} # Correct label
print("DEBUG:ROUTES", list(routes.keys())) # Verify labels
Why it works: Matching labels ensures parsing finds a valid destination and runs the route.
Avoid next time: Keep a destination glossary card and test each route once.
Debugging 6.8.2 – Navigation to the wrong destination
Problem: Robot runs the wrong route for the message.
Clues: Parsed fields show TO is empty or truncated.
Broken code:
for part in text.split("|"):
k, v = part.split(":") # Fails on messages with extra ':' in MSG
Fixed code:
k, v = part.split(":", 1) # Split only on the first ':' to preserve message body
print("DEBUG:PARSE", k, v) # Verify parsed pairs
Why it works: Splitting once avoids breaking MSG when the text contains ‘:’ characters.
Avoid next time: Always parse with cautious splits and validate fields before moving.
Final checklist
- WiFi connects and prints local IP
- UDP socket listens and receives text reliably
- TO/MSG/ID fields parse correctly even with ‘:’ inside MSG
- Robot runs the correct route steps and stops cleanly
- OLED shows TO and MSG; LED/buzzer celebrate delivery
- Confirmation UDP reply includes ID, TO, STATUS, and TIME
- Queue processes multiple messages in order without blocking
Extras
- 🧠 Student tip: Add “QUIET:ON/OFF” to switch buzzer off at night—keep LED only.
- 🧑🏫 Instructor tip: Provide a class‑wide destination map (ROOM_A/B/C/HOME) so everyone’s routes match.
- 📖 Glossary:
- Tag: A short key that labels message parts (e.g., TO, MSG, ID).
- Queue: A list that processes items in arrival order.
- Confirmation: A return message that proves delivery happened.
- 💡 Mini tips:
- Keep messages short and consistent: “TO:X|MSG:Y|ID:Z”.
- Use fixed ports and IP reservations to simplify classroom networking.
- Log “CONFIRM:SENT” on the sender PC to verify the full loop.