#include <WiFi.h>
#include "ESPAsyncWebSrv.h" //Wokwi
//#include <ESPAsyncwebServer.h> //NodeMCU
#include <LittleFS.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <LiquidCrystal.h>
#include <Keypad.h>
// #include <Tone32.h>
#define FORMAT_LITTLEFS_IF_FAILED true
// Defina as credenciais de WiFi
const char *ssid = "AirBombAjotark";
const char *password = "ajotark";
// Crie uma instância do servidor
AsyncWebServer server(80);
// Crie uma instância do display 16x2 (endereço I2C 0x27)
LiquidCrystal lcd(7, 6, 5, 4, 3, 2);
// Variável global de configurações
StaticJsonDocument<256> configDoc;
// Variável para armazenar a senha gerada
String generatedPassword = "";
// Variável para armazenar se a senha já foi validada
bool isAuthenticated = false;
// Configuração do teclado 4x4
const byte ROWS = 4; // quatro linhas
const byte COLS = 4; // quatro colunas
char keys[ROWS][COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}};
byte rowPins[ROWS] = {19, 18, 5, 17}; // Pines conectados às linhas do teclado
byte colPins[COLS] = {16, 4, 0, 2}; // Pines conectados às colunas do teclado
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
#define BUZZER_PIN 15 // Pino do buzzer
// Variáveis do menu
enum GameMode
{
PAGE_INIT,
SABOTAGE,
DOMINATION,
SEARCH_DESTROY
};
GameMode currentMode = PAGE_INIT;
// Estrutura para armazenar as mensagens carregadas do arquivo JSON
struct Messages
{
String page_init;
String sabotage;
String domination;
String search_destroy;
String starting_game;
String canceled;
};
Messages messages;
// Função para carregar as mensagens de idioma
bool loadLanguage(const char *language)
{
String filePath = "/data/language/";
filePath += language;
filePath += ".json";
File file = LittleFS.open(filePath, "r");
if (!file)
{
Serial.println("Falha ao abrir o arquivo de idioma.");
return;
}
// Carregar o conteúdo do arquivo JSON
StaticJsonDocument<256> doc;
DeserializationError error = deserializeJson(doc, file);
if (error)
{
Serial.println("Erro ao carregar JSON de idioma.");
file.close();
return false;
}
// Armazenar as mensagens no struct
messages.page_init = doc["page_init"].as<String>();
messages.sabotage = doc["sabotage"].as<String>();
messages.domination = doc["domination"].as<String>();
messages.search_destroy = doc["search_destroy"].as<String>();
messages.starting_game = doc["starting_game"].as<String>();
messages.canceled = doc["canceled"].as<String>();
file.close();
return true;
}
// Função para exibir o menu com a seta e o jogo selecionado
void displayMenu()
{
lcd.clear();
// Exibe o jogo selecionado
switch (currentMode)
{
case PAGE_INIT:
lcd.setCursor(1, 0); // Desloca 1 caractere para deixar espaço para a seta
lcd.print(messages.page_init);
break;
case SABOTAGE:
lcd.setCursor(1, 0);
lcd.print(messages.sabotage);
break;
case DOMINATION:
lcd.setCursor(1, 0);
lcd.print(messages.domination);
break;
case SEARCH_DESTROY:
lcd.setCursor(1, 0);
lcd.print(messages.search_destroy);
break;
}
// Exibe as setas de navegação
lcd.setCursor(16, 0); // A posição 0,0 é para a seta na esquerda da linha
if (currentMode > PAGE_INIT)
{
lcd.write(0x7E); // Exibe a seta para cima (código do caractere `↑`)
}
lcd.setCursor(16, 1); // A posição 0,1 é para a seta para baixo
if (currentMode < SEARCH_DESTROY)
{
lcd.write(0x7F); // Exibe a seta para baixo (código do caractere `↓`)
}
}
void handleKeypadForMenu()
{
char key = keypad.getKey(); // Verifica se uma tecla foi pressionada
if (key)
{
tone(BUZZER_PIN, 1000, 100); // Toca o buzzer
switch (key)
{
case 'A': // Mover para cima
currentMode = (GameMode)(((int)currentMode - 1 + 4) % 4); // Loop circular
displayMenu();
break;
case 'B': // Mover para baixo
currentMode = (GameMode)(((int)currentMode + 1) % 4); // Loop circular
displayMenu();
break;
case 'C': // Cancelar
lcd.clear();
lcd.print(messages.canceled);
delay(1000);
break;
case 'D': // Confirmar seleção
lcd.clear();
lcd.print(messages.starting_game);
switch (currentMode)
{
case SABOTAGE:
lcd.print(messages.sabotage);
startSabotageGame();
break;
case DOMINATION:
lcd.print(messages.domination);
startDominationGame();
break;
case SEARCH_DESTROY:
lcd.print(messages.search_destroy);
startSearchDestroyGame();
break;
default:
lcd.print(messages.page_init);
break;
}
break;
}
}
}
// Funções para iniciar os modos de jogo (exemplo)
void startSabotageGame()
{
// Lógica para iniciar o modo Sabotage
}
void startDominationGame()
{
// Lógica para iniciar o modo Domination
}
void startSearchDestroyGame()
{
// Lógica para iniciar o modo Search & Destroy
}
// Função para carregar o config.json para a variável global
void loadConfig()
{
File file = LittleFS.open("/config.json", "r");
if (!file)
{
Serial.println("Erro ao abrir o arquivo config.json");
return;
}
// Desserializa o arquivo JSON
DeserializationError error = deserializeJson(configDoc, file);
if (error)
{
Serial.println("Erro ao desserializar o arquivo JSON");
}
String language = configDoc["language"].as<String>();
loadLanguage(language);
file.close();
}
// Função para salvar o config.json a partir da variável global
void saveConfig()
{
File file = LittleFS.open("/config.json", "w");
if (!file)
{
Serial.println("Erro ao abrir o arquivo config.json para escrita");
return;
}
// Serializa o documento JSON e grava no arquivo
serializeJson(configDoc, file);
file.close();
}
// Função para exibir informações no display
void updateDisplay(const char *line1, const char *line2, bool center1 = false, bool center2 = false)
{
lcd.clear();
// Exibe ou rola a primeira linha
if (strlen(line1) > 16)
{
scrollText(line1, 0); // Rola o texto na primeira linha
}
else
{
if (center1)
{
int len = strlen(line1);
int pad = (16 - len) / 2;
lcd.setCursor(pad, 0); // Centraliza
}
else
{
lcd.setCursor(0, 0);
}
lcd.print(line1);
}
// Exibe ou rola a segunda linha
if (strlen(line2) > 16)
{
scrollText(line2, 1); // Rola o texto na segunda linha
}
else
{
if (center2)
{
int len = strlen(line2);
int pad = (16 - len) / 2;
lcd.setCursor(pad, 1); // Centraliza
}
else
{
lcd.setCursor(0, 1);
}
lcd.print(line2);
}
}
// Função para rolar o texto no display (marquee effect) com "loop" contínuo
void scrollText(const char *message, int row)
{
int messageLen = strlen(message);
// Rola indefinidamente
while (true)
{
// Rola a janela de 16 caracteres pela mensagem
for (int i = 0; i < messageLen - 16 + 1; i++)
{
lcd.clear();
lcd.setCursor(0, row);
for (int j = 0; j < 16; j++)
{
lcd.print(message[i + j]);
}
delay(300); // Atraso para o efeito de rolagem
}
// Quando chegar ao final, volta a rolar o texto desde o início
for (int i = 0; i < 16; i++)
{
lcd.clear();
lcd.setCursor(0, row);
// Rola a parte final da mensagem e começa a repetir do início
for (int j = 0; j < 16; j++)
{
lcd.print(message[(i + j) % messageLen]); // Usa o operador '%' para criar o loop
}
delay(300); // Atraso para o efeito de rolagem
}
}
}
// Função para exibir o IP e a senha gerada no display
void displayIPAndPassword()
{
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("IP:");
lcd.print(WiFi.localIP());
lcd.setCursor(0, 1);
lcd.print("Senha:");
lcd.print(generatedPassword);
}
// Função para gerar uma senha aleatória de 4 dígitos
String generateRandomPassword()
{
String password = "";
for (int i = 0; i < 4; i++)
{
password += String(random(0, 10)); // Gera um número aleatório entre 0 e 9
}
return password;
}
// Função chamada ao salvar o config.json via requisição POST
void handleSaveConfig(AsyncWebServerRequest *request, uint8_t *data, size_t len)
{
String jsonData = "";
for (size_t i = 0; i < len; i++)
{
jsonData += (char)data[i];
}
DeserializationError error = deserializeJson(configDoc, jsonData);
if (error)
{
request->send(400, "application/json", "{\"status\":\"Erro ao processar JSON\"}");
return;
}
saveConfig();
request->send(200, "application/json", "{\"status\":\"Configurações salvas com sucesso!\"}");
updateDisplay("Config Atualizada!", "");
}
// Verifica a senha digitada pelo usuário
void handlePasswordCheck(AsyncWebServerRequest *request)
{
if (request->hasParam("password"))
{
String password = request->getParam("password")->value();
if (password == generatedPassword)
{
isAuthenticated = true;
request->send(200, "application/json", "{\"status\":\"Autenticado!\"}");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Acesso Liberado");
}
else
{
request->send(401, "application/json", "{\"status\":\"Senha incorreta!\"}");
}
}
else
{
request->send(400, "application/json", "{\"status\":\"Senha não fornecida!\"}");
}
}
// Função para tocar o buzzer
void playBuzzer()
{
tone(BUZZER_PIN, 1000); // Frequência do som
delay(100); // Duração
noTone(BUZZER_PIN);
}
void splashScreen()
{
delay(5000);
}
void setup()
{
Serial.begin(115200);
if (!LittleFS.begin())
{
Serial.println("Erro ao montar o sistema de arquivos LittleFS!");
return;
}
lcd.begin(16, 2);
lcd.init();
lcd.backlight();
splashScreen();
loadConfig();
generatedPassword = generateRandomPassword();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.println("Conectando ao WiFi...");
updateDisplay("Conectando WiFi...", "");
}
Serial.println("WiFi conectado!");
displayIPAndPassword();
// Configura o servidor web
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
server.on("/config.json", HTTP_GET, [](AsyncWebServerRequest *request)
{
if (isAuthenticated) {
String jsonString;
serializeJson(configDoc, jsonString);
request->send(200, "application/json", jsonString);
} else {
request->send(403, "application/json", "{\"status\":\"Não autenticado\"}");
} });
server.on("/save-config", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, handleSaveConfig);
server.on("/check-password", HTTP_POST, handlePasswordCheck);
server.begin();
if (isAuthenticated)
{
displayMenu();
}
}
void loop()
{
handleKeypadForMenu();
}