Cellular library for MTS Socket Modem Arduino Shield devices from Multi-Tech Systems

Dependents:   mtsas mtsas mtsas mtsas

Files at this revision

API Documentation at this revision

Comitter:
Vanger
Date:
Mon Jul 14 17:33:24 2014 +0000
Parent:
29:edc613ed3f2e
Child:
31:529db15abda7
Commit message:
For EasyIP.cpp:; Made sendEscapeCommand() and socketCheck() socket closed verification; connect(), disconnect(), and isConnected() were tweaked; IMPL. bind(),open(),isOpen(),close(),read(),write(),readable(),writeable(),reset(); Fixed bug in CellUtils.h;

Changed in this revision

Cellular/CellularFactory.cpp Show annotated file Show diff for this revision Revisions of this file
Cellular/EasyIP.cpp Show annotated file Show diff for this revision Revisions of this file
Cellular/EasyIP.h Show annotated file Show diff for this revision Revisions of this file
Utils/CellUtils.h Show annotated file Show diff for this revision Revisions of this file
--- a/Cellular/CellularFactory.cpp	Wed Jul 02 15:06:03 2014 +0000
+++ b/Cellular/CellularFactory.cpp	Mon Jul 14 17:33:24 2014 +0000
@@ -11,7 +11,7 @@
     std::string reply;
     Cellular::Radio type;
     Cellular* cell;
-
+    
     /* wait for radio to get into a good state */
     while (true) {
         if (sendCommand(io, "AT", 1000).find("OK") != string::npos) {
@@ -20,14 +20,13 @@
         } else {
             logTrace("waiting on radio...");
         }
-
         wait(1);
     }
