Solderless Portable Mini Network Operating Center Under $8

by avidgamer in Circuits > Computers

235 Views, 3 Favorites, 0 Comments

Solderless Portable Mini Network Operating Center Under $8

WhatsApp Video 2025-06-14 at 13.05 (online-video-cutter.com) (1).gif

Hey there everyone! Here is how to build your own Network Operations Center (NOC) device to monitor your home WiFi network in real-time! This compact device features a TFT display showing connection status, signal strength graphs, and speed tests, plus a web dashboard accessible from any (internet enabled) device on your network.

There are 2 ways to make this, one with soldering and one without. I thought it would be better to show how to make the solderless version of this, so anyone (10 years or older) can follow along.

If you have Arduino IDE + Libraries + ESP32 package downloaded, go ahead and skip ahead, if not, follow along.

Note: Albeit only 5v, we will be working with current, so always be careful when handling electricity in any way.

Supplies

Materials (4).png

Materials

  1. 1.8 inch TFT LCD Module - $1.79 on Aliexpress
  2. ESP32 C3 Supermini - $2.39 on Aliexpress
  3. USB C to USB charging cable - $1.25 at Dollar Store
  4. 30g PLA Filament - $2
  5. Female to Female Dupont Connectors
  6. Male to Female Dupont Connectors

Other

  1. Computer to transfer code
  2. Data Cable to transfer code
  3. Wifi connection (to monitor)
  4. Arduino IDE
  5. 3D Printer

Solder or workaround (only need either solder or a pin badge not both)

  1. Soldering iron + solder
  2. Pin Badge (explained in step 12-13)

What Is a NOC?

network-operations-center-1-1.jpg

A NOC (Network Operations Center) is a centralized location where IT teams monitor, manage, and maintain computer networks and WiFi infrastructure. It serves as the "command center" for network operations.

Key functions of a NOC include:

  1. Monitoring - Continuously watching network performance, server health, and system alerts 24/7
  2. Incident Response - Quickly identifying and responding to network outages, security breaches, or performance issues
  3. Maintenance - Performing routine updates, patches, and preventive maintenance on network equipment
  4. Troubleshooting - Diagnosing and resolving network problems to minimize downtime

In this project, we are going to create a mini version of this!

Overview of Features

Display & Interface

  1. 1.8" ST7735 TFT color display with 3 rotating pages
  2. Auto-dimming after 15 seconds of inactivity
  3. Web dashboard accessible via the device's IP address

Connectivity Monitoring

  1. Real-time WiFi connection status and signal strength (RSSI)
  2. Visual signal strength bars on display
  3. Automatic reconnection with retry logic
  4. Connection history tracking over 2-minute periods

Performance Testing

  1. Automated speed/response time tests every 30 seconds
  2. Connection quality assessment (Excellent/Good/Fair/Poor)
  3. HTTP response time measurement for network latency

Data Visualization

  1. Live signal strength graph showing historical RSSI data
  2. Status page with uptime, IP address, and connection details
  3. Speed test results with animated progress indicators

Network Operations

  1. 24/7 uptime tracking
  2. Automatic page cycling every 5 seconds
  3. Remote monitoring via built-in web server
  4. Persistent monitoring with automatic recovery

It's essentially a pocket-sized network monitoring station that provides both local display and remote web access to WiFi network health metrics.

Use Cases

Troubleshooting - Find WiFi dead zones, diagnose connection drops, identify interference sources

Network Planning - Test router placement, validate coverage areas, optimize signal strength

Remote Monitoring - Check network status remotely via web interface, monitor unmanned locations

Small Business - Track WiFi performance for POS systems, kiosks, or critical connections

Field Work - Portable network diagnostics for technicians, on-site testing tool

Education - Learn network concepts, prototype monitoring solutions, demonstrate WiFi behavior

Essentially serves as an affordable, portable network monitoring tool for situations where enterprise-grade solutions are overkill.

Download Arduino IDE

Screenshot 2025-06-15 151301.png

If you do not have it already, go to https://www.arduino.cc/en/software/ and download the IDE

ESP32 Packages

Screenshot 2025-06-15 151337.png
Screenshot 2025-06-15 151344.png
Screenshot 2025-06-15 151359.png

