diff options
Diffstat (limited to 'src/components/ble')
-rw-r--r-- | src/components/ble/AlertNotificationService.cpp | 3 | ||||
-rw-r--r-- | src/components/ble/BatteryInformationService.cpp | 2 | ||||
-rw-r--r-- | src/components/ble/BleController.h | 11 | ||||
-rw-r--r-- | src/components/ble/NimbleController.cpp | 238 | ||||
-rw-r--r-- | src/components/ble/NimbleController.h | 23 | ||||
-rw-r--r-- | src/components/ble/weather/WeatherData.h | 385 | ||||
-rw-r--r-- | src/components/ble/weather/WeatherService.cpp | 604 | ||||
-rw-r--r-- | src/components/ble/weather/WeatherService.h | 172 |
8 files changed, 1390 insertions, 48 deletions
diff --git a/src/components/ble/AlertNotificationService.cpp b/src/components/ble/AlertNotificationService.cpp index f616cce8..04819122 100644 --- a/src/components/ble/AlertNotificationService.cpp +++ b/src/components/ble/AlertNotificationService.cpp @@ -53,8 +53,9 @@ int AlertNotificationService::OnAlert(uint16_t conn_handle, uint16_t attr_handle // Ignore notifications with empty message const auto packetLen = OS_MBUF_PKTLEN(ctxt->om); - if (packetLen <= headerSize) + if (packetLen <= headerSize) { return 0; + } size_t bufferSize = std::min(packetLen + stringTerminatorSize, maxBufferSize); auto messageSize = std::min(maxMessageSize, (bufferSize - headerSize)); diff --git a/src/components/ble/BatteryInformationService.cpp b/src/components/ble/BatteryInformationService.cpp index 9a3f86f5..82df7b15 100644 --- a/src/components/ble/BatteryInformationService.cpp +++ b/src/components/ble/BatteryInformationService.cpp @@ -17,7 +17,7 @@ BatteryInformationService::BatteryInformationService(Controllers::Battery& batte characteristicDefinition {{.uuid = &batteryLevelUuid.u, .access_cb = BatteryInformationServiceCallback, .arg = this, - .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_READ_ENC | BLE_GATT_CHR_F_READ_AUTHEN | BLE_GATT_CHR_F_NOTIFY, .val_handle = &batteryLevelHandle}, {0}}, serviceDefinition { diff --git a/src/components/ble/BleController.h b/src/components/ble/BleController.h index 2cba26a9..72b87663 100644 --- a/src/components/ble/BleController.h +++ b/src/components/ble/BleController.h @@ -9,7 +9,7 @@ namespace Pinetime { public: using BleAddress = std::array<uint8_t, 6>; enum class FirmwareUpdateStates { Idle, Running, Validated, Error }; - enum class AddressTypes { Public, Random }; + enum class AddressTypes { Public, Random, RPA_Public, RPA_Random }; Ble() = default; bool IsConnected() const { @@ -48,6 +48,12 @@ namespace Pinetime { void AddressType(AddressTypes t) { addressType = t; } + void SetPairingKey(uint32_t k) { + pairingKey = k; + } + uint32_t GetPairingKey() const { + return pairingKey; + } private: bool isConnected = false; @@ -57,6 +63,7 @@ namespace Pinetime { FirmwareUpdateStates firmwareUpdateState = FirmwareUpdateStates::Idle; BleAddress address; AddressTypes addressType; + uint32_t pairingKey = 0; }; } -}
\ No newline at end of file +} diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp index 43a8b0d6..acf4f94b 100644 --- a/src/components/ble/NimbleController.cpp +++ b/src/components/ble/NimbleController.cpp @@ -1,4 +1,6 @@ #include "components/ble/NimbleController.h" +#include <cstring> + #include <hal/nrf_rtc.h> #define min // workaround: nimble's min/max macros conflict with libstdc++ #define max @@ -6,13 +8,16 @@ #include <host/ble_hs.h> #include <host/ble_hs_id.h> #include <host/util/util.h> -#undef max -#undef min +#include <controller/ble_ll.h> +#include <controller/ble_hw.h> #include <services/gap/ble_svc_gap.h> #include <services/gatt/ble_svc_gatt.h> +#undef max +#undef min #include "components/ble/BleController.h" #include "components/ble/NotificationManager.h" #include "components/datetime/DateTimeController.h" +#include "components/fs/FS.h" #include "systemtask/SystemTask.h" using namespace Pinetime::Controllers; @@ -24,37 +29,43 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask, Controllers::Battery& batteryController, Pinetime::Drivers::SpiNorFlash& spiNorFlash, Controllers::HeartRateController& heartRateController, - Controllers::MotionController& motionController) + Controllers::MotionController& motionController, + Pinetime::Controllers::FS& fs) : systemTask {systemTask}, bleController {bleController}, dateTimeController {dateTimeController}, notificationManager {notificationManager}, spiNorFlash {spiNorFlash}, + fs {fs}, dfuService {systemTask, bleController, spiNorFlash}, + currentTimeClient {dateTimeController}, anService {systemTask, notificationManager}, alertNotificationClient {systemTask, notificationManager}, currentTimeService {dateTimeController}, musicService {systemTask}, + weatherService {systemTask, dateTimeController}, navService {systemTask}, batteryInformationService {batteryController}, immediateAlertService {systemTask, notificationManager}, heartRateService {systemTask, heartRateController}, - motionService{systemTask, motionController}, + motionService {systemTask, motionController}, serviceDiscovery({¤tTimeClient, &alertNotificationClient}) { } void nimble_on_reset(int reason) { - NRF_LOG_INFO("Resetting state; reason=%d\n", reason); + NRF_LOG_INFO("Nimble lost sync, resetting state; reason=%d", reason); } void nimble_on_sync(void) { - int rc; + int rc; + + NRF_LOG_INFO("Nimble is synced"); - rc = ble_hs_util_ensure_addr(0); - ASSERT(rc == 0); + rc = ble_hs_util_ensure_addr(0); + ASSERT(rc == 0); - nptr->StartAdvertising(); + nptr->StartAdvertising(); } int GAPEventCallback(struct ble_gap_event* event, void* arg) { @@ -69,6 +80,7 @@ void NimbleController::Init() { nptr = this; ble_hs_cfg.reset_cb = nimble_on_reset; ble_hs_cfg.sync_cb = nimble_on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; ble_svc_gap_init(); ble_svc_gatt_init(); @@ -77,6 +89,7 @@ void NimbleController::Init() { currentTimeClient.Init(); currentTimeService.Init(); musicService.Init(); + weatherService.Init(); navService.Init(); anService.Init(); dfuService.Init(); @@ -97,28 +110,38 @@ void NimbleController::Init() { Pinetime::Controllers::Ble::BleAddress address; rc = ble_hs_id_copy_addr(addrType, address.data(), nullptr); ASSERT(rc == 0); - bleController.AddressType((addrType == 0) ? Ble::AddressTypes::Public : Ble::AddressTypes::Random); + bleController.Address(std::move(address)); + switch (addrType) { + case BLE_OWN_ADDR_PUBLIC: + bleController.AddressType(Ble::AddressTypes::Public); + break; + case BLE_OWN_ADDR_RANDOM: + bleController.AddressType(Ble::AddressTypes::Random); + break; + case BLE_OWN_ADDR_RPA_PUBLIC_DEFAULT: + bleController.AddressType(Ble::AddressTypes::RPA_Public); + break; + case BLE_OWN_ADDR_RPA_RANDOM_DEFAULT: + bleController.AddressType(Ble::AddressTypes::RPA_Random); + break; + } rc = ble_gatts_start(); ASSERT(rc == 0); - if (!ble_gap_adv_active() && !bleController.IsConnected()) + RestoreBond(); + + if (!ble_gap_adv_active() && !bleController.IsConnected()) { StartAdvertising(); + } } void NimbleController::StartAdvertising() { - int rc; - - /* set adv parameters */ struct ble_gap_adv_params adv_params; struct ble_hs_adv_fields fields; - /* advertising payload is split into advertising data and advertising - response, because all data cannot fit into single packet; name of device - is sent as response to scan request */ struct ble_hs_adv_fields rsp_fields; - /* fill all fields and parameters with zeros */ memset(&adv_params, 0, sizeof(adv_params)); memset(&fields, 0, sizeof(fields)); memset(&rsp_fields, 0, sizeof(rsp_fields)); @@ -141,10 +164,11 @@ void NimbleController::StartAdvertising() { fields.uuids128_is_complete = 1; fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; - rsp_fields.name = (uint8_t*) deviceName; + rsp_fields.name = reinterpret_cast<const uint8_t*>(deviceName); rsp_fields.name_len = strlen(deviceName); rsp_fields.name_is_complete = 1; + int rc; rc = ble_gap_adv_set_fields(&fields); ASSERT(rc == 0); @@ -159,15 +183,14 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { switch (event->type) { case BLE_GAP_EVENT_ADV_COMPLETE: NRF_LOG_INFO("Advertising event : BLE_GAP_EVENT_ADV_COMPLETE"); - NRF_LOG_INFO("reason=%d; status=%d", event->adv_complete.reason, event->connect.status); + NRF_LOG_INFO("reason=%d; status=%0X", event->adv_complete.reason, event->connect.status); StartAdvertising(); break; case BLE_GAP_EVENT_CONNECT: - NRF_LOG_INFO("Advertising event : BLE_GAP_EVENT_CONNECT"); - /* A new connection was established or a connection attempt failed. */ - NRF_LOG_INFO("connection %s; status=%d ", event->connect.status == 0 ? "established" : "failed", event->connect.status); + NRF_LOG_INFO("Connect event : BLE_GAP_EVENT_CONNECT"); + NRF_LOG_INFO("connection %s; status=%0X ", event->connect.status == 0 ? "established" : "failed", event->connect.status); if (event->connect.status != 0) { /* Connection failed; resume advertising. */ @@ -186,10 +209,14 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { break; case BLE_GAP_EVENT_DISCONNECT: - NRF_LOG_INFO("Advertising event : BLE_GAP_EVENT_DISCONNECT"); + /* Connection terminated; resume advertising. */ + NRF_LOG_INFO("Disconnect event : BLE_GAP_EVENT_DISCONNECT"); NRF_LOG_INFO("disconnect reason=%d", event->disconnect.reason); - /* Connection terminated; resume advertising. */ + if (event->disconnect.conn.sec_state.bonded) { + PersistBond(event->disconnect.conn); + } + currentTimeClient.Reset(); alertNotificationClient.Reset(); connectionHandle = BLE_HS_CONN_HANDLE_NONE; @@ -199,18 +226,67 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { break; case BLE_GAP_EVENT_CONN_UPDATE: - NRF_LOG_INFO("Advertising event : BLE_GAP_EVENT_CONN_UPDATE"); /* The central has updated the connection parameters. */ - NRF_LOG_INFO("update status=%d ", event->conn_update.status); + NRF_LOG_INFO("Update event : BLE_GAP_EVENT_CONN_UPDATE"); + NRF_LOG_INFO("update status=%0X ", event->conn_update.status); + break; + + case BLE_GAP_EVENT_CONN_UPDATE_REQ: + /* The central has requested updated connection parameters */ + NRF_LOG_INFO("Update event : BLE_GAP_EVENT_CONN_UPDATE_REQ"); + NRF_LOG_INFO("update request : itvl_min=%d itvl_max=%d latency=%d supervision=%d", + event->conn_update_req.peer_params->itvl_min, + event->conn_update_req.peer_params->itvl_max, + event->conn_update_req.peer_params->latency, + event->conn_update_req.peer_params->supervision_timeout); break; case BLE_GAP_EVENT_ENC_CHANGE: /* Encryption has been enabled or disabled for this connection. */ - NRF_LOG_INFO("encryption change event; status=%d ", event->enc_change.status); + NRF_LOG_INFO("Security event : BLE_GAP_EVENT_ENC_CHANGE"); + NRF_LOG_INFO("encryption change event; status=%0X ", event->enc_change.status); + + if (event->enc_change.status == 0) { + struct ble_gap_conn_desc desc; + ble_gap_conn_find(event->enc_change.conn_handle, &desc); + if (desc.sec_state.bonded) { + PersistBond(desc); + } + + NRF_LOG_INFO("new state: encrypted=%d authenticated=%d bonded=%d key_size=%d", + desc.sec_state.encrypted, + desc.sec_state.authenticated, + desc.sec_state.bonded, + desc.sec_state.key_size); + } + break; + + case BLE_GAP_EVENT_PASSKEY_ACTION: + /* Authentication has been requested for this connection. + * + * BLE authentication is determined by the combination of I/O capabilities + * on the central and peripheral. When the peripheral is display only and + * the central has a keyboard and display then passkey auth is selected. + * When both the central and peripheral have displays and support yes/no + * buttons then numeric comparison is selected. We currently advertise + * display capability only so we only handle the "display" action here. + * + * Standards insist that the rand() PRNG be deterministic. + * Use the nimble TRNG here since rand() is predictable. + */ + NRF_LOG_INFO("Security event : BLE_GAP_EVENT_PASSKEY_ACTION"); + if (event->passkey.params.action == BLE_SM_IOACT_DISP) { + struct ble_sm_io pkey = {0}; + pkey.action = event->passkey.params.action; + pkey.passkey = ble_ll_rand() % 1000000; + bleController.SetPairingKey(pkey.passkey); + systemTask.PushMessage(Pinetime::System::Messages::OnPairing); + ble_sm_inject_io(event->passkey.conn_handle, &pkey); + } break; case BLE_GAP_EVENT_SUBSCRIBE: - NRF_LOG_INFO("subscribe event; conn_handle=%d attr_handle=%d " + NRF_LOG_INFO("Subscribe event; conn_handle=%d attr_handle=%d " "reason=%d prevn=%d curn=%d previ=%d curi=???\n", event->subscribe.conn_handle, event->subscribe.attr_handle, @@ -219,26 +295,24 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { event->subscribe.cur_notify, event->subscribe.prev_indicate); - if(event->subscribe.reason == BLE_GAP_SUBSCRIBE_REASON_TERM) { + if (event->subscribe.reason == BLE_GAP_SUBSCRIBE_REASON_TERM) { heartRateService.UnsubscribeNotification(event->subscribe.conn_handle, event->subscribe.attr_handle); motionService.UnsubscribeNotification(event->subscribe.conn_handle, event->subscribe.attr_handle); - } - else if(event->subscribe.prev_notify == 0 && event->subscribe.cur_notify == 1) { + } else if (event->subscribe.prev_notify == 0 && event->subscribe.cur_notify == 1) { heartRateService.SubscribeNotification(event->subscribe.conn_handle, event->subscribe.attr_handle); motionService.SubscribeNotification(event->subscribe.conn_handle, event->subscribe.attr_handle); - } - else if(event->subscribe.prev_notify == 1 && event->subscribe.cur_notify == 0) { + } else if (event->subscribe.prev_notify == 1 && event->subscribe.cur_notify == 0) { heartRateService.UnsubscribeNotification(event->subscribe.conn_handle, event->subscribe.attr_handle); motionService.UnsubscribeNotification(event->subscribe.conn_handle, event->subscribe.attr_handle); } break; case BLE_GAP_EVENT_MTU: - NRF_LOG_INFO("mtu update event; conn_handle=%d cid=%d mtu=%d\n", - event->mtu.conn_handle, event->mtu.channel_id, event->mtu.value); + NRF_LOG_INFO("MTU Update event; conn_handle=%d cid=%d mtu=%d", event->mtu.conn_handle, event->mtu.channel_id, event->mtu.value); break; case BLE_GAP_EVENT_REPEAT_PAIRING: { + NRF_LOG_INFO("Pairing event : BLE_GAP_EVENT_REPEAT_PAIRING"); /* We already have a bond with the peer, but it is attempting to * establish a new secure link. This app sacrifices security for * convenience: just throw away the old bond and accept the new link. @@ -257,6 +331,8 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { case BLE_GAP_EVENT_NOTIFY_RX: { /* Peer sent us a notification or indication. */ + /* Attribute data is contained in event->notify_rx.attr_data. */ + NRF_LOG_INFO("Notify event : BLE_GAP_EVENT_NOTIFY_RX"); size_t notifSize = OS_MBUF_PKTLEN(event->notify_rx.om); NRF_LOG_INFO("received %s; conn_handle=%d attr_handle=%d " @@ -268,10 +344,17 @@ int NimbleController::OnGAPEvent(ble_gap_event* event) { alertNotificationClient.OnNotification(event); } break; - /* Attribute data is contained in event->notify_rx.attr_data. */ + + case BLE_GAP_EVENT_NOTIFY_TX: + NRF_LOG_INFO("Notify event : BLE_GAP_EVENT_NOTIFY_TX"); + break; + + case BLE_GAP_EVENT_IDENTITY_RESOLVED: + NRF_LOG_INFO("Identity event : BLE_GAP_EVENT_IDENTITY_RESOLVED"); + break; default: - // NRF_LOG_INFO("Advertising event : %d", event->type); + NRF_LOG_INFO("UNHANDLED GAP event : %d", event->type); break; } return 0; @@ -292,3 +375,82 @@ void NimbleController::NotifyBatteryLevel(uint8_t level) { batteryInformationService.NotifyBatteryLevel(connectionHandle, level); } } + +void NimbleController::PersistBond(struct ble_gap_conn_desc& desc) { + union ble_store_key key; + union ble_store_value our_sec, peer_sec, peer_cccd_set[MYNEWT_VAL(BLE_STORE_MAX_CCCDS)] = {0}; + int rc; + + memset(&key, 0, sizeof key); + memset(&our_sec, 0, sizeof our_sec); + key.sec.peer_addr = desc.peer_id_addr; + rc = ble_store_read_our_sec(&key.sec, &our_sec.sec); + + if (memcmp(&our_sec.sec, &bondId, sizeof bondId) == 0) { + return; + } + + memcpy(&bondId, &our_sec.sec, sizeof bondId); + + memset(&key, 0, sizeof key); + memset(&peer_sec, 0, sizeof peer_sec); + key.sec.peer_addr = desc.peer_id_addr; + rc += ble_store_read_peer_sec(&key.sec, &peer_sec.sec); + + if (rc == 0) { + memset(&key, 0, sizeof key); + key.cccd.peer_addr = desc.peer_id_addr; + int peer_count = 0; + ble_store_util_count(BLE_STORE_OBJ_TYPE_CCCD, &peer_count); + for (int i = 0; i < peer_count; i++) { + key.cccd.idx = peer_count; + ble_store_read_cccd(&key.cccd, &peer_cccd_set[i].cccd); + } + + /* Wakeup Spi and SpiNorFlash before accessing the file system + * This should be fixed in the FS driver + */ + systemTask.PushMessage(Pinetime::System::Messages::GoToRunning); + systemTask.PushMessage(Pinetime::System::Messages::DisableSleeping); + vTaskDelay(10); + + lfs_file_t file_p; + + rc = fs.FileOpen(&file_p, "/bond.dat", LFS_O_WRONLY | LFS_O_CREAT); + if (rc == 0) { + fs.FileWrite(&file_p, reinterpret_cast<uint8_t*>(&our_sec.sec), sizeof our_sec); + fs.FileWrite(&file_p, reinterpret_cast<uint8_t*>(&peer_sec.sec), sizeof peer_sec); + fs.FileWrite(&file_p, reinterpret_cast<const uint8_t*>(&peer_count), 1); + for (int i = 0; i < peer_count; i++) { + fs.FileWrite(&file_p, reinterpret_cast<uint8_t*>(&peer_cccd_set[i].cccd), sizeof(struct ble_store_value_cccd)); + } + fs.FileClose(&file_p); + } + systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping); + } +} + +void NimbleController::RestoreBond() { + lfs_file_t file_p; + union ble_store_value sec, cccd; + uint8_t peer_count = 0; + + if (fs.FileOpen(&file_p, "/bond.dat", LFS_O_RDONLY) == 0) { + memset(&sec, 0, sizeof sec); + fs.FileRead(&file_p, reinterpret_cast<uint8_t*>(&sec.sec), sizeof sec); + ble_store_write_our_sec(&sec.sec); + + memset(&sec, 0, sizeof sec); + fs.FileRead(&file_p, reinterpret_cast<uint8_t*>(&sec.sec), sizeof sec); + ble_store_write_peer_sec(&sec.sec); + + fs.FileRead(&file_p, &peer_count, 1); + for (int i = 0; i < peer_count; i++) { + fs.FileRead(&file_p, reinterpret_cast<uint8_t*>(&cccd.cccd), sizeof(struct ble_store_value_cccd)); + ble_store_write_cccd(&cccd.cccd); + } + + fs.FileClose(&file_p); + fs.FileDelete("/bond.dat"); + } +} diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h index 895b87f2..12bd6924 100644 --- a/src/components/ble/NimbleController.h +++ b/src/components/ble/NimbleController.h @@ -14,12 +14,14 @@ #include "components/ble/CurrentTimeService.h" #include "components/ble/DeviceInformationService.h" #include "components/ble/DfuService.h" +#include "components/ble/HeartRateService.h" #include "components/ble/ImmediateAlertService.h" #include "components/ble/MusicService.h" #include "components/ble/NavigationService.h" #include "components/ble/ServiceDiscovery.h" -#include "components/ble/HeartRateService.h" #include "components/ble/MotionService.h" +#include "components/ble/weather/WeatherService.h" +#include "components/fs/FS.h" namespace Pinetime { namespace Drivers { @@ -45,7 +47,8 @@ namespace Pinetime { Controllers::Battery& batteryController, Pinetime::Drivers::SpiNorFlash& spiNorFlash, Controllers::HeartRateController& heartRateController, - Controllers::MotionController& motionController); + Controllers::MotionController& motionController, + Pinetime::Controllers::FS& fs); void Init(); void StartAdvertising(); int OnGAPEvent(ble_gap_event* event); @@ -70,6 +73,9 @@ namespace Pinetime { Pinetime::Controllers::AlertNotificationService& alertService() { return anService; }; + Pinetime::Controllers::WeatherService& weather() { + return weatherService; + }; uint16_t connHandle(); void NotifyBatteryLevel(uint8_t level); @@ -79,12 +85,16 @@ namespace Pinetime { } private: + void PersistBond(struct ble_gap_conn_desc& desc); + void RestoreBond(); + static constexpr const char* deviceName = "InfiniTime"; Pinetime::System::SystemTask& systemTask; Pinetime::Controllers::Ble& bleController; DateTime& dateTimeController; Pinetime::Controllers::NotificationManager& notificationManager; Pinetime::Drivers::SpiNorFlash& spiNorFlash; + Pinetime::Controllers::FS& fs; Pinetime::Controllers::DfuService dfuService; DeviceInformationService deviceInformationService; @@ -93,23 +103,24 @@ namespace Pinetime { AlertNotificationClient alertNotificationClient; CurrentTimeService currentTimeService; MusicService musicService; + WeatherService weatherService; NavigationService navService; BatteryInformationService batteryInformationService; ImmediateAlertService immediateAlertService; HeartRateService heartRateService; MotionService motionService; + ServiceDiscovery serviceDiscovery; - uint8_t addrType; // 1 = Random, 0 = PUBLIC + uint8_t addrType; uint16_t connectionHandle = BLE_HS_CONN_HANDLE_NONE; uint8_t fastAdvCount = 0; + uint8_t bondId[16] = {0}; ble_uuid128_t dfuServiceUuid { .u {.type = BLE_UUID_TYPE_128}, .value = {0x23, 0xD1, 0xBC, 0xEA, 0x5F, 0x78, 0x23, 0x15, 0xDE, 0xEF, 0x12, 0x12, 0x30, 0x15, 0x00, 0x00}}; - - ServiceDiscovery serviceDiscovery; }; - static NimbleController* nptr; + static NimbleController* nptr; } } diff --git a/src/components/ble/weather/WeatherData.h b/src/components/ble/weather/WeatherData.h new file mode 100644 index 00000000..613d5acb --- /dev/null +++ b/src/components/ble/weather/WeatherData.h @@ -0,0 +1,385 @@ +/* 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/>. +*/ +#pragma once + +/** + * Different weather events, weather data structures used by {@link WeatherService.h} + * + * How to upload events to the timeline? + * + * All timeline write payloads are simply CBOR-encoded payloads of the structs described below. + * + * All payloads have a mandatory header part and the dynamic part that + * depends on the event type specified in the header. If you don't, + * you'll get an error returned. Data is relatively well-validated, + * so keep in the bounds of the data types given. + * + * Write all struct members (CamelCase keys) into a single finite-sized map, and write it to the characteristic. + * Mind the MTU. + * + * How to debug? + * + * There's a Screen that you can compile into your firmware that shows currently valid events. + * You can adapt that to display something else. That part right now is very much work in progress + * because the exact requirements are not yet known. + * + * + * Implemented based on and other material: + * https://en.wikipedia.org/wiki/METAR + * https://www.weather.gov/jetstream/obscurationtypes + * http://www.faraim.org/aim/aim-4-03-14-493.html + */ + +namespace Pinetime { + namespace Controllers { + class WeatherData { + public: + /** + * Visibility obscuration types + */ + enum class obscurationtype { + /** No obscuration */ + None = 0, + /** Water particles suspended in the air; low visibility; does not fall */ + Fog = 1, + /** Tiny, dry particles in the air; invisible to the eye; opalescent */ + Haze = 2, + /** Small fire-created particles suspended in the air */ + Smoke = 3, + /** Fine rock powder, from for example volcanoes */ + Ash = 4, + /** Fine particles of earth suspended in the air by the wind */ + Dust = 5, + /** Fine particles of sand suspended in the air by the wind */ + Sand = 6, + /** Water particles suspended in the air; low-ish visibility; temperature is near dewpoint */ + Mist = 7, + /** This is SPECIAL in the sense that the thing raining down is doing the obscuration */ + Precipitation = 8, + Length + }; + + /** + * Types of precipitation + */ + enum class precipitationtype { + /** + * No precipitation + * + * Theoretically we could just _not_ send the event, but then + * how do we differentiate between no precipitation and + * no information about precipitation + */ + None = 0, + /** Drops larger than a drizzle; also widely separated drizzle */ + Rain = 1, + /** Fairly uniform rain consisting of fine drops */ + Drizzle = 2, + /** Rain that freezes upon contact with objects and ground */ + FreezingRain = 3, + /** Rain + hail; ice pellets; small translucent frozen raindrops */ + Sleet = 4, + /** Larger ice pellets; falling separately or in irregular clumps */ + Hail = 5, + /** Hail with smaller grains of ice; mini-snowballs */ + SmallHail = 6, + /** Snow... */ + Snow = 7, + /** Frozen drizzle; very small snow crystals */ + SnowGrains = 8, + /** Needles; columns or plates of ice. Sometimes described as "diamond dust". In very cold regions */ + IceCrystals = 9, + /** It's raining down ash, e.g. from a volcano */ + Ash = 10, + Length + }; + + /** + * These are special events that can "enhance" the "experience" of existing weather events + */ + enum class specialtype { + /** Strong wind with a sudden onset that lasts at least a minute */ + Squall = 0, + /** Series of waves in a water body caused by the displacement of a large volume of water */ + Tsunami = 1, + /** Violent; rotating column of air */ + Tornado = 2, + /** Unplanned; unwanted; uncontrolled fire in an area */ + Fire = 3, + /** Thunder and/or lightning */ + Thunder = 4, + Length + }; + + /** + * These are used for weather timeline manipulation + * that isn't just adding to the stack of weather events + */ + enum class controlcodes { + /** How much is stored already */ + GetLength = 0, + /** This wipes the entire timeline */ + DelTimeline = 1, + /** There's a currently valid timeline event with the given type */ + HasValidEvent = 3, + Length + }; + + /** + * Events have types + * then they're easier to parse after sending them over the air + */ + enum class eventtype : uint8_t { + /** @see obscuration */ + Obscuration = 0, + /** @see precipitation */ + Precipitation = 1, + /** @see wind */ + Wind = 2, + /** @see temperature */ + Temperature = 3, + /** @see airquality */ + AirQuality = 4, + /** @see special */ + Special = 5, + /** @see pressure */ + Pressure = 6, + /** @see location */ + Location = 7, + /** @see cloud */ + Clouds = 8, + /** @see humidity */ + Humidity = 9, + Length + }; + + /** + * Valid event query + * + * NOTE: Not currently available, until needs are better known + */ + class ValidEventQuery { + public: + static constexpr controlcodes code = controlcodes::HasValidEvent; + eventtype eventType; + }; + + /** The header used for further parsing */ + class TimelineHeader { + public: + /** + * UNIX timestamp + * TODO: This is currently WITH A TIMEZONE OFFSET! + * Please send events with the timestamp offset by the timezone. + **/ + uint64_t timestamp; + /** + * Time in seconds until the event expires + * + * 32 bits ought to be enough for everyone + * + * If there's a newer event of the same type then it overrides this one, even if it hasn't expired + */ + uint32_t expires; + /** + * What type of weather-related event + */ + eventtype eventType; + }; + + /** Specifies how cloudiness is stored */ + class Clouds : public TimelineHeader { + public: + /** Cloud coverage in percentage, 0-100% */ + uint8_t amount; + }; + + /** Specifies how obscuration is stored */ + class Obscuration : public TimelineHeader { + public: + /** Type of precipitation */ + obscurationtype type; + /** + * Visibility distance in meters + * 65535 is reserved for unspecified + */ + uint16_t amount; + }; + + /** Specifies how precipitation is stored */ + class Precipitation : public TimelineHeader { + public: + /** Type of precipitation */ + precipitationtype type; + /** + * How much is it going to rain? In millimeters + * 255 is reserved for unspecified + **/ + uint8_t amount; + }; + + /** + * How wind speed is stored + * + * In order to represent bursts of wind instead of constant wind, + * you have minimum and maximum speeds. + * + * As direction can fluctuate wildly and some watchfaces might wish to display it nicely, + * we're following the aerospace industry weather report option of specifying a range. + */ + class Wind : public TimelineHeader { + public: + /** Meters per second */ + uint8_t speedMin; + /** Meters per second */ + uint8_t speedMax; + /** Unitless direction between 0-255; approximately 1 unit per 0.71 degrees */ + uint8_t directionMin; + /** Unitless direction between 0-255; approximately 1 unit per 0.71 degrees */ + uint8_t directionMax; + }; + + /** + * How temperature is stored + * + * As it's annoying to figure out the dewpoint on the watch, + * please send it from the companion + * + * We don't do floats, picodegrees are not useful. Make sure to multiply. + */ + class Temperature : public TimelineHeader { + public: + /** + * Temperature °C but multiplied by 100 (e.g. -12.50°C becomes -1250) + * -32768 is reserved for "no data" + */ + int16_t temperature; + /** + * Dewpoint °C but multiplied by 100 (e.g. -12.50°C becomes -1250) + * -32768 is reserved for "no data" + */ + int16_t dewPoint; + }; + + /** + * How location info is stored + * + * This can be mostly static with long expiration, + * as it usually is, but it could change during a trip for ex. + * so we allow changing it dynamically. + * + * Location info can be for some kind of map watchface + * or daylight calculations, should those be required. + * + */ + class Location : public TimelineHeader { + public: + /** Location name */ + std::string location; + /** Altitude relative to sea level in meters */ + int16_t altitude; + /** Latitude, EPSG:3857 (Google Maps, Openstreetmaps datum) */ + int32_t latitude; + /** Longitude, EPSG:3857 (Google Maps, Openstreetmaps datum) */ + int32_t longitude; + }; + + /** + * How humidity is stored + */ + class Humidity : public TimelineHeader { + public: + /** Relative humidity, 0-100% */ + uint8_t humidity; + }; + + /** + * How air pressure is stored + */ + class Pressure : public TimelineHeader { + public: + /** Air pressure in hectopascals (hPa) */ + int16_t pressure; + }; + + /** + * How special events are stored + */ + class Special : public TimelineHeader { + public: + /** Special event's type */ + specialtype type; + }; + + /** + * How air quality is stored + * + * These events are a bit more complex because the topic is not simple, + * the intention is to heavy-lift the annoying preprocessing from the watch + * this allows watchface or watchapp makers to generate accurate alerts and graphics + * + * If this needs further enforced standardization, pull requests are welcome + */ + class AirQuality : public TimelineHeader { + public: + /** + * The name of the pollution + * + * for the sake of better compatibility with watchapps + * that might want to use this data for say visuals + * don't localize the name. + * + * Ideally watchapp itself localizes the name, if it's at all needed. + * + * E.g. + * For generic ones use "PM0.1", "PM5", "PM10" + * For chemical compounds use the molecular formula e.g. "NO2", "CO2", "O3" + * For pollen use the genus, e.g. "Betula" for birch or "Alternaria" for that mold's spores + */ + std::string polluter; + /** + * Amount of the pollution in SI units, + * otherwise it's going to be difficult to create UI, alerts + * and so on and for. + * + * See more: + * https://ec.europa.eu/environment/air/quality/standards.htm + * http://www.ourair.org/wp-content/uploads/2012-aaqs2.pdf + * + * Example units: + * count/m³ for pollen + * µgC/m³ for micrograms of organic carbon + * µg/m³ sulfates, PM0.1, PM1, PM2, PM10 and so on, dust + * mg/m³ CO2, CO + * ng/m³ for heavy metals + * + * List is not comprehensive, should be improved. + * The current ones are what watchapps assume! + * + * Note: ppb and ppm to concentration should be calculated on the companion, using + * the correct formula (taking into account temperature and air pressure) + * + * Note2: The amount is off by times 100, for two decimal places of precision. + * E.g. 54.32µg/m³ is 5432 + * + */ + uint32_t amount; + }; + }; + } +} diff --git a/src/components/ble/weather/WeatherService.cpp b/src/components/ble/weather/WeatherService.cpp new file mode 100644 index 00000000..23f53b74 --- /dev/null +++ b/src/components/ble/weather/WeatherService.cpp @@ -0,0 +1,604 @@ +/* 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" + +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); +} + +namespace Pinetime { + namespace Controllers { + WeatherService::WeatherService(System::SystemTask& system, DateTime& dateTimeController) + : system(system), dateTimeController(dateTimeController) { + nullHeader = &nullTimelineheader; + nullTimelineheader->timestamp = 0; + } + + void WeatherService::Init() { + uint8_t res = 0; + res = ble_gatts_count_cfg(serviceDefinition); + ASSERT(res == 0); + + res = ble_gatts_add_svcs(serviceDefinition); + ASSERT(res == 0); + } + + int WeatherService::OnCommand(uint16_t connHandle, uint16_t attrHandle, struct ble_gatt_access_ctxt* ctxt) { + if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + const uint8_t packetLen = OS_MBUF_PKTLEN(ctxt->om); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + if (packetLen <= 0) { + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + // Decode + QCBORDecodeContext decodeContext; + UsefulBufC encodedCbor = {ctxt->om->om_data, OS_MBUF_PKTLEN(ctxt->om)}; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + + QCBORDecode_Init(&decodeContext, encodedCbor, QCBOR_DECODE_MODE_NORMAL); + // KINDLY provide us a fixed-length map + QCBORDecode_EnterMap(&decodeContext, nullptr); + // Always encodes to the smallest number of bytes based on the value + int64_t tmpTimestamp = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Timestamp", &tmpTimestamp); + if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + int64_t tmpExpires = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Expires", &tmpExpires); + if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS || tmpExpires < 0 || tmpExpires > 4294967295) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + int64_t tmpEventType = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "EventType", &tmpEventType); + if (QCBORDecode_GetError(&decodeContext) != QCBOR_SUCCESS || tmpEventType < 0 || + tmpEventType >= static_cast<int64_t>(WeatherData::eventtype::Length)) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + + switch (static_cast<WeatherData::eventtype>(tmpEventType)) { + case WeatherData::eventtype::AirQuality: { + std::unique_ptr<WeatherData::AirQuality> airquality = std::make_unique<WeatherData::AirQuality>(); + airquality->timestamp = tmpTimestamp; + airquality->eventType = static_cast<WeatherData::eventtype>(tmpEventType); + airquality->expires = tmpExpires; + + UsefulBufC stringBuf; // TODO: Everything ok with lifecycle here? + QCBORDecode_GetTextStringInMapSZ(&decodeContext, "Polluter", &stringBuf); + if (UsefulBuf_IsNULLOrEmptyC(stringBuf) != 0) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + airquality->polluter = std::string(static_cast<const char*>(stringBuf.ptr), stringBuf.len); + + int64_t tmpAmount = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); + if (tmpAmount < 0 || tmpAmount > 4294967295) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + airquality->amount = tmpAmount; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + if (!AddEventToTimeline(std::move(airquality))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + case WeatherData::eventtype::Obscuration: { + std::unique_ptr<WeatherData::Obscuration> obscuration = std::make_unique<WeatherData::Obscuration>(); + obscuration->timestamp = tmpTimestamp; + obscuration->eventType = static_cast<WeatherData::eventtype>(tmpEventType); + obscuration->expires = tmpExpires; + + int64_t tmpType = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Type", &tmpType); + if (tmpType < 0 || tmpType >= static_cast<int64_t>(WeatherData::obscurationtype::Length)) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + obscuration->type = static_cast<WeatherData::obscurationtype>(tmpType); + + int64_t tmpAmount = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); + if (tmpAmount < 0 || tmpAmount > 65535) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + obscuration->amount = tmpAmount; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + if (!AddEventToTimeline(std::move(obscuration))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + case WeatherData::eventtype::Precipitation: { + std::unique_ptr<WeatherData::Precipitation> precipitation = std::make_unique<WeatherData::Precipitation>(); + precipitation->timestamp = tmpTimestamp; + precipitation->eventType = static_cast<WeatherData::eventtype>(tmpEventType); + precipitation->expires = tmpExpires; + + int64_t tmpType = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Type", &tmpType); + if (tmpType < 0 || tmpType >= static_cast<int64_t>(WeatherData::precipitationtype::Length)) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + precipitation->type = static_cast<WeatherData::precipitationtype>(tmpType); + + int64_t tmpAmount = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); + if (tmpAmount < 0 || tmpAmount > 255) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + precipitation->amount = tmpAmount; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + if (!AddEventToTimeline(std::move(precipitation))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + case WeatherData::eventtype::Wind: { + std::unique_ptr<WeatherData::Wind> wind = std::make_unique<WeatherData::Wind>(); + wind->timestamp = tmpTimestamp; + wind->eventType = static_cast<WeatherData::eventtype>(tmpEventType); + wind->expires = tmpExpires; + + int64_t tmpMin = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "SpeedMin", &tmpMin); + if (tmpMin < 0 || tmpMin > 255) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + wind->speedMin = tmpMin; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + int64_t tmpMax = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "SpeedMin", &tmpMax); + if (tmpMax < 0 || tmpMax > 255) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + wind->speedMax = tmpMax; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + int64_t tmpDMin = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "DirectionMin", &tmpDMin); + if (tmpDMin < 0 || tmpDMin > 255) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + wind->directionMin = tmpDMin; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + int64_t tmpDMax = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "DirectionMax", &tmpDMax); + if (tmpDMax < 0 || tmpDMax > 255) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + wind->directionMax = tmpDMax; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + if (!AddEventToTimeline(std::move(wind))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + case WeatherData::eventtype::Temperature: { + std::unique_ptr<WeatherData::Temperature> temperature = std::make_unique<WeatherData::Temperature>(); + temperature->timestamp = tmpTimestamp; + temperature->eventType = static_cast<WeatherData::eventtype>(tmpEventType); + temperature->expires = tmpExpires; + + int64_t tmpTemperature = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Temperature", &tmpTemperature); + if (tmpTemperature < -32768 || tmpTemperature > 32767) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + temperature->temperature = + static_cast<int16_t>(tmpTemperature); // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + int64_t tmpDewPoint = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "DewPoint", &tmpDewPoint); + if (tmpDewPoint < -32768 || tmpDewPoint > 32767) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + temperature->dewPoint = + static_cast<int16_t>(tmpDewPoint); // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + if (!AddEventToTimeline(std::move(temperature))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + case WeatherData::eventtype::Special: { + std::unique_ptr<WeatherData::Special> special = std::make_unique<WeatherData::Special>(); + special->timestamp = tmpTimestamp; + special->eventType = static_cast<WeatherData::eventtype>(tmpEventType); + special->expires = tmpExpires; + + int64_t tmpType = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Type", &tmpType); + if (tmpType < 0 || tmpType >= static_cast<int64_t>(WeatherData::specialtype::Length)) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + special->type = static_cast<WeatherData::specialtype>(tmpType); + + if (!AddEventToTimeline(std::move(special))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + case WeatherData::eventtype::Pressure: { + std::unique_ptr<WeatherData::Pressure> pressure = std::make_unique<WeatherData::Pressure>(); + pressure->timestamp = tmpTimestamp; + pressure->eventType = static_cast<WeatherData::eventtype>(tmpEventType); + pressure->expires = tmpExpires; + + int64_t tmpPressure = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Pressure", &tmpPressure); + if (tmpPressure < 0 || tmpPressure >= 65535) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + pressure->pressure = tmpPressure; // NOLINT(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + + if (!AddEventToTimeline(std::move(pressure))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + case WeatherData::eventtype::Location: { + std::unique_ptr<WeatherData::Location> location = std::make_unique<WeatherData::Location>(); + location->timestamp = tmpTimestamp; + location->eventType = static_cast<WeatherData::eventtype>(tmpEventType); + location->expires = tmpExpires; + + UsefulBufC stringBuf; // TODO: Everything ok with lifecycle here? + QCBORDecode_GetTextStringInMapSZ(&decodeContext, "Location", &stringBuf); + if (UsefulBuf_IsNULLOrEmptyC(stringBuf) != 0) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + location->location = std::string(static_cast<const char*>(stringBuf.ptr), stringBuf.len); + + int64_t tmpAltitude = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Altitude", &tmpAltitude); + if (tmpAltitude < -32768 || tmpAltitude >= 32767) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + location->altitude = static_cast<int16_t>(tmpAltitude); + + int64_t tmpLatitude = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Latitude", &tmpLatitude); + if (tmpLatitude < -2147483648 || tmpLatitude >= 2147483647) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + location->latitude = static_cast<int32_t>(tmpLatitude); + + int64_t tmpLongitude = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Longitude", &tmpLongitude); + if (tmpLongitude < -2147483648 || tmpLongitude >= 2147483647) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + location->latitude = static_cast<int32_t>(tmpLongitude); + + if (!AddEventToTimeline(std::move(location))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + case WeatherData::eventtype::Clouds: { + std::unique_ptr<WeatherData::Clouds> clouds = std::make_unique<WeatherData::Clouds>(); + clouds->timestamp = tmpTimestamp; + clouds->eventType = static_cast<WeatherData::eventtype>(tmpEventType); + clouds->expires = tmpExpires; + + int64_t tmpAmount = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Amount", &tmpAmount); + if (tmpAmount < 0 || tmpAmount > 255) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + clouds->amount = static_cast<uint8_t>(tmpAmount); + + if (!AddEventToTimeline(std::move(clouds))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + 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; + + int64_t tmpType = 0; + QCBORDecode_GetInt64InMapSZ(&decodeContext, "Humidity", &tmpType); + if (tmpType < 0 || tmpType >= 255) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + humidity->humidity = static_cast<uint8_t>(tmpType); + + if (!AddEventToTimeline(std::move(humidity))) { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + break; + } + default: { + CleanUpQcbor(&decodeContext); + return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; + } + } + + QCBORDecode_ExitMap(&decodeContext); + GetTimelineLength(); + TidyTimeline(); + + if (QCBORDecode_Finish(&decodeContext) != QCBOR_SUCCESS) { + return BLE_ATT_ERR_INSUFFICIENT_RES; + } + } else if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + // Encode + uint8_t buffer[64]; + QCBOREncodeContext encodeContext; + /* TODO: This is very much still a test endpoint + * it needs a characteristic UUID check + * and actual implementations that show + * what actually has to be read. + * WARN: Consider commands not part of the API for now! + */ + 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; + } + + std::unique_ptr<WeatherData::Clouds>& WeatherService::GetCurrentClouds() { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Clouds && IsEventStillValid(header, currentTimestamp)) { + return reinterpret_cast<std::unique_ptr<WeatherData::Clouds>&>(header); + } + } + + return reinterpret_cast<std::unique_ptr<WeatherData::Clouds>&>(*this->nullHeader); + } + + std::unique_ptr<WeatherData::Obscuration>& WeatherService::GetCurrentObscuration() { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Obscuration && IsEventStillValid(header, currentTimestamp)) { + return reinterpret_cast<std::unique_ptr<WeatherData::Obscuration>&>(header); + } + } + + return reinterpret_cast<std::unique_ptr<WeatherData::Obscuration>&>(*this->nullHeader); + } + + std::unique_ptr<WeatherData::Precipitation>& WeatherService::GetCurrentPrecipitation() { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Precipitation && IsEventStillValid(header, currentTimestamp)) { + return reinterpret_cast<std::unique_ptr<WeatherData::Precipitation>&>(header); + } + } + + return reinterpret_cast<std::unique_ptr<WeatherData::Precipitation>&>(*this->nullHeader); + } + + std::unique_ptr<WeatherData::Wind>& WeatherService::GetCurrentWind() { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Wind && IsEventStillValid(header, currentTimestamp)) { + return reinterpret_cast<std::unique_ptr<WeatherData::Wind>&>(header); + } + } + + return reinterpret_cast<std::unique_ptr<WeatherData::Wind>&>(*this->nullHeader); + } + + std::unique_ptr<WeatherData::Temperature>& WeatherService::GetCurrentTemperature() { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Temperature && IsEventStillValid(header, currentTimestamp)) { + return reinterpret_cast<std::unique_ptr<WeatherData::Temperature>&>(header); + } + } + + return reinterpret_cast<std::unique_ptr<WeatherData::Temperature>&>(*this->nullHeader); + } + + std::unique_ptr<WeatherData::Humidity>& WeatherService::GetCurrentHumidity() { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Humidity && IsEventStillValid(header, currentTimestamp)) { + return reinterpret_cast<std::unique_ptr<WeatherData::Humidity>&>(header); + } + } + + return reinterpret_cast<std::unique_ptr<WeatherData::Humidity>&>(*this->nullHeader); + } + + std::unique_ptr<WeatherData::Pressure>& WeatherService::GetCurrentPressure() { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Pressure && IsEventStillValid(header, currentTimestamp)) { + return reinterpret_cast<std::unique_ptr<WeatherData::Pressure>&>(header); + } + } + + return reinterpret_cast<std::unique_ptr<WeatherData::Pressure>&>(*this->nullHeader); + } + + std::unique_ptr<WeatherData::Location>& WeatherService::GetCurrentLocation() { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Location && IsEventStillValid(header, currentTimestamp)) { + return reinterpret_cast<std::unique_ptr<WeatherData::Location>&>(header); + } + } + + return reinterpret_cast<std::unique_ptr<WeatherData::Location>&>(*this->nullHeader); + } + + std::unique_ptr<WeatherData::AirQuality>& WeatherService::GetCurrentQuality() { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::AirQuality && IsEventStillValid(header, currentTimestamp)) { + return reinterpret_cast<std::unique_ptr<WeatherData::AirQuality>&>(header); + } + } + + return reinterpret_cast<std::unique_ptr<WeatherData::AirQuality>&>(*this->nullHeader); + } + + size_t WeatherService::GetTimelineLength() const { + return timeline.size(); + } + + bool WeatherService::AddEventToTimeline(std::unique_ptr<WeatherData::TimelineHeader> event) { + if (timeline.size() == timeline.max_size()) { + return false; + } + + timeline.push_back(std::move(event)); + return true; + } + + bool WeatherService::HasTimelineEventOfType(const WeatherData::eventtype type) const { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + for (auto&& header : timeline) { + if (header->eventType == type && IsEventStillValid(header, currentTimestamp)) { + return true; + } + } + return false; + } + + void WeatherService::TidyTimeline() { + uint64_t timeCurrent = GetCurrentUnixTimestamp(); + timeline.erase(std::remove_if(std::begin(timeline), + std::end(timeline), + [&](std::unique_ptr<WeatherData::TimelineHeader> const& header) { + return !IsEventStillValid(header, timeCurrent); + }), + std::end(timeline)); + + std::sort(std::begin(timeline), std::end(timeline), CompareTimelineEvents); + } + + bool WeatherService::CompareTimelineEvents(const std::unique_ptr<WeatherData::TimelineHeader>& first, + const std::unique_ptr<WeatherData::TimelineHeader>& second) { + return first->timestamp > second->timestamp; + } + + bool WeatherService::IsEventStillValid(const std::unique_ptr<WeatherData::TimelineHeader>& uniquePtr, const uint64_t timestamp) { + // Not getting timestamp in isEventStillValid for more speed + return uniquePtr->timestamp + uniquePtr->expires >= timestamp; + } + + uint64_t WeatherService::GetCurrentUnixTimestamp() const { + return std::chrono::duration_cast<std::chrono::seconds>(dateTimeController.CurrentDateTime().time_since_epoch()).count(); + } + + int16_t WeatherService::GetTodayMinTemp() const { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + uint64_t currentDayEnd = currentTimestamp - ((24 - dateTimeController.Hours()) * 60 * 60) - + ((60 - dateTimeController.Minutes()) * 60) - (60 - dateTimeController.Seconds()); + int16_t result = -32768; + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Temperature && IsEventStillValid(header, currentTimestamp) && + header->timestamp < currentDayEnd && + reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature != -32768) { + int16_t temperature = reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature; + if (result == -32768) { + result = temperature; + } else if (result > temperature) { + result = temperature; + } else { + // The temperature in this item is higher than the lowest we've found + } + } + } + + return result; + } + + int16_t WeatherService::GetTodayMaxTemp() const { + uint64_t currentTimestamp = GetCurrentUnixTimestamp(); + uint64_t currentDayEnd = currentTimestamp - ((24 - dateTimeController.Hours()) * 60 * 60) - + ((60 - dateTimeController.Minutes()) * 60) - (60 - dateTimeController.Seconds()); + int16_t result = -32768; + for (auto&& header : this->timeline) { + if (header->eventType == WeatherData::eventtype::Temperature && IsEventStillValid(header, currentTimestamp) && + header->timestamp < currentDayEnd && + reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature != -32768) { + int16_t temperature = reinterpret_cast<const std::unique_ptr<WeatherData::Temperature>&>(header)->temperature; + if (result == -32768) { + result = temperature; + } else if (result < temperature) { + result = temperature; + } else { + // The temperature in this item is lower than the highest we've found + } + } + } + + return result; + } + + void WeatherService::CleanUpQcbor(QCBORDecodeContext* decodeContext) { + QCBORDecode_ExitMap(decodeContext); + QCBORDecode_Finish(decodeContext); + } + } +} diff --git a/src/components/ble/weather/WeatherService.h b/src/components/ble/weather/WeatherService.h new file mode 100644 index 00000000..eca70cbd --- /dev/null +++ b/src/components/ble/weather/WeatherService.h @@ -0,0 +1,172 @@ +/* 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/>. +*/ +#pragma once + +#include <cstdint> +#include <string> +#include <vector> +#include <memory> + +#define min // workaround: nimble's min/max macros conflict with libstdc++ +#define max +#include <host/ble_gap.h> +#include <host/ble_uuid.h> +#undef max +#undef min + +#include "WeatherData.h" +#include "libs/QCBOR/inc/qcbor/qcbor.h" +#include "components/datetime/DateTimeController.h" + +int WeatherCallback(uint16_t connHandle, uint16_t attrHandle, struct ble_gatt_access_ctxt* ctxt, void* arg); + +namespace Pinetime { + namespace System { + class SystemTask; + } + namespace Controllers { + + class WeatherService { + public: + explicit WeatherService(System::SystemTask& system, DateTime& dateTimeController); + + void Init(); + + int OnCommand(uint16_t connHandle, uint16_t attrHandle, struct ble_gatt_access_ctxt* ctxt); + + /* + * Helper functions for quick access to currently valid data + */ + std::unique_ptr<WeatherData::Location>& GetCurrentLocation(); + std::unique_ptr<WeatherData::Clouds>& GetCurrentClouds(); + std::unique_ptr<WeatherData::Obscuration>& GetCurrentObscuration(); + std::unique_ptr<WeatherData::Precipitation>& GetCurrentPrecipitation(); + std::unique_ptr<WeatherData::Wind>& GetCurrentWind(); + std::unique_ptr<WeatherData::Temperature>& GetCurrentTemperature(); + std::unique_ptr<WeatherData::Humidity>& GetCurrentHumidity(); + std::unique_ptr<WeatherData::Pressure>& GetCurrentPressure(); + std::unique_ptr<WeatherData::AirQuality>& GetCurrentQuality(); + + /** + * Searches for the current day's maximum temperature + * @return -32768 if there's no data, degrees Celsius times 100 otherwise + */ + int16_t GetTodayMaxTemp() const; + /** + * Searches for the current day's minimum temperature + * @return -32768 if there's no data, degrees Celsius times 100 otherwise + */ + int16_t GetTodayMinTemp() const; + + /* + * Management functions + */ + /** + * Adds an event to the timeline + * @return + */ + bool AddEventToTimeline(std::unique_ptr<WeatherData::TimelineHeader> event); + /** + * Gets the current timeline length + */ + size_t GetTimelineLength() const; + /** + * Checks if an event of a certain type exists in the timeline + */ + bool HasTimelineEventOfType(WeatherData::eventtype type) const; + + private: + // 00040000-78fc-48fe-8e23-433b3a1942d0 + static constexpr ble_uuid128_t BaseUuid() { + return CharUuid(0x00, 0x00); + } + + // 0004yyxx-78fc-48fe-8e23-433b3a1942d0 + static constexpr ble_uuid128_t CharUuid(uint8_t x, uint8_t y) { + return ble_uuid128_t {.u = {.type = BLE_UUID_TYPE_128}, + .value = {0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, y, x, 0x04, 0x00}}; + } + + ble_uuid128_t weatherUuid {BaseUuid()}; + + /** + * Just write timeline data here. + * + * See {@link WeatherData.h} for more information. + */ + ble_uuid128_t weatherDataCharUuid {CharUuid(0x00, 0x01)}; + /** + * This doesn't take timeline data, provides some control over it. + * + * NOTE: Currently not supported. Companion app implementer feedback required. + * There's very little point in solidifying an API before we know the needs. + */ + ble_uuid128_t weatherControlCharUuid {CharUuid(0x00, 0x02)}; + + const struct ble_gatt_chr_def characteristicDefinition[3] = { + {.uuid = &weatherDataCharUuid.u, + .access_cb = WeatherCallback, + .arg = this, + .flags = BLE_GATT_CHR_F_WRITE, + .val_handle = &eventHandle}, + {.uuid = &weatherControlCharUuid.u, .access_cb = WeatherCallback, .arg = this, .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_READ}, + {nullptr}}; + const struct ble_gatt_svc_def serviceDefinition[2] = { + {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = &weatherUuid.u, .characteristics = characteristicDefinition}, {0}}; + + uint16_t eventHandle {}; + + Pinetime::System::SystemTask& system; + Pinetime::Controllers::DateTime& dateTimeController; + + std::vector<std::unique_ptr<WeatherData::TimelineHeader>> timeline; + std::unique_ptr<WeatherData::TimelineHeader> nullTimelineheader = std::make_unique<WeatherData::TimelineHeader>(); + std::unique_ptr<WeatherData::TimelineHeader>* nullHeader; + + /** + * Cleans up the timeline of expired events + */ + void TidyTimeline(); + + /** + * Compares two timeline events + */ + static bool CompareTimelineEvents(const std::unique_ptr<WeatherData::TimelineHeader>& first, + const std::unique_ptr<WeatherData::TimelineHeader>& second); + + /** + * Returns current UNIX timestamp + */ + uint64_t GetCurrentUnixTimestamp() const; + + /** + * Checks if the event hasn't gone past and expired + * + * @param header timeline event to check + * @param currentTimestamp what's the time right now + * @return if the event is valid + */ + static bool IsEventStillValid(const std::unique_ptr<WeatherData::TimelineHeader>& uniquePtr, const uint64_t timestamp); + + /** + * This is a helper function that closes a QCBOR map and decoding context cleanly + */ + void CleanUpQcbor(QCBORDecodeContext* decodeContext); + }; + } +} |