Fix Indicator transport taps and fresh LoRa defaults

This commit is contained in:
mikecarper committed 2026-08-31 15:56:54 -07:00
1 parent 7791f3f8c9
commit bbb9312340
13 files changed
+344 -123

No files matched your search

+3 -1
View File
@@ -1490,7 +1490,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.autoadd_config = DEFAULT_AUTOADD_CONFIG;
_prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE;
_prefs.radio_fem_txgain = 0;
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
#ifdef DEFAULT_RX_DELAY_BASE
_prefs.rx_delay_base = DEFAULT_RX_DELAY_BASE;
#endif
_prefs.setRepeatEn(false);
#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) \
|| defined(USE_LR2021) || defined(SX126X_RX_BOOSTED_GAIN) \
+80 -41
View File
@@ -86,18 +86,28 @@ static void drawCompanionTransportChoice(DisplayDriver& display,
}
const bool large_transport_text = display.height() >= 96;
display.setTextSize(large_transport_text ? 3 : 1);
const bool show_status_label = height >= 44;
const int label_y = show_status_label
? y + (large_transport_text ? 17 : 12)
: y + (height - 8) / 2;
display.drawTextCentered(x + width / 2, label_y, label);
if (large_transport_text) {
display.setTextSize(4);
if (strcmp(label, "WiFi") == 0) {
// A single size-4 "WiFi" row is wider than one half of the screen.
// Reflow it into two large rows instead of shrinking both choices.
display.drawTextCentered(x + width / 2, y + 2, "Wi");
display.drawTextCentered(x + width / 2, y + 36, "Fi");
} else {
display.drawTextCentered(x + width / 2, y + 25, label);
}
} else {
display.setTextSize(1);
const int label_y = show_status_label ? y + 12 : y + (height - 8) / 2;
display.drawTextCentered(x + width / 2, label_y, label);
}
if (show_status_label && (active || selected)) {
display.setTextSize(large_transport_text ? 2 : 1);
display.setTextSize(large_transport_text ? 3 : 1);
display.drawTextCentered(
x + width / 2,
y + height - (large_transport_text ? 25 : 17),
active ? "ACTIVE"
active ? (large_transport_text ? "ON" : "ACTIVE")
: (large_transport_text ? "NEXT" : "NEXT BOOT"));
}
}
@@ -381,17 +391,19 @@ public:
mesh::ui::usesExpandedCompanionHomeTypography(
display.width(), display.height(),
display.renderWidth(), display.renderHeight());
sprintf(tmp, expanded_home ? "INBOX %d" : "INBOX: %d",
_task->getPreviewCount());
display.setTextSize(expanded_home ? 3 : 2);
if (expanded_home && display.getTextWidth(tmp) > display.width() - 4) {
if (expanded_home) {
display.setTextSize(4);
display.drawTextCentered(display.width() / 2, 20, "INBOX");
} else {
sprintf(tmp, "INBOX: %d", _task->getPreviewCount());
display.setTextSize(2);
display.drawTextCentered(display.width() / 2, 22, tmp);
}
#else
display.setTextSize(2);
sprintf(tmp, "INBOX: %d", _task->getPreviewCount());
#endif
display.drawTextCentered(display.width() / 2, 22, tmp);
#endif
#ifdef UI_DEDICATED_PAIRING_BLOCK
const mesh::ui::CompanionHomeLayout layout =
mesh::ui::makeLargeCompanionHomeLayout(display.width(),
@@ -405,27 +417,35 @@ public:
mesh::ui::clearDisplayRegion(display, layout.info);
mesh::ui::clearDisplayRegion(display, layout.pairing);
display.setTextSize(expanded_home ? 2 : 1);
display.setColor(UIColor::secondary_txt);
mesh::ui::drawTextCenteredEllipsized(
display, layout.info, layout.instruction_y,
expanded_home ? "tap inbox" : "tap center: inbox");
#ifdef WIFI_SSID
if (isCompanionWiFiEnabled()) {
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d",
ip[0], ip[1], ip[2], ip[3]);
} else {
strcpy(tmp, "WiFi: OFF");
}
display.setTextSize(expanded_home ? 2 : 1);
if (expanded_home) {
snprintf(tmp, sizeof(tmp), "%d", _task->getPreviewCount());
display.setTextSize(4);
if (display.getTextWidth(tmp) > layout.info.width) {
display.setTextSize(1);
display.setTextSize(3);
}
display.setColor(UIColor::primary_txt);
mesh::ui::drawTextCenteredEllipsized(
display, layout.info, layout.network_y, tmp);
#endif
display, layout.info, layout.instruction_y, tmp);
} else {
display.setTextSize(1);
display.setColor(UIColor::secondary_txt);
mesh::ui::drawTextCenteredEllipsized(
display, layout.info, layout.instruction_y,
"tap center: inbox");
#ifdef WIFI_SSID
if (isCompanionWiFiEnabled()) {
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d",
ip[0], ip[1], ip[2], ip[3]);
} else {
strcpy(tmp, "WiFi: OFF");
}
display.setTextSize(1);
mesh::ui::drawTextCenteredEllipsized(
display, layout.info, layout.network_y, tmp);
#endif
}
const bool bluetooth_enabled = _task->isBluetoothEnabled();
const bool bluetooth_connected = _task->hasBluetoothConnection();
@@ -436,10 +456,11 @@ public:
char pairing_pin[16];
if (bluetooth_connected) {
pairing_label = expanded_home ? "BLE" : "BLUETOOTH";
pairing_value = "CONNECTED";
pairing_value = expanded_home ? "LINKED" : "CONNECTED";
if (expanded_home) pairing_value_size = 3;
} else if (mesh::ui::shouldDisplayBluetoothPairingPin(
bluetooth_enabled, bluetooth_connected, bluetooth_pin)) {
pairing_label = expanded_home ? "BLE PIN" : "BLUETOOTH PIN";
pairing_label = expanded_home ? "PIN" : "BLUETOOTH PIN";
snprintf(pairing_pin, sizeof(pairing_pin), "%06u",
(unsigned int)bluetooth_pin);
pairing_value = pairing_pin;
@@ -450,12 +471,30 @@ public:
display.setColor(UIColor::title_bkg);
mesh::ui::clearDisplayRegion(display, layout.pairing);
display.setColor(UIColor::title_txt);
display.setTextSize(expanded_home ? 2 : 1);
display.setTextSize(expanded_home ? 3 : 1);
mesh::ui::drawTextCenteredEllipsized(
display, layout.pairing, layout.pairing_label_y, pairing_label);
display.setTextSize(pairing_value_size);
mesh::ui::drawTextCenteredEllipsized(
display, layout.pairing, layout.pairing_value_y, pairing_value);
} else if (expanded_home) {
display.setColor(UIColor::secondary_txt);
display.setTextSize(3);
mesh::ui::drawTextCenteredEllipsized(
display, layout.pairing, layout.pairing_label_y, "TAP");
#ifdef WIFI_SSID
if (isCompanionWiFiEnabled()) {
IPAddress ip = WiFi.localIP();
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d",
ip[0], ip[1], ip[2], ip[3]);
display.setTextSize(1);
} else {
strcpy(tmp, "OFF");
display.setTextSize(3);
}
mesh::ui::drawTextCenteredEllipsized(
display, layout.pairing, layout.pairing_value_y, tmp);
#endif
}
#else
const bool bluetooth_enabled = _task->isBluetoothEnabled();
@@ -611,16 +650,15 @@ public:
mesh::ui::makeCompanionTransportSelectorLayout(
display.width(), display.height());
// Keep this page independent of the additional native-480 text boost:
// the size-3 choice names and size-2 status already consume nearly the
// full half-screen width, and should remain equally readable in the
// memory-saving 320 profile.
// Keep this page independent of the additional native-480 text boost.
// Its deliberately reflowed size-4 choices then retain the same large
// physical dimensions in the 320 and 480 render profiles.
display.setCompactText(true);
if (layout.show_title) {
display.setColor(UIColor::primary_txt);
display.setTextSize(2);
display.setTextSize(3);
display.drawTextCentered(
display.width() / 2, layout.title_y, "TRANSPORT");
display.width() / 2, layout.title_y, "MODE");
}
drawCompanionTransportChoice(
display, layout.wifi.x, layout.wifi.y,
@@ -632,10 +670,11 @@ public:
"BLE", !wifi_active, !wifi_selected);
display.setColor(UIColor::secondary_txt);
display.setTextSize(1);
display.setTextSize(layout.show_title ? 2 : 1);
#ifdef HAS_TOUCH
display.drawTextCentered(
display.width() / 2, layout.prompt_y, "tap a box");
display.width() / 2, layout.prompt_y,
layout.show_title ? "TAP SIDE" : "tap a box");
#else
display.drawTextCentered(
display.width() / 2, layout.prompt_y, PRESS_LABEL);
+18 -8
View File
@@ -48,16 +48,26 @@ class UITask : public AbstractUITask {
#ifndef TOUCH_CENTER_ZONE_PERCENT
#define TOUCH_CENTER_ZONE_PERCENT 34
#endif
#if defined(TOUCH_REVERSE_SWIPE) \
&& defined(TOUCH_SEPARATE_VERTICAL_SWIPES)
mesh::ui::TouchInput touch_input{true, true, TOUCH_CENTER_ZONE_PERCENT};
#elif defined(TOUCH_REVERSE_SWIPE)
mesh::ui::TouchInput touch_input{true, false, TOUCH_CENTER_ZONE_PERCENT};
#elif defined(TOUCH_SEPARATE_VERTICAL_SWIPES)
mesh::ui::TouchInput touch_input{false, true, TOUCH_CENTER_ZONE_PERCENT};
#ifdef TOUCH_REVERSE_SWIPE
static constexpr bool TOUCH_REVERSE_SWIPE_ENABLED = true;
#else
mesh::ui::TouchInput touch_input{false, false, TOUCH_CENTER_ZONE_PERCENT};
static constexpr bool TOUCH_REVERSE_SWIPE_ENABLED = false;
#endif
#ifdef TOUCH_SEPARATE_VERTICAL_SWIPES
static constexpr bool TOUCH_SEPARATE_VERTICAL_SWIPES_ENABLED = true;
#else
static constexpr bool TOUCH_SEPARATE_VERTICAL_SWIPES_ENABLED = false;
#endif
#ifdef TOUCH_MIRROR_TAP_X
static constexpr bool TOUCH_MIRROR_TAP_X_ENABLED = true;
#else
static constexpr bool TOUCH_MIRROR_TAP_X_ENABLED = false;
#endif
mesh::ui::TouchInput touch_input{
TOUCH_REVERSE_SWIPE_ENABLED,
TOUCH_SEPARATE_VERTICAL_SWIPES_ENABLED,
TOUCH_CENTER_ZONE_PERCENT,
TOUCH_MIRROR_TAP_X_ENABLED};
unsigned long next_touch_check = 0;
#endif
#ifdef PIN_STATUS_LED
+9 -5
View File
@@ -70,13 +70,17 @@ inline CompanionHomeLayout makeLargeCompanionHomeLayout(int width,
int height,
bool expanded = false) {
if (expanded && width == 160 && height == 160) {
// Native 480 uses three large vertical bands: an INBOX title above this
// count region, then one lower region that is either the BLE state or a
// large tap/network hint. Reusing the lower region conditionally gives
// the prominent text room to grow without overlapping another status.
return {
{2, 51, 156, 46},
{8, 100, 144, 58},
52,
76,
{2, 61, 156, 39},
{8, 102, 144, 58},
61,
61,
102,
126,
131,
};
}
@@ -23,10 +23,10 @@ inline CompanionTransportSelectorLayout makeCompanionTransportSelectorLayout(
const bool tall_display = height >= 96;
const int margin = 2;
const int gap = 4;
const int box_y = tall_display ? 36 : 20;
const int prompt_y = height - (tall_display ? 20 : 11);
int box_height = prompt_y - box_y - (tall_display ? 6 : 8);
if (box_height > 98) box_height = 98;
const int box_y = tall_display ? 40 : 20;
const int prompt_y = height - (tall_display ? 17 : 11);
int box_height = prompt_y - box_y - (tall_display ? 3 : 8);
if (box_height > 100) box_height = 100;
if (box_height < 20) box_height = 20;
const int box_width = (width - margin * 2 - gap) / 2;
const int bluetooth_x = margin + box_width + gap;
@@ -34,7 +34,7 @@ inline CompanionTransportSelectorLayout makeCompanionTransportSelectorLayout(
return {
{margin, box_y, box_width, box_height},
{bluetooth_x, box_y, box_width, box_height},
tall_display ? 17 : 0,
tall_display ? 14 : 0,
prompt_y,
tall_display,
};
+18 -9
View File
@@ -32,6 +32,7 @@ class TouchInput {
bool _active = false;
bool _reverse_swipes;
bool _separate_vertical_swipes;
bool _mirror_tap_x;
uint8_t _center_zone_percent;
uint8_t _touch_samples = 0;
uint8_t _release_samples = 0;
@@ -57,9 +58,11 @@ class TouchInput {
public:
explicit TouchInput(bool reverse_swipes = false,
bool separate_vertical_swipes = false,
uint8_t center_zone_percent = 34)
uint8_t center_zone_percent = 34,
bool mirror_tap_x = false)
: _reverse_swipes(reverse_swipes),
_separate_vertical_swipes(separate_vertical_swipes),
_mirror_tap_x(mirror_tap_x),
_center_zone_percent(center_zone_percent > 100
? 100
: center_zone_percent) {}
@@ -113,15 +116,21 @@ public:
return verticalSwipeAction(dy < 0);
}
// Some panels report a mirrored X axis. Swipe direction has its own
// independent policy because gesture direction and stationary screen
// coordinates are different concerns. Apply the panel correction only
// after movement has been ruled out so it cannot alter swipe detection.
const int tap_x = _mirror_tap_x ? width - 1 - _start_x : _start_x;
// Message screens may reserve the otherwise empty ends of their bottom
// status bar as forgiving arrow buttons. Keep the label in the middle
// inert so an imprecise arrow tap cannot accidentally close the screen.
if (bottom_selector && _separate_vertical_swipes
&& _start_y >= (height * 3) / 4) {
if (_start_x < width / 4) {
if (tap_x < width / 4) {
return TouchAction::VerticalPrevious;
}
if (_start_x >= (width * 3) / 4) {
if (tap_x >= (width * 3) / 4) {
return TouchAction::VerticalNext;
}
return TouchAction::None;
@@ -135,13 +144,13 @@ public:
|| _start_y >= split_selector->top_y + split_selector->height) {
return TouchAction::None;
}
if (_start_x >= split_selector->left_x
&& _start_x < split_selector->left_x
if (tap_x >= split_selector->left_x
&& tap_x < split_selector->left_x
+ split_selector->left_width) {
return TouchAction::SelectLeft;
}
if (_start_x >= split_selector->right_x
&& _start_x < split_selector->right_x
if (tap_x >= split_selector->right_x
&& tap_x < split_selector->right_x
+ split_selector->right_width) {
return TouchAction::SelectRight;
}
@@ -156,8 +165,8 @@ public:
const int side_percent = (100 - _center_zone_percent) / 2;
const int center_left = (width * side_percent) / 100;
const int center_right = width - center_left;
if (_start_x < center_left) return TouchAction::Previous;
if (_start_x >= center_right) return TouchAction::Next;
if (tap_x < center_left) return TouchAction::Previous;
if (tap_x >= center_right) return TouchAction::Next;
return TouchAction::Select;
}
+41 -29
View File
@@ -20,8 +20,8 @@ int main() {
if (!layout.show_title) return 1;
if (layout.wifi.x != 2 || layout.wifi.width != 76) return 2;
if (layout.bluetooth.x != 82 || layout.bluetooth.width != 76) return 3;
if (layout.wifi.y != 36 || layout.wifi.height != 98) return 4;
if (layout.title_y != 17 || layout.prompt_y != 140) return 5;
if (layout.wifi.y != 40 || layout.wifi.height != 100) return 4;
if (layout.title_y != 14 || layout.prompt_y != 143) return 5;
const auto small = mesh::ui::makeCompanionTransportSelectorLayout(128, 64);
if (small.show_title) return 6;
@@ -56,21 +56,25 @@ int main() {
)
self.assertEqual(run_result.returncode, 0, run_result.stderr)
choice_glyph_width = 6 * 3
choice_glyph_height = 8 * 3
for text in ("WiFi", "BLE"):
choice_glyph_width = 6 * 4
choice_glyph_height = 8 * 4
for text in ("Wi", "Fi", "BLE"):
self.assertLessEqual(len(text) * choice_glyph_width, 76)
status_glyph_width = 6 * 2
status_glyph_height = 8 * 2
for text in ("ACTIVE", "NEXT"):
status_glyph_width = 6 * 3
status_glyph_height = 8 * 3
for text in ("ON", "NEXT"):
self.assertLessEqual(len(text) * status_glyph_width, 76)
title_glyph_width = 6 * 2
for text in ("TRANSPORT", "tap a box"):
self.assertLessEqual(len(text) * title_glyph_width, 160)
self.assertLessEqual(17 + 8 * 2, 36)
self.assertLessEqual(53 + choice_glyph_height, 134)
self.assertLessEqual(109 + status_glyph_height, 134)
self.assertLessEqual(140 + 8, 160)
self.assertLessEqual(len("MODE") * 6 * 3, 160)
self.assertLessEqual(len("TAP SIDE") * 6 * 2, 160)
self.assertLessEqual(14 + 8 * 3, 40)
wifi_first_row_y = 42
wifi_second_row_y = 76
self.assertLessEqual(wifi_first_row_y + choice_glyph_height,
wifi_second_row_y)
self.assertLessEqual(wifi_first_row_y + choice_glyph_height, 140)
self.assertLessEqual(76 + choice_glyph_height, 140)
self.assertLessEqual(115 + status_glyph_height, 140)
self.assertLessEqual(143 + 8 * 2, 160)
def test_touch_split_selector_distinguishes_taps_from_swipes(self):
source = r'''
@@ -88,19 +92,19 @@ static TouchAction release(TouchInput& input,
}
int main() {
TouchInput input(true, true, 70);
TouchInput input(true, true, 70, true);
const auto layout = mesh::ui::makeCompanionTransportSelectorLayout(160, 160);
const TouchSplitSelector selector{
layout.wifi.x, layout.wifi.width,
layout.bluetooth.x, layout.bluetooth.width,
layout.wifi.y, layout.wifi.height};
input.update(true, 30, 70, 160, 160, false, &selector);
input.update(true, 31, 70, 160, 160, false, &selector);
input.update(true, 129, 70, 160, 160, false, &selector);
input.update(true, 128, 70, 160, 160, false, &selector);
if (release(input, &selector) != TouchAction::SelectLeft) return 1;
input.update(true, 130, 70, 160, 160, false, &selector);
input.update(true, 129, 70, 160, 160, false, &selector);
input.update(true, 30, 70, 160, 160, false, &selector);
input.update(true, 31, 70, 160, 160, false, &selector);
if (release(input, &selector) != TouchAction::SelectRight) return 2;
input.update(true, 80, 70, 160, 160, false, &selector);
@@ -121,10 +125,10 @@ int main() {
// A quick selector tap may occupy only one 25 ms poll. Its bounded target
// remains unambiguous after the normal two-sample release debounce.
input.update(true, 30, 120, 160, 160, false, &selector);
input.update(true, 129, 120, 160, 160, false, &selector);
if (release(input, &selector) != TouchAction::SelectLeft) return 7;
input.update(true, 130, 120, 160, 160, false, &selector);
input.update(true, 30, 120, 160, 160, false, &selector);
if (release(input, &selector) != TouchAction::SelectRight) return 8;
// The gap, title, and exclusive bottom edge remain inert even for a quick
@@ -132,14 +136,14 @@ int main() {
input.update(true, 80, 120, 160, 160, false, &selector);
if (release(input, &selector) != TouchAction::None) return 9;
input.update(true, 30, layout.wifi.y - 1, 160, 160, false, &selector);
input.update(true, 129, layout.wifi.y - 1, 160, 160, false, &selector);
if (release(input, &selector) != TouchAction::None) return 10;
input.update(true, 30, layout.wifi.y + layout.wifi.height,
input.update(true, 129, layout.wifi.y + layout.wifi.height,
160, 160, false, &selector);
if (release(input, &selector) != TouchAction::None) return 11;
input.update(true, 30, layout.wifi.y + layout.wifi.height - 1,
input.update(true, 129, layout.wifi.y + layout.wifi.height - 1,
160, 160, false, &selector);
if (release(input, &selector) != TouchAction::SelectLeft) return 12;
@@ -182,12 +186,16 @@ int main() {
self.assertIn('"WiFi", wifi_active, wifi_selected', source)
self.assertIn('"BLE", !wifi_active, !wifi_selected', source)
self.assertIn("display.fillRect(x, y, width, height);", source)
self.assertIn('"ACTIVE"', source)
self.assertIn('large_transport_text ? 3 : 1', source)
self.assertIn('large_transport_text ? "ON" : "ACTIVE"', source)
self.assertIn('display.setTextSize(4);', source)
self.assertIn('display.drawTextCentered(x + width / 2, y + 2, "Wi");', source)
self.assertIn('display.drawTextCentered(x + width / 2, y + 36, "Fi");', source)
self.assertIn("const bool show_status_label = height >= 44;", source)
self.assertIn('active ? "ACTIVE"', source)
self.assertIn('active ? (large_transport_text ? "ON" : "ACTIVE")', source)
self.assertIn('large_transport_text ? "NEXT" : "NEXT BOOT"', source)
self.assertIn('display.setTextSize(2);', source)
self.assertIn('display.setTextSize(3);', source)
self.assertIn('display.width() / 2, layout.title_y, "MODE"', source)
self.assertIn('layout.show_title ? "TAP SIDE" : "tap a box"', source)
self.assertIn("makeCompanionTransportSelectorLayout", source)
handler_start = source.index(
@@ -221,6 +229,10 @@ int main() {
def test_only_split_page_maps_box_taps_to_transport_keys(self):
source = UI.read_text(encoding="utf-8")
profile = (
ROOT / "variants" / "sensecap_indicator-espnow" / "platformio.ini"
).read_text(encoding="utf-8")
self.assertIn("-D TOUCH_MIRROR_TAP_X", profile)
self.assertIn(
"curr == home\n"
" && static_cast<HomeScreen*>(home)"
@@ -192,23 +192,21 @@ TEST(DisplayDriver, NativeIndicatorExpandsHomeTypographyRegions) {
const mesh::ui::CompanionHomeLayout layout =
mesh::ui::makeLargeCompanionHomeLayout(160, 160, true);
EXPECT_EQ(2, layout.info.x);
EXPECT_EQ(51, layout.info.y);
EXPECT_EQ(61, layout.info.y);
EXPECT_EQ(156, layout.info.width);
EXPECT_EQ(46, layout.info.height);
EXPECT_EQ(39, layout.info.height);
EXPECT_EQ(8, layout.pairing.x);
EXPECT_EQ(100, layout.pairing.y);
EXPECT_EQ(102, layout.pairing.y);
EXPECT_EQ(144, layout.pairing.width);
EXPECT_EQ(58, layout.pairing.height);
EXPECT_FALSE(mesh::ui::displayRegionsOverlap(layout.info,
layout.pairing));
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
layout.info, layout.instruction_y, 20));
layout.info, layout.instruction_y, 39));
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
layout.info, layout.network_y, 20));
layout.pairing, layout.pairing_label_y, 29));
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
layout.pairing, layout.pairing_label_y, 20));
EXPECT_TRUE(mesh::ui::displayRegionContainsLine(
layout.pairing, layout.pairing_value_y, 30));
layout.pairing, layout.pairing_value_y, 29));
}
TEST(DisplayDriver, IndicatorPairingValuesFitAtRenderedTextSizes) {
+24 -14
View File
@@ -135,24 +135,34 @@ class IndicatorDisplayProfileTest(unittest.TestCase):
self.assertLessEqual(height, 30)
# Native 480 reflows the spacious home page instead of globally
# enlarging every dense UI row. Its prominent strings fit at their
# requested sizes, while a two-digit inbox count takes the deliberate
# size-2 fallback.
native_title = dimensions("INBOX 0", 3, 3, 1.2)
native_two_digit_title = dimensions("INBOX 10", 3, 3, 1.2)
native_two_digit_fallback = dimensions("INBOX 10", 2, 3, 1.2)
# enlarging every dense UI row. The title, count, action, and pairing
# state each occupy their own large row.
native_title = dimensions("INBOX", 4, 3, 1.2)
native_count = dimensions("9999", 4, 3, 1.2)
native_count_fallback = dimensions("99999", 3, 3, 1.2)
self.assertLessEqual(native_title[0], 156)
self.assertGreater(native_two_digit_title[0], 156)
self.assertLessEqual(native_two_digit_fallback[0], 156)
self.assertLessEqual(native_title[1], 39)
self.assertLessEqual(native_count[0], 156)
self.assertLessEqual(native_count_fallback[0], 156)
for text, region_width in (
("tap inbox", 156),
("WiFi: OFF", 156),
("BLE PIN", 144),
for text, requested_size, region_width in (
("TAP", 3, 144),
("OFF", 3, 144),
("PIN", 3, 144),
("LINKED", 3, 144),
("123456", 3, 144),
("IP: 192.168.100.200", 1, 144),
):
width, height = dimensions(text, 2, 3, 1.2)
width, height = dimensions(text, requested_size, 3, 1.2)
self.assertLessEqual(width, region_width)
self.assertLessEqual(height, 20)
self.assertLessEqual(height, 29 if requested_size == 3 else 10)
# The two lower size-3 rows must not depend on blank pixels inside the
# checked-in VLW: the built-in fallback font can fill its whole cell.
pairing_label_y = 102
pairing_value_y = 131
self.assertLessEqual(pairing_label_y + 29, pairing_value_y)
self.assertLessEqual(pairing_value_y + 29, 160)
if __name__ == "__main__":
+71
View File
@@ -9,6 +9,9 @@ import unittest
ROOT = Path(__file__).resolve().parents[1]
BUILD = ROOT / "build.sh"
MAIN = ROOT / "examples" / "companion_radio" / "main.cpp"
MESH = ROOT / "examples" / "companion_radio" / "MyMesh.cpp"
DATA_STORE = ROOT / "examples" / "companion_radio" / "DataStore.cpp"
PROFILE = ROOT / "variants" / "sensecap_indicator-espnow" / "platformio.ini"
class IndicatorRenderProfileTest(unittest.TestCase):
@@ -117,6 +120,74 @@ int main() {
self.assertIn("static_cast<uint16_t>(renderWidth()), _compactText", display)
self.assertIn("* profile_scale;", display)
def test_lora_fresh_install_uses_compiled_cascade_defaults(self):
profile = PROFILE.read_text(encoding="utf-8")
lora = profile[profile.index("[SenseCapIndicator-LoRa]") :]
lora = lora[: lora.index("[env:")]
self.assertIn("-UMESH_PRIMARY_ESPNOW", lora)
self.assertIn("-D SENSECAP_INDICATOR_LORA", lora)
self.assertIn("-D USE_SX1262", lora)
self.assertIn("-<helpers/esp32/ESPNOWRadio.cpp>", lora)
mesh = MESH.read_text(encoding="utf-8")
self.assertIn("_prefs.freq = LORA_FREQ;", mesh)
self.assertIn("_prefs.sf = LORA_SF;", mesh)
self.assertIn("_prefs.bw = LORA_BW;", mesh)
self.assertIn("_prefs.cr = LORA_CR;", mesh)
guarded_rx_delay = (
"#ifdef DEFAULT_RX_DELAY_BASE\n"
" _prefs.rx_delay_base = DEFAULT_RX_DELAY_BASE;\n"
"#endif"
)
self.assertIn(guarded_rx_delay, mesh)
# Saved preferences load after constructor defaults, so an app-only
# update remains non-destructive while an erased install uses them.
begin = mesh[mesh.index("void MyMesh::begin(") :]
self.assertIn("_store->loadPrefs(_prefs", begin)
store = DATA_STORE.read_text(encoding="utf-8")
self.assertIn('if (_fs->exists("/new_prefs"))', store)
self.assertIn('loadPrefsInt("/new_prefs", prefs', store)
def test_explicit_usa_cascadia_and_cascade_build_flags(self):
command = r'''
set -e
source "$1"
PLATFORMIO_BUILD_FLAGS=""
RADIO_FREQ_OVERRIDE=910.525
RADIO_BW_OVERRIDE=62.5
RADIO_SF_OVERRIDE=7
RADIO_CR_OVERRIDE=5
FIRMWARE_PROFILE_OVERRIDE=cascade
apply_radio_overrides
apply_firmware_profile_overrides
printf '%s\n' "$PLATFORMIO_BUILD_FLAGS"
'''
result = subprocess.run(
["bash", "-c", command, "test", str(BUILD)],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
flags = result.stdout.split()
for expected in (
"-DLORA_FREQ=910.525",
"-DLORA_BW=62.5",
"-DLORA_SF=7",
"-DLORA_CR=5",
"-DCASCADE_PROFILE=1",
"-DDEFAULT_RX_DELAY_BASE=2.0f",
):
self.assertIn(expected, flags)
build = BUILD.read_text(encoding="utf-8")
self.assertIn(
"'SenseCapIndicator-LoRa_comp_radio_usb_wifi|"
"SenseCapIndicator-LoRa_companion_radio_full'",
build,
)
if __name__ == "__main__":
unittest.main()
@@ -218,6 +218,47 @@ TEST(TouchInput, CanReverseSwipesWithoutReversingTapZones) {
EXPECT_EQ(release(input), TouchAction::Next);
}
TEST(TouchInput, CanMirrorStationaryTapZonesIndependentlyOfSwipes) {
TouchInput input(true, true, 70, true);
const TouchSplitSelector selector{2, 76, 82, 76, 40, 100};
// The controller's raw-right coordinate is the visual-left WiFi box.
input.update(true, 129, 80, 160, 160, false, &selector);
EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector),
TouchAction::None);
EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector),
TouchAction::SelectLeft);
// The controller's raw-left coordinate is the visual-right BLE box.
input.update(true, 30, 80, 160, 160, false, &selector);
input.update(true, 31, 80, 160, 160, false, &selector);
EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector),
TouchAction::None);
EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector),
TouchAction::SelectRight);
// Generic side taps and the message footer arrows use the same visual-X
// correction, while their center zones stay centered.
input.update(true, 150, 60, 160, 160);
input.update(true, 150, 60, 160, 160);
EXPECT_EQ(release(input, 160, 160), TouchAction::Previous);
input.update(true, 10, 140, 160, 160, true);
input.update(true, 10, 140, 160, 160, true);
EXPECT_EQ(input.update(false, -1, -1, 160, 160, true),
TouchAction::None);
EXPECT_EQ(input.update(false, -1, -1, 160, 160, true),
TouchAction::VerticalNext);
// Swipe direction remains governed only by reverse_swipes.
input.update(true, 130, 70, 160, 160, false, &selector);
input.update(true, 30, 72, 160, 160, false, &selector);
EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector),
TouchAction::None);
EXPECT_EQ(input.update(false, -1, -1, 160, 160, false, &selector),
TouchAction::Previous);
}
TEST(TouchInput, KeepsGestureAcrossOneMissingTouchSample) {
TouchInput input;
+25 -3
View File
@@ -1,5 +1,22 @@
# SenseCAP Indicator controls
The primary mesh radio is selected by the firmware image, not by the WiFi/BLE
screen. Use `SenseCapIndicator-LoRa_companion_radio_full` for a LoRa-equipped
D1L or D1Pro. Build a fresh image explicitly with:
```sh
bash build.sh build-firmware SenseCapIndicator-LoRa_companion_radio_full \
--radio-preset usa-cascadia --profile cascade --clean
```
The build uses the live USA/Canada preset when the settings service is
available; its offline USA Cascadia fallback is 910.525 MHz, BW 62.5, SF7,
CR5. The Cascade profile also supplies its fresh-install defaults, including
RX delay 2. `SenseCapIndicator-ESPNow_companion_radio_full` remains the image
for an Indicator without the LoRa assembly. An application-only update
preserves the previously saved numeric radio tuple; erase/install the LoRa
merged image when the compiled fresh defaults must replace it.
The Indicator preserves the existing 160x160 UI coordinate system. Ordinary
images use a 320x320, 4-bit internal canvas scaled 1.5x onto the 480x480 panel.
Full Companion images first request a native 480x480 canvas, then normally
@@ -16,6 +33,9 @@ wakes it without selecting anything.
side performs Previous or Next; horizontal swipes remain available anywhere.
- A gesture is committed after a stable release. Brief missing touch samples
are ignored so one swipe cannot become a second, opposite endpoint tap.
- The SenseCAP controller reports a mirrored physical X coordinate. The board
profile corrects stationary visual left/right taps independently from its
existing swipe-direction correction.
- The transport page is the exception for a very quick stationary contact:
either tall, outlined WiFi/BLE box can accept one sampled point after the
same stable-release debounce. Taps in the title, center gap, or footer stay
@@ -117,10 +137,12 @@ available in both modes for recovery.
The Indicator transport page presents WiFi and BLE as full-height side-by-side
choices. Tapping either choice saves it and reboots only when it differs from
the active mode. Its size-3 choice labels render at the same physical size in
the active mode. WiFi is split over two size-4 rows, BLE uses size 4, and the
short `ON`/`NEXT` state uses size 3; all render at the same physical size in
the 320 and 480 profiles. On a retained native 480 canvas, the first home page
also reflows its inbox heading and short status labels at larger sizes; long IP
addresses automatically fall back to the smaller row size instead of clipping.
uses separate full-width size-4 `INBOX` and count rows plus a large lower
action or BLE-status block. Long IP addresses retain a smaller bounded row
instead of clipping.
LoRa remains the primary radio in both LoRa modes. On the ESP-NOW layout,
selecting BLE leaves the primary ESP-NOW WiFi radio and its fixed channel
@@ -53,6 +53,9 @@ build_flags =
-D PIN_USER_BTN=38
-D HAS_TOUCH
-D TOUCH_REVERSE_SWIPE
; The panel's physical X axis is mirrored. Swipe direction is corrected
; separately; stationary taps also need visual left/right hit testing.
-D TOUCH_MIRROR_TAP_X
-D TOUCH_SEPARATE_VERTICAL_SWIPES
; The outer 15% remains available for Previous/Next taps; the middle 70%
; is Select. This makes the primary tap target much easier on a 480px panel.