/*
======================================================================================
  Motion Activated Model Train (ESP32 C3 Super Mini) 

  Author:
    Stefan Bracher https://stefan.bracher.info/microcontrollers.php#Train-Motion-Activated-ESP32

  Behaviour:
    - PIR detects visitors
    - Train accelerates
    - Train cruises until 3 REED passes are counted
    - Train slows down and stops
    - Waits 10 seconds
    - If PIR still detects motion, train restarts
    - If no motion, train stays stopped

  Required Libraries:
    - Adafruit GFX Library
    - Adafruit ST7735 and ST7789 Library

  Wiring:
    PIR VCC   -> 5V
    PIR GND   -> GND
    PIR OUT   -> GPIO 3

    REED      -> GPIO 4 and GND

    L298N ENA -> GPIO 5
    L298N IN1 -> GPIO 6
    L298N IN2 -> GPIO 7
    L298N GND -> ESP GND

    TFT GND   -> GND
    TFT VCC   -> 3.3V
    TFT SCK   -> GPIO 10
    TFT SDA   -> GPIO 2
    TFT RES   -> GPIO 0
    TFT DC    -> GPIO 1
    TFT CS    -> GPIO 20
    TFT BL    -> 3.3V

    This source code may be used and modified for personal and educational purposes.
    Attribution appreciated.
=======================================================================================
*/


#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>


// --- Pins ----------------------------------------------------------------------------
const int pirPin  = 3;
const int reedPin = 4;

const int enaPin = 5;
const int in1Pin = 6;
const int in2Pin = 7;

const int tftSckPin = 10;
const int tftSdaPin = 2;
const int tftResPin = 0;
const int tftDcPin  = 1;
const int tftCsPin  = 20;
// ------------------------------------------------------------------------------------


// --- TFT -----------------------------------------------------------------------------
Adafruit_ST7735 tft = Adafruit_ST7735(
  tftCsPin,
  tftDcPin,
  tftSdaPin,
  tftSckPin,
  tftResPin
);
// ------------------------------------------------------------------------------------


// --- Settings ------------------------------------------------------------------------
const unsigned long baudRate = 115200;

const int pwmFrequency = 20000;
const int pwmResolution = 8;

const int targetSpeed = 50;
const int rampStep = 1;
const unsigned long rampDelayMs = 80;

const int passesToStop = 3;

const unsigned long reedDebounceMs = 40;
const unsigned long minPassTimeMs = 1000;

const unsigned long restartDelayMs = 10000;
const unsigned long waitingDisplayUpdateMs = 1000;
// ------------------------------------------------------------------------------------


// --- Global Variables ----------------------------------------------------------------
int currentSpeed = 0;
int passCounter = 0;

String trainStatus = "Stopped";

int lastRawReedState = HIGH;
int stableReedState = HIGH;
unsigned long lastReedChangeTime = 0;
unsigned long lastPassTime = 0;

unsigned long lastWaitingDisplayUpdate = 0;

// Cached display values, used to avoid unnecessary redraws
String displayedTrainStatus = "";
String displayedPirStatus = "";
String displayedReedStatus = "";
int displayedLoopsUntilStop = -1;
// ------------------------------------------------------------------------------------


// --- Print Wiring --------------------------------------------------------------------
void printWiring() {
  Serial.println();
  Serial.println("=====================================================");
  Serial.println(" ESP32-C3 Motion Activated Model Train");
  Serial.println("=====================================================");
  Serial.println("PIR OUT   -> GPIO 3");
  Serial.println("REED      -> GPIO 4 and GND");
  Serial.println("L298N ENA -> GPIO 5");
  Serial.println("L298N IN1 -> GPIO 6");
  Serial.println("L298N IN2 -> GPIO 7");
  Serial.println("TFT SCK   -> GPIO 10");
  Serial.println("TFT SDA   -> GPIO 2");
  Serial.println("TFT RES   -> GPIO 0");
  Serial.println("TFT DC    -> GPIO 1");
  Serial.println("TFT CS    -> GPIO 20");
  Serial.println("TFT BL    -> 3.3V");
  Serial.println("=====================================================");
  Serial.println();
}
// ------------------------------------------------------------------------------------


