Initial Weather service skeleton
This commit is contained in:
parent
6ba2878605
commit
bda96dc595
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
338
src/components/ble/weather/WeatherData.h
Normal file
338
src/components/ble/weather/WeatherData.h
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
208
src/components/ble/weather/WeatherService.cpp
Normal file
208
src/components/ble/weather/WeatherService.cpp
Normal file
|
@ -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 <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 conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt, void* arg) {
|
||||
return static_cast<Pinetime::Controllers::WeatherService*>(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<int64_t*>(&(timelineHeader.timestamp)));
|
||||
QCBORDecode_GetInt64InMapSZ(&decodeContext, "Expires", reinterpret_cast<int64_t*>(&(timelineHeader.expires)));
|
||||
QCBORDecode_GetInt64InMapSZ(&decodeContext, "EventType", reinterpret_cast<int64_t*>(&(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<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 && 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<WeatherData::timelineheader> 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<WeatherData::timelineheader>& first,
|
||||
const std::unique_ptr<WeatherData::timelineheader>& second) {
|
||||
return first->timestamp > second->timestamp;
|
||||
}
|
||||
|
||||
uint64_t WeatherService::getCurrentUNIXTimestamp() const {
|
||||
return std::chrono::duration_cast<std::chrono::seconds>(dateTimeController.CurrentDateTime().time_since_epoch()).count();
|
||||
}
|
||||
}
|
||||
}
|
139
src/components/ble/weather/WeatherService.h
Normal file
139
src/components/ble/weather/WeatherService.h
Normal file
|
@ -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 <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 <components/datetime/DateTimeController.h>
|
||||
|
||||
// 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<WeatherData::timelineheader> 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<ble_uuid_t*>(&wDataCharUuid),
|
||||
.access_cb = WeatherCallback,
|
||||
.arg = this,
|
||||
.flags = BLE_GATT_CHR_F_NOTIFY,
|
||||
.val_handle = &eventHandle},
|
||||
{.uuid = reinterpret_cast<ble_uuid_t*>(&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<ble_uuid_t*>(&msUuid), .characteristics = characteristicDefinition},
|
||||
{0}};
|
||||
|
||||
uint16_t eventHandle {};
|
||||
|
||||
Pinetime::System::SystemTask& system;
|
||||
Pinetime::Controllers::DateTime& dateTimeController;
|
||||
|
||||
std::vector<std::unique_ptr<WeatherData::timelineheader>> timeline;
|
||||
|
||||
/**
|
||||
* Cleans up the timeline of expired events
|
||||
* @return result code
|
||||
*/
|
||||
void tidyTimeline();
|
||||
|
||||
/**
|
||||
* Compares two timeline events
|
||||
*/
|
||||
static bool compareTimelineEvents(const std::unique_ptr<WeatherData::timelineheader>& first,
|
||||
const std::unique_ptr<WeatherData::timelineheader>& second);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
uint64_t getCurrentUNIXTimestamp() const;
|
||||
};
|
||||
}
|
||||
}
|
246
src/displayapp/screens/Weather.cpp
Normal file
246
src/displayapp/screens/Weather.cpp
Normal file
|
@ -0,0 +1,246 @@
|
|||
#include "Weather.h"
|
||||
#include <lvgl/lvgl.h>
|
||||
#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<Screen> {
|
||||
return CreateScreen1();
|
||||
},
|
||||
[this]() -> std::unique_ptr<Screen> {
|
||||
return CreateScreen2();
|
||||
},
|
||||
[this]() -> std::unique_ptr<Screen> {
|
||||
return CreateScreen3();
|
||||
},
|
||||
[this]() -> std::unique_ptr<Screen> {
|
||||
return CreateScreen4();
|
||||
},
|
||||
[this]() -> std::unique_ptr<Screen> {
|
||||
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<Screen> 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<Screen>(new Screens::Label(0, 5, app, label));
|
||||
}
|
||||
|
||||
std::unique_ptr<Screen> Weather::CreateScreen2() {
|
||||
auto batteryPercent = static_cast<uint8_t>(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<uint8_t>(batteryVoltage); // truncate whole numbers
|
||||
batteryVoltageBytes[0] =
|
||||
static_cast<uint8_t>((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<uint8_t>(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<Screen>(new Screens::Label(1, 4, app, label));
|
||||
}
|
||||
|
||||
std::unique_ptr<Screen> 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<Screen>(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<Screen> 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<Screen>(new Screens::Label(3, 5, app, infoTask));
|
||||
}
|
||||
|
||||
std::unique_ptr<Screen> 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<Screen>(new Screens::Label(4, 5, app, label));
|
||||
}
|
54
src/displayapp/screens/Weather.h
Normal file
54
src/displayapp/screens/Weather.h
Normal file
|
@ -0,0 +1,54 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#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<Screen> CreateScreen1();
|
||||
std::unique_ptr<Screen> CreateScreen2();
|
||||
std::unique_ptr<Screen> CreateScreen3();
|
||||
std::unique_ptr<Screen> CreateScreen4();
|
||||
std::unique_ptr<Screen> CreateScreen5();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue