Community project

CDP LLDP Switch Monitor

emuenersej

Published August 14, 2026

ESP32
Photo of CDP LLDP Switch MonitorGenerated with AI

This project turns an ESP32 into a network discovery tool that captures and displays CDP and LLDP frames from connected network switches. By monitoring these standard network protocols, the device reveals switch information like device name, port details, and MAC addresses—useful for network troubleshooting and device identification without needing a full network analyzer.

The build combines an ESP32 microcontroller with a W5500 Ethernet module for raw frame capture and an SSD1306 OLED display for real-time information. This guide provides a complete parts list, wiring diagram showing all connections between the three boards, step-by-step assembly instructions, and the full firmware with CDP and LLDP parsing logic ready to upload.

Wiring diagram

Interactive · read-only
Wiring diagram for CDP LLDP Switch Monitor

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
W5500 Ethernet Module3.3 V SPI version1Hardwired TCP/IP stack SPI Ethernet module based on the WIZnet W5500 IC with integrated RJ45 connector. Supports full TCP/IP stack in hardware (TCP, UDP, ICMP, IPv4, ARP, IGMP, PPPoE) over SPI. Operates on 3.3V logic (most breakout boards include onboard 3.3V LDO and level-shifting for 5V compatibility). Widely used with ESP32 for Modbus TCP and other TCP/IP protocols over LAN. SPI bus can be shared with other SPI devices (e.g. MicroSD) using separate CS pins.
SSD1306 OLED0.96 in, 128x64, I2C address 0x3C10.96 inch 128x64 OLED display with I2C interface

Assembly

5 steps
  1. Place the three boards

    Put the ESP32 DevKit V1, the W5500 Ethernet module, and the 0.96 inch OLED where their labeled pins are easy to reach. Do not connect USB power yet.

    • Tip: Check that the Ethernet module is a 3.3 V SPI version before wiring it.
    • Tip: The RJ45 socket is the larger network socket on the W5500 board.
    • Do not power a 3.3 V-only W5500 module from the ESP32 5 V pin; too much voltage can damage it.
  2. Connect power and ground

    Use red jumper wires from the ESP32 3V3 pin to W5500 VCC and OLED VCC. Use black jumper wires from an ESP32 GND pin to W5500 GND and OLED GND. These three boards must share the same ground wire reference.

    • Tip: VCC → 3V3 (power) on both modules; GND → GND (ground) on both modules.
    • Tip: A single ESP32 GND pin may feed both black ground wires through a breadboard ground row.
    • Make sure VCC and GND are not swapped — swapped power can damage either module.
  3. Wire the Ethernet module data pins

    Connect W5500 MOSI to ESP32 GPIO23, W5500 MISO to GPIO19, W5500 SCK to GPIO18, and W5500 CS to GPIO4. Leave the W5500 INT and RST pins unconnected for this build.

    • Tip: MOSI → GPIO23 (data), MISO → GPIO19 (data back), SCK → GPIO18 (timing), CS → GPIO4 (selects the Ethernet module).
    • Tip: Follow the labels printed beside the W5500 header; module pin order differs between sellers.
    • Do not connect the W5500 to the ESP32 5 V pin unless its own board clearly states it accepts 5 V logic; this design uses 3.3 V signals.
  4. Wire the small screen

    Connect OLED SDA to ESP32 GPIO21 and OLED SCL to GPIO22. Keep the OLED’s red VCC wire on 3V3 and black GND wire on GND from the earlier step.

    • Tip: SDA → GPIO21 (screen data) and SCL → GPIO22 (screen timing).
    • Tip: Most 0.96 inch SSD1306 OLED modules use address 0x3C, which this build expects.
    • Make sure VCC and GND are not swapped — swapped power can damage the screen.
  5. Connect the switch network

    Plug a normal Ethernet patch cable from the W5500 RJ45 socket into a switch port. For the most useful result, use a switch that sends CDP or LLDP announcements on that port.

    • Tip: The device listens only; it does not need a network password or an IP address.
    • Tip: The display changes only when it receives a CDP or LLDP announcement, which many switches repeat periodically.
    • Use only the low-voltage Ethernet socket on ordinary network equipment; never connect this project to telephone, PoE injector output, or mains wiring.

Pin assignments

Board wiring reference
PinConnectionType
3V3w5500_1 VCCpower
GNDw5500_1 GNDground
GPIO 23w5500_1 MOSIspi
GPIO 19w5500_1 MISOspi
GPIO 18w5500_1 SCKspi
GPIO 4w5500_1 CSspi
3V3oled_1 VCCpower
GNDoled_1 GNDground
GPIO 21oled_1 SDAi2c
GPIO 22oled_1 SCLi2c

Firmware

ESP32
main.cppDeploy to device
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>


struct SwitchInfo {
  String protocol = "Waiting";
  String name = "No CDP / LLDP";
  String port = "frames received yet";
  String address = "Connect to a switch";
};