// --- Display Setup -------------------------------------------------------------------
void setupDisplay() {
  tft.initR(INITR_BLACKTAB);
  tft.setRotation(1);
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextWrap(false);

  tft.setTextSize(1);
  tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK);

  tft.setCursor(4, 4);
  tft.println("Small Layout Train");

  tft.drawLine(0, 16, 160, 16, ST77XX_WHITE);

  tft.setCursor(4, 28);
  tft.print("Motion:");

  tft.setCursor(4, 48);
  tft.print("REED:");

  tft.setCursor(4, 68);
  tft.print("Train:");

  tft.setCursor(4, 88);
  tft.print("Loops left:");

  tft.setCursor(4, 112);
  tft.print("stefan.bracher.info");
}
// ------------------------------------------------------------------------------------


// --- Clear Field ---------------------------------------------------------------------
void clearField(int x, int y, int w, int h) {
  tft.fillRect(x, y, w, h, ST77XX_BLACK);
}
// ------------------------------------------------------------------------------------


// --- Update Display ------------------------------------------------------------------
void updateDisplay() {
  String pirStatus;
  String reedStatus;
  int loopsUntilStop = passesToStop - passCounter;

  if (loopsUntilStop < 0) loopsUntilStop = 0;

  if (digitalRead(pirPin) == HIGH) {
    pirStatus = "Detected";
  } else {
    pirStatus = "None";
  }

  if (stableReedState == LOW) {
    reedStatus = "Triggered";
  } else {
    reedStatus = "Open";
  }

  tft.setTextSize(1);
  tft.setTextColor(ST77XX_WHITE, ST77XX_BLACK);

  if (pirStatus != displayedPirStatus) {
    clearField(76, 28, 80, 10);
    tft.setCursor(76, 28);
    tft.print(pirStatus);
    displayedPirStatus = pirStatus;
  }

  if (reedStatus != displayedReedStatus) {
    clearField(76, 48, 80, 10);
    tft.setCursor(76, 48);
    tft.print(reedStatus);
    displayedReedStatus = reedStatus;
  }

  if (trainStatus != displayedTrainStatus) {
    clearField(76, 68, 80, 10);
    tft.setCursor(76, 68);
    tft.print(trainStatus);
    displayedTrainStatus = trainStatus;
  }

  if (loopsUntilStop != displayedLoopsUntilStop) {
    clearField(76, 88, 80, 10);
    tft.setCursor(76, 88);
    tft.print(loopsUntilStop);
    displayedLoopsUntilStop = loopsUntilStop;
  }
}
// ------------------------------------------------------------------------------------


// --- Convert Percent to PWM Duty -----------------------------------------------------
int percentToDuty(int percent) {
  if (percent < 0) percent = 0;
  if (percent > 100) percent = 100;

  return (percent * 255) / 100;
}
// ------------------------------------------------------------------------------------


// --- Apply Motor Speed ---------------------------------------------------------------
void applyMotor() {
  int duty = percentToDuty(currentSpeed);

  if (currentSpeed <= 0) {
    digitalWrite(in1Pin, LOW);
    digitalWrite(in2Pin, LOW);
    ledcWrite(enaPin, 0);
    return;
  }

  digitalWrite(in1Pin, HIGH);
  digitalWrite(in2Pin, LOW);
  ledcWrite(enaPin, duty);
}
// ------------------------------------------------------------------------------------


