From 5abc9b9ee536c57ee26edd532252e166bb25eabb Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Thu, 26 Mar 2026 00:22:57 +0500 Subject: [PATCH 1/5] Fix stale operator list never cleared across registration attempts Promote operatorListExhaustedCount from local variable to class member (registrationFailCount_) so it persists across multiple calls to startNetworkRegistration(). The counter is loaded/saved via setOperators() to allow the application to persist it across power cycles. When the threshold (3) is reached, the operator list is cleared, forcing a fresh operator scan on the next registration attempt. --- src/cellularModule.cpp | 5 +++- src/cellularModule.h | 4 ++- src/cellularModuleA7672xx.cpp | 48 ++++++++++++++++++++++++++--------- src/cellularModuleA7672xx.h | 7 ++++- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/cellularModule.cpp b/src/cellularModule.cpp index 19e9584..1d2cc70 100644 --- a/src/cellularModule.cpp +++ b/src/cellularModule.cpp @@ -35,7 +35,8 @@ CellResult CellularModule::resolveDNS(const std::string &hostname) return CellResult(); } -bool CellularModule::setOperators(const std::string &serialized, uint32_t operatorId) { +bool CellularModule::setOperators(const std::string &serialized, uint32_t operatorId, + uint32_t registrationFailCount) { return false; } @@ -43,6 +44,8 @@ std::string CellularModule::getSerializedOperators() const { return std::string( uint32_t CellularModule::getCurrentOperatorId() const { return 0; } +uint32_t CellularModule::getRegistrationFailCount() const { return 0; } + CellReturnStatus CellularModule::isNetworkRegistered(CellTechnology ct) { return CellReturnStatus(); } diff --git a/src/cellularModule.h b/src/cellularModule.h index 6f9d990..b960f06 100644 --- a/src/cellularModule.h +++ b/src/cellularModule.h @@ -57,9 +57,11 @@ class CellularModule { virtual CellResult retrieveSignal(); virtual CellResult retrieveIPAddr(); virtual CellResult resolveDNS(const std::string &hostname); - virtual bool setOperators(const std::string &serialized, uint32_t operatorId); + virtual bool setOperators(const std::string &serialized, uint32_t operatorId, + uint32_t registrationFailCount = 0); virtual std::string getSerializedOperators() const; virtual uint32_t getCurrentOperatorId() const; + virtual uint32_t getRegistrationFailCount() const; virtual CellReturnStatus isNetworkRegistered(CellTechnology ct); virtual CellResult startNetworkRegistration(CellTechnology ct, const std::string &apn, diff --git a/src/cellularModuleA7672xx.cpp b/src/cellularModuleA7672xx.cpp index cf8021f..122cd68 100644 --- a/src/cellularModuleA7672xx.cpp +++ b/src/cellularModuleA7672xx.cpp @@ -286,9 +286,6 @@ CellularModuleA7672XX::startNetworkRegistration(CellTechnology ct, const std::st uint32_t manualOperatorStartTime = 0; // Track time per operator in manual mode (60 sec timeout) uint32_t serviceStatusStartTime = 0; // Track time in CHECK_SERVICE_STATUS (30 sec timeout) - // Track operator list exhaustion (full iterations through all operators) - uint32_t operatorListExhaustedCount = 0; - const uint32_t MAX_OPERATOR_LIST_EXHAUSTION = 3; const uint32_t SERVICE_STATUS_TIMEOUT = 30000; // 30 seconds NetworkRegistrationState state = CHECK_MODULE_READY; @@ -326,18 +323,19 @@ CellularModuleA7672XX::startNetworkRegistration(CellTechnology ct, const std::st case OPERATOR_LIST_EXHAUSTED: { // All operators exhausted, increment exhaustion counter - operatorListExhaustedCount++; + registrationFailCount_++; AG_LOGW(TAG, "Operator list exhausted (attempt %" PRIu32 " of %" PRIu32 ")", - operatorListExhaustedCount, MAX_OPERATOR_LIST_EXHAUSTION); + registrationFailCount_, MAX_REGISTRATION_FAILURES); - if (operatorListExhaustedCount >= MAX_OPERATOR_LIST_EXHAUSTION) { + if (registrationFailCount_ >= MAX_REGISTRATION_FAILURES) { // Reached maximum exhaustion attempts, fail registration AG_LOGE(TAG, "Failed after %" PRIu32 " full iterations through operator list", - MAX_OPERATOR_LIST_EXHAUSTION); - // Clear operator list and saved operator + registrationFailCount_); + // Clear operator list and saved operator, reset fail counter availableOperators_.clear(); currentOperatorId_ = 0; currentOperatorIndex_ = 0; + registrationFailCount_ = 0; finish = true; continue; } @@ -397,10 +395,29 @@ CellularModuleA7672XX::startNetworkRegistration(CellTechnology ct, const std::st } if (state != NETWORK_READY) { - AG_LOGW(TAG, "Network registration failed! Final state: %d", state); + // Increment fail counter for operation-timeout exits that didn't go through OPERATOR_LIST_EXHAUSTED + if (!availableOperators_.empty()) { + registrationFailCount_++; + AG_LOGW(TAG, "Network registration failed! Final state: %d (fail count: %" PRIu32 " of %" PRIu32 ")", + state, registrationFailCount_, MAX_REGISTRATION_FAILURES); + + if (registrationFailCount_ >= MAX_REGISTRATION_FAILURES) { + AG_LOGW(TAG, "Clearing stale operator list after %" PRIu32 " consecutive failures", + registrationFailCount_); + availableOperators_.clear(); + currentOperatorId_ = 0; + currentOperatorIndex_ = 0; + registrationFailCount_ = 0; + } + } else { + AG_LOGW(TAG, "Network registration failed! Final state: %d", state); + } return result; } + // Registration succeeded, reset fail counter + registrationFailCount_ = 0; + AG_LOGI(TAG, "Warming up for %" PRIu32 "ms...", _warmUpTimeMs); DELAY_MS(_warmUpTimeMs); @@ -2182,9 +2199,12 @@ int CellularModuleA7672XX::_calculateResponseTimeout(int connectionTimeout, int return waitActionTimeout; } -bool CellularModuleA7672XX::setOperators(const std::string &serialized, uint32_t operatorId) { - AG_LOGI(TAG, "Setting operators from serialized string: %s, current operatorId: %" PRIu32, - serialized.c_str(), operatorId); +bool CellularModuleA7672XX::setOperators(const std::string &serialized, uint32_t operatorId, + uint32_t registrationFailCount) { + AG_LOGI(TAG, "Setting operators from serialized string: %s, current operatorId: %" PRIu32 + ", failCount: %" PRIu32, + serialized.c_str(), operatorId, registrationFailCount); + registrationFailCount_ = registrationFailCount; // Clear existing operators availableOperators_.clear(); @@ -2283,4 +2303,8 @@ uint32_t CellularModuleA7672XX::getCurrentOperatorId() const { return currentOperatorId_; } +uint32_t CellularModuleA7672XX::getRegistrationFailCount() const { + return registrationFailCount_; +} + #endif // ESP8266 diff --git a/src/cellularModuleA7672xx.h b/src/cellularModuleA7672xx.h index 88653e2..1a555f9 100644 --- a/src/cellularModuleA7672xx.h +++ b/src/cellularModuleA7672xx.h @@ -51,10 +51,13 @@ class CellularModuleA7672XX : public CellularModule { int accessTech; // Access technology: 0=GSM, 2=UTRAN, 7=E-UTRAN(LTE) }; + static constexpr uint32_t MAX_REGISTRATION_FAILURES = 3; + // Operator selection for manual network registration std::vector availableOperators_; // Persisted operator list with IDs and access tech size_t currentOperatorIndex_ = 0; // Track position in manual mode uint32_t currentOperatorId_ = 0; // Current operator PLMN ID (saved successful operator) + uint32_t registrationFailCount_ = 0; // Consecutive registration failures (persisted via setOperators) public: // Structure to hold detailed registration status @@ -121,9 +124,11 @@ class CellularModuleA7672XX : public CellularModule { CellResult udpReceive(uint32_t timeout); CellResult resolveDNS(const std::string &hostname); // Operator serialization/deserialization - bool setOperators(const std::string &serialized, uint32_t operatorId); + bool setOperators(const std::string &serialized, uint32_t operatorId, + uint32_t registrationFailCount = 0); std::string getSerializedOperators() const; uint32_t getCurrentOperatorId() const; + uint32_t getRegistrationFailCount() const; private: const int DEFAULT_HTTP_CONNECT_TIMEOUT = 120; // seconds From f36848f11f73077506556334974bffd18d8a489c Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Thu, 26 Mar 2026 00:28:22 +0500 Subject: [PATCH 2/5] Remove double-counting of fail counter on operation-timeout exit Only increment registrationFailCount_ via the OPERATOR_LIST_EXHAUSTED handler (full pass through all operators). The operation-timeout exit path preserves whatever count was accumulated during the call. --- src/cellularModuleA7672xx.cpp | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/cellularModuleA7672xx.cpp b/src/cellularModuleA7672xx.cpp index 122cd68..f48e11c 100644 --- a/src/cellularModuleA7672xx.cpp +++ b/src/cellularModuleA7672xx.cpp @@ -395,23 +395,8 @@ CellularModuleA7672XX::startNetworkRegistration(CellTechnology ct, const std::st } if (state != NETWORK_READY) { - // Increment fail counter for operation-timeout exits that didn't go through OPERATOR_LIST_EXHAUSTED - if (!availableOperators_.empty()) { - registrationFailCount_++; - AG_LOGW(TAG, "Network registration failed! Final state: %d (fail count: %" PRIu32 " of %" PRIu32 ")", - state, registrationFailCount_, MAX_REGISTRATION_FAILURES); - - if (registrationFailCount_ >= MAX_REGISTRATION_FAILURES) { - AG_LOGW(TAG, "Clearing stale operator list after %" PRIu32 " consecutive failures", - registrationFailCount_); - availableOperators_.clear(); - currentOperatorId_ = 0; - currentOperatorIndex_ = 0; - registrationFailCount_ = 0; - } - } else { - AG_LOGW(TAG, "Network registration failed! Final state: %d", state); - } + AG_LOGW(TAG, "Network registration failed! Final state: %d (fail count: %" PRIu32 " of %" PRIu32 ")", + state, registrationFailCount_, MAX_REGISTRATION_FAILURES); return result; } From b516c62c5fa13372323f09ca5cb3e3d06c2a2e54 Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Fri, 27 Mar 2026 09:47:04 +0500 Subject: [PATCH 3/5] Track last attempted operator instead of last successful Set currentOperatorId_ when attempting an operator (not only on success) so that on the next wake cycle, registration resumes from where it left off rather than restarting from index 0. This ensures the device can eventually exhaust the full operator list across wake cycles. --- src/cellularModuleA7672xx.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cellularModuleA7672xx.cpp b/src/cellularModuleA7672xx.cpp index f48e11c..5640582 100644 --- a/src/cellularModuleA7672xx.cpp +++ b/src/cellularModuleA7672xx.cpp @@ -1148,7 +1148,6 @@ CellularModuleA7672XX::_implCheckNetworkRegistration(CellTechnology ct, AG_LOGW(TAG, "This operator %" PRIu32 " has really low signal %d (csq), moving on..", currentOperatorId_, signal); - currentOperatorId_ = 0; // Clear saved operator currentOperatorIndex_++; REGIS_RETRY_DELAY(); return CONFIGURE_MANUAL_NETWORK; @@ -1183,7 +1182,6 @@ CellularModuleA7672XX::_implCheckNetworkRegistration(CellTechnology ct, // Still denied/emergency after confirmation period if (stat == 3 || stat == 11) { AG_LOGW(TAG, "Registration still denied/emergency (status=%d) after 10s, trying next operator", stat); - currentOperatorId_ = 0; // Clear saved operator currentOperatorIndex_++; return CONFIGURE_MANUAL_NETWORK; } @@ -1192,7 +1190,6 @@ CellularModuleA7672XX::_implCheckNetworkRegistration(CellTechnology ct, // Not registered, check timeout if ((MILLIS() - manualOperatorStartTime) > TIMEOUT_WAIT_REGISTERED) { AG_LOGW(TAG, "Not registered with current operator after 60 seconds, trying next"); - currentOperatorId_ = 0; // Clear saved operator currentOperatorIndex_++; return CONFIGURE_MANUAL_NETWORK; } @@ -1287,6 +1284,7 @@ CellularModuleA7672XX::_implConfigureManualNetwork() { } OperatorInfo opInfo = availableOperators_[currentOperatorIndex_]; + currentOperatorId_ = opInfo.operatorId; // Track last attempted operator for persistence AG_LOGI(TAG, "Configuring manual operator: %" PRIu32 " with AcT: %d (index %zu of %zu)", opInfo.operatorId, opInfo.accessTech, currentOperatorIndex_ + 1, availableOperators_.size()); DELAY_MS(5000); From 41ee85a63940e29c7737ab2ce1ecf798cb5801ce Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Mon, 30 Mar 2026 19:08:15 +0500 Subject: [PATCH 4/5] Add cellular registration flow --- cellular-registration-flow.mmd | 137 +++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 cellular-registration-flow.mmd diff --git a/cellular-registration-flow.mmd b/cellular-registration-flow.mmd new file mode 100644 index 0000000..728fc7d --- /dev/null +++ b/cellular-registration-flow.mmd @@ -0,0 +1,137 @@ +--- +title: Cellular Network Registration State Machine +--- +stateDiagram-v2 + direction TB + + [*] --> CHECK_MODULE_READY + + %% ─── CHECK_MODULE_READY ─── + state CHECK_MODULE_READY { + direction LR + cmr1: Test AT + cmr2: Check SIM ready + cmr1 --> cmr2 + } + CHECK_MODULE_READY --> PREPARE_MODULE : AT ok, SIM ready + CHECK_MODULE_READY --> FAILED : AT or SIM not ready + + %% ─── PREPARE_MODULE ─── + state PREPARE_MODULE { + direction LR + pm1: Disable URC + pm2: Apply cell technology + pm3: Apply APN + pm4: Check operator list + pm1 --> pm2 + pm2 --> pm3 + pm3 --> pm4 + } + PREPARE_MODULE --> SCAN_OPERATOR : No cached operator list + PREPARE_MODULE --> CONFIGURE_MANUAL_NETWORK : Has cached operator list + PREPARE_MODULE --> CHECK_MODULE_READY : Timeout + + %% ─── SCAN_OPERATOR ─── + state SCAN_OPERATOR { + direction LR + so1: AT+COPS=? + so2: Parse operator list + so1 --> so2 + } + SCAN_OPERATOR --> CONFIGURE_MANUAL_NETWORK : Operators found + SCAN_OPERATOR --> CHECK_MODULE_READY : Scan failed / timeout + + %% ─── CONFIGURE_MANUAL_NETWORK ─── + state CONFIGURE_MANUAL_NETWORK { + direction TB + mn1: Find saved operator (by currentOperatorId_) + mn2: Check index >= list size? + mn3: Select operator via AT+COPS + mn1 --> mn2 + mn2 --> mn3 : Has operators left + } + CONFIGURE_MANUAL_NETWORK --> OPERATOR_LIST_EXHAUSTED : All operators tried (index >= size) + CONFIGURE_MANUAL_NETWORK --> CHECK_NETWORK_REGISTRATION : Operator selected OK + CONFIGURE_MANUAL_NETWORK --> CONFIGURE_MANUAL_NETWORK : Selection error, try next + CONFIGURE_MANUAL_NETWORK --> CHECK_MODULE_READY : Selection timeout, try next + + %% ─── CHECK_NETWORK_REGISTRATION ─── + state CHECK_NETWORK_REGISTRATION { + direction TB + nr1: Query registration status + nr2: Query signal strength + nr1 --> nr2 + } + CHECK_NETWORK_REGISTRATION --> CHECK_SERVICE_STATUS : Registered (stat=1,5) with good signal + CHECK_NETWORK_REGISTRATION --> CONFIGURE_MANUAL_NETWORK : Low signal (< 10 csq), try next + CHECK_NETWORK_REGISTRATION --> CONFIGURE_MANUAL_NETWORK : Denied/emergency (stat=3,11) after 10s confirm + CHECK_NETWORK_REGISTRATION --> CONFIGURE_MANUAL_NETWORK : Not registered after 60s, try next + CHECK_NETWORK_REGISTRATION --> CHECK_NETWORK_REGISTRATION : Still searching, retry (3s) + CHECK_NETWORK_REGISTRATION --> CHECK_MODULE_READY : Status check timeout + + %% ─── CHECK_SERVICE_STATUS ─── + state CHECK_SERVICE_STATUS { + direction TB + ss1: Check service available (CNSMOD) + ss2: Activate PDP context + ss3: Ensure packet domain attached + ss1 --> ss2 + ss2 --> ss3 + } + CHECK_SERVICE_STATUS --> NETWORK_READY : Service ready, PDP active + CHECK_SERVICE_STATUS --> CHECK_SERVICE_STATUS : Service/PDP not ready, retry + CHECK_SERVICE_STATUS --> CHECK_NETWORK_REGISTRATION : Service check timeout (30s) + CHECK_SERVICE_STATUS --> CHECK_MODULE_READY : AT timeout + + %% ─── NETWORK_READY ─── + state NETWORK_READY { + direction LR + ready1: Validate signal + ready2: Retrieve IP address + ready1 --> ready2 + } + NETWORK_READY --> SUCCESS : Signal valid, IP obtained + NETWORK_READY --> CHECK_SERVICE_STATUS : Invalid signal / no IP + + %% ─── OPERATOR_LIST_EXHAUSTED ─── + state OPERATOR_LIST_EXHAUSTED { + direction TB + ex1: registrationFailCount_++ + ex2: Check >= MAX_REGISTRATION_FAILURES (3)? + ex1 --> ex2 + } + OPERATOR_LIST_EXHAUSTED --> CHECK_MODULE_READY : Under threshold:\nreset module, retry from index 0 + OPERATOR_LIST_EXHAUSTED --> FAILED : Threshold reached:\nclear operator list,\nreset counter + + %% ─── Terminal states ─── + state SUCCESS { + direction LR + s1: Reset registrationFailCount_ = 0 + s2: Save currentOperatorId_ + s1 --> s2 + } + SUCCESS --> [*] + + state FAILED { + direction LR + f1: Persisted state read back\nby application for NVS save + } + FAILED --> [*] + + %% ─── Operation timeout (outer while loop) ─── + note right of FAILED + Any state can exit to FAILED + when operationTimeoutMs expires. + currentOperatorId_ = last attempted operator + registrationFailCount_ preserved as-is. + Both persisted to NVS by application. + end note + + note right of CONFIGURE_MANUAL_NETWORK + On wake cycle resume: + setOperators() restores list, + currentOperatorId_, and + registrationFailCount_ from NVS. + Registration resumes from the + last attempted operator position. + end note From 6efb26b43fe7eda9929234b49a12a6498ec779fc Mon Sep 17 00:00:00 2001 From: samuelbles07 Date: Mon, 30 Mar 2026 19:16:00 +0500 Subject: [PATCH 5/5] Add public API to set CoAP domain Add setCoapDomain() and setCoapDomainDefault() to AirgradientClient, mirroring the existing HTTP domain API. setCoapDomainDefault() resets to the hardcoded IP (AIRGRADIENT_COAP_IP) to preserve current behavior. --- src/airgradientClient.cpp | 4 ++++ src/airgradientClient.h | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/airgradientClient.cpp b/src/airgradientClient.cpp index 359f3f6..39db282 100644 --- a/src/airgradientClient.cpp +++ b/src/airgradientClient.cpp @@ -23,6 +23,10 @@ void AirgradientClient::setHttpDomain(const std::string &target) { httpDomain = void AirgradientClient::setHttpDomainDefault() { httpDomain = AIRGRADIENT_HTTP_DOMAIN; } +void AirgradientClient::setCoapDomain(const std::string &target) { coapHostTarget = target; } + +void AirgradientClient::setCoapDomainDefault() { coapHostTarget = AIRGRADIENT_COAP_IP; } + void AirgradientClient::setExtendedPmMeasures(bool enable) {} bool AirgradientClient::isClientReady() { return clientReady; } diff --git a/src/airgradientClient.h b/src/airgradientClient.h index a7500a6..c68dad7 100644 --- a/src/airgradientClient.h +++ b/src/airgradientClient.h @@ -105,6 +105,8 @@ class AirgradientClient { */ void setHttpDomain(const std::string &target); void setHttpDomainDefault(); + void setCoapDomain(const std::string &target); + void setCoapDomainDefault(); bool isClientReady(); void setClientReady(bool isReady); void resetFetchConfigurationStatus();