Community project

Connected Tree Health Monitor

Photo of Connected Tree Health Monitor
Generated with AI

FB-Prof(a). Rosangela Nezi

Published November 4, 2026

The Connected Tree Health Monitor uses an ESP32 microcontroller to continuously track the wellbeing of trees by measuring soil moisture, air temperature, humidity, atmospheric pressure, and ambient light. Three I2C sensors—an Adafruit STEMMA capacitive soil moisture sensor, a BME280 environmental sensor, and a BH1750 light sensor—feed real-time data to the ESP32, which stores readings locally and uploads them to a remote server every 15 minutes.

This guide provides a complete parts list, wiring diagram, and step-by-step assembly instructions for mounting the weatherproof IP65/IP66 enclosure directly on a tree trunk. Included is the full Arduino firmware with WiFi connectivity, a web-based setup interface, and intelligent soil condition analysis that interprets moisture levels to help growers optimize tree care.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

Adafruit STEMMA Soil Sensor – I2C Capacitive Moisture Sensor

Adafruit STEMMA capacitive soil moisture sensor using an I2C interface. Operates at 3.3V or 5V. Reports moisture (and optionally temperature) via I2C at default address 0x36. Compatible with common 3.3V/5V microcontroller and Raspberry Pi I2C hosts. No analog pin required; uses internal SAMD10 microcontroller to digitise readings over I2C.

1

Adafruit BME280 I2C or SPI Temperature Humidity Pressure Sensor

Adafruit breakout for the Bosch BME280 sensor, measuring temperature, relative humidity and barometric pressure. It talks over I2C (address 0x77, or 0x76 with SDO to ground) or SPI, runs from 3 to 5 V through an on-board regulator with level shifting, and connects through STEMMA QT/Qwiic connectors or a 7-pin 0.1 in header.

1

BH1750

Rohm BH1750FVI 16-bit digital ambient light sensor with I2C interface. Measures illuminance from 1 to 65535 lux. Two selectable I2C addresses (0x23 / 0x5C) via ADDR pin.

1

Caixa plástica externa IP65/IP66 com tampa

A caixa selada que protege o ESP32 e as conexões da chuva enquanto os sensores ficam no local onde medem.

Assemble it in 6 steps

1. Deixe a placa sem energia

Coloque o ESP32 DevKit V1 e os três módulos sobre uma mesa seca. Não conecte o cabo USB ainda; assim você não encosta fios de energia no lugar errado enquanto monta.

  • Use fios curtos para os sensores que ficarão perto da placa.
  • Não alimente nenhum sensor com 5 V: estes três módulos serão ligados ao pino 3V3 do ESP32.

2. Ligue o sensor de umidade do solo

No sensor soil_sensor, ligue VCC ao pino 3V3 do ESP32 (energia), GND a um pino GND (terra), SDA ao GPIO21 (dados) e SCL ao GPIO22 (relógio).

  • Os pinos GPIO21 e GPIO22 podem estar impressos como 21 e 22 na placa.
  • Insira somente a parte de medição do sensor na terra; deixe a placa eletrônica fora da região molhada.
  • Não mergulhe a placa do sensor nem as conexões na terra molhada; água nas conexões pode causar leituras erradas ou curto-circuito.

3. Ligue o sensor de clima

No climate_sensor, ligue VIN ao 3V3 do ESP32 (energia), GND ao GND (terra), SCK ao GPIO22 (relógio) e SDI ao GPIO21 (dados). Ligue também SDO ao GND; isso faz o módulo usar o endereço correto nos fios compartilhados.

  • SCK no módulo BME280 é o mesmo fio chamado SCL em outros módulos, e SDI é o mesmo fio chamado SDA.
  • Deixe CS sem ligar: ele não é usado neste modo.
  • Não troque VIN e GND, pois energia invertida pode danificar o módulo.

4. Ligue o sensor de luz

No light_sensor, ligue VCC ao 3V3 do ESP32 (energia), GND ao GND (terra), SDA ao GPIO21 (dados), SCL ao GPIO22 (relógio) e ADDR ao GND. Os três sensores podem usar os mesmos fios GPIO21 e GPIO22 porque cada um tem seu próprio endereço.

  • Posicione este módulo onde ele receba a luz que chega à árvore, mas proteja-o de chuva direta.
  • Não ligue ADDR ao 3V3 nesta montagem: ele precisa ficar no GND para coincidir com a configuração do programa.

5. Prepare a caixa que fica no tronco

