Глава 2: Реакция на кнопку и тревожный индикатор ================================================ В этой главе мы расширим функциональность нашего GUI-приложения для Raspberry Pi Zero 2 W. Мы добавим интерактивность с физической кнопкой, подключенной к GPIO, и реализуем систему тревожного оповещения, которая будет активироваться как по нажатию физической кнопки, так и через кнопку в графическом интерфейсе. --- Введение -------- Наше приложение теперь будет выполнять следующие действия при активации тревоги: * Светодиод, подключенный к Raspberry Pi, начнет мигать. * В окне графического интерфейса появится мигающая надпись **ТРЕВОГА**. * В интерфейс будет добавлена специальная кнопка для ручного управления состоянием тревоги (включение/отключение). --- Подключение компонентов ----------------------- Для этой главы нам понадобятся следующие компоненты и их подключение к Raspberry Pi: * **Светодиод с резистором 220 Ом**: Подключается к **GPIO17 (пин 11)**. Это тот же светодиод, что и в Главе 1. * **Кнопка**: Подключается к **GPIO18 (пин 12)**. Для кнопки используется схема с подтяжкой к питанию (pull-up) и подтяжкой к земле (pull-down) с помощью двух резисторов по 10 кОм. Ниже представлена схема подключения кнопки и светодиода: .. image:: ../_static/chapter02_schematic.png :align: center :alt: Схема подключения кнопки и светодиода **Схема подключения кнопки к GPIO18 (с подтяжкой):** :: +3.3V │ [10k] (Pull-up резистор) │ ├──────> GPIO18 │ ┌──┴──┐ │ BTN │ └──┬──┘ │ GND --- *Примечание: Эта схема обеспечивает четкое состояние пина GPIO18 (либо HIGH, либо LOW) независимо от того, нажата кнопка или нет, предотвращая "плавающее" состояние.* --- Подтяжка (Pull-up/Pull-down) резисторы --------------------------------------- При работе с цифровыми входами микроконтроллеров, таких как GPIO на Raspberry Pi, часто возникает проблема "плавающего" состояния (floating state). Это происходит, когда входной пин не подключен ни к источнику питания (HIGH), ни к земле (LOW) напрямую. В таком состоянии пин может "ловить" электрические шумы из окружающей среды, что приводит к непредсказуемым и ложным срабатываниям. Для решения этой проблемы используются **подтягивающие (pull-up)** или **стягивающие (pull-down)** резисторы. * **Подтягивающий (Pull-up) резистор**: Подключается между входным пином и источником питания (например, +3.3V). Его функция — "подтягивать" напряжение на пине к высокому логическому уровню (HIGH), когда кнопка не нажата (цепь разомкнута). При нажатии кнопки, которая соединяет пин с землей, напряжение на пине становится низким (LOW). * **Схема подключения:** * Резистор (например, 10 кОм) подключается между **GPIO-пином** и **источником питания (+3.3V)**. * Кнопка подключается между **GPIO-пином** и **землей (GND)**. * **Визуальная схема:** :: +3.3V │ [10k] (Pull-up резистор) │ ├──────> GPIO-пин │ ┌──┴──┐ │ BTN │ └──┬──┘ │ GND * **Логика работы:** * **Кнопка не нажата:** Пин GPIO "подтянут" к +3.3V через резистор. Микроконтроллер считывает **HIGH (логическую "1")**. * **Кнопка нажата:** Кнопка замыкает пин на GND. Напряжение на пине падает до 0V. Микроконтроллер считывает **LOW (логический "0")**. * **Особенности:** * При нажатии кнопки через резистор протекает небольшой ток (например, 3.3V / 10 кОм = 0.33 мА). * Это **наиболее распространенный вариант** подключения кнопок. * Многие микроконтроллеры (включая Raspberry Pi) имеют **встроенные программно-активируемые pull-up резисторы**, что позволяет вообще не использовать внешний резистор для этой цели. --- * **Стягивающий (Pull-down) резистор**: Подключается между входным пином и землей (GND). Его функция — "стягивать" напряжение на пине к низкому логическому уровню (LOW), когда кнопка не нажата. При нажатии кнопки, которая соединяет пин с источником питания, напряжение на пине становится высоким (HIGH). * **Схема подключения:** * Резистор (например, 10 кОм) подключается между **GPIO-пином** и **землей (GND)**. * Кнопка подключается между **GPIO-пином** и **источником питания (+3.3V)**. * **Визуальная схема:** :: +3.3V │ ┌──┴──┐ │ BTN │ └──┬──┘ │ ├──────> GPIO-пин │ [10k] (Pull-down резистор) │ GND * **Логика работы:** * **Кнопка не нажата:** Пин GPIO "стянут" к GND через резистор. Микроконтроллер считывает **LOW (логический "0")**. * **Кнопка нажата:** Кнопка замыкает пин на +3.3V. Напряжение на пине поднимается до +3.3V. Микроконтроллер считывает **HIGH (логическую "1")**. **Зачем они нужны с кнопками?** Кнопка — это механический переключатель. Когда кнопка не нажата, цепь разомкнута. Без подтягивающего или стягивающего резистора, входной пин GPIO в этот момент будет находиться в неопределенном состоянии. Микроконтроллер не сможет надежно определить, является ли это состояние логическим HIGH или LOW, что приведет к нестабильной работе и ложным срабатываниям (так называемому "дребезгу" контактов или случайным переключениям). В нашей схеме для кнопки на GPIO18 используется pull-up к +3.3V. Это обеспечивает четкий логический уровень на пине GPIO18 в обоих состояниях кнопки: * Когда кнопка **не нажата**, пин GPIO18 подтягивается к HIGH (+3.3V). * Когда кнопка **нажата**, она замыкает пин GPIO18 на GND, и пин переходит в состояние LOW (0V). Таким образом, мы всегда имеем определенное состояние на входе GPIO, что делает считывание кнопки надежным. --- Принцип работы -------------- Логика работы приложения будет следующей: 1. **Светодиод** продолжает использоваться с предыдущей главы и подключен к **GPIO17**. 2. **Кнопка** подключена к **GPIO18**. 3. **При нажатии физической кнопки**: * Если тревога неактивна, она **активируется**. * Светодиод начинает мигать. * На экране появляется мигающий лейбл **ТРЕВОГА**. 4. **Кнопка в графическом интерфейсе** позволяет **отключать и включать тревогу вручную**, независимо от состояния физической кнопки. 5. **Окно GTK+3** будет обновляться с помощью таймеров для реализации мигания светодиода и текста. --- Интерфейс --------- Графический интерфейс будет включать два основных элемента: * **Кнопка**: С текстом "Включить тревогу" или "Отключить тревогу" в зависимости от текущего состояния. * **Мигающий лейбл**: Отображает текст "**⚠ ТРЕВОГА ⚠**" при активной тревоге и исчезает, когда тревога неактивна. Вот как может выглядеть макет GUI: .. image:: ../_static/chapter02_gui_mockup.png :align: center :alt: GUI с тревогой --- Код --- Ниже представлен полный C-код для реализации описанной функциональности. .. code-block:: c #include // Основная библиотека GTK для создания графического интерфейса #include // Библиотека libgpiod для взаимодействия с GPIO #include // Стандартная библиотека ввода/вывода (например, для perror) #include // Стандартная библиотека для общих утилит #include // Для использования булевых типов (true/false) // Определение констант для удобства #define CONSUMER "GUI_for_Zero2W" // Имя потребителя для линий GPIO #define CHIPNAME "gpiochip0" // Имя GPIO-чипа на Raspberry Pi #define LED_GPIO 17 // Номер GPIO-пина для светодиода #define BUTTON_GPIO 18 // Номер GPIO-пина для кнопки // Структура для хранения указателей на виджеты и состояния приложения // Эта структура будет передаваться между функциями через gpointer user_data struct app_widgets { GtkWidget *button_toggle_alarm; // Указатель на кнопку GTK для управления тревогой GtkWidget *label_alarm; // Указатель на лейбл GTK для отображения текста "ТРЕВОГА" struct gpiod_line *led_line; // Указатель на линию GPIO для светодиода struct gpiod_line *button_line; // Указатель на линию GPIO для кнопки bool alarm_active; // Флаг: true, если тревога активна; false, если нет bool led_on; // Флаг: true, если светодиод включен; false, если выключен (для мигания) guint blink_timer; // Идентификатор таймера для мигания (0, если таймер не активен) guint poll_timer; // Идентификатор таймера для опроса кнопки (0, если таймер не активен) }; // Функция, вызываемая по таймеру для мигания светодиода и текста тревоги gboolean blink_led(gpointer user_data) { struct app_widgets *app = user_data; // Приводим user_data к типу нашей структуры if (app->alarm_active) { // Если тревога активна, переключаем состояние светодиода и текста app->led_on = !app->led_on; // Инвертируем состояние светодиода gpiod_line_set_value(app->led_line, app->led_on); // Устанавливаем значение на GPIO // Обновляем текст лейбла: "ТРЕВОГА" или пустая строка для мигания gtk_label_set_text(GTK_LABEL(app->label_alarm), app->led_on ? "⚠ ТРЕВОГА ⚠" : ""); return TRUE; // Возвращаем TRUE, чтобы таймер продолжал работать } else { // Если тревога неактивна, выключаем светодиод и очищаем текст gpiod_line_set_value(app->led_line, 0); // Выключаем светодиод gtk_label_set_text(GTK_LABEL(app->label_alarm), ""); // Очищаем текст лейбла return FALSE; // Возвращаем FALSE, чтобы остановить таймер мигания } } // Функция, вызываемая по таймеру для опроса состояния физической кнопки gboolean poll_button(gpointer user_data) { struct app_widgets *app = user_data; // Приводим user_data к типу нашей структуры int val = gpiod_line_get_value(app->button_line); // Считываем значение с линии кнопки if (val == 0) { // Если кнопка нажата (предполагаем активный низкий уровень при нажатии) // Если тревога еще не активна, активируем ее if (!app->alarm_active) { app->alarm_active = true; // Устанавливаем флаг тревоги в true // Если таймер мигания не запущен, запускаем его if (!app->blink_timer) { app->blink_timer = g_timeout_add(500, blink_led, app); // Запускаем таймер с интервалом 500 мс } // Обновляем текст на кнопке GUI gtk_button_set_label(GTK_BUTTON(app->button_toggle_alarm), "Отключить тревогу"); } } return TRUE; // Возвращаем TRUE, чтобы таймер опроса продолжал работать } // Функция, вызываемая при нажатии кнопки "Включить/Отключить тревогу" в GUI void on_toggle_alarm(GtkButton *button, gpointer user_data) { struct app_widgets *app = user_data; // Приводим user_data к типу нашей структуры app->alarm_active = !app->alarm_active; // Инвертируем состояние тревоги if (app->alarm_active) { // Если тревога активирована через GUI gtk_button_set_label(button, "Отключить тревогу"); // Меняем текст кнопки if (!app->blink_timer) { // Если таймер мигания не запущен, запускаем его app->blink_timer = g_timeout_add(500, blink_led, app); } } else { // Если тревога деактивирована через GUI gtk_button_set_label(button, "Включить тревогу"); // Меняем текст кнопки if (app->blink_timer) { // Если таймер мигания запущен, останавливаем его g_source_remove(app->blink_timer); app->blink_timer = 0; // Сбрасываем идентификатор таймера } gpiod_line_set_value(app->led_line, 0); // Выключаем светодиод gtk_label_set_text(GTK_LABEL(app->label_alarm), ""); // Очищаем текст лейбла } } // Главная функция приложения int main(int argc, char *argv[]) { gtk_init(&argc, &argv); // Инициализация библиотеки GTK // Инициализация GPIO struct gpiod_chip *chip = gpiod_chip_open_by_name(CHIPNAME); // Открываем GPIO-чип if (!chip) { perror("Ошибка открытия GPIO-чипа"); return 1; } // Получаем линии GPIO для светодиода и кнопки struct gpiod_line *led_line = gpiod_chip_get_line(chip, LED_GPIO); struct gpiod_line *button_line = gpiod_chip_get_line(chip, BUTTON_GPIO); if (!led_line || !button_line) { perror("Ошибка получения линий GPIO"); gpiod_chip_close(chip); return 1; } // Запрашиваем линии GPIO: светодиод как выход, кнопка как вход if (gpiod_line_request_output(led_line, CONSUMER, 0) < 0 || // LED как выход, начальное значение 0 gpiod_line_request_input(button_line, CONSUMER) < 0) { // BUTTON как вход perror("Ошибка запроса линий GPIO"); gpiod_chip_close(chip); return 1; } // Инициализация структуры app_widgets struct app_widgets app = { .led_line = led_line, // Присваиваем указатель на линию светодиода .button_line = button_line, // Присваиваем указатель на линию кнопки .alarm_active = false, // Тревога изначально неактивна .led_on = false, // Светодиод изначально выключен .blink_timer = 0 // Таймер мигания неактивен }; // Создание и настройка GTK UI GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); // Создаем главное окно gtk_window_set_title(GTK_WINDOW(window), "Тревожный сигнал"); // Устанавливаем заголовок окна gtk_window_set_default_size(GTK_WINDOW(window), 250, 150); // Устанавливаем размер окна // Подключаем сигнал закрытия окна к функции выхода из GTK g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL); // Создаем вертикальный контейнер для размещения виджетов GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); // Отступ 10 пикселей gtk_container_add(GTK_CONTAINER(window), vbox); // Добавляем контейнер в окно // Создаем лейбл для отображения текста тревоги app.label_alarm = gtk_label_new(""); // Добавляем лейбл в контейнер, расширяя его по вертикали и горизонтали gtk_box_pack_start(GTK_BOX(vbox), app.label_alarm, TRUE, TRUE, 10); // Создаем кнопку для управления тревогой app.button_toggle_alarm = gtk_button_new_with_label("Включить тревогу"); // Добавляем кнопку в контейнер gtk_box_pack_start(GTK_BOX(vbox), app.button_toggle_alarm, TRUE, TRUE, 10); // Подключаем сигнал нажатия кнопки к нашей функции on_toggle_alarm g_signal_connect(app.button_toggle_alarm, "clicked", G_CALLBACK(on_toggle_alarm), &app); gtk_widget_show_all(window); // Показываем все виджеты в окне // Запускаем таймер для периодического опроса физической кнопки app.poll_timer = g_timeout_add(100, poll_button, &app); // Опрос каждые 100 мс gtk_main(); // Запускаем основной цикл обработки событий GTK // --- Очистка ресурсов перед завершением программы --- gpiod_line_set_value(led_line, 0); // Убедимся, что светодиод выключен gpiod_line_release(led_line); // Освобождаем линию светодиода gpiod_line_release(button_line); // Освобождаем линию кнопки gpiod_chip_close(chip); // Закрываем GPIO-чип return 0; // Успешное завершение программы } Компиляция и запуск Для компиляции и запуска этого приложения на вашем Raspberry Pi Zero 2 W выполните следующие шаги: Сохраните код: Сохраните приведенный выше C-код в файл, например, led_alarm_gui.c. Компиляция: Используйте gcc для компиляции. Убедитесь, что у вас установлены библиотеки libgtk-3-dev и libgpiod-dev (как в Главе 1). .. code-block:: bash gcc led_alarm_gui.c -o led_alarm_gui $(pkg-config --cflags --libs gtk+-3.0 libgpiod) -Wall -Wextra **gcc**: Компилятор C. **led_alarm_gui.c** : Исходный файл с вашим кодом. **-o led_alarm_gui** : Имя выходного исполняемого файла. **$(pkg-config --cflags --libs gtk+-3.0 libgpiod)** : Автоматически включает необходимые флаги компилятора и библиотеки для GTK3 и libgpiod. **-Wall -Wextra** : Включает все стандартные и дополнительные предупреждения компилятора, что помогает выявлять потенциальные ошибки. Запуск: После успешной компиляции запустите программу: .. code-block:: bash ./led_alarm_gui Теперь вы должны увидеть окно GTK с кнопкой и мигающей надписью "ТРЕВОГА" при активации тревоги (либо нажатием физической кнопки, либо кнопкой в GUI).