добавление возможности управления виджетами из модулей

This commit is contained in:
Dmitry Borisenko
2022-12-04 01:37:27 +01:00
parent 3285a70a88
commit 08ffc74e8d
9 changed files with 147 additions and 68 deletions

View File

@@ -19,15 +19,16 @@ boolean publish(const String& topic, const String& data);
boolean publishData(const String& topic, const String& data); boolean publishData(const String& topic, const String& data);
boolean publishChartMqtt(const String& topic, const String& data); boolean publishChartMqtt(const String& topic, const String& data);
boolean publishControl(String id, String topic, String state); boolean publishControl(String id, String topic, String state);
boolean publishChart_test(const String& topic, const String& data); boolean publishJsonMqtt(const String& topic, const String& json);
boolean publishStatusMqtt(const String& topic, const String& data); boolean publishStatusMqtt(const String& topic, const String& data);
boolean publishEvent(const String& topic, const String& data); boolean publishEvent(const String& topic, const String& data);
boolean publishInfo(const String& topic, const String& data); boolean publishInfo(const String& topic, const String& data);
boolean publishAnyJsonKey(const String& topic, const String& key, const String& data); boolean publishAnyJsonKeyMqtt(const String& topic, const String& key, const String& data);
bool publishChartFileToMqtt(String path, String id, int maxCount); bool publishChartFileToMqtt(String path, String id, int maxCount);
void publishWidgets(); void publishWidgets();
void publishState(); void publishMainWidgetsValues();
void publishSubWidgetsValues();
void mqttCallback(char* topic, uint8_t* payload, size_t length); void mqttCallback(char* topic, uint8_t* payload, size_t length);
void handleMqttStatus(bool send); void handleMqttStatus(bool send);

View File

