Week 11 — Networking and Communications
Assignment
Individual: Design, build, and connect wired or wireless node(s) with network or bus addresses and local input and/or output device(s).
Group: Send a message between two projects.
Here is my plan for the week:

Group Assignment
For this weeks group assignment you can click here: Group Assignment
What I Built
This week I built a wireless weather station using two XIAO ESP32-C3 boards — one reads temperature and humidity from a DHT11 sensor and sends the data wirelessly, the other receives it and displays it live on a 0.96" OLED screen. I used two protocols: ESP-NOW for the wireless communication between the boards, and I2C for the OLED display on the receiver side.

Understanding the Protocols
Before jumping in, I want to quickly explain the protocols I used because the assignment specifically asks for network or bus addresses — and I want to be clear about what those are in my project.
UART is the simplest serial protocol — just two wires, TX and RX, no clock. Both devices agree on a baud rate beforehand and that's how they stay in sync. I've been using this every time I open the Serial Monitor in Arduino — that's UART between my ESP32 and my laptop.
I2C uses two wires — SDA (data) and SCL (clock). It's synchronous, meaning one device drives a clock signal and everyone follows it. What makes I2C special is addressing — every device on the bus has a unique 7-bit address. My OLED's I2C address is 0x3C, which is how the ESP32 knows which device to talk to even if multiple things share the same two wires.
SPI is faster than I2C, uses four wires (MOSI, MISO, SCK, CS), and selects devices using a chip select pin. I used this with my TFT display in Week 10.
ESP-NOW is a wireless protocol by Espressif that lets ESP32 boards talk directly to each other without needing a WiFi router. Each ESP32 has a unique MAC address — a 6-byte hardware ID burned into the chip at the factory. The sender stores the receiver's MAC address and sends data packets directly to it. No internet, no router, just board to board. The MAC address is the "network address" this week's assignment is asking for.
The Plan

Both protocol requirements are covered — ESP-NOW handles the wireless network layer between boards, and I2C handles the wired connection between the receiver and the OLED.
Wiring
Sender board — DHT11 to XIAO ESP32-C3
| DHT11 Pin | XIAO ESP32-C3 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| DATA | D2 (GPIO4) |

Receiver board — OLED to XIAO ESP32-C3
| OLED Pin | XIAO ESP32-C3 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SDA | D4 (GPIO6) |
| SCL | D5 (GPIO7) |

Getting the Receiver's MAC Address
The first thing I had to do was get the MAC address of the receiver board. ESP-NOW needs it hardcoded in the sender so it knows exactly where to send the data. I uploaded a simple sketch to the receiver and read the address from Serial Monitor.
I generated this code using Claude AI with the following prompt:
"I am doing Fab Academy Week 11 networking and communications. I have two XIAO ESP32-C3 boards. I need a simple sketch to get the MAC address of my receiver board so I can use it for ESP-NOW. Factcheck before you answer."


The receiver's MAC address came out as D4:F9:8D:04:04:40. I plugged this into the sender code as:
Testing the Sensor First
Before adding any wireless stuff I tested the DHT11 alone on the sender to make sure it was reading correctly. Good habit — test each part in isolation before combining everything.

