#include #include #include #include #include #include #include #include "data/output.h" #include "data/bear.h" #include "data/kitleft.h" // Константы const uint8_t *FONT = Font_BOLD; const int WIDTH = 4; // Количество матриц в ширину const int HEIGHT = 3; // Количество матриц в высоту const int DISPLAY_WIDTH = WIDTH * 32; const int DISPLAY_HEIGHT = HEIGHT * 16; const int TEXT_Y_OFFSET = 17; const int SCROLL_DELAY = 5; // Задержка для скролла в секундах const int SCROLL_SPEED = 30; // Скорость скролла в мс const int TEXT_HEIGHT = 16; // Высота области для текста const int TOP_LOGO_HEIGHT = 16; // Высота верхнего логотипа const int BOTTOM_LOGO_HEIGHT = 16; // Высота нижнего логотипа // Настройки WiFi const char *SSID = "SSID"; const char *PASSWORD = "PASSWORD"; IPAddress staticIP(10, 131, 170, 4); IPAddress gateway(10, 131, 170, 1); IPAddress subnet(255, 255, 255, 0); // Текстовые константы const char SKB[] PROGMEM = "СКБ"; const char KIT[] PROGMEM = "\"КИТ\""; const char ICTIB[] PROGMEM = "ИКТИБ"; // Перечисления для состояний enum class PanelState : int { STATIC_LOGO = 0, SCROLLING_TEXT = 1, ANIMATED_LOGO = 2, BEAR = 3, UNKNOWN = -1 }; enum class PanelPower : int { POWER_OFF = 0, POWER_ON = 1 }; // Глобальные переменные AsyncWebServer server(80); String displayText = "СКБ~\"КИТ\""; PanelState currentState = PanelState::ANIMATED_LOGO; PanelPower panelPower = PanelPower::POWER_ON; bool showText = true; bool isUpdater = false; bool scrollEnabled = false; int scrollPosition = 0; unsigned long lastScrollTime = 0; SPIDMD dmd(WIDTH, HEIGHT); DMD_TextBox *box = nullptr; Ticker modeTicker; Ticker scrollTicker; void setupWiFi() { Serial.print("Подключение к WiFi "); Serial.println(SSID); WiFi.begin(SSID, PASSWORD); if (!WiFi.config(staticIP, gateway, subnet)) { Serial.println("Ошибка настройки статического IP!"); } else { Serial.println("Статический IP настроен"); } while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nПодключено к WiFi"); Serial.print("IP адрес: "); Serial.println(WiFi.localIP()); // WiFi.forceSleepWake(); } void drawBinaryArray(const uint8_t *data) { dmd.clearScreen(); for (int y = 0; y < DISPLAY_HEIGHT; y++) { for (int x = 0; x < DISPLAY_WIDTH; x++) { uint8_t pixel = pgm_read_byte(&data[y * DISPLAY_WIDTH + x]); dmd.setPixel(x, y, pixel ? GRAPHICS_ON : GRAPHICS_OFF); } } } void switchDisplayMode() { if (panelPower == PanelPower::POWER_ON) { if (showText) { drawScrollingText(); } else { drawBinaryArray(output); } showText = !showText; } } void drawScrollingText() { if(box){ delete box; box = nullptr; } // Вычисляем ширину текста int textWidth = dmd.stringWidth(displayText.c_str()); // Определяем, нужен ли скроллинг scrollEnabled = (textWidth > DISPLAY_WIDTH); // Очищаем экран dmd.clearScreen(); // Рисуем статические элементы drawKitLogoTop(); drawKitLogoBottom(); // Устанавливаем начальную позицию scrollPosition = scrollEnabled ? DISPLAY_WIDTH : (DISPLAY_WIDTH - textWidth) / 2; // Рисуем начальное положение текста if (box) { delete box; } box = new DMD_TextBox(dmd, scrollPosition, TEXT_Y_OFFSET, DISPLAY_WIDTH, TEXT_HEIGHT); box->print(displayText.c_str()); // Управляем тикером scrollTicker.detach(); if (scrollEnabled) { scrollTicker.attach_ms(SCROLL_SPEED, handleScroll); } else { scrollTicker.detach(); } } void handleScroll() { if (!scrollEnabled || panelPower != PanelPower::POWER_ON || currentState != PanelState::SCROLLING_TEXT) { return; } unsigned long currentTime = millis(); if (currentTime - lastScrollTime < SCROLL_SPEED) { return; } lastScrollTime = currentTime; // Вычисляем ширину текста int textWidth = dmd.stringWidth(displayText.c_str()); // Обновляем позицию scrollPosition--; // Проверяем, нужно ли перезапустить скроллинг if (scrollPosition < -textWidth) { scrollPosition = DISPLAY_WIDTH; } // Очищаем только текстовую область for (int y = TEXT_Y_OFFSET; y < TEXT_Y_OFFSET + TEXT_HEIGHT; y++) { for (int x = 0; x < DISPLAY_WIDTH; x++) { dmd.setPixel(x, y, GRAPHICS_OFF); } } // Рисуем текст с помощью DMD_TextBox if (box) { delete box; } box = new DMD_TextBox(dmd, scrollPosition, TEXT_Y_OFFSET, DISPLAY_WIDTH, TEXT_HEIGHT); box->print(displayText.c_str()); } void drawKitLogoTop() { dmd.drawLine(0, 14, 16, 14, GRAPHICS_ON); // x y x y dmd.drawLine(0, 13, 16, 13, GRAPHICS_ON); dmd.drawLine(16, 14, 20, 5, GRAPHICS_ON); dmd.drawLine(16, 13, 20, 4, GRAPHICS_ON); dmd.drawLine(20, 5, 30, 5, GRAPHICS_ON); dmd.drawLine(20, 4, 30, 4, GRAPHICS_ON); dmd.drawCircle(33, 4, 3, GRAPHICS_ON); dmd.drawCircle(33, 4, 2, GRAPHICS_ON); // 1 line dmd.drawLine(20, 14, 50, 14, GRAPHICS_ON); dmd.drawLine(20, 13, 50, 13, GRAPHICS_ON); dmd.drawLine(50, 14, 55, 5, GRAPHICS_ON); dmd.drawLine(50, 13, 55, 4, GRAPHICS_ON); dmd.drawLine(55, 5, 70, 5, GRAPHICS_ON); dmd.drawLine(55, 4, 70, 4, GRAPHICS_ON); dmd.drawLine(70, 5, 75, 14, GRAPHICS_ON); dmd.drawLine(70, 4, 75, 13, GRAPHICS_ON); dmd.drawLine(75, 14, 85, 14, GRAPHICS_ON); dmd.drawLine(75, 13, 85, 13, GRAPHICS_ON); dmd.drawCircle(88, 13, 3, GRAPHICS_ON); dmd.drawCircle(88, 13, 2, GRAPHICS_ON); // 2 line dmd.drawCircle(78, 4, 3, GRAPHICS_ON); dmd.drawCircle(78, 4, 2, GRAPHICS_ON); dmd.drawLine(81, 5, 95, 5, GRAPHICS_ON); dmd.drawLine(81, 4, 95, 4, GRAPHICS_ON); dmd.drawLine(95, 5, 100, 14, GRAPHICS_ON); dmd.drawLine(95, 4, 100, 13, GRAPHICS_ON); dmd.drawLine(100, 14, 110, 14, GRAPHICS_ON); dmd.drawLine(100, 13, 110, 13, GRAPHICS_ON); dmd.drawLine(110, 14, 115, 5, GRAPHICS_ON); dmd.drawLine(110, 13, 115, 4, GRAPHICS_ON); dmd.drawLine(115, 5, 128, 5, GRAPHICS_ON); dmd.drawLine(115, 4, 128, 4, GRAPHICS_ON); // 3 line } void drawKitLogoBottom() { dmd.drawLine(0, 35, 16, 35, GRAPHICS_ON); dmd.drawLine(0, 36, 16, 36, GRAPHICS_ON); dmd.drawLine(16, 35, 21, 43, GRAPHICS_ON); dmd.drawLine(16, 36, 21, 44, GRAPHICS_ON); dmd.drawLine(21, 43, 31, 43, GRAPHICS_ON); dmd.drawLine(21, 44, 31, 44, GRAPHICS_ON); dmd.drawCircle(34, 44, 3, GRAPHICS_ON); dmd.drawCircle(34, 44, 2, GRAPHICS_ON); // 1 line dmd.drawLine(20, 35, 50, 35, GRAPHICS_ON); dmd.drawLine(20, 36, 50, 36, GRAPHICS_ON); dmd.drawLine(50, 35, 55, 43, GRAPHICS_ON); dmd.drawLine(50, 36, 55, 44, GRAPHICS_ON); // 2 line dmd.drawLine(55, 43, 70, 43, GRAPHICS_ON); dmd.drawLine(55, 44, 70, 44, GRAPHICS_ON); dmd.drawLine(70, 43, 75, 35, GRAPHICS_ON); dmd.drawLine(70, 44, 75, 36, GRAPHICS_ON); dmd.drawLine(75, 35, 85, 35, GRAPHICS_ON); dmd.drawLine(75, 36, 85, 36, GRAPHICS_ON); dmd.drawCircle(88, 36, 3, GRAPHICS_ON); dmd.drawCircle(88, 36, 2, GRAPHICS_ON); dmd.drawCircle(78, 44, 3, GRAPHICS_ON); dmd.drawCircle(78, 44, 2, GRAPHICS_ON); dmd.drawLine(81, 43, 95, 43, GRAPHICS_ON); dmd.drawLine(81, 44, 95, 44, GRAPHICS_ON); dmd.drawLine(95, 43, 100, 35, GRAPHICS_ON); dmd.drawLine(95, 44, 100, 36, GRAPHICS_ON); dmd.drawLine(100, 35, 110, 35, GRAPHICS_ON); dmd.drawLine(100, 36, 110, 36, GRAPHICS_ON); dmd.drawLine(110, 35, 115, 43, GRAPHICS_ON); dmd.drawLine(110, 36, 115, 44, GRAPHICS_ON); dmd.drawLine(115, 43, 128, 43, GRAPHICS_ON); dmd.drawLine(115, 44, 128, 44, GRAPHICS_ON); } void drawBear(){ drawBinaryArray(bear); } void drawKitLeftLogoWithText() { drawBinaryArray(kitleft); dmd.drawString_P(79, 2, SKB); dmd.drawString_P(71, 17, KIT); dmd.drawString_P(68, 32, ICTIB); } void updateScreen() { modeTicker.detach(); scrollTicker.detach(); if (panelPower == PanelPower::POWER_OFF) { dmd.clearScreen(); return; } switch (currentState) { case PanelState::SCROLLING_TEXT: drawKitLogoTop(); // Статичный верхний логотип drawScrollingText(); // Прокручиваемый текст drawKitLogoBottom(); // Статичный нижний логотип if (scrollEnabled) { scrollTicker.attach_ms(SCROLL_SPEED, handleScroll); } break; case PanelState::ANIMATED_LOGO: dmd.clearScreen(); drawText(); modeTicker.attach(SCROLL_DELAY, switchDisplayMode); dmd.clearScreen(); drawBinaryArray(output); break; case PanelState::STATIC_LOGO: dmd.clearScreen(); drawKitLeftLogoWithText(); break; case PanelState::BEAR: dmd.clearScreen(); drawBear(); break; default: dmd.clearScreen(); drawKitLeftLogoWithText(); break; } } void drawText() { int textWidth = dmd.stringWidth(displayText, Font_BOLD); int xOffset = (textWidth < DISPLAY_WIDTH) ? (DISPLAY_WIDTH - textWidth) / 2 : 0; if (box) { delete box; box = nullptr; } box = new DMD_TextBox(dmd, xOffset, TEXT_Y_OFFSET, DISPLAY_WIDTH, DISPLAY_HEIGHT); box->print(displayText.c_str()); } PanelState intToPanelState(int state) { switch (state) { case 0: return PanelState::STATIC_LOGO; case 1: return PanelState::SCROLLING_TEXT; case 2: return PanelState::ANIMATED_LOGO; case 3: return PanelState::BEAR; default: return PanelState::UNKNOWN; } } String panelStateToString(PanelState state) { switch (state) { case PanelState::STATIC_LOGO: return "0"; case PanelState::SCROLLING_TEXT: return "1"; case PanelState::ANIMATED_LOGO: return "2"; case PanelState::BEAR: return "3"; default: return "-1"; } } void handleText(AsyncWebServerRequest *request, const JsonVariant &json) { if (!json.containsKey("text")) { request->send(400, "application/json", "{\"error\": \"Missing text field\"}"); return; } displayText = json["text"].as(); isUpdater = true; updateScreen(); request->send(200, "application/json", "{\"status\": \"OK\"}"); } void handleTurn(AsyncWebServerRequest *request, const JsonVariant &json) { bool powerChanged = false; bool modeChanged = false; // Обработка включения/выключения панели if (json.containsKey("panel")) { String panelTurn = json["panel"].as(); PanelPower newPower; if (panelTurn == "on") { newPower = PanelPower::POWER_ON; } else if (panelTurn == "off") { newPower = PanelPower::POWER_OFF; } else { request->send(400, "application/json", "{\"error\": \"Invalid panel value, use 'on' or 'off'\"}"); return; } powerChanged = (panelPower != newPower); panelPower = newPower; } // Обработка изменения режима if (json.containsKey("state")) { int panelStateInt = json["state"].as(); PanelState newState = intToPanelState(panelStateInt); if (newState == PanelState::UNKNOWN) { request->send(400, "application/json", "{\"error\": \"Invalid state value\"}"); return; } modeChanged = (currentState != newState); currentState = newState; } if (powerChanged || modeChanged) { updateScreen(); } String response = "{\"panel\":\"" + String(panelPower == PanelPower::POWER_ON ? "on" : "off") + "\",\"state\":" + panelStateToString(currentState) + "}"; request->send(200, "application/json", response); } void setup() { Serial.begin(9600); setupWiFi(); Serial.println("\n\nStarting server..."); dmd.setBrightness(255); dmd.selectFont(FONT); dmd.begin(); dmd.clearScreen(); panelPower = PanelPower::POWER_ON; updateScreen(); server.on( "/api/text", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t, size_t) { StaticJsonDocument<256> jsonReq; DeserializationError error = deserializeJson(jsonReq, data, len); if (error) { request->send(400, "application/json", "{\"error\": \"Invalid JSON\"}"); return; } handleText(request, jsonReq.as()); }); server.on("/api/led", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "application/json", String("{\"panel\":\"") + (panelPower == PanelPower::POWER_ON ? "on" : "off") + "\",\"state\":" + String(static_cast(currentState)) + "}"); }); server.on( "/api/led", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t, size_t) { StaticJsonDocument<256> jsonReq; DeserializationError error = deserializeJson(jsonReq, data, len); if (error) { request->send(400, "application/json", "{\"error\": \"Invalid JSON\"}"); return; } handleTurn(request, jsonReq.as()); }); server.begin(); } void loop() { if (isUpdater) { isUpdater = false; updateScreen(); } }