Veja a página da tarefa em grupo.

Depois de brincar um tempo com um kit de desenvolvimento ESP32, eu já tinha uma ideia do que precisava na minha placa. Então vamos ver a lista de funções que eu quero nela:
Com os requisitos em mãos, fui procurar uma biblioteca do Eagle para o ESP-WROOM-32 e achei várias:
Depois de avaliar todas, decidi usar a do MacroYau/MacroYau.

Como quero o máximo de compatibilidade com os dev kits de ESP32 com que eu estava acostumado, fui procurar, e achei, uma pinagem do ESP32-WROOM, que usei para dar nome aos pinos.

O passo seguinte foi ligar fios de net a cada pino. Queria ter feito isso na ordem em que estou apresentando, porque teria me poupado de refazer muitos fios que liguei errado da primeira vez. A lição é que os números lógicos e os números físicos dos pinos nem sempre são iguais; com isso em mente, desta vez liguei tudo certo.

O resultado final é este:

O relé que achei no snapeda não é o mesmo modelo do que eu tenho, mas o footprint é.
Achei o circuito do relé online aqui, no web.archive; o esquemático está abaixo.

Com o esquemático em mãos, desenhei o seguinte no Eagle:

Para o circuito da bateria, segui um circuito da página Random Nerd Tutorials.

Como por enquanto não tenho o módulo TP4056, meu circuito vai ter só a parte de medição.

O resistor do divisor de tensão foi calculado com a calculadora online aqui.

Para o futuro, achei a biblioteca do TP4056 aqui, graças a uma dica do Luis Carvão, meu instrutor remoto.
A biblioteca do sensor de umidade está aqui. Com ela em mãos, só precisei ligar alguns fios de net ao pino 32 do ESP32.

Para o sensor de corrente, escolhi usar o ACS712. Achei a biblioteca neste GitHub. O esquemático pronto está abaixo:

Também peguei a biblioteca do relógio de tempo real no SnapEDA, mas por enquanto não vou usar, então o esquemático completo que desenhei está abaixo:

Depois de ficar um tempo olhando para o esquemático, percebi alguns erros. Esqueci de ligar o resistor do LED dos relés ao VCC, e também esqueci de colocar um botão de reset e um de programação, necessários para colocar o ESP em modo de programação, além de headers para o modo serial e para expansão. Por fim, decidi incluir um cristal para marcar melhor o tempo.
Depois desenhei os botões de Reset e de Program:

Aqui corrigi os relés adicionando o VCC, e também coloquei um optoacoplador entre o sinal e os relés, para evitar qualquer retorno dos relés.

Aqui adicionei um header TX RX, um cristal para o RTC interno marcar melhor o tempo e headers de compatibilidade com o ESP32DEVKIT.

Então, espero eu, finalmente meu esquemático final...

Agora vou começar o roteamento da placa. Confesso que estou meio ansioso, porque nunca fiz uma placa tão complexa!
Que animador o que vejo quando passo para o modo board no Eagle!!!

A primeira coisa que faço é mover o footprint do devkit para o centro, junto com o ESP32.

Depois de tentar um tempo ligar à mão, dou uma chance ao autorouter. Tive que parar depois de alguns minutos, e o resultado é ridículo. Vou tentar de novo subindo o ESP32 e, se não der, vou desistir da compatibilidade com o ESP32devkit.

Então desisti da compatibilidade com o DevKit e decidi ir devagar desta vez. Fiz um backup do meu trabalho, abri numa outra janela do Eagle e apaguei todos os componentes em que eu não estava trabalhando. O esquemático e a placa ficaram assim. Minha ideia é copiar os componentes de volta do backup à medida que termino cada passo. Então, abaixo, é assim que fica o passo um.

Na placa, dá para ver o nº 1, o relé, o nº 2, o circuito do sensor de corrente, e o nº 3, o LED da placa.

Aí copiei o relé da primeira válvula e, quando fui para a placa, o Eagle me mostrou os componentes colados. Acho que assim é bem mais fácil.