-
+    
     /* "ATI4" gets us the model (HE910, DE910, etc) */
     for (int i = 0; i < 5; i++) {
         model = sendCommand(io, "ATI4", 2000);
-        if (model.find("error") == string::npos && model.find("ERROR") == string::npos) {
+        if (model.find("error") == string::npos && model.find("ERROR") == string::npos && !model.empty()) {
             /* didn't get an error - keep going */
             break;
         }
--- a/Cellular/EasyIP.cpp	Wed Jul 02 15:06:03 2014 +0000
+++ b/Cellular/EasyIP.cpp	Mon Jul 14 17:33:24 2014 +0000
@@ -12,6 +12,112 @@
 
 using namespace mts;
 
+bool EasyIP::sendEscapeCommand()
+{
+    //string Cellular::sendCommand(const std::string& command, unsigned int timeoutMillis, char esc)
+    if(io == NULL) {
+        logError("MTSBufferedIO not set");
+        return false;
+    }
+    if(!socketOpened) {
+        logError("Socket is not open. Can not send AT escape sequence (+++)");
+        return false;
+    }
+    
+    io->rxClear();
+    io->txClear();
+    
+    std::string result;
+    unsigned int timeoutMillis = 2000;
+    const int size_cmd = 3;
+    //Attempt to write command
+    wait(1); //Format for +++ command is 1 second wait, send +++, then another second wait
+             //1s wait after command is implemented as a polling function for 2 seconds
+             //Option: Could change wait periods to be longer/shorter (0-255)*50ms
+    if(io->write("+++", size_cmd, timeoutMillis) != size_cmd) {
+        //Failed to write command
+        logError("failed to send command to radio within %d milliseconds", timeoutMillis);
+        return false;
+    }
+    
+    int timer = 0;
+    char tmp[256];
+    tmp[255] = 0;
+    bool done = false;
+    io->read(tmp,255,0);
+    bool exitmode = false;
+    
+    do {
+        wait(0.1);
+        timer += 100;
+        //Make a non-blocking read call by passing timeout of zero
+            int size = io->read(tmp,255,0);    //1 less than allocated (timeout is instant)
+            if(size > 0) {
+                result.append(tmp, size);
+            }
+        if(result.find("OK") != std::string::npos) {
+            exitmode = true;
+            done = true;
+        } else if(result.find("NO CARRIER") != std::string::npos) {
+            exitmode = true;
+            done = true;
+        } else if(result.find("ERROR") != std::string::npos) {
+            exitmode = false;
+            done = true;
+        }
+        if(timer >= timeoutMillis) {
+            logDebug("Escape sequence [+++] timed out after %d milliseconds", timeoutMillis);
+            exitmode = true;
+            done = true;
+        }
+    } while (!done);
+    
+    return exitmode;
+}
+
+bool EasyIP::socketCheck() {
+    bool status = false;
+    std::string socketInfo = "9"; //9 is unrecognized
+    std::vector<std::string> params;
+    
+    if(sendEscapeCommand()) {
+        socketOpened = false;
+        if(sendBasicCommand("AT", 1000) == MTS_SUCCESS) {
+            socketInfo = sendCommand("AT#SS=1", 2000);
+            if(socketInfo.find("OK") != std::string::npos) {
+                //Found valid response
+                params = Text::split(socketInfo, "\r\n");
+                params = Text::split(params[1], ",");
+                socketInfo = params[1];
+                //Check comparison of params[1] to response codes
+            } else {
+                logError("Could not determine socket status[%s]",socketInfo.c_str());
+                socketInfo == "9"; //9 is unrecognized
+            }
+        }
+    } else {
+        status = false; //Return value of socketOpened when checking
+    }
+    //Check socket status query
+    if(socketInfo == "2" || socketInfo == "3" || socketInfo == "1" || socketInfo == "4") {
+        status = true; //2 and 3 are suspended connections
+    } else if(socketInfo == "0" || socketInfo == "5") {
+        status = false; //0 is closed socket, probably won't occur
+    } else {
+        logError("Could not determine socket status");
+        status = false; //anything else is unknown status
+    }
+    
+    if(status) {
+        std::string reconnect = sendCommand("AT#SO=1", 2000);
+        if(reconnect.find("CONNECT") != std::string::npos || reconnect.find("OK") != std::string::npos) {
+        } else {
+            logError("Failed to resume socket connection");
+        }
+    }
+    return status;
+}
+
 EasyIP::EasyIP(Radio type)
 {
     //Not sure how the construction process is done, 
@@ -78,7 +184,6 @@
     }
     //Create an mbed timer object
     Timer tmr;
-
     //Check Registration: AT+CREG? == 0,1
     //(Does the AT command inside Cellular class)
     tmr.start();
@@ -90,15 +195,14 @@
         } else {
             break;
         }
-    } while(tmr.read() < 30);
-
+    } while(tmr.read() < 30); 
     //Check RSSI: AT+CSQ
     //Does the command inside Cellular
     tmr.reset();
     do {
         int rssi = getSignalStrength();
         logDebug("Signal strength: %d", rssi);
-        if(rssi == 99) {
+        if((rssi == 99) || (rssi == -1)) {
             logTrace("No Signal ... waiting");
             wait(1);
         } else {
@@ -113,11 +217,11 @@
         logDebug("Making PPP Connection Attempt");
     }
     //The main thing going on; Sends the AT command to start a connection
-    //Assuming context is already stored in the modem...If not, will need to set context from classes/data
-    std::string pppResult = sendCommand("AT#SGACT=1,1", 120000);
-  
+    //Assuming context is already stored in the modem
+    std::string pppResult = sendCommand("AT#SGACT=1,1", 2000);
+    std::vector<std::string> parts;
     if(pppResult.find("OK") != std::string::npos) {
-        std::vector<std::string> parts = Text::split(pppResult, "\r\n");
+        parts = Text::split(pppResult, "\r\n");
         if(parts.size() >= 2) {
             parts = Text::split(parts[1], " ");
             local_address = parts[1];
@@ -126,7 +230,16 @@
         pppConnected = true;
 
     } else {
-        pppConnected = false;
+        pppResult = sendCommand("AT#SGACT?", 2000);
+        if(pppResult.empty() || (pppResult.find("ERROR") != std::string::npos)) {
+            pppConnected = false;
+        } else {
+            if(pppResult.find("1,1") != std::string::npos) {
+                pppConnected = true;
+            } else {
+                pppConnected = false;
+            }
+        }
     }
 
     return pppConnected;
@@ -134,32 +247,30 @@
 
 void EasyIP::disconnect()
 {
-    bool complete = false;
-    Timer dctmr;
     //AT#SGACT=1,0: Close a PPP connection
     logDebug("Closing PPP Connection");    
     if(socketOpened) {
-        close(); //Calls another EasyIP 
-                 //function to close socket before disconnect
+        if(!close()) { //Calls another EasyIP function to close socket before disconnect
+            logDebug("Failed to close socket for disconnect");
+            return; //Can't close connection without AT commands
+                    //(and thus need socket closed)
+        }
     }
     //Sends AT#SGACT=1,0 command
-    dctmr.start();
-    do {
-        if(sendBasicCommand("AT#SGACT=1,0", 10000) == MTS_SUCCESS) {
-            complete = true;
-        } else {
-            wait(0.050);
-        }
-    } while((!complete) && (dctmr.read() < 5));
-    logDebug("Successfully closed PPP Connection");
+    if(sendBasicCommand("AT#SGACT=1,0", 1000) == MTS_SUCCESS) {
+        pppConnected = false;
+        logDebug("Successfully closed PPP Connection");
+    }
     pppConnected = false; //Cell will drop connection if we go silent
+    return;
 }
 
 bool EasyIP::isConnected()
 {
     std::string stateString;
     std::vector<std::string> pieces;
-    bool signal = false, regist = false, active = false, ping = false;
+    //state flags for various connection checks
+    bool signal = false, regist = false, active = false;
     
     //1) Check if APN was set if we're on an HSPA radio
     if (type == MTSMC_H5_IP || type == MTSMC_H5 || type == MTSMC_G3) {
@@ -175,9 +286,10 @@
         return true;
     }
     
+    
     //3) Query the radio
     //Check antenna signal
-    std::string reply = sendCommand("AT+CSQ", 1000);
+    std::string reply = sendCommand("AT+CSQ", 500);
     if(reply.empty() || (reply.find("ERROR") != std::string::npos)) {
         signal = false;
     } else {
@@ -195,7 +307,7 @@
     }
     
     //Check cell tower registration
-    reply = sendCommand("AT+CREG?", 1000);
+    reply = sendCommand("AT+CREG?", 500);
     if(reply.empty() || (reply.find("ERROR") != std::string::npos)) {
         regist = false;
     } else {
@@ -212,11 +324,8 @@
         }
     }
     
-    //Check internet connection through ping, admittedly uses data over cell network
-    ping = EasyIP::ping("www.google.com");
-    
     //Check active mode (SGACT = 1,1)
-    reply = sendCommand("AT#SGACT?", 1000);
+    reply = sendCommand("AT#SGACT?", 500);
     if(reply.empty() || (reply.find("ERROR") != std::string::npos)) {
         active = false;
     } else {
@@ -232,80 +341,350 @@
             }
         }
     }
-    
-    if(ping) {
-        if(!pppConnected) {
-            logWarning("Internal PPP state tracking differs from radio (DISCONNECTED:CONNECTED)");
-        }
-        pppConnected = true;
-    } else {
-        std::string stateStr;
+    //4) Determine radio state
+    if(regist && signal) {
         if(pppConnected) {
-                //New code for state from boolean values
-                if(regist && signal) {
-                    if(active) {
-                        stateString = "AUTHENTICATING";
-                    } else {
-                        stateString = "IDLE";
-                    }
-                } else if(regist != signal) {
-                    stateString = "CHECKING";
+            if(active) {
+                if(ping()) {
+                    stateString = "CONNECTED";
+                    pppConnected = true;
                 } else {
-                    stateString = "DISCONNECTED";
+                    stateString = "AUTHENTICATING";
+                    pppConnected = true;
+                    return false; //Return false instead of changing pppConnected due to the fact
+                                  //that it is connected to ppp, it just can't ping successfully
                 }
-                logWarning("Internal PPP state tracking differs from radio (CONNECTED:%s)", stateString.c_str());
+            } else {
+                stateString = "DISCONNECTING";
+                pppConnected = false;
+            }
+        } else {
+            if(active) {
+                if(ping()) {
+                    pppConnected = true;
+                    logWarning("Internal PPP state tracking differs from radio (DISCONNECTED:CONNECTED)");
+                    stateString = "CONNECTED";
+                } else {
+                    stateString = "CONNECTING";
+                }
+            } else {
+                stateString = "IDLE";
+            }
         }
+    } else if(regist != signal) {
+        stateString = "CHECKING";
         pppConnected = false;
+    } else if(!regist && !signal) {
+        stateString = "DISCONNECTED";
+        pppConnected = false;
+    }
+    //Log results if necessary
+    if(stateString != "CONNECTED") {
+        logWarning("Internal PPP state tracking differs from radio (CONNECTED:%s)",stateString.c_str());
     }
     return pppConnected;
 }
+
 //Binds the socket to a specific port if able
 bool EasyIP::bind(unsigned int port)
 {
-   
-   
+    if(socketOpened) {
+        logError("socket is open. Can not set local port");
+        return false;
+    }
+    if(port > 65535) {
+        logError("port out of range (0-65535)");
+        return false;
+    }
+    local_port = port;
     return true;
 }
 
 bool EasyIP::open(const std::string& address, unsigned int port, Mode mode)
 {
-   
+    char sOpenSocketCmd[256] = {0}; //String for AT command
+    std::string sMode = "";
+    int typeSocket;
+    
+    //1) Check that we do not have a live connection up
+    if(socketOpened) {
+        //Check that the address, port, and mode match
+        if(host_address != address || host_port != port || socketMode != mode) {
+            if(socketMode == TCP) {
+                logError("TCP socket already opened [%s:%d]", host_address.c_str(), host_port);
+            } else {
+                logError("UDP socket already opened [%s:%d]", host_address.c_str(), host_port);
+            }
+            return false;
+        }
+
+        logDebug("Socket already opened");
+        return true;
+    }
+
+    //2) Check Parameters
+    if(port > 65535) {
+        logError("port out of range (0-65535)");
+        return false;
+    }
+
+    //3) Check PPP connection
+    if(!isConnected()) {
+        logError("PPP not established.  Attempting to connect");
+        if(!connect()) {
+            logError("PPP connection failed");
+            return false;
+        } else {
+            logDebug("PPP connection established");
+        }
+    }
+    //No way to "set" port except on socket call;
+    //Going to need to warn if local_port was not set.
+    if(!local_port) {
+        logDebug("No local port was set: 0");
+    }
+    
+    //4) Set escape sequence to not be transmitted
+    if(sendBasicCommand("AT#SKIPESC=1", 2000) != MTS_SUCCESS) {
+        logWarning("Failed to disable escape sequence transmission on data mode suspension");
+    }
+    
+    if(mode == TCP) {
+        typeSocket = 0;
+        sMode = "TCP";
+    } else {
+        typeSocket = 1;
+        sMode = "UDP";
+    }
+    //4) Close Socket  
+    sprintf(sOpenSocketCmd, "AT#SD=1,%d,%d,%s,0,%d,0", typeSocket, port, address.c_str(), local_port);
+    std::string response = sendCommand(sOpenSocketCmd, 5000);
+    
+    if(response.find("CONNECT") != std::string::npos) {
+        host_address = address;
+        host_port = port;
+        
+        logInfo("Opened %s Socket [%s:%d]", sMode.c_str(), address.c_str(), port);
+        socketOpened = true;
+        socketMode = mode;
+    } else {
+        logWarning("Unable to open %s Socket [%s:%d]", sMode.c_str(), address.c_str(), port);
+        socketOpened = false;
+    }
+    
     return socketOpened;
 }
 
 bool EasyIP::isOpen()
 {
-    
+    if(io->readable()) {
+        logDebug("Assuming open, data available to read.\n\r");
+        return true;
+    }
     return socketOpened;
 }
 
 bool EasyIP::close()
 {
     
-    return true;
+    if(io == NULL) {
+        logError("MTSBufferedIO not set");
+        return false;
+    }
+
+    if(!socketOpened) {
+        logWarning("Socket close() called, but socket was not open");
+        return true;
+    }
+
+    if(!socketCloseable) {
+        logError("Socket is not closeable");
+        return false;
+    }
+    
+    if(!sendEscapeCommand()) {
+        logError("Failed to exit online mode");
+        return false;
+    } else {
+        socketOpened = false;
+    }
+    
+    if(sendBasicCommand("AT#SH=1", 2000) != MTS_SUCCESS) {
+        logDebug("Failed to close socket connection");
+    }
+    
+    Timer tmr;
+    int counter = 0;
+    char tmp[256];
+    tmr.start();
+    do {
+        if(socketOpened == false) {
+            break;
+        }
+        read(tmp, 256, 1000);
+    } while(counter++ < 10);
+
+    io->rxClear();
+    io->txClear();
+
+    return !socketOpened;
 }
 
 int EasyIP::read(char* data, int max, int timeout)
 {
-   
-    return 1;
+    if(io == NULL) {
+        logError("MTSBufferedIO not set");
+        return -1;
+    }
+
+    //Check that nothing is in the rx buffer
+    if(!socketOpened && !io->readable()) {
+        logError("Socket is not open");
+        return -1;
+    }
+
+    int bytesRead = 0;
+    
+
+    if(timeout >= 0) {
+        bytesRead = io->read(data, max, static_cast<unsigned int>(timeout));
+    } else {
+        bytesRead = io->read(data, max);
+    }
+    
+    //Scan for socket closed message
+    for(size_t i = 0; i < bytesRead; i++) {
+        if(data[i] == 'N') {
+            if(strstr(&data[i], "NO CARRIER")) {
+                logTrace("Found socket closed message. Checking validity");
+                //Close socket and Cut Off End of Message
+                socketOpened = socketCheck(); //Verifies legitimacy of socket disconnect
+                if(socketOpened) {
+                    logDebug("Socket still open");
+                    continue;
+                } else {
+                    logDebug("Socket closed");
+                    data[i] = '\0';
+                    bytesRead = i;
+                    break;
+                }
+            }
+        }
+    }
+    return bytesRead;
 }
 
 int EasyIP::write(const char* data, int length, int timeout)
 {
-    
-    return 1;
+    if(io == NULL) {
+        logError("MTSBufferedIO not set");
+        return -1;
+    }
+
+    if(!socketOpened) {
+        logError("Socket is not open");
+        return -1;
+    }
+
+    //In order to avoid allocating another buffer, capture indices of
+    //characters to escape during write
+    int specialWritten = 0;
+    std::vector<int> vSpecial;
+    if(socketCloseable) {
+        for(int i = 0; i < length; i++) {
+            if(data[i] == ETX || data[i] == DLE) {
+                //Push back index of special characters
+                vSpecial.push_back(i);
+            }
+        }
+    }
+
+    int bytesWritten = 0;
+    if(timeout >= 0) {
+        Timer tmr;
+        tmr.start();
+        do {
+            int available = io->writeable();
+            if (available > 0) {
+                if(specialWritten < vSpecial.size()) {
+                    //Check if current index is at a special character
+                    if(bytesWritten == vSpecial[specialWritten]) {
+                        if(available < 2) {
+                            //Requires at least two bytes of space
+                            wait(0.05);
+                            continue;
+                        }
+                        //Ready to write special character
+                        if(io->write(DLE)) {
+                            specialWritten++;
+                            if(io->write(data[bytesWritten])) {
+                                bytesWritten++;
+                            }
+                        } else {
+                            //Unable to write escape character, try again next round
+                            wait(0.05);
+                        }
+                    } else {
+                        //We want to write all the way up to the next special character
+                        int relativeIndex = vSpecial[specialWritten] - bytesWritten;
+                        int size = MIN(available, relativeIndex);
+                        bytesWritten += io->write(&data[bytesWritten], size);
+                    }
+                } else {
+                    int size = MIN(available, length - bytesWritten);
+                    bytesWritten += io->write(&data[bytesWritten], size);
+                }
+            } else {
+                wait(0.05);
+            }
+        } while (tmr.read_ms() <= timeout && bytesWritten < length);
+    } else {
+        for(int i = 0; i < vSpecial.size(); i++) {
+            //Write up to the special character, then write the special character
+            int size = vSpecial[i] - bytesWritten;
+            int currentWritten = io->write(&data[bytesWritten], size);
+            bytesWritten += currentWritten;
+            if(currentWritten != size) {
+                //Failed to write up to the special character.
+                return bytesWritten;
+            }
+            if(io->write(DLE) && io->write(data[bytesWritten])) {
+                bytesWritten++;
+            } else {
+                //Failed to write the special character.
+                return bytesWritten;
+            }
+        }
+
+        bytesWritten = io->write(&data[bytesWritten], length - bytesWritten);
+    }
+
+    return bytesWritten;
 }
 
 unsigned int EasyIP::readable()
 {
-    
+    if(io == NULL) {
+        logWarning("MTSBufferedIO not set");
+        return 0;
+    }
+    if(!socketOpened && !io->readable()) {
+        logWarning("Socket is not open");
+        return 0;
+    }
     return io->readable();
 }
 
 unsigned int EasyIP::writeable()
 {
-    
+    if(io == NULL) {
+        logWarning("MTSBufferedIO not set");
+        return 0;
+    }
+    if(!socketOpened) {
+        logWarning("Socket is not open");
+        return 0;
+    }
 
     return io->writeable();
 }
@@ -338,6 +717,13 @@
 
 void EasyIP::reset()
 {
+    disconnect();
+    if(sendBasicCommand("AT#REBOOT", 10000) != MTS_SUCCESS) {
+        logError("Socket Modem did not accept RESET command\n\r");
+    } else {
+        logWarning("Socket Modem is resetting, allow 30 seconds for it to come back\n\r");
+        return;
+    }
 }
 
 std::string EasyIP::getDeviceIP()
@@ -368,27 +754,22 @@
     int Timeout=0;
     
     //Format parameters for sending to radio
-    sprintf(buffer, "AT#PING=%s,1,32,%d", address.c_str(), (5*PINGDELAY));
+    sprintf(buffer, "AT#PING=%s,1,32,%d", address.c_str(), (PINGDELAY*10));
     
     for(int pngs=0; pngs<PINGNUM; pngs++) {
-        std::string response = sendCommand(buffer, (PINGDELAY*1000)); //Send 1 ping
-        //printf("Response [%s]\n", response.c_str()); //remove
+        std::string response = sendCommand(buffer, (PINGDELAY*1010)); //Send 1 ping
         if(response.empty()) {
-            //printf("Response empty!\n"); //remove
             continue; //Skip current loop if send command fails
         }
         if(response.find("ERROR") != std::string::npos) {
-            //printf("ERROR found\n"); //remove
             continue; //Skip current loop if send command fails
         }
         parts = Text::split(response, "\r\n");
         if(parts.size() < 2) {
-            //printf("Response newline-split size %d\n", parts.size()); //remove
             continue;
         }
         parts = Text::split(parts[1], ",");
         if(parts.size() < 4) {
-            //printf("Response comma-split size %d\n", parts.size()); //remove
             continue;
         }
         //Parse TTL and Timeout values
@@ -409,6 +790,6 @@
 //Pass 0 to disable socket closeable
 Code EasyIP::setSocketCloseable(bool enabled)
 {
-
+    
     return MTS_SUCCESS;
 }
--- a/Cellular/EasyIP.h	Wed Jul 02 15:06:03 2014 +0000
+++ b/Cellular/EasyIP.h	Mon Jul 14 17:33:24 2014 +0000
@@ -25,6 +25,15 @@
 */
 class EasyIP : public Cellular     //Inherits from Cellular.
 {
+private:
+    /*Function that sends +++ to radio to exit data mode
+      returns true if successful exit from online mode, else false
+    */
+    virtual bool sendEscapeCommand(); 
+    /*Switches to command mode, queries socket connection status,
+      then returns true if there is an active socket connection, 
+      else it returns false.*/
+    virtual bool socketCheck();
 public:
 /** This static function is used to create or get a reference to a
     * Cellular object. Cellular uses the singleton pattern, which means
@@ -41,7 +50,6 @@
     * @returns a reference to the single Cellular obect that has been created.
     */
     EasyIP(Radio type);
-
     /** Destructs a Cellular object and frees all related resources.
     */
     ~EasyIP();
@@ -64,7 +72,7 @@
     virtual int write(const char* data, int length, int timeout = -1);
     virtual unsigned int readable();
     virtual unsigned int writeable();
-    virtual bool ping(const std::string& address = "8.8.8.8"); //Can this default address be different?
+    virtual bool ping(const std::string& address = "8.8.8.8"); //Google DNS server
     virtual std::string getDeviceIP();
     virtual bool setDeviceIP(std::string address = "DHCP");//What does this do? Run DHCP to configure the IP?
     
--- a/Utils/CellUtils.h	Wed Jul 02 15:06:03 2014 +0000
+++ b/Utils/CellUtils.h	Mon Jul 14 17:33:24 2014 +0000
@@ -84,7 +84,7 @@
         if(size > 0) {
             result.append(tmp, size);
         }
-        done =  (result.size() == previous);
+        done =  (result.size() == previous && previous > 0);
         if(timer >= timeoutMillis) {
             if (command != "AT" && command != "at") {
                 logWarning("sendCommand [%s] timed out after %d milliseconds", command.c_str(), timeoutMillis);