cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
boot.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
12
13#include "pos_app.h"
14
15/* Quiet CW_Logger: swallows the SDK's verbose connection/retry chatter on the
16 * UART. Our own logs go through ESP_LOGx. */
17class NullLogger : public CW_Logger {
18public:
19 bool begin(unsigned long) override { return true; }
20 void print(const __FlashStringHelper*) override {}
21 void print(const char*) override {}
22 void print(char) override {}
23 void print(uint8_t, int) override {}
24 void print(uint16_t, int) override {}
25 void print(uint32_t, int) override {}
26 void print(int, int) override {}
27 void println() override {}
28 void println(const __FlashStringHelper*) override {}
29 void println(const char*) override {}
30 void println(char) override {}
31 void println(uint8_t, int) override {}
32 void println(uint16_t, int) override {}
33 void println(uint32_t, int) override {}
34 void println(int, int) override {}
35};
36
37/* PN532 on I²C, wired to the CYD CN1 connector. */
38#define PN532_I2C_PORT 0
39#define PN532_SDA 27
40#define PN532_SCL 22
41#define PN532_IRQ (-1)
42#define PN532_RST (-1)
43#define PN532_I2C_HZ 100000U
44
45/* The CYD's onboard RGB LED, common anode: HIGH is off, a floating pin glows.
46 * Unused, so it is driven off at boot. */
47#define LED_R GPIO_NUM_4
48#define LED_G GPIO_NUM_16
49#define LED_B GPIO_NUM_17
50
51/* Startup retry budgets. The effort is spent out here rather than inside
52 * net_wifi_connect(), since each call resets the association properly:
53 * 3 x (1 + WIFI_MAX_RETRY) associations, 45 s worst case. */
54#define WIFI_SAVED_ATTEMPTS 3U
55#define TIME_SYNC_ATTEMPTS 3U
56
57/* Picker notes, shared by boot and the settings Wi-Fi change. */
58const char *const NOTE_JOIN_FAILED =
59 "Could not join that network - check the password";
60const char *const NOTE_NO_TIME =
61 "No network time - this Wi-Fi has no usable internet";
62
63/* Credentials the picker just joined with, held until a clock sync proves the
64 * network usable end to end (see wifi_keep_or_drop). Associating is not enough:
65 * a captive-portal or offline AP joins fine and would then be reached for on
66 * every boot. Deferring the write also means a transient NTP outage never
67 * erases a saved network that does work. */
68static char s_join_ssid[33] = { 0 };
69static char s_join_pass[65] = { 0 };
70
77void wifi_keep_or_drop(bool keep)
78{
79 if (keep && (s_join_ssid[0] != '\0')) {
81 ESP_LOGI(TAG, "saved network '%s' (clock synced)", s_join_ssid);
82 }
83 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_join_pass), sizeof(s_join_pass));
84 (void)memset(s_join_ssid, 0, sizeof(s_join_ssid));
85}
86
95{
96 ui_msg_t msg;
97 bool got = false;
98 while (!got) {
99 if (xQueueReceive(s_ui_queue, &msg, portMAX_DELAY) != pdTRUE) { continue; }
100 got = (msg.event == want);
101 }
102 (void)xQueueReset(s_ui_queue);
103}
104
114{
115 char ssid[33] = { 0 };
116 char pass[65] = { 0 };
117 if (!settings_get_wifi(ssid, sizeof(ssid), pass, sizeof(pass))) {
118 return false;
119 }
120
121 ui_set_boot_status("Connecting to Wi-Fi");
122 bool ok = false;
123 for (uint32_t a = 1U; (a <= WIFI_SAVED_ATTEMPTS) && !ok; a++) {
124 ESP_LOGI(TAG, "Wi-Fi '%s': attempt %" PRIu32 "/%u",
125 ssid, a, WIFI_SAVED_ATTEMPTS);
126 ok = net_wifi_connect(ssid, pass);
127 }
128 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(pass), sizeof(pass));
129 if (!ok) {
130 ESP_LOGW(TAG, "saved network '%s' failed %u times",
131 ssid, WIFI_SAVED_ATTEMPTS);
132 }
133 return ok;
134}
135
151bool wifi_picker(const char *note)
152{
153 net_wifi_ap_t aps[16];
154 uint16_t n = net_wifi_scan(aps, 16);
155 ui_show_wifi_list(aps, n, note);
156
157 ui_msg_t msg;
158 while (true) {
159 if (xQueueReceive(s_ui_queue, &msg, portMAX_DELAY) != pdTRUE) { continue; }
160
161 if (msg.event == UI_EVENT_WIFI_TRY) {
162 char w_ssid[33] = { 0 };
163 char w_pass[65] = { 0 };
164 bool ok = false;
165 if (ui_take_wifi_creds(w_ssid, sizeof(w_ssid),
166 w_pass, sizeof(w_pass)) > 0U) {
167 /* Interactive: the operator expects to see the attempt. */
169 ok = net_wifi_connect(w_ssid, w_pass);
170 if (ok) {
171 /* Staged, not saved — wifi_keep_or_drop() decides once the
172 * clock has proven this network carries real internet. */
173 (void)snprintf(s_join_ssid, sizeof(s_join_ssid), "%s", w_ssid);
174 (void)snprintf(s_join_pass, sizeof(s_join_pass), "%s", w_pass);
175 }
176 }
177 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(w_pass), sizeof(w_pass));
178 if (ok) { return true; } /* the picker owns the screen */
179 note = NOTE_JOIN_FAILED;
180 } else if (msg.event == UI_EVENT_WIFI_SCAN) {
181 note = NULL; /* rescan asked for — the old reason is stale */
182 }
183
184 /* WIFI_SCAN, a failed connect, or any stray event: re-scan and show
185 * the list again so the user stays in setup until connected. */
186 n = net_wifi_scan(aps, 16);
187 ui_show_wifi_list(aps, n, note);
188 }
189}
190
209static bool resolve_evm_payout(bool *rejected);
210
211/* How long a startup fault stays on the panel before the terminal restarts. */
212#define BOOT_FAULT_RESTART_S 30U
213
223void boot_fault(ui_boot_err_t kind, const char *detail)
224{
225 ui_show_boot_error(kind, detail);
226 ESP_LOGE(TAG, "startup fault - restarting in %u s",
227 static_cast<unsigned>(BOOT_FAULT_RESTART_S));
228 vTaskDelay(pdMS_TO_TICKS(BOOT_FAULT_RESTART_S * 1000U));
229 esp_restart();
230}
231
232static bool resolve_evm_payout(bool *rejected)
233{
234 if (settings_get_payout(false, s_payout_eth, sizeof(s_payout_eth)) &&
236 ESP_LOGE(TAG, "stored Ethereum payout address rejected");
237 (void)snprintf(s_payout_eth, sizeof(s_payout_eth), "0x%s", ADDR_TO);
238 *rejected = true;
239 }
240 /* Twice, each pass independent — that is what makes it a dual store. */
241 if (!eth_addr_parse(s_payout_eth, s_dest.addr) ||
242 !eth_addr_parse(s_payout_eth, s_dest.addr_echo)) {
243 ESP_LOGE(TAG, "Bad ADDR_TO in config");
244 boot_fault(UI_BOOT_ERR_CONFIG, "Bad ADDR_TO in config");
245 return false; /* not reached */
246 }
247 return true;
248}
249
269bool run_wizard(CryptnoxWallet &wallet, Pn532NfcTransport &transport,
270 CW_CryptoProvider &crypto, bool wifi_only)
271{
273 ESP_LOGE(TAG, "setup portal unavailable - falling back to the panel");
274 return false;
275 }
276
277 /* The Wi-Fi scan runs on THIS task as the step opens: it hops the radio
278 * across channels and briefly drops SoftAP clients, so inside an HTTP handler
279 * it would drop the very browser asking. */
280 auto enter_step = [](prov_step_t step) {
281 prov_set_step(step);
282 ui_show_prov(step);
283 if (step == PROV_STEP_WIFI) {
284 net_wifi_ap_t aps[16];
285 const uint16_t n_aps = net_wifi_scan(aps, 16);
286 prov_set_scan(aps, n_aps);
287 }
288 };
289
290 /* A configured terminal that only lost its network opens on the Wi-Fi step
291 * with nothing to authorise. The page still cannot store anything without the
292 * panel, and the AP passphrase it was reached through is on that panel. */
293 if (wifi_only) { prov_set_wifi_only(); }
294 enter_step(wifi_only ? PROV_STEP_WIFI : PROV_STEP_AUTH);
295
296 /* One tap yields both addresses but only one value can wait on the panel, so
297 * the second is parked here until the first is resolved. */
298 char card_eth[SETTINGS_PAYOUT_MAX] = "";
299 char card_tron[SETTINGS_PAYOUT_MAX] = "";
300
301 ui_msg_t msg;
302 while (true) {
303 if (xQueueReceive(s_ui_queue, &msg, portMAX_DELAY) != pdTRUE) { continue; }
304
305 switch (msg.event) {
307 /* A browser asks to be let in: demand the admin code on the panel.
308 * prov_auth_resolve() reports a grant back as PROV_NEXT; a refusal
309 * reports nothing and the browser can ask again. */
311 break;
312
313 /* One event moves the flow on, whether it came from the browser's
314 * Continue button or from prov_auth_resolve() letting it in. */
316 if (!prov_authed()) { break; }
317 if (prov_step() == PROV_STEP_AUTH) {
318 prov_set_note("");
319 enter_step(wifi_only ? PROV_STEP_WIFI : PROV_STEP_ADDR);
320 } else if (prov_step() == PROV_STEP_ADDR) {
321 /* Refuse to leave the address step with nothing set: with no
322 * payout address the wizard would end on a till that cannot
323 * take a payment. */
324 if (!settings_has_payout(false) && !settings_has_payout(true)) {
325 prov_set_note("Set at least one payout address first.");
326 break;
327 }
328 prov_set_note("");
329 /* A terminal that already has a network is finished here; the
330 * addresses were all that was missing. The AP goes down with the
331 * portal, so the last word is on the panel. */
332 if (settings_has_wifi()) {
333 prov_stop();
335 } else {
336 ui_set_prov_note(""); /* the address step's reason, if any */
337 enter_step(PROV_STEP_WIFI);
338 }
339 } else {
340 /* Nowhere left to go, but refresh the panel: this is also how
341 * the Wi-Fi-only flow reports that a browser walked in. */
343 }
344 break;
345
346 case UI_EVENT_PROV_SCAN: {
347 net_wifi_ap_t aps[16];
348 const uint16_t n_aps = net_wifi_scan(aps, 16);
349 prov_set_scan(aps, n_aps);
350 break;
351 }
352
354 /* Collect the PIN first: the card will not export a key without
355 * it. */
356 card_eth[0] = '\0';
357 card_tron[0] = '\0';
358 s_user_cancelled = false;
359 prov_set_note("Follow the terminal screen.");
361 break;
362
363 case UI_EVENT_CARD_PIN: {
364 char pin[16] = { 0 };
365 size_t pin_chars = ui_take_pin(pin, sizeof(pin));
366 char err[48] = { 0 };
367 const bool got = card_read_payouts(wallet, transport, crypto,
368 pin, pin_chars,
369 card_eth, sizeof(card_eth),
370 card_tron, sizeof(card_tron),
371 err, sizeof(err));
372 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(pin), sizeof(pin));
373
374 ui_show_prov(prov_step()); /* back to the QR/step screen */
375 if (!got) {
376 prov_set_note(err);
377 ui_set_prov_note(err); /* and on the panel they tapped */
378 break;
379 }
380 prov_set_note("Accept each address on the terminal screen.");
381 /* Ethereum first if there is one; the Tron one waits in card_tron
382 * and is offered when this proposal is resolved. */
383 if (card_eth[0] != '\0') {
384 (void)prov_propose(PROV_ASK_PAYOUT_ETH, card_eth);
385 card_eth[0] = '\0';
386 } else if (card_tron[0] != '\0') {
387 (void)prov_propose(PROV_ASK_PAYOUT_TRON, card_tron);
388 card_tron[0] = '\0';
389 }
390 break;
391 }
392
394 ui_show_prov_confirm(); /* decimals: see token_decimals_ok */
395 break;
396
399 /* Accepted or refused, the panel slot is free again — so offer the
400 * second card-derived address if one is still parked. */
401 if (card_tron[0] != '\0') {
402 (void)prov_propose(PROV_ASK_PAYOUT_TRON, card_tron);
403 card_tron[0] = '\0';
404 }
405 break;
406
407 case UI_EVENT_WIFI_TRY: {
408 if (!prov_authed()) { break; }
409 ui_set_prov_note(""); /* a new attempt drops the last reason */
410 char w_ssid[33] = { 0 };
411 char w_pass[65] = { 0 };
412 bool joined = false;
413 if (ui_take_wifi_creds(w_ssid, sizeof(w_ssid),
414 w_pass, sizeof(w_pass)) > 0U) {
416 /* Associating proves nothing (a captive-portal or offline AP
417 * joins fine). The clock is the proof, and TLS needs it too. */
418 if (!net_wifi_connect(w_ssid, w_pass)) {
420 ui_set_prov_note(NOTE_JOIN_FAILED); /* and on the panel */
421 /* It may still be half-associated (DHCP times out in
422 * net_wifi_connect(), not the driver); a late lease would
423 * put the setup forms on that LAN. Back to AP-only. */
425 } else if (!net_time_sync(15000U)) {
428 /* Joined but not kept, and the portal stays up. Its HTTP
429 * server binds every interface, so the association would
430 * leave the setup forms open to the venue LAN, which the
431 * AP passphrase does not guard. Drop it. */
433 } else {
434 settings_set_wifi(w_ssid, w_pass);
435 joined = true;
436 }
437 }
438 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(w_pass),
439 sizeof(w_pass));
440 if (joined) {
441 /* Done. The AP goes down with the portal (the phone was
442 * dropped when we joined), so the last word is on the panel. */
443 prov_stop();
445 } else {
446 /* Back on the setup AP alone. Rescan: the list is a minute old
447 * and the network that would not join may have gone. */
448 enter_step(PROV_STEP_WIFI);
449 }
450 break;
451 }
452
454 /* Asked for by the UI (see request_prov_stop in ui.cpp), done
455 * here because it blocks for up to ~2 s. */
456 if (prov_mode() != PROV_MODE_OFF) { prov_stop(); }
457 break;
458
460 /* Restart to apply. The recipient and contract dual stores are
461 * built at boot from validated strings; rebuilding them in place
462 * would be a second, re-validating path into the money code. */
463 prov_stop();
464 ESP_LOGW(TAG, "setup finished - restarting to apply it");
465 ui_set_boot_status("Applying settings");
466 vTaskDelay(pdMS_TO_TICKS(600)); /* let the screen land */
467 esp_restart();
468 break;
469
470 default:
471 break;
472 }
473 }
474}
475
483bool sync_time(void)
484{
485 for (uint32_t a = 1U; a <= TIME_SYNC_ATTEMPTS; a++) {
486 ESP_LOGI(TAG, "SNTP sync: attempt %" PRIu32 "/%u", a, TIME_SYNC_ATTEMPTS);
487 if (net_time_sync(15000U)) { return true; }
488 }
489 return false;
490}
491
497{
498 ESP_LOGI(TAG, "===== cryptnox-pos boot =====");
499
500 /* Before anything slow: the LED is lit from reset until this runs. */
501 for (gpio_num_t p : { LED_R, LED_G, LED_B }) {
502 (void)gpio_set_direction(p, GPIO_MODE_OUTPUT);
503 (void)gpio_set_level(p, 1);
504 }
505#ifdef CRYPTNOX_POS_DEV_BUILD
506 /* build timestamp helps firmware fingerprinting — dev builds only. */
507 ESP_LOGI(TAG, "Build: %s %s", __DATE__, __TIME__);
508#endif
509
510 /* pn532 at INFO emits only a few useful init lines; the adapters stay at
511 * WARN because their per-APDU chatter is verbose. */
512 esp_log_level_set("pn532", ESP_LOG_INFO);
513 esp_log_level_set("pn532_adapter", ESP_LOG_WARN);
514 esp_log_level_set("Pn532NfcTransport", ESP_LOG_WARN);
515
516 /* The PN532 NACKs its address while busy (normal protocol), and i2c.master
517 * logs an error burst for every poll. Mute it; real I2C failures still
518 * propagate through the SDK's return codes. */
519 esp_log_level_set("i2c.master", ESP_LOG_NONE);
520
521 /* ── NVS first: required by the WiFi driver AND by the UI task, which
522 * reads the saved backlight level / Wi-Fi credentials at startup. ── */
523 esp_err_t nvs_ret = nvs_flash_init();
524 if ((nvs_ret == ESP_ERR_NVS_NO_FREE_PAGES) ||
525 (nvs_ret == ESP_ERR_NVS_NEW_VERSION_FOUND)) {
526 ESP_ERROR_CHECK(nvs_flash_erase());
527 nvs_ret = nvs_flash_init();
528 }
529 ESP_ERROR_CHECK(nvs_ret);
530
531 /* Before the UI task, Wi-Fi driver and recipient: erasing the partition needs
532 * every NVS handle shut. Wipes only when this image's BUILD_ID differs from
533 * the one that last ran here (settings.h). */
534 const bool wiped = settings_wipe_if_new_build();
535
536 /* ── UI: splash visible while the rest boots ───────── */
537 s_ui_queue = xQueueCreate(8, sizeof(ui_msg_t));
540 /* No ui_show_splash() here — ui_init() already selects it, and asking twice
541 * races the UI task into rebuilding the screen and replaying the logo. */
542
543 /* Who is paid on EVM: operator-set if there is one, config.h otherwise,
544 * dual-stored either way — see resolve_evm_payout for why a stored value is
545 * allowed to fail and a config.h one is not. */
546 (void)resolve_evm_payout(&s_payout_bad[0]); /* false does not return: boot_fault */
547 /* The panel clock's zone: SNTP sets UTC and the band adds this operator
548 * setting. Logged only. */
549 ESP_LOGI(TAG, "clock: UTC%+d:%02d, DST rule %u",
553 static_cast<unsigned>(settings_get_tz_dst()));
554
555 ESP_LOGI(TAG, "networks: %s",
556 settings_get_mainnet() ? "PRODUCTION" : "test");
557
558 /* Warn if the recipient carries no EIP-55 checksum (no upper-case hex
559 * letter) — the boot-time typo check above is a no-op on an all-lowercase
560 * address. Skip the "0x" so the 'x' is never read as hex. */
561 bool addr_checksummed = false;
562 for (const char *pc = s_payout_eth + 2; *pc != '\0'; ++pc) {
563 if ((*pc >= 'A') && (*pc <= 'F')) { addr_checksummed = true; break; }
564 }
565 if (!addr_checksummed) {
566 ESP_LOGW(TAG, "recipient is all-lowercase: no EIP-55 checksum verified");
567 }
568
569 /* ── PN532 NFC reader ──────────────────────────────────────── */
570 ui_set_boot_status("Starting NFC reader");
571 static pn532_t nfc; /* this and the stack below outlive pos_boot() */
572 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(&nfc), sizeof(nfc));
573
574 pn532_config_t nfc_cfg;
575 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(&nfc_cfg), sizeof(nfc_cfg));
576 nfc_cfg.transport = PN532_TRANSPORT_I2C;
577 nfc_cfg.i2c_port = PN532_I2C_PORT;
578 nfc_cfg.pin_sda = PN532_SDA;
579 nfc_cfg.pin_scl = PN532_SCL;
580 nfc_cfg.pin_irq = PN532_IRQ;
581 nfc_cfg.pin_rst = PN532_RST;
582 nfc_cfg.i2c_clock_hz = PN532_I2C_HZ;
583
584 /* Keep the error code — unplugged reader and misconfigured bus look
585 * identical on screen otherwise. */
586 esp_err_t nfc_ret = pn532_init(&nfc, &nfc_cfg);
587 if (nfc_ret != ESP_OK) {
588 ESP_LOGE(TAG, "PN532 bus init failed: %s", esp_err_to_name(nfc_ret));
589 boot_fault(UI_BOOT_ERR_NFC, esp_err_to_name(nfc_ret));
590 }
591
592 /* pn532_init() only brings up the bus and ignores its own probe results
593 * (pn532.h), so it returns ESP_OK with no reader attached. Probe here —
594 * 0 means no answer — or an absent reader is reported as a wallet fault. */
595 uint32_t nfc_fw = pn532_get_firmware_version(&nfc);
596 if (nfc_fw == 0U) {
597 ESP_LOGE(TAG, "PN532 did not answer GetFirmwareVersion - reader absent?");
598 boot_fault(UI_BOOT_ERR_NFC, "No answer to GetFirmwareVersion");
599 }
600 ESP_LOGI(TAG, "PN532 firmware: IC 0x%02X, version %u.%u",
601 (unsigned)((nfc_fw >> 24) & 0xFFU),
602 (unsigned)((nfc_fw >> 16) & 0xFFU),
603 (unsigned)((nfc_fw >> 8) & 0xFFU));
604
605 /* ── Wallet ────────────────────────────────────────────────── */
606 ui_set_boot_status("Opening wallet");
607 static NullLogger logger;
608 (void)logger.begin(115200UL);
609 static ESP32CryptoProvider cryptoProvider;
610
611 /* Tron recipient: base58check-decoded by the SDK, which validates the
612 * checksum, so a mistyped config.h address fails the boot instead of sending
613 * TRX to a stranger. Decoded twice, independently, into the dual store (§7.1). */
614 uint8_t tron_to21[CW_TRON_ADDRESS_BYTES];
615 uint8_t tron_to21_echo[CW_TRON_ADDRESS_BYTES];
616 /* Same fallback as the Ethereum recipient. This is the authoritative
617 * base58check on a stored Tron address: the setup page only checks length,
618 * prefix and alphabet (the decoder needs the crypto provider). */
619 if (settings_get_payout(true, s_payout_tron, sizeof(s_payout_tron)) &&
620 !CW_Tron::decodeAddress(s_payout_tron, cryptoProvider, tron_to21)) {
621 ESP_LOGE(TAG, "stored Tron payout address rejected - sales refused");
622 (void)snprintf(s_payout_tron, sizeof(s_payout_tron), "%s", TRON_ADDR_TO);
623 s_payout_bad[1] = true;
624 }
625 if (!CW_Tron::decodeAddress(s_payout_tron, cryptoProvider, tron_to21) ||
626 !CW_Tron::decodeAddress(s_payout_tron, cryptoProvider, tron_to21_echo)) {
627 ESP_LOGE(TAG, "Bad TRON_ADDR_TO in config");
628 boot_fault(UI_BOOT_ERR_CONFIG, "Bad TRON_ADDR_TO in config");
629 }
630 (void)CW_Utils::safe_memcpy(s_tron_dest.addr, sizeof(s_tron_dest.addr),
631 &tron_to21[1], ETH_ADDR_LEN);
632 (void)CW_Utils::safe_memcpy(s_tron_dest.addr_echo,
633 sizeof(s_tron_dest.addr_echo),
634 &tron_to21_echo[1], ETH_ADDR_LEN);
635 /* The resolved address, not the literal: the boot log shows where takings
636 * actually go. */
637 ESP_LOGI(TAG, "Tron recipient: %s", s_payout_tron);
638
639 /* Every token's contract, parsed twice into its own store. Non-fatal: a
640 * placeholder contract must still boot; selecting that asset is refused. */
641 for (size_t i = 0U; i < TOKEN_CFG_COUNT; i++) {
642 token_load(&TOKEN_CFG[i], cryptoProvider);
643 }
644
645 /* Refuse to come up selling an asset whose payout address nobody set.
646 *
647 * The picker blocks *switching* to such a network, but the stored chain may
648 * already be one (Ethereum configured, Tron not, chain still Tron), which
649 * would look normal and pay the compile-time recipient. Corrected here, where
650 * both the stored chain and the resolved addresses are in hand. */
655 ESP_LOGW(TAG, "selected chain has no payout address - switching to %d",
656 static_cast<int>(to));
659 } else {
660 /* Neither network is configured: every sale would be refused at
661 * UI_EVENT_AMOUNT_CONFIRMED. The setup below runs the address step
662 * before the main loop; logged for the "won't take payments" case. */
663 ESP_LOGW(TAG, "no payout address configured - running setup");
664 }
665 }
666 static Pn532NfcTransport nfcTransport(&nfc, logger);
667 static ESP32Platform platform;
668 static CryptnoxWallet wallet(nfcTransport, logger, cryptoProvider, platform);
669
670 if (!wallet.begin()) {
671 ESP_LOGE(TAG, "Wallet begin failed");
673 }
674
675 /* The bar for keeping a firmware update: the panel, the card reader and the
676 * wallet layer all came up on this image. Before this line any reset sends
677 * the bootloader back to the previous slot, which is right for a build that
678 * cannot drive its own hardware — see ota.h.
679 *
680 * Deliberately NOT after the network, the setup wizard or an RPC round-trip.
681 * A new build wipes the settings, so its first boot runs the whole wizard; a
682 * power cut, a router still booting or an RPC outage during setup would then
683 * roll a good image back and wipe the settings again. Those are the venue's
684 * problems, not the image's, and they are retried below. */
685 const bool fresh_update = ota_mark_valid();
686
687 /* The first boot on a new image would look like a plain power-cycle, so greet
688 * and name the version on the first screen an operator sees. The wipe itself
689 * happened at the top of this function. */
690 char greeting[96] = "";
691 if (fresh_update || wiped) {
692 char shown[OTA_VERSION_SHOWN_MAX];
693 (void)snprintf(greeting, sizeof(greeting), "Updated to %s.%s",
695 sizeof(shown)),
696 wiped ? " Settings are cleared - set the terminal up again."
697 : "");
698 }
699
700 /* ── WiFi + RPC ────────────────────────────────────────────── */
701 /* URL, credentials and pinned certificate for the selected EVM network.
702 * Re-applied at each payment; here it only aims the readiness probe below. */
705#ifdef TRON_CA_CERT_PEM
706 /* Optional: the Tron node is not trusted anyway (every transaction it
707 * serialises is re-derived and compared before signing, tron_tx.h), but
708 * pinning makes an attacker beat both that check and TLS. */
709 tron_rpc_set_ca_cert(TRON_CA_CERT_PEM);
710#endif
711 /* Name the unpinned endpoints: each trusts any of the ~150 CAs in the
712 * bundle. A release build should normally have none of these lines. */
713#ifndef RPC_CA_CERT_PEM
714 ESP_LOGW(TAG, "TLS: Ethereum RPC not pinned (RPC_CA_CERT_PEM) - full CA bundle");
715#endif
716#ifndef POLY_CA_CERT_PEM
717 ESP_LOGW(TAG, "TLS: Polygon RPC not pinned (POLY_CA_CERT_PEM) - full CA bundle");
718#endif
719#ifndef TRON_CA_CERT_PEM
720 ESP_LOGW(TAG, "TLS: Tron RPC not pinned (TRON_CA_CERT_PEM) - full CA bundle");
721#endif
722 /* ── Not configured: greet, take the admin code, then hand over to the browser ──
723 *
724 * Either one missing runs setup:
725 *
726 * - the admin code: a virgin or factory-reset terminal. Gets the greeting
727 * and the code screen.
728 * - a payout address: a *half*-configured terminal, the worse state. It
729 * looks normal, boots to the amount screen and refuses every sale with
730 * nothing on the panel saying why. Testing for the code alone would skip
731 * the wizard on such a terminal (an admin code survives an update).
732 *
733 * The admin code is the one step that stays on this panel: its value is that
734 * it is never on a network. It must also exist first, because the Wi-Fi
735 * picker's back arrow (honoured by the UI task alone) lands on the amount
736 * screen with the burger menu while main is still in setup; with no code, that
737 * menu would open the settings freely. The creation screen has no way out.
738 *
739 * run_wizard() ends in a restart, which applies the addresses. It only returns
740 * if the portal could not be raised; the panel picker below is the fallback. */
741 const bool no_code = !settings_has_admin_code();
742 const bool no_payout = !settings_has_payout(false) && !settings_has_payout(true);
743 const bool setup_run = no_code || no_payout;
744 if (no_code) {
745 ESP_LOGI(TAG, "no admin code - first-run setup");
746 /* First-run wording, or the update greeting: shown here, since the
747 * wizard ends in a restart and would never reach it. */
748 ui_show_welcome((greeting[0] != '\0') ? greeting : NULL);
749 greeting[0] = '\0';
751
754 } else if (no_payout) {
755 ESP_LOGW(TAG, "admin code stored but no payout address - setup resumes at "
756 "the addresses");
757 }
758 if (setup_run) {
759 (void)run_wizard(wallet, nfcTransport, cryptoProvider, false);
760 }
761
762 ui_set_boot_status("Starting network");
764
765 /* Wi-Fi and a valid clock are one bring-up step, since TLS needs both: a
766 * failed sync sends the operator back to setup with the reason.
767 *
768 * A configured terminal (a payout address, not merely an admin code) that only
769 * lost its network gets the wizard cut short to the Wi-Fi step. The panel
770 * picker is only reached when the SoftAP would not come up. */
771 bool try_saved = true;
772 /* The block above already ran the full wizard, so it has had its turn. */
773 bool offer_setup = !setup_run;
774 const char *net_note = NULL;
775 bool synced = false;
776 /* A fully set-up terminal boots unattended, often before the router after a
777 * power cut. It comes up offline rather than into setup: Wi-Fi re-join and SNTP
778 * retry in the background, and sales are refused until both are back (see
779 * UI_EVENT_AMOUNT_CONFIRMED). */
780 const bool unattended = settings_has_wifi() &&
782 while (true) {
783 const bool joined = try_saved && wifi_try_saved();
784 if (!joined && try_saved && unattended) {
785 ESP_LOGW(TAG, "saved network down - coming up offline, re-joining "
786 "in the background");
789 break;
790 }
791 if (!joined) {
792 /* No usable saved network. The browser flow first, once; then the panel
793 * picker, which is also where a failed clock sync sends us. */
794 if (offer_setup) {
795 offer_setup = false;
796 /* Cut short only for a terminal with a payout address: an admin
797 * code proves setup started, not that it finished, and a terminal
798 * with no address needs the whole wizard. */
799 const bool configured = settings_has_payout(false) ||
801 (void)run_wizard(wallet, nfcTransport, cryptoProvider, configured);
802 /* Only reached if the SoftAP would not come up. */
803 ESP_LOGW(TAG, "no setup portal - panel Wi-Fi picker");
804 }
805 /* Only blame the saved network if there was one — on a terminal that
806 * has never been on a network this is the first screen, not a failure. */
807 if ((net_note == NULL) && settings_has_wifi()) {
808 net_note = "Could not join the saved network";
809 }
810 (void)wifi_picker(net_note);
811 ui_show_splash(); /* the picker took the screen */
812 }
813
814 ui_set_boot_status("Syncing clock");
815 if (sync_time()) {
816 wifi_keep_or_drop(true); /* proven usable — safe to persist */
817 synced = true;
818 break;
819 }
820 ESP_LOGE(TAG, "SNTP time sync failed on this network");
821 if (joined && unattended) {
822 /* The saved network, up but without time yet — a WAN link that is
823 * slower to return than the LAN. Keep it; SNTP retries by itself.
824 * (Re-subscribed in case the failure was the back-dated-clock
825 * refusal, which unsubscribes.) */
826 ESP_LOGW(TAG, "no network time yet - retrying in the background");
828 break;
829 }
830 /* Drop the staged credentials, but leave an already-saved network alone:
831 * the outage is often transient. */
832 wifi_keep_or_drop(false);
833 /* Force the picker: retrying the same network loops straight back here. */
834 try_saved = false;
835 net_note = NOTE_NO_TIME;
836 }
837
838 /* One RPC round-trip at boot: it proves the endpoint is reachable, and its
839 * authenticated Date header can contradict the unauthenticated SNTP clock.
840 * A clock wrong in the *forward* direction (carrying a certificate past its
841 * notAfter) would otherwise only surface mid-payment.
842 *
843 * A warning, not a gate: every payment request re-does the Date check, so
844 * nothing is waved through, and an RPC provider's bad minute must not leave
845 * the till dead. Retried, since one failure is usually a flaky uplink.
846 * Skipped with no clock: TLS cannot succeed yet. */
847 bool rpc_ok = false;
848 for (int attempt = 0; synced && (attempt < 3) && !rpc_ok; attempt++) {
849 uint64_t boot_nonce = 0U;
850 rpc_ok = eth_rpc_get_nonce(&boot_nonce);
851 }
852 if (synced && !rpc_ok) {
853 ESP_LOGE(TAG, "RPC unreachable or clock rejected at boot - carrying on, "
854 "each sale re-checks");
855 }
856
857 ESP_LOGI(TAG, "Ready%s", synced ? "" : " (offline - re-joining in the background)");
858 /* Heap baseline: everything is up and nothing transient has run. Compare with
859 * prov_start()'s line to see what the config page and an upload cost. */
860 ESP_LOGI(TAG, "heap at ready: %u free, %u largest block",
861 (unsigned)esp_get_free_heap_size(),
862 (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
863
864 /* The update greeting, unless the first-run welcome already carried it. */
865 if (greeting[0] != '\0') {
866 ui_show_welcome(greeting);
868 }
869
870
871 return pos_hw_t{ wallet, nfcTransport, cryptoProvider };
872}
#define PN532_IRQ
Definition boot.cpp:41
bool run_wizard(CryptnoxWallet &wallet, Pn532NfcTransport &transport, CW_CryptoProvider &crypto, bool wifi_only)
Run the browser wizard until the operator presses Finish.
Definition boot.cpp:269
#define LED_G
Definition boot.cpp:48
#define LED_B
Definition boot.cpp:49
#define PN532_I2C_PORT
Definition boot.cpp:38
#define BOOT_FAULT_RESTART_S
Definition boot.cpp:212
static bool resolve_evm_payout(bool *rejected)
Resolve the EVM payout address into its dual store, at boot.
Definition boot.cpp:232
#define PN532_SDA
Definition boot.cpp:39
#define PN532_RST
Definition boot.cpp:42
#define PN532_SCL
Definition boot.cpp:40
pos_hw_t pos_boot(void)
Everything before the main loop. Returns the card stack, which lives for the life of the program.
Definition boot.cpp:496
static char s_join_ssid[33]
Definition boot.cpp:68
const char *const NOTE_NO_TIME
Definition boot.cpp:60
#define PN532_I2C_HZ
Definition boot.cpp:43
const char *const NOTE_JOIN_FAILED
Definition boot.cpp:58
void wifi_keep_or_drop(bool keep)
Persist or discard the pending picker credentials, then scrub them.
Definition boot.cpp:77
void boot_fault(ui_boot_err_t kind, const char *detail)
Show a startup fault, then restart. Does not return.
Definition boot.cpp:223
void wait_for_ui_event(ui_event_t want)
Block until the UI reports want, discarding anything else.
Definition boot.cpp:94
#define TIME_SYNC_ATTEMPTS
Definition boot.cpp:55
static char s_join_pass[65]
Definition boot.cpp:69
#define WIFI_SAVED_ATTEMPTS
Definition boot.cpp:54
bool sync_time(void)
Block on an SNTP sync so TLS certificate validity-period checks run against real time instead of the ...
Definition boot.cpp:483
bool wifi_picker(const char *note)
Run the panel network picker (scan → list → keyboard → connect) until connected.
Definition boot.cpp:151
#define LED_R
Definition boot.cpp:47
bool wifi_try_saved(void)
Try the saved credentials, staying on the splash while it happens.
Definition boot.cpp:113
bool card_read_payouts(CryptnoxWallet &wallet, Pn532NfcTransport &transport, CW_CryptoProvider &crypto, const char *pin, size_t pin_chars, char *eth_out, size_t eth_n, char *tron_out, size_t tron_n, char *err, size_t err_n)
Read the card's payout addresses and put them through the panel.
Definition card_io.cpp:233
bool begin(unsigned long) override
Definition boot.cpp:19
void println(char) override
Definition boot.cpp:30
void print(uint32_t, int) override
Definition boot.cpp:25
void println(uint32_t, int) override
Definition boot.cpp:33
void println(const char *) override
Definition boot.cpp:29
void println(uint8_t, int) override
Definition boot.cpp:31
void println(uint16_t, int) override
Definition boot.cpp:32
void print(char) override
Definition boot.cpp:22
void print(uint16_t, int) override
Definition boot.cpp:24
void println() override
Definition boot.cpp:27
void print(uint8_t, int) override
Definition boot.cpp:23
void print(int, int) override
Definition boot.cpp:26
void println(int, int) override
Definition boot.cpp:34
void print(const char *) override
Definition boot.cpp:21
void print(const __FlashStringHelper *) override
Definition boot.cpp:20
void println(const __FlashStringHelper *) override
Definition boot.cpp:28
#define TRON_URL_MAIN
bool eth_addr_parse(const char *hex, uint8_t out[ETH_ADDR_LEN])
Parse a 20-byte Ethereum address from a hex string.
Definition eth_addr.cpp:34
#define ETH_ADDR_LEN
Length of a raw (binary) Ethereum address, in bytes.
Definition eth_addr.h:25
bool eth_rpc_get_nonce(uint64_t *nonce_out)
Fetch the confirmed transaction count (nonce) for from_addr.
Definition eth_rpc.cpp:157
static const char *const TAG
Definition eth_rpc.cpp:33
QueueHandle_t s_ui_queue
Definition main.cpp:23
std::atomic< bool > s_user_cancelled
Definition main.cpp:27
void ui_event_dispatch(ui_event_t event, uint64_t payload)
UI-task callback: forward a touch event to the main task queue.
Definition main.cpp:39
void net_wifi_disconnect(void)
Drop the station association, without the retry loop pulling it back.
Definition net.cpp:268
void net_time_background(void)
Subscribe SNTP without waiting, so the clock is set whenever the network comes back.
Definition net.cpp:529
void net_wifi_keep_trying(void)
Hold on to the network last configured, even though it is down now.
Definition net.cpp:430
bool net_time_sync(uint32_t timeout_ms)
Block until the system clock has been set via SNTP and sanity-checked.
Definition net.cpp:536
bool net_wifi_connect(const char *ssid, const char *password)
Connect to a WiFi network and block until an IP is obtained (up to 30 s). May be called repeatedly to...
Definition net.cpp:376
uint16_t net_wifi_scan(net_wifi_ap_t *out, uint16_t max)
Scan for nearby access points (blocking).
Definition net.cpp:322
void net_wifi_init(void)
Bring up the WiFi driver in station mode (idempotent).
Definition net.cpp:175
const char * ota_running_version(void)
The running firmware's version, from the image header.
Definition ota.cpp:263
bool ota_mark_valid(void)
Confirm the running image, cancelling the rollback armed by the bootloader.
Definition ota.cpp:231
#define OTA_VERSION_SHOWN_MAX
Bytes needed by ota_version_display: the version, a 'v', a NUL.
static const char * ota_version_display(const char *v, char *buf, size_t n)
The display form of a version: 1.0.0 reads as v1.0.0 on screen.
const token_cfg_t TOKEN_CFG[]
Definition pay.cpp:32
void token_load(const token_cfg_t *cfg, CW_CryptoProvider &crypto)
Load one token at boot: the operator's contract if one is set and parses, config.h otherwise.
Definition pay.cpp:84
char s_payout_tron[SETTINGS_PAYOUT_MAX]
Definition pay.cpp:28
const size_t TOKEN_CFG_COUNT
Definition pay.cpp:41
pos_addr_t s_dest
Definition pay.cpp:177
void ui_refresh_addresses(void)
Implemented by main: repoint those two rows at the selected chain.
Definition pay.cpp:170
pos_addr_t s_tron_dest
Definition pay.cpp:185
bool s_payout_bad[2]
Definition pay.cpp:182
char s_payout_eth[SETTINGS_PAYOUT_MAX]
Definition pay.cpp:27
void eth_rpc_select(void)
Definition pay_evm.cpp:51
Private to the application files (main, boot, card_io, pay, pay_evm, pay_tron): the state they share ...
static bool chain_is_tron(void)
true when the operator has switched the terminal to Tron.
Definition pos_app.h:74
void prov_set_step(prov_step_t step)
Tell the portal which wizard step is current.
prov_step_t prov_step(void)
The current step.
void prov_set_note(const char *note)
Put a one-line message on the page.
void prov_set_wifi_only(void)
Cut the wizard down to the Wi-Fi step, with no admin code.
bool prov_propose(prov_ask_t kind, const char *addr)
Propose a value on behalf of the panel itself.
static esp_err_t ok(httpd_req_t *req, const char *msg)
200 with a plain-text message.
bool prov_authed(void)
Whether the browser session is authorised to change anything.
prov_mode_t prov_mode(void)
Which mode is running, or PROV_MODE_OFF.
void prov_stop(void)
Stop the portal, drop the AP, and withdraw anything unaccepted.
void prov_set_scan(const net_wifi_ap_t *aps, uint16_t n)
Hand the portal a Wi-Fi scan for the browser to choose from.
bool prov_start(prov_mode_t mode, ui_event_cb_t cb)
Raise the portal.
@ PROV_ASK_PAYOUT_TRON
Definition provision.h:131
@ PROV_ASK_PAYOUT_ETH
Definition provision.h:130
prov_step_t
Where the wizard has got to.
Definition provision.h:118
@ PROV_STEP_WIFI
Definition provision.h:122
@ PROV_STEP_AUTH
Definition provision.h:120
@ PROV_STEP_ADDR
Definition provision.h:121
@ PROV_STEP_DONE
Definition provision.h:123
@ PROV_MODE_WIZARD
Definition provision.h:106
@ PROV_MODE_OFF
Definition provision.h:105
bool settings_has_wifi(void)
true if a Wi-Fi SSID has been stored.
Definition settings.cpp:284
bool settings_get_wifi(char *ssid, size_t ssid_n, char *pass, size_t pass_n)
Read the stored Wi-Fi credentials.
Definition settings.cpp:298
bool settings_wipe_if_new_build(void)
Erase NVS unless this exact build is the one that wrote it.
Definition settings.cpp:578
const char * settings_net_str(const char *testnet, const char *mainnet)
Pick the string belonging to the network the terminal is on.
Definition settings.cpp:210
int16_t settings_get_tz_offset_min(void)
The panel clock's standard (winter) offset from UTC, in minutes east.
Definition settings.cpp:226
uint8_t settings_get_tz_dst(void)
The clock's DST rule, a civil_dst_t; CIVIL_DST_NONE when unset.
Definition settings.cpp:243
void settings_set_wifi(const char *ssid, const char *pass)
Persist Wi-Fi credentials (plaintext — see README threat model).
Definition settings.cpp:324
bool settings_get_payout(bool tron, char *out, size_t n)
Read the payout address for a network.
Definition settings.cpp:525
void settings_set_chain(pos_chain_t chain)
Persist the selected chain.
Definition settings.cpp:181
bool settings_has_admin_code(void)
true once an admin code exists.
Definition settings.cpp:372
bool settings_get_mainnet(void)
true when the terminal is on the production networks.
Definition settings.cpp:189
bool settings_has_payout(bool tron)
Whether an operator has actually set the payout address for a network.
Definition settings.cpp:534
pos_chain_t
Which chain (and therefore which asset) the terminal charges in.
Definition settings.h:33
@ POS_CHAIN_ETH_USDC
Definition settings.h:34
@ POS_CHAIN_TRON_TRX
Definition settings.h:35
#define SETTINGS_PAYOUT_MAX
Longest payout address plus NUL — "0x" + 40 hex, or 34 base58 Tron.
Definition settings.h:214
A scanned access point (subset of fields the UI needs).
Definition net.h:33
The card stack pos_boot() brought up; lives for the program.
Definition pos_app.h:252
ui_event_t event
Definition pos_app.h:241
void tron_rpc_set_ca_cert(const char *ca_pem)
Optional: pin the Tron endpoint's TLS certificate.
Definition tron_rpc.cpp:195
void tron_rpc_init(const char *base_url)
Set the Tron HTTP API base URL (no trailing slash).
Definition tron_rpc.cpp:190
void ui_set_prov_note(const char *msg)
Put a failure line on the setup screen, in red under the step title.
Definition ui.cpp:880
void ui_show_prov(int step)
Show the setup screen: QR code, AP name and passphrase.
Definition ui.cpp:850
void ui_show_prov_auth(void)
Demand the admin code so a browser can be authorised.
Definition ui.cpp:861
void ui_show_welcome(const char *sub)
Greet the operator at the start of first-run setup, or after an update.
Definition ui.cpp:807
void ui_set_boot_status(const char *step)
Set the one-line progress note on the splash screen.
Definition ui.cpp:776
void ui_show_wifi_list(const net_wifi_ap_t *aps, uint16_t n, const char *note)
Show the scanned Wi-Fi networks for the user to pick from.
Definition ui.cpp:747
void ui_show_card_pin(void)
Ask for the card PIN before reading an address off a Cryptnox card.
Definition ui.cpp:873
void ui_init(ui_event_cb_t cb)
Initialise display + touch and start the UI task.
Definition ui.cpp:696
size_t ui_take_wifi_creds(char *ssid, size_t ssid_n, char *pass, size_t pass_n)
Fetch the selected SSID + entered password and wipe the UI's copy.
Definition ui.cpp:827
void ui_show_prov_confirm(void)
Raise the modal that asks the operator to accept a value a browser proposed, reading the pending prop...
Definition ui.cpp:856
void ui_show_admin_set(void)
Run the first-run admin-code creation (enter, then confirm).
Definition ui.cpp:818
void ui_show_splash(void)
Switch to the splash screen.
Definition ui.cpp:706
void ui_show_wifi_connecting(const char *ssid)
Show a "Connecting to <ssid>…" screen while main associates.
Definition ui.cpp:770
void ui_show_boot_error(ui_boot_err_t kind, const char *detail)
Show a startup fault, naming the cause and what to do about it.
Definition ui.cpp:795
size_t ui_take_pin(char *out, size_t n)
Copy the most recently entered PIN out and wipe the UI's copy.
Definition ui.cpp:733
ui_event_t
Events emitted by the UI task towards the main task.
Definition ui.h:55
@ UI_EVENT_PROV_AUTH
Definition ui.h:66
@ UI_EVENT_CARD_PIN
Definition ui.h:80
@ UI_EVENT_WIFI_TRY
Definition ui.h:61
@ UI_EVENT_ADMIN_SET
Definition ui.h:64
@ UI_EVENT_PROV_VALUE_NO
Definition ui.h:71
@ UI_EVENT_WELCOME_DONE
Definition ui.h:65
@ UI_EVENT_PROV_FINISH
Definition ui.h:79
@ UI_EVENT_WIFI_SCAN
Definition ui.h:60
@ UI_EVENT_PROV_CARD
Definition ui.h:75
@ UI_EVENT_PROV_VALUE
Definition ui.h:68
@ UI_EVENT_PROV_SCAN
Definition ui.h:77
@ UI_EVENT_PROV_NEXT
Definition ui.h:78
@ UI_EVENT_PROV_STOP
Definition ui.h:86
@ UI_EVENT_PROV_VALUE_SET
Definition ui.h:70
ui_boot_err_t
Startup faults shown on UI_SCREEN_BOOT_ERROR.
Definition ui.h:113
@ UI_BOOT_ERR_WALLET
Definition ui.h:115
@ UI_BOOT_ERR_CONFIG
Definition ui.h:116
@ UI_BOOT_ERR_NFC
Definition ui.h:114