Até agora, a placa está assim. Ainda não roteei nenhum fio de alimentação, incluindo GND, 3.3V e 5V. Agora vou tentar o autorouter para ter uma ideia e talvez rotear à mão. O processo de roteamento completo está mais bem descrito na semana de Input Devices.

Depois de terminar o layout da placa, decidi colocar o logo. Clique em File, Import e Bitmap (por enquanto, o Eagle só aceita .bmp).

Escolha a cor a ser importada.

Depois escolha a unidade, mm no meu caso, e o tamanho, e clique em OK.

Como eu queria fresar o logo em vez de carimbar, tive que mover para a camada top.

O layout final da placa com furos e contornos de fresagem, como descrito na semana de Output Devices.

Aqui mostro a visualização das rotas de G-code, com o vermelho sendo o traço mais perto do cobre que vai ficar.

Como sempre, e de novo mais bem descrito na semana de Input Devices, aqui vai uma olhada no software de autolevel.


Inspecionando a placa contra a luz.
Para verificar se as trilhas estão todas boas, coloco a luz embaixo da placa e reviso tudo procurando curtos ou trilhas faltando.

O lado de baixo sendo montado.
Coloco os componentes grandes por último, para não dificultar a solda dos componentes SMD pequenos.

Depois de soldar tudo, ligo a placa ao meu adaptador serial. Abaixo está o esquemático.

