├── __init__.py ├── .gitignore ├── requirements.txt ├── demo.gif ├── pcb.zip ├── tlc5916_tester.py ├── tlc5916_driver.py ├── README.md ├── main.py └── wifiSniffer └── WifiSniffer.ino /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | __pycache__ 3 | *.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyserial==3.5 2 | RPi.GPIO==0.7.1 3 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexchang0229/network-activity-visualizer/HEAD/demo.gif -------------------------------------------------------------------------------- /pcb.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexchang0229/network-activity-visualizer/HEAD/pcb.zip -------------------------------------------------------------------------------- /tlc5916_tester.py: -------------------------------------------------------------------------------- 1 | import time 2 | from tlc5916_driver import LEDController 3 | 4 | data_out_pin = 11 5 | clock_pin = 12 6 | latch_pin = 13 7 | oe_pin = 15 8 | num_leds = 40 9 | pwm_freq = 200 10 | pwm_load = 100 11 | 12 | tlc5916_driver = LEDController( 13 | data_out_pin, 14 | clock_pin, 15 | latch_pin, 16 | oe_pin, 17 | num_leds, 18 | pwm_freq, 19 | pwm_load, 20 | ) 21 | 22 | try: 23 | while True: 24 | for lednum in range(num_leds): 25 | # loops through all 40 leds and turns them on individually for a second 26 | tlc5916_driver.change_state_led(lednum) 27 | time.sleep(0.1) 28 | tlc5916_driver.change_state_led(lednum) 29 | except KeyboardInterrupt: 30 | tlc5916_driver.clear_leds() 31 | -------------------------------------------------------------------------------- /tlc5916_driver.py: -------------------------------------------------------------------------------- 1 | import RPi.GPIO as GPIO 2 | 3 | 4 | class LEDController: 5 | def __init__( 6 | self, 7 | data_out_pin=11, 8 | clock_pin=12, 9 | latch_pin=13, 10 | oe_pin=15, 11 | num_leds=40, 12 | pwm_freq=200, 13 | pwm_load=100, 14 | ): 15 | self.DataOutPin = data_out_pin 16 | self.ClockPin = clock_pin 17 | self.LatchPin = latch_pin 18 | self.OEPin = oe_pin 19 | self.num_leds = num_leds 20 | self.pwm_freq = pwm_freq 21 | self.pwm_load = pwm_load 22 | self.led_array = [0] * self.num_leds 23 | self._setup_gpio() 24 | self.pwm = GPIO.PWM(oe_pin, pwm_freq) 25 | self.pwm.start(self.pwm_load) 26 | self.set_pwm(100) 27 | self.clear_leds() 28 | 29 | def _setup_gpio(self): 30 | GPIO.setwarnings(False) 31 | GPIO.setmode(GPIO.BOARD) 32 | GPIO.setup(self.OEPin, GPIO.OUT) 33 | GPIO.setup(self.LatchPin, GPIO.OUT) 34 | GPIO.setup(self.ClockPin, GPIO.OUT) 35 | GPIO.setup(self.DataOutPin, GPIO.OUT) 36 | GPIO.output(self.OEPin, True) 37 | GPIO.output(self.LatchPin, False) 38 | GPIO.output(self.ClockPin, False) 39 | 40 | def clear_leds(self): 41 | self.led_array = [0] * self.num_leds 42 | for _ in self.led_array: 43 | GPIO.output(self.ClockPin, False) 44 | GPIO.output(self.DataOutPin, False) 45 | GPIO.output(self.ClockPin, True) 46 | GPIO.output(self.LatchPin, True) 47 | GPIO.output(self.LatchPin, False) 48 | 49 | def set_pwm(self, new_pwm): 50 | self.pwm.ChangeDutyCycle(self.pwm_load - new_pwm) 51 | 52 | def change_state_led(self, led_num): 53 | if 0 <= led_num < self.num_leds: 54 | self.led_array[led_num] = ~self.led_array[led_num] 55 | for state in self.led_array: 56 | GPIO.output(self.ClockPin, False) 57 | GPIO.output(self.DataOutPin, bool(state)) 58 | GPIO.output(self.ClockPin, True) 59 | GPIO.output(self.LatchPin, True) 60 | GPIO.output(self.LatchPin, False) 61 | else: 62 | raise ValueError("LED number out of range.") 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Network activity visualizer 2 | This project is a 3D printed [geodesic dome](https://en.wikipedia.org/wiki/Geodesic_dome) hooked up with flashing LEDs and fiber optic cables to visualize the nearby network activity, it looks pretty cool in the dark. 3 | 4 | ![image](./demo.gif) 5 | 6 | I used an ESP-32 in promiscuous mode to sniff WiFi packets and get their MAC addresses, and 40 individually addressable LEDs that flash when a packet is detected. 7 | ## How 8 | To assemble the geodesic sphere, I printed 30 x hexagonal joints and 12 x pentagonal joints. They are joined with 3/16" wooden dowels, cut 60 dowels 155 mm long and 60 dowels 164 mm long. 9 | ![image](https://github.com/user-attachments/assets/cbb0a9e2-af94-4c25-a8a7-b5604b92b205) 10 | 11 | The 3D files can be found on my thingiverse [here](https://www.thingiverse.com/thing:6896625). I used ~75 m of [1.5 mm side glow fiber optic cable](https://www.aliexpress.com/item/32807597828.html?spm=a2g0o.order_list.order_list_main.36.3d891802ASmCDw), each fiber optic cable is connected to the PCB holder, onto each LED, then routed around the 3D printed joints to create a web pattern. The fiber optics are held in place with crazy glue. 12 | 13 | 14 | The KiCad PCB schematics can be found in `pcb.zip`, I had it made with JLC PCB. 15 | pcb 16 | 17 | On the board I soldered 40 x SMD5730 white LEDs, these are controlled by 5 x TLC5916 LED sink drivers. The TLC5916s are controlled by a Raspberry Pi. 18 | 19 | The Pi and the TLC5916s are powered by a 5V wall power adapter, that power is also used by a buck converter to bring it down to 3.2 V for the LEDs. 20 | 21 | The code for the ESP-32 can be found in `./wifiSniffer/WifiSniffer.ino`, the code is from [ESP-EOS/ESP32-WiFi-Sniffer](https://github.com/ESP-EOS/ESP32-WiFi-Sniffer). The ESP-32 is connected to the Raspberry Pi by USB, it prints detected WiFi packets to serial, and the Python script on the Raspberry Pi reads it. 22 | 23 | After connecting the ESP-32 to the Pi and running `main.py` the intercepted packets will print in the terminal 24 | ![image](https://github.com/user-attachments/assets/e69a98d0-4dc8-40d5-92c0-8eb889e31741) 25 | 26 | And that's it, enjoy the light show! 27 | 28 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import serial 2 | from tlc5916_driver import LEDController 3 | import random 4 | from datetime import datetime 5 | import asyncio 6 | 7 | ser = serial.Serial("/dev/ttyUSB0", 115200, timeout=None) 8 | ser.reset_input_buffer() 9 | data_out_pin = 11 10 | clock_pin = 12 11 | latch_pin = 13 12 | oe_pin = 15 13 | num_leds = 40 14 | pwm_freq = 200 15 | pwm_load = 100 16 | 17 | macArray = [ 18 | {"Last_seen": datetime.now().timestamp(), "MAC": "", "LED_num": ind} 19 | for ind in range(num_leds) 20 | ] 21 | 22 | tlc5916_driver = LEDController( 23 | data_out_pin, 24 | clock_pin, 25 | latch_pin, 26 | oe_pin, 27 | num_leds, 28 | pwm_freq, 29 | pwm_load, 30 | ) 31 | 32 | DEBUG = True 33 | 34 | 35 | def log(s): 36 | if DEBUG: 37 | print(s) 38 | 39 | 40 | async def blinkLed(lednum): 41 | tlc5916_driver.change_state_led(lednum) 42 | await asyncio.sleep(3) 43 | tlc5916_driver.change_state_led(lednum) 44 | return 45 | 46 | 47 | async def handleNewMAC(macIn): 48 | if not any(x["MAC"] == "" for x in macArray): 49 | log(f"""No room for MAC: {macIn}""") 50 | # There is no more room 51 | return 52 | else: 53 | log(f"""Saving new MAC: {macIn}""") 54 | # Get free LEDs 55 | freeLeds = [x for x in macArray if x["MAC"] == ""] 56 | # Assign LED randomly 57 | randInd = random.randint(0, len(freeLeds) - 1) 58 | ledToAssign = freeLeds[randInd]["LED_num"] 59 | macArray[ledToAssign] = { 60 | "Last_seen": datetime.now().timestamp(), 61 | "MAC": macIn, 62 | "LED_num": ledToAssign, 63 | } 64 | await blinkLed(ledToAssign) 65 | return 66 | 67 | 68 | async def handleSeenBefore(macIn): 69 | freeLeds = [x for x in macArray if x["MAC"] == ""] 70 | log(f"free leds: {len(freeLeds)}") 71 | 72 | MACobj = list(filter(lambda x: x["MAC"] == macIn, macArray))[0] 73 | log(f"Seen before MAC: {macIn}") 74 | await blinkLed(MACobj["LED_num"]) 75 | return 76 | 77 | 78 | async def handleRemoveStaleMAC(): 79 | for ind, MACobj in enumerate(macArray): 80 | if ( 81 | datetime.now().timestamp() - MACobj["Last_seen"] > 60 82 | and MACobj["MAC"] != "" 83 | ): 84 | log(f"""removing old MAC: {MACobj["MAC"]}""") 85 | macArray[ind] = { 86 | "Last_seen": datetime.now().timestamp(), 87 | "MAC": "", 88 | "LED_num": ind, 89 | } 90 | 91 | 92 | async def main(): 93 | while True: 94 | line = ser.read_until() 95 | line1 = line.decode("utf-8").strip("\r\n").split(",") 96 | 97 | # Uncomment this if statment to skip over management packets 98 | # if "MGMT" in line1[0]: 99 | # continue 100 | 101 | log(line1) 102 | try: 103 | macIn = line1[-2].split("=")[1] 104 | except: 105 | continue 106 | """ 107 | If haven't seen this MAC before: 108 | There is no more room: 109 | continue 110 | There is more room: 111 | assign LED number 112 | update last seen 113 | Blink LED 114 | 115 | If seen before, update last seen: 116 | Blink LED 117 | 118 | For every assigned LED: 119 | If last seen > 1 min: 120 | unassign LED 121 | """ 122 | if not any(x["MAC"] == macIn for x in macArray): 123 | asyncio.create_task(handleNewMAC(macIn)) 124 | else: 125 | asyncio.create_task(handleSeenBefore(macIn)) 126 | 127 | asyncio.create_task(handleRemoveStaleMAC()) 128 | await asyncio.sleep(0.05) 129 | 130 | 131 | try: 132 | asyncio.run(main()) 133 | except KeyboardInterrupt: 134 | pass 135 | finally: 136 | tlc5916_driver.clear_leds() 137 | -------------------------------------------------------------------------------- /wifiSniffer/WifiSniffer.ino: -------------------------------------------------------------------------------- 1 | #include "freertos/FreeRTOS.h" 2 | #include "esp_wifi.h" 3 | #include "esp_wifi_types.h" 4 | #include "esp_system.h" 5 | #include "esp_event.h" 6 | #include "esp_event_loop.h" 7 | #include "nvs_flash.h" 8 | #include "driver/gpio.h" 9 | 10 | #define LED_GPIO_PIN 5 11 | #define WIFI_CHANNEL_SWITCH_INTERVAL (100) 12 | #define WIFI_CHANNEL_MAX (13) 13 | 14 | uint8_t level = 0, channel = 1; 15 | 16 | static wifi_country_t wifi_country = {.cc="CN", .schan = 1, .nchan = 13}; //Most recent esp32 library struct 17 | 18 | typedef struct { 19 | unsigned frame_ctrl:16; 20 | unsigned duration_id:16; 21 | uint8_t addr1[6]; /* receiver address */ 22 | uint8_t addr2[6]; /* sender address */ 23 | uint8_t addr3[6]; /* filtering address */ 24 | unsigned sequence_ctrl:16; 25 | uint8_t addr4[6]; /* optional */ 26 | } wifi_ieee80211_mac_hdr_t; 27 | 28 | typedef struct { 29 | wifi_ieee80211_mac_hdr_t hdr; 30 | uint8_t payload[0]; /* network data ended with 4 bytes csum (CRC32) */ 31 | } wifi_ieee80211_packet_t; 32 | 33 | static esp_err_t event_handler(void *ctx, system_event_t *event); 34 | static void wifi_sniffer_init(void); 35 | static void wifi_sniffer_set_channel(uint8_t channel); 36 | static const char *wifi_sniffer_packet_type2str(wifi_promiscuous_pkt_type_t type); 37 | static void wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type); 38 | 39 | esp_err_t event_handler(void *ctx, system_event_t *event) 40 | { 41 | return ESP_OK; 42 | } 43 | 44 | void wifi_sniffer_init(void) 45 | { 46 | nvs_flash_init(); 47 | tcpip_adapter_init(); 48 | ESP_ERROR_CHECK( esp_event_loop_init(event_handler, NULL) ); 49 | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 50 | ESP_ERROR_CHECK( esp_wifi_init(&cfg) ); 51 | ESP_ERROR_CHECK( esp_wifi_set_country(&wifi_country) ); /* set country for channel range [1, 13] */ 52 | ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) ); 53 | ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_NULL) ); 54 | ESP_ERROR_CHECK( esp_wifi_start() ); 55 | esp_wifi_set_promiscuous(true); 56 | esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); 57 | } 58 | 59 | void wifi_sniffer_set_channel(uint8_t channel) 60 | { 61 | esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); 62 | } 63 | 64 | const char * wifi_sniffer_packet_type2str(wifi_promiscuous_pkt_type_t type) 65 | { 66 | switch(type) { 67 | case WIFI_PKT_MGMT: return "MGMT"; 68 | case WIFI_PKT_DATA: return "DATA"; 69 | default: 70 | case WIFI_PKT_MISC: return "MISC"; 71 | } 72 | } 73 | 74 | void wifi_sniffer_packet_handler(void* buff, wifi_promiscuous_pkt_type_t type) 75 | { 76 | // if (type != WIFI_PKT_MGMT) 77 | // return; 78 | 79 | const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff; 80 | const wifi_ieee80211_packet_t *ipkt = (wifi_ieee80211_packet_t *)ppkt->payload; 81 | const wifi_ieee80211_mac_hdr_t *hdr = &ipkt->hdr; 82 | 83 | printf("PACKET TYPE=%s, CHAN=%02d, RSSI=%02d," 84 | " ADDR1=%02x:%02x:%02x:%02x:%02x:%02x," 85 | " ADDR2=%02x:%02x:%02x:%02x:%02x:%02x," 86 | " ADDR3=%02x:%02x:%02x:%02x:%02x:%02x\n", 87 | wifi_sniffer_packet_type2str(type), 88 | ppkt->rx_ctrl.channel, 89 | ppkt->rx_ctrl.rssi, 90 | /* ADDR1 */ 91 | hdr->addr1[0],hdr->addr1[1],hdr->addr1[2], 92 | hdr->addr1[3],hdr->addr1[4],hdr->addr1[5], 93 | /* ADDR2 */ 94 | hdr->addr2[0],hdr->addr2[1],hdr->addr2[2], 95 | hdr->addr2[3],hdr->addr2[4],hdr->addr2[5], 96 | /* ADDR3 */ 97 | hdr->addr3[0],hdr->addr3[1],hdr->addr3[2], 98 | hdr->addr3[3],hdr->addr3[4],hdr->addr3[5] 99 | ); 100 | } 101 | 102 | // the setup function runs once when you press reset or power the board 103 | void setup() { 104 | // initialize digital pin 5 as an output. 105 | Serial.begin(115200); 106 | delay(10); 107 | wifi_sniffer_init(); 108 | } 109 | 110 | // the loop function runs over and over again forever 111 | void loop() { 112 | //Serial.print("inside loop"); 113 | delay(20); // wait for a second 114 | 115 | vTaskDelay(WIFI_CHANNEL_SWITCH_INTERVAL / portTICK_PERIOD_MS); 116 | wifi_sniffer_set_channel(channel); 117 | channel = (channel % WIFI_CHANNEL_MAX) + 1; 118 | } 119 | --------------------------------------------------------------------------------