2026-09-25 23:33:11 +02:00
#pragma once
2026-09-26 09:14:58 +02:00
// Home > Repeater -- ui-new's Tools > Repeater: relay other people's
2026-09-25 23:33:11 +02:00
// messages from this device. The switch, the network it relays on (the
// companion's own frequency, or a dedicated profile with its preset / freq /
// SF / BW / CR), and the flood filters (adverts, hop limit, yield, min SNR,
// duplicate suppression, scope only, extra scopes). Every change applies at
// once and is saved; the logic is ui-core/RepeaterControl.h, the relaying MyMesh.
//
// Single-TU fragment: included at the end of ui-lvgl/UITask.cpp.
namespace rptview {
enum : uint8_t { RP_ON , RP_NETWORK , RP_PRESET , RP_SF , RP_BW , RP_CR , RP_HOPS , RP_YIELD , RP_SNR };
2026-09-26 09:14:58 +02:00
static const int OPTS_LEN = 768 ;
static char * s_opts = psramBuf < char > ( OPTS_LEN ); // dropdown options (LVGL copies them)
2026-09-25 23:33:11 +02:00
static lv_obj_t * s_scopes_sub = nullptr ; // "Extra scopes" row's count, updated from the popup
2026-10-01 11:27:48 +02:00
static lv_obj_t * s_on_sub = nullptr ; // the Repeater switch's hint
2026-09-25 23:33:11 +02:00
} // namespace rptview
static void onOpenRepeater ( lv_event_t * e ) { ( void ) e ; s_ui -> showRepeater (); }
static void onRptDropdown ( lv_event_t * e ) {
2026-09-27 01:17:48 +02:00
s_ui -> repeaterSet (( int )( uintptr_t ) lv_event_get_user_data ( e ), choiceSelected (( lv_obj_t * ) lv_event_get_target ( e )));
2026-09-25 23:33:11 +02:00
}
static void onRptSwitch ( lv_event_t * e ) {
s_ui -> repeaterSet (( int )( uintptr_t ) lv_event_get_user_data ( e ),
lv_obj_has_state (( lv_obj_t * ) lv_event_get_target ( e ), LV_STATE_CHECKED ) ? 1 : 0 );
}
static void onRptNetwork ( lv_event_t * e ) {
s_ui -> repeaterSet ( rptview :: RP_NETWORK , ( int ) lv_buttonmatrix_get_selected_button (( lv_obj_t * ) lv_event_get_target ( e )));
}
static void onRptFreq ( lv_event_t * e ) { ( void ) e ; s_ui -> radioFreqPopup ( true ); }
static void onRptScopes ( lv_event_t * e ) { ( void ) e ; s_ui -> repeaterScopesPopup (); }
static void onRptScopeSwitch ( lv_event_t * e ) {
s_ui -> repeaterScopeSet (( uint8_t )( uintptr_t ) lv_event_get_user_data ( e ),
lv_obj_has_state (( lv_obj_t * ) lv_event_get_target ( e ), LV_STATE_CHECKED ));
}
static void extraScopesSummary ( const NodePrefs * p , char * b , int n ) {
const ScopeList & sl = the_mesh . scopeList ();
if ( sl . count == 0 ) snprintf ( b , n , "No scopes set up (Settings > Radio)" );
else snprintf ( b , n , "%d of %u relayed besides the default" , rptctl :: extraScopesPicked ( p ), ( unsigned ) sl . count );
}
void UITask :: showRepeater () {
_screen = SCR_REPEATER ;
buildRepeater ();
}
void UITask :: buildRepeater () {
using namespace rptview ;
lv_obj_t * body = newScreen ( "Repeater" , true );
2026-09-30 08:53:15 +02:00
s_scopes_sub = nullptr ; // made with its group (repeaterRelays)
2026-09-25 23:33:11 +02:00
NodePrefs * p = _prefs ;
2026-09-27 01:17:48 +02:00
lv_obj_t * g = group ( body , nullptr );
2026-11-02 10:32:23 +01:00
lv_obj_t * sw = switchRow ( g , "Repeater" , p -> client_repeat ? "Relaying for others" : "Relay others' messages" , p -> client_repeat , onRptSwitch , ( void * )( uintptr_t ) RP_ON );
2026-10-01 11:27:48 +02:00
s_on_sub = rowSub ( lv_obj_get_parent ( sw ));
2026-09-27 01:17:48 +02:00
groupNote ( body , "Uses more battery; auto power pauses." );
2026-09-30 08:53:15 +02:00
fill ( & UITask :: repeaterGroup , 1 , 3 ); // the rest as the loop goes
}
2026-09-25 23:33:11 +02:00
2026-09-30 08:53:15 +02:00
void UITask :: repeaterGroup ( int i ) {
using namespace rptview ;
if ( i == 2 ) { repeaterRelays (); return ; }
NodePrefs * p = _prefs ;
lv_obj_t * g = group ( _body , "NETWORK" );
2026-09-27 01:17:48 +02:00
lv_obj_t * row = settingRow ( g , "Relay on" , p -> repeater_use_profile ? "Its own frequency" : "Your chat frequency" );
2026-09-25 23:33:11 +02:00
static const char * NET [] = { "Current" , "Custom" , "" };
2026-09-27 01:17:48 +02:00
radioview :: rowSegmented ( row , NET , p -> repeater_use_profile ? 1 : 0 , 150 , onRptNetwork , 0 );
2026-09-25 23:33:11 +02:00
if ( p -> repeater_use_profile ) {
2026-09-27 01:17:48 +02:00
static const uint8_t IDS [ 4 ] = { RP_PRESET , RP_SF , RP_BW , RP_CR };
radioview :: paramRows ( g , p , rptctl :: currentPreset ( p ), p -> repeater_freq , p -> repeater_sf , p -> repeater_bw ,
p -> repeater_cr , onRptDropdown , IDS , onRptFreq , nullptr );
2026-09-25 23:33:11 +02:00
}
2026-09-30 08:53:15 +02:00
}
2026-09-25 23:33:11 +02:00
2026-09-30 08:53:15 +02:00
void UITask :: repeaterRelays () {
using namespace rptview ;
using radioview :: rowDropdown ;
NodePrefs * p = _prefs ;
lv_obj_t * g = group ( _body , "WHAT IT RELAYS" );
2026-09-27 01:17:48 +02:00
switchRow ( g , "Skip adverts" , "Relay messages, not adverts" , & p -> repeat_skip_adverts );
2026-09-25 23:33:11 +02:00
char v [ 12 ];
int o = 0 ;
for ( int i = 0 ; i <= rptctl :: MAX_HOPS ; i ++ ) {
rptctl :: fmtHops ( v , sizeof ( v ), ( uint8_t ) i );
2026-09-26 09:14:58 +02:00
o += snprintf ( s_opts + o , OPTS_LEN - o , "%s%s" , i ? " \n " : "" , v );
2026-09-25 23:33:11 +02:00
}
2026-09-27 01:17:48 +02:00
rowDropdown ( settingRow ( g , "Max hops" , "Hop limit for floods" ), s_opts , p -> repeat_max_hops , 0 , onRptDropdown , RP_HOPS );
2026-09-25 23:33:11 +02:00
o = 0 ;
for ( int i = 0 ; i <= rptctl :: MAX_YIELD ; i ++ ) {
rptctl :: fmtYield ( v , sizeof ( v ), ( uint8_t ) i );
2026-09-26 09:14:58 +02:00
o += snprintf ( s_opts + o , OPTS_LEN - o , "%s%s" , i ? " \n " : "" , v );
2026-09-25 23:33:11 +02:00
}
2026-09-27 01:17:48 +02:00
rowDropdown ( settingRow ( g , "Yield" , "Let fixed repeaters go first" ), s_opts , p -> repeat_delay_boost , 0 ,
2026-09-25 23:33:11 +02:00
onRptDropdown , RP_YIELD );
o = 0 ;
for ( int i = 0 ; i < rptctl :: snrChoiceCount (); i ++ ) {
rptctl :: fmtSnr ( v , sizeof ( v ), rptctl :: snrFromChoice ( i ));
2026-09-26 09:14:58 +02:00
o += snprintf ( s_opts + o , OPTS_LEN - o , "%s%s" , i ? " \n " : "" , v );
2026-09-25 23:33:11 +02:00
}
2026-09-27 01:17:48 +02:00
rowDropdown ( settingRow ( g , "Min SNR" , "Ignore weaker packets" ), s_opts , rptctl :: snrToChoice ( p -> repeat_min_snr ), 0 ,
2026-09-25 23:33:11 +02:00
onRptDropdown , RP_SNR );
2026-09-27 01:17:48 +02:00
switchRow ( g , "Skip duplicates" , "Skip if already relayed" , & p -> repeat_suppress_dup );
switchRow ( g , "Scope only" , "Only floods in your scopes" , & p -> repeat_scope_only );
2026-09-25 23:33:11 +02:00
char sub [ 48 ];
extraScopesSummary ( p , sub , sizeof ( sub ));
2026-09-27 01:17:48 +02:00
s_scopes_sub = rowSub ( listRow ( g , "Extra scopes" , sub , onRptScopes , NULL ));
2026-09-25 23:33:11 +02:00
}
// Rebuilds the screen where it was scrolled to.
void UITask :: rebuildRepeater () {
int32_t y = _body ? lv_obj_get_scroll_y ( _body ) : 0 ;
buildRepeater ();
2026-09-30 08:53:15 +02:00
fillFlush ();
2026-10-01 10:30:27 +02:00
if ( _body ) { layoutNow ( _body ); lv_obj_scroll_to_y ( _body , y , LV_ANIM_OFF ); }
2026-09-25 23:33:11 +02:00
}
void UITask :: repeaterSet ( int which , int v ) {
using namespace rptview ;
NodePrefs * p = _prefs ;
if ( ! p ) return ;
switch ( which ) {
case RP_ON :
rptctl :: setEnabled ( p , v != 0 );
2026-10-01 11:27:48 +02:00
if ( s_on_sub ) lv_label_set_text ( s_on_sub , p -> client_repeat ? "Relaying for others" : "Relay others' messages" );
2026-09-25 23:33:11 +02:00
break ;
case RP_NETWORK : rptctl :: setUseProfile ( p , v != 0 ); break ;
case RP_PRESET : if ( v == 0 ) return ; rptctl :: choosePreset ( p , v - 1 ); break ;
case RP_SF : p -> repeater_sf = ( uint8_t )( 5 + v ); rptctl :: applyProfile (); break ;
case RP_BW : if ( v >= 0 && v < LORA_BW_OPT_COUNT ) { p -> repeater_bw = LORA_BW_OPTS [ v ]; rptctl :: applyProfile (); } break ;
case RP_CR : p -> repeater_cr = ( uint8_t )( 5 + v ); rptctl :: applyProfile (); break ;
case RP_HOPS : p -> repeat_max_hops = ( uint8_t ) v ; break ;
case RP_YIELD : p -> repeat_delay_boost = ( uint8_t ) v ; break ;
case RP_SNR : p -> repeat_min_snr = rptctl :: snrFromChoice ( v ); break ;
}
2026-09-28 15:47:57 +02:00
prefsSave ();
2026-10-01 11:27:48 +02:00
if ( which == RP_NETWORK ) rebuildSoon ( & UITask :: rebuildRepeater ); // the profile rows come or go, once it's moved
else if ( which > RP_NETWORK && which <= RP_CR ) rebuildRepeater (); // hints, the profile rows and the preset name follow
2026-09-25 23:33:11 +02:00
refreshStatusBar ();
}
// A switch per named scope: relayed besides the default one.
void UITask :: repeaterScopesPopup () {
const ScopeList & sl = the_mesh . scopeList ();
if ( sl . count == 0 ) { showToast ( "Set up scopes in Settings > Radio first" ); return ; }
lv_obj_t * panel = navPopupPanel ( "Extra scopes" , sl . count > 2 );
if ( sl . count > 2 ) lv_obj_add_flag ( panel , LV_OBJ_FLAG_SCROLLABLE ); // up to 8 rows
2026-09-28 22:35:26 +02:00
noteLabel ( panel , "Relayed besides the default scope." );
2026-09-25 23:33:11 +02:00
for ( uint8_t i = 0 ; i < sl . count ; i ++ ) {
lv_obj_t * sw = switchRow ( panel , sl . name (( uint8_t )( i + 1 )), ( i + 1 ) == sl . default_idx ? "Default - always relayed" : nullptr ,
nullptr );
if ( rptctl :: extraScope ( _prefs , i )) lv_obj_add_state ( sw , LV_STATE_CHECKED );
lv_obj_add_event_cb ( sw , onRptScopeSwitch , LV_EVENT_VALUE_CHANGED , ( void * )( uintptr_t ) i );
}
}
void UITask :: repeaterScopeSet ( uint8_t i , bool on ) {
rptctl :: setExtraScope ( _prefs , i , on );
2026-09-28 15:47:57 +02:00
prefsSave ();
2026-09-25 23:33:11 +02:00
char sub [ 48 ];
extraScopesSummary ( _prefs , sub , sizeof ( sub ));
if ( rptview :: s_scopes_sub ) lv_label_set_text ( rptview :: s_scopes_sub , sub );
}