diff --git a/src/TinyGsmClient.h b/src/TinyGsmClient.h index a8ae763e..db117429 100644 --- a/src/TinyGsmClient.h +++ b/src/TinyGsmClient.h @@ -56,6 +56,11 @@ typedef TinyGsmSim5360::GsmClientSim5360 TinyGsmClient; typedef TinyGsmSim7600 TinyGsm; typedef TinyGsmSim7600::GsmClientSim7600 TinyGsmClient; +#elif defined(TINY_GSM_MODEM_EC20) || defined(TINY_GSM_MODEM_EC25) +#include "TinyGsmClientEC20.h" +typedef TinyGsmEC20 TinyGsm; +typedef TinyGsmEC20::GsmClientEC20 TinyGsmClient; + #elif defined(TINY_GSM_MODEM_UBLOX) #include "TinyGsmClientUBLOX.h" typedef TinyGsmUBLOX TinyGsm; diff --git a/src/TinyGsmClientEC20.h b/src/TinyGsmClientEC20.h new file mode 100644 index 00000000..7a1c8a75 --- /dev/null +++ b/src/TinyGsmClientEC20.h @@ -0,0 +1,823 @@ +/** + * @file TinyGsmClientEC20.h + * @author Volodymyr Shymanskyy, Manoj Sahukar + * @license LGPL-3.0 + * @copyright Copyright (c) 2016 Volodymyr Shymanskyy, Manoj Sahukar + * @date Jan 2023 + */ + +#ifndef SRC_TinyGsmClientEC20_H_ +#define SRC_TinyGsmClientEC20_H_ +// #pragma message("TinyGSM: TinyGsmClientEC20") + +// #define TINY_GSM_DEBUG Serial + +#define TINY_GSM_MUX_COUNT 12 +#define TINY_GSM_BUFFER_READ_AND_CHECK_SIZE + +#include "TinyGsmBattery.tpp" +#include "TinyGsmCalling.tpp" +#include "TinyGsmGPRS.tpp" +#include "TinyGsmGPS.tpp" +#include "TinyGsmModem.tpp" +#include "TinyGsmSMS.tpp" +#include "TinyGsmTCP.tpp" +#include "TinyGsmTemperature.tpp" +#include "TinyGsmTime.tpp" +#include "TinyGsmNTP.tpp" + +#define GSM_NL "\r\n" +static const char GSM_OK[] TINY_GSM_PROGMEM = "OK" GSM_NL; +static const char GSM_ERROR[] TINY_GSM_PROGMEM = "ERROR" GSM_NL; +#if defined TINY_GSM_DEBUG +static const char GSM_CME_ERROR[] TINY_GSM_PROGMEM = GSM_NL "+CME ERROR:"; +static const char GSM_CMS_ERROR[] TINY_GSM_PROGMEM = GSM_NL "+CMS ERROR:"; +#endif + +enum RegStatus { + REG_NO_RESULT = -1, + REG_UNREGISTERED = 0, + REG_SEARCHING = 2, + REG_DENIED = 3, + REG_OK_HOME = 1, + REG_OK_ROAMING = 5, + REG_UNKNOWN = 4, +}; + +class TinyGsmEC20 : public TinyGsmModem, + public TinyGsmGPRS, + public TinyGsmTCP, + public TinyGsmCalling, + public TinyGsmSMS, + public TinyGsmTime, + public TinyGsmNTP, + public TinyGsmGPS, + public TinyGsmBattery, + public TinyGsmTemperature { + friend class TinyGsmModem; + friend class TinyGsmGPRS; + friend class TinyGsmTCP; + friend class TinyGsmCalling; + friend class TinyGsmSMS; + friend class TinyGsmTime; + friend class TinyGsmNTP; + friend class TinyGsmGPS; + friend class TinyGsmBattery; + friend class TinyGsmTemperature; + + /* + * Inner Client + */ + public: + class GsmClientEC20 : public GsmClient { + friend class TinyGsmEC20; + + public: + GsmClientEC20() {} + + explicit GsmClientEC20(TinyGsmEC20& modem, uint8_t mux = 0) { + init(&modem, mux); + } + + bool init(TinyGsmEC20* modem, uint8_t mux = 0) { + this->at = modem; + sock_available = 0; + prev_check = 0; + sock_connected = false; + got_data = false; + + if (mux < TINY_GSM_MUX_COUNT) { + this->mux = mux; + } else { + this->mux = (mux % TINY_GSM_MUX_COUNT); + } + at->sockets[this->mux] = this; + + return true; + } + + public: + virtual int connect(const char* host, uint16_t port, int timeout_s) { + stop(); + TINY_GSM_YIELD(); + rx.clear(); + sock_connected = at->modemConnect(host, port, mux, false, timeout_s); + return sock_connected; + } + TINY_GSM_CLIENT_CONNECT_OVERRIDES + + void stop(uint32_t maxWaitMs) { + uint32_t startMillis = millis(); + dumpModemBuffer(maxWaitMs); + at->sendAT(GF("+QICLOSE="), mux); + sock_connected = false; + at->waitResponse((maxWaitMs - (millis() - startMillis))); + } + void stop() override { + stop(15000L); + } + + /* + * Extended API + */ + + String remoteIP() TINY_GSM_ATTR_NOT_IMPLEMENTED; + }; + + /* + * Inner Secure Client + */ + + /* + class GsmClientSecureBG96 : public GsmClientEC20 + { + public: + GsmClientSecure() {} + + GsmClientSecure(TinyGsmEC20& modem, uint8_t mux = 0) + : public GsmClient(modem, mux) + {} + + + public: + int connect(const char* host, uint16_t port, int timeout_s) override { + stop(); + TINY_GSM_YIELD(); + rx.clear(); + sock_connected = at->modemConnect(host, port, mux, true, timeout_s); + return sock_connected; + } + TINY_GSM_CLIENT_CONNECT_OVERRIDES + }; + */ + + /* + * Constructor + */ + public: + explicit TinyGsmEC20(Stream& stream) : stream(stream) { + memset(sockets, 0, sizeof(sockets)); + } + + /* + * Basic functions + */ + protected: + bool initImpl(const char* pin = NULL) { + DBG(GF("### TinyGSM Version:"), TINYGSM_VERSION); + DBG(GF("### TinyGSM Compiled Module: TinyGsmClientEC20")); + + if (!testAT()) { return false; } + + sendAT(GF("E0")); // Echo Off + if (waitResponse() != 1) { return false; } + +#ifdef TINY_GSM_DEBUG + sendAT(GF("+CMEE=2")); // turn on verbose error codes +#else + sendAT(GF("+CMEE=0")); // turn off error codes +#endif + waitResponse(); + + DBG(GF("### Modem:"), getModemName()); + + // Disable time and time zone URC's + sendAT(GF("+CTZR=0")); + if (waitResponse(10000L) != 1) { return false; } + + // Enable automatic time zone update + sendAT(GF("+CTZU=1")); + if (waitResponse(10000L) != 1) { return false; } + + SimStatus ret = getSimStatus(); + // if the sim isn't ready and a pin has been provided, try to unlock the sim + if (ret != SIM_READY && pin != NULL && strlen(pin) > 0) { + simUnlock(pin); + return (getSimStatus() == SIM_READY); + } else { + // if the sim is ready, or it's locked but no pin has been provided, + // return true + return (ret == SIM_READY || ret == SIM_LOCKED); + } + } + + String getModemNameImpl() { + String name = "EC20F"; + + sendAT(GF("+CGMM")); + String res2; + if (waitResponse(1000L, res2) != 1) { return name; } + res2.replace(GSM_NL "OK" GSM_NL, ""); + res2.replace("_", " "); + res2.trim(); + + name = res2; + DBG("### Modem:", name); + return name; + } + + bool factoryDefaultImpl() { // these commands aren't supported + return false; + } + + /* + * Power functions + */ + protected: + bool restartImpl(const char* pin = NULL) { + if (!testAT()) { return false; } + if (!setPhoneFunctionality(1, true)) { return false; } + waitResponse(10000L, GF("APP RDY")); + return init(pin); + } + + bool powerOffImpl() { + sendAT(GF("+QPOWD=1")); + waitResponse(300); // returns OK first + return waitResponse(300, GF("POWERED DOWN")) == 1; + } + + bool radioOffImpl() { + if (!setPhoneFunctionality(4)) { return false; } + delay(3000); + return true; + } + // When entering into sleep mode is enabled, DTR is pulled up, and WAKEUP_IN + // is pulled up, the module can directly enter into sleep mode.If entering + // into sleep mode is enabled, DTR is pulled down, and WAKEUP_IN is pulled + // down, there is a need to pull the DTR pin and the WAKEUP_IN pin up first, + // and then the module can enter into sleep mode. + bool sleepEnableImpl(bool enable = true) { + sendAT(GF("+QSCLK="), enable); + return waitResponse() == 1; + } + + bool setPhoneFunctionalityImpl(uint8_t fun, bool reset = false) { + sendAT(GF("+CFUN="), fun, reset ? ",1" : ""); + return waitResponse(10000L, GF("OK")) == 1; + } + + /* + * Generic network functions + */ + public: + RegStatus getRegistrationStatus() { + // Check first for EPS registration + RegStatus epsStatus = (RegStatus)getRegistrationStatusXREG("CEREG"); + + // If we're connected on EPS, great! + if (epsStatus == REG_OK_HOME || epsStatus == REG_OK_ROAMING) { + return epsStatus; + } else { + // Otherwise, check generic network status + return (RegStatus)getRegistrationStatusXREG("CREG"); + } + } + + protected: + bool isNetworkConnectedImpl() { + RegStatus s = getRegistrationStatus(); + return (s == REG_OK_HOME || s == REG_OK_ROAMING); + } + + public: + String getNetworkModes() { + sendAT(GF("+QCFG=?")); + if (waitResponse(GF(GSM_NL "+QCFG:")) != 1) { return ""; } + String res = stream.readStringUntil('\n'); + waitResponse(); + return res; + } + + int16_t getNetworkMode() { + sendAT(GF("+QCFG=\"nwscanmode\"")); + if (waitResponse(GF(GSM_NL "+QCFG:")) != 1) { return false; } + int16_t mode = streamGetIntBefore('\n'); + waitResponse(); + return mode; + } + + bool setNetworkMode(uint8_t mode) { + sendAT(GF("+QCFG=\"nwscanmode\","), mode); + return waitResponse() == 1; + } + + String getLocalIPImpl() { + // sendAT(GF("+IPADDR")); // Inquire Socket PDP address + sendAT(GF("+CGPADDR=1")); // Show PDP address + String res; + if (waitResponse(10000L, res) != 1) { return ""; } + res.replace("+CGPADDR: 1,", ""); + res.replace(GSM_NL "OK" GSM_NL, ""); + res.replace(GSM_NL, ""); + res.trim(); + return res; + } + + /* + * GPRS functions + */ + protected: + bool gprsConnectImpl(const char* apn, const char* user = NULL, + const char* pwd = NULL) { + gprsDisconnect(); + + // Configure the TCPIP Context + sendAT(GF("+QICSGP=1,1,\""), apn, GF("\",\""), user, GF("\",\""), pwd, + GF("\""), GF(","), 1); + if (waitResponse() != 1) { return false; } + // Activate GPRS/CSD Context + sendAT(GF("+QIACT=1")); + if (waitResponse(150000L) != 1) { return false; } + + // Attach to Packet Domain service - is this necessary? + sendAT(GF("+CGATT=1")); + if (waitResponse(60000L) != 1) { return false; } + + return true; + } + + bool gprsDisconnectImpl() { + sendAT(GF("+QIDEACT=1")); // Deactivate the bearer context + if (waitResponse(40000L) != 1) { return false; } + return true; + } + + bool isGprsConnectedImpl() { + sendAT(GF("+CGATT?")); + // May return +NETOPEN: 1, 0. We just confirm that the first number is 1 + if (waitResponse(GF(GSM_NL "+CGATT: 1")) != 1) { return false; } + waitResponse(); + + sendAT(GF("+CGPADDR=1")); // Inquire Socket PDP address + // sendAT(GF("+CGPADDR=1")); // Show PDP address + if (waitResponse() != 1) { return false; } + + return true; + } + + /* + * SIM card functions + */ + protected: + String getSimCCIDImpl() { + sendAT(GF("+QCCID")); + if (waitResponse(GF(GSM_NL "+QCCID:")) != 1) { return ""; } + String res = stream.readStringUntil('\n'); + waitResponse(); + res.trim(); + return res; + } + + /* + * Phone Call functions + */ + protected: + bool callHangupImpl() { + sendAT(GF("+CHUP")); + return waitResponse() == 1; + } + + /* + * Messaging functions + */ + protected: + // Follows all messaging functions per template + + /* + * GSM Location functions + */ + protected: + // NOTE: As of application firmware version 01.016.01.016 triangulated + // locations can be obtained via the QuecLocator service and accompanying AT + // commands. As this is a separate paid service which I do not have access + // to, I am not implementing it here. + + /* + * GPS/GNSS/GLONASS location functions + */ + protected: + // enable GPS + bool enableGPSImpl() { + sendAT(GF("+QGPS=1")); + if (waitResponse() != 1) { return false; } + return true; + } + + bool disableGPSImpl() { + sendAT(GF("+QGPSEND")); + if (waitResponse() != 1) { return false; } + return true; + } + + // get the RAW GPS output + String getGPSrawImpl() { + sendAT(GF("+QGPSLOC=2")); + if (waitResponse(10000L, GF(GSM_NL "+QGPSLOC:")) != 1) { return ""; } + String res = stream.readStringUntil('\n'); + waitResponse(); + res.trim(); + return res; + } + + // get GPS informations + bool getGPSImpl(float* lat, float* lon, float* speed = 0, float* alt = 0, + int* vsat = 0, int* usat = 0, float* accuracy = 0, + int* year = 0, int* month = 0, int* day = 0, int* hour = 0, + int* minute = 0, int* second = 0) { + sendAT(GF("+QGPSLOC=2")); + if (waitResponse(10000L, GF(GSM_NL "+QGPSLOC:")) != 1) { + // NOTE: Will return an error if the position isn't fixed + return false; + } + + // init variables + float ilat = 0; + float ilon = 0; + float ispeed = 0; + float ialt = 0; + int iusat = 0; + float iaccuracy = 0; + int iyear = 0; + int imonth = 0; + int iday = 0; + int ihour = 0; + int imin = 0; + float secondWithSS = 0; + + // UTC date & Time + ihour = streamGetIntLength(2); // Two digit hour + imin = streamGetIntLength(2); // Two digit minute + secondWithSS = streamGetFloatBefore(','); // 6 digit second with subseconds + + ilat = streamGetFloatBefore(','); // Latitude + ilon = streamGetFloatBefore(','); // Longitude + iaccuracy = streamGetFloatBefore(','); // Horizontal precision + ialt = streamGetFloatBefore(','); // Altitude from sea level + streamSkipUntil(','); // GNSS positioning mode + streamSkipUntil(','); // Course Over Ground based on true north + streamSkipUntil(','); // Speed Over Ground in Km/h + ispeed = streamGetFloatBefore(','); // Speed Over Ground in knots + + iday = streamGetIntLength(2); // Two digit day + imonth = streamGetIntLength(2); // Two digit month + iyear = streamGetIntBefore(','); // Two digit year + + iusat = streamGetIntBefore(','); // Number of satellites, + streamSkipUntil('\n'); // The error code of the operation. If it is not + // 0, it is the type of error. + + // Set pointers + if (lat != NULL) *lat = ilat; + if (lon != NULL) *lon = ilon; + if (speed != NULL) *speed = ispeed; + if (alt != NULL) *alt = ialt; + if (vsat != NULL) *vsat = 0; + if (usat != NULL) *usat = iusat; + if (accuracy != NULL) *accuracy = iaccuracy; + if (iyear < 2000) iyear += 2000; + if (year != NULL) *year = iyear; + if (month != NULL) *month = imonth; + if (day != NULL) *day = iday; + if (hour != NULL) *hour = ihour; + if (minute != NULL) *minute = imin; + if (second != NULL) *second = static_cast(secondWithSS); + + waitResponse(); // Final OK + return true; + } + + /* + * Time functions + */ + protected: + String getGSMDateTimeImpl(TinyGSMDateTimeFormat format) { + sendAT(GF("+QLTS=2")); + if (waitResponse(2000L, GF("+QLTS: \"")) != 1) { return ""; } + + String res; + + switch (format) { + case DATE_FULL: res = stream.readStringUntil('"'); break; + case DATE_TIME: + streamSkipUntil(','); + res = stream.readStringUntil('"'); + break; + case DATE_DATE: res = stream.readStringUntil(','); break; + } + waitResponse(); // Ends with OK + return res; + } + + // The BG96 returns UTC time instead of local time as other modules do in + // response to CCLK, so we're using QLTS where we can specifically request + // local time. + bool getNetworkTimeImpl(int* year, int* month, int* day, int* hour, + int* minute, int* second, float* timezone) { + sendAT(GF("+QLTS=2")); + if (waitResponse(2000L, GF("+QLTS: \"")) != 1) { return false; } + + int iyear = 0; + int imonth = 0; + int iday = 0; + int ihour = 0; + int imin = 0; + int isec = 0; + int itimezone = 0; + + // Date & Time + iyear = streamGetIntBefore('/'); + imonth = streamGetIntBefore('/'); + iday = streamGetIntBefore(','); + ihour = streamGetIntBefore(':'); + imin = streamGetIntBefore(':'); + isec = streamGetIntLength(2); + char tzSign = stream.read(); + itimezone = streamGetIntBefore(','); + if (tzSign == '-') { itimezone = itimezone * -1; } + streamSkipUntil('\n'); // DST flag + + // Set pointers + if (iyear < 2000) iyear += 2000; + if (year != NULL) *year = iyear; + if (month != NULL) *month = imonth; + if (day != NULL) *day = iday; + if (hour != NULL) *hour = ihour; + if (minute != NULL) *minute = imin; + if (second != NULL) *second = isec; + if (timezone != NULL) *timezone = static_cast(itimezone) / 4.0; + + // Final OK + waitResponse(); // Ends with OK + return true; + } + + /* + * NTP server functions + */ + + byte NTPServerSyncImpl(String server = "pool.ntp.org", byte = -5) { + // Request network synchronization + // AT+QNTP=,[,][,] + sendAT(GF("+QNTP=1,\""), server, '"'); + if (waitResponse(10000L, GF("+QNTP:"))) { + String result = stream.readStringUntil(','); + streamSkipUntil('\n'); + result.trim(); + if (TinyGsmIsValidNumber(result)) { return result.toInt(); } + } else { + return -1; + } + return -1; + } + + String ShowNTPErrorImpl(byte error) TINY_GSM_ATTR_NOT_IMPLEMENTED; + + /* + * Battery functions + */ + protected: + // returns volts, multiply by 1000 to get mV + uint16_t getBattVoltageImpl() { + sendAT(GF("+CBC")); + if (waitResponse(GF(GSM_NL "+CBC:")) != 1) { return 0; } + + // get voltage in VOLTS + float voltage = streamGetFloatBefore('\n'); + // Wait for final OK + waitResponse(); + // Return millivolts + uint16_t res = voltage * 1000; + return res; + } + + int8_t getBattPercentImpl() TINY_GSM_ATTR_NOT_AVAILABLE; + + uint8_t getBattChargeStateImpl() TINY_GSM_ATTR_NOT_AVAILABLE; + + bool getBattStatsImpl(uint8_t& chargeState, int8_t& percent, + uint16_t& milliVolts) { + chargeState = 0; + percent = 0; + milliVolts = getBattVoltage(); + return true; + } + + /* + * Temperature functions + */ + protected: + // get temperature in degree celsius + uint16_t getTemperatureImpl() { + sendAT(GF("+QTEMP")); + if (waitResponse(GF(GSM_NL "+QTEMP:")) != 1) { return 0; } + // return temperature in C + uint16_t res = + streamGetIntBefore(','); // read PMIC (primary ic) temperature + streamSkipUntil(','); // skip XO temperature ?? + streamSkipUntil('\n'); // skip PA temperature ?? + // Wait for final OK + waitResponse(); + return res; + } + + /* + * Client related functions + */ + protected: + bool modemConnect(const char* host, uint16_t port, uint8_t mux, + bool ssl = false, int timeout_s = 150) { + if (ssl) { DBG("SSL not yet supported on this module!"); } + + uint32_t timeout_ms = ((uint32_t)timeout_s) * 1000; + + // (1-16), (0-11), + // "TCP/UDP/TCP LISTENER/UDPSERVICE", "/", + // ,,(0-2; 0=buffer) + sendAT(GF("+QIOPEN=1,"), mux, GF(",\""), GF("TCP"), GF("\",\""), host, + GF("\","), port, GF(",0,0")); + waitResponse(); + + if (waitResponse(timeout_ms, GF(GSM_NL "+QIOPEN:")) != 1) { return false; } + + if (streamGetIntBefore(',') != mux) { return false; } + // Read status + return (0 == streamGetIntBefore('\n')); + } + + int16_t modemSend(const void* buff, size_t len, uint8_t mux) { + sendAT(GF("+QISEND="), mux, ',', (uint16_t)len); + if (waitResponse(GF(">")) != 1) { return 0; } + stream.write(reinterpret_cast(buff), len); + stream.flush(); + if (waitResponse(GF(GSM_NL "SEND OK")) != 1) { return 0; } + // TODO(?): Wait for ACK? AT+QISEND=id,0 + return len; + } + + size_t modemRead(size_t size, uint8_t mux) { + if (!sockets[mux]) return 0; + sendAT(GF("+QIRD="), mux, ',', (uint16_t)size); + if (waitResponse(GF("+QIRD:")) != 1) { return 0; } + int16_t len = streamGetIntBefore('\n'); + + for (int i = 0; i < len; i++) { moveCharFromStreamToFifo(mux); } + waitResponse(); + // DBG("### READ:", len, "from", mux); + sockets[mux]->sock_available = modemGetAvailable(mux); + return len; + } + + size_t modemGetAvailable(uint8_t mux) { + if (!sockets[mux]) return 0; + sendAT(GF("+QIRD="), mux, GF(",0")); + size_t result = 0; + if (waitResponse(GF("+QIRD:")) == 1) { + streamSkipUntil(','); // Skip total received + streamSkipUntil(','); // Skip have read + result = streamGetIntBefore('\n'); + if (result) { DBG("### DATA AVAILABLE:", result, "on", mux); } + waitResponse(); + } + if (!result) { sockets[mux]->sock_connected = modemGetConnected(mux); } + return result; + } + + bool modemGetConnected(uint8_t mux) { + sendAT(GF("+QISTATE=1,"), mux); + // +QISTATE: 0,"TCP","151.139.237.11",80,5087,4,1,0,0,"uart1" + + if (waitResponse(GF("+QISTATE:")) != 1) { return false; } + + streamSkipUntil(','); // Skip mux + streamSkipUntil(','); // Skip socket type + streamSkipUntil(','); // Skip remote ip + streamSkipUntil(','); // Skip remote port + streamSkipUntil(','); // Skip local port + int8_t res = streamGetIntBefore(','); // socket state + + waitResponse(); + + // 0 Initial, 1 Opening, 2 Connected, 3 Listening, 4 Closing + return 2 == res; + } + + /* + * Utilities + */ + public: + // TODO(vshymanskyy): Optimize this! + int8_t waitResponse(uint32_t timeout_ms, String& data, + GsmConstStr r1 = GFP(GSM_OK), + GsmConstStr r2 = GFP(GSM_ERROR), +#if defined TINY_GSM_DEBUG + GsmConstStr r3 = GFP(GSM_CME_ERROR), + GsmConstStr r4 = GFP(GSM_CMS_ERROR), +#else + GsmConstStr r3 = NULL, GsmConstStr r4 = NULL, +#endif + GsmConstStr r5 = NULL) { + /*String r1s(r1); r1s.trim(); + String r2s(r2); r2s.trim(); + String r3s(r3); r3s.trim(); + String r4s(r4); r4s.trim(); + String r5s(r5); r5s.trim(); + DBG("### ..:", r1s, ",", r2s, ",", r3s, ",", r4s, ",", r5s);*/ + data.reserve(64); + uint8_t index = 0; + uint32_t startMillis = millis(); + do { + TINY_GSM_YIELD(); + while (stream.available() > 0) { + TINY_GSM_YIELD(); + int8_t a = stream.read(); + if (a <= 0) continue; // Skip 0x00 bytes, just in case + data += static_cast(a); + if (r1 && data.endsWith(r1)) { + index = 1; + goto finish; + } else if (r2 && data.endsWith(r2)) { + index = 2; + goto finish; + } else if (r3 && data.endsWith(r3)) { +#if defined TINY_GSM_DEBUG + if (r3 == GFP(GSM_CME_ERROR)) { + streamSkipUntil('\n'); // Read out the error + } +#endif + index = 3; + goto finish; + } else if (r4 && data.endsWith(r4)) { + index = 4; + goto finish; + } else if (r5 && data.endsWith(r5)) { + index = 5; + goto finish; + } else if (data.endsWith(GF(GSM_NL "+QIURC:"))) { + streamSkipUntil('\"'); + String urc = stream.readStringUntil('\"'); + streamSkipUntil(','); + if (urc == "recv") { + int8_t mux = streamGetIntBefore('\n'); + DBG("### URC RECV:", mux); + if (mux >= 0 && mux < TINY_GSM_MUX_COUNT && sockets[mux]) { + sockets[mux]->got_data = true; + } + } else if (urc == "closed") { + int8_t mux = streamGetIntBefore('\n'); + DBG("### URC CLOSE:", mux); + if (mux >= 0 && mux < TINY_GSM_MUX_COUNT && sockets[mux]) { + sockets[mux]->sock_connected = false; + } + } else { + streamSkipUntil('\n'); + } + data = ""; + } + } + } while (millis() - startMillis < timeout_ms); + finish: + if (!index) { + data.trim(); + if (data.length()) { DBG("### Unhandled:", data); } + data = ""; + } + // data.replace(GSM_NL, "/"); + // DBG('<', index, '>', data); + return index; + } + + int8_t waitResponse(uint32_t timeout_ms, GsmConstStr r1 = GFP(GSM_OK), + GsmConstStr r2 = GFP(GSM_ERROR), +#if defined TINY_GSM_DEBUG + GsmConstStr r3 = GFP(GSM_CME_ERROR), + GsmConstStr r4 = GFP(GSM_CMS_ERROR), +#else + GsmConstStr r3 = NULL, GsmConstStr r4 = NULL, +#endif + GsmConstStr r5 = NULL) { + String data; + return waitResponse(timeout_ms, data, r1, r2, r3, r4, r5); + } + + int8_t waitResponse(GsmConstStr r1 = GFP(GSM_OK), + GsmConstStr r2 = GFP(GSM_ERROR), +#if defined TINY_GSM_DEBUG + GsmConstStr r3 = GFP(GSM_CME_ERROR), + GsmConstStr r4 = GFP(GSM_CMS_ERROR), +#else + GsmConstStr r3 = NULL, GsmConstStr r4 = NULL, +#endif + GsmConstStr r5 = NULL) { + return waitResponse(1000, r1, r2, r3, r4, r5); + } + + public: + Stream& stream; + + protected: + GsmClientEC20* sockets[TINY_GSM_MUX_COUNT]; + const char* gsmNL = GSM_NL; +}; + +#endif // SRC_TinyGsmClientEC20_H_