Navigate your way to File>Preferences>Additional Boards Manager, and paste the following (separated by commas):

https://dl.espressif.com/dl/package_esp32_index.json
https://espressif.github.io/arduino-esp32/package_esp32_index.json

Libraries

Screenshot 2025-06-15 151840.png
Screenshot 2025-06-15 151848.png

We will also need 2 additional libraries, so install the above indicated libraries from library manager.

Inputting of Code

IMG-20250615-WA0001.jpg

With the packages and libraries installed, upload the following code into the ESP32 C3:

#include <WiFi.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <WebServer.h>
#include <HTTPClient.h>

// Replace with your network credentials
const char* ssid = "YOURWIFISSID";
const char* password = "YOURWIFIPASSWORD";

// ST7735 TFT display pin definitions for ESP32-C3 SuperMini
#define TFT_CS 7 // Chip select
#define TFT_RST 6 // Reset
#define TFT_DC 5 // Data/Command
#define TFT_MOSI 4 // SDA/MOSI
#define TFT_SCLK 3 // SCL/SCLK

// Create display object
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);

// Web server
WebServer server(80);

// Display pages
enum DisplayPage {
PAGE_STATUS,
PAGE_SPEED_TEST,
PAGE_HISTORY_GRAPH,
PAGE_COUNT
};

DisplayPage currentPage = PAGE_STATUS;
unsigned long lastPageSwitch = 0;
const unsigned long pageInterval = 5000; // Switch pages every 5 seconds

// Variables for tracking uptime
unsigned long startTime;
unsigned long lastUpdate = 0;
const unsigned long updateInterval = 5000; // Update every 5 seconds

// WiFi retry logic variables
unsigned long lastWiFiCheck = 0;
const unsigned long wifiCheckInterval = 5000; // Check WiFi every 5 seconds
bool wasConnected = false;
int retryCount = 0;
const int maxRetries = 3;

// Speed test variables
float downloadSpeedMbps = 0.0;
float uploadSpeedMbps = 0.0;
unsigned long lastSpeedTest = 0;
const unsigned long speedTestInterval = 30000; // Test every 30 seconds
bool speedTestInProgress = false;
String speedTestStatus = "Ready";

// Connection history for graph (last 60 data points)
int rssiHistory[60];
int historyIndex = 0;
bool historyFull = false;
unsigned long lastHistoryUpdate = 0;
const unsigned long historyInterval = 2000; // Update history every 2 seconds

// Display dimming
bool isDimmed = false;
unsigned long dimTimer = 0;
const unsigned long dimDelay = 15000; // Dim after 15 seconds
unsigned long lastPowerOn = 0; // Track power cycles

void setup() {
Serial.begin(115200);
// Initialize display
tft.initR(INITR_BLACKTAB);
tft.setRotation(1); // Landscape orientation
tft.fillScreen(ST77XX_BLACK);
// Initialize history array
for (int i = 0; i < 60; i++) {
rssiHistory[i] = -100; // Initialize with very poor signal
}
// Display startup message
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
tft.setCursor(10, 10);
tft.println("ESP32-C3 Mini NOC");
tft.setCursor(10, 30);
tft.println("Connecting to WiFi...");
// Connect to WiFi
connectToWiFi();
startTime = millis();
lastPowerOn = millis();
dimTimer = millis();
// Initialize web server
setupWebServer();
// Clear screen after connection
tft.fillScreen(ST77XX_BLACK);
}

void loop() {
// Handle web server
server.handleClient();
// Check WiFi connection status periodically
if (millis() - lastWiFiCheck >= wifiCheckInterval) {
checkWiFiConnection();
lastWiFiCheck = millis();
}
// Update connection history
if (millis() - lastHistoryUpdate >= historyInterval && WiFi.status() == WL_CONNECTED) {
updateConnectionHistory();
lastHistoryUpdate = millis();
}
// Perform speed test periodically
if (millis() - lastSpeedTest >= speedTestInterval && WiFi.status() == WL_CONNECTED && !speedTestInProgress) {
performSpeedTest();
lastSpeedTest = millis();
}
// Handle display dimming
handleDisplayDimming();
// Switch pages every 5 seconds
if (millis() - lastPageSwitch >= pageInterval) {
currentPage = (DisplayPage)((currentPage + 1) % PAGE_COUNT);
lastPageSwitch = millis();
tft.fillScreen(ST77XX_BLACK); // Clear entire screen before switching pages
updateDisplay(); // Immediately draw new page
}
// Update display
if (millis() - lastUpdate >= updateInterval) {
updateDisplay();
lastUpdate = millis();
}
delay(100);
}

