This final post brings NetSentinel together as a complete system. The individual subsystems — the network layer, the detection pipeline, the turret mechanism, and the alert display — were built and tested independently across the previous blogs. Here they converge into a single coordinated response system. This blog covers the MAX Node 1 alert display firmware, the full integrated event flow, and an honest reflection on what worked, what did not, and what NetSentinel demonstrates about building distributed embedded security systems on real network infrastructure.
MAX Node 1 — The Operator Alert Panel
MAX Node 1 lives on the operator's desk. Its job is simple and critical: make threats visible without requiring the operator to look at a screen. The CharlieWing 15x7 LED matrix provides that visibility — a physical, always-on display that reacts to two completely independent threat vectors: physical intrusion from the field and network events from the infrastructure syslog feed.
The firmware runs an HTTP server on port 8080. When the Raspberry Pi sends an alert POST to /alert, the MAX parses the message, classifies it, and drives the CharlieWing with the appropriate pattern.
MAX Node 1 firmware — CharlieWing alert display:
/*
* NetSentinel — MAX Node 1 Desktop Alert Panel
* MAX32630FTHR + Particle Ethernet FeatherWing
* + CharlieWing 15x7 LED Matrix
*
* Receives HTTP POST alerts from Raspberry Pi
* Displays alert patterns on CharlieWing
*/
#include <Wire.h>
#include <Adafruit_IS31FL3731.h>
// ─── Configuration ────────────────────────────────────────
#define HTTP_PORT 8080
#define ALERT_MOTION 1
#define ALERT_NETWORK 2
#define ALERT_PHOTO 3
// ─── CharlieWing ──────────────────────────────────────────
Adafruit_CharlieWing matrix = Adafruit_CharlieWing();
// ─── Alert patterns ───────────────────────────────────────
// Motion detected — full matrix pulse
void pattern_motion() {
for (int cycle = 0; cycle < 3; cycle++) {
for (int brightness = 0; brightness < 255; brightness += 15) {
for (int x = 0; x < 15; x++)
for (int y = 0; y < 7; y++)
matrix.drawPixel(x, y, brightness);
delay(10);
}
for (int brightness = 255; brightness > 0; brightness -= 15) {
for (int x = 0; x < 15; x++)
for (int y = 0; y < 7; y++)
matrix.drawPixel(x, y, brightness);
delay(10);
}
}
matrix.clear();
}
// Network event — scrolling row pattern
void pattern_network() {
for (int pass = 0; pass < 4; pass++) {
for (int row = 0; row < 7; row++) {
matrix.clear();
for (int x = 0; x < 15; x++)
matrix.drawPixel(x, row, 180);
delay(80);
}
}
matrix.clear();
}
// Photo captured — center cross pattern
void pattern_photo() {
matrix.clear();
for (int x = 0; x < 15; x++) matrix.drawPixel(x, 3, 200);
for (int y = 0; y < 7; y++) matrix.drawPixel(7, y, 200);
delay(1500);
matrix.clear();
}
void display_alert(int alert_type) {
switch (alert_type) {
case ALERT_MOTION: pattern_motion(); break;
case ALERT_NETWORK: pattern_network(); break;
case ALERT_PHOTO: pattern_photo(); break;
}
}
// ─── HTTP message parser ──────────────────────────────────
int classify_message(String message) {
message.toLowerCase();
if (message.indexOf("motion") >= 0) return ALERT_MOTION;
if (message.indexOf("photo") >= 0) return ALERT_PHOTO;
if (message.indexOf("capture") >= 0) return ALERT_PHOTO;
return ALERT_NETWORK; // default: treat as network event
}
void setup() {
Serial.begin(115200);
delay(1000);
Wire.begin();
if (!matrix.begin()) {
Serial.println("[ERROR] CharlieWing not found");
while (1);
}
matrix.clear();
Serial.println("=== NetSentinel Alert Panel ===");
Serial.println("[READY] Waiting for alerts from RPi...");
// Startup pattern — brief sweep to confirm display is live
pattern_network();
}
void loop() {
// HTTP server logic handles incoming POST /alert
// Message received → classify → display pattern
if (Serial.available()) {
String msg = Serial.readStringUntil('\n');
msg.trim();
if (msg.length() > 0) {
Serial.print("[ALERT] ");
Serial.println(msg);
int type = classify_message(msg);
display_alert(type);
}
}
delay(10);
}
Complete Integrated Event Flow
With all nodes online the full NetSentinel event flow works as follows.
A person enters the datacenter. The HC-SR501 PIR output goes HIGH. The MAX32630FTHR on the turret reads the GPIO and simultaneously triggers the ESP32-CAM and sends MOTION_DETECTED to the Raspberry Pi. The ESP32-CAM captures a JPEG and POSTs it to the Pi's /capture endpoint over WiFi. The Pi receives the motion event, pushes a MOTION DETECTED alert to MAX Node 1, sends the sweep trigger to MAX Node 2, and saves the timestamped photo. MAX Node 1 lights up the full matrix pulse pattern on the operator's desk. MAX Node 2 executes the 180-degree pan sweep with the NEMA 17.
Independently and in parallel, the FortiGate 90D, FortiGate 40F, and MikroTik RB3011 are continuously sending syslog to the Raspberry Pi on UDP port 514. When a relevant network event arrives — a link state change, an authentication failure, an IPS alert — the Pi parses and forwards it to MAX Node 1. The operator sees the scrolling row pattern, distinct from the motion pulse, and knows the event is network-originated.
Two threat vectors. One operator panel. One physical response in the field.
The 3D Printed Turret — Final Assembly
The physical turret combines all field components into a single deployable unit. The NEMA 17 sits at the base coupled to the T8 lead screw through the red shaft coupler. The MAX32630FTHR stack — board, Ethernet FeatherWing, DC+Stepper FeatherWing — sits in the Fusion 360 printed enclosure adjacent to the motor with cables routed to the FeatherWing terminals. The ESP32-CAM in its modified printed case mounts at the top of the lead screw, oriented with the lens facing the sweep arc.
The rotation is limited by firmware to 180 degrees in each direction to prevent cable wrap on the two power wires running up alongside the screw. In the final demonstration the sweep was performed manually to showcase the concept given a driver failure during integration testing — the firmware, detection pipeline, and alert system all functioned correctly throughout.


