Software Advanced - pion-labs/pion-educational-kits GitHub Wiki
Como uma biblioteca foi desenvolvida para o uso do seu Kit Educacional PION, há alguns conceitos que foram abstraídos para facilitar essa utilização. Dentre esses conceitos, encontram-se algumas chamadas de funções e conceitos que podem auxiliar usuários mais avançados em sua programação.
Em todos os exemplos de uso da biblioteca a classe PION_System.h foi utilizada, no entanto, essa classe possui declarações de outras classes que também controlam outras funcionalidades do pelo usuário podem ser declaradas separadamente ou entendidas separadamente como as classes:
Classe | Arquivo |
---|---|
System | PION_System.h |
Storage | PION_Storage.h |
Sensors | PION_Sensors.h |
Interface | PION_Interface.h |
Network | PION_Network.h |
Cada uma dessas classes possui suas funções próprias que estão relacionadas a funcionalidade que esse arquivo representa, dentre essas funções algumas podem ser sobrescritas pelo usuário para adicionar ou remover métodos de acordo com o que é necessitado.
A classe de conexão WiFi ou Network
possui três funções que podem ser sobrescritas, para modificar a funcionalidade de conexão ou envio de dados:
- void networkConnect()
- void serverResponse()
- void sendDataWs()
- void handleWebSocketMessage(void *, uint8_t *, size_t)
Para modificar o modo como a WiFi do kit se comporta você pode sobrescrever a função de conexão wifi que originalmente funciona como um WiFi Access Point
, ou seja, cria uma WiFi com o nome PION Satélite xxxxxx. Você pode por exemplo modificar o modo para que o seu Kit se conecte a outra WiFi.
// Função original
void networkConnect(){
// Transforma o serial number do sistema em uma String
String id = String(System::getSerialNumber());
// Cria uma nova String com o nome PION Satélite + o serial
String ssid = baseName + id;
// Adapta a string para um array de caracteres
ssid.toCharArray(ssidId, sizeof(ssidId));
// Para qualquer aplicação bluetooth que possa existir
btStop();
// Inicializa um WiFi Access Point com um nome + serial e a senha padrão
WiFi.mode(WIFI_AP);
WiFi.softAP(ssidId, password);
WiFi.setHostname("PION Satelite");
}
// Modificação para conectar a uma wifi
void networkConnect(){
// Para qualquer aplicação bluetooth que possa existir
btStop();
// Começa WiFi se conectando ao SSID fornecido com a senha fornecida
WiFi.begin("NomeDaSuaWiFi", "senhadaSuaWiFi");
// Espera o Status de conectado
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// Exibe via serial o endereço IP do seu kit na rede
Serial.println("");
Serial.println("WiFi conectada");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
Para modificar a página web que é mostrada ao se acessar o endereço IP do kit em 192.168.4.1
.
// Função original
void serverResponse(){
// Define qual a resposta do servidor a um acesso ao endereço raiz em 192.168.4.1
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
// Envia o dashboard de dados PION para o usuário
AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", DASH_HTML, DASH_HTML_SIZE);
response->addHeader("Content-Encoding","gzip");
request->send(response);
});
}
// Modificação para servir uma página html
void serverResponse(){
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", "<html>\
<head>\
<meta http-equiv='refresh' content='5'/>\
<title>PION Kit HTML</title>\
<style>\
body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
</style>\
</head>\
<body>\
<h1>Hello from ESP32!</h1>\
</body>\
</html>");
});
}
O Dashboard do seu kit PION utiliza um WebSocket para fazer a conexão entre os dados gerados pelos sensores e atuadores e a aplicação web que os torna visiveis. A Função que organiza todos os dados em um JSON e os envia pelo WebSocket também pode ser sobrescrita para enviar dados diferentes ou o que o usuário achar necessário.
A biblioteca ArduinoJson é utilizada para facilitar a utilização dos dados no dashboard de dados.
O WebSocket utilizado aqui pode também ser utilizado como um código externo como por exemplo um código em python para recever os dados, clique aqui para saber mais!
// Função original
void sendDataWs() {
// Faz a alocação de 512 Bytes para o JSON
DynamicJsonDocument jsonBuffer(512);
// Adiciona todas as leituras necessárias com o formato chave:valor do JSON
jsonBuffer["bateria"] = System::battery;
jsonBuffer["pressao"] = Sensors::pressure;
jsonBuffer["temperatura"] = Sensors::temperature;
jsonBuffer["humidade"] = Sensors::humidity;
jsonBuffer["co2"] = Sensors::CO2Level;
jsonBuffer["luminosidade"] = Sensors::luminosity;
jsonBuffer["sdCard"] = sdStatusMessage[Storage::sdStatus];
jsonBuffer["siren"] = sirenMessage[Interface::sirenAction];
// Adiciona as leituras a um array dentro do JSON
JsonArray accel = jsonBuffer.createNestedArray("acelerometro");
accel.add(Sensors::accel[0]);
accel.add(Sensors::accel[1]);
accel.add(Sensors::accel[2]);
JsonArray gyro = jsonBuffer.createNestedArray("giroscopio");
gyro.add(Sensors::gyro[0]);
gyro.add(Sensors::gyro[1]);
gyro.add(Sensors::gyro[2]);
JsonArray mag = jsonBuffer.createNestedArray("magnetometro");
mag.add(Sensors::mag[0]);
mag.add(Sensors::mag[1]);
mag.add(Sensors::mag[2]);
// Mede o tamanho do buffer do JSON
size_t len = measureJson(jsonBuffer);
// Cria um espaço na RAM de (len + 1)
AsyncWebSocketMessageBuffer * buffer = ws.makeBuffer(len);
if (buffer) {
// Transforma o JSON em um grande texto e o coloca no espaço criado anteriormente
serializeJson(jsonBuffer,(char *)buffer->get(), len + 1);
// Envia pelo WebSocket para todos os usuários
ws.textAll(buffer);
}
}
// Modificado para enviar uma nova variável
void sendDataWs() {
// Faz a alocação de 512 Bytes para o JSON
DynamicJsonDocument jsonBuffer(512);
// Adiciona todas as leituras necessárias com o formato chave:valor do JSON
jsonBuffer["bateria"] = System::battery;
jsonBuffer["pressao"] = Sensors::pressure;
jsonBuffer["temperatura"] = Sensors::temperature;
jsonBuffer["humidade"] = Sensors::humidity;
jsonBuffer["co2"] = Sensors::CO2Level;
jsonBuffer["luminosidade"] = Sensors::luminosity;
jsonBuffer["sdCard"] = sdStatusMessage[Storage::sdStatus];
jsonBuffer["siren"] = sirenMessage[Interface::sirenAction];
// Novo dado
jsonBuffer["dadoExtra'] = 200;
// Adiciona as leituras a um array dentro do JSON
JsonArray accel = jsonBuffer.createNestedArray("acelerometro");
accel.add(Sensors::accel[0]);
accel.add(Sensors::accel[1]);
accel.add(Sensors::accel[2]);
JsonArray gyro = jsonBuffer.createNestedArray("giroscopio");
gyro.add(Sensors::gyro[0]);
gyro.add(Sensors::gyro[1]);
gyro.add(Sensors::gyro[2]);
JsonArray mag = jsonBuffer.createNestedArray("magnetometro");
mag.add(Sensors::mag[0]);
mag.add(Sensors::mag[1]);
mag.add(Sensors::mag[2]);
// Mede o tamanho do buffer do JSON
size_t len = measureJson(jsonBuffer);
// Cria um espaço na RAM de (len + 1)
AsyncWebSocketMessageBuffer * buffer = ws.makeBuffer(len);
if (buffer) {
// Transforma o JSON em um grande texto e o coloca no espaço criado anteriormente
serializeJson(jsonBuffer,(char *)buffer->get(), len + 1);
// Envia pelo WebSocket para todos os usuários
ws.textAll(buffer);
}
}
O mesmo WebSocket utilizado pela Dashboard do seu kit PION para fazer a conexão entre os dados gerados pelos sensores e a aplicação web também é utilizado para conectar os botões de ação do Dashboard a funções que o seu Kit pode executar. Essas funções recebem o evento de clique do botão com seu nome específico e são controladas pela função a seguir:
// Função Original
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
// Armazena todas as informações recebidas pelo WebSocket
AwsFrameInfo *info = (AwsFrameInfo*)arg;
// Se o comando recebido for um texto compare
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
// Testa o texto recebido para diversas funções
if (strcmp((char*)data, "toggleRGB") == 0) {
Interface::shouldChangeRGB = true;
} else if (strcmp((char*)data, "toggleLED") == 0) {
Interface::shouldChangeLed = true;
} else if (strcmp((char*)data, "toggleSiren") == 0) {
Interface::toggleSiren();
} else if (strcmp((char*)data, "toggleSD1") == 0) {
Storage::toggleSD(0);
} else if (strcmp((char*)data, "toggleSD2") == 0) {
Storage::toggleSD(1);
} else if (strcmp((char*)data, "reboot") == 0) {
ESP.restart();
}
}
}
// Função Modificada
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) {
// Armazena todas as informações recebidas pelo WebSocket
AwsFrameInfo *info = (AwsFrameInfo*)arg;
// Se o comando recebido for um texto compare
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0;
// Testa o texto recebido para diversas funções
if (strcmp((char*)data, "toggleRGB") == 0) {
Interface::shouldChangeRGB = true;
} else if (strcmp((char*)data, "toggleLED") == 0) {
Interface::shouldChangeLed = true;
} else if (strcmp((char*)data, "toggleSiren") == 0) {
Interface::toggleSiren();
} else if (strcmp((char*)data, "toggleSD1") == 0) {
Storage::toggleSD(0);
} else if (strcmp((char*)data, "toggleSD2") == 0) {
Storage::toggleSD(1);
} else if (strcmp((char*)data, "reboot") == 0) {
ESP.restart();
} else if (strcmp((char*)data, "reiniciar") == 0) { // Modificação simples adicionada
ESP.restart();
}
}
}
A classe de armazenamento de dados ou Storage
possui duas funções que podem ser sobrescritas, para modificar a funcionalidade do armazenamento de dados:
- void createFileFirstLine(fs::FS &, const char *)
- void appendFile(fs::FS &, const char *, TickType_t)
O sistema de dados cria sempre um arquivo do tipo CSV para o armazenamento dos dados, a presença de um cabeçalho no arquivo ajuda o usuário a compreender melhor a posição dos dados em seu arquivo e a leitura posterior por softwares como Excel
e Google Sheets
. Essa função pode ser modificada para acomodar dados gerados pelo usuário.
**Atenção: **para sobrescrever essa função também é necessário incluir a biblioteca FS
para o FileSystem com #include "FS.h"
// Função Original
void createFileFirstLine(fs::FS &fs, const char * path){
// Mostra o nome do arquivo
Serial.printf("Escrevendo em: %s\n", path);
//Abre o arquivo do SD para a memória RAM
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Falha ao abrir para escrita");
return;
}
// Cria a primeira linha separada por vírgulas do CSV.
const char * message = "tempo(ms),temperatura(C),umidade(%),pressao(Pa),co2(ppm),luminosidade(%),acelX(m/s2),accelY,acelZ,giroX(graus/s),giroY,giroZ,magX(uT),magY,magZ,bateria(%)";
// Escreve a mensagem criada anteriormente
if(file.println(message)){
Serial.println("Escrita Começou");
} else {
Serial.println("Falha na escrita");
}
// Fecha o arquivo
file.close();
}
// Função Modificada
void createFileFirstLine(fs::FS &fs, const char * path){
// Mostra o nome do arquivo
Serial.printf("Escrevendo em: %s\n", path);
//Abre o arquivo do SD para a memória RAM
File file = fs.open(path, FILE_WRITE);
if(!file){
Serial.println("Falha ao abrir para escrita");
return;
}
// Cria a primeira linha modificada e separada por vírgulas do CSV.
const char * message = "tempo(ms),temperatura(C),umidade(%),co2(ppm),bateria(%)";
// Escreve a mensagem criada anteriormente
if(file.println(message)){
Serial.println("Escrita Começou");
} else {
Serial.println("Falha na escrita");
}
// Fecha o arquivo
file.close();
}
Essa função é utilizada periodicamente para armazenar os dados no cartão SD. Como o aquivo utilizado sempre é um CSV
, os dados sempre devem estar separados por vírgulas e deve haver uma quebra de linha(\n
) no final de cada linha de dados.
Como a vírgula(,) só ocupa 1 byte recomenda-se utilizar file.write(',')
.
O ultimo dado salvo deve conter uma quebra de linha, portanto file.println(dado)
ou file.write('\n')
.
// Função Original
void appendFile(fs::FS &fs, const char * path, TickType_t time){
//Abre o arquivo do SD para a memória RAM
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Falha ao abrir para gravacao");
return;
}
// Salva no CSV o dado, seguido de uma vírgula.
file.print(time);
file.write(',');
file.print(Sensors::temperature);
file.write(',');
file.print(Sensors::humidity);
file.write(',');
file.print(Sensors::pressure);
file.write(',');
file.print(Sensors::CO2Level);
file.write(',');
file.print(Sensors::luminosity);
file.write(',');
file.print(Sensors::accel[0]);
file.write(',');
file.print(Sensors::accel[1]);
file.write(',');
file.print(Sensors::accel[2]);
file.write(',');
file.print(Sensors::gyro[0]);
file.write(',');
file.print(Sensors::gyro[1]);
file.write(',');
file.print(Sensors::gyro[2]);
file.write(',');
file.print(Sensors::mag[0]);
file.write(',');
file.print(Sensors::mag[1]);
file.write(',');
file.print(Sensors::mag[2]);
file.write(',');
file.println(System::battery);
// Fecha o arquivo
file.close();
}
// Função Modicada para armazenamento
void appendFile(fs::FS &fs, const char * path, TickType_t time){
//Abre o arquivo do SD para a memória RAM
File file = fs.open(path, FILE_APPEND);
if(!file){
Serial.println("Falha ao abrir para gravacao");
return;
}
// Salva no CSV o dado, seguido de uma vírgula.
file.print(time);
file.write(',');
file.print(cubeSat.getTemperature());
file.write(',');
file.print(cubeSat.getHumidity());
file.write(',');
file.print(cubeSat.getCO2Level());
file.write(',');
file.println(cubeSat.getBattery());
// Fecha o arquivo
file.close();
}
Todo sistema utiliza as Tasks do FreeRTOS para executar mais de uma ação ao mesmo tempo com uma ação tendo prioridade sobre outra em qualquer um dos núcleos do ESP32. Se necessário, você também pode criar sua própria Task , ou Tarefa, utilizando o seguinte comando:
// Configura uma nova Task
xTaskCreatePinnedToCore(
TaskBlink // Função a ser executada na Task
, "TaskBlink" // Nome apenas para Identificação
, 1024 // Tamanho da stack alocada para essa função
, NULL // Parâmetros a serem passados para a função
, 2 // Prioridade da tarefa, com 3 sendo a maior, e 0 sendo a menor.
, NULL // TaskHandle_t variável para acessar propriedades dessa task
, 0); // Nucleo 0 ou 1 do seu ESP32
// Função da Task
void TaskBlink(void *pvParameters){
(void) pvParameters;
// Inicializa a task como no void setup()
pinMode(BUZZER, OUTPUT);
// Roda o código da task que nunca deve retornar algo ou quebrar o for
for (;;) {
digitalWrite(LED_BUILTIN, HIGH); // Liga o Buzzer
vTaskDelay(1000); // Espera 1000 millisegundos liberando o sistema para fazer outras tarefas
digitalWrite(LED_BUILTIN, LOW); // Desliga o Buzzer
vTaskDelay(1000); // Espera 1000 millisegundos liberando o sistema para fazer outras tarefas
}
}
Para saber mais recomenda-se esse artigo do blog Embarcados, ou a documentação da Espressif sobre a implementação do FreeRTOS.
Um dos métodos que pode ser utilizado para receber os dados enviados pelo WebSocket gerenciado pelo seu Kit é o Script em python a seguir:
# Work In Progress
print("Teste")