Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions BirdsEye/BirdsEye.ino
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,12 @@ const int PAGE_DRAG_DISTANCE = -17;
// construction, which is exactly what a pinned page needs.
const int PAGE_DRAG_MODE = -18; // Automatic / Manual / Back
const int PAGE_DRAG_STAGING = -19; // tree countdown / run / results
// SensorEgg pairing UI (plan 0017). Ids defined unconditionally like
// PAGE_CAMERA_TEST; the pages are only reachable on a
// BIRDSEYE_ENABLE_SENSOREGG build (menu row, renderers and wiring are
// all gated).
const int PAGE_PAIR_EGG = -20; // egg pairing / paired-status screen
const int PAGE_EGG_TEST = -21; // egg bench/live-data page (never idle-sleeps)

// running menu (these must be in order)
#if BIRDSEYE_ENABLE_PROFILING
Expand Down
4 changes: 4 additions & 0 deletions BirdsEye/display_pages.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ void displayPage_usb_storage();
void displayPage_pair_camera();
void displayPage_camera_test();
void displayPage_camera_serial_entry();
#if BIRDSEYE_ENABLE_SENSOREGG
void displayPage_pair_egg(); // egg pairing / paired-status (plan 0017)
void displayPage_egg_test(); // egg bench/live-data page (plan 0017)
#endif
void displayPage_internal_fault();
void displayPage_internal_warning();
void displayPage_sd_format();
Expand Down
180 changes: 175 additions & 5 deletions BirdsEye/display_pages.ino
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,11 @@ void displayPage_main_menu() {
// a size-1 scroll-hint line. Four full size-2 rows fill the panel's
// nominal 64 px exactly, but the last row is cut off on real hardware
// — so the window follows the selection instead.
static const char* const kMenuItems[] = {"Race", "Drag", "Review", "Transfer", "Create", "Camera"};
static const char* const kMenuItems[] = {"Race", "Drag", "Review", "Transfer", "Create", "Camera"
#if BIRDSEYE_ENABLE_SENSOREGG
, "Egg" // pairing + bench test for the wireless EGT pod (plan 0017)
#endif
};
const int itemCount = (int)(sizeof(kMenuItems) / sizeof(kMenuItems[0]));
const int visibleRows = 3;

Expand Down Expand Up @@ -610,10 +614,15 @@ void displayPage_camera_test() {

#if BIRDSEYE_ENABLE_SENSOREGG
// SensorEgg readout (bottom line): live Temp1 or NA when the egg is
// silent (>1 s) / faulted. Makes this page the coexistence soak-test
// harness: camera linked above + egg streaming here, and the page never
// idle-sleeps (the idle-shutdown and USB-charging entries are
// main-menu-only), so it can sit on a desk indefinitely.
// silent (>1 s) / faulted. Entering this page latches the egg bench
// mode (sensoreggTestEnterMode, plan 0017) so the race-gated scanner
// actually runs here — the plan-0012 gate had silently broken the
// desk-soak behavior this line was built for (it read NA off-track).
// With the latch, this page is again the camera+egg coexistence soak
// harness: camera linked above + egg streaming here, and the page
// never idle-sleeps (idle-shutdown and USB-charging entries are
// main-menu-only), so it can sit on a desk indefinitely. The EGG TEST
// page (plan 0017) shows the full egg picture.
display.print(F("egg: "));
const float soakEgtF = sensoregg_protocol::celsiusToFahrenheit(sensoreggEgtC());
if (isNanF(soakEgtF)) { // isNanF: plain isnan() folds to false under -Ofast
Expand All @@ -627,6 +636,167 @@ void displayPage_camera_test() {
safeDisplayUpdate();
}

#if BIRDSEYE_ENABLE_SENSOREGG

void displayPage_pair_egg() {
resetDisplay();

if (sensoreggIsPaired()) {
// Paired: stored MAC + Back/Test/Unpair. This branch also takes over
// the frame after a capture (insideMenu is derived per frame). Back
// first (index 0): a "Cancel" press landing one frame late must not
// hit Unpair and erase the just-captured MAC (camera precedent).
display.setTextSize(1);
display.println(F(" EGG"));

char mac[sensoregg_protocol::kMacStrLen];
sensoreggPairedMac(mac, sizeof(mac));
display.println(mac); // 17 chars — a "Paired: " prefix would not fit

display.setTextSize(2);
display.print(menuSelectionIndex == 0 ? "->" : " ");
display.println(F("Back"));
display.print(menuSelectionIndex == 1 ? "->" : " ");
display.println(F("Test"));
display.print(menuSelectionIndex == 2 ? "->" : " ");
display.println(F("Unpair"));
} else {
// Unpaired: window-gated capture status. The scanner is forced on
// while the window is open and observes EVERY egg in range; capture
// waits for one advertising ITS OWN pairing window (the egg-side
// long-press) — physical possession is the authorization.
display.setTextSize(1);
display.println(F(" PAIR EGG"));
display.println();

if (sensoreggPairingInProgress()) {
display.println(F("Hold button on egg"));
display.println(F("to start pairing..."));
display.print(F("Egg: "));
if (sensoreggLinkUp()) {
display.print(F("heard v"));
display.println(sensoreggProtoVersion());
} else {
display.println(F("---"));
}
display.print(F("Window: "));
display.println(sensoreggPairingFlag() ? F("OPEN") : F("--"));
} else {
// Window closed without a capture (2-min timeout or a cancel that
// landed while the page was still up).
display.println(F("Pairing stopped"));
display.println();
display.println();
display.println();
}

display.println();
display.println(F("B2:Cancel"));
}

safeDisplayUpdate();
}

void displayPage_egg_test() {
resetDisplay();

// Eight size-1 rows, 21 chars each. Entering this page latched the
// bench scan (sensoreggTestEnterMode), so everything below is live on
// a desk — this is the egg's soak/diagnostic harness, and like the
// camera test page it never idle-sleeps.
display.setTextSize(1);

// Row 0: title, link tri-state (HUNG outranks OK — packets arriving
// but the sequence frozen means the egg needs a power cycle), and the
// protocol version of the latest frame.
display.print(F("EGG TEST rf:"));
if (sensoreggAppHung()) {
display.print(F("HUNG"));
} else if (sensoreggLinkUp()) {
display.print(F("OK"));
} else {
display.print(F("--"));
}
display.print(F(" v"));
const uint8_t eggVer = sensoreggProtoVersion();
if (eggVer == 0) {
display.println(F("-"));
} else {
display.println(eggVer);
}

// Rows 1-2: temperatures (Fahrenheit at render — house rule; logging
// stays Celsius) and battery. Every NaN check is isNanF: plain
// isnan() folds to false under -Ofast.
const float egtF = sensoregg_protocol::celsiusToFahrenheit(sensoreggEgtC());
const float cjF =
sensoregg_protocol::celsiusToFahrenheit(sensoreggJunctionC());
const float auxF = sensoregg_protocol::celsiusToFahrenheit(sensoreggAuxC());
display.print(F("EGT "));
if (isNanF(egtF)) {
display.print(F("---"));
} else {
display.print(egtF, 1);
display.print(F("F"));
}
display.print(F(" CJ "));
if (isNanF(cjF)) {
display.println(F("---"));
} else {
display.print((int)lroundf(cjF)); // rounded: keeps the row <= 21 chars
display.println(F("F"));
}

display.print(F("AUX "));
if (isNanF(auxF)) {
display.print(F("---")); // v1 egg, stale link, or divider sentinel
} else {
display.print(auxF, 1);
display.print(F("F"));
}
display.print(F(" BAT "));
const uint8_t eggBatt = sensoreggBatteryPct();
if (eggBatt == 0xFF) {
display.println(F("--%"));
} else {
display.print(eggBatt);
display.println(F("%"));
}

// Row 3: raw sequence counter (first real consumer of
// sensoreggSequence()) + measured packet rate (~9-10 Hz healthy).
display.print(F("SEQ "));
display.print(sensoreggSequence());
display.print(F(" "));
display.print(sensoreggPacketHz(), 1);
display.println(F("Hz"));

// Row 4: live flags from the latest frame.
display.print(F("FLG"));
if (sensoreggPairingFlag()) display.print(F(" PAIR"));
if (sensoreggTcFault()) display.print(F(" FAULT"));
if (!sensoreggPairingFlag() && !sensoreggTcFault()) display.print(F(" -"));
display.println();

// Row 5: which egg the filter accepts.
char eggMac[sensoregg_protocol::kMacStrLen];
if (sensoreggPairedMac(eggMac, sizeof(eggMac))) {
display.print(F("MAC ")); // 4 + 17 = 21 chars exactly
display.println(eggMac);
} else {
display.println(F("MAC any (unpaired)"));
}

// Rows 6-7: spacer + the single menu row.
display.println();
display.print(menuSelectionIndex == 0 ? F("->") : F(" "));
display.println(F("Back"));

safeDisplayUpdate();
}

#endif // BIRDSEYE_ENABLE_SENSOREGG

void displayPage_camera_serial_entry() {
resetDisplay();

Expand Down
93 changes: 92 additions & 1 deletion BirdsEye/display_ui.ino
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,25 @@ void handleMenuPageSelection() {
internalNotification[sizeof(internalNotification) - 1] = '\0';
switchToDisplayPage(PAGE_INTERNAL_WARNING);
}
} else {
} else if (menuSelectionIndex == 5) {
// Camera selected — paired shows status/unpair, unpaired starts pairing
debugln(F("Main Menu: Camera selected"));
if (!cameraIsPaired()) {
cameraRequestPair(); // pairing begins as the page comes up
}
switchToDisplayPage(PAGE_PAIR_CAMERA);
#if BIRDSEYE_ENABLE_SENSOREGG
} else {
// Egg selected (index 6, plan 0017) — paired shows status/unpair,
// unpaired opens the capture window as the page comes up (camera
// pattern). On a stock build menuLimit caps the index at 5, so the
// flag-gated else is unreachable there and compiled out entirely.
debugln(F("Main Menu: Egg selected"));
if (!sensoreggIsPaired()) {
sensoreggRequestPair();
}
switchToDisplayPage(PAGE_PAIR_EGG);
#endif
}
} else if (currentPage == PAGE_DRAG_DISTANCE) {
// Five distances + Back (last row). A distance opens the mode page
Expand Down Expand Up @@ -405,6 +417,10 @@ void handleMenuPageSelection() {
} else if (menuSelectionIndex == 1) {
debugln(F("Camera: Test selected"));
cameraTestEnterMode();
// Latch the egg bench scan too (no-op on stock builds): this page's
// egg soak line was dead on a desk since plan 0012 race-gated the
// scanner — see the comment at the readout in display_pages.ino.
sensoreggTestEnterMode();
switchToDisplayPage(PAGE_CAMERA_TEST);
} else {
debugln(F("Camera: Unpair selected"));
Expand Down Expand Up @@ -452,14 +468,50 @@ void handleMenuPageSelection() {
// mode first or cameraTestActive would stay latched (FSM
// suppressed) with no way back to this menu's Back action.
cameraTestExitMode();
sensoreggTestExitMode(); // drop the egg soak latch with it
switchToDisplayPage(PAGE_INTERNAL_WARNING);
}
} else {
debugln(F("Camera Test: Back"));
cameraTestExitMode();
sensoreggTestExitMode(); // drop the egg soak latch with it
switchToDisplayPage(PAGE_PAIR_CAMERA);
}
forceDisplayRefresh();
#if BIRDSEYE_ENABLE_SENSOREGG
} else if (currentPage == PAGE_PAIR_EGG) {
// Menu only while paired (Back / Test / Unpair) — the unpaired
// capture screen handles its buttons in the custom branch in
// displayLoop(). Back is index 0 so a late "Cancel" press right
// after a capture can't land on Test or Unpair (camera precedent).
if (menuSelectionIndex == 0) {
debugln(F("Egg: Back selected"));
switchToDisplayPage(PAGE_MAIN_MENU);
} else if (menuSelectionIndex == 1) {
debugln(F("Egg: Test selected"));
sensoreggTestEnterMode(); // latch the scanner on outside races
switchToDisplayPage(PAGE_EGG_TEST);
} else {
debugln(F("Egg: Unpair selected"));
if (sensoreggUnpair()) {
switchToDisplayPage(PAGE_MAIN_MENU);
} else {
// Persist-first refused: the settings write failed (SD trouble)
// and nothing changed — warn instead of silently diverging.
strncpy(internalNotification, "Settings write\nfailed!",
sizeof(internalNotification) - 1);
internalNotification[sizeof(internalNotification) - 1] = '\0';
switchToDisplayPage(PAGE_INTERNAL_WARNING);
}
}
} else if (currentPage == PAGE_EGG_TEST) {
// Single row: Back. Drop the bench latch so the race gate owns the
// scanner again.
debugln(F("Egg Test: Back"));
sensoreggTestExitMode();
switchToDisplayPage(PAGE_PAIR_EGG);
forceDisplayRefresh();
#endif
} else if (currentPage == PAGE_TRANSFER_MENU) {
if (menuSelectionIndex == 2) {
// Back — the only non-rebooting way off this page.
Expand Down Expand Up @@ -684,6 +736,12 @@ void displayLoop() {
displayPage_pair_camera();
} else if (currentPage == PAGE_CAMERA_TEST) {
displayPage_camera_test();
#if BIRDSEYE_ENABLE_SENSOREGG
} else if (currentPage == PAGE_PAIR_EGG) {
displayPage_pair_egg();
} else if (currentPage == PAGE_EGG_TEST) {
displayPage_egg_test();
#endif
} else if (currentPage == PAGE_COURSE_TRACK) {
displayPage_course_track();
} else if (currentPage == PAGE_COURSE_TYPE) {
Expand Down Expand Up @@ -790,11 +848,21 @@ void displayLoop() {
// flips the page into menu mode the moment a serial is captured.
(currentPage == PAGE_PAIR_CAMERA && cameraIsPaired()) ||
currentPage == PAGE_CAMERA_TEST ||
#if BIRDSEYE_ENABLE_SENSOREGG
// Egg page mirrors the camera page: menu only while paired; the
// unpaired capture screen uses the custom branch below (plan 0017).
(currentPage == PAGE_PAIR_EGG && sensoreggIsPaired()) ||
currentPage == PAGE_EGG_TEST ||
#endif
courseCreatorActive()
) {
insideMenu = true;
if (currentPage == PAGE_MAIN_MENU) {
#if BIRDSEYE_ENABLE_SENSOREGG
menuLimit = 7; // Race, Drag, Review, Transfer, Create Course, Camera, Egg
#else
menuLimit = 6; // Race, Drag, Review, Transfer, Create Course, Camera
#endif
} else if (currentPage == PAGE_DRAG_DISTANCE) {
menuLimit = drag_timer::kDistanceCount + 1; // distances + Back
} else if (currentPage == PAGE_DRAG_MODE) {
Expand All @@ -813,6 +881,12 @@ void displayLoop() {
menuLimit = 3; // Back, Test, Unpair
} else if (currentPage == PAGE_CAMERA_TEST) {
menuLimit = 4; // Wake, Record, Power Off, Back
#if BIRDSEYE_ENABLE_SENSOREGG
} else if (currentPage == PAGE_PAIR_EGG) {
menuLimit = 3; // Back, Test, Unpair
} else if (currentPage == PAGE_EGG_TEST) {
menuLimit = 1; // Back
#endif
} else if (
currentPage == LOGGING_STOP_CONFIRM ||
currentPage == PAGE_COURSE_PRUNE ||
Expand Down Expand Up @@ -857,6 +931,12 @@ void displayLoop() {
currentPage == PAGE_DRAG_MODE ||
currentPage == PAGE_PAIR_CAMERA ||
currentPage == PAGE_CAMERA_TEST ||
#if BIRDSEYE_ENABLE_SENSOREGG
// Statically rendered top-to-bottom, exactly
// like the camera pages (bug #7 class).
currentPage == PAGE_PAIR_EGG ||
currentPage == PAGE_EGG_TEST ||
#endif
currentPage == PAGE_COURSE_PRUNE ||
currentPage == PAGE_TRANSFER_MENU ||
courseCreatorActive());
Expand Down Expand Up @@ -940,6 +1020,17 @@ void displayLoop() {
cameraCancelPair();
switchToDisplayPage(PAGE_MAIN_MENU);
}
#if BIRDSEYE_ENABLE_SENSOREGG
} else if (currentPage == PAGE_PAIR_EGG) {
// Unpaired capture screen only (the paired variant is a menu above).
// B2 (Select) = cancel. B1 stays deliberately unbound — reserved for
// a future manual MAC entry page (plan 0017 deferred it).
if (btn2->pressed) {
debugln(F("Pair Egg: cancel"));
sensoreggCancelPair();
switchToDisplayPage(PAGE_MAIN_MENU);
}
#endif
} else if (currentPage == PAGE_CAMERA_SERIAL_ENTRY) {
// Manual serial entry button map:
// B1 (Left) = cycle char backward (cursor 0-5) / toggle OK-CANCEL
Expand Down
Loading
Loading