IoT

Luz de Gravação Para Home Studio

Eletrogate 29 de novembro de 2024

Introdução

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!


Materiais Necessários Para o Projeto

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

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)


Função

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:

Blog-Eletrogate-Logic1

Blog-Eletrogate-Logic2

 

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:

 

Blog-Eletrogate-ProTools1

Blog-Eletrogate-ProTools2

 

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
}

Motivação

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.


Objetivos Específicos

 

  1. Receber a sequência de caracteres enviados pelos softwares Pro Tools, Logic Pro X e Reaper e acionar o Relé para ligar ou desligar a luz de gravação do estúdio. A sequência recebida é enviada ao Navegador para atualizar a interface do usuário via WebSocket.
  2. Implementar um servidor HTTP na porta 80 para receber os seguintes comandos através de um navegador:
    1. / ⮕ para mostrar o Menu Principal e suas opções
    2. /lighton ⮕ para acender a luz de estúdio
    3. /lightoff ⮕ para desligar a luz de estúdio
    4. /status ⮕ para mostrar as informações do servidor
    5. /database ⮕ para mostrar a base de softwares
    6. /commit ⮕ para atualizar ou inserir software
    7. /delete ⮕ para deletar um software
    8. /softwares ⮕ para mostrar a lista de softwares
    9. /software ⮕ para mostrar os detalhes de um software
    10. /update⮕ para atualizar o firmware via OTA
  3. Pressionando-se o botão BOOT do ESP32 (não o botão de RESET) 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:
    1. 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 (Figura 7 – Reconhecimento do ESP32 no Modo AP).
    2. Conectar-se no ESP32_AP_nnnnnn onde ficará conectado, porém sem internet (Figura 8 – Conectado no Modo AP de Configuração)
    3. Ir para a URL http://192.168.4.1:8080
    4. Pressionar o botão Configure WiFi (Figura 9 – Tela Principal de Configuração)
    5. Informar o SSID, a senha, o nome do Estúdio (até 15 bytes), tempo de timeout da interface MIDI (1000 ms é o default), senha (até 15 bytes) para atualizações de firmware e o autoreboot (0 ou 1). Veja Figura 10 – Parâmetros a serem definidos.
    6. 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 correr tudo bem.
  4. Fazer a sincronização do Timer interno do ESP32 com o site NTP Oficial do Brasil.
  5. 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) pode não aceitar.
  6. Permitir a autoatualização do programa (firmware) para novas versões a partir do arquivo .bin gerado pelo ambiente IDE do Arduino sem a necessidade de conexão física entre a placa ESP32 e o computador contendo o compilador (ambiente de desenvolvimento). Este tipo de atualização é chamado de OTA ou Over-The-Air.

Detalhes da Implementação

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

 


Simulação de um Software DAW

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


Código do Simulador DAW

//-------------------------------------------------------------------------------------------
// 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);         
      } 
   }
}

 


Código da Aplicação Principal

//---------------------------------------------------------------------------------------------
// 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&atilde;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;
}

 


Demonstração

 


Sobre o Autor


Alberto de Almeida Menezes
tinho.menezes@gmail.com

Bacharel em Engenharia de Áudio e Produção Musical pela Berklee College of Music.


Dailton de Oliveira Menezes
dailton.menezes@gmail.com

Bacharel em Ciência da Computação pela Universidade Federal de Minas Gerais.


Eletrogate

29 de novembro de 2024

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!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!