#include <Adafruit_BMP280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <Arduino.h>
#include <DHT.h>
#include <ESPAsyncWebServer.h>
#include <RTClib.h>
#include <SPI.h>
#include <WiFi.h>
#include <Wire.h>
#include <driver/i2s.h>
#include <time.h>
#define WIFI_SSID "xxx" // put your wifi ssid
#define WIFI_PASS "xxx" // put your wifi password
#define DEVICE_IP "xxx.xxx.xxx.xxx" // put your ip
#define NTP_SERVER_1 "pool.ntp.org"
#define NTP_SERVER_2 "time.nist.gov"
#define GMT_OFFSET_SEC \
19800
#define DAYLIGHT_OFFSET_S 0
#define TFT_CS 4
#define TFT_RST 5
#define TFT_DC 6
#define TFT_MOSI 7
#define TFT_SCLK 15
#define MIC_SCK 16
#define MIC_WS 17
#define MIC_SD 18
#define I2C_SDA 8
#define I2C_SCL 9
#define BMP_SDA I2C_SDA
#define BMP_SCL I2C_SCL
#define SEALEVELPRESSURE_HPA (1013.25)
#define DHTPIN 13
#define DHTTYPE DHT22
#define MQ4_ANALOG_PIN 1
#define TURB_ANALOG_PIN 2
#define TDS_ANALOG_PIN 3
#define MOISTURE_ANALOG_PIN \
14
#define SOIL_DRY_AIR_RAW 3200
#define SOIL_WET_WATER_RAW 1300
#define ENCODER_CLK 10
#define ENCODER_DT 11
#define ENCODER_SW 12
#define SERVO_PIN 21
#define SERVO_MIN_ANGLE 0
#define SERVO_MAX_ANGLE 180
#define SERVO_CENTER_ANGLE 90
#define FIRE_SERVO_ANGLE 58
#define PUMP_ENA 38
#define PUMP_IN1 39
#define PUMP_IN2 40
bool pumpOn = false;
void startPump() {
if (pumpOn)
return;
digitalWrite(PUMP_IN1, HIGH);
digitalWrite(PUMP_IN2, LOW);
analogWrite(PUMP_ENA, 100);
pumpOn = true;
Serial.println("[PUMP] Fire detected -> Water pump ON (GPIO 38/39/40)");
}
void stopPump() {
if (!pumpOn)
return;
analogWrite(PUMP_ENA, 0);
digitalWrite(PUMP_IN1, LOW);
digitalWrite(PUMP_IN2, LOW);
pumpOn = false;
Serial.println("[PUMP] Fire clear -> Water pump OFF");
}
const int SERVO_PWM_CHANNEL = 0;
const int SERVO_PWM_FREQ = 50;
const int SERVO_PWM_RES =
14;
int currentServoAngle = SERVO_CENTER_ANGLE;
int targetServoAngle = SERVO_CENTER_ANGLE;
bool fireTrackingActive = false;
unsigned long lastFireMsgTime = 0;
WiFiServer tcpServer(8888);
void applyServoPWM(int angle) {
angle = constrain(angle, 0, 180);
uint32_t duty = map(angle, 0, 180, 410, 2048);
ledcWrite(SERVO_PWM_CHANNEL, duty);
}
void setServoAngle(int angle, bool fromFireTracker = false) {
currentServoAngle = constrain(angle, SERVO_MIN_ANGLE, SERVO_MAX_ANGLE);
targetServoAngle = currentServoAngle;
applyServoPWM(
currentServoAngle);
if (fromFireTracker) {
fireTrackingActive = true;
lastFireMsgTime = millis();
}
}
void initServo() {
ledcSetup(SERVO_PWM_CHANNEL, SERVO_PWM_FREQ, SERVO_PWM_RES);
ledcAttachPin(SERVO_PIN, SERVO_PWM_CHANNEL);
setServoAngle(SERVO_CENTER_ANGLE, false);
Serial.printf("[OK] Extinguisher Servo initialized on GPIO %d (Box limits: "
"%d° to %d°)\n",
SERVO_PIN, SERVO_MIN_ANGLE, SERVO_MAX_ANGLE);
}
void handleTcpClients() {
WiFiClient client = tcpServer.available();
if (client) {
while (client.connected() && client.available()) {
String line = client.readStringUntil('\n');
line.trim();
if (line.startsWith("F:")) {
int val = line.substring(2).toInt();
if (val == 0) {
fireTrackingActive = false;
setServoAngle(SERVO_CENTER_ANGLE, false);
stopPump();
} else {
setServoAngle(val, true);
startPump();
}
client.println("OK:" + String(currentServoAngle));
} else if (line.startsWith("SET:")) {
int val = line.substring(4).toInt();
setServoAngle(val, false);
client.println("OK:" + String(currentServoAngle));
}
}
client.stop();
}
}
#define MIC_SAMPLE_RATE 16000
#define MIC_RECORD_SECS 5
#define MIC_NUM_SAMPLES (MIC_SAMPLE_RATE * MIC_RECORD_SECS)
#define WAV_HEADER_SIZE 44
#define WAV_DATA_SIZE (MIC_NUM_SAMPLES * 2)
#define WAV_FILE_SIZE (WAV_HEADER_SIZE + WAV_DATA_SIZE)
#define COLOR_BG 0x0000
#define COLOR_HEADER_BG 0x0A0F
#define COLOR_TITLE 0xFFFF
#define COLOR_LABEL 0xB596
#define COLOR_TEMP 0xFD20
#define COLOR_HUMID 0x07FF
#define COLOR_PRESS 0x07E0
#define COLOR_ALT 0xF81F
#define COLOR_ERROR 0xF800
#define COLOR_OK 0x07E0
#define COLOR_ACCENT 0x07FF
#define COLOR_DIVIDER 0x2104
#define COLOR_REC 0xF800
#define COLOR_SOIL_DRY 0xF800
#define COLOR_SOIL_OPT 0x07E0
#define COLOR_SOIL_WET 0x07FF
enum ScreenMode {
SCREEN_BMP280 = 0,
SCREEN_DHT22 = 1,
SCREEN_MQ4 = 2,
SCREEN_TURB = 3,
SCREEN_TDS = 4,
SCREEN_MOISTURE = 5,
SCREEN_SYSTEM = 6,
NUM_SCREENS = 7
};
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
Adafruit_BMP280 bmp;
RTC_DS3231 rtc;
DHT dht(DHTPIN, DHTTYPE);
AsyncWebServer webServer(80);
void renderCurrentScreen();
void recordAudio();
void updateCachedSensors();
bool bmpFound = false;
bool rtcFound = false;
int currentScreen = SCREEN_BMP280;
int lastClkState = HIGH;
unsigned long lastButtonPress = 0;
volatile bool isRecording = false;
volatile bool hasRecording = false;
volatile int recordingProgress = 0;
volatile int recordingElapsedSec = 0;
volatile bool triggerRecordWeb = false;
uint8_t *wavBuffer = nullptr;
float cachedBmpTemp = 0, cachedBmpPress = 0, cachedBmpAlt = 0;
float cachedDhtTemp = 0, cachedDhtHum = 0, cachedDhtHeat = 0;
bool cachedDhtValid = false;
int cachedMq4Raw = 0;
float cachedMq4Volt = 0.0;
String cachedMq4Status = "CLEAN";
int cachedTurbRaw = 0;
float cachedTurbVolt = 0.0;
String cachedTurbStatus = "--";
int cachedTdsRaw = 0;
float cachedTdsVolt = 0.0;
float cachedTdsPpm = 0.0;
String cachedTdsStatus = "--";
int cachedMoistureRaw = 0;
float cachedMoistureVolt = 0.0;
int cachedMoisturePercent = 0;
String cachedMoistureStatus = "OPTIMAL";
String cachedRtcDate = "----/--/--";
String cachedRtcTime = "--:--:--";
float cachedRtcTemp = 0.0;
void writeWavHeader(uint8_t *buf, uint32_t dataSize) {
uint32_t fileSize = dataSize + 36;
uint16_t numChannels = 1;
uint32_t sampleRate = MIC_SAMPLE_RATE;
uint16_t bitsPerSample = 16;
uint32_t byteRate = sampleRate * numChannels * (bitsPerSample / 8);
uint16_t blockAlign = numChannels * (bitsPerSample / 8);
memcpy(buf + 0, "RIFF", 4);
memcpy(buf + 4, &fileSize, 4);
memcpy(buf + 8, "WAVE", 4);
memcpy(buf + 12, "fmt ", 4);
uint32_t subchunk1Size = 16;
memcpy(buf + 16, &subchunk1Size, 4);
uint16_t audioFormat = 1;
memcpy(buf + 20, &audioFormat, 2);
memcpy(buf + 22, &numChannels, 2);
memcpy(buf + 24, &sampleRate, 4);
memcpy(buf + 28, &byteRate, 4);
memcpy(buf + 32, &blockAlign, 2);
memcpy(buf + 34, &bitsPerSample, 2);
memcpy(buf + 36, "data", 4);
memcpy(buf + 40, &dataSize, 4);
}
void initMicrophone() {
i2s_config_t i2s_config = {.mode =
(i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = MIC_SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4,
.dma_buf_len = 512,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0};
i2s_pin_config_t pin_config = {.bck_io_num = MIC_SCK,
.ws_io_num = MIC_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = MIC_SD};
esp_err_t err = i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
if (err != ESP_OK) {
Serial.printf("[ERROR] Failed installing I2S driver: %d\n", err);
return;
}
err = i2s_set_pin(I2S_NUM_1, &pin_config);
if (err != ESP_OK) {
Serial.printf("[ERROR] Failed setting I2S pins: %d\n", err);
return;
}
Serial.println("[OK] INMP441 I2S microphone driver initialized on Port 1");
}
void drawHeader(const char *subtitle) {
tft.fillRect(0, 0, tft.width(), 28, COLOR_HEADER_BG);
tft.setTextSize(1);
tft.setTextColor(COLOR_TITLE);
tft.setCursor(8, 4);
tft.print("KRISHI KAVACH");
tft.setTextColor(COLOR_OK);
tft.setCursor(8, 16);
tft.print(subtitle);
tft.drawFastHLine(0, 28, tft.width(), COLOR_DIVIDER);
}
void drawStatusBar(const char *text, uint16_t color) {
int y = tft.height() - 12;
tft.fillRect(0, y, tft.width(), 12, COLOR_HEADER_BG);
tft.setTextSize(1);
tft.setTextColor(color);
tft.setCursor(4, y + 2);
tft.print(text);
}
void clearContent() {
tft.fillRect(0, 30, tft.width(), tft.height() - 42, COLOR_BG);
}
void showStatus(const char *line1, const char *line2, uint16_t color) {
clearContent();
tft.setTextSize(1);
tft.setTextColor(color);
tft.setCursor(8, 44);
tft.print(line1);
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 62);
tft.print(line2);
}
void drawBMP280Screen() {
drawHeader("BMP280 Sensor (1/7)");
clearContent();
tft.setTextSize(1);
if (!bmpFound) {
tft.setTextColor(COLOR_ERROR);
tft.setCursor(8, 38);
tft.print("BMP280 NOT FOUND!");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 54);
tft.print("Check SDA/SCL wiring");
drawStatusBar("[Turn: Switch | Press: Rec]", COLOR_ERROR);
return;
}
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 36);
tft.print("TEMP:");
tft.setTextColor(COLOR_TEMP);
tft.setCursor(55, 36);
tft.print(String(cachedBmpTemp, 1) + " C");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 52);
tft.print("PRESS:");
tft.setTextColor(COLOR_PRESS);
tft.setCursor(55, 52);
tft.print(String(cachedBmpPress, 1) + " hPa");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 68);
tft.print("ALT:");
tft.setTextColor(COLOR_ALT);
tft.setCursor(55, 68);
tft.print(String(cachedBmpAlt, 1) + " m");
tft.setTextColor(COLOR_TITLE);
tft.setCursor(8, 86);
tft.print("Press SW -> Record 5s");
drawStatusBar("BMP280 OK | Click SW Rec", COLOR_OK);
}
void drawDHT22Screen() {
drawHeader("DHT22 Sensor (2/7)");
clearContent();
tft.setTextSize(1);
if (!cachedDhtValid) {
tft.setTextColor(COLOR_ERROR);
tft.setCursor(8, 38);
tft.print("DHT22 DISCONNECTED!");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 54);
tft.print("Check VCC/GND/GPIO13");
drawStatusBar("DHT22 OFFLINE", COLOR_ERROR);
return;
}
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 36);
tft.print("TEMP:");
tft.setTextColor(COLOR_TEMP);
tft.setCursor(55, 36);
tft.print(String(cachedDhtTemp, 1) + " C");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 52);
tft.print("HUMID:");
tft.setTextColor(COLOR_HUMID);
tft.setCursor(55, 52);
tft.print(String(cachedDhtHum, 1) + " %");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 68);
tft.print("HEAT ID:");
tft.setTextColor(COLOR_ALT);
tft.setCursor(55, 68);
tft.print(String(cachedDhtHeat, 1) + " C");
tft.setTextColor(COLOR_TITLE);
tft.setCursor(8, 86);
tft.print("Press SW -> Record 5s");
drawStatusBar("DHT22 OK | Click SW Rec", COLOR_OK);
}
void drawMQ4Screen() {
drawHeader("MQ-4 Gas Sensor (3/7)");
clearContent();
tft.setTextSize(1);
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 36);
tft.print("RAW ADC:");
tft.setTextColor(COLOR_TEMP);
tft.setCursor(62, 36);
tft.print(String(cachedMq4Raw) + " / 4095");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 52);
tft.print("VOLTAGE:");
tft.setTextColor(COLOR_ACCENT);
tft.setCursor(62, 52);
tft.print(String(cachedMq4Volt, 2) + " V");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 68);
tft.print("STATUS:");
if (cachedMq4Raw < 800) {
tft.setTextColor(COLOR_OK);
} else if (cachedMq4Raw < 1800) {
tft.setTextColor(COLOR_TEMP);
} else {
tft.setTextColor(COLOR_ERROR);
}
tft.setCursor(55, 68);
tft.print(cachedMq4Status);
tft.setTextColor(COLOR_TITLE);
tft.setCursor(8, 86);
tft.print("Press SW -> Record 5s");
uint16_t statusCol = (cachedMq4Raw >= 1800)
? COLOR_ERROR
: ((cachedMq4Raw >= 800) ? COLOR_TEMP : COLOR_OK);
drawStatusBar("MQ-4 (CH4) Sensor Active", statusCol);
}
void drawTurbScreen() {
drawHeader("Turbidity (4/7)");
clearContent();
tft.setTextSize(1);
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 36);
tft.print("RAW ADC:");
tft.setTextColor(COLOR_TEMP);
tft.setCursor(62, 36);
tft.print(String(cachedTurbRaw) + " / 4095");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 52);
tft.print("VOLTAGE:");
tft.setTextColor(COLOR_ACCENT);
tft.setCursor(62, 52);
tft.print(String(cachedTurbVolt, 2) + " V");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 68);
tft.print("CLARITY:");
if (cachedTurbRaw > 3500) {
tft.setTextColor(COLOR_OK);
} else if (cachedTurbRaw > 2000) {
tft.setTextColor(COLOR_ACCENT);
} else if (cachedTurbRaw > 1000) {
tft.setTextColor(COLOR_TEMP);
} else {
tft.setTextColor(COLOR_ERROR);
}
tft.setCursor(62, 68);
tft.print(cachedTurbStatus);
uint16_t statusCol = (cachedTurbRaw < 1000)
? COLOR_ERROR
: ((cachedTurbRaw < 2000) ? COLOR_TEMP : COLOR_OK);
drawStatusBar("Turbidity Sensor Active", statusCol);
}
void drawTDSScreen() {
drawHeader("TDS Water Meter (5/7)");
clearContent();
tft.setTextSize(1);
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 36);
tft.print("RAW ADC:");
tft.setTextColor(COLOR_TEMP);
tft.setCursor(62, 36);
tft.print(String(cachedTdsRaw) + " / 4095");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 52);
tft.print("VOLTAGE:");
tft.setTextColor(COLOR_ACCENT);
tft.setCursor(62, 52);
tft.print(String(cachedTdsVolt, 2) + " V");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 68);
tft.print("TDS:");
if (cachedTdsPpm < 300) {
tft.setTextColor(COLOR_OK);
} else if (cachedTdsPpm < 600) {
tft.setTextColor(COLOR_ACCENT);
} else if (cachedTdsPpm < 900) {
tft.setTextColor(COLOR_TEMP);
} else {
tft.setTextColor(COLOR_ERROR);
}
tft.setCursor(40, 68);
tft.print(String(cachedTdsPpm, 0) + " ppm");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 84);
tft.print("QUALITY:");
tft.setCursor(62, 84);
if (cachedTdsPpm < 300) {
tft.setTextColor(COLOR_OK);
} else if (cachedTdsPpm < 900) {
tft.setTextColor(COLOR_TEMP);
} else {
tft.setTextColor(COLOR_ERROR);
}
tft.print(cachedTdsStatus);
uint16_t statusCol = (cachedTdsPpm >= 900)
? COLOR_ERROR
: ((cachedTdsPpm >= 300) ? COLOR_TEMP : COLOR_OK);
drawStatusBar("TDS Water Quality Active", statusCol);
}
void drawMoistureScreen() {
drawHeader("Soil Moisture (6/7)");
clearContent();
tft.setTextSize(1);
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 34);
tft.print("MOISTURE:");
uint16_t col;
if (cachedMoisturePercent < 25)
col = COLOR_SOIL_DRY;
else if (cachedMoisturePercent <= 75)
col = COLOR_SOIL_OPT;
else
col = COLOR_SOIL_WET;
tft.setTextColor(col);
tft.setCursor(72, 34);
tft.print(String(cachedMoisturePercent) + " %");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 48);
tft.print("RAW / VOLT:");
tft.setTextColor(COLOR_TITLE);
tft.setCursor(76, 48);
tft.print(String(cachedMoistureRaw) + " | " + String(cachedMoistureVolt, 2) +
"V");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 62);
tft.print("STATUS:");
tft.setTextColor(col);
tft.setCursor(55, 62);
tft.print(cachedMoistureStatus);
int barX = 8;
int barY = 78;
int barW = 144;
int barH = 10;
tft.drawRect(barX, barY, barW, barH, COLOR_DIVIDER);
int fillW =
map(constrain(cachedMoisturePercent, 0, 100), 0, 100, 0, barW - 2);
if (fillW > 0) {
tft.fillRect(barX + 1, barY + 1, fillW, barH - 2, col);
}
if (fillW < barW - 2) {
tft.fillRect(barX + 1 + fillW, barY + 1, (barW - 2) - fillW, barH - 2,
COLOR_BG);
}
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 92);
tft.print("0% (Dry) ------ 100% (Wet)");
drawStatusBar("Soil Moisture Sensor Pin 14", col);
}
void drawSystemScreen() {
drawHeader("System & RTC (7/7)");
clearContent();
tft.setTextSize(1);
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 34);
tft.print("RTC:");
if (rtcFound) {
tft.setTextColor(COLOR_TEMP);
tft.setCursor(34, 34);
tft.print(cachedRtcTime);
tft.setTextColor(COLOR_LABEL);
tft.setCursor(90, 34);
tft.print(cachedRtcDate.substring(5));
} else {
tft.setTextColor(COLOR_ERROR);
tft.setCursor(34, 34);
tft.print("NOT FOUND");
}
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 50);
tft.print("IP:");
tft.setTextColor(COLOR_ACCENT);
tft.setCursor(30, 50);
tft.print(WiFi.localIP().toString());
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 66);
tft.print("MIC:");
tft.setTextColor(COLOR_OK);
tft.setCursor(38, 66);
tft.print(hasRecording ? "CLIP READY" : "IDLE");
tft.setTextColor(COLOR_LABEL);
tft.setCursor(8, 82);
tft.print("PSRAM:");
tft.setTextColor(COLOR_ALT);
tft.setCursor(50, 82);
tft.print(String(ESP.getFreePsram() / 1024) + " KB free");
drawStatusBar("WiFi Connected | IP Ready", COLOR_OK);
}
void renderCurrentScreen() {
switch (currentScreen) {
case SCREEN_BMP280:
drawBMP280Screen();
break;
case SCREEN_DHT22:
drawDHT22Screen();
break;
case SCREEN_MQ4:
drawMQ4Screen();
break;
case SCREEN_TURB:
drawTurbScreen();
break;
case SCREEN_TDS:
drawTDSScreen();
break;
case SCREEN_MOISTURE:
drawMoistureScreen();
break;
case SCREEN_SYSTEM:
drawSystemScreen();
break;
default:
drawBMP280Screen();
break;
}
}
void recordAudio() {
if (isRecording)
return;
isRecording = true;
drawHeader("RECORDING MIC...");
clearContent();
tft.setTextSize(1);
tft.setTextColor(COLOR_REC);
tft.setCursor(8, 40);
tft.print("RECORDING (5 SEC)...");
tft.setTextColor(COLOR_TITLE);
tft.setCursor(8, 56);
tft.print("Speak into INMP441!");
drawStatusBar("REC IN PROGRESS...", COLOR_REC);
if (wavBuffer == nullptr) {
wavBuffer = (uint8_t *)ps_malloc(WAV_FILE_SIZE);
if (wavBuffer == nullptr) {
Serial.println("[ERROR] Failed to allocate PSRAM for WAV buffer!");
isRecording = false;
showStatus("MIC Error!", "PSRAM alloc failed", COLOR_ERROR);
vTaskDelay(pdMS_TO_TICKS(1500));
renderCurrentScreen();
return;
}
}
writeWavHeader(wavBuffer, WAV_DATA_SIZE);
int16_t *audioData = (int16_t *)(wavBuffer + WAV_HEADER_SIZE);
i2s_zero_dma_buffer(I2S_NUM_1);
const size_t CHUNK_SAMPLES = 1024;
int32_t raw_i2s_chunk[CHUNK_SAMPLES];
size_t samples_recorded = 0;
size_t bytes_read = 0;
Serial.println("[MIC] Starting 5-second audio capture...");
unsigned long recStartTime = millis();
while (samples_recorded < MIC_NUM_SAMPLES) {
size_t samples_to_read = min((size_t)CHUNK_SAMPLES,
(size_t)(MIC_NUM_SAMPLES - samples_recorded));
esp_err_t res =
i2s_read(I2S_NUM_1, raw_i2s_chunk, samples_to_read * sizeof(int32_t),
&bytes_read, portMAX_DELAY);
if (res == ESP_OK && bytes_read > 0) {
size_t actual_samples = bytes_read / sizeof(int32_t);
for (size_t i = 0; i < actual_samples; i++) {
audioData[samples_recorded + i] = (int16_t)(raw_i2s_chunk[i] >> 14);
}
samples_recorded += actual_samples;
}
recordingProgress = (samples_recorded * 100) / MIC_NUM_SAMPLES;
recordingElapsedSec = (millis() - recStartTime) / 1000;
int barW = map(recordingProgress, 0, 100, 0, tft.width() - 16);
tft.fillRect(8, 76, barW, 8, COLOR_REC);
}
Serial.println("[MIC] Recording complete! Total samples: " +
String(samples_recorded));
hasRecording = true;
isRecording = false;
recordingProgress = 100;
showStatus("Recording Saved!", "5s WAV Ready in PSRAM", COLOR_OK);
drawStatusBar("Audio Received & Ready", COLOR_OK);
vTaskDelay(pdMS_TO_TICKS(1500));
renderCurrentScreen();
}
const char DASHBOARD_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KRISHI KAVACH - Farm Node Dashboard</title>
<style>
:root {
--bg: #0b0f19;
--card-bg: rgba(22, 30, 49, 0.85);
--border: rgba(255, 255, 255, 0.08);
--text: #f1f5f9;
--text-dim: #94a3b8;
--accent-safe: #10b981;
--accent-warn: #f59e0b;
--accent-danger: #ef4444;
--accent-blue: #38bdf8;
--accent-cyan: #06b6d4;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body { background: var(--bg); color: var(--text); padding: 1.5rem; display: flex; flex-direction: column; gap: 1.5rem; min-height: 100vh; }
header { background: var(--card-bg); padding: 1rem 1.5rem; border-radius: 12px; border: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
.brand { font-size: 1.3rem; font-weight: bold; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.25rem; }
.card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 12px; padding: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem; }
.card-title { font-size: 0.9rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; display: flex; justify-content: space-between; }
.val-large { font-size: 1.8rem; font-weight: 700; color: var(--text); }
.val-sub { font-size: 0.85rem; color: var(--text-dim); }
.badge { padding: 0.2rem 0.6rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
.badge-ok { background: rgba(16,185,129,0.2); color: var(--accent-safe); }
.badge-warn { background: rgba(245,158,11,0.2); color: var(--accent-warn); }
.badge-danger { background: rgba(239,68,68,0.2); color: var(--accent-danger); }
.btn { background: #2563eb; color: white; border: none; padding: 0.6rem 1.2rem; border-radius: 8px; cursor: pointer; font-weight: 600; text-align: center; text-decoration: none; display: inline-block; }
.btn:hover { background: #1d4ed8; }
.progress-bar { width: 100%; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--accent-blue); width: 0%; transition: width 0.3s; }
</style>
</head>
<body>
<header>
<div class="brand">🌱 KRISHI KAVACH <span style="font-size: 0.9rem; color: var(--accent-safe); font-weight: normal;">ESP32-S3 Agri-Node</span></div>
<div id="clockDisplay" style="font-family: monospace; font-size: 1rem; color: var(--accent-cyan);">--:--:--</div>
</header>
<div class="grid">
<!-- Soil Moisture -->
<div class="card">
<div class="card-title">
<span>🌱 Soil Moisture (GPIO 14)</span>
<span class="badge badge-ok" id="moistBadge">OPTIMAL</span>
</div>
<div class="val-large" id="moistPercent">-- %</div>
<div class="progress-bar"><div class="progress-fill" id="moistBar"></div></div>
<div class="val-sub" id="moistSub">Raw: -- | Voltage: -- V</div>
</div>
<!-- BMP280 -->
<div class="card">
<div class="card-title"><span>🌡️ Air Temp & Pressure</span></div>
<div class="val-large" id="bmpTemp">-- °C</div>
<div class="val-sub" id="bmpPress">Pressure: -- hPa | Alt: -- m</div>
</div>
<!-- DHT22 -->
<div class="card">
<div class="card-title"><span>💧 Humidity & Heat Index</span></div>
<div class="val-large" id="dhtHum">-- %</div>
<div class="val-sub" id="dhtHeat">Temp: -- °C | Heat Index: -- °C</div>
</div>
<!-- MQ-4 Gas -->
<div class="card">
<div class="card-title">
<span>🔥 Methane / Gas (MQ-4)</span>
<span class="badge badge-ok" id="mq4Badge">CLEAN</span>
</div>
<div class="val-large" id="mq4Raw">--</div>
<div class="val-sub" id="mq4Volt">Voltage: -- V</div>
</div>
<!-- Turbidity -->
<div class="card">
<div class="card-title"><span>🌊 Water Turbidity</span></div>
<div class="val-large" id="turbStatus">--</div>
<div class="val-sub" id="turbRaw">ADC: -- / 4095 | Voltage: -- V</div>
</div>
<!-- TDS -->
<div class="card">
<div class="card-title"><span>🧪 TDS Water Quality</span></div>
<div class="val-large" id="tdsPpm">-- ppm</div>
<div class="val-sub" id="tdsStatus">Quality: --</div>
</div>
<!-- Microphone & Audio -->
<div class="card">
<div class="card-title"><span>🎙️ INMP441 Mic Audio</span></div>
<div style="display: flex; gap: 0.5rem; flex-direction: column;">
<button class="btn" onclick="triggerRecord()">🔴 Record 5s Clip</button>
<a href="/audio.wav" class="btn" style="background: #059669;" download>⬇️ Download WAV</a>
</div>
<div class="val-sub" id="micStatus">Status: Idle</div>
</div>
<!-- Servo Control (Full Width Card) -->
<div class="card" style="grid-column: 1 / -1;">
<div class="card-title">
<span>🎯 Extinguisher Aim Servo (GPIO 21 — Full Range: 0° to 180°)</span>
<span class="badge badge-ok" id="servoBadge">90° (CENTER)</span>
</div>
<div style="display: flex; align-items: center; gap: 1.25rem; margin-top: 0.5rem;">
<input type="range" id="servoSlider" min="0" max="180" value="90" style="flex: 1; height: 12px; accent-color: #38bdf8; cursor: pointer;" oninput="onServoSlider(this.value)">
<span class="val-large" id="servoAngleDisplay" style="min-width: 90px; text-align: right; color: var(--accent-blue);">90°</span>
</div>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.75rem;">
<button class="btn" style="background: #334155; padding: 0.5rem 1rem; font-size: 0.85rem;" onclick="setServo(0)">0° (Full Left)</button>
<button class="btn" style="background: #334155; padding: 0.5rem 1rem; font-size: 0.85rem;" onclick="setServo(45)">45°</button>
<button class="btn" style="background: #2563eb; padding: 0.5rem 1rem; font-size: 0.85rem;" onclick="setServo(90)">90° (Center)</button>
<button class="btn" style="background: #334155; padding: 0.5rem 1rem; font-size: 0.85rem;" onclick="setServo(135)">135°</button>
<button class="btn" style="background: #334155; padding: 0.5rem 1rem; font-size: 0.85rem;" onclick="setServo(180)">180° (Full Right)</button>
<button class="btn" id="sweepBtn" style="background: #7c3aed; padding: 0.5rem 1rem; font-size: 0.85rem;" onclick="toggleAutoSweep()">🔄 Full Sweep</button>
</div>
</div>
</div>
<script>
let isUserDragging = false;
let autoSweepTimer = null;
async function updateData() {
try {
const res = await fetch('/api/sensors');
if (!res.ok) return;
const d = await res.json();
document.getElementById('clockDisplay').innerText = `${d.date} ${d.time}`;
document.getElementById('moistPercent').innerText = d.moisture_percent + ' %';
document.getElementById('moistBar').style.width = Math.min(100, Math.max(0, d.moisture_percent)) + '%';
document.getElementById('moistSub').innerText = `Raw: ${d.moisture_raw} | Voltage: ${d.moisture_volt.toFixed(2)} V`;
const mBadge = document.getElementById('moistBadge');
mBadge.innerText = d.moisture_status;
mBadge.className = 'badge ' + (d.moisture_percent < 25 ? 'badge-danger' : (d.moisture_percent <= 75 ? 'badge-ok' : 'badge-warn'));
document.getElementById('bmpTemp').innerText = d.bmp_temp.toFixed(1) + ' °C';
document.getElementById('bmpPress').innerText = `Pressure: ${d.bmp_press.toFixed(1)} hPa | Alt: ${d.bmp_alt.toFixed(0)} m`;
document.getElementById('dhtHum').innerText = d.dht_hum.toFixed(1) + ' %';
document.getElementById('dhtHeat').innerText = `Temp: ${d.dht_temp.toFixed(1)} °C | Heat Idx: ${d.dht_heat.toFixed(1)} °C`;
document.getElementById('mq4Raw').innerText = d.mq4_raw;
document.getElementById('mq4Volt').innerText = `Voltage: ${d.mq4_volt.toFixed(2)} V`;
const mqBadge = document.getElementById('mq4Badge');
mqBadge.innerText = d.mq4_status;
mqBadge.className = 'badge ' + (d.mq4_raw >= 1800 ? 'badge-danger' : (d.mq4_raw >= 800 ? 'badge-warn' : 'badge-ok'));
document.getElementById('turbStatus').innerText = d.turb_status;
document.getElementById('turbRaw').innerText = `ADC: ${d.turb_raw} | Voltage: ${d.turb_volt.toFixed(2)} V`;
document.getElementById('tdsPpm').innerText = d.tds_ppm.toFixed(0) + ' ppm';
document.getElementById('tdsStatus').innerText = `Quality: ${d.tds_status}`;
if (!isUserDragging && d.servo_angle !== undefined && !autoSweepTimer) {
document.getElementById('servoSlider').value = d.servo_angle;
document.getElementById('servoAngleDisplay').innerText = d.servo_angle + '°';
updateServoBadge(d.servo_angle, d.fire_tracking);
}
} catch (e) {}
}
function updateServoBadge(angle, isFire) {
const badge = document.getElementById('servoBadge');
if (isFire) {
badge.innerText = angle + '° (🚨 FIRE LOCKED)';
badge.className = 'badge badge-danger';
} else if (angle == 90) {
badge.innerText = '90° (CENTER)';
badge.className = 'badge badge-ok';
} else if (angle <= 20) {
badge.innerText = angle + '° (FULL LEFT)';
badge.className = 'badge badge-warn';
} else if (angle >= 160) {
badge.innerText = angle + '° (FULL RIGHT)';
badge.className = 'badge badge-warn';
} else {
badge.innerText = angle + '°';
badge.className = 'badge badge-ok';
}
}
let lastSent = 0;
function onServoSlider(val) {
isUserDragging = true;
document.getElementById('servoAngleDisplay').innerText = val + '°';
updateServoBadge(val, false);
const now = Date.now();
if (now - lastSent > 80) {
lastSent = now;
sendServoAngle(val);
}
clearTimeout(window.dragTimeout);
window.dragTimeout = setTimeout(() => { isUserDragging = false; sendServoAngle(val); }, 300);
}
async function setServo(angle) {
if (autoSweepTimer) toggleAutoSweep();
document.getElementById('servoSlider').value = angle;
document.getElementById('servoAngleDisplay').innerText = angle + '°';
updateServoBadge(angle, false);
await sendServoAngle(angle);
}
async function sendServoAngle(angle) {
try {
await fetch('/api/servo?angle=' + angle, { method: 'POST' });
} catch (e) {}
}
function toggleAutoSweep() {
const btn = document.getElementById('sweepBtn');
if (autoSweepTimer) {
clearInterval(autoSweepTimer);
autoSweepTimer = null;
btn.innerText = "🔄 Full Sweep";
btn.style.background = "#7c3aed";
setServo(90);
} else {
btn.innerText = "⏹️ Stop Sweep";
btn.style.background = "#ef4444";
let curAngle = 0;
let dir = 5;
autoSweepTimer = setInterval(() => {
curAngle += dir;
if (curAngle >= 180) dir = -5;
if (curAngle <= 0) dir = 5;
document.getElementById('servoSlider').value = curAngle;
document.getElementById('servoAngleDisplay').innerText = curAngle + '°';
updateServoBadge(curAngle, false);
sendServoAngle(curAngle);
}, 150);
}
}
...
This file has been truncated, please download it to see its full contents.
Comments