/*
  Open and close a dog door with a servo and limit switches via the web
*/

#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <ArduinoMDNS.h>
#include <Servo.h>
#include <AsyncTimer.h>


EthernetUDP udp;
MDNS mdns(udp);

byte mac[] = {
  0xD0, 0x99, 0x1E, 0xD0, 0x99, 0x1E
};
//IPAddress ip(192, 168, 10, 99);

EthernetServer server(80);

bool USE_SERIAL=false; // ideally don't use serial during normal operation so it doesn't hang
bool USE_ETHERNET=true;
int SAFETY_STOP_TIME_MS=10000; // how many milliseconds to turn the servo before giving up
int SERVO_DEBOUNCE_TIME_MS=200; // how many milliseconds to wait before checking the limit switch status again
bool DETACH_SERVO_WHEN_DONE=true; // detaching lets the servo rotate semi-freely, remaining attached holds the servo at "stop"

Servo myservo; 
AsyncTimer t;
int limitUpPin = 2;
int limitDownPin = 3;
int servoPin = 9;
int ethernetPin = 10;

int downward = 100; // lower to lessen down speed, max 180
int upward = 0;    // raise to lessen up speed, min 0
int stop = 90;      // adjust in case 90 isn't "stop" for your servo, but if you experience jitter probably adjust the servo potentiometer

bool isMaxUp = false;
bool isMaxDown = false;

// const unsigned long TIMER0_COUNT = 500;    // 500 msec timer interval

/*
// TIMER0 interrupt handler
ISR( TIMER0_COMPA_vect ) {
  // recheck the sensors periodically just in case
  handleSensor();
}
*/

// the setup routine runs once when you press reset:
void setup() {
  // give ethernet a bit of time to stabilize power on startup before init
  if (USE_ETHERNET) delay(5000);

  // initialize serial communication at 9600 bits per second:
  if (USE_SERIAL) Serial.begin(9600);
  pinMode(limitUpPin, INPUT_PULLUP);
  pinMode(limitDownPin, INPUT_PULLUP);

/*
  // *** TIMER0 initialization ***
  cli();                              // turn off all interrupts
  TIMSK0 = 0;                         // turn off timer0 for lower jitter
  OCR0A  = 0xBB;                      // arbitrary interrupt count
  TIMSK0 |= _BV( OCIE0A );            // piggy back onto interrupt
  sei();                              // turn interrupts back on
*/

  attachInterrupt(digitalPinToInterrupt(limitUpPin), handleSensor, CHANGE);
  attachInterrupt(digitalPinToInterrupt(limitDownPin), handleSensor, CHANGE);

  // initialize the ethernet shield
  if (USE_ETHERNET) Ethernet.init(ethernetPin);
  // start the Ethernet connection via DHCP:
  if (USE_ETHERNET && Ethernet.begin(mac) == 0) {
    if (USE_SERIAL) Serial.println("Failed to configure Ethernet using DHCP");
  }

    
  if (USE_ETHERNET && Ethernet.hardwareStatus() == EthernetNoHardware) {
    if (USE_SERIAL) Serial.println("Ethernet shield was not found!");
    //while (true) {
    //  delay(1); // do nothing, no point running without Ethernet hardware
    //}
  }
  if (USE_ETHERNET && Ethernet.linkStatus() == LinkOFF) {
    if (USE_SERIAL) Serial.println("Ethernet cable is not connected!");
  }

  // start the server
  if (USE_ETHERNET) server.begin();
  if (USE_SERIAL) Serial.print("IP: ");
  if (USE_SERIAL) Serial.println(Ethernet.localIP());

  // start mDNS
  mdns.begin(Ethernet.localIP(), "doggie-door");
  if (USE_SERIAL) Serial.println("or doggie-door.local");
  // Currently the mDNS library addServiceRecord function is buggy and causes reliability issues
  /*mdns.addServiceRecord("Doggie Door Control._http",
                        80,
                        MDNSServiceTCP);*/
}

void stopServo() {
  myservo.write(stop);
  if (DETACH_SERVO_WHEN_DONE) myservo.detach();
}

void handleSensor(){
  // the limit switches are pulled up but attached as Normally Closed and shorted to ground,
  // which means true is unpressed and false is pressed. this allows "unplugged" to read as
  // "pressed" aka fail-secure.

  delay(SERVO_DEBOUNCE_TIME_MS); // debounce a bit and let the flag solidly contact the switch (there's a lot of bounce going up)

  // only update state and continue if there's a change
  bool changed = false;
  if ((bool)digitalRead(limitUpPin) != isMaxUp) {
    isMaxUp = !isMaxUp;
    changed = true;
  }
  if ((bool)digitalRead(limitDownPin) != isMaxDown) {
    isMaxDown = !isMaxDown;
    changed = true;
  }

  if (changed) {
    if (isMaxUp && myservo.read() == upward) {
      stopServo();
    }
    if (isMaxDown && myservo.read() == downward) {
      stopServo();
    }
    // String out = "interrupt: maxup: ";
    // out = out + (isMaxUp ? "y" : "n")+" maxdown: "+(isMaxDown ? "y" : "n");
    // if (USE_SERIAL) Serial.println(out);
  }
}

