Глава 3: Игра 'Двоичное Число' и GPIO-индикация =============================================== В этой главе мы создадим интерактивную обучающую игру для Raspberry Pi Zero 2 W, которая поможет освоить двоичную систему счисления. Приложение будет использовать GPIO-пины для визуализации двоичных чисел с помощью светодиодов, а пользователь будет вводить десятичные эквиваленты через графический интерфейс. --- Введение -------- Это приложение превратит ваш Raspberry Pi в увлекательный инструмент для практики перевода чисел из двоичной системы в десятичную. Оно будет генерировать случайные двоичные числа, отображать их на светодиодах, а вы будете вводить свои догадки через GUI. Мы также добавим подсчет правильных и неправильных ответов. --- Подключение компонентов ----------------------- Для этой главы нам понадобятся следующие компоненты и их подключение к Raspberry Pi: * **8 светодиодов с резисторами 220 Ом каждый**: Подключаются к следующим GPIO-пинам (каждый светодиод через резистор к пину, а другой конец светодиода к GND): * **GPIO17 (пин 11)** * **GPIO18 (пин 12)** * **GPIO27 (пин 13)** * **GPIO22 (пин 15)** * **GPIO23 (пин 16)** * **GPIO24 (пин 18)** * **GPIO25 (пин 22)** * **GPIO4 (пин 7)** Ниже представлена схема подключения светодиодов: .. image:: ../_static/chapter03_schematic.png :align: center :alt: --- .. list-table:: Соответствие GPIO-пинов и битов :widths: 20 20 20 40 :header-rows: 1 * - Индекс бита (в коде) - GPIO-пин (BCM) - Физический пин - Значимость бита * - 0 - GPIO4 - 7 - Младший бит (LSB) * - 1 - GPIO25 - 22 - * - 2 - GPIO24 - 18 - * - 3 - GPIO23 - 16 - * - 4 - GPIO22 - 15 - * - 5 - GPIO27 - 13 - * - 6 - GPIO18 - 12 - * - 7 - GPIO17 - 11 - Старший бит (MSB) --- Принцип работы -------------- Логика игры будет следующей: 1. **Начало игры**: При нажатии кнопки "Старт" приложение генерирует случайное число от 0 до 255. 2. **Двоичная индикация**: Это число преобразуется в 8-битное двоичное представление. Каждый бит этого числа управляет состоянием одного из восьми светодиодов: если бит равен 1, соответствующий светодиод включается; если 0 — выключается. 3. **Пользовательский ввод**: Игрок вводит десятичное число, которое, по его мнению, соответствует двоичному числу, отображаемому на светодиодах, в текстовое поле. 4. **Проверка ответа**: При нажатии кнопки "Ваш ответ" приложение сравнивает введенное пользователем число с загаданным. 5. **Подсчет очков**: В зависимости от правильности ответа, обновляются счетчики "Правильных" и "Ошибок". 6. **Новое число**: После каждого ответа генерируется новое случайное число, и светодиоды обновляются. 7. **Завершение игры**: Кнопка "Стоп" сбрасывает все светодиоды и останавливает игру. Для взаимодействия с GPIO используется библиотека `libgpiod`, а графический интерфейс построен с помощью `GTK+3`. --- Интерфейс --------- Графический интерфейс игры будет включать следующие элементы: * **8 цветных квадратов/кругов**: Визуальное представление светодиодов на экране. Они будут менять цвет (например, с белого на красный), имитируя включение/выключение. * **Поле ввода**: Для ввода пользователем десятичного числа. * **Кнопки управления**: * "Старт": Начинает игру и генерирует первое число. * "Ваш ответ": Проверяет введенный ответ и генерирует следующее число. * "Стоп": Завершает игру и сбрасывает счетчики. * **Счетчики**: Два лейбла, отображающие количество правильных ответов и ошибок. Вот как может выглядеть макет GUI: .. image:: ../_static/chapter03_gui_mockup.png :align: center :alt: --- Код --- Ниже представлен полный C-код для реализации описанной функциональности. .. code-block:: c #include // Подключаем библиотеку GTK для создания графического интерфейса #include // Подключаем библиотеку libgpiod для работы с GPIO #include // Подключаем стандартную библиотеку для функций, таких как rand() и atoi() #include // Подключаем библиотеку для работы со временем, используемой для инициализации rand() // Определение констант для имени GPIO-чипа и потребителя (имя, которое будет видно в системе) #define CHIPNAME "gpiochip0" #define CONSUMER "BinaryGame" // Массив номеров GPIO-пинов, к которым подключены светодиоды // Эти пины будут использоваться для отображения 8-битного двоичного числа int gpio_pins[8] = {17, 18, 27, 22, 23, 24, 25, 4}; // Структура для хранения всех данных приложения, включая указатели на виджеты GTK, // линии GPIO, счетчики игры и состояние игры. struct app_data { struct gpiod_chip *chip; // Указатель на GPIO-чип struct gpiod_line *lines[8]; // Массив указателей на линии GPIO для светодиодов GtkWidget *entry; // Указатель на виджет поля ввода (GtkEntry) GtkWidget *correct_label; // Указатель на лейбл для отображения количества правильных ответов (GtkLabel) GtkWidget *wrong_label; // Указатель на лейбл для отображения количества ошибок (GtkLabel) GtkWidget *start_button; // Указатель на кнопку "Старт" (GtkButton) GtkWidget *stop_button; // Указатель на кнопку "Стоп" (GtkButton) GtkWidget *submit_button; // Указатель на кнопку "Ваш ответ" (GtkButton) GtkWidget *led_indicators[8]; // Массив указателей на виджеты-лейблы, имитирующие светодиоды в GUI int correct_count; // Счетчик правильных ответов int wrong_count; // Счетчик ошибок int current_value; // Текущее загаданное десятичное число gboolean game_running; // Флаг состояния игры (TRUE - игра активна, FALSE - нет) }; // Функция update_leds: Устанавливает состояние физических светодиодов и их GUI-индикаторов // в соответствии с двоичным представлением заданного числа. void update_leds(struct app_data *data, int value) { for (int i = 0; i < 8; i++) { // Извлекаем i-й бит из числа 'value' // Оператор >> (побитовый сдвиг вправо) сдвигает биты числа на 'i' позиций вправо. // Оператор & 1 (побитовое И с 1) извлекает самый младший бит (то есть, текущий i-й бит). int bit = (value >> i) & 1; // Устанавливаем значение на соответствующей GPIO-линии. // 0 - выключить светодиод, 1 - включить светодиод. gpiod_line_set_value(data->lines[i], bit); // Визуально обновляем индикатор в GUI: // Делаем его видимым (включенным), если бит равен 1, и невидимым (выключенным), если бит равен 0. gtk_widget_set_visible(data->led_indicators[i], bit); } } // Функция reset_leds: Выключает все физические светодиоды и их GUI-индикаторы. void reset_leds(struct app_data *data) { update_leds(data, 0); // Вызываем update_leds с значением 0, чтобы выключить все светодиоды } // Обработчик кнопки "Старт": Начинает новую игру. void start_game(GtkButton *btn, gpointer user_data) { struct app_data *data = (struct app_data *)user_data; // Генерируем случайное число от 0 до 255. // rand() % 256 дает остаток от деления на 256, что гарантирует число в диапазоне [0, 255]. data->current_value = rand() % 256; // Обновляем светодиоды в соответствии с новым загаданным числом. update_leds(data, data->current_value); // Устанавливаем флаг, что игра запущена. data->game_running = TRUE; // Очищаем текстовое поле ввода, чтобы пользователь мог ввести новый ответ. gtk_entry_set_text(GTK_ENTRY(data->entry), ""); } // Обработчик кнопки "Ваш ответ": Проверяет ответ пользователя. void check_answer(GtkButton *btn, gpointer user_data) { struct app_data *data = (struct app_data *)user_data; // Если игра не запущена, игнорируем нажатие кнопки "Ваш ответ". if (!data->game_running) return; // Получаем текст из поля ввода. const char *input = gtk_entry_get_text(GTK_ENTRY(data->entry)); // Преобразуем введенную строку в целое число. int user_value = atoi(input); // Сравниваем ответ пользователя с текущим загаданным числом. if (user_value == data->current_value) { data->correct_count++; // Увеличиваем счетчик правильных ответов. } else { data->wrong_count++; // Увеличиваем счетчик ошибок. } // Обновляем текст на лейблах, отображающих статистику. char buf[64]; // Буфер для форматирования строк snprintf(buf, sizeof(buf), "Правильных: %d", data->correct_count); gtk_label_set_text(GTK_LABEL(data->correct_label), buf); snprintf(buf, sizeof(buf), "Ошибок: %d", data->wrong_count); gtk_label_set_text(GTK_LABEL(data->wrong_label), buf); // Генерируем новое случайное число для следующего раунда игры. data->current_value = rand() % 256; update_leds(data, data->current_value); // Обновляем светодиоды для нового числа. gtk_entry_set_text(GTK_ENTRY(data->entry), ""); // Очищаем поле ввода для нового ответа. } // Обработчик кнопки "Стоп": Завершает текущую игру. void stop_game(GtkButton *btn, gpointer user_data) { struct app_data *data = (struct app_data *)user_data; reset_leds(data); // Выключаем все светодиоды. data->game_running = FALSE; // Устанавливаем флаг, что игра остановлена. // Сбрасываем счетчики правильных и неправильных ответов. data->correct_count = 0; data->wrong_count = 0; // Обновляем лейблы статистики. gtk_label_set_text(GTK_LABEL(data->correct_label), "Правильных: 0"); gtk_label_set_text(GTK_LABEL(data->wrong_label), "Ошибок: 0"); gtk_entry_set_text(GTK_ENTRY(data->entry), ""); // Очищаем поле ввода. } // Обработчик сигнала "destroy" окна: Освобождает все захваченные ресурсы и завершает приложение. void on_destroy(GtkWidget *widget, gpointer user_data) { struct app_data *data = (struct app_data *)user_data; reset_leds(data); // Убедимся, что все светодиоды выключены перед выходом. // Освобождаем каждую GPIO-линию, которая была запрошена. for (int i = 0; i < 8; i++) { if (data->lines[i]) { // Проверяем, что указатель на линию не NULL (т.е. линия была успешно получена) gpiod_line_release(data->lines[i]); } } // Закрываем GPIO-чип. if (data->chip) { // Проверяем, что указатель на чип не NULL (т.е. чип был успешно открыт) gpiod_chip_close(data->chip); } gtk_main_quit(); // Завершаем основной цикл обработки событий GTK, что приводит к завершению приложения. } // Главная функция приложения, точка входа. int main(int argc, char *argv[]) { gtk_init(&argc, &argv); // Инициализация библиотеки GTK. Это должно быть вызвано в начале. srand(time(NULL)); // Инициализация генератора случайных чисел. // time(NULL) возвращает текущее время, обеспечивая разную последовательность чисел при каждом запуске. struct app_data data = {0}; // Создаем и инициализируем нулями структуру app_data. data.correct_count = 0; // Устанавливаем начальное количество правильных ответов. data.wrong_count = 0; // Устанавливаем начальное количество ошибок. data.game_running = FALSE; // Игра изначально не запущена. // Инициализация GPIO data.chip = gpiod_chip_open_by_name(CHIPNAME); // Открываем GPIO-чип по его имени. if (!data.chip) { perror("Ошибка: не удалось открыть GPIO chip"); // Выводим сообщение об ошибке, если чип не открылся. return 1; // Завершаем программу с кодом ошибки. } // Запрашиваем и настраиваем все 8 GPIO-линий как выходы. for (int i = 0; i < 8; i++) { // Получаем указатель на конкретную GPIO-линию по ее номеру. data.lines[i] = gpiod_chip_get_line(data.chip, gpio_pins[i]); // Запрашиваем линию как выход с начальным значением 0 (выключено). // Проверяем, что линия получена и запрос успешен. if (!data.lines[i] || gpiod_line_request_output(data.lines[i], CONSUMER, 0) < 0) { perror("Ошибка: не удалось настроить пины"); // Сообщение об ошибке. // В случае ошибки, освобождаем все линии, которые были успешно запрошены до этого. for (int j = 0; j < i; j++) { if (data.lines[j]) gpiod_line_release(data.lines[j]); } gpiod_chip_close(data.chip); // Закрываем чип. return 1; // Завершаем программу. } } // Создание и настройка GTK интерфейса GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); // Создаем главное окно приложения. gtk_window_set_title(GTK_WINDOW(window), "Игра: Двоичное Число"); // Устанавливаем заголовок окна. gtk_window_set_default_size(GTK_WINDOW(window), 400, 300); // Устанавливаем размер окна по умолчанию. // Подключаем обработчик сигнала "destroy" (закрытие окна) к функции on_destroy. g_signal_connect(window, "destroy", G_CALLBACK(on_destroy), &data); GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); // Создаем вертикальный контейнер с отступом 10 пикселей. gtk_container_add(GTK_CONTAINER(window), vbox); // Добавляем контейнер в главное окно. // Контейнер для светодиодов (визуальное представление битов) GtkWidget *led_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); // Создаем горизонтальный контейнер для 8 "светодиодов" с отступом 5 пикселей. // Создаем 8 виджетов-лейблов, которые будут имитировать светодиоды. for (int i = 7; i >= 0; i--) { // Идем от 7 к 0, чтобы старший бит (самый значимый) был слева в GUI. GtkWidget *led = gtk_label_new("⬤"); // Используем Unicode символ "черный круг" как "светодиод". gtk_widget_set_visible(led, FALSE); // Изначально все "светодиоды" выключены (невидимы). gtk_widget_set_name(led, "led"); // Устанавливаем имя для CSS-стилизации (если потребуется). gtk_box_pack_start(GTK_BOX(led_box), led, TRUE, TRUE, 0); // Добавляем "светодиод" в горизонтальный контейнер. data.led_indicators[i] = led; // Сохраняем указатель на виджет в структуре данных. } gtk_box_pack_start(GTK_BOX(vbox), led_box, FALSE, FALSE, 0); // Добавляем контейнер светодиодов в основной вертикальный контейнер. // Поле ввода для ответа пользователя data.entry = gtk_entry_new(); // Создаем новое текстовое поле ввода. gtk_entry_set_placeholder_text(GTK_ENTRY(data.entry), "Введите число"); // Устанавливаем текст-подсказку. gtk_box_pack_start(GTK_BOX(vbox), data.entry, FALSE, FALSE, 0); // Добавляем поле ввода в основной контейнер. // Контейнер для кнопок управления игрой GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); // Горизонтальный контейнер для кнопок. data.start_button = gtk_button_new_with_label("Старт"); // Создаем кнопку "Старт". g_signal_connect(data.start_button, "clicked", G_CALLBACK(start_game), &data); // Подключаем обработчик нажатия. gtk_box_pack_start(GTK_BOX(button_box), data.start_button, TRUE, TRUE, 0); // Добавляем кнопку. data.submit_button = gtk_button_new_with_label("Ваш ответ"); // Создаем кнопку "Ваш ответ". g_signal_connect(data.submit_button, "clicked", G_CALLBACK(check_answer), &data); // Подключаем обработчик. gtk_box_pack_start(GTK_BOX(button_box), data.submit_button, TRUE, TRUE, 0); // Добавляем кнопку. data.stop_button = gtk_button_new_with_label("Стоп"); // Создаем кнопку "Стоп". g_signal_connect(data.stop_button, "clicked", G_CALLBACK(stop_game), &data); // Подключаем обработчик. gtk_box_pack_start(GTK_BOX(button_box), data.stop_button, TRUE, TRUE, 0); // Добавляем кнопку. gtk_box_pack_start(GTK_BOX(vbox), button_box, FALSE, FALSE, 0); // Добавляем контейнер кнопок в основной контейнер. // Лейблы для отображения статистики (правильных/неправильных ответов) data.correct_label = gtk_label_new("Правильных: 0"); // Создаем лейбл для правильных ответов. data.wrong_label = gtk_label_new("Ошибок: 0"); // Создаем лейбл для ошибок. gtk_box_pack_start(GTK_BOX(vbox), data.correct_label, FALSE, FALSE, 0); // Добавляем лейбл в основной контейнер. gtk_box_pack_start(GTK_BOX(vbox), data.wrong_label, FALSE, FALSE, 0); // Добавляем лейбл в основной контейнер. gtk_widget_show_all(window); // Отображаем все виджеты, содержащиеся в окне. gtk_main(); // Запускаем основной цикл обработки событий GTK. Приложение будет активно до вызова gtk_main_quit(). // Ресурсы GPIO освобождаются в on_destroy, но здесь можно добавить дополнительную очистку, // если on_destroy не будет вызвана (например, при аварийном завершении). // В данном случае on_destroy гарантированно вызывается при закрытии окна. return 0; // Успешное завершение программы. } Компиляция и запуск Для компиляции и запуска этой игры на вашем Raspberry Pi Zero 2 W выполните следующие шаги: Сохраните код: Сохраните приведенный выше C-код в файл, например, binary_game.c. Компиляция: Откройте терминал на вашем Raspberry Pi и перейдите в директорию, где вы сохранили файл binary_game.c. Затем выполните следующую команду: .. code-block:: bash gcc binary_game.c -o binary_game $(pkg-config --cflags --libs gtk+-3.0 libgpiod) -Wall -Wextra gcc: Компилятор C. binary_game.c: Исходный файл с вашим кодом. -o binary_game: Имя выходного исполняемого файла, который будет создан после компиляции. $(pkg-config --cflags --libs gtk+-3.0 libgpiod): Эта команда автоматически находит необходимые флаги компилятора (--cflags) и библиотеки (--libs) для GTK3 и libgpiod, упрощая процесс компиляции. -Wall -Wextra: Включает все стандартные и дополнительные предупреждения компилятора, что помогает выявлять потенциальные ошибки и улучшать качество кода. Запуск: После успешной компиляции запустите программу, выполнив в терминале: .. code-block:: bash ./binary_game Теперь вы должны увидеть окно GTK с игрой "Двоичное Число". Нажмите "Старт", чтобы начать игру, и попробуйте угадать десятичное значение, отображаемое на светодиодах! Создание ярлыка (Launcher) для рабочего стола Чтобы запускать игру прямо с рабочего стола Raspberry Pi OS без использования терминала, вы можете создать файл .desktop (ярлык). Откройте текстовый редактор (например, nano или geany) на вашем Raspberry Pi. .. code-block:: bash nano ~/.local/share/applications/binary_game.desktop (Если папки ~/.local/share/applications/ не существует, создайте ее: mkdir -p ~/.local/share/applications/) Вставьте следующее содержимое в файл binary_game.desktop: .. code-block:: bash [Desktop Entry] Version=1.0 Type=Application Name=Игра "Двоичное Число" Comment=Обучающая игра для Raspberry Pi: угадай двоичное число Exec=/home/alex/projects/Github/GUI_for_Zero2W/binary_game Icon=applications-games Terminal=false Categories=Game;Education; Важные моменты: ^^^^^^^^^^^^^^^ **Exec:** Укажите полный путь к вашему исполняемому файлу binary_game. Убедитесь, что путь /home/alex/projects/Github/GUI_for_Zero2W/ соответствует фактическому расположению вашего проекта. **Icon:** Вы можете использовать стандартные иконки системы (например, applications-games, education-science, utilities-terminal) или указать путь к собственному файлу иконки (например, /home/alex/projects/Github/GUI_for_Zero2W/icon.png). **Terminal=false**: Указывает, что приложение не требует запуска в терминале. Если ваше приложение выводит что-то важное в терминал, можете установить true. Сохраните файл (Ctrl+O, Enter, Ctrl+X для nano). Сделайте ярлык исполняемым: .. code-block:: bash chmod +x ~/.local/share/applications/binary_game.desktop Теперь вы должны увидеть ярлык "Игра 'Двоичное Число'" в меню приложений (обычно в разделе "Игры" или "Образование") или на рабочем столе. Возможно, потребуется перезагрузить рабочий стол или систему, чтобы ярлык появился. Поздравляю!