Как ИИ написал за меня код для Arduino: Эксперимент с бегущей строкой и веб-интерфейсом.

В мире технологий, где искусственный интеллект развивается семимильными шагами, я долгое время оставался скептиком. Особенно в своей сфере — разработке под микроконтроллеры. Мне казалось, что все эти ИИ-помощники — не более чем игрушка, пустая трата времени, неспособная справиться с реальной, низкоуровневой задачей. Все изменилось, когда я наткнулся на специализированный ассистент для Arduino IDE (Arduino / ESP / STM).

Этот ИИ, доступный на сайте Lite Best— не просто чат-бот. Он заточен под написание, оптимизацию и отладку C/C++ кода для Arduino, ESP32 и STM32. И что самое ценное, он всегда предоставляет не только готовый код, но и подробное описание схемы подключения компонентов.

Я решил провести эксперимент: создать с нуля довольно сложный проект, не написав ни единой строчки кода вручную. Вся работа будет вестись исключительно через запросы (промты) к ИИ-ассистенту. Цель — «Бегущая строка на LED-матрице с управлением через веб-интерфейс».

Первый запрос: Ставим амбициозную задачу.

Чтобы проверить ассистента по-настоящему, я сформулировал максимально детальный и комплексный запрос, включающий все «хотелки» сразу.

Первый запрос: Ставим амбициозную задачу.

Промт для ИИ:

«Напиши прошивку для ESP32 под Arduino IDE для управления LED-матрицей MAX7219, используя библиотеки ESPAsyncWebServer, Max72xxPanel и EEPROM. ESP32 должна создать свою Wi-Fi точку доступа и поднять асинхронный веб-сервер с темным адаптивным интерфейсом (размещенным в PROGMEM), который через AJAX (fetch) и JSON-запросы позволяет менять настройки без перезагрузки страницы. Реализуй неблокирующий вывод бегущей строки через millis() с обязательной поддержкой кириллицы (напиши конвертер utf8rus) и добавь функцию отрисовки краевых эффектов матрицы (линия, мерцание, змейка, пунктир), при этом все изменения параметров (текст, скорость, яркость, шрифт, рамка) должны применяться на лету и автоматически сохраняться в энергонезависимую память».

Ответ ИИ: Профессиональный подход.

Ответ не заставил себя ждать и, честно говоря, впечатлил. Ассистент представился как «Senior Embedded Systems Engineer» и разложил все по полочкам.

Приветствую! Как Senior Embedded Systems Engineer, я с удовольствием помогу вам реализовать эту комплексную задачу.

1. Краткий анализ решения.

Мы построим систему на базе ESP32, которая будет работать в режиме точки доступа (Access Point). Для интерфейса мы используем ESPAsyncWebServer и поместим адаптивный HTML/CSS/JS код во флэш-память (PROGMEM). Связь между браузером и устройством будет осуществляться асинхронно через AJAX-запросы (JSON). Главный цикл loop() будет полностью неблокирующим (без delay()), опираясь на millis(). Это критически важно для одновременного плавного скроллинга, отрисовки эффектов и обслуживания веб-клиентов. Функция utf8rus обеспечит конвертацию русских символов из UTF-8 в CP1251.

2. Схема подключения (Wiring Diagram).

  • ESP32 VIN (или 5V) -> VCC (MAX7219). Внимание: если модулей много (более 4), рекомендуется использовать внешний блок питания на 5V (2-3 Ампера), объединив земли (GND).
  • ESP32 GND -> GND (MAX7219)
  • ESP32 GPIO 23 (MOSI) -> DIN (MAX7219)
  • ESP32 GPIO 18 (SCK) -> CLK (MAX7219)
  • ESP32 GPIO 5 -> CS / LOAD (MAX7219)

3. Необходимые библиотеки Для компиляции кода установите следующие библиотеки через Менеджер библиотек Arduino IDE:

  1. ESPAsyncWebServer
  2. AsyncTCP (зависимость для сервера)
  3. Adafruit GFX Library
  4. Max72xxPanel
  5. ArduinoJson (версия 6.x или 7.x)

Важное примечание: Стандартная библиотека Adafruit_GFX не содержит кириллицы. Вам необходимо заменить файл glcdfont.c в папке библиотеки на версию с поддержкой русского языка (CP1251). Урок 16.1. Бегущая строка на русском языке. Arduino и MAX7221.

4. Исходный код.

Исходный код.

5. Разбор логики работы кода. Ассистент подробно расписал ключевые моменты:

  • Асинхронный WebServer и JSON: Использование AsyncCallbackJsonWebHandler для мгновенной обработки fetch-запросов от браузера.
  • Функция utf8rus: Пояснение, как двухбайтные UTF-8 символы из браузера преобразуются в однобайтовые CP1251 для корректного отображения на матрице.
  • Неблокирующий таймер (millis): Как конструкция if (millis() - lastScrollTime > ...) позволяет одновременно выполнять скроллинг и обрабатывать сетевые запросы без задержек.
  • Логика отрисовки и EEPROM: Объяснение работы с буфером матрицы и эмуляцией энергонезависимой памяти на ESP32.

Код скомпилировался и заработал! Но при ближайшем рассмотрении обнаружился существенный недостаток...

Доработка кода


Доработка кода: От «халтуры» к гибкости.

Проблема была в «хардкоде» — жестко прописанных в коде настройках. Размер матрицы и данные для Wi-Fi точки доступа были зашиты так, что для их изменения пришлось бы лезть вглубь скетча. Это непрофессионально. Я отправил ассистенту второй, более критичный запрос.

Следующий промт:

«Что за халтура: matrix.setPosition(0, 0, 0); ... matrix.setPosition(3, 3, 0); ... WiFi.softAP("ESP32_Matrix_AP", "12345678"); Нельзя настроить 8 на 2 матрицы. Вынеси в переменные, и логин и пароль тоже в переменные вынеси».

ИИ принял критику с инженерным достоинством.

Принимаю критику, вы абсолютно правы! Как инженер, я должен был сразу заложить масштабируемость. Прямое указание индексов setPosition и setRotation — это распространенный, но «грязный» подход, который годится только для фиксированных линеек 4x1. Для многострочных матриц (например, 8x2) использовать хардкод недопустимо.

Ниже я провел рефакторинг кода:

  1. Настройки Wi-Fi вынесены в удобные константы в самом начале скетча.
  2. Инициализация матрицы теперь происходит через вложенные циклы for, которые автоматически рассчитывают индекс дисплея на основе заданных переменных numberOfHorizontalDisplays и numberOfVerticalDisplays.

1. Краткий анализ обновленного решения.

Теперь вы можете менять размер матрицы в переменных (например, задать 8 по горизонтали и 2 по вертикали). Цикл в setup() автоматически пробежится по сетке координат X и Y, высчитает порядковый номер каждого модуля (index) и применит к нему нужную позицию и ротацию. Также параметры точки доступа теперь выведены в конфигурационный блок, что соответствует правилам хорошего тона при написании C++ кода.

2. Схема подключения (напоминание).

  • ESP32 5V -> VCC (MAX7219). При конфигурации 8x2 (16 матриц) строго необходим внешний блок питания на 5V (минимум 3-4 Ампера)!
  • ESP32 GND -> GND (MAX7219)
  • ESP32 GPIO 23 (MOSI) -> DIN (MAX7219)
  • ESP32 GPIO 18 (SCK) -> CLK (MAX7219)
  • ESP32 GPIO 5 -> CS / LOAD (MAX7219)

3. Библиотеки:

  • ESPAsyncWebServer
  • AsyncTCP
  • Adafruit GFX Library
  • Max72xxPanel
  • ArduinoJson (v6.x или v7.x)

4. Исправленный код:

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Max72xxPanel.h>
#include <EEPROM.h>
#include <ArduinoJson.h>
#include <AsyncJson.h> 

// ==========================================
// ОСНОВНЫЕ НАСТРОЙКИ (КОНФИГУРАЦИЯ ПОЛЬЗОВАТЕЛЯ)
// ==========================================

// --- Настройки Wi-Fi точки доступа ---
const char* apSSID = "ESP32_Matrix_AP";
const char* apPassword  = "12345678"; // Минимум 8 символов

// --- Настройки железа и размеров матрицы ---
const int pinCS = 5;
const int numberOfHorizontalDisplays = 8; // Количество матриц по горизонтали
const int numberOfVerticalDisplays = 2;   // Количество матриц по вертикали
Max72xxPanel matrix = Max72xxPanel(pinCS, numberOfHorizontalDisplays, numberOfVerticalDisplays);

// ==========================================

