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