// Forward declarations
void w5500Select();
void w5500Deselect();
void w5500Write(uint16_t address, uint8_t block, const uint8_t *data, uint16_t length);
void w5500Read(uint16_t address, uint8_t block, uint8_t *data, uint16_t length);
uint8_t w5500Read8(uint16_t address, uint8_t block);
uint16_t w5500Read16(uint16_t address, uint8_t block);
void w5500Write8(uint16_t address, uint8_t block, uint8_t value);
void w5500Write16(uint16_t address, uint8_t block, uint16_t value);
void readRxRing(uint16_t pointer, uint8_t *destination, uint16_t length);
String macText(const uint8_t *mac);
String printableText(const uint8_t *data, uint16_t length);
void showLatest();
void parseLldp(const uint8_t *frame, uint16_t length);
void parseCdp(const uint8_t *frame, uint16_t length);
void inspectFrame(const uint8_t *frame, uint16_t length);
void beginW5500();
void receiveFrames();

constexpr uint8_t W5500_CS_PIN = 4;
constexpr uint8_t W5500_SCK_PIN = 18;
constexpr uint8_t W5500_MISO_PIN = 19;
constexpr uint8_t W5500_MOSI_PIN = 23;
constexpr uint8_t OLED_SDA_PIN = 21;
constexpr uint8_t OLED_SCL_PIN = 22;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr int SCREEN_WIDTH = 128;
constexpr int SCREEN_HEIGHT = 64;
constexpr uint16_t MAX_FRAME = 1522;
constexpr uint16_t RX_BUFFER_MASK = 0x3FFF;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);



SwitchInfo latest;
String lastScreen;

void w5500Select() { digitalWrite(W5500_CS_PIN, LOW); }
void w5500Deselect() { digitalWrite(W5500_CS_PIN, HIGH); }

void w5500Write(uint16_t address, uint8_t block, const uint8_t *data, uint16_t length) {
  w5500Select();
  SPI.transfer(address >> 8);
  SPI.transfer(address & 0xFF);
  SPI.transfer((block << 3) | 0x04); // write, variable-length SPI transfer
  for (uint16_t i = 0; i < length; ++i) SPI.transfer(data[i]);
  w5500Deselect();
}

void w5500Read(uint16_t address, uint8_t block, uint8_t *data, uint16_t length) {
  w5500Select();
  SPI.transfer(address >> 8);
  SPI.transfer(address & 0xFF);
  SPI.transfer((block << 3) | 0x0C); // read, variable-length SPI transfer
  for (uint16_t i = 0; i < length; ++i) data[i] = SPI.transfer(0);
  w5500Deselect();
}

uint8_t w5500Read8(uint16_t address, uint8_t block) {
  uint8_t value;
  w5500Read(address, block, &value, 1);
  return value;
}

uint16_t w5500Read16(uint16_t address, uint8_t block) {
  uint8_t bytes[2];
  w5500Read(address, block, bytes, 2);
  return (uint16_t(bytes[0]) << 8) | bytes[1];
}

void w5500Write8(uint16_t address, uint8_t block, uint8_t value) {
  w5500Write(address, block, &value, 1);
}

void w5500Write16(uint16_t address, uint8_t block, uint16_t value) {
  uint8_t bytes[2] = {uint8_t(value >> 8), uint8_t(value & 0xFF)};
  w5500Write(address, block, bytes, 2);
}

void readRxRing(uint16_t pointer, uint8_t *destination, uint16_t length) {
  for (uint16_t i = 0; i < length; ++i) {
    w5500Read((pointer + i) & RX_BUFFER_MASK, 3, &destination[i], 1); // socket 0 RX buffer
  }
}

