upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-19 13:14:48 +0530
committerYour Name <you@example.com>2026-05-19 13:14:48 +0530
commitfe6aa9663d4cdabdc6e71db6068f8cd9e3739ffe (patch)
tree8cadb07243c07a6b3fa9453b239c9ac5cb02b454 /main
parent77031f06a9a87320d011f501590985161d1eb305 (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.txt2
-rw-r--r--main/beacon_price.c103
-rw-r--r--main/beacon_price.h44
-rw-r--r--main/config.h4
-rw-r--r--main/cvm_server.c3
-rw-r--r--main/market.c237
-rw-r--r--main/market.h40
-rw-r--r--main/tollgate_api.c43
-rw-r--r--main/tollgate_client.c14
-rw-r--r--main/tollgate_main.c54
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
9static const char *TAG = "beacon_price";
10static bool s_active = false;
11
12void 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
19void 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
26void 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
65esp_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
93esp_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
16typedef 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
30typedef 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
38esp_err_t beacon_price_start(void);
39esp_err_t beacon_price_stop(void);
40void beacon_price_build_ie(tollgate_price_ie_t *ie);
41void beacon_price_hash_mint(const char *mint_url, uint8_t hash_out[4]);
42void 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
74void tollgate_config_derive_unique(tollgate_config_t *cfg); 78void 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
11static const char *TAG = "market";
12static market_t s_market;
13static bool s_initialized = false;
14
15static int64_t get_time_ms(void)
16{
17 return (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
18}
19
20static 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
25static 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
35static 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
51void 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
97static 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
106static 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
143static esp_event_handler_instance_t s_scan_done_handler = NULL;
144
145esp_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
168void 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
206const market_t *market_get(void)
207{
208 return &s_market;
209}
210
211int 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
12typedef 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
26typedef 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
34esp_err_t market_init(void);
35void market_tick(void);
36const market_t *market_get(void);
37int market_find_cheapest(void);
38void 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
471static const httpd_uri_t uri_wallet_swap = { .uri = "/wallet/swap", .method = HTTP_POST, .handler = api_post_wallet_swap }; 474static const httpd_uri_t uri_wallet_swap = { .uri = "/wallet/swap", .method = HTTP_POST, .handler = api_post_wallet_swap };
472static const httpd_uri_t uri_wallet_send = { .uri = "/wallet/send", .method = HTTP_POST, .handler = api_post_wallet_send }; 475static const httpd_uri_t uri_wallet_send = { .uri = "/wallet/send", .method = HTTP_POST, .handler = api_post_wallet_send };
473 476
477static 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
514static const httpd_uri_t uri_market = { .uri = "/market", .method = HTTP_GET, .handler = api_get_market };
515
474esp_err_t tollgate_api_start(void) 516esp_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
32static const char *TAG = "tollgate_main"; 34static const char *TAG = "tollgate_main";
@@ -38,6 +40,8 @@ static esp_netif_t *s_sta_netif = NULL;
38static esp_netif_t *s_ap_netif = NULL; 40static esp_netif_t *s_ap_netif = NULL;
39static int s_retry_count = 0; 41static int s_retry_count = 0;
40static bool s_services_running = false; 42static bool s_services_running = false;
43static bool s_ap_services_running = false;
44static bool s_sta_connecting = false;
41static SemaphoreHandle_t s_services_mutex = NULL; 45static SemaphoreHandle_t s_services_mutex = NULL;
42static char s_ap_ip_str[16] = "10.0.0.1"; 46static 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
47static void start_services(void); 51static void start_services(void);
48static void stop_services(void); 52static void stop_services(void);
53static void start_ap_services(void);
54
55static 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
50static void wifi_event_handler(void *arg, esp_event_base_t event_base, 67static 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}