#include <stdint.h>

/*
   Bathroom fan delay.

   Light switch signal is delayed by a configurable time. The fan is enabled
    - if the light is switched off and the "light on" duration was at least a specific time.
    - if the light in on for a longer time than a configured duration.

   2018-07-28: Frank Bättermann
*/


// Minimum "light on" duration which will enable the fan after turning the light off
#define DURATION_FAN_AFTER_LIGHTOFF         (3 * 60L) // [s]
// Minimum "light on" duration which will enable the fan while the light is still on
#define DURATION_FAN_WHILE_LIGHTON          (15 * 60L) // [s]
// Fan runtime after turning off the light with already running fan
#define FAN_RT_EANBLED_LIGHTON              (15 * 60L) // [s]
// Minimum runtime of fan after turning off the light (measured)
#define FAN_RT_AFTER_LIGHT_TURNOFF          100L   // [s]

// Pin definitions for Arduino Nano
#define PIN_LIGHT_ON                2 // D2
#define PIN_FAN_ON                  3 // D3
#define PIN_FAN_ON_LED              LED_BUILTIN // D13/SCK (used to indicate fan start during debugging)

// Uncomment define to enable serial debug output
#define SERIAL_DEBUG

uint32_t runtime; // UINT32_MAX in seconds equals 136 years of runtime
uint16_t runtime_ms;
uint32_t millis_last;

// Current runtime when the light was turned on
uint32_t lighton_start_runtime; // [s]
// Flag to remember we saved the runtime on light activation
bool lighton_runtime_saved;
// Current runtime when fan was turned on
uint32_t fanon_start_runtime; // [s]
// Runtime until when the fan is kept active
uint32_t fanon_end_runtime; // [s]

union
{
  struct
  {
    uint8_t light_on: 1;
  } one;
  uint8_t all;
} inputs;


union
{
  struct
  {
    uint8_t fan_on: 1;
  } one;
  uint8_t all;
} outputs;

void setup()
{
#ifdef SERIAL_DEBUG
  // Initialize seriel port
  Serial.begin(9600);
  while (!Serial) { }
#endif // SERIAL_DEBUG

  runtime = 0;
  runtime_ms = 0;
  millis_last = millis();
  lighton_start_runtime = 0;
  lighton_runtime_saved = false;
  fanon_start_runtime = 0;
  fanon_end_runtime = 0;
  inputs.all = 0;
  outputs.all = 0;

  pinMode(PIN_LIGHT_ON, INPUT); // High when light is on
  pinMode(PIN_FAN_ON, OUTPUT); // High starts fan
  pinMode(PIN_FAN_ON_LED, OUTPUT); // Debug
}


void loop()
{
  uint32_t millis_now = millis();
  uint16_t millis_diff = 0;
  uint8_t  seconds = 0; // seconds passed between two loop()-calls

  // Count milliseconds into a runtime in seconds
  runtime_ms += (millis_now > millis_last ? millis_now - millis_last : UINT32_MAX - millis_last + millis_now + 1);
  while (runtime_ms >= 1000)
  {
    // Runtime will overflow at UINT32_MAX (after 136 years)
    runtime_ms -= 1000;
    runtime++;
    seconds++;
  }

#ifdef SERIAL_DEBUG
  if (seconds > 0)
  {
    // (At least) one second passed
    Serial.print(F("runtime: ")); Serial.print(runtime, DEC);
    Serial.print(F(", light on: ")); Serial.print(inputs.one.light_on, DEC);
    Serial.print(F(", fan on: ")); Serial.print(outputs.one.fan_on, DEC);
    Serial.print(F(", lighton_start_runtime ")); Serial.print(lighton_start_runtime, DEC);
    Serial.print(F(", fanon_start_runtime ")); Serial.print(fanon_start_runtime, DEC);
    Serial.print(F(", fanon_end_runtime ")); Serial.println(fanon_end_runtime, DEC);
  }
#endif // SERIAL_DEBUG

  if (outputs.one.fan_on == 1 && inputs.one.light_on == 0)
  {
    // Fan timeout
    if (runtime >= fanon_end_runtime && runtime - fanon_end_runtime < UINT32_MAX / 2)
    {
      // Turn off fan (also keep uint32 wrap around in mind)
#ifdef SERIAL_DEBUG
      Serial.print(F("turn off fan at runtime: "));
      Serial.println(runtime, DEC);
#endif // SERIAL_DEBUG
      fan_off();
    }
  }

  check_inputs();
  delay(20); // Delay helpful with debouncing inputs
  millis_last = millis_now;
}