Coloque o ESP32 e as emendas de fios dentro da weatherproof_box. Faça as passagens dos cabos pela parte de baixo e use prensa-cabos ou vedação própria para que a água não entre. Antes de cada cabo entrar na caixa, deixe uma pequena volta voltada para baixo; a chuva pinga ali em vez de correr pelo fio para dentro.

  • Uma caixa de aproximadamente 160 × 120 × 75 mm costuma comportar a placa e as conexões.
  • Mantenha a porta USB acessível para poder ligar o cabo quando precisar atualizar o ESP32.
  • Não coloque o BME280 dentro da caixa fechada: o calor da placa faria a temperatura medida parecer maior que a temperatura do ar.

6. Prenda a caixa e posicione os sensores

Prenda a weatherproof_box em uma estaca ou no lado sombreado do tronco, a 30–80 cm do chão, usando uma cinta larga ajustável. Não aperte a cinta: a árvore cresce. Enterre a parte de medição do soil_sensor a 10–20 cm de profundidade, longe de raízes grossas. Deixe climate_sensor do lado de fora, sob uma pequena cobertura ventilada que bloqueie sol e chuva. Fixe light_sensor voltado para cima, sob uma proteção transparente inclinada para a água escorrer.

  • Em árvores maiores, coloque o sensor de solo aproximadamente sob a borda da copa, onde há raízes que absorvem água.
  • Faça os cabos descerem pelo lado de trás da caixa para ficarem menos visíveis e receberem menos chuva direta.
  • Não use arame, pregos ou abraçadeiras finas no tronco; eles podem cortar a casca conforme a árvore cresce.
  • Não deixe água acumulada ao redor do sensor de solo, pois isso mostra uma umidade que não representa o restante da terra.

Review all connections

1. Connections between "soil_sensor" and "ESP32"

Functionsoil_sensorESP32
powerVCC → 3.3 V power3V3
groundGND → common groundGND
i2cSDA → shared sensor dataGPIO 21
i2cSCL → shared sensor clockGPIO 22

2. Connections between "climate_sensor" and "ESP32"

Functionclimate_sensorESP32
powerVIN → 3.3 V power3V3
groundGND → common groundGND
groundSDO → selects I2C address 0x76GND
i2cSCK → Adafruit STEMMA Soil Sensor – I2C Capacitive Moisture Sensor SCLEXT
i2cSDI → Adafruit STEMMA Soil Sensor – I2C Capacitive Moisture Sensor SDAEXT

3. Connections between "light_sensor" and "ESP32"

Functionlight_sensorESP32
powerVCC → 3.3 V power3V3
groundGND → common groundGND
groundADDR → selects I2C address 0x23GND
i2cSDA → Adafruit STEMMA Soil Sensor – I2C Capacitive Moisture Sensor SDAEXT
i2cSCL → Adafruit STEMMA Soil Sensor – I2C Capacitive Moisture Sensor SCLEXT

Deploy the firmware

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <Adafruit_seesaw.h>
#include <BH1750.h>

#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22

const char *AP_NAME = "SaudeArvore-Setup";
const char *AP_PASSWORD = "Arvore2026";
const unsigned long READ_INTERVAL_MS = 10000UL;
const unsigned long UPLOAD_INTERVAL_MS = 900000UL;

WebServer server(80);
Preferences preferences;
Adafruit_BME280 bme;
Adafruit_seesaw soil;
BH1750 luxMeter;

bool bmeReady = false;
bool soilReady = false;
bool lightReady = false;
float airTemperature = NAN;
float airHumidity = NAN;
float airPressure = NAN;
float lightLux = NAN;
uint16_t soilRaw = 0;
unsigned long lastReadMs = 0;
unsigned long lastUploadMs = 0;

String htmlEscape(const String &text) {
  String out;
  for (size_t i = 0; i < text.length(); i++) {
    char c = text[i];
    if (c == '&') out += "&amp;";
    else if (c == '<') out += "&lt;";
    else if (c == '>') out += "&gt;";
    else if (c == '\"') out += "&quot;";
    else out += c;
  }
  return out;
}

String numberOrDash(float value, uint8_t decimals) {
  return isnan(value) ? "--" : String(value, static_cast<unsigned int>(decimals));
}

String conditionText() {
  if (!soilReady || soilRaw == 0) return "Sensor de solo indisponivel";
  if (soilRaw < 350) return "Solo provavelmente muito seco";
  if (soilRaw > 900) return "Solo provavelmente muito umido";
  return "Umidade do solo em faixa intermediaria";
}

void readSensors() {
  if (bmeReady) {
    airTemperature = bme.readTemperature();
    airHumidity = bme.readHumidity();
    airPressure = bme.readPressure() / 100.0F;
  }
  if (soilReady) soilRaw = soil.touchRead(0);
  if (lightReady) lightLux = luxMeter.readLightLevel();
  Serial.printf("Solo:%u Temp:%.1fC Umidade:%.1f%% Pressao:%.1fhPa Luz:%.1flux\n", soilRaw, airTemperature, airHumidity, airPressure, lightLux);
}