void handleInput(char input, EthernetClient client){
  // must be d,u,s,q
  if (input == 'd') {
    if (isMaxDown) {
      if (USE_SERIAL) Serial.println("can't go more down!");
      if (USE_ETHERNET) client.print("can't go more down");
      return;
    }
    if (USE_SERIAL) Serial.println("down...");
    if (USE_ETHERNET) client.print("going down");
    myservo.attach(servoPin);
    myservo.write(downward);
    t.setTimeout([]() { // asynctimer
      if (myservo.read() == downward) {
        stopServo();
        if (USE_SERIAL) Serial.println("safety stop.");
      }
    }, SAFETY_STOP_TIME_MS);
  } else if (input == 'u') {
    if (isMaxUp) {
      if (USE_SERIAL) Serial.println("can't go more up!");
      if (USE_ETHERNET) client.print("can't go more up");
      return;
    }
    if (USE_SERIAL) Serial.println("up...");
    if (USE_ETHERNET) client.print("going up");
    myservo.attach(servoPin);
    myservo.write(upward);
    t.setTimeout([]() { // asynctimer
      if (myservo.read() == upward) {
        stopServo();
        if (USE_SERIAL) Serial.println("safety stop.");
      }
    }, SAFETY_STOP_TIME_MS);
  } else if (input == 's') {
    stopServo();
    if (USE_SERIAL) Serial.println("stop.");
    if (USE_ETHERNET) client.print("stopped.");
  } else if (input == 'q') {
    if (USE_SERIAL) Serial.println("status via ethernet only");
    if (USE_ETHERNET) {
      client.print("{\"maxup\":");
      client.print(isMaxUp ? "1" : "0");
      client.print(",\"maxdown\":");
      client.print(isMaxDown ? "1" : "0");
      client.print("}");
    }
    // if (USE_SERIAL) Serial.println(out);
    // if (USE_ETHERNET) client.print(out);
  } else {
    if (USE_SERIAL) Serial.println("inv:");
    if (USE_SERIAL) Serial.println(input);
    if (USE_ETHERNET) client.print("inv: ");
    if (USE_ETHERNET) client.print(input);
  }
}

// the loop routine runs over and over again forever:
void loop() {

  t.handle(); //asynctimer
  mdns.run(); // handle mDNS requests/responses

  // ideally don't use serial during normal operation so it doesn't hang
  if (!USE_ETHERNET && USE_SERIAL) {
    while (Serial.available() > 0) {
      String input = Serial.readStringUntil('\n');
      handleInput(input[0], NULL);
    }
  }

  // listen for incoming clients
  else if (USE_ETHERNET) {
    EthernetClient client = server.available();
    if (client) {
      if (USE_SERIAL) Serial.println("new client");
      // an HTTP request ends with a blank line
      bool currentLineIsBlank = true;
      String clientBuf = "";

      while (client.connected()) {
        if (client.available()) {
          char c = client.read();
          if (USE_SERIAL) Serial.write(c);

          //read char by char HTTP request up to 300 chars
          if (clientBuf.length() < 300) {
            //store characters to string
            clientBuf += c;
          }

          // if you've gotten to the end of the line (received a newline
          // character) and the line is blank, the HTTP request has ended,
          // so you can send a reply
          if (c == '\n' && currentLineIsBlank) {
            long len = clientBuf.length();
            long idx = clientBuf.indexOf("?");
            long idxj = clientBuf.indexOf("json");
            const char* cc = "Connection: close";
            const char* cth = "Content-Type: text/html";
            if (len > 0 && idx > 0) {
              char cmd = clientBuf[idx+1];
              // must be d,u,s,q
              if (cmd == 'd' || cmd == 'u' || cmd == 's' || cmd == 'q') {
                client.println("HTTP/1.1 200 OK");
              } else {
                client.println("HTTP/1.1 400 Bad Request");
              }

              if (idxj > 0 || len > 20) { // junk 123 logic to always go here despite compiler
                client.println("Content-Type: application/json");
                client.println(cc);
                client.println();
                handleInput(cmd, client);
              } else {
                client.println(cth);
                client.println(cc);
                client.println();
                // client.print("<pre>");
                handleInput(cmd, client);
                // client.print("</pre><br><a href=/>Back</a>");
                // client.print(clientBuf);
                // client.print("-->");
              }
            } else {
              const char* its = "<input type=submit name=";
              // send a standard HTTP response header
              client.println("HTTP/1.1 200 OK");
              client.println(cth);
              client.println(cc);  // the connection will be closed after completion of the response
              client.println("Refresh: 5");  // refresh the page automatically every 5 sec
              client.println();
              client.println("<!DOCTYPE HTML>");
              client.println("<html>");
              handleInput('q', client);
              client.print("<br><form action=/ method=GET>");
              client.print(its);
              client.print("u value=Up />");
              client.print(its);
              client.print("d value=Down />");
              client.print(its);
              client.print("s value=Stop /></form></html>");
            }
            break;
          }
          if (c == '\n') {
            // you're starting a new line
            currentLineIsBlank = true;
          } else if (c != '\r') {
            // you've gotten a character on the current line
            currentLineIsBlank = false;
          }
        }
      }
      // give the web browser time to receive the data
      delay(1);
      // close the connection:
      client.stop();
      if (USE_SERIAL) Serial.println("client disconnected");
    }
  }

}
