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.
Por que usar deep sleep no ESP32?
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.
Como desabilitar WiFi e Bluetooth no ESP32?
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.
E se o firmware permitir atualizar o ESP32 por OTA?
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().
Limitação de espaço no ESP32
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.
Placa de gravação da T Wristband
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.
Deep sleep no ESP32
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();
Máquina de estado no ESP32
É 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.
Seleção e ENTER
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.
Seleção de um item dentro de um intervalo de tempo
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.
Primeiro projeto com o T Wristband
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.
Código para deep sleep, OTA e máquina de estado no ESP32
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); //Do bit Ao Byte tft.drawCentreString("Do bit Ao Byte ",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.
Onde comprar a LilyGo T Wristband?
Por enquanto, só por importação. Tenho certeza que em breve teremos parceiros aqui no Brasil vendendo essa belezinha.
Vídeo de apresentação
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
Artigos relacionados
ESP32 Modem Sleep
Funções lambda no MicroPython
Debug do ESP32 no VS Code com ESP-Prog