/** * @file TinyGsmCommon.h * @author Volodymyr Shymanskyy * @license LGPL-3.0 * @copyright Copyright (c) 2016 Volodymyr Shymanskyy * @date Nov 2016 */ #ifndef SRC_TINYGSMCOMMON_H_ #define SRC_TINYGSMCOMMON_H_ // The current library version number #define TINYGSM_VERSION "0.9.19" #if defined(SPARK) || defined(PARTICLE) #include "Particle.h" #elif defined(ARDUINO) #if ARDUINO >= 100 #include "Arduino.h" #else #include "WProgram.h" #endif #endif #if defined(ARDUINO_DASH) #include #else #include #endif #include "TinyGsmFifo.h" #ifndef TINY_GSM_YIELD_MS #define TINY_GSM_YIELD_MS 0 #endif #ifndef TINY_GSM_YIELD #define TINY_GSM_YIELD() \ { delay(TINY_GSM_YIELD_MS); } #endif #if !defined(TINY_GSM_RX_BUFFER) #define TINY_GSM_RX_BUFFER 64 #endif #define TINY_GSM_ATTR_NOT_AVAILABLE \ __attribute__((error("Not available on this modem type"))) #define TINY_GSM_ATTR_NOT_IMPLEMENTED __attribute__((error("Not implemented"))) #if defined(__AVR__) #define TINY_GSM_PROGMEM PROGMEM typedef const __FlashStringHelper* GsmConstStr; #define GFP(x) (reinterpret_cast(x)) #define GF(x) F(x) #else #define TINY_GSM_PROGMEM typedef const char* GsmConstStr; #define GFP(x) x #define GF(x) x #endif #ifdef TINY_GSM_DEBUG namespace { template static void DBG_PLAIN(T last) { TINY_GSM_DEBUG.println(last); } template static void DBG_PLAIN(T head, Args... tail) { TINY_GSM_DEBUG.print(head); TINY_GSM_DEBUG.print(' '); DBG_PLAIN(tail...); } template static void DBG(Args... args) { TINY_GSM_DEBUG.print(GF("[")); TINY_GSM_DEBUG.print(millis()); TINY_GSM_DEBUG.print(GF("] ")); DBG_PLAIN(args...); } } // namespace #else #define DBG_PLAIN(...) #define DBG(...) #endif template const T& TinyGsmMin(const T& a, const T& b) { return (b < a) ? b : a; } template const T& TinyGsmMax(const T& a, const T& b) { return (b < a) ? a : b; } template uint32_t TinyGsmAutoBaud(T& SerialAT, uint32_t minimum = 9600, uint32_t maximum = 115200) { static uint32_t rates[] = {115200, 57600, 38400, 19200, 9600, 74400, 74880, 230400, 460800, 2400, 4800, 14400, 28800}; for (unsigned i = 0; i < sizeof(rates) / sizeof(rates[0]); i++) { uint32_t rate = rates[i]; if (rate < minimum || rate > maximum) continue; DBG("Trying baud rate", rate, "..."); SerialAT.begin(rate); delay(10); for (int j = 0; j < 10; j++) { SerialAT.print("AT\r\n"); String input = SerialAT.readString(); if (input.indexOf("OK") >= 0) { DBG("Modem responded at rate", rate); return rate; } } } return 0; } enum modemInternalBuffferType { NO_MODEM_BUFFER = 0, // For modules that do not store incoming data in any sort of buffer READ_NO_CHECK = 1, // Data is stored in a buffer, but we can only read from // the buffer, not check how much data is stored in it READ_AND_CHECK_SIZE = 2, // Data is stored in a buffer and we can both read // and check the size of the buffer }; enum SimStatus { SIM_ERROR = 0, SIM_READY = 1, SIM_LOCKED = 2, SIM_ANTITHEFT_LOCKED = 3, }; enum TinyGSMDateTimeFormat { DATE_FULL = 0, DATE_TIME = 1, DATE_DATE = 2 }; template 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 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(); } void maintain() { return thisModem().maintainImpl(); } bool factoryDefault() { return thisModem().factoryDefaultImpl(); } bool hasSSL() { return thisModem().thisHasSSL(); } bool hasWifi() { return thisModem().thisHasWifi(); } bool hasGPRS() { return thisModem().thisHasGPRS(); } /* * 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); } /* * SIM card functions */ // Unlocks the SIM bool simUnlock(const char* pin) { return thisModem().simUnlockImpl(pin); } // Gets the CCID of a sim card via AT+CCID String getSimCCID() { return thisModem().getSimCCIDImpl(); } // Asks for TA Serial Number Identification (IMEI) String getIMEI() { return thisModem().getIMEIImpl(); } SimStatus getSimStatus(uint32_t timeout_ms = 10000L) { return thisModem().getSimStatusImpl(timeout_ms); } /* * 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(); } /* * GPRS functions */ bool gprsConnect(const char* apn, const char* user = NULL, const char* pwd = NULL) { return thisModem().gprsConnectImpl(apn, user, pwd); } bool gprsDisconnect() { return thisModem().gprsDisconnectImpl(); } // Checks if current attached to GPRS/EPS service bool isGprsConnected() { return thisModem().isGprsConnectedImpl(); } // Gets the current network operator String getOperator() { return thisModem().getOperatorImpl(); } /* * WiFi functions */ bool networkConnect(const char* ssid, const char* pwd) { return thisModem().networkConnectImpl(ssid, pwd); } bool networkDisconnect() { return thisModem().networkDisconnectImpl(); } /* * GPRS functions */ String getLocalIP() { return thisModem().getLocalIPImpl(); } IPAddress localIP() { return thisModem().TinyGsmIpFromString(thisModem().getLocalIP()); } /* * Phone Call functions */ bool callAnswer() { return thisModem().callAnswerImpl(); } bool callNumber(const String& number) { return thisModem().callNumberImpl(number); } bool callHangup() { return thisModem().callHangupImpl(); } bool dtmfSend(char cmd, int duration_ms = 100) { return thisModem().dtmfSendImpl(cmd, duration_ms); } /* * Messaging functions */ String sendUSSD(const String& code) { return thisModem().sendUSSDImpl(code); } bool sendSMS(const String& number, const String& text) { return thisModem().sendSMSImpl(number, text); } bool sendSMS_UTF16(const char* const number, const void* text, size_t len) { return thisModem().sendSMS_UTF16Impl(number, text, len); } /* * Location functions */ String getGsmLocation() { return thisModem().getGsmLocationImpl(); } /* * GPS location functions */ // No template interface or implementation of these functions /* * Time functions */ String getGSMDateTime(TinyGSMDateTimeFormat format) { return thisModem().getGSMDateTimeImpl(format); } /* * Battery & temperature functions */ uint16_t getBattVoltage() { return thisModem().getBattVoltageImpl(); } int8_t getBattPercent() { return thisModem().getBattPercentImpl(); } uint8_t getBattChargeState() { return thisModem().getBattChargeStateImpl(); } bool getBattStats(uint8_t& chargeState, int8_t& percent, uint16_t& milliVolts) { return thisModem().getBattStatsImpl(chargeState, percent, milliVolts); } float getTemperature() { return thisModem().getTemperatureImpl(); } /* * CRTP Helper */ protected: const modemType& thisModem() const { return static_cast(*this); } modemType& thisModem() { return static_cast(*this); } /* * Inner Client */ public: class GsmClient : public Client { // Make all classes created from the modem template friends friend class TinyGsmModem; typedef TinyGsmFifo RxFifo; public: // bool init(modemType* modem, uint8_t); // int connect(const char* host, uint16_t port, int timeout_s); // Connect to a IP address given as an IPAddress object by // converting said IP address to text // int connect(IPAddress ip, uint16_t port, int timeout_s) { // return connect(TinyGsmStringFromIp(ip).c_str(), port, // timeout_s); // } // int connect(const char* host, uint16_t port) override { // return connect(host, port, 75); // } // int connect(IPAddress ip, uint16_t port) override { // return connect(ip, port, 75); // } static String TinyGsmStringFromIp(IPAddress ip) { String host; host.reserve(16); host += ip[0]; host += "."; host += ip[1]; host += "."; host += ip[2]; host += "."; host += ip[3]; return host; } // void stop(uint32_t maxWaitMs); // void stop() override { // stop(15000L); // } // Writes data out on the client using the modem send functionality size_t write(const uint8_t* buf, size_t size) override { TINY_GSM_YIELD(); at->maintain(); return at->modemSend(buf, size, mux); } size_t write(uint8_t c) override { return write(&c, 1); } size_t write(const char* str) { if (str == NULL) return 0; return write((const uint8_t*)str, strlen(str)); } int available() override { TINY_GSM_YIELD(); switch (bufType) { // Returns the number of characters available in the TinyGSM fifo case NO_MODEM_BUFFER: if (!rx.size() && sock_connected) { at->maintain(); } return rx.size(); // Returns the combined number of characters available in the TinyGSM // fifo and the modem chips internal fifo. case READ_NO_CHECK: if (!rx.size()) { at->maintain(); } return rx.size() + sock_available; // Returns the combined number of characters available in the TinyGSM // fifo and the modem chips internal fifo, doing an extra check-in // with the modem to see if anything has arrived without a UURC. case READ_AND_CHECK_SIZE: if (!rx.size()) { if (millis() - prev_check > 500) { got_data = true; prev_check = millis(); } at->maintain(); } return rx.size() + sock_available; } } int read(uint8_t* buf, size_t size) override { TINY_GSM_YIELD(); size_t cnt = 0; uint32_t _startMillis = millis(); switch (bufType) { // Reads characters out of the TinyGSM fifo, waiting for any URC's // from the modem for new data if there's nothing in the fifo. case NO_MODEM_BUFFER: while (cnt < size && millis() - _startMillis < _timeout) { size_t chunk = TinyGsmMin(size - cnt, rx.size()); if (chunk > 0) { rx.get(buf, chunk); buf += chunk; cnt += chunk; continue; } /* TODO: Read directly into user buffer? */ if (!rx.size() && sock_connected) { at->maintain(); } } return cnt; // Reads characters out of the TinyGSM fifo, and from the modem chip's // internal fifo if avaiable. case READ_NO_CHECK: at->maintain(); while (cnt < size) { size_t chunk = TinyGsmMin(size - cnt, rx.size()); if (chunk > 0) { rx.get(buf, chunk); buf += chunk; cnt += chunk; continue; } /* TODO: Read directly into user buffer? */ at->maintain(); if (sock_available > 0) { int n = at->modemRead( TinyGsmMin((uint16_t)rx.free(), sock_available), mux); if (n == 0) break; } else { break; } } return cnt; // Reads characters out of the TinyGSM fifo, and from the modem chips // internal fifo if avaiable, also double checking with the modem if // data has arrived without issuing a UURC. case READ_AND_CHECK_SIZE: at->maintain(); while (cnt < size) { size_t chunk = TinyGsmMin(size - cnt, rx.size()); if (chunk > 0) { rx.get(buf, chunk); buf += chunk; cnt += chunk; continue; } // Workaround: Some modules "forget" to notify about data arrival if (millis() - prev_check > 500) { got_data = true; prev_check = millis(); } // TODO(vshymanskyy): Read directly into user buffer? at->maintain(); if (sock_available > 0) { int n = at->modemRead( TinyGsmMin((uint16_t)rx.free(), sock_available), mux); if (n == 0) break; } else { break; } } return cnt; } } int read() override { uint8_t c; if (read(&c, 1) == 1) { return c; } return -1; } // TODO(SRGDamia1): Implement peek int peek() override { return -1; } void flush() override { at->stream.flush(); } uint8_t connected() override { if (available()) { return true; } return sock_connected; } operator bool() override { return connected(); } /* * Extended API */ String remoteIP() TINY_GSM_ATTR_NOT_IMPLEMENTED; protected: // Read and dump anything remaining in the modem's internal buffer. // Using this in the client stop() function. // The socket will appear open in response to connected() even after it // closes until all data is read from the buffer. // Doing it this way allows the external mcu to find and get all of the // data that it wants from the socket even if it was closed externally. void dumpModemBuffer(uint32_t maxWaitMs) { TINY_GSM_YIELD(); rx.clear(); at->maintain(); uint32_t startMillis = millis(); while (sock_available > 0 && (millis() - startMillis < maxWaitMs)) { at->modemRead(TinyGsmMin((uint16_t)rx.free(), sock_available), mux); rx.clear(); at->maintain(); } } modemType* at; uint8_t mux; uint16_t sock_available; uint32_t prev_check; bool sock_connected; bool got_data; RxFifo rx; }; /* * Inner Secure Client */ /* * Constructor */ protected: /* * Basic functions */ 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"; } res1.replace("\r\nOK\r\n", ""); res1.replace("\rOK\r", ""); res2.trim(); String name = res1 + String(' ') + res2; DBG("### Modem:", name); return name; } void maintainImpl() { switch (bufType) { case READ_AND_CHECK_SIZE: // Keep listening for modem URC's and proactively iterate through // sockets asking if any data is avaiable for (int mux = 0; mux < muxCount; mux++) { GsmClient* sock = thisModem().sockets[mux]; if (sock && sock->got_data) { sock->got_data = false; sock->sock_available = thisModem().modemGetAvailable(mux); } } while (thisModem().stream.available()) { thisModem().waitResponse(15, NULL, NULL); } break; default: // Just listen for any URC's thisModem().waitResponse(100, NULL, NULL); break; } } 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() { thisModem().sendAT(GF("+CFUN=0")); if (thisModem().waitResponse(10000L) != 1) { return false; } delay(3000); return true; } bool sleepEnableImpl(bool enable = true) TINY_GSM_ATTR_NOT_IMPLEMENTED; /* * SIM card functions */ protected: // Unlocks a sim via the 3GPP TS command AT+CPIN bool simUnlockImpl(const char* pin) { if (pin && strlen(pin) > 0) { thisModem().sendAT(GF("+CPIN=\""), pin, GF("\"")); return thisModem().waitResponse() == 1; } return true; } // Gets the CCID of a sim card via AT+CCID String getSimCCIDImpl() { thisModem().sendAT(GF("+CCID")); if (thisModem().waitResponse(GF("+CCID:")) != 1) { return ""; } String res = thisModem().stream.readStringUntil('\n'); thisModem().waitResponse(); res.trim(); return res; } // Asks for TA Serial Number Identification (IMEI) via the V.25TER standard // AT+GSN command String getIMEIImpl() { thisModem().sendAT(GF("+GSN")); String res = thisModem().stream.readStringUntil('\n'); thisModem().waitResponse(); res.trim(); return res; } SimStatus getSimStatusImpl(uint32_t timeout_ms = 10000L) { for (uint32_t start = millis(); millis() - start < timeout_ms;) { thisModem().sendAT(GF("+CPIN?")); if (thisModem().waitResponse(GF("+CPIN:")) != 1) { delay(1000); continue; } int status = thisModem().waitResponse(GF("READY"), GF("SIM PIN"), GF("SIM PUK"), GF("NOT INSERTED"), GF("NOT READY")); thisModem().waitResponse(); switch (status) { case 2: case 3: return SIM_LOCKED; case 1: return SIM_READY; default: return SIM_ERROR; } } return SIM_ERROR; } /* * 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 int getRegistrationStatusXREG(const char* regCommand) { thisModem().sendAT('+', regCommand, '?'); // check for any of the three for simplicity int 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().stream.readStringUntil('\n').toInt(); 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 int16_t getSignalQualityImpl() { thisModem().sendAT(GF("+CSQ")); if (thisModem().waitResponse(GF("+CSQ:")) != 1) { return 99; } int res = thisModem().stream.readStringUntil(',').toInt(); thisModem().waitResponse(); return res; } /* * GPRS functions */ protected: // Checks if current attached to GPRS/EPS service bool isGprsConnectedImpl() { thisModem().sendAT(GF("+CGATT?")); if (thisModem().waitResponse(GF("+CGATT:")) != 1) { return false; } int res = thisModem().stream.readStringUntil('\n').toInt(); thisModem().waitResponse(); if (res != 1) { return false; } return thisModem().localIP() != IPAddress(0, 0, 0, 0); } // Gets the current network operator via the 3GPP TS command AT+COPS String getOperatorImpl() { thisModem().sendAT(GF("+COPS?")); if (thisModem().waitResponse(GF("+COPS:")) != 1) { return ""; } thisModem().streamSkipUntil('"'); /* Skip mode and format */ String res = thisModem().stream.readStringUntil('"'); thisModem().waitResponse(); return res; } /* * WiFi functions */ bool networkConnectImpl(const char* ssid, const char* pwd) { return false; } bool networkDisconnectImpl() { return thisModem().gprsConnectImpl(); } /* * IP Address functions */ protected: 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 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]); } /* * Phone Call functions */ protected: bool callAnswerImpl() { thisModem().sendAT(GF("A")); return thisModem().waitResponse() == 1; } // Returns true on pick-up, false on error/busy bool callNumberImpl(const String& number) { if (number == GF("last")) { thisModem().sendAT(GF("DL")); } else { thisModem().sendAT(GF("D"), number, ";"); } int status = thisModem().waitResponse(60000L, GF("OK"), GF("BUSY"), GF("NO ANSWER"), GF("NO CARRIER")); switch (status) { case 1: return true; case 2: case 3: return false; default: return false; } } bool callHangupImpl() { thisModem().sendAT(GF("H")); return thisModem().waitResponse() == 1; } // 0-9,*,#,A,B,C,D bool dtmfSendImpl(char cmd, int duration_ms = 100) { duration_ms = constrain(duration_ms, 100, 1000); thisModem().sendAT(GF("+VTD="), duration_ms / 100); // VTD accepts in 1/10 of a second thisModem().waitResponse(); thisModem().sendAT(GF("+VTS="), cmd); return thisModem().waitResponse(10000L) == 1; } /* * Messaging functions */ protected: static inline String TinyGsmDecodeHex7bit(String& instr) { String result; byte reminder = 0; int bitstate = 7; for (unsigned i = 0; i < instr.length(); i += 2) { char buf[4] = { 0, }; buf[0] = instr[i]; buf[1] = instr[i + 1]; byte b = strtol(buf, NULL, 16); byte bb = b << (7 - bitstate); char c = (bb + reminder) & 0x7F; result += c; reminder = b >> bitstate; bitstate--; if (bitstate == 0) { char cc = reminder; result += cc; reminder = 0; bitstate = 7; } } return result; } static inline String TinyGsmDecodeHex8bit(String& instr) { String result; for (unsigned i = 0; i < instr.length(); i += 2) { char buf[4] = { 0, }; buf[0] = instr[i]; buf[1] = instr[i + 1]; char b = strtol(buf, NULL, 16); result += b; } return result; } static inline String TinyGsmDecodeHex16bit(String& instr) { String result; for (unsigned i = 0; i < instr.length(); i += 4) { char buf[4] = { 0, }; buf[0] = instr[i]; buf[1] = instr[i + 1]; char b = strtol(buf, NULL, 16); if (b) { // If high byte is non-zero, we can't handle it ;( #if defined(TINY_GSM_UNICODE_TO_HEX) result += "\\x"; result += instr.substring(i, i + 4); #else result += "?"; #endif } else { buf[0] = instr[i + 2]; buf[1] = instr[i + 3]; b = strtol(buf, NULL, 16); result += b; } } return result; } String sendUSSDImpl(const String& code) { // Set preferred message format to text mode thisModem().sendAT(GF("+CMGF=1")); thisModem().waitResponse(); // Set 8-bit hexadecimal alphabet (3GPP TS 23.038) thisModem().sendAT(GF("+CSCS=\"HEX\"")); thisModem().waitResponse(); // Send the message thisModem().sendAT(GF("+CUSD=1,\""), code, GF("\"")); if (thisModem().waitResponse() != 1) { return ""; } if (thisModem().waitResponse(10000L, GF("+CUSD:")) != 1) { return ""; } thisModem().stream.readStringUntil('"'); String hex = thisModem().stream.readStringUntil('"'); thisModem().stream.readStringUntil(','); int dcs = thisModem().stream.readStringUntil('\n').toInt(); if (dcs == 15) { return TinyGsmDecodeHex8bit(hex); } else if (dcs == 72) { return TinyGsmDecodeHex16bit(hex); } else { return hex; } } bool sendSMSImpl(const String& number, const String& text) { // Set preferred message format to text mode thisModem().sendAT(GF("+CMGF=1")); thisModem().waitResponse(); // Set GSM 7 bit default alphabet (3GPP TS 23.038) thisModem().sendAT(GF("+CSCS=\"GSM\"")); thisModem().waitResponse(); thisModem().sendAT(GF("+CMGS=\""), number, GF("\"")); if (thisModem().waitResponse(GF(">")) != 1) { return false; } thisModem().stream.print(text); // Actually send the message thisModem().stream.write(static_cast(0x1A)); // Terminate the message thisModem().stream.flush(); return thisModem().waitResponse(60000L) == 1; } // Common methods for UTF8/UTF16 SMS. // Supported by: BG96, M95, MC60, SIM5360, SIM7000, SIM7600, SIM800 class UTF8Print : public Print { public: explicit UTF8Print(Print& p) : p(p) {} size_t write(const uint8_t c) override { if (prv < 0xC0) { if (c < 0xC0) printHex(c); prv = c; } else { uint16_t v = uint16_t(prv) << 8 | c; v -= (v >> 8 == 0xD0) ? 0xCC80 : 0xCD40; printHex(v); prv = 0; } return 1; } private: Print& p; uint8_t prv = 0; void printHex(const uint16_t v) { uint8_t c = v >> 8; if (c < 0x10) p.print('0'); p.print(c, HEX); c = v & 0xFF; if (c < 0x10) p.print('0'); p.print(c, HEX); } }; bool sendSMS_UTF8_begin(const char* const number) { thisModem().sendAT(GF("+CMGF=1")); thisModem().waitResponse(); thisModem().sendAT(GF("+CSCS=\"HEX\"")); thisModem().waitResponse(); thisModem().sendAT(GF("+CSMP=17,167,0,8")); thisModem().waitResponse(); thisModem().sendAT(GF("+CMGS=\""), number, GF("\"")); return thisModem().waitResponse(GF(">")) == 1; } bool sendSMS_UTF8_end() { thisModem().stream.write(static_cast(0x1A)); thisModem().stream.flush(); return thisModem().waitResponse(60000L) == 1; } UTF8Print sendSMS_UTF8_stream() { return UTF8Print(thisModem().stream); } bool sendSMS_UTF16Impl(const char* const number, const void* text, size_t len) { if (!sendSMS_UTF8_begin(number)) { return false; } uint16_t* t = reinterpret_cast(text); for (size_t i = 0; i < len; i++) { uint8_t c = t[i] >> 8; if (c < 0x10) { thisModem().stream.print('0'); } thisModem().stream.print(c, HEX); c = t[i] & 0xFF; if (c < 0x10) { thisModem().stream.print('0'); } thisModem().stream.print(c, HEX); } return sendSMS_UTF8_end(); } /* * Location functions */ protected: String getGsmLocationImpl() { thisModem().sendAT(GF("+CIPGSMLOC=1,1")); if (thisModem().waitResponse(10000L, GF("+CIPGSMLOC:")) != 1) { return ""; } String res = thisModem().stream.readStringUntil('\n'); thisModem().waitResponse(); res.trim(); return res; } /* * GPS location functions */ public: /* * Time functions */ protected: String getGSMDateTimeImpl(TinyGSMDateTimeFormat format) { thisModem().sendAT(GF("+CCLK?")); if (thisModem().waitResponse(2000L, GF("+CCLK: \"")) != 1) { return ""; } String res; switch (format) { case DATE_FULL: res = thisModem().stream.readStringUntil('"'); break; case DATE_TIME: thisModem().streamSkipUntil(','); res = thisModem().stream.readStringUntil('"'); break; case DATE_DATE: res = thisModem().stream.readStringUntil(','); break; } return res; } /* * Battery & temperature functions */ protected: // Use: float vBatt = modem.getBattVoltage() / 1000.0; uint16_t getBattVoltageImpl() { thisModem().sendAT(GF("+CBC")); if (thisModem().waitResponse(GF("+CBC:")) != 1) { return 0; } thisModem().streamSkipUntil(','); // Skip battery charge status thisModem().streamSkipUntil(','); // Skip battery charge level // return voltage in mV uint16_t res = thisModem().stream.readStringUntil(',').toInt(); // Wait for final OK thisModem().waitResponse(); return res; } int8_t getBattPercentImpl() { thisModem().sendAT(GF("+CBC")); if (thisModem().waitResponse(GF("+CBC:")) != 1) { return false; } thisModem().streamSkipUntil(','); // Skip battery charge status // Read battery charge level int res = thisModem().stream.readStringUntil(',').toInt(); // Wait for final OK thisModem().waitResponse(); return res; } uint8_t getBattChargeStateImpl() { thisModem().sendAT(GF("+CBC?")); if (thisModem().waitResponse(GF("+CBC:")) != 1) { return false; } // Read battery charge status int res = thisModem().stream.readStringUntil(',').toInt(); // Wait for final OK thisModem().waitResponse(); return res; } bool getBattStatsImpl(uint8_t& chargeState, int8_t& percent, uint16_t& milliVolts) { thisModem().sendAT(GF("+CBC?")); if (thisModem().waitResponse(GF("+CBC:")) != 1) { return false; } chargeState = thisModem().stream.readStringUntil(',').toInt(); percent = thisModem().stream.readStringUntil(',').toInt(); milliVolts = thisModem().stream.readStringUntil('\n').toInt(); // Wait for final OK thisModem().waitResponse(); return true; } float getTemperatureImpl() TINY_GSM_ATTR_NOT_AVAILABLE; /* * Client related functions */ protected: /* Utilities */ // Utility templates for writing/skipping characters on a stream template void streamWrite(T last) { thisModem().stream.print(last); } template void streamWrite(T head, Args... tail) { thisModem().stream.print(head); thisModem().streamWrite(tail...); } // template void sendAT(Args... cmd) { // thisModem().streamWrite("AT", cmd..., thisModem().gsmNL); // thisModem().stream.flush(); // TINY_GSM_YIELD(); /* DBG("### AT:", cmd...); */ // } 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; } // Yields up to a time-out period and then reads a character from the stream // into the mux FIFO // TODO(SRGDamia1): Do we need to wait two _timeout periods for no // character return? Will wait once in the first "while // !stream.available()" and then will wait again in the stream.read() // function. void moveCharFromStreamToFifo(uint8_t mux) { uint32_t startMillis = millis(); while (!thisModem().stream.available() && (millis() - startMillis < thisModem().sockets[mux]->_timeout)) { TINY_GSM_YIELD(); } char c = thisModem().stream.read(); thisModem().sockets[mux]->rx.put(c); } }; #endif // SRC_TINYGSMCOMMON_H_