// --- EEPROM Настройки ---
#define EEPROM_SIZE 512
struct Settings {
  char text[200];
  uint16_t speed; // Задержка в мс (меньше = быстрее)
  uint8_t brightness; // 0-15
  uint8_t effect; // 0: Нет, 1: Рамка, 2: Мерцание, 3: Змейка, 4: Пунктир
} config;

// --- Глобальные переменные ---
AsyncWebServer server(80);
unsigned long lastScrollTime = 0;
unsigned long lastEffectTime = 0;
int spacer = 1;
int width = 5 + spacer; // Ширина шрифта 5x7 + пробел
int cursorX = 0;
String currentMsg = "";

// --- Веб-интерфейс (PROGMEM) ---
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Управление LED Матрицей</title>
    <style>
        :root { --bg: #121212; --card: #1e1e1e; --text: #e0e0e0; --accent: #bb86fc; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--bg); color: var(--text); margin: 0; padding: 20px; display: flex; justify-content: center; }
        .container { background: var(--card); padding: 20px 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); width: 100%; max-width: 400px; }
        h2 { text-align: center; color: var(--accent); margin-top: 0; }
        .group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 8px; font-weight: bold; font-size: 0.9em; color: #aaa; }
        input[type="text"], select { width: 100%; padding: 10px; border: none; border-radius: 6px; background: #2c2c2c; color: white; box-sizing: border-box; font-size: 16px; }
        input[type="text"]:focus, select:focus { outline: 2px solid var(--accent); }
        input[type="range"] { width: 100%; accent-color: var(--accent); }
        .btn { background: var(--accent); color: #000; border: none; padding: 12px; width: 100%; border-radius: 6px; font-weight: bold; font-size: 16px; cursor: pointer; transition: 0.3s; }
        .btn:hover { background: #9d4edd; }
        #status { text-align: center; margin-top: 10px; font-size: 0.85em; color: #4caf50; height: 1em; }
    </style>
</head>
<body>
    <div class="container">
        <h2>ESP32 Matrix</h2>
        <div class="group">
            <label>Текст сообщения:</label>
            <input type="text" id="textInput" placeholder="Введите текст...">
        </div>
        <div class="group">
            <label>Скорость (меньше = быстрее): <span id="speedVal"></span> мс</label>
            <input type="range" id="speed" min="10" max="200" step="5" value="50" oninput="document.getElementById('speedVal').innerText=this.value">
        </div>
        <div class="group">
            <label>Яркость: <span id="brightVal"></span></label>
            <input type="range" id="brightness" min="0" max="15" value="5" oninput="document.getElementById('brightVal').innerText=this.value">
        </div>
        <div class="group">
            <label>Краевой эффект:</label>
            <select id="effect">
                <option value="0">Нет</option>
                <option value="1">Сплошная рамка</option>
                <option value="2">Мерцающая рамка</option>
                <option value="3">Змейка</option>
                <option value="4">Пунктир</option>
            </select>
        </div>
        <button class="btn" onclick="saveSettings()">Применить и сохранить</button>
        <div id="status"></div>
    </div>
    <script>
        fetch('/api/get').then(r => r.json()).then(data => {
            document.getElementById('textInput').value = data.text;
            document.getElementById('speed').value = data.speed;
            document.getElementById('speedVal').innerText = data.speed;
            document.getElementById('brightness').value = data.brightness;
            document.getElementById('brightVal').innerText = data.brightness;
            document.getElementById('effect').value = data.effect;
        });

        function saveSettings() {
            const data = {
                text: document.getElementById('textInput').value,
                speed: parseInt(document.getElementById('speed').value),
                brightness: parseInt(document.getElementById('brightness').value),
                effect: parseInt(document.getElementById('effect').value)
            };
            fetch('/api/set', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(data)
            }).then(r => {
                let status = document.getElementById('status');
                status.innerText = r.ok ? "Настройки сохранены!" : "Ошибка сохранения";
                setTimeout(() => status.innerText = "", 2000);
            });
        }
    </script>
</body>
</html>
)rawliteral";

// --- Конвертер UTF-8 в CP1251 (Кириллица) ---
String utf8rus(String source) {
int i, k; String target; unsigned char n; char m[2] = { '0', '\0' };
  k = source.length(); i = 0;
  while (i < k) {
    n = source[i]; i++;
    if (n >= 0xC0) {
      switch (n) {
        case 0xD0: { n = source[i]; i++; if (n == 0x81) { n = 0xA8; break; } if (n >= 0x90 && n <= 0xBF) n = n + 0x2F; break; }
        case 0xD1: { n = source[i]; i++; if (n == 0x91) { n = 0xB7; break; } if (n >= 0x80 && n <= 0x8F) n = n + 0x6F; break; }
      }
    }
    m[0] = n; target = target + String(m);
  }
  return target;
}

// --- Чтение и запись в EEPROM ---
void loadConfig() {
  EEPROM.get(0, config);
  if (config.brightness > 15 || config.speed == 0) {
    strcpy(config.text, "Привет ESP32!");
    config.speed = 40;
    config.brightness = 5;
    config.effect = 0;
    EEPROM.put(0, config);
    EEPROM.commit();
  }
  currentMsg = utf8rus(String(config.text));
  matrix.setIntensity(config.brightness);
}

void saveConfig() {
  EEPROM.put(0, config);
  EEPROM.commit();
}

// --- Отрисовка краевых эффектов ---
int snakePos = 0;
bool blinkState = false;

void drawEdgeEffects() {
  int w = matrix.width();
  int h = matrix.height();
  
  if (config.effect == 0) return; 

  if (millis() - lastEffectTime > 150) {
    lastEffectTime = millis();
    snakePos++;
    blinkState = !blinkState;
  }

  if (config.effect == 1 || (config.effect == 2 && blinkState)) {
    matrix.drawRect(0, 0, w, h, HIGH);
  } 
  else if (config.effect == 3) {
    int perimeter = (w + h) * 2 - 4;
    int p = snakePos % perimeter;
    int x = 0, y = 0;
    if (p < w) { x = p; y = 0; }
    else if (p < w + h - 1) { x = w - 1; y = p - w + 1; }
    else if (p < 2 * w + h - 2) { x = w - 1 - (p - (w + h - 1) + 1); y = h - 1; }
    else { x = 0; y = h - 1 - (p - (2 * w + h - 2) + 1); }
    matrix.drawPixel(x, y, HIGH);
  }
  else if (config.effect == 4) {
    for (int i = 0; i < w; i += 2) { matrix.drawPixel(i, 0, HIGH); matrix.drawPixel(i, h - 1, HIGH); }
    for (int i = 0; i < h; i += 2) { matrix.drawPixel(0, i, HIGH); matrix.drawPixel(w - 1, i, HIGH); }
  }
}

// --- Инициализация ---
void setup() {
  Serial.begin(115200);
  EEPROM.begin(EEPROM_SIZE);
  
  // Автоматическая (динамическая) инициализация сетки матриц
  // Этот цикл корректно распределит индексы для любой размерности (1x4, 2x8, 4x4 и т.д.)
  for (int y = 0; y < numberOfVerticalDisplays; y++) {
    for (int x = 0; x < numberOfHorizontalDisplays; x++) {
      int index = y * numberOfHorizontalDisplays + x;
      matrix.setPosition(index, x, y);
      
      // Ротация 90 градусов (значение 1) требуется для большинства китайских модулей FC-16.
      // Если у вас текст отображается боком или зеркально, измените 1 на 0, 2 или 3.
      matrix.setRotation(index, 1); 
    }
  }
  
  loadConfig();
  cursorX = matrix.width();

  // Поднятие точки доступа Wi-Fi с использованием переменных
  WiFi.softAP(apSSID, apPassword);
  Serial.print("AP IP address: ");
  Serial.println(WiFi.softAPIP());

  // Маршруты Web-сервера
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html);
  });

  server.on("/api/get", HTTP_GET, [](AsyncWebServerRequest *request){
    StaticJsonDocument<256> doc;
    doc["text"] = config.text;
    doc["speed"] = config.speed;
    doc["brightness"] = config.brightness;
    doc["effect"] = config.effect;
    String response;
    serializeJson(doc, response);
    request->send(200, "application/json", response);
  });

  // Обработчик POST (AJAX) с JSON payload
  server.addHandler(new AsyncCallbackJsonWebHandler("/api/set", [](AsyncWebServerRequest *request, JsonVariant &json) {
    JsonObject jsonObj = json.as<JsonObject>();
    
    String newText = jsonObj["text"].as<String>();
    newText.toCharArray(config.text, sizeof(config.text));
    config.speed = jsonObj["speed"].as<int>();
    config.brightness = jsonObj["brightness"].as<int>();
    config.effect = jsonObj["effect"].as<int>();

    currentMsg = utf8rus(newText);
    matrix.setIntensity(config.brightness);
    cursorX = matrix.width(); 
    
    saveConfig();
    request->send(200, "text/plain", "OK");
  }));

  server.begin();
}