Colei a primeira saída do ESP32 com o firmware de fábrica. Ele dá boot!
--- Available filters and text transformations: colorize, debug, default, direct, esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at http://bit.ly/pio-monitor-filters
--- Miniterm on COM5 115200,8,N,1 ---
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
ets Jun 8 2016 00:22:57
rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0x00
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0008,len:8
load:0x3fff0010,len:3480
load:0x40078000,len:7804
ho 0 tail 12 room 4
load:0x40080000,len:252
entry 0x40080034
␛[0;32mI (45) boot: ESP-IDF v2.0-3-gbef9896 2nd stage bootloader␛[0m
␛[0;32mI (45) boot: compile time 05:59:45␛[0m
␛[0;32mI (45) boot: Enabling RNG early entropy source...␛[0m
␛[0;32mI (64) boot: SPI Speed : 40MHz␛[0m
␛[0;32mI (77) boot: SPI Mode : DIO␛[0m
␛[0;32mI (89) boot: SPI Flash Size : 4MB␛[0m
␛[0;32mI (101) boot: Partition Table:␛[0m
␛[0;32mI (113) boot: ## Label Usage Type ST Offset Length␛[0m
␛[0;32mI (136) boot: 0 phy_init RF data 01 01 0000f000 00001000␛[0m
␛[0;32mI (159) boot: 1 otadata OTA data 01 00 00010000 00002000␛[0m
␛[0;32mI (182) boot: 2 nvs WiFi data 01 02 00012000 0000e000␛[0m
␛[0;32mI (205) boot: 3 at_customize unknown 40 00 00020000 000e0000␛[0m
␛[0;32mI (228) boot: 4 ota_0 OTA app 00 10 00100000 00180000␛[0m
␛[0;32mI (252) boot: 5 ota_1 OTA app 00 11 00280000 00180000␛[0m
␛[0;32mI (275) boot: End of partition table␛[0m
␛[0;32mI (288) boot: Disabling RNG early entropy source...␛[0m
␛[0;32mI (305) boot: Loading app partition at offset 00100000␛[0m
␛[0;32mI (1481) boot: segment 0: paddr=0x00100018 vaddr=0x00000000 size=0x0ffe8 ( 65512) ␛[0m
␛[0;32mI (1481) boot: segment 1: paddr=0x00110008 vaddr=0x3f400010 size=0x1c5f0 (116208) map␛[0m
␛[0;32mI (1498) boot: segment 2: paddr=0x0012c600 vaddr=0x3ffb0000 size=0x0215c ( 8540) load␛[0m
␛[0;32mI (1528) boot: segment 3: paddr=0x0012e764 vaddr=0x40080000 size=0x00400 ( 1024) load␛[0m
␛[0;32mI (1551) boot: segment 4: paddr=0x0012eb6c vaddr=0x40080400 size=0x1b028 (110632) load␛[0m
␛[0;32mI (1630) boot: segment 5: paddr=0x00149b9c vaddr=0x400c0000 size=0x00034 ( 52) load␛[0m
␛[0;32mI (1631) boot: segment 6: paddr=0x00149bd8 vaddr=0x00000000 size=0x06430 ( 25648) ␛[0m
␛[0;32mI (1648) boot: segment 7: paddr=0x00150010 vaddr=0x400d0018 size=0x7a56c (501100) map␛[0m
␛[0;32mI (1675) heap_alloc_caps: Initializing. RAM available for dynamic allocation:␛[0m
␛[0;32mI (1697) heap_alloc_caps: At 3FFBA6B8 len 00025948 (150 KiB): DRAM␛[0m
␛[0;32mI (1719) heap_alloc_caps: At 3FFE8000 len 00018000 (96 KiB): D/IRAM␛[0m
␛[0;32mI (1740) heap_alloc_caps: At 4009B428 len 00004BD8 (18 KiB): IRAM␛[0m
␛[0;32mI (1761) cpu_start: Pro cpu up.␛[0m
␛[0;32mI (1773) cpu_start: Single core mode␛[0m
␛[0;32mI (1786) cpu_start: Pro cpu start user code␛[0m
␛[0;32mI (1846) cpu_start: Starting scheduler on PRO CPU.␛[0m
␛[0;32mI (2085) uart: queue free spaces: 10␛[0m
Bin version:0.10.0
I (2087) wifi: wifi firmware version: c604573
I (2087) wifi: config NVS flash: enabled
I (2088) wifi: config nano formating: disabled
I (2096) wifi: Init dynamic tx buffer num: 32
I (2097) wifi: wifi driver task: 3ffc4eac, prio:23, stack:3584
I (2102) wifi: Init static rx buffer num: 10
I (2106) wifi: Init dynamic rx buffer num: 0
I (2110) wifi: Init rx ampdu len mblock:7
I (2114) wifi: Init lldesc rx ampdu entry mblock:4
I (2118) wifi: wifi power manager task: 0x3ffca254 prio: 21 stack: 2560
I (2125) wifi: wifi timer task: 3ffcb2d4, prio:22, stack:3584
␛[0;31mE (2130) phy_init: PHY data partition validated␛[0m
␛[0;32mI (2152) phy: phy_version: 329, Feb 22 2017, 15:58:07, 0, 0␛[0m
I (2152) wifi: mode : softAP (3c:71:bf:10:66:e1)
I (2155) wifi: mode : sta (3c:71:bf:10:66:e0) + softAP (3c:71:bf:10:66:e1)
I (2159) wifi: mode : softAP (3c:71:bf:10:66:e1)
Para gravar meu firmware, uso o Microsoft VS Code com o PlatformIO.
Antes de começar a gravação, primeiro tive que configurar o ambiente do PlatformIO. Descrevo isso aqui, na semana de Project Development.
Depois seguro o botão do GPIO0 e aperto o reset para entrar no modo de gravação do firmware. Aí clico no ícone de gravar o firmware, e ele grava. Sucesso!
Depurando a placa.
Acabei tirando todos os componentes para revisar os sinais produzidos diretamente pelo microcontrolador.
O resultado da depuração foi que descobri que não havia nada errado com o código, com a solda do microcontrolador nem com as trilhas próximas.
O que descobri foi que eu precisava colocar resistores de 220 ohms entre o sinal e o anodo, pino número 1, de cada optoacoplador. Acontece que o optoacoplador tem um LED interno, e a corrente usada para acionar esse LED precisa ser limitada.

Então R=(3.3V-1.2V)/20mA=105ohms, ou

Aí cortei as trilhas e soldei os resistores de 220 ohms, porque era o que eu tinha à mão, e funcionou, mas o valor certo é 120 ohms, como mostra a calculadora de resistores online.

E é assim que fica o esquemático do relé:

Tem dois tipos de comunicação acontecendo na minha placa que posso descrever.
A rede entre o celular e o ESP32 funciona graças a algumas bibliotecas trabalhando juntas:
A documentação dela descreve assim:
"Permite a conexão de rede (local e Internet) usando o WiFi embutido do ESP32. Com esta biblioteca, você pode iniciar servidores, clientes e enviar/receber pacotes UDP via WiFi. O shield pode se conectar a redes abertas ou criptografadas (WEP, WPA). O endereço IP pode ser atribuído de forma estática ou por DHCP. A biblioteca também pode gerenciar DNS."
A documentação dela descreve assim:
"Esta é uma biblioteca TCP totalmente assíncrona, feita para permitir um ambiente de rede sem problemas e com múltiplas conexões para os MCUs ESP32 da Espressif. Esta biblioteca é a base do ESPAsyncWebServer"
A documentação dela descreve assim:
"Servidor HTTP e WebSocket assíncrono para ESP8266 Arduino"
Então, basicamente, no meu projeto, essas bibliotecas são responsáveis por fazer a conexão de rede com um roteador e também por criar um ponto de acesso.
Elas também criam um servidor web e um servidor DHCP, e com isso ele entrega um endereço IP caso alguém se conecte pelo ponto de acesso criado com este código:
//Soft Wifi Access point setup
WiFi.softAP("softap", "password_here");
IPAddress IP = WiFi.softAPIP();
A conexão com o roteador é feita com este código:
//start wifi sessions as a client.
//Wifi client setup
const char* ssid = doc["data"]["ssid"];
const char* password = doc["data"]["pass"];
ssid = "fabfarm";
password = "password_here";
WiFi.begin(ssid, password);
Atenção: o ssid e o password lidos do JSON são sobrescritos logo em seguida pelos valores fixos das duas linhas seguintes, então as credenciais do arquivo JSON nunca são usadas. Tire as duas atribuições fixas se quiser que os valores do JSON valham.
O servidor web é iniciado com este código: AsyncWebServer server(81);
JSON é o formato que escolhi para a comunicação entre a interface web e o microcontrolador.
A documentação do ArduinoJson.h descreve assim:
O ArduinoJson é uma biblioteca JSON em C++ para Arduino e IoT (Internet das Coisas)
Uma ótima fonte de documentação, e um ótimo assistente para criar o programa de parsing deserializeJson() e o de serialização serializeJson(doc, Serial), é o site do ArduinoJson.
Para usar a biblioteca, primeiro tive que criar um arquivo json. O assistente vai usar esse arquivo para gerar o código para o C++.
Com o arquivo JSON criado à mão, é hora de criar os programas em C++. O assistente então cria o código C++ para serializar e também fazer o parsing dos dados de e para o arquivo JSON que fica no sistema de arquivos.

Na caixa de diálogo à esquerda, colei o conteúdo do arquivo json.

O site gera o código automaticamente.

Abaixo mostro o código gerado no assistente.
const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_ARRAY_SIZE(2) + 2*JSON_ARRAY_SIZE(3) + JSON_OBJECT_SIZE(2) + 6*JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + 3*JSON_OBJECT_SIZE(6) + 540;
DynamicJsonDocument doc(capacity);
const char* json = "{\"data\":{\"currentTime\":\"Wednesday, June 24 2020 23:49:23\",\"temperature\":\"22.30\",\"humidity\":\"74.00\",\"override\":0,\"ssid\":\"rato\",\"pass\":\"imakestuff\"},\"relays\":[{\"name\":\"Fruit Tree\",\"pin\":25,\"isRunning\":1,\"isEnabled\":0,\"times\":[{\"startTime\":\"01:01\",\"duration\":0,\"hour\":1,\"min\":1}]},{\"name\":\"Vegie Garden\",\"pin\":26,\"isRunning\":1,\"isEnabled\":0,\"status\":1,\"times\":[{\"startTime\":\"02:02\",\"duration\":32,\"hour\":2,\"min\":2},{\"startTime\":\"02:02\",\"duration\":32,\"hour\":2,\"min\":2}]},{\"name\":\"Cypress Hill\",\"pin\":33,\"isRunning\":1,\"isEnabled\":0,\"status\":1,\"times\":[{\"startTime\":\"03:03\",\"duration\":60,\"hour\":3,\"min\":3},{\"startTime\":\"03:03\",\"duration\":60,\"hour\":3,\"min\":3},{\"startTime\":\"03:03\",\"duration\":60,\"hour\":3,\"min\":3}]}]}";
deserializeJson(doc, json);
JsonObject data = doc["data"];
const char* data_currentTime = data["currentTime"]; // "Wednesday, June 24 2020 23:49:23"
const char* data_temperature = data["temperature"]; // "22.30"
const char* data_humidity = data["humidity"]; // "74.00"
int data_override = data["override"]; // 0
const char* data_ssid = data["ssid"]; // "rato"
const char* data_pass = data["pass"]; // "imakestuff"
JsonArray relays = doc["relays"];
JsonObject relays_0 = relays[0];
const char* relays_0_name = relays_0["name"]; // "Fruit Tree"
int relays_0_pin = relays_0["pin"]; // 25
int relays_0_isRunning = relays_0["isRunning"]; // 1
int relays_0_isEnabled = relays_0["isEnabled"]; // 0
JsonObject relays_0_times_0 = relays_0["times"][0];
const char* relays_0_times_0_startTime = relays_0_times_0["startTime"]; // "01:01"
int relays_0_times_0_duration = relays_0_times_0["duration"]; // 0
int relays_0_times_0_hour = relays_0_times_0["hour"]; // 1
int relays_0_times_0_min = relays_0_times_0["min"]; // 1
JsonObject relays_1 = relays[1];
const char* relays_1_name = relays_1["name"]; // "Vegie Garden"
int relays_1_pin = relays_1["pin"]; // 26
int relays_1_isRunning = relays_1["isRunning"]; // 1
int relays_1_isEnabled = relays_1["isEnabled"]; // 0
int relays_1_status = relays_1["status"]; // 1
JsonObject relays_1_times_0 = relays_1["times"][0];
const char* relays_1_times_0_startTime = relays_1_times_0["startTime"]; // "02:02"
int relays_1_times_0_duration = relays_1_times_0["duration"]; // 32
int relays_1_times_0_hour = relays_1_times_0["hour"]; // 2
int relays_1_times_0_min = relays_1_times_0["min"]; // 2
JsonObject relays_1_times_1 = relays_1["times"][1];
const char* relays_1_times_1_startTime = relays_1_times_1["startTime"]; // "02:02"
int relays_1_times_1_duration = relays_1_times_1["duration"]; // 32
int relays_1_times_1_hour = relays_1_times_1["hour"]; // 2
int relays_1_times_1_min = relays_1_times_1["min"]; // 2
JsonObject relays_2 = relays[2];
const char* relays_2_name = relays_2["name"]; // "Cypress Hill"
int relays_2_pin = relays_2["pin"]; // 33
int relays_2_isRunning = relays_2["isRunning"]; // 1
int relays_2_isEnabled = relays_2["isEnabled"]; // 0
int relays_2_status = relays_2["status"]; // 1
JsonArray relays_2_times = relays_2["times"];
JsonObject relays_2_times_0 = relays_2_times[0];
const char* relays_2_times_0_startTime = relays_2_times_0["startTime"]; // "03:03"
int relays_2_times_0_duration = relays_2_times_0["duration"]; // 60
int relays_2_times_0_hour = relays_2_times_0["hour"]; // 3
int relays_2_times_0_min = relays_2_times_0["min"]; // 3
JsonObject relays_2_times_1 = relays_2_times[1];
const char* relays_2_times_1_startTime = relays_2_times_1["startTime"]; // "03:03"
int relays_2_times_1_duration = relays_2_times_1["duration"]; // 60
int relays_2_times_1_hour = relays_2_times_1["hour"]; // 3
int relays_2_times_1_min = relays_2_times_1["min"]; // 3
JsonObject relays_2_times_2 = relays_2_times[2];
const char* relays_2_times_2_startTime = relays_2_times_2["startTime"]; // "03:03"
int relays_2_times_2_duration = relays_2_times_2["duration"]; // 60
int relays_2_times_2_hour = relays_2_times_2["hour"]; // 3
int relays_2_times_2_min = relays_2_times_2["min"]; // 3
const size_t capacity = JSON_ARRAY_SIZE(1) + JSON_ARRAY_SIZE(2) + 2*JSON_ARRAY_SIZE(3) + JSON_OBJECT_SIZE(2) + 6*JSON_OBJECT_SIZE(4) + JSON_OBJECT_SIZE(5) + 3*JSON_OBJECT_SIZE(6);
DynamicJsonDocument doc(capacity);
JsonObject data = doc.createNestedObject("data");
data["currentTime"] = "Wednesday, June 24 2020 23:49:23";
data["temperature"] = "22.30";
data["humidity"] = "74.00";
data["override"] = 0;
data["ssid"] = "rato";
data["pass"] = "imakestuff";
JsonArray relays = doc.createNestedArray("relays");
JsonObject relays_0 = relays.createNestedObject();
relays_0["name"] = "Fruit Tree";
relays_0["pin"] = 25;
relays_0["isRunning"] = 1;
relays_0["isEnabled"] = 0;
JsonArray relays_0_times = relays_0.createNestedArray("times");
JsonObject relays_0_times_0 = relays_0_times.createNestedObject();
relays_0_times_0["startTime"] = "01:01";
relays_0_times_0["duration"] = 0;
relays_0_times_0["hour"] = 1;
relays_0_times_0["min"] = 1;
JsonObject relays_1 = relays.createNestedObject();
relays_1["name"] = "Vegie Garden";
relays_1["pin"] = 26;
relays_1["isRunning"] = 1;
relays_1["isEnabled"] = 0;
relays_1["status"] = 1;
JsonArray relays_1_times = relays_1.createNestedArray("times");
JsonObject relays_1_times_0 = relays_1_times.createNestedObject();
relays_1_times_0["startTime"] = "02:02";
relays_1_times_0["duration"] = 32;
relays_1_times_0["hour"] = 2;
relays_1_times_0["min"] = 2;
JsonObject relays_1_times_1 = relays_1_times.createNestedObject();
relays_1_times_1["startTime"] = "02:02";
relays_1_times_1["duration"] = 32;
relays_1_times_1["hour"] = 2;
relays_1_times_1["min"] = 2;
JsonObject relays_2 = relays.createNestedObject();
relays_2["name"] = "Cypress Hill";
relays_2["pin"] = 33;
relays_2["isRunning"] = 1;
relays_2["isEnabled"] = 0;
relays_2["status"] = 1;
JsonArray relays_2_times = relays_2.createNestedArray("times");
JsonObject relays_2_times_0 = relays_2_times.createNestedObject();
relays_2_times_0["startTime"] = "03:03";
relays_2_times_0["duration"] = 60;
relays_2_times_0["hour"] = 3;
relays_2_times_0["min"] = 3;
JsonObject relays_2_times_1 = relays_2_times.createNestedObject();
relays_2_times_1["startTime"] = "03:03";
relays_2_times_1["duration"] = 60;
relays_2_times_1["hour"] = 3;
relays_2_times_1["min"] = 3;
JsonObject relays_2_times_2 = relays_2_times.createNestedObject();
relays_2_times_2["startTime"] = "03:03";
relays_2_times_2["duration"] = 60;
relays_2_times_2["hour"] = 3;
relays_2_times_2["min"] = 3;
serializeJson(doc, Serial);
Com a ajuda do meu amigo Jeff Knight, sem o qual eu não teria conseguido sozinho, o código gerado foi simplificado para o meu programa. Depois criei algumas funções, e a imagem a seguir mostra o monitor serial imprimindo o resultado da desserialização do arquivo JSON lido.

Fiz este diagrama para não esquecer como funciona:

No fim, funciona assim:
← (29 abr) Moldagem e Fundição · todas as semanas · Programação de Interfaces e Aplicações (13 mai) →