Sensor was working fine. Room was around 19-20°C which matched what I expected.
Sender Code
The sender reads DHT11 every 3 seconds and packs the temperature and humidity into a struct, then fires it off via ESP-NOW to the receiver's MAC address. I use 3 seconds instead of 2 because the DHT11 has a hardware limitation — it can only take a fresh reading every 2 seconds minimum, and right on that edge it sometimes fails. 3 seconds gives it enough breathing room.
I also ran into a compile error with the ESP-NOW send callback. In ESP32 Arduino core 3.x, the callback signature changed — it now takes wifi_tx_info_t* instead of uint8_t*. So the onSent function had to be updated to match.
I generated this code using Claude AI with the following prompt:
"Write an Arduino sender sketch for XIAO ESP32-C3 using ESP-NOW. It should read temperature and humidity from a DHT11 connected to D2 (GPIO4) and send the data wirelessly to a receiver board with MAC address D4:F9:8D:04:04:40. Factcheck the code before you answer me."
When I tried to compile I got a callback signature error. I debugged it with Claude using the prompt:
"I am getting this error: invalid conversion from void ()(const uint8_t, esp_now_send_status_t) to esp_now_send_cb_t. I am using ESP32 Arduino core 3.x on a XIAO ESP32-C3. Fix it and explain why."
#include <esp_now.h>
#include <WiFi.h>
#include "DHT.h"
#define DHTPIN 4 // D2 on XIAO ESP32-C3
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
// Receiver MAC address
uint8_t receiverMAC[] = {0xD4, 0xF9, 0x8D, 0x04, 0x04, 0x40};
// Data structure to send
typedef struct {
float temperature;
float humidity;
} SensorData;
SensorData data;
// Callback - fires after send attempt
// Note: core 3.x changed the signature to wifi_tx_info_t*
void onSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) {
if (status == ESP_NOW_SEND_SUCCESS) {
Serial.println("Send status: SUCCESS");
} else {
Serial.println("Send status: FAILED — retrying...");
esp_now_send(receiverMAC, (uint8_t *)&data, sizeof(data));
}
}
void setup() {
Serial.begin(115200);
dht.begin();
delay(2000); // Let DHT11 stabilize on boot
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed");
return;
}
esp_now_register_send_cb(onSent);
esp_now_peer_info_t peerInfo = {};
memcpy(peerInfo.peer_addr, receiverMAC, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Failed to add peer");
return;
}
Serial.println("Sender ready");
}
void loop() {
float temp = dht.readTemperature();
float hum = dht.readHumidity();
// Validate reading before sending
if (isnan(temp) || isnan(hum)) {
Serial.println("DHT11 read failed — skipping");
delay(3000);
return;
}
data.temperature = temp;
data.humidity = hum;
Serial.print("Sending — Temp: ");
Serial.print(data.temperature);
Serial.print("°C Humidity: ");
Serial.print(data.humidity);
Serial.println("%");
esp_now_send(receiverMAC, (uint8_t *)&data, sizeof(data));
delay(3000); // 3s gives DHT11 enough time for a fresh reading
}


Receiver Code
The receiver sits and waits. When ESP-NOW data arrives, a callback function fires, unpacks the struct, and immediately updates the OLED via I2C. The display shows "Waiting for data.." on boot and switches to live readings as soon as the first packet arrives.
I generated this code using Claude AI with the following prompt:
"Write an Arduino receiver sketch for XIAO ESP32-C3 using ESP-NOW. When data arrives it should display the temperature and humidity on a 0.96 inch OLED connected via I2C. The OLED I2C address is 0x3C. Factcheck the code before you answer me."
#include <esp_now.h>
#include <WiFi.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C // I2C address of the OLED
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Must match sender struct exactly
typedef struct {
float temperature;
float humidity;
} SensorData;
SensorData incomingData;
// Callback - fires when data arrives via ESP-NOW
void onReceive(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) {
memcpy(&incomingData, data, sizeof(incomingData));
Serial.print("Temp: ");
Serial.print(incomingData.temperature);
Serial.print("°C Humidity: ");
Serial.print(incomingData.humidity);
Serial.println("%");
// Update OLED display via I2C
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("-- Weather Station --");
display.setTextSize(2);
display.setCursor(0, 20);
display.print("T: ");
display.print(incomingData.temperature, 1);
display.println(" C");
display.setCursor(0, 44);
display.print("H: ");
display.print(incomingData.humidity, 1);
display.println(" %");
display.display();
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
// Init OLED over I2C
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println("OLED init failed");
while (true);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 28);
display.println(" Waiting for data..");
display.display();
// Init ESP-NOW
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed");
return;
}
esp_now_register_recv_cb(onReceive);
Serial.println("Receiver ready");
}
void loop() {}


Results
Both boards running, data flying wirelessly, OLED updating live every 3 seconds. The OLED talks to the ESP32 over I2C at address 0x3C, and the two boards communicate via ESP-NOW using the receiver's MAC address D4:F9:8D:04:04:40 as the network address.


What I Learned
I finally actually understand what a bus address is — it's not just some abstract thing in code, it's literally how devices identify themselves on a shared line. The OLED's 0x3C and the receiver's MAC address D4:F9:8D:04:04:40 are both real examples of that. The ESP32 core 3.x callback issue was annoying but I'm glad I ran into it as I learned some new lessons.