Глава 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)

Ниже представлена схема подключения светодиодов:

Соответствие GPIO-пинов и битов

Индекс бита (в коде)

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:

Код

Ниже представлен полный C-код для реализации описанной функциональности.

#include <gtk/gtk.h>       // Подключаем библиотеку GTK для создания графического интерфейса
#include <gpiod.h>         // Подключаем библиотеку libgpiod для работы с GPIO
#include <stdlib.h>        // Подключаем стандартную библиотеку для функций, таких как rand() и atoi()
#include <time.h>          // Подключаем библиотеку для работы со временем, используемой для инициализации 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. Затем выполните следующую команду:

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: Включает все стандартные и дополнительные предупреждения компилятора, что помогает выявлять потенциальные ошибки и улучшать качество кода.

Запуск: После успешной компиляции запустите программу, выполнив в терминале:

./binary_game

Теперь вы должны увидеть окно GTK с игрой «Двоичное Число». Нажмите «Старт», чтобы начать игру, и попробуйте угадать десятичное значение, отображаемое на светодиодах!

Создание ярлыка (Launcher) для рабочего стола Чтобы запускать игру прямо с рабочего стола Raspberry Pi OS без использования терминала, вы можете создать файл .desktop (ярлык).

Откройте текстовый редактор (например, nano или geany) на вашем Raspberry Pi.

nano ~/.local/share/applications/binary_game.desktop

(Если папки ~/.local/share/applications/ не существует, создайте ее: mkdir -p ~/.local/share/applications/)

Вставьте следующее содержимое в файл binary_game.desktop:

[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).

Сделайте ярлык исполняемым:

chmod +x ~/.local/share/applications/binary_game.desktop

Теперь вы должны увидеть ярлык «Игра „Двоичное Число“» в меню приложений (обычно в разделе «Игры» или «Образование») или на рабочем столе. Возможно, потребуется перезагрузить рабочий стол или систему, чтобы ярлык появился.

Поздравляю!