void check_inputs(void)
{
  static uint8_t in_light_prev = 0;

  uint8_t in_light = digitalRead(PIN_LIGHT_ON);

  // Debounce and remember this input status
  if (in_light > 0 && in_light_prev > 0)
  {
    // Two readings in a row read the same and light not recognized as "on", yet.
    eval_lighton();
    inputs.one.light_on = 1;
  }
  else
  {
    // React on two subsequent low states
    eval_lightoff();
    inputs.one.light_on = 0;
  }

  in_light_prev = in_light;
}


/*
   Compute duration the light has been on for.
*/
uint32_t inline lighton_duration(void)
{
  return runtime > lighton_start_runtime ? runtime - lighton_start_runtime : UINT32_MAX - lighton_start_runtime + runtime + 1;
}


/*
   Compute the end runtime for the active fan
*/
void inline fanon_set_end(uint32_t duration)
{
  if (duration > FAN_RT_AFTER_LIGHT_TURNOFF)
  {
    fanon_end_runtime = (UINT32_MAX - duration > runtime ? runtime + duration : duration - (UINT32_MAX - runtime) - 1) - FAN_RT_AFTER_LIGHT_TURNOFF;
#ifdef SERIAL_DEBUG
    Serial.print(F("fan will be kept on until "));
    Serial.println(fanon_end_runtime, DEC);
#endif // SERIAL_DEBUG
  }
  else
  {
    // Minimal runtime
    fanon_end_runtime = runtime + 2;
  }
}


void eval_lighton(void)
{
  if (lighton_runtime_saved)
  {
    // Fan is inactive and light has been on for at least a while:
    if (outputs.one.fan_on == 0 && lighton_duration() >= DURATION_FAN_WHILE_LIGHTON)
      // Turn on fan with light still on
      fan_on();
  }
  else
  {
    // Light was turned on right now
    lighton_start_runtime = runtime;
    lighton_runtime_saved = true;
  }
}


void eval_lightoff(void)
{
  if (!lighton_runtime_saved)
    return;

  uint32_t duration = lighton_duration();

#ifdef SERIAL_DEBUG
  Serial.print(F("light duration: "));
  Serial.println(duration, DEC);
#endif // SERIAL_DEBUG

  if (duration >= DURATION_FAN_WHILE_LIGHTON)
  {
    // Fan was already enabled while light was on: Keep fan on for defined duration
    fanon_set_end(FAN_RT_EANBLED_LIGHTON);
    //fan_on();
  }
  else if (duration >= DURATION_FAN_AFTER_LIGHTOFF)
  {
    // Turn on fan after turning off light: Turn on fan for the duration the light was on
    fanon_set_end(duration);
    fan_on();
  }

  // Clear counters
  lighton_start_runtime = 0;
  lighton_runtime_saved = false;
}


void fan_on(void)
{
  outputs.one.fan_on = 1;
  fanon_start_runtime = runtime;
  digitalWrite(PIN_FAN_ON, HIGH);
  digitalWrite(PIN_FAN_ON_LED, HIGH); // Debug
}


void fan_off(void)
{
  outputs.one.fan_on = 0;
  fanon_start_runtime = 0;
  fanon_end_runtime = 0;
  digitalWrite(PIN_FAN_ON, LOW);
  digitalWrite(PIN_FAN_ON_LED, LOW); // Debug
}

