Avoid duplicate pairing PINs across Companion displays

This commit is contained in:
mikecarper committed 2026-10-25 00:51:06 -07:00
1 parent 587d5b79a6
commit 83e4b45221
4 files changed
+92 -44

No files matched your search

+13 -8
View File
@@ -466,6 +466,7 @@ class HomeScreen : public UIScreen {
CompanionNodePrefs* _node_prefs;
uint8_t _page;
bool _shutdown_init;
bool _pairing_pin_visible = false;
#if UI_WIFI_SETUP_HOME_PAGE == 1
bool _wifi_setup_was_active;
bool _wifi_was_connected;
@@ -960,7 +961,7 @@ public:
#endif
_page = HomePage::FIRST;
}
bool isFirstPage() const { return _page == HomePage::FIRST; }
bool hasPairingPin() const { return _pairing_pin_visible; }
#if COMPANION_FEATURE_READER
bool isRadioPage() const {
@@ -1027,6 +1028,7 @@ public:
}
int render(DisplayDriver& display) override {
_pairing_pin_visible = false;
#if UI_POCKET_GUI
return renderPocket(display);
#endif
@@ -1191,6 +1193,9 @@ public:
display.setTextSize(pairing_value_size);
mesh::ui::drawTextCenteredEllipsized(
display, layout.pairing, layout.pairing_value_y, pairing_value);
_pairing_pin_visible = pairing_value == pairing_pin
&& display.getTextWidth(pairing_value) <= layout.pairing.width
&& layout.pairing_value_y + display.textLineHeight() <= display.height();
} else if (expanded_home) {
display.setColor(UIColor::secondary_txt);
display.setTextSize(3);
@@ -1237,8 +1242,10 @@ public:
pairing_value = pairing_pin;
}
mesh::ui::drawBottomPairingBlock(display, home_content_top,
pairing_label, pairing_value);
const bool value_drawn =
mesh::ui::drawBottomPairingBlock(display, home_content_top,
pairing_label, pairing_value);
_pairing_pin_visible = value_drawn && !show_client;
} else {
mesh::ui::BoundedTextRows rows(display,
{0, home_content_top, display.width(),
@@ -2617,11 +2624,9 @@ void UITask::renderPairingBanner() {
if (_display == NULL || !isPairingScreenActive()) return;
const uint32_t pairing_pin = the_mesh.getBLEPin();
if (pairing_pin == 0) return;
#if defined(HELTEC_T096)
// The native T096 home page already shows the larger PIN at the bottom.
if (curr == home && static_cast<HomeScreen*>(home)->isFirstPage()
&& _display->height() > 64) return;
#endif
// Keep one complete PIN per frame. Other pages and layouts without their
// own PIN still need this overlay during pairing.
if (curr == home && static_cast<HomeScreen*>(home)->hasPairingPin()) return;
char prompt[24];
snprintf(prompt, sizeof(prompt), "PAIR PIN %06u",
+5 -3
View File
@@ -195,13 +195,14 @@ public:
// PIN/connection status owns the bottom of every non-Indicator home screen,
// including native 160x80 T096 and 250x122 MeshPocket panels. The caller must
// not draw ordinary instructions/network rows in this same area.
inline void drawBottomPairingBlock(DisplayDriver& display, int content_top,
// Return true only when the complete value fits and was drawn.
inline bool drawBottomPairingBlock(DisplayDriver& display, int content_top,
const char* label, const char* value) {
const int bottom = display.height() - (display.height() > 64 ? 2 : 0);
const int available = bottom - content_top;
const int margin = display.width() >= 16 ? 4 : 0;
const int width = display.width() - 2 * margin;
if (available <= 0 || width <= 0) return;
if (available <= 0 || width <= 0) return false;
display.setTextSize(1);
const int label_height = display.textLineHeight();
int value_size = 2;
@@ -211,7 +212,7 @@ inline void drawBottomPairingBlock(DisplayDriver& display, int content_top,
display.setTextSize(value_size = 1);
}
const int value_height = display.textLineHeight();
if (value_height <= 0 || value_height > available) return;
if (value_height <= 0 || value_height > available) return false;
// Prefer a readable complete PIN; omit its label on unusually short
// viewports instead of colliding with the title or clipping the digits.
const bool show_label = label_height > 0
@@ -227,6 +228,7 @@ inline void drawBottomPairingBlock(DisplayDriver& display, int content_top,
}
display.setTextSize(value_size);
drawTextCenteredEllipsized(display, region, bottom - value_height, value);
return display.getTextWidth(value) <= width;
}
} // namespace ui
+34 -13
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Run the production pairing UI methods and OLED selection with USB present."""
"""Run production pairing UI methods and keep exactly one visible PIN."""
from pathlib import Path
import subprocess
import tempfile
@@ -52,9 +52,10 @@ struct Interfaces {
};
struct UIScreen {};
struct HomeScreen : UIScreen {
bool first = true;
bool isFirstPage() const { return first; }
void showFirstPage() { first = true; }
bool pairing_pin_visible = false;
bool hasPairingPin() const { return pairing_pin_visible; }
void showFirstPage() {}
void flushPocketSettings() {}
};
struct MsgPreviewScreen : UIScreen { bool hasMessages() const { return false; } };
struct Mesh {
@@ -96,23 +97,28 @@ struct UITask {
};
@METHODS@
static void homePairing(UITask* _task, TestDisplay& display) {
bool& _pairing_pin_visible = _task->home_screen.pairing_pin_visible;
_pairing_pin_visible = false;
#if !UI_POCKET_GUI
const int home_content_top = 20;
@HOME_SELECTION@
#endif
}
static void checkPairingPresentation(UITask& ui, TestDisplay& display) {
display.text.clear();
homePairing(&ui, display);
const bool home_pin = std::count(display.text.begin(), display.text.end(), "246810") == 1;
assert(ui.home_screen.hasPairingPin() == home_pin);
ui.renderPairingBanner();
#if defined(HELTEC_T096)
if (display.height() > 64) {
if (home_pin) {
assert(std::count(display.text.begin(), display.text.end(), "246810") == 1);
assert(std::count(display.text.begin(), display.text.end(), "PAIR PIN 246810") == 0);
// Another home page or asynchronous screen still needs the top PIN.
ui.home_screen.first = false;
ui.home_screen.pairing_pin_visible = false;
display.text.clear();
ui.renderPairingBanner();
assert(display.text.back() == "PAIR PIN 246810");
ui.home_screen.showFirstPage();
ui.home_screen.pairing_pin_visible = true;
ui.curr = ui.msg_preview;
display.text.clear();
ui.renderPairingBanner();
@@ -120,7 +126,6 @@ static void checkPairingPresentation(UITask& ui, TestDisplay& display) {
ui.curr = ui.home;
return;
}
#endif
assert(display.text.back() == "PAIR PIN 246810");
}
int main(int argc, char** argv) {
@@ -155,7 +160,13 @@ int main(int argc, char** argv) {
assert(!ui.isPairingScreenActive() && !ui._pairing_from_button);
display.text.clear();
homePairing(&ui, display);
assert(display.text.back() == "246810"); // USB must not replace the idle PIN.
#if UI_POCKET_GUI
assert(display.text.empty()); // Pocket's board view has no standalone PIN.
#else
if (display.height() >= 64)
assert(display.text.back() == "246810"); // USB must not replace the idle PIN.
else assert(display.text.empty()); // Too short: only the pairing overlay fits.
#endif
interfaces.pending = true;
ui.servicePairingState();
@@ -164,7 +175,10 @@ int main(int argc, char** argv) {
assert(!ui.isPairingScreenActive());
display.text.clear();
homePairing(&ui, display);
assert(display.text.back() == "CONNECTED");
#if !UI_POCKET_GUI
if (display.height() >= 64) assert(display.text.back() == "CONNECTED");
else assert(display.text.empty());
#endif
// Explicit off must remain off; disabling BLE must not reveal a stale PIN.
interfaces.ble = false;
@@ -183,7 +197,11 @@ int main(int argc, char** argv) {
assert(display.isOn() && !ui.isPairingScreenActive());
display.text.clear();
homePairing(&ui, display);
assert(display.text.back() == "CONNECTED"); // Only USB status remains.
#if !UI_POCKET_GUI
if (display.height() >= 64)
assert(display.text.back() == "CONNECTED"); // Only USB status remains.
else assert(display.text.empty());
#endif
}
'''
@@ -213,8 +231,11 @@ class CompanionPairingDisplayTests(unittest.TestCase):
cpp, binary = Path(temp) / "test.cpp", Path(temp) / "test"
cpp.write_text(program)
for profile, flags, sizes in (
("oled", [], [(128, 64)]),
("shared", [], [(128, 64), (160, 80), (80, 160), (128, 128),
(240, 135), (320, 170), (320, 240), (240, 320),
(250, 122), (200, 200), (128, 24)]),
("t096", ["-DHELTEC_T096=1"], [(160, 80), (80, 160), (128, 64)]),
("pocket", ["-DUI_POCKET_GUI=1"], [(250, 122), (200, 200)]),
):
with self.subTest(profile=profile):
result = subprocess.run([
+40 -20
View File
@@ -21,17 +21,20 @@ ColorVal UIColor::window_bkg=0, UIColor::title_bkg=0, UIColor::title_txt=1;
ColorVal UIColor::primary_txt=1, UIColor::secondary_txt=1, UIColor::warning_txt=1;
ColorVal UIColor::popup_bkg=0, UIColor::popup_txt=1, UIColor::corp_blue=1;
#define PRESS_LABEL "Press"
#define SELECT_LABEL "Select"
struct Display : DisplayDriver {
struct Row { mesh::ui::DisplayRegion box; std::string text; };
std::vector<Row> rows;
int size=1,x=0,y=0,small,large;
Display(int w,int h,int a,int b):DisplayDriver(w,h),small(a),large(b) {}
int size=1,x=0,y=0,small,large,render_size;
Display(int w,int h,int a,int b,int r=0):DisplayDriver(w,h),small(a),large(b),render_size(r) {}
int renderWidth() const override { return render_size?render_size:width(); }
int renderHeight() const override { return render_size?render_size:height(); }
bool isOn() override { return true; }
void turnOn() override {} void turnOff() override {}
void clear() override { rows.clear(); }
void startFrame(ColorVal=0) override { clear(); } void endFrame() override {}
void setTextSize(int s) override { size=s; }
int textLineHeight() override { return size==1?small:large; }
int textLineHeight() override { return size==1?small:size==2?large:size*small; }
void setColor(ColorVal) override {}
void setCursor(int a,int b) override { x=a;y=b; }
uint16_t getTextWidth(const char* text) override {
@@ -86,24 +89,37 @@ struct Board { int getBattMilliVolts(){return 4100;} };
SCENARIOS = r'''
int main() {
for(auto profile: {std::vector<int>{128,64,8,16}, {160,80,8,16},
{250,122,21,27}, {250,122,22,29}, {200,200,22,29}, {240,135,10,20}}) {
Display d(profile[0],profile[1],profile[2],profile[3]); Task task;
for(int state=0;state<5;++state) for(int count:{0,99,99999}) {
#ifdef UI_DEDICATED_PAIRING_BLOCK
const std::vector<std::vector<int>> profiles={{160,160,8,16,320},{160,160,8,16,480}};
#else
const std::vector<std::vector<int>> profiles={{128,64,8,16}, {160,80,8,16},
{250,122,21,27}, {250,122,22,29}, {200,200,22,29}, {240,135,10,20}};
#endif
for(auto profile:profiles) {
Display d(profile[0],profile[1],profile[2],profile[3],profile.size()>4?profile[4]:0); Task task;
bool pairing_pin_visible=true;
for(int state=0;state<5;++state) for(int count:{0,99,99999}) for(uint32_t pin:{123456U,12345U}) {
task.enabled=state!=2; task.connected=state==1; task.prompt=state==4;
task.count=count; the_mesh.pin=state==3?0:123456;
d.clear(); home(d,&task);
task.count=count; the_mesh.pin=state==3?0:pin;
d.clear(); home(d,&task,pairing_pin_visible);
bool pin_expected=task.enabled && !task.connected && the_mesh.pin
&& (!task.prompt || d.height()>64);
assert((d.find("123456")!=nullptr)==pin_expected);
char pin_text[16]; snprintf(pin_text,sizeof(pin_text),"%06u",(unsigned)the_mesh.pin);
assert((d.find(pin_text)!=nullptr)==pin_expected);
assert(pairing_pin_visible==(d.find(pin_text)!=nullptr));
if(pin_expected) {
auto pin=d.find("123456");
auto pin=d.find(pin_text);
assert(pin->box.y>=d.height()/2);
#ifndef UI_DEDICATED_PAIRING_BLOCK
assert(pin->box.bottom()==d.height()-(d.height()>64?2:0));
#endif
assert(!d.find("Press: inbox"));
}
if(task.connected) assert(d.find("CONNECTED"));
if(task.connected) assert(d.find("CONNECTED") || d.find("LINKED"));
}
// A render on another page must clear a PIN retained from the first page.
pairing_pin_visible=true; d.clear(); home(d,&task,pairing_pin_visible,1);
assert(!pairing_pin_visible);
Prefs prefs; Board board; d.clear(); repeater(&d,&prefs,&board);
assert(d.rows.size()==5); // Never solve overlap by losing a status row.
d.clear(); d.setTextSize(1);
@@ -118,9 +134,10 @@ int main() {
}
// Sweep font sizes and short viewports: retain complete digits, omit only
// a label that cannot fit, and never draw outside the reserved lower band.
for(int h=16;h<=160;++h) for(int line=8;line<=32;++line) {
Display d(160,h,line,2*line);
mesh::ui::drawBottomPairingBlock(d,h/3,"BLUETOOTH PIN","012345");
for(int width:{32,80,160}) for(int h=16;h<=160;++h) for(int line=8;line<=32;++line) {
Display d(width,h,line,2*line);
const bool pin_visible=mesh::ui::drawBottomPairingBlock(d,h/3,"BLUETOOTH PIN","012345");
assert(pin_visible==(d.find("012345")!=nullptr));
for(const auto& row:d.rows) assert(row.box.y>=h/3);
}
}
@@ -130,23 +147,26 @@ int main() {
class HomeTextSpacingTest(unittest.TestCase):
def test_actual_home_and_repeater_branches(self):
source = (ROOT / 'examples/companion_radio/ui-new/UITask.cpp').read_text()
start = source.index(' const int normal_line =')
start = source.index(' _pairing_pin_visible = false;')
metrics = source[start:source.index(' display.setColor', start)]
start = source.index(' if (_page == HomePage::FIRST) {', start)
home = source[start:source.index('#if UI_MESSAGES_HOME_PAGE == 1', start)]
repeater = (ROOT / 'examples/simple_repeater/UITask.cpp').read_text()
start = repeater.index(' // Reserve a full measured font-height')
repeater = repeater[start:repeater.index('\n }\n}', start)]
implementation = ('void home(DisplayDriver& display, Task* _task) {\n'
'display.setTextSize(1); char tmp[80]; int _page=0;\n'
implementation = ('void home(DisplayDriver& display, Task* _task, '
'bool& _pairing_pin_visible, int _page=0) {\n'
'char tmp[80];\n'
+ metrics + 'display.drawTextEllipsized(0,2,display.width(),"Node name");\n'
+ home + '\n}}\n'
'void repeater(DisplayDriver* _display, Prefs* _node_prefs, Board* _board) {'
'char tmp[80];\n' + repeater + '\n}\n')
for wifi in (False, True):
with self.subTest(wifi=wifi), tempfile.TemporaryDirectory() as temp:
for wifi, dedicated in ((False, False), (True, False), (False, True), (True, True)):
with self.subTest(wifi=wifi, dedicated=dedicated), tempfile.TemporaryDirectory() as temp:
binary = str(Path(temp) / 'spacing')
flags = ['-DWIFI_SSID="test"', '-DWITH_MQTT_BRIDGE'] if wifi else []
if dedicated:
flags.append('-DUI_DEDICATED_PAIRING_BLOCK=1')
result = subprocess.run(['c++', '-std=c++11', '-fsanitize=address,undefined',
'-fno-pie', '-no-pie', '-I'+str(ROOT/'src'),
*flags, '-x', 'c++', '-', '-o', binary],