void connectToWiFi() {
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) { // 30 second timeout
delay(1000);
Serial.println("Connecting to WiFi...");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
wasConnected = true;
retryCount = 0;
dimTimer = millis(); // Reset dim timer on connection
} else {
Serial.println("WiFi connection failed!");
wasConnected = false;
}
}

void checkWiFiConnection() {
if (WiFi.status() != WL_CONNECTED && wasConnected) {
Serial.println("WiFi disconnected! Attempting to reconnect...");
retryCount++;
if (retryCount <= maxRetries) {
WiFi.disconnect();
delay(1000);
connectToWiFi();
} else {
Serial.println("Max retry attempts reached. Will keep trying...");
retryCount = 0; // Reset counter to keep trying
}
}
// Update connection state
if (WiFi.status() == WL_CONNECTED) {
wasConnected = true;
} else {
wasConnected = false;
}
}

void updateConnectionHistory() {
if (WiFi.status() == WL_CONNECTED) {
rssiHistory[historyIndex] = WiFi.RSSI();
} else {
rssiHistory[historyIndex] = -100; // Disconnected
}
historyIndex = (historyIndex + 1) % 60;
if (historyIndex == 0) historyFull = true;
}

void performSpeedTest() {
speedTestInProgress = true;
speedTestStatus = "Testing...";
// Memory-efficient ping-based connection quality test
HTTPClient http;
http.begin("http://httpbin.org/status/200"); // Just get HTTP status, no data download
http.setTimeout(5000); // 5 second timeout
unsigned long startTime = millis();
int httpCode = http.GET();
unsigned long endTime = millis();
if (httpCode == 200) {
float responseTime = endTime - startTime; // Response time in ms
// Estimate "speed" based on response time (lower is better)
// This is more of a connection quality indicator than actual speed
if (responseTime < 100) {
downloadSpeedMbps = 10.0; // Excellent connection
} else if (responseTime < 300) {
downloadSpeedMbps = 5.0; // Good connection
} else if (responseTime < 800) {
downloadSpeedMbps = 2.0; // Fair connection
} else {
downloadSpeedMbps = 0.5; // Poor connection
}
speedTestStatus = String(responseTime) + "ms";
} else {
downloadSpeedMbps = 0.0;
speedTestStatus = "Failed";
}
http.end();
speedTestInProgress = false;
}

void handleDisplayDimming() {
// Check if we should dim (15 seconds after power on or connection)
if (!isDimmed && (millis() - dimTimer > dimDelay)) {
isDimmed = true;
// Reduce brightness by setting a darker background and text colors
}
// Reset dim timer on new connection
if (WiFi.status() == WL_CONNECTED && !wasConnected) {
dimTimer = millis();
isDimmed = false;
}
}

void updateDisplay() {
uint16_t textColor = isDimmed ? 0x7BEF : ST77XX_WHITE; // Dimmed or normal text
uint16_t accentColor = isDimmed ? 0x4208 : ST77XX_CYAN; // Dimmed or normal accent
switch (currentPage) {
case PAGE_STATUS:
drawStatusPage(textColor, accentColor);
break;
case PAGE_SPEED_TEST:
drawSpeedTestPage(textColor, accentColor);
break;
case PAGE_HISTORY_GRAPH:
drawHistoryGraphPage(textColor, accentColor);
break;
}
}

