Se você é músico(a)/amante do áudio e tem um home studio, provavelmente já se deparou com a seguinte situação: você está gravando uma performance musical estelar, fazendo aquele take perfeito quando alguém entra no quarto e a magia é interrompida. Isso já aconteceu tantas vezes comigo que decidi providenciar uma luz que indicasse a quem está fora do meu home studio quando uma gravação está em curso. Percebi então que não há produtos automatizados para tal finalidade no Brasil, apenas luzes que são ativadas por interruptor. Como gasto muito tempo entre takes fazendo edição, preparando tracks, ensaiando, treinando etc., preferi me desafiar (e desafiar meu pai, Dailton Menezes) com este projeto, de maneira que a luz só esteja acesa durante a gravação, sem que eu tenha que ficar acendendo-a e desligando-a manualmente.
Quem se aventura no mundo do áudio sabe que há infinitas opções de DAW (Digital Audio Workstation – software para captação, edição, mixagem e/ou masterização de áudio e MIDI). Muitos produtores musicais (eu, inclusive) utilizam mais de um DAW para suas produções. Atualmente, os meus dois DAWs de escolha são o Logic Pro X e o Pro Tools. Portanto, mergulhamos no desafio de realizar esse projeto de maneira que a luz de gravação automatizada fosse compatível com ambos os programas. Entretanto, decidimos utilizar os princípios aqui apresentados para fazer com que o sistema funcione com qualquer DAW (desde que o DAW possua funcionalidade MIDI que indique quando uma gravação está em curso). O grande diferencial desse projeto em comparação a outros que você pode encontrar por aí na Internet está nisso: com o nosso programa, você consegue descobrir quais bytes estão sendo emitidos pelo seu DAW de escolha e alterar facilmente o programa para que a luz acenda com os bytes encontrados, tudo pelo seu celular. Além disso, com o ESP32 é possível também utilizar seu celular para acender a luz manualmente. Fique ligado até o final do post para saber mais detalhes!
1 x Módulo WiFi ESP32 Bluetooth 30 pinos
1 x Módulo Relé 1 Canal 5V
1 x Transistor NPN BC548
1 x WorkPlate de 400
3 x Mini Protoboard 170 pontos
1 x Diodo 1N4148
1 x Conversor de Nível Lógico 3.3V-5V Bidirecional – 2 Canais
1 x Resistor 470R 1/4W
1 x Resistor 220R 1/4W
2 x Resistor 10K 1/4W
1 x Conector MIDI 5 pinos Fêmea
1 x Optoacoplador 6N138 Dip-08
1 x Fonte 12V 3A Bivolt
1 x Módulo Regulador de Tensão Step Down LM2596 com Display
1 x Cabo USB-MIDI
1 x Fita Led 12V
Opcional: 1 x Uno R3 + Cabo Usb para Arduino
Bibliotecas (ESP32)
ESPAsyncWebServer
AsyncTCP
ESP32Ping
WiFiManager
FS, SPIFFS, Time, ArduinoJson, HardwareSerial, ESPmDNS e ElegantOTA (instalados através do Gerenciador de Bibliotecas)
Bibliotecas (UNO)
SoftwareSerial (Instalado através do Gerenciador de Bibliotecas)
Tanto o Pro Tools quanto o Logic Pro X emitem informação MIDI quando uma gravação é iniciada/terminada. O Logic Pro X tem uma interface muito mais fácil de navegar: a função de Luz de Gravação (Recording Light) através do menu CONTROL SURFACES. Quando esta função está ativada, o software manda uma sequência de caracteres pela interface MIDI para informar o início e fim do modo de gravação. É muito fácil selecionar em qual canal MIDI essa informação vai ser transmitida, além de vários parâmetros da mensagem MIDI em si. Veja as figuras:
Esta mesma função no Pro Tools 11, entretanto, já se mostrou muito mais desafiadora. Não encontramos nenhuma referência na Internet sobre o funcionamento dessa função no Pro Tools, nem do fabricante e nem mesmo de outra pessoa que tenha feito um projeto parecido. Até encontramos projetos parecidos para outros DAWs, mas nenhum especificamente para o Pro Tools. Tudo o que conseguimos aprender sobre tal interface MIDI do Pro Tools foi na base da experimentação.
O Pro Tools constantemente emite mensagens MIDI, e a natureza dessas mensagens varia de acordo com o hardware MIDI selecionado no menu. Veja as figuras:
Nós analisamos as várias opções que nos são disponíveis, até encontrar uma em que a mensagem MIDI de início/fim de gravação fosse mais simples de interpretar. Depois disso, ajustamos o código de maneira a contemplar os bytes únicos que se repetem quando uma gravação se inicia e se interrompe.
Tal princípio pode ser aplicado a qualquer DAW: basta aprender a visualizar os bytes que são emitidos (utilizando o CONSOLE do microcontrolador) e interpretá-los. O Pro Tools, Logic Pro e o Reaper são previamente cadastrados, mas é possível inserir, alterar, deletar outros produtos através de um FORM html. A lista de softwares ficará armazenada no sistema de arquivos do ESP32 chamado SPIFFS no formato JSON.
{ "nomeEstudio": "Studio", "intervaloTimeout": "1000", "softwares": [ { "nome": "ProTools", "lightOn": "176,116,0,176,118,127,176,117,127", "lightOff": "176,117,0,176,116,127,176,118,0" }, { "nome": "LogicPro", "lightOn": "145,25,127", "lightOff": "145,25,0" }, { "nome": "Reaper", "lightOn": "144,0,0,176,12,14", "lightOff": "144,0,0,176,12,14" } ], "usuarioOTA": "admin", "senhaOTA": "midi@2024", "autorebootOTA": true }
Este projeto foi baseado em outro projeto listado na Referência 1, estendendo para suportar os comandos para os três produtos (ou mais) pois cada um manda sequências diferentes. Adicionalmente, utilizando o microcontrolador ESP32 foi possível implementar um servidor HTTP para receber comandos via http. Assim, implementamos a manutenção no cadastro de softwares para evitar que as informações das sequências de ligar/desligar sejam fixas no código, e assim não haja a necessidade de realizar recompilações do programa a cada novo software ou mudanças nas sequências.
Conforme mencionado anteriormente, utilizamos o circuito proposto na Referência 1 para converter o sinal MIDI para a interface serial do ESP32. A figura a seguir nos mostra o circuito original que utilizamos como ponto de partida. Para maiores informações sobre o circuito original, sugerimos a leitura do artigo da Referência 1.
Figura 1 – Diagrama Esquemático do Circuito Original
Como o ESP32 trabalha no nível lógico de 3,3V, utilizamos um conversor de nível lógico para compatibilizar com a tensão do ESP32 e obtivemos o circuito representado na figura a seguir:
Figura 2 – Diagrama do Circuito Adaptado para o ESP32
Utilizamos uma fonte de 12V 3A para alimentar a fita LED que iluminará a placa de GRAVANDO e ao mesmo tempo alimenta o regulador de tensão LM2596 que regula a tensão de saída para 5V para alimentar a porta VIN do ESP32. Utilizamos também o transistor BC548 para acionar o relé de 5V uma vez que o nível lógico do ESP32 é de 3,3V. Uma outra alternativa seria adotar um relé de 3V.
Figura 3 – Circuito em Bancada
Para complementar o projeto, implementamos o WifiManager para permitir a configuração ou reconfiguração da rede Wifi sem ter que recompilar o programa e utilizamos a biblioteca Elegant OTA para atualização do programa, em caso de novas versões, sem a necessidade de ter que deslocar o circuito até um desktop ou notebook para fazer o UPLOAD do novo código.
A seguir apresentamos as telas da aplicação principal, do WifiManager e do Elegant OTA:
Figura 4 – Tela Principal Light OFF
Figura 5 – Tela Principal Light ON
Figura 6 – Tela Principal Status
Figura 7 – Tela Principal Recepção de Bytes MIDI
Figura 8 – Tela de Manutenção dos Softwares
Figura 9 – WifiManager Reconhecimento do ESP32 no Modo AP
Figura 10 – WifiManager Confirmação de Conexão
Figura 11 – WifiManager Conectado no Modo AP
Figura 12 – WifiManager Tela Principal para Configuração
Figura 13 – WifiManager Configuração da Rede Wifi e Parâmetros
Figura 14 – Atualização OTA Autenticação
Figura 15 – Atualização OTA Seleção da Imagem a Atualizar
Figura 16 – Luz de Estúdio Ativa com a Fita LED
Figura 17 – Parâmetros de Compilação
Figura 18 – Mensagens Emitidas na Console
Opcionalmente, podemos usar o Arduino UNO para simular o envio dos bytes caso você não tenha um dos softwares de edição de música como o ProTools, Logic Pro ou Reaper, que são softwares pagos, e/ou não tenha o cabo MIDI. Para isso, precisaremos de um pequeno circuito para o envio dos bytes via Serial, como mostra a figura 19. Adaptaremos o TX para o pino 3 pois usaremos a biblioteca SoftwareSerial e adicionalmente não usaremos o pino GND (figura 20). Os bytes podem ser enviados através da Console separando cada byte por vírgula. A console deve ser definida com “Retorno de Carro” e o “Baudrate” deve ser 115200 conforme indicado na figura 21.
Figura 19 – Circuito Simulador para envio pela Interface MIDI
Figura 20 – Diagrama do Simulador de Envio pela Interface MIDI
Figura 21 – Console para Envio dos Bytes pela Interface MIDI
Figura 22 – Circuito Simulador em Bancada
//------------------------------------------------------------------------------------------- // Função : Este programa tem como objetivo simular o envio de bytes como se fosse uma // interface MIDI ligada a softwares de edição de música como o ProTools, // Logic Pro e o Reaper. // // Autores: Alberto Menezes e // Dailton Menezes // // Versão : 1.0 Mar/2024 //------------------------------------------------------------------------------------------- #include <SoftwareSerial.h> #define TX_PIN 3 // Define o pino TX usado para a comunicação serial virtual #define RX_PIN 2 // Define o pino RX usado para a comunicação serial virtual SoftwareSerial midiSerial(RX_PIN, TX_PIN); // Inicializa a comunicação serial virtual //------------------------------ // Rotina de Inicialização //------------------------------ void setup() { // Inicializa a comunicação serial Serial.begin(115200); while (!Serial); // Inicializa a comunicação serial MIDI midiSerial.begin(31250); // Hello na Console Serial.println("Simulação do Envio de Bytes MIDI V1.0 Mar/2024"); } //------------------------------ // Loop Principal //------------------------------ void loop() { // Verifica se há dados disponíveis na porta serial if (Serial.available() > 0) { // Lê a string de bytes da porta serial String bytesString = Serial.readStringUntil('\n'); // Divide a string de bytes pelo delimitador ',' while (bytesString.length() > 0) { int pos = bytesString.indexOf(','); if (pos != -1) { // Extrai o próximo byte da string String byteStr = bytesString.substring(0, pos); // Elimina o string extraído do string original bytesString = bytesString.substring(pos + 1); // Converte a string para um byte e envia para o pino de saída byteStr.trim(); byte byteToSend = byteStr.toInt(); Serial.print(byteToSend); Serial.print(", "); // Envie o byte para os pinos de saída midiSerial.write(byteToSend); } else { // Se não houver mais vírgulas na string, envia o último byte // e sai do loop bytesString.trim(); byte byteToSend = bytesString.toInt(); Serial.print(byteToSend); Serial.println(); midiSerial.write(byteToSend); break; } // Aguarda um curto período de tempo antes de enviar o próximo byte delay(10); } } }
//--------------------------------------------------------------------------------------------- // Função : Este programa tem como objetivo acender a chamada lâmpada de gravação de estúdio // quando os softwares de edição de música entram no modo de gravação e // apagar a referida lâmpada quando o modo de gravação é desativado. Produtos como // ProTools, LogicPro, Reaper, etc. possuem tal função de acionar o luz de estúdio // através da interface MIDI. O usuário pode fazer o cadastramento dos Softwares e // suas respectivas sequências de ligar e desligar. Adicionalmente, o programa // implementa um servidor http para responder requisições via navegadores na porta 80. // // Objetivos específicos: // // 1) Esperar os bytes enviados pela Serial e analisar os comandos de acender/desligar // a luz de estúdio para os softwares cadastrados anteriormente. A sequência recebida // é enviada ao Navegador para atualizar a interface via WebSocket. // // 2) Escutar a porta 80 para aceitar as requisições a seguir: // / => para mostrar o Menu Principal e suas opções // /lighton => para acender a luz de estúdio // /lightoff => para desligar a luz de estúdio // /status => para mostrar as informações do servidor // /database => para mostrar a base de sofwares // /commit => para atualizar ou inserir software // /delete => para deletar um software // /softwares => para mostrar a lista de sofwares // /software => para mostrar os detalhes de um software // /update => para atualizar o firmware via OTA // // 3) Pressionando-se o botão BOOT do ESP32 durante a operação normal, o ESP32 entra // no modo AP para configuração da Rede Wifi na porta 8080. Deve-se seguir os // seguintes passos para configurar: // . Através de um celular/desktop, procurar um dispositivo na Rede Wifi com o // nome ESP32_AP_nnnnn, onde nnnnn é o número do serial do ESP32 específico. // . Conectar no ESP32_AP_nnnnnn onde ficará conectado porém sem internet // . Ir para a URL http://192.168.4.1:8080 // . Pressionar o botão Configure WiFi // . Informar o SSID, a senha, o nome do Estúdio (até 50 bytes) e o tempo de // timeout da interface MIDI em ms (1000 ms é o default). // . Confirmar o diálogo e o ESP32 voltará para o modo normal conectando na rede // wifi recém configurada. O LED Azul do ESP32 ficará acesso se tudo correr bem. // // 4) Atualizar o relógio interno do ESP32-CAM sincronizado com o servidor NTP do Brasil. // // 5) Três produtos já vêem pré-definidos: ProTools, LogicPro e Reaper. O Usuário // pode adicionar, alterar e delatar novos softwares e suas sequèncias de ligar e // desligar a partir de um FORM html. A lista de softwares é persistida no // SPIFFS no formato JSON. // // 6) Adicionar o <nome do estúdio> no mDNS para facilitar o acesso ao servidor web // através da URL http://<nome do estúdio>.local para evitar ter que descobrir qual // IP foi atribuído pela rede Wi-Fi. Sugerimos que o nome do estúdio não contenha // caracteres especiais e nem acentuados pois o serviço de resolução de nomes da // rede (DNS) não aceita. // // 7) O firmware do ESP32 pode ser atualizado over-the-air através da OTA. Basta // usar a URL /update para cair no FORM de atualização. Será necessário ter a // senha para atualizar. O arquivo .bin do sketch pode ser obtido pelo IDE do // Arduino através do Menu Sketch | Exportar Binário Compilado. Isso facilita // atualizações de versão do programa sem a necessidade de se ter a placa // conectada fisicamente no desktop para a carga. Muitas vezes a placa pode // estar num lugar de difícil remoção para liogar num desktop. Adicionalmente, // o desenvolvedor pode enviar o arquivo .bin de uma nova versão para o usuário // fazer a carga via OTA. Portanto, isso facilita a atualização em qualquer // lugar do mundo. // // Observações: // // 1) O software LogicPRO envia uma sequência de 3 bytes para definir o comando // ON e OFF da lâmpada de estúdio: // // ON => 145 | 25 | 127 // OFF => 145 | 25 | 0 // // 2) O software ProTools envia uma sequência de 3 bytes para definir o comando // ON e OFF da lâmpada de estúdio: // // ON => 176 | 116 | 0 | 176 | 118 | 127 | 176 | 117 | 127 // OFF => 176 | 117 | 0 | 176 | 116 | 127 | 176 | 118 | 0 // // 3) O software Reaper envia uma sequência de 3 bytes para definir o comando // ON e OFF da lâmpada de estúdio: // // ON => 144 | 0 | 0 | 176 | 12 | 14 // OFF => 144 | 0 | 0 | 176 | 12 | 14 // // 4) Um pequeno circuito foi projetado para permitir a conexão do cabo MIDI para // recepção via serial (vide Referências) // // 5) O cadastro de softwares será armazenado no formato JSON e persistido no SPIFFS // // { // "nomeEstudio": "Studio", // "intervaloTimeout": "1000", // "softwares": [ // { // "nome": "ProTools", // "lightOn": "176,116,0,176,118,127,176,117,127", // "lightOff": "176,117,0,176,116,127,176,118,0" // }, // { // "nome": "LogicPro", // "lightOn": "145,25,127", // "lightOff": "145,25,0" // }, // { // "nome": "Reaper", // "lightOn": "144,0,0,176,12,14", // "lightOff": "144,0,0,176,12,14" // } // ], // "usuarioOTA": "admin", // "senhaOTA": "midi@2024", // "autorebootOTA": true // } // // 6) IMPORTANTE : A ESP32 Board não pode estar na versão 3. Sugeríamos a última // versão 2.0.17. A versão 3 modificou algumas constantes usadas // e isso gerará erro na compilação. // // Componentes : 1) 1 x ESP32 30 pinos // 2) 1 x Módulo Relé 1 Canal 5V // 3) 1 x Transitor BC547 ou BC548 // 4) 1 x WorkPlate de 400 // 5) 3 x Mini Protoboard 170 pontos // 6) 2 x Resistor de 10K // 7) 1 x Diodo 1N4148 // 8) 1 x Conversor de Nível Lógico 3V/5V 2 canais // 9) 1 x Resistor 470 Ohms // 10) 1 x Resistor 220 Ohms // 11) 1 x Conector MIDI 5 pinos Fêmea // 12) 1 x Optoacoplador 6N138 Dip-08 // 13) 1 x Fonte 12V 3A // 14) 1 x Regulador de Tensão LM2696 com display // 15) 1 x Cabo USB-MIDI // 16) 1 x Fita Led 12V // // Bibliotecas: ESPAsyncWebServer => https://github.com/me-no-dev/ESPAsyncWebServer // AsyncTCP => https://github.com/me-no-dev/AsyncTCP // ESP32Ping => https://github.com/marian-craciunescu/ESP32Ping // WiFiManager => https://github.com/tzapu/WiFiManager // HardwareSerial, // DHT, FS, SPIFFS, // Time, ArduinoJson, // LiquidCrystal_I2C, // ESPmDNS, // ElegantOTA => Instalado através do Gerenciador de Bibliotecas // // Referências: 1) https://www.instructables.com/Midi-Controlled-Recording-Light-for-Logic-Pro-X/ // 2) https://www.youtube.com/watch?v=XKyX1_II3bc // 3) https://www.utmel.com/components/6n138-optocouplers-features-pinout-and-datasheet?id=994 // 4) https://pdf1.alldatasheet.com/datasheet-pdf/view/15021/PHILIPS/1N4148.html // 5) https://www.youtube.com/watch?v=gNv8tzyb0BU&t=160s // 6) https://www.instructables.com/Send-and-Receive-MIDI-with-Arduino/ // 7) https://docs.elegantota.pro/ // // Autores: Alberto Menezes e // Dailton Menezes // // Versão : 2.4B Out/2024 - Interface MIDI com ESP32 Assíncrono Web Server com WebSocket e OTA //-------------------------------------------------------------------------------------------- //--------------------------------------------- // Definição das Bibliotecas //--------------------------------------------- #include <HardwareSerial.h> #include <Arduino.h> // Biblioteca Arduino #include <WiFi.h> // Biblioteca WiFi #include <AsyncTCP.h> // Biblioteca AsyncTCP usado pelo Web #include <ESP32Ping.h> // Biblioteca Ping #include <FS.h> // Biblioteca FileSystem #include <SPIFFS.h> // Biblioteca SPIFFS #include <WiFiManager.h> // Biblioteca WiFi Manager #include <ESPAsyncWebServer.h> // Biblioteca Asynch Web Server #include <time.h> // Biblioteca Time para manipulação de data/hora #include <ArduinoJson.h> // Biblioteca JSON para comunicação e parãmetros #include <ESPmDNS.h> // Biblioteca para inclusão do hostname no mDNS #include <ElegantOTA.h> // Biblioteca para atualização via Web //--------------------------------------------- // Definição dos Pinos Utilizados //--------------------------------------------- #define LED_BUILTIN 2 // Pino para o Led Interno do ESP32 #define pin_rele 4 // pino do Rele #define pin_md_rx 16 // pino RX do Midi #define pin_md_tx 17 // pino TX do Midi #define pin_led LED_BUILTIN // pino do Led interno #define TRIGGER_PIN 0 // Pino do botão para forçar a entrada no modo de configuração do WiFi //--------------------------------------------- // Outras Definições //--------------------------------------------- #define RELE_OFF LOW // Estado do Relay OFF #define RELE_ON HIGH // Estado do Relay ON #define TIMEOUT 1000 // Timeout na Serial de 1000 ms #define LEDDELAY 1000 // Delay para acionar o Led Interno #define BAUDRATE 31250 // Baudarate do interface MIDI #define TIMERECONEXAO 60000 // Tempo para tentar reconectar a Internet #define MAX_EDIT_LEN 30 // Tamanho máximo de campos de EDIT #define MAX_NUM_LEN 4 // Tamanho máximo de campos NUMÈRICO #define ESP_DRD_USE_SPIFFS true // Uso com SPIFFS #define JSON_CONFIG_FILE "/config.json" // Arquivo JSON de configuração #define ESP_getChipId() ((uint32_t)ESP.getEfuseMac() // Simular ID da placa ESP #define USER_UPDATE "admin" // Usuário para atualização via OTA #define PASS_UPDATE "midi@2024" // Senha para atualização via OTA #define DEFAULT_STUDIO "Studio" // Nome default para o estúdio #define DEFAULT_PASS_AP "12345678" // Senha default do modo AP WifiManager //--------------------------------------------- // Variáveis para a comunicação com o MIDI //--------------------------------------------- String stack=""; // pilha de bytes recebidos separados por vírgula unsigned long lastRecebido=0; // Momento do último byte recebido char aux[20]; // para formatação de dados JsonDocument dbSW; // Base de dados de softwares //--------------------------------------------- // Variável para controlar o Botão de BOOT //--------------------------------------------- volatile bool buttonState = false; // Estado do botão Boot para Reconfiguração do WiFi //--------------------------------------------- // Variáveis para controle do Server http //--------------------------------------------- IPAddress ip (1, 1, 1, 1); // The remote ip to ping, DNS do Google unsigned long semInternet; // Momento da queda da Internet bool lastInternet; // Última verificação da internet bool atualInternet; // Se tem internet no momento unsigned long lastCleanUp; // Última limpeza de conexões perdidas de navegadores para não estourar o http server AsyncWebServer sv(80); // Servidor http na porta 80 (WifiManager rodará na 8080) AsyncWebSocket ws("/ws"); // Socket para cleanup de conexões antigas perto do limite máximo de conexões simultâneas const char* NTP_SERVER = "a.st1.ntp.br"; // Dados do Servidor NTP do Brasil const char* TZ_INFO = "<-03>3"; // Definição do Fuso time_t startup; // hora do startup String estadoLight[2] = {"OFF", "ON"}; // Estados da Luz de Estúdio bool atual_estado_light = false; // Atual estado da Lud de Estúdio String htmlResposta = ""; // Respostas no rodapé da página principal String myVersion="V2.4B Out/2024 - ESP32"; // Versão do Programa bool ledState =LOW; // Estado do Led interno do ESP32 unsigned long lastLed=0; // Momento do último acionamento do led interno unsigned long lastReconexao=0; // Momento da última tentativa de reconexão da Internet //--------------------------------------------- // Variáveis para controle do OTA //--------------------------------------------- bool autoRebootOTA = true; // Se deve fazer autoreboot após a atualização OTA char user_OTA[MAX_EDIT_LEN] = USER_UPDATE; // Usuário para atualização OTA char pass_OTA[MAX_EDIT_LEN] = PASS_UPDATE; // Senha para atualização OTA char val_autoreboot[2] = "1"; // AutoRebbot Default //--------------------------------------------- // Variáveis para controle do WifiManger/OTA //--------------------------------------------- WiFiManager wm; // Define o Objeto WiFiManager bool shouldSaveConfig = false; // Flag se deve persistir os parãmetros char studioID[MAX_EDIT_LEN+1] = DEFAULT_STUDIO; // Nome default do estação do MIDI char valTimeout[MAX_NUM_LEN+1]= "1000"; // Timeout default int intervaloTimeout = TIMEOUT; // Para receber o Intervalo default do timeout (1000 ms) char ssid_config[MAX_EDIT_LEN+1]; // SSID para o modo AP de Configuração char pass_config[] = DEFAULT_PASS_AP; // Senha para o modo AP de Configuração WiFiManagerParameter custom_studioID("StudioID", "Informe o Id do Estúdio (< 15)", studioID, MAX_EDIT_LEN); // Parâmetro Nome do Studio WiFiManagerParameter custom_intervaloTimeout("IntervaloTimeout", "Informe o intervalo de Timeout em ms (< 9999)", valTimeout, MAX_NUM_LEN); // Parâmetro Timeout WiFiManagerParameter custom_user_ota("Usuario", "Informe o Usuário para Atualizações (< 15)", user_OTA, MAX_EDIT_LEN); // Parâmetro Nome do Usuário OTA WiFiManagerParameter custom_pass_ota("Senha", "Informe a Senha para Atualizações (< 15)", pass_OTA, MAX_EDIT_LEN); // Parâmetro Senha do Usuário OTA WiFiManagerParameter custom_autoreboot_ota("AutoReboot", "AutoReboot após Atualizações (0 ou 1)", val_autoreboot, 1); // Parâmetro Timeout //------------------------------------ // Define o JSON Default dos Softwares //------------------------------------ const char dbDefault[] PROGMEM = R"( { "nomeEstudio": "Studio", "intervaloTimeout": "1000", "softwares": [ { "nome": "ProTools", "lightOn": "176,116,0,176,118,127,176,117,127", "lightOff": "176,117,0,176,116,127,176,118,0" }, { "nome": "LogicPro", "lightOn": "145,25,127", "lightOff": "145,25,0" }, { "nome": "Reaper", "lightOn": "144,0,0,176,12,14", "lightOff": "144,0,0,176,12,14" } ], "usuarioOTA": "admin", "senhaOTA": "midi@2024", "autorebootOTA": true })"; //------------------------------------ // Define o HTML para Página Principal //------------------------------------ const char index_html[] PROGMEM = R"rawliteral( <!DOCTYPE html> <html lang="pt-br"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>Esp32-CAM Server</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4;} header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;} button {width: 140px; margin: 5px; padding: 10px; font-size: 16px; background-color: #4CAF50; color: #fff; border: none; border-radius: 5px; cursor: pointer;} p {font-size: 16px; color: #444444; margin-bottom: 5px; margin-top: 5px;} form, input, ol {font-size: 16px; color: #444444;} table, th, td {border: none;} th, td {color: #444444;} table {margin-left: auto; margin-right: auto;} th {background-color: #4CAF50; color: white;} #mensagem { width: 300px; /* Largura fixa */ border: 1px solid #ccc; /* Borda de 1px sólida cinza */ padding: 10px; /* Adiciona um preenchimento interno de 10px */ overflow: auto; /* Adiciona uma barra de rolagem se necessário */ max-height: 200px; /* Altura máxima do div antes de adicionar a barra de rolagem */ } </style> </head> <body> <div id="webpage"> <header> <h2>Interface MIDI - Luz de Gravação</h2> </header> <BR> <center> <div id="statusLuz"> <p>Luz de %nomestudio% : <b>%luzstudio%</b></p> </div> </center> <main> <div style="display: flex; flex-wrap: wrap; justify-content: center;"> <button onclick="acaoBotao('/lighton')">Ligar Luz</button> <button onclick="acaoBotao('/lightoff')">Desligar Luz</button> </div> <div style="display: flex; flex-wrap: wrap; justify-content: center;"> <button onclick="acaoBotao('/status')">Mostrar Status</button> <button onclick="acaoBotao('/database')">Sofwares</button> </div> </main> <br> <center> <div id="mensagem"> <p>%BUTTONPLACEHOLDER%</p> </div> </center> <script> // Cria uma instância do WebSocket const socket = new WebSocket('ws://' + window.location.hostname + '/ws'); // Substitua localhost e a porta pelo seu endereço IP e porta // Evento para lidar com o fechamento da página window.addEventListener('unload', function(event) { // Verifica se a conexão WebSocket está aberta antes de fechar if (socket.readyState === WebSocket.OPEN) { // Fecha a conexão WebSocket socket.close(); } }); // Evento chamado quando a conexão é aberta socket.onopen = function(event) { console.log('Conexão estabelecida.'); }; // Evento chamado quando uma mensagem é recebida socket.onmessage = function(event) { // Divide a mensagem recebida em duas partes usando o caractere ; var partes = event.data.split(';'); // Define a primeira parte no div statusLuz document.getElementById('statusLuz').innerHTML = partes[0]; // Define a segunda parte no div mensagem document.getElementById('mensagem').innerHTML = partes[1]; }; // Evento chamado quando ocorre um erro socket.onerror = function(error) { console.error('Erro na conexão WebSocket:', error); }; // Evento chamado quando a conexão é fechada socket.onclose = function(event) { console.log('Conexão fechada.'); }; function acaoBotao(acao) { window.location.href = acao; } </script> </div> </body> </html> )rawliteral"; //------------------------------------ // Define o HTML para Gerenciamento SW //------------------------------------ const char database_html[] PROGMEM = R"rawliteral( <!DOCTYPE html> <html lang="pt-br"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>Gerenciador de Softwares</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4;} header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;} main { display: flex; flex-direction: column; align-items: center; padding: 20px;} button {width: 280px; margin: 5px; padding: 10px; font-size: 16px; background-color: #4CAF50; color: #fff; border: none; border-radius: 5px; cursor: pointer;} p {font-size: 20px; color: #444444; margin-bottom: 10px;} form {font-size: 16px; color: #444444;} input, select {width: 280px; font-size: 16px; color: #444444;} /* Estilo para o botão "Home" */ .btn-home { width: 280px; margin: 5px; padding: 10px; font-size: 16px; background-color: #0074E4; /* Cor de fundo azul */ color: white; /* Cor do texto branco */ border: none; /* Sem borda */ border-radius: 5px; cursor: pointer; /* Cursor de ponteiro ao passar o mouse */ } </style> </head> <body> <div id="webpage"> <header> <h2>Gerenciador de Software</h2> </header> <center> <main> <select id="softwareSelect" onchange="fillForm()"> <option value="">Selecione um software</option> </select> <h3>Atualizar ou Deletar Software</h3> <form id="softwareForm" method="post"> <input type="hidden" id="softwareIndex"> <label for="softwareName">Nome do Software:</label><br> <input type="text" id="softwareName" name="softwareName" required placeholder="Informe o Nome do Software"> <br><br> <label for="ligarBytes">Sequência para ligar a luz:</label><br> <input type="text" id="ligarBytes" name="ligarBytes" required placeholder="Informe inteiros separados por vírgula"> <br><br> <label for="desligarBytes">Sequência para desligar a luz:</label><br> <input type="text" id="desligarBytes" name="desligarBytes" required placeholder="Informe inteiros separados por vírgula"> <br><br> <button type="button" onclick="setFormAction('/commit')">Atualizar</button><br> <button type="button" onclick="setFormAction('/delete')">Deletar</button><br> </form> <form action='/'> <input type='submit' class='btn-home' value='Retornar'> </form> </main> <script> // Função para preencher o campo datalist com a lista de softwares function fillSelect() { fetch('/softwares') .then(response => response.json()) .then(data => { var select = document.getElementById("softwareSelect"); select.innerHTML = '<option value="">Selecione um software</option>'; data.forEach(function(software) { var option = document.createElement('option'); option.value = software.nome; option.textContent = software.nome; select.appendChild(option); }); }) .catch(error => console.error('Erro ao obter a lista de softwares:', error)); } // Função para preencher o formulário com os dados do software selecionado function fillForm() { var select = document.getElementById("softwareSelect"); var selectedIndex = select.selectedIndex; if (selectedIndex !== -1) { var selectedSoftwareName = select.options[selectedIndex].value; fetch('/software/' + encodeURIComponent(selectedSoftwareName)) .then(response => response.json()) .then(data => { document.getElementById("softwareName").value = data.nome; document.getElementById("ligarBytes").value = data.lightOn; document.getElementById("desligarBytes").value = data.lightOff; }) .catch(error => console.error('Erro ao obter os detalhes do software:', error)); } } // Função para ativar a ação do FORM /update ou /delete function setFormAction(action) { // Obter os valores dos campos var softwareName = document.getElementById('softwareName').value; var ligarBytes = document.getElementById('ligarBytes').value; var desligarBytes = document.getElementById('desligarBytes').value; // Verificar se o nome do software está vazio if (softwareName === "") { alert("O nome do software não pode estar vazio!"); return; // Abortar o envio do pedido POST } // Verificar a validade dos bytes de ligar if (!isValidByteList(ligarBytes)) { alert("Os bytes para ligar devem ser uma lista de inteiros separados por vírgula (0<= inteiro <= 255)!"); return; } // Verificar a validade dos bytes de desligar if (!isValidByteList(desligarBytes)) { alert("Os bytes para desligar devem ser uma lista de inteiros separados por vírgula (0<= inteiro <= 255)!"); return; } // Confirmar a exclusão com o usuário if (!confirm("Confirme a operação de '" + action + "' para o software '" + softwareName + "'?")) { return; // Abortar a operação } // Atribuir a ação ao formulário e enviar document.getElementById('softwareForm').action = action; document.getElementById('softwareForm').submit(); } // Função para verificar se uma lista de bytes é válida function isValidByteList(byteList) { var bytes = byteList.split(","); for (var i = 0; i < bytes.length; i++) { var byte = parseInt(bytes[i]); if (isNaN(byte) || byte < 0 || byte > 255) { return false; // Encontrou um byte inválido } } return true; // Todos os bytes são válidos } // Ao carregar a página, preencher o campo datalist com a lista de softwares window.onload = function () { fillSelect(); }; </script> <p>%BUTTONPLACEHOLDER%</p> </center> </div> </body> </html> )rawliteral"; //------------------------------------------- // Prototipação das rotinas usadas //------------------------------------------- void lightON(String sw); // Liga a lâmpada de estúdio e o Led interno void lightOFF(String sw); // Desliga a lâmpada de estúdio e o Led interno void trataMIDI(); // Rotina para tratar o timer void resetaBuffer(); // Reseta o buffer e os controles da recepção void saveConfigFile(); // Persiste CPUID e Intervalo no SPIFFS do ESP32 bool loadConfigFile(); // Recupera CPUID e Intervalo do SPIFFS do ESP32 void saveConfigCallback(); // Callback para informação do processo de configuração WiFi void configModeCallback(WiFiManager *myWiFiManager); // Callback para WifiManager bool getNTPtime(int sec); // Sincroniza o horário do ESP32 com NTP server brasileiro void WiFiEvent(WiFiEvent_t event); // Evento chamado no processo de conexão do Wifi void Check_WiFiManager(bool forceConfig); // Inicialização/Configuração WiFi Manager no ESP32 void displayRequest(AsyncWebServerRequest *request); // Mostra informações da requisição http na Console String processor(const String& var); // Expande a página principal String colorirLuz(bool estado); // Dar a cor adequada para o estado da Luz de Estúdio String getTimeStamp(); // Devolve o localtime dd/mm/aaaa hh:mm:ss void nao_encontrado(AsyncWebServerRequest *request); // Responde a páginas não encontrada String textToHtml(String texto); // Converte uma mensagem com tags html void buttonISR(); // Rotina de Tratamento da Interrupção do Botão Boot void IRAM_ATTR Serial1_RX_ISR(); // Rotina de Tratamento da Interrupção do RX da Serial1 void handleGetSoftwares(AsyncWebServerRequest *request); // handle para preencher o selectlist void handleGetSoftwareDetails(AsyncWebServerRequest *request); // Handle para devolver o detalhe de um software void toggleLed(); // Alterna o Led Interno ON-OFF void setLedState(bool state); // Seta o estado do led para um determinado valor bool setDNSNAME(String nome); // Define o HostName como DNSNAME //--------------------------------------------- // Inicialização do Programa e recursos usados //--------------------------------------------- void setup() { // Inicializa a Serial Serial.begin(115200); while (!Serial) ; // Define o CALLBACK do modo CONFIG com alteração wm.setSaveConfigCallback(saveConfigCallback); // Define o CALLBACK do modo CONFIG wm.setAPCallback(configModeCallback); // Adiciona os campos de parâmetros no MENU do WifiManager wm.addParameter(&custom_studioID); wm.addParameter(&custom_intervaloTimeout); wm.addParameter(&custom_user_ota); wm.addParameter(&custom_pass_ota); wm.addParameter(&custom_autoreboot_ota); // Define o handle para tratar os eventos do Wifi WiFi.onEvent(WiFiEvent); // Inicializa o Botão interno do ESP32 pinMode(TRIGGER_PIN, INPUT_PULLUP); // Configura a interrupção para detectar a borda de descida do botão Boot attachInterrupt(digitalPinToInterrupt(TRIGGER_PIN), buttonISR, FALLING); // Inicializa o LED interno pinMode(pin_led, OUTPUT); digitalWrite(pin_led,LOW); // Inicializa o RELE pinMode(pin_rele, OUTPUT); digitalWrite(pin_rele,RELE_OFF); // Inicializa a Serial da Interface MIDI Serial1.begin(BAUDRATE, SERIAL_8N1, pin_md_rx, pin_md_tx); // Defina a porta do WiFiManager para 8080 no modo AP para não conflitar com a // porta 80 que vamos utilizar para responder as requisições wm.setHttpPort(8080); // Chama Wifi_Manager para conectar no Wifi ou entrar em modo de configuração // caso os parãmetros SSID, Senha, CPIID e Intervalo do TIMER não estejam persistidos Check_WiFiManager(!wm.getWiFiIsSaved()); // Verifica se teve está conectado na Internet if (WiFi.status() == WL_CONNECTED) { // Se chegamos até aqui é porque estamos conectados Serial.println("WiFi conectado..."); Serial.print("IP address: "); Serial.println(WiFi.localIP()); // Imprime o MAC Serial.print("MAC: "); Serial.println(WiFi.macAddress()); // Imprime o Sinal Wifi Serial.print("Sinal: "); Serial.print(WiFi.RSSI()); Serial.println(" db"); // Verifica se está navegando pela internet pois às vezes fica conectado no AP porém sem internet lastInternet = Ping.ping(ip,4); if (!lastInternet) { semInternet = millis(); Serial.println("Sem internet no momento..."); } else { Serial.print("Internet ativa com média de "); Serial.print(Ping.averageTime()); Serial.println(" ms"); } // Sincroniza o horário interno com o Servidor NTP nacional Serial.print("Tentando sincronismo com o servidor NTP "); Serial.print(NTP_SERVER); Serial.print(" com TimeZone "); Serial.println(TZ_INFO); configTime(0, 0, NTP_SERVER); setenv("TZ", TZ_INFO, 1); tzset(); if (getNTPtime(10)) { // wait up to 10sec to sync Serial.println("NTP Server sincronizado"); } else { Serial.println("\nTimer interno não foi sincronizado"); //ESP.restart(); } // Define o HostName para o servidor web para facilitar o acesso na rede local // sem conhecer o IP previamente Serial.print("Adicionando " + String(studioID) + " no MDNS... "); if (setDNSNAME(studioID)) { Serial.println("adicionado corretamente no MDNS!"); } else { Serial.println("Erro ao adicionar no MDNS!"); } } // Pega a hora do startup time(&startup); localtime(&startup); // Atende a requisição / sv.on("/", HTTP_GET, [](AsyncWebServerRequest * request) { // Responde a Página Principal mostrando as opções para a Interface MIDI displayRequest(request); htmlResposta = ""; request->send_P(200, "text/html", index_html, processor); }); // Atende a requisição /lighton sv.on("/lighton", HTTP_GET, [](AsyncWebServerRequest * request) { // Responde a requisição de ligar a luz de estúdio displayRequest(request); lightON("Web"); htmlResposta = ""; request->send_P(200, "text/html", index_html, processor); }); // Atende a requisição /lighton sv.on("/lightoff", HTTP_GET, [](AsyncWebServerRequest * request) { // Responde a requisição de ligar a luz de estúdio displayRequest(request); lightOFF("Web"); htmlResposta = ""; request->send_P(200, "text/html", index_html, processor); }); // Atende a requisição /status sv.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) { // Atende a requisição para mostrar a data/hora do servidor displayRequest(request); char tempo[30]; strftime(tempo, 30, "%d/%m/%Y %T", localtime(&startup)); IPAddress ip = WiFi.localIP(); String resposta = "<b>Inicialização</b> = " + String(tempo) + "<br>" + "<b>Data/Hora</b> = " + getTimeStamp() + "<br>" + "<b>Versão</b>=" + myVersion + "<br>" + "<b>SSID</b>=" + WiFi.SSID() + "<br>" + "<b>IP</b>=" + ip.toString() + "<br>" + "<b>MAC</b>=" + WiFi.macAddress() + "<br>" + "<b>DB</b>=" + WiFi.RSSI(); htmlResposta = textToHtml(resposta); request->send_P(200, "text/html", index_html, processor); }); // Atende a requisição /database sv.on("/database", HTTP_GET, [](AsyncWebServerRequest * request) { // Responde a Página Principal mostrando as opções para a Interface MIDI displayRequest(request); htmlResposta = ""; request->send_P(200, "text/html", database_html, processor); }); // Configuração da rota para lidar com solicitações POST de atualização de software sv.on("/commit", HTTP_POST, [](AsyncWebServerRequest *request) { // Responde a Requisição de /update displayRequest(request); // Verifica a existência dos parâmetros do FORM if(request->hasParam("softwareName", true) && request->hasParam("ligarBytes", true) && request->hasParam("desligarBytes", true)) { // Obtém os parâmetros do Form de SW String softwareName = request->getParam("softwareName", true)->value(); String lightOn = request->getParam("ligarBytes", true)->value(); String lightOff= request->getParam("desligarBytes", true)->value(); // Vamos pesquisar na base de dados. Se existir atualizaremos e // senão existir inseriremos JsonArray softwares = dbSW["softwares"]; bool achou=false; for (JsonObject software : softwares) { if (softwareName.equalsIgnoreCase(software["nome"].as<String>())) { // Existe logo vamos atualizar software["lightOn"] = lightOn; software["lightOff"] = lightOff; htmlResposta = textToHtml("<b>Sucesso</b>: Software atualizado com sucesso!"); achou=true; break; } } // Verifica se não achou para inserir if (!achou) { // Não Existe logo vamos inserir JsonObject novoSoftware = softwares.createNestedObject(); // Definir os valores para o novo software novoSoftware["nome"] = softwareName; novoSoftware["lightOn"] = lightOn; novoSoftware["lightOff"] = lightOff; htmlResposta = textToHtml("<b>Sucesso</b>: Software inserido com sucesso!"); } // Salvar a base de dados saveConfigFile(); } else { htmlResposta = textToHtml("<b>Erro</b>: Parâmetros do Software inválidos"); } request->send_P(200, "text/html", database_html, processor); }); // Configuração da rota para lidar com solicitações POST de exclusão de software sv.on("/delete", HTTP_POST, [](AsyncWebServerRequest *request) { // Responde a Requisição de /delete displayRequest(request); // Verifica a existência dos parâmetros do FORM if(request->hasParam("softwareName", true)) { // Obtém os parâmetros do Form de SW String softwareName = request->getParam("softwareName", true)->value(); // Vamos pesquisar na base de dados. Se existir deletaremos JsonArray softwares = dbSW["softwares"]; bool achou=false; for (size_t i = 0; i < softwares.size(); i++) { // Obter o objeto de software atual JsonObject software = softwares[i].as<JsonObject>(); if (softwareName.equalsIgnoreCase(software["nome"].as<String>())) { // Existe logo posso deletar softwares.remove(i); // Salvar a base de dados no SPIFFS saveConfigFile(); achou = true; break; } } if (achou) { htmlResposta = textToHtml("<b>Sucesso</b>: Software deletado com sucesso!"); } else { htmlResposta = textToHtml("<b>Erro</b>: Software não encontrado!"); } } else { htmlResposta = textToHtml("<b>Erro</b>: Parâmetros do Software inválidos"); } request->send_P(200, "text/html", database_html, processor); }); // Atende a uma requisição não existente sv.onNotFound(nao_encontrado); // Adicionar rota para obter a lista de softwares sv.on("/softwares", HTTP_GET, handleGetSoftwares); // Adicionar rota para obter os detalhes do software selecionado sv.on("/software", HTTP_GET, handleGetSoftwareDetails); // Inicializa o WebSocket Event para conectar/desconectar clientes de streaming ws.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { // Registra na Console a conexão e desconexão de clientes socket's if (type == WS_EVT_CONNECT) { Serial.print("Cliente Socket conectado no IP "); Serial.println(client->remoteIP()); } else if (type == WS_EVT_DISCONNECT) { Serial.print("Cliente Socket desconectado do IP "); Serial.println(client->remoteIP()); } }); // Adiciona o manipulador de WebSocket ao servidor sv.addHandler(&ws); // Credenciais para atualizações via OTA ElegantOTA.setAuth(user_OTA,pass_OTA); // Habilita/Desabilita AutoRebbot após a atualização ElegantOTA.setAutoReboot(autoRebootOTA); // Inicia o OTA para atualização via Web ElegantOTA.begin(&sv); // Inicia o servidor propriamente dito sv.begin(); // Monta o SSID do modo AP para permitir a configuração sprintf(ssid_config, "ESP32_%X",(uint32_t)ESP.getEfuseMac()); // Hello na Console Serial.print("\nGeneric MIDI - Recording Light "); Serial.println(myVersion); Serial.println("By Dailton Menezes and Alberto Menezes"); Serial.print("SSID do Modo de Configuração: "); Serial.println(ssid_config); Serial.print("Nome do Estúdio: "); Serial.println(studioID); Serial.print("Timeout da Interface MIDI (ms): "); Serial.println(intervaloTimeout); } //--------------------------------------------- // Loop principal esperando as interfaces MIDI // Configuração do WifiManager ou OTA //--------------------------------------------- void loop() { //-------------------------------------------------------------- // Função 1 : tenta receber todos os bytes da interface MIDI // no buffer para posterior análise //-------------------------------------------------------------- // Verifica a interface MIDI while (Serial1.available() > 0) { // Verifica se deve inserir uma vírgula if (stack.length()>0) stack += ","; // Lê o byte disponível e empilha stack += String(Serial1.read()); lastRecebido = millis(); } //-------------------------------------------------------------- // Função 2 : Verifica se ocorreu timeout para analisar o buffer // como um todo //-------------------------------------------------------------- if (millis()-lastRecebido>intervaloTimeout) { // Verifica se tem bytes no buffer if (stack.length()>0) { // Mostra a stack na console Serial.print("Stack: "); Serial.println(stack); // Verifica as sequências dos softwares LogicPro/ProTools/Reaper String nomeSW; JsonArray softwares = dbSW["softwares"]; for (JsonObject software : softwares) { nomeSW = software["nome"].as<String>(); String lightOn = software["lightOn"].as<String>(); String lightOff = software["lightOff"].as<String>(); // Serial.print("SW : "); Serial.println(nomeSW); // Serial.print("ON : "); Serial.println(lightOn); // Serial.print("OFF:"); Serial.println(lightOff); if (stack.indexOf(lightOn)>=0 && !atual_estado_light) { lightON(nomeSW); break; } else if (stack.indexOf(lightOff)>=0 && atual_estado_light) { lightOFF(nomeSW); break; } else nomeSW = "desconhecido"; } // Envia os bytes via WebSocket para todos os clientes conectados String parte1 = String("<p>Luz de ") + "<a href=\"/update\" title=\"Click para fazer update do código\">" + String(studioID) + "</a>" + " : <b>" + colorirLuz(atual_estado_light) + "</b></p>"; String parte2 = "<b>Data/Hora</b>: " + getTimeStamp() + "<br>" + "<b>Software</b>: " + nomeSW + "<br>" + "<b>Sequência</b>:" + stack; ws.textAll(parte1 + ";" + parte2); } resetaBuffer(); } //-------------------------------------------------------------- // Função 3 : Verifica se está sem internet para piscar o Led // e/ou tentar a reconexão //-------------------------------------------------------------- if (WiFi.status() != WL_CONNECTED) { // Verifica se deve atualizar o Led Interno if (millis()-lastLed > LEDDELAY) { toggleLed(); } // Verifica se deve tentar a reconexão da Internet if (millis()-lastReconexao > TIMERECONEXAO) { wm.autoConnect(); lastReconexao = millis(); } } //-------------------------------------------------------------------------------------------------- // Bloco 4 : Verifica se o botão de BOOT foi apertado para forçar a entrada no modo de configuração. // É útil quando a senha do wifi mudou ou está se conectando em outra rede wifi. Isso // evita ter o SSID/senha no código, a recompilação e upload do código no ESP32. //-------------------------------------------------------------------------------------------------- if (buttonState) { // Reseta o estado do botão buttonState = false; // Garante que a Luz do Estúdio fique apagada durante a reconfiguração por medida de economia lightOFF("Config"); // Força a entrada em modo de configuração wm.resetSettings(); ESP.restart(); } //-------------------------------------------------------------- // Função 5 : checa o OTA para saber se há atualização //-------------------------------------------------------------- ElegantOTA.loop(); } //-------------------------------------- // Liga a Lâmpada de Estúdio/Led Interno //-------------------------------------- void lightON(String sw) { Serial.print("Software["); Serial.print(sw); Serial.print("]: "); Serial.println("ON"); digitalWrite(pin_rele, RELE_ON); atual_estado_light = true; } //----------------------------------------- // Desliga a Lâmpada de Estúdio/Led Interno //----------------------------------------- void lightOFF(String sw) { Serial.print("Software["); Serial.print(sw); Serial.print("]: "); Serial.println("OFF"); digitalWrite(pin_rele, RELE_OFF); atual_estado_light = false; } //----------------------------------------- // Reseta o controle do buffer da interface // MIDI para receber nova sequência //----------------------------------------- void resetaBuffer() { stack=""; lastRecebido = millis(); } //------------------------------------------------ // Persiste CPUID e Intervalo no SPIFFS //------------------------------------------------ void saveConfigFile() // O arquivo de Config é salvo no formato JSON { Serial.println(F("Persistindo a configuração...")); // Atualiza a base de software e parâmetros gerais dbSW["nomeEstudio"] = studioID; dbSW["intervaloTimeout"] = intervaloTimeout; dbSW["usuarioOTA"] = user_OTA; dbSW["senhaOTA"] = pass_OTA; dbSW["autorebootOTA"] = autoRebootOTA; // Abre o arquivo de configuração File configFile = SPIFFS.open(JSON_CONFIG_FILE, "w"); if (!configFile) { // Erro, arquino não foi aberto Serial.println("Erro ao persistir a configuração"); } // Serializa os dados do JSON no arquivo serializeJsonPretty(dbSW, Serial); Serial.println(); if (serializeJson(dbSW, configFile) == 0) { // Erro ai gravar o arquivo Serial.println(F("Erro ao gravar o arquivo de configuração")); } // Fecha o Arquivo configFile.close(); } //------------------------------------------------ // Recupera CPUID e Intervalo do SPIFFS //------------------------------------------------ bool loadConfigFile() // Carrega o arquivo de Configuração { // Verifica se o SPIFFS já foi inicializado if (!SPIFFS.begin(true)) { SPIFFS.format(); Serial.println("Sistema de Arquivo no SPIFFS foi formatado"); } // Lê as configurações no formato JSON Serial.println("Montando o FileSystem..."); // Força a entrada na primeira vez if (SPIFFS.begin(true)) { Serial.println("FileSystem montado..."); //Serial.println("Removendo o arquivo de configuração..."); //SPIFFS.remove(JSON_CONFIG_FILE); if (SPIFFS.exists(JSON_CONFIG_FILE)) { // o arquivo existe, vamos ler Serial.println("Lendo o arquivo de configuração"); File configFile = SPIFFS.open(JSON_CONFIG_FILE, "r"); if (configFile) { Serial.println("Arquivo de configuração aberto..."); DeserializationError error = deserializeJson(dbSW, configFile); if (!error) { Serial.println("JSON do SPIFFS recuperado..."); serializeJsonPretty(dbSW, Serial); Serial.println(); if (dbSW.containsKey("nomeEstudio")) strcpy(studioID, dbSW["nomeEstudio"]); else strcpy(studioID, "STUDIO"); if (dbSW.containsKey("intervaloTimeout")) intervaloTimeout = dbSW["intervaloTimeout"].as<int>(); else { intervaloTimeout = 1000; strcpy(valTimeout,"1000"); } if (dbSW.containsKey("usuarioOTA")) strcpy(user_OTA, dbSW["usuarioOTA"]); else strcpy(user_OTA, USER_UPDATE); if (dbSW.containsKey("senhaOTA")) strcpy(pass_OTA, dbSW["senhaOTA"]); else strcpy(pass_OTA, PASS_UPDATE); if (dbSW.containsKey("autorebootOTA")) { autoRebootOTA = dbSW["autorebootOTA"]; if (autoRebootOTA) strcpy(val_autoreboot,"1"); else strcpy(val_autoreboot,"0"); } else { autoRebootOTA = true; strcpy(val_autoreboot,"1"); } return true; } else { // Erro ao ler o JSON Serial.println("Erro ao carregar o JSON da configuração..."); } } } else { // Monta base default DeserializationError error = deserializeJson(dbSW, dbDefault); // Verificar se há erro no parsing if (!error) { Serial.println("JSON default recuperado..."); serializeJsonPretty(dbSW, Serial); Serial.println(); strcpy(studioID, dbSW["nomeEstudio"]); intervaloTimeout = dbSW["intervaloTimeout"].as<int>(); // Salva o default no SPIFFS saveConfigFile(); return true; } else { // Erro ao ler o JSON Serial.println("Erro ao carregar o JSON da configuração..."); } } } else { // Erro ao montar o FileSystem Serial.println("Erro ao montar o FileSystem"); } return false; } //---------------------------------------------------------- // Callback para informação do processo de configuração WiFi //---------------------------------------------------------- void saveConfigCallback() // Callback para nos lembrar de salvar o arquivo de configuração { Serial.println("Persistência necessária..."); shouldSaveConfig = true; } //---------------------------------------------------------- // Callback para WifiManager //---------------------------------------------------------- void configModeCallback(WiFiManager *myWiFiManager) // É chamado no modo de configuração { Serial.println("Entrando no modo de configuração..."); Serial.print("Config SSID: "); Serial.println(myWiFiManager->getConfigPortalSSID()); Serial.print("Config IP Address: "); Serial.println(WiFi.softAPIP()); } //--------------------------------------------------------- // Sincroniza o horário do ESP32 com NTP server brasileiro //--------------------------------------------------------- bool getNTPtime(int sec) { { uint32_t start = millis(); tm timeinfo; time_t now; int cont=0; do { time(&now); localtime_r(&now, &timeinfo); if (++cont % 80 == 0) Serial.println(); else Serial.print("."); delay(10); } while (((millis() - start) <= (1000 * sec)) && (timeinfo.tm_year < (2016 - 1900))); if (timeinfo.tm_year <= (2016 - 1900)) return false; // the NTP call was not successful Serial.print("\nnow "); Serial.println(now); Serial.print("Time "); Serial.println(getTimeStamp()); } return true; } //------------------------------------------------ // Evento chamado no processo de conexão do Wifi //------------------------------------------------ void WiFiEvent(WiFiEvent_t event) { Serial.printf("[Evento Wi-Fi] evento: %d\n", event); switch (event) { case SYSTEM_EVENT_WIFI_READY: Serial.println("interface WiFi pronta"); break; case SYSTEM_EVENT_SCAN_DONE: Serial.println("Pesquisa por AP completada"); break; case SYSTEM_EVENT_STA_START: Serial.println("Cliente WiFi iniciado"); break; case SYSTEM_EVENT_STA_STOP: Serial.println("Clientes WiFi cancelados"); break; case SYSTEM_EVENT_STA_CONNECTED: Serial.println("Conectado ao AP"); digitalWrite(LED_BUILTIN, HIGH); break; case SYSTEM_EVENT_STA_DISCONNECTED: Serial.println("Desconectado do AP WiFi"); setLedState(false); break; case SYSTEM_EVENT_STA_AUTHMODE_CHANGE: Serial.println("Modo de Autenticação do AP mudou"); break; case SYSTEM_EVENT_STA_GOT_IP: Serial.print("Endereço IP obtido: "); Serial.println(WiFi.localIP()); break; case SYSTEM_EVENT_STA_LOST_IP: Serial.println("Endereço IP perdido e foi resetado para 0"); break; case SYSTEM_EVENT_STA_WPS_ER_SUCCESS: Serial.println("WPS: modo enrollee bem sucedido"); break; case SYSTEM_EVENT_STA_WPS_ER_FAILED: Serial.println("WPS: modo enrollee falhou"); break; case SYSTEM_EVENT_STA_WPS_ER_TIMEOUT: Serial.println("WPS: timeout no modo enrollee"); break; case SYSTEM_EVENT_STA_WPS_ER_PIN: Serial.println("WPS: pin code no modo enrollee"); break; case SYSTEM_EVENT_AP_START: Serial.println("AP Wifi Iniciado"); break; case SYSTEM_EVENT_AP_STOP: Serial.println("AP Wifi parado"); break; case SYSTEM_EVENT_AP_STACONNECTED: Serial.println("Cliente conectado"); break; case SYSTEM_EVENT_AP_STADISCONNECTED: Serial.println("Cliente desconectado"); break; case SYSTEM_EVENT_AP_STAIPASSIGNED: Serial.println("IP associado ao Cliente"); break; case SYSTEM_EVENT_AP_PROBEREQRECVED: Serial.println("Requisição de probe recebida"); break; case SYSTEM_EVENT_GOT_IP6: Serial.println("IPv6 é preferencial"); break; case SYSTEM_EVENT_ETH_START: Serial.println("Interface Ethernet iniciada"); break; case SYSTEM_EVENT_ETH_STOP: Serial.println("Interface Ethernet parada"); break; case SYSTEM_EVENT_ETH_CONNECTED: Serial.println("Interface Ethernet conectada"); break; case SYSTEM_EVENT_ETH_DISCONNECTED: Serial.println("Interface Ethernet desconectada"); break; case SYSTEM_EVENT_ETH_GOT_IP: Serial.println("Endereço IP obtido"); break; default: break; } } //---------------------------------------------------- // Inicialização/Configuração do WiFi Manager no ESP32 //---------------------------------------------------- void Check_WiFiManager(bool forceConfig) { // Tenta carregar os parâmetros do SPIFFS bool spiffsSetup = loadConfigFile(); if (!spiffsSetup) { Serial.println(F("Forçando o modo de configuração...")); forceConfig = true; } // Copia os campos para o FORM do WifiManager custom_studioID.setValue(studioID, MAX_EDIT_LEN+1); custom_intervaloTimeout.setValue(String(intervaloTimeout).c_str(), MAX_NUM_LEN+1); custom_user_ota.setValue(user_OTA, MAX_EDIT_LEN+1); custom_pass_ota.setValue(pass_OTA, MAX_EDIT_LEN+1); custom_autoreboot_ota.setValue(val_autoreboot,sizeof(val_autoreboot)); if (forceConfig) { // Apaga o Led Interno setLedState(false); // reseta configurações wm.resetSettings(); // Define o modo AP WiFi.mode(WIFI_STA); // Entra no modo de AP de configuração ... com senha fixa if (!wm.startConfigPortal(ssid_config, pass_config)) { Serial.println("Erro na conexão com timeout no modo AP..."); //setStateWifiEEPROM(true); } //else setStateWifiEEPROM(false); } else { // Entra no modo de conexão normal recuperando o SSID/Senha anteriores if (!wm.autoConnect()) { Serial.println("Erro na conexão com timeout..."); } //setStateWifiEEPROM(false); } // Recupera o campo cpuId preenchido na interface do WifiManager strncpy(studioID, custom_studioID.getValue(), sizeof(studioID)); if (strlen(studioID)==0) strcpy(studioID,"ESP32_ALIEN"); Serial.print("studioID: "); Serial.println(studioID); // Recupera o campo intervaloTimer do WifiManager preenchido na interface convertendo para inteiro intervaloTimeout = atoi(custom_intervaloTimeout.getValue()); if (intervaloTimeout < 50) intervaloTimeout = TIMEOUT; Serial.print("intervaloTimeout: "); Serial.println(intervaloTimeout); // Recupera o campo usuário da Atualização do WifiManager strncpy(user_OTA, custom_user_ota.getValue(), sizeof(user_OTA)); Serial.print("User_OTA: "); Serial.println(user_OTA); // Recupera o campo senha da Atualização do WifiManager strncpy(pass_OTA, custom_pass_ota.getValue(), sizeof(pass_OTA)); Serial.print("Pass_OTA: "); Serial.println(pass_OTA); // Recupera o campo AutoReboot da Atualização do WifiManager strncpy(val_autoreboot, custom_autoreboot_ota.getValue(), sizeof(val_autoreboot)); Serial.print("AutoReboot_OTA: "); Serial.println(val_autoreboot); autoRebootOTA = (strcmp(val_autoreboot, "1") == 0) ? true : false; // Salva os parâmetros no FileSystem FLASH -> não perde quando desligado if (shouldSaveConfig) { saveConfigFile(); } } //---------------------------------------------------- // Mostra as informações da requisição http na console //---------------------------------------------------- void displayRequest(AsyncWebServerRequest *request) { Serial.print("Método: "); Serial.print(request->methodToString()); Serial.print("\t| URL: "); Serial.print(request->url()); Serial.print("\t| IP: "); Serial.println(request->client()->remoteIP()); } //------------------------------------------------ // Expande a Página Principal //------------------------------------------------ String processor(const String& var) { if (var.equalsIgnoreCase("BUTTONPLACEHOLDER") && htmlResposta !="") { String resp = htmlResposta; htmlResposta = ""; return resp; } else if (var.equalsIgnoreCase("nomestudio")) { String linkUpdate ="<a href=\"/update\" title=\"Click para fazer update do código\">" + String(studioID) + "</a>"; return linkUpdate; } else if (var.equalsIgnoreCase("luzstudio")) { return colorirLuz(atual_estado_light); } return String(); } //------------------------------------------------------- // Dar a cor adequada ao Estado da Luz de Estúdio no HTML //------------------------------------------------------- String colorirLuz(bool estado) { String result = "<span style=\"color:"; if (!estado) result += "red"; else result += "green"; result += "\">"; result += estadoLight[estado]; result += "</span>"; return result; } //------------------------------------------------ // Devolve o localtime dd/mm/aaaa hh:mm:ss //------------------------------------------------ String getTimeStamp() { time_t now; time(&now); char timestamp[30]; strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&now)); return String(timestamp); } //------------------------------------------------ // Responde a URL inválida //------------------------------------------------ void nao_encontrado(AsyncWebServerRequest *request) { // Sub-rotina para caso seja retornado um erro Serial.println("Requisição não encontrada"); displayRequest(request); // Retorna a mensagem de erro em caso de um retorno 404 request->send(404, "text/html", "<h1>Erro: Não encontrado</h1>"); } //------------------------------------------------ // Encapsula o texto em html //------------------------------------------------ String textToHtml(String texto) { return texto + "\n"; } //-------------------------------------------------- // Rotina de Tratamento da Interrupção do Botão Boot //-------------------------------------------------- void buttonISR() { buttonState = true; } //------------------------------------------------ // Handle para preencher o selectlist //------------------------------------------------ void handleGetSoftwares(AsyncWebServerRequest *request) { displayRequest(request); JsonArray softwares = dbSW["softwares"]; AsyncResponseStream *response = request->beginResponseStream("application/json"); serializeJson(softwares, *response); request->send(response); } //------------------------------------------------ // Handle para devolver o detalhe de um software //------------------------------------------------ void handleGetSoftwareDetails(AsyncWebServerRequest *request) { displayRequest(request); String softwareName = request->url().substring(10); // Remove '/software/' from the URL softwareName.replace("%20", " "); // Replace '%20' with space if any if (!softwareName.isEmpty()) { JsonArray softwares = dbSW["softwares"]; for (JsonVariant software : softwares) { if (softwareName.equalsIgnoreCase(software["nome"].as<String>())) { AsyncResponseStream *response = request->beginResponseStream("application/json"); serializeJson(software, *response); request->send(response); return; } } } request->send(404, "application/json", "{\"error\": \"Software não encontrado\"}"); } //------------------------------------------------ // Alterna o estado do Led Interno por 1 seg //------------------------------------------------ void toggleLed() { setLedState(!ledState); } //------------------------------------------------ // Seta o estado do led para um determinado valor //------------------------------------------------ void setLedState(bool state) { ledState = state; digitalWrite(pin_led,ledState); lastLed = millis(); } //------------------------------------------------- // Função de interrupção de recebimento de dados na // porta serial 1 //------------------------------------------------- void IRAM_ATTR Serial1_RX_ISR() { // Lê o estado do pino RX byte receivedByte = digitalRead(pin_md_rx); // Verifica se deve inserir uma vírgula no buffer if (stack.length()>0) stack += ","; // Empilha o byte recebido no buffer stack += String(receivedByte); lastRecebido = millis(); } //------------------------------------------------------- // Define o HostName como DNS NAME //------------------------------------------------------- bool setDNSNAME(String nome) { WiFi.setHostname(nome.c_str()); bool ok = MDNS.begin(nome.c_str()); if (ok) { MDNS.addService("http", "tcp", 80); MDNS.setInstanceName(nome.c_str()); // Adicionar o nome da instância } return ok; }
|
|
A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.
Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!