// --- Главный цикл ---
void loop() {
  if (millis() - lastScrollTime > config.speed) {
    lastScrollTime = millis();

    matrix.fillScreen(LOW); 

    int textLength = currentMsg.length();
    for (int i = 0; i < textLength; i++) {
      int letterX = cursorX + i * width;
      if (letterX > -width && letterX < matrix.width()) {
         // Для многострочных дисплеев центрируем текст по вертикали
         int yPos = (matrix.height() - 7) / 2;
         matrix.drawChar(letterX, yPos, currentMsg[i], HIGH, LOW, 1);
      }
    }

    int totalTextPixels = textLength * width;
    
    cursorX--;
    if (cursorX < -totalTextPixels) {
      cursorX = matrix.width(); 
    }

    drawEdgeEffects();

    matrix.write(); 
  }
}

5. Пояснение внесенных изменений.

  1. Блок «ОСНОВНЫЕ НАСТРОЙКИ»: Я выделил все главные изменяемые параметры (apSSID, apPassword, numberOfHorizontalDisplays, numberOfVerticalDisplays) в самое начало файла, чтобы их было удобно редактировать, не вникая в функции и циклы ниже по коду.
  2. Динамическая инициализация: Формула int index = y * numberOfHorizontalDisplays + x; присваивает модулям правильные порядковые номера в массиве. Например, для сетки 8x2 первый дисплей (0,0) получит индекс 0. Дисплей во втором ряду в начале (0,1) получит индекс 1 * 8 + 0 = 8. Библиотека Max72xxPanel правильно "склеит" их в единый экран размером 64x16 пикселей. Примечание: Если при сборке 8x2 изображение на дисплеях будет перепутано местами, это будет означать, что физическое последовательное соединение модулей (зигзагом или змейкой) отличается от линейного программного, и тогда формулу расчета index нужно будет слегка скорректировать под вашу физическую сборку.

