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