// Lab 1 Smart Lamp - ESP32 (Arduino)
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Adafruit_NeoPixel.h>
#include <Preferences.h>
#include <time.h>
#include "config.h"
// ===== NeoPixel =====
Adafruit_NeoPixel ring(NEOPIXEL_COUNT, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
// ===== WiFi/MQTT =====
WiFiClient espClient;
PubSubClient mqtt(espClient);
// ===== Preferences (NVS) =====
Preferences prefs;
// ===== Device state =====
enum Mode { OFFM, ONM, AUTOM };
Mode mode = AUTOM;
bool isOn = false;
uint32_t color = 0xFFFFFF;
uint8_t brightness = 128;
bool motion = false; // підсумковий стан "є присутність"
int lux = 0;
int luxRaw = 0;
unsigned long lastSwitchMs = 0;
unsigned long lastTeleMs = 0;
// ===== Habit model =====
static const int DAYS = 7;
static const int SLOTS = 96;
uint16_t habit[DAYS][SLOTS];
float habitPrior = 1.0f;
float habitThresh = 0.5f;
// ===== PIR edge latch (симулятор) =====
#define MOTION_WINDOW_MS 2000 // тримати присутність 2 с після імпульсу
volatile bool pirEdge = false; // ISR лише ставить прапорець
void IRAM_ATTR pirISR() { pirEdge = true; }
// ===== FWD decl =====
void setLamp(bool on);
void publishState(const char* reason);
void publishTelemetry();
bool predictShouldTurnOn();
void handleAuto();
void handleButton();
void saveHabit();
void loadHabit();
void onMqttMsg(char* topic, byte* payload, unsigned int len);
void ensureMqtt();
// ===== Utils: time/slots =====
int slot15min() {
time_t t = time(nullptr);
struct tm ti; localtime_r(&t, &ti);
return ti.tm_hour*4 + (ti.tm_min/15);
}
int dowMon0() {
time_t t = time(nullptr);
struct tm ti; localtime_r(&t, &ti);
return (ti.tm_wday + 6) % 7; // Mon=0..Sun=6
}
// ===== IO helpers =====
int readLuxOnce() {
static int filt = 0;
int raw = analogRead(PIN_LDR); // 0..4095
int plux = map(raw, 4095, 0, 0, 1000); // 0..1000 (більше = світліше)
filt = (filt*3 + plux) / 4; // просте згладжування
luxRaw = raw;
return filt;
}
void setLamp(bool on) {
isOn = on;
ring.setBrightness(brightness);
uint32_t c = on ? color : 0x000000;
for (int i=0;i<NEOPIXEL_COUNT;i++) ring.setPixelColor(i, c);
ring.show();
lastSwitchMs = millis();
}
void publishState(const char* reason) {
StaticJsonDocument<256> doc;
doc["mode"] = (mode==OFFM?"off":mode==ONM?"on":"auto");
doc["isOn"] = isOn; doc["brightness"] = brightness; doc["color"] = color;
doc["lux"] = lux; doc["motion"] = motion; doc["reason"] = reason;
#if USE_NETWORK
char buf[256]; size_t n = serializeJson(doc, buf, sizeof(buf));
mqtt.publish(TOPIC_STATE, buf, n);
#endif
serializeJson(doc, Serial); Serial.println();
}
void publishTelemetry() {
StaticJsonDocument<256> doc;
time_t t = time(nullptr);
doc["ts"] = (uint32_t)t;
doc["lux"] = lux; doc["motion"] = motion;
doc["mode"] = (mode==OFFM?"off":mode==ONM?"on":"auto");
doc["isOn"] = isOn;
doc["predictedOn"] = predictShouldTurnOn();
#if USE_NETWORK
char buf[256]; size_t n = serializeJson(doc, buf, sizeof(buf));
mqtt.publish(TOPIC_TELE, buf, n);
#endif
serializeJson(doc, Serial); Serial.println();
}
// ===== Habit model =====
void recordManualOn() {
int d = dowMon0(), s = slot15min();
if (d>=0 && d<DAYS && s>=0 && s<SLOTS && habit[d][s] < 65535) habit[d][s]++;
}
bool predictShouldTurnOn() {
int d = dowMon0(), s = slot15min();
float hits = (d>=0 && d<DAYS && s>=0 && s<SLOTS) ? (float)habit[d][s] : 0.0f;
float p = hits / (hits + habitPrior);
return (p > habitThresh) && (lux < LUX_ON_THRESH);
}
void saveHabit() { prefs.begin("habit", false); prefs.putBytes("hist", habit, sizeof(habit)); prefs.end(); }
void loadHabit() { memset(habit, 0, sizeof(habit)); prefs.begin("habit", true); prefs.getBytes("hist", habit, sizeof(habit)); prefs.end(); }
// ===== MQTT =====
void onMqttMsg(char* topic, byte* payload, unsigned int len) {
StaticJsonDocument<256> doc;
if (deserializeJson(doc, payload, len)) return;
if (doc.containsKey("state")) {
String s = doc["state"].as<String>();
if (s=="on") { mode=ONM; setLamp(true); recordManualOn(); publishState("manual_on"); saveHabit(); }
if (s=="off") { mode=OFFM; setLamp(false); publishState("manual_off"); }
if (s=="auto"){ mode=AUTOM; publishState("auto_mode"); }
}
if (doc.containsKey("brightness")) { brightness = doc["brightness"]; if (isOn) setLamp(true); publishState("set_brightness"); }
if (doc.containsKey("color")) {
const char* hex = doc["color"];
if (hex && strlen(hex)>=7 && hex[0]=='#') {
long v = strtol(hex+1, nullptr, 16);
color = ((v>>16)&0xFF)<<16 | ((v>>8)&0xFF)<<8 | (v&0xFF);
if (isOn) setLamp(true);
publishState("set_color");
}
}
}
void ensureMqtt() {
#if USE_NETWORK
if (mqtt.connected()) return;
while (!mqtt.connected()) {
String cid = String("esp32-") + String((uint32_t)ESP.getEfuseMac(), HEX);
bool ok = String(MQTT_USER).length() ? mqtt.connect(cid.c_str(), MQTT_USER, MQTT_PASS)
: mqtt.connect(cid.c_str());
if (ok) { mqtt.subscribe(TOPIC_CMD); publishState("reconnect"); }
else delay(1000);
}
#endif
}
// ===== FSM =====
void handleAuto() {
unsigned long now = millis();
bool canSwitchOn = (now - lastSwitchMs) > HOLD_OFF_MS;
bool canSwitchOff = (now - lastSwitchMs) > HOLD_ON_MS;
bool dark = lux < LUX_ON_THRESH;
bool light = lux > LUX_OFF_THRESH;
bool predictedOn = predictShouldTurnOn();
if (!isOn && canSwitchOn && ((dark && motion) || predictedOn)) {
setLamp(true); publishState(predictedOn ? "ai_on" : "dark+motion");
} else if (isOn && canSwitchOff && ( light || (!motion && !predictedOn) )) {
setLamp(false); publishState(light ? "light_off" : "idle_off");
}
}
// ===== Button =====
void handleButton() {
static uint8_t last = HIGH;
static unsigned long lastDebounce = 0;
uint8_t cur = digitalRead(PIN_BUTTON);
if (cur != last) { lastDebounce = millis(); last = cur; }
if (millis() - lastDebounce > 30 && cur == LOW) {
if (mode==OFFM) { mode = ONM; setLamp(true); recordManualOn(); saveHabit(); publishState("button_on"); }
else if (mode==ONM) { mode = OFFM; setLamp(false); publishState("button_off"); }
else { setLamp(!isOn); if (isOn){ recordManualOn(); saveHabit(); publishState("button_toggle_on"); } else publishState("button_toggle_off"); }
delay(250);
}
}
void setup() {
Serial.begin(115200); delay(200);
// ADC для LDR
analogReadResolution(12);
analogSetPinAttenuation(PIN_LDR, ADC_11db);
pinMode(PIN_PIR, INPUT);
pinMode(PIN_BUTTON, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_PIR), pirISR, RISING);
ring.begin(); ring.clear(); ring.show();
#if USE_NETWORK
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMqttMsg);
ensureMqtt();
setenv("TZ", TZ_INFO, 1); tzset();
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
#endif
loadHabit();
setLamp(false);
publishState("boot");
}
void loop() {
ensureMqtt();
#if USE_NETWORK
mqtt.loop();
#endif
// LDR
lux = readLuxOnce();
// PIR: фронт + вікно присутності
static unsigned long lastMotion = 0;
if (pirEdge) { lastMotion = millis(); pirEdge = false; }
bool pirLevel = digitalRead(PIN_PIR) == HIGH;
motion = pirLevel || (millis() - lastMotion) < MOTION_WINDOW_MS;
if (mode == AUTOM) handleAuto();
handleButton();
if (millis() - lastTeleMs > TELEMETRY_MS) {
lastTeleMs = millis();
publishTelemetry();
}
// видимий дебаг раз/0.5с
static unsigned long dbgMs = 0;
if (millis() - dbgMs > 500) {
dbgMs = millis();
Serial.printf("raw=%d lux=%d motion=%d isOn=%d\n", luxRaw, lux, motion, isOn);
}
}