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 /tests/integration/test-market.mjs | |
| 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 'tests/integration/test-market.mjs')
| -rw-r--r-- | tests/integration/test-market.mjs | 60 |
1 files changed, 60 insertions, 0 deletions
diff --git a/tests/integration/test-market.mjs b/tests/integration/test-market.mjs new file mode 100644 index 0000000..20f062f --- /dev/null +++ b/tests/integration/test-market.mjs | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | import { execSync } from 'child_process'; | ||
| 2 | |||
| 3 | const API_URL = `http://${process.env.TOLLGATE_IP || '10.192.45.1'}:2121`; | ||
| 4 | |||
| 5 | function run(cmd) { | ||
| 6 | try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } | ||
| 7 | catch (e) { return e.stdout || null; } | ||
| 8 | } | ||
| 9 | |||
| 10 | function runJson(cmd) { | ||
| 11 | const out = run(cmd); | ||
| 12 | try { return out ? JSON.parse(out) : null; } | ||
| 13 | catch { return null; } | ||
| 14 | } | ||
| 15 | |||
| 16 | let passed = 0, failed = 0; | ||
| 17 | function assert(cond, msg) { | ||
| 18 | if (cond) { console.log(` PASS: ${msg}`); passed++; } | ||
| 19 | else { console.log(` FAIL: ${msg}`); failed++; } | ||
| 20 | } | ||
| 21 | |||
| 22 | console.log('=== test-market (GET /market) ===\n'); | ||
| 23 | |||
| 24 | console.log('--- /market endpoint responds ---'); | ||
| 25 | { | ||
| 26 | const data = runJson(`curl -s --connect-timeout 5 ${API_URL}/market`); | ||
| 27 | assert(data !== null, '/market returns valid JSON'); | ||
| 28 | assert(typeof data.count === 'number', `count is number (got ${data?.count})`); | ||
| 29 | assert(Array.isArray(data.entries), 'entries is array'); | ||
| 30 | } | ||
| 31 | |||
| 32 | console.log('\n--- /market entry structure ---'); | ||
| 33 | { | ||
| 34 | const data = runJson(`curl -s --connect-timeout 5 ${API_URL}/market`); | ||
| 35 | if (data && data.entries && data.entries.length > 0) { | ||
| 36 | const e = data.entries[0]; | ||
| 37 | assert(typeof e.bssid === 'string', `bssid is string (got ${e.bssid})`); | ||
| 38 | assert(typeof e.ssid === 'string', `ssid is string (got ${e.ssid})`); | ||
| 39 | assert(typeof e.rssi === 'number', `rssi is number (got ${e.rssi})`); | ||
| 40 | assert(typeof e.price_per_step === 'number', `price_per_step is number (got ${e.price_per_step})`); | ||
| 41 | assert(typeof e.step_size === 'number', `step_size is number (got ${e.step_size})`); | ||
| 42 | assert(typeof e.metric === 'string', `metric is string (got ${e.metric})`); | ||
| 43 | } else { | ||
| 44 | console.log(' SKIP: no entries found (scan may not have run yet)'); | ||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | console.log('\n--- /market with no discovered TollGates ---'); | ||
| 49 | { | ||
| 50 | const data = runJson(`curl -s --connect-timeout 5 ${API_URL}/market`); | ||
| 51 | if (data && data.count === 0) { | ||
| 52 | assert(data.entries.length === 0, 'empty entries array when count=0'); | ||
| 53 | console.log(' INFO: no nearby TollGates discovered yet (expected if only one board)'); | ||
| 54 | } else if (data && data.count > 0) { | ||
| 55 | console.log(` INFO: ${data.count} nearby TollGate(s) discovered`); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); | ||
| 60 | process.exit(failed > 0 ? 1 : 0); | ||