From 93322bba86d6aa175a0c2c8e10d5c9ddf1c1aadc Mon Sep 17 00:00:00 2001 From: pennam Date: Thu, 11 Apr 2024 10:25:54 +0200 Subject: [PATCH 1/4] Add ArduinoCloudThing --- src/ArduinoIoTCloudThing.cpp | 172 +++++++++++++++++++++++++++++++++++ src/ArduinoIoTCloudThing.h | 69 ++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 src/ArduinoIoTCloudThing.cpp create mode 100644 src/ArduinoIoTCloudThing.h diff --git a/src/ArduinoIoTCloudThing.cpp b/src/ArduinoIoTCloudThing.cpp new file mode 100644 index 00000000..6ea6dd90 --- /dev/null +++ b/src/ArduinoIoTCloudThing.cpp @@ -0,0 +1,172 @@ +/* + 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 + +#ifdef HAS_TCP + +#include "ArduinoIoTCloudThing.h" +#include "interfaces/CloudProcess.h" +#include "property/types/CloudWrapperInt.h" +#include "property/types/CloudWrapperUnsignedInt.h" + +/****************************************************************************** + * CTOR/DTOR + ******************************************************************************/ +ArduinoCloudThing::ArduinoCloudThing(MessageStream* ms) +: CloudProcess(ms), +_state{State::Init}, +_syncAttempt(0, 0), +_propertyContainer(0), +_propertyContainerIndex(0), +_utcOffset(0), +_utcOffsetProperty(nullptr), +_utcOffsetExpireTime(0), +_utcOffsetExpireTimeProperty(nullptr) { +} + +/****************************************************************************** + * PUBLIC MEMBER FUNCTIONS + ******************************************************************************/ + +void ArduinoCloudThing::begin() { + Property* property; + + property = new CloudWrapperInt(_utcOffset); + _utcOffsetProperty = &addPropertyToContainer(getPropertyContainer(), + *property, + "tz_offset", + Permission::ReadWrite, -1); + _utcOffsetProperty->writeOnDemand(); + property = new CloudWrapperUnsignedInt(_utcOffsetExpireTime); + _utcOffsetExpireTimeProperty = &addPropertyToContainer(getPropertyContainer(), + *property, + "tz_dst_until", + Permission::ReadWrite, -1); + _utcOffsetExpireTimeProperty->writeOnDemand(); +} + +void ArduinoCloudThing::update() { + /* Run through the state machine. */ + State nextState = _state; + switch (_state) { + case State::Init: nextState = handleInit(); break; + case State::RequestLastValues: nextState = handleRequestLastValues(); break; + case State::Connected: nextState = handleConnected(); break; + case State::Disconnect: nextState = handleDisconnect(); break; + } + + /* Handle external events */ + switch (_command) { + case LastValuesUpdateCmdId: + if (_state == State::RequestLastValues) { + DEBUG_VERBOSE("CloudThing::%s Thing is synced", __FUNCTION__); + nextState = State::Connected; + } + break; + + /* We have received a reset command */ + case ResetCmdId: + nextState = State::Init; + break; + + default: + break; + } + + _command = UnknownCmdId; + _state = nextState; +} + +int ArduinoCloudThing::connected() { + return _state > State::Disconnect ? 1 : 0; +} + +void ArduinoCloudThing::handleMessage(Message* m) { + _command = UnknownCmdId; + if (m != nullptr) { + _command = m->id; + } +} + +ArduinoCloudThing::State ArduinoCloudThing::handleInit() { + _syncAttempt.begin(AIOT_CONFIG_TIMEOUT_FOR_LASTVALUES_SYNC_ms); + return State::RequestLastValues; +} + +ArduinoCloudThing::State ArduinoCloudThing::handleRequestLastValues() { + /* Check whether or not we need to send a new request. */ + if (_syncAttempt.isRetry() && !_syncAttempt.isExpired()) { + return State::RequestLastValues; + } + + /* 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 (_syncAttempt.getRetryCount() > AIOT_CONFIG_LASTVALUES_SYNC_MAX_RETRY_CNT) { + return State::Disconnect; + } + + _syncAttempt.retry(); + + /* Send message upstream to inform infrastructure we need to request thing + * last values + */ + DEBUG_VERBOSE("CloudThing::%s not int sync. %d next sync request in %d ms", + __FUNCTION__, _syncAttempt.getRetryCount(), _syncAttempt.getWaitTime()); + Message message = { LastValuesBeginCmdId }; + deliver(&message); + + return State::RequestLastValues; +} + +ArduinoCloudThing::State ArduinoCloudThing::handleConnected() { + /* Check if a primitive property wrapper is locally changed. + * This function requires an existing time service which in + * turn requires an established connection. Not having that + * leads to a wrong time set in the time service which inhibits + * the connection from being established due to a wrong data + * in the reconstructed certificate. + */ + updateTimestampOnLocallyChangedProperties(getPropertyContainer()); + + /* Configure Time service with timezone data: + * _utcOffset [offset + dst] + * _utcOffsetExpireTime [posix timestamp until _utcOffset is valid] + */ + if (_utcOffsetProperty->isDifferentFromCloud() || + _utcOffsetExpireTimeProperty->isDifferentFromCloud()) { + _utcOffsetProperty->fromCloudToLocal(); + _utcOffsetExpireTimeProperty->fromCloudToLocal(); + TimeService.setTimeZoneData(_utcOffset, _utcOffsetExpireTime); + } + + /* Check if any property needs encoding and send them to the cloud */ + Message message = { PropertiesUpdateCmdId }; + deliver(&message); + + if (getTime() > _utcOffsetExpireTime) { + return State::RequestLastValues; + } + + return State::Connected; +} + +ArduinoCloudThing::State ArduinoCloudThing::handleDisconnect() { + return State::Disconnect; +} + +#endif /* HAS_TCP */ diff --git a/src/ArduinoIoTCloudThing.h b/src/ArduinoIoTCloudThing.h new file mode 100644 index 00000000..de52bc00 --- /dev/null +++ b/src/ArduinoIoTCloudThing.h @@ -0,0 +1,69 @@ +/* + 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 ARDUINO_IOT_CLOUD_THING_H +#define ARDUINO_IOT_CLOUD_THING_H + +/****************************************************************************** + * INCLUDE + ******************************************************************************/ + +#include "interfaces/CloudProcess.h" +#include "utility/time/TimedAttempt.h" +#include "property/PropertyContainer.h" + +/****************************************************************************** + * CLASS DECLARATION + ******************************************************************************/ + +class ArduinoCloudThing : public CloudProcess { +public: + + ArduinoCloudThing(MessageStream *stream); + virtual void update() override; + virtual void handleMessage(Message *m) override; + + virtual void begin(); + virtual int connected(); + + inline PropertyContainer &getPropertyContainer() { + return _propertyContainer; + }; + inline unsigned int &getPropertyContainerIndex() { + return _propertyContainerIndex; + } + +private: + + enum class State { + Disconnect, + Init, + RequestLastValues, + Connected, + }; + + State _state; + CommandId _command; + TimedAttempt _syncAttempt; + PropertyContainer _propertyContainer; + unsigned int _propertyContainerIndex; + int _utcOffset; + Property *_utcOffsetProperty; + unsigned int _utcOffsetExpireTime; + Property *_utcOffsetExpireTimeProperty; + + State handleInit(); + State handleRequestLastValues(); + State handleConnected(); + State handleDisconnect(); +}; + +#endif /* ARDUINO_IOT_CLOUD_THING_H */ From 51682bd53fe9093ad876b90fef29b2ec6213cec1 Mon Sep 17 00:00:00 2001 From: pennam Date: Wed, 28 Feb 2024 11:34:00 +0100 Subject: [PATCH 2/4] TimeService: make isTimeValid public --- src/utility/time/TimeService.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utility/time/TimeService.h b/src/utility/time/TimeService.h index 6e6af6bb..794ec344 100644 --- a/src/utility/time/TimeService.h +++ b/src/utility/time/TimeService.h @@ -56,6 +56,8 @@ class TimeServiceClass */ static unsigned long getTimeFromString(const String& input); + static bool isTimeValid(unsigned long const time); + private: ConnectionHandler * _con_hdl; @@ -74,7 +76,6 @@ class TimeServiceClass void initRTC(); void setRTC(unsigned long time); unsigned long getRTC(); - static bool isTimeValid(unsigned long const time); static bool isTimeZoneOffsetValid(long const offset); }; From 21a5a405e2d868a021f4b75ebf2c86342702b75e Mon Sep 17 00:00:00 2001 From: pennam Date: Wed, 28 Feb 2024 12:23:03 +0100 Subject: [PATCH 3/4] ArduinoIoTCloudTCP: Use isTimeValid() to check NTP time --- src/ArduinoIoTCloudTCP.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ArduinoIoTCloudTCP.cpp b/src/ArduinoIoTCloudTCP.cpp index 7457eb8d..fead2f60 100644 --- a/src/ArduinoIoTCloudTCP.cpp +++ b/src/ArduinoIoTCloudTCP.cpp @@ -328,12 +328,13 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_ConnectPhy() ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SyncTime() { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-variable" - unsigned long const internal_posix_time = _time_service.getTime(); -#pragma GCC diagnostic pop - DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s internal clock configured to posix timestamp %d", __FUNCTION__, internal_posix_time); - return State::ConnectMqttBroker; + if (TimeServiceClass::isTimeValid(getTime())) + { + DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s internal clock configured to posix timestamp %d", __FUNCTION__, getTime()); + return State::ConnectMqttBroker; + } + + return State::ConnectPhy; } ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_ConnectMqttBroker() From 3130afab294f2624b10542d207cffe13bca5f687 Mon Sep 17 00:00:00 2001 From: pennam Date: Thu, 11 Apr 2024 10:26:07 +0200 Subject: [PATCH 4/4] ArduinoIoTCloudTCP: adapt state machine to use Thing process --- src/ArduinoIoTCloud.cpp | 1 - src/ArduinoIoTCloud.h | 1 - src/ArduinoIoTCloudTCP.cpp | 122 +++++++++++++------------------------ src/ArduinoIoTCloudTCP.h | 18 ++---- 4 files changed, 50 insertions(+), 92 deletions(-) diff --git a/src/ArduinoIoTCloud.cpp b/src/ArduinoIoTCloud.cpp index ec2d4826..0603b675 100644 --- a/src/ArduinoIoTCloud.cpp +++ b/src/ArduinoIoTCloud.cpp @@ -29,7 +29,6 @@ ArduinoIoTCloudClass::ArduinoIoTCloudClass() : _connection{nullptr} , _time_service(TimeService) , _thing_id{""} -, _thing_id_property{nullptr} , _lib_version{AIOT_CONFIG_LIB_VERSION} , _device_id{""} , _cloud_event_callback{nullptr} diff --git a/src/ArduinoIoTCloud.h b/src/ArduinoIoTCloud.h index 2c5bc4fe..7c0bcc5f 100644 --- a/src/ArduinoIoTCloud.h +++ b/src/ArduinoIoTCloud.h @@ -148,7 +148,6 @@ class ArduinoIoTCloudClass ConnectionHandler * _connection; TimeServiceClass & _time_service; String _thing_id; - Property * _thing_id_property; String _lib_version; void execCloudEventCallback(ArduinoIoTCloudEvent const event); diff --git a/src/ArduinoIoTCloudTCP.cpp b/src/ArduinoIoTCloudTCP.cpp index fead2f60..e06e218b 100644 --- a/src/ArduinoIoTCloudTCP.cpp +++ b/src/ArduinoIoTCloudTCP.cpp @@ -60,13 +60,10 @@ unsigned long getTime() ArduinoIoTCloudTCP::ArduinoIoTCloudTCP() : _state{State::ConnectPhy} , _connection_attempt(0,0) +, _message_stream(std::bind(&ArduinoIoTCloudTCP::sendMessage, this, std::placeholders::_1)) +, _thing(&_message_stream) +, _thing_id_property{nullptr} , _device_property_container{0} -, _thing_property_container{0} -, _last_checked_property_index{0} -, _tz_offset{0} -, _tz_offset_property{nullptr} -, _tz_dst_until{0} -, _tz_dst_until_property{nullptr} , _mqtt_data_buf{0} , _mqtt_data_len{0} , _mqtt_data_request_retransmit{false} @@ -214,10 +211,8 @@ int ArduinoIoTCloudTCP::begin(bool const enable_watchdog, String brokerAddress, #endif /* OTA_ENABLED */ p = new CloudWrapperString(_thing_id); _thing_id_property = &addPropertyToContainer(_device_property_container, *p, "thing_id", Permission::ReadWrite, -1).writeOnDemand(); - p = new CloudWrapperInt(_tz_offset); - _tz_offset_property = &addPropertyToContainer(_thing_property_container, *p, "tz_offset", Permission::ReadWrite, -1).writeOnDemand(); - p = new CloudWrapperUnsignedInt(_tz_dst_until); - _tz_dst_until_property = &addPropertyToContainer(_thing_property_container, *p, "tz_dst_until", Permission::ReadWrite, -1).writeOnDemand(); + + _thing.begin(); #if OTA_ENABLED _ota_cap = OTA::isCapable(); @@ -274,7 +269,6 @@ void ArduinoIoTCloudTCP::update() case State::SubscribeDeviceTopic: next_state = handle_SubscribeDeviceTopic(); break; case State::CheckDeviceConfig: next_state = handle_CheckDeviceConfig(); break; case State::SubscribeThingTopics: next_state = handle_SubscribeThingTopics(); break; - case State::RequestLastValues: next_state = handle_RequestLastValues(); break; case State::Connected: next_state = handle_Connected(); break; case State::Disconnect: next_state = handle_Disconnect(); break; } @@ -478,38 +472,12 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_SubscribeThingTopics() /* 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; -} - -ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_RequestLastValues() -{ - if (!_mqttClient.connected() || _thing_id_property->isDifferentFromCloud()) - { - return State::Disconnect; - } - - /* Check whether or not we need to send a new request. */ - if (_connection_attempt.isRetry() && !_connection_attempt.isExpired()) - return State::RequestLastValues; - - /* 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) - { - return State::Disconnect; - } - - _connection_attempt.retry(); - requestLastValue(); - DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] last values requested", __FUNCTION__, _time_service.getTime()); - return State::RequestLastValues; + return State::Connected; } ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_Connected() { - if (!_mqttClient.connected()) + if (!_mqttClient.connected() || _thing_id_property->isDifferentFromCloud() || !_thing.connected()) { /* The last message was definitely lost, trigger a retransmit. */ _mqtt_data_request_retransmit = true; @@ -518,20 +486,6 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_Connected() /* We are connected so let's to our stuff here. */ else { - if (_thing_id_property->isDifferentFromCloud()) - { - return State::Disconnect; - } - - /* Check if a primitive property wrapper is locally changed. - * This function requires an existing time service which in - * turn requires an established connection. Not having that - * leads to a wrong time set in the time service which inhibits - * the connection from being established due to a wrong data - * in the reconstructed certificate. - */ - updateTimestampOnLocallyChangedProperties(_thing_property_container); - /* Retransmit data in case there was a lost transaction due * to phy layer or MQTT connectivity loss. */ @@ -540,27 +494,11 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_Connected() _mqtt_data_request_retransmit = false; } - /* Configure Time service with timezone data: - * _tz_offset [offset + dst] - * _tz_dst_until [posix timestamp until _tz_offset is valid] - */ - if (_tz_offset_property->isDifferentFromCloud() || _tz_dst_until_property->isDifferentFromCloud()) { - _tz_offset_property->fromCloudToLocal(); - _tz_dst_until_property->fromCloudToLocal(); - _time_service.setTimeZoneData(_tz_offset, _tz_dst_until); - } + /* Call CloudThing process to synchronize properties */ + _thing.update(); - /* Check if any properties need encoding and send them to - * the cloud if necessary. - */ - sendThingPropertiesToCloud(); + return State::Connected; - unsigned long const internal_posix_time = _time_service.getTime(); - if (internal_posix_time < _tz_dst_until) { - return State::Connected; - } else { - return State::RequestLastValues; - } } } @@ -605,9 +543,15 @@ ArduinoIoTCloudTCP::State ArduinoIoTCloudTCP::handle_Disconnect() /* TODO add device topic */ _mqttClient.stop(); } + + Message message = { ResetCmdId }; + _thing.handleMessage(&message); + DEBUG_INFO("Disconnected from Arduino IoT Cloud"); execCloudEventCallback(ArduinoIoTCloudEvent::DISCONNECT); + updateThingTopics(); + /* 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; @@ -631,25 +575,47 @@ void ArduinoIoTCloudTCP::handleMessage(int length) /* Topic for OTA properties and device configuration */ if (_deviceTopicIn == topic) { CBORDecoder::decode(_device_property_container, (uint8_t*)bytes, length); - _state = State::CheckDeviceConfig; + if (_thing_id_property->isDifferentFromCloud() && (_thing_id.length() != 0)) { + _state = State::Disconnect; + } else { + _state = State::CheckDeviceConfig; + } } /* Topic for user input data */ if (_dataTopicIn == topic) { - CBORDecoder::decode(_thing_property_container, (uint8_t*)bytes, length); + CBORDecoder::decode(_thing.getPropertyContainer(), (uint8_t*)bytes, length); } /* Topic for sync Thing last values on connect */ - if ((_shadowTopicIn == topic) && (_state == State::RequestLastValues)) + if (_shadowTopicIn == topic) { DEBUG_VERBOSE("ArduinoIoTCloudTCP::%s [%d] last values received", __FUNCTION__, millis()); - CBORDecoder::decode(_thing_property_container, (uint8_t*)bytes, length, true); - _time_service.setTimeZoneData(_tz_offset, _tz_dst_until); + CBORDecoder::decode(_thing.getPropertyContainer(), (uint8_t*)bytes, length, true); + Message message = { LastValuesUpdateCmdId }; + _thing.handleMessage(&message); execCloudEventCallback(ArduinoIoTCloudEvent::SYNC); _state = State::Connected; } } +void ArduinoIoTCloudTCP::sendMessage(Message * msg) +{ + switch (msg->id) + { + case PropertiesUpdateCmdId: + sendThingPropertiesToCloud(); + break; + + case LastValuesBeginCmdId: + requestLastValue(); + break; + + default: + break; + } +} + void ArduinoIoTCloudTCP::sendPropertyContainerToCloud(String const topic, PropertyContainer & property_container, unsigned int & current_property_index) { int bytes_encoded = 0; @@ -670,7 +636,7 @@ void ArduinoIoTCloudTCP::sendPropertyContainerToCloud(String const topic, Proper void ArduinoIoTCloudTCP::sendThingPropertiesToCloud() { - sendPropertyContainerToCloud(_dataTopicOut, _thing_property_container, _last_checked_property_index); + sendPropertyContainerToCloud(_dataTopicOut, _thing.getPropertyContainer(), _thing.getPropertyContainerIndex()); } void ArduinoIoTCloudTCP::sendDevicePropertiesToCloud() diff --git a/src/ArduinoIoTCloudTCP.h b/src/ArduinoIoTCloudTCP.h index bf8396b4..3d23e56a 100644 --- a/src/ArduinoIoTCloudTCP.h +++ b/src/ArduinoIoTCloudTCP.h @@ -25,7 +25,7 @@ #include #include #include -#include +#include #if defined(BOARD_HAS_SECURE_ELEMENT) #include @@ -75,7 +75,6 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass ArduinoIoTCloudTCP(); virtual ~ArduinoIoTCloudTCP() { } - virtual void update () override; virtual int connected () override; virtual void printDebugInfo() override; @@ -91,7 +90,7 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass inline String getBrokerAddress() const { return _brokerAddress; } inline uint16_t getBrokerPort () const { return _brokerPort; } - inline PropertyContainer &getThingPropertyContainer() { return _thing_property_container; } + inline PropertyContainer &getThingPropertyContainer() { return _thing.getPropertyContainer(); } #if OTA_ENABLED /* The callback is triggered when the OTA is initiated and it gets executed until _ota_req flag is cleared. @@ -118,21 +117,16 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass SubscribeDeviceTopic, CheckDeviceConfig, SubscribeThingTopics, - RequestLastValues, Connected, Disconnect, }; State _state; TimedAttempt _connection_attempt; + MessageStream _message_stream; + ArduinoCloudThing _thing; + Property * _thing_id_property; PropertyContainer _device_property_container; - PropertyContainer _thing_property_container; - unsigned int _last_checked_property_index; - - int _tz_offset; - Property * _tz_offset_property; - unsigned int _tz_dst_until; - Property * _tz_dst_until_property; String _brokerAddress; uint16_t _brokerPort; @@ -200,12 +194,12 @@ class ArduinoIoTCloudTCP: public ArduinoIoTCloudClass State handle_CheckDeviceConfig(); State handle_SubscribeDeviceTopic(); State handle_SubscribeThingTopics(); - State handle_RequestLastValues(); State handle_Connected(); State handle_Disconnect(); static void onMessage(int length); void handleMessage(int length); + void sendMessage(Message * msg); void sendPropertyContainerToCloud(String const topic, PropertyContainer & property_container, unsigned int & current_property_index); void sendThingPropertiesToCloud(); void sendDevicePropertiesToCloud();