Files
P10Panel/ESP82_PANEL.ino
2025-07-05 13:10:16 +00:00

480 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <ESP8266WiFi.h>
#include <Ticker.h>
#include <DMD2.h>
#include <ESPAsyncWebServer.h>
#include <ESPAsyncTCP.h>
#include <ArduinoJson.h>
#include <fonts/Font_BOLD.h>
#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<String>();
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<String>();
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<int>();
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<JsonVariant>());
});
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<int>(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<JsonVariant>());
});
server.begin();
}
void loop() {
if (isUpdater) {
isUpdater = false;
updateScreen();
}
}