◄ PAGE 10 PAGE 12 ►
WEEK #11

NETWORKING & COMMUNICATIONS

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.

THE CODE LOGIC

// 1. SENDER (BLE SERVER)
#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

#define SHT31_ADDR 0x44

// BLE UUIDs
#define SERVICE_UUID        "12345678-1234-1234-1234-1234567890ab"
#define CHARACTERISTIC_UUID "abcdefab-1234-1234-1234-abcdef123456"

BLECharacteristic *pCharacteristic;
BLEServer *pServer;
bool deviceConnected = false;

float temperature = 0.0;
float humidity = 0.0;
unsigned long lastUpdate = 0;

class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
    Serial.println("BLE Client connected");
  }

  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    Serial.println("BLE Client disconnected");
    BLEDevice::startAdvertising();
    Serial.println("Restarting advertising...");
  }
};

void readSHT3X() {
  Wire.beginTransmission(SHT31_ADDR);
  Wire.write(0x24);
  Wire.write(0x00);

  if (Wire.endTransmission() != 0) {
    Serial.println("Error sending command to SHT3X");
    return;
  }

  delay(20);

  Wire.requestFrom(SHT31_ADDR, 6);

  if (Wire.available() == 6) {
    uint8_t data[6];

    for (int i = 0; i < 6; i++) {
      data[i] = Wire.read();
    }

    uint16_t rawTemp = (data[0] << 8) | data[1];
    uint16_t rawHum  = (data[3] << 8) | data[4];

    temperature = -45.0 + (175.0 * rawTemp / 65535.0);
    humidity    = 100.0 * rawHum / 65535.0;
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Wire.begin();

  BLEDevice::init("SHT3X_BLE_SERVER");
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  BLEService *pService = pServer->createService(SERVICE_UUID);

  pCharacteristic = pService->createCharacteristic(
    CHARACTERISTIC_UUID,
    BLECharacteristic::PROPERTY_READ |
    BLECharacteristic::PROPERTY_NOTIFY
  );

  pCharacteristic->addDescriptor(new BLE2902());
  pCharacteristic->setValue("0.0,0.0");

  pService->start();

  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->start();
}

void loop() {
  if (millis() - lastUpdate > 2000) {
    lastUpdate = millis();

    readSHT3X();

    // Package the floats into a String payload separated by a comma
    String payload = String(temperature, 1) + "," + String(humidity, 1);
    pCharacteristic->setValue(payload.c_str());

    if (deviceConnected) {
      pCharacteristic->notify(); // Push the data to the client
    }
  }
}
// 2. RECEIVER (BLE CLIENT)
#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.

THE CODE LOGIC

// MASTER ROUTING DATA
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>

#define i2c_Address 0x3C // Slave 1: OLED Display
#define SHT31_ADDR 0x44  // Slave 2: Temp/Hum Sensor

Adafruit_SH1106G display(128, 64, &Wire, -1);

void setup() {
  Serial.begin(115200);
  Wire.begin(); // Join I2C Bus as Master
  display.begin(i2c_Address, true);
}

void loop() {
  // 1. TALK TO SLAVE 2 (SENSOR)
  Wire.beginTransmission(SHT31_ADDR);
  Wire.write(0x24); // Request measurement command
  Wire.write(0x00);
  Wire.endTransmission();
  delay(20);

  Wire.requestFrom(SHT31_ADDR, 6);

  if (Wire.available() == 6) {
    uint8_t data[6];
    for (int i = 0; i < 6; i++) {
      data[i] = Wire.read();
    }

    uint16_t rawTemp = (data[0] << 8) | data[1];
    uint16_t rawHum  = (data[3] << 8) | data[4];

    float temperature = -45.0 + (175.0 * rawTemp / 65535.0);
    float humidity    = 100.0 * rawHum / 65535.0;

    // 2. TALK TO SLAVE 1 (OLED)
    display.clearDisplay();
    display.setCursor(10, 25);
    display.print(temperature, 1);
    display.setCursor(10, 45);
    display.print(humidity, 1);
    display.display(); // Push drawing buffer to OLED
  }
  delay(2000);
}

05. MQTT CLOUD PROTOCOL

PUBLISH & SUBSCRIBE

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());
  }
}

MISSION ACCOMPLISHED

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.

← BACK TO HQ (INDEX)