Group Assignment: Making microcontrollers talk to each other!
MISSION BRIEFING
For the networking and communications week, our group set out to verify 5 different standard communication protocols. We created a modular test rig where multiple microcontrollers could communicate using different methods. Our objective was to measure the reliability, power usage, and speed of: BLE, ESP-NOW, HTTP, I2C, and MQTT All communications were proved with two ESP32-c6 boards.
Team Members: Javier Vega and Rodrigo Zárate
01. BLUETOOTH LOW ENERGY (BLE)
SERVER/CLIENT ARCHITECTURE
BLE is excellent for low-power, short-range communication. In our test, Board A acts as a BLE Server. It reads the SHT3X sensor every 2 seconds, converts the Temperature and Humidity into a comma-separated String (e.g., "24.5,60.2"), and updates a specific BLE Characteristic.
Board B acts as the BLE Client. It actively scans the area looking for our specific SERVICE_UUID. Once found, it connects, subscribes to notifications, and whenever the server updates the string, the client splits it at the comma and displays the separate values on the OLED screen.
Hero Test Video: BLE Client receiving sensor data from the BLE Server.
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEClient.h>
#include <BLERemoteCharacteristic.h>
#include <BLEScan.h>
#define i2c_Address 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SERVICE_UUID "12345678-1234-1234-1234-1234567890ab"
#define CHARACTERISTIC_UUID "abcdefab-1234-1234-1234-abcdef123456"
Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
BLEAdvertisedDevice* myDevice = nullptr;
BLERemoteCharacteristic* pRemoteCharacteristic = nullptr;
BLEClient* pClient = nullptr;
bool doConnect = false;
bool connected = false;
bool doScan = false;
float temperature = 0.0;
float humidity = 0.0;
// Callback function when Server pushes new data
static void notifyCallback(
BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData,
size_t length,
bool isNotify
) {
String received = "";
for (size_t i = 0; i < length; i++) {
received += (char)pData[i];
}
// Parse the String using the comma as a separator
int commaIndex = received.indexOf(',');
if (commaIndex > 0) {
temperature = received.substring(0, commaIndex).toFloat();
humidity = received.substring(commaIndex + 1).toFloat();
// Update the OLED screen
display.clearDisplay();
display.setCursor(10, 22);
display.print(temperature, 1);
display.setCursor(10, 45);
display.print(humidity, 1);
display.display();
}
}
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
if (advertisedDevice.haveServiceUUID() &&
advertisedDevice.isAdvertisingService(BLEUUID(SERVICE_UUID))) {
myDevice = new BLEAdvertisedDevice(advertisedDevice);
doConnect = true;
BLEDevice::getScan()->stop();
}
}
};
bool connectToServer() {
pClient = BLEDevice::createClient();
pClient->connect(myDevice);
BLERemoteService* pRemoteService = pClient->getService(BLEUUID(SERVICE_UUID));
pRemoteCharacteristic = pRemoteService->getCharacteristic(BLEUUID(CHARACTERISTIC_UUID));
if (pRemoteCharacteristic->canNotify()) {
pRemoteCharacteristic->registerForNotify(notifyCallback);
}
connected = true;
return true;
}
void setup() {
Serial.begin(115200);
Wire.begin();
display.begin(i2c_Address, true);
BLEDevice::init("");
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
pBLEScan->start(5, false);
}
void loop() {
if (doConnect) {
connectToServer();
doConnect = false;
}
}
02. ESP-NOW
POINT-TO-POINT MESH
ESP-NOW is a fast, connectionless protocol created by Espressif. It bypasses the need for a WiFi router entirely! Board A acts as the Transmitter, packaging our float variables into a clean data structure (struct_message) and shooting them directly to Board B using its hardcoded MAC address.
Board B (the Receiver) simply registers a callback function (OnDataRecv). Whenever a packet arrives through the airwaves, it copies the bytes back into its own struct and updates the OLED display instantly.
Hero Test Video: Instant response using peer-to-peer ESP-NOW.
THE CODE LOGIC
// 1. GET MAC ADDRESS (RUN ON RECEIVER FIRST)
#include <WiFi.h>
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
Serial.println(WiFi.macAddress()); // We copy this MAC into the Sender code
}
void loop() {
}
// 2. TRANSMITTER (SENDER)
#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <esp_now.h>
#define SHT31_ADDR 0x44
// The MAC address of the Receiver board
uint8_t receiverAddress[] = {0x58, 0xE6, 0xC5, 0x10, 0x4D, 0xC0};
// The Structure to pack the data efficiently
typedef struct struct_message {
float temperature;
float humidity;
} struct_message;
struct_message sensorData;
esp_now_peer_info_t peerInfo;
void setup() {
Serial.begin(115200);
Wire.begin();
// ESP-NOW requires the board to be in Station Mode
WiFi.mode(WIFI_STA);
WiFi.disconnect();
esp_now_init();
// Register the peer
memcpy(peerInfo.peer_addr, receiverAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
esp_now_add_peer(&peerInfo);
}
void loop() {
// Read Sensor (Math omitted for brevity, see previous examples)
// ...
sensorData.temperature = 25.5; // Example populated data
sensorData.humidity = 60.0; // Example populated data
// Send the struct package over the air
esp_now_send(receiverAddress, (uint8_t *) &sensorData, sizeof(sensorData));
delay(2000);
}
// 3. RECEIVER
#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <esp_now.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDR 0x3C
Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Must match the Sender's structure exactly!
typedef struct struct_message {
float temperature;
float humidity;
} struct_message;
struct_message receivedData;
// Callback function executed when data is received
void OnDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {
// Copy the raw bytes into our struct
memcpy(&receivedData, incomingData, sizeof(receivedData));
// Update OLED Display
display.clearDisplay();
display.setCursor(10, 22);
display.print(receivedData.temperature, 1);
display.setCursor(10, 45);
display.print(receivedData.humidity, 1);
display.display();
}
void setup() {
Serial.begin(115200);
Wire.begin();
display.begin(OLED_ADDR, true);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
esp_now_init();
// Link the callback function
esp_now_register_recv_cb(OnDataRecv);
}
void loop() {
// Nothing to do here! The callback handles everything asynchronously.
}
03. HTTP SERVER/CLIENT
LOCAL WEB SERVER
We set up a microcontroller as a local HTTP Web Server connected to the WiFi network. It acts much like a standard web server on the internet, but contained within our local router.
When a client (like our laptop browser) visits the ESP32's IP Address, the handleRoot() function triggers. It takes fresh readings from the sensor and injects them into a Raw String containing HTML and CSS. The crucial trick here is the <meta http-equiv="refresh" content="2"> tag in the HTML head, which forces the client's browser to reload the page automatically every 2 seconds to fetch the latest sensor updates.
Hero Test Video: Auto-refreshing the HTTP page to get new data.
THE CODE LOGIC
// HTTP WEBSERVER SETUP
#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#define SHT31_ADDR 0x44
const char* ssid = "RoyZ";
const char* password = "Zarate28";
WebServer server(80); // Standard HTTP port
float temperature = 0.0;
float humidity = 0.0;
// Read Sensor function (Omitted math for brevity)
void readSHT3X() { /* Reads temp and hum from I2C */ }
void handleRoot() {
readSHT3X(); // Update data variables
// The Raw Literal string allows us to write HTML with quotes without breaking C++
String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="2"> <!-- The Auto-Refresh Magic -->
<title>SHT3X Monitor</title>
<style>
body { background-color: #111; color: white; text-align: center; }
.card { background: #222; padding: 30px; border-radius: 20px; }
</style>
</head>
<body>
<div class="card">
<h1>SHT3X Sensor</h1>
<p>🌡 Temperature: )rawliteral";
html += String(temperature, 1); // Inject dynamic variable
html += R"rawliteral( °C</p>
<p>💧 Humidity: )rawliteral";
html += String(humidity, 1); // Inject dynamic variable
html += R"rawliteral( %</p>
</div>
</body>
</html>
)rawliteral";
// Send HTML payload to the browser
server.send(200, "text/html", html);
}
void setup() {
Serial.begin(115200);
Wire.begin();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
Serial.println(WiFi.localIP()); // Print IP so we can access it on PC
server.on("/", handleRoot); // Route the root path to our HTML function
server.begin();
}
void loop() {
server.handleClient(); // Keep server listening for incoming requests
}
04. I2C COMMUNICATION
MASTER & MULTIPLE SLAVES
We validated the classic I2C wired protocol. The beauty of I2C is that it acts as a data bus where the Master (ESP32) dictates the flow of traffic to multiple Slaves using just the SDA (Data) and SCL (Clock) lines.
In our code, the ESP32 orchestrates everything. First, it uses Wire.beginTransmission(0x44) to talk to the SHT3X sensor, requesting 6 bytes of environmental data. Once the math is done, it immediately pivots to address 0x3C to send formatting commands and text strings to the OLED screen. This proved that we could run multiple devices seamlessly over the same two physical pins.
Hero Test Video: Master routing data from the Sensor Slave to the OLED Slave.
Our most complex integration used MQTT to push sensor data to a cloud broker (test.mosquitto.org). The architecture here relies on "Topics". The board publishes a formatted JSON payload (containing Temperature, Humidity, and WiFi Signal Strength) to the topic fabacademy/xiao/sht3x every 5 seconds. Any dashboard subscribed to that topic will receive the updates globally.
Simultaneously, the board subscribes to the topic fabacademy/xiao/command. If another client pushes the string "on" or "off" to that topic, our board intercepts it via the mqttCallback() function and turns its physical LED on or off!
Hero Test Video: Visualizing data changes on the cloud dashboard via MQTT.
THE CODE LOGIC
// PUBLISH & SUBSCRIBE TO BROKER
#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "RoyZ";
const char* WIFI_PASSWORD = "Zarate28";
const char* MQTT_BROKER = "test.mosquitto.org";
const int MQTT_PORT = 1883;
String clientId = "xiao_" + String((uint32_t)ESP.getEfuseMac(), HEX);
// Topics
const char* MQTT_PUBLISH_TOPIC = "fabacademy/xiao/sht3x";
const char* MQTT_SUBSCRIBE_TOPIC = "fabacademy/xiao/command";
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
unsigned long lastPublishTime = 0;
float temperature = 0.0, humidity = 0.0;
const int LED_PIN = LED_BUILTIN;
// Callback: Triggers when a message arrives on our Subscribed Topic
void mqttCallback(char* topic, byte* payload, unsigned int length) {
String message = "";
for (unsigned int i = 0; i < length; i++) {
message += (char)payload[i];
}
// Act on the remote command
if (message == "on") {
digitalWrite(LED_PIN, HIGH);
} else if (message == "off") {
digitalWrite(LED_PIN, LOW);
}
}
void connectToMQTT() {
while (!mqttClient.connected()) {
if (mqttClient.connect(clientId.c_str())) {
// Upon connection, subscribe to listen for commands
mqttClient.subscribe(MQTT_SUBSCRIBE_TOPIC);
} else {
delay(2000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
Wire.begin();
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(500); }
mqttClient.setServer(MQTT_BROKER, MQTT_PORT);
mqttClient.setCallback(mqttCallback);
}
void loop() {
if (!mqttClient.connected()) {
connectToMQTT();
}
mqttClient.loop(); // Keeps the MQTT connection alive
// Publish Data every 5 seconds
if (millis() - lastPublishTime > 5000) {
lastPublishTime = millis();
// Construct a JSON payload
String payload = "{";
payload += "\"temperature\":" + String(25.5) + ","; // Example Temp
payload += "\"humidity\":" + String(60.0) + ","; // Example Hum
payload += "\"wifi_rssi\":" + String(WiFi.RSSI());
payload += "}";
// Push the payload to the Cloud Broker
mqttClient.publish(MQTT_PUBLISH_TOPIC, payload.c_str());
}
}
Week 11 Group Mission is complete. We successfully tested and documented the 5 networking protocols, verifying they all work perfectly for our upcoming individual projects.