From f6228ff7b2c720cbc08acb4f411265faab923237 Mon Sep 17 00:00:00 2001
From: Scott Brust <scott.brust@particle.io>
Date: Thu, 28 Dec 2023 11:52:14 -0800
Subject: [PATCH 1/5] [msom] Add GNSS test FQC command

---
 user/applications/tinker/src/burnin_test.cpp |  50 ++++--
 user/applications/tinker/src/burnin_test.h   |   1 +
 user/applications/tinker/src/fqc_test.cpp    | 175 ++++++++++++++++++-
 user/applications/tinker/src/fqc_test.h      |  14 +-
 4 files changed, 225 insertions(+), 15 deletions(-)

diff --git a/user/applications/tinker/src/burnin_test.cpp b/user/applications/tinker/src/burnin_test.cpp
index a47e3e87a7..013f5bfaca 100644
--- a/user/applications/tinker/src/burnin_test.cpp
+++ b/user/applications/tinker/src/burnin_test.cpp
@@ -557,7 +557,18 @@ static int callbackGPSGGA(int type, const char* buf, int len, bool* gnssLocked)
     return 1;
 }
 
-bool BurninTest::testGnss() {
+static int callbackQGPS(int type, const char* buf, int len, bool* cmdSuccess) {
+    //Log.trace("%d : %s", strlen(buf), buf);
+
+    // If string is `OK` or `+CME ERROR: 504` (ie GNSS already started) then GPS engine is enabled
+    if (!strcmp(buf, "\r\n+CME ERROR: 504\r\n") || !strcmp(buf, "\r\nOK\r\n")) {
+        *cmdSuccess = true;
+    }
+    return 0;
+}
+
+
+bool BurninTest::initGnss() {
     // Turn on GNSS + Modem
     pinMode(GNSS_ANT_PWR, OUTPUT);
     digitalWrite(GNSS_ANT_PWR, HIGH);
@@ -568,25 +579,40 @@ bool BurninTest::testGnss() {
 
     // Enable GNSS. It can take some time after the modem AT interface comes up for the GNSS engine to start
     const int RETRIES = 10;
-    int r = 0;
-    for (int i = 0; i < RETRIES && r != RESP_OK; i++) {
-        r = Cellular.command("AT+QGPS=1\r\n");
+    
+    bool success = false; 
+    for (int i = 0; i < RETRIES && !success; i++) {
+        Cellular.command(callbackQGPS, &success, 5000, "AT+QGPS=1");
         delay(1000);
     }
 
-    if (r != RESP_OK) {
-        strcpy(BurninErrorMessage, "AT+QGPS=1 failed, GNSS not enabled");
+    if (!success) {
+        Log.error("AT+QGPS=1 failed, GNSS not enabled");
         return false;
     }
 
-    // Configure antenna for GNSS priority
-    for (int i = 0; i < RETRIES && r != RESP_OK; i++) {
-        r = Cellular.command("AT+QGPSCFG=\"priority\",0");
-        delay(1000);
+    hal_device_hw_info deviceInfo = {};
+    hal_get_device_hw_info(&deviceInfo, nullptr);
+    if (deviceInfo.ncp[0] == PLATFORM_NCP_QUECTEL_BG95_M5) {
+        int r = 0;
+        // Configure antenna for GNSS priority
+        for (int i = 0; i < RETRIES && r != RESP_OK; i++) {
+            r = Cellular.command("AT+QGPSCFG=\"priority\",0");
+            delay(1000);
+        }
+
+        if (r != RESP_OK) {
+            Log.error("AT+QGPSCFG=\"priority\",0 failed, GNSS not prioritized");
+            return false;
+        }
     }
 
-    if (r != RESP_OK) {
-        strcpy(BurninErrorMessage, "AT+QGPSCFG=\"priority\",0 failed, GNSS not prioritized");
+    return true;
+}
+
+bool BurninTest::testGnss() {
+    if (!initGnss()) {
+        strcpy(BurninErrorMessage, "Failed to initialize GNSS");
         return false;
     }
 
diff --git a/user/applications/tinker/src/burnin_test.h b/user/applications/tinker/src/burnin_test.h
index 3f9755426b..3ca7a4db32 100644
--- a/user/applications/tinker/src/burnin_test.h
+++ b/user/applications/tinker/src/burnin_test.h
@@ -14,6 +14,7 @@ class BurninTest {
 
     void setup(bool forceEnable = false);
     void loop();
+    bool initGnss();
 
     enum class BurninTestState : uint32_t {
         NONE,
diff --git a/user/applications/tinker/src/fqc_test.cpp b/user/applications/tinker/src/fqc_test.cpp
index 6060f14481..9358afe11e 100644
--- a/user/applications/tinker/src/fqc_test.cpp
+++ b/user/applications/tinker/src/fqc_test.cpp
@@ -53,7 +53,7 @@ namespace {
 FqcTest::FqcTest() :
         writer((char *)json_response_buffer, sizeof(json_response_buffer)),
         tcpClient(),
-        inited_(true) {
+        inited_(true) { // TODO: Other member vars
     memset(json_response_buffer, 0x00, sizeof(json_response_buffer));
 }
 
@@ -93,6 +93,8 @@ bool FqcTest::process(JSONValue test){
         return wifiNetcat(test);
     } else if (has(test, "WIFI_SCAN_NETWORKS")) { 
         return wifiScanNetworks(test);
+    } else if (has(test, "GNSS_TEST")) { 
+        return gnssTest(test);
     } else if (has(test, "BURNIN_TEST")) { 
         BurninTest::instance()->setup(true);
         return true;
@@ -101,9 +103,15 @@ bool FqcTest::process(JSONValue test){
     return false;
 }
 
-bool FqcTest::passResponse(bool success) {
+bool FqcTest::passResponse(bool success, String message, int errorCode) {
     writer.beginObject();
     writer.name("pass").value(success);
+    if (message.length()) {
+        writer.name("message").value(message);
+    }
+    if (errorCode) {
+        writer.name("errorCode").value(errorCode);
+    }
     writer.endObject();
     return true;
 }
@@ -507,6 +515,169 @@ bool FqcTest::wifiScanNetworks(JSONValue req) {
     return true;
 }
 
+#if PLATFORM_ID == PLATFORM_MSOM
+static int callbackGPSGSV(int type, const char* buf, int len, FqcTest* self) {
+    // EXAMPLE:
+    // $<TalkerID>GSV,<TotalNumSen>,<SenNum>,<TotalNumSat>
+    //  {,<SatID>,<SatElev>,<SatAz>,<SatCN0>},
+    // <SignalID>*<Checksum><CR><LF>
+
+    // Single sat:
+    // > AT+QGPSGNMEA="GSV"
+    // < +QGPSGNMEA: $GPGSV,1,1,01,30,,,32,1*67
+    // < OK
+    
+    // Multiples:
+    // > AT+QGPSGNMEA="GSV"
+    // < +QGPSGNMEA: $GPGSV,3,1,11,30,46,293,32,04,23,134,00,05,11,319,00,07,68,346,00,1*6B
+    // < +QGPSGNMEA: $GPGSV,3,2,11,08,37,096,00,09,61,141,00,14,33,221,00,16,01,042,00,1*68
+    // < +QGPSGNMEA: $GPGSV,3,3,11,20,23,285,00,22,13,218,00,27,21,053,00,1*51
+    // < +QGPSGNMEA: $GLGSV,3,1,09,78,59,010,00,86,,,00,77,08,046,00,80,01,239,00,1*44
+    // < +QGPSGNMEA: $GLGSV,3,2,09,79,48,263,00,69,44,322,00,88,02,103,00,87,08,055,00,1*7E
+    // < +QGPSGNMEA: $GLGSV,3,3,09,67,24,162,00,1*43
+    // < OK
+
+    // GL = Glonass
+    // GP = GPS
+    // PQ = BeiDou
+    // GA = Galileo
+    
+    //Log.trace("%d : %s", strlen(buf), buf);
+
+    // If this is the trailing OK response, exit
+    if(!strcmp(buf, "\r\nOK\r\n")) {
+        return 0;
+    }
+
+    const int MAX_GSV_STR_LEN = 128;
+    char gsvSentence[MAX_GSV_STR_LEN] = {};
+    strlcpy(gsvSentence, buf, MAX_GSV_STR_LEN);
+    
+    const char * delimiters = ", ";
+    char * token = strtok(gsvSentence, delimiters);
+    int i = 1;
+    while (token) {
+        //Log.trace("%d %s", i, token);
+        token = strtok(NULL, delimiters);
+        i++;
+
+        switch (i) {
+            case 5: // TotalNumSat 
+            {   
+                int numberSattelites = atoi(token);
+                //Log.info("numberSattelites %d", numberSattelites);
+                if (numberSattelites > 0) {
+                    self->gnssSatelliteCount_ = numberSattelites;    
+                }
+                break;
+            }
+            // TODO: parse/store SatCN0
+            default:
+                break;
+        }
+    }
+
+    // Ask for more GSV lines from the AT parser
+    return WAIT;
+}
+
+void FqcTest::gnssLoop(void* arg) {
+    FqcTest* self = static_cast<FqcTest*>(arg);
+
+    hal_device_hw_info deviceInfo = {};
+    hal_get_device_hw_info(&deviceInfo, nullptr);
+    bool isBG95 = deviceInfo.ncp[0] == PLATFORM_NCP_QUECTEL_BG95_M5;
+
+    while(true)
+    {
+        if(self->gnssEnableSearch_) {
+            auto timeout = millis() + self->gnssPollTimeoutMs_;
+
+            if (isBG95) {
+                Cellular.command("AT+QGPSCFG=\"priority\",0");
+            }
+            
+            while (millis() < timeout && !self->gnssSatelliteCount_) {
+                Cellular.command(callbackGPSGSV, self, 1000, "AT+QGPSGNMEA=\"GSV\"");
+                Log.info("count %lu", self->gnssSatelliteCount_);
+                delay(1000);
+            }
+            self->gnssEnableSearch_ = false;
+
+            if (isBG95) {
+                Cellular.command("AT+QGPSCFG=\"priority\",1");
+            }
+        }
+        delay(1000);
+    }
+}
+
+bool FqcTest::gnssTest(JSONValue req) {
+
+   auto parameters = get(req, "GNSS_TEST");
+
+    if (parameters.isValid()) {
+        auto command = String(getValue(parameters, "command").toString());
+        auto timeout = getValue(parameters, "timeout").toInt();
+
+        if (command != nullptr) {
+            Log.info("GNSS test command: %s", command.c_str());    
+        }
+
+        gnssPollTimeoutMs_ = timeout ? timeout * 1000 : GNSS_POLL_TIMEOUT_DEFAULT_MS;
+        Log.info("GNSS test timeout: %lu", gnssPollTimeoutMs_);
+
+        if (command == String("start")) {
+            if (!gnssThread_) {
+                gnssSatelliteCount_ = 0;
+                gnssEnableSearch_ = true;
+
+                BurninTest::instance()->initGnss();
+                gnssThread_ = new Thread("gnss-test", gnssLoop, this, OS_THREAD_PRIORITY_DEFAULT);
+                SPARK_ASSERT(gnssThread_);
+
+                passResponse(true);
+            } else if(gnssEnableSearch_) {
+                auto warning = "Already searching for signal";
+                Log.warn(warning);
+                passResponse(false, warning);
+            } else {
+                gnssSatelliteCount_ = 0;
+                gnssEnableSearch_ = true;
+                passResponse(true);
+            }
+        } else if (command == String("status")) {
+
+            Log.info("gnssSatelliteCount_ %lu", gnssSatelliteCount_);
+            
+            if (!gnssThread_) {
+                Log.warn("Test not started");
+                passResponse(false, "Test not started");
+            }
+            else if (gnssEnableSearch_) {
+                Log.info("Pending");
+                passResponse(false, "Test in progress", SYSTEM_ERROR_BUSY);
+            } else if(gnssSatelliteCount_ == 0) {
+                Log.info("Timed out");
+                passResponse(false, "No satellite signal detected before timeout", SYSTEM_ERROR_TIMEOUT);
+            } else {
+                Log.info("Success");
+                String successMessage = String("Found ") + gnssSatelliteCount_ + String(" satellites");
+                passResponse(true, successMessage);
+            }
+        } else {
+            passResponse(false, String("Unrecognized Command: ") + command);
+        }
+    }
+
+    return true;
+}
+#else
+bool FqcTest::gnssTest(JSONValue req) {
+         return passResponse(false, "Platform does not support gnss test");
+}
+#endif // PLATFORM_ID == PLATFORM_MSOM
+
 }
 #endif // HAL_PLATFORM_RTL872X
 
diff --git a/user/applications/tinker/src/fqc_test.h b/user/applications/tinker/src/fqc_test.h
index 0862934d52..0df4a81c1a 100644
--- a/user/applications/tinker/src/fqc_test.h
+++ b/user/applications/tinker/src/fqc_test.h
@@ -21,6 +21,8 @@ class FqcTest {
     char * reply();
     size_t replySize();
 
+    uint32_t gnssSatelliteCount_ = 0;
+
 private:
     void initWriter();
 
@@ -30,10 +32,19 @@ class FqcTest {
     char json_response_buffer[2048];
     TCPClient tcpClient;
     bool inited_;
+
+    Thread* gnssThread_;
+    static const uint32_t GNSS_POLL_TIMEOUT_DEFAULT_MS = 30000;
+    uint32_t gnssPollTimeoutMs_;
+    std::atomic_bool gnssEnableSearch_;
+
+    // TODO: Vector for each type of sattelite?
     
-    bool passResponse(bool success);
+    bool passResponse(bool success, String message = String(), int errorCode = 0);
     bool tcpErrorResponse(int tcpError);
 
+    static void gnssLoop(void* arg);
+
     void parseIpAndPort(JSONValue parameters);
     int sendTCPMessage(const char * tx_data, char * rx_data_buffer, int rx_data_buffer_length, int response_poll_ms = 5000);
     
@@ -43,6 +54,7 @@ class FqcTest {
     bool ioTest(JSONValue req);
     bool wifiNetcat(JSONValue req);
     bool wifiScanNetworks(JSONValue req);
+    bool gnssTest(JSONValue req);
 };
 
 } // namespace particle

From 1eed3cd0974af5e6149c0b0a8942e2d011d253da Mon Sep 17 00:00:00 2001
From: Scott Brust <scott.brust@particle.io>
Date: Thu, 28 Dec 2023 14:59:15 -0800
Subject: [PATCH 2/5] fixup / rename / Ensure gps engine enabled for bg95

---
 user/applications/tinker/src/fqc_test.cpp | 106 +++++++++++-----------
 user/applications/tinker/src/fqc_test.h   |  15 ++-
 2 files changed, 61 insertions(+), 60 deletions(-)

diff --git a/user/applications/tinker/src/fqc_test.cpp b/user/applications/tinker/src/fqc_test.cpp
index 9358afe11e..cdc73a5fb8 100644
--- a/user/applications/tinker/src/fqc_test.cpp
+++ b/user/applications/tinker/src/fqc_test.cpp
@@ -51,10 +51,11 @@ namespace {
 }
 
 FqcTest::FqcTest() :
-        writer((char *)json_response_buffer, sizeof(json_response_buffer)),
-        tcpClient(),
-        inited_(true) { // TODO: Other member vars
-    memset(json_response_buffer, 0x00, sizeof(json_response_buffer));
+        writer_((char *)json_response_buffer_, sizeof(json_response_buffer_)),
+        tcpClient_(),
+        inited_(true),
+        gnssEnableSearch_(false) {
+    memset(json_response_buffer_, 0x00, sizeof(json_response_buffer_));
 }
 
 FqcTest::~FqcTest() {
@@ -66,16 +67,16 @@ FqcTest* FqcTest::instance() {
 }
 
 char * FqcTest::reply() {
-    return writer.buffer();
+    return writer_.buffer();
 }
 
 size_t FqcTest::replySize() {
-    return writer.dataSize();
+    return writer_.dataSize();
 }
 
 void FqcTest::initWriter(){
-    memset(json_response_buffer, 0x00, sizeof(json_response_buffer));
-    writer = JSONBufferWriter((char *)json_response_buffer, sizeof(json_response_buffer));
+    memset(json_response_buffer_, 0x00, sizeof(json_response_buffer_));
+    writer_ = JSONBufferWriter((char *)json_response_buffer_, sizeof(json_response_buffer_));
 }
 
 bool FqcTest::process(JSONValue test){
@@ -104,22 +105,22 @@ bool FqcTest::process(JSONValue test){
 }
 
 bool FqcTest::passResponse(bool success, String message, int errorCode) {
-    writer.beginObject();
-    writer.name("pass").value(success);
+    writer_.beginObject();
+    writer_.name("pass").value(success);
     if (message.length()) {
-        writer.name("message").value(message);
+        writer_.name("message").value(message);
     }
     if (errorCode) {
-        writer.name("errorCode").value(errorCode);
+        writer_.name("errorCode").value(errorCode);
     }
-    writer.endObject();
+    writer_.endObject();
     return true;
 }
 
 bool FqcTest::tcpErrorResponse(int tcpError) {
-    writer.beginObject();
-    writer.name("pass").value(false);
-    writer.name("errorCode").value(tcpError);
+    writer_.beginObject();
+    writer_.name("pass").value(false);
+    writer_.name("errorCode").value(tcpError);
     String errorMessage;
     if(tcpError == SYSTEM_ERROR_NETWORK){
         errorMessage = "Wifi not ready";
@@ -137,8 +138,8 @@ bool FqcTest::tcpErrorResponse(int tcpError) {
         errorMessage = "Netcat command parameters malformed";
     }
 
-    writer.name("message").value(errorMessage);
-    writer.endObject();
+    writer_.name("message").value(errorMessage);
+    writer_.endObject();
     return true;
 }
 
@@ -157,8 +158,8 @@ void FqcTest::parseIpAndPort(JSONValue parameters) {
     }
 
     // Store FQC station port/ip
-    this->tcpPort = port;
-    memcpy(this->tcpServer, server, sizeof(server));
+    this->tcpPort_ = port;
+    memcpy(this->tcpServer_, server, sizeof(server));
 }
 
 int FqcTest::sendTCPMessage(const char * tx_data, char * rx_data_buffer, int rx_data_buffer_length, int response_poll_ms) {
@@ -169,9 +170,9 @@ int FqcTest::sendTCPMessage(const char * tx_data, char * rx_data_buffer, int rx_
         return SYSTEM_ERROR_NETWORK;
     }
     
-    if (!tcpClient.connected()) {
-        if (tcpClient.connect(this->tcpServer, this->tcpPort)) {
-            Log.info("TCP client connected to %s", tcpClient.remoteIP().toString().c_str());
+    if (!tcpClient_.connected()) {
+        if (tcpClient_.connect(this->tcpServer_, this->tcpPort_)) {
+            Log.info("TCP client connected to %s", tcpClient_.remoteIP().toString().c_str());
         }
         else {
             Log.error("TCP client connection failed");
@@ -179,18 +180,18 @@ int FqcTest::sendTCPMessage(const char * tx_data, char * rx_data_buffer, int rx_
         }
     }
 
-    if (tcpClient.connected()) {
+    if (tcpClient_.connected()) {
         Log.info("Sending message: %s len %d", tx_data, strlen(tx_data));
-        tcpClient.write((uint8_t *)tx_data, strlen(tx_data));
+        tcpClient_.write((uint8_t *)tx_data, strlen(tx_data));
 
         // Poll for response
         int delay_period_ms = 500;
-        for(int i = 0; i < (response_poll_ms / delay_period_ms) && !tcpClient.available(); i++){
+        for(int i = 0; i < (response_poll_ms / delay_period_ms) && !tcpClient_.available(); i++){
             delay(delay_period_ms);
         }
         
-        while(tcpClient.available() && (bytesRead < rx_data_buffer_length)) {
-           rx_data_buffer[bytesRead++] = tcpClient.read();
+        while(tcpClient_.available() && (bytesRead < rx_data_buffer_length)) {
+           rx_data_buffer[bytesRead++] = tcpClient_.read();
         }
 
         if(bytesRead) {
@@ -203,7 +204,7 @@ int FqcTest::sendTCPMessage(const char * tx_data, char * rx_data_buffer, int rx_
             return SYSTEM_ERROR_TIMEOUT;
         }
 
-        tcpClient.stop();
+        tcpClient_.stop();
     }
 
     return bytesRead;
@@ -356,10 +357,10 @@ bool FqcTest::ioTest(JSONValue req) {
 
     if (result != SYSTEM_ERROR_NONE) {
         Log.error("Could not read logical efuse");
-        writer.beginObject();
-        writer.name("pass").value(false);
-        writer.name("message").value("could not read logical efuse");
-        writer.endObject();
+        writer_.beginObject();
+        writer_.name("pass").value(false);
+        writer_.name("message").value("could not read logical efuse");
+        writer_.endObject();
         return true;
     } else {
         Log.info("Hardware Model: 0x%X Variant: %lu", (unsigned int)model, variant);    
@@ -410,13 +411,13 @@ bool FqcTest::ioTest(JSONValue req) {
         passResponse(true);
     }
     else {
-        writer.beginObject();
-        writer.name("pass").value(false);
-        writer.name("pinA").value(pinNumberToPinName(pinA));
-        writer.name("pinB").value(pinNumberToPinName(pinB));
-        writer.name("errorPin").value(pinNumberToPinName(errorPin));
-        writer.name("message").value(failedTest);
-        writer.endObject();
+        writer_.beginObject();
+        writer_.name("pass").value(false);
+        writer_.name("pinA").value(pinNumberToPinName(pinA));
+        writer_.name("pinB").value(pinNumberToPinName(pinB));
+        writer_.name("errorPin").value(pinNumberToPinName(errorPin));
+        writer_.name("message").value(failedTest);
+        writer_.endObject();
     }
 
     return true;
@@ -494,24 +495,24 @@ bool FqcTest::wifiScanNetworks(JSONValue req) {
     }
 
     // Serialize to JSON and return this list over USB, like the regular WIFI scan does
-    writer.beginObject();
-    writer.name("pass").value(true);
-    writer.name("networks").beginArray();
+    writer_.beginObject();
+    writer_.name("pass").value(true);
+    writer_.name("networks").beginArray();
     for(WiFiAccessPoint& network: networks){
-        writer.beginObject();
-        writer.name("ssid").value(String(network.ssid, network.ssidLength));
+        writer_.beginObject();
+        writer_.name("ssid").value(String(network.ssid, network.ssidLength));
 
         char bssidStr[32] = {};
         sprintf(bssidStr, "%02X:%02X:%02X:%02X:%02X:%02X", network.bssid[0], network.bssid[1], network.bssid[2], network.bssid[3], network.bssid[4], network.bssid[5]);
-        writer.name("bssid").value(String(bssidStr));
+        writer_.name("bssid").value(String(bssidStr));
 
-        writer.name("security").value((int)network.security);
-        writer.name("channel").value(network.channel);
-        writer.name("rssi").value(network.rssi);
-        writer.endObject();
+        writer_.name("security").value((int)network.security);
+        writer_.name("channel").value(network.channel);
+        writer_.name("rssi").value(network.rssi);
+        writer_.endObject();
     }
-    writer.endArray();
-    writer.endObject();
+    writer_.endArray();
+    writer_.endObject();
     return true;
 }
 
@@ -594,6 +595,7 @@ void FqcTest::gnssLoop(void* arg) {
             auto timeout = millis() + self->gnssPollTimeoutMs_;
 
             if (isBG95) {
+                Cellular.command("AT+QGPS=1");
                 Cellular.command("AT+QGPSCFG=\"priority\",0");
             }
             
diff --git a/user/applications/tinker/src/fqc_test.h b/user/applications/tinker/src/fqc_test.h
index 0df4a81c1a..7833e1f2ec 100644
--- a/user/applications/tinker/src/fqc_test.h
+++ b/user/applications/tinker/src/fqc_test.h
@@ -24,21 +24,20 @@ class FqcTest {
     uint32_t gnssSatelliteCount_ = 0;
 
 private:
+    static const uint32_t GNSS_POLL_TIMEOUT_DEFAULT_MS = 30000;
+
     void initWriter();
 
-    uint8_t tcpServer[4];
-    int tcpPort;
-    JSONBufferWriter writer; 
-    char json_response_buffer[2048];
-    TCPClient tcpClient;
+    uint8_t tcpServer_[4];
+    int tcpPort_;
+    JSONBufferWriter writer_; 
+    char json_response_buffer_[2048];
+    TCPClient tcpClient_;
     bool inited_;
 
     Thread* gnssThread_;
-    static const uint32_t GNSS_POLL_TIMEOUT_DEFAULT_MS = 30000;
     uint32_t gnssPollTimeoutMs_;
     std::atomic_bool gnssEnableSearch_;
-
-    // TODO: Vector for each type of sattelite?
     
     bool passResponse(bool success, String message = String(), int errorCode = 0);
     bool tcpErrorResponse(int tcpError);

From 8c3e4eb794a305b60e26255abcef1d3f887104ef Mon Sep 17 00:00:00 2001
From: Scott Brust <scott.brust@particle.io>
Date: Thu, 4 Jan 2024 11:12:18 -0800
Subject: [PATCH 3/5] fixup spelling

---
 user/applications/tinker/src/burnin_test.cpp | 8 ++++----
 user/applications/tinker/src/fqc_test.cpp    | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/user/applications/tinker/src/burnin_test.cpp b/user/applications/tinker/src/burnin_test.cpp
index 013f5bfaca..d5500e917b 100644
--- a/user/applications/tinker/src/burnin_test.cpp
+++ b/user/applications/tinker/src/burnin_test.cpp
@@ -519,7 +519,7 @@ static int callbackGPSGGA(int type, const char* buf, int len, bool* gnssLocked)
     strlcpy(gpggaSentence, buf, MAX_GPGGA_STR_LEN);
 
     String lattitudeLongitude("LAT/LONG:");
-    int numberSattelites = 0;
+    int numberSatellites = 0;
     
     const char * delimiters = ",";
     char * token = strtok(gpggaSentence, delimiters);
@@ -543,10 +543,10 @@ static int callbackGPSGGA(int type, const char* buf, int len, bool* gnssLocked)
                 lattitudeLongitude.concat(token);
                 break;
             case 8: // Number satellites
-                numberSattelites = (int)String(token).toInt();
-                if (numberSattelites > 0) {
+                numberSatellites = (int)String(token).toInt();
+                if (numberSatellites > 0) {
                     *gnssLocked = true;    
-                    Log.info("%s Satellites: %d", lattitudeLongitude.c_str(), numberSattelites);
+                    Log.info("%s Satellites: %d", lattitudeLongitude.c_str(), numberSatellites);
                 }
                 break;
             default:
diff --git a/user/applications/tinker/src/fqc_test.cpp b/user/applications/tinker/src/fqc_test.cpp
index cdc73a5fb8..8023bdc27f 100644
--- a/user/applications/tinker/src/fqc_test.cpp
+++ b/user/applications/tinker/src/fqc_test.cpp
@@ -565,10 +565,10 @@ static int callbackGPSGSV(int type, const char* buf, int len, FqcTest* self) {
         switch (i) {
             case 5: // TotalNumSat 
             {   
-                int numberSattelites = atoi(token);
-                //Log.info("numberSattelites %d", numberSattelites);
-                if (numberSattelites > 0) {
-                    self->gnssSatelliteCount_ = numberSattelites;    
+                int numberSatellites = atoi(token);
+                //Log.info("numberSatellites %d", numberSatellites);
+                if (numberSatellites > 0) {
+                    self->gnssSatelliteCount_ = numberSatellites;    
                 }
                 break;
             }

From bc6b2e14b9f4499016cdeba0f48855a7a4d39b16 Mon Sep 17 00:00:00 2001
From: Scott Brust <scott.brust@particle.io>
Date: Fri, 5 Jan 2024 16:12:04 -0800
Subject: [PATCH 4/5] return full raw GSV sentences, simplify parsing

---
 user/applications/tinker/src/fqc_test.cpp | 57 ++++++++++-------------
 user/applications/tinker/src/fqc_test.h   |  1 +
 2 files changed, 25 insertions(+), 33 deletions(-)

diff --git a/user/applications/tinker/src/fqc_test.cpp b/user/applications/tinker/src/fqc_test.cpp
index 8023bdc27f..de528d5515 100644
--- a/user/applications/tinker/src/fqc_test.cpp
+++ b/user/applications/tinker/src/fqc_test.cpp
@@ -55,6 +55,7 @@ FqcTest::FqcTest() :
         tcpClient_(),
         inited_(true),
         gnssEnableSearch_(false) {
+    gnssGpsvStrings_ = String();
     memset(json_response_buffer_, 0x00, sizeof(json_response_buffer_));
 }
 
@@ -545,37 +546,19 @@ static int callbackGPSGSV(int type, const char* buf, int len, FqcTest* self) {
     
     //Log.trace("%d : %s", strlen(buf), buf);
 
-    // If this is the trailing OK response, exit
-    if(!strcmp(buf, "\r\nOK\r\n")) {
+    // If this is the trailing OK response, exit from the parser
+    if (String(buf) == String("\r\nOK\r\n")) {
         return 0;
     }
 
-    const int MAX_GSV_STR_LEN = 128;
-    char gsvSentence[MAX_GSV_STR_LEN] = {};
-    strlcpy(gsvSentence, buf, MAX_GSV_STR_LEN);
+    // Store the raw NMEA string
+    self->gnssGpsvStrings_.concat(buf);
     
-    const char * delimiters = ", ";
-    char * token = strtok(gsvSentence, delimiters);
-    int i = 1;
-    while (token) {
-        //Log.trace("%d %s", i, token);
-        token = strtok(NULL, delimiters);
-        i++;
-
-        switch (i) {
-            case 5: // TotalNumSat 
-            {   
-                int numberSatellites = atoi(token);
-                //Log.info("numberSatellites %d", numberSatellites);
-                if (numberSatellites > 0) {
-                    self->gnssSatelliteCount_ = numberSatellites;    
-                }
-                break;
-            }
-            // TODO: parse/store SatCN0
-            default:
-                break;
-        }
+    int numberSatellites = 0;
+    int ret = sscanf(buf,"\r\n+QGPSGNMEA: $%*5c,%*d,%*d,%d,", &numberSatellites);
+    Log.trace("ret = %d, numberSatellites: %d", ret, numberSatellites);
+    if (numberSatellites > 0) {
+        self->gnssSatelliteCount_ = numberSatellites;
     }
 
     // Ask for more GSV lines from the AT parser
@@ -588,21 +571,30 @@ void FqcTest::gnssLoop(void* arg) {
     hal_device_hw_info deviceInfo = {};
     hal_get_device_hw_info(&deviceInfo, nullptr);
     bool isBG95 = deviceInfo.ncp[0] == PLATFORM_NCP_QUECTEL_BG95_M5;
+    const uint32_t POLL_MS = 1000;
+    const uint32_t EXTRA_POLL_MS = 3000;
 
     while(true)
     {
         if(self->gnssEnableSearch_) {
             auto timeout = millis() + self->gnssPollTimeoutMs_;
+            bool extendSearch = true;
 
             if (isBG95) {
                 Cellular.command("AT+QGPS=1");
                 Cellular.command("AT+QGPSCFG=\"priority\",0");
             }
             
-            while (millis() < timeout && !self->gnssSatelliteCount_) {
+            while (millis() < timeout) {
+                self->gnssGpsvStrings_ = String();
                 Cellular.command(callbackGPSGSV, self, 1000, "AT+QGPSGNMEA=\"GSV\"");
-                Log.info("count %lu", self->gnssSatelliteCount_);
-                delay(1000);
+                delay(POLL_MS);
+
+                // Run a bit longer after finding a satellite to see if we detect more
+                if (self->gnssSatelliteCount_ && extendSearch) {
+                    timeout = millis() + EXTRA_POLL_MS;
+                    extendSearch = false;
+                }
             }
             self->gnssEnableSearch_ = false;
 
@@ -610,7 +602,7 @@ void FqcTest::gnssLoop(void* arg) {
                 Cellular.command("AT+QGPSCFG=\"priority\",1");
             }
         }
-        delay(1000);
+        delay(POLL_MS);
     }
 }
 
@@ -664,8 +656,7 @@ bool FqcTest::gnssTest(JSONValue req) {
                 passResponse(false, "No satellite signal detected before timeout", SYSTEM_ERROR_TIMEOUT);
             } else {
                 Log.info("Success");
-                String successMessage = String("Found ") + gnssSatelliteCount_ + String(" satellites");
-                passResponse(true, successMessage);
+                passResponse(true, gnssGpsvStrings_);
             }
         } else {
             passResponse(false, String("Unrecognized Command: ") + command);
diff --git a/user/applications/tinker/src/fqc_test.h b/user/applications/tinker/src/fqc_test.h
index 7833e1f2ec..138a33eeb9 100644
--- a/user/applications/tinker/src/fqc_test.h
+++ b/user/applications/tinker/src/fqc_test.h
@@ -22,6 +22,7 @@ class FqcTest {
     size_t replySize();
 
     uint32_t gnssSatelliteCount_ = 0;
+    String gnssGpsvStrings_;
 
 private:
     static const uint32_t GNSS_POLL_TIMEOUT_DEFAULT_MS = 30000;

From 76e9ca29dee4ec0878ecc26a8f1acdef984287aa Mon Sep 17 00:00:00 2001
From: Scott Brust <scott.brust@particle.io>
Date: Mon, 8 Jan 2024 15:54:30 -0800
Subject: [PATCH 5/5] Switch to GGA nmea output, require fix for fqc pass

---
 user/applications/tinker/src/fqc_test.cpp | 72 ++++++++---------------
 user/applications/tinker/src/fqc_test.h   |  5 +-
 2 files changed, 28 insertions(+), 49 deletions(-)

diff --git a/user/applications/tinker/src/fqc_test.cpp b/user/applications/tinker/src/fqc_test.cpp
index de528d5515..4c31223df2 100644
--- a/user/applications/tinker/src/fqc_test.cpp
+++ b/user/applications/tinker/src/fqc_test.cpp
@@ -55,7 +55,7 @@ FqcTest::FqcTest() :
         tcpClient_(),
         inited_(true),
         gnssEnableSearch_(false) {
-    gnssGpsvStrings_ = String();
+    gnssNmeaOutput_ = String();
     memset(json_response_buffer_, 0x00, sizeof(json_response_buffer_));
 }
 
@@ -519,31 +519,12 @@ bool FqcTest::wifiScanNetworks(JSONValue req) {
 
 #if PLATFORM_ID == PLATFORM_MSOM
 static int callbackGPSGSV(int type, const char* buf, int len, FqcTest* self) {
-    // EXAMPLE:
-    // $<TalkerID>GSV,<TotalNumSen>,<SenNum>,<TotalNumSat>
-    //  {,<SatID>,<SatElev>,<SatAz>,<SatCN0>},
-    // <SignalID>*<Checksum><CR><LF>
-
-    // Single sat:
-    // > AT+QGPSGNMEA="GSV"
-    // < +QGPSGNMEA: $GPGSV,1,1,01,30,,,32,1*67
-    // < OK
-    
-    // Multiples:
-    // > AT+QGPSGNMEA="GSV"
-    // < +QGPSGNMEA: $GPGSV,3,1,11,30,46,293,32,04,23,134,00,05,11,319,00,07,68,346,00,1*6B
-    // < +QGPSGNMEA: $GPGSV,3,2,11,08,37,096,00,09,61,141,00,14,33,221,00,16,01,042,00,1*68
-    // < +QGPSGNMEA: $GPGSV,3,3,11,20,23,285,00,22,13,218,00,27,21,053,00,1*51
-    // < +QGPSGNMEA: $GLGSV,3,1,09,78,59,010,00,86,,,00,77,08,046,00,80,01,239,00,1*44
-    // < +QGPSGNMEA: $GLGSV,3,2,09,79,48,263,00,69,44,322,00,88,02,103,00,87,08,055,00,1*7E
-    // < +QGPSGNMEA: $GLGSV,3,3,09,67,24,162,00,1*43
+    // > AT+QGPSGNMEA="GGA"
+    // < +QGPSGNMEA: $GPGGA,233906.00,3804.385678,N,12209.936243,W,1,05,1.4,149.0,M,-25.0,M,,*5C
     // < OK
 
-    // GL = Glonass
-    // GP = GPS
-    // PQ = BeiDou
-    // GA = Galileo
-    
+    // $<TalkerID>GGA,<UTC>,<Lat>,<N/S>,<Lon>,<E/W>,<Quality>,<NumSatUsed>,<HDOP>,<Alt>,M,<Sep>,M,<DiffAge>,<DiffStation>*<Checksum><CR><LF>
+
     //Log.trace("%d : %s", strlen(buf), buf);
 
     // If this is the trailing OK response, exit from the parser
@@ -552,13 +533,13 @@ static int callbackGPSGSV(int type, const char* buf, int len, FqcTest* self) {
     }
 
     // Store the raw NMEA string
-    self->gnssGpsvStrings_.concat(buf);
+    self->gnssNmeaOutput_.concat(buf);
     
-    int numberSatellites = 0;
-    int ret = sscanf(buf,"\r\n+QGPSGNMEA: $%*5c,%*d,%*d,%d,", &numberSatellites);
-    Log.trace("ret = %d, numberSatellites: %d", ret, numberSatellites);
-    if (numberSatellites > 0) {
-        self->gnssSatelliteCount_ = numberSatellites;
+    int fixQuality = 0;
+    int ret = sscanf(buf,"\r\n+QGPSGNMEA: $%*5c,%*d.%*d,%*d.%*d,%*c,%*d.%*d,%*c,%d,", &fixQuality);
+    Log.trace("ret = %d, fixQuality: %d", ret, fixQuality);
+    if (fixQuality > 0) {
+        self->gnssFixQuality_ = fixQuality;
     }
 
     // Ask for more GSV lines from the AT parser
@@ -572,28 +553,25 @@ void FqcTest::gnssLoop(void* arg) {
     hal_get_device_hw_info(&deviceInfo, nullptr);
     bool isBG95 = deviceInfo.ncp[0] == PLATFORM_NCP_QUECTEL_BG95_M5;
     const uint32_t POLL_MS = 1000;
-    const uint32_t EXTRA_POLL_MS = 3000;
 
     while(true)
     {
         if(self->gnssEnableSearch_) {
-            auto timeout = millis() + self->gnssPollTimeoutMs_;
-            bool extendSearch = true;
+            auto startMillis = millis();
+            auto timeout = startMillis + self->gnssPollTimeoutMs_;
 
             if (isBG95) {
                 Cellular.command("AT+QGPS=1");
                 Cellular.command("AT+QGPSCFG=\"priority\",0");
             }
             
-            while (millis() < timeout) {
-                self->gnssGpsvStrings_ = String();
-                Cellular.command(callbackGPSGSV, self, 1000, "AT+QGPSGNMEA=\"GSV\"");
+            while (millis() < timeout && !self->gnssFixQuality_ ) {
+                self->gnssNmeaOutput_ = String();
+                Cellular.command(callbackGPSGSV, self, 1000, "AT+QGPSGNMEA=\"GGA\"");
                 delay(POLL_MS);
 
-                // Run a bit longer after finding a satellite to see if we detect more
-                if (self->gnssSatelliteCount_ && extendSearch) {
-                    timeout = millis() + EXTRA_POLL_MS;
-                    extendSearch = false;
+                if (self->gnssFixQuality_) {
+                    self->gnssTimeToFix_ = millis() - startMillis;
                 }
             }
             self->gnssEnableSearch_ = false;
@@ -623,7 +601,7 @@ bool FqcTest::gnssTest(JSONValue req) {
 
         if (command == String("start")) {
             if (!gnssThread_) {
-                gnssSatelliteCount_ = 0;
+                gnssFixQuality_ = 0;
                 gnssEnableSearch_ = true;
 
                 BurninTest::instance()->initGnss();
@@ -636,13 +614,13 @@ bool FqcTest::gnssTest(JSONValue req) {
                 Log.warn(warning);
                 passResponse(false, warning);
             } else {
-                gnssSatelliteCount_ = 0;
+                gnssFixQuality_ = 0;
                 gnssEnableSearch_ = true;
                 passResponse(true);
             }
         } else if (command == String("status")) {
 
-            Log.info("gnssSatelliteCount_ %lu", gnssSatelliteCount_);
+            Log.info("gnssFixQuality_ %lu", gnssFixQuality_);
             
             if (!gnssThread_) {
                 Log.warn("Test not started");
@@ -651,12 +629,12 @@ bool FqcTest::gnssTest(JSONValue req) {
             else if (gnssEnableSearch_) {
                 Log.info("Pending");
                 passResponse(false, "Test in progress", SYSTEM_ERROR_BUSY);
-            } else if(gnssSatelliteCount_ == 0) {
+            } else if(gnssFixQuality_ == 0) {
                 Log.info("Timed out");
-                passResponse(false, "No satellite signal detected before timeout", SYSTEM_ERROR_TIMEOUT);
+                passResponse(false, "No fix detected before timeout", SYSTEM_ERROR_TIMEOUT);
             } else {
-                Log.info("Success");
-                passResponse(true, gnssGpsvStrings_);
+                Log.info("Success, time to fix: %lu", gnssTimeToFix_);
+                passResponse(true, gnssNmeaOutput_);
             }
         } else {
             passResponse(false, String("Unrecognized Command: ") + command);
diff --git a/user/applications/tinker/src/fqc_test.h b/user/applications/tinker/src/fqc_test.h
index 138a33eeb9..d88b917f7d 100644
--- a/user/applications/tinker/src/fqc_test.h
+++ b/user/applications/tinker/src/fqc_test.h
@@ -21,8 +21,8 @@ class FqcTest {
     char * reply();
     size_t replySize();
 
-    uint32_t gnssSatelliteCount_ = 0;
-    String gnssGpsvStrings_;
+    uint32_t gnssFixQuality_ = 0;
+    String gnssNmeaOutput_;
 
 private:
     static const uint32_t GNSS_POLL_TIMEOUT_DEFAULT_MS = 30000;
@@ -39,6 +39,7 @@ class FqcTest {
     Thread* gnssThread_;
     uint32_t gnssPollTimeoutMs_;
     std::atomic_bool gnssEnableSearch_;
+    uint32_t gnssTimeToFix_;
     
     bool passResponse(bool success, String message = String(), int errorCode = 0);
     bool tcpErrorResponse(int tcpError);