Group assignment
Individual assignment
As a remote student joining from Kuwait, I was online with the VujaDé Lab session in Saudi Arabia when the team ran this week's group experiment. The session is documented in detail by my colleague Sarah AlDosary. Because I was remote I couldn't pair with another in-person student, so my own networking experimentation in the individual assignment below uses two of my own ESP32-C3 XIAO Connection Nodes communicating over ESP-NOW — the same Node-to-Node pattern that will run between the left and right clips in my final project.
Understanding the four communication protocols available on the XIAO ESP32-C3 Microcontroller was the starting point for choosing how the two Connection Nodes in my final project would talk to each other and to the phone app. Each protocol has a different sweet spot for speed, range, power consumption, and whether it needs an external router. I used to gather the summaries below.
Prompt to Gemini: I want to start documenting Week 11 — FabLab Networking and Communication. First, let's have a short brief about the following communications: wired USB-C, wireless Wi-Fi, wireless Bluetooth Low Energy, ESP-NOW — I'll be using the XIAO ESP32-C3. (Verbatim prompt — kept as sent.)
01 | USB-C (Serial) — a wired connection. Very high speed, medium power draw on the bus, range is the length of the cable (short). It uses physical TX / RX lines internal to the USB chip to transmit and receive. The ESP32-C3 has a built-in USB Serial / JTAG controller, so plugging in a Type-C cable establishes a direct, high-speed, reliable wired serial link. Best for: debugging via the Serial Monitor, flashing firmware, and high-reliability, short-distance data transfer between the Microcontroller and a computer.
02 | Wi-Fi — a wireless protocol that requires a router. Range 50–100 m indoors, high speed, high power consumption. Operates primarily on 2.4 GHz for microcontrollers, using standard IP routing (TCP/IP, UDP, HTTP, MQTT). On the XIAO ESP32-C3, the 2.4 GHz Wi-Fi subsystem is fully integrated — the board can act as a Station (STA) to connect to a home / lab router, or as an Access Point (AP) so a phone can connect directly to it. Best for: IoT projects, uploading data to cloud dashboards, hosting a local web server, and long-range indoor communication through walls via a router.
03 | Bluetooth Low Energy (BLE) — wireless personal-area-network technology. Low speed, very low power consumption, range 10–30 m, no router required. Unlike classic Bluetooth (which streams heavy audio), BLE stays in sleep mode until a connection is initiated. It organises data into Services and Characteristics using a client-server structure. The chip supports Bluetooth 5.0 and BLE mesh, and is power-efficient enough for the XIAO to broadcast advertisements that smartphones or BLE gateways can pick up. Best for: connecting the XIAO to a smartphone app, battery-powered sensors, and close-range proximity tracking.
04 | ESP-NOW — wireless, medium speed, range up to 200 m in open air, low power consumption, no router required. ESP-NOW is a proprietary protocol developed by Espressif (the makers of the ESP32). It enables direct peer-to-peer wireless communication between ESP chips without needing a Wi-Fi router. It uses the same 2.4 GHz radio hardware as Wi-Fi but skips the lengthy Wi-Fi handshake and connection process — letting one Microcontroller flash a packet to another ESP32 / ESP8266 in milliseconds. Best for: ultra-fast, low-latency communication between multiple Fab Lab Connection Nodes, remote controls, and battery-optimised mesh networks where no router is available.
Testing the protocols using Thonny IDE — the MicroPython toolchain setup is documented in Week 04 — Embedded Programming. I used Gemini for coding assistance to establish the Node-to-Node connection in MicroPython. The plan: Connection Node 1 is the sender — an ESP32-C3 XIAO with an MPU6050 (input from W09). Connection Node 2 is the receiver — an ESP32-C3 XIAO with an LED (output from W10). When Node 1 detects motion past a calibrated threshold, it transmits an ESP-NOW packet to Node 2, which blinks the LED.
01 | I'm using two XIAO ESP32-C3 Microcontrollers that are already mounted on the PCBs from previous weeks: Connection Node 2 — the receiver (PCB with an LED) and Connection Node 1 — the sender (PCB with an MPU6050 and an LED).
02 | Finding the MAC address of Connection Node 2 (the receiver) so Node 1 knows where to send packets. The output is in the machine format: b'\x24\xdc\xc3\x12\x34\x56' — raw bytes the radio transmitter can use directly. Microcontrollers have limited processing power, so the chip doesn't want to waste cycles parsing colon-separated strings on every transmit; it wants the raw bytes already. The human format — 24:DC:C3:1A:2B:3C — is the same address as a string literal for use in config menus, command lines, and documentation.
import network
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
print("Connection Node 2 MAC Address:", wlan.config('mac'))
03 | On Connection Node 1 (the sender), I uploaded this lightweight MPU6050 driver and saved it on the Microcontroller's filesystem as mpu6050.py. To confirm the upload, the file appears in the file-explorer side panel of Thonny.
import machine
class MPU6050:
def __init__(self, i2c, addr=0x68):
self.i2c = i2c
self.addr = addr
# Wake up the MPU-6050 (it starts in sleep mode)
self.i2c.writeto_mem(self.addr, 0x6B, b'\x00')
def get_values(self):
# Read 6 bytes of data starting from ACCEL_XOUT_H (0x3B)
raw = self.i2c.readfrom_mem(self.addr, 0x3B, 6)
# Combine high and low bytes and convert to signed 16-bit integers
x = (raw[0] << 8 | raw[1])
y = (raw[2] << 8 | raw[3])
z = (raw[4] << 8 | raw[5])
if x > 32767: x -= 65536
if y > 32767: y -= 65536
if z > 32767: z -= 65536
return {"X": x, "Y": y, "Z": z}
04 | To confirm the MPU6050 is connected to Connection Node 1 on the correct GPIO pins, I ran this I²C-scan helper. It tries the common SDA / SCL pairs for the XIAO ESP32-C3 and reports which pair finds the sensor at address 0x68.
import machine
# Common pairs for XIAO ESP32-C3: (SDA, SCL)
pin_pairs = [
(6, 7), # True ESP32-C3 hardware mapping for D4/D5
(4, 5), # MicroPython default mapping alias
(2, 3), # Alternative GPIO mapping
]
found = False
for sda_pin, scl_pin in pin_pairs:
print(f"Testing pair -> SDA: GPIO {sda_pin}, SCL: GPIO {scl_pin}...")
try:
i2c = machine.I2C(0, sda=machine.Pin(sda_pin), scl=machine.Pin(scl_pin))
devices = i2c.scan()
if devices:
print(f"🎉 SUCCESS! Found device {devices} at SDA:{sda_pin}, SCL:{scl_pin}")
found = True
break
except Exception as e:
print(f" Error on pins {sda_pin}/{scl_pin}: {e}")
if not found:
print("\n❌ No devices found on any common pin pairs.")
print("If the green light is on, double-check that your SDA and SCL wires aren't swapped accidentally!")
05 | With the I²C pins confirmed (SDA = 6, SCL = 7), I started reading live values from the MPU6050. Moving the sensor in different directions showed the X / Y / Z values changing on screen in Thonny's REPL — confirmation that the input chain is healthy before we add networking on top.
import machine
import time
from mpu6050 import MPU6050
# Initialize I2C using your verified pins (SDA: 6, SCL: 7)
i2c = machine.I2C(0, scl=machine.Pin(7), sda=machine.Pin(6))
sensor = MPU6050(i2c)
print("Reading live data from MPU-6050... Press Ctrl+C to stop.\n")
time.sleep(1)
while True:
try:
data = sensor.get_values()
# Print the data in a clear layout
print(f"X: {data['X']:6d} | Y: {data['Y']:6d} | Z: {data['Z']:6d}")
except Exception as e:
print("Error reading sensor:", e)
time.sleep(0.2) # Read 5 times a second
06 | Then I connected the two Microcontroller Nodes by hard-coding the MAC address of Connection Node 2 (the receiver) into the code running on Connection Node 1 (the MPU6050 sender). The sender calibrates a baseline at startup, then continuously compares live readings against it — when motion exceeds the THRESHOLD, it sends a BLINK_LED packet to Node 2 over ESP-NOW.
Connection Node 1 — the MPU6050 sender:
import network
import espnow
import machine
import time
# --- 1. THE INLINED MPU-6050 DRIVER ---
class MPU6050:
def __init__(self, i2c, addr=0x68):
self.i2c = i2c
self.addr = addr
self.i2c.writeto_mem(self.addr, 0x6B, b'\x00') # Wake up
def get_values(self):
raw = self.i2c.readfrom_mem(self.addr, 0x3B, 6)
x = (raw[0] << 8 | raw[1])
y = (raw[2] << 8 | raw[3])
z = (raw[4] << 8 | raw[5])
if x > 32767: x -= 65536
if y > 32767: y -= 65536
if z > 32767: z -= 65536
return {"X": x, "Y": y, "Z": z}
# --- 2. INITIALIZE ESP-NOW WIRELESS ---
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
e = espnow.ESPNow()
e.active(True)
# !!! REPLACE WITH THE EXACT RECEIVER MAC ADDRESS !!!
peer_mac = b'\x24\xdc\xc3\x12\x34\x56'
e.add_peer(peer_mac)
# --- 3. INITIALIZE HARDWARE I2C (SDA:6, SCL:7) ---
i2c = machine.I2C(0, scl=machine.Pin(7), sda=machine.Pin(6))
sensor = MPU6050(i2c)
# --- 4. CALIBRATION RUN ---
THRESHOLD = 3000
print("Calibrating baseline... Keep sensor still.")
time.sleep(1)
baseline = sensor.get_values()
print("Sender Armed! Move the sensor to transmit triggers.\n")
while True:
try:
current = sensor.get_values()
# Calculate absolute motion differences
diff_x = abs(current["X"] - baseline["X"])
diff_y = abs(current["Y"] - baseline["Y"])
# Stream live data to Thonny Shell
print(f"LIVE DATA -> X: {current['X']:6d} | Y: {current['Y']:6d} ", end="")
# Check if movement breaks threshold limits
if diff_x > THRESHOLD or diff_y > THRESHOLD:
print(f" 📡 WIRELESS PACKET TRANSMITTED! (Delta X:{diff_x} Y:{diff_y})")
# Send the trigger payload to Connection Node 2
e.send(peer_mac, b'BLINK_LED')
time.sleep(0.8) # Prevent packet flooding while shaking
baseline = sensor.get_values() # Reset baseline point
else:
print("\r", end="") # Clean lines refresh trick
except Exception as err:
print("\nSensor Loop Error:", err)
time.sleep(0.1)
Connection Node 2 — the receiver / blinker:
import network
import espnow
import machine
import time
# 1. Initialize Wi-Fi in Station Mode
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
e = espnow.ESPNow()
e.active(True)
# 2. Configure Pin 10 as Output for the built-in blue LED
led = machine.Pin(10, machine.Pin.OUT)
led.value(1) # Ensure LED is off initially
print("Connection Node 2 listening for MPU status triggers...")
while True:
host, msg = e.recv()
if msg:
print(f"Received message: {msg} from address: {host}")
if msg == b'BLINK_LED':
print("Executing blink sequence!")
for _ in range(4):
led.value(0) # LED ON
time.sleep(0.1)
led.value(1) # LED OFF
time.sleep(0.1)
BLINK_LED packet; Node 2 receives the packet and blinks an LED. All in MicroPython on Thonny.THRESHOLD = 3000 value is a guess from one calibration session — it triggered some false positives during normal hand movement and missed a few intentional shakes. Needs proper characterisation per Microcontroller.