text: /**
   * @file       TinyGsmModem.tpp
   * @author     Volodymyr Shymanskyy
   * @license    LGPL-3.0
   * @copyright  Copyright (c) 2016 Volodymyr Shymanskyy
   * @date       Nov 2016
   */
  
  #ifndef SRC_TINYGSMMODEM_H_
  #define SRC_TINYGSMMODEM_H_
  
  #include "TinyGsmCommon.h"
  
  template <class modemType>
  class TinyGsmModem {
   public:
    /*
     * Basic functions
     */
    bool begin(const char* pin = NULL) {
      return thisModem().initImpl(pin);
    }
    bool init(const char* pin = NULL) {
      return thisModem().initImpl(pin);
    }
    template <typename... Args>
    inline void sendAT(Args... cmd) {
      thisModem().streamWrite("AT", cmd..., thisModem().gsmNL);
      thisModem().stream.flush();
      TINY_GSM_YIELD(); /* DBG("### AT:", cmd...); */
    }
    void setBaud(uint32_t baud) {
      thisModem().setBaudImpl(baud);
    }
    // Test response to AT commands
    bool testAT(uint32_t timeout_ms = 10000L) {
      return thisModem().testATImpl(timeout_ms);
    }
  
    // Asks for modem information via the V.25TER standard ATI command
    // NOTE:  The actual value and style of the response is quite varied
    String getModemInfo() {
      return thisModem().getModemInfoImpl();
    }
    // Gets the modem name (as it calls itself)
    String getModemName() {
      return thisModem().getModemNameImpl();
    }
    bool factoryDefault() {
      return thisModem().factoryDefaultImpl();
    }
  
    /*
     * Power functions
     */
    bool restart() {
      return thisModem().restartImpl();
    }
    bool poweroff() {
      return thisModem().powerOffImpl();
    }
    bool radioOff() {
      return thisModem().radioOffImpl();
    }
    bool sleepEnable(bool enable = true) {
      return thisModem().sleepEnableImpl(enable);
    }
    bool setPhoneFunctionality(uint8_t fun, bool reset = false) {
      return thisModem().setPhoneFunctionalityImpl(fun, reset);
    }
  
    /*
     * Generic network functions
     */
    // RegStatus getRegistrationStatus() {}
    bool isNetworkConnected() {
      return thisModem().isNetworkConnectedImpl();
    }
    // Waits for network attachment
    bool waitForNetwork(uint32_t timeout_ms = 60000L) {
      return thisModem().waitForNetworkImpl(timeout_ms);
    }
    // Gets signal quality report
    int16_t getSignalQuality() {
      return thisModem().getSignalQualityImpl();
    }
    String getLocalIP() {
      return thisModem().getLocalIPImpl();
    }
    IPAddress localIP() {
      return thisModem().TinyGsmIpFromString(thisModem().getLocalIP());
    }
  
    /*
     * CRTP Helper
     */
   protected:
    inline const modemType& thisModem() const {
      return static_cast<const modemType&>(*this);
    }
    inline modemType& thisModem() {
      return static_cast<modemType&>(*this);
    }
  
    /*
     * Basic functions
     */
   protected:
    void setBaudImpl(uint32_t baud) {
      thisModem().sendAT(GF("+IPR="), baud);
      thisModem().waitResponse();
    }
  
    bool testATImpl(uint32_t timeout_ms = 10000L) {
      for (uint32_t start = millis(); millis() - start < timeout_ms;) {
        thisModem().sendAT(GF(""));
        if (thisModem().waitResponse(200) == 1) { return true; }
        delay(100);
      }
      return false;
    }
  
    String getModemInfoImpl() {
      thisModem().sendAT(GF("I"));
      String res;
      if (thisModem().waitResponse(1000L, res) != 1) { return ""; }
      // Do the replaces twice so we cover both \r and \r\n type endings
      res.replace("\r\nOK\r\n", "");
      res.replace("\rOK\r", "");
      res.replace("\r\n", " ");
      res.replace("\r", " ");
      res.trim();
      return res;
    }
  
    String getModemNameImpl() {
      thisModem().sendAT(GF("+CGMI"));
      String res1;
      if (thisModem().waitResponse(1000L, res1) != 1) { return "unknown"; }
      res1.replace("\r\nOK\r\n", "");
      res1.replace("\rOK\r", "");
      res1.trim();
  
      thisModem().sendAT(GF("+GMM"));
      String res2;
      if (thisModem().waitResponse(1000L, res2) != 1) { return "unknown"; }
      res2.replace("\r\nOK\r\n", "");
      res2.replace("\rOK\r", "");
      res2.trim();
  
      String name = res1 + String(' ') + res2;
      DBG("### Modem:", name);
      return name;
    }
  
    bool factoryDefaultImpl() {
      thisModem().sendAT(GF("&FZE0&W"));  // Factory + Reset + Echo Off + Write
      thisModem().waitResponse();
      thisModem().sendAT(GF("+IPR=0"));  // Auto-baud
      thisModem().waitResponse();
      thisModem().sendAT(GF("&W"));  // Write configuration
      return thisModem().waitResponse() == 1;
    }
  
    /*
     * Power functions
     */
   protected:
    bool radioOffImpl() {
      if (!thisModem().setPhoneFunctionality(0)) { return false; }
      delay(3000);
      return true;
    }
  
    bool sleepEnableImpl(bool enable = true) TINY_GSM_ATTR_NOT_IMPLEMENTED;
  
    bool setPhoneFunctionalityImpl(uint8_t fun, bool reset = false)
        TINY_GSM_ATTR_NOT_IMPLEMENTED;
  
    /*
     * Generic network functions
     */
   protected:
    // Gets the modem's registration status via CREG/CGREG/CEREG
    // CREG = Generic network registration
    // CGREG = GPRS service registration
    // CEREG = EPS registration for LTE modules
    int8_t getRegistrationStatusXREG(const char* regCommand) {
      thisModem().sendAT('+', regCommand, '?');
      // check for any of the three for simplicity
      int8_t resp = thisModem().waitResponse(GF("+CREG:"), GF("+CGREG:"),
                                             GF("+CEREG:"));
      if (resp != 1 && resp != 2 && resp != 3) { return -1; }
      thisModem().streamSkipUntil(','); /* Skip format (0) */
      int status = thisModem().streamGetIntBefore('\n');
      thisModem().waitResponse();
      return status;
    }
  
    bool waitForNetworkImpl(uint32_t timeout_ms = 60000L) {
      for (uint32_t start = millis(); millis() - start < timeout_ms;) {
        if (thisModem().isNetworkConnected()) { return true; }
        delay(250);
      }
      return false;
    }
  
    // Gets signal quality report according to 3GPP TS command AT+CSQ
    int8_t getSignalQualityImpl() {
      thisModem().sendAT(GF("+CSQ"));
      if (thisModem().waitResponse(GF("+CSQ:")) != 1) { return 99; }
      int8_t res = thisModem().streamGetIntBefore(',');
      thisModem().waitResponse();
      return res;
    }
  
    String getLocalIPImpl() {
      thisModem().sendAT(GF("+CGPADDR=1"));
      if (thisModem().waitResponse(GF("+CGPADDR:")) != 1) { return ""; }
      thisModem().streamSkipUntil(',');  // Skip context id
      String res = thisModem().stream.readStringUntil('\r');
      if (thisModem().waitResponse() != 1) { return ""; }
      return res;
    }
  
    static inline IPAddress TinyGsmIpFromString(const String& strIP) {
      int Parts[4] = {
          0,
      };
      int Part = 0;
      for (uint8_t i = 0; i < strIP.length(); i++) {
        char c = strIP[i];
        if (c == '.') {
          Part++;
          if (Part > 3) { return IPAddress(0, 0, 0, 0); }
          continue;
        } else if (c >= '0' && c <= '9') {
          Parts[Part] *= 10;
          Parts[Part] += c - '0';
        } else {
          if (Part == 3) break;
        }
      }
      return IPAddress(Parts[0], Parts[1], Parts[2], Parts[3]);
    }
  
    /*
     Utilities
     */
   public:
    // Utility templates for writing/skipping characters on a stream
    template <typename T>
    inline void streamWrite(T last) {
      thisModem().stream.print(last);
    }
  
    template <typename T, typename... Args>
    inline void streamWrite(T head, Args... tail) {
      thisModem().stream.print(head);
      thisModem().streamWrite(tail...);
    }
  
    inline void streamClear() {
      while (thisModem().stream.available()) {
        thisModem().waitResponse(50, NULL, NULL);
      }
    }
  
   protected:
    inline bool streamGetLength(char* buf, int8_t numChars,
                                const uint32_t timeout_ms = 1000L) {
      if (!buf) { return false; }
  
      int8_t   numCharsReady = -1;
      uint32_t startMillis   = millis();
      while (millis() - startMillis < timeout_ms &&
             (numCharsReady = thisModem().stream.available()) < numChars) {
        TINY_GSM_YIELD();
      }
  
      if (numCharsReady >= numChars) {
        thisModem().stream.readBytes(buf, numChars);
        return true;
      }
  
      return false;
    }
  
    inline int16_t streamGetIntLength(int8_t         numChars,
                                      const uint32_t timeout_ms = 1000L) {
      char buf[numChars + 1];
      if (streamGetLength(buf, numChars, timeout_ms)) {
        buf[numChars] = '\0';
        return atoi(buf);
      }
  
      return -9999;
    }
  
    inline int16_t streamGetIntBefore(char lastChar) {
      char   buf[7];
      size_t bytesRead = thisModem().stream.readBytesUntil(
          lastChar, buf, static_cast<size_t>(7));
      // if we read 7 or more bytes, it's an overflow
      if (bytesRead && bytesRead < 7) {
        buf[bytesRead] = '\0';
        int16_t res    = atoi(buf);
        return res;
      }
  
      return -9999;
    }
  
    inline float streamGetFloatLength(int8_t         numChars,
                                      const uint32_t timeout_ms = 1000L) {
      char buf[numChars + 1];
      if (streamGetLength(buf, numChars, timeout_ms)) {
        buf[numChars] = '\0';
        return atof(buf);
      }
  
      return -9999.0F;
    }
  
    inline float streamGetFloatBefore(char lastChar) {
      char   buf[16];
      size_t bytesRead = thisModem().stream.readBytesUntil(
          lastChar, buf, static_cast<size_t>(16));
      // if we read 16 or more bytes, it's an overflow
      if (bytesRead && bytesRead < 16) {
        buf[bytesRead] = '\0';
        float res      = atof(buf);
        return res;
      }
  
      return -9999.0F;
    }
  
    inline bool streamSkipUntil(const char c, const uint32_t timeout_ms = 1000L) {
      uint32_t startMillis = millis();
      while (millis() - startMillis < timeout_ms) {
        while (millis() - startMillis < timeout_ms &&
               !thisModem().stream.available()) {
          TINY_GSM_YIELD();
        }
        if (thisModem().stream.read() == c) { return true; }
      }
      return false;
    }
  };
  
  #endif  // SRC_TINYGSMMODEM_H_
  