// STM32F103C8T6 (蓝丸最小系统板) · 传感器学习程序
// 复用 HC-SR04_STM32_OLED版 项目同一块板子+OLED接线，HC-SR04已拿掉，
// 换成温度自动控制继电器的练习：温度超过设定档位就开继电器+黄灯。
//
// 接线：
//   OLED(SSD1306,I2C,0x3C): SCL=PB8, SDA=PB9 (跟原HC-SR04+OLED项目一致)
//     GND/VCC 用 PB6/PB7 这两个GPIO模拟供电(拉低当地、拉高当3.3V)
//   DHT11 数据脚: PB13 (VCC/GND直接接3.3V/GND，走板载上拉，不用MCU模拟供电)
//   继电器模块信号脚: PB12
//   LED(10mm黄灯): PA1，反接线(负极接MCU、正极接3.3V)，GPIO拉低才亮、拉高才灭
//   按键: PC14，另一端接3.3V(正极)，按下为高电平，用内部下拉——单击切换"手动强制"
//     开关状态(锁存)，不是按住才生效
//   4档按键: PA3(ADC)，黄/绿/红/蓝4个按键分压。2026-07-25实测校准过，raw ADC:
//     黄507/绿559/红713/蓝854(松开不按=0)。原本是"选4个固定档位"，后来改成
//     "+/-调节键"(黄+1/绿-1/红+5/蓝-5)，可以实时手动调阈值，不用改代码重新烧录。
//   串口也能直接设阈值：eagleCom发送框输入数字(如32)回车，直接设成32°C，
//     跟4键调节共用同一个阈值变量，两种方式随便用哪种都行。
//
// ⚠️ 继电器模块触发电平——大多数便宜的继电器模块是低电平触发(拉低导通)，这里先按
//    RELAY_ACTIVE_LOW=true 假设，如果实测方向反了(该开的时候关/该关的时候开)，
//    改成 false 即可。
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define DHT11_PIN PB13
#define RELAY_PIN PB12
#define LED_PIN PA1
#define BUTTON_PIN PC14
#define SELECTOR_PIN PA3

#define OLED_FAKE_GND_PIN PB6
#define OLED_FAKE_VCC_PIN PB7
#define OLED_SCL_PIN PB8
#define OLED_SDA_PIN PB9
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

#define RELAY_ACTIVE_LOW false // 2026-07-25实测：模块跳线设的是高电平触发，不是常见的低电平触发

HardwareSerial DebugSerial(PA10, PA9); // RX, TX，跟原HC-SR04项目共用同一路CP2102
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

const float THRESHOLD_MIN = 15.0;
const float THRESHOLD_MAX = 50.0;
const float THRESHOLD_STEP[4] = {1.0, -1.0, 5.0, -5.0}; // 黄+1 绿-1 红+5 蓝-5

void setRelay(bool on) {
  if (RELAY_ACTIVE_LOW) digitalWrite(RELAY_PIN, on ? LOW : HIGH);
  else digitalWrite(RELAY_PIN, on ? HIGH : LOW);
}

void setLed(bool on) {
  digitalWrite(LED_PIN, on ? LOW : HIGH); // 反接线，拉低才亮
}

// 4档按键分类：0=黄 1=绿 2=红 3=蓝，对应THRESHOLD_STEP的+1/-1/+5/-5
// 分界点取实测4个值(507/559/713/854)的相邻中点：533/636/784
int classifySelector(int raw) {
  if (raw < 533) return 0;   // 黄507 (含松开时的0)
  else if (raw < 636) return 1; // 绿559
  else if (raw < 784) return 2; // 红713
  else return 3;                // 蓝854
}

// DHT11 单总线读取，成功返回true并写入tempC/humidity。
// 失败时把具体卡在哪一步打到串口——"完全没响应"(接线/供电问题)和"校验和不对"
// (数据能收到，说明接线基本OK，是个别bit解码有偏差)是两种完全不同的毛病，
// 不能都只报一个笼统的false，不然没法对症下药。
bool readDHT11(float &tempC, float &humidity) {
  uint8_t data[5] = {0, 0, 0, 0, 0};

  pinMode(DHT11_PIN, OUTPUT);
  digitalWrite(DHT11_PIN, LOW);
  delay(20);
  digitalWrite(DHT11_PIN, HIGH);
  delayMicroseconds(30);
  pinMode(DHT11_PIN, INPUT_PULLUP);

  // 注：DHT11真实的80us握手脉冲/50us位起始脉冲，跟STM32duino下digitalRead()+
  // micros()的调用开销比起来余量很小，之前100us的超时太紧，正常时序也可能被
  // 误判成超时——这里统一放宽，给足MCU调用开销的余量。
  unsigned long t0 = micros();
  while (digitalRead(DHT11_PIN) == HIGH)
    if (micros() - t0 > 150) {
      DebugSerial.println("DHT11: no response (line stuck HIGH) -- 查接线/供电，数据脚没被拉低过");
      return false;
    }
  t0 = micros();
  while (digitalRead(DHT11_PIN) == LOW)
    if (micros() - t0 > 200) {
      DebugSerial.println("DHT11: response LOW too long -- 传感器没有按时结束80us低电平");
      return false;
    }
  t0 = micros();
  while (digitalRead(DHT11_PIN) == HIGH)
    if (micros() - t0 > 200) {
      DebugSerial.println("DHT11: response HIGH too long -- 传感器没有按时进入数据段");
      return false;
    }

  for (int i = 0; i < 40; i++) {
    t0 = micros();
    while (digitalRead(DHT11_PIN) == LOW)
      if (micros() - t0 > 200) {
        DebugSerial.print("DHT11: timeout waiting bit-start at bit ");
        DebugSerial.println(i);
        return false;
      }
    unsigned long start = micros();
    while (digitalRead(DHT11_PIN) == HIGH)
      if (micros() - start > 200) break;
    unsigned long len = micros() - start;
    data[i / 8] <<= 1;
    if (len > 40) data[i / 8] |= 1;
  }

  uint8_t checksum = data[0] + data[1] + data[2] + data[3];
  if (checksum != data[4]) {
    DebugSerial.print("DHT11: checksum mismatch, got ");
    DebugSerial.print(data[0]); DebugSerial.print(",");
    DebugSerial.print(data[1]); DebugSerial.print(",");
    DebugSerial.print(data[2]); DebugSerial.print(",");
    DebugSerial.print(data[3]); DebugSerial.print(" sum=");
    DebugSerial.print(checksum); DebugSerial.print(" expected=");
    DebugSerial.println(data[4]);
    return false;
  }

  humidity = data[0];
  tempC = data[2];
  return true;
}

void setup() {
  DebugSerial.begin(115200);
  pinMode(PC13, OUTPUT);

  pinMode(BUTTON_PIN, INPUT_PULLDOWN);
  pinMode(LED_PIN, OUTPUT);
  pinMode(RELAY_PIN, OUTPUT);
  setLed(false);
  setRelay(false);

  pinMode(OLED_FAKE_GND_PIN, OUTPUT);
  digitalWrite(OLED_FAKE_GND_PIN, LOW);
  pinMode(OLED_FAKE_VCC_PIN, OUTPUT);
  digitalWrite(OLED_FAKE_VCC_PIN, HIGH);
  delay(10);

  Wire.setSCL(OLED_SCL_PIN);
  Wire.setSDA(OLED_SDA_PIN);
  Wire.begin();

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
}