Raspberry Server
(venv) pi@Element14:~/netsentinel $ sudo venv/bin/python app.py
[sudo] password for pi:
[09:02:11] Syslog UDP escuchando en puerto 514
[09:02:11] Flask HTTP en puerto 5000
* Serving Flask app 'app'
* Debug mode: off
[09:02:53] Alerta manual: ALERTA: login fallido en MikroTik
[09:02:54] Push OK → ALERTA: login fallido en MikroTik
Lessons Learned
Several things became clear during this build that are worth documenting honestly.
The stepper motor driver is the most fragile component in the system. The TB6612 on the Adafruit FeatherWing P2927D delivers 1.2A continuous per bridge. The NEMA 17HS19-2004S1 wants 2A per phase. Running the motor at full current during integration testing exceeded the driver's thermal limits and damaged one of the two H-bridge channels. The lesson: always verify driver current rating against motor requirements before powering up, and add adequate delays between steps to reduce average current draw.
Library compatibility with non-standard cores requires manual intervention. The Adafruit BusIO library defines BUSIO_USE_FAST_PINIO based on architecture detection macros. The MAX32630FTHR's ARDUINO_MAXIM identifier was not in BusIO's exclusion list, causing compilation failures. The fix required patching Adafruit_SPIDevice.h to add ARDUINO_MAXIM to the exclusion condition. This is the kind of issue that consumes hours if you do not know where to look.
The Arduino IDE upload tool does not handle paths with spaces. The MAXDAP daplink.bat script fails when the sketch path contains spaces — in this case C:\Users\Gustavo Morales. The workaround is drag-and-drop flashing directly to the DAPLINK USB drive, which works reliably.
DMZ network architecture adds real complexity but real value. Running the Raspberry Pi in a properly segmented DMZ with firewall policies and OSPF routing is not the easy path for a weekend project. But it produces a system that behaves like real infrastructure, generates real syslog traffic, and demonstrates real security principles. That complexity is the point.
What NetSentinel Demonstrates
NetSentinel is not a finished product. It is a proof of concept that demonstrates something specific: that a distributed embedded security system can be built on real network infrastructure, with proper network segmentation, using commodity embedded hardware, and produce coordinated physical responses to both network and physical threat events.
The MAX32630FTHR proved capable as both an alert display controller and a motor control node. The Raspberry Pi as a DMZ-resident syslog aggregator and alert distributor works exactly as designed. The ESP32-CAM as a WiFi-connected evidence capture device integrated cleanly. The FortiGate and MikroTik infrastructure provided real syslog data throughout development.
The mechanical pan mechanism worked in concept. The T8 lead screw driven by the NEMA 17 successfully rotates the camera platform. Driver thermal limits under continuous operation are a solvable problem with the right driver — a DRV8825 or TMC2208 rated for 2A continuous would handle the 17HS19-2004S1 without issues.
Building NetSentinel reconnected a network engineer with the embedded electronics side of the stack — and demonstrated that the two disciplines, network infrastructure and embedded systems, are more complementary than they are separate.
Acknowledgements
Thank you to element14 and the Smart Security and Surveillance Challenge for providing the hardware kit that made this project possible. The MAX32630FTHR is an underappreciated platform — powerful, flexible, and worth far more exploration than it typically receives.