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.
Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!