1 // SPDX-License-Identifier: GPL-2.0-or-later 2 /* 3 * tui.c ncurses text user interface for TMON program 4 * 5 * Copyright (C) 2013 Intel Corporation. All rights reserved. 6 * 7 * Author: Jacob Pan <jacob.jun.pan@linux.intel.com> 8 */ 9 10 #include <unistd.h> 11 #include <stdio.h> 12 #include <stdlib.h> 13 #include <string.h> 14 #include <stdint.h> 15 #include <ncurses.h> 16 #include <time.h> 17 #include <syslog.h> 18 #include <panel.h> 19 #include <pthread.h> 20 #include <signal.h> 21 22 #include "tmon.h" 23 24 #define min(x, y) ({ \ 25 typeof(x) _min1 = (x); \ 26 typeof(y) _min2 = (y); \ 27 (void) (&_min1 == &_min2); \ 28 _min1 < _min2 ? _min1 : _min2; }) 29 30 #define max(x, y) ({ \ 31 typeof(x) _max1 = (x); \ 32 typeof(y) _max2 = (y); \ 33 (void) (&_max1 == &_max2); \ 34 _max1 > _max2 ? _max1 : _max2; }) 35 36 static PANEL *data_panel; 37 static PANEL *dialogue_panel; 38 static PANEL *top; 39 40 static WINDOW *title_bar_window; 41 static WINDOW *tz_sensor_window; 42 static WINDOW *cooling_device_window; 43 static WINDOW *control_window; 44 static WINDOW *status_bar_window; 45 static WINDOW *thermal_data_window; 46 static WINDOW *dialogue_window; 47 48 char status_bar_slots[10][40]; 49 static void draw_hbar(WINDOW *win, int y, int start, int len, 50 unsigned long pattern, bool end); 51 52 static int maxx, maxy; 53 static int maxwidth = 200; 54 55 #define TITLE_BAR_HIGHT 1 56 #define SENSOR_WIN_HIGHT 4 /* one row for tz name, one for trip points */ 57 58 59 /* daemon mode flag (set by startup parameter -d) */ 60 static int tui_disabled; 61 62 static void close_panel(PANEL *p) 63 { 64 if (p) { 65 del_panel(p); 66 p = NULL; 67 } 68 } 69 70 static void close_window(WINDOW *win) 71 { 72 if (win) { 73 delwin(win); 74 win = NULL; 75 } 76 } 77 78 void close_windows(void) 79 { 80 if (tui_disabled) 81 return; 82 /* must delete panels before their attached windows */ 83 if (dialogue_window) 84 close_panel(dialogue_panel); 85 if (cooling_device_window) 86 close_panel(data_panel); 87 88 close_window(title_bar_window); 89 close_window(tz_sensor_window); 90 close_window(status_bar_window); 91 close_window(cooling_device_window); 92 close_window(control_window); 93 close_window(thermal_data_window); 94 close_window(dialogue_window); 95 96 } 97 98 void write_status_bar(int x, char *line) 99 { 100 mvwprintw(status_bar_window, 0, x, "%s", line); 101 wrefresh(status_bar_window); 102 } 103 104 /* wrap at 5 */ 105 #define DIAG_DEV_ROWS 5 106 /* 107 * list cooling devices + "set temp" entry; wraps after 5 rows, if they fit 108 */ 109 static int diag_dev_rows(void) 110 { 111 int entries = ptdata.nr_cooling_dev + 1; 112 int rows = max(DIAG_DEV_ROWS, (entries + 1) / 2); 113 return min(rows, entries); 114 } 115 116 void setup_windows(void) 117 { 118 int y_begin = 1; 119 120 if (tui_disabled) 121 return; 122 123 getmaxyx(stdscr, maxy, maxx); 124 resizeterm(maxy, maxx); 125 126 title_bar_window = subwin(stdscr, TITLE_BAR_HIGHT, maxx, 0, 0); 127 y_begin += TITLE_BAR_HIGHT; 128 129 tz_sensor_window = subwin(stdscr, SENSOR_WIN_HIGHT, maxx, y_begin, 0); 130 y_begin += SENSOR_WIN_HIGHT; 131 132 cooling_device_window = subwin(stdscr, ptdata.nr_cooling_dev + 3, maxx, 133 y_begin, 0); 134 y_begin += ptdata.nr_cooling_dev + 3; /* 2 lines for border */ 135 /* two lines to show borders, one line per tz show trip point position 136 * and value. 137 * dialogue window is a pop-up, when needed it lays on top of cdev win 138 */ 139 140 dialogue_window = subwin(stdscr, diag_dev_rows() + 5, maxx-50, 141 DIAG_Y, DIAG_X); 142 143 thermal_data_window = subwin(stdscr, ptdata.nr_tz_sensor * 144 NR_LINES_TZDATA + 3, maxx, y_begin, 0); 145 y_begin += ptdata.nr_tz_sensor * NR_LINES_TZDATA + 3; 146 control_window = subwin(stdscr, 4, maxx, y_begin, 0); 147 148 scrollok(cooling_device_window, TRUE); 149 maxwidth = maxx - 18; 150 status_bar_window = subwin(stdscr, 1, maxx, maxy-1, 0); 151 152 strcpy(status_bar_slots[0], " Ctrl-c - Quit "); 153 strcpy(status_bar_slots[1], " TAB - Tuning "); 154 wmove(status_bar_window, 1, 30); 155 156 /* prepare panels for dialogue, if panel already created then we must 157 * be doing resizing, so just replace windows with new ones, old ones 158 * should have been deleted by close_window 159 */ 160 data_panel = new_panel(cooling_device_window); 161 if (!data_panel) 162 syslog(LOG_DEBUG, "No data panel\n"); 163 else { 164 if (dialogue_window) { 165 dialogue_panel = new_panel(dialogue_window); 166 if (!dialogue_panel) 167 syslog(LOG_DEBUG, "No dialogue panel\n"); 168 else { 169 /* Set up the user pointer to the next panel*/ 170 set_panel_userptr(data_panel, dialogue_panel); 171 set_panel_userptr(dialogue_panel, data_panel); 172 top = data_panel; 173 } 174 } else 175 syslog(LOG_INFO, "no dialogue win, term too small\n"); 176 } 177 doupdate(); 178 werase(stdscr); 179 refresh(); 180 } 181 182 void resize_handler(int sig) 183 { 184 /* start over when term gets resized, but first we clean up */ 185 close_windows(); 186 endwin(); 187 refresh(); 188 clear(); 189 getmaxyx(stdscr, maxy, maxx); /* get the new screen size */ 190 setup_windows(); 191 /* rate limit */ 192 sleep(1); 193 syslog(LOG_DEBUG, "SIG %d, term resized to %d x %d\n", 194 sig, maxy, maxx); 195 signal(SIGWINCH, resize_handler); 196 } 197 198 const char cdev_title[] = " COOLING DEVICES "; 199 void show_cooling_device(void) 200 { 201 int i, j, x, y = 0; 202 203 if (tui_disabled || !cooling_device_window) 204 return; 205 206 werase(cooling_device_window); 207 wattron(cooling_device_window, A_BOLD); 208 mvwprintw(cooling_device_window, 1, 1, 209 "ID Cooling Dev Cur Max Thermal Zone Binding"); 210 wattroff(cooling_device_window, A_BOLD); 211 for (j = 0; j < ptdata.nr_cooling_dev; j++) { 212 /* draw cooling device list on the left in the order of 213 * cooling device instances. skip unused idr. 214 */ 215 mvwprintw(cooling_device_window, j + 2, 1, 216 "%02d %12.12s%6d %6d", 217 ptdata.cdi[j].instance, 218 ptdata.cdi[j].type, 219 ptdata.cdi[j].cur_state, 220 ptdata.cdi[j].max_state); 221 } 222 223 /* show cdev binding, y is the global cooling device instance */ 224 for (i = 0; i < ptdata.nr_tz_sensor; i++) { 225 int tz_inst = ptdata.tzi[i].instance; 226 for (j = 0; j < ptdata.nr_cooling_dev; j++) { 227 int cdev_inst; 228 y = j; 229 x = tz_inst * TZONE_RECORD_SIZE + TZ_LEFT_ALIGN; 230 231 draw_hbar(cooling_device_window, y+2, x, 232 TZONE_RECORD_SIZE-1, ACS_VLINE, false); 233 234 /* draw a column of spaces to separate thermal zones */ 235 mvwprintw(cooling_device_window, y+2, x-1, " "); 236 if (ptdata.tzi[i].cdev_binding) { 237 cdev_inst = ptdata.cdi[j].instance; 238 unsigned long trip_binding = 239 ptdata.tzi[i].trip_binding[cdev_inst]; 240 int k = 0; /* per zone trip point id that 241 * binded to this cdev, one to 242 * many possible based on the 243 * binding bitmask. 244 */ 245 syslog(LOG_DEBUG, 246 "bind tz%d cdev%d tp%lx %d cdev%lx\n", 247 i, j, trip_binding, y, 248 ptdata.tzi[i].cdev_binding); 249 /* draw each trip binding for the cdev */ 250 while (trip_binding >>= 1) { 251 k++; 252 if (!(trip_binding & 1)) 253 continue; 254 /* draw '*' to show binding */ 255 mvwprintw(cooling_device_window, 256 y + 2, 257 x + ptdata.tzi[i].nr_trip_pts - 258 k - 1, "*"); 259 } 260 } 261 } 262 } 263 /* draw border after data so that border will not be messed up 264 * even there is not enough space for all the data to be shown 265 */ 266 wborder(cooling_device_window, 0, 0, 0, 0, 0, 0, 0, 0); 267 wattron(cooling_device_window, A_BOLD); 268 mvwprintw(cooling_device_window, 0, maxx/2 - sizeof(cdev_title), 269 cdev_title); 270 wattroff(cooling_device_window, A_BOLD); 271 272 wrefresh(cooling_device_window); 273 } 274 275 const char DIAG_TITLE[] = "[ TUNABLES ]"; 276 void show_dialogue(void) 277 { 278 int j, x = 0, y = 0; 279 int rows, cols; 280 WINDOW *w = dialogue_window; 281 282 if (tui_disabled || !w) 283 return; 284 285 getmaxyx(w, rows, cols); 286 287 /* Silence compiler 'unused' warnings */ 288 (void)cols; 289 290 werase(w); 291 box(w, 0, 0); 292 mvwprintw(w, 0, maxx/4, DIAG_TITLE); 293 /* list all the available tunables */ 294 for (j = 0; j <= ptdata.nr_cooling_dev; j++) { 295 y = j % diag_dev_rows(); 296 if (y == 0 && j != 0) 297 x += 20; 298 if (j == ptdata.nr_cooling_dev) 299 /* save last choice for target temp */ 300 mvwprintw(w, y+1, x+1, "%C-%.12s", 'A'+j, "Set Temp"); 301 else 302 mvwprintw(w, y+1, x+1, "%C-%.10s-%2d", 'A'+j, 303 ptdata.cdi[j].type, ptdata.cdi[j].instance); 304 } 305 wattron(w, A_BOLD); 306 mvwprintw(w, diag_dev_rows()+1, 1, "Enter Choice [A-Z]?"); 307 wattroff(w, A_BOLD); 308 /* print legend at the bottom line */ 309 mvwprintw(w, rows - 2, 1, 310 "Legend: A=Active, P=Passive, C=Critical"); 311 312 wrefresh(dialogue_window); 313 } 314 315 void write_dialogue_win(char *buf, int y, int x) 316 { 317 WINDOW *w = dialogue_window; 318 319 mvwprintw(w, y, x, "%s", buf); 320 } 321 322 const char control_title[] = " CONTROLS "; 323 void show_control_w(void) 324 { 325 unsigned long state; 326 327 get_ctrl_state(&state); 328 329 if (tui_disabled || !control_window) 330 return; 331 332 werase(control_window); 333 mvwprintw(control_window, 1, 1, 334 "PID gain: kp=%2.2f ki=%2.2f kd=%2.2f Output %2.2f", 335 p_param.kp, p_param.ki, p_param.kd, p_param.y_k); 336 337 mvwprintw(control_window, 2, 1, 338 "Target Temp: %2.1fC, Zone: %d, Control Device: %.12s", 339 p_param.t_target, target_thermal_zone, ctrl_cdev); 340 341 /* draw border last such that everything is within boundary */ 342 wborder(control_window, 0, 0, 0, 0, 0, 0, 0, 0); 343 wattron(control_window, A_BOLD); 344 mvwprintw(control_window, 0, maxx/2 - sizeof(control_title), 345 control_title); 346 wattroff(control_window, A_BOLD); 347 348 wrefresh(control_window); 349 } 350 351 void initialize_curses(void) 352 { 353 if (tui_disabled) 354 return; 355 356 initscr(); 357 start_color(); 358 keypad(stdscr, TRUE); /* enable keyboard mapping */ 359 nonl(); /* tell curses not to do NL->CR/NL on output */ 360 cbreak(); /* take input chars one at a time */ 361 noecho(); /* dont echo input */ 362 curs_set(0); /* turn off cursor */ 363 use_default_colors(); 364 365 init_pair(PT_COLOR_DEFAULT, COLOR_WHITE, COLOR_BLACK); 366 init_pair(PT_COLOR_HEADER_BAR, COLOR_BLACK, COLOR_WHITE); 367 init_pair(PT_COLOR_ERROR, COLOR_BLACK, COLOR_RED); 368 init_pair(PT_COLOR_RED, COLOR_WHITE, COLOR_RED); 369 init_pair(PT_COLOR_YELLOW, COLOR_WHITE, COLOR_YELLOW); 370 init_pair(PT_COLOR_GREEN, COLOR_WHITE, COLOR_GREEN); 371 init_pair(PT_COLOR_BLUE, COLOR_WHITE, COLOR_BLUE); 372 init_pair(PT_COLOR_BRIGHT, COLOR_WHITE, COLOR_BLACK); 373 374 } 375 376 void show_title_bar(void) 377 { 378 int i; 379 int x = 0; 380 381 if (tui_disabled || !title_bar_window) 382 return; 383 384 wattrset(title_bar_window, COLOR_PAIR(PT_COLOR_HEADER_BAR)); 385 wbkgd(title_bar_window, COLOR_PAIR(PT_COLOR_HEADER_BAR)); 386 werase(title_bar_window); 387 388 mvwprintw(title_bar_window, 0, 0, 389 " TMON v%s", VERSION); 390 391 wrefresh(title_bar_window); 392 393 werase(status_bar_window); 394 395 for (i = 0; i < 10; i++) { 396 if (strlen(status_bar_slots[i]) == 0) 397 continue; 398 wattron(status_bar_window, A_REVERSE); 399 mvwprintw(status_bar_window, 0, x, "%s", status_bar_slots[i]); 400 wattroff(status_bar_window, A_REVERSE); 401 x += strlen(status_bar_slots[i]) + 1; 402 } 403 wrefresh(status_bar_window); 404 } 405 406 static void handle_input_val(int ch) 407 { 408 char buf[32]; 409 int val; 410 char path[256]; 411 WINDOW *w = dialogue_window; 412 413 echo(); 414 keypad(w, TRUE); 415 wgetnstr(w, buf, 31); 416 val = atoi(buf); 417 418 if (ch == ptdata.nr_cooling_dev) { 419 snprintf(buf, 31, "Invalid Temp %d! %d-%d", val, 420 MIN_CTRL_TEMP, MAX_CTRL_TEMP); 421 if (val < MIN_CTRL_TEMP || val > MAX_CTRL_TEMP) 422 write_status_bar(40, buf); 423 else { 424 p_param.t_target = val; 425 snprintf(buf, 31, "Set New Target Temp %d", val); 426 write_status_bar(40, buf); 427 } 428 } else { 429 snprintf(path, 256, "%s/%s%d", THERMAL_SYSFS, 430 CDEV, ptdata.cdi[ch].instance); 431 sysfs_set_ulong(path, "cur_state", val); 432 } 433 noecho(); 434 dialogue_on = 0; 435 show_data_w(); 436 show_control_w(); 437 438 top = (PANEL *)panel_userptr(top); 439 top_panel(top); 440 } 441 442 static void handle_input_choice(int ch) 443 { 444 char buf[48]; 445 int base = 0; 446 int cdev_id = 0; 447 448 if ((ch >= 'A' && ch <= 'A' + ptdata.nr_cooling_dev) || 449 (ch >= 'a' && ch <= 'a' + ptdata.nr_cooling_dev)) { 450 base = (ch < 'a') ? 'A' : 'a'; 451 cdev_id = ch - base; 452 if (ptdata.nr_cooling_dev == cdev_id) 453 snprintf(buf, sizeof(buf), "New Target Temp:"); 454 else 455 snprintf(buf, sizeof(buf), "New Value for %.10s-%2d: ", 456 ptdata.cdi[cdev_id].type, 457 ptdata.cdi[cdev_id].instance); 458 write_dialogue_win(buf, diag_dev_rows() + 2, 2); 459 handle_input_val(cdev_id); 460 } else { 461 snprintf(buf, sizeof(buf), "Invalid selection %d", ch); 462 write_dialogue_win(buf, 8, 2); 463 } 464 } 465 466 void *handle_tui_events(void *arg) 467 { 468 int ch; 469 470 keypad(cooling_device_window, TRUE); 471 while ((ch = wgetch(cooling_device_window)) != EOF) { 472 if (tmon_exit) 473 break; 474 /* when term size is too small, no dialogue panels are set. 475 * we need to filter out such cases. 476 */ 477 if (!data_panel || !dialogue_panel || 478 !cooling_device_window || 479 !dialogue_window) { 480 481 continue; 482 } 483 pthread_mutex_lock(&input_lock); 484 if (dialogue_on) { 485 handle_input_choice(ch); 486 /* top panel filter */ 487 if (ch == 'q' || ch == 'Q') 488 ch = 0; 489 } 490 switch (ch) { 491 case KEY_LEFT: 492 box(cooling_device_window, 10, 0); 493 break; 494 case 9: /* TAB */ 495 top = (PANEL *)panel_userptr(top); 496 top_panel(top); 497 if (top == dialogue_panel) { 498 dialogue_on = 1; 499 show_dialogue(); 500 } else { 501 dialogue_on = 0; 502 /* force refresh */ 503 show_data_w(); 504 show_control_w(); 505 } 506 break; 507 case 'q': 508 case 'Q': 509 tmon_exit = 1; 510 break; 511 } 512 update_panels(); 513 doupdate(); 514 pthread_mutex_unlock(&input_lock); 515 } 516 517 if (arg) 518 *(int *)arg = 0; /* make gcc happy */ 519 520 return NULL; 521 } 522 523 /* draw a horizontal bar in given pattern */ 524 static void draw_hbar(WINDOW *win, int y, int start, int len, unsigned long ptn, 525 bool end) 526 { 527 mvwaddch(win, y, start, ptn); 528 whline(win, ptn, len); 529 if (end) 530 mvwaddch(win, y, MAX_DISP_TEMP+TDATA_LEFT, ']'); 531 } 532 533 static char trip_type_to_char(int type) 534 { 535 switch (type) { 536 case THERMAL_TRIP_CRITICAL: return 'C'; 537 case THERMAL_TRIP_HOT: return 'H'; 538 case THERMAL_TRIP_PASSIVE: return 'P'; 539 case THERMAL_TRIP_ACTIVE: return 'A'; 540 default: 541 return '?'; 542 } 543 } 544 545 /* fill a string with trip point type and value in one line 546 * e.g. P(56) C(106) 547 * maintain the distance one degree per char 548 */ 549 static void draw_tp_line(int tz, int y) 550 { 551 int j; 552 int x; 553 554 for (j = 0; j < ptdata.tzi[tz].nr_trip_pts; j++) { 555 x = ptdata.tzi[tz].tp[j].temp / 1000; 556 mvwprintw(thermal_data_window, y + 0, x + TDATA_LEFT, 557 "%c%d", trip_type_to_char(ptdata.tzi[tz].tp[j].type), 558 x); 559 syslog(LOG_INFO, "%s:tz %d tp %d temp = %lu\n", __func__, 560 tz, j, ptdata.tzi[tz].tp[j].temp); 561 } 562 } 563 564 const char data_win_title[] = " THERMAL DATA "; 565 void show_data_w(void) 566 { 567 int i; 568 569 570 if (tui_disabled || !thermal_data_window) 571 return; 572 573 werase(thermal_data_window); 574 wattron(thermal_data_window, A_BOLD); 575 mvwprintw(thermal_data_window, 0, maxx/2 - sizeof(data_win_title), 576 data_win_title); 577 wattroff(thermal_data_window, A_BOLD); 578 /* draw a line as ruler */ 579 for (i = 10; i < MAX_DISP_TEMP; i += 10) 580 mvwprintw(thermal_data_window, 1, i+TDATA_LEFT, "%2d", i); 581 582 for (i = 0; i < ptdata.nr_tz_sensor; i++) { 583 int temp = trec[cur_thermal_record].temp[i] / 1000; 584 int y = 0; 585 586 y = i * NR_LINES_TZDATA + 2; 587 /* y at tz temp data line */ 588 mvwprintw(thermal_data_window, y, 1, "%6.6s%2d:[%3d][", 589 ptdata.tzi[i].type, 590 ptdata.tzi[i].instance, temp); 591 draw_hbar(thermal_data_window, y, TDATA_LEFT, temp, ACS_RARROW, 592 true); 593 draw_tp_line(i, y); 594 } 595 wborder(thermal_data_window, 0, 0, 0, 0, 0, 0, 0, 0); 596 wrefresh(thermal_data_window); 597 } 598 599 const char tz_title[] = "THERMAL ZONES(SENSORS)"; 600 601 void show_sensors_w(void) 602 { 603 int i, j; 604 char buffer[512]; 605 606 if (tui_disabled || !tz_sensor_window) 607 return; 608 609 werase(tz_sensor_window); 610 611 memset(buffer, 0, sizeof(buffer)); 612 wattron(tz_sensor_window, A_BOLD); 613 mvwprintw(tz_sensor_window, 1, 1, "Thermal Zones:"); 614 wattroff(tz_sensor_window, A_BOLD); 615 616 mvwprintw(tz_sensor_window, 1, TZ_LEFT_ALIGN, "%s", buffer); 617 /* fill trip points for each tzone */ 618 wattron(tz_sensor_window, A_BOLD); 619 mvwprintw(tz_sensor_window, 2, 1, "Trip Points:"); 620 wattroff(tz_sensor_window, A_BOLD); 621 622 /* draw trip point from low to high for each tz */ 623 for (i = 0; i < ptdata.nr_tz_sensor; i++) { 624 int inst = ptdata.tzi[i].instance; 625 626 mvwprintw(tz_sensor_window, 1, 627 TZ_LEFT_ALIGN+TZONE_RECORD_SIZE * inst, "%.9s%02d", 628 ptdata.tzi[i].type, ptdata.tzi[i].instance); 629 for (j = ptdata.tzi[i].nr_trip_pts - 1; j >= 0; j--) { 630 /* loop through all trip points */ 631 char type; 632 int tp_pos; 633 /* reverse the order here since trips are sorted 634 * in ascending order in terms of temperature. 635 */ 636 tp_pos = ptdata.tzi[i].nr_trip_pts - j - 1; 637 638 type = trip_type_to_char(ptdata.tzi[i].tp[j].type); 639 mvwaddch(tz_sensor_window, 2, 640 inst * TZONE_RECORD_SIZE + TZ_LEFT_ALIGN + 641 tp_pos, type); 642 syslog(LOG_DEBUG, "draw tz %d tp %d ch:%c\n", 643 inst, j, type); 644 } 645 } 646 wborder(tz_sensor_window, 0, 0, 0, 0, 0, 0, 0, 0); 647 wattron(tz_sensor_window, A_BOLD); 648 mvwprintw(tz_sensor_window, 0, maxx/2 - sizeof(tz_title), tz_title); 649 wattroff(tz_sensor_window, A_BOLD); 650 wrefresh(tz_sensor_window); 651 } 652 653 void disable_tui(void) 654 { 655 tui_disabled = 1; 656 } 657