# include "tools/replay/consoleui.h"
# include <initializer_list>
# include <string>
# include <tuple>
# include <utility>
# include <QApplication>
# include "common/version.h"
namespace {
const int BORDER_SIZE = 3 ;
const std : : initializer_list < std : : pair < std : : string , std : : string > > keyboard_shortcuts [ ] = {
{
{ " s " , " +10s " } ,
{ " shift+s " , " -10s " } ,
{ " m " , " +60s " } ,
{ " shift+m " , " -60s " } ,
{ " space " , " Pause/Resume " } ,
{ " e " , " Next Engagement " } ,
{ " d " , " Next Disengagement " } ,
{ " t " , " Next User Tag " } ,
{ " i " , " Next Info " } ,
{ " w " , " Next Warning " } ,
{ " c " , " Next Critical " } ,
} ,
{
{ " enter " , " Enter seek request " } ,
{ " +/- " , " Playback speed " } ,
{ " q " , " Exit " } ,
} ,
} ;
enum Color {
Default ,
Debug ,
Yellow ,
Green ,
Red ,
Cyan ,
BrightWhite ,
Engaged ,
Disengaged ,
} ;
void add_str ( WINDOW * w , const char * str , Color color = Color : : Default , bool bold = false ) {
if ( color ! = Color : : Default ) wattron ( w , COLOR_PAIR ( color ) ) ;
if ( bold ) wattron ( w , A_BOLD ) ;
waddstr ( w , str ) ;
if ( bold ) wattroff ( w , A_BOLD ) ;
if ( color ! = Color : : Default ) wattroff ( w , COLOR_PAIR ( color ) ) ;
}
} // namespace
ConsoleUI : : ConsoleUI ( Replay * replay , QObject * parent ) : replay ( replay ) , sm ( { " carState " , " liveParameters " } ) , QObject ( parent ) {
// Initialize curses
initscr ( ) ;
clear ( ) ;
curs_set ( false ) ;
cbreak ( ) ; // Line buffering disabled. pass on everything
noecho ( ) ;
keypad ( stdscr , true ) ;
nodelay ( stdscr , true ) ; // non-blocking getchar()
// Initialize all the colors. https://www.ditig.com/256-colors-cheat-sheet
start_color ( ) ;
init_pair ( Color : : Debug , 246 , COLOR_BLACK ) ; // #949494
init_pair ( Color : : Yellow , 184 , COLOR_BLACK ) ;
init_pair ( Color : : Red , COLOR_RED , COLOR_BLACK ) ;
init_pair ( Color : : Cyan , COLOR_CYAN , COLOR_BLACK ) ;
init_pair ( Color : : BrightWhite , 15 , COLOR_BLACK ) ;
init_pair ( Color : : Disengaged , COLOR_BLUE , COLOR_BLUE ) ;
init_pair ( Color : : Engaged , 28 , 28 ) ;
init_pair ( Color : : Green , 34 , COLOR_BLACK ) ;
initWindows ( ) ;
qRegisterMetaType < uint64_t > ( " uint64_t " ) ;
qRegisterMetaType < ReplyMsgType > ( " ReplyMsgType " ) ;
installMessageHandler ( [ this ] ( ReplyMsgType type , const std : : string msg ) {
emit logMessageSignal ( type , QString : : fromStdString ( msg ) ) ;
} ) ;
installDownloadProgressHandler ( [ this ] ( uint64_t cur , uint64_t total , bool success ) {
emit updateProgressBarSignal ( cur , total , success ) ;
} ) ;
QObject : : connect ( replay , & Replay : : streamStarted , this , & ConsoleUI : : updateSummary ) ;
QObject : : connect ( & notifier , SIGNAL ( activated ( int ) ) , SLOT ( readyRead ( ) ) ) ;
QObject : : connect ( this , & ConsoleUI : : updateProgressBarSignal , this , & ConsoleUI : : updateProgressBar ) ;
QObject : : connect ( this , & ConsoleUI : : logMessageSignal , this , & ConsoleUI : : logMessage ) ;
sm_timer . callOnTimeout ( this , & ConsoleUI : : updateStatus ) ;
sm_timer . start ( 100 ) ;
getch_timer . start ( 1000 , this ) ;
readyRead ( ) ;
}
ConsoleUI : : ~ ConsoleUI ( ) {
endwin ( ) ;
}
void ConsoleUI : : initWindows ( ) {
getmaxyx ( stdscr , max_height , max_width ) ;
w . fill ( nullptr ) ;
w [ Win : : Title ] = newwin ( 1 , max_width , 0 , 0 ) ;
w [ Win : : Stats ] = newwin ( 2 , max_width - 2 * BORDER_SIZE , 2 , BORDER_SIZE ) ;
w [ Win : : Timeline ] = newwin ( 4 , max_width - 2 * BORDER_SIZE , 5 , BORDER_SIZE ) ;
w [ Win : : TimelineDesc ] = newwin ( 1 , 100 , 10 , BORDER_SIZE ) ;
w [ Win : : CarState ] = newwin ( 3 , 100 , 12 , BORDER_SIZE ) ;
w [ Win : : DownloadBar ] = newwin ( 1 , 100 , 16 , BORDER_SIZE ) ;
if ( int log_height = max_height - 27 ; log_height > 4 ) {
w [ Win : : LogBorder ] = newwin ( log_height , max_width - 2 * ( BORDER_SIZE - 1 ) , 17 , BORDER_SIZE - 1 ) ;
box ( w [ Win : : LogBorder ] , 0 , 0 ) ;
w [ Win : : Log ] = newwin ( log_height - 2 , max_width - 2 * BORDER_SIZE , 18 , BORDER_SIZE ) ;
scrollok ( w [ Win : : Log ] , true ) ;
}
w [ Win : : Help ] = newwin ( 5 , max_width - ( 2 * BORDER_SIZE ) , max_height - 6 , BORDER_SIZE ) ;
// set the title bar
wbkgd ( w [ Win : : Title ] , A_REVERSE ) ;
mvwprintw ( w [ Win : : Title ] , 0 , 3 , " openpilot replay %s " , COMMA_VERSION ) ;
// show windows on the real screen
refresh ( ) ;
displayTimelineDesc ( ) ;
displayHelp ( ) ;
updateSummary ( ) ;
updateTimeline ( ) ;
for ( auto win : w ) {
if ( win ) wrefresh ( win ) ;
}
}
void ConsoleUI : : timerEvent ( QTimerEvent * ev ) {
if ( ev - > timerId ( ) ! = getch_timer . timerId ( ) ) return ;
if ( is_term_resized ( max_height , max_width ) ) {
for ( auto win : w ) {
if ( win ) delwin ( win ) ;
}
endwin ( ) ;
clear ( ) ;
refresh ( ) ;
initWindows ( ) ;
rWarning ( " resize term %dx%d " , max_height , max_width ) ;
}
updateTimeline ( ) ;
}
void ConsoleUI : : updateStatus ( ) {
auto write_item = [ this ] ( int y , int x , const char * key , const std : : string & value , const std : : string & unit ,
bool bold = false , Color color = Color : : BrightWhite ) {
auto win = w [ Win : : CarState ] ;
wmove ( win , y , x ) ;
add_str ( win , key ) ;
add_str ( win , value . c_str ( ) , color , bold ) ;
add_str ( win , unit . c_str ( ) ) ;
} ;
static const std : : pair < const char * , Color > status_text [ ] = {
{ " loading... " , Color : : Red } ,
{ " playing " , Color : : Green } ,
{ " paused... " , Color : : Yellow } ,
} ;
sm . update ( 0 ) ;
if ( status ! = Status : : Paused ) {
auto events = replay - > events ( ) ;
uint64_t current_mono_time = replay - > routeStartTime ( ) + replay - > currentSeconds ( ) * 1e9 ;
bool playing = ! events - > empty ( ) & & events - > back ( ) - > mono_time > current_mono_time ;
status = playing ? Status : : Playing : Status : : Waiting ;
}
auto [ status_str , status_color ] = status_text [ status ] ;
write_item ( 0 , 0 , " STATUS: " , status_str , " " , false , status_color ) ;
std : : string current_segment = " - " + std : : to_string ( ( int ) ( replay - > currentSeconds ( ) / 60 ) ) ;
write_item ( 0 , 25 , " TIME: " , replay - > currentDateTime ( ) . toString ( " ddd MMMM dd hh:mm:ss " ) . toStdString ( ) , current_segment , true ) ;
auto p = sm [ " liveParameters " ] . getLiveParameters ( ) ;
write_item ( 1 , 0 , " STIFFNESS: " , util : : string_format ( " %.2f %% " , p . getStiffnessFactor ( ) * 100 ) , " " ) ;
write_item ( 1 , 25 , " SPEED: " , util : : string_format ( " %.2f " , sm [ " carState " ] . getCarState ( ) . getVEgo ( ) ) , " m/s " ) ;
write_item ( 2 , 0 , " STEER RATIO: " , util : : string_format ( " %.2f " , p . getSteerRatio ( ) ) , " " ) ;
auto angle_offsets = util : : string_format ( " %.2f|%.2f " , p . getAngleOffsetAverageDeg ( ) , p . getAngleOffsetDeg ( ) ) ;
write_item ( 2 , 25 , " ANGLE OFFSET(AVG|INSTANT): " , angle_offsets , " deg " ) ;
wrefresh ( w [ Win : : CarState ] ) ;
}
void ConsoleUI : : displayHelp ( ) {
for ( int i = 0 ; i < std : : size ( keyboard_shortcuts ) ; + + i ) {
wmove ( w [ Win : : Help ] , i * 2 , 0 ) ;
for ( auto & [ key , desc ] : keyboard_shortcuts [ i ] ) {
wattron ( w [ Win : : Help ] , A_REVERSE ) ;
waddstr ( w [ Win : : Help ] , ( ' ' + key + ' ' ) . c_str ( ) ) ;
wattroff ( w [ Win : : Help ] , A_REVERSE ) ;
waddstr ( w [ Win : : Help ] , ( ' ' + desc + ' ' ) . c_str ( ) ) ;
}
}
wrefresh ( w [ Win : : Help ] ) ;
}
void ConsoleUI : : displayTimelineDesc ( ) {
std : : tuple < Color , const char * , bool > indicators [ ] {
{ Color : : Engaged , " Engaged " , false } ,
{ Color : : Disengaged , " Disengaged " , false } ,
{ Color : : Green , " Info " , true } ,
{ Color : : Yellow , " Warning " , true } ,
{ Color : : Red , " Critical " , true } ,
{ Color : : Cyan , " User Tag " , true } ,
} ;
for ( auto [ color , name , bold ] : indicators ) {
add_str ( w [ Win : : TimelineDesc ] , " __ " , color , bold ) ;
add_str ( w [ Win : : TimelineDesc ] , name ) ;
}
}
void ConsoleUI : : logMessage ( ReplyMsgType type , const QString & msg ) {
if ( auto win = w [ Win : : Log ] ) {
Color color = Color : : Default ;
if ( type = = ReplyMsgType : : Debug ) {
color = Color : : Debug ;
} else if ( type = = ReplyMsgType : : Warning ) {
color = Color : : Yellow ;
} else if ( type = = ReplyMsgType : : Critical ) {
color = Color : : Red ;
}
add_str ( win , qPrintable ( msg + " \n " ) , color ) ;
wrefresh ( win ) ;
}
}
void ConsoleUI : : updateProgressBar ( uint64_t cur , uint64_t total , bool success ) {
werase ( w [ Win : : DownloadBar ] ) ;
if ( success & & cur < total ) {
const int width = 35 ;
const float progress = cur / ( double ) total ;
const int pos = width * progress ;
wprintw ( w [ Win : : DownloadBar ] , " Downloading [%s>%s] %d%% %s " , std : : string ( pos , ' = ' ) . c_str ( ) ,
std : : string ( width - pos , ' ' ) . c_str ( ) , int ( progress * 100.0 ) , formattedDataSize ( total ) . c_str ( ) ) ;
}
wrefresh ( w [ Win : : DownloadBar ] ) ;
}
void ConsoleUI : : updateSummary ( ) {
const auto & route = replay - > route ( ) ;
mvwprintw ( w [ Win : : Stats ] , 0 , 0 , " Route: %s, %lu segments " , qPrintable ( route - > name ( ) ) , route - > segments ( ) . size ( ) ) ;
mvwprintw ( w [ Win : : Stats ] , 1 , 0 , " Car Fingerprint: %s " , replay - > carFingerprint ( ) . c_str ( ) ) ;
wrefresh ( w [ Win : : Stats ] ) ;
}
void ConsoleUI : : updateTimeline ( ) {
auto win = w [ Win : : Timeline ] ;
int width = getmaxx ( win ) ;
werase ( win ) ;
wattron ( win , COLOR_PAIR ( Color : : Disengaged ) ) ;
mvwhline ( win , 1 , 0 , ' ' , width ) ;
mvwhline ( win , 2 , 0 , ' ' , width ) ;
wattroff ( win , COLOR_PAIR ( Color : : Disengaged ) ) ;
const int total_sec = replay - > totalSeconds ( ) ;
for ( auto [ begin , end , type ] : replay - > getTimeline ( ) ) {
int start_pos = ( begin / total_sec ) * width ;
int end_pos = ( end / total_sec ) * width ;
if ( type = = TimelineType : : Engaged ) {
mvwchgat ( win , 1 , start_pos , end_pos - start_pos + 1 , A_COLOR , Color : : Engaged , NULL ) ;
mvwchgat ( win , 2 , start_pos , end_pos - start_pos + 1 , A_COLOR , Color : : Engaged , NULL ) ;
} else if ( type = = TimelineType : : UserFlag ) {
mvwchgat ( win , 3 , start_pos , end_pos - start_pos + 1 , ACS_S3 , Color : : Cyan , NULL ) ;
} else {
auto color_id = Color : : Green ;
if ( type ! = TimelineType : : AlertInfo ) {
color_id = type = = TimelineType : : AlertWarning ? Color : : Yellow : Color : : Red ;
}
mvwchgat ( win , 3 , start_pos , end_pos - start_pos + 1 , ACS_S3 , color_id , NULL ) ;
}
}
int cur_pos = ( ( double ) replay - > currentSeconds ( ) / total_sec ) * width ;
wattron ( win , COLOR_PAIR ( Color : : BrightWhite ) ) ;
mvwaddch ( win , 0 , cur_pos , ACS_VLINE ) ;
mvwaddch ( win , 3 , cur_pos , ACS_VLINE ) ;
wattroff ( win , COLOR_PAIR ( Color : : BrightWhite ) ) ;
wrefresh ( win ) ;
}
void ConsoleUI : : readyRead ( ) {
int c ;
while ( ( c = getch ( ) ) ! = ERR ) {
handleKey ( c ) ;
}
}
void ConsoleUI : : pauseReplay ( bool pause ) {
replay - > pause ( pause ) ;
status = pause ? Status : : Paused : Status : : Waiting ;
}
void ConsoleUI : : handleKey ( char c ) {
if ( c = = ' \n ' ) {
// pause the replay and blocking getchar()
pauseReplay ( true ) ;
updateStatus ( ) ;
getch_timer . stop ( ) ;
curs_set ( true ) ;
nodelay ( stdscr , false ) ;
// Wait for user input
rWarning ( " Waiting for input... " ) ;
int y = getmaxy ( stdscr ) - 9 ;
move ( y , BORDER_SIZE ) ;
add_str ( stdscr , " Enter seek request: " , Color : : BrightWhite , true ) ;
refresh ( ) ;
// Seek to choice
echo ( ) ;
int choice = 0 ;
scanw ( ( char * ) " %d " , & choice ) ;
noecho ( ) ;
pauseReplay ( false ) ;
replay - > seekTo ( choice , false ) ;
// Clean up and turn off the blocking mode
move ( y , 0 ) ;
clrtoeol ( ) ;
nodelay ( stdscr , true ) ;
curs_set ( false ) ;
refresh ( ) ;
getch_timer . start ( 1000 , this ) ;
} else if ( c = = ' + ' | | c = = ' = ' ) {
auto it = std : : upper_bound ( speed_array . begin ( ) , speed_array . end ( ) , replay - > getSpeed ( ) ) ;
if ( it ! = speed_array . end ( ) ) {
rWarning ( " playback speed: %.1fx " , * it ) ;
replay - > setSpeed ( * it ) ;
}
} else if ( c = = ' _ ' | | c = = ' - ' ) {
auto it = std : : lower_bound ( speed_array . begin ( ) , speed_array . end ( ) , replay - > getSpeed ( ) ) ;
if ( it ! = speed_array . begin ( ) ) {
auto prev = std : : prev ( it ) ;
rWarning ( " playback speed: %.1fx " , * prev ) ;
replay - > setSpeed ( * prev ) ;
}
} else if ( c = = ' e ' ) {
replay - > seekToFlag ( FindFlag : : nextEngagement ) ;
} else if ( c = = ' d ' ) {
replay - > seekToFlag ( FindFlag : : nextDisEngagement ) ;
} else if ( c = = ' t ' ) {
replay - > seekToFlag ( FindFlag : : nextUserFlag ) ;
} else if ( c = = ' i ' ) {
replay - > seekToFlag ( FindFlag : : nextInfo ) ;
} else if ( c = = ' w ' ) {
replay - > seekToFlag ( FindFlag : : nextWarning ) ;
} else if ( c = = ' c ' ) {
replay - > seekToFlag ( FindFlag : : nextCritical ) ;
} else if ( c = = ' m ' ) {
replay - > seekTo ( + 60 , true ) ;
} else if ( c = = ' M ' ) {
replay - > seekTo ( - 60 , true ) ;
} else if ( c = = ' s ' ) {
replay - > seekTo ( + 10 , true ) ;
} else if ( c = = ' S ' ) {
replay - > seekTo ( - 10 , true ) ;
} else if ( c = = ' ' ) {
pauseReplay ( ! replay - > isPaused ( ) ) ;
} else if ( c = = ' q ' | | c = = ' Q ' ) {
replay - > stop ( ) ;
qApp - > exit ( ) ;
}
}