Revision:
0:25fa8795676b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/simple-mbed-cloud-client/mbed-cloud-client/mbed-client/source/m2mreporthandler.cpp	Sun Apr 18 15:20:23 2021 +0000
@@ -0,0 +1,721 @@
+/*
+ * Copyright (c) 2015 ARM Limited. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ * Licensed under the Apache License, Version 2.0 (the License); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Needed for PRIu64 on FreeRTOS
+#include <stdio.h>
+// Note: this macro is needed on armcc to get the the limit macros like UINT16_MAX
+#ifndef __STDC_LIMIT_MACROS
+#define __STDC_LIMIT_MACROS
+#endif
+
+// Note: this macro is needed on armcc to get the the PRI*32 macros
+// from inttypes.h in a C++ code.
+#ifndef __STDC_FORMAT_MACROS
+#define __STDC_FORMAT_MACROS
+#endif
+
+#include "mbed-client/m2mreportobserver.h"
+#include "mbed-client/m2mconstants.h"
+#include "mbed-client/m2mtimer.h"
+#include "include/m2mreporthandler.h"
+#include "mbed-trace/mbed_trace.h"
+#include <string.h>
+#include <stdlib.h>
+
+#define TRACE_GROUP "mClt"
+
+M2MReportHandler::M2MReportHandler(M2MReportObserver &observer, M2MBase::DataType type)
+: _observer(observer),
+  _is_under_observation(false),
+  _observation_level(M2MBase::None),
+  _attribute_state(0),
+  _token_length(0),
+  _resource_type(type),
+  _notify(false),
+  _pmin_exceeded(false),
+  _pmax_exceeded(false),
+  _observation_number(0),
+  _pmin_timer(*this),
+  _pmax_timer(*this),
+  _token(NULL),
+  _pmax(-1.0f),
+  _pmin(1.0f),
+  _gt(0.0f),
+  _lt(0.0f),
+  _st(0.0f),
+  _notification_send_in_progress(false),
+  _notification_in_queue(false),
+  _blockwise_notify(false),
+  _pmin_quiet_period(false)
+{
+    tr_debug("M2MReportHandler::M2MReportHandler()");
+    if (_resource_type == M2MBase::FLOAT) {
+        _high_step.float_value = 0;
+        _low_step.float_value = 0;
+        _last_value.float_value = -1;
+        _current_value.float_value = 0;
+    } else {
+        _high_step.int_value = 0;
+        _low_step.int_value = 0;
+        _last_value.int_value = -1;
+        _current_value.int_value = 0;
+    }
+}
+
+M2MReportHandler::~M2MReportHandler()
+{
+    tr_debug("M2MReportHandler::~M2MReportHandler()");
+    free(_token);
+}
+
+void M2MReportHandler::set_under_observation(bool observed)
+{
+    tr_debug("M2MReportHandler::set_under_observation(observed %d)", (int)observed);
+
+    _is_under_observation = observed;
+
+    stop_timers();
+    if (observed) {
+        handle_timers();
+    }
+    else {
+        set_default_values();
+    }
+}
+
+void M2MReportHandler::set_value_float(float value)
+{
+    tr_debug("M2MReportHandler::set_value_float() - current %f, last %f", value, _last_value.float_value);
+    _current_value.float_value = value;
+
+    if (_current_value.float_value != _last_value.float_value) {
+        send_value();
+        _high_step.float_value = _last_value.float_value + _st;
+        _low_step.float_value = _last_value.float_value - _st;
+    }
+}
+
+void M2MReportHandler::set_value_int(int64_t value)
+{
+    tr_debug("M2MReportHandler::set_value_int() - current %" PRId64 ", last % " PRId64, value, _last_value.int_value);
+    _current_value.int_value = value;
+
+    if (_current_value.int_value != _last_value.int_value) {
+        send_value();
+        _high_step.int_value = _last_value.int_value + _st;
+        _low_step.int_value = _last_value.int_value - _st;
+    }
+}
+
+void M2MReportHandler::set_notification_trigger(uint16_t obj_instance_id)
+{
+    tr_debug("M2MReportHandler::set_notification_trigger(): %d", obj_instance_id);
+    // Add to array if not there yet
+    m2m::Vector<uint16_t>::const_iterator it;
+    it = _changed_instance_ids.begin();
+    bool found = false;
+    for ( ; it != _changed_instance_ids.end(); it++) {
+        if ((*it) == obj_instance_id) {
+            found = true;
+            break;
+        }
+    }
+    if (!found) {
+        _changed_instance_ids.push_back(obj_instance_id);
+    }
+
+    if (_resource_type == M2MBase::FLOAT) {
+        _current_value.float_value = 0;
+        _last_value.float_value = 1;
+    } else {
+        _current_value.int_value = 0;
+        _last_value.int_value = 1;
+    }
+    set_notification_in_queue(true);
+    schedule_report();
+}
+
+bool M2MReportHandler::parse_notification_attribute(const char *query,
+                                                    M2MBase::BaseType type,
+                                                    M2MResourceInstance::ResourceType resource_type)
+{
+    tr_debug("M2MReportHandler::parse_notification_attribute(Query %s, Base type %d)", query, (int)type);
+    bool success = false;
+    const char* sep_pos = strchr(query, '&');
+    const char* rest = query;
+    if( sep_pos != NULL ){
+        char query_options[5][20];
+        float pmin = _pmin;
+        float pmax = _pmax;
+        float lt = _lt;
+        float gt = _gt;
+        float st = _st;
+        high_step_t high = _high_step;
+        low_step_t low = _low_step;
+        uint8_t attr = _attribute_state;
+
+        memset(query_options, 0, sizeof(query_options[0][0]) * 5 * 20);
+        uint8_t num_options = 0;
+        while( sep_pos != NULL && num_options < 5){
+            size_t len = (size_t)(sep_pos-rest);
+            if( len > 19 ){
+                len = 19;
+            }
+            memcpy(query_options[num_options], rest, len);
+            sep_pos++;
+            rest = sep_pos;
+            sep_pos = strchr(rest, '&');
+            num_options++;
+        }
+        if( num_options < 5 && strlen(rest) > 0){
+            size_t len = (size_t)strlen(rest);
+            if( len > 19 ){
+                len = 19;
+            }
+            memcpy(query_options[num_options++], rest, len);
+        }
+
+        for (int option = 0; option < num_options; option++) {
+            success = set_notification_attribute(query_options[option],type, resource_type);
+            if (!success) {
+                tr_error("M2MReportHandler::parse_notification_attribute - break");
+                break;
+            }
+        }
+
+        if(success) {
+             success = check_attribute_validity();
+        }
+        else {
+            tr_debug("M2MReportHandler::parse_notification_attribute - not valid query");
+            _pmin = pmin;
+            _pmax = pmax;
+            _st = st;
+            _lt = lt;
+            _gt = gt;
+            _high_step = high;
+            _low_step = low;
+            _attribute_state = attr;
+        }
+    }
+    else {
+        if(set_notification_attribute(query, type, resource_type)) {
+            success = check_attribute_validity();
+        }
+    }
+
+    return success;
+}
+
+void M2MReportHandler::timer_expired(M2MTimerObserver::Type type)
+{
+    switch(type) {
+        case M2MTimerObserver::PMinTimer: {
+            tr_debug("M2MReportHandler::timer_expired - PMIN");
+
+            _pmin_exceeded = true;
+            if (_notify ||
+                (_pmin > 0 && (_attribute_state & M2MReportHandler::Pmax) != M2MReportHandler::Pmax)){
+                report();
+            }
+
+            // If value hasn't changed since last expiration, next value change should send notification immediately
+            if (_resource_type == M2MBase::FLOAT) {
+                if (_current_value.float_value == _last_value.float_value) {
+                    _pmin_quiet_period = true;
+                }
+            } else {
+                if (_current_value.int_value == _last_value.int_value) {
+                    _pmin_quiet_period = true;
+                }
+            }
+        }
+        break;
+        case M2MTimerObserver::PMaxTimer: {
+            tr_debug("M2MReportHandler::timer_expired - PMAX");
+            _pmax_exceeded = true;
+            if (_pmin_exceeded ||
+                    (_attribute_state & M2MReportHandler::Pmin) != M2MReportHandler::Pmin ) {
+                report();
+            }
+        }
+        break;
+        default:
+            break;
+    }
+}
+
+bool M2MReportHandler::set_notification_attribute(const char* option,
+                                                  M2MBase::BaseType type,
+                                                  M2MResourceInstance::ResourceType resource_type)
+{
+    tr_debug("M2MReportHandler::set_notification_attribute()");
+    bool success = false;
+    const int max_size = 20;
+    char attribute[max_size];
+    char value[max_size];
+
+    const char* pos = strstr(option, EQUAL);
+    if (pos) {
+        size_t attr_len = pos - option;
+        // Skip the "=" mark
+        pos++;
+        size_t value_len = strlen(pos);
+        if (value_len && value_len < max_size && attr_len < max_size) {
+            memcpy(attribute, option, attr_len);
+            attribute[attr_len] = '\0';
+            memcpy(value, pos, value_len);
+            value[value_len] = '\0';
+            success = true;
+        }
+    }
+
+    if (success) {
+        if (strcmp(attribute, PMIN) == 0) {
+           _pmin = atoi(value);
+            success = true;
+            _attribute_state |= M2MReportHandler::Pmin;
+            tr_info("M2MReportHandler::set_notification_attribute %s to %" PRId32, attribute, _pmin);
+        }
+        else if(strcmp(attribute, PMAX) == 0) {
+            _pmax = atoi(value);
+            success = true;
+            _attribute_state |= M2MReportHandler::Pmax;
+            tr_info("M2MReportHandler::set_notification_attribute %s to %" PRId32, attribute, _pmax);
+        }
+        else if(strcmp(attribute, GT) == 0 &&
+                (M2MBase::Resource == type)){
+            success = true;
+            _gt = atof(value);
+            _attribute_state |= M2MReportHandler::Gt;
+            tr_info("M2MReportHandler::set_notification_attribute %s to %f", attribute, _gt);
+        }
+        else if(strcmp(attribute, LT) == 0 &&
+                (M2MBase::Resource == type)){
+            success = true;
+            _lt = atof(value);
+            _attribute_state |= M2MReportHandler::Lt;
+            tr_info("M2MReportHandler::set_notification_attribute %s to %f", attribute, _lt);
+        }
+        else if((strcmp(attribute, ST_SIZE) == 0 || (strcmp(attribute, STP) == 0))
+                && (M2MBase::Resource == type)){
+            success = true;
+            _st = atof(value);
+            if (_resource_type == M2MBase::FLOAT) {
+                _high_step.float_value = _current_value.float_value + _st;
+                _low_step.float_value = _current_value.float_value - _st;
+            } else {
+                _high_step.int_value = _current_value.int_value + _st;
+                _low_step.int_value = _current_value.int_value - _st;
+            }
+
+            _attribute_state |= M2MReportHandler::St;
+            tr_info("M2MReportHandler::set_notification_attribute %s to %f", attribute, _st);
+        } else {
+            tr_error("M2MReportHandler::set_notification_attribute - unknown write attribute!");
+            success = false;
+        }
+
+        // Return false if try to set gt,lt or st when the resource type is something else than numerical
+        if (success &&
+            (resource_type != M2MResourceInstance::INTEGER && resource_type != M2MResourceInstance::FLOAT) &&
+            ((_attribute_state & M2MReportHandler::Gt) == M2MReportHandler::Gt ||
+            (_attribute_state & M2MReportHandler::Lt) == M2MReportHandler::Lt ||
+            (_attribute_state & M2MReportHandler::St) == M2MReportHandler::St)) {
+            tr_debug("M2MReportHandler::set_notification_attribute - not numerical resource");
+            success = false;
+        }
+    } else {
+        tr_error("M2MReportHandler::set_notification_attribute - failed to parse query!");
+    }
+    return success;
+}
+
+void M2MReportHandler::schedule_report(bool in_queue)
+{
+    tr_debug("M2MReportHandler::schedule_report()");
+    _notify = true;
+
+    if ((_attribute_state & M2MReportHandler::Pmin) != M2MReportHandler::Pmin ||
+         _pmin_exceeded ||
+         _pmin_quiet_period) {
+        report(in_queue);
+    }
+}
+
+void M2MReportHandler::report(bool in_queue)
+{
+    if (_resource_type == M2MBase::FLOAT) {
+        tr_debug("M2MReportHandler::report() - current %2f, last %2f, notify %d, queued %d", _current_value.float_value, _last_value.float_value, _notify, in_queue);
+    } else {
+        tr_debug("M2MReportHandler::report() - current %" PRId64 ", last % " PRId64 ", notify %d, queued %d", _current_value.int_value, _last_value.int_value, _notify, in_queue);
+    }
+
+    bool value_changed = false;
+
+    if (_resource_type == M2MBase::FLOAT) {
+        if (_current_value.float_value != _last_value.float_value) {
+            value_changed = true;
+        }
+    } else {
+        if (_current_value.int_value != _last_value.int_value) {
+            value_changed = true;
+        }
+    }
+
+    if((value_changed && _notify) || in_queue) {
+        if (_pmin_exceeded) {
+            tr_debug("M2MReportHandler::report()- send with PMIN expiration");
+        } else {
+            tr_debug("M2MReportHandler::report()- send with VALUE change");
+        }
+
+        _pmin_exceeded = false;
+        _pmax_exceeded = false;
+        _notify = false;
+        _pmin_quiet_period = false;
+        _observation_number++;
+
+        if (_observation_number == 1) {
+            // Increment the observation number by 1 if it is already 1 because CoAP specification has reserved 1 for DEREGISTER notification
+            _observation_number++;
+        }
+
+        if (_observer.observation_to_be_sent(_changed_instance_ids, observation_number())) {
+            _changed_instance_ids.clear();
+            set_notification_send_in_progress(true);
+            if (_resource_type == M2MBase::FLOAT) {
+                _last_value.float_value = _current_value.float_value;
+            } else {
+                _last_value.int_value = _current_value.int_value;
+            }
+        }
+
+        _pmax_timer.stop_timer();
+    }
+    else {
+        if (_pmax_exceeded) {
+            tr_debug("M2MReportHandler::report()- send with PMAX expiration");
+            _observation_number++;
+
+            if (_observation_number == 1) {
+                // Increment the observation number by 1 if it is already 1 because CoAP specification has reserved 1 for DEREGISTER notification
+                _observation_number++;
+            }
+
+            if (_observer.observation_to_be_sent(_changed_instance_ids, observation_number(), true)) {
+                _changed_instance_ids.clear();
+                set_notification_send_in_progress(true);
+            } else {
+                set_notification_in_queue(true);
+            }
+            if (_resource_type == M2MBase::FLOAT) {
+                _last_value.float_value = _current_value.float_value;
+            } else {
+                _last_value.int_value = _current_value.int_value;
+            }
+        }
+        else {
+            tr_debug("M2MReportHandler::report()- no need to send");
+        }
+    }
+    handle_timers();
+}
+
+void M2MReportHandler::handle_timers()
+{
+    tr_debug("M2MReportHandler::handle_timers()");
+    uint64_t time_interval = 0;
+    if ((_attribute_state & M2MReportHandler::Pmin) == M2MReportHandler::Pmin) {
+        if (_pmin == _pmax) {
+            _pmin_exceeded = true;
+        } else {
+            _pmin_exceeded = false;
+            time_interval = (uint64_t) ((uint64_t)_pmin * 1000);
+            tr_debug("M2MReportHandler::handle_timers() - Start PMIN interval: %d", (int)time_interval);
+            _pmin_timer.start_timer(time_interval,
+                                     M2MTimerObserver::PMinTimer,
+                                     true);
+        }
+    }
+    if ((_attribute_state & M2MReportHandler::Pmax) == M2MReportHandler::Pmax) {
+        if (_pmax > 0) {
+            time_interval = (uint64_t) ((uint64_t)_pmax * 1000);
+            tr_debug("M2MReportHandler::handle_timers() - Start PMAX interval: %d", (int)time_interval);
+            _pmax_timer.start_timer(time_interval,
+                                     M2MTimerObserver::PMaxTimer,
+                                     true);
+        }
+    }
+}
+
+bool M2MReportHandler::check_attribute_validity() const
+{
+    bool success = true;
+    if ((_attribute_state & M2MReportHandler::Pmax) == M2MReportHandler::Pmax &&
+        ((_pmax >= -1.0) && (_pmin > _pmax))) {
+        success = false;
+    }
+    float low = _lt + 2 * _st;
+    if ((_attribute_state & M2MReportHandler::Gt) == M2MReportHandler::Gt &&
+        (low >= _gt)) {
+        success = false;
+    }
+    return success;
+}
+
+void M2MReportHandler::stop_timers()
+{
+    tr_debug("M2MReportHandler::stop_timers()");
+
+    _pmin_exceeded = false;
+    _pmin_timer.stop_timer();
+
+    _pmax_exceeded = false;
+    _pmax_timer.stop_timer();
+
+    tr_debug("M2MReportHandler::stop_timers() - out");
+}
+
+void M2MReportHandler::set_default_values()
+{
+    tr_debug("M2MReportHandler::set_default_values");
+    _pmax = -1.0;
+    _pmin = 1.0;
+    _gt = 0.0f;
+    _lt = 0.0f;
+    _st = 0.0f;
+    _pmin_exceeded = false;
+    _pmax_exceeded = false;
+    _attribute_state = 0;
+    _changed_instance_ids.clear();
+    _notification_in_queue = false;
+    _notification_send_in_progress = false;
+    _pmin_quiet_period = false;
+    if (_resource_type == M2MBase::FLOAT) {
+        _high_step.float_value = 0.0f;
+        _low_step.float_value = 0.0f;
+        _last_value.float_value = -1.0f;
+    } else {
+        _high_step.int_value = 0;
+        _low_step.int_value = 0;
+        _last_value.int_value = -1;
+    }
+}
+
+bool M2MReportHandler::check_threshold_values() const
+{
+    tr_debug("M2MReportHandler::check_threshold_values");
+    if (_resource_type == M2MBase::FLOAT) {
+        tr_debug("Current value: %f", _current_value.float_value);
+        tr_debug("Last value: %f", _last_value.float_value);
+        tr_debug("High step: %f", _high_step.float_value);
+        tr_debug("Low step: %f", _low_step.float_value);
+    } else {
+        tr_debug("Current value: %" PRId64, _current_value.int_value);
+        tr_debug("Last value: %" PRId64, _last_value.int_value);
+        tr_debug("High step: %" PRId64, _high_step.int_value);
+        tr_debug("Low step: %" PRId64, _low_step.int_value);
+    }
+
+    tr_debug("Less than: %f", _lt);
+    tr_debug("Greater than: %f", _gt);
+    tr_debug("Step: %f", _st);
+
+    bool can_send = check_gt_lt_params();
+    if (can_send) {
+        if ((_attribute_state & M2MReportHandler::St) == M2MReportHandler::St) {
+            can_send = false;
+
+            if (_resource_type == M2MBase::FLOAT) {
+                if (_current_value.float_value >= _high_step.float_value ||
+                    _current_value.float_value <= _low_step.float_value) {
+                    can_send = true;
+                }
+            } else {
+                if ((_current_value.int_value >= _high_step.int_value ||
+                    _current_value.int_value <= _low_step.int_value)) {
+                    can_send = true;
+                }
+            }
+        }
+    }
+
+    tr_debug("M2MReportHandler::check_threshold_values - value can be sent = %d", (int)can_send);
+    return can_send;
+}
+
+bool M2MReportHandler::check_gt_lt_params() const
+{
+    tr_debug("M2MReportHandler::check_gt_lt_params");
+    bool can_send = false;
+    // GT & LT set.
+    if ((_attribute_state & (M2MReportHandler::Lt | M2MReportHandler::Gt)) ==
+        (M2MReportHandler::Lt | M2MReportHandler::Gt)) {
+        if (_resource_type == M2MBase::FLOAT) {
+            if (_current_value.float_value > _gt || _current_value.float_value < _lt) {
+                can_send = true;
+            }
+        } else {
+            if (_current_value.int_value > _gt || _current_value.int_value < _lt) {
+                can_send = true;
+            }
+        }
+    }
+    // Only LT
+    else if ((_attribute_state & M2MReportHandler::Lt) == M2MReportHandler::Lt &&
+             (_attribute_state & M2MReportHandler::Gt) == 0 ) {
+        if (_resource_type == M2MBase::FLOAT) {
+            if (_current_value.float_value < _lt) {
+                can_send = true;
+            }
+        } else {
+            if (_current_value.int_value < _lt) {
+                can_send = true;
+            }
+        }
+
+    }
+    // Only GT
+    else if ((_attribute_state & M2MReportHandler::Gt) == M2MReportHandler::Gt &&
+             (_attribute_state & M2MReportHandler::Lt) == 0 ) {
+        if (_resource_type == M2MBase::FLOAT) {
+            if (_current_value.float_value > _gt) {
+                can_send = true;
+            }
+        } else {
+            if (_current_value.int_value > _gt) {
+                can_send = true;
+            }
+        }
+
+    }
+    // GT & LT not set.
+    else {
+        can_send = true;
+    }
+    tr_debug("M2MReportHandler::check_gt_lt_params - value in range = %d", (int)can_send);
+    return can_send;
+}
+
+uint8_t M2MReportHandler::attribute_flags() const
+{
+    return _attribute_state;
+}
+
+void M2MReportHandler::set_observation_token(const uint8_t *token, const uint8_t length)
+{
+     free(_token);
+     _token = NULL;
+     _token_length = 0;
+
+    if( token != NULL && length > 0 ) {
+        _token = alloc_copy((uint8_t *)token, length);
+        if(_token) {
+            _token_length = length;
+        }
+    }
+}
+
+void M2MReportHandler::get_observation_token(uint8_t *token, uint8_t &token_length) const
+{
+    memcpy(token, _token, _token_length);
+    token_length = _token_length;
+}
+
+uint16_t M2MReportHandler::observation_number() const
+{
+    return _observation_number;
+}
+
+void M2MReportHandler::add_observation_level(M2MBase::Observation obs_level)
+{
+    _observation_level = (M2MBase::Observation)(_observation_level | obs_level);
+}
+
+void M2MReportHandler::remove_observation_level(M2MBase::Observation obs_level)
+{
+    _observation_level = (M2MBase::Observation)(_observation_level & ~obs_level);
+}
+
+M2MBase::Observation M2MReportHandler::observation_level() const
+{
+    return _observation_level;
+}
+
+bool M2MReportHandler::is_under_observation() const
+{
+    return _is_under_observation;
+}
+
+uint8_t* M2MReportHandler::alloc_copy(const uint8_t* source, uint32_t size)
+{
+    assert(source != NULL);
+
+    uint8_t* result = (uint8_t*)malloc(size);
+    if (result) {
+        memcpy(result, source, size);
+    }
+    return result;
+}
+
+void M2MReportHandler::set_notification_in_queue(bool to_queue)
+{
+    _notification_in_queue = to_queue;
+}
+
+bool M2MReportHandler::notification_in_queue() const
+{
+    return _notification_in_queue;
+}
+
+void M2MReportHandler::set_notification_send_in_progress(bool progress)
+{
+    _notification_send_in_progress = progress;
+}
+
+bool M2MReportHandler::notification_send_in_progress() const
+{
+    return _notification_send_in_progress;
+}
+
+void M2MReportHandler::set_blockwise_notify(bool blockwise_notify)
+{
+    _blockwise_notify = blockwise_notify;
+}
+
+bool M2MReportHandler::blockwise_notify() const
+{
+    return _blockwise_notify;
+}
+
+void M2MReportHandler::send_value()
+{
+    tr_debug("M2MReportHandler::send_value() - new value");
+    set_notification_in_queue(true);
+    if (check_threshold_values()) {
+        schedule_report();
+    } else {
+        tr_debug("M2MReportHandler::send_value - value not in range");
+        _notify = false;
+        if ((_attribute_state & M2MReportHandler::Lt) == M2MReportHandler::Lt ||
+            (_attribute_state & M2MReportHandler::Gt) == M2MReportHandler::Gt ||
+            (_attribute_state & M2MReportHandler::St) == M2MReportHandler::St) {
+            tr_debug("M2MReportHandler::send_value - stop pmin timer");
+            _pmin_timer.stop_timer();
+            _pmin_exceeded = true;
+        }
+    }
+}