// Heltec WiFi LoRa 32 V3 (SX1262) thermostat slave, continuous control
// - DS18B20 on GPIO7  (inside / control sensor)
// - DS18B20 on GPIO5  (greenhouse)
// - DS18B20 on GPIO4  (outside)
// - Relay on GPIO6 (via transistor + your LED)
// - Buzzer / alarm on GPIO1 (BUZZER_PIN)
//
//   GPT_TStat_tune_alarm_BEDRM.ino
//
// - Device ID prefix: commands must start with DEVICE_ID
//
// sudo /home/pi/lora/WIFI_GPT_lora/send_command_wifi_arg.py BEDRMt1502 set 15 deg 2 sec hysteresis   -   beeper BEDRMb15 15 sec
//
// Commands:
//
//   1) Set config + read:
//      "BEDRMtTTHH"
//                             sudo /home/pi/lora/WIFI_GPT_lora/send_command_wifi_arg.py BEDRMt1502
//      Example: BEDRMt3002  => setpoint 30C, hysteresis 2C
//      Replies "IN,GH,OUT,SETPOINT"
//
//   2) Read-only (no config change):
//      "BEDRMr0000" (or simply "BEDRMr")
//      Replies "IN,GH,OUT,SETPOINT"
//
//   3) Tune / alarm:
//      "BEDRMbXX"
//      XX = duration seconds (e.g. 15)
//      Plays a simple tune for XX seconds (non-blocking)
//      Also replies "IN,GH,OUT,SETPOINT"
//              must use
//                                sudo /home/pi/lora/WIFI_GPT_lora/send_command_wifi_arg.py  BEDRMb14
//


//
// OLED:
//   - Big top: inside temp + "RL ON"/"RL OFF" at top-right
//   - Line2: "GH:xx.x OUT:yy.y"
//   - Line3: "SP:xx H:yy"
//   - Line4: "RSS:-nn BEDRM"

#define HELTEC_POWER_BUTTON   // long-press PRG = power control
#include <heltec_unofficial.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <math.h>
#include <esp32-hal-ledc.h>

// ---- Device ID prefix ----
#define DEVICE_ID "BEDRM"     // 5-character ID for this node




// ---- DS18B20 pins ----
#define PIN_INSIDE     7   // inside / control sensor
#define PIN_GREENHOUSE 5   // greenhouse sensor
#define PIN_OUTSIDE    4   // outside sensor

// ---- Relay on GPIO6 ----
#define RELAY_PIN 6

// ---- Buzzer / alarm on GPIO1 ----
#define BUZZER_PIN 1

// ---- OneWire + DallasTemperature instances ----
OneWire oneWireInside(PIN_INSIDE);
OneWire oneWireGH(PIN_GREENHOUSE);
OneWire oneWireOut(PIN_OUTSIDE);

DallasTemperature sensorInside(&oneWireInside);
DallasTemperature sensorGH(&oneWireGH);
DallasTemperature sensorOut(&oneWireOut);

// ---- Temperature variables ----
float tempInsideC     = NAN;   // control temperature (inside)
float tempGreenhouseC = NAN;   // greenhouse
float tempOutsideC    = NAN;   // outside

// ---- Thermostat settings (initial) ----
float setpointC   = 16.0;
float hysteresisC = 2.0;

// LoRa receive
String rxData;
volatile bool rxFlag = false;

// Relay state (true = ON)
bool relayOn = false;

// Periodic temperature update interval
const unsigned long TEMP_INTERVAL_MS = 5000; // 5 seconds
unsigned long lastTempMillis = 0;

// Last radio stats for OLED
float lastRSSI = NAN;

// ---------------------------
// TUNE / ALARM (LEDC tone)
// ---------------------------
//static const int BUZZER_CH = 0;       // LEDC channel





// ---------------------------
// TUNE / ALARM (LEDC tone)  -- Arduino-ESP32 core 3.x API
// ---------------------------
static const int BUZZER_RES_BITS = 8; // PWM resolution bits
static bool buzzerPwmReady = false;

void buzzerInit() {
  if (buzzerPwmReady) return;

  // New API: channel is managed internally; you attach by pin.
  // ledcAttach(pin, freq, resolutionBits)
  ledcAttach(BUZZER_PIN, 2000, BUZZER_RES_BITS); // initial freq (placeholder)
  ledcWriteTone(BUZZER_PIN, 0);                  // silent
  buzzerPwmReady = true;
}

void buzzerTone(int freqHz) {
  if (!buzzerPwmReady) buzzerInit();
  // New API: functions take "pin" where old API used "channel"
  ledcWriteTone(BUZZER_PIN, freqHz > 0 ? freqHz : 0);
}






struct Note { uint16_t f; uint16_t d; };

/*
const Note tune_bunessan[] = {
  // "Morning has broken"
  {523, 200},   // C5  "Morn-"
  {587, 200},   // D5  "-ing"
  {0,   100},   // pause
  {659, 200},   // E5  "has"
  {0,   100},   // pause
  {784, 300},   // G5  "bro-"
  {880, 300},  // A5  "-ken"
  {0,   4000},   // pause
*/


  // 349.23, 440.00, 523.25, 698.46, 783.99
    // 659.25, 587.33, 523.25, 587.33, 523.25


const Note tune_bunessan[] = {


  {349, 200},   //   "Morn-"
  {440, 200},   //   "-ing"
  {0,   50},   // pause
  {523, 200},   //   "has"
  {0,   50},   // pause
  {698, 300},   //   "bro-"
  {784, 300},  //   "-ken"
 
  {0,   300},   // pause

  {659, 200},   //   "Like"
  {587, 200},   //   "the"
  {0,   50},   // pause
  {523, 200},   //   "first"
  {0,   50},   // pause
  {587, 300},   //   "mor"
  {523, 300},  //   "ning"

  {0,   300},   // pause

  {349, 200},   //   "Black"
  {392, 200},   //   "bird"
 //{0,   50},   //    pause
  {440, 200},   //   "has"
  {0,   50},   //    pause
  {523, 300},   //   "spoke"
  {587, 300},  //   "en"

  {0,   300},   // pause

  {523, 200},   //   "Like"
  {440, 200},   //   "the"
  {349, 200},   //   "first"
  {392, 300},   //   "bird"
  {0,   4000},   // pause

};



const uint16_t TUNE_LEN = sizeof(tune_bunessan) / sizeof(tune_bunessan[0]);

bool alarmActive = false;
unsigned long alarmEndMs = 0;

bool tuneActive = false;
uint16_t tuneIndex = 0;
unsigned long tuneNextMs = 0;

void startTune() {
  tuneActive = true;
  tuneIndex = 0;
  tuneNextMs = 0; // play immediately
}

void stopTune() {
  tuneActive = false;
  buzzerTone(0);
  tuneNextMs = 0;
}

void updateTune() {
  if (!tuneActive) return;
  unsigned long now = millis();
  if (now < tuneNextMs) return;

  // if (tuneIndex >= TUNE_LEN) {
  //  // loop tune while alarm active
  //  tuneIndex = 0;
  // }


if (tuneIndex >= TUNE_LEN) {
  // tune finished - stay silent
  stopTune();
  return;
}

  Note n = tune_bunessan[tuneIndex++];
  buzzerTone(n.f);
  tuneNextMs = now + n.d;
}

void updateAlarmTimer() {
  static bool alreadyPrintedEnd = false;

  if (!alarmActive) {
    alreadyPrintedEnd = false;
    return;
  }

  if (millis() >= alarmEndMs) {
    alarmActive = false;
    stopTune();
    if (!alreadyPrintedEnd) {
      Serial.println("Alarm ended.");
      alreadyPrintedEnd = true;
    }
  }
}


// ISR forward declaration
void onRx();

// ---- Generic DS18B20 read ----
float readTemperature(DallasTemperature &s) {
  s.requestTemperatures();
  float t = s.getTempCByIndex(0);
  if (t == DEVICE_DISCONNECTED_C) {
    return NAN;
  }
  return t;
}

// ---- Read all three sensors ----
void readAllTemperatures() {
  tempInsideC     = readTemperature(sensorInside);
  tempGreenhouseC = readTemperature(sensorGH);
  tempOutsideC    = readTemperature(sensorOut);

  Serial.print("Temps: IN=");
  if (isnan(tempInsideC)) Serial.print("NaN"); else Serial.printf("%.2f", tempInsideC);
  Serial.print("  GH=");
  if (isnan(tempGreenhouseC)) Serial.print("NaN"); else Serial.printf("%.2f", tempGreenhouseC);
  Serial.print("  OUT=");
  if (isnan(tempOutsideC)) Serial.print("NaN"); else Serial.printf("%.2f", tempOutsideC);
  Serial.println();
}

// ---- Apply thermostat logic (heating mode, using inside temp) ----
void updateThermostat(float insideC) {
  if (isnan(insideC)) {
    relayOn = false;
  } else {
    float onThreshold  = setpointC - hysteresisC;
    float offThreshold = setpointC;

    if (!relayOn && insideC <= onThreshold) {
      relayOn = true;
    } else if (relayOn && insideC >= offThreshold) {
      relayOn = false;
    }
  }

  digitalWrite(RELAY_PIN, relayOn ? HIGH : LOW);

  Serial.printf("Thermostat: set=%.1fC, hyst=%.1fC, IN=%.2fC, relay=%s\n",
                setpointC, hysteresisC,
                isnan(insideC) ? -999.0 : insideC,
                relayOn ? "ON" : "OFF");
}

// ---- Build and send reply: IN,GH,OUT,SETPOINT ----
/*
void sendTemperatureReply() {
  char replyMsg[64];
  snprintf(replyMsg, sizeof(replyMsg),
           "%.2f,%.2f,%.2f,%.2f",
           tempInsideC, tempGreenhouseC, tempOutsideC, setpointC);

  Serial.printf("Replying with: %s\n", replyMsg);

  radio.clearDio1Action();
  heltec_led(50);

  RADIOLIB(radio.transmit(replyMsg));
  Serial.printf("[RadioLib] radio.transmit(replyMsg) returned %d\n", _radiolib_status);

  heltec_led(0);
  radio.setDio1Action(onRx);
  RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));
}
*/


void sendTemperatureReply() {
  char replyMsg[96];
  snprintf(replyMsg, sizeof(replyMsg),
           "%s,%.2f,%.2f,%.2f,%.2f",
           DEVICE_ID, tempInsideC, tempGreenhouseC, tempOutsideC, setpointC);

  Serial.printf("Replying with: %s\n", replyMsg);

  radio.clearDio1Action();
  heltec_led(50);

  RADIOLIB(radio.transmit(replyMsg));
  Serial.printf("[RadioLib] radio.transmit(replyMsg) returned %d\n", _radiolib_status);

  heltec_led(0);
  radio.setDio1Action(onRx);
  RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));
}




// ---- OLED UI ----
void updateDisplay() {
  display.clear();

  display.setTextAlignment(TEXT_ALIGN_LEFT);
  display.setFont(ArialMT_Plain_24);

  String tempStr;
  if (isnan(tempInsideC)) tempStr = "--.-C";
  else tempStr = String(tempInsideC, 1) + "C";
  display.drawString(0, 0, tempStr);

  display.setFont(ArialMT_Plain_10);
  display.setTextAlignment(TEXT_ALIGN_RIGHT);
  String rlStr = relayOn ? "RL ON" : "RL OFF";
  display.drawString(127, 4, rlStr);

  display.setTextAlignment(TEXT_ALIGN_LEFT);

  String line2 = "GH:";
  if (isnan(tempGreenhouseC)) line2 += "--.-";
  else                        line2 += String(tempGreenhouseC, 1);
  line2 += " OUT:";
  if (isnan(tempOutsideC)) line2 += "--.-";
  else                     line2 += String(tempOutsideC, 1);
  display.drawString(0, 28, line2);

  int spInt = (int)round(setpointC);
  int hyInt = (int)round(hysteresisC);
  String line3 = "SP:" + String(spInt) + " H:" + String(hyInt);
  display.drawString(0, 40, line3);

  String line4 = "RSS:";
  if (isnan(lastRSSI)) line4 += "--";
  else                 line4 += String((int)lastRSSI);
  line4 += " ";
  line4 += DEVICE_ID;
  display.drawString(0, 52, line4);

  display.display();
}

// -------------------------------------------------------------------
// SETUP
// -------------------------------------------------------------------
void setup() {
  heltec_setup();
  Serial.println("Heltec V3 LoRa thermostat SLAVE (3 sensors, ID-only commands + tune alarm)");
  Serial.println("Initialising radio and DS18B20s...");

  sensorInside.begin();
  sensorGH.begin();
  sensorOut.begin();

  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW);
  relayOn = false;

  // Tune buzzer init
  buzzerInit();
  alarmActive = false;
  alarmEndMs  = 0;
  stopTune();

  RADIOLIB_OR_HALT(radio.begin());

  RADIOLIB_OR_HALT(radio.setCRC(0));
  RADIOLIB_OR_HALT(radio.setFrequency(868.0));
  RADIOLIB_OR_HALT(radio.setBandwidth(125.0));
  RADIOLIB_OR_HALT(radio.setSpreadingFactor(9));
  RADIOLIB_OR_HALT(radio.setCodingRate(5));   // 4/5
  RADIOLIB_OR_HALT(radio.setPreambleLength(8));
  RADIOLIB_OR_HALT(radio.setSyncWord(0x34));
  RADIOLIB_OR_HALT(radio.setOutputPower(17));

  radio.setDio1Action(onRx);
  RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));

  Serial.println("Commands:");
  Serial.println("  " DEVICE_ID "tTTHH  -> set setpoint/hyst, reply IN,GH,OUT,SET");
  Serial.println("  " DEVICE_ID "r0000  -> read-only, reply IN,GH,OUT,SET");
  Serial.println("  " DEVICE_ID "bXX    -> tune alarm for XX seconds, reply IN,GH,OUT,SET");
  Serial.println("Example: " DEVICE_ID "t3002 (30C, hyst 2C)");

  readAllTemperatures();
  updateThermostat(tempInsideC);
  lastTempMillis = millis();

  updateDisplay();
}

// -------------------------------------------------------------------
// LOOP
// -------------------------------------------------------------------
void loop() {
  heltec_loop();

  unsigned long now = millis();

  // Periodic background temperature check and thermostat control
  if (now - lastTempMillis >= TEMP_INTERVAL_MS) {
    lastTempMillis = now;
    readAllTemperatures();
    updateThermostat(tempInsideC);
    updateDisplay();
  }

  // Alarm/tune engine (non-blocking)
  updateAlarmTimer();
  updateTune();

  // Handle received LoRa packets
  if (rxFlag) {
    rxFlag = false;

    radio.readData(rxData);

    if (_radiolib_status == RADIOLIB_ERR_NONE) {
      rxData.trim();

      Serial.printf("RX: \"%s\"\n", rxData.c_str());
      float rssi = radio.getRSSI();
      float snr  = radio.getSNR();
      Serial.printf("  RSSI: %.2f dBm\n", rssi);
      Serial.printf("  SNR:  %.2f dB\n", snr);

      lastRSSI = rssi;

      if (rxData.length() >= 6) {
        String prefix = rxData.substring(0, 5);
        char cmdType  = rxData.charAt(5);

        if (prefix == DEVICE_ID) {

          // 1) Config command: DEVICE_ID + 't' + TTHH  (>= 10 chars)
          if (cmdType == 't') {
            if (rxData.length() >= 10) {
              String setStr  = rxData.substring(6, 8);
              String hystStr = rxData.substring(8, 10);

              int setInt  = setStr.toInt();
              int hystInt = hystStr.toInt();

              if (setInt >= -40 && setInt <= 99 && hystInt >= 0 && hystInt <= 50) {
                setpointC   = (float)setInt;
                hysteresisC = (float)hystInt;

                Serial.printf("New config: set=%.1fC, hyst=%.1fC\n", setpointC, hysteresisC);

                readAllTemperatures();
                updateThermostat(tempInsideC);
                updateDisplay();
                sendTemperatureReply();
              } else {
                Serial.println("Out-of-range setpoint/hysteresis, ignoring.");
              }
            } else {
              Serial.println("Thermostat command too short, ignoring.");
            }
          }

          // 2) Read-only command
          else if (cmdType == 'r') {
            Serial.println("Read-only command: no config change.");
            readAllTemperatures();
            updateThermostat(tempInsideC);
            updateDisplay();
            sendTemperatureReply();
          }

          // 3) Tune alarm command: DEVICE_ID + 'b' + XX
          else if (cmdType == 'b') {
            Serial.println("Tune alarm command received.");

            int durationSec = 0;
            if (rxData.length() >= 8) {
              String durStr = rxData.substring(6, 8);  // XX
              durationSec = durStr.toInt();
            }

            // Clamp 1..60 seconds
            if (durationSec <= 0) durationSec = 1;
            if (durationSec > 60) durationSec = 60;

            alarmActive = true;
            alarmEndMs  = millis() + (unsigned long)durationSec * 1000UL;
            startTune();
            Serial.printf("Alarm started for %d seconds.\n", durationSec);

            // Also reply as usual
            readAllTemperatures();
            updateThermostat(tempInsideC);
            updateDisplay();
            sendTemperatureReply();
          }

          else {
            Serial.println("Unknown command type for this device, ignoring.");
          }

        } else {
          Serial.println("Prefix does not match this device, ignoring.");
        }

      } else {
        Serial.println("Received too-short packet, ignoring.");
      }

      RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));

    } else {
      Serial.printf("RX error (%d), restarting RX\n", _radiolib_status);
      RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF));
    }
  }
}

// ---- ISR – keep it very short: just set a flag ----
void onRx() {
  rxFlag = true;
}



/*

Lyric Phrase     Musical Notes    Frequencies (Hz)
Morn-ing has bro-ken    F4 - A4 - C5 - F5 - G5    349.23, 440.00, 523.25, 698.46, 783.99
Like the first morn-ing    E5 - D5 - C5 - D5 - C5    659.25, 587.33, 523.25, 587.33, 523.25
Black-bird has spo-ken    F4 - G4 - A4 - C5 - D5    349.23, 392.00, 440.00, 523.25, 587.33
Like the first bird    C5 - A4 - F4 - G4    523.25, 440.00, 349.23, 392.00
Praise for the sing-ing    C5 - A4 - C5 - F5 - D5    523.25, 440.00, 523.25, 698.46, 587.33
Praise for the morn-ing    C5 - A4 - F4 - F4 - G4    523.25, 440.00, 349.23, 349.23, 392.00
Praise for them spring-ing    A4 - G4 - A4 - C5 - D5    440.00, 392.00, 440.00, 523.25, 587.33
Fresh from the world!    G4 - A4 - G4 - F4    392.00, 440.00, 392.00, 349.23

*/