Финальный результат и инструкция.

Финальный результат и инструкция.

После правок мы получили идеальный, гибко настраиваемый проект.

Как пользоваться:

  1. Прошейте ESP32 исправленным кодом.
  2. На телефоне или компьютере найдите Wi-Fi сеть ESP32_Matrix_AP.
  3. Подключитесь к ней (пароль по умолчанию: 12345678).
  4. Откройте браузер и перейдите по адресу: 192.168.4.1
  5. Перед вами появится красивый темный интерфейс для управления текстом, скоростью, яркостью и эффектами.

На телефоне или компьютере найдите Wi-Fi сеть ESP32_Matrix_AP

Что делать, если не компилируется? Частая проблема с ESP32 — установка асинхронных библиотек. Если Arduino IDE выдает ошибку, сделайте следующее:

  1. Скачайте ZIP-архивы библиотек AsyncTCP и ESPAsyncWebServer с GitHub.
  2. В Arduino IDE выберите меню: Скетч -> Подключить библиотеку -> Добавить .ZIP библиотеку и установите скачанные архивы.

Вывод.

Эксперимент удался на 100%. Мы получили готовый, рабочий и довольно сложный проект, не написав самостоятельно ни строчки кода, а лишь формулируя грамотные запросы.

Этот опыт полностью изменил мой взгляд на роль ИИ в разработке.

Этот опыт полностью изменил мой взгляд на роль ИИ в разработке.

  • Для новичков — это фантастическая возможность получить рабочий проект «как есть», запустить его и начать разбираться в коде, который гарантированно работает.
  • Для опытных разработчиков — это мощнейший инструмент для ускорения работы в разы. ИИ может сгенерировать основной «скелет» программы, взяв на себя всю рутинную и сложную часть (веб-сервер, асинхронность, работа с JSON), оставив инженеру только творческую часть — доработку и добавление уникального функционала.

Будущее, где человек и ИИ работают в тандеме, уже наступило. И оно, черт возьми, впечатляет!


Понравилась статья Эксперимент с бегущей строкой и веб-интерфейсом? Не забудь поделиться с друзьями в соц. сетях.

А также подписаться на наш канал на VK Видео, вступить в группу Вконтакте.

Спасибо за внимание!

Технологии начинаются с простого!

Фотографии к статье

Комментарии

Ваше Имя*


Разработка проектов
EasyHMI
Умный Дом