2021-06-09 21:44:49 +00:00
|
|
|
/* Copyright (C) 2021 Avamander
|
|
|
|
|
|
|
|
This file is part of InfiniTime.
|
|
|
|
|
|
|
|
InfiniTime is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published
|
|
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
InfiniTime is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
#include <qcbor/qcbor_spiffy_decode.h>
|
|
|
|
#include "WeatherService.h"
|
|
|
|
#include "libs/QCBOR/inc/qcbor/qcbor.h"
|
|
|
|
#include "systemtask/SystemTask.h"
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
int WeatherCallback(uint16_t connHandle, uint16_t attrHandle, struct ble_gatt_access_ctxt* ctxt, void* arg) {
|
|
|
|
return static_cast<Pinetime::Controllers::WeatherService*>(arg)->OnCommand(connHandle, attrHandle, ctxt);
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
namespace Pinetime {
|
|
|
|
namespace Controllers {
|
|
|
|
WeatherService::WeatherService(System::SystemTask& system, DateTime& dateTimeController)
|
|
|
|
: system(system), dateTimeController(dateTimeController) {
|
|
|
|
}
|
|
|
|
|
|
|
|
void WeatherService::Init() {
|
|
|
|
uint8_t res = 0;
|
|
|
|
res = ble_gatts_count_cfg(serviceDefinition);
|
2021-06-20 18:37:53 +00:00
|
|
|
ASSERT(res == 0);
|
2021-06-09 21:44:49 +00:00
|
|
|
|
|
|
|
res = ble_gatts_add_svcs(serviceDefinition);
|
|
|
|
ASSERT(res == 0);
|
|
|
|
}
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
int WeatherService::OnCommand(uint16_t connHandle, uint16_t attrHandle, struct ble_gatt_access_ctxt* ctxt) {
|
2021-06-16 20:31:17 +00:00
|
|
|
// TODO: Detect control messages
|
2021-06-09 21:44:49 +00:00
|
|
|
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
|
|
|
const auto packetLen = OS_MBUF_PKTLEN(ctxt->om);
|
|
|
|
if (packetLen <= 0) {
|
|
|
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
|
|
|
}
|
|
|
|
// Decode
|
|
|
|
QCBORDecodeContext decodeContext;
|
2021-11-28 18:58:28 +00:00
|
|
|
UsefulBufC encodedCbor = {ctxt->om->om_data, OS_MBUF_PKTLEN(ctxt->om)};
|
2021-06-16 20:31:17 +00:00
|
|
|
|
|
|
|
QCBORDecode_Init(&decodeContext, encodedCbor, QCBOR_DECODE_MODE_NORMAL);
|
2021-11-28 18:58:28 +00:00
|
|
|
// KINDLY provide us a fixed-length map
|
2021-06-09 21:44:49 +00:00
|
|
|
QCBORDecode_EnterMap(&decodeContext, nullptr);
|
|
|
|
// Always encodes to the smallest number of bytes based on the value
|
2021-06-16 20:31:17 +00:00
|
|
|
int64_t tmpTimestamp = 0;
|
|
|
|
QCBORDecode_GetInt64InMapSZ(&decodeContext, "Timestamp", &tmpTimestamp);
|
2021-11-28 18:58:28 +00:00
|
|
|
if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS) {
|
|
|
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
|
|
|
}
|
2021-06-16 20:31:17 +00:00
|
|
|
int64_t tmpExpires = 0;
|
|
|
|
QCBORDecode_GetInt64InMapSZ(&decodeContext, "Expires", &tmpExpires);
|
2021-06-20 18:37:53 +00:00
|
|
|
if (tmpExpires < 0 || tmpExpires > 4294967295) {
|
2021-06-16 20:31:17 +00:00
|
|
|
// TODO: Return better error?
|
|
|
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
|
|
|
}
|
|
|
|
int64_t tmpEventType = 0;
|
|
|
|
QCBORDecode_GetInt64InMapSZ(&decodeContext, "EventType", &tmpEventType);
|
2021-06-20 18:37:53 +00:00
|
|
|
if (tmpEventType < 0 || tmpEventType > static_cast<int64_t>(WeatherData::eventtype::Length)) {
|
2021-06-16 20:31:17 +00:00
|
|
|
// TODO: Return better error?
|
|
|
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (static_cast<WeatherData::eventtype>(tmpEventType)) {
|
|
|
|
// TODO: Populate
|
2021-06-09 21:44:49 +00:00
|
|
|
case WeatherData::eventtype::AirQuality: {
|
2021-06-16 20:31:17 +00:00
|
|
|
std::unique_ptr<WeatherData::AirQuality> airquality = std::make_unique<WeatherData::AirQuality>();
|
|
|
|
airquality->timestamp = tmpTimestamp;
|
|
|
|
airquality->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
airquality->expires = tmpExpires;
|
2021-06-20 18:37:53 +00:00
|
|
|
UsefulBufC String;
|
|
|
|
QCBORDecode_GetTextStringInMapSZ(&decodeContext, "Polluter", &String);
|
|
|
|
if (UsefulBuf_IsNULLOrEmptyC(String) != 0) {
|
|
|
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
|
|
|
}
|
2021-11-28 13:33:06 +00:00
|
|
|
airquality->polluter = std::string(static_cast<const char*>(String.ptr), String.len);
|
2021-06-20 18:37:53 +00:00
|
|
|
int64_t tmpAmount = 0;
|
|
|
|
QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount);
|
2021-08-21 18:58:03 +00:00
|
|
|
if (tmpAmount < 0) {
|
2021-06-20 18:37:53 +00:00
|
|
|
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
|
|
|
}
|
|
|
|
airquality->amount = tmpAmount;
|
2021-06-16 20:31:17 +00:00
|
|
|
timeline.push_back(std::move(airquality));
|
2021-06-09 21:44:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case WeatherData::eventtype::Obscuration: {
|
2021-06-16 20:31:17 +00:00
|
|
|
std::unique_ptr<WeatherData::Obscuration> obscuration = std::make_unique<WeatherData::Obscuration>();
|
|
|
|
obscuration->timestamp = tmpTimestamp;
|
|
|
|
obscuration->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
obscuration->expires = tmpExpires;
|
|
|
|
|
|
|
|
timeline.push_back(std::move(obscuration));
|
2021-06-09 21:44:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case WeatherData::eventtype::Precipitation: {
|
2021-06-16 20:31:17 +00:00
|
|
|
std::unique_ptr<WeatherData::Precipitation> precipitation = std::make_unique<WeatherData::Precipitation>();
|
|
|
|
precipitation->timestamp = tmpTimestamp;
|
|
|
|
precipitation->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
precipitation->expires = tmpExpires;
|
|
|
|
timeline.push_back(std::move(precipitation));
|
2021-06-09 21:44:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case WeatherData::eventtype::Wind: {
|
2021-06-16 20:31:17 +00:00
|
|
|
std::unique_ptr<WeatherData::Wind> wind = std::make_unique<WeatherData::Wind>();
|
|
|
|
wind->timestamp = tmpTimestamp;
|
|
|
|
wind->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
wind->expires = tmpExpires;
|
|
|
|
timeline.push_back(std::move(wind));
|
2021-06-09 21:44:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case WeatherData::eventtype::Temperature: {
|
2021-06-16 20:31:17 +00:00
|
|
|
std::unique_ptr<WeatherData::Temperature> temperature = std::make_unique<WeatherData::Temperature>();
|
|
|
|
temperature->timestamp = tmpTimestamp;
|
|
|
|
temperature->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
temperature->expires = tmpExpires;
|
|
|
|
timeline.push_back(std::move(temperature));
|
2021-06-09 21:44:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case WeatherData::eventtype::Special: {
|
2021-06-16 20:31:17 +00:00
|
|
|
std::unique_ptr<WeatherData::Special> special = std::make_unique<WeatherData::Special>();
|
|
|
|
special->timestamp = tmpTimestamp;
|
|
|
|
special->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
special->expires = tmpExpires;
|
|
|
|
timeline.push_back(std::move(special));
|
2021-06-09 21:44:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case WeatherData::eventtype::Pressure: {
|
2021-06-16 20:31:17 +00:00
|
|
|
std::unique_ptr<WeatherData::Pressure> pressure = std::make_unique<WeatherData::Pressure>();
|
|
|
|
pressure->timestamp = tmpTimestamp;
|
|
|
|
pressure->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
pressure->expires = tmpExpires;
|
|
|
|
timeline.push_back(std::move(pressure));
|
2021-06-09 21:44:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case WeatherData::eventtype::Location: {
|
2021-06-16 20:31:17 +00:00
|
|
|
std::unique_ptr<WeatherData::Location> location = std::make_unique<WeatherData::Location>();
|
|
|
|
location->timestamp = tmpTimestamp;
|
|
|
|
location->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
location->expires = tmpExpires;
|
|
|
|
timeline.push_back(std::move(location));
|
2021-06-09 21:44:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case WeatherData::eventtype::Clouds: {
|
2021-06-16 20:31:17 +00:00
|
|
|
std::unique_ptr<WeatherData::Clouds> clouds = std::make_unique<WeatherData::Clouds>();
|
|
|
|
clouds->timestamp = tmpTimestamp;
|
|
|
|
clouds->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
clouds->expires = tmpExpires;
|
|
|
|
timeline.push_back(std::move(clouds));
|
2021-06-09 21:44:49 +00:00
|
|
|
break;
|
|
|
|
}
|
2021-08-21 18:58:03 +00:00
|
|
|
case WeatherData::eventtype::Humidity: {
|
|
|
|
std::unique_ptr<WeatherData::Humidity> humidity = std::make_unique<WeatherData::Humidity>();
|
|
|
|
humidity->timestamp = tmpTimestamp;
|
|
|
|
humidity->eventType = static_cast<WeatherData::eventtype>(tmpEventType);
|
|
|
|
humidity->expires = tmpExpires;
|
|
|
|
timeline.push_back(std::move(humidity));
|
|
|
|
break;
|
|
|
|
}
|
2021-06-09 21:44:49 +00:00
|
|
|
default: {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-06-16 20:31:17 +00:00
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
GetCurrentPressure();
|
|
|
|
TidyTimeline();
|
|
|
|
GetTimelineLength();
|
2021-06-09 21:44:49 +00:00
|
|
|
QCBORDecode_ExitMap(&decodeContext);
|
|
|
|
|
2021-11-28 18:58:28 +00:00
|
|
|
if (QCBORDecode_Finish(&decodeContext) != QCBOR_SUCCESS) {
|
2021-06-09 21:44:49 +00:00
|
|
|
return BLE_ATT_ERR_INSUFFICIENT_RES;
|
|
|
|
}
|
|
|
|
} else if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
|
|
|
|
// Encode
|
|
|
|
uint8_t buffer[64];
|
|
|
|
QCBOREncodeContext encodeContext;
|
|
|
|
QCBOREncode_Init(&encodeContext, UsefulBuf_FROM_BYTE_ARRAY(buffer));
|
|
|
|
QCBOREncode_OpenMap(&encodeContext);
|
|
|
|
QCBOREncode_AddTextToMap(&encodeContext, "test", UsefulBuf_FROM_SZ_LITERAL("test"));
|
|
|
|
QCBOREncode_AddInt64ToMap(&encodeContext, "test", 1ul);
|
|
|
|
QCBOREncode_CloseMap(&encodeContext);
|
|
|
|
|
|
|
|
UsefulBufC encodedEvent;
|
|
|
|
auto uErr = QCBOREncode_Finish(&encodeContext, &encodedEvent);
|
|
|
|
if (uErr != 0) {
|
|
|
|
return BLE_ATT_ERR_INSUFFICIENT_RES;
|
|
|
|
}
|
|
|
|
auto res = os_mbuf_append(ctxt->om, &buffer, sizeof(buffer));
|
|
|
|
if (res == 0) {
|
|
|
|
return BLE_ATT_ERR_INSUFFICIENT_RES;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
WeatherData::Clouds WeatherService::GetCurrentClouds() const {
|
2021-08-21 18:58:03 +00:00
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == WeatherData::eventtype::Clouds && header->timestamp + header->expires <= currentTimestamp) {
|
|
|
|
return reinterpret_cast<const WeatherData::Clouds&>(header);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
2021-06-24 22:18:56 +00:00
|
|
|
|
|
|
|
WeatherData::Obscuration WeatherService::GetCurrentObscuration() const {
|
2021-08-21 18:58:03 +00:00
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == WeatherData::eventtype::Obscuration && header->timestamp + header->expires <= currentTimestamp) {
|
|
|
|
return reinterpret_cast<const WeatherData::Obscuration&>(header);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
2021-06-24 22:18:56 +00:00
|
|
|
|
|
|
|
WeatherData::Precipitation WeatherService::GetCurrentPrecipitation() const {
|
2021-08-21 18:58:03 +00:00
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == WeatherData::eventtype::Precipitation && header->timestamp + header->expires <= currentTimestamp) {
|
|
|
|
return reinterpret_cast<const WeatherData::Precipitation&>(header);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
2021-06-24 22:18:56 +00:00
|
|
|
|
|
|
|
WeatherData::Wind WeatherService::GetCurrentWind() const {
|
2021-08-21 18:58:03 +00:00
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == WeatherData::eventtype::Wind && header->timestamp + header->expires <= currentTimestamp) {
|
|
|
|
return reinterpret_cast<const WeatherData::Wind&>(header);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
2021-06-24 22:18:56 +00:00
|
|
|
|
|
|
|
WeatherData::Temperature WeatherService::GetCurrentTemperature() const {
|
2021-08-21 18:58:03 +00:00
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == WeatherData::eventtype::Temperature && header->timestamp + header->expires <= currentTimestamp) {
|
|
|
|
return reinterpret_cast<const WeatherData::Temperature&>(header);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
2021-06-24 22:18:56 +00:00
|
|
|
|
|
|
|
WeatherData::Humidity WeatherService::GetCurrentHumidity() const {
|
2021-08-21 18:58:03 +00:00
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == WeatherData::eventtype::Humidity && header->timestamp + header->expires <= currentTimestamp) {
|
|
|
|
return reinterpret_cast<const WeatherData::Humidity&>(header);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
2021-06-24 22:18:56 +00:00
|
|
|
|
|
|
|
WeatherData::Pressure WeatherService::GetCurrentPressure() const {
|
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
2021-06-09 21:44:49 +00:00
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == WeatherData::eventtype::Pressure && header->timestamp + header->expires <= currentTimestamp) {
|
2021-08-21 18:58:03 +00:00
|
|
|
return reinterpret_cast<const WeatherData::Pressure&>(header);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
WeatherData::Location WeatherService::GetCurrentLocation() const {
|
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == WeatherData::eventtype::Location && header->timestamp + header->expires <= currentTimestamp) {
|
|
|
|
return reinterpret_cast<const WeatherData::Location&>(header);
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
|
|
|
}
|
2021-08-21 18:58:03 +00:00
|
|
|
return {};
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
WeatherData::AirQuality WeatherService::GetCurrentQuality() const {
|
2021-08-21 18:58:03 +00:00
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == WeatherData::eventtype::AirQuality && header->timestamp + header->expires <= currentTimestamp) {
|
|
|
|
return reinterpret_cast<const WeatherData::AirQuality&>(header);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
size_t WeatherService::GetTimelineLength() const {
|
2021-06-09 21:44:49 +00:00
|
|
|
return timeline.size();
|
|
|
|
}
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
bool WeatherService::AddEventToTimeline(std::unique_ptr<WeatherData::TimelineHeader> event) {
|
2021-06-09 21:44:49 +00:00
|
|
|
if (timeline.size() == timeline.max_size()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
timeline.push_back(std::move(event));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
bool WeatherService::HasTimelineEventOfType(const WeatherData::eventtype type) const {
|
|
|
|
uint64_t currentTimestamp = GetCurrentUnixTimestamp();
|
2021-06-09 21:44:49 +00:00
|
|
|
for (auto&& header : timeline) {
|
|
|
|
if (header->eventType == type && header->timestamp + header->expires <= currentTimestamp) {
|
|
|
|
// TODO: Check if its currently valid
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
void WeatherService::TidyTimeline() {
|
2021-06-09 21:44:49 +00:00
|
|
|
uint64_t timeCurrent = 0;
|
|
|
|
timeline.erase(std::remove_if(std::begin(timeline),
|
|
|
|
std::end(timeline),
|
2021-06-16 20:31:17 +00:00
|
|
|
[&](std::unique_ptr<WeatherData::TimelineHeader> const& header) {
|
2021-06-09 21:44:49 +00:00
|
|
|
return header->timestamp + header->expires > timeCurrent;
|
|
|
|
}),
|
|
|
|
std::end(timeline));
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
std::sort(std::begin(timeline), std::end(timeline), CompareTimelineEvents);
|
2021-06-09 21:44:49 +00:00
|
|
|
}
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
bool WeatherService::CompareTimelineEvents(const std::unique_ptr<WeatherData::TimelineHeader>& first,
|
2021-06-16 20:31:17 +00:00
|
|
|
const std::unique_ptr<WeatherData::TimelineHeader>& second) {
|
2021-06-09 21:44:49 +00:00
|
|
|
return first->timestamp > second->timestamp;
|
|
|
|
}
|
|
|
|
|
2021-06-24 22:18:56 +00:00
|
|
|
uint64_t WeatherService::GetCurrentUnixTimestamp() const {
|
2021-06-09 21:44:49 +00:00
|
|
|
return std::chrono::duration_cast<std::chrono::seconds>(dateTimeController.CurrentDateTime().time_since_epoch()).count();
|
|
|
|
}
|
|
|
|
}
|
2021-06-24 23:52:59 +00:00
|
|
|
}
|