/*
============================================================
GreenOS - ESP32 Smart Greenhouse Controller (Wokwi)
============================================================
Arsitektur FreeRTOS dual-core:
Core 0 : taskNet -> WiFi, NTP (WIB), MQTT (telemetri + perintah)
Core 1 : taskSensors -> DHT22, LDR, tanah, HC-SR04, MPU6050
taskControl -> logika otomatis (histeresis, P-control, proteksi)
taskUI -> OLED + menu rotary encoder, NeoPixel, buzzer
Perintah MQTT (topik: greenos/<DEVICE_ID>/cmd), format "kunci=nilai":
auto=1|0 temp=28.5 soil=35 light=40
pump=1|0 lamp=1|0 vent=0..90 (pump/lamp/vent hanya efektif saat auto=0)
============================================================
*/
#include <Arduino.h>
#include <WiFi.h>
#include <Wire.h>
#include <time.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <PubSubClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
#include <DHT.h>
#if __has_include(<esp_arduino_version.h>)
#include <esp_arduino_version.h>
#endif
#ifndef ESP_ARDUINO_VERSION_MAJOR
#define ESP_ARDUINO_VERSION_MAJOR 2
#endif
// ===================== KONFIGURASI =====================
const char* WIFI_SSID = "Wokwi-GUEST";
const char* WIFI_PASS = "";
const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;
const char* DEVICE_ID = "greenos-ganti-saya-7421"; // <-- GANTI agar unik di broker publik!
const long GMT_OFFSET_S = 7 * 3600; // WIB (UTC+7)
const float TANK_EMPTY_CM = 30.0f; // jarak sensor saat tangki kosong
const float TANK_FULL_CM = 4.0f; // jarak sensor saat tangki penuh
const uint32_t PUMP_MAX_MS = 25000; // batas nyala pompa terus-menerus
const uint32_t PUMP_REST_MS = 20000; // jeda wajib setelah batas tercapai
// ======================= PIN ===========================
#define PIN_SDA 21
#define PIN_SCL 22
#define PIN_DHT 4
#define PIN_LDR 34
#define PIN_SOIL 35
#define PIN_TRIG 5
#define PIN_ECHO 18
#define PIN_SERVO 13
#define PIN_RELAY 26
#define PIN_RING 27
#define PIN_BUZZER 25
#define PIN_ENC_CLK 32
#define PIN_ENC_DT 33
#define PIN_ENC_SW 14
#define PIN_LED_LAMP 19
#define PIN_LED_ALARM 23
#define MPU_ADDR 0x68
// Kanal LEDC dipisah supaya servo (50 Hz) dan buzzer tidak berbagi timer
#define CH_SERVO 0
#define CH_BUZZ 2
#if ESP_ARDUINO_VERSION_MAJOR >= 3
#define PWM_ATTACH(pin, ch, freq, res) ledcAttachChannel(pin, freq, res, ch)
#define PWM_WRITE(pin, ch, duty) ledcWrite(pin, duty)
#define PWM_TONE(pin, ch, f) ledcWriteTone(pin, f)
#else
#define PWM_ATTACH(pin, ch, freq, res) do { ledcSetup(ch, freq, res); ledcAttachPin(pin, ch); } while (0)
#define PWM_WRITE(pin, ch, duty) ledcWrite(ch, duty)
#define PWM_TONE(pin, ch, f) ledcWriteTone(ch, f)
#endif
// ================== TIPE DATA BERSAMA ==================
enum : uint8_t { ALM_HOT = 1, ALM_TANK = 2, ALM_SHOCK = 4, ALM_SENSOR = 8 };
enum Page { PG_DASH, PG_SYS, PG_SET };
struct Settings {
float tempSet = 28.0f; // suhu target (ventilasi mulai terbuka di atas ini)
int soilDry = 35; // pompa ON jika kelembapan tanah < nilai ini (%)
int lightMin = 40; // lampu ON jika cahaya < nilai ini (%)
int manVent = 0; // sudut ventilasi mode manual
bool autoMode = true;
bool manPump = false;
bool manLamp = false;
};
struct State {
float temp = NAN, hum = NAN, accel = 1.0f;
int soil = 50, light = 50, tank = 50, vent = 0;
bool pump = false, lamp = false, wifi = false, mqtt = false;
bool shock = false, dhtFail = false, tankFail = false;
uint8_t alarm = 0;
};
Settings gCfg;
State gS;
SemaphoreHandle_t gStateMtx, gI2cMtx;
#define LOCK_S() xSemaphoreTake(gStateMtx, portMAX_DELAY)
#define UNLOCK_S() xSemaphoreGive(gStateMtx)
#define LOCK_I() xSemaphoreTake(gI2cMtx, portMAX_DELAY)
#define UNLOCK_I() xSemaphoreGive(gI2cMtx)
State snapState() { LOCK_S(); State s = gS; UNLOCK_S(); return s; }
Settings snapCfg() { LOCK_S(); Settings c = gCfg; UNLOCK_S(); return c; }
inline int soilWet(const Settings& c) { return min(95, c.soilDry + 25); }
// ===================== PERIFERAL =======================
DHT dht(PIN_DHT, DHT22);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
Adafruit_NeoPixel ring(16, PIN_RING, NEO_GRB + NEO_KHZ800);
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
char T_TELE[72], T_CMD[72], T_STAT[72];
// Rotary encoder (ISR)
volatile int encDelta = 0;
portMUX_TYPE encMux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR isrEncoder() {
static uint32_t last = 0;
uint32_t t = micros();
if (t - last < 1500) return; // buang pantulan kontak
last = t;
int step = digitalRead(PIN_ENC_DT) ? +1 : -1; // tukar tanda jika arah terbalik
portENTER_CRITICAL_ISR(&encMux);
encDelta += step;
portEXIT_CRITICAL_ISR(&encMux);
}
void servoAngle(int deg) {
deg = constrain(deg, 0, 180);
uint32_t us = 500 + (uint32_t)deg * 2000 / 180; // 500..2500 us
uint32_t duty = (uint32_t)((uint64_t)us * 16383 / 20000); // periode 20 ms, 14 bit
PWM_WRITE(PIN_SERVO, CH_SERVO, duty);
}
float readDistanceCm() {
digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
unsigned long us = pulseIn(PIN_ECHO, HIGH, 30000UL);
if (us == 0) return NAN;
return us * 0.0343f / 2.0f;
}
bool mpuRead(float& g) {
bool ok = false;
LOCK_I();
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B);
if (Wire.endTransmission(false) == 0 && Wire.requestFrom((uint8_t)MPU_ADDR, (uint8_t)6) == 6) {
int16_t v[3];
for (int i = 0; i < 3; i++) {
uint8_t hi = Wire.read();
uint8_t lo = Wire.read();
v[i] = (int16_t)((hi << 8) | lo);
}
float x = v[0] / 16384.0f, y = v[1] / 16384.0f, z = v[2] / 16384.0f;
g = sqrtf(x * x + y * y + z * z);
ok = true;
}
UNLOCK_I();
return ok;
}
// ===================== PERINTAH ========================
void applyCommand(const char* key, float v) {
LOCK_S();
if (!strcmp(key, "auto")) gCfg.autoMode = v > 0.5f;
else if (!strcmp(key, "temp")) gCfg.tempSet = constrain(v, 20.0f, 40.0f);
else if (!strcmp(key, "soil")) gCfg.soilDry = constrain((int)v, 5, 70);
else if (!strcmp(key, "light")) gCfg.lightMin = constrain((int)v, 5, 90);
else if (!strcmp(key, "pump")) gCfg.manPump = v > 0.5f;
else if (!strcmp(key, "lamp")) gCfg.manLamp = v > 0.5f;
else if (!strcmp(key, "vent")) gCfg.manVent = constrain((int)v, 0, 90);
UNLOCK_S();
}
void onMqtt(char*, byte* payload, unsigned int len) {
char buf[48];
if (len > sizeof(buf) - 1) len = sizeof(buf) - 1;
memcpy(buf, payload, len);
buf[len] = 0;
char* eq = strchr(buf, '=');
if (!eq) return;
*eq = 0;
applyCommand(buf, atof(eq + 1));
Serial.printf("[MQTT] cmd %s=%s\n", buf, eq + 1);
}
// ==================== TASK: SENSOR =====================
void taskSensors(void*) {
dht.begin();
bool mpuOk = false;
float baseG = 1.0f;
LOCK_I();
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x6B); Wire.write(0x00); // bangunkan MPU6050
mpuOk = (Wire.endTransmission() == 0);
UNLOCK_I();
if (mpuOk) { // kalibrasi baseline (auto-zero)
float sum = 0; int n = 0;
for (int i = 0; i < 10; i++) {
float g;
if (mpuRead(g)) { sum += g; n++; }
vTaskDelay(pdMS_TO_TICKS(20));
}
if (n) baseG = sum / n;
}
uint32_t t0 = millis() - 5000;
uint32_t tDht = t0, tTank = t0, tMpu = t0, shockUntil = 0;
float tankF = -1;
int dhtBad = 0, tankBad = 0, tank = 50;
bool dhtFail = false, tankFail = false;
float temp = NAN, hum = NAN, g = baseG;
for (;;) {
uint32_t now = millis();
long ls = 0, ss = 0; // rata-rata 8 sampel ADC
for (int i = 0; i < 8; i++) { ls += analogRead(PIN_LDR); ss += analogRead(PIN_SOIL); }
int light = (int)(ls / 8 * 100 / 4095);
int soil = (int)(ss / 8 * 100 / 4095);
if (now - tDht >= 2500) {
tDht = now;
float t = dht.readTemperature(), h = dht.readHumidity();
if (isnan(t) || isnan(h)) { if (++dhtBad >= 3) dhtFail = true; }
else { dhtBad = 0; dhtFail = false; temp = t; hum = h; }
}
if (now - tTank >= 400) {
tTank = now;
float d = readDistanceCm();
if (isnan(d)) { if (++tankBad >= 3) tankFail = true; }
else {
tankBad = 0; tankFail = false;
float pct = constrain((TANK_EMPTY_CM - d) / (TANK_EMPTY_CM - TANK_FULL_CM) * 100.0f, 0.0f, 100.0f);
tankF = (tankF < 0) ? pct : tankF * 0.6f + pct * 0.4f; // filter EMA
tank = (int)(tankF + 0.5f);
}
}
if (mpuOk && now - tMpu >= 50) {
tMpu = now;
float gg;
if (mpuRead(gg)) {
g = gg;
if (fabsf(g - baseG) > 0.4f) shockUntil = now + 8000; // getaran/benturan
}
}
LOCK_S();
gS.temp = temp; gS.hum = hum; gS.soil = soil; gS.light = light; gS.tank = tank;
gS.accel = g; gS.shock = (now < shockUntil);
gS.dhtFail = dhtFail; gS.tankFail = tankFail;
UNLOCK_S();
vTaskDelay(pdMS_TO_TICKS(20));
}
}
// ==================== TASK: KONTROL ====================
void taskControl(void*) {
int vent = 0;
bool pump = false, lamp = false;
uint32_t pumpOnAt = 0, lockUntil = 0, lastLog = 0;
for (;;) {
State s = snapState();
Settings c = snapCfg();
uint32_t now = millis();
int ventTarget;
if (c.autoMode) {
// --- pompa: histeresis + proteksi tangki kosong + batas waktu nyala ---
if (!pump && s.soil < c.soilDry && s.tank >= 10 && !s.tankFail && now >= lockUntil) {
pump = true; pumpOnAt = now;
}
if (pump && (s.soil >= soilWet(c) || s.tank < 5 || s.tankFail)) pump = false;
if (pump && now - pumpOnAt > PUMP_MAX_MS) { pump = false; lockUntil = now + PUMP_REST_MS; }
// --- lampu tumbuh: histeresis 8% ---
if (!lamp && s.light < c.lightMin) lamp = true;
else if (lamp && s.light > c.lightMin + 8) lamp = false;
// --- ventilasi: kontrol proporsional (18 derajat per derajat Celsius) ---
float err = isnan(s.temp) ? 0.0f : (s.temp - c.tempSet);
ventTarget = constrain((int)(err * 18.0f), 0, 90);
if (!isnan(s.hum) && s.hum > 85.0f) ventTarget = max(ventTarget, 30);
} else {
pump = c.manPump; lamp = c.manLamp; ventTarget = c.manVent;
if (pump && (s.tank < 5 || s.tankFail)) pump = false; // anti dry-run
}
// gerakkan servo halus (3 derajat per 100 ms)
if (vent < ventTarget) vent = min(vent + 3, ventTarget);
else if (vent > ventTarget) vent = max(vent - 3, ventTarget);
servoAngle(vent);
digitalWrite(PIN_RELAY, pump);
digitalWrite(PIN_LED_LAMP, lamp);
uint8_t a = 0;
if (!isnan(s.temp) && s.temp > c.tempSet + 6.0f) a |= ALM_HOT;
if (s.tank < 10 && !s.tankFail) a |= ALM_TANK;
if (s.shock) a |= ALM_SHOCK;
if (s.dhtFail || s.tankFail) a |= ALM_SENSOR;
digitalWrite(PIN_LED_ALARM, a ? ((now / 300) % 2) : LOW);
LOCK_S(); gS.pump = pump; gS.lamp = lamp; gS.vent = vent; gS.alarm = a; UNLOCK_S();
if (now - lastLog >= 2000) {
lastLog = now;
Serial.printf("[%5lus] T=%.1f H=%.0f soil=%d%% light=%d%% tank=%d%% g=%.2f | %s pump=%d vent=%d lamp=%d alarm=0x%02X\n",
(unsigned long)(now / 1000), s.temp, s.hum, s.soil, s.light, s.tank, s.accel,
c.autoMode ? "AUTO" : "MAN", pump, vent, lamp, a);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// ====================== TASK: NET ======================
void publishTelemetry() {
State s = snapState();
Settings c = snapCfg();
char j[320];
snprintf(j, sizeof(j),
"{\"temp\":%.1f,\"hum\":%.0f,\"soil\":%d,\"light\":%d,\"tank\":%d,\"g\":%.2f,"
"\"pump\":%d,\"vent\":%d,\"lamp\":%d,\"auto\":%d,\"alarm\":%u,\"up\":%lu}",
isnan(s.temp) ? -99.0f : s.temp, isnan(s.hum) ? -1.0f : s.hum,
s.soil, s.light, s.tank, s.accel,
s.pump, s.vent, s.lamp, c.autoMode, (unsigned)s.alarm, (unsigned long)(millis() / 1000));
mqtt.publish(T_TELE, j);
}
void taskNet(void*) {
snprintf(T_TELE, sizeof(T_TELE), "greenos/%s/telemetry", DEVICE_ID);
snprintf(T_CMD, sizeof(T_CMD), "greenos/%s/cmd", DEVICE_ID);
snprintf(T_STAT, sizeof(T_STAT), "greenos/%s/status", DEVICE_ID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS, 6);
configTime(GMT_OFFSET_S, 0, "pool.ntp.org", "time.google.com");
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMqtt);
mqtt.setBufferSize(512);
uint32_t lastTry = 0, lastPub = 0;
bool first = true;
for (;;) {
uint32_t now = millis();
bool w = (WiFi.status() == WL_CONNECTED);
bool m = false;
if (w) {
if (!mqtt.connected()) {
if (first || now - lastTry > 5000) {
first = false; lastTry = now;
char cid[64];
snprintf(cid, sizeof(cid), "%s-%04x", DEVICE_ID, (unsigned)(esp_random() & 0xFFFF));
if (mqtt.connect(cid, NULL, NULL, T_STAT, 0, true, "offline")) {
mqtt.subscribe(T_CMD);
mqtt.publish(T_STAT, "online", true);
Serial.println("[MQTT] terhubung");
} else {
Serial.printf("[MQTT] gagal, rc=%d\n", mqtt.state());
}
}
} else {
mqtt.loop();
if (now - lastPub >= 3000) { lastPub = now; publishTelemetry(); }
}
m = mqtt.connected();
}
LOCK_S(); gS.wifi = w; gS.mqtt = m; UNLOCK_S();
vTaskDelay(pdMS_TO_TICKS(20));
}
}
// ======================== UI ===========================
const int N_ITEMS = 8;
const char* ITEM_NAME[N_ITEMS] = {
"Mode", "Suhu target", "Pompa ON <", "Lampu ON <",
"Vent manual", "Pompa manual", "Lampu manual", "<< Kembali"
};
void adjustItem(int item, int d) {
LOCK_S();
switch (item) {
case 0: gCfg.autoMode = !gCfg.autoMode; break;
case 1: gCfg.tempSet = constrain(gCfg.tempSet + 0.5f * d, 20.0f, 40.0f); break;
case 2: gCfg.soilDry = constrain(gCfg.soilDry + d, 5, 70); break;
case 3: gCfg.lightMin = constrain(gCfg.lightMin + d, 5, 90); break;
case 4: gCfg.manVent = constrain(gCfg.manVent + 5 * d, 0, 90); break;
case 5: gCfg.manPump = !gCfg.manPump; break;
case 6: gCfg.manLamp = !gCfg.manLamp; break;
}
UNLOCK_S();
}
void itemValue(int i, const Settings& c, char* out, size_t n) {
switch (i) {
case 0: snprintf(out, n, "%s", c.autoMode ? "AUTO" : "MANUAL"); break;
case 1: snprintf(out, n, "%.1fC", c.tempSet); break;
case 2: snprintf(out, n, "%d%%", c.soilDry); break;
case 3: snprintf(out, n, "%d%%", c.lightMin); break;
case 4: snprintf(out, n, "%d deg", c.manVent); break;
case 5: snprintf(out, n, "%s", c.manPump ? "ON" : "OFF"); break;
case 6: snprintf(out, n, "%s", c.manLamp ? "ON" : "OFF"); break;
default: out[0] = 0;
}
}
void bar(int x, int y, int w, int h, int pct) {
pct = constrain(pct, 0, 100);
display.drawRect(x, y, w, h, SSD1306_WHITE);
display.fillRect(x + 1, y + 1, (w - 2) * pct / 100, h - 2, SSD1306_WHITE);
}
void getClock(char* out, size_t n) {
struct tm t;
if (getLocalTime(&t, 0)) strftime(out, n, "%H:%M", &t);
else strlcpy(out, "--:--", n);
}
void alarmText(uint8_t a, char* out, size_t n) {
out[0] = 0;
if (a & ALM_HOT) strlcat(out, "PANAS ", n);
if (a & ALM_TANK) strlcat(out, "TANGKI ", n);
if (a & ALM_SHOCK) strlcat(out, "GETAR ", n);
if (a & ALM_SENSOR) strlcat(out, "SENSOR", n);
}
void drawDashboard(const State& s, const Settings& c, uint32_t now) {
char clk[8]; getClock(clk, sizeof(clk));
display.setCursor(0, 0); display.print("GREENOS");
display.setCursor(56, 0); display.print(s.wifi ? "W" : "-"); display.print(s.mqtt ? "M" : "-");
display.setCursor(92, 0); display.print(clk);
display.setCursor(0, 11);
if (isnan(s.temp)) display.print("T:--.-C H:--%");
else display.printf("T:%.1fC H:%.0f%%", s.temp, s.hum);
const char* lbl[3] = { "Tanah", "Cahaya", "Tangki" };
int val[3] = { s.soil, s.light, s.tank };
for (int i = 0; i < 3; i++) {
int y = 22 + i * 10;
display.setCursor(0, y); display.print(lbl[i]);
bar(38, y, 62, 8, val[i]);
display.setCursor(104, y); display.printf("%d", val[i]);
}
display.setCursor(0, 55);
if (s.alarm && (now / 500) % 2 == 0) {
char at[32]; alarmText(s.alarm, at, sizeof(at));
display.print("!"); display.print(at);
} else {
display.printf("%s P:%s V:%d L:%s", c.autoMode ? "AUTO" : "MAN",
s.pump ? "ON" : "--", s.vent, s.lamp ? "ON" : "--");
}
}
void drawSystem(const State& s, uint32_t now) {
display.setCursor(0, 0); display.print("== SISTEM ==");
display.setCursor(0, 11); display.printf("Uptime: %lus", (unsigned long)(now / 1000));
display.setCursor(0, 21); display.printf("IP: %s", s.wifi ? WiFi.localIP().toString().c_str() : "-");
display.setCursor(0, 31); display.printf("RSSI:%d MQTT:%s", s.wifi ? WiFi.RSSI() : 0, s.mqtt ? "OK" : "off");
display.setCursor(0, 41); display.printf("Getaran: %.2f g", s.accel);
display.setCursor(0, 51); display.printf("Heap: %u KB", (unsigned)(ESP.getFreeHeap() / 1024));
}
void drawSettings(const Settings& c, int cursor, bool editing) {
display.setCursor(0, 0);
display.print(editing ? "== UBAH NILAI ==" : "== PENGATURAN ==");
int first = constrain(cursor - 2, 0, N_ITEMS - 6);
for (int r = 0; r < 6; r++) {
int i = first + r, y = 11 + r * 9;
bool sel = (i == cursor);
if (sel) { display.fillRect(0, y - 1, 128, 9, SSD1306_WHITE); display.setTextColor(SSD1306_BLACK); }
else display.setTextColor(SSD1306_WHITE);
display.setCursor(2, y); display.print(ITEM_NAME[i]);
char v[16], vv[20];
itemValue(i, c, v, sizeof(v));
if (sel && editing) snprintf(vv, sizeof(vv), "<%s>", v); else snprintf(vv, sizeof(vv), "%s", v);
display.setCursor(126 - (int)strlen(vv) * 6, y); display.print(vv);
}
display.setTextColor(SSD1306_WHITE);
}
void drawRing(const State& s, uint32_t now) {
ring.clear();
if (s.alarm) {
if ((now / 250) % 2 == 0) for (int i = 0; i < 16; i++) ring.setPixelColor(i, ring.Color(220, 0, 0));
} else {
int n = s.tank * 16 / 100;
uint32_t col = s.tank > 50 ? ring.Color(0, 150, 40) : s.tank > 25 ? ring.Color(180, 120, 0) : ring.Color(200, 0, 0);
for (int i = 0; i < n; i++) ring.setPixelColor(i, col);
if (s.pump) ring.setPixelColor((now / 80) % 16, ring.Color(0, 200, 255)); // animasi air mengalir
}
ring.show();
}
void taskUI(void*) {
LOCK_I();
bool oledOk = display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
UNLOCK_I();
ring.begin(); ring.setBrightness(40); ring.show();
int page = PG_DASH, cursor = 0;
bool editing = false, btnDown = false, longFired = false;
uint8_t acked = 0;
uint32_t btnT = 0, tDraw = 0, tRing = 0, beepUntil = 0;
int lastFreq = -1;
for (;;) {
uint32_t now = millis();
State s = snapState();
acked &= s.alarm;
bool unacked = (s.alarm & ~acked) != 0;
// ---- rotary encoder ----
int d;
portENTER_CRITICAL(&encMux); d = encDelta; encDelta = 0; portEXIT_CRITICAL(&encMux);
if (d) {
if (page == PG_SET) {
if (editing) adjustItem(cursor, d);
else cursor = (cursor + (d > 0 ? 1 : -1) + N_ITEMS) % N_ITEMS;
} else {
page = (page == PG_DASH) ? PG_SYS : PG_DASH;
}
beepUntil = now + 15;
}
// ---- tombol: klik singkat = pilih/ubah, tekan lama = AUTO/MANUAL ----
bool pressed = (digitalRead(PIN_ENC_SW) == LOW);
if (pressed && !btnDown) { btnDown = true; longFired = false; btnT = now; }
if (pressed && btnDown && !longFired && now - btnT >= 700) {
longFired = true;
LOCK_S(); gCfg.autoMode = !gCfg.autoMode; UNLOCK_S();
beepUntil = now + 120;
}
if (!pressed && btnDown) {
btnDown = false;
if (!longFired && now - btnT > 30) {
if (unacked) acked = s.alarm; // senyapkan alarm
else if (page != PG_SET) { page = PG_SET; cursor = 0; editing = false; }
else if (cursor == N_ITEMS - 1) { page = PG_DASH; editing = false; }
else editing = !editing;
beepUntil = now + 30;
}
}
// ---- buzzer ----
int f = 0;
if (now < beepUntil) f = 1800;
else if (unacked) f = ((now / 250) % 2) ? 2300 : 1700;
if (f != lastFreq) { PWM_TONE(PIN_BUZZER, CH_BUZZ, f); lastFreq = f; }
// ---- tampilan ----
if (oledOk && now - tDraw >= 100) {
tDraw = now;
Settings c = snapCfg();
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
if (page == PG_DASH) drawDashboard(s, c, now);
else if (page == PG_SYS) drawSystem(s, now);
else drawSettings(c, cursor, editing);
LOCK_I(); display.display(); UNLOCK_I();
}
if (now - tRing >= 50) { tRing = now; drawRing(s, now); }
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// ======================== SETUP ========================
void setup() {
Serial.begin(115200);
Serial.println("\n=== GreenOS booting ===");
pinMode(PIN_RELAY, OUTPUT); digitalWrite(PIN_RELAY, LOW);
pinMode(PIN_LED_LAMP, OUTPUT); pinMode(PIN_LED_ALARM, OUTPUT);
pinMode(PIN_TRIG, OUTPUT); pinMode(PIN_ECHO, INPUT);
pinMode(PIN_ENC_CLK, INPUT_PULLUP);
pinMode(PIN_ENC_DT, INPUT_PULLUP);
pinMode(PIN_ENC_SW, INPUT_PULLUP);
gStateMtx = xSemaphoreCreateMutex();
gI2cMtx = xSemaphoreCreateMutex();
Wire.begin(PIN_SDA, PIN_SCL);
Wire.setClock(400000);
PWM_ATTACH(PIN_SERVO, CH_SERVO, 50, 14);
PWM_ATTACH(PIN_BUZZER, CH_BUZZ, 2000, 10);
PWM_TONE(PIN_BUZZER, CH_BUZZ, 0);
servoAngle(0);
analogReadResolution(12);
attachInterrupt(digitalPinToInterrupt(PIN_ENC_CLK), isrEncoder, FALLING);
xTaskCreatePinnedToCore(taskNet, "net", 8192, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(taskSensors, "sensors", 4096, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(taskControl, "control", 4096, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(taskUI, "ui", 6144, NULL, 2, NULL, 1);
}
void loop() {
delay(1000); // semua pekerjaan ada di task FreeRTOS
}
Loading
esp32-devkit-c-v4
esp32-devkit-c-v4
Loading
ssd1306
ssd1306