diff --git a/hal/inc/hal_platform.h b/hal/inc/hal_platform.h index f0958c6b69..31a2d0c67d 100644 --- a/hal/inc/hal_platform.h +++ b/hal/inc/hal_platform.h @@ -58,6 +58,10 @@ # endif /* HAL_PLATFORM_CELLULAR_SERIAL */ #endif /* HAL_PLATFORM_CELLULAR */ +#ifndef HAL_PLATFORM_CELLULAR_LOW_POWER +#define HAL_PLATFORM_CELLULAR_LOW_POWER (0) +#endif /* HAL_PLATFORM_CELLULAR_LOW_POWER */ + #ifndef HAL_PLATFORM_MESH_DEPRECATED #define HAL_PLATFORM_MESH_DEPRECATED 0 #endif /* HAL_PLATFORM_MESH_DEPRECATED */ diff --git a/hal/network/lwip/ppp_client.cpp b/hal/network/lwip/ppp_client.cpp index 71fb8195c2..9c69d16194 100644 --- a/hal/network/lwip/ppp_client.cpp +++ b/hal/network/lwip/ppp_client.cpp @@ -52,6 +52,10 @@ netif_ext_callback_t Client::netifCb_ = {}; int Client::netifClientDataIdx_ = -1; constexpr const char* Client::eventNames_[]; constexpr const char* Client::stateNames_[]; +const auto NCP_CLIENT_LCP_ECHO_INTERVAL_SECONDS_DEFAULT = 5; +const auto NCP_CLIENT_LCP_ECHO_INTERVAL_SECONDS_R510 = 240; // 4 minutes (4.25 minutes max) +const auto NCP_CLIENT_LCP_ECHO_MAX_FAILS_DEFAULT = 10; +const auto NCP_CLIENT_LCP_ECHO_MAX_FAILS_R510 = 1; namespace { @@ -126,6 +130,13 @@ void Client::init() { pcb_->settings.fsm_ignore_conf_req_opened = 1; } + if (state_ == STATE_CONNECTED || state_ == STATE_DISCONNECTED) { + // Allows the R510 to drop into low power mode (USPV=1) automatically after ~9s when idle + pcb_->settings.lcp_echo_interval = NCP_CLIENT_LCP_ECHO_INTERVAL_SECONDS_R510; + pcb_->settings.lcp_echo_fails = NCP_CLIENT_LCP_ECHO_MAX_FAILS_R510; + pcb_->lcp_echos_pending = 0; // reset echo count + } + pppapi_set_notify_phase_callback(pcb_, &Client::notifyPhaseCb); os_queue_create(&queue_, sizeof(QueueEvent), 5, nullptr); @@ -235,6 +246,7 @@ int Client::input(const uint8_t* data, size_t size) { case STATE_DISCONNECTING: case STATE_CONNECTED: { LOG_DEBUG(TRACE, "RX: %lu", size); + // LOG_DUMP(TRACE, data, size); if (platform_primary_ncp_identifier() == PLATFORM_NCP_SARA_R410) { auto pppos = (pppos_pcb*)pcb_->link_ctx_cb; @@ -489,6 +501,7 @@ uint32_t Client::outputCb(ppp_pcb* pcb, uint8_t* data, uint32_t len, void* ctx) uint32_t Client::output(const uint8_t* data, size_t len) { LOG_DEBUG(TRACE, "TX: %lu", len); + // LOG_DUMP(TRACE, data, len); if (oCb_) { auto r = oCb_(data, len, oCbCtx_); @@ -555,6 +568,20 @@ void Client::notifyNetif(netif_nsc_reason_t reason, const netif_ext_callback_arg void Client::transition(State newState) { LOG(TRACE, "State %s -> %s", stateNames_[state_], stateNames_[newState]); + if (newState != state_) { + if (platform_primary_ncp_identifier() == PLATFORM_NCP_SARA_R510) { + if (newState == STATE_CONNECTED || newState == STATE_DISCONNECTED) { + // Allows the R510 to drop into low power mode (USPV=1) automatically after ~9s when idle + pcb_->settings.lcp_echo_interval = NCP_CLIENT_LCP_ECHO_INTERVAL_SECONDS_R510; + pcb_->settings.lcp_echo_fails = NCP_CLIENT_LCP_ECHO_MAX_FAILS_R510; + pcb_->lcp_echos_pending = 0; // reset echo count + } else { + // Resume default keep alive when not CONNECTED/DISCONNECTED for R510 + pcb_->settings.lcp_echo_interval = NCP_CLIENT_LCP_ECHO_INTERVAL_SECONDS_DEFAULT; + pcb_->settings.lcp_echo_fails = NCP_CLIENT_LCP_ECHO_MAX_FAILS_DEFAULT; + } + } + } state_ = newState; { diff --git a/hal/network/ncp/cellular/cellular_ncp_client.h b/hal/network/ncp/cellular/cellular_ncp_client.h index 1e0cd404dc..c5b58facfa 100644 --- a/hal/network/ncp/cellular/cellular_ncp_client.h +++ b/hal/network/ncp/cellular/cellular_ncp_client.h @@ -81,6 +81,14 @@ enum class CellularAccessTechnology { LTE_NB_IOT = 9 }; +enum class CellularPowerSavingValue { + NONE = -1, + UPSV_DISABLED = 0, + UPSV_ENABLED_TIMER = 1, + UPSV_ENABLED_RTS = 2, + UPSV_ENABLED_DTR = 3, +}; + enum class CellularOperationMode { NONE = -1, PS_ONLY = 0, diff --git a/hal/network/ncp_client/sara/sara_ncp_client.cpp b/hal/network/ncp_client/sara/sara_ncp_client.cpp index 10e698a035..ea48f2c389 100644 --- a/hal/network/ncp_client/sara/sara_ncp_client.cpp +++ b/hal/network/ncp_client/sara/sara_ncp_client.cpp @@ -101,7 +101,8 @@ const auto UBLOX_NCP_R4_APP_FW_VERSION_LATEST_02B_01 = 204; const auto UBLOX_NCP_R4_APP_FW_VERSION_0512 = 219; const auto UBLOX_NCP_MAX_MUXER_FRAME_SIZE = 1509; -const auto UBLOX_NCP_KEEPALIVE_PERIOD = 5000; // milliseconds +const auto UBLOX_NCP_KEEPALIVE_PERIOD_DEFAULT = 5000; // milliseconds +const auto UBLOX_NCP_KEEPALIVE_PERIOD_R510 = 0; // disables muxer keep alive after CONNECTED const auto UBLOX_NCP_KEEPALIVE_MAX_MISSED = 5; // FIXME: for now using a very large buffer @@ -199,6 +200,7 @@ int SaraNcpClient::init(const NcpClientConfig& conf) { firmwareInstallRespCodeR510_ = -1; lastFirmwareInstallRespCodeR510_ = -1; waitReadyRetries_ = 0; + sleepNoPPPWrite_ = false; registrationTimeout_ = REGISTRATION_TIMEOUT; resetRegistrationState(); if (modemPowerState()) { @@ -466,13 +468,21 @@ int SaraNcpClient::disconnect() { if (connState_ == NcpConnectionState::DISCONNECTED) { return SYSTEM_ERROR_NONE; } + + SCOPE_GUARD({ + resetRegistrationState(); + connectionState(NcpConnectionState::DISCONNECTED); + }); + + // If we disconnect due to the AT interface being dead, a forced check + // is required because ready_ is true and parserError_ is 0. + const int r = parser_.execCommand(1000, "AT"); + if (r != AtResponse::OK) { + parserError_ = r; + } CHECK(checkParser()); CHECK_PARSER(setModuleFunctionality(CellularFunctionality::MINIMUM)); - // CHECK_TRUE(r == AtResponse::OK, SYSTEM_ERROR_AT_NOT_OK); - - resetRegistrationState(); - connectionState(NcpConnectionState::DISCONNECTED); return SYSTEM_ERROR_NONE; } @@ -527,7 +537,10 @@ int SaraNcpClient::dataChannelWrite(int id, const uint8_t* data, size_t size) { } } - int err = muxer_.writeChannel(UBLOX_NCP_PPP_CHANNEL, data, size); + int err = gsm0710::GSM0710_ERROR_NONE; + if (!sleepNoPPPWrite_) { + err = muxer_.writeChannel(UBLOX_NCP_PPP_CHANNEL, data, size); + } if (err == gsm0710::GSM0710_ERROR_FLOW_CONTROL) { // Not an error LOG_DEBUG(WARN, "Remote side flow control"); @@ -1484,6 +1497,7 @@ int SaraNcpClient::initReady(ModemState state) { } } } + // Check that the modem is responsive at the new baudrate skipAll(serial_.get(), 1000); CHECK(waitAtResponse(10000)); @@ -1523,12 +1537,19 @@ int SaraNcpClient::initReady(ModemState state) { // Disable Cat-M1 low power modes CHECK(disablePsmEdrx()); + // Allows the R510 to drop into low power mode automatically after ~9s when idle + if (ncpId() == PLATFORM_NCP_SARA_R510) { + // XXX: R510 UPSV=1 appears to have issues dropping into low power mode unless we ensure to disable and enable on boot + CHECK_PARSER_OK(setPowerSavingValue(CellularPowerSavingValue::UPSV_DISABLED, true /* check */)); + CHECK_PARSER_OK(setPowerSavingValue(CellularPowerSavingValue::UPSV_ENABLED_TIMER)); + } + } else { // Force Power Saving mode to be disabled // // TODO: if we enable this feature in the future add logic to CHECK_PARSER macro(s) // to wait longer for device to become active (see MDMParser::_atOk) - CHECK_PARSER_OK(parser_.execCommand("AT+UPSV=0")); + CHECK_PARSER_OK(setPowerSavingValue(CellularPowerSavingValue::UPSV_DISABLED, true /* check */)); } if (state != ModemState::MuxerAtChannel) { @@ -1702,7 +1723,11 @@ int SaraNcpClient::initMuxer() { // Initialize muxer muxer_.setStream(serial_.get()); muxer_.setMaxFrameSize(UBLOX_NCP_MAX_MUXER_FRAME_SIZE); - muxer_.setKeepAlivePeriod(UBLOX_NCP_KEEPALIVE_PERIOD); + if (ncpId() == PLATFORM_NCP_SARA_R510 && connState_ == NcpConnectionState::DISCONNECTED) { + muxer_.setKeepAlivePeriod(UBLOX_NCP_KEEPALIVE_PERIOD_R510); + } else { + muxer_.setKeepAlivePeriod(UBLOX_NCP_KEEPALIVE_PERIOD_DEFAULT); + } muxer_.setKeepAliveMaxMissed(UBLOX_NCP_KEEPALIVE_MAX_MISSED); muxer_.setMaxRetransmissions(3); muxer_.setAckTimeout(UBLOX_MUXER_T1); @@ -1780,6 +1805,35 @@ int SaraNcpClient::checkSimReadiness(bool checkForRfReset) { return r; } +int SaraNcpClient::getPowerSavingValue() { + auto respUpsv = parser_.sendCommand("AT+UPSV?"); + int upsvVal = -1; + // +UPSV: 1,2000,1 + auto upsvValCnt = respUpsv.scanf("+UPSV: %d,%*d,%*d", &upsvVal); + CHECK_PARSER_OK(respUpsv.readResult()); + CHECK_TRUE(upsvValCnt == 1, SYSTEM_ERROR_AT_RESPONSE_UNEXPECTED); + + return upsvVal; +} + +int SaraNcpClient::setPowerSavingValue(CellularPowerSavingValue upsv, bool check) { + if (check) { + if ((int)upsv == CHECK(getPowerSavingValue())) { + // Already in required state + return SYSTEM_ERROR_NONE; + } + } + + int r = SYSTEM_ERROR_UNKNOWN; + + r = parser_.execCommand(1000, "AT+UPSV=%d",(int)upsv); + + CHECK_PARSER_OK(r); + + // AtResponse::Result! + return r; +} + int SaraNcpClient::getOperationModeCached(CellularOperationMode& cemode) { uint32_t systemCacheOperationMode = 0; cemode = CellularOperationMode::NONE; @@ -1823,7 +1877,7 @@ int SaraNcpClient::setOperationMode(CellularOperationMode cemode, bool check, bo } if (check) { - if (cemode == CHECK(getOperationMode())) { + if ((int)cemode == CHECK(getOperationMode())) { if (save) { setOperationModeCached(cemode); } @@ -1855,7 +1909,7 @@ int SaraNcpClient::getModuleFunctionality() { int SaraNcpClient::setModuleFunctionality(CellularFunctionality cfun, bool check) { if (check) { - if (cfun == CHECK(getModuleFunctionality())) { + if ((int)cfun == CHECK(getModuleFunctionality())) { // Already in required state return 0; } @@ -2069,8 +2123,18 @@ int SaraNcpClient::enterDataMode() { // There is some kind of a bug in 02.19 R410 modem firmware in where it does not accept // any AT commands over the second muxed channel, unless we send something over channel 1 first. - if (ncpId() == PLATFORM_NCP_SARA_R410) { - CHECK(waitAtResponse(parser_, 5000)); + // + // If R510 and CONNECTED already, in the case where we are in low power mode and the keepalives + // may be diabled/longer, just in case we are trying to resume a broken connection, let's timeout + // faster with an AT/OK check prior to the following AT+CGATT? call that takes 90s to timeout. + // This will also skip a follow up AT+CPWROFF call because ready_ will be set to false. + if ((ncpId() == PLATFORM_NCP_SARA_R410) || + (ncpId() == PLATFORM_NCP_SARA_R510 && connState_ == NcpConnectionState::CONNECTED)) { + const int r = parser_.execCommand(1000, "AT"); + if (r != AtResponse::OK) { + parserError_ = r; + } + CHECK(checkParser()); } // CGATT should be enabled before we dial @@ -2182,6 +2246,7 @@ int SaraNcpClient::getMtu() { int SaraNcpClient::urcs(bool enable) { const NcpClientLock lock(this); if (enable) { + sleepNoPPPWrite_ = false; CHECK_TRUE(muxer_.resumeChannel(UBLOX_NCP_AT_CHANNEL) == 0, SYSTEM_ERROR_INTERNAL); if (ncpId() == PLATFORM_NCP_SARA_U201) { // Make sure the modem is responsive again. U201 modems do take a while to @@ -2189,7 +2254,14 @@ int SaraNcpClient::urcs(bool enable) { CHECK(waitAtResponse(5000, gsm0710::proto::DEFAULT_T2)); } } else { + sleepNoPPPWrite_ = true; + // R510 may be in low power mode, wake it up so we can get our muxer data channel suspend + // echo, before we get into sleep and potentially prematurely wake by "network activity" (RX data) + if (ncpId() == PLATFORM_NCP_SARA_R510) { + CHECK(waitAtResponse(5000, 2000)); + } CHECK_TRUE(muxer_.suspendChannel(UBLOX_NCP_AT_CHANNEL) == 0, SYSTEM_ERROR_INTERNAL); + HAL_Delay_Milliseconds(100); // allow a bit of time for muxer echo response } return SYSTEM_ERROR_NONE; } @@ -2204,6 +2276,14 @@ void SaraNcpClient::connectionState(NcpConnectionState state) { LOG(TRACE, "NCP connection state changed: %d", (int)state); connState_ = state; + if (ncpId() == PLATFORM_NCP_SARA_R510) { + if (connState_ == NcpConnectionState::CONNECTED || connState_ == NcpConnectionState::DISCONNECTED) { + muxer_.setKeepAlivePeriod(UBLOX_NCP_KEEPALIVE_PERIOD_R510); + } else { + muxer_.setKeepAlivePeriod(UBLOX_NCP_KEEPALIVE_PERIOD_DEFAULT); + } + } + if (connState_ == NcpConnectionState::CONNECTED) { // Reset CGATT workaround flag cgattWorkaroundApplied_ = false; diff --git a/hal/network/ncp_client/sara/sara_ncp_client.h b/hal/network/ncp_client/sara/sara_ncp_client.h index 78fb9dd5f3..8baf040fab 100644 --- a/hal/network/ncp_client/sara/sara_ncp_client.h +++ b/hal/network/ncp_client/sara/sara_ncp_client.h @@ -121,6 +121,7 @@ class SaraNcpClient: public CellularNcpClient { int firmwareInstallRespCodeR510_ = 0; int lastFirmwareInstallRespCodeR510_ = 0; int waitReadyRetries_ = 0; + bool sleepNoPPPWrite_ = false; system_tick_t lastWindow_ = 0; size_t bytesInWindow_ = 0; @@ -171,6 +172,8 @@ class SaraNcpClient: public CellularNcpClient { int waitAtResponseFromPowerOn(ModemState& modemState); int disablePsmEdrx(); int checkSimReadiness(bool checkForRfReset = false); + int getPowerSavingValue(); + int setPowerSavingValue(CellularPowerSavingValue upsv, bool check = false); int getOperationModeCached(CellularOperationMode& cemode); int setOperationModeCached(CellularOperationMode cemode); int getOperationMode(); diff --git a/hal/src/boron/hal_platform_config.h b/hal/src/boron/hal_platform_config.h index 348f539836..37d9167e0c 100644 --- a/hal/src/boron/hal_platform_config.h +++ b/hal/src/boron/hal_platform_config.h @@ -7,6 +7,7 @@ #define HAL_PLATFORM_NCP_AT (1) #define HAL_PLATFORM_CELLULAR (1) #define HAL_PLATFORM_CELLULAR_SERIAL (HAL_USART_SERIAL2) +#define HAL_PLATFORM_CELLULAR_LOW_POWER (1) #define HAL_PLATFORM_SETUP_BUTTON_UX (1) #define HAL_PLATFORM_MUXER_MAY_NEED_DELAY_IN_TX (1) #define HAL_PLATFORM_SPI_NUM (2) diff --git a/hal/src/nRF52840/sleep_hal.cpp b/hal/src/nRF52840/sleep_hal.cpp index 48a55bfb52..9346a0bfee 100644 --- a/hal/src/nRF52840/sleep_hal.cpp +++ b/hal/src/nRF52840/sleep_hal.cpp @@ -47,6 +47,7 @@ #include "exrtc_hal.h" #endif #include "spark_wiring_vector.h" +#include "platform_ncp.h" #if HAL_PLATFORM_IO_EXTENSION && MODULE_FUNCTION != MOD_FUNC_BOOTLOADER #if HAL_PLATFORM_MCP23S17 @@ -680,13 +681,29 @@ static bool isWokenUpByLpcomp() { return NVIC_GetPendingIRQ(COMP_LPCOMP_IRQn); } -static bool isWokenUpByNetwork(const hal_wakeup_source_network_t* networkWakeup) { +static bool isWokenUpByNetwork(const hal_wakeup_source_network_t* networkWakeup, const uint32_t ncpId) { // TODO: More than one network interface are supported on platform. if (networkWakeup->flags & HAL_SLEEP_NETWORK_FLAG_INACTIVE_STANDBY) { return false; } #if HAL_PLATFORM_CELLULAR if (networkWakeup->index == NETWORK_INTERFACE_CELLULAR && NVIC_GetPendingIRQ(UARTE1_IRQn)) { + // XXX: u-blox issue where RXD pin toggles to HI-Z for ~10us about 1ms after CTS goes HIGH + // while modem is in UPSV=1 mode and in a low power state. We see a low pulse due to + // 10k pull-down on RXD. These will occur every 1.28s. If we wake up via R510 cellular + // network activity, and CTS is HIGH, go back to sleep. + // Is CTS pin HIGH? (clear pending interrupt and return false) + if (ncpId == PLATFORM_NCP_SARA_R510) { + hal_pin_info_t* halPinMap = hal_pin_map(); + uint32_t cts1Pin = NRF_GPIO_PIN_MAP(halPinMap[CTS1].gpio_port, halPinMap[CTS1].gpio_pin); + uint32_t cts1State = nrf_gpio_pin_read(cts1Pin); + if (cts1State) { + nrf_uarte_event_clear(NRF_UARTE1, NRF_UARTE_EVENT_RXDRDY); + nrf_uarte_int_enable(NRF_UARTE1, NRF_UARTE_INT_RXDRDY_MASK); + NVIC_ClearPendingIRQ(UARTE1_IRQn); + return false; + } + } return true; } #endif @@ -816,6 +833,7 @@ static void fpu_sleep_prepare(void) { static int enterStopBasedSleep(const hal_sleep_config_t* config, hal_wakeup_source_base_t** wakeupReason) { int ret = SYSTEM_ERROR_NONE; + uint32_t ncpId = platform_primary_ncp_identifier(); // save before external flash is put to sleep // Detach USB HAL_USB_Detach(); @@ -1059,7 +1077,7 @@ static int enterStopBasedSleep(const hal_sleep_config_t* config, hal_wakeup_sour break; // Stop traversing the wakeup sources list. } else if (wakeupSource->type == HAL_WAKEUP_SOURCE_TYPE_NETWORK) { auto networkWakeup = reinterpret_cast(wakeupSource); - if (isWokenUpByNetwork(networkWakeup)) { + if (isWokenUpByNetwork(networkWakeup, ncpId)) { wakeupSourceType = HAL_WAKEUP_SOURCE_TYPE_NETWORK; netif = networkWakeup->index; exitSleepMode = true; diff --git a/user/tests/integration/communication/functions/functions.cpp b/user/tests/integration/communication/functions/functions.cpp index 0351dd3ab2..86a4a6413c 100644 --- a/user/tests/integration/communication/functions/functions.cpp +++ b/user/tests/integration/communication/functions/functions.cpp @@ -50,6 +50,13 @@ test(03_call_function_and_check_return_value) { } test(04_check_function_argument_value) { + // Loop a bit before we check this to make sure the device app thread has received this message. + // 60s is kind of long, but we are not validating how fast it can receive this data, just that it + // does. We want this test to be reliable. + for (auto start = millis(); fn1Arg != "argument string" && millis() - start < 60000;) { + Particle.process(); // pump application events + } + assertTrue(fn1Arg == "argument string"); // Original 1500-character string used in the spec file String s = "WlbOWabfZl6J5H5vrB6KDnJhsI18avqx4RZHSJyDjQGOahaTyT52rHrE1cfUMLxwxZPFxjmDYeQuRUZKLEdwBynABXmQpRkJ09hdBbYZesdUiMqhCS5CcqXApZfidk5w8zb9LoWSPbI45vLod8cjSPsSykOhj64VUzH9FtamoU0a4Mq9pXr3Sz51kwNFpFpBG6a0dfWEzVBL5iYJn680MbUSK6RsMPUTSLrQLUbTlCt5loI4DKmppmg9yc4wsagDvd0AbU86dGeQTwLWHPL1i8k7EpEhjs3Ynf5mvcVwRv5Ik8HAQrtehSrAsWnvez1xqbMa7VxuLsgrBCeoTkiAyeIvXUUXy4DuwyTuM1VQ9kx9uksOLrQDPQEFfhchMe27SnWFwmPOD8vHQPR6P31YRgdnhL1dA91icnULzWD5qh4B9HyTshF0x4bRLRRQwgqJMkURXXqeYhn1vO1OxuSMl3yc0uAGiqBNrgD8DKVHsmnPMoQWN1igBYTvr6EGnuOFgmqhOVRPj9LJ4qfQFFJ7EkKPXSpddca2LoeWcxWNxBBMqISoeWV4GfynNQcLtPvPchj4mf9J1at4j0jiQsSF65Sw5Cy18RbYqazcl1pkchOl3YBk0jkJEF19KCniHD64uPeSeQT8HRtczK1bYCWjeQzZMyKMoRx8YdoUlt55gJfS8MSvDgvsdpMb39WWlCCnYJzQMOCFXmWlOEhWnABcML5ozmIWlB6LaDNcP3kC1Ou87VyslM5IbLSIBw8GNHBetNDnVsFhOntGldkVAapcIDakhunQHXcWB8QbECpy9ijZvSSDtMSGsYu7xFNznSR5QwXJQToITOsvpSffblarWmaXlCWnwfqTKxzAife4U7EmFdOrHQwq4RIHDgOUTtogzgkia9O3e4g1e7qEuweGEJm6YDZqXeGUdcxXvHmQxFmvKoGijur3jEGKVkv2SSanVQ5weOklb4se91mEcA6nUtQXZePx7T6aSf9UumuUeoAToIGTGweMViIxjI2wV5Eikqazfn7AEiXWbRLzKfC7XAZ6ZzqzWqFME4bPgxIbI2aHXe50sa3iA7NEkXZVWfLs8bCjNF66DyfZHahCMjevIpzclwUnlT2Q1Bg1LMyHXD4d6frplmlTvYAKAwVyGz10kZ38oxPtQoYGVF1wkNwXT8cBG1yT6vU6NUpkB22oHdXHzjL6fJ0jvPhIm2a4Sh5pO0Gg7NCktVRBj9jGqn3qo6uggAPTiqBPAoJM2yNJ3aQ5tJLdrN3EM3Brkdm27OaWvbbDQrFneBDjQy8aijwxjtYPz8jXxjGG9c3AW5hPTHxzDKuu64XvTjvbFLr8048on0V7RvvNg7PNmX4mRJr34PKYuEkoJ1KxRvttzziVVlQbvAiy72Inw6DqON0JBpDhg6RhHxXseqZkyrQtFJsOWB1A81cPu5eTzdD9izYpjo7kxpe9iUrb5AL36fhc5WJOBJIFwYEpsD7zh3GycAKc3yWb9BBv"; @@ -72,5 +79,9 @@ test(06_register_many_functions) { } Particle.connect(); waitUntil(Particle.connected); - delay(6000); // Give the system some time to send a blockwise Describe message + + // Give the system some time to send a blockwise Describe message + for (auto start = millis(); millis() - start < 10000;) { + Particle.process(); // pump application events + } } diff --git a/user/tests/integration/communication/variables/variables.cpp b/user/tests/integration/communication/variables/variables.cpp index a9bf9221db..b9a40892dc 100644 --- a/user/tests/integration/communication/variables/variables.cpp +++ b/user/tests/integration/communication/variables/variables.cpp @@ -90,7 +90,13 @@ test(01_register_variables) { // Connect to the cloud Particle.connect(); waitUntil(Particle.connected); - delay(3000); + + // Loop a bit before we check this (JS processes after this test case) to make sure the device app + // thread has received this message. 10s is kind of long, but we are not validating how fast it can + // process this data, just that it does. We want this test to be reliable. + for (auto start = millis(); millis() - start < 10000;) { + Particle.process(); // pump application events + } } test(02_check_variable_values) { @@ -111,10 +117,24 @@ test(03_publish_variable_limits) { test(04_verify_max_variable_value_size) { // 1500-character string strObjVar = "XvFclXWVOG6n99rUYpsLzrp8VyPWdpKfm4z4SdX2GwxLwoJOSPpHL5jF6ajMaJhJdWUuDPSfmqoDmb5DQZRWZFM2f6tSsqmDzVPojUr5qZJQKEgb8WPndRRnD6y9AA5RPfkoqNZKfTgmDCWSGDHygLaFvYOUsM6ggZD8pBLnyrfs5c1fMrM6qZsRglUfaEit4hrDKfsdHoD2SUmdckgU6vqmYHpeVEwW6xitwwFRtyHSvCUb4XbZIWBHJypHEHS17wUpDbTPHcaowsod9Ogp1UjD2ybAUaNd1ul0yPvPigNAqdBsOQ8viVEnOyADAnf0TPQjaXEQ5LWgLJNIheO2qmniPFL9WSnQFPZSY7lwjANoK07ys62nRGoAgwS1sNL0LOvweWwklUxhVDw7foEWBDSXoLaaHieQ7sUvcxAH05S0LMd4m3QbFbkxwFnZPjqvdS98dtAIcvAZqGwbHtnGIInWT5LArXrsyAmiGouezRbgMwS6IFn6ObkGyvEmGqyIGmTdhGlDUSMVMzRXXKoXDn36yqKimGwLhiBKEc4oq7TpwfQ8P17DjO3rVC8hA9cf0UFNHSIhrK4bHtOKSoXEIDv4O4p86xG9oJ84yuUxz4psJHolfwFFlZ6m5csmSOk6urU3kpxh9FyuBnwGrICGIfTxMNfOU0EiV1ajMudqz9G2L2IBgxsqjKaOmeGjja4tgg9cW1UMFnEK9QaXs88kdUmXiJRnIHuZlCg1rOUvQgxmoUlPR7lZ9R6ZfWOivmX2gs7kxiSxK84JmVirVjqgE1gHASoSXjUj3YhJ5h0c6yR5QN6QrHc4zPN2jnI1Tukt8mS7WXbRmGPz31dZSUC9LYVqifY9bw77QYiqenXFbtX4vEeOKFxCvXbzZv3QKKCReobPu0eTM0iLNcrVXUocZXjOnfU7e42UrV8HBGrkB0ozu0mgmVcDlW0M5wp8gcx4ekXLlmvfYH0WO3YamV1ioraHwXJ0MmRSjaFuau7CqOZyUPfhspnM7Yo8yz8J58oVs7oxTzdkgINbr0zBclRyNY6Box9p1MMOtR5t5oNiRYs7g8WxIN4KCKY5CWnlUNUByCwNHhnEGRIIi5guZNt22FHsBPtoztLDwJ7YUY26GTJUypdXm3QOho3vw4IP68w651rcJU7SWX9aEw7pkTS7FqHYT0vsyt2H2Jzx5QQsBcbVei2RL9lgnNRB2UvxNSOyiifjeIECvapmMLiTdTYgq2ZVBDjoTJyZ5DPRdCsJlpKzlNvoomiXnIPyVfhMWhGk5IKieNvkYtTqQZEVhwysndg3MkQLHqlSpU061PPrEoPUtJSvX4c5JtBnISKDT3sFpIHnUayITBUjzKUpJABiPr8E2zBJP1WFJd5yWEBf1JsRBFmrnP7qq6b6zNraRw1NrBvxva04kxIcW8wiTuOkrvlGwChxy5vG4AtVVDga2TSDotdzu5W2mLW3QI9r05zNY0MWVPwpZVmbGZjcYBqE2As7Gl67"; + + // Loop a bit before we check this (JS processes after this test case) to make sure the device app + // thread has received this message. 10s is kind of long, but we are not validating how fast it can + // process this data, just that it does. We want this test to be reliable. + for (auto start = millis(); millis() - start < 10000;) { + Particle.process(); // pump application events + } } test(05_empty_string_variable) { strObjVar = ""; + + // Loop a bit before we check this (JS processes after this test case) to make sure the device app + // thread has received this message. 10s is kind of long, but we are not validating how fast it can + // process this data, just that it does. We want this test to be reliable. + for (auto start = millis(); millis() - start < 10000;) { + Particle.process(); // pump application events + } } test(06_check_current_thread) { @@ -133,5 +153,11 @@ test(07_register_many_variables) { } Particle.connect(); waitUntil(Particle.connected); - delay(6000); // Give the system some time to send a blockwise Describe message + + // Loop a bit before we check this (JS processes after this test case) to make sure the device app + // thread has received this message. 10s is kind of long, but we are not validating how fast it can + // process this data, just that it does. We want this test to be reliable. + for (auto start = millis(); millis() - start < 10000;) { + Particle.process(); // pump application events + } } diff --git a/user/tests/integration/wiring/no_fixture_power_saving b/user/tests/integration/wiring/no_fixture_power_saving new file mode 120000 index 0000000000..cc70b99a44 --- /dev/null +++ b/user/tests/integration/wiring/no_fixture_power_saving @@ -0,0 +1 @@ +../../wiring/no_fixture_power_saving \ No newline at end of file diff --git a/user/tests/wiring/no_fixture_long_running/network.cpp b/user/tests/wiring/no_fixture_long_running/network.cpp index a68010e8f8..b50d38ab7a 100644 --- a/user/tests/wiring/no_fixture_long_running/network.cpp +++ b/user/tests/wiring/no_fixture_long_running/network.cpp @@ -195,8 +195,8 @@ test(NETWORK_01_LargePacketsDontCauseIssues_ResolveMtu) { #if HAL_PLATFORM_NCP_AT || HAL_PLATFORM_CELLULAR test(NETWORK_02_network_connection_recovers_after_ncp_failure) { - // 15 min gives the device time to go through a 10 min timeout & power cycle - const system_tick_t WAIT_TIMEOUT = 15 * 60 * 1000; + // 20 min gives the device time to go through a 10-15 min timeout & power cycle + const system_tick_t WAIT_TIMEOUT = 20 * 60 * 1000; const system_tick_t NCP_FAILURE_TIMEOUT = 15000; Network.on(); @@ -241,8 +241,8 @@ test(NETWORK_02_network_connection_recovers_after_ncp_failure) { static bool s_networkStatusChanged = false; test(NETWORK_03_network_connection_recovers_after_ncp_uart_sleep) { - // 15 min gives the device time to go through a 10 min timeout & power cycle - const system_tick_t WAIT_TIMEOUT = 15 * 60 * 1000; + // 20 min gives the device time to go through a 10-15 min timeout & power cycle + const system_tick_t WAIT_TIMEOUT = 20 * 60 * 1000; SCOPE_GUARD({ Particle.disconnect(); diff --git a/user/tests/wiring/no_fixture_power_saving/application.cpp b/user/tests/wiring/no_fixture_power_saving/application.cpp new file mode 100644 index 0000000000..3620e294bc --- /dev/null +++ b/user/tests/wiring/no_fixture_power_saving/application.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#ifndef PARTICLE_TEST_RUNNER + +#include "application.h" +#include "unit-test/unit-test.h" + +SYSTEM_MODE(MANUAL); + +// make clean all TEST=wiring/no_fixture_long_running PLATFORM=boron -s COMPILE_LTO=n program-dfu DEBUG_BUILD=y +// make clean all TEST=wiring/no_fixture_long_running PLATFORM=boron -s COMPILE_LTO=n program-dfu DEBUG_BUILD=y USE_THREADING=y +// +// Serial1LogHandler logHandler(115200, LOG_LEVEL_ALL, { +// { "comm", LOG_LEVEL_NONE }, // filter out comm messages +// { "system", LOG_LEVEL_INFO } // only info level for system messages +// }); + +UNIT_TEST_APP(); + +// Enable threading if compiled with "USE_THREADING=y" +#if PLATFORM_THREADING == 1 && USE_THREADING == 1 +SYSTEM_THREAD(ENABLED); +#endif + +#endif // PARTICLE_TEST_RUNNER \ No newline at end of file diff --git a/user/tests/wiring/no_fixture_power_saving/no_fixture_power_saving.spec.js b/user/tests/wiring/no_fixture_power_saving/no_fixture_power_saving.spec.js new file mode 100644 index 0000000000..eaed1ddf71 --- /dev/null +++ b/user/tests/wiring/no_fixture_power_saving/no_fixture_power_saving.spec.js @@ -0,0 +1,140 @@ +suite('No fixture power saving'); + +platform('boron','bsom','esomx'); +timeout(32 * 60 * 1000); + + +let api = null; +let auth = null; +let device = null; +let deviceId = null; +let limits = null; +let skipTest = false; +let returnVal = 12345; + +async function delayMs(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function getDeviceFunctionsWithRetries({ deviceId, auth, expectedFuncNum = 0, retries = 10, delay = 1000 } = {}) { + let lastError; + for (let i = 0; i < retries; i++) { + try { + const resp = await api.getDevice({ deviceId, auth }); + const funcs = resp.body.functions; + if (expectedFuncNum > 0 && funcs.length !== expectedFuncNum) { + throw new Error('Number of functions returned from device does not match expected'); + } + return resp; + } catch (e) { + lastError = e; + } + await delayMs(i * delay); + } + if (lastError) { + // console.log(lastError); + throw lastError; + } + throw new Error('Error fetching functions from device'); +} + +before(function() { + api = this.particle.apiClient.instance; + auth = this.particle.apiClient.token; + device = this.particle.devices[0]; + deviceId = device.id; + + // This code will run after the appropriate mailbox message is received. + device.on('mailbox', async (msg) => { + if (msg.d === "skip_test") { + skipTest = true; + return; + } + // console.log('waiting for device to enter low power'); + await delayMs(30000); + // console.log('waking up device with a function call'); + let lastError; + try { + let resp = await api.callFunction({ deviceId, name: 'fnlp1', argument: 'argument string low power sleep', auth }); + // console.log(resp.body.return_value + ' == ' + (returnVal+1)); + expect(resp.body.return_value).to.equal(returnVal + 1); + } catch (e) { + lastError = e; + } + if (lastError) { + // console.log(lastError); + throw lastError; + } + }); +}); + +test('POWER_SAVING_00_setup', async function() { + // See power_saving_mode.cpp +}); + +test('POWER_SAVING_01_particle_publish_publishes_an_event_after_low_power_active', async function() { + if (skipTest) { + return; + } + + const data = await this.particle.receiveEvent('my_event_low_power'); + expect(data).to.equal('event data low power'); +}); + +test('POWER_SAVING_02_register_function_and_connect_to_cloud', async function() { + if (skipTest) { + return; + } + + const expectedFuncs = ['fnlp1']; + const resp = await getDeviceFunctionsWithRetries({ deviceId, auth, expectedFuncNum: expectedFuncs.length }); + const funcs = resp.body.functions; + expect(funcs).to.include.members(expectedFuncs); +}); + +test('POWER_SAVING_03_call_function_and_check_return_value_after_low_power_active', async function() { + if (skipTest) { + return; + } + + let resp = await api.callFunction({ deviceId, name: 'fnlp1', argument: 'argument string low power', auth }); + // console.log(resp.body.return_value + ' == ' + returnVal); + expect(resp.body.return_value).to.equal(returnVal); +}); + +test('POWER_SAVING_04_check_function_argument_value', async function () { + if (skipTest) { + return; + } + + // See power_saving_mode.cpp +}); + +test('POWER_SAVING_05_check_current_thread', async function () { + if (skipTest) { + return; + } + + // See power_saving_mode.cpp +}); + +test('POWER_SAVING_06_system_sleep_with_configuration_object_ultra_low_power_mode_wake_by_network', async function() { + if (skipTest) { + return; + } + + // See power_saving_mode.cpp +}); + +test('POWER_SAVING_07_check_function_argument_value', async function () { + if (skipTest) { + return; + } + + // See power_saving_mode.cpp +}); + +test('POWER_SAVING_99_cleanup', async function() { + device.removeAllListeners('mailbox'); + skipTest = false; +}); diff --git a/user/tests/wiring/no_fixture_power_saving/power_saving_mode.cpp b/user/tests/wiring/no_fixture_power_saving/power_saving_mode.cpp new file mode 100644 index 0000000000..f2682056f4 --- /dev/null +++ b/user/tests/wiring/no_fixture_power_saving/power_saving_mode.cpp @@ -0,0 +1,233 @@ +#include "application.h" +#include "test.h" + +#if HAL_PLATFORM_CELLULAR_LOW_POWER + +// Serial1LogHandler logHandler(115200, LOG_LEVEL_ALL); + +namespace { + +constexpr system_tick_t CLOUD_CONNECT_TIMEOUT = 10 * 60 * 1000; +constexpr system_tick_t CLOUD_DISCONNECT_TIMEOUT = 1 * 60 * 1000; +constexpr uint32_t WAIT_FOR_LOW_POWER_ACTIVE_MS = 20000; +constexpr uint32_t WAIT_FOR_LOW_POWER_MEAS_MS = 10000; +constexpr uint32_t LOW_POWER_ATTEMPTS_MAX = 10; // XXX: extreme cases take up to 2.5 minutes to drop into low power mode +constexpr char skip_test_msg[] = "skip_test"; +int ncpId = DEV_UNKNOWN; +String fnLp1Arg; +bool appThread = true; +int returnVal = 12345; +bool publishResult = false; +bool lowPowerResult = false; +int attempts = 0; +uint32_t period = 0; + +int fnLp1(const String& arg) { + if (!application_thread_current(nullptr)) { + appThread = false; + } + fnLp1Arg = arg; + return returnVal++; +} + +} // namespace + +uint32_t measureAvgPeriodMs(pin_t pin, uint32_t wait_ms) { + int high_pulse = 0; + int low_pulse = 0; + int count = 0; + // Dummy read to sync pulse edge + pulseIn(pin, HIGH); + pulseIn(pin, LOW); + uint32_t s = millis(); + while (millis() - s < wait_ms) { + // This works better without an atomic block + // ATOMIC_BLOCK() { + high_pulse += pulseIn(pin, HIGH); + low_pulse += pulseIn(pin, LOW); + // } + count++; + } + high_pulse = high_pulse/count; + low_pulse = low_pulse/count; + + return (high_pulse + low_pulse)/1000; +} + +test(POWER_SAVING_00_setup) { + auto info = System.hardwareInfo(); + assertTrue(info.isValid()); +#if HAL_PLATFORM_NCP + assertNotEqual(info.ncp().size(), 0); + auto ncpIds = info.ncp(); + ncpId = ncpIds[0]; +#else + ncpId = PLATFORM_NCP_NONE; +#endif // HAL_PLATFORM_NCP + + if (ncpId != PLATFORM_NCP_SARA_R510) { + pushMailbox(MailboxEntry().type(MailboxEntry::Type::DATA).data(skip_test_msg, sizeof(skip_test_msg) - 1)); + skip(); + return; + } + + Particle.disconnect(); + assertTrue(waitFor(Particle.disconnected, CLOUD_DISCONNECT_TIMEOUT)); + Particle.function("fnlp1", fnLp1); +} + +test(POWER_SAVING_01_particle_publish_publishes_an_event_after_low_power_active) { + if (ncpId != PLATFORM_NCP_SARA_R510) { + skip(); + return; + } + + publishResult = false; + lowPowerResult = false; + attempts = 0; + period = 0; + do { + Particle.connect(); + assertTrue(waitFor(Particle.connected, CLOUD_CONNECT_TIMEOUT)); + delay(WAIT_FOR_LOW_POWER_ACTIVE_MS); // wait for UPSV=1 default delay of ~9.2s before modem drops into low power mode idle mode. + + period = measureAvgPeriodMs(CTS1, WAIT_FOR_LOW_POWER_MEAS_MS); + if (period <= 1280 + 128 && period >= 1280 - 128) { // 1.28s +/- 10% if in low power mode + lowPowerResult = true; + } else { + lowPowerResult = false; + } + + publishResult = Particle.publish("my_event_low_power", "event data low power", PRIVATE | WITH_ACK); + // Log.info("period: %lu, publishResult: %d", period, publishResult); + } while (!(publishResult && lowPowerResult) && attempts++ < LOW_POWER_ATTEMPTS_MAX); + assertMoreOrEqual(period, 1280 - 128); // 1.28s +/- 10% if in low power mode + assertLessOrEqual(period, 1280 + 128); + assertTrue(publishResult); +} + +test(POWER_SAVING_02_register_function_and_connect_to_cloud) { + if (ncpId != PLATFORM_NCP_SARA_R510) { + skip(); + return; + } + + lowPowerResult = false; + attempts = 0; + period = 0; + do { + Particle.connect(); + assertTrue(waitFor(Particle.connected, CLOUD_CONNECT_TIMEOUT)); + delay(WAIT_FOR_LOW_POWER_ACTIVE_MS); // wait for UPSV=1 default delay of ~9.2s before modem drops into low power mode idle mode. + + period = measureAvgPeriodMs(CTS1, WAIT_FOR_LOW_POWER_MEAS_MS); + if (period <= 1280 + 128 && period >= 1280 - 128) { // 1.28s +/- 10% if in low power mode + lowPowerResult = true; + } else { + lowPowerResult = false; + } + // Log.info("period: %lu", period); + + } while (!lowPowerResult && attempts++ < LOW_POWER_ATTEMPTS_MAX); + assertMoreOrEqual(period, 1280 - 128); // 1.28s +/- 10% if in low power mode + assertLessOrEqual(period, 1280 + 128); +} + +test(POWER_SAVING_03_call_function_and_check_return_value_after_low_power_active) { + if (ncpId != PLATFORM_NCP_SARA_R510) { + skip(); + return; + } + + // See no_fixture_long_running.spec.js +} + +test(POWER_SAVING_04_check_function_argument_value) { + if (ncpId != PLATFORM_NCP_SARA_R510) { + skip(); + return; + } + + // Loop a bit before we check this to make sure the device app thread has received this message. + // 60s is kind of long, but we are not validating how fast it can receive this data, just that it + // does. We want this test to be reliable. + for (auto start = millis(); fnLp1Arg != "argument string low power" && millis() - start < 60000;) { + Particle.process(); // pump application events + } + assertTrue(fnLp1Arg == "argument string low power"); +} + +test(POWER_SAVING_05_check_current_thread) { + // Verify that all function calls have been performed in the application thread + assertTrue(appThread); +} + +test(POWER_SAVING_06_system_sleep_with_configuration_object_ultra_low_power_mode_wake_by_network) { + if (ncpId != PLATFORM_NCP_SARA_R510) { + skip(); + return; + } + + Particle.connect(); + assertTrue(waitFor(Particle.connected, CLOUD_CONNECT_TIMEOUT)); + + delay(15s); // a bit of delay required to avoid premature wake, even with SystemSleepFlag::WAIT_CLOUD + lowPowerResult = false; + attempts = 0; + period = 0; + do { + Particle.connect(); + assertTrue(waitFor(Particle.connected, CLOUD_CONNECT_TIMEOUT)); + delay(WAIT_FOR_LOW_POWER_ACTIVE_MS); // wait for UPSV=1 default delay of ~9.2s before modem drops into low power mode idle mode. + + period = measureAvgPeriodMs(CTS1, WAIT_FOR_LOW_POWER_MEAS_MS); + if (period <= 1280 + 128 && period >= 1280 - 128) { // 1.28s +/- 10% if in low power mode + lowPowerResult = true; + } else { + lowPowerResult = false; + } + // Log.info("period: %lu", period); + + } while (!lowPowerResult && attempts++ < LOW_POWER_ATTEMPTS_MAX); + assertMoreOrEqual(period, 1280 - 128); // 1.28s +/- 10% if in low power mode + assertLessOrEqual(period, 1280 + 128); + + assertEqual(0, pushMailbox(MailboxEntry().type(MailboxEntry::Type::RESET_PENDING), 30000)); + system_tick_t start = millis(); + SystemSleepConfiguration config; + config.mode(SystemSleepMode::ULTRA_LOW_POWER) + .duration(60s) + .network(NETWORK_INTERFACE_CELLULAR); + SystemSleepResult result = System.sleep(config); + + // in sleep for 60s, should wake up after 30s due to function call + + assertEqual((int)result.wakeupReason(), (int)SystemSleepWakeupReason::BY_NETWORK); + assertLessOrEqual(millis() - start, 50 * 1000); +} + +test(POWER_SAVING_07_check_function_argument_value) { + if (ncpId != PLATFORM_NCP_SARA_R510) { + skip(); + return; + } + + // Loop a bit before we check this to make sure the device app thread has received this message. + // 60s is kind of long, but we are not validating how fast it can receive this data, just that it + // does. We want this test to be reliable. + for (auto start = millis(); fnLp1Arg != "argument string low power sleep" && millis() - start < 60000;) { + Particle.process(); // pump application events + } + assertTrue(fnLp1Arg == "argument string low power sleep"); +} + +test(POWER_SAVING_99_cleanup) { + if (ncpId != PLATFORM_NCP_SARA_R510) { + skip(); + return; + } + + // See no_fixture_long_running.spec.js +} + +#endif // HAL_PLATFORM_CELLULAR_LOW_POWER