diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e2b69b8b..eef66e3b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -364,6 +364,7 @@ list(APPEND SOURCE_FILES BootloaderVersion.cpp logging/NrfLogger.cpp displayapp/DisplayApp.cpp + displayapp/WeatherHelper.cpp displayapp/screens/Screen.cpp displayapp/screens/Tile.cpp displayapp/screens/InfiniPaint.cpp @@ -588,6 +589,7 @@ set(INCLUDE_FILES logging/Logger.h logging/NrfLogger.h displayapp/DisplayApp.h + displayapp/WeatherHelper.h displayapp/Messages.h displayapp/TouchEvents.h displayapp/screens/Screen.h diff --git a/src/displayapp/WeatherHelper.cpp b/src/displayapp/WeatherHelper.cpp new file mode 100644 index 00000000..7a6e78a6 --- /dev/null +++ b/src/displayapp/WeatherHelper.cpp @@ -0,0 +1,110 @@ +/* Copyright (C) 2024 Caleb Fontenot + + 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 "WeatherHelper.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Pinetime::Applications; + //Linear gradient temperature color calculator :) + + int16_t WeatherHelper::RoundTemperature(int16_t temp) { + return temp = temp / 100 + (temp % 100 >= 50 ? 1 : 0); + } + std::tuple rgb565to888(int r, int g, int b) { + return { + ( r * 527 + 23 ) >> 6, + ( g * 259 + 33 ) >> 6, + ( b * 527 + 23 ) >> 6 + }; + } + + const char* WeatherHelper::floatToRgbHex(lv_color_t rgb) { + std::tuple tuple = rgb565to888(LV_COLOR_GET_R(rgb), LV_COLOR_GET_G(rgb), LV_COLOR_GET_B(rgb)); + char *rgbHex = new char[7]; + snprintf(rgbHex, 7, "%02X%02X%02X", std::get<0>(tuple), std::get<1>(tuple), std::get<2>(tuple)); + return rgbHex; + } + + lv_color_t hexToFloat(int rgb) { + return lv_color_hex(rgb); + } + + float normalize(float value) { + if (value < 0.0f) { + return 0.0f; + } else if (value > 1.0f) { + return 1.0f; + } else { + return value; + } +} + + // reference: https://dev.to/ndesmic/linear-color-gradients-from-scratch-1a0e + const lv_color_t lerp(lv_color_t pointA, lv_color_t pointB, float normalValue) { + auto [redA, greenA, blueA] = rgb565to888(LV_COLOR_GET_R(pointA), LV_COLOR_GET_G(pointA), LV_COLOR_GET_B(pointA)); + auto [redB, greenB, blueB] = rgb565to888(LV_COLOR_GET_R(pointB), LV_COLOR_GET_G(pointB), LV_COLOR_GET_B(pointB)); + NRF_LOG_INFO("Normal value: %f", normalValue); + + int outputRed = (redA + (redB - redA) * normalValue); + int outputGreen = (greenA + (greenB - greenA) * normalValue); + int outputBlue = (blueA + (blueB - blueA) * normalValue); + + //increase brightness + float incAmount = 1.2; + outputRed = std::min(255, static_cast(outputRed*incAmount)); + outputGreen = std::min(255, static_cast(outputGreen*incAmount)); + outputBlue = std::min(255, static_cast(outputBlue*incAmount)); + + auto lerpOutput = LV_COLOR_MAKE(outputRed, outputGreen, outputBlue); + NRF_LOG_INFO("pointA: %i, %i, %i", redA, greenA, blueA); + NRF_LOG_INFO("pointB: %i, %i, %i", redB, greenB, blueB); + NRF_LOG_INFO("lerpOutput: %i, %i, %i", LV_COLOR_GET_R(lerpOutput), LV_COLOR_GET_G(lerpOutput), LV_COLOR_GET_B(lerpOutput)); + return lerpOutput; + } + + constexpr std::array getColors() { + const std::array colors = {0x5555ff, 0x00c9ff, 0xff9b00, 0xff0000}; + std::array stops; + int8_t i = 0; + for (auto colorVal: colors) { + stops[i++] = (hexToFloat(colorVal)); + } + return stops; + } + + const lv_color_t WeatherHelper::TemperatureColor(int16_t temperature) { + std::array stops = getColors(); + int tempRounded = RoundTemperature(temperature); + if (tempRounded < 0) { + tempRounded = 1; + } + // convert temperature to range between newMin and newMax + float oldRange = (oldMax - oldMin); + float newRange = (newMax - newMin); + float newValue = (((tempRounded - oldMin) * newRange) / oldRange) + newMin; + newValue = normalize(newValue); + return lerp(stops[0], stops[3], newValue); + } diff --git a/src/displayapp/WeatherHelper.h b/src/displayapp/WeatherHelper.h new file mode 100644 index 00000000..bb3aba81 --- /dev/null +++ b/src/displayapp/WeatherHelper.h @@ -0,0 +1,35 @@ +/* Copyright (C) 2024 Caleb Fontenot + + 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 + +namespace Pinetime { + namespace Applications { + class WeatherHelper { + public: + static int16_t RoundTemperature(int16_t temp); + static const lv_color_t TemperatureColor(int16_t temperature); + static const char* floatToRgbHex(lv_color_t rgb); + constexpr static float oldMax = 50; + constexpr static float oldMin = 0; + constexpr static float newMax = 1; + constexpr static float newMin = 0; + }; + } + } diff --git a/src/displayapp/screens/WatchFaceTerminal.cpp b/src/displayapp/screens/WatchFaceTerminal.cpp index 96d77741..020a6b1d 100644 --- a/src/displayapp/screens/WatchFaceTerminal.cpp +++ b/src/displayapp/screens/WatchFaceTerminal.cpp @@ -1,14 +1,21 @@ +#include #include #include "displayapp/screens/WatchFaceTerminal.h" #include "displayapp/screens/BatteryIcon.h" #include "displayapp/screens/NotificationIcon.h" #include "displayapp/screens/Symbols.h" +#include "displayapp/InfiniTimeTheme.h" #include "components/battery/BatteryController.h" #include "components/ble/BleController.h" #include "components/ble/NotificationManager.h" #include "components/heartrate/HeartRateController.h" #include "components/motion/MotionController.h" #include "components/settings/Settings.h" +#include "displayapp/WeatherHelper.h" +#include +#include +#include +#include using namespace Pinetime::Applications::Screens; @@ -18,7 +25,8 @@ WatchFaceTerminal::WatchFaceTerminal(Controllers::DateTime& dateTimeController, Controllers::NotificationManager& notificationManager, Controllers::Settings& settingsController, Controllers::HeartRateController& heartRateController, - Controllers::MotionController& motionController) + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherController) : currentDateTime {{}}, dateTimeController {dateTimeController}, batteryController {batteryController}, @@ -26,14 +34,15 @@ WatchFaceTerminal::WatchFaceTerminal(Controllers::DateTime& dateTimeController, notificationManager {notificationManager}, settingsController {settingsController}, heartRateController {heartRateController}, - motionController {motionController} { + motionController {motionController}, + weatherController {weatherController} { batteryValue = lv_label_create(lv_scr_act(), nullptr); lv_label_set_recolor(batteryValue, true); lv_obj_align(batteryValue, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, -20); connectState = lv_label_create(lv_scr_act(), nullptr); lv_label_set_recolor(connectState, true); - lv_obj_align(connectState, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 40); + lv_obj_align(connectState, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 80); notificationIcon = lv_label_create(lv_scr_act(), nullptr); lv_obj_align(notificationIcon, nullptr, LV_ALIGN_IN_LEFT_MID, 0, -100); @@ -47,7 +56,7 @@ WatchFaceTerminal::WatchFaceTerminal(Controllers::DateTime& dateTimeController, lv_label_set_text_static(label_prompt_1, "user@watch:~ $ now"); label_prompt_2 = lv_label_create(lv_scr_act(), nullptr); - lv_obj_align(label_prompt_2, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 60); + lv_obj_align(label_prompt_2, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 100); lv_label_set_text_static(label_prompt_2, "user@watch:~ $"); label_time = lv_label_create(lv_scr_act(), nullptr); @@ -62,6 +71,14 @@ WatchFaceTerminal::WatchFaceTerminal(Controllers::DateTime& dateTimeController, lv_label_set_recolor(stepValue, true); lv_obj_align(stepValue, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 0); + weatherStatus = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(weatherStatus, true); + lv_obj_align(weatherStatus, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 40); + + bytesFree = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_recolor(bytesFree, true); + lv_obj_align(bytesFree, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 0, 60); + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); Refresh(); } @@ -71,6 +88,42 @@ WatchFaceTerminal::~WatchFaceTerminal() { lv_obj_clean(lv_scr_act()); } +// https://www.wpc.ncep.noaa.gov/html/contract.html +const char* WeatherString(uint8_t weatherID) { + switch (weatherID) { + case 0: { + return "CLR"; + } + case 1: { + return "SLGT CLD"; + } + case 2: { + return "CLD"; + } + case 3: { + return "EXTRM CLD"; + } + case 4: { + return "TSRA"; + } + case 5: { + return "RA"; + } + case 6: { + return "TSTM"; + } + case 7: { + return "SN"; + } + case 8: { + return "BR"; + } + default: { + return "UNKN"; + } + } +} + void WatchFaceTerminal::Refresh() { powerPresent = batteryController.IsPowerPresent(); batteryPercentRemaining = batteryController.PercentRemaining(); @@ -104,6 +157,41 @@ void WatchFaceTerminal::Refresh() { } } + // Following along from the example given by Weather.cpp... + + // get an instance of the weather service? not exactly sure what this does... + currentWeather = weatherController.Current(); + // check to see if we have valid weather data + if (currentWeather.IsUpdated()) { + auto optCurrentWeather = currentWeather.Get(); // the actual weather data + if (optCurrentWeather) { + int16_t temp = optCurrentWeather->temperature.PreciseCelsius(); // current temperature + uint8_t weatherId = static_cast(optCurrentWeather->iconId); // weather type + NRF_LOG_INFO("Raw temp: %d", temp); + NRF_LOG_INFO("Rounded temp: %d", WeatherHelper::RoundTemperature(temp)); + // testColor(); //testVal * 100 + auto color = WeatherHelper::floatToRgbHex(WeatherHelper::TemperatureColor(temp)); // call temperature color BEFORE unit conversion + // unit conversion + char tempUnit = 'C'; + if (settingsController.GetWeatherFormat() == Controllers::Settings::WeatherFormat::Imperial) { + temp = optCurrentWeather->temperature.PreciseFahrenheit(); + tempUnit = 'F'; + } + NRF_LOG_INFO("Color hex: %s", color); + lv_label_set_text_fmt(weatherStatus, + "[WTHR]#%s %d#°%c %s", + color, + WeatherHelper::RoundTemperature(temp), + tempUnit, + WeatherString(weatherId)); + delete[] color; + } else { + lv_label_set_text_static(weatherStatus, "[WTHR]No Data"); + } + } + + lv_label_set_text_fmt(bytesFree, "[MEM] %i B Free", xPortGetFreeHeapSize()); + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); if (currentDateTime.IsUpdated()) { uint8_t hour = dateTimeController.Hours(); diff --git a/src/displayapp/screens/WatchFaceTerminal.h b/src/displayapp/screens/WatchFaceTerminal.h index bf460866..4841ba39 100644 --- a/src/displayapp/screens/WatchFaceTerminal.h +++ b/src/displayapp/screens/WatchFaceTerminal.h @@ -7,6 +7,7 @@ #include #include "displayapp/screens/Screen.h" #include "components/datetime/DateTimeController.h" +#include "components/ble/SimpleWeatherService.h" #include "utility/DirtyValue.h" namespace Pinetime { @@ -30,7 +31,9 @@ namespace Pinetime { Controllers::NotificationManager& notificationManager, Controllers::Settings& settingsController, Controllers::HeartRateController& heartRateController, - Controllers::MotionController& motionController); + Controllers::MotionController& motionController, + Controllers::SimpleWeatherService& weatherService + ); ~WatchFaceTerminal() override; void Refresh() override; @@ -45,6 +48,7 @@ namespace Pinetime { Utility::DirtyValue heartbeat {}; Utility::DirtyValue heartbeatRunning {}; Utility::DirtyValue notificationState {}; + Utility::DirtyValue> currentWeather {}; Utility::DirtyValue> currentDate; lv_obj_t* label_time; @@ -56,6 +60,8 @@ namespace Pinetime { lv_obj_t* stepValue; lv_obj_t* notificationIcon; lv_obj_t* connectState; + lv_obj_t* weatherStatus; + lv_obj_t* bytesFree; Controllers::DateTime& dateTimeController; const Controllers::Battery& batteryController; @@ -64,7 +70,7 @@ namespace Pinetime { Controllers::Settings& settingsController; Controllers::HeartRateController& heartRateController; Controllers::MotionController& motionController; - + Controllers::SimpleWeatherService& weatherController; lv_task_t* taskRefresh; }; } @@ -81,7 +87,8 @@ namespace Pinetime { controllers.notificationManager, controllers.settingsController, controllers.heartRateController, - controllers.motionController); + controllers.motionController, + *controllers.weatherController); }; static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) {