Week 11: Networking and Communications¶

*Two PCBs connected via ESP-NOW
Group Assignment¶
During networking week, Mariam and I learned to control hardware over a network by connecting two ESP32 C3s via the MQTT protocol and a public HiveMQ broker. We started by establishing basic data transmission between a designated publisher board and a subscriber board, such as sending a simple “hello” message. This initial experiment taught us how to configure the PubSubClient library, manage network subscriptions, and read data payloads through callback functions.
Next, we introduced Mariam’s joystick button to trigger these network messages. Mariam updated her publisher code to send data only when the pin state changed, so when she would press the button I would receive “Pressed!” message. And once this was achieve, the only thing left to do was to put the commands into actions.
Finally, we applied these concepts to remotely deploy my drone rescuing parachute. The receiver board would rotate the connected servo when the joystick message arrived. This final project successfully demonstrated an end-to-end IoT orchestration by bridging a physical sensor input on one network node to a mechanical actuator on another.
Individual Assignment¶
This week’s individual work is based on the results of weeks 8 and 9. In these consecutive two weeks I had designed and produced two iterations of a PCB – the first iteration had male-to-female pin headers for removablility [week 8], whereas the second iteration had surface-mount components[week 9].
Initially, in week 8, I had a XIAO RP2040 in the place of the MCU, but as the footprints are the same, I easily swapped it with a XIAO ESP32 C3 [image below], as these have a built-in WiFi antenna. This helped me skip the production of a new board, as the RP2040 simply would not allow me to connect to another WiFi compatible MCU.

What was the assignments about?
I took a pair of XIAO ESP32 C3s [image below], and connected them via ESP-NOW. The first board [week 8 – receiver] would receive data to the buzzer from the accelerometer from the second board [week 9 – transmitter]. The pitch output by the buzzer would changed based on the pitch and tilt of the accelerometer. In order to reduce the noise logged but the accelerometer, I simplifed the data collection to only a single axis, which then would allow easier processing.