void loop() {
  digitalWrite(PC13, !digitalRead(PC13));

  static float lastTempC = NAN, lastHumidity = NAN;
  static bool dhtValid = false;
  static unsigned long lastDhtRead = 0;

  // DHT11最快约1秒采一次，这里每2秒读一次，之间沿用上一次的有效读数
  if (millis() - lastDhtRead > 2000) {
    lastDhtRead = millis();
    float t, h;
    if (readDHT11(t, h)) {
      lastTempC = t;
      lastHumidity = h;
      dhtValid = true;
    } else {
      dhtValid = false; // 读失败不清空旧值，只标记这一轮无效，继电器逻辑仍用旧值
    }
  }

  // 4档按键改成"+/-调节"：每按一下(检测上升沿，不是按住)在当前阈值上加/减一次，
  // 松手后阈值保持调整后的值，可以现场手动调，不用改代码重新烧录
  static float manualThreshold = 30.0; // 初始阈值，之后靠按键或串口调整
  static int lastRawGear = -1; // -1表示上一轮没有键被按住，用来做边沿检测
  int raw = analogRead(SELECTOR_PIN);
  const int IDLE_THRESHOLD = 200; // 明显低于黄档最低约500，用来判断"当前有没有键被按住"
  int rawGear = (raw > IDLE_THRESHOLD) ? classifySelector(raw) : -1;
  if (rawGear != -1 && lastRawGear == -1) {
    manualThreshold += THRESHOLD_STEP[rawGear];
    manualThreshold = constrain(manualThreshold, THRESHOLD_MIN, THRESHOLD_MAX);
  }
  lastRawGear = rawGear;

  // 串口直接设阈值：在eagleCom发送框输入一个数字(比如32)回车发送，
  // 直接把阈值设成这个数，不用一直按+/-键。跟按键调节共用同一个manualThreshold变量。
  static char serialBuf[8];
  static uint8_t serialLen = 0;
  while (DebugSerial.available()) {
    char c = DebugSerial.read();
    if (c == '\n' || c == '\r') {
      if (serialLen > 0) {
        // 只取整数部分，手动解析，不用atof()——atof/strtod会把整套C库浮点
        // 字符串解析代码链接进来，体积很大，这里只要整数精度完全用不上
        int intVal = 0;
        bool neg = false;
        for (uint8_t i = 0; i < serialLen; i++) {
          char c2 = serialBuf[i];
          if (c2 == '-') { neg = true; continue; }
          if (c2 >= '0' && c2 <= '9') intVal = intVal * 10 + (c2 - '0');
          else break;
        }
        manualThreshold = constrain((float)(neg ? -intVal : intVal), THRESHOLD_MIN, THRESHOLD_MAX);
        DebugSerial.print("[Serial] Threshold set to ");
        DebugSerial.println(manualThreshold, 0);
        serialLen = 0;
      }
    } else if (serialLen < sizeof(serialBuf) - 1) {
      serialBuf[serialLen++] = c;
    }
  }

  float threshold = manualThreshold;

  static bool manualOverride = false;
  static bool lastButtonState = false;
  bool buttonState = digitalRead(BUTTON_PIN) == HIGH;
  if (buttonState && !lastButtonState) {
    manualOverride = !manualOverride; // 检测按下的上升沿，每按一次翻转一次(锁存)，不需要按住
  }
  lastButtonState = buttonState;

  bool overTemp = !isnan(lastTempC) && lastTempC > threshold;
  bool relayOn = manualOverride || overTemp;

  setRelay(relayOn);
  setLed(relayOn);

  DebugSerial.print("Temp: ");
  DebugSerial.print(isnan(lastTempC) ? -1 : lastTempC, 0);
  DebugSerial.print(" C, Humidity: ");
  DebugSerial.print(isnan(lastHumidity) ? -1 : lastHumidity, 0);
  DebugSerial.print(" %, Threshold: ");
  DebugSerial.print(threshold, 0);
  DebugSerial.print(" C, raw ADC: ");
  DebugSerial.print(raw);
  DebugSerial.print(", Manual: ");
  DebugSerial.print(manualOverride ? "ON" : "off");
  DebugSerial.print(", Relay: ");
  DebugSerial.println(relayOn ? "ON" : "off");

  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Sensor Kit Monitor");
  display.drawLine(0, 10, 127, 10, SSD1306_WHITE);

  display.setCursor(0, 16);
  display.print("Temp: ");
  if (isnan(lastTempC)) display.print("--");
  else display.print(lastTempC, 0);
  display.print(" C  Hum: ");
  if (isnan(lastHumidity)) display.print("--");
  else display.print(lastHumidity, 0);
  display.println(" %");
  if (!dhtValid) display.println("(DHT11 read stale)");
  else display.println();

  display.setCursor(0, 34);
  display.print("Threshold: ");
  display.print(threshold, 0);
  display.println(" C");
  display.print("Y+1 G-1 R+5 B-5");

  display.setCursor(0, 54);
  display.print("Relay: ");
  display.print(relayOn ? "ON" : "off");
  if (manualOverride) display.print(" (manual)");

  display.display();
  delay(300);
}
