Community project
Smart Humidity Controller

This smart humidity controller automatically maintains optimal moisture levels for plants, terrariums, or other environments using an ESP32 microcontroller. The system reads temperature and humidity from a DHT22 sensor, monitors water levels, and controls an ultrasonic mist maker through a relay module. Real-time data displays on an OLED screen, while remote monitoring and control happen through the Blynk 2.0 cloud platform.
This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions to build the controller from breadboard to working system. The included firmware handles sensor readings, relay switching, OLED display updates, and Blynk cloud integration with customizable humidity and temperature thresholds. Makers will learn how to wire multiple sensors to an ESP32, control AC/DC devices with relays, and create an IoT application with real-time alerts.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| DHT22 | 1 | Digital temperature and humidity sensor |
| 1-Channel 5V Relay Module (3.3V trigger compatible) | 1 | Single-channel 5V relay module with optocoupler isolation. Most common Chinese relay modules (SRD-05VDC-SL-C) trigger reliably from 3.3V GPIO when the IN pin is active-LOW — the optocoupler LED forward voltage is ~1.2V so 3.3V is sufficient. VCC is 5V (coil), GND and IN come from the ESP32. COM/NO are the switched relay contacts for the mist maker 5V line. |
| Ultrasonic Mist Maker Module (5V) | 1 | 5V USB ultrasonic piezoelectric mist maker disc with driver board. Power-switched via relay for on/off control. Do not drive the disc directly from GPIO. Must be submerged in water during operation. |
| Water Level Sensor | 1 | An analog water level sensor module that outputs a voltage proportional to the water level. It uses ten interleaved copper traces and an S8050 NPN transistor to convert water conductivity into an analog voltage signal. The higher the water level, the higher the output voltage. Operates on 3.3V to 5V. To extend sensor lifespan, it is recommended to power the sensor only during readings by controlling the VCC pin via a digital output pin rather than connecting it permanently to 5V. |
| SSD1306 OLED | 1 | 0.96 inch 128x64 OLED display with I2C interface |
Assembly
8 stepsMateriais necessários
Separa todos os componentes: ESP32 DevKit v1, DHT22, módulo relé 5V, módulo de névoa ultrassónico 5V (com placa USB), sensor de nível de água, OLED SSD1306 0.96", breadboard, fios jumper, cabo USB para o ESP32, carregador USB separado para o módulo de névoa.
- Tip: Usa uma breadboard grande — tens 5 componentes para ligar.
- Tip: Prepara fios de cores diferentes: vermelho=5V, laranja=3.3V, preto=GND, outras cores para sinais.
Coloca o ESP32 na breadboard
Encaixa o ESP32 DevKit v1 ao centro da breadboard, com cada fila de pinos num lado diferente. Deixa espaço suficiente em redor para os fios.
- Tip: O ESP32 DevKit ocupa quase toda a largura de uma breadboard de 30 colunas — usa uma de 60 colunas se tiveres.
Liga o OLED SSD1306
Liga o display OLED SSD1306 ao ESP32 pelos pinos I2C: • VCC → 3.3V do ESP32 • GND → GND do ESP32 • SDA → GPIO21 • SCL → GPIO22 O display fica imediatamente funcional — ao ligar o ESP32 deves ver o ecrã de arranque.
- Tip: Muitos módulos OLED têm a ordem VCC/GND trocada — verifica a serigrafia da tua placa antes de ligar.
- Tip: O OLED funciona a 3.3V — não ligues ao 5V ou queimas o ecrã.
- ⚠ Verifica sempre a ordem dos pinos no teu módulo OLED específico — não é universal.
Liga o DHT22
Liga o sensor DHT22: • VCC → 3.3V do ESP32 • GND → GND do ESP32 • DATA → GPIO4 Se o teu DHT22 vier como módulo (3 pinos), tem resistência de pull-up integrada. Se vier como sensor simples (4 pinos), adiciona uma resistência de 10kΩ entre VCC e DATA.
- Tip: Coloca o DHT22 longe do módulo de névoa — pelo menos 30 cm. A névoa satura o sensor e dará sempre 100%.
Liga o módulo relé
Liga o módulo relé 5V: • VCC do relé → 5V (VIN) do ESP32 • GND do relé → GND do ESP32 • IN do relé → GPIO26 Nos terminais de potência do relé: • COM → fio vermelho (VCC) cortado do cabo USB do módulo de névoa • NO → fio vermelho que vai para o módulo de névoa
- Tip: Usa um cabo USB barato para cortar — não estragues um cabo de qualidade.
- Tip: O fio preto (GND) do cabo USB vai direto ao carregador, sem passar pelo relé.
- ⚠ Verifica que os fios cortados estão bem isolados com fita isolante ou termoretrátil antes de ligar à corrente.
Liga o sensor de nível de água
Liga o sensor de nível de água: • + (VCC) → GPIO27 (alimentado só durante leitura) • - (GND) → GND do ESP32 • S (SIG) → GPIO32 Positiona o sensor verticalmente dentro do reservatório, com os pentes de cobre submersos.
- Tip: Submerge apenas a parte dos pentes — a parte eletrónica (componentes) não deve entrar em contacto com água.
- Tip: Usa silicone ou cola quente para fixar o sensor à parede do reservatório na posição vertical.
- ⚠ Nunca submerjas a placa de controlo do sensor — só os pentes condutores.
Monta o reservatório e módulo de névoa
Coloca o módulo de névoa ultrassónico num recipiente não metálico (plástico ou cerâmica) com água. O disco piezoelétrico deve ficar submerso entre 3 a 5 cm abaixo da superfície da água. Liga o cabo USB do módulo ao carregador USB externo (separado do ESP32).
- Tip: Usa água destilada ou filtrada para evitar depósitos de calcário no disco.
- Tip: O reservatório deve ter pelo menos 1 litro para uma autonomia razoável.
- ⚠ Nunca ligues o módulo de névoa sem água — o disco funde em segundos a seco.
- ⚠ Mantém toda a eletrónica (ESP32, relé, OLED) longe de salpicos de água.
Verificação final e Deploy
Antes de ligar: 1. Verifica todas as ligações contra a tabela de pinos 2. Confirma que os fios cortados do USB estão bem isolados 3. Liga o ESP32 via USB ao computador 4. Substitui as credenciais Wi-Fi e Blynk no firmware 5. Faz Deploy pelo botão Schematik 6. Abre o Monitor Série — deves ver as leituras a cada 5 segundos 7. Calibra o sensor de nível: anota o valor com sensor a seco (RAW_EMPTY) e submerso (RAW_FULL), atualiza o firmware e faz novo Deploy
- Tip: O display OLED deve acender com o ecrã de arranque logo ao ligar — é uma boa confirmação de que o I2C está correto.
- Tip: Se o OLED não aparecer, verifica se o endereço I2C é 0x3C ou 0x3D (alguns módulos usam 0x3D) — edita OLED_ADDRESS no código.
- ⚠ Só liga o carregador USB do módulo de névoa depois de confirmar que o firmware está a correr e o relé responde aos comandos.
Firmware
ESP32#include <Arduino.h>
// ============================================================
// Orquidea IA v1.0
// Humidificador Inteligente com Blynk 2.0 + Display OLED
// ESP32 + DHT22 + Módulo de Névoa Ultrassónico (via relé)
// + Sensor de Nível de Água + SSD1306 OLED 0.96"
// ============================================================
//
// ANTES DE COMPILAR:
// 1. Cria uma conta gratuita em https://blynk.io
// 2. Cria um novo Template (Hardware: ESP32, Connection: WiFi)
// 3. Cria os seguintes Datastreams virtuais:
// V0 – Temperature (Double, -10 a 60 °C) → Gauge
// V1 – Humidity (Double, 0 a 100 %) → Gauge
// V2 – Mist On/Off (Integer, 0 / 1) → Switch
// V3 – Target Humidity (Double, 30 a 95 %) → Slider
// V4 – Water Level % (Double, 0 a 100 %) → Gauge
// V5 – Water Alert (Integer, 0 / 1) → LED / Notification
// V6 – Hum Min (Double, 30 a 95 %) → Slider
// V7 – Hum Max (Double, 30 a 95 %) → Slider
// V8 – Temp Min (Double, 0 a 50 °C) → Slider
// V9 – Temp Max (Double, 0 a 50 °C) → Slider
// 4. Substitui as credenciais abaixo com os teus dados.
// 5. Para notificações de temperatura: cria um Event no Blynk
// com o nome "temp_alert" (Events → Create Event).
// O firmware dispara-o automaticamente quando a temperatura
// sair da faixa definida nos sliders V8 e V9.
// 6. Para notificações de água baixa: configura um Automation
// no Blynk que dispara quando V5 == 1 e envia notificação push.
// ============================================================
#define BLYNK_TEMPLATE_ID "TMPLxxxxxxxx" // ← substitui
#define BLYNK_TEMPLATE_NAME "Humidificador" // ← substitui
#define BLYNK_AUTH_TOKEN "YourAuthToken" // ← substitui
#include <WiFi.h>
#include <BlynkSimpleEsp32.h>
#include <DHT.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <time.h>
// ── Orquídea pixel art 32x32 (PROGMEM) ──────────────────────
// Flor estilizada de orquídea — 5 pétalas + centro
static const unsigned char PROGMEM orchid_bmp[] = {
0x00,0x00,0x00,0x00,
0x00,0x01,0x80,0x00,
0x00,0x03,0xC0,0x00,
0x00,0x07,0xE0,0x00,
0x00,0x0F,0xF0,0x00,
0x00,0x1C,0x38,0x00,
0x00,0x38,0x1C,0x00,
0x00,0x70,0x0E,0x00,
0x03,0xE0,0x07,0xC0,
0x07,0xC0,0x03,0xE0,
0x0F,0x80,0x01,0xF0,
0x1F,0x00,0x00,0xF8,
0x1E,0x03,0xC0,0x78,
0x3C,0x07,0xE0,0x3C,
0x38,0x0F,0xF0,0x1C,
0x30,0x1F,0xF8,0x0C,
0x30,0x1F,0xF8,0x0C,
0x38,0x0F,0xF0,0x1C,
0x3C,0x07,0xE0,0x3C,
0x1E,0x03,0xC0,0x78,
0x1F,0x00,0x00,0xF8,
0x0F,0x80,0x01,0xF0,
0x07,0xC0,0x03,0xE0,
0x03,0xE0,0x07,0xC0,
0x00,0x70,0x0E,0x00,
0x00,0x38,0x1C,0x00,
0x00,0x1C,0x38,0x00,
0x00,0x0F,0xF0,0x00,
0x00,0x07,0xE0,0x00,
0x00,0x03,0xC0,0x00,
0x00,0x01,0x80,0x00,
0x00,0x00,0x00,0x00
};
// ── Credenciais Wi-Fi ────────────────────────────────────────
char ssid[] = "O_Teu_WiFi"; // ← substitui
char pass[] = "A_Tua_Password"; // ← substitui
// ── Pinos ────────────────────────────────────────────────────
#define DHT_PIN 4
#define RELAY_PIN 26
#define WATER_SIG_PIN 32
#define WATER_PWR_PIN 27
#define OLED_SDA 21
#define OLED_SCL 22
// ── OLED ─────────────────────────────────────────────────────
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// ── DHT22 ────────────────────────────────────────────────────
#define DHTTYPE DHT22
DHT dht(DHT_PIN, DHTTYPE);
// ── NTP / Data-Hora ──────────────────────────────────────────
// String POSIX oficial de Portugal — muda automaticamente em
// março (último domingo) e outubro (último domingo)
#define NTP_SERVER "pool.ntp.org"
#define TZ_PORTUGAL "WET0WEST,M3.5.0/1,M10.5.0"
char timeStr[9] = "--:--:--";
char dateStr[11] = "--/--/----";
// ── Estado ───────────────────────────────────────────────────
BlynkTimer timer;
float targetHumidity = 60.0;
float humMin = 60.0; // faixa mínima (Blynk V6)
float humMax = 70.0; // faixa máxima (Blynk V7)
float tempMin = 20.0; // faixa mínima temperatura (Blynk V8)
float tempMax = 28.0; // faixa máxima temperatura (Blynk V9)
bool manualOverride = false;
bool mistRunning = false;
// Histerese ±2%
const float HYST = 2.0;
// Alertas de temperatura
bool tempAlertLow = false; // abaixo de tempMin
bool tempAlertHigh = false; // acima de tempMax
// Nível de água
const int RAW_EMPTY = 0;
const int RAW_FULL = 3200;
const float WATER_LOW_THRESHOLD = 20.0;
bool waterAlertSent = false;
// Últimos valores lidos (para o display)
float lastTemp = 0.0;
float lastHumidity = 0.0;
float lastWater = 0.0;
// ── Forward declarations ──────────────────────────────────────
void mistOn();
void mistOff();
void updateDisplay();
void showBootScreen();
float readWaterLevel();
void readAndControl();
void updateTimeStrings();
// ── Relé: activo-LOW ─────────────────────────────────────────
void mistOn() { digitalWrite(RELAY_PIN, LOW); mistRunning = true; }
void mistOff() { digitalWrite(RELAY_PIN, HIGH); mistRunning = false; }
// ── Hora NTP ─────────────────────────────────────────────────
void updateTimeStrings() {
struct tm tinfo;
if (!getLocalTime(&tinfo)) return;
strftime(timeStr, sizeof(timeStr), "%H:%M:%S", &tinfo);
strftime(dateStr, sizeof(dateStr), "%d/%m/%Y", &tinfo);
}
// ── Ecrã de arranque ─────────────────────────────────────────
void showBootScreen() {
display.clearDisplay();
// Orquídea à esquerda (32x32)
display.drawBitmap(2, 16, orchid_bmp, 32, 32, SSD1306_WHITE);
// Título
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(40, 10);
display.print(F("Orquidea IA"));
// Versão
display.setCursor(40, 22);
display.print(F("v1.0"));
// Linha separadora vertical
display.drawLine(37, 8, 37, 50, SSD1306_WHITE);
// Estado
display.setCursor(40, 36);
display.print(F("A ligar WiFi..."));
display.display();
}
// ── Display principal ─────────────────────────────────────────
void updateDisplay() {
updateTimeStrings();
display.clearDisplay();
// ══════════════════════════════════════════════════════════
// LINHA 1 — Título + versão
// ══════════════════════════════════════════════════════════
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(2, 0);
display.print(F("Orquidea IA"));
display.setCursor(92, 0);
display.print(F("v1.0"));
display.drawLine(0, 9, 127, 9, SSD1306_WHITE);
// ══════════════════════════════════════════════════════════
// LINHA 2 — Temperatura | gama 20C - 28C
// ══════════════════════════════════════════════════════════
display.setCursor(0, 12);
display.print(F("Temp:"));
display.setCursor(32, 12);
display.print(lastTemp, 1);
display.print(F(" C"));
// divisor vertical
display.drawLine(79, 10, 79, 22, SSD1306_WHITE);
display.setCursor(82, 12);
display.print((int)tempMin);
display.print(F("C-"));
display.print((int)tempMax);
display.print(F("C"));
// ══════════════════════════════════════════════════════════
// LINHA 3 — Humidade | gama 60% - 70%
// ══════════════════════════════════════════════════════════
display.setCursor(0, 23);
display.print(F("Hum: "));
display.setCursor(32, 23);
display.print(lastHumidity, 1);
display.print(F(" %"));
display.drawLine(79, 22, 79, 34, SSD1306_WHITE);
display.setCursor(82, 23);
display.print((int)humMin);
display.print(F("%"));
display.print((int)humMax);
display.print(F("%"));
// Linha separadora
display.drawLine(0, 33, 127, 33, SSD1306_WHITE);
// ══════════════════════════════════════════════════════════
// LINHA 4 — Nebulizador + Nível de água
// ══════════════════════════════════════════════════════════
display.setCursor(0, 36);
if (mistRunning) {
display.print(F("NEBULIZ:ON "));
} else {
display.print(F("NEBULIZ:OFF"));
}
// Barra de água
display.setCursor(68, 36);
display.print(F("H2O:"));
int barW = 34;
int filled = (int)((lastWater / 100.0f) * barW);
filled = constrain(filled, 0, barW);
display.drawRect(90, 36, barW + 2, 7, SSD1306_WHITE);
display.fillRect(91, 37, filled, 5, SSD1306_WHITE);
// Percentagem + alerta
display.setCursor(68, 46);
display.print(F("H2O:"));
display.print((int)lastWater);
display.print(F("%"));
if (waterAlertSent) {
display.setCursor(112, 46);
display.print(F("!"));
}
// Linha separadora
display.drawLine(0, 54, 127, 54, SSD1306_WHITE);
// ══════════════════════════════════════════════════════════
// LINHA 5 — Data / Hora (NTP)
// ══════════════════════════════════════════════════════════
display.setCursor(0, 56);
display.print(dateStr);
display.setCursor(74, 56);
display.print(timeStr);
display.display();
}
// ── Blynk: interruptor manual (V2) ──────────────────────────
BLYNK_WRITE(V2) {
int val = param.asInt();
if (val == 1) {
manualOverride = true;
mistOn();
} else {
manualOverride = false;
}
updateDisplay();
}
// ── Blynk: humidade alvo pelo slider (V3) ───────────────────
BLYNK_WRITE(V3) {
targetHumidity = param.asFloat();
Serial.print(F("Nova humidade alvo: "));
Serial.print(targetHumidity);
Serial.println(F(" %"));
updateDisplay();
}
// ── Blynk: faixa mínima de humidade (V6) ────────────────────
BLYNK_WRITE(V6) {
humMin = param.asFloat();
// Garante que min não ultrapassa max
if (humMin > humMax) humMin = humMax - 1.0;
// Atualiza alvo para centro da faixa
targetHumidity = (humMin + humMax) / 2.0;
Serial.print(F("Faixa hum: "));
Serial.print(humMin); Serial.print(F("% - "));
Serial.print(humMax); Serial.println(F("%"));
updateDisplay();
}
// ── Blynk: faixa máxima de humidade (V7) ────────────────────
BLYNK_WRITE(V7) {
humMax = param.asFloat();
// Garante que max não é inferior a min
if (humMax < humMin) humMax = humMin + 1.0;
// Atualiza alvo para centro da faixa
targetHumidity = (humMin + humMax) / 2.0;
Serial.print(F("Faixa hum: "));
Serial.print(humMin); Serial.print(F("% - "));
Serial.print(humMax); Serial.println(F("%"));
updateDisplay();
}
// ── Blynk: temperatura mínima (V8) ──────────────────────────
BLYNK_WRITE(V8) {
tempMin = param.asFloat();
if (tempMin > tempMax) tempMin = tempMax - 1.0;
// Reset alertas para reavaliar com nova faixa
tempAlertLow = false;
tempAlertHigh = false;
Serial.print(F("Faixa temp: "));
Serial.print(tempMin); Serial.print(F("C - "));
Serial.print(tempMax); Serial.println(F("C"));
updateDisplay();
}
// ── Blynk: temperatura máxima (V9) ──────────────────────────
BLYNK_WRITE(V9) {
tempMax = param.asFloat();
if (tempMax < tempMin) tempMax = tempMin + 1.0;
// Reset alertas para reavaliar com nova faixa
tempAlertLow = false;
tempAlertHigh = false;
Serial.print(F("Faixa temp: "));
Serial.print(tempMin); Serial.print(F("C - "));
Serial.print(tempMax); Serial.println(F("C"));
updateDisplay();
}
// ── Leitura do nível de água ─────────────────────────────────
float readWaterLevel() {
digitalWrite(WATER_PWR_PIN, HIGH);
delay(10);
int raw = analogRead(WATER_SIG_PIN);
digitalWrite(WATER_PWR_PIN, LOW);
float pct = ((float)(raw - RAW_EMPTY) / (float)(RAW_FULL - RAW_EMPTY)) * 100.0f;
return constrain(pct, 0.0f, 100.0f);
}
// ── Leitura e controlo principal ─────────────────────────────
void readAndControl() {
float humidity = dht.readHumidity();
float temperature = dht.readTemperature();
if (isnan(humidity) || isnan(temperature)) {
Serial.println(F("Erro: falha na leitura do DHT22!"));
return;
}
float waterPct = readWaterLevel();
// Guarda para o display
lastTemp = temperature;
lastHumidity = humidity;
lastWater = waterPct;
Serial.print(F("Temp: ")); Serial.print(temperature, 1);
Serial.print(F(" C | Hum: ")); Serial.print(humidity, 1);
Serial.print(F(" % | Alvo: ")); Serial.print(targetHumidity, 1);
Serial.print(F(" % | Agua: ")); Serial.print(waterPct, 0);
Serial.println(F(" %"));
// Envia para Blynk
Blynk.virtualWrite(V0, temperature);
Blynk.virtualWrite(V1, humidity);
Blynk.virtualWrite(V4, waterPct);
// ── Alertas de temperatura ────────────────────────────────
if (temperature < tempMin) {
if (!tempAlertLow) {
Blynk.logEvent("temp_alert",
String("Temperatura baixa! ") + String(temperature, 1) + String(" C (min: ") + String(tempMin, 1) + String(" C)"));
tempAlertLow = true;
tempAlertHigh = false;
Serial.println(F("ALERTA: Temperatura abaixo do minimo!"));
}
} else if (temperature > tempMax) {
if (!tempAlertHigh) {
Blynk.logEvent("temp_alert",
String("Temperatura alta! ") + String(temperature, 1) + String(" C (max: ") + String(tempMax, 1) + String(" C)"));
tempAlertHigh = true;
tempAlertLow = false;
Serial.println(F("ALERTA: Temperatura acima do maximo!"));
}
} else {
// Temperatura voltou à faixa — reseta alertas
tempAlertLow = false;
tempAlertHigh = false;
}
// ── Alerta nível baixo ─────────────────────────────────────
if (waterPct < WATER_LOW_THRESHOLD) {
if (!waterAlertSent) {
Blynk.virtualWrite(V5, 1);
waterAlertSent = true;
Serial.println(F("ALERTA: Nivel de agua baixo!"));
}
// Abaixo de 10% desliga nebulizador para proteger disco
if (waterPct < 10.0f && !manualOverride) {
mistOff();
Blynk.virtualWrite(V2, 0);
}
} else {
if (waterAlertSent) {
Blynk.virtualWrite(V5, 0);
waterAlertSent = false;
}
}
// ── Controlo automático ────────────────────────────────────
if (!manualOverride) {
// Controla com base na faixa definida (humMin / humMax)
if (humidity < humMin) {
mistOn();
Blynk.virtualWrite(V2, 1);
} else if (humidity > humMax) {
mistOff();
Blynk.virtualWrite(V2, 0);
}
}
updateDisplay();
}
// ── Setup ─────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
mistOff();
pinMode(WATER_PWR_PIN, OUTPUT);
digitalWrite(WATER_PWR_PIN, LOW);
// Inicia I2C e OLED
Wire.begin(OLED_SDA, OLED_SCL);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
Serial.println(F("OLED nao encontrado!"));
} else {
showBootScreen();
}
dht.begin();
Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
// Sincroniza hora via NTP com fuso horário automático de Portugal
// Muda sozinho para horário de verão/inverno (março e outubro)
configTzTime(TZ_PORTUGAL, NTP_SERVER);
delay(1500);
// Timer de leitura e controlo (5s)
timer.setInterval(5000L, readAndControl);
// Timer de atualização do ecrã (1s) — mantém hora em tempo real
timer.setInterval(1000L, updateDisplay);
Serial.println(F("Orquidea IA v1.0 iniciada!"));
}
// ── Loop ──────────────────────────────────────────────────────
void loop() {
Blynk.run();
timer.run();
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
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.