void drawStatusPage(uint16_t textColor, uint16_t accentColor) {
// Page title
tft.setTextColor(accentColor);
tft.setTextSize(1);
tft.setCursor(5, 5);
tft.println("WiFi Status [1/3]");
// WiFi connection status
tft.setCursor(5, 25);
tft.setTextColor(accentColor);
tft.print("Status: ");
if (WiFi.status() == WL_CONNECTED) {
tft.setTextColor(isDimmed ? 0x2604 : ST77XX_GREEN);
tft.print("Connected");
} else {
tft.setTextColor(isDimmed ? 0x4800 : ST77XX_RED);
tft.print("Disconnected");
}
// Signal strength with bars
tft.setTextColor(accentColor);
tft.setCursor(5, 40);
tft.print("Signal: ");
if (WiFi.status() == WL_CONNECTED) {
int rssi = WiFi.RSSI();
tft.setTextColor(textColor);
tft.print(rssi);
tft.print(" dBm");
drawSignalBars(rssi, 120, 40, isDimmed);
} else {
tft.setTextColor(textColor);
tft.print("N/A");
}
// Uptime
tft.setTextColor(accentColor);
tft.setCursor(5, 55);
tft.print("Uptime: ");
tft.setTextColor(textColor);
unsigned long uptime = millis() - startTime;
formatUptime(uptime);
// IP Address
tft.setTextColor(accentColor);
tft.setCursor(5, 70);
tft.print("IP: ");
tft.setTextColor(textColor);
if (WiFi.status() == WL_CONNECTED) {
tft.print(WiFi.localIP());
} else {
tft.print("Not assigned");
}
// Web interface info
tft.setTextColor(0x7BEF);
tft.setCursor(5, 85);
tft.print("Web: ");
if (WiFi.status() == WL_CONNECTED) {
tft.print(WiFi.localIP());
} else {
tft.print("Offline");
}
}

void drawSpeedTestPage(uint16_t textColor, uint16_t accentColor) {
// Page title
tft.setTextColor(accentColor);
tft.setTextSize(1);
tft.setCursor(5, 5);
tft.println("Speed Test [2/3]");
// Speed test status
tft.setTextColor(accentColor);
tft.setCursor(5, 25);
tft.print("Status: ");
tft.setTextColor(textColor);
tft.print(speedTestStatus);
// Connection quality/response time
tft.setTextColor(accentColor);
tft.setCursor(5, 45);
tft.print("Response: ");
tft.setTextColor(textColor);
if (speedTestStatus.indexOf("ms") > 0) {
tft.print(speedTestStatus); // Shows response time
} else {
tft.print(speedTestStatus); // Shows status
}
// Connection quality indicator
tft.setTextColor(accentColor);
tft.setCursor(5, 65);
tft.print("Quality: ");
tft.setTextColor(textColor);
if (WiFi.status() == WL_CONNECTED) {
int rssi = WiFi.RSSI();
if (rssi > -50) tft.print("Excellent");
else if (rssi > -60) tft.print("Good");
else if (rssi > -70) tft.print("Fair");
else tft.print("Poor");
} else {
tft.print("Disconnected");
}
// Progress bar for speed test
if (speedTestInProgress) {
tft.drawRect(5, 85, 100, 8, accentColor);
// Simple animated progress bar
int progress = (millis() / 200) % 100;
tft.fillRect(6, 86, progress, 6, isDimmed ? 0x2604 : ST77XX_GREEN);
}
}

void drawHistoryGraphPage(uint16_t textColor, uint16_t accentColor) {
// Page title
tft.setTextColor(accentColor);
tft.setTextSize(1);
tft.setCursor(5, 5);
tft.println("Signal History [3/3]");
// Draw graph axes
int graphX = 10;
int graphY = 25;
int graphWidth = 140;
int graphHeight = 50;
tft.drawLine(graphX, graphY + graphHeight, graphX + graphWidth, graphY + graphHeight, accentColor); // X-axis
tft.drawLine(graphX, graphY, graphX, graphY + graphHeight, accentColor); // Y-axis
// Y-axis labels
tft.setTextColor(0x7BEF);
tft.setTextSize(1);
tft.setCursor(2, graphY - 2);
tft.print("-30");
tft.setCursor(2, graphY + graphHeight/2 - 2);
tft.print("-65");
tft.setCursor(2, graphY + graphHeight - 8);
tft.print("-100");
// Draw signal history graph
int dataPoints = historyFull ? 60 : historyIndex;
if (dataPoints > 1) {
for (int i = 1; i < dataPoints && i < graphWidth; i++) {
int idx1 = historyFull ? (historyIndex + i - 1) % 60 : i - 1;
int idx2 = historyFull ? (historyIndex + i) % 60 : i;
// Map RSSI values (-30 to -100) to graph coordinates
int y1 = graphY + graphHeight - ((rssiHistory[idx1] + 100) * graphHeight / 70);
int y2 = graphY + graphHeight - ((rssiHistory[idx2] + 100) * graphHeight / 70);
// Clamp to graph bounds
y1 = constrain(y1, graphY, graphY + graphHeight);
y2 = constrain(y2, graphY, graphY + graphHeight);
uint16_t lineColor = isDimmed ? 0x2604 : ST77XX_GREEN;
if (rssiHistory[idx2] < -70) lineColor = isDimmed ? 0x8400 : ST77XX_YELLOW;
if (rssiHistory[idx2] < -80) lineColor = isDimmed ? 0x4800 : ST77XX_RED;
tft.drawLine(graphX + i - 1, y1, graphX + i, y2, lineColor);
}
}
// Current RSSI value
tft.setTextColor(accentColor);
tft.setCursor(5, 85);
tft.print("Current: ");
tft.setTextColor(textColor);
if (WiFi.status() == WL_CONNECTED) {
tft.print(WiFi.RSSI());
tft.print(" dBm");
} else {
tft.print("Disconnected");
}
// Time indicator
tft.setTextColor(0x7BEF);
tft.setCursor(5, 100);
tft.print("Last 2 mins displayed");
}

void drawSignalBars(int rssi, int x, int y, bool dimmed) {
int barWidth = 4;
int barHeight = 8;
int barSpacing = 6;
// Clear previous bars
tft.fillRect(x, y, 35, barHeight, ST77XX_BLACK);
// Determine number of bars based on RSSI
int bars = 0;
if (rssi > -50) bars = 4; // Excellent
else if (rssi > -60) bars = 3; // Good
else if (rssi > -70) bars = 2; // Fair
else if (rssi > -80) bars = 1; // Poor
else bars = 0; // Very poor
// Draw signal bars
for (int i = 0; i < 4; i++) {
int currentBarHeight = (i + 1) * 2; // Increasing height
int currentBarY = y + (barHeight - currentBarHeight);
uint16_t barColor;
if (i < bars) {
// Filled bars - color based on signal strength
if (dimmed) {
if (bars >= 3) barColor = 0x2604; // Dimmed green
else if (bars >= 2) barColor = 0x8400; // Dimmed yellow
else barColor = 0x4800; // Dimmed red
} else {
if (bars >= 3) barColor = ST77XX_GREEN;
else if (bars >= 2) barColor = ST77XX_YELLOW;
else barColor = ST77XX_RED;
}
} else {
// Empty bars
barColor = 0x31A6; // Dark gray
}
tft.fillRect(x + (i * barSpacing), currentBarY, barWidth, currentBarHeight, barColor);
}
}

void formatUptime(unsigned long uptime) {
unsigned long seconds = uptime / 1000;
unsigned long minutes = seconds / 60;
unsigned long hours = minutes / 60;
unsigned long days = hours / 24;
seconds %= 60;
minutes %= 60;
hours %= 24;
if (days > 0) {
tft.print(days);
tft.print("d ");
}
if (hours > 0 || days > 0) {
tft.print(hours);
tft.print("h ");
}
tft.print(minutes);
tft.print("m ");
tft.print(seconds);
tft.print("s");
}