String macText(const uint8_t *mac) {
  char text[18];
  snprintf(text, sizeof(text), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  return String(text);
}

String printableText(const uint8_t *data, uint16_t length) {
  String result;
  for (uint16_t i = 0; i < length; ++i) {
    char c = char(data[i]);
    result += (c >= 32 && c <= 126) ? c : ' ';
  }
  result.trim();
  return result;
}

void showLatest() {
  String signature = latest.protocol + "|" + latest.name + "|" + latest.port + "|" + latest.address;
  if (signature == lastScreen) return;
  lastScreen = signature;
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("Switch discovery: ");
  display.println(latest.protocol);
  display.println("---------------------");
  display.println(latest.name.substring(0, 21));
  display.println(latest.port.substring(0, 21));
  display.println(latest.address.substring(0, 21));
  display.display();
}

void parseLldp(const uint8_t *frame, uint16_t length) {
  if (length < 16) return;
  uint16_t pos = 14;
  String chassis;
  String port;
  String systemName;
  while (pos + 2 <= length) {
    uint16_t header = (uint16_t(frame[pos]) << 8) | frame[pos + 1];
    uint8_t type = header >> 9;
    uint16_t valueLength = header & 0x01FF;
    pos += 2;
    if (pos + valueLength > length) break;
    if (type == 0) break;
    if (type == 1 && valueLength > 1) chassis = printableText(frame + pos + 1, valueLength - 1);
    if (type == 2 && valueLength > 1) port = printableText(frame + pos + 1, valueLength - 1);
    if (type == 5 && valueLength > 0) systemName = printableText(frame + pos, valueLength);
    pos += valueLength;
  }
  latest.protocol = "LLDP";
  latest.name = systemName.length() ? systemName : "Unnamed switch";
  latest.port = port.length() ? "Port: " + port : "Port not announced";
  latest.address = chassis.length() ? "ID: " + chassis : "ID: " + macText(frame + 6);
  showLatest();
}

void parseCdp(const uint8_t *frame, uint16_t length) {
  // CDP is IEEE 802.3 + LLC/SNAP. Its Cisco protocol ID is 0x2000.
  if (length < 26) return;
  uint16_t pos = 22; // version, TTL, checksum follow the LLC/SNAP header
  pos += 4;
  String device;
  String port;
  String platform;
  while (pos + 4 <= length) {
    uint16_t type = (uint16_t(frame[pos]) << 8) | frame[pos + 1];
    uint16_t tlvLength = (uint16_t(frame[pos + 2]) << 8) | frame[pos + 3];
    if (tlvLength < 4 || pos + tlvLength > length) break;
    const uint8_t *value = frame + pos + 4;
    uint16_t valueLength = tlvLength - 4;
    if (type == 0x0001) device = printableText(value, valueLength); // device ID
    if (type == 0x0003) port = printableText(value, valueLength);   // port ID
    if (type == 0x0006) platform = printableText(value, valueLength); // platform
    pos += tlvLength;
  }
  latest.protocol = "CDP";
  latest.name = device.length() ? device : "Cisco device";
  latest.port = port.length() ? "Port: " + port : "Port not announced";
  latest.address = platform.length() ? platform : "From " + macText(frame + 6);
  showLatest();
}

void inspectFrame(const uint8_t *frame, uint16_t length) {
  if (length < 14) return;
  uint16_t etherType = (uint16_t(frame[12]) << 8) | frame[13];
  if (etherType == 0x88CC) {
    parseLldp(frame, length);
    return;
  }
  // 802.3 length frame, LLC AA AA 03 and Cisco SNAP OUI 00:00:0C / PID 0x2000.
  if (etherType <= 1500 && length >= 22 && frame[14] == 0xAA && frame[15] == 0xAA && frame[16] == 0x03 && frame[17] == 0x00 && frame[18] == 0x00 && frame[19] == 0x0C && frame[20] == 0x20 && frame[21] == 0x00) {
    parseCdp(frame, length);
  }
}

void beginW5500() {
  pinMode(W5500_CS_PIN, OUTPUT);
  digitalWrite(W5500_CS_PIN, HIGH);
  SPI.begin(W5500_SCK_PIN, W5500_MISO_PIN, W5500_MOSI_PIN, W5500_CS_PIN);
  delay(50);
  w5500Write8(0x0000, 0, 0x80); // software reset
  delay(50);
  uint8_t mac[6] = {0x02, 0xCD, 0x10, 0x00, 0x00, 0x01};
  w5500Write(0x0009, 0, mac, sizeof(mac));
  // Give raw socket 0 all 16 KB of the W5500 receive buffer.
  w5500Write8(0x001E, 1, 16);
  w5500Write8(0x0000, 1, 0x04); // socket 0: MACRAW mode
  w5500Write8(0x0001, 1, 0x01); // OPEN command
  delay(10);
}

void receiveFrames() {
  uint16_t received = w5500Read16(0x0026, 1); // socket 0 RX received size
  if (received < 2) return;
  uint16_t readPointer = w5500Read16(0x0028, 1); // socket 0 RX read pointer
  uint8_t header[2];
  readRxRing(readPointer, header, 2);
  uint16_t frameLength = (uint16_t(header[0]) << 8) | header[1];
  uint16_t consume = frameLength + 2;
  if (frameLength >= 14 && frameLength <= MAX_FRAME && consume <= received) {
    static uint8_t frame[MAX_FRAME];
    readRxRing(readPointer + 2, frame, frameLength);
    inspectFrame(frame, frameLength);
  } else if (frameLength > received) {
    return; // wait until the complete frame has reached the chip
  }
  w5500Write16(0x0028, 1, readPointer + consume);
  w5500Write8(0x0001, 1, 0x40); // RECV: release this frame buffer space
}

void setup() {
  Serial.begin(115200);
  Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
  display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Starting Ethernet");
  display.println("discovery listener...");
  display.display();
  beginW5500();
  showLatest();
}

void loop() {
  receiveFrames();
  delay(5);
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik