Pull to refresh

Терминальная графика

Reading time 8 min
Views 53K
Когда printf — мало, а ncurses — много


Когда данных становится слишком много, бывает не хватает стандартного вывода printf в консольной программе. Особенно если различных событий много и различные данные превращаются в безумный листинг. Эти данные могут поступать от контроллера через UART, и тут нечего и думать о какой-то gui-программе. Может так же быть и обычный bash-скрипт, к которому хочется прикрутить какой-никакой псевдографический интерфейс.

Скажу сразу, может поначалу показаться это изобретением велосипеда, ведь есть прекрасная библиотека ncurses. Ничуть не умаляю её достоинств, но часто её возможности бывают слишком избыточны. Плюс она слабо переносима в контроллеры и там уж точно избыточна. А вот пару макросов и функций для работы с псевдографикой можно набросать и самим.

Постановка задачи


Те кто занимался программированием контроллеров часто для вывода информации при отладке используют консоль аки UART или УАПП по-русски. Раньше я давал описание данного интерфейса в своих постах на хабре. При достаточно большом количестве разнообразной информации отлавливать её становится совершенно невозможно. Получается многостраничный лог, особенно если непрерывно идут данные с каких-то аналоговых датчиков, плюс надо контролировать состояние портов и т.п. В результате люди начинают писать свои обработчики на стороне компьютера и решение становится совсем не переносимым, а главное тратится впустую куча времени. Но, на самом деле можно всю разнообразную информацию с контроллера уместить в одном окне терминала и удобно её использовать, без написания стороннего софта. В результате наше решение становится переносимым (нужна любая терминалка), и экономится куча программисто-часов на отладку.

Методический пример


Данный пример будет скорее учебный, методический, но вы его легко сможете адаптировать к своему реальному устройство на любом контроллере. Так же по аналогии это всё будет работать на bash, pyton и прочем.

Пусть у нас есть контроллер, где к 8-ми разрядному порту подключены контактные датчики, которые могут принимать состояние ВКЛ и ВЫКЛ, есть аналоговый трёхосевой датчик, и мы должны выводить их состояние, плюс в контроллере есть время, дата, и мы хотим контролировать правильность отображения времени; и наконец там может случится событие (например сбой по питанию), где мы должны срочно информировать оператора. Подытожим:

  • Состояние 8-ми пинов порта (on/off)
  • Трёхосевой датчик (x, y, z) — отдельный вывод каждой оси
  • Дата и время
  • Сообщение оператору об ошибке

Можно сразу прикинуть, что если подобную инфу просто тупо слать по UART, то это будет нечитаемый мусор. Но её можно упорядочить, и выглядеть всё должно примерно таким образом (эскиз в текстовом редакторе):


Рамочки я скопировал тупо из mc. Те, кто хочет красоты, могут посмотреть какие есть рамочки в unicode. Сверху цифры написаны специально для координат.

Вроде как идея понятна, но как это реализовать в терминале? А вот тут начинается настоящая магия! "А теперь надо обязательно дунуть! Если не дунуть, никакого чуда не произойдет!" Амаяк Акопян

Управляющие последовательности


Изначально планировал в этой главе развернуть лекцию про управление терминалом с помощью ESC-последовательностей. Но тогда бы статья разрослась просто до неприличных размеров, поэтому я дам лишь некоторые понятия, а остальное можно найти в интернете или в мануалах.
Терминал — это устройство ввода-вывода, которое позволяет вводить и отображать данные. Раньше терминал представлял из себя монитор и клавиатуру, подключаемые по СОМ-порту к ЭВМ, а в более ранних версиях даже телетайп.


Телетайп Model 33 (картинка с википедии)

Сегодня терминал представляет собой программу, которая может быть виртуальным терминалом, как в linux или терминалом, которая работает с СОМ-портом. Все эти терминалы, даже классический HyperTerminal отвечают некоторым стандартам. Как не сложно догадаться, такими терминалами нужно управлять. Есть обычные ASCII символы, которые отображаются на экране, а есть специальные последовательности символов, которые позволяют задавать координаты курсора, очищать экран, задавать цвет и т.п. В своих статьях на хабре и гиктаймс я уже неоднократно касался тем ESC-последовательностей. Мы их применяли в управлении дисплеем при написание драйверов под Linux (описание в спойлере, который никто не читал), и в статье про Дисплей покупателя для wifi-радио, который так же управляется ESC-последовательностями.

Если кто-то из вас застал времена BBS, то вы помните какие «красивые» были эти доски: цветные, у которых были определённые поля ввода-вывода информации и т.п. Вся эта радость выводилась с помощью таких Управляющих символов. Символ считается управляющим, если (до преобразования согласно таблице перекодировки) он содержит один из 14-и кодов: 00 (NUL), 0x07 (BEL), 0x08 (BS), 0x09 (HT), 0x0a (LF), 0x0b (VT), 0x0c (FF), 0x0d (CR), 0x0e (SO), 0x0f (SI), 0x18 (CAN), 0x1a (SUB), 0x1b (ESC), 0x7f (DEL). Нас интересует в первую очередь символ ESC=0x1b, '\033' или же '\e'. Подробнее о этих последовательностях можно прочитать в мануалах man console_codes, либо в интернете на русском.

Кроме управления курсором, можно ещё раскрашивать терминал в разные цвета (если он, конечно, цветной). Проверить цвета вашего терминала можно простейшим BASH-скриптом:

for fgbg in 38 48 ; do #Foreground/Background
	for color in {0..256} ; do #Colors
		#Display the color
		echo -en "\e[${fgbg};5;${color}m ${color}\t\e[0m"
		#Display 10 colors per lines
		if [ $((($color + 1) % 10)) == 0 ] ; then
			echo #New line
		fi
	done
	echo #New line
done
exit 0

Результат выполнения будет примерно такой:


Более детально почитать о цветах и шрифтах в консоли (и любых терминалах) с примерами можно вот тут.

Вот, вы уже владеете необходимой информацией, чтобы сделать весёлые пасхалки в BASH-скриптах, как например картинка, вынесенная в заголовок поста.


Более детально, как я делал такую пасхалку на bash, можно почитать у меня в ЖЖ.

Оформим всё на си


Данный пример я даю на своих мастер-классах по программированию и приведу его здесь. Сделаем макросы установления атрибутов текста, очистки экрана, перемещения курсора в верхний угол и перемещения курсора в заданную позицию:

#define home() 			printf(ESC "[H") //Move cursor to the indicated row, column (origin at 1,1)
#define clrscr()		printf(ESC "[2J") //lear the screen, move to (1,1)
#define gotoxy(x,y)		printf(ESC "[%d;%dH", y, x);
#define visible_cursor() printf(ESC "[?251");
//Set Display Attribute Mode	<ESC>[{attr1};...;{attrn}m
#define resetcolor() printf(ESC "[0m")
#define set_display_atrib(color) 	printf(ESC "[%dm",color)

По возможности, особенно на медленных системах лучше не использовать printf, так как его работа весьма медленная. Лучше заменять более легковесными функциями вывода на дисплей (COM-порт).

Сами атрибуты цветов закинем в отдельный заголовочный файл в виде отдельных макросов.

Заголовочный файл
#ifndef __TERM_EXAMPLE__
#define __TERM_EXAMPLE__

#define ESC "\033"

//Format text
#define RESET 		0
#define BRIGHT 		1
#define DIM			2
#define UNDERSCORE	3
#define BLINK		4
#define REVERSE		5
#define HIDDEN		6

//Foreground Colours (text)

#define F_BLACK 	30
#define F_RED		31
#define F_GREEN		32
#define F_YELLOW	33
#define F_BLUE		34
#define F_MAGENTA 	35
#define F_CYAN		36
#define F_WHITE		37

//Background Colours
#define B_BLACK 	40
#define B_RED		41
#define B_GREEN		42
#define B_YELLOW	44
#define B_BLUE		44
#define B_MAGENTA 	45
#define B_CYAN		46
#define B_WHITE		47

#endif /*__TERM_EXAMPLE__*/


Обратите внимание, что Foreground — раскрашивает сам текст, а Background подложку текста.

Сделаем небольшую програмку, демонстрирующую работу всех макросов.

Демонстрационная програмка
int main (void) {
	home();
	clrscr();
	printf("Home + clrscr\n");
	gotoxy(20,7);
	printf("gotoxy(20,7)");
	
	gotoxy(1,10);
	printf("gotoxy(1,10)  \n\n");
	
	set_display_atrib(BRIGHT);
	printf("Formatting text:\n");
	resetcolor();
	
	set_display_atrib(BRIGHT);
	printf("Bold\n");
	resetcolor();
	
	set_display_atrib(DIM);
	printf("Dim\n");
	resetcolor();

	set_display_atrib(BLINK);
	printf("Blink\n");
	resetcolor();

	set_display_atrib(REVERSE);
	printf("Reverse\n");
	printf("\n");
	

	set_display_atrib(BRIGHT);
	printf("Text color example:\n");
	resetcolor();
	
	set_display_atrib(F_RED);
	printf("Red\n");
	resetcolor();
	
	set_display_atrib(F_GREEN);
	printf("Green\n");
	resetcolor();

	set_display_atrib(F_BLUE);
	printf("Blue\n");
	resetcolor();

	set_display_atrib(F_CYAN);
	printf("Cyan\n");
	resetcolor();

	set_display_atrib(BRIGHT);
	printf("\nBottom color example:\n");
	resetcolor();	
	
	set_display_atrib(B_RED);
	printf("Red\n");
	resetcolor();
	
	set_display_atrib(B_GREEN);
	printf("Green\n");
	resetcolor();

	set_display_atrib(B_BLUE);
	printf("Blue\n");
	resetcolor();

	set_display_atrib(B_CYAN);
	printf("Cyan\n");
	printf("\n");
	resetcolor();
	return 0;
}


Результат работы:



Обращаю ваше внимание, что атрибуты текста следует сбрасывать, иначе они наследуются!

Симуляция данных выводимых контроллером



Дело стало за малым, набросать программу, которая будет отвечать нашему ТЗ: выводить состояние пинов порта, акселерометр, дата и время, сообщение об ошибке. Код написан таким образом, что легко может быть перенесён на любой контроллер. Не буду загромождать текст кусками кода, тем более что можно его легко посмотреть на github, остановлюсь только на некоторых моментах.

В функции main мы переводим курсор в левый верхний угол, затем очищаем экран и рисуем рамку с помощью функции frame_draw ();. Рамку данных мы рисуем только один раз, потом все данные выводим просто каждую в своей позиции. После передаём управление функции controller_emulator ();.

В функции controller_emulator (); мы каждые 30 секунд меняем сообщение пользователю с хорошо на ошибку, генерируем данные в порт (там реализован «бегущий» бит) и генерируем случайные числа на акселерометре. Вы можете легко переписать эту функцию под свои задачи. Выводим все данные с помощью трёх функций:

		print_accelerometer(a);
		print_port_bits(port);
		print_time_date(tm_info);

Рамка рисуется элементарно просто, одной функцией puts с предварительно заданными атрибутами цвета:

void frame_draw () {
	home();
	set_display_atrib(B_BLUE);
//            123456789012345678901
	puts(	"┌─────────┐┌─────────┐\n" //0
		"│         ││         │\n" //1
		"│         ││         │\n" //2
		"│         ││         │\n" //3
		"│         │├─────────┤\n" //4
		"│         ││         │\n" //5
		"│         ││         │\n" //6
		"│         ││         │\n" //7
		"│         ││         │\n" //8
		"└─────────┘└─────────┘\n" //9
		"┌────────────────────┐\n" //10
		"│                    │\n" //11
		"└────────────────────┘");  //12
	resetcolor();
}

И для примера разберу функцию print_port_bits.

void print_port_bits (unsigned char port) {
	int i;
	unsigned char maxPow = 1<<(8-1);
	set_display_atrib(B_BLUE);
	set_display_atrib(BRIGHT);
	for(i=0;i<8;++i){
		// print last bit and shift left.
		gotoxy(2,2 + i);
		if (port & maxPow) {
			set_display_atrib(F_GREEN);
			printf("pin%d= on ",i);
		} else {
			set_display_atrib(F_RED);
			printf("pin%d= off",i);			
		}
		port = port<<1;
	}
	resetcolor();
}

Она принимает без знаковый байт. Устанавливает атрибуты текста (жирный, подложка голубая) и по очереди выводит бит, каждый в свою позицию с помощью макроса gotoxy(2,2 + i); и в зависимости от того бит равен нулю или единице, окрашивает текст соответственно в красный или зелёный цвет.

Результат работы программы лучше посмотреть самостоятельно, как и код. Но тем кому лень git-ить программу и хочет просто посмотреть как это работает, я привожу гифку работы:



Что хочу отметить, что все позиции обозначайте #define и лучше всего, чтобы они рассчитывались от какой-то позиции. Таким образом, вы сможете перемещать вывод данных в нужное место, не перепахивая весь код, а просто изменив макросы в одном месте.

Итого


С помощью управляющих последовательностей можно даже получать размер окна терминала, события нажатий мыши и т.п. Специально не стал приводить эти примеры в данной статье, чтобы не загромождать её. Вы сможете сами найти данные примеры. В программе под линукс вообще доступно много всяких плюшек с управлением терминалом, отображением эха, скорости и прочим-прочим.

На мой взгляд, знать механизмы взаимодействия с терминальными программами нужно и полезно. Особенно разработчикам контроллеров и встраиваемых решений, где отладка программ чаще всего осуществляется с помощью СОМ-порта и его производных.

Полезные ссылки:

1. Управляющие и ESC-последовательности консоли Linux
2. Как раскрасить консоль и всё об атрибутах цвета с примерами
3. Пример программы из данной статьи
4. Пример создания пасхалки в bash-скрипте
Tags:
Hubs:
+47
Comments 69
Comments Comments 69

Articles