From 78dd599277b8e8b2ddc39a4ae710ec91d737272e Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 17 May 2026 04:21:39 +0530 Subject: Phase 4: TollGate client detection + auto-payment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New tollgate_client.c/h: detect upstream TollGate (kind=10021), auto-pay via nucula wallet, session monitoring with 20% renewal - State machine: IDLE→DETECTING→NEEDS_PAY→PAYING→PAID→RENEWING - Blocking: upstream payment before local services start - Synchronous wallet init (was async task) - Client config: enabled, steps_to_buy, renewal_threshold_pct - Updated PLAN.md with Phases 4-7 (client, payout, bytes, CVM) - Updated CHECKLIST.md with all new phase items - 30 new unit tests (all passing), 116 total --- main/CMakeLists.txt | 1 + main/config.c | 22 ++- main/config.h | 5 + main/tollgate_client.c | 457 +++++++++++++++++++++++++++++++++++++++++++++++++ main/tollgate_client.h | 46 +++++ main/tollgate_main.c | 15 +- 6 files changed, 542 insertions(+), 4 deletions(-) create mode 100644 main/tollgate_client.c create mode 100644 main/tollgate_client.h (limited to 'main') diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index df69283..be4d564 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -10,6 +10,7 @@ idf_component_register(SRCS "tollgate_main.c" "nostr_event.c" "geohash.c" "wifistr.c" + "tollgate_client.c" INCLUDE_DIRS "." REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server lwip json esp_http_client mbedtls esp-tls log spiffs diff --git a/main/config.c b/main/config.c index 47d631f..c074410 100644 --- a/main/config.c +++ b/main/config.c @@ -22,6 +22,10 @@ esp_err_t tollgate_config_init(void) g_config.step_size_ms = 60000; g_config.persist_threshold_sats = 1; g_config.nostr_publish_interval_s = 21600; + g_config.client_enabled = false; + g_config.client_steps_to_buy = 1; + g_config.client_renewal_threshold_pct = 20; + g_config.client_retry_interval_ms = 30000; esp_vfs_spiffs_conf_t conf = { .base_path = "/spiffs", @@ -49,7 +53,11 @@ esp_err_t tollgate_config_init(void) "\"step_size_ms\":60000," "\"nostr_geohash\":\"u281w0dfz\"," "\"nostr_relays\":[\"wss://relay.damus.io\",\"wss://nos.lol\"]," - "\"nostr_publish_interval_s\":21600" + "\"nostr_publish_interval_s\":21600," + "\"client_enabled\":false," + "\"client_steps_to_buy\":1," + "\"client_renewal_threshold_pct\":20," + "\"client_retry_interval_ms\":30000" "}"; f = fopen("/spiffs/config.json", "w"); if (f) { @@ -146,6 +154,18 @@ esp_err_t tollgate_config_init(void) cJSON *pub_interval = cJSON_GetObjectItem(root, "nostr_publish_interval_s"); if (pub_interval) g_config.nostr_publish_interval_s = pub_interval->valueint; + cJSON *client_enabled = cJSON_GetObjectItem(root, "client_enabled"); + if (client_enabled && cJSON_IsBool(client_enabled)) g_config.client_enabled = cJSON_IsTrue(client_enabled); + + cJSON *client_steps = cJSON_GetObjectItem(root, "client_steps_to_buy"); + if (client_steps) g_config.client_steps_to_buy = client_steps->valueint; + + cJSON *client_renewal = cJSON_GetObjectItem(root, "client_renewal_threshold_pct"); + if (client_renewal) g_config.client_renewal_threshold_pct = client_renewal->valueint; + + cJSON *client_retry = cJSON_GetObjectItem(root, "client_retry_interval_ms"); + if (client_retry) g_config.client_retry_interval_ms = client_retry->valueint; + cJSON_Delete(root); if (g_config.nostr_relay_count == 0) { diff --git a/main/config.h b/main/config.h index 8254a62..4c6116e 100644 --- a/main/config.h +++ b/main/config.h @@ -49,6 +49,11 @@ typedef struct { int nostr_publish_interval_s; bool identity_initialized; + + bool client_enabled; + int client_steps_to_buy; + int client_renewal_threshold_pct; + int client_retry_interval_ms; } tollgate_config_t; void tollgate_config_derive_unique(tollgate_config_t *cfg); diff --git a/main/tollgate_client.c b/main/tollgate_client.c new file mode 100644 index 0000000..ac8dcfe --- /dev/null +++ b/main/tollgate_client.c @@ -0,0 +1,457 @@ +#include "tollgate_client.h" +#include "config.h" +#include "nucula_wallet.h" +#include "esp_log.h" +#include "esp_http_client.h" +#include "esp_crt_bundle.h" +#include "cJSON.h" +#include +#include + +static const char *TAG = "tg_client"; + +static tollgate_client_state_t s_state = TG_CLIENT_IDLE; +static tollgate_discovery_t s_discovery = {0}; +static char s_gw_ip[TG_CLIENT_MAX_GW_IP_LEN] = {0}; +static int64_t s_allotment_ms = 0; +static int64_t s_remaining_ms = -1; +static int64_t s_last_pay_time_ms = 0; +static int s_retry_count = 0; + +static int64_t get_time_ms(void) { + return (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); +} + +static esp_err_t http_get(const char *url, char *resp_buf, size_t resp_buf_size, int *status_out) +{ + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_GET, + .timeout_ms = 10000, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) return ESP_FAIL; + + esp_err_t err = esp_http_client_open(client, 0); + if (err != ESP_OK) { + esp_http_client_cleanup(client); + return ESP_FAIL; + } + + int content_length = esp_http_client_fetch_headers(client); + (void)content_length; + int status = esp_http_client_get_status_code(client); + if (status_out) *status_out = status; + + int resp_len = esp_http_client_read(client, resp_buf, resp_buf_size - 1); + esp_http_client_cleanup(client); + + if (resp_len < 0) return ESP_FAIL; + resp_buf[resp_len] = '\0'; + return ESP_OK; +} + +static esp_err_t http_post_text(const char *url, const char *body, char *resp_buf, size_t resp_buf_size, int *status_out) +{ + esp_http_client_config_t config = { + .url = url, + .method = HTTP_METHOD_POST, + .timeout_ms = 15000, + .crt_bundle_attach = esp_crt_bundle_attach, + }; + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) return ESP_FAIL; + + esp_http_client_set_header(client, "Content-Type", "text/plain"); + esp_err_t err = esp_http_client_open(client, strlen(body)); + if (err != ESP_OK) { + esp_http_client_cleanup(client); + return ESP_FAIL; + } + + esp_http_client_write(client, body, strlen(body)); + + int content_length = esp_http_client_fetch_headers(client); + (void)content_length; + int status = esp_http_client_get_status_code(client); + if (status_out) *status_out = status; + + int resp_len = esp_http_client_read(client, resp_buf, resp_buf_size - 1); + esp_http_client_cleanup(client); + + if (resp_len < 0) return ESP_FAIL; + resp_buf[resp_len] = '\0'; + return ESP_OK; +} + +static bool parse_discovery_response(const char *json_str, tollgate_discovery_t *out) +{ + cJSON *root = cJSON_Parse(json_str); + if (!root) return false; + + cJSON *kind = cJSON_GetObjectItemCaseSensitive(root, "kind"); + if (!kind || !cJSON_IsNumber(kind) || kind->valueint != 10021) { + cJSON_Delete(root); + return false; + } + + memset(out, 0, sizeof(tollgate_discovery_t)); + out->is_tollgate = true; + + cJSON *tags = cJSON_GetObjectItemCaseSensitive(root, "tags"); + if (!tags || !cJSON_IsArray(tags)) { + cJSON_Delete(root); + return true; + } + + int tag_count = cJSON_GetArraySize(tags); + for (int i = 0; i < tag_count; i++) { + cJSON *tag = cJSON_GetArrayItem(tags, i); + if (!tag || !cJSON_IsArray(tag)) continue; + + int tag_len = cJSON_GetArraySize(tag); + if (tag_len < 2) continue; + + cJSON *tag_name = cJSON_GetArrayItem(tag, 0); + if (!tag_name || !cJSON_IsString(tag_name)) continue; + + if (strcmp(tag_name->valuestring, "metric") == 0) { + cJSON *val = cJSON_GetArrayItem(tag, 1); + if (val && cJSON_IsString(val)) { + strncpy(out->metric, val->valuestring, sizeof(out->metric) - 1); + } + } else if (strcmp(tag_name->valuestring, "step_size") == 0) { + cJSON *val = cJSON_GetArrayItem(tag, 1); + if (val && cJSON_IsString(val)) { + out->step_size_ms = atoi(val->valuestring); + } + } else if (strcmp(tag_name->valuestring, "price_per_step") == 0 && tag_len >= 6) { + cJSON *amount = cJSON_GetArrayItem(tag, 2); + cJSON *mint = cJSON_GetArrayItem(tag, 4); + + if (amount && cJSON_IsString(amount)) { + out->price_per_step = atoi(amount->valuestring); + } + if (mint && cJSON_IsString(mint)) { + strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1); + } + } + } + + cJSON_Delete(root); + return true; +} + +static bool parse_session_response(const char *json_str, int64_t *allotment_ms_out) +{ + cJSON *root = cJSON_Parse(json_str); + if (!root) return false; + + cJSON *kind = cJSON_GetObjectItemCaseSensitive(root, "kind"); + if (!kind || !cJSON_IsNumber(kind)) { + cJSON_Delete(root); + return false; + } + + if (kind->valueint != 1022) { + cJSON_Delete(root); + return false; + } + + cJSON *tags = cJSON_GetObjectItemCaseSensitive(root, "tags"); + if (tags && cJSON_IsArray(tags)) { + int tag_count = cJSON_GetArraySize(tags); + for (int i = 0; i < tag_count; i++) { + cJSON *tag = cJSON_GetArrayItem(tags, i); + if (!tag || !cJSON_IsArray(tag)) continue; + cJSON *tag_name = cJSON_GetArrayItem(tag, 0); + if (tag_name && cJSON_IsString(tag_name) && strcmp(tag_name->valuestring, "allotment") == 0) { + cJSON *val = cJSON_GetArrayItem(tag, 1); + if (val && cJSON_IsString(val)) { + *allotment_ms_out = atoll(val->valuestring); + } + } + } + } + + cJSON_Delete(root); + return true; +} + +static bool parse_usage_response(const char *resp, int64_t *remaining_out, int64_t *total_out) +{ + char remaining_str[32] = {0}; + char total_str[32] = {0}; + const char *slash = strchr(resp, '/'); + if (!slash) return false; + + size_t rlen = slash - resp; + if (rlen >= sizeof(remaining_str)) return false; + memcpy(remaining_str, resp, rlen); + strncpy(total_str, slash + 1, sizeof(total_str) - 1); + + *remaining_out = atoll(remaining_str); + *total_out = atoll(total_str); + return true; +} + +esp_err_t tollgate_client_detect(const char *gw_ip, tollgate_discovery_t *discovery) +{ + char url[128]; + snprintf(url, sizeof(url), "http://%s:2121/", gw_ip); + + char *resp_buf = malloc(4096); + if (!resp_buf) return ESP_ERR_NO_MEM; + + int status = 0; + esp_err_t err = http_get(url, resp_buf, 4096, &status); + + if (err != ESP_OK || status != 200) { + ESP_LOGI(TAG, "detect: no TollGate at %s (status=%d, err=%s)", gw_ip, status, esp_err_to_name(err)); + free(resp_buf); + return ESP_ERR_NOT_FOUND; + } + + bool found = parse_discovery_response(resp_buf, discovery); + free(resp_buf); + + if (found && discovery->is_tollgate) { + ESP_LOGI(TAG, "TollGate detected at %s: price=%d sats, step=%dms, mint=%s, metric=%s", + gw_ip, discovery->price_per_step, discovery->step_size_ms, + discovery->mint_url, discovery->metric); + return ESP_OK; + } + + ESP_LOGI(TAG, "detect: response at %s not a TollGate", gw_ip); + return ESP_ERR_NOT_FOUND; +} + +static esp_err_t tollgate_client_pay(const char *gw_ip, int amount_sats, int64_t *allotment_ms_out) +{ + uint64_t balance = nucula_wallet_balance(); + if (balance < (uint64_t)amount_sats) { + ESP_LOGW(TAG, "insufficient balance: %llu < %d", (unsigned long long)balance, amount_sats); + return ESP_ERR_INVALID_STATE; + } + + char token_buf[8192]; + esp_err_t err = nucula_wallet_send((uint64_t)amount_sats, token_buf, sizeof(token_buf)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "wallet send failed: %s", esp_err_to_name(err)); + return err; + } + + ESP_LOGI(TAG, "created token (%d sats), posting to %s:2121", amount_sats, gw_ip); + + char url[128]; + snprintf(url, sizeof(url), "http://%s:2121/", gw_ip); + + char *resp_buf = malloc(8192); + if (!resp_buf) return ESP_ERR_NO_MEM; + + int status = 0; + err = http_post_text(url, token_buf, resp_buf, 8192, &status); + if (err != ESP_OK) { + ESP_LOGE(TAG, "payment POST failed: %s", esp_err_to_name(err)); + free(resp_buf); + return err; + } + + ESP_LOGI(TAG, "payment response: status=%d, body=%s", status, resp_buf); + + int64_t allotment = 0; + if (status == 200 && parse_session_response(resp_buf, &allotment)) { + *allotment_ms_out = allotment; + ESP_LOGI(TAG, "payment accepted: allotment=%lldms", (long long)allotment); + free(resp_buf); + return ESP_OK; + } + + ESP_LOGE(TAG, "payment rejected: status=%d", status); + free(resp_buf); + return ESP_FAIL; +} + +static esp_err_t tollgate_client_query_usage(const char *gw_ip, int64_t *remaining_ms, int64_t *total_ms) +{ + char url[128]; + snprintf(url, sizeof(url), "http://%s:2121/usage", gw_ip); + + char resp_buf[256]; + int status = 0; + esp_err_t err = http_get(url, resp_buf, sizeof(resp_buf), &status); + if (err != ESP_OK || status != 200) { + return ESP_FAIL; + } + + return parse_usage_response(resp_buf, remaining_ms, total_ms) ? ESP_OK : ESP_FAIL; +} + +esp_err_t tollgate_client_init(void) +{ + s_state = TG_CLIENT_IDLE; + memset(&s_discovery, 0, sizeof(s_discovery)); + memset(s_gw_ip, 0, sizeof(s_gw_ip)); + s_allotment_ms = 0; + s_remaining_ms = -1; + s_last_pay_time_ms = 0; + s_retry_count = 0; + return ESP_OK; +} + +esp_err_t tollgate_client_on_sta_connected(const char *gw_ip_str) +{ + const tollgate_config_t *cfg = tollgate_config_get(); + + if (!cfg->client_enabled) { + ESP_LOGI(TAG, "client disabled, skipping upstream detection"); + return ESP_OK; + } + + strncpy(s_gw_ip, gw_ip_str, sizeof(s_gw_ip) - 1); + s_state = TG_CLIENT_DETECTING; + s_retry_count = 0; + + ESP_LOGI(TAG, "detecting upstream TollGate at %s", gw_ip_str); + + esp_err_t err = tollgate_client_detect(gw_ip_str, &s_discovery); + if (err != ESP_OK) { + s_state = TG_CLIENT_NO_TOLLGATE; + ESP_LOGI(TAG, "no upstream TollGate detected"); + return ESP_OK; + } + + s_state = TG_CLIENT_NEEDS_PAY; + + int steps = cfg->client_steps_to_buy; + if (steps <= 0) steps = 1; + int amount_sats = steps * s_discovery.price_per_step; + + s_state = TG_CLIENT_PAYING; + int64_t allotment = 0; + err = tollgate_client_pay(gw_ip_str, amount_sats, &allotment); + if (err != ESP_OK) { + s_state = TG_CLIENT_ERROR; + ESP_LOGE(TAG, "upstream payment failed"); + return err; + } + + s_allotment_ms = allotment; + s_remaining_ms = allotment; + s_last_pay_time_ms = get_time_ms(); + s_state = TG_CLIENT_PAID; + + ESP_LOGI(TAG, "upstream TollGate paid: %lldms allotment", (long long)allotment); + return ESP_OK; +} + +void tollgate_client_on_sta_disconnected(void) +{ + ESP_LOGI(TAG, "STA disconnected, resetting client state"); + s_state = TG_CLIENT_IDLE; + memset(&s_discovery, 0, sizeof(s_discovery)); + memset(s_gw_ip, 0, sizeof(s_gw_ip)); + s_allotment_ms = 0; + s_remaining_ms = -1; + s_last_pay_time_ms = 0; + s_retry_count = 0; +} + +void tollgate_client_tick(void) +{ + if (s_state != TG_CLIENT_PAID && s_state != TG_CLIENT_RENEWING && s_state != TG_CLIENT_ERROR) { + return; + } + + if (s_state == TG_CLIENT_ERROR) { + const tollgate_config_t *cfg = tollgate_config_get(); + int64_t now = get_time_ms(); + int64_t elapsed = now - s_last_pay_time_ms; + if (elapsed < cfg->client_retry_interval_ms) return; + + if (s_gw_ip[0] == '\0') return; + + s_state = TG_CLIENT_PAYING; + int steps = cfg->client_steps_to_buy; + if (steps <= 0) steps = 1; + int amount_sats = steps * s_discovery.price_per_step; + + int64_t allotment = 0; + esp_err_t err = tollgate_client_pay(s_gw_ip, amount_sats, &allotment); + if (err == ESP_OK) { + s_allotment_ms = allotment; + s_remaining_ms = allotment; + s_last_pay_time_ms = get_time_ms(); + s_state = TG_CLIENT_PAID; + s_retry_count = 0; + ESP_LOGI(TAG, "retry payment succeeded: %lldms", (long long)allotment); + } else { + s_last_pay_time_ms = get_time_ms(); + s_retry_count++; + s_state = TG_CLIENT_ERROR; + ESP_LOGW(TAG, "retry payment failed (attempt %d)", s_retry_count); + } + return; + } + + if (s_gw_ip[0] == '\0') return; + + int64_t remaining = 0, total = 0; + esp_err_t err = tollgate_client_query_usage(s_gw_ip, &remaining, &total); + if (err == ESP_OK) { + s_remaining_ms = remaining; + s_allotment_ms = total; + } + + const tollgate_config_t *cfg = tollgate_config_get(); + int threshold_pct = cfg->client_renewal_threshold_pct; + if (threshold_pct <= 0) threshold_pct = 20; + + if (s_allotment_ms > 0 && s_remaining_ms >= 0) { + int remaining_pct = (int)((s_remaining_ms * 100) / s_allotment_ms); + if (remaining_pct <= threshold_pct) { + ESP_LOGI(TAG, "session nearing expiry (%lld/%lldms, %d%%), renewing", + (long long)s_remaining_ms, (long long)s_allotment_ms, remaining_pct); + + s_state = TG_CLIENT_RENEWING; + int steps = cfg->client_steps_to_buy; + if (steps <= 0) steps = 1; + int amount_sats = steps * s_discovery.price_per_step; + + int64_t allotment = 0; + err = tollgate_client_pay(s_gw_ip, amount_sats, &allotment); + if (err == ESP_OK) { + s_allotment_ms = allotment; + s_remaining_ms = allotment; + s_last_pay_time_ms = get_time_ms(); + s_state = TG_CLIENT_PAID; + ESP_LOGI(TAG, "renewal succeeded: %lldms", (long long)allotment); + } else { + s_state = TG_CLIENT_ERROR; + s_last_pay_time_ms = get_time_ms(); + ESP_LOGE(TAG, "renewal payment failed"); + } + } + } +} + +tollgate_client_state_t tollgate_client_get_state(void) +{ + return s_state; +} + +const tollgate_discovery_t *tollgate_client_get_discovery(void) +{ + return &s_discovery; +} + +int64_t tollgate_client_get_remaining_ms(void) +{ + return s_remaining_ms; +} + +int64_t tollgate_client_get_allotment_ms(void) +{ + return s_allotment_ms; +} diff --git a/main/tollgate_client.h b/main/tollgate_client.h new file mode 100644 index 0000000..2055e52 --- /dev/null +++ b/main/tollgate_client.h @@ -0,0 +1,46 @@ +#ifndef TOLLGATE_CLIENT_H +#define TOLLGATE_CLIENT_H + +#include "esp_err.h" +#include +#include + +#define TG_CLIENT_MAX_GW_IP_LEN 16 +#define TG_CLIENT_MAX_MINT_URL 256 +#define TG_CLIENT_MAX_METRIC 32 + +typedef enum { + TG_CLIENT_IDLE, + TG_CLIENT_DETECTING, + TG_CLIENT_NO_TOLLGATE, + TG_CLIENT_NEEDS_PAY, + TG_CLIENT_PAYING, + TG_CLIENT_PAID, + TG_CLIENT_RENEWING, + TG_CLIENT_ERROR +} tollgate_client_state_t; + +typedef struct { + bool is_tollgate; + int price_per_step; + int step_size_ms; + char mint_url[TG_CLIENT_MAX_MINT_URL]; + char metric[TG_CLIENT_MAX_METRIC]; +} tollgate_discovery_t; + +esp_err_t tollgate_client_init(void); + +esp_err_t tollgate_client_on_sta_connected(const char *gw_ip_str); + +void tollgate_client_on_sta_disconnected(void); + +void tollgate_client_tick(void); + +tollgate_client_state_t tollgate_client_get_state(void); + +const tollgate_discovery_t *tollgate_client_get_discovery(void); + +int64_t tollgate_client_get_remaining_ms(void); +int64_t tollgate_client_get_allotment_ms(void); + +#endif diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 7fa1be1..d4dcf0d 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c @@ -19,6 +19,7 @@ #include "tollgate_api.h" #include "nucula_wallet.h" #include "wifistr.h" +#include "tollgate_client.h" #define MAX_STA_RETRY 5 static const char *TAG = "tollgate_main"; @@ -48,6 +49,7 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { s_retry_count++; ESP_LOGW(TAG, "WiFi disconnected, retry %d/%d", s_retry_count, MAX_STA_RETRY); + tollgate_client_on_sta_disconnected(); if (s_services_running) stop_services(); if (s_retry_count < MAX_STA_RETRY) { esp_wifi_connect(); @@ -80,9 +82,17 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base, { if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; - ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip)); + ESP_LOGI(TAG, "Got IP:" IPSTR ", GW:" IPSTR, IP2STR(&event->ip_info.ip), IP2STR(&event->ip_info.gw)); s_retry_count = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + + const tollgate_config_t *cfg = tollgate_config_get(); + nucula_wallet_init(cfg->mint_url); + + char gw_ip_str[16]; + snprintf(gw_ip_str, sizeof(gw_ip_str), IPSTR, IP2STR(&event->ip_info.gw)); + tollgate_client_on_sta_connected(gw_ip_str); + start_services(); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { ESP_LOGW(TAG, "Lost IP address"); @@ -126,8 +136,6 @@ static void start_services(void) firewall_init(ap_ip_info.ip); session_manager_init(); - xTaskCreate(wallet_init_task, "wallet_init", 32768, NULL, 5, NULL); - const tollgate_config_t *cfg = tollgate_config_get(); dns_server_start(ap_ip_info.ip, upstream_dns); captive_portal_start(cfg->ap_ip_str); @@ -273,5 +281,6 @@ void app_main(void) while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); session_tick(); + tollgate_client_tick(); } } -- cgit v1.2.3