11. Networking and Communications
This week I repurposed the PCBs I made for the Input Devices and Output Devices weeks to allow my microcontrollers to communicate accross a wifi network. I used 1 Seeed Xiao ESP32C3 microcontroller as the input to send instructions to a webapp which then send a message to a second ESP32C3. The message is then displayed on an 8x8 LED matrix.
Generating the Code
I used Claude.ai to help me in every aspect of the coding for this project. I started by explaining the components I would be using, the existing traces and connections on my PCBs, and the functions I wanted each board and code to perform.
First Prompt
I have 2 xiao esp32c3 microcontrollers. the first is a switch connected to D0 and another switch connected to D1. The other esp32c3 has an 8x8 LED matrix connected to D0. for the first esp32c3, assign the hostname 'esp32c3_1' and the hostname 'esp32c3_2' for the second controller. write an html webapp for the devices to connect to and represent the desired functions. follow the following instructions: When switch D0 on esp32c3_1 is pressed, the LED matrix on esp32c3_2 displays "HI". When switch D1 on esp32c3_1 is pressed, the LED matrix on esp32c3_2 displays "BYE". in the webapp, allow speed, color and text to be customizable. Do not hardcode any messages or animations on esp32c3_2. It should be entirely dependent on input from the esp32c3_1 and the webapp
The Architecture Claude produced
[esp32c3_1] ──────────────────────────────────┐
D0 switch → "switch_press" {switch:0} │ WebSocket
D1 switch → "switch_press" {switch:1} ▼
[server.js] ←→ [Browser / index.html]
│ ↑ live preview
"display" cmd ↑ send text, color, speed
▼ ↑ configure switch mappings
[esp32c3_2] ──────────────────────────────────┘
receives → scrolls/shows text on LED matrix
The plan for this assignment meant that I would need to create a WebSocket and Hostnames for each ESP32C3 microcontroller. This allows them to connect to the webapp without needing to know the IP address. This is useful because the IP address will change depending on the network or availability. By using hostnames, I can ensure that the webapp can always connect to the devices as long as they are connected to a wifi network.
The hostname 'esp32c3_1' is assigned to the first microcontroller, and 'esp32c3_2' is assigned to the second.
I used this second Claide session to trouble shoot issues and errors in the first session. Some of the problems I encountered included, the matrix not responding, Claude including the wrong protocals and libraries in the code which were either incompatible with or unnecessary for Seeed Xiao ESP32C3 microcontrollers.
ESP32C3_1
This is the first ESP32C3 microcontroller, which acts as the input device. It has two switches connected to D0 and D1. The Code below is from the firmware .INO file I ran in Arduino IDE
/*
* ╔══════════════════════════════════════════════════════════════╗
* ║ ESP32-C3 #1 — Switch Input Controller ║
* ║ Hostname : esp32c3_1 (reachable at esp32c3_1.local) ║
* ╠══════════════════════════════════════════════════════════════╣
* ║ Wiring ║
* ║ D0 → Switch 0 (other leg to GND, uses PULLUP) ║
* ║ D1 → Switch 1 (other leg to GND, uses PULLUP) ║
* ╠══════════════════════════════════════════════════════════════╣
* ║ Role ║
* ║ • Hosts a WebSocket SERVER on port 80 ║
* ║ – Browser connects to receive switch events and ║
* ║ push switch-map config ║
* ║ • Acts as WebSocket CLIENT to esp32c3_2:82 ║
* ║ – Forwards display commands on switch press ║
* ╠══════════════════════════════════════════════════════════════╣
* ║ Libraries (Arduino Library Manager) ║
* ║ • WebSockets by Markus Sattler (Links2004) ← BOTH ║
* ║ search: "WebSockets" — install the one by Markus ║
* ║ Sattler, currently v2.4.x ║
* ║ • ArduinoJson by Benoit Blanchon ║
* ║ NOTE: Do NOT install AsyncTCP or ESPAsyncWebServer — ║
* ║ those do not support the ESP32-C3. ║
* ╚══════════════════════════════════════════════════════════════╝
*/
#include
#include
#include // WS server (Links2004/arduinoWebSockets)
#include // WS client (same library)
#include
// ── Network ──────────────────────────────────────────────────────
const char* WIFI_SSID = "WIFI_SSID";
const char* WIFI_PASSWORD = "WIFI_PASSWORD";
const char* HOSTNAME = "esp32c3_1";
const char* MATRIX_HOST = "esp32c3_2.local";
const uint16_t MATRIX_PORT = 82;
const uint16_t SERVER_PORT = 80;
// ── Pins ─────────────────────────────────────────────────────────
const int PIN_SW0 = D0;
const int PIN_SW1 = D1;
// ── Debounce ─────────────────────────────────────────────────────
const unsigned long DEBOUNCE_MS = 50;
// ── Switch → display mapping (configurable from webapp) ──────────
struct SwitchMap { char text[64]; int speed; char color[8]; };
SwitchMap switchMap[2] = {
{ "HI", 60, "#00FF41" },
{ "BYE", 40, "#FF4500" },
};
// ── WebSocket server (browser) & client (matrix) ─────────────────
WebSocketsServer wsServer(SERVER_PORT);
WebSocketsClient matrixClient;
bool matrixConnected = false;
unsigned long lastMCon = 0;
// ── Switch state ─────────────────────────────────────────────────
bool sw0Last = HIGH, sw1Last = HIGH;
unsigned long sw0T = 0, sw1T = 0;
bool sw0Fired = false, sw1Fired = false;
// ── Helpers ──────────────────────────────────────────────────────
String buildDisplayCmd(int idx) {
StaticJsonDocument<256> doc;
doc["type"] = "display";
doc["text"] = switchMap[idx].text;
doc["speed"] = switchMap[idx].speed;
doc["color"] = switchMap[idx].color;
String s; serializeJson(doc, s); return s;
}
void broadcastToBrowser(int idx) {
StaticJsonDocument<256> doc;
doc["type"] = "switch_press";
doc["switch"] = idx;
doc["text"] = switchMap[idx].text;
doc["speed"] = switchMap[idx].speed;
doc["color"] = switchMap[idx].color;
String s; serializeJson(doc, s);
wsServer.broadcastTXT(s);
}
void sendState(uint8_t clientId) {
StaticJsonDocument<512> doc;
doc["type"] = "state";
JsonArray arr = doc.createNestedArray("switchMap");
for (int i = 0; i < 2; i++) {
JsonObject m = arr.createNestedObject();
m["index"] = i;
m["text"] = switchMap[i].text;
m["speed"] = switchMap[i].speed;
m["color"] = switchMap[i].color;
}
String s; serializeJson(doc, s);
wsServer.sendTXT(clientId, s);
}
void handleSwitchPress(int idx) {
Serial.printf("[Switch] D%d → \"%s\"\n", idx, switchMap[idx].text);
broadcastToBrowser(idx);
if (matrixConnected) {
String cmd = buildDisplayCmd(idx);
matrixClient.sendTXT(cmd);
Serial.printf("[WSC→matrix] %s\n", cmd.c_str());
} else {
Serial.println("[WSC] matrix not connected");
}
}
// ── WebSocket SERVER callback (browser) ──────────────────────────
void onWsServerEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
if (type == WStype_CONNECTED) {
Serial.printf("[WSS] browser #%u connected\n", num);
sendState(num);
return;
}
if (type == WStype_DISCONNECTED) {
Serial.printf("[WSS] browser #%u disconnected\n", num);
return;
}
if (type == WStype_TEXT) {
StaticJsonDocument<256> doc;
if (deserializeJson(doc, payload, length) != DeserializationError::Ok) return;
const char* t = doc["type"] | "";
if (strcmp(t, "set_switch_map") == 0) {
int idx = doc["switch"] | -1;
if (idx < 0 || idx > 1) return;
strlcpy(switchMap[idx].text, doc["text"] | "", 64);
switchMap[idx].speed = doc["speed"] | 50;
strlcpy(switchMap[idx].color, doc["color"] | "#00FF41", 8);
Serial.printf("[Config] sw%d → \"%s\" spd=%d col=%s\n",
idx, switchMap[idx].text, switchMap[idx].speed, switchMap[idx].color);
// Ack
StaticJsonDocument<64> ack;
ack["type"] = "map_saved";
ack["switch"] = idx;
String s; serializeJson(ack, s);
wsServer.broadcastTXT(s);
}
}
}
// ── WebSocket CLIENT callback (matrix) ───────────────────────────
void onMatrixClientEvent(WStype_t type, uint8_t* payload, size_t length) {
if (type == WStype_CONNECTED) {
matrixConnected = true;
Serial.println("[WSC] connected to esp32c3_2");
}
if (type == WStype_DISCONNECTED) {
matrixConnected = false;
Serial.println("[WSC] disconnected from esp32c3_2");
}
}
// ── WiFi + mDNS ──────────────────────────────────────────────────
void connectWiFi() {
WiFi.setHostname(HOSTNAME);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WiFi] connecting");
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print('.'); }
Serial.printf("\n[WiFi] IP: %s\n", WiFi.localIP().toString().c_str());
if (MDNS.begin(HOSTNAME))
Serial.printf("[mDNS] %s.local\n", HOSTNAME);
}
// ── Setup ─────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
delay(400);
pinMode(PIN_SW0, INPUT_PULLUP);
pinMode(PIN_SW1, INPUT_PULLUP);
connectWiFi();
// Start WS server for browser
wsServer.begin();
wsServer.onEvent(onWsServerEvent);
Serial.printf("[WSS] server on port %d\n", SERVER_PORT);
// Start WS client toward matrix
matrixClient.begin(MATRIX_HOST, MATRIX_PORT, "/ws");
matrixClient.onEvent(onMatrixClientEvent);
matrixClient.setReconnectInterval(5000);
matrixClient.enableHeartbeat(15000, 3000, 2);
}
// ── Loop ──────────────────────────────────────────────────────────
void loop() {
wsServer.loop();
matrixClient.loop();
unsigned long now = millis();
// Switch 0
bool s0 = digitalRead(PIN_SW0);
if (s0 != sw0Last) { sw0T = now; sw0Last = s0; sw0Fired = false; }
if (!sw0Fired && sw0Last == LOW && (now - sw0T) > DEBOUNCE_MS) {
sw0Fired = true; handleSwitchPress(0);
}
// Switch 1
bool s1 = digitalRead(PIN_SW1);
if (s1 != sw1Last) { sw1T = now; sw1Last = s1; sw1Fired = false; }
if (!sw1Fired && sw1Last == LOW && (now - sw1T) > DEBOUNCE_MS) {
sw1Fired = true; handleSwitchPress(1);
}
}
ESP32C3_2
This is the second ESP32C3 microcontroller, which acts as the output device. It has an 8x8 LED matrix connected to D0. The Code below is from the firmware .INO file I ran in Arduino IDE
/*
* ╔══════════════════════════════════════════════════════════════╗
* ║ ESP32-C3 #2 — LED Matrix Display Controller ║
* ║ Hostname : esp32c3_2 (reachable at esp32c3_2.local) ║
* ╠══════════════════════════════════════════════════════════════╣
* ║ Wiring (WS2812B 8×8 NeoPixel matrix) ║
* ║ D0 → DIN (single data wire) ║
* ║ 5V → VCC (use 5V rail, NOT 3V3) ║
* ║ GND → GND ║
* ╠══════════════════════════════════════════════════════════════╣
* ║ Role ║
* ║ • Hosts a WebSocket SERVER on port 82 ║
* ║ – esp32c3_1 connects here to forward switch presses ║
* ║ – Browser also connects to send direct commands ║
* ║ • All display content is driven by incoming messages. ║
* ║ Nothing is hardcoded. ║
* ╠══════════════════════════════════════════════════════════════╣
* ║ Libraries (Arduino Library Manager) ║
* ║ • WebSockets by Markus Sattler (Links2004) ║
* ║ • FastLED by Daniel Garcia ║
* ║ • ArduinoJson by Benoit Blanchon ║
* ║ NOTE: Do NOT install AsyncTCP or ESPAsyncWebServer — ║
* ║ those do not support the ESP32-C3. ║
* ╚══════════════════════════════════════════════════════════════╝
*/
#include
#include
#include
#include
#include
// ── Network ──────────────────────────────────────────────────────
const char* WIFI_SSID = "WIFI_SSID";
const char* WIFI_PASSWORD = "WIFI_PASSWORD";
const char* HOSTNAME = "esp32c3_2";
const uint16_t WS_PORT = 82;
// ── Matrix hardware ──────────────────────────────────────────────
#define DATA_PIN D0
#define MATRIX_W 8
#define MATRIX_H 8
#define NUM_LEDS (MATRIX_W * MATRIX_H)
#define BRIGHTNESS 40 // 0-255; keep low to avoid power issues over USB
CRGB leds[NUM_LEDS];
// ── Display state ────────────────────────────────────────────────
char currentText[128] = "";
CRGB currentColor = CRGB(0, 255, 65); // default: green
int scrollSpeed = 50; // 1-100
// Scroll state
int scrollX = MATRIX_W;
unsigned long lastStep = 0;
bool scrolling = false;
// ── WebSocket server ─────────────────────────────────────────────
WebSocketsServer wsServer(WS_PORT);
// ── 5×7 font (column-major, bit 0 = top row) ────────────────────
static const uint8_t FONT5x7[][5] PROGMEM = {
{0x7E,0x09,0x09,0x09,0x7E}, // A [0]
{0x7F,0x49,0x49,0x49,0x36}, // B
{0x3E,0x41,0x41,0x41,0x22}, // C
{0x7F,0x41,0x41,0x41,0x3E}, // D
{0x7F,0x49,0x49,0x49,0x41}, // E
{0x7F,0x09,0x09,0x09,0x01}, // F
{0x3E,0x41,0x41,0x51,0x72}, // G
{0x7F,0x08,0x08,0x08,0x7F}, // H
{0x41,0x7F,0x41,0x00,0x00}, // I
{0x20,0x40,0x41,0x3F,0x01}, // J
{0x7F,0x08,0x14,0x22,0x41}, // K
{0x7F,0x40,0x40,0x40,0x40}, // L
{0x7F,0x02,0x04,0x02,0x7F}, // M
{0x7F,0x04,0x08,0x10,0x7F}, // N
{0x3E,0x41,0x41,0x41,0x3E}, // O
{0x7F,0x09,0x09,0x09,0x06}, // P
{0x3E,0x41,0x51,0x21,0x5E}, // Q
{0x7F,0x09,0x19,0x29,0x46}, // R
{0x26,0x49,0x49,0x49,0x32}, // S
{0x01,0x01,0x7F,0x01,0x01}, // T
{0x3F,0x40,0x40,0x40,0x3F}, // U
{0x1F,0x20,0x40,0x20,0x1F}, // V
{0x7F,0x20,0x18,0x20,0x7F}, // W
{0x63,0x14,0x08,0x14,0x63}, // X
{0x03,0x04,0x78,0x04,0x03}, // Y
{0x61,0x51,0x49,0x45,0x43}, // Z [25]
{0x3E,0x51,0x49,0x45,0x3E}, // 0 [26]
{0x00,0x42,0x7F,0x40,0x00}, // 1
{0x42,0x61,0x51,0x49,0x46}, // 2
{0x21,0x41,0x49,0x4D,0x33}, // 3
{0x18,0x14,0x12,0x7F,0x10}, // 4
{0x27,0x45,0x45,0x45,0x39}, // 5
{0x3C,0x4A,0x49,0x49,0x30}, // 6
{0x01,0x71,0x09,0x05,0x03}, // 7
{0x36,0x49,0x49,0x49,0x36}, // 8
{0x06,0x49,0x49,0x29,0x1E}, // 9 [35]
{0x00,0x00,0x00,0x00,0x00}, // ' '[36]
{0x00,0x00,0x5F,0x00,0x00}, // ! [37]
{0x08,0x08,0x08,0x08,0x08}, // - [38]
{0x00,0x60,0x60,0x00,0x00}, // . [39]
{0x00,0x36,0x49,0x55,0x22}, // ? [40]
{0x08,0x08,0x3E,0x08,0x08}, // + [41]
};
const uint8_t* getGlyph(char c) {
if (c >= 'A' && c <= 'Z') return FONT5x7[c - 'A'];
if (c >= 'a' && c <= 'z') return FONT5x7[c - 'a'];
if (c >= '0' && c <= '9') return FONT5x7[26 + (c - '0')];
if (c == ' ') return FONT5x7[36];
if (c == '!') return FONT5x7[37];
if (c == '-') return FONT5x7[38];
if (c == '.') return FONT5x7[39];
if (c == '?') return FONT5x7[40];
if (c == '+') return FONT5x7[41];
return FONT5x7[36]; // unknown → space
}
// ── WS2812B pixel mapping (serpentine) ───────────────────────────
// Even rows left→right, odd rows right→left.
// If text appears mirrored or upside-down, adjust here.
int xyToLed(int x, int y) {
if (y < 0 || y >= MATRIX_H || x < 0 || x >= MATRIX_W) return -1;
int fy = (MATRIX_H - 1 - y);
if (fy % 2 == 0) return fy * MATRIX_W + x;
else return fy * MATRIX_W + (MATRIX_W - 1 - x);
}
// ── Rendering ────────────────────────────────────────────────────
void clearLeds() {
fill_solid(leds, NUM_LEDS, CRGB::Black);
}
int textPixelWidth() {
int len = strlen(currentText);
if (len == 0) return 0;
return len * 6 - 1; // 5px glyph + 1px gap, no trailing gap
}
void drawTextColumn(int screenX, int textCol, CRGB color) {
if (screenX < 0 || screenX >= MATRIX_W) return;
int charIdx = textCol / 6;
int colInChar = textCol % 6;
if (charIdx >= (int)strlen(currentText)) return;
if (colInChar == 5) return; // gap column
const uint8_t* glyph = getGlyph(currentText[charIdx]);
uint8_t colBits = pgm_read_byte(&glyph[colInChar]);
for (int row = 0; row < MATRIX_H; row++) {
int bit = (row < 7) ? ((colBits >> row) & 1) : 0;
int idx = xyToLed(screenX, row);
if (idx >= 0) leds[idx] = bit ? color : CRGB::Black;
}
}
void renderFrame() {
clearLeds();
int tpw = textPixelWidth();
if (tpw == 0) { FastLED.show(); return; }
if (tpw <= MATRIX_W) {
// Short text: centre statically
int startX = (MATRIX_W - tpw) / 2;
for (int tc = 0; tc < tpw; tc++)
drawTextColumn(startX + tc, tc, currentColor);
} else {
// Scrolling
for (int sc = 0; sc < MATRIX_W; sc++) {
int tc = sc - scrollX;
if (tc < 0 || tc >= tpw) continue;
drawTextColumn(sc, tc, currentColor);
}
}
FastLED.show();
}
// ── Helpers ───────────────────────────────────────────────────────
int speedToStepMs(int speed) {
return map(constrain(speed, 1, 100), 1, 100, 200, 20);
}
CRGB parseColor(const char* hex) {
if (!hex || hex[0] != '#' || strlen(hex) < 7) return CRGB(0, 255, 65);
long v = strtol(hex + 1, nullptr, 16);
return CRGB((v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF);
}
void startDisplay(const char* text, int speed, const char* colorHex) {
strlcpy(currentText, text ? text : "", sizeof(currentText));
if (speed > 0) scrollSpeed = constrain(speed, 1, 100);
if (colorHex && colorHex[0] == '#') currentColor = parseColor(colorHex);
scrollX = MATRIX_W; // start off right edge
lastStep = millis();
scrolling = (textPixelWidth() > MATRIX_W);
Serial.printf("[Display] \"%s\" speed=%d\n", currentText, scrollSpeed);
renderFrame();
}
// ── WebSocket SERVER callback ─────────────────────────────────────
void onWsEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) {
if (type == WStype_CONNECTED) {
Serial.printf("[WSS] client #%u connected\n", num);
StaticJsonDocument<256> doc;
doc["type"] = "state";
doc["text"] = currentText;
String s; serializeJson(doc, s);
wsServer.sendTXT(num, s);
return;
}
if (type == WStype_DISCONNECTED) {
Serial.printf("[WSS] client #%u disconnected\n", num);
return;
}
if (type == WStype_TEXT) {
StaticJsonDocument<256> doc;
if (deserializeJson(doc, payload, length) != DeserializationError::Ok) return;
const char* t = doc["type"] | "";
if (strcmp(t, "display") == 0) {
startDisplay(doc["text"] | "", doc["speed"] | 50, doc["color"] | "#00FF41");
StaticJsonDocument<256> ack;
ack["type"] = "display_ack";
ack["text"] = currentText;
String s; serializeJson(ack, s);
wsServer.broadcastTXT(s);
}
if (strcmp(t, "clear") == 0) {
startDisplay("", 50, nullptr);
wsServer.broadcastTXT("{\"type\":\"display_ack\",\"text\":\"\"}");
}
}
}
// ── WiFi + mDNS ──────────────────────────────────────────────────
void connectWiFi() {
WiFi.setHostname(HOSTNAME);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("[WiFi] connecting");
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print('.'); }
Serial.printf("\n[WiFi] IP: %s\n", WiFi.localIP().toString().c_str());
if (MDNS.begin(HOSTNAME))
Serial.printf("[mDNS] %s.local\n", HOSTNAME);
}
// ── Setup ─────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
delay(400);
FastLED.addLeds(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
clearLeds();
FastLED.show();
// Brief green flash to confirm hardware is alive
leds[xyToLed(0, 0)] = CRGB::Green;
FastLED.show();
delay(500);
clearLeds();
FastLED.show();
connectWiFi();
wsServer.begin();
wsServer.onEvent(onWsEvent);
Serial.printf("[WSS] server on port %d\n", WS_PORT);
}
// ── Loop ──────────────────────────────────────────────────────────
void loop() {
wsServer.loop();
if (!scrolling || textPixelWidth() <= MATRIX_W) return;
unsigned long now = millis();
if (now - lastStep >= (unsigned long)speedToStepMs(scrollSpeed)) {
lastStep = now;
scrollX--;
if (scrollX < -(textPixelWidth())) scrollX = MATRIX_W; // wrap
renderFrame();
}
}
Web Interface
I also created a web interface to allow the user to customize the text, color and speed of the LED matrix. The web interface also allows the user to see the current state of the switches and the LED matrix. The web interface is built using HTML and CSS. It automatically tries to connect to the WebSocket servers on each of the microcontrollers and displays their hostnames to indicate the connection status.
Hardware Assembly
This is the failed attempt at sending the messages to the LED Matrix. The reason this did not work is because the protocols and libraries were not properly configured in the firmware for ESP32C3_2.
The Input Board with the switch 0 on D0 and switch 1 on D1. The antenna on the ESP32C3 allows it to connect to the WiFi network.
The Output Board with the LED Matrix connected to the ESP32C3. This also has an antenna for the same reason as the Input Board.
This video shows the full series of operations working. when a switch is pressed, the interface registers the input and displays the corresponding message with the options to edit the text, color and speed. The message is then sent to the LED Matrix as the output.