@@ -10,7 +10,7 @@ struct IoTValue {
class IoTItem { class IoTItem {
public: public:
IoTItem(const String &parameters); IoTItem(const String& parameters);
virtual ~IoTItem() {} virtual ~IoTItem() {}
virtual void loop(); virtual void loop();
virtual void doByInterval(); virtual void doByInterval();
@@ -29,6 +29,9 @@ class IoTItem {
long getInterval(); long getInterval();
bool isGlobal(); bool isGlobal();
void sendSubWidgetsValues(String& id, String& json);
virtual void handleSendSubWidgetsValues();
void setInterval(long interval); void setInterval(long interval);
void setIntFromNet(int interval); void setIntFromNet(int interval);
@@ -38,7 +41,7 @@ class IoTItem {
IoTValue value; // хранение основного значения, которое обновляется из сценария, execute(), loop() или doByInterval() IoTValue value; // хранение основного значения, которое обновляется из сценария, execute(), loop() или doByInterval()
//bool iAmDead = false; // признак необходимости удалить объект из базы // bool iAmDead = false; // признак необходимости удалить объект из базы
bool iAmLocal = true; // признак того, что айтем был создан локально bool iAmLocal = true; // признак того, что айтем был создан локально
bool enableDoByInt = true; bool enableDoByInt = true;
@@ -63,11 +66,11 @@ class IoTItem {
protected: protected:
bool _needSave = false; // признак необходимости сохранять и загружать значение элемента на flash bool _needSave = false; // признак необходимости сохранять и загружать значение элемента на flash
String _subtype = ""; String _subtype = "";
String _id = "errorId"; // если будет попытка создания Item без указания id, то элемент оставит это значение String _id = "errorId"; // если будет попытка создания Item без указания id, то элемент оставит это значение
long _interval = 0; long _interval = 0;
int _intFromNet = -2; // количество секунд доверия, пришедших из сети вместе с данными для текущего ИД int _intFromNet = -2; // количество секунд доверия, пришедших из сети вместе с данными для текущего ИД
// -2 - данные не приходили, скорее всего, элемент локальный, доверие есть // -2 - данные не приходили, скорее всего, элемент локальный, доверие есть
// -1 - данные приходили и обратный отсчет дошел до нуля, значит доверия нет // -1 - данные приходили и обратный отсчет дошел до нуля, значит доверия нет
float _multiply; // умножаем на значение float _multiply; // умножаем на значение
float _plus; // увеличиваем на значение float _plus; // увеличиваем на значение
@@ -80,9 +83,9 @@ class IoTItem {
bool _global = false; // характеристика айтема, что ему нужно слать и принимать события из внешнего мира bool _global = false; // характеристика айтема, что ему нужно слать и принимать события из внешнего мира
}; };
IoTItem* findIoTItem(const String& name); // поиск экземпляра элемента модуля по имени IoTItem* findIoTItem(const String& name); // поиск экземпляра элемента модуля по имени
String getItemValue(const String& name); // поиск плюс получение значения String getItemValue(const String& name); // поиск плюс получение значения
bool isItemExist(const String& name); // существует ли айтем bool isItemExist(const String& name); // существует ли айтем
StaticJsonDocument<JSON_BUFFER_SIZE>* getLocalItemsAsJSON(); // сбор всех локальных значений Items StaticJsonDocument<JSON_BUFFER_SIZE>* getLocalItemsAsJSON(); // сбор всех локальных значений Items
IoTItem* createItemFromNet(const String& itemId, const String& value, int interval); IoTItem* createItemFromNet(const String& itemId, const String& value, int interval);

View File

@@ -113,8 +113,8 @@ build_src_filter =
${env:esp8266_4mb_fromitems.build_src_filter} ${env:esp8266_4mb_fromitems.build_src_filter}
[env:esp32_4mb] [env:esp32_4mb]
upload_port = COM8 ;upload_port = COM13
monitor_port = COM8 ;monitor_port = COM13
lib_deps = lib_deps =
${common_env_data.lib_deps_external} ${common_env_data.lib_deps_external}
${env:esp32_4mb_fromitems.lib_deps} ${env:esp32_4mb_fromitems.lib_deps}

View File

@@ -17,7 +17,6 @@ void globalVarsSync() {
valuesFlashJson = readFile(F("values.json"), 4096); valuesFlashJson = readFile(F("values.json"), 4096);
valuesFlashJson.replace("\r\n", ""); valuesFlashJson.replace("\r\n", "");
mqttPrefix = jsonReadStr(settingsFlashJson, F("mqttPrefix")); mqttPrefix = jsonReadStr(settingsFlashJson, F("mqttPrefix"));
mqttRootDevice = mqttPrefix + "/" + chipId; mqttRootDevice = mqttPrefix + "/" + chipId;
jsonWriteStr_(settingsFlashJson, "root", mqttRootDevice); jsonWriteStr_(settingsFlashJson, "root", mqttRootDevice);
@@ -27,6 +26,7 @@ void globalVarsSync() {
// jsonWriteStr_(ssidListHeapJson, "ssids_", ""); //метка для парсинга удалить // jsonWriteStr_(ssidListHeapJson, "ssids_", ""); //метка для парсинга удалить
} }
//к удалению. не используется
String getParamsJson() { String getParamsJson() {
String json; String json;
serializeJson(*getLocalItemsAsJSON(), json); serializeJson(*getLocalItemsAsJSON(), json);

View File

@@ -133,7 +133,8 @@ void mqttCallback(char* topic, uint8_t* payload, size_t length) {
if (payloadStr.startsWith("HELLO")) { if (payloadStr.startsWith("HELLO")) {
SerialPrint("i", F("MQTT"), F("Full update")); SerialPrint("i", F("MQTT"), F("Full update"));
publishWidgets(); publishWidgets();
publishState(); publishMainWidgetsValues();
publishSubWidgetsValues();
//обращение к логированию из ядра //обращение к логированию из ядра
//отправка данных графиков //отправка данных графиков
@@ -223,9 +224,9 @@ boolean publishControl(String id, String topic, String state) {
return mqtt.publish(path.c_str(), state.c_str(), false); return mqtt.publish(path.c_str(), state.c_str(), false);
} }
boolean publishChart_test(const String& topic, const String& data) { boolean publishJsonMqtt(const String& topic, const String& json) {
String path = mqttRootDevice + "/" + topic + "/status"; String path = mqttRootDevice + "/" + topic + "/status";
return mqtt.publish(path.c_str(), data.c_str(), false); return mqtt.publish(path.c_str(), json.c_str(), false);
} }
boolean publishStatusMqtt(const String& topic, const String& data) { boolean publishStatusMqtt(const String& topic, const String& data) {
@@ -235,7 +236,7 @@ boolean publishStatusMqtt(const String& topic, const String& data) {
return mqtt.publish(path.c_str(), json.c_str(), false); return mqtt.publish(path.c_str(), json.c_str(), false);
} }
boolean publishAnyJsonKey(const String& topic, const String& key, const String& data) { boolean publishAnyJsonKeyMqtt(const String& topic, const String& key, const String& data) {
String path = mqttRootDevice + "/" + topic + "/status"; String path = mqttRootDevice + "/" + topic + "/status";
String json = "{}"; String json = "{}";
jsonWriteStr(json, key, data); jsonWriteStr(json, key, data);
@@ -272,21 +273,35 @@ void publishWidgets() {
file.close(); file.close();
} }
void publishState() { //устаревшая версия к удалению
String json = getParamsJson(); // void publishMainWidgetsValues() {
SerialPrint("i", F("DATA"), json); // String json = getParamsJson();
json.replace("{", ""); // SerialPrint("i", F("DATA"), json);
json.replace("}", ""); // json.replace("{", "");
json.replace("\"", ""); // json.replace("}", "");
json += ","; // json.replace("\"", "");
while (json.length() != 0) { // json += ",";
String tmp = selectToMarker(json, ","); // while (json.length() != 0) {
String topic = selectToMarker(tmp, ":"); // String tmp = selectToMarker(json, ",");
String state = deleteBeforeDelimiter(tmp, ":"); // String topic = selectToMarker(tmp, ":");
if (topic != "" && state != "") { // String state = deleteBeforeDelimiter(tmp, ":");
publishStatusMqtt(topic, state); // if (topic != "" && state != "") {
} // publishStatusMqtt(topic, state);
json = deleteBeforeDelimiter(json, ","); // }
// json = deleteBeforeDelimiter(json, ",");
// }
//}
//оптимизированная версия
void publishMainWidgetsValues() {
for (std::list<IoTItem*>::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) {
if ((*it)->iAmLocal) publishStatusMqtt((*it)->getID(), (*it)->getValue());
}
}
void publishSubWidgetsValues() {
for (std::list<IoTItem*>::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) {
if ((*it)->iAmLocal) (*it)->handleSendSubWidgetsValues();
} }
} }

View File

@@ -5,7 +5,6 @@
#include "ESPConfiguration.h" #include "ESPConfiguration.h"
#include "EventsAndOrders.h" #include "EventsAndOrders.h"
//получение параметров в экземпляр класса
IoTItem::IoTItem(const String& parameters) { IoTItem::IoTItem(const String& parameters) {
jsonRead(parameters, F("int"), _interval, false); jsonRead(parameters, F("int"), _interval, false);
if (_interval <= 0) enableDoByInt = false; if (_interval <= 0) enableDoByInt = false;
@@ -33,11 +32,10 @@ IoTItem::IoTItem(const String& parameters) {
setValue(valAsStr, false); setValue(valAsStr, false);
jsonRead(parameters, F("needSave"), _needSave, false); jsonRead(parameters, F("needSave"), _needSave, false);
if (_needSave && jsonRead(valuesFlashJson, _id, valAsStr, false)) // пробуем достать из сохранения значение элемента, если указано, что нужно сохранять if (_needSave && jsonRead(valuesFlashJson, _id, valAsStr, false)) // пробуем достать из сохранения значение элемента, если указано, что нужно сохранять
setValue(valAsStr, false); setValue(valAsStr, false);
} }
//луп выполняющий переодическое дерганье
void IoTItem::loop() { void IoTItem::loop() {
if (enableDoByInt) { if (enableDoByInt) {
currentMillis = millis(); currentMillis = millis();
@@ -49,7 +47,6 @@ void IoTItem::loop() {
} }
} }
//получить
String IoTItem::getValue() { String IoTItem::getValue() {
if (value.isDecimal) { if (value.isDecimal) {
return getRoundValue(); return getRoundValue();
@@ -59,9 +56,8 @@ String IoTItem::getValue() {
long IoTItem::getInterval() { return _interval; } long IoTItem::getInterval() { return _interval; }
bool IoTItem::isGlobal() { return _global;} bool IoTItem::isGlobal() { return _global; }
//определяем тип прилетевшей величины
void IoTItem::setValue(const String& valStr, bool genEvent) { void IoTItem::setValue(const String& valStr, bool genEvent) {
value.isDecimal = isDigitDotCommaStr(valStr); value.isDecimal = isDigitDotCommaStr(valStr);
@@ -73,7 +69,6 @@ void IoTItem::setValue(const String& valStr, bool genEvent) {
setValue(value, genEvent); setValue(value, genEvent);
} }
//
void IoTItem::setValue(const IoTValue& Value, bool genEvent) { void IoTItem::setValue(const IoTValue& Value, bool genEvent) {
value = Value; value = Value;
@@ -84,6 +79,15 @@ void IoTItem::setValue(const IoTValue& Value, bool genEvent) {
} }
} }
//метод отправки из модуля дополнительных json полей виджета в приложение и веб интерфейс, необходимый для изменения виджетов "на лету" из модуля
void IoTItem::sendSubWidgetsValues(String& id, String& json) {
publishJsonMqtt(id, json);
// to do publishJsonWs
}
//метод который нужен что бы из ядра заставить модуль отправить его дополнительные json поля виджета
void IoTItem::handleSendSubWidgetsValues() {}
//когда событие случилось //когда событие случилось
void IoTItem::regEvent(const String& value, const String& consoleInfo, bool error, bool genEvent) { void IoTItem::regEvent(const String& value, const String& consoleInfo, bool error, bool genEvent) {
if (_needSave) { if (_needSave) {
@@ -92,7 +96,7 @@ void IoTItem::regEvent(const String& value, const String& consoleInfo, bool erro
} }
publishStatusMqtt(_id, value); publishStatusMqtt(_id, value);
publishStatusWs(_id, value); publishStatusWs(_id, value);
//SerialPrint("i", "Sensor", consoleInfo + " '" + _id + "' data: " + value + "'"); // SerialPrint("i", "Sensor", consoleInfo + " '" + _id + "' data: " + value + "'");
if (genEvent) { if (genEvent) {
generateEvent(_id, value); generateEvent(_id, value);
@@ -107,7 +111,7 @@ void IoTItem::regEvent(const String& value, const String& consoleInfo, bool erro
String json = "{}"; String json = "{}";
jsonWriteStr_(json, "id", _id); jsonWriteStr_(json, "id", _id);
jsonWriteStr_(json, "val", value); jsonWriteStr_(json, "val", value);
jsonWriteInt_(json, "int", _interval/1000); jsonWriteInt_(json, "int", _interval / 1000);
publishEvent(_id, json); publishEvent(_id, json);
SerialPrint("i", F("<=MQTT"), "Broadcast event: " + json); SerialPrint("i", F("<=MQTT"), "Broadcast event: " + json);
} }
@@ -154,7 +158,7 @@ void IoTItem::getNetEvent(String& event) {
event = "{}"; event = "{}";
jsonWriteStr_(event, "id", _id); jsonWriteStr_(event, "id", _id);
jsonWriteStr_(event, "val", getValue()); jsonWriteStr_(event, "val", getValue());
jsonWriteInt_(event, "int", _interval/1000); jsonWriteInt_(event, "int", _interval / 1000);
} }
void IoTItem::setIntFromNet(int interval) { void IoTItem::setIntFromNet(int interval) {
@@ -199,7 +203,6 @@ IoTGpio* IoTItem::getGpioDriver() {
return nullptr; return nullptr;
} }
//сетевое общение==================================================================================================================================== //сетевое общение====================================================================================================================================
// externalVariable::externalVariable(const String& parameters) : IoTItem(parameters) { // externalVariable::externalVariable(const String& parameters) : IoTItem(parameters) {
@@ -262,8 +265,8 @@ IoTItem* createItemFromNet(const String& itemId, const String& value, int interv
// создаем временную копию элемента из сети на основе события // создаем временную копию элемента из сети на основе события
IoTItem* createItemFromNet(const String& msgFromNet) { IoTItem* createItemFromNet(const String& msgFromNet) {
IoTItem *tmpp = new IoTItem(msgFromNet); IoTItem* tmpp = new IoTItem(msgFromNet);
if (tmpp->getInterval()) tmpp->setIntFromNet(tmpp->getInterval()/1000 + 5); if (tmpp->getInterval()) tmpp->setIntFromNet(tmpp->getInterval() / 1000 + 5);
tmpp->iAmLocal = false; tmpp->iAmLocal = false;
IoTItems.push_back(tmpp); IoTItems.push_back(tmpp);
generateEvent(tmpp->getID(), tmpp->getValue()); generateEvent(tmpp->getID(), tmpp->getValue());
@@ -271,7 +274,7 @@ IoTItem* createItemFromNet(const String& msgFromNet) {
} }
void analyzeMsgFromNet(const String& msg, String altId) { void analyzeMsgFromNet(const String& msg, String altId) {
if (!jsonRead(msg, F("id"), altId, altId == "") && altId == "") return; // ничего не предпринимаем, если ошибка и altId = "", вообще данная конструкция нужна для совместимости с форматом данных 3 версией if (!jsonRead(msg, F("id"), altId, altId == "") && altId == "") return; // ничего не предпринимаем, если ошибка и altId = "", вообще данная конструкция нужна для совместимости с форматом данных 3 версией
IoTItem* itemExist = findIoTItem(altId); IoTItem* itemExist = findIoTItem(altId);
if (itemExist) { if (itemExist) {
String valAsStr = msg; String valAsStr = msg;
@@ -279,9 +282,9 @@ void analyzeMsgFromNet(const String& msg, String altId) {
long interval = 0; long interval = 0;
jsonRead(msg, F("int"), interval, false); jsonRead(msg, F("int"), interval, false);
itemExist->setInterval(interval); // устанавливаем такой же интервал как на источнике события itemExist->setInterval(interval); // устанавливаем такой же интервал как на источнике события
itemExist->setValue(valAsStr, false); // только регистрируем изменения в интерфейсе без создания цикла сетевых событий itemExist->setValue(valAsStr, false); // только регистрируем изменения в интерфейсе без создания цикла сетевых событий
if (interval) itemExist->setIntFromNet(interval+5); // если пришедший интервал =0, значит не нужно контролировать доверие, иначе даем фору в 5 сек if (interval) itemExist->setIntFromNet(interval + 5); // если пришедший интервал =0, значит не нужно контролировать доверие, иначе даем фору в 5 сек
generateEvent(altId, valAsStr); generateEvent(altId, valAsStr);
} else { } else {
// временно зафиксируем данные в базе, если локально элемент отсутствует // временно зафиксируем данные в базе, если локально элемент отсутствует

View File

@@ -3,6 +3,7 @@
#include "Arduino.h" #include "Arduino.h"
#include "MySensorsGate.h" #include "MySensorsGate.h"
#ifdef MYSENSORS
// callback библиотеки mysensors // callback библиотеки mysensors
void receive(const MyMessage& message) { void receive(const MyMessage& message) {
String inMsg = String(message.getSender()) + "," + // node-id String inMsg = String(message.getSender()) + "," + // node-id
@@ -48,6 +49,7 @@ String parseToString(const MyMessage& message) {
} }
} }
#endif
class MySensorsGate : public IoTItem { class MySensorsGate : public IoTItem {
private: private:
public: public:
@@ -344,20 +346,57 @@ class MySensorsGate : public IoTItem {
class MySensorsNode : public IoTItem { class MySensorsNode : public IoTItem {
private: private:
String id;
int _minutesPassed = 0;
String json = "{}";
bool dataFromNode = false;
public: public:
MySensorsNode(String parameters) : IoTItem(parameters) { MySensorsNode(String parameters) : IoTItem(parameters) {
SerialPrint("i", "MySensors", "Node initialized"); SerialPrint("i", "MySensors", "Node initialized");
jsonRead(parameters, F("id"), id);
dataFromNode = false;
} }
void setValue(const IoTValue& Value, bool genEvent = true) { void setValue(const IoTValue& Value, bool genEvent = true) {
value = Value; value = Value;
regEvent(value.valD, "MySensorsNode", false, genEvent); regEvent(value.valD, "MySensorsNode", false, genEvent);
_minutesPassed = 0;
prevMillis = millis();
dataFromNode = true;
setNewWidgetAttributes();
} }
void doByInterval() { void doByInterval() {
_minutesPassed++;
setNewWidgetAttributes();
} }
void loop() { void loop() {
currentMillis = millis();
difference = currentMillis - prevMillis;
if (difference > 60000) {
prevMillis = millis();
this->doByInterval();
}
}
void handleSendSubWidgetsValues() {
setNewWidgetAttributes();
}
void setNewWidgetAttributes() {
if (dataFromNode) {
jsonWriteStr(json, "info", String(_minutesPassed) + " min");
if (_minutesPassed >= 60) {
jsonWriteStr(json, "color", "orange"); //сделаем виджет оранжевым когда более 60 минут нода не выходила на связь
} else if (_minutesPassed >= 120) {
jsonWriteStr(json, "color", "red"); //сделаем виджет красным когда более 120 минут нода не выходила на связь
}
} else {
jsonWriteStr(json, "info", "awaiting");
}
sendSubWidgetsValues(id, json);
} }
~MySensorsNode(){}; ~MySensorsNode(){};

View File

@@ -1,4 +1,22 @@
#pragma once #pragma once
#include "Const.h"
#ifdef MYSENSORS
/*
* DESCRIPTION
* The ESP32 gateway sends data received from sensors to the WiFi link.
* The gateway also accepts input on ethernet interface, which is then sent out to the radio network.
* ----------- PINOUT --------------
* | IO | RF24 | RFM69 | RFM95 |
|------|------|-------|-------|
| MOSI | 23 | 23 | 23 |
| MISO | 19 | 19 | 19 |
| SCK | 18 | 18 | 18 |
| CSN | 5 | 5 | 5 |
| CE | 17 | - | - |
| RST | - | 17 | 17 |
| IRQ | 16* | 16 | 16 |
*/
// Enable debug prints to serial monitor // Enable debug prints to serial monitor
//#define MY_DEBUG //#define MY_DEBUG
@@ -7,7 +25,6 @@
//#define MY_RF24_CS_PIN 9 //#define MY_RF24_CS_PIN 9
// Use a bit lower baudrate for serial prints on ESP8266 than default in MyConfig.h // Use a bit lower baudrate for serial prints on ESP8266 than default in MyConfig.h
#define MY_BAUD_RATE 115200 #define MY_BAUD_RATE 115200
// Enables and select radio type (if attached) // Enables and select radio type (if attached)
@@ -22,7 +39,7 @@
// Set LOW transmit power level as default, if you have an amplified NRF-module and // Set LOW transmit power level as default, if you have an amplified NRF-module and
// power your radio separately with a good regulator you can turn up PA level. // power your radio separately with a good regulator you can turn up PA level.
#define MY_RF24_PA_LEVEL RF24_PA_LOW #define MY_RF24_PA_LEVEL RF24_PA_MAX
// используем гейт в режиме serial хотя нам этот режим не нужен, поэтому в библиотеки отключаем MY_SERIALDEVICE.print // используем гейт в режиме serial хотя нам этот режим не нужен, поэтому в библиотеки отключаем MY_SERIALDEVICE.print
// в файле MyGatewayTransportSerial.cpp в строчке 35 // в файле MyGatewayTransportSerial.cpp в строчке 35
@@ -33,3 +50,5 @@
#include <MySensors.h> #include <MySensors.h>
extern String parseToString(const MyMessage& message); extern String parseToString(const MyMessage& message);
#endif

View File

@@ -32,17 +32,16 @@
"moduleVersion": "1.0", "moduleVersion": "1.0",
"usedRam": { "usedRam": {
"esp32_4mb": 15, "esp32_4mb": 15,
"esp8266_4mb": 15 "esp8266_4mb": 0
}, },
"title": "My Sensors Gate", "title": "Гейт MySensors",
"moduleDesc": "", "moduleDesc": "Гейт состоит из esp32 и подключенному к нему радиомодулю NRF24L01. Вместе в связке они образуют гейт, способный принимать данные датчиков. Датчики способны работать до нескольких лет на батарейках",
"retInfo": "", "retInfo": "",
"propInfo": { "propInfo": {
"int": "", "id": "Для настройки следует выбрать один раз MySensorsGate и выбрать сколько необходимо раз MySensorsNode. Вместо ID нужно указать ID ноды дефис ID значения данной ноды. Например 100-1 - будет значить нода с ID 100 величина 1."
"pin": ""
} }
}, },
"defActive": true, "defActive": false,
"usedLibs": { "usedLibs": {
"esp32_4mb": [] "esp32_4mb": []
} }