// =============================================================================
// Network Visualizer for WS2812B LED Strip
// =============================================================================
//
// HARDWARE CONNECTIONS:
// WS2812B strip data pin → D6
// Rotary encoder CLK → D2 (interrupt-capable)
// Rotary encoder DT → D3 (interrupt-capable)
// Rotary encoder SW → D4 (click button)
// Mode selector pin 0 → D8 (LSB of 2-bit binary selector)
// Mode selector pin 1 → D9 (MSB of 2-bit binary selector)
//
// MODE SELECTOR WIRING:
// A simple 4-position rotary switch with a common GND and 4 output pins can
// be encoded to 2 bits using two diodes and two resistors (a simple priority
// encoder), OR you can use a 4-position switch wired to pins D8–D11 and read
// them directly (see readModeSwitch() below — currently set up for 4 direct
// pins D8–D11 with INPUT_PULLUP, one pin pulled LOW per position).
//
// REQUIRED LIBRARIES:
// FastLED — install via Arduino Library Manager
//
// =============================================================================
#include <FastLED.h>
// =============================================================================
// HARDWARE CONFIGURATION
// =============================================================================
//#define LED_PIN 6 // WS2812B data line
#define LED_PIN 16 // WS2812B data line
#define NUM_LEDS 50 // Total LEDs = total nodes
//#define ENC_CLK 2 // Encoder clock (must be interrupt pin)
//#define ENC_DT 3 // Encoder data (must be interrupt pin)
//#define ENC_SW 4 // Encoder push button
#define ENC_CLK 32 // Encoder clock (must be interrupt pin)
#define ENC_DT 33 // Encoder data (must be interrupt pin)
#define ENC_SW 25 // Encoder push button
// 4-position rotary mode switch: one pin per position, active LOW (INPUT_PULLUP)
// Wire the switch so exactly one pin is pulled to GND per position.
//#define MODE_PIN_0 8 // Position 0 → Edit mode
//#define MODE_PIN_1 9 // Position 1 → Cliques mode
//#define MODE_PIN_2 10 // Position 2 → Bridges mode8
//#define MODE_PIN_3 11 // Position 3 → Distance mode
#define MODE_PIN_0 26 // Position 0 → Edit mode
#define MODE_PIN_1 27 // Position 1 → Cliques mode
#define MODE_PIN_2 14 // Position 2 → Bridges mode8
#define MODE_PIN_3 13 // Position 3 → Distance mode
#define BRIGHTNESS 80 // Global LED brightness (0–255)
// =============================================================================
// NETWORK DEFINITION
// =============================================================================
// Define your network as an adjacency matrix.
// NETWORK[i][j] = 1 means there is an edge between node i and node j.
// The matrix must be symmetric (undirected graph).
// Nodes are indexed 0 to NUM_NODES-1, each corresponding to one LED.
#define NUM_NODES 50
// Adjacency matrix — edit this to define your network topology.
// This default example creates a structured graph with several clusters,
// some bridges, and a few cliques for demonstration purposes.
const uint8_t NETWORK[NUM_NODES][NUM_NODES] = {
// Nodes 0–7: fully connected clique (clique A)
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
/* 0 */ { 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 1 */ { 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 2 */ { 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 3 */ { 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 4 */ { 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 5 */ { 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 6 */ { 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 7 */ { 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Node 7→8 and 7→9 are bridges to the next cluster
/* 8 */ { 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Nodes 8–13: another clique (clique B)
/* 9 */ { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 10 */ { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 11 */ { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 12 */ { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 13 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Node 13→14 bridge to cluster C
/* 14 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Nodes 14–19: cluster C (triangle + chain)
/* 15 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 16 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 17 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 18 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 19 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Node 19→20 bridge to cluster D
/* 20 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Nodes 20–26: clique D (5-clique)
/* 21 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 22 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 23 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 24 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Node 24→25 bridge to cluster E
/* 25 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Nodes 25–32: cluster E (ring + chord)
/* 26 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 27 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 28 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 29 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 30 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 31 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Node 31→32 bridge to cluster F
/* 32 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Nodes 32–38: cluster F (4-clique)
/* 33 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 34 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 35 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Node 34→36 bridge to cluster G
/* 36 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
// Nodes 36–43: cluster G (chain + small clique)
/* 37 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 38 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 39 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 40 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
/* 41 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0 },
/* 42 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0 },
/* 43 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0 },
// Node 43→44 bridge to final cluster H
/* 44 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
// Nodes 44–49: cluster H (small clique)
/* 45 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0 },
/* 46 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0 },
/* 47 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1 },
/* 48 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1 },
/* 49 */ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0 },
};
// =============================================================================
// COLOR PALETTE
// =============================================================================
// Colors used across display modes.
// Distance mode: hop colors. Index = hop count from selected node.
// Index 0 = selected node itself (green), 1 = 1 hop away, 2 = 2 hops, etc.
// Unreachable active nodes use COLOR_UNREACHABLE.
// The palette wraps after MAX_DIST_COLORS hops, cycling through again.
#define MAX_DIST_COLORS 7
const CRGB DIST_COLORS[MAX_DIST_COLORS] = {
CRGB(0, 255, 0), // 0 hops — green (selected node)
CRGB(80, 200, 0), // 1 hop — yellow-green
CRGB(180, 180, 0), // 2 hops — yellow
CRGB(255, 100, 0), // 3 hops — orange
CRGB(255, 0, 0), // 4 hops — red
CRGB(180, 0, 180), // 5 hops — magenta
CRGB(0, 0, 255), // 6 hops — blue
};
const CRGB COLOR_UNREACHABLE = CRGB(40, 0, 80); // dark purple — active but unreachable
const CRGB COLOR_INACTIVE = CRGB(5, 5, 5); // near-black — inactive node
const CRGB COLOR_SELECTED_EDIT = CRGB(0, 200, 255); // cyan — selected node in edit mode
// Clique mode: up to 8 distinct clique colors.
// If there are more cliques than colors, colors wrap around.
#define MAX_CLIQUE_COLORS 8
const CRGB CLIQUE_COLORS[MAX_CLIQUE_COLORS] = {
CRGB(255, 0, 0), // red
CRGB(0, 200, 255), // cyan
CRGB(255, 150, 0), // orange
CRGB(0, 255, 100), // spring green
CRGB(180, 0, 255), // violet
CRGB(255, 255, 0), // yellow
CRGB(0, 100, 255), // sky blue
CRGB(255, 0, 150), // pink
};
// Bridge mode colors
const CRGB COLOR_BRIDGE = CRGB(255, 50, 0); // orange-red — bridge edge endpoint
const CRGB COLOR_NORMAL_NODE = CRGB(0, 60, 120); // dim blue — non-bridge active node
// =============================================================================
// MODE DEFINITIONS
// =============================================================================
enum Mode {
MODE_EDIT = 0, // Select and toggle nodes active/inactive
MODE_CLIQUES = 1, // Highlight maximal cliques
MODE_BRIDGES = 2, // Highlight bridge nodes (articulation points)
MODE_DISTANCE = 3 // Color nodes by hop distance from selected node
};
// =============================================================================
// GLOBAL STATE
// =============================================================================
CRGB leds[NUM_LEDS]; // FastLED pixel buffer
bool nodeActive[NUM_NODES]; // true = node is active in the network
int selectedNode = 0; // Currently selected node (edit mode)
Mode currentMode = MODE_EDIT; // Active display mode
// --- Encoder state (volatile because modified in ISR) ---
volatile int encoderCount = 0; // Accumulated encoder pulses
volatile bool encoderChanged = false; // Set true by ISR when encoder moves
// --- Button debounce ---
unsigned long lastButtonPress = 0;
#define DEBOUNCE_MS 200
// --- Computed graph results (recalculated when topology changes) ---
int distanceMap[NUM_NODES]; // BFS distances from selectedNode (-1 = unreachable)
bool isBridgeNode[NUM_NODES]; // true if node is an articulation point
int cliqueId[NUM_NODES]; // which clique each node belongs to (-1 = none/multiple)
bool graphDirty = true; // true = recompute needed before next render
// =============================================================================
// ENCODER INTERRUPT SERVICE ROUTINE
// =============================================================================
// Called on every falling edge of ENC_CLK.
// Reads ENC_DT to determine rotation direction.
void encoderISR() {
// If DT is HIGH when CLK falls, direction is clockwise (+1)
// If DT is LOW when CLK falls, direction is counter-clockwise (-1)
if (digitalRead(ENC_DT) == HIGH) {
encoderCount++;
} else {
encoderCount--;
}
encoderChanged = true;
}
// =============================================================================
// SETUP
// =============================================================================
void setup() {
// --- FastLED init ---
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
FastLED.clear();
FastLED.show();
// --- Encoder pins ---
pinMode(ENC_CLK, INPUT_PULLUP);
pinMode(ENC_DT, INPUT_PULLUP);
pinMode(ENC_SW, INPUT_PULLUP); // Active LOW (pressed = LOW)
// Attach interrupt to CLK pin, trigger on FALLING edge
attachInterrupt(digitalPinToInterrupt(ENC_CLK), encoderISR, FALLING);
// --- Mode switch pins (4 pins, one per mode, active LOW) ---
pinMode(MODE_PIN_0, INPUT_PULLUP);
pinMode(MODE_PIN_1, INPUT_PULLUP);
pinMode(MODE_PIN_2, INPUT_PULLUP);
pinMode(MODE_PIN_3, INPUT_PULLUP);
// --- Initialize all nodes as active ---
for (int i = 0; i < NUM_NODES; i++) {
nodeActive[i] = true;
}
// Seed the distance map as uncomputed
for (int i = 0; i < NUM_NODES; i++) {
distanceMap[i] = -1;
isBridgeNode[i] = false;
cliqueId[i] = -1;
}
Serial.begin(9600); // Optional: useful for debugging via Serial Monitor
}
// =============================================================================
// MAIN LOOP
// =============================================================================
void loop() {
// 1. Read mode selector switch
Mode newMode = readModeSwitch();
if (newMode != currentMode) {
currentMode = newMode;
graphDirty = true; // Switching mode may need a recompute
}
// 2. Handle encoder rotation (only meaningful in edit mode for node selection)
if (encoderChanged && currentMode == MODE_EDIT) {
noInterrupts();
int delta = encoderCount;
encoderCount = 0;
encoderChanged = false;
interrupts();
// Move selected node, wrapping around
selectedNode = (selectedNode + delta + NUM_NODES) % NUM_NODES;
graphDirty = true;
} else if (encoderChanged) {
// Consume encoder movement silently in non-edit modes
noInterrupts();
encoderCount = 0;
encoderChanged = false;
interrupts();
}
// 3. Handle encoder button press — toggle active/inactive in edit mode
if (currentMode == MODE_EDIT) {
if (digitalRead(ENC_SW) == LOW) { // Button pressed (active LOW)
unsigned long now = millis();
if (now - lastButtonPress > DEBOUNCE_MS) {
lastButtonPress = now;
nodeActive[selectedNode] = !nodeActive[selectedNode];
graphDirty = true;
Serial.print("Node ");
Serial.print(selectedNode);
Serial.println(nodeActive[selectedNode] ? " activated" : " deactivated");
}
}
}
// 4. Recompute graph analytics if the topology or selection changed
if (graphDirty) {
computeAll();
graphDirty = false;
}
// 5. Render LEDs based on current mode
renderLEDs();
FastLED.show();
delay(20); // ~50 FPS, enough for smooth encoder response
}
// =============================================================================
// MODE SWITCH READER
// =============================================================================
// Reads which of the four mode pins is pulled LOW.
// Returns the corresponding Mode enum value.
Mode readModeSwitch() {
if (digitalRead(MODE_PIN_0) == LOW) return MODE_EDIT;
if (digitalRead(MODE_PIN_1) == LOW) return MODE_CLIQUES;
if (digitalRead(MODE_PIN_2) == LOW) return MODE_BRIDGES;
if (digitalRead(MODE_PIN_3) == LOW) return MODE_DISTANCE;
return currentMode; // No pin LOW — keep current mode (handles floating/transition)
}
// =============================================================================
// GRAPH COMPUTATION
// =============================================================================
// All graph algorithms operate only on ACTIVE nodes.
// An inactive node is treated as if it and all its edges have been removed.
// -----------------------------------------------------------------------------
// computeAll() — dispatch all needed analytics
// -----------------------------------------------------------------------------
void computeAll() {
computeDistances(); // BFS from selectedNode
computeBridges(); // Tarjan's articulation point algorithm
computeCliques(); // Bron-Kerbosch maximal clique finder
}
// -----------------------------------------------------------------------------
// computeDistances()
// BFS from selectedNode through active nodes only.
// Populates distanceMap[]: -1 = unreachable or inactive, >=0 = hop count.
// -----------------------------------------------------------------------------
void computeDistances() {
for (int i = 0; i < NUM_NODES; i++) distanceMap[i] = -1;
// If the selected node itself is inactive, nothing to compute
if (!nodeActive[selectedNode]) return;
// BFS queue — a simple array used as a circular buffer is overkill here;
// since NUM_NODES is small, a linear queue array is fine.
int queue[NUM_NODES];
int head = 0, tail = 0;
distanceMap[selectedNode] = 0;
queue[tail++] = selectedNode;
while (head < tail) {
int current = queue[head++];
for (int neighbor = 0; neighbor < NUM_NODES; neighbor++) {
// Skip if no edge, already visited, or neighbor is inactive
if (NETWORK[current][neighbor] == 0) continue;
if (!nodeActive[neighbor]) continue;
if (distanceMap[neighbor] != -1) continue;
distanceMap[neighbor] = distanceMap[current] + 1;
queue[tail++] = neighbor;
}
}
}
// -----------------------------------------------------------------------------
// computeBridges()
// Finds articulation points (bridge nodes) using Tarjan's DFS algorithm.
// An articulation point is a node whose removal disconnects the active graph.
// Populates isBridgeNode[].
// -----------------------------------------------------------------------------
// Persistent arrays for Tarjan — declared outside to avoid stack overflow
// on Arduino's small stack.
int disc[NUM_NODES]; // Discovery time in DFS
int low[NUM_NODES]; // Lowest discovery time reachable
int parent[NUM_NODES]; // Parent in DFS tree
bool visited[NUM_NODES]; // Visited flag for DFS
int dfsTimer = 0; // Incremented at each DFS visit
// Recursive DFS for Tarjan's articulation point algorithm.
// node — current node being visited
// active[] — which nodes are part of the active subgraph
void tarjanDFS(int node) {
visited[node] = true;
disc[node] = low[node] = dfsTimer++;
int childCount = 0; // Number of children in DFS tree (used for root check)
for (int neighbor = 0; neighbor < NUM_NODES; neighbor++) {
if (NETWORK[node][neighbor] == 0) continue; // No edge
if (!nodeActive[neighbor]) continue; // Neighbor is inactive
if (!visited[neighbor]) {
childCount++;
parent[neighbor] = node;
tarjanDFS(neighbor); // Recurse
// Update low value: can we reach higher up the tree via this subtree?
low[node] = min(low[node], low[neighbor]);
// Articulation point condition 1: root of DFS tree with >=2 children
if (parent[node] == -1 && childCount > 1) {
isBridgeNode[node] = true;
}
// Articulation point condition 2: non-root node where no back-edge
// from the subtree of 'neighbor' reaches an ancestor of 'node'
if (parent[node] != -1 && low[neighbor] >= disc[node]) {
isBridgeNode[node] = true;
}
} else if (neighbor != parent[node]) {
// Back edge: update low value with ancestor's discovery time
low[node] = min(low[node], disc[neighbor]);
}
}
}
void computeBridges() {
// Reset all state
for (int i = 0; i < NUM_NODES; i++) {
isBridgeNode[i] = false;
visited[i] = false;
parent[i] = -1;
disc[i] = 0;
low[i] = 0;
}
dfsTimer = 0;
// Run DFS from every unvisited active node (handles disconnected components)
for (int i = 0; i < NUM_NODES; i++) {
if (nodeActive[i] && !visited[i]) {
tarjanDFS(i);
}
}
}
// -----------------------------------------------------------------------------
// computeCliques()
// Finds maximal cliques using the Bron-Kerbosch algorithm with pivoting.
// Assigns each active node a clique ID from the FIRST maximal clique it appears in.
//
// Note: For very dense graphs with many cliques this can be slow. For a
// 50-node sparse graph with small clusters it runs comfortably in real time.
// -----------------------------------------------------------------------------
// Bron-Kerbosch working sets — kept global to avoid deep stack usage.
// Each is a bitmask represented as a 64-bit integer (supports up to 64 nodes).
// For >64 nodes you would need a different representation.
#define NODE_MASK uint64_t // Bitmask type: 1 bit per node
int nextCliqueId = 0; // Counter to assign unique IDs to cliques
int cliqueSizeMin = 3; // Ignore cliques smaller than this (just edges/singletons)
// Helper: set bit i in mask
inline NODE_MASK setBit(NODE_MASK mask, int i) { return mask | ((NODE_MASK)1 << i); }
// Helper: clear bit i in mask
inline NODE_MASK clearBit(NODE_MASK mask, int i) { return mask & ~((NODE_MASK)1 << i); }
// Helper: test bit i in mask
inline bool testBit(NODE_MASK mask, int i) { return (mask >> i) & 1; }
// Build adjacency bitmask for a given node (active neighbors only)
NODE_MASK neighborMask(int node) {
NODE_MASK mask = 0;
for (int j = 0; j < NUM_NODES; j++) {
if (NETWORK[node][j] && nodeActive[j]) {
mask = setBit(mask, j);
}
}
return mask;
}
// Bron-Kerbosch recursive function.
// R = current clique being built (bitmask)
// P = candidates that can extend R (bitmask)
// X = nodes already processed (bitmask)
void bronKerbosch(NODE_MASK R, NODE_MASK P, NODE_MASK X) {
if (P == 0 && X == 0) {
// R is a maximal clique — count its size
int size = __builtin_popcountll(R); // popcount = number of set bits
if (size >= cliqueSizeMin) {
// Assign this clique's ID to all members not yet in a clique
for (int i = 0; i < NUM_NODES; i++) {
if (testBit(R, i) && cliqueId[i] == -1) {
cliqueId[i] = nextCliqueId % MAX_CLIQUE_COLORS;
}
}
nextCliqueId++;
}
return;
}
// Choose pivot: the vertex in P ∪ X with the most neighbors in P
// (this pruning reduces the number of recursive calls significantly)
NODE_MASK PX = P | X;
int pivot = -1;
int maxN = -1;
for (int u = 0; u < NUM_NODES; u++) {
if (!testBit(PX, u)) continue;
int cnt = __builtin_popcountll(neighborMask(u) & P);
if (cnt > maxN) {
maxN = cnt;
pivot = u;
}
}
// Iterate over P \ N(pivot)
NODE_MASK candidates = P & ~neighborMask(pivot);
for (int v = 0; v < NUM_NODES; v++) {
if (!testBit(candidates, v)) continue;
NODE_MASK Nv = neighborMask(v);
bronKerbosch(setBit(R, v), P & Nv, X & Nv);
P = clearBit(P, v);
X = setBit(X, v);
}
}
void computeCliques() {
// Reset clique assignments
for (int i = 0; i < NUM_NODES; i++) cliqueId[i] = -1;
nextCliqueId = 0;
// Build initial P = all active nodes, R = empty, X = empty
NODE_MASK P = 0;
for (int i = 0; i < NUM_NODES; i++) {
if (nodeActive[i]) P = setBit(P, i);
}
bronKerbosch(0, P, 0);
}
// =============================================================================
// LED RENDERING
// =============================================================================
// Each mode maps computed graph data to LED colors.
void renderLEDs() {
switch (currentMode) {
case MODE_EDIT: renderEdit(); break;
case MODE_CLIQUES: renderCliques(); break;
case MODE_BRIDGES: renderBridges(); break;
case MODE_DISTANCE: renderDistance(); break;
}
}
// -----------------------------------------------------------------------------
// Edit mode:
// - Inactive nodes → dim/off (COLOR_INACTIVE)
// - Active nodes → dim blue
// - Selected node → bright cyan (pulses to draw attention)
// -----------------------------------------------------------------------------
void renderEdit() {
// Slow pulse for selected node: uses millis() to create a sine-wave brightness
uint8_t pulse = (uint8_t)(128 + 127 * sin(millis() / 300.0));
for (int i = 0; i < NUM_NODES; i++) {
if (!nodeActive[i]) {
leds[i] = COLOR_INACTIVE;
} else if (i == selectedNode) {
// Blend cyan with pulse brightness
leds[i] = CRGB(0, (uint8_t)(200 * pulse / 255), 255);
} else {
leds[i] = CRGB(0, 30, 80); // Dim blue for other active nodes
}
}
}
// -----------------------------------------------------------------------------
// Distance mode:
// - Inactive nodes → COLOR_INACTIVE
// - Active unreachable nodes → COLOR_UNREACHABLE
// - Active nodes → DIST_COLORS[hop % MAX_DIST_COLORS]
// -----------------------------------------------------------------------------
void renderDistance() {
for (int i = 0; i < NUM_NODES; i++) {
if (!nodeActive[i]) {
leds[i] = COLOR_INACTIVE;
} else if (distanceMap[i] < 0) {
leds[i] = COLOR_UNREACHABLE;
} else {
int hop = distanceMap[i] % MAX_DIST_COLORS;
leds[i] = DIST_COLORS[hop];
}
}
}
// -----------------------------------------------------------------------------
// Cliques mode:
// - Inactive nodes → COLOR_INACTIVE
// - Active nodes in a clique → CLIQUE_COLORS[cliqueId[i]]
// - Active nodes in no tracked clique → dim white
// -----------------------------------------------------------------------------
void renderCliques() {
for (int i = 0; i < NUM_NODES; i++) {
if (!nodeActive[i]) {
leds[i] = COLOR_INACTIVE;
} else if (cliqueId[i] >= 0) {
leds[i] = CLIQUE_COLORS[cliqueId[i] % MAX_CLIQUE_COLORS];
} else {
leds[i] = CRGB(30, 30, 30); // Dim white for nodes not in any tracked clique
}
}
}
// -----------------------------------------------------------------------------
// Bridges mode:
// - Inactive nodes → COLOR_INACTIVE
// - Bridge/articulation → COLOR_BRIDGE (orange-red)
// - Normal active nodes → COLOR_NORMAL_NODE (dim blue)
// -----------------------------------------------------------------------------
void renderBridges() {
for (int i = 0; i < NUM_NODES; i++) {
if (!nodeActive[i]) {
leds[i] = COLOR_INACTIVE;
} else if (isBridgeNode[i]) {
leds[i] = COLOR_BRIDGE;
} else {
leds[i] = COLOR_NORMAL_NODE;
}
}
}
Loading
esp32-devkit-c-v4
esp32-devkit-c-v4