Skip to content

Commit 691dcad

Browse files
committed
feat(probe_monitor): Captures "Probe Request" frames to see what SSIDs nearby devices are searching for
1 parent 5cc666b commit 691dcad

2 files changed

Lines changed: 244 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2025 HIGH CODE LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef PROBE_MONITOR_H
16+
#define PROBE_MONITOR_H
17+
18+
#include <stdint.h>
19+
#include <stdbool.h>
20+
21+
typedef struct {
22+
uint8_t mac[6];
23+
int8_t rssi;
24+
char ssid[33];
25+
uint32_t last_seen_timestamp;
26+
} probe_record_t;
27+
28+
bool probe_monitor_start(void);
29+
void probe_monitor_stop(void);
30+
probe_record_t* probe_monitor_get_results(uint16_t *count);
31+
void probe_monitor_free_results(void);
32+
bool probe_monitor_save_results_to_internal_flash(void);
33+
bool probe_monitor_save_results_to_sd_card(void);
34+
35+
#endif // PROBE_MONITOR_H
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
// Copyright (c) 2025 HIGH CODE LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "probe_monitor.h"
16+
#include "wifi_service.h"
17+
#include "wifi_80211.h"
18+
#include "esp_wifi.h"
19+
#include "esp_log.h"
20+
#include "esp_heap_caps.h"
21+
#include "esp_timer.h"
22+
#include "freertos/FreeRTOS.h"
23+
#include "freertos/task.h"
24+
#include <string.h>
25+
#include "cJSON.h"
26+
#include "storage_write.h"
27+
#include "sd_card_write.h"
28+
#include "sd_card_init.h"
29+
#include "mac_vendor.h"
30+
31+
static const char *TAG = "PROBE_MONITOR";
32+
33+
static TaskHandle_t monitor_task_handle = NULL;
34+
static StackType_t *monitor_task_stack = NULL;
35+
static StaticTask_t *monitor_task_tcb = NULL;
36+
#define MONITOR_STACK_SIZE 4096
37+
38+
static probe_record_t *scan_results = NULL;
39+
static uint16_t scan_count = 0;
40+
static uint16_t max_scan_results = 300;
41+
static bool is_running = false;
42+
43+
static void add_or_update_probe(const uint8_t *mac, const char *ssid, int8_t rssi) {
44+
if (scan_results == NULL) return;
45+
46+
for (int i = 0; i < scan_count; i++) {
47+
if (memcmp(scan_results[i].mac, mac, 6) == 0 && strcmp(scan_results[i].ssid, ssid) == 0) {
48+
scan_results[i].rssi = rssi;
49+
scan_results[i].last_seen_timestamp = (uint32_t)(esp_timer_get_time() / 1000000);
50+
return;
51+
}
52+
}
53+
54+
if (scan_count < max_scan_results) {
55+
memcpy(scan_results[scan_count].mac, mac, 6);
56+
strncpy(scan_results[scan_count].ssid, ssid, 32);
57+
scan_results[scan_count].ssid[32] = '\0';
58+
scan_results[scan_count].rssi = rssi;
59+
scan_results[scan_count].last_seen_timestamp = (uint32_t)(esp_timer_get_time() / 1000000);
60+
scan_count++;
61+
62+
ESP_LOGI(TAG, "New Probe: [%02x:%02x:%02x:%02x:%02x:%02x] searching for '%s' (RSSI: %d)",
63+
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], ssid, rssi);
64+
}
65+
}
66+
67+
static void sniffer_callback(void *buf, wifi_promiscuous_pkt_type_t type) {
68+
if (type != WIFI_PKT_MGMT) return;
69+
70+
const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buf;
71+
const wifi_mac_header_t *mac_header = (const wifi_mac_header_t *)ppkt->payload;
72+
const wifi_frame_control_t *fc = (const wifi_frame_control_t *)&mac_header->frame_control;
73+
74+
if (fc->type == 0 && fc->subtype == 4) {
75+
const uint8_t *payload = ppkt->payload + 24;
76+
int payload_len = ppkt->rx_ctrl.sig_len - 24;
77+
78+
if (payload_len < 2) return;
79+
80+
int offset = 0;
81+
char ssid[33] = {0};
82+
bool ssid_found = false;
83+
84+
while (offset + 2 <= payload_len) {
85+
uint8_t tag = payload[offset];
86+
uint8_t len = payload[offset + 1];
87+
88+
if (offset + 2 + len > payload_len) break;
89+
90+
if (tag == 0) {
91+
int ssid_len = (len > 32) ? 32 : len;
92+
memcpy(ssid, &payload[offset + 2], ssid_len);
93+
ssid[ssid_len] = '\0';
94+
ssid_found = true;
95+
break;
96+
}
97+
offset += 2 + len;
98+
}
99+
100+
if (ssid_found) {
101+
if (strlen(ssid) == 0) {
102+
strcpy(ssid, "<Broadcast>");
103+
}
104+
add_or_update_probe(mac_header->addr2, ssid, ppkt->rx_ctrl.rssi);
105+
}
106+
}
107+
}
108+
109+
static bool save_results_to_path(const char *path, bool use_sd_driver) {
110+
if (scan_results == NULL || scan_count == 0) {
111+
ESP_LOGW(TAG, "No results to save.");
112+
return false;
113+
}
114+
115+
cJSON *root = cJSON_CreateArray();
116+
for (int i = 0; i < scan_count; i++) {
117+
probe_record_t *rec = &scan_results[i];
118+
cJSON *entry = cJSON_CreateObject();
119+
120+
char mac_str[18];
121+
snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x",
122+
rec->mac[0], rec->mac[1], rec->mac[2],
123+
rec->mac[3], rec->mac[4], rec->mac[5]);
124+
cJSON_AddStringToObject(entry, "mac", mac_str);
125+
cJSON_AddStringToObject(entry, "vendor", get_vendor_name(rec->mac));
126+
cJSON_AddStringToObject(entry, "ssid", rec->ssid);
127+
cJSON_AddNumberToObject(entry, "rssi", rec->rssi);
128+
cJSON_AddNumberToObject(entry, "timestamp", rec->last_seen_timestamp);
129+
130+
cJSON_AddItemToArray(root, entry);
131+
}
132+
133+
char *json_string = cJSON_PrintUnformatted(root);
134+
if (json_string == NULL) {
135+
cJSON_Delete(root);
136+
return false;
137+
}
138+
139+
esp_err_t err;
140+
if (use_sd_driver) {
141+
if (!sd_is_mounted()) {
142+
free(json_string);
143+
cJSON_Delete(root);
144+
return false;
145+
}
146+
err = sd_write_string(path, json_string);
147+
} else {
148+
err = storage_write_string(path, json_string);
149+
}
150+
151+
free(json_string);
152+
cJSON_Delete(root);
153+
154+
if (err != ESP_OK) {
155+
ESP_LOGE(TAG, "Failed to save results to %s", path);
156+
return false;
157+
}
158+
159+
ESP_LOGI(TAG, "Results saved to %s", path);
160+
return true;
161+
}
162+
163+
bool probe_monitor_start(void) {
164+
if (is_running) return false;
165+
166+
if (scan_results) heap_caps_free(scan_results);
167+
scan_results = (probe_record_t *)heap_caps_malloc(max_scan_results * sizeof(probe_record_t), MALLOC_CAP_SPIRAM);
168+
if (scan_results == NULL) return false;
169+
170+
scan_count = 0;
171+
is_running = true;
172+
173+
wifi_promiscuous_filter_t filter = { .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT };
174+
wifi_service_promiscuous_start(sniffer_callback, &filter);
175+
wifi_service_start_channel_hopping();
176+
177+
ESP_LOGI(TAG, "Probe Request Monitor started.");
178+
return true;
179+
}
180+
181+
void probe_monitor_stop(void) {
182+
if (!is_running) return;
183+
wifi_service_promiscuous_stop();
184+
wifi_service_stop_channel_hopping();
185+
is_running = false;
186+
ESP_LOGI(TAG, "Probe Request Monitor stopped.");
187+
}
188+
189+
probe_record_t* probe_monitor_get_results(uint16_t *count) {
190+
*count = scan_count;
191+
return scan_results;
192+
}
193+
194+
void probe_monitor_free_results(void) {
195+
probe_monitor_stop();
196+
if (scan_results) {
197+
heap_caps_free(scan_results);
198+
scan_results = NULL;
199+
}
200+
scan_count = 0;
201+
}
202+
203+
bool probe_monitor_save_results_to_internal_flash(void) {
204+
return save_results_to_path("/assets/storage/wifi/probe_monitor.json", false);
205+
}
206+
207+
bool probe_monitor_save_results_to_sd_card(void) {
208+
return save_results_to_path("/probe_monitor.json", true);
209+
}

0 commit comments

Comments
 (0)