diff options
| author | Your Name <you@example.com> | 2026-05-19 13:14:48 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-19 13:14:48 +0530 |
| commit | fe6aa9663d4cdabdc6e71db6068f8cd9e3739ffe (patch) | |
| tree | 8cadb07243c07a6b3fa9453b239c9ac5cb02b454 /main | |
| parent | 77031f06a9a87320d011f501590985161d1eb305 (diff) | |
feat: WiFi beacon price discovery via Vendor IE (two-board verified)
Price discovery allows TollGate ESP32 boards to advertise their per-step
price via WiFi Vendor-Specific Information Elements (OUI 0xC0FFEE) in
beacon and probe response frames. Nearby boards passively scan and build
a market view of competing TollGates without requiring internet access.
Features:
- beacon_price.c/h: 26-byte packed Vendor IE payload (price, step, metric,
mint_hash, geohash, npub_hash), injected via esp_wifi_set_vendor_ie()
- market.c/h: Passive WiFi scan receiver, vendor IE callback parsing,
BSSID-correlated market entries, effective price ranking
- GET /market API endpoint: JSON market snapshot with discovered entries
- AP-only services: beacon + market + API start on WIFI_EVENT_AP_START,
independent of STA connectivity
- STA reconnect fix: 2s delay between retries creates scan windows;
s_sta_connecting guard prevents double-connect
- write-config-ap-only-a/b Makefile targets for STA-less testing
- market_tick() in main loop, client price comparison logging
Hardware verified: both boards discover each other via Vendor IE beacons.
Board A sees TollGate-C0E9CA (RSSI=-30), Board B sees TollGate-B96D80
(RSSI=-25). test-market.mjs: 9/9, test-price-discovery.mjs: 7/7 per board.
Unit tests: 45 new assertions across test_beacon_price (28) and test_market
(17). All 15 test suites pass. ESP-IDF build clean for ESP32-S3.
Diffstat (limited to 'main')
| -rw-r--r-- | main/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | main/beacon_price.c | 103 | ||||
| -rw-r--r-- | main/beacon_price.h | 44 | ||||
| -rw-r--r-- | main/config.h | 4 | ||||
| -rw-r--r-- | main/cvm_server.c | 3 | ||||
| -rw-r--r-- | main/market.c | 237 | ||||
| -rw-r--r-- | main/market.h | 40 | ||||
| -rw-r--r-- | main/tollgate_api.c | 43 | ||||
| -rw-r--r-- | main/tollgate_client.c | 14 | ||||
| -rw-r--r-- | main/tollgate_main.c | 54 |
10 files changed, 534 insertions, 10 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 6408e14..abbe53b 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt | |||
| @@ -21,6 +21,8 @@ idf_component_register(SRCS "tollgate_main.c" | |||
| 21 | "local_relay.c" | 21 | "local_relay.c" |
| 22 | "relay_selector.c" | 22 | "relay_selector.c" |
| 23 | "sync_manager.c" | 23 | "sync_manager.c" |
| 24 | "beacon_price.c" | ||
| 25 | "market.c" | ||
| 24 | INCLUDE_DIRS "." | 26 | INCLUDE_DIRS "." |
| 25 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server | 27 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server |
| 26 | lwip json esp_http_client mbedtls esp-tls log spiffs | 28 | lwip json esp_http_client mbedtls esp-tls log spiffs |
diff --git a/main/beacon_price.c b/main/beacon_price.c new file mode 100644 index 0000000..b87e289 --- /dev/null +++ b/main/beacon_price.c | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | #include "beacon_price.h" | ||
| 2 | #include "config.h" | ||
| 3 | #include "identity.h" | ||
| 4 | #include "esp_log.h" | ||
| 5 | #include "esp_wifi.h" | ||
| 6 | #include "mbedtls/sha256.h" | ||
| 7 | #include <string.h> | ||
| 8 | |||
| 9 | static const char *TAG = "beacon_price"; | ||
| 10 | static bool s_active = false; | ||
| 11 | |||
| 12 | void beacon_price_hash_mint(const char *mint_url, uint8_t hash_out[4]) | ||
| 13 | { | ||
| 14 | uint8_t full_hash[32]; | ||
| 15 | mbedtls_sha256((const unsigned char *)mint_url, strlen(mint_url), full_hash, 0); | ||
| 16 | memcpy(hash_out, full_hash, 4); | ||
| 17 | } | ||
| 18 | |||
| 19 | void beacon_price_hash_npub(const char *npub_hex, uint8_t hash_out[4]) | ||
| 20 | { | ||
| 21 | uint8_t full_hash[32]; | ||
| 22 | mbedtls_sha256((const unsigned char *)npub_hex, strlen(npub_hex), full_hash, 0); | ||
| 23 | memcpy(hash_out, full_hash, 4); | ||
| 24 | } | ||
| 25 | |||
| 26 | void beacon_price_build_ie(tollgate_price_ie_t *ie) | ||
| 27 | { | ||
| 28 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 29 | const tollgate_identity_t *id = identity_get(); | ||
| 30 | |||
| 31 | memset(ie, 0, sizeof(*ie)); | ||
| 32 | ie->element_id = WIFI_VENDOR_IE_ELEMENT_ID; | ||
| 33 | ie->length = 4 + TOLLGATE_IE_PAYLOAD_SIZE; | ||
| 34 | ie->vendor_oui[0] = TOLLGATE_OUI_0; | ||
| 35 | ie->vendor_oui[1] = TOLLGATE_OUI_1; | ||
| 36 | ie->vendor_oui[2] = TOLLGATE_OUI_2; | ||
| 37 | ie->vendor_oui_type = TOLLGATE_IE_TYPE; | ||
| 38 | |||
| 39 | tollgate_price_payload_t *p = &ie->payload; | ||
| 40 | p->version = TOLLGATE_IE_VERSION; | ||
| 41 | p->metric = (strcmp(cfg->metric, "bytes") == 0) ? 1 : 0; | ||
| 42 | p->price_per_step = (uint16_t)cfg->price_per_step; | ||
| 43 | |||
| 44 | bool is_bytes = (strcmp(cfg->metric, "bytes") == 0); | ||
| 45 | p->step_size = is_bytes ? (uint32_t)cfg->step_size_bytes : (uint32_t)cfg->step_size_ms; | ||
| 46 | |||
| 47 | beacon_price_hash_mint(cfg->mint_url, p->mint_hash); | ||
| 48 | |||
| 49 | p->geohash_len = (uint8_t)strnlen(cfg->nostr_geohash, TOLLGATE_IE_GEOHASH_MAX); | ||
| 50 | memcpy(p->geohash, cfg->nostr_geohash, p->geohash_len); | ||
| 51 | if (p->geohash_len < TOLLGATE_IE_GEOHASH_MAX) { | ||
| 52 | memset(p->geohash + p->geohash_len, 0, TOLLGATE_IE_GEOHASH_MAX - p->geohash_len); | ||
| 53 | } | ||
| 54 | |||
| 55 | if (id && id->initialized) { | ||
| 56 | beacon_price_hash_npub(id->npub_hex, p->npub_hash); | ||
| 57 | } | ||
| 58 | |||
| 59 | ESP_LOGI(TAG, "Built IE: price=%lu sats, step=%lu, metric=%s, geohash=%.*s", | ||
| 60 | (unsigned long)p->price_per_step, (unsigned long)p->step_size, | ||
| 61 | p->metric ? "bytes" : "milliseconds", | ||
| 62 | p->geohash_len, p->geohash); | ||
| 63 | } | ||
| 64 | |||
| 65 | esp_err_t beacon_price_start(void) | ||
| 66 | { | ||
| 67 | if (s_active) { | ||
| 68 | ESP_LOGW(TAG, "Already active"); | ||
| 69 | return ESP_OK; | ||
| 70 | } | ||
| 71 | |||
| 72 | static tollgate_price_ie_t s_ie; | ||
| 73 | beacon_price_build_ie(&s_ie); | ||
| 74 | |||
| 75 | esp_err_t ret = esp_wifi_set_vendor_ie(true, WIFI_VND_IE_TYPE_BEACON, | ||
| 76 | WIFI_VND_IE_ID_0, &s_ie); | ||
| 77 | if (ret != ESP_OK) { | ||
| 78 | ESP_LOGE(TAG, "Failed to set beacon vendor IE: %s", esp_err_to_name(ret)); | ||
| 79 | return ret; | ||
| 80 | } | ||
| 81 | |||
| 82 | ret = esp_wifi_set_vendor_ie(true, WIFI_VND_IE_TYPE_PROBE_RESP, | ||
| 83 | WIFI_VND_IE_ID_1, &s_ie); | ||
| 84 | if (ret != ESP_OK) { | ||
| 85 | ESP_LOGW(TAG, "Failed to set probe resp vendor IE: %s", esp_err_to_name(ret)); | ||
| 86 | } | ||
| 87 | |||
| 88 | s_active = true; | ||
| 89 | ESP_LOGI(TAG, "Price advertising started (beacon + probe response)"); | ||
| 90 | return ESP_OK; | ||
| 91 | } | ||
| 92 | |||
| 93 | esp_err_t beacon_price_stop(void) | ||
| 94 | { | ||
| 95 | if (!s_active) return ESP_OK; | ||
| 96 | |||
| 97 | esp_wifi_set_vendor_ie(false, WIFI_VND_IE_TYPE_BEACON, WIFI_VND_IE_ID_0, NULL); | ||
| 98 | esp_wifi_set_vendor_ie(false, WIFI_VND_IE_TYPE_PROBE_RESP, WIFI_VND_IE_ID_1, NULL); | ||
| 99 | |||
| 100 | s_active = false; | ||
| 101 | ESP_LOGI(TAG, "Price advertising stopped"); | ||
| 102 | return ESP_OK; | ||
| 103 | } | ||
diff --git a/main/beacon_price.h b/main/beacon_price.h new file mode 100644 index 0000000..cb2eb5b --- /dev/null +++ b/main/beacon_price.h | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | #ifndef BEACON_PRICE_H | ||
| 2 | #define BEACON_PRICE_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdbool.h> | ||
| 7 | |||
| 8 | #define TOLLGATE_OUI_0 0xC0 | ||
| 9 | #define TOLLGATE_OUI_1 0xFF | ||
| 10 | #define TOLLGATE_OUI_2 0xEE | ||
| 11 | #define TOLLGATE_IE_TYPE 0x01 | ||
| 12 | #define TOLLGATE_IE_VERSION 1 | ||
| 13 | |||
| 14 | #define TOLLGATE_IE_GEOHASH_MAX 9 | ||
| 15 | |||
| 16 | typedef struct __attribute__((packed)) { | ||
| 17 | uint8_t version; | ||
| 18 | uint8_t metric; | ||
| 19 | uint16_t price_per_step; | ||
| 20 | uint32_t step_size; | ||
| 21 | uint8_t mint_hash[4]; | ||
| 22 | uint8_t geohash_len; | ||
| 23 | char geohash[TOLLGATE_IE_GEOHASH_MAX]; | ||
| 24 | uint8_t npub_hash[4]; | ||
| 25 | } tollgate_price_payload_t; | ||
| 26 | |||
| 27 | #define TOLLGATE_IE_PAYLOAD_SIZE sizeof(tollgate_price_payload_t) | ||
| 28 | #define TOLLGATE_IE_TOTAL_SIZE (6 + TOLLGATE_IE_PAYLOAD_SIZE) | ||
| 29 | |||
| 30 | typedef struct __attribute__((packed)) { | ||
| 31 | uint8_t element_id; | ||
| 32 | uint8_t length; | ||
| 33 | uint8_t vendor_oui[3]; | ||
| 34 | uint8_t vendor_oui_type; | ||
| 35 | tollgate_price_payload_t payload; | ||
| 36 | } tollgate_price_ie_t; | ||
| 37 | |||
| 38 | esp_err_t beacon_price_start(void); | ||
| 39 | esp_err_t beacon_price_stop(void); | ||
| 40 | void beacon_price_build_ie(tollgate_price_ie_t *ie); | ||
| 41 | void beacon_price_hash_mint(const char *mint_url, uint8_t hash_out[4]); | ||
| 42 | void beacon_price_hash_npub(const char *npub_hex, uint8_t hash_out[4]); | ||
| 43 | |||
| 44 | #endif | ||
diff --git a/main/config.h b/main/config.h index 1e580e9..af372af 100644 --- a/main/config.h +++ b/main/config.h | |||
| @@ -69,6 +69,10 @@ typedef struct { | |||
| 69 | int nostr_seed_relay_count; | 69 | int nostr_seed_relay_count; |
| 70 | int nostr_sync_interval_s; | 70 | int nostr_sync_interval_s; |
| 71 | int nostr_fallback_sync_interval_s; | 71 | int nostr_fallback_sync_interval_s; |
| 72 | |||
| 73 | bool market_enabled; | ||
| 74 | int market_scan_interval_s; | ||
| 75 | bool client_auto_switch; | ||
| 72 | } tollgate_config_t; | 76 | } tollgate_config_t; |
| 73 | 77 | ||
| 74 | void tollgate_config_derive_unique(tollgate_config_t *cfg); | 78 | void tollgate_config_derive_unique(tollgate_config_t *cfg); |
diff --git a/main/cvm_server.c b/main/cvm_server.c index 644738b..a4804d2 100644 --- a/main/cvm_server.c +++ b/main/cvm_server.c | |||
| @@ -8,6 +8,7 @@ | |||
| 8 | #include "nucula_wallet.h" | 8 | #include "nucula_wallet.h" |
| 9 | #include "cJSON.h" | 9 | #include "cJSON.h" |
| 10 | #include "esp_log.h" | 10 | #include "esp_log.h" |
| 11 | #include "esp_timer.h" | ||
| 11 | #include "esp_tls.h" | 12 | #include "esp_tls.h" |
| 12 | #include "esp_crt_bundle.h" | 13 | #include "esp_crt_bundle.h" |
| 13 | #include "esp_random.h" | 14 | #include "esp_random.h" |
| @@ -576,7 +577,7 @@ static void cvm_relay_task(void *arg) | |||
| 576 | char *text = parse_ws_text_frame(buf, rlen); | 577 | char *text = parse_ws_text_frame(buf, rlen); |
| 577 | if (text) { | 578 | if (text) { |
| 578 | if (strlen(text) > 0) { | 579 | if (strlen(text) > 0) { |
| 579 | process_relay_message(tls, relay_url, text); | 580 | process_relay_message(relay_url, text); |
| 580 | } | 581 | } |
| 581 | free(text); | 582 | free(text); |
| 582 | } | 583 | } |
diff --git a/main/market.c b/main/market.c new file mode 100644 index 0000000..c8a0b6d --- /dev/null +++ b/main/market.c | |||
| @@ -0,0 +1,237 @@ | |||
| 1 | #include "market.h" | ||
| 2 | #include "beacon_price.h" | ||
| 3 | #include "config.h" | ||
| 4 | #include "identity.h" | ||
| 5 | #include "esp_log.h" | ||
| 6 | #include "esp_wifi.h" | ||
| 7 | #include "freertos/FreeRTOS.h" | ||
| 8 | #include "freertos/task.h" | ||
| 9 | #include <string.h> | ||
| 10 | |||
| 11 | static const char *TAG = "market"; | ||
| 12 | static market_t s_market; | ||
| 13 | static bool s_initialized = false; | ||
| 14 | |||
| 15 | static int64_t get_time_ms(void) | ||
| 16 | { | ||
| 17 | return (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; | ||
| 18 | } | ||
| 19 | |||
| 20 | static bool oui_matches(const uint8_t oui[3]) | ||
| 21 | { | ||
| 22 | return oui[0] == TOLLGATE_OUI_0 && oui[1] == TOLLGATE_OUI_1 && oui[2] == TOLLGATE_OUI_2; | ||
| 23 | } | ||
| 24 | |||
| 25 | static int find_entry_by_bssid(const uint8_t bssid[6]) | ||
| 26 | { | ||
| 27 | for (int i = 0; i < MARKET_MAX_ENTRIES; i++) { | ||
| 28 | if (s_market.entries[i].valid && memcmp(s_market.entries[i].bssid, bssid, 6) == 0) { | ||
| 29 | return i; | ||
| 30 | } | ||
| 31 | } | ||
| 32 | return -1; | ||
| 33 | } | ||
| 34 | |||
| 35 | static int find_free_slot(void) | ||
| 36 | { | ||
| 37 | for (int i = 0; i < MARKET_MAX_ENTRIES; i++) { | ||
| 38 | if (!s_market.entries[i].valid) return i; | ||
| 39 | } | ||
| 40 | int oldest = 0; | ||
| 41 | int64_t oldest_time = s_market.entries[0].discovered_ms; | ||
| 42 | for (int i = 1; i < MARKET_MAX_ENTRIES; i++) { | ||
| 43 | if (s_market.entries[i].discovered_ms < oldest_time) { | ||
| 44 | oldest_time = s_market.entries[i].discovered_ms; | ||
| 45 | oldest = i; | ||
| 46 | } | ||
| 47 | } | ||
| 48 | return oldest; | ||
| 49 | } | ||
| 50 | |||
| 51 | void market_parse_vendor_ie(const uint8_t sa[6], const vendor_ie_data_t *ie, int rssi) | ||
| 52 | { | ||
| 53 | if (!ie || ie->length < 4 + TOLLGATE_IE_PAYLOAD_SIZE) return; | ||
| 54 | if (!oui_matches(ie->vendor_oui)) return; | ||
| 55 | if (ie->vendor_oui_type != TOLLGATE_IE_TYPE) return; | ||
| 56 | |||
| 57 | const tollgate_price_payload_t *payload = (const tollgate_price_payload_t *)ie->payload; | ||
| 58 | if (payload->version != TOLLGATE_IE_VERSION) return; | ||
| 59 | |||
| 60 | const tollgate_identity_t *id = identity_get(); | ||
| 61 | if (id && id->initialized) { | ||
| 62 | uint8_t my_npub_hash[4]; | ||
| 63 | beacon_price_hash_npub(id->npub_hex, my_npub_hash); | ||
| 64 | if (memcmp(payload->npub_hash, my_npub_hash, 4) == 0) return; | ||
| 65 | } | ||
| 66 | |||
| 67 | int idx = find_entry_by_bssid(sa); | ||
| 68 | if (idx < 0) { | ||
| 69 | idx = find_free_slot(); | ||
| 70 | if (s_market.count < MARKET_MAX_ENTRIES) s_market.count++; | ||
| 71 | } | ||
| 72 | |||
| 73 | market_entry_t *entry = &s_market.entries[idx]; | ||
| 74 | memcpy(entry->bssid, sa, 6); | ||
| 75 | entry->rssi = (int8_t)rssi; | ||
| 76 | entry->price_per_step = payload->price_per_step; | ||
| 77 | entry->step_size = payload->step_size; | ||
| 78 | entry->metric = payload->metric; | ||
| 79 | memcpy(entry->mint_hash, payload->mint_hash, 4); | ||
| 80 | memcpy(entry->npub_hash, payload->npub_hash, 4); | ||
| 81 | |||
| 82 | uint8_t gh_len = payload->geohash_len; | ||
| 83 | if (gh_len > TOLLGATE_IE_GEOHASH_MAX) gh_len = TOLLGATE_IE_GEOHASH_MAX; | ||
| 84 | memcpy(entry->geohash, payload->geohash, gh_len); | ||
| 85 | entry->geohash[gh_len] = '\0'; | ||
| 86 | |||
| 87 | entry->discovered_ms = get_time_ms(); | ||
| 88 | entry->valid = true; | ||
| 89 | entry->ssid[0] = '\0'; | ||
| 90 | |||
| 91 | ESP_LOGI(TAG, "Discovered TollGate %02X:%02X:%02X:%02X:%02X:%02X price=%lu sats step=%lu metric=%s RSSI=%d", | ||
| 92 | sa[0], sa[1], sa[2], sa[3], sa[4], sa[5], | ||
| 93 | (unsigned long)payload->price_per_step, (unsigned long)payload->step_size, | ||
| 94 | payload->metric ? "bytes" : "milliseconds", rssi); | ||
| 95 | } | ||
| 96 | |||
| 97 | static void vendor_ie_cb(void *ctx, wifi_vendor_ie_type_t type, | ||
| 98 | const uint8_t sa[6], const vendor_ie_data_t *vnd_ie, int rssi) | ||
| 99 | { | ||
| 100 | (void)ctx; | ||
| 101 | (void)type; | ||
| 102 | if (!vnd_ie) return; | ||
| 103 | market_parse_vendor_ie(sa, vnd_ie, rssi); | ||
| 104 | } | ||
| 105 | |||
| 106 | static void scan_done_cb(void *arg, esp_event_base_t event_base, | ||
| 107 | int32_t event_id, void *event_data) | ||
| 108 | { | ||
| 109 | (void)arg; | ||
| 110 | (void)event_base; | ||
| 111 | (void)event_id; | ||
| 112 | (void)event_data; | ||
| 113 | |||
| 114 | s_market.scanning = false; | ||
| 115 | |||
| 116 | uint16_t ap_count = 0; | ||
| 117 | esp_wifi_scan_get_ap_num(&ap_count); | ||
| 118 | if (ap_count == 0) return; | ||
| 119 | |||
| 120 | uint16_t max_aps = ap_count > 20 ? 20 : ap_count; | ||
| 121 | wifi_ap_record_t *ap_records = malloc(max_aps * sizeof(wifi_ap_record_t)); | ||
| 122 | if (!ap_records) return; | ||
| 123 | |||
| 124 | esp_wifi_scan_get_ap_records(&max_aps, ap_records); | ||
| 125 | |||
| 126 | for (int i = 0; i < max_aps; i++) { | ||
| 127 | for (int j = 0; j < MARKET_MAX_ENTRIES; j++) { | ||
| 128 | if (!s_market.entries[j].valid) continue; | ||
| 129 | if (memcmp(s_market.entries[j].bssid, ap_records[i].bssid, 6) == 0) { | ||
| 130 | memcpy(s_market.entries[j].ssid, ap_records[i].ssid, 32); | ||
| 131 | s_market.entries[j].ssid[32] = '\0'; | ||
| 132 | s_market.entries[j].rssi = ap_records[i].rssi; | ||
| 133 | break; | ||
| 134 | } | ||
| 135 | } | ||
| 136 | } | ||
| 137 | free(ap_records); | ||
| 138 | s_market.last_scan_ms = get_time_ms(); | ||
| 139 | |||
| 140 | ESP_LOGI(TAG, "Scan complete: %d APs, %d TollGates found", max_aps, s_market.count); | ||
| 141 | } | ||
| 142 | |||
| 143 | static esp_event_handler_instance_t s_scan_done_handler = NULL; | ||
| 144 | |||
| 145 | esp_err_t market_init(void) | ||
| 146 | { | ||
| 147 | memset(&s_market, 0, sizeof(s_market)); | ||
| 148 | |||
| 149 | esp_err_t ret = esp_wifi_set_vendor_ie_cb(vendor_ie_cb, NULL); | ||
| 150 | if (ret != ESP_OK) { | ||
| 151 | ESP_LOGE(TAG, "Failed to register vendor IE callback: %s", esp_err_to_name(ret)); | ||
| 152 | return ret; | ||
| 153 | } | ||
| 154 | |||
| 155 | if (!s_scan_done_handler) { | ||
| 156 | ret = esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, | ||
| 157 | scan_done_cb, NULL, &s_scan_done_handler); | ||
| 158 | if (ret != ESP_OK) { | ||
| 159 | ESP_LOGW(TAG, "Failed to register scan done handler: %s", esp_err_to_name(ret)); | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 163 | s_initialized = true; | ||
| 164 | ESP_LOGI(TAG, "Market scanner initialized"); | ||
| 165 | return ESP_OK; | ||
| 166 | } | ||
| 167 | |||
| 168 | void market_tick(void) | ||
| 169 | { | ||
| 170 | if (!s_initialized) return; | ||
| 171 | |||
| 172 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 173 | if (!cfg->market_enabled) return; | ||
| 174 | |||
| 175 | if (s_market.scanning) return; | ||
| 176 | |||
| 177 | int64_t now = get_time_ms(); | ||
| 178 | int64_t elapsed = now - s_market.last_scan_ms; | ||
| 179 | int64_t interval_ms = (int64_t)cfg->market_scan_interval_s * 1000; | ||
| 180 | if (elapsed < interval_ms) return; | ||
| 181 | |||
| 182 | wifi_scan_config_t scan_config = { | ||
| 183 | .ssid = NULL, | ||
| 184 | .bssid = NULL, | ||
| 185 | .channel = 0, | ||
| 186 | .show_hidden = false, | ||
| 187 | .scan_type = WIFI_SCAN_TYPE_PASSIVE, | ||
| 188 | .scan_time.passive = 120, | ||
| 189 | }; | ||
| 190 | |||
| 191 | esp_err_t ret = esp_wifi_scan_start(&scan_config, false); | ||
| 192 | if (ret == ESP_OK) { | ||
| 193 | s_market.scanning = true; | ||
| 194 | s_market.last_scan_ms = now; | ||
| 195 | s_market.consecutive_failures = 0; | ||
| 196 | ESP_LOGD(TAG, "Market scan started"); | ||
| 197 | } else { | ||
| 198 | s_market.consecutive_failures++; | ||
| 199 | s_market.last_scan_ms = now; | ||
| 200 | if (s_market.consecutive_failures <= 3 || s_market.consecutive_failures % 30 == 0) { | ||
| 201 | ESP_LOGW(TAG, "Scan start failed: %s (failures: %d)", esp_err_to_name(ret), s_market.consecutive_failures); | ||
| 202 | } | ||
| 203 | } | ||
| 204 | } | ||
| 205 | |||
| 206 | const market_t *market_get(void) | ||
| 207 | { | ||
| 208 | return &s_market; | ||
| 209 | } | ||
| 210 | |||
| 211 | int market_find_cheapest(void) | ||
| 212 | { | ||
| 213 | int cheapest = -1; | ||
| 214 | uint32_t best_eff_price = UINT32_MAX; | ||
| 215 | |||
| 216 | for (int i = 0; i < MARKET_MAX_ENTRIES; i++) { | ||
| 217 | if (!s_market.entries[i].valid) continue; | ||
| 218 | if (s_market.entries[i].ssid[0] == '\0') continue; | ||
| 219 | |||
| 220 | uint32_t step = s_market.entries[i].step_size; | ||
| 221 | if (step == 0) continue; | ||
| 222 | |||
| 223 | uint32_t eff; | ||
| 224 | if (s_market.entries[i].metric == 0) { | ||
| 225 | eff = (uint32_t)s_market.entries[i].price_per_step * 60000 / step; | ||
| 226 | } else { | ||
| 227 | uint32_t eff_mb = (uint32_t)s_market.entries[i].price_per_step * 1048576 / step; | ||
| 228 | eff = eff_mb; | ||
| 229 | } | ||
| 230 | |||
| 231 | if (eff < best_eff_price) { | ||
| 232 | best_eff_price = eff; | ||
| 233 | cheapest = i; | ||
| 234 | } | ||
| 235 | } | ||
| 236 | return cheapest; | ||
| 237 | } | ||
diff --git a/main/market.h b/main/market.h new file mode 100644 index 0000000..6dbf43c --- /dev/null +++ b/main/market.h | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | #ifndef MARKET_H | ||
| 2 | #define MARKET_H | ||
| 3 | |||
| 4 | #include "beacon_price.h" | ||
| 5 | #include "esp_wifi.h" | ||
| 6 | #include "esp_err.h" | ||
| 7 | #include <stdint.h> | ||
| 8 | #include <stdbool.h> | ||
| 9 | |||
| 10 | #define MARKET_MAX_ENTRIES 10 | ||
| 11 | |||
| 12 | typedef struct { | ||
| 13 | uint8_t bssid[6]; | ||
| 14 | char ssid[33]; | ||
| 15 | int8_t rssi; | ||
| 16 | uint16_t price_per_step; | ||
| 17 | uint32_t step_size; | ||
| 18 | uint8_t metric; | ||
| 19 | uint8_t mint_hash[4]; | ||
| 20 | uint8_t npub_hash[4]; | ||
| 21 | char geohash[TOLLGATE_IE_GEOHASH_MAX + 1]; | ||
| 22 | int64_t discovered_ms; | ||
| 23 | bool valid; | ||
| 24 | } market_entry_t; | ||
| 25 | |||
| 26 | typedef struct { | ||
| 27 | market_entry_t entries[MARKET_MAX_ENTRIES]; | ||
| 28 | int count; | ||
| 29 | int64_t last_scan_ms; | ||
| 30 | bool scanning; | ||
| 31 | int consecutive_failures; | ||
| 32 | } market_t; | ||
| 33 | |||
| 34 | esp_err_t market_init(void); | ||
| 35 | void market_tick(void); | ||
| 36 | const market_t *market_get(void); | ||
| 37 | int market_find_cheapest(void); | ||
| 38 | void market_parse_vendor_ie(const uint8_t sa[6], const vendor_ie_data_t *ie, int rssi); | ||
| 39 | |||
| 40 | #endif | ||
diff --git a/main/tollgate_api.c b/main/tollgate_api.c index 650b0f3..15640c7 100644 --- a/main/tollgate_api.c +++ b/main/tollgate_api.c | |||
| @@ -4,7 +4,10 @@ | |||
| 4 | #include "session.h" | 4 | #include "session.h" |
| 5 | #include "firewall.h" | 5 | #include "firewall.h" |
| 6 | #include "nucula_wallet.h" | 6 | #include "nucula_wallet.h" |
| 7 | #include "mint_health.h" | ||
| 8 | #include "market.h" | ||
| 7 | #include "esp_log.h" | 9 | #include "esp_log.h" |
| 10 | #include "esp_system.h" | ||
| 8 | #include "cJSON.h" | 11 | #include "cJSON.h" |
| 9 | #include "lwip/sockets.h" | 12 | #include "lwip/sockets.h" |
| 10 | #include "lwip/netdb.h" | 13 | #include "lwip/netdb.h" |
| @@ -471,6 +474,45 @@ static const httpd_uri_t uri_wallet = { .uri = "/wallet", .method = HTTP_GET, .h | |||
| 471 | static const httpd_uri_t uri_wallet_swap = { .uri = "/wallet/swap", .method = HTTP_POST, .handler = api_post_wallet_swap }; | 474 | static const httpd_uri_t uri_wallet_swap = { .uri = "/wallet/swap", .method = HTTP_POST, .handler = api_post_wallet_swap }; |
| 472 | static const httpd_uri_t uri_wallet_send = { .uri = "/wallet/send", .method = HTTP_POST, .handler = api_post_wallet_send }; | 475 | static const httpd_uri_t uri_wallet_send = { .uri = "/wallet/send", .method = HTTP_POST, .handler = api_post_wallet_send }; |
| 473 | 476 | ||
| 477 | static esp_err_t api_get_market(httpd_req_t *req) | ||
| 478 | { | ||
| 479 | const market_t *mkt = market_get(); | ||
| 480 | |||
| 481 | cJSON *root = cJSON_CreateObject(); | ||
| 482 | cJSON_AddNumberToObject(root, "count", mkt->count); | ||
| 483 | cJSON_AddNumberToObject(root, "last_scan_s", (double)(mkt->last_scan_ms / 1000)); | ||
| 484 | |||
| 485 | cJSON *entries = cJSON_CreateArray(); | ||
| 486 | for (int i = 0; i < MARKET_MAX_ENTRIES; i++) { | ||
| 487 | if (!mkt->entries[i].valid) continue; | ||
| 488 | const market_entry_t *e = &mkt->entries[i]; | ||
| 489 | |||
| 490 | cJSON *entry = cJSON_CreateObject(); | ||
| 491 | char bssid_str[18]; | ||
| 492 | snprintf(bssid_str, sizeof(bssid_str), "%02X:%02X:%02X:%02X:%02X:%02X", | ||
| 493 | e->bssid[0], e->bssid[1], e->bssid[2], | ||
| 494 | e->bssid[3], e->bssid[4], e->bssid[5]); | ||
| 495 | cJSON_AddStringToObject(entry, "bssid", bssid_str); | ||
| 496 | cJSON_AddStringToObject(entry, "ssid", e->ssid[0] ? e->ssid : "unknown"); | ||
| 497 | cJSON_AddNumberToObject(entry, "rssi", e->rssi); | ||
| 498 | cJSON_AddNumberToObject(entry, "price_per_step", e->price_per_step); | ||
| 499 | cJSON_AddNumberToObject(entry, "step_size", (double)e->step_size); | ||
| 500 | cJSON_AddStringToObject(entry, "metric", e->metric ? "bytes" : "milliseconds"); | ||
| 501 | if (e->geohash[0]) cJSON_AddStringToObject(entry, "geohash", e->geohash); | ||
| 502 | cJSON_AddItemToArray(entries, entry); | ||
| 503 | } | ||
| 504 | cJSON_AddItemToObject(root, "entries", entries); | ||
| 505 | |||
| 506 | char *json = cJSON_PrintUnformatted(root); | ||
| 507 | httpd_resp_set_type(req, "application/json"); | ||
| 508 | httpd_resp_send(req, json, strlen(json)); | ||
| 509 | cJSON_free(json); | ||
| 510 | cJSON_Delete(root); | ||
| 511 | return ESP_OK; | ||
| 512 | } | ||
| 513 | |||
| 514 | static const httpd_uri_t uri_market = { .uri = "/market", .method = HTTP_GET, .handler = api_get_market }; | ||
| 515 | |||
| 474 | esp_err_t tollgate_api_start(void) | 516 | esp_err_t tollgate_api_start(void) |
| 475 | { | 517 | { |
| 476 | if (s_api_server) return ESP_OK; | 518 | if (s_api_server) return ESP_OK; |
| @@ -494,6 +536,7 @@ esp_err_t tollgate_api_start(void) | |||
| 494 | httpd_register_uri_handler(s_api_server, &uri_wallet); | 536 | httpd_register_uri_handler(s_api_server, &uri_wallet); |
| 495 | httpd_register_uri_handler(s_api_server, &uri_wallet_swap); | 537 | httpd_register_uri_handler(s_api_server, &uri_wallet_swap); |
| 496 | httpd_register_uri_handler(s_api_server, &uri_wallet_send); | 538 | httpd_register_uri_handler(s_api_server, &uri_wallet_send); |
| 539 | httpd_register_uri_handler(s_api_server, &uri_market); | ||
| 497 | 540 | ||
| 498 | ESP_LOGI(TAG, "TollGate API started on port 2121"); | 541 | ESP_LOGI(TAG, "TollGate API started on port 2121"); |
| 499 | return ESP_OK; | 542 | return ESP_OK; |
diff --git a/main/tollgate_client.c b/main/tollgate_client.c index ac8dcfe..a81d16f 100644 --- a/main/tollgate_client.c +++ b/main/tollgate_client.c | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | #include "tollgate_client.h" | 1 | #include "tollgate_client.h" |
| 2 | #include "config.h" | 2 | #include "config.h" |
| 3 | #include "nucula_wallet.h" | 3 | #include "nucula_wallet.h" |
| 4 | #include "market.h" | ||
| 4 | #include "esp_log.h" | 5 | #include "esp_log.h" |
| 5 | #include "esp_http_client.h" | 6 | #include "esp_http_client.h" |
| 6 | #include "esp_crt_bundle.h" | 7 | #include "esp_crt_bundle.h" |
| @@ -343,6 +344,19 @@ esp_err_t tollgate_client_on_sta_connected(const char *gw_ip_str) | |||
| 343 | s_state = TG_CLIENT_PAID; | 344 | s_state = TG_CLIENT_PAID; |
| 344 | 345 | ||
| 345 | ESP_LOGI(TAG, "upstream TollGate paid: %lldms allotment", (long long)allotment); | 346 | ESP_LOGI(TAG, "upstream TollGate paid: %lldms allotment", (long long)allotment); |
| 347 | |||
| 348 | const market_t *mkt = market_get(); | ||
| 349 | int cheapest = market_find_cheapest(); | ||
| 350 | if (cheapest >= 0 && mkt->entries[cheapest].valid && mkt->entries[cheapest].ssid[0] != '\0') { | ||
| 351 | uint32_t upstream_step = s_discovery.step_size_ms > 0 ? s_discovery.step_size_ms : 1; | ||
| 352 | uint32_t upstream_eff = (uint32_t)s_discovery.price_per_step * 60000 / upstream_step; | ||
| 353 | uint32_t cheap_step = mkt->entries[cheapest].step_size > 0 ? mkt->entries[cheapest].step_size : 1; | ||
| 354 | uint32_t cheap_eff = (uint32_t)mkt->entries[cheapest].price_per_step * 60000 / cheap_step; | ||
| 355 | if (cheap_eff < upstream_eff) { | ||
| 356 | ESP_LOGW(TAG, "CHEAPER TOLLGATE AVAILABLE: %s at %lu sats/min vs upstream %lu sats/min", | ||
| 357 | mkt->entries[cheapest].ssid, (unsigned long)cheap_eff, (unsigned long)upstream_eff); | ||
| 358 | } | ||
| 359 | } | ||
| 346 | return ESP_OK; | 360 | return ESP_OK; |
| 347 | } | 361 | } |
| 348 | 362 | ||
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 4741765..f062cb6 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c | |||
| @@ -27,6 +27,8 @@ | |||
| 27 | #include "local_relay.h" | 27 | #include "local_relay.h" |
| 28 | #include "relay_selector.h" | 28 | #include "relay_selector.h" |
| 29 | #include "sync_manager.h" | 29 | #include "sync_manager.h" |
| 30 | #include "beacon_price.h" | ||
| 31 | #include "market.h" | ||
| 30 | 32 | ||
| 31 | #define MAX_STA_RETRY 5 | 33 | #define MAX_STA_RETRY 5 |
| 32 | static const char *TAG = "tollgate_main"; | 34 | static const char *TAG = "tollgate_main"; |
| @@ -38,6 +40,8 @@ static esp_netif_t *s_sta_netif = NULL; | |||
| 38 | static esp_netif_t *s_ap_netif = NULL; | 40 | static esp_netif_t *s_ap_netif = NULL; |
| 39 | static int s_retry_count = 0; | 41 | static int s_retry_count = 0; |
| 40 | static bool s_services_running = false; | 42 | static bool s_services_running = false; |
| 43 | static bool s_ap_services_running = false; | ||
| 44 | static bool s_sta_connecting = false; | ||
| 41 | static SemaphoreHandle_t s_services_mutex = NULL; | 45 | static SemaphoreHandle_t s_services_mutex = NULL; |
| 42 | static char s_ap_ip_str[16] = "10.0.0.1"; | 46 | static char s_ap_ip_str[16] = "10.0.0.1"; |
| 43 | 47 | ||
| @@ -46,23 +50,42 @@ static sync_manager_t s_sync_manager; | |||
| 46 | 50 | ||
| 47 | static void start_services(void); | 51 | static void start_services(void); |
| 48 | static void stop_services(void); | 52 | static void stop_services(void); |
| 53 | static void start_ap_services(void); | ||
| 54 | |||
| 55 | static void start_ap_services(void) | ||
| 56 | { | ||
| 57 | if (s_ap_services_running) return; | ||
| 58 | |||
| 59 | tollgate_api_start(); | ||
| 60 | beacon_price_start(); | ||
| 61 | market_init(); | ||
| 62 | |||
| 63 | s_ap_services_running = true; | ||
| 64 | ESP_LOGI(TAG, "=== AP-only services started (no STA) ==="); | ||
| 65 | } | ||
| 49 | 66 | ||
| 50 | static void wifi_event_handler(void *arg, esp_event_base_t event_base, | 67 | static void wifi_event_handler(void *arg, esp_event_base_t event_base, |
| 51 | int32_t event_id, void *event_data) | 68 | int32_t event_id, void *event_data) |
| 52 | { | 69 | { |
| 53 | if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { | 70 | if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { |
| 54 | wifi_config_t wifi_cfg; | 71 | if (!s_sta_connecting) { |
| 55 | if (tollgate_config_get_wifi(&wifi_cfg) == ESP_OK) { | 72 | wifi_config_t wifi_cfg; |
| 56 | esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); | 73 | if (tollgate_config_get_wifi(&wifi_cfg) == ESP_OK) { |
| 74 | esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg); | ||
| 75 | } | ||
| 76 | s_sta_connecting = true; | ||
| 77 | esp_wifi_connect(); | ||
| 57 | } | 78 | } |
| 58 | esp_wifi_connect(); | ||
| 59 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { | 79 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { |
| 80 | wifi_event_sta_disconnected_t *disc = (wifi_event_sta_disconnected_t *)event_data; | ||
| 60 | s_retry_count++; | 81 | s_retry_count++; |
| 61 | ESP_LOGW(TAG, "WiFi disconnected, retry %d/%d", s_retry_count, MAX_STA_RETRY); | 82 | s_sta_connecting = false; |
| 83 | ESP_LOGW(TAG, "WiFi disconnected, reason=%d, retry %d/%d", disc->reason, s_retry_count, MAX_STA_RETRY); | ||
| 62 | tollgate_client_on_sta_disconnected(); | 84 | tollgate_client_on_sta_disconnected(); |
| 63 | if (s_services_running) stop_services(); | 85 | if (s_services_running) stop_services(); |
| 64 | if (s_retry_count < MAX_STA_RETRY) { | 86 | if (s_retry_count < MAX_STA_RETRY) { |
| 65 | vTaskDelay(pdMS_TO_TICKS(2000)); | 87 | vTaskDelay(pdMS_TO_TICKS(2000)); |
| 88 | s_sta_connecting = true; | ||
| 66 | esp_wifi_connect(); | 89 | esp_wifi_connect(); |
| 67 | } else { | 90 | } else { |
| 68 | wifi_config_t wifi_cfg; | 91 | wifi_config_t wifi_cfg; |
| @@ -72,7 +95,11 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, | |||
| 72 | int idx = cfg->current_network; | 95 | int idx = cfg->current_network; |
| 73 | ESP_LOGI(TAG, "Trying WiFi network %d: %s", idx, cfg->networks[idx].ssid); | 96 | ESP_LOGI(TAG, "Trying WiFi network %d: %s", idx, cfg->networks[idx].ssid); |
| 74 | s_retry_count = 0; | 97 | s_retry_count = 0; |
| 98 | vTaskDelay(pdMS_TO_TICKS(2000)); | ||
| 99 | s_sta_connecting = true; | ||
| 75 | esp_wifi_connect(); | 100 | esp_wifi_connect(); |
| 101 | } else { | ||
| 102 | ESP_LOGI(TAG, "All WiFi networks exhausted, STA stopped (market scans active)"); | ||
| 76 | } | 103 | } |
| 77 | } | 104 | } |
| 78 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) { | 105 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) { |
| @@ -85,6 +112,8 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, | |||
| 85 | ESP_LOGI(TAG, "Station disconnected: MAC=%02x:%02x:%02x:%02x:%02x:%02x", | 112 | ESP_LOGI(TAG, "Station disconnected: MAC=%02x:%02x:%02x:%02x:%02x:%02x", |
| 86 | event->mac[0], event->mac[1], event->mac[2], | 113 | event->mac[0], event->mac[1], event->mac[2], |
| 87 | event->mac[3], event->mac[4], event->mac[5]); | 114 | event->mac[3], event->mac[4], event->mac[5]); |
| 115 | } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) { | ||
| 116 | start_ap_services(); | ||
| 88 | } | 117 | } |
| 89 | } | 118 | } |
| 90 | 119 | ||
| @@ -163,7 +192,11 @@ static void start_services(void) | |||
| 163 | 192 | ||
| 164 | dns_server_start(ap_ip_info.ip, upstream_dns); | 193 | dns_server_start(ap_ip_info.ip, upstream_dns); |
| 165 | captive_portal_start(cfg->ap_ip_str); | 194 | captive_portal_start(cfg->ap_ip_str); |
| 166 | tollgate_api_start(); | 195 | if (!s_ap_services_running) { |
| 196 | tollgate_api_start(); | ||
| 197 | beacon_price_start(); | ||
| 198 | market_init(); | ||
| 199 | } | ||
| 167 | 200 | ||
| 168 | relay_selector_init(&s_relay_selector); | 201 | relay_selector_init(&s_relay_selector); |
| 169 | relay_selector_seed_from_config(&s_relay_selector); | 202 | relay_selector_seed_from_config(&s_relay_selector); |
| @@ -198,7 +231,10 @@ static void stop_services(void) | |||
| 198 | } | 231 | } |
| 199 | 232 | ||
| 200 | captive_portal_stop(); | 233 | captive_portal_stop(); |
| 201 | tollgate_api_stop(); | 234 | if (!s_ap_services_running) { |
| 235 | tollgate_api_stop(); | ||
| 236 | beacon_price_stop(); | ||
| 237 | } | ||
| 202 | dns_server_stop(); | 238 | dns_server_stop(); |
| 203 | cvm_server_stop(); | 239 | cvm_server_stop(); |
| 204 | sync_manager_stop(&s_sync_manager); | 240 | sync_manager_stop(&s_sync_manager); |
| @@ -321,8 +357,7 @@ void app_main(void) | |||
| 321 | ESP_LOGI(TAG, "STA configured for SSID: %s", tcfg2->networks[tcfg2->current_network].ssid); | 357 | ESP_LOGI(TAG, "STA configured for SSID: %s", tcfg2->networks[tcfg2->current_network].ssid); |
| 322 | } | 358 | } |
| 323 | 359 | ||
| 324 | ESP_ERROR_CHECK(esp_wifi_set_country_code("DE", false)); | 360 | ESP_ERROR_CHECK(esp_wifi_set_country_code("DE", true)); |
| 325 | ESP_LOGI(TAG, "WiFi country code set to DE (EU regulatory domain)"); | ||
| 326 | 361 | ||
| 327 | ESP_ERROR_CHECK(esp_wifi_start()); | 362 | ESP_ERROR_CHECK(esp_wifi_start()); |
| 328 | 363 | ||
| @@ -341,5 +376,6 @@ void app_main(void) | |||
| 341 | session_tick(); | 376 | session_tick(); |
| 342 | tollgate_client_tick(); | 377 | tollgate_client_tick(); |
| 343 | lightning_payout_tick(); | 378 | lightning_payout_tick(); |
| 379 | market_tick(); | ||
| 344 | } | 380 | } |
| 345 | } | 381 | } |