onlyn00bs-badge

OnlyN00bs: a DEF CON 34 friend-finder badge. ESP32 firmware, Web Bluetooth setup app, printable case
git clone https://git.virtualshack.io/onlyn00bs-badge.git
Log | Files | Refs | README | LICENSE

display_serial.cpp (14273B)


      1 // Serial implementation of the display interface.
      2 //
      3 // This lets you flash the badge to ANY ESP32 dev board and validate the whole
      4 // protocol — provisioning, signed beacons, the peer table, RSSI sorting, the
      5 // encounter log and the report — over the serial monitor, before the e-ink
      6 // renderer exists. It's the "rule zero: prove the firmware before you touch
      7 // hardware" build.
      8 
      9 #if defined(BADGE_DISPLAY_SERIAL)
     10 
     11 #include <Arduino.h>
     12 #include "display.h"
     13 
     14 void display_init() {
     15   // Serial is already started in main setup(); nothing else to do.
     16 }
     17 
     18 void display_sleep() {
     19   Serial.println("[PWR] display off (headless build — no panel to power down)");
     20 }
     21 
     22 void display_set_brightness(uint8_t level) {
     23   (void)level;                 // no panel to dim — the setting still persists for the OLED build
     24 }
     25 
     26 void display_boot(const char* line) {
     27   Serial.printf("\n[BOOT] %s\n", line);
     28 }
     29 
     30 void display_boot_splash(int style) {
     31   (void)style;                              // serial doesn't animate — one banner
     32   Serial.println();
     33   Serial.println("  ▁▂▃▄▅▆▇  F R I E N D - F I N D E R  ▇▆▅▄▃▂▁");
     34   Serial.println("        signed ESP-NOW · crew radar · v1");
     35 }
     36 
     37 void display_boot_logos() {                 // no panel — the OLED's logo card as one line
     38   Serial.println("                  O n l y N 0 0 b s");
     39 }
     40 
     41 void display_disclaimer() {                 // heat/LiPo safety note — no hold on serial
     42   Serial.println("\n  [WARNING] This badge runs on a LiPo battery. We're in Vegas. "
     43                  "Keep an eye on how hot it gets. Spicy pillow? Discontinue use.");
     44 }
     45 
     46 void display_provisioning(const char* device_name, const char* code) {
     47   Serial.println("\n┌─────────────────────────────────────┐");
     48   Serial.println(  "│  PROVISIONING MODE                  │");
     49   Serial.printf(   "│  Connect over BLE to \"%s\"\n", device_name);
     50   Serial.printf(   "│  Pairing code: %s  (enter in the app)\n", code);
     51   Serial.println(  "│  then write your handle.            │");
     52   Serial.println(  "│  Hold BUTTON 5s to exit setup mode. │");
     53   Serial.println(  "└─────────────────────────────────────┘");
     54 }
     55 
     56 void display_whoami(const char* handle, const char* mac) {
     57   Serial.printf("\n── whoami ──\n   user: %s\n   mac:  %s\n",
     58                 (handle && handle[0]) ? handle : "(unset)", mac);
     59 }
     60 
     61 // crude ASCII signal bar from RSSI (dBm): -50 strong … -90 weak
     62 static const char* rssi_bar(int8_t rssi) {
     63   if (rssi >= -55) return "▁▂▃▄▅";
     64   if (rssi >= -65) return "▁▂▃▄ ";
     65   if (rssi >= -75) return "▁▂▃  ";
     66   if (rssi >= -85) return "▁▂   ";
     67   return                  "▁    ";
     68 }
     69 
     70 void display_peers(const char* my_handle, const Peer* peers, int n, bool show_rssi,
     71                    uint8_t mult, int page) {
     72   (void)show_rssi;   // serial has room for both the pet face and dBm, no toggle
     73   (void)page;        // serial lists every peer (no 5-row cap), so paging is OLED-only
     74   char mtag[16] = "";
     75   if (mult >= 2) snprintf(mtag, sizeof(mtag), " ── x%u bonus", (unsigned)mult);
     76   Serial.printf("\n── finder ── you: %s ── %d nearby%s ──\n", my_handle, n, mtag);
     77   if (n == 0) {
     78     Serial.println("   (nobody in range yet)");
     79     return;
     80   }
     81   for (int i = 0; i < n; i++) {
     82     Serial.printf("   %s  %-8s %-24s  %4d dBm\n",
     83                   rssi_bar(peers[i].rssi), peers[i].pet_face, peers[i].handle,
     84                   peers[i].rssi);
     85   }
     86 }
     87 
     88 static void fmt_dur(uint32_t s, char* out, size_t n) {
     89   uint32_t h = s / 3600, m = (s % 3600) / 60, sec = s % 60;
     90   if (h)      snprintf(out, n, "%uh%02um", (unsigned)h, (unsigned)m);
     91   else if (m) snprintf(out, n, "%um%02us", (unsigned)m, (unsigned)sec);
     92   else        snprintf(out, n, "%us", (unsigned)sec);
     93 }
     94 
     95 void display_report(const char* my_handle, const Report& r) {
     96   char dur[24];
     97   Serial.println("\n╔═══════════════════════════════════════════════╗");
     98   Serial.printf(  "║  CON REPORT — %-31s ║\n", my_handle);
     99   Serial.println("╚═══════════════════════════════════════════════╝");
    100   fmt_dur(r.total_seconds, dur, sizeof(dur));
    101   Serial.printf("  %d friends · %d crossings · %s together\n\n",
    102                 r.unique, r.total_encounters, dur);
    103 
    104   if (r.n == 0) {
    105     Serial.println("  (no encounters logged yet)");
    106   } else {
    107     Serial.println("  friend                  seen  time-together  closest");
    108     Serial.println("  ─────────────────────── ────  ─────────────  ───────");
    109     for (int i = 0; i < r.n; i++) {
    110       fmt_dur(r.friends[i].total_seconds, dur, sizeof(dur));
    111       Serial.printf("  %-23s  x%-3u  %-13s  %4d dBm\n",
    112                     r.friends[i].handle, (unsigned)r.friends[i].encounters,
    113                     dur, (int)r.friends[i].rssi_max);
    114     }
    115   }
    116   if (!r.time_synced)
    117     Serial.println("\n  (times relative — clock unsynced/stale; re-sync via Options -> Badge Settings -> Provisioning Mode)");
    118   Serial.println();
    119 }
    120 
    121 void display_points(const char* my_handle, uint32_t points,
    122                     uint32_t prox_seconds, int peers_now, bool earning,
    123                     const PointSample* hist, int hist_n,
    124                     uint32_t graph_span_s, const char* window_label, bool anchor_newest,
    125                     uint8_t mult, bool stealth) {
    126   Serial.printf("\n── points ── you: %s ──\n", my_handle);
    127   Serial.printf("   %u points  (%um%02us in proximity)\n",
    128                 (unsigned)points,
    129                 (unsigned)(prox_seconds / 60), (unsigned)(prox_seconds % 60));
    130   if (earning) {
    131     // with a group bonus the tally ticks ×mult per wall second → wait = rem ÷ mult
    132     uint32_t rem = 60 - prox_seconds % 60;
    133     if (mult > 1) rem = (rem + mult - 1) / mult;
    134     Serial.printf("   earning%s — %d crew in range, +1 pt in %us\n",
    135                   mult >= 3 ? " x3" : mult >= 2 ? " x2" : "",
    136                   peers_now, (unsigned)rem);
    137   }
    138   else if (stealth)             // check first: crew can be in range AND in-gate, so
    139     Serial.println("   paused — STEALTH on (not broadcasting)");   // "below gate" would lie
    140   else if (peers_now > 0)
    141     Serial.printf("   paused — %d crew in range but below RSSI gate\n", peers_now);
    142   else
    143     Serial.println("   paused — no crew in range");
    144 
    145   // points-over-time sparkline (oldest→newest), Y auto-scaled to the window;
    146   // a '┊' marks a reboot break (seg_start). Trailing label notes whether the
    147   // samples carry real wall-clock epochs or fell back to relative ordering.
    148   if (hist_n >= 2) {
    149     static const char* lv[8] = {"▁","▂","▃","▄","▅","▆","▇","█"};
    150     bool synced = (hist[hist_n - 1].epoch != 0);
    151     uint32_t anchor = (anchor_newest && synced && hist[hist_n - 1].epoch > graph_span_s)
    152                         ? hist[hist_n - 1].epoch - graph_span_s : hist[0].epoch;
    153     // in-window (epoch ≥ anchor, or all if unsynced) min/max
    154     bool any = false; uint16_t mn = 0, mx = 0; int inwin = 0;
    155     for (int i = 0; i < hist_n; i++) {
    156       if (synced && hist[i].epoch < anchor) continue;
    157       inwin++;
    158       if (!any) { mn = mx = hist[i].points; any = true; }
    159       else { if (hist[i].points < mn) mn = hist[i].points; if (hist[i].points > mx) mx = hist[i].points; }
    160     }
    161     uint32_t yspan = (uint32_t)mx - mn;
    162     Serial.print("   ");
    163     bool first = true;
    164     for (int i = 0; i < hist_n; i++) {
    165       if (synced && hist[i].epoch < anchor) continue;
    166       if (!first && hist[i].seg_start) Serial.print("┊");   // reboot break
    167       int idx = yspan ? (int)(((uint32_t)(hist[i].points - mn) * 7) / yspan) : 0;
    168       Serial.print(lv[idx]);
    169       first = false;
    170     }
    171     Serial.printf("  [%u..%u pts, %d smpl, %s, %s]\n", (unsigned)mn, (unsigned)mx,
    172                   inwin, window_label, synced ? "wall-clock" : "relative");
    173   }
    174 }
    175 
    176 void display_battery(const char* my_handle, uint16_t mv, uint8_t pct, bool low) {
    177   Serial.printf("\n── battery ── you: %s ──\n", my_handle);
    178   Serial.printf("   %u%%  (%u.%02u V)%s\n",
    179                 (unsigned)pct, mv / 1000, (mv % 1000) / 10,
    180                 low ? "   ** LOW — charge soon **" : "");
    181 }
    182 
    183 void display_storage(const char* my_handle, size_t used, size_t total, bool full) {
    184   size_t freeb = (total > used) ? total - used : 0;
    185   unsigned pct = total ? (unsigned)((uint64_t)used * 100 / total) : 0;    // % USED (disk convention)
    186   Serial.printf("\n── storage ── you: %s ──\n", my_handle);
    187   Serial.printf("   %u%% used  (%u B free of %u)%s\n",
    188                 pct, (unsigned)freeb, (unsigned)total,
    189                 full ? "   ** FULL — export soon **" : "");
    190 }
    191 
    192 void display_friends(const char* my_handle, const PointFriend* top, int n,
    193                      uint32_t total_points) {
    194   Serial.printf("\n── best friends ── you: %s ── %u pts total ──\n",
    195                 my_handle, (unsigned)total_points);
    196   if (n == 0) {
    197     Serial.println("   (no points yet — go find your crew)");
    198     return;
    199   }
    200   int shown = n < 3 ? n : 3;
    201   for (int i = 0; i < shown; i++) {
    202     uint32_t s = top[i].seconds;
    203     Serial.printf("   %d. %-24s %u pts  (%um%02us close)\n",
    204                   i + 1, top[i].handle, (unsigned)(s / 60),
    205                   (unsigned)(s / 60), (unsigned)(s % 60));
    206   }
    207 }
    208 
    209 void display_pet(const char* name, const char* face) {
    210   Serial.printf("\n── avatar: %s ──\n", name);
    211   Serial.printf("      %s\n", face);
    212   Serial.printf("   (tap to swap avatar)\n");
    213 }
    214 
    215 void display_home(const char* handle, const char* face, uint32_t points,
    216                   uint8_t batt_pct, bool batt_low, uint8_t disk_pct, const char* clock_str,
    217                   bool radio_on) {
    218   Serial.printf("\n── home ── %s ── %s ──%s\n", handle, clock_str,
    219                 radio_on ? "" : " [STEALTH]");
    220   Serial.printf("   %s   %u pts · bat %u%%%s · disk %u%% used\n",
    221                 face, (unsigned)points, (unsigned)batt_pct,
    222                 batt_low ? " (LOW)" : "", (unsigned)disk_pct);
    223 }
    224 
    225 void display_reset_confirm(int cursor) {
    226   Serial.println("\n── factory reset ──");
    227   Serial.println("   Erases handle, avatar, points, encounter log and settings.");
    228   Serial.printf("   %s Cancel\n", cursor == 0 ? ">" : " ");
    229   Serial.printf("   %s ERASE\n",  cursor == 1 ? ">" : " ");
    230 }
    231 
    232 void display_menu(int splash_sel) {
    233   static const char* names[BOOT_SPLASH_COUNT] = { "radar", "terminal", "glitch", "matrix" };
    234   Serial.println("\n── boot splash ──");
    235   for (int i = 0; i < BOOT_SPLASH_COUNT; i++)
    236     Serial.printf("   %s %s\n", i == splash_sel ? ">" : " ", names[i]);
    237   Serial.println("   (tap to change)");
    238 }
    239 
    240 void display_pet_popup(const char* face, const char* line1, const char* line2, uint16_t hold_ms) {
    241   (void)hold_ms;   // serial popup is a one-line print, no hold
    242   if (line2) Serial.printf("   [avatar] %s  — %s: %s\n", face, line1, line2);
    243   else       Serial.printf("   [avatar] %s  — %s\n", face, line1);
    244 }
    245 
    246 void display_toast(const char* face, const char* line1, const char* line2) {
    247   if (line2) Serial.printf("   [toast] %s  — %s: %s\n", face, line1, line2);
    248   else       Serial.printf("   [toast] %s  — %s\n", face, line1);
    249 }
    250 
    251 void display_options_menu(int screen, int cursor, const OptView& v) {
    252   if (screen == OPTM_USER) {
    253     Serial.println("\n── user settings ──");
    254     Serial.printf("   %s Back\n",            cursor == 0 ? ">" : " ");   // first = default row
    255     Serial.printf("   %s Avatar: %s\n",      cursor == 1 ? ">" : " ", v.pet);
    256     Serial.printf("   %s Finder View: %s\n", cursor == 2 ? ">" : " ", v.finder_mode);
    257     Serial.printf("   %s Reminders\n",       cursor == 3 ? ">" : " ");
    258   } else if (screen == OPTM_DISPLAY) {
    259     Serial.println("\n── display ──");
    260     Serial.printf("   %s Back\n",         cursor == 0 ? ">" : " ");
    261     Serial.printf("   %s Splash: %s\n",   cursor == 1 ? ">" : " ", v.splash);
    262     Serial.printf("   %s Bright: %s\n",   cursor == 2 ? ">" : " ", v.bright);
    263   } else if (screen == OPTM_DEVOPTS) {
    264     Serial.println("\n── developer options ──");
    265     Serial.printf("   %s Back\n",              cursor == 0 ? ">" : " ");
    266     Serial.printf("   %s Whoami\n",            cursor == 1 ? ">" : " ");
    267     Serial.printf("   %s Debug:  %s\n",        cursor == 2 ? ">" : " ", v.test_state);
    268     Serial.printf("   %s Provisioning Mode\n", cursor == 3 ? ">" : " ");
    269     Serial.printf("   %s Factory Reset\n",     cursor == 4 ? ">" : " ");
    270   } else if (screen == OPTM_REMIND) {
    271     Serial.println("\n── reminders ──");
    272     Serial.printf("   %s Back\n",         cursor == 0 ? ">" : " ");
    273     Serial.printf("   %s Global: %s\n",   cursor == 1 ? ">" : " ", v.remind_global);
    274     Serial.printf("   %s Shower: %s\n",   cursor == 2 ? ">" : " ", v.remind_shower);
    275   } else if (screen == OPTM_BADGE) {
    276     Serial.println("\n── badge settings ──");
    277     Serial.printf("   %s Back\n",              cursor == 0 ? ">" : " ");
    278     Serial.printf("   %s Display\n",           cursor == 1 ? ">" : " ");
    279     Serial.printf("   %s Stealth: %s\n",       cursor == 2 ? ">" : " ", v.stealth);
    280     Serial.printf("   %s Developer Options\n", cursor == 3 ? ">" : " ");
    281     Serial.printf("   %s Power\n",             cursor == 4 ? ">" : " ");
    282   } else if (screen == OPTM_POWER) {
    283     Serial.println("\n── power ──");
    284     Serial.printf("   %s Back\n",       cursor == 0 ? ">" : " ");
    285     Serial.printf("   %s Reboot\n",     cursor == 1 ? ">" : " ");
    286     Serial.printf("   %s Deep sleep\n", cursor == 2 ? ">" : " ");
    287   } else {  // OPTM_ROOT
    288     Serial.println("\n── options ──");
    289     Serial.printf("   %s Back\n",           cursor == 0 ? ">" : " ");
    290     Serial.printf("   %s User Settings\n",  cursor == 1 ? ">" : " ");
    291     Serial.printf("   %s Badge Settings\n", cursor == 2 ? ">" : " ");
    292   }
    293   Serial.println("   (tap: move · double-tap: enter)");
    294 }
    295 
    296 #endif  // BADGE_DISPLAY_SERIAL