void setupWebServer() {
server.on("/", HTTP_GET, []() {
String html = "<!DOCTYPE html><html><head><title>ESP32-C3 Mini NOC</title>";
html += "<meta http-equiv='refresh' content='5'>";
html += "<style>body{font-family:Arial;margin:20px;background:#f0f0f0;}";
html += ".container{background:white;padding:20px;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,0.1);}";
html += ".status{display:flex;justify-content:space-between;margin:10px 0;}";
html += ".connected{color:green;} .disconnected{color:red;}";
html += "</style></head><body>";
html += "<div class='container'>";
html += "<h1>ESP32-C3 Mini NOC Dashboard</h1>";
html += "<div class='status'><span>WiFi Status:</span><span class='";
html += (WiFi.status() == WL_CONNECTED) ? "connected'>Connected" : "disconnected'>Disconnected";
html += "</span></div>";
if (WiFi.status() == WL_CONNECTED) {
html += "<div class='status'><span>SSID:</span><span>" + WiFi.SSID() + "</span></div>";
html += "<div class='status'><span>IP Address:</span><span>" + WiFi.localIP().toString() + "</span></div>";
html += "<div class='status'><span>RSSI:</span><span>" + String(WiFi.RSSI()) + " dBm</span></div>";
html += "<div class='status'><span>Download Speed:</span><span>" + String(downloadSpeedMbps, 2) + " Mbps</span></div>";
}
unsigned long uptime = millis() - startTime;
html += "<div class='status'><span>Uptime:</span><span>" + String(uptime/1000) + " seconds</span></div>";
html += "<div class='status'><span>Speed Test Status:</span><span>" + speedTestStatus + "</span></div>";
html += "</div></body></html>";
server.send(200, "text/html", html);
});
server.begin();
Serial.println("Web server started");
}

Breadboard Wiring

IMG-20250615-WA0009.jpg
IMG-20250615-WA0007.jpg

To start, lets wire the screen to the ESP32 C3. The wiring should be done according to the following diagram (FM and FF mean Female Female or Male Female Dupont connectors):

Power Connections
3V3 → FM
GND → GND FF
VCC → FM
LED → FM

SPI Communication Connections
Pin 3 → SCL (SCLK) FF
Pin 4 → SDA (MOSI) FF
Pin 5 → DC FF
Pin 6 → RST FF
Pin 7 → CS FF

Testing

IMG-20250615-WA0012.jpg
WhatsApp Video 2025-06-14 at 13.05 (online-video-cutter.com) (2).gif

By providing power to the ESP32 it should immediately start trying to connect to WiFi. If this does not happen, look over the previous steps, or feel free to comment so I can help you out. (Gif is from old version of UI, new version of UI [the one I provided code for] will look much cleaner)

Case

Screenshot 2025-06-15 153723.png

Next we want to house the screen and ESP32 C3 in a case. Feel free to create your own, or use the one I have attached.


Courtesy of Fusion 360 Student Plan.

Downloads

3D Printing Case

VID-20250615-WA0001.gif

When 3D printing the case, make sure that you monitor your 3D printer when it creates the locking mechanism, or it may mess it up like mine. In this case simply use a wire cutter or scissors to trim off the extra plastic.

Fitting in the Screen

IMG-20250615-WA0010.jpg

To start, fit the screen into the case, making sure that the right way goes up, and that pins to in towards the right. Click on the image for clarification.

Inputting the Cable

IMG-20250615-WA0011.jpg

After that, carefully slide the USB C side of the cable through the hole into the case like so.

Inputting the ESP32

IMG-20250615-WA0008.jpg

After that, put the esp32 into the cavity, and connect it to the cable like so.

Solderless Workaround

Screenshot 2025-06-15 154531.png

As I said in the introduction, this is a fully solderless project, so to work around needing to solder the VCC and LED to the 3.3v, simply use the thing that connects a badge/pin to a shirt. (If one can be found made of an insulator like rubber, its even better)

Input All Three FM Dupont Cables Into Pin Holder

IMG-20250615-WA0005.jpg

Simply put all 3 Dupont connecters into the pin holder like so.

Fit Everything Into Box

IMG-20250615-WA0003.jpg
IMG-20250615-WA0006.jpg

Add Lid

IMG-20250615-WA0004.jpg
IMG-20250615-WA0002.jpg

Web Interface

Screenshot 2025-06-15 160056.png

In order to access the data collected by the mini NOC remotely, you can open up the webpage by typing in the IP address it says after 'web' directly into any browser. It will open up the above indicated page to monitor WiFi stats.

Completion!

WhatsApp Video 2025-06-14 at 13.05 (online-video-cutter.com) (1).gif

Now you have your own mini Network Operations Center, thanks for following along!