String jsonPayload() {
  String body = "{";
  body += "\"soil_raw\":" + String(soilRaw) + ",";
  body += "\"air_temperature_c\":" + numberOrDash(airTemperature, 2) + ",";
  body += "\"air_humidity_percent\":" + numberOrDash(airHumidity, 2) + ",";
  body += "\"air_pressure_hpa\":" + numberOrDash(airPressure, 2) + ",";
  body += "\"light_lux\":" + numberOrDash(lightLux, 1);
  body += "}";
  return body;
}

void uploadIfConfigured() {
  String endpoint = preferences.getString("endpoint", "");
  if (WiFi.status() != WL_CONNECTED || endpoint.length() == 0) return;
  HTTPClient http;
  http.setTimeout(8000);
  http.begin(endpoint);
  http.addHeader("Content-Type", "application/json");
  int status = http.POST(jsonPayload());
  Serial.printf("Envio HTTP: %d\n", status);
  http.end();
}

void handleRoot() {
  String ssid = preferences.getString("ssid", "");
  String endpoint = preferences.getString("endpoint", "");
  String page = "<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>";
  page += "<style>body{font-family:Arial;max-width:680px;margin:24px auto;padding:0 16px;color:#173b22}h1{color:#27713a}.card{background:#eef8ef;padding:14px;margin:10px 0;border-radius:8px}input{width:100%;padding:9px;margin:5px 0 12px;box-sizing:border-box}button{padding:10px;background:#27713a;color:white;border:0;border-radius:5px}</style></head><body>";
  page += "<h1>Monitor da arvore</h1><div class='card'><b>Leitura atual</b><br>Solo (valor bruto): " + String(soilRaw) + "<br>Temperatura: " + numberOrDash(airTemperature, 1) + " C<br>Umidade do ar: " + numberOrDash(airHumidity, 1) + " %<br>Pressao: " + numberOrDash(airPressure, 1) + " hPa<br>Luz: " + numberOrDash(lightLux, 1) + " lux<br><b>Interpretacao:</b> " + conditionText() + "</div>";
  page += "<div class='card'><b>Configuracao de rede e envio</b><p>Deixe o endereco de envio vazio para usar somente esta pagina local.</p><form method='post' action='/save'>";
  page += "Nome da rede Wi-Fi<input name='ssid' value='" + htmlEscape(ssid) + "'>Senha Wi-Fi<input type='password' name='pass' placeholder='digite para alterar'>";
  page += "Endereco para receber JSON (HTTP/HTTPS)<input name='endpoint' value='" + htmlEscape(endpoint) + "' placeholder='https://seu-servidor/exemplo'>";
  page += "<button type='submit'>Salvar e reiniciar</button></form></div></body></html>";
  server.send(200, "text/html; charset=utf-8", page);
}

void handleSave() {
  if (server.hasArg("ssid")) preferences.putString("ssid", server.arg("ssid"));
  if (server.hasArg("pass") && server.arg("pass").length() > 0) preferences.putString("pass", server.arg("pass"));
  if (server.hasArg("endpoint")) preferences.putString("endpoint", server.arg("endpoint"));
  server.send(200, "text/html; charset=utf-8", "<p>Configuracao salva. O monitor vai reiniciar.</p>");
  delay(1000);
  ESP.restart();
}

void startNetwork() {
  String ssid = preferences.getString("ssid", "");
  String pass = preferences.getString("pass", "");
  if (ssid.length() > 0) {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid.c_str(), pass.c_str());
    unsigned long started = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - started < 20000UL) delay(250);
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("Conectado em: http://");
    Serial.println(WiFi.localIP());
  } else {
    WiFi.mode(WIFI_AP);
    WiFi.softAP(AP_NAME, AP_PASSWORD);
    Serial.print("Modo de configuracao: http://");
    Serial.println(WiFi.softAPIP());
  }
}

void setup() {
  Serial.begin(115200);
  preferences.begin("tree-monitor", false);
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  bmeReady = bme.begin(0x76);
  soilReady = soil.begin(0x36);
  lightReady = luxMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE, 0x23, &Wire);
  startNetwork();
  server.on("/", HTTP_GET, handleRoot);
  server.on("/save", HTTP_POST, handleSave);
  server.begin();
  readSensors();
}

void loop() {
  server.handleClient();
  unsigned long now = millis();
  if (now - lastReadMs >= READ_INTERVAL_MS) {
    lastReadMs = now;
    readSensors();
  }
  if (now - lastUploadMs >= UPLOAD_INTERVAL_MS) {
    lastUploadMs = now;
    uploadIfConfigured();
  }
  delay(10);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik