Skip to content

Commit

Permalink
Add optional argument in dynamic mode to limit allocation for JsonDoc…
Browse files Browse the repository at this point in the history
…ument
  • Loading branch information
MathewHDYT committed Sep 12, 2024
1 parent ec7dca0 commit 3bdb077
Show file tree
Hide file tree
Showing 15 changed files with 366 additions and 319 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ const std::array<IAPI_Implementation*, 1U> apis = {
ThingsBoardSized<32> tb(mqttClient, 128, apis);
```
Alternatively, to remove the need for the `MaxRPC` template argument in the constructor template list, see the [Dynamic ThingsBoard section](https://github.com/thingsboard/thingsboard-client-sdk?tab=readme-ov-file#dynamic-thingsboard-usage) section. This will instead expect an additional parameter `responseSize` in the `RPC_Callback` constructor argument list, which shows the internal size the [`JsonDocument`](https://arduinojson.org/v6/api/jsondocument/) needs to have to contain the response. Use `JSON_OBJECT_SIZE()` and pass the amount of key value pair to calculate the estimated size. See https://arduinojson.org/v6/assistant/ for more information.
Alternatively, to remove the need for the `MaxRPC` template argument in the constructor template list, see the [Dynamic ThingsBoard section](https://github.com/thingsboard/thingsboard-client-sdk?tab=readme-ov-file#dynamic-thingsboard-usage) section. This will instead expect an additional parameter response size in the `RPC_Callback` constructor argument list, which shows the internal size the [`JsonDocument`](https://arduinojson.org/v6/api/jsondocument/) needs to have to contain the response. Use `JSON_OBJECT_SIZE()` and pass the amount of key value pair to calculate the estimated size. See https://arduinojson.org/v6/assistant/ for more information.
### Server-side RPC response overflowed
Expand Down
46 changes: 23 additions & 23 deletions src/Attribute_Request.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ class Attribute_Request : public IAPI_Implementation {
if (attribute_request.Get_Request_ID() != request_id) {
continue;
}
char const * const attributeResponseKey = attribute_request.Get_Attribute_Key();
if (attributeResponseKey == nullptr) {
char const * const attribute_response_key = attribute_request.Get_Attribute_Key();
if (attribute_response_key == nullptr) {
#if THINGSBOARD_ENABLE_DEBUG
Logger::println(ATT_KEY_NOT_FOUND);
#endif // THINGSBOARD_ENABLE_DEBUG
Expand All @@ -104,8 +104,8 @@ class Attribute_Request : public IAPI_Implementation {
goto delete_callback;
}

if (data.containsKey(attributeResponseKey)) {
data = data[attributeResponseKey];
if (data.containsKey(attribute_response_key)) {
data = data[attribute_response_key];
}
attribute_request.Stop_Timeout_Timer();
attribute_request.Call_Callback(data);
Expand Down Expand Up @@ -159,13 +159,13 @@ class Attribute_Request : public IAPI_Implementation {
/// @brief Requests one client-side or shared attribute calllback,
/// that will be called if the key-value pair from the server for the given client-side or shared attributes is received
/// @param callback Callback method that will be called
/// @param attributeRequestKey Key of the key-value pair that will contain the attributes we want to request
/// @param attributeResponseKey Key of the key-value pair that will contain the attributes we got as a response
/// @param attribute_request_key Key of the key-value pair that will contain the attributes we want to request
/// @param attribute_response_key Key of the key-value pair that will contain the attributes we got as a response
/// @return Whether requesting the given callback was successful or not
#if THINGSBOARD_ENABLE_DYNAMIC
bool Attributes_Request(Attribute_Request_Callback const & callback, char const * const attributeRequestKey, char const * const attributeResponseKey) {
bool Attributes_Request(Attribute_Request_Callback const & callback, char const * const attribute_request_key, char const * const attribute_response_key) {
#else
bool Attributes_Request(Attribute_Request_Callback<MaxAttributes> const & callback, char const * const attributeRequestKey, char const * const attributeResponseKey) {
bool Attributes_Request(Attribute_Request_Callback<MaxAttributes> const & callback, char const * const attribute_request_key, char const * const attribute_response_key) {
#endif // THINGSBOARD_ENABLE_DYNAMIC
auto const & attributes = callback.Get_Attributes();

Expand All @@ -176,29 +176,29 @@ class Attribute_Request : public IAPI_Implementation {
#endif // THINGSBOARD_ENABLE_DEBUG
return false;
}
else if (attributeRequestKey == nullptr || attributeResponseKey == nullptr) {
else if (attribute_request_key == nullptr || attribute_response_key == nullptr) {
#if THINGSBOARD_ENABLE_DEBUG
Logger::println(ATT_KEY_NOT_FOUND);
#endif // THINGSBOARD_ENABLE_DEBUG
return false;
}

#if THINGSBOARD_ENABLE_DYNAMIC
Attribute_Request_Callback * registeredCallback = nullptr;
Attribute_Request_Callback * registered_callback = nullptr;
#else
Attribute_Request_Callback<MaxAttributes> * registeredCallback = nullptr;
Attribute_Request_Callback<MaxAttributes> * registered_callback = nullptr;
#endif // THINGSBOARD_ENABLE_DYNAMIC
if (!Attributes_Request_Subscribe(callback, registeredCallback)) {
if (!Attributes_Request_Subscribe(callback, registered_callback)) {
return false;
}
else if (registeredCallback == nullptr) {
else if (registered_callback == nullptr) {
return false;
}

// String are const char* and therefore stored as a pointer --> zero copy, meaning the size for the strings is 0 bytes,
// Data structure size depends on the amount of key value pairs passed + the default clientKeys or sharedKeys
// See https://arduinojson.org/v6/assistant/ for more information on the needed size for the JsonDocument
StaticJsonDocument<JSON_OBJECT_SIZE(1)> requestBuffer;
StaticJsonDocument<JSON_OBJECT_SIZE(1)> request_buffer;

// Calculate the size required for the char buffer containing all the attributes seperated by a comma,
// before initalizing it so it is possible to allocate it on the stack
Expand Down Expand Up @@ -231,7 +231,7 @@ class Attribute_Request : public IAPI_Implementation {
// Ensure to cast to const, this is done so that ArduinoJson does not copy the value but instead simply store the pointer, which does not require any more memory,
// besides the base size needed to allocate one key-value pair. Because if we don't the char array would be copied
// and because there is not enough space the value would simply be "undefined" instead. Which would cause the request to not be sent correctly
requestBuffer[attributeRequestKey] = static_cast<const char*>(request);
request_buffer[attribute_request_key] = static_cast<const char*>(request);

size_t * p_request_id = m_get_request_id_callback.Call_Callback();
if (p_request_id == nullptr) {
Expand All @@ -240,23 +240,23 @@ class Attribute_Request : public IAPI_Implementation {
}
auto & request_id = *p_request_id;

registeredCallback->Set_Request_ID(++request_id);
registeredCallback->Set_Attribute_Key(attributeResponseKey);
registeredCallback->Start_Timeout_Timer();
registered_callback->Set_Request_ID(++request_id);
registered_callback->Set_Attribute_Key(attribute_response_key);
registered_callback->Start_Timeout_Timer();

char topic[Helper::detectSize(ATTRIBUTE_REQUEST_TOPIC, request_id)] = {};
(void)snprintf(topic, sizeof(topic), ATTRIBUTE_REQUEST_TOPIC, request_id);
return m_send_json_callback.Call_Callback(topic, requestBuffer, Helper::Measure_Json(requestBuffer));
return m_send_json_callback.Call_Callback(topic, request_buffer, Helper::Measure_Json(request_buffer));
}

/// @brief Subscribes to attribute response topic
/// @param callback Callback method that will be called
/// @param registeredCallback Editable pointer to a reference of the local version that was copied from the passed callback
/// @param registered_callback Editable pointer to a reference of the local version that was copied from the passed callback
/// @return Whether requesting the given callback was successful or not
#if THINGSBOARD_ENABLE_DYNAMIC
bool Attributes_Request_Subscribe(Attribute_Request_Callback const & callback, Attribute_Request_Callback * & registeredCallback) {
bool Attributes_Request_Subscribe(Attribute_Request_Callback const & callback, Attribute_Request_Callback * & registered_callback) {
#else
bool Attributes_Request_Subscribe(Attribute_Request_Callback<MaxAttributes> const & callback, Attribute_Request_Callback<MaxAttributes> * & registeredCallback) {
bool Attributes_Request_Subscribe(Attribute_Request_Callback<MaxAttributes> const & callback, Attribute_Request_Callback<MaxAttributes> * & registered_callback) {
#endif // THINGSBOARD_ENABLE_DYNAMIC
#if !THINGSBOARD_ENABLE_DYNAMIC
if (m_attribute_request_callbacks.size() + 1 > m_attribute_request_callbacks.capacity()) {
Expand All @@ -269,7 +269,7 @@ class Attribute_Request : public IAPI_Implementation {
return false;
}
m_attribute_request_callbacks.push_back(callback);
registeredCallback = &m_attribute_request_callbacks.back();
registered_callback = &m_attribute_request_callbacks.back();
return true;
}

Expand Down
14 changes: 7 additions & 7 deletions src/Callback.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ using Vector = std::vector<T>;
/// @brief General purpose safe callback wrapper. Expects either c-style or c++ style function pointer,
/// depending on if the C++ STL has been implemented on the given device or not.
/// Simply wraps that function pointer and before calling it ensures it actually exists
/// @tparam returnType Type the given callback method should return
/// @tparam argumentTypes Types the given callback method should receive
template<typename returnType, typename... argumentTypes>
/// @tparam return_typ Type the given callback method should return
/// @tparam argument_types Types the given callback method should receive
template<typename return_typ, typename... argument_types>
class Callback {
public:
/// @brief Callback signature
#if THINGSBOARD_ENABLE_STL
using function = std::function<returnType(argumentTypes... arguments)>;
using function = std::function<return_typ(argument_types... arguments)>;
#else
using function = returnType (*)(argumentTypes... arguments);
using function = return_typ (*)(argument_types... arguments);
#endif // THINGSBOARD_ENABLE_STL

/// @brief Constructs empty callback, will result in never being called. Internals are simply default constructed as nullptr
Expand All @@ -57,9 +57,9 @@ class Callback {
/// @param ...arguments Optional additional arguments that are simply formwarded to the subscribed callback if it exists
/// @return Argument returned by the previously subscribed callback or if none or nullptr is subscribed
/// we instead return a defaulted instance of the requested return variable
returnType Call_Callback(argumentTypes const &... arguments) const {
return_typ Call_Callback(argument_types const &... arguments) const {
if (!m_callback) {
return returnType();
return return_typ();
}
return m_callback(arguments...);
}
Expand Down
8 changes: 4 additions & 4 deletions src/Callback_Watchdog.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ class Callback_Watchdog : public Callback<void> {
Callback_Watchdog() = default;

/// @brief Constructs callback, will be called if the timeout time passes without detach() being called
/// @param cb Callback method that will be called as soon as the internal software timers have processed that the given timeout time passed
explicit Callback_Watchdog(function cb)
: Callback(cb)
/// @param callback Callback method that will be called as soon as the internal software timers have processed that the given timeout time passed
explicit Callback_Watchdog(function callback)
: Callback(callback)
#if THINGSBOARD_USE_ESP_TIMER
, m_oneshot_timer(nullptr)
#else
Expand Down Expand Up @@ -137,7 +137,7 @@ class Callback_Watchdog : public Callback<void> {
#if THINGSBOARD_USE_ESP_TIMER
esp_timer_handle_t m_oneshot_timer = {}; // ESP Timer handle that is used to start and stop the oneshot timer
#else
Timer<1, micros> m_oneshot_timer = {}; // Ticker instance that handles the timer under the hood, if possible we directly use esp timer instead because it is more efficient
Timer<1, micros> m_oneshot_timer = {}; // Ticker instance that handles the timer under the hood, if possible we directly use esp timer instead because it is more efficient
#endif // THINGSBOARD_USE_ESP_TIMER
};

Expand Down
40 changes: 20 additions & 20 deletions src/Client_Side_RPC.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ char constexpr RPC_RESPONSE_SUBSCRIBE_TOPIC[] = "v1/devices/me/rpc/response/+";
char constexpr RPC_RESPONSE_TOPIC[] = "v1/devices/me/rpc/response/";
char constexpr RPC_SEND_REQUEST_TOPIC[] = "v1/devices/me/rpc/request/%u";
// Log messages.
char constexpr CLIENT_RPC_METHOD_NULL[] = "Client-side RPC methodName is NULL";
char constexpr CLIENT_RPC_METHOD_NULL[] = "Client-side RPC method name is NULL";
#if !THINGSBOARD_ENABLE_DYNAMIC
char constexpr RPC_REQUEST_OVERFLOWED[] = "Client-side RPC request overflowed, increase MaxRequestRPC (%u)";
char constexpr CLIENT_SIDE_RPC_SUBSCRIPTIONS[] = "client-side RPC";
Expand All @@ -29,7 +29,7 @@ template <typename Logger = DefaultLogger>
/// Once the maximum amount has been reached it is not possible to increase the size, this is done because it allows to allcoate the memory on the stack instead of the heap, default = Default_Subscriptions_Amount (1)
/// @tparam MaxRequestRPC Maximum amount of key-value pairs that will ever be sent as parameters to the requests client side rpc method, allows to use a StaticJsonDocument on the stack in the background.
/// Is expected to only request client side rpc requests, that do not additionally send any parameters. If we attempt to send parameters, we have to adjust the size accordingly.
/// Default value is big enough to hold no parameters, but simply the default methodName and params key needed for the request, if additional parameters are sent with the request the size has to be increased by one for each key-value pair.
/// Default value is big enough to hold no parameters, but simply the default method name and params key needed for the request, if additional parameters are sent with the request the size has to be increased by one for each key-value pair.
/// See https://arduinojson.org/v6/assistant/ for more information on how to estimate the required size and divide the result by 16 and add 2 to receive the required MaxRequestRPC value, default = Default_Request_RPC_Amount (2)
template<size_t MaxSubscriptions = Default_Subscriptions_Amount, size_t MaxRequestRPC = Default_Request_RPC_Amount, typename Logger = DefaultLogger>
#endif // THINGSBOARD_ENABLE_DYNAMIC
Expand All @@ -46,43 +46,43 @@ class Client_Side_RPC : public IAPI_Implementation {
/// @param callback Callback method that will be called
/// @return Whether requesting the given callback was successful or not
bool RPC_Request(RPC_Request_Callback const & callback) {
char const * methodName = callback.Get_Name();
char const * method_name = callback.Get_Name();

if (Helper::stringIsNullorEmpty(methodName)) {
if (Helper::stringIsNullorEmpty(method_name)) {
Logger::println(CLIENT_RPC_METHOD_NULL);
return false;
}
RPC_Request_Callback * registeredCallback = nullptr;
if (!RPC_Request_Subscribe(callback, registeredCallback)) {
RPC_Request_Callback * registered_callback = nullptr;
if (!RPC_Request_Subscribe(callback, registered_callback)) {
return false;
}
else if (registeredCallback == nullptr) {
else if (registered_callback == nullptr) {
return false;
}

JsonArray const * const parameters = callback.Get_Parameters();

#if THINGSBOARD_ENABLE_DYNAMIC
// String are const char* and therefore stored as a pointer --> zero copy, meaning the size for the strings is 0 bytes,
// Data structure size depends on the amount of key value pairs passed + the default methodName and params key needed for the request.
// Data structure size depends on the amount of key value pairs passed + the default method name and params key needed for the request.
// See https://arduinojson.org/v6/assistant/ for more information on the needed size for the JsonDocument
TBJsonDocument requestBuffer(JSON_OBJECT_SIZE(parameters != nullptr ? parameters->size() + 2U : 2U));
TBJsonDocument request_buffer(JSON_OBJECT_SIZE(parameters != nullptr ? parameters->size() + 2U : 2U));
#else
// Ensure to have enough size for the infinite amount of possible parameters that could be sent to the cloud
StaticJsonDocument<JSON_OBJECT_SIZE(MaxRequestRPC)> requestBuffer;
StaticJsonDocument<JSON_OBJECT_SIZE(MaxRequestRPC)> request_buffer;
#endif // THINGSBOARD_ENABLE_DYNAMIC

requestBuffer[RPC_METHOD_KEY] = methodName;
request_buffer[RPC_METHOD_KEY] = method_name;

if (parameters != nullptr && !parameters->isNull()) {
requestBuffer[RPC_PARAMS_KEY] = *parameters;
request_buffer[RPC_PARAMS_KEY] = *parameters;
}
else {
requestBuffer[RPC_PARAMS_KEY] = RPC_EMPTY_PARAMS_VALUE;
request_buffer[RPC_PARAMS_KEY] = RPC_EMPTY_PARAMS_VALUE;
}

#if !THINGSBOARD_ENABLE_DYNAMIC
if (requestBuffer.overflowed()) {
if (request_buffer.overflowed()) {
Logger::printfln(RPC_REQUEST_OVERFLOWED, MaxRequestRPC);
return false;
}
Expand All @@ -95,12 +95,12 @@ class Client_Side_RPC : public IAPI_Implementation {
}
auto & request_id = *p_request_id;

registeredCallback->Set_Request_ID(++request_id);
registeredCallback->Start_Timeout_Timer();
registered_callback->Set_Request_ID(++request_id);
registered_callback->Start_Timeout_Timer();

char topic[Helper::detectSize(RPC_SEND_REQUEST_TOPIC, request_id)] = {};
(void)snprintf(topic, sizeof(topic), RPC_SEND_REQUEST_TOPIC, request_id);
return m_send_json_callback.Call_Callback(topic, requestBuffer, Helper::Measure_Json(requestBuffer));
return m_send_json_callback.Call_Callback(topic, request_buffer, Helper::Measure_Json(request_buffer));
}

API_Process_Type Get_Process_Type() const override {
Expand Down Expand Up @@ -172,9 +172,9 @@ class Client_Side_RPC : public IAPI_Implementation {
/// that will be called if a reponse from the server for the method with the given name is received.
/// See https://thingsboard.io/docs/user-guide/rpc/#client-side-rpc for more information
/// @param callback Callback method that will be called
/// @param registeredCallback Editable pointer to a reference of the local version that was copied from the passed callback
/// @param registered_callback Editable pointer to a reference of the local version that was copied from the passed callback
/// @return Whether requesting the given callback was successful or not
bool RPC_Request_Subscribe(RPC_Request_Callback const & callback, RPC_Request_Callback * & registeredCallback) {
bool RPC_Request_Subscribe(RPC_Request_Callback const & callback, RPC_Request_Callback * & registered_callback) {
#if !THINGSBOARD_ENABLE_DYNAMIC
if (m_rpc_request_callbacks.size() + 1 > m_rpc_request_callbacks.capacity()) {
Logger::printfln(MAX_SUBSCRIPTIONS_EXCEEDED, MAX_SUBSCRIPTIONS_TEMPLATE_NAME, CLIENT_SIDE_RPC_SUBSCRIPTIONS);
Expand All @@ -186,7 +186,7 @@ class Client_Side_RPC : public IAPI_Implementation {
return false;
}
m_rpc_request_callbacks.push_back(callback);
registeredCallback = &m_rpc_request_callbacks.back();
registered_callback = &m_rpc_request_callbacks.back();
return true;
}

Expand Down
Loading

0 comments on commit 3bdb077

Please sign in to comment.