// --- Ramp Motor ----------------------------------------------------------------------
void rampToSpeed(int target) {
  if (target > currentSpeed) {
    trainStatus = "Accelerating";
  } else if (target < currentSpeed) {
    trainStatus = "Slowing down";
  }

  updateDisplay();

  while (currentSpeed < target) {
    currentSpeed += rampStep;
    if (currentSpeed > target) currentSpeed = target;

    applyMotor();
    delay(rampDelayMs);
  }

  while (currentSpeed > target) {
    currentSpeed -= rampStep;
    if (currentSpeed < target) currentSpeed = target;

    applyMotor();
    delay(rampDelayMs);
  }

  if (currentSpeed == 0) {
    trainStatus = "Stopped";
  } else {
    trainStatus = "Cruising";
  }

  updateDisplay();
}
// ------------------------------------------------------------------------------------


// --- Check REED Sensor ----------------------------------------------------------------
bool reedPassDetected() {
  int rawReedState = digitalRead(reedPin);
  unsigned long now = millis();

  if (rawReedState != lastRawReedState) {
    lastRawReedState = rawReedState;
    lastReedChangeTime = now;
  }

  if (now - lastReedChangeTime >= reedDebounceMs) {
    if (rawReedState != stableReedState) {
      stableReedState = rawReedState;
      updateDisplay();

      if (stableReedState == LOW && now - lastPassTime >= minPassTimeMs) {
        lastPassTime = now;

        Serial.println("REED pass detected");

        return true;
      }
    }
  }

  return false;
}
// ------------------------------------------------------------------------------------


// --- Run Train Sequence --------------------------------------------------------------
void runTrainSequence() {
  Serial.println();
  Serial.println("Motion detected. Starting train.");

  passCounter = 0;
  trainStatus = "Accelerating";
  updateDisplay();

  rampToSpeed(targetSpeed);

  Serial.println("Train cruising. Waiting for REED passes.");

  trainStatus = "Cruising";
  updateDisplay();

  while (passCounter < passesToStop) {
    if (reedPassDetected()) {
      passCounter++;

      Serial.print("Pass count: ");
      Serial.print(passCounter);
      Serial.print(" / ");
      Serial.println(passesToStop);

      updateDisplay();
    }

    delay(5);
  }

  Serial.println("Third pass detected. Slowing down.");

  trainStatus = "Slowing down";
  updateDisplay();

  rampToSpeed(0);

  Serial.println("Train stopped.");
}
// ------------------------------------------------------------------------------------


// --- Wait Before Restart -------------------------------------------------------------
void waitBeforeRestart() {
  Serial.println("Waiting 10 seconds before checking PIR again...");

  trainStatus = "Stopped";
  updateDisplay();

  delay(restartDelayMs);

  Serial.println("Checking PIR again...");
}
// ------------------------------------------------------------------------------------


// --- Setup ---------------------------------------------------------------------------
void setup() {
  Serial.begin(baudRate);
  delay(1000);

  printWiring();

  pinMode(pirPin, INPUT);
  pinMode(reedPin, INPUT_PULLUP);

  pinMode(in1Pin, OUTPUT);
  pinMode(in2Pin, OUTPUT);

  ledcAttach(enaPin, pwmFrequency, pwmResolution);

  setupDisplay();

  currentSpeed = 0;
  passCounter = 0;
  trainStatus = "Stopped";
  applyMotor();
  updateDisplay();

  Serial.println("Setup complete.");
  Serial.println("Waiting for PIR motion...");
}
// ------------------------------------------------------------------------------------


// --- Main Loop -----------------------------------------------------------------------
void loop() {
  int pirState = digitalRead(pirPin);
  unsigned long now = millis();

  if (pirState == HIGH) {
    runTrainSequence();
    waitBeforeRestart();
  }

  else {
    trainStatus = "Stopped";
    currentSpeed = 0;
    applyMotor();

    if (now - lastWaitingDisplayUpdate >= waitingDisplayUpdateMs) {
      updateDisplay();
      Serial.println("No motion. Train stays stopped.");
      lastWaitingDisplayUpdate = now;
    }
  }
}
// ------------------------------------------------------------------------------------