Manual
do
Maker
.
com
Ainda não está completo, mas agora vamos entrar em detalhes importantes sobre a T Wristband. Nesse artigo disponibilizo o código para deep sleep, ESP32 OTA e máquina de estado no ESP32, como também discorro sobre informações importantes das situações que me deparei durante o desenvolvimento.
No caso específico da T Wristband, porque sua bateria é de 80mAh e o que não falta no ESP32 são rádios para consumir energia.
Utilizar o deep sleep nos permite aumentar a duração da carga da bateria, pois nesse modo os rádios não estarão funcionando. Mas dá pra ir além.
E com ele ligado, pode ser que não precisemos do WiFi e/ou bluetooth. Nesse caso, há uma maneira também de desligar os rádios quando o ESP32 estiver ligado. Implementei nesse código que disponho mais adiante.
Pois é. O programa foi feito para fazer atualização ESP32 OTA, utilizando o browser. Mas se o WiFi estiver desligado não tem como. Por isso implementei duas funções; startOTA() e stopOTA(). Ao iniciar com startOTA(), o WiFi é ligado e o modo AP+STAé configurado. Ao chamar a função stopOTA(), o WiFi é desligado e o modo deep sleep é executado através da função dream().
Pela primeira vez consegui esgotar o espaço do ESP32! Acredito que seja possível reduzir esse consumo, mas a quantidade de bibliotecas utilizadas para facilitar as coisas foi realmente demasiada. E nem implementei o IMU de 9 eixos ainda. A questão é que meu propósito era utilizar o bluetooth e manter também a atualização via ESP32 OTA, mas não houve espaço para ambos. A biblioteca do PCF8563 foi removida e implementei diretamente através do datasheet, mas cometi alguma cag... digo, algum equívoco, pois não estou conseguindo ajustar a hora ainda. Mas sem problemas, em algum momento dará certo, estou focado em outro objetivo agora.
Enquanto mexendo diretamente com a conexão WiFi e com o touch, achei prudente manter a gravação do firmware pela placa, uma vez que qualquer erro poderia impossibilitar a atualização por OTA no ESP32.
Abaixo, a foto da placa conectada ao T Wristband.
Para fazer o deep sleep, duas linhas foram utilizadas.
esp_sleep_enable_ext1_wakeup(GPIO_SEL_33, ESP_EXT1_WAKEUP_ANY_HIGH);
esp_deep_sleep_start();
A primeira define o modo utilizado para fazer o wakeup. No caso, utilizando o pino 33 e qualquer pino em HIGH. No GPIO 33 está conectado o TP223, que é o sensor de toque, portanto para tirar o ESP32 do deep sleep, basta tocar. Poderia também fazer wakeup a partir do timer, mas não tem muito propósito para mim por enquanto.
Dependendo do momento que for feito o deep sleep (por exemplo, após uma atualização via OTA), será necessário desligar o WiFi e bluetooth previamente. Para isso, temos a função stopOTA(), que desliga os recursos e o servidor web:
server.stop();
WiFi.mode(WIFI_OFF);
btStop();
É bem fácil pegar em um loop o estado do sensor de toque e contabilizar o tempo pressionado. O problema é que se tivermos 5 funções no menu precisaremos de um cronômetro para acessar cada função. A melhor opção é caminhar através de um menu, e isso pode ser feito de duas formas.
Podemos contabilizar apenas 2 itens; o Enter e a caminhada por um array. Fica fácil; se o toque for menor que 1 segundo, muda o item do menu. Se o toque for superior a 1 segundo, executa a função correspondente ao item do menu.
Esse é mais difícil de implementar. Escolhi esse modo por diversão, mas não é ideal para menus longos onde todo o controle fica por conta de 1 botão - no caso, o sensor de toque.
Uma função fica rodando em um loop; se houver toque, dispara uma contagem de tempo de X segundos. O número de toques é contabilizado e, ao estourar os X segundos, a função correspondente ao item selecionado é executada.
Tem 2 desvantagens em usar esse modo: se estourar o tempo antes de selecionar o item correto, executará a função que estiver no momento. Mesmo se selecionar logo a função, terá que esperar estourar o tempo para que a função seja executada. Defini 3 segundos para fazer a seleção.
Esse mês ministro uma palestra e um mini-curso no Arduino Day UNIVAG. Para trocar os slides, pretendo utilizar o T Wristband. O primeiro passo era ter esse controle sobre o menu e a ideia inicial era utilizar o bluetooth, mas ao que parece a opção tangível para esse momento de pressa (estamos em cima da data) é utilizar o WiFi. Vou concluir o projeto nos próximos dias e farei uma apresentação, mas mesmo sem funcionalidades efetivas, já dá pra fazer o vídeo que prometi no artigo anterior relacionado, portanto já amanhã ou depois devo colocar o vídeo do estado atual do programa e assim consigo apresentar melhor a T Wristband.
Esse código será modificado para a apresentação em vídeo, mas já está bastante funcional. Só que as partes que funcionavam por temporização do toque agora devem ser migradas para a máquina de estado para voltarem a funcionar.
No código tem também recursos comentados, como o bluetooth. Tem um scanner i2c para detectar os endereços da IMU e do RTC PCF8563, além da função iniciada do bluetooth.
#include <Arduino.h>
#include <WiFi.h>
#include <Wire.h>
#include <TFT_eSPI.h>
#include <ESPmDNS.h>
#include <Update.h>
#include <ESPmDNS.h>
#include <WebServer.h>
#include "BluetoothSerial.h"
#include "/home/djames/.platformio/lib/TFT_eSPI_ID1559/examples/320 x 240/Free_Font_Demo/Free_Fonts.h"
#define TP_PIN_PIN 33
#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
#define IMU_INT_PIN 38
#define RTC_INT_PIN 34
#define BATT_ADC_PIN 35
#define VBUS_PIN 36
#define TP_PWR_PIN 25
#define LED_PIN 4
#define CHARGE_PIN 32
#define TIMEOUT_TO_TIME 5000
#define RTC_ADDR 0x51
const char* hostn = "esp32";
const char* ap_ssid = "Wristband";
const char* ap_password = "030041975";
const char* sta_ssid = "SuhankoFamily";
const char* sta_passwd = "fsjmr112";
bool initial = 1;
bool timeTosleep = false;
bool ota_running = false;
bool showTimeRunning = false;
bool was_touched = false;
TFT_eSPI tft = TFT_eSPI();
uint8_t xcolon = 0;
uint8_t touched_times = 0;
uint32_t targetTime = 0;
uint32_t touched_time = 0;
int vref = 1100;
int pressed_time = 0;
/* Style */
String style =
"<style>#file-input,input{width:100%;height:44px;border-radius:4px;margin:10px auto;font-size:15px}"
"input{background:#f1f1f1;border:0;padding:0 15px}body{background:#3498db;font-family:sans-serif;font-size:14px;color:#777}"
"#file-input{padding:0;border:1px solid #ddd;line-height:44px;text-align:left;display:block;cursor:pointer}"
"#bar,#prgbar{background-color:#f1f1f1;border-radius:10px}#bar{background-color:#3498db;width:0%;height:10px}"
"form{background:#fff;max-width:258px;margin:75px auto;padding:30px;border-radius:5px;text-align:center}"
".btn{background:#3498db;color:#fff;cursor:pointer}</style>";
/* Login page */
String loginIndex =
"<form name=loginForm>"
"<h1>ESP32 Login</h1>"
"<input name=userid placeholder='User ID'> "
"<input name=pwd placeholder=Password type=Password> "
"<input type=submit onclick=check(this.form) class=btn value=Login></form>"
"<script>"
"function check(form) {"
"if(form.userid.value=='admin' && form.pwd.value=='admin')"
"{window.open('/serverIndex')}"
"else"
"{alert('Error Password or Username')}"
"}"
"</script>" + style;
/* Server Index Page */
String serverIndex =
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update' id='file' onchange='sub(this)' style=display:none>"
"<label id='file-input' for='file'> Choose file...</label>"
"<input type='submit' class=btn value='Update'>"
"<br><br>"
"<div id='prg'></div>"
"<br><div id='prgbar'><div id='bar'></div></div><br></form>"
"<script>"
"function sub(obj){"
"var fileName = obj.value.split('\\\\');"
"document.getElementById('file-input').innerHTML = ' '+ fileName[fileName.length-1];"
"};"
"$('form').submit(function(e){"
"e.preventDefault();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
"$.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {"
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;"
"$('#prg').html('progress: ' + Math.round(per*100) + '%');"
"$('#bar').css('width',Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!') "
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>" + style;
byte second = 0;
byte minute = 0;
byte hour = 0;
byte dayOfMonth = 0;
byte dayOfWeek = 0;
byte month = 0;
byte year = 0;
//Rtc_Pcf8563 rtc;
WebServer server(80);
BluetoothSerial SerialBT;
byte bcdToDec(byte value){
Serial.print("decToBcd ");
return ( (value/16*10) + (value%16) );
Serial.println(( (value/10*16) + (value%10) ));
}
byte decToBcd(byte value){
Serial.print("decToBcd ");
Serial.println(( (value/10*16) + (value%10) ));
return ( (value/10*16) + (value%10) );
}
void showClock();
void dream();
void stopOTA(){
server.stop();
WiFi.mode(WIFI_OFF);
btStop();
}
/*
void scanI2Cdevice(void)
{
uint8_t err, addr;
int nDevices = 0;
for (addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
err = Wire.endTransmission();
if (err == 0) {
Serial.print("I2C device found at address 0x");
if (addr < 16)
Serial.print("0");
Serial.print(addr, HEX);
Serial.println(" !");
nDevices++;
} else if (err == 4) {
Serial.print("Unknow error at address 0x");
if (addr < 16)
Serial.print("0");
Serial.println(addr, HEX);
}
}
if (nDevices == 0)
Serial.println("No I2C devices found\n");
else
Serial.println("Done\n");
}
*/
void readPCF8563(){
Wire.beginTransmission(RTC_ADDR);
Wire.write(0x02);
Wire.endTransmission();
Wire.requestFrom(RTC_ADDR, 7);
second = bcdToDec(Wire.read() & B01111111); // remove VL error bit
minute = bcdToDec(Wire.read() & B01111111); // remove unwanted bits from MSB
hour = bcdToDec(Wire.read() & B00111111);
dayOfMonth = bcdToDec(Wire.read() & B00111111);
dayOfWeek = bcdToDec(Wire.read() & B00000111);
month = bcdToDec(Wire.read() & B00011111); // remove century bit, 1999 is over
year = bcdToDec(Wire.read());
Serial.print(hour);
Serial.print(":");
Serial.print(minute);
}
/*
void setPCF8563(){
Wire.beginTransmission(RTC_ADDR);
Wire.write(0x02);
Wire.write(decToBcd(second));
Wire.write(decToBcd(minute));
Wire.write(decToBcd(hour));
Wire.write(decToBcd(dayOfMonth));
Wire.write(decToBcd(dayOfWeek));
Wire.write(decToBcd(month));
Wire.write(decToBcd(year));
Wire.endTransmission();
}*/
void readTime(){
Wire.beginTransmission(RTC_ADDR);
Wire.write(0x03);
Wire.endTransmission();
Wire.requestFrom(RTC_ADDR,1);
minute = bcdToDec(Wire.read() & B01111111);
//hour = bcdToDec(Wire.read() & B00111111);
Serial.print(hour);
Serial.print(":");
Serial.println(minute);
}
void changeSlide(){
//Foi tocado? Se não, retorna.
// if (!was_touched) {
// return;
// }
if (touched_times == 0 && was_touched){
touched_time = millis();
tft.fillScreen(TFT_SKYBLUE);
tft.setTextColor(TFT_BLACK,TFT_DARKCYAN);
tft.drawCentreString("Temporizador",6,0,4);
}
else if (touched_times == 1 && was_touched){
tft.fillScreen(TFT_SKYBLUE);
tft.setTextColor(TFT_BLACK,TFT_DARKCYAN);
tft.drawCentreString(" Avancar >>",6,0,4);
}
else if (touched_times == 2 && was_touched){
tft.fillScreen(TFT_SKYBLUE);
tft.setTextColor(TFT_BLACK,TFT_DARKCYAN);
tft.drawCentreString("<< Voltar ",6,0,4);
}
int interval = millis()-touched_time;
//Foi tocado!
//Serial.println("foi tocado");
//Faz menos de 2 segundos? Se sim, incrementa o número de toques
if (interval < 2000 && was_touched){
touched_times += 1;
vTaskDelay(pdMS_TO_TICKS(50));
}
//Passaram-se 2 segundos e foi tocado?
if (interval >1999 && touched_times > 0){
Serial.println("TIMOUT COM TOQUE");
switch(touched_times){
case 2:
Serial.println("NEXT: >>");
tft.fillScreen(TFT_SKYBLUE);
tft.setTextColor(TFT_BLACK,TFT_DARKCYAN);
tft.drawCentreString("Avancar [OK]",6,0,4);
break;
case 3:
Serial.println("PREVIOUS: <<");
tft.fillScreen(TFT_SKYBLUE);
tft.setTextColor(TFT_BLACK,TFT_DARKCYAN);
tft.drawCentreString("Voltar [OK]",6,0,4);
break;
case 1:
Serial.println("Wakeup or clock requested");
break;
}
touched_times = 0; //se passaram-se 2 segundos, começa novamente
touched_time = millis(); //zera o timer
vTaskDelay(pdMS_TO_TICKS(1000));
tft.fillScreen(TFT_BLACK);
dream();
}
else if (interval> 1999){
touched_time = millis();
Serial.println("TIMOUT");
touched_times = 0;
}
was_touched = false; // < proximo
/*
btStart();
vTaskDelay(pdMS_TO_TICKS(200));
SerialBT.begin("Djames Suhanko");
delay(2000);
SerialBT.end();
Serial.println("desligado");
SerialBT.begin("Joao");
Serial.println("agora eh Joao");
*/
}
/*O processador estará dormindo. Um toque mostra a hora por TIMEOUT_TO_TIME
e volta a dormir
*/
void helloMan(){
tft.fillScreen(TFT_SKYBLUE);
tft.setTextColor(TFT_BLACK,TFT_DARKCYAN);
//Manual do Maker
tft.drawCentreString("Manual do Maker ",6,0,4);
}
void showTime(void *pvParameters){
if (showTimeRunning){
vTaskDelete(NULL);
}
showTimeRunning = true;
long int initial_time = millis();
Serial.println("showTime()");
showClock();
while ((millis() - initial_time) < TIMEOUT_TO_TIME){
//showClock();
vTaskDelay(pdMS_TO_TICKS(2));
}
timeTosleep = true;
showTimeRunning = false;
vTaskDelete(NULL);
}
void startOTA(){
tft.fillScreen(TFT_BLACK);
Serial.print("Setting AP (Access Point)…");
WiFi.mode(WIFI_AP_STA);
WiFi. begin(sta_ssid, sta_passwd);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(" (wifi) ");
}
WiFi.softAP(ap_ssid, ap_password);
if (!MDNS.begin(hostn)) { //http://esp32.local
Serial.println("Error setting up MDNS responder!");
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
Serial.println("mDNS responder started");
/*return index page which is stored in serverIndex */
server.on("/", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", loginIndex);
});
server.on("/serverIndex", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", serverIndex);
});
/*handling uploading firmware file */
server.on("/update", HTTP_POST, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
/* flashing firmware to ESP*/
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) { //true to set the size to the current progress
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
} else {
Update.printError(Serial);
}
}
});
server.begin();
Serial.println("Server started");
}
/*
String getVoltage(){
uint16_t v = analogRead(BATT_ADC_PIN);
float battery_voltage = ((float)v / 4095.0) * 2.0 * 3.3 * (vref / 1000.0);
return String(battery_voltage) + "V";
}
*/
void touchMonitor(void *pvParameters){
touched_time = millis();
while (true){
changeSlide();
//pega o tempo inicial toda a vez que der uma volta
long int initial_time = millis();
//se o touch estiver pressionado, acumula tempo
while (digitalRead(TP_PIN_PIN) > 0){
was_touched = true;
if ((millis()-initial_time) >2999){
//(essa variavel nao deixa o loop fazer deep sleep)
ota_running = !ota_running;
if (ota_running){
startOTA();
tft.fillScreen(TFT_BLACK);
Serial.println("OTA RUNNING");
tft.setTextColor(TFT_BLACK,TFT_RED);
//tft.drawCentreString("OTA",80,28,4);
tft.drawCentreString("::Over The Air::",6,0,4);
vTaskDelay(pdMS_TO_TICKS(500));
}
else{
Serial.println("OTA STOPPED");
tft.setTextColor(TFT_BLACK,TFT_CYAN);
//tft.drawCentreString("OTA",80,28,4);
tft.drawCentreString("::OTA stopped::",6,0,4);
stopOTA();
vTaskDelay(pdMS_TO_TICKS(2000));
tft.fillScreen(TFT_BLACK);
timeTosleep = true;
ota_running = false;
}
}
vTaskDelay(pdMS_TO_TICKS(1));
}
//se foi maior ou igual a 2 segundos...
//...senão, se foi menor que 2 segundos, mostra o relogio. Se não houve toque, não tem tempo acumulado.
if ((millis()-initial_time) > 1000 && (millis()-initial_time) < 3000 ){
xTaskCreatePinnedToCore(showTime,"showtime", 10000, NULL, 1, NULL,0);
Serial.println("touched. showing time");
vTaskDelay(pdMS_TO_TICKS(100));
showTimeRunning = true;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void showClock(){
readTime();
tft.fillScreen(TFT_BLACK);
pressed_time = millis();
if (targetTime < millis()) {
targetTime = millis() + 1000;
if (second == 0 || initial) {
initial = 0;
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor (8, 60);
tft.print(__DATE__); // This uses the standard ADAFruit small font
}
//tft.setTextColor(TFT_BLUE, TFT_BLACK);
//tft.drawCentreString(getVoltage(), 120, 60, 1); // Next size up font 2
// Update digital time
uint8_t xpos = 6;
uint8_t ypos = 0;
//if (omm != mm) { // Only redraw every minute to minimise flicker
if (true){
// Uncomment ONE of the next 2 lines, using the ghost image demonstrates text overlay as time is drawn over it
tft.setTextColor(0x39C4, TFT_BLACK); // Leave a 7 segment ghost image, comment out next line!
//tft.setTextColor(TFT_BLACK, TFT_BLACK); // Set font colour to black to wipe image
// Font 7 is to show a pseudo 7 segment display.
// Font 7 only contains characters [space] 0 1 2 3 4 5 6 7 8 9 0 : .
tft.drawString("88:88", xpos, ypos, 7); // Overwrite the text to clear it
tft.setTextColor(0xFBE0, TFT_BLACK);
if (hour < 10) xpos += tft.drawChar('0', xpos, ypos, 7);
xpos += tft.drawNumber(hour, xpos, ypos, 7);
xcolon = xpos;
xpos += tft.drawChar(':', xpos, ypos, 7);
if (minute < 10) xpos += tft.drawChar('0', xpos, ypos, 7);
tft.drawNumber(minute, xpos, ypos, 7);
}
if (second % 2) { // Flash the colon
tft.setTextColor(0x39C4, TFT_BLACK);
xpos += tft.drawChar(':', xcolon, ypos, 7);
tft.setTextColor(0xFBE0, TFT_BLACK);
}
else {
tft.drawChar(':', xcolon, ypos, 7);
}
}
}
void dream(){
uint8_t xpos = 6;
uint8_t ypos = 0;
tft.setTextColor(0x39C4, TFT_BLACK);
tft.drawString("88:88", xpos, ypos, 7);
vTaskDelay(pdMS_TO_TICKS(100));
tft.setTextColor(0x39C4, TFT_BLACK);
tft.drawString("OFF", xpos, ypos, 7);
vTaskDelay(pdMS_TO_TICKS(2000));
esp_sleep_enable_ext1_wakeup(GPIO_SEL_33, ESP_EXT1_WAKEUP_ANY_HIGH);
esp_deep_sleep_start();
}
void setup() {
tft.init();
tft.setRotation(1);
Serial.begin(9600);
Wire.setClock(400000);
pinMode(TP_PIN_PIN,INPUT);
pinMode(TP_PWR_PIN, PULLUP);
pinMode(RTC_INT_PIN, INPUT_PULLUP);
pinMode(CHARGE_PIN, INPUT_PULLUP);
stopOTA();
second = 0;
minute = 20;
hour = 13;
dayOfWeek = 4;
dayOfMonth = 4;
month = 3;
year = 20;
//setPCF8563();
vTaskDelay(pdMS_TO_TICKS(100));
readPCF8563();
xTaskCreatePinnedToCore(touchMonitor,"touchMonitor", 10000, NULL, 1, NULL,0);
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
helloMan();
//scanI2Cdevice();
}
void loop(){
server.handleClient();
delay(1);
if (timeTosleep && !ota_running){
timeTosleep = false;
dream();
}
}
Esse código tem algum aproveitamento do sketch de exemplo da própria LilyGo.
Por enquanto, só por importação. Tenho certeza que em breve teremos parceiros aqui no Brasil vendendo essa belezinha.
O vídeo estará disponível em nosso canal DobitaobyteBrasil no Youtube. Se não é inscrito, inscreva-se e clique no sininho para receber notificações. Aproveite o momento em que estou disponibilizando o curso de Raspberry Pi que outrora estava na Udemy, mas resolvi tirar e disponibilizar para o público devido às insatisfações pelas mudanças feitas na Udemy, onde não pretendo voltar a colocar nada.
Não percam o vídeo, sério. Está uma delícia esse programa!
Revisão: Ricardo Amaral de Andrade
Inscreva-se no nosso canal Manual do Maker no YouTube.
Também estamos no Instagram.
Autor do blog "Do bit Ao Byte / Manual do Maker".
Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.