diff --git a/.gitmodules b/.gitmodules
index 815fc022..8d302ae7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
[submodule "src/libs/littlefs"]
path = src/libs/littlefs
url = https://github.com/littlefs-project/littlefs.git
+[submodule "src/libs/QCBOR"]
+ path = src/libs/QCBOR
+ url = https://github.com/laurencelundblade/QCBOR.git
diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp
index 43a8b0d6..9ef2d057 100644
--- a/src/components/ble/NimbleController.cpp
+++ b/src/components/ble/NimbleController.cpp
@@ -36,6 +36,7 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask,
alertNotificationClient {systemTask, notificationManager},
currentTimeService {dateTimeController},
musicService {systemTask},
+ weatherService {systemTask, dateTimeController},
navService {systemTask},
batteryInformationService {batteryController},
immediateAlertService {systemTask, notificationManager},
@@ -77,6 +78,7 @@ void NimbleController::Init() {
currentTimeClient.Init();
currentTimeService.Init();
musicService.Init();
+ weatherService.Init();
navService.Init();
anService.Init();
dfuService.Init();
diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h
index 895b87f2..a21cbe81 100644
--- a/src/components/ble/NimbleController.h
+++ b/src/components/ble/NimbleController.h
@@ -20,6 +20,7 @@
#include "components/ble/ServiceDiscovery.h"
#include "components/ble/HeartRateService.h"
#include "components/ble/MotionService.h"
+#include "components/ble/weather/WeatherService.h"
namespace Pinetime {
namespace Drivers {
@@ -93,6 +94,7 @@ namespace Pinetime {
AlertNotificationClient alertNotificationClient;
CurrentTimeService currentTimeService;
MusicService musicService;
+ WeatherService weatherService;
NavigationService navService;
BatteryInformationService batteryInformationService;
ImmediateAlertService immediateAlertService;
diff --git a/src/components/ble/weather/WeatherData.h b/src/components/ble/weather/WeatherData.h
new file mode 100644
index 00000000..c1d53f4e
--- /dev/null
+++ b/src/components/ble/weather/WeatherData.h
@@ -0,0 +1,338 @@
+/* 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 .
+*/
+#pragma once
+
+/**
+ * Different weather events, weather data structures used by {@link WeatherService.h}
+ *
+ *
+ * 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,
+ /** Extremely small, 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,
+ };
+
+ /**
+ * 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
+ };
+
+ /**
+ * 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,
+ };
+
+ /**
+ * 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
+ };
+
+ /**
+ * Events have types
+ * then they're easier to parse after sending them over the air
+ */
+ enum class eventtype {
+ /** @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,
+ };
+
+ /**
+ * Valid event query
+ */
+ class valideventquery {
+ public:
+ static constexpr controlcodes code = controlcodes::HasValidEvent;
+ eventtype eventType;
+ };
+
+ /** The header used for further parsing */
+ class timelineheader {
+ public:
+ /** UNIX timestamp */
+ 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 */
+ obscurationtype type;
+ /** Visibility distance in meters */
+ uint8_t amount;
+ };
+
+ /** Specifies how precipitation is stored */
+ class precipitation : public timelineheader {
+ public:
+ /** Type */
+ precipitationtype type;
+ /** How much is it going to rain? In millimeters */
+ 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, microdegrees 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) */
+ int16_t temperature;
+ /** Dewpoint °C but multiplied by 100 (e.g. -12.50°C becomes -1250) */
+ 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;
+ };
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/components/ble/weather/WeatherService.cpp b/src/components/ble/weather/WeatherService.cpp
new file mode 100644
index 00000000..006fc6c1
--- /dev/null
+++ b/src/components/ble/weather/WeatherService.cpp
@@ -0,0 +1,208 @@
+/* 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 .
+*/
+#include
+#include "WeatherService.h"
+#include "libs/QCBOR/inc/qcbor/qcbor.h"
+#include "systemtask/SystemTask.h"
+
+int WeatherCallback(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt, void* arg) {
+ return static_cast(arg)->OnCommand(conn_handle, attr_handle, ctxt);
+}
+
+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);
+ ASSERT(res == 0)
+
+ res = ble_gatts_add_svcs(serviceDefinition);
+ ASSERT(res == 0);
+ }
+
+ int WeatherService::OnCommand(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt) {
+ if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
+ getCurrentPressure();
+ tidyTimeline();
+ getTimelineLength();
+ const auto packetLen = OS_MBUF_PKTLEN(ctxt->om);
+ if (packetLen <= 0) {
+ return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
+ }
+ // Decode
+ QCBORDecodeContext decodeContext;
+ UsefulBufC EncodedCBOR;
+ // TODO: Check uninit fine
+ QCBORDecode_Init(&decodeContext, EncodedCBOR, QCBOR_DECODE_MODE_NORMAL);
+ QCBORDecode_EnterMap(&decodeContext, nullptr);
+ WeatherData::timelineheader timelineHeader {};
+ // Always encodes to the smallest number of bytes based on the value
+ QCBORDecode_GetInt64InMapSZ(&decodeContext, "Timestamp", reinterpret_cast(&(timelineHeader.timestamp)));
+ QCBORDecode_GetInt64InMapSZ(&decodeContext, "Expires", reinterpret_cast(&(timelineHeader.expires)));
+ QCBORDecode_GetInt64InMapSZ(&decodeContext, "EventType", reinterpret_cast(&(timelineHeader.eventType)));
+ switch (timelineHeader.eventType) {
+ // TODO: Populate
+ case WeatherData::eventtype::AirQuality: {
+ break;
+ }
+ case WeatherData::eventtype::Obscuration: {
+ break;
+ }
+ case WeatherData::eventtype::Precipitation: {
+ break;
+ }
+ case WeatherData::eventtype::Wind: {
+ break;
+ }
+ case WeatherData::eventtype::Temperature: {
+ break;
+ }
+ case WeatherData::eventtype::Special: {
+ break;
+ }
+ case WeatherData::eventtype::Pressure: {
+ break;
+ }
+ case WeatherData::eventtype::Location: {
+ break;
+ }
+ case WeatherData::eventtype::Clouds: {
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ QCBORDecode_ExitMap(&decodeContext);
+
+ auto uErr = QCBORDecode_Finish(&decodeContext);
+ if (uErr != 0) {
+ return BLE_ATT_ERR_INSUFFICIENT_RES;
+ }
+ } else if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
+ // TODO: Detect control messages
+
+ // 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;
+ }
+
+ WeatherData::location WeatherService::getCurrentLocation() const {
+ return WeatherData::location();
+ }
+ WeatherData::clouds WeatherService::getCurrentClouds() const {
+ return WeatherData::clouds();
+ }
+ WeatherData::obscuration WeatherService::getCurrentObscuration() const {
+ return WeatherData::obscuration();
+ }
+ WeatherData::precipitation WeatherService::getCurrentPrecipitation() const {
+ return WeatherData::precipitation();
+ }
+ WeatherData::wind WeatherService::getCurrentWind() const {
+ return WeatherData::wind();
+ }
+ WeatherData::temperature WeatherService::getCurrentTemperature() const {
+ return WeatherData::temperature();
+ }
+ WeatherData::humidity WeatherService::getCurrentHumidity() const {
+ return WeatherData::humidity();
+ }
+ WeatherData::pressure WeatherService::getCurrentPressure() const {
+ uint64_t currentTimestamp = getCurrentUNIXTimestamp();
+ for (auto&& header : timeline) {
+ if (header->eventType == WeatherData::eventtype::Pressure && header->timestamp + header->expires <= currentTimestamp) {
+ return WeatherData::pressure();
+ }
+ }
+ return WeatherData::pressure();
+ }
+
+ WeatherData::airquality WeatherService::getCurrentQuality() const {
+ return WeatherData::airquality();
+ }
+
+ size_t WeatherService::getTimelineLength() const {
+ return timeline.size();
+ }
+
+ bool WeatherService::addEventToTimeline(std::unique_ptr 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 && header->timestamp + header->expires <= currentTimestamp) {
+ // TODO: Check if its currently valid
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void WeatherService::tidyTimeline() {
+ uint64_t timeCurrent = 0;
+ timeline.erase(std::remove_if(std::begin(timeline),
+ std::end(timeline),
+ [&](std::unique_ptr const& header) {
+ return header->timestamp + header->expires > timeCurrent;
+ }),
+ std::end(timeline));
+
+ std::sort(std::begin(timeline), std::end(timeline), compareTimelineEvents);
+ }
+
+ bool WeatherService::compareTimelineEvents(const std::unique_ptr& first,
+ const std::unique_ptr& second) {
+ return first->timestamp > second->timestamp;
+ }
+
+ uint64_t WeatherService::getCurrentUNIXTimestamp() const {
+ return std::chrono::duration_cast(dateTimeController.CurrentDateTime().time_since_epoch()).count();
+ }
+ }
+}
diff --git a/src/components/ble/weather/WeatherService.h b/src/components/ble/weather/WeatherService.h
new file mode 100644
index 00000000..ef99db86
--- /dev/null
+++ b/src/components/ble/weather/WeatherService.h
@@ -0,0 +1,139 @@
+/* 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 .
+*/
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#define min // workaround: nimble's min/max macros conflict with libstdc++
+#define max
+#include
+#include
+#undef max
+#undef min
+
+#include "WeatherData.h"
+#include
+
+// 00030000-78fc-48fe-8e23-433b3a1942d0
+#define WEATHER_SERVICE_UUID_BASE \
+ { 0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, 0x00, 0x00, 0x03, 0x00 }
+#define WEATHER_SERVICE_CHAR_UUID(y, x) \
+ { 0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, (x), (y), 0x03, 0x00 }
+
+int WeatherCallback(uint16_t conn_handle, uint16_t attr_handle, 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 conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt);
+
+ /*
+ * Helper functions for quick access to currently valid data
+ */
+ WeatherData::location getCurrentLocation() const;
+ WeatherData::clouds getCurrentClouds() const;
+ WeatherData::obscuration getCurrentObscuration() const;
+ WeatherData::precipitation getCurrentPrecipitation() const;
+ WeatherData::wind getCurrentWind() const;
+ WeatherData::temperature getCurrentTemperature() const;
+ WeatherData::humidity getCurrentHumidity() const;
+ WeatherData::pressure getCurrentPressure() const;
+ WeatherData::airquality getCurrentQuality() const;
+
+ /*
+ * Management functions
+ */
+ /**
+ * Adds an event to the timeline
+ * @return
+ */
+ bool addEventToTimeline(std::unique_ptr event);
+ /**
+ * Gets the current timeline length
+ */
+ size_t getTimelineLength() const;
+ /**
+ * Checks if an event of a certain type exists in the timeline
+ * @return
+ */
+ bool hasTimelineEventOfType(WeatherData::eventtype type) const;
+
+ private:
+ ble_uuid128_t msUuid {.u = {.type = BLE_UUID_TYPE_128}, .value = WEATHER_SERVICE_UUID_BASE};
+
+ /**
+ * Just write timeline data here
+ */
+ ble_uuid128_t wDataCharUuid {.u = {.type = BLE_UUID_TYPE_128}, .value = WEATHER_SERVICE_CHAR_UUID(0x00, 0x01)};
+ /**
+ * This doesn't take timeline data
+ * but provides some control over it
+ */
+ ble_uuid128_t wControlCharUuid {.u = {.type = BLE_UUID_TYPE_128}, .value = WEATHER_SERVICE_CHAR_UUID(0x00, 0x02)};
+
+ const struct ble_gatt_chr_def characteristicDefinition[2] = {{.uuid = reinterpret_cast(&wDataCharUuid),
+ .access_cb = WeatherCallback,
+ .arg = this,
+ .flags = BLE_GATT_CHR_F_NOTIFY,
+ .val_handle = &eventHandle},
+ {.uuid = reinterpret_cast(&wControlCharUuid),
+ .access_cb = WeatherCallback,
+ .arg = this,
+ .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_READ}};
+ const struct ble_gatt_svc_def serviceDefinition[2] = {
+ {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = reinterpret_cast(&msUuid), .characteristics = characteristicDefinition},
+ {0}};
+
+ uint16_t eventHandle {};
+
+ Pinetime::System::SystemTask& system;
+ Pinetime::Controllers::DateTime& dateTimeController;
+
+ std::vector> timeline;
+
+ /**
+ * Cleans up the timeline of expired events
+ * @return result code
+ */
+ void tidyTimeline();
+
+ /**
+ * Compares two timeline events
+ */
+ static bool compareTimelineEvents(const std::unique_ptr& first,
+ const std::unique_ptr& second);
+
+ /**
+ *
+ */
+ uint64_t getCurrentUNIXTimestamp() const;
+ };
+ }
+}
diff --git a/src/displayapp/screens/Weather.cpp b/src/displayapp/screens/Weather.cpp
new file mode 100644
index 00000000..014761bf
--- /dev/null
+++ b/src/displayapp/screens/Weather.cpp
@@ -0,0 +1,246 @@
+#include "Weather.h"
+#include
+#include "../DisplayApp.h"
+#include "Label.h"
+#include "Version.h"
+#include "components/battery/BatteryController.h"
+#include "components/ble/BleController.h"
+#include "components/brightness/BrightnessController.h"
+#include "components/datetime/DateTimeController.h"
+#include "drivers/Watchdog.h"
+#include "components/ble/weather/WeatherData.h"
+
+using namespace Pinetime::Applications::Screens;
+
+Weather::Weather(Pinetime::Applications::DisplayApp* app,
+ Pinetime::Controllers::DateTime& dateTimeController,
+ Pinetime::Controllers::Battery& batteryController,
+ Pinetime::Controllers::BrightnessController& brightnessController,
+ Pinetime::Controllers::Ble& bleController,
+ Pinetime::Drivers::WatchdogView& watchdog)
+ : Screen(app),
+ dateTimeController {dateTimeController},
+ batteryController {batteryController},
+ brightnessController {brightnessController},
+ bleController {bleController},
+ watchdog {watchdog},
+ screens {app,
+ 0,
+ {[this]() -> std::unique_ptr {
+ return CreateScreen1();
+ },
+ [this]() -> std::unique_ptr {
+ return CreateScreen2();
+ },
+ [this]() -> std::unique_ptr {
+ return CreateScreen3();
+ },
+ [this]() -> std::unique_ptr {
+ return CreateScreen4();
+ },
+ [this]() -> std::unique_ptr {
+ return CreateScreen5();
+ }},
+ Screens::ScreenListModes::UpDown} {
+}
+
+Weather::~Weather() {
+ lv_obj_clean(lv_scr_act());
+}
+
+bool Weather::Refresh() {
+ if (running) {
+ screens.Refresh();
+ }
+ return running;
+}
+
+bool Weather::OnButtonPushed() {
+ running = false;
+ return true;
+}
+
+bool Weather::OnTouchEvent(Pinetime::Applications::TouchEvents event) {
+ return screens.OnTouchEvent(event);
+}
+
+std::unique_ptr Weather::CreateScreen1() {
+ lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr);
+ lv_label_set_recolor(label, true);
+ lv_label_set_text_fmt(label,
+ "#FFFF00 InfiniTime#\n\n"
+ "#444444 Version# %ld.%ld.%ld\n\n"
+ "#444444 Build date#\n"
+ "%s\n"
+ "%s\n",
+ Version::Major(),
+ Version::Minor(),
+ Version::Patch(),
+ __DATE__,
+ __TIME__);
+ lv_label_set_align(label, LV_LABEL_ALIGN_CENTER);
+ lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0);
+ return std::unique_ptr(new Screens::Label(0, 5, app, label));
+}
+
+std::unique_ptr Weather::CreateScreen2() {
+ auto batteryPercent = static_cast(batteryController.PercentRemaining());
+ float batteryVoltage = batteryController.Voltage();
+
+ auto resetReason = [this]() {
+ switch (watchdog.ResetReason()) {
+ case Drivers::Watchdog::ResetReasons::Watchdog:
+ return "wtdg";
+ case Drivers::Watchdog::ResetReasons::HardReset:
+ return "hardr";
+ case Drivers::Watchdog::ResetReasons::NFC:
+ return "nfc";
+ case Drivers::Watchdog::ResetReasons::SoftReset:
+ return "softr";
+ case Drivers::Watchdog::ResetReasons::CpuLockup:
+ return "cpulock";
+ case Drivers::Watchdog::ResetReasons::SystemOff:
+ return "off";
+ case Drivers::Watchdog::ResetReasons::LpComp:
+ return "lpcomp";
+ case Drivers::Watchdog::ResetReasons::DebugInterface:
+ return "dbg";
+ case Drivers::Watchdog::ResetReasons::ResetPin:
+ return "rst";
+ default:
+ return "?";
+ }
+ }();
+
+ // uptime
+ static constexpr uint32_t secondsInADay = 60 * 60 * 24;
+ static constexpr uint32_t secondsInAnHour = 60 * 60;
+ static constexpr uint32_t secondsInAMinute = 60;
+ uint32_t uptimeSeconds = dateTimeController.Uptime().count();
+ uint32_t uptimeDays = (uptimeSeconds / secondsInADay);
+ uptimeSeconds = uptimeSeconds % secondsInADay;
+ uint32_t uptimeHours = uptimeSeconds / secondsInAnHour;
+ uptimeSeconds = uptimeSeconds % secondsInAnHour;
+ uint32_t uptimeMinutes = uptimeSeconds / secondsInAMinute;
+ uptimeSeconds = uptimeSeconds % secondsInAMinute;
+ // TODO handle more than 100 days of uptime
+
+ if (batteryPercent == -1)
+ batteryPercent = 0;
+
+ // hack to not use the flot functions from printf
+ uint8_t batteryVoltageBytes[2];
+ batteryVoltageBytes[1] = static_cast(batteryVoltage); // truncate whole numbers
+ batteryVoltageBytes[0] =
+ static_cast((batteryVoltage - batteryVoltageBytes[1]) * 100); // remove whole part of flt and shift 2 places over
+ //
+
+ lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr);
+ lv_label_set_recolor(label, true);
+ lv_label_set_text_fmt(label,
+ "#444444 Date# %02d/%02d/%04d\n"
+ "#444444 Time# %02d:%02d:%02d\n"
+ "#444444 Uptime#\n %02lud %02lu:%02lu:%02lu\n"
+ "#444444 Battery# %d%%/%1i.%02iv\n"
+ "#444444 Backlight# %s\n"
+ "#444444 Last reset# %s\n",
+ dateTimeController.Day(),
+ static_cast(dateTimeController.Month()),
+ dateTimeController.Year(),
+ dateTimeController.Hours(),
+ dateTimeController.Minutes(),
+ dateTimeController.Seconds(),
+ uptimeDays,
+ uptimeHours,
+ uptimeMinutes,
+ uptimeSeconds,
+ batteryPercent,
+ batteryVoltageBytes[1],
+ batteryVoltageBytes[0],
+ brightnessController.ToString(),
+ resetReason);
+ lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0);
+ return std::unique_ptr(new Screens::Label(1, 4, app, label));
+}
+
+std::unique_ptr Weather::CreateScreen3() {
+ lv_mem_monitor_t mon;
+ lv_mem_monitor(&mon);
+
+ lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr);
+ lv_label_set_recolor(label, true);
+ auto& bleAddr = bleController.Address();
+ lv_label_set_text_fmt(label,
+ "#444444 BLE MAC#\n"
+ " %02x:%02x:%02x:%02x:%02x:%02x"
+ "\n"
+ "#444444 Memory#\n"
+ " #444444 used# %d (%d%%)\n"
+ " #444444 frag# %d%%\n"
+ " #444444 free# %d"
+ "\n"
+ "#444444 Steps# %li",
+ bleAddr[5],
+ bleAddr[4],
+ bleAddr[3],
+ bleAddr[2],
+ bleAddr[1],
+ bleAddr[0],
+ (int) mon.total_size - mon.free_size,
+ mon.used_pct,
+ mon.frag_pct,
+ (int) mon.free_biggest_size,
+ 0);
+ lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0);
+ return std::unique_ptr(new Screens::Label(2, 5, app, label));
+}
+
+bool sortById(const TaskStatus_t& lhs, const TaskStatus_t& rhs) {
+ return lhs.xTaskNumber < rhs.xTaskNumber;
+}
+
+std::unique_ptr Weather::CreateScreen4() {
+ TaskStatus_t tasksStatus[7];
+ lv_obj_t* infoTask = lv_table_create(lv_scr_act(), NULL);
+ lv_table_set_col_cnt(infoTask, 3);
+ lv_table_set_row_cnt(infoTask, 8);
+ lv_obj_set_pos(infoTask, 10, 10);
+
+ lv_table_set_cell_value(infoTask, 0, 0, "#");
+ lv_table_set_col_width(infoTask, 0, 50);
+ lv_table_set_cell_value(infoTask, 0, 1, "Task");
+ lv_table_set_col_width(infoTask, 1, 80);
+ lv_table_set_cell_value(infoTask, 0, 2, "Free");
+ lv_table_set_col_width(infoTask, 2, 90);
+
+ auto nb = uxTaskGetSystemState(tasksStatus, 7, nullptr);
+ std::sort(tasksStatus, tasksStatus + nb, sortById);
+ for (uint8_t i = 0; i < nb; i++) {
+
+ lv_table_set_cell_value(infoTask, i + 1, 0, std::to_string(tasksStatus[i].xTaskNumber).c_str());
+ lv_table_set_cell_value(infoTask, i + 1, 1, tasksStatus[i].pcTaskName);
+ if (tasksStatus[i].usStackHighWaterMark < 20) {
+ std::string str1 = std::to_string(tasksStatus[i].usStackHighWaterMark) + " low";
+ lv_table_set_cell_value(infoTask, i + 1, 2, str1.c_str());
+ } else {
+ lv_table_set_cell_value(infoTask, i + 1, 2, std::to_string(tasksStatus[i].usStackHighWaterMark).c_str());
+ }
+ }
+ return std::unique_ptr(new Screens::Label(3, 5, app, infoTask));
+}
+
+std::unique_ptr Weather::CreateScreen5() {
+ lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr);
+ lv_label_set_recolor(label, true);
+ lv_label_set_text_static(label,
+ "Software Licensed\n"
+ "under the terms of\n"
+ "the GNU General\n"
+ "Public License v3\n"
+ "#444444 Source code#\n"
+ "#FFFF00 https://github.com/#\n"
+ "#FFFF00 JF002/InfiniTime#");
+ lv_label_set_align(label, LV_LABEL_ALIGN_CENTER);
+ lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0);
+ return std::unique_ptr(new Screens::Label(4, 5, app, label));
+}
diff --git a/src/displayapp/screens/Weather.h b/src/displayapp/screens/Weather.h
new file mode 100644
index 00000000..8b393ca1
--- /dev/null
+++ b/src/displayapp/screens/Weather.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include
+#include "Screen.h"
+#include "ScreenList.h"
+
+namespace Pinetime {
+ namespace Controllers {
+ class DateTime;
+ class Battery;
+ class BrightnessController;
+ class Ble;
+ }
+
+ namespace Drivers {
+ class WatchdogView;
+ }
+
+ namespace Applications {
+ class DisplayApp;
+
+ namespace Screens {
+ class Weather : public Screen {
+ public:
+ explicit Weather(DisplayApp* app,
+ Pinetime::Controllers::DateTime& dateTimeController,
+ Pinetime::Controllers::Battery& batteryController,
+ Pinetime::Controllers::BrightnessController& brightnessController,
+ Pinetime::Controllers::Ble& bleController,
+ Pinetime::Drivers::WatchdogView& watchdog);
+ ~Weather() override;
+ bool Refresh() override;
+ bool OnButtonPushed() override;
+ bool OnTouchEvent(TouchEvents event) override;
+
+ private:
+ bool running = true;
+
+ Pinetime::Controllers::DateTime& dateTimeController;
+ Pinetime::Controllers::Battery& batteryController;
+ Pinetime::Controllers::BrightnessController& brightnessController;
+ Pinetime::Controllers::Ble& bleController;
+ Pinetime::Drivers::WatchdogView& watchdog;
+
+ ScreenList<5> screens;
+ std::unique_ptr CreateScreen1();
+ std::unique_ptr CreateScreen2();
+ std::unique_ptr CreateScreen3();
+ std::unique_ptr CreateScreen4();
+ std::unique_ptr CreateScreen5();
+ };
+ }
+ }
+}
\ No newline at end of file