This shows you the differences between two versions of the page.
pm:prj2025:avaduva:vlad_andrei.chira [2025/05/24 20:04] vlad_andrei.chira [Hardware Design] |
pm:prj2025:avaduva:vlad_andrei.chira [2025/05/24 21:37] (current) vlad_andrei.chira [Hardware Design] |
||
---|---|---|---|
Line 4: | Line 4: | ||
This project is a handheld Wi-Fi analyzer based on an ESP32 microcontroller. It scans nearby Wi-Fi networks, displays their signal strength, channel usage, and other technical details on a built-in screen. Users can interact with the device using physical controls to navigate and inspect network data. | This project is a handheld Wi-Fi analyzer based on an ESP32 microcontroller. It scans nearby Wi-Fi networks, displays their signal strength, channel usage, and other technical details on a built-in screen. Users can interact with the device using physical controls to navigate and inspect network data. | ||
- | It's purpose is to be a portable tool for users to understand the wireless environment: troubleshoot issues, find optimal channels, detect signal strength, monitor traffic. | + | Its purpose is to be a portable tool for users to understand the wireless environment: troubleshoot issues, find optimal channels, detect signal strength, monitor traffic. |
===== General description ===== | ===== General description ===== | ||
Line 10: | Line 10: | ||
The main component is the ESP32 microcontroller development board with built-in Wifi. The fast processor, low battery use and number of GPIOs make it a good choice for this project. | The main component is the ESP32 microcontroller development board with built-in Wifi. The fast processor, low battery use and number of GPIOs make it a good choice for this project. | ||
- | To show information to the user, this project uses a 3.5" TFT LCD display based on the ILI9481 driver. It has a resolution of 480x320 full color pixels and uses 16bit parallel interface. The large size and decent resolution makes it a great for display text and graphics. | + | To show information to the user, this project uses a 2.8" TFT SPI LCD display based on the ST7789 driver. It has a resolution of 480x320 full color pixels. The decent size and resolution makes it a great for display text and graphics. |
For user input, I chose a rotary encoder module along with 2 buttons. The encoder makes it faster to move around menus than traditional buttons (scrolling, for example), and thus the 2 buttons I use are for "enter" and "back". | For user input, I chose a rotary encoder module along with 2 buttons. The encoder makes it faster to move around menus than traditional buttons (scrolling, for example), and thus the 2 buttons I use are for "enter" and "back". | ||
- | Since this is a handheld device, it uses a battery along with a charging module. The battery is a 2500mAh 3.7V LiPo battery connected to a TP4056 charging module with protection. Since the 3.7V are not enough to power the ESP32 and display, I use a 5V 1A boost converter to raise the voltage. USB power is also an alternative to the battery. | + | Since this is a handheld device, it uses a battery along with a charging module. The battery is a 750mAh 3.7V LiPo battery connected to a TP4056 charging module with protection. Since the 3.7V are not enough to power the ESP32 and display, I use a 5V 1A boost converter to raise the voltage. USB power is also an alternative to the battery. |
{{:pm:prj2025:avaduva:overview_vlad_andrei-chira.png?400|}} | {{:pm:prj2025:avaduva:overview_vlad_andrei-chira.png?400|}} | ||
Line 34: | Line 34: | ||
{{:pm:prj2025:avaduva:schematic1.png?800|}} | {{:pm:prj2025:avaduva:schematic1.png?800|}} | ||
+ | |||
+ | As seen above in the schematic, here are the pins I used | ||
+ | For the display: | ||
+ | * Vcc --> 3.3V | ||
+ | * Gnd --> Gnd | ||
+ | * LED --> 3.3V | ||
+ | * MOSI --> Port 23 | ||
+ | * SCK --> Port 18 | ||
+ | * CS --> Port 15 | ||
+ | * RESET --> Port 4 | ||
+ | * DC --> Port 2 | ||
+ | The pin mapping does not really matter, as the mapping can be configured in the library used to control the display. | ||
+ | |||
+ | For the encoder: | ||
+ | * CLK --> Port 32 | ||
+ | * DT --> Port 35 | ||
+ | * + --> 3.3V | ||
+ | * Gnd --> Gnd | ||
+ | |||
+ | For the pushbutton: one side --> Gnd, other side --> Port 33 | ||
+ | |||
+ | The battery positive line goes into a switch that disables the battery when using the USB. Then it goes into the TP4056 charging module, who's output is then stepped up to 5V by the miniature boost converter. | ||
===== Software Design ===== | ===== Software Design ===== | ||
+ | This project is more focused on software, so there will be a lot of code. Here are the features that must be implemented: | ||
+ | * Scan WiFi networks, store and display their information: SSID, channel, signal strength, authentication protocol, hidden SSID etc. | ||
+ | * Graph channel occupancy: bar chart of how many networks are per channel | ||
+ | * Signal strength graph: display signal strength over time for a particular network | ||
+ | * MAC sniffing: use Promiscuous Mode to capture WiFi Management Frames by channel hopping, record the MAC addresses of all devices around (education purposes only) | ||
+ | * <del>Deauth attack: Perform deauthentication attacks on nearby devices (own devices only)</del> Due to a limitation of ESP32 firmaware, the ESP32 cannot send deauthentication management frames. Instead, this will be a //monitoring// for deauthentication attacks only feature. | ||
- | <note tip> | + | To access all of these features, a Main Menu will be provided on the display in the form of a 3x2 grid. Each cell is a selectable button and will lead the user to a new window with the particular feature. |
- | Descrierea codului aplicaţiei (firmware): | + | |
- | * mediu de dezvoltare (if any) (e.g. AVR Studio, CodeVisionAVR) | + | |
- | * librării şi surse 3rd-party (e.g. Procyon AVRlib) | + | |
- | * algoritmi şi structuri pe care plănuiţi să le implementaţi | + | |
- | * (etapa 3) surse şi funcţii implementate | + | |
- | </note> | + | |
- | ===== Rezultate Obţinute ===== | + | To organize the code, the state of the application will be modeled using a Finite State Machine. For each state, there will be different elements drawn to the screen as well as conditions for transitioning into different states (for example, go back to Main Menu). |
- | <note tip> | + | For the code, I opted for Arduino IDE. |
- | Care au fost rezultatele obţinute în urma realizării proiectului vostru. | + | External libraries used : |
- | </note> | + | * TFT_eSPI by Bodmer |
+ | * <del>ImGui</del> | ||
+ | I really wanted to port ImGui library to embedded, because it is an amazing library and fantastic for this UI use case. It is hardware and OS agnostic, it just requires a way to draw triangles essentially. That means a software renderer drawing into a framebuffer is absolutely possible. The framebuffer is then passed to the TFT library and blit it to the screen. Alas, this ESP32 does not have external PSRAM, so it's not possible to store the entire framebuffer into memory. You don't need to store the entire framebuffer, especially if you write a wrapper on top of the TFT library and call those functions to draw the primitives. Due to time constraints, I gave up trying to port it. But I may come back one day and finish the port! | ||
+ | |||
+ | **WiFi Controller** | ||
+ | |||
+ | This class is used to scan for WiFi networks. Since the ESP only spends a few milliseconds per channel to capture access points, every scan will be different: some networks will be missing. Which is why this class caches networks and evicts them if they haven't been seen in more than 30 seconds. The cache is a map with bssid as keys and a custom data type made up of <code>MyNetwork</code> and <code>lastSeen</code> as values. | ||
+ | <code cpp> | ||
+ | // 1) Perform active scan (blocking) | ||
+ | int found = WiFi.scanNetworks( | ||
+ | /*async=*/false, | ||
+ | /*hidden=*/showHiddenSSIDs, | ||
+ | /*passive=*/false, | ||
+ | /*max_ms_per_channel=*/scanTimePerChannelMs | ||
+ | ); | ||
+ | </code> | ||
+ | |||
+ | <code cpp> | ||
+ | // write into cache (or update existing) | ||
+ | CacheEntry &entry = networkCache_[net.bssid]; | ||
+ | entry.net = net; | ||
+ | entry.lastSeen = now; | ||
+ | </code> | ||
+ | <code cpp> | ||
+ | // 3) Prune stale entries and build return vector | ||
+ | std::vector<MyNetwork> aggregated; | ||
+ | for (auto it = networkCache_.begin(); it != networkCache_.end();) { | ||
+ | if (now - it->second.lastSeen > aggregationTimeoutMs_) { | ||
+ | // drop old | ||
+ | it = networkCache_.erase(it); | ||
+ | } else { | ||
+ | // include in output | ||
+ | aggregated.push_back(it->second.net); | ||
+ | ++it; | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | ** Sniffer Controller ** | ||
+ | |||
+ | This class handles turning on Promiscuous Mode and sniffing all management frames by channel hopping around. It is responsible both for recording the MAC addresses it finds as well as monitoring for deauth frames. | ||
+ | |||
+ | The way the ESP32 firmware handles this is by registering a callback function that will be called for every captured frame. It only provides the buffer, so I must define a custom data type that defines an IEEE 802.11 header and cast to it: | ||
+ | <code cpp> | ||
+ | typedef struct { | ||
+ | uint16_t frame_ctrl; | ||
+ | uint16_t duration_id; | ||
+ | uint8_t addr1[6]; | ||
+ | uint8_t addr2[6]; | ||
+ | uint8_t addr3[6]; | ||
+ | uint16_t seq_ctrl; | ||
+ | } __attribute__((packed)) ieee80211_hdr_t; | ||
+ | </code> | ||
+ | |||
+ | <code cpp> | ||
+ | |||
+ | esp_wifi_stop(); // tear down any previous mode | ||
+ | delay(100); | ||
+ | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); | ||
+ | esp_wifi_init(&cfg); | ||
+ | esp_wifi_set_storage(WIFI_STORAGE_RAM); | ||
+ | esp_wifi_set_mode(WIFI_MODE_NULL); | ||
+ | esp_wifi_start(); | ||
+ | delay(100); | ||
+ | |||
+ | esp_wifi_set_promiscuous_rx_cb(_promiscCallback); | ||
+ | wifi_promiscuous_filter_t flt = {}; | ||
+ | flt.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT; | ||
+ | esp_wifi_set_promiscuous_filter(&flt); | ||
+ | |||
+ | // Enable promiscuous mode | ||
+ | esp_wifi_set_promiscuous(true); | ||
+ | </code> | ||
+ | |||
+ | I can then retrieve the MAC address and the management frame type like this: | ||
+ | <code cpp> | ||
+ | // In 802.11 header: skip Frame Control (2), Duration (2), Addr1 (6) = offset 10 | ||
+ | const uint8_t* srcMac = pkt->payload + 10; | ||
+ | char macStr[18]; | ||
+ | snprintf(macStr, sizeof(macStr), | ||
+ | "%02X:%02X:%02X:%02X:%02X:%02X", | ||
+ | srcMac[0], srcMac[1], srcMac[2], | ||
+ | srcMac[3], srcMac[4], srcMac[5]); | ||
+ | |||
+ | Serial.printf("MAC: %s RSSI: %d CH: %u\n", | ||
+ | macStr, | ||
+ | pkt->rx_ctrl.rssi, | ||
+ | pkt->rx_ctrl.channel); | ||
+ | </code> | ||
+ | |||
+ | To channel hop, in the loop() function I must run: | ||
+ | <code cpp> | ||
+ | // Channel hop 1–13 every 200 ms to catch all APs | ||
+ | static uint8_t channel = 1; | ||
+ | esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); | ||
+ | channel = (channel % 13) + 1; | ||
+ | delay(200); | ||
+ | </code> | ||
+ | |||
+ | ** UI code ** | ||
+ | |||
+ | The TFT library only provides primitives and text, so drawing graphs means drawing them from scratch. The DisplayManager class handles the UI based on the state of the application (see Finite State Machine approach above). | ||
+ | |||
+ | <code cpp> | ||
+ | void DisplayManager::update(bool dirty) { | ||
+ | if (currentState_ == lastState_ && !dirty) return; | ||
+ | |||
+ | switch (currentState_) { | ||
+ | case State::SplashScreen: drawSplashScreen(); break; | ||
+ | case State::MainMenu: drawMainMenu(); break; | ||
+ | case State::NetworkScan: drawNetworkScan(); break; | ||
+ | case State::ChannelOccupancy: drawChannelOccupancy(); break; | ||
+ | case State::SignalStrength: drawSignalStrength(); break; | ||
+ | case State::MacSniffing: drawMacSniffing(); break; | ||
+ | case State::DeauthMonitoring: drawDeauthMonitoring(); break; | ||
+ | } | ||
+ | lastState_ = currentState_; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | SPI is not fast enough to draw the entire screen continuously without flicker, so only if the state changed or the dirty flag is set manually will (re)rendering occur. Alternatively I can only draw things that will change, such as a graph curve, but keep all labels and axes untouched. | ||
+ | ===== Results ===== | ||
+ | |||
+ | Here are some photos of the UI: | ||
+ | |||
+ | {{:pm:prj2025:avaduva:splash.jpeg?700|}} | ||
+ | |||
+ | {{:pm:prj2025:avaduva:scan_list.jpeg?700|}} | ||
+ | |||
+ | {{:pm:prj2025:avaduva:main_menu.jpeg?700|}} | ||
+ | |||
+ | {{:pm:prj2025:avaduva:channel_occ.jpeg?700|}} | ||
===== Concluzii ===== | ===== Concluzii ===== | ||
Line 61: | Line 214: | ||
</note> | </note> | ||
- | ===== Jurnal ===== | ||
- | |||
- | <note tip> | ||
- | Puteți avea și o secțiune de jurnal în care să poată urmări asistentul de proiect progresul proiectului. | ||
- | </note> | ||
===== Bibliografie/Resurse ===== | ===== Bibliografie/Resurse ===== |