Skip to content

Commit

Permalink
Add Class to handle retries and state transitions
Browse files Browse the repository at this point in the history
  • Loading branch information
pennam committed Apr 9, 2024
1 parent 0db7e25 commit 58062ca
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 62 deletions.
103 changes: 49 additions & 54 deletions src/ArduinoIoTCloudTCP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,11 @@ unsigned long getTime()

ArduinoIoTCloudTCP::ArduinoIoTCloudTCP()
: _state{State::ConnectPhy}
, _connection_attempt(0,0)
, _tz_offset{0}
, _tz_offset_property{nullptr}
, _tz_dst_until{0}
, _tz_dst_until_property{nullptr}
, _next_connection_attempt_tick{0}
, _last_connection_attempt_cnt{0}
, _next_device_subscribe_attempt_tick{0}
, _last_device_subscribe_cnt{0}
, _next_thing_subscribe_attempt_tick{0}
, _last_thing_subscribe_attempt_cnt{0}
, _next_sync_attempt_tick{0}
, _last_sync_attempt_cnt{0}
, _mqtt_data_buf{0}
, _mqtt_data_len{0}
, _mqtt_data_request_retransmit{false}
Expand Down Expand Up @@ -113,7 +106,12 @@ int ArduinoIoTCloudTCP::begin(ConnectionHandler & connection, bool const enable_
#else
_brokerPort = brokerPort;
#endif

/* Setup TimeService */
_time_service.begin(&connection);

/* Setup retry timers */
_connection_attempt.begin(AIOT_CONFIG_RECONNECTION_RETRY_DELAY_ms, AIOT_CONFIG_MAX_RECONNECTION_RETRY_DELAY_ms);
return begin(enable_watchdog, _brokerAddress, _brokerPort);
}

Expand All @@ -132,15 +130,16 @@ int ArduinoIoTCloudTCP::begin(bool const enable_watchdog, String brokerAddress,
if(!_password.length())
{
#endif

#if defined(BOARD_HAS_SECURE_ELEMENT)
if (!_selement.begin())
{
DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not initialize secure element.", __FUNCTION__);
#if defined(ARDUINO_UNOWIFIR4)
#if defined(ARDUINO_UNOWIFIR4)
if (String(WiFi.firmwareVersion()) < String("0.4.1")) {
DEBUG_ERROR("ArduinoIoTCloudTCP::%s In order to read device certificate, WiFi firmware needs to be >= 0.4.1, current %s", __FUNCTION__, WiFi.firmwareVersion());
}
#endif
#endif
return 0;
}
if (!SElementArduinoCloudDeviceId::read(_selement, getDeviceId(), SElementArduinoCloudSlot::DeviceId))
Expand Down Expand Up @@ -317,8 +316,7 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_ConnectPhy()
{
if (_connection->check() == NetworkConnectionState::CONNECTED)
{
bool const is_retry_attempt = (_last_connection_attempt_cnt > 0);
if (!is_retry_attempt || (is_retry_attempt && (millis() > _next_connection_attempt_tick)))
if (!_connection_attempt.isRetry() || (_connection_attempt.isRetry() && _connection_attempt.isExpired()))
return State::SyncTime;
}

Expand All @@ -339,18 +337,20 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_ConnectMqttBroker()
{
if (_mqttClient.connect(_brokerAddress.c_str(), _brokerPort))
{
_last_connection_attempt_cnt = 0;
DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s connected to %s:%d", __FUNCTION__, _brokerAddress.c_str(), _brokerPort);
/* Reconfigure timers for next state */
_connection_attempt.begin(AIOT_CONFIG_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms, AIOT_CONFIG_MAX_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms);
return State::SendDeviceProperties;
}

/* Can't connect to the broker. Wait: 2s -> 4s -> 8s -> 16s -> 32s -> 32s ... */
_last_connection_attempt_cnt++;
unsigned long reconnection_retry_delay = (1 << _last_connection_attempt_cnt) * AIOT_CONFIG_RECONNECTION_RETRY_DELAY_ms;
reconnection_retry_delay = min(reconnection_retry_delay, static_cast<unsigned long>(AIOT_CONFIG_MAX_RECONNECTION_RETRY_DELAY_ms));
_next_connection_attempt_tick = millis() + reconnection_retry_delay;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
unsigned long const reconnection_retry_delay = _connection_attempt.retry();
#pragma GCC diagnostic pop

DEBUG_ERROR("ArduinoIoTCloudTCP::%s could not connect to %s:%d", __FUNCTION__, _brokerAddress.c_str(), _brokerPort);
DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s %d next connection attempt in %d ms", __FUNCTION__, _last_connection_attempt_cnt, reconnection_retry_delay);
DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s %d next connection attempt in %d ms", __FUNCTION__, _connection_attempt.getRetryCount(), reconnection_retry_delay);
/* Go back to ConnectPhy and retry to get time from network (invalid time for SSL handshake?)*/
return State::ConnectPhy;
}
Expand All @@ -375,11 +375,10 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SubscribeDeviceTopic()
return State::Disconnect;
}

bool const is_retry_attempt = (_last_device_subscribe_cnt > 0);
if (is_retry_attempt && (millis() < _next_device_subscribe_attempt_tick))
if (_connection_attempt.isRetry() && !_connection_attempt.isExpired())
return State::SubscribeDeviceTopic;

if (is_retry_attempt)
if (_connection_attempt.isRetry())
{
/* Configuration not received or device not attached to a valid thing. Try to resubscribe */
DEBUG_ERROR("ArduinoIoTCloudTCP::%s device waiting for valid thing_id %d", __FUNCTION__, _time_service.getTime());
Expand All @@ -396,18 +395,17 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SubscribeDeviceTopic()
}

/* Max retry than disconnect */
if (_last_device_subscribe_cnt > AIOT_CONFIG_DEVICE_TOPIC_MAX_RETRY_CNT)
if (_connection_attempt.getRetryCount() > AIOT_CONFIG_DEVICE_TOPIC_MAX_RETRY_CNT)
{
_last_device_subscribe_cnt = 0;
return State::Disconnect;
}

/* No device configuration received. Wait: 4s -> 8s -> 16s -> 32s -> 32s ...*/
_last_device_subscribe_cnt++;
unsigned long subscribe_retry_delay = (1 << _last_device_subscribe_cnt) * AIOT_CONFIG_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms;
subscribe_retry_delay = min(subscribe_retry_delay, static_cast<unsigned long>(AIOT_CONFIG_MAX_DEVICE_TOPIC_SUBSCRIBE_RETRY_DELAY_ms));
_next_device_subscribe_attempt_tick = millis() + subscribe_retry_delay;
DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s %d next configuration request in %d ms", __FUNCTION__, _last_device_subscribe_cnt, subscribe_retry_delay);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
unsigned long const subscribe_retry_delay = _connection_attempt.retry();
#pragma GCC diagnostic pop
DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s %d next configuration request in %d ms", __FUNCTION__, _connection_attempt.getRetryCount(), subscribe_retry_delay);

return State::SubscribeDeviceTopic;
}
Expand All @@ -426,18 +424,18 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_CheckDeviceConfig()
/* Device configuration received, but invalid thing_id. Do not increase counter, but recompute delay.
* Device not attached. Wait: 40s -> 80s -> 160s -> 320s -> 640s -> 1280s -> 1280s ...
*/
unsigned long attach_retry_delay = (1 << _last_device_subscribe_cnt) * AIOT_CONFIG_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms;
attach_retry_delay = min(attach_retry_delay, static_cast<unsigned long>(AIOT_CONFIG_MAX_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms));
_next_device_subscribe_attempt_tick = millis() + attach_retry_delay;

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
unsigned long const attach_retry_delay = _connection_attempt.reconfigure(AIOT_CONFIG_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms, AIOT_CONFIG_MAX_DEVICE_TOPIC_ATTACH_RETRY_DELAY_ms);
#pragma GCC diagnostic pop
DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s device not attached, next configuration request in %d ms", __FUNCTION__, attach_retry_delay);
return State::SubscribeDeviceTopic;
}

DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s device attached to a new valid thing_id %s %d", __FUNCTION__, getThingId().c_str(), _time_service.getTime());

/* Received valid thing_id reset counters and go on */
_last_device_subscribe_cnt = 0;
/* Received valid thing_id, reconfigure timers for next state and go on */
_connection_attempt.begin(AIOT_CONFIG_THING_TOPICS_SUBSCRIBE_RETRY_DELAY_ms);

return State::SubscribeThingTopics;
}
Expand All @@ -449,18 +447,15 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SubscribeThingTopics()
return State::Disconnect;
}

bool const is_retry_attempt = (_last_thing_subscribe_attempt_cnt > 0);
if (is_retry_attempt && (millis() < _next_thing_subscribe_attempt_tick))
if (_connection_attempt.isRetry() && !_connection_attempt.isExpired())
return State::SubscribeThingTopics;

if (_last_thing_subscribe_attempt_cnt > AIOT_CONFIG_THING_TOPICS_SUBSCRIBE_MAX_RETRY_CNT)
if (_connection_attempt.getRetryCount() > AIOT_CONFIG_THING_TOPICS_SUBSCRIBE_MAX_RETRY_CNT)
{
_last_thing_subscribe_attempt_cnt = 0;
return State::Disconnect;
}

_next_thing_subscribe_attempt_tick = millis() + AIOT_CONFIG_THING_TOPICS_SUBSCRIBE_RETRY_DELAY_ms;
_last_thing_subscribe_attempt_cnt++;
_connection_attempt.retry();

if (!_mqttClient.subscribe(_dataTopicIn))
{
Expand All @@ -480,6 +475,8 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SubscribeThingTopics()
DEBUG_INFO("Thing ID: %s", getThingId().c_str());
execCloudEventCallback(ArduinoIoTCloudEvent::CONNECT);

/* Successfully subscribed to thing topics, reconfigure timers for next state and go on */
_connection_attempt.begin(AIOT_CONFIG_TIMEOUT_FOR_LASTVALUES_SYNC_ms);
return State::RequestLastValues;
}

Expand All @@ -491,25 +488,21 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_RequestLastValues()
}

/* Check whether or not we need to send a new request. */
bool const is_retry_attempt = (_last_sync_attempt_cnt > 0);
if (is_retry_attempt && (millis() < _next_sync_attempt_tick))
if (_connection_attempt.isRetry() && !_connection_attempt.isExpired())
return State::RequestLastValues;

if (_last_sync_attempt_cnt > AIOT_CONFIG_LASTVALUES_SYNC_MAX_RETRY_CNT)
/* Track the number of times a get-last-values request was sent to the cloud.
* If no data is received within a certain number of retry-requests it's a better
* strategy to disconnect and re-establish connection from the ground up.
*/
if (_connection_attempt.getRetryCount() > AIOT_CONFIG_LASTVALUES_SYNC_MAX_RETRY_CNT)
{
/* Track the number of times a get-last-values request was sent to the cloud.
* If no data is received within a certain number of retry-requests it's a better
* strategy to disconnect and re-establish connection from the ground up.
*/
_last_sync_attempt_cnt = 0;
return State::Disconnect;
}

DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] last values requested", __FUNCTION__, _time_service.getTime());
_connection_attempt.retry();
requestLastValue();
_next_sync_attempt_tick = millis() + AIOT_CONFIG_TIMEOUT_FOR_LASTVALUES_SYNC_ms;
_last_sync_attempt_cnt++;

DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] last values requested", __FUNCTION__, _time_service.getTime());
return State::RequestLastValues;
}

Expand Down Expand Up @@ -608,10 +601,14 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_Disconnect()
} else {
_mqttClient.unsubscribe(_shadowTopicIn);
_mqttClient.unsubscribe(_dataTopicIn);
/* TODO add device topic */
_mqttClient.stop();
}
DEBUG_INFO("Disconnected from Arduino IoT Cloud");
execCloudEventCallback(ArduinoIoTCloudEvent::DISCONNECT);

/* Setup timer for broker connection and restart */
_connection_attempt.begin(AIOT_CONFIG_RECONNECTION_RETRY_DELAY_ms, AIOT_CONFIG_MAX_RECONNECTION_RETRY_DELAY_ms);
return State::ConnectPhy;
}

Expand All @@ -633,7 +630,6 @@ void ArduinoIoTCloudTCP::handleMessage(int length)
/* Topic for OTA properties and device configuration */
if (_deviceTopicIn == topic) {
CBORDecoder::decode(_device_property_container, (uint8_t*)bytes, length);
_last_device_subscribe_cnt = 0;
_state = State::CheckDeviceConfig;
}

Expand All @@ -649,7 +645,6 @@ void ArduinoIoTCloudTCP::handleMessage(int length)
CBORDecoder::decode(_thing_property_container, (uint8_t*)bytes, length, true);
_time_service.setTimeZoneData(_tz_offset, _tz_dst_until);
execCloudEventCallback(ArduinoIoTCloudEvent::SYNC);
_last_sync_attempt_cnt = 0;
_state = State::Connected;
}
}
Expand Down
10 changes: 2 additions & 8 deletions src/ArduinoIoTCloudTCP.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <AIoTC_Config.h>
#include <ArduinoIoTCloud.h>
#include <ArduinoMqttClient.h>
#include <utility/time/TimedAttempt.h>

#if defined(BOARD_HAS_SECURE_ELEMENT)
#include <Arduino_SecureElement.h>
Expand Down Expand Up @@ -121,20 +122,13 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass
};

State _state;
TimedAttempt _connection_attempt;

int _tz_offset;
Property * _tz_offset_property;
unsigned int _tz_dst_until;
Property * _tz_dst_until_property;

unsigned long _next_connection_attempt_tick;
unsigned int _last_connection_attempt_cnt;
unsigned long _next_device_subscribe_attempt_tick;
unsigned int _last_device_subscribe_cnt;
unsigned long _next_thing_subscribe_attempt_tick;
unsigned int _last_thing_subscribe_attempt_cnt;
unsigned long _next_sync_attempt_tick;
unsigned int _last_sync_attempt_cnt;
String _brokerAddress;
uint16_t _brokerPort;
uint8_t _mqtt_data_buf[MQTT_TRANSMIT_BUFFER_SIZE];
Expand Down
75 changes: 75 additions & 0 deletions src/utility/time/TimedAttempt.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
This file is part of the Arduino_SecureElement library.
Copyright (c) 2024 Arduino SA
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

/******************************************************************************
* INCLUDE
******************************************************************************/

#include <Arduino.h>
#include "TimedAttempt.h"

/******************************************************************************
* CTOR/DTOR
******************************************************************************/

TimedAttempt::TimedAttempt(unsigned long minDelay, unsigned long maxDelay)
: _minDelay(minDelay)
, _maxDelay(maxDelay) {
}

/******************************************************************************
* PUBLIC MEMBER FUNCTIONS
******************************************************************************/

void TimedAttempt::begin(unsigned long delay) {
_retryCount = 0;
_minDelay = delay;
_maxDelay = delay;
}

void TimedAttempt::begin(unsigned long minDelay, unsigned long maxDelay) {
_retryCount = 0;
_minDelay = minDelay;
_maxDelay = maxDelay;
}

unsigned long TimedAttempt::reconfigure(unsigned long minDelay, unsigned long maxDelay) {
_minDelay = minDelay;
_maxDelay = maxDelay;
return reload();
}

unsigned long TimedAttempt::retry() {
_retryCount++;
return reload();
}

unsigned long TimedAttempt::reload() {
unsigned long retryDelay = (1 << _retryCount) * _minDelay;
retryDelay = min(retryDelay, _maxDelay);
_nextRetryTick = millis() + retryDelay;
return retryDelay;
}

void TimedAttempt::reset() {
_retryCount = 0;
}

bool TimedAttempt::isRetry() {
return _retryCount > 0;
}

bool TimedAttempt::isExpired() {
return millis() > _nextRetryTick;
}

unsigned int TimedAttempt::getRetryCount() {
return _retryCount;
}
40 changes: 40 additions & 0 deletions src/utility/time/TimedAttempt.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
This file is part of the Arduino_SecureElement library.
Copyright (c) 2024 Arduino SA
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

#ifndef TIMED_ATTEMPT_H
#define TIMED_ATTEMPT_H

/******************************************************************************
* CLASS DECLARATION
******************************************************************************/

class TimedAttempt {

public:
TimedAttempt(unsigned long minDelay, unsigned long maxDelay);

void begin(unsigned long delay);
void begin(unsigned long minDelay, unsigned long maxDelay);
unsigned long reconfigure(unsigned long minDelay, unsigned long maxDelay);
unsigned long retry();
unsigned long reload();
void reset();
bool isRetry();
bool isExpired();
unsigned int getRetryCount();

private:
unsigned long _minDelay;
unsigned long _maxDelay;
unsigned long _nextRetryTick;
unsigned int _retryCount;
};

#endif /* TIMED_ATTEMPT_H */

0 comments on commit 58062ca

Please sign in to comment.