The Code¶
Here’s the prompt I had used to generate the code:
I have two XIAO ESP32 C3 boards — one with a GY-521 [MPU6050], the other with a passive buzzer. Connect them over ESP-NOW so the sensor board’s tilt controls the buzzer’s pitch. Simplify the mapping to a single tilt axis rather than juggling all three — use whichever gives the cleanest, most intuitive control, with tilt in one direction raising the pitch and the other lowering it. Add smoothing/filtering so sensor noise doesn’t make the buzzer warble. See the wiring below:
MPU6050: SDA → D1 SCL → D0 VCC → 3.3V GND → GND Buzzer: (+) → D2
The code for the Transmitter:
C++
/*
* SENDER — XIAO ESP32-C3 + GY-521 (MPU6050)
* Reads board TILT (from the accelerometer, not the gyro), reduces it to ONE
* tilt angle, maps that to a musical pitch, and broadcasts the frequency over
* ESP-NOW to the buzzer board.
*
* Wiring (XIAO ESP32-C3):
* MPU6050 SDA -> D1 (GPIO3)
* MPU6050 SCL -> D0 (GPIO2)
* MPU6050 VCC -> 3V3
* MPU6050 GND -> GND
*
* Built for ESP32 Arduino core 3.x (current).
*/
#include <esp_now.h>
#include <WiFi.h>
#include <esp_wifi.h>
#include <Wire.h>
#include <math.h>
// ---------- Pins ----------
static const int PIN_SDA = 3; // D1
static const int PIN_SCL = 2; // D0
// ---------- MPU6050 ----------
static const uint8_t MPU_ADDR = 0x68;
static const uint8_t REG_PWR_MGMT_1 = 0x6B;
static const uint8_t REG_WHO_AM_I = 0x75;
static const uint8_t REG_ACCEL_XOUT_H= 0x3B;
// ---------- ESP-NOW ----------
uint8_t broadcastAddress[] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
static const uint8_t WIFI_CHANNEL = 1; // both boards must match
typedef struct __attribute__((packed)) {
uint16_t freq; // Hz (0 = silent)
} message_t;
message_t msg;
esp_now_peer_info_t peerInfo;
// ---------- Tilt -> pitch mapping ----------
static const float ANGLE_MIN = -75.0f; // deg (tilt one way)
static const float ANGLE_MAX = 75.0f; // deg (tilt the other way)
static const float FREQ_MIN = 150.0f; // Hz at full tilt one way
static const float FREQ_MAX = 1500.0f; // Hz at full tilt the other way
// ---------- Noise control ----------
static const float ALPHA = 0.15f; // low-pass: lower = smoother/slower
static const float HYST = 0.6f; // semitones of slack before pitch changes
float angleFilt = 0.0f; // filtered tilt angle
float curNote = 0.0f; // current quantized semitone (rel. A4)
void readAccel(int16_t &ax, int16_t &ay, int16_t &az) {
Wire.beginTransmission(MPU_ADDR);
Wire.write(REG_ACCEL_XOUT_H);
Wire.endTransmission(false);
Wire.requestFrom((int)MPU_ADDR, 6, (int)true);
ax = (int16_t)((Wire.read() << 8) | Wire.read());
ay = (int16_t)((Wire.read() << 8) | Wire.read());
az = (int16_t)((Wire.read() << 8) | Wire.read());
}
void setup() {
Serial.begin(115200);
delay(300);
// --- I2C + wake MPU6050 ---
Wire.begin(PIN_SDA, PIN_SCL);
Wire.setClock(400000);
Wire.beginTransmission(MPU_ADDR);
Wire.write(REG_PWR_MGMT_1);
Wire.write(0x00); // clear sleep bit
Wire.endTransmission(true);
delay(100);
// quick presence check
Wire.beginTransmission(MPU_ADDR);
Wire.write(REG_WHO_AM_I);
Wire.endTransmission(false);
Wire.requestFrom((int)MPU_ADDR, 1, (int)true);
uint8_t who = Wire.available() ? Wire.read() : 0xFF;
if (who != 0x68) Serial.printf("WARNING: MPU6050 not found (WHO_AM_I=0x%02X)\n", who);
// --- WiFi / ESP-NOW ---
WiFi.mode(WIFI_STA);
WiFi.disconnect();
esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed; restarting");
ESP.restart();
}
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = WIFI_CHANNEL;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK)
Serial.println("Failed to add broadcast peer");
// seed filter with first real reading so it doesn't ramp from 0
int16_t ax, ay, az;
readAccel(ax, ay, az);
angleFilt = atan2f((float)ax, sqrtf((float)ay*ay + (float)az*az)) * 180.0f / PI;
curNote = 12.0f * log2f((FREQ_MIN + FREQ_MAX) * 0.5f / 440.0f);
Serial.println("Sender ready");
}
void loop() {
int16_t ax, ay, az;
readAccel(ax, ay, az);
// ONE tilt axis: angle of gravity vector (flat = 0, +/- = tilt each way)
float angle = atan2f((float)ax, sqrtf((float)ay*ay + (float)az*az)) * 180.0f / PI;
// low-pass filter (kills accelerometer jitter)
angleFilt = ALPHA * angle + (1.0f - ALPHA) * angleFilt;
// clamp + linear map to a continuous frequency
float a = angleFilt;
if (a < ANGLE_MIN) a = ANGLE_MIN;
if (a > ANGLE_MAX) a = ANGLE_MAX;
float freq = FREQ_MIN + (a - ANGLE_MIN) * (FREQ_MAX - FREQ_MIN) / (ANGLE_MAX - ANGLE_MIN);
// snap to nearest musical semitone, with hysteresis so it won't flutter
float semis = 12.0f * log2f(freq / 440.0f); // continuous semitone index
if (fabsf(semis - curNote) > HYST) curNote = roundf(semis);
float snapped = 440.0f * powf(2.0f, curNote / 12.0f);
msg.freq = (uint16_t)(snapped + 0.5f);
esp_now_send(broadcastAddress, (uint8_t*)&msg, sizeof(msg));
Serial.printf("angle=%6.1f freq=%u Hz\n", angleFilt, msg.freq);
delay(30); // ~33 updates/sec
}
The code for the Receiver:
C++
/*
* RECEIVER — XIAO ESP32-C3 + passive buzzer
* Receives a frequency over ESP-NOW and plays it on the buzzer. If packets
* stop arriving it silences the buzzer (watchdog) so it never gets stuck.
*
* Wiring (XIAO ESP32-C3):
* Buzzer + -> D2 (GPIO2)
* Buzzer - -> GND
*
* IMPORTANT: pitch control needs a PASSIVE (piezo) buzzer or small speaker.
* An ACTIVE buzzer has its own oscillator and only does on/off at a fixed
* pitch — tone() cannot change its frequency.
*
* Built for ESP32 Arduino core 3.x (current).
*/
#include <esp_now.h>
#include <WiFi.h>
#include <esp_wifi.h>
static const int PIN_BUZZER = 2; // D2
static const uint8_t WIFI_CHANNEL = 1; // must match the sender
static const uint32_t TIMEOUT_MS = 500; // silence if no packet within this
typedef struct __attribute__((packed)) {
uint16_t freq; // Hz (0 = silent)
} message_t;
volatile uint16_t targetFreq = 0;
volatile uint32_t lastRxMs = 0;
/* ---- ESP-NOW receive callback (core 3.x signature) ----
* If you are on the OLDER core 2.x, replace the line below with:
* void onRecv(const uint8_t *mac, const uint8_t *data, int len) {
*/
void onRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
if (len < (int)sizeof(message_t)) return;
message_t m;
memcpy(&m, data, sizeof(m));
targetFreq = m.freq;
lastRxMs = millis();
}
void setup() {
Serial.begin(115200);
delay(300);
pinMode(PIN_BUZZER, OUTPUT);
noTone(PIN_BUZZER);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);
if (esp_now_init() != ESP_OK) {
Serial.println("ESP-NOW init failed; restarting");
ESP.restart();
}
esp_now_register_recv_cb(onRecv);
Serial.println("Receiver ready");
}
uint16_t curFreq = 0;
void loop() {
uint16_t f = targetFreq;
uint32_t last = lastRxMs;
// watchdog: go quiet if the sender has gone silent
if (millis() - last > TIMEOUT_MS) f = 0;
if (f != curFreq) {
curFreq = f;
if (f == 0) noTone(PIN_BUZZER);
else tone(PIN_BUZZER, f);
}
delay(5);
}
Result¶
In the video below you can both hear and see how the output from the buzzer changes as the second board chnages its position.
Conclusion¶
I must say, I did not think networking week would turn out so exciting. Surely, it is not magic. But working with MQTT went quite smoothly, whereas the practices of a subscriber and publisher were quite intuitive. I also liked working with ESP NOW, it is quite simple!
References¶
• Transmitter
• Receiver