Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Weather App #293

Merged
merged 3 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/child_image/hci_ipc.conf
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y
# increase of ble throughput
CONFIG_BT_BUF_ACL_RX_SIZE=502
CONFIG_BT_BUF_ACL_TX_SIZE=502
CONFIG_BT_CTLR_DATA_LENGTH_MAX=251

# Temporary fix as there is a strange behaviour with some Android
# phones.
CONFIG_BT_DATA_LEN_UPDATE=n
#CONFIG_BT_CTLR_DATA_LENGTH_MAX=251
4 changes: 2 additions & 2 deletions app/prj.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=6000
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=10000
CONFIG_HEAP_MEM_POOL_SIZE=32000
CONFIG_CBPRINTF_FP_SUPPORT=y
CONFIG_DEBUG_THREAD_INFO=y
Expand Down Expand Up @@ -81,7 +81,7 @@ CONFIG_LV_USE_MENU=y
CONFIG_LV_USE_METER=y
CONFIG_LV_USE_MSGBOX=y
CONFIG_LV_USE_SPINBOX=n
CONFIG_LV_USE_SPINNER=n
CONFIG_LV_USE_SPINNER=y
CONFIG_LV_USE_TABVIEW=n
CONFIG_LV_USE_TILEVIEW=y
CONFIG_LV_USE_WIN=n
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/create_custom_resource_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from struct import *

MAX_FILE_NAME = 32
FILE_TABLE_MAX_LEN = 25000
FILE_TABLE_MAX_LEN = 32000
"""
magic_number:uint32
header_len:uint32
Expand Down
32 changes: 22 additions & 10 deletions app/src/applications/trivia/trivia_app.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,31 @@ static int trivia_app_add(void)
return 0;
}

static void http_rsp_cb(ble_http_status_code_t status, cJSON *response)
static void http_rsp_cb(ble_http_status_code_t status, char *response)
{
if (status == BLE_HTTP_STATUS_OK && active) {
cJSON *question = cJSON_GetObjectItem(response, "question");
cJSON *correct_answer = cJSON_GetObjectItem(response, "correct_answer");
if (question == NULL || correct_answer == NULL) {
LOG_ERR("Failed to parse JSON data");
return;
cJSON *parsed_response = cJSON_Parse(response);
if (parsed_response == NULL) {
LOG_ERR("Failed to parse JSON rsp data from HTTP request");
} else {
cJSON *results = cJSON_GetObjectItem(parsed_response, "results");
if (cJSON_GetArraySize(results) == 1) {
cJSON *result = cJSON_GetArrayItem(results, 0);
cJSON *question = cJSON_GetObjectItem(result, "question");
cJSON *correct_answer = cJSON_GetObjectItem(result, "correct_answer");
if (question == NULL || correct_answer == NULL) {
LOG_ERR("Failed to parse JSON data");
return;
}
memset(trivia_app_question.question, 0, sizeof(trivia_app_question.question));
strncpy(trivia_app_question.question, question->valuestring, sizeof(trivia_app_question.question) - 1);
trivia_app_question.correct_answer = (correct_answer->valuestring[0] == 'F') ? false : true;
trivia_ui_update_question(trivia_app_question.question);
} else {
LOG_ERR("Unexpected number of results: %d, expected 1", cJSON_GetArraySize(results));
}
}
memset(trivia_app_question.question, 0, sizeof(trivia_app_question.question));
strncpy(trivia_app_question.question, question->valuestring, sizeof(trivia_app_question.question));
trivia_app_question.correct_answer = (correct_answer->valuestring[0] == 'F') ? false : true;
trivia_ui_update_question(trivia_app_question.question);
cJSON_Delete(parsed_response);
}
}

Expand Down
33 changes: 15 additions & 18 deletions app/src/applications/watchface/watchface_app.c
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ static delayed_work_item_t general_work_item;
static struct k_work_sync canel_work_sync;

static K_WORK_DEFINE(update_ui_work, update_ui_from_event);
static ble_comm_cb_data_t last_data_update;
static ble_comm_data_type_t last_data_update_type;
static ble_comm_weather_t last_weather_data;
static ble_comm_music_info_t last_music_info;
static struct battery_sample_event last_batt_evt = {.percent = 100, .mV = 4300};
Expand Down Expand Up @@ -354,19 +354,18 @@ static void disconnected(struct bt_conn *conn, uint8_t reason)
static void update_ui_from_event(struct k_work *item)
{
if (running && !is_suspended) {
if (last_data_update.type == BLE_COMM_DATA_TYPE_WEATHER) {
LOG_DBG("Weather: %s t: %d hum: %d code: %d wind: %d dir: %d", last_data_update.data.weather.report_text,
last_data_update.data.weather.temperature_c, last_data_update.data.weather.humidity,
last_data_update.data.weather.weather_code,
last_data_update.data.weather.wind,
last_data_update.data.weather.wind_direction);
watchfaces[watchface_settings.watchface_index]->set_weather(last_data_update.data.weather.temperature_c,
last_data_update.data.weather.weather_code);
} else if (last_data_update.type == BLE_COMM_DATA_TYPE_SET_TIME) {
if (last_data_update_type == BLE_COMM_DATA_TYPE_WEATHER) {
LOG_DBG("Weather: %s t: %d hum: %d code: %d wind: %d dir: %d", last_weather_data.report_text,
last_weather_data.temperature_c, last_weather_data.humidity,
last_weather_data.weather_code,
last_weather_data.wind,
last_weather_data.wind_direction);
watchfaces[watchface_settings.watchface_index]->set_weather(last_weather_data.temperature_c,
last_weather_data.weather_code);
} else if (last_data_update_type == BLE_COMM_DATA_TYPE_SET_TIME) {
k_work_reschedule(&date_work.work, K_NO_WAIT);
} else if (last_data_update.type == BLE_COMM_DATA_TYPE_MUSIC_INFO) {
zsw_watchface_dropdown_ui_set_music_info(last_data_update.data.music_info.track_name,
last_data_update.data.music_info.artist);
} else if (last_data_update_type == BLE_COMM_DATA_TYPE_MUSIC_INFO) {
zsw_watchface_dropdown_ui_set_music_info(last_music_info.track_name, last_music_info.artist);
}
return;
}
Expand All @@ -375,14 +374,12 @@ static void update_ui_from_event(struct k_work *item)
static void zbus_ble_comm_data_callback(const struct zbus_channel *chan)
{
const struct ble_data_event *event = zbus_chan_const_msg(chan);
// TODO getting this callback again before workqueue has ran will
// cause previous to be lost.
memcpy(&last_data_update, &event->data, sizeof(ble_comm_cb_data_t));
last_data_update_type = event->data.type;
if (event->data.type == BLE_COMM_DATA_TYPE_WEATHER) {
memcpy(&last_weather_data, &last_data_update.data.weather, sizeof(last_data_update.data.weather));
memcpy(&last_weather_data, &event->data.data.weather, sizeof(event->data.data.weather));
}
if (event->data.type == BLE_COMM_DATA_TYPE_MUSIC_INFO) {
memcpy(&last_music_info, &last_data_update.data.music_info, sizeof(last_data_update.data.music_info));
memcpy(&last_music_info, &event->data.data.music_info, sizeof(event->data.data.music_info));
}
if (running && !is_suspended) {
k_work_submit(&update_ui_work);
Expand Down
4 changes: 4 additions & 0 deletions app/src/applications/weather/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
if (CONFIG_ZSWATCH_PCB_REV GREATER_EQUAL 4)
FILE(GLOB app_sources *.c)
target_sources(app PRIVATE ${app_sources})
endif()
155 changes: 155 additions & 0 deletions app/src/applications/weather/weather_app.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#include <zephyr/kernel.h>
#include <zephyr/init.h>
#include <zephyr/zbus/zbus.h>
#include <zephyr/logging/log.h>
#include "cJSON.h"

#include "managers/zsw_app_manager.h"
#include "ui/utils/zsw_ui_utils.h"
#include "events/ble_event.h"
#include <ble/ble_http.h>
#include "weather_ui.h"
#include <zsw_clock.h>

LOG_MODULE_REGISTER(weather_app, LOG_LEVEL_DBG);

#define HTTP_REQUEST_URL_FMT "https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f&current=wind_speed_10m,temperature_2m,apparent_temperature,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,precipitation_sum,rain_sum,precipitation_probability_max&wind_speed_unit=ms&timezone=auto&forecast_days=%d"

#define MAX_GPS_AGED_TIME_MS 30 * 60 * 1000

// Functions needed for all applications
static void weather_app_start(lv_obj_t *root, lv_group_t *group);
static void weather_app_stop(void);
static void on_zbus_ble_data_callback(const struct zbus_channel *chan);

ZBUS_CHAN_DECLARE(ble_comm_data_chan);
ZBUS_LISTENER_DEFINE(weather_ble_comm_lis, on_zbus_ble_data_callback);
ZBUS_CHAN_ADD_OBS(ble_comm_data_chan, weather_ble_comm_lis, 1);

ZSW_LV_IMG_DECLARE(weather_app_icon);

static bool active;
static uint64_t last_update_gps_time;
static uint64_t last_update_weather_time;
static double last_lat;
static double last_lon;

static application_t app = {
.name = "Weather",
.icon = ZSW_LV_IMG_USE(weather_app_icon),
.start_func = weather_app_start,
.stop_func = weather_app_stop
};

static void http_rsp_cb(ble_http_status_code_t status, char *response)
{
zsw_timeval_t time_now;
weather_ui_current_weather_data_t current_weather;
weather_ui_forecast_data_t forecasts[WEATHER_UI_NUM_FORECASTS];
char *days[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};

if (!active) {
return;
}

if (status == BLE_HTTP_STATUS_OK) {
zsw_clock_get_time(&time_now);
cJSON *parsed_response = cJSON_Parse(response);
cJSON *current = cJSON_GetObjectItem(parsed_response, "current");
cJSON *current_temperature_2m = cJSON_GetObjectItem(current, "temperature_2m");
current_weather.temperature = current_temperature_2m->valuedouble;
cJSON *current_weather_code = cJSON_GetObjectItem(current, "weather_code");
current_weather.icon = zsw_ui_utils_icon_from_wmo_weather_code(current_weather_code->valueint, &current_weather.color,
&current_weather.text);
cJSON *current_wind_speed = cJSON_GetObjectItem(current, "wind_speed_10m");
current_weather.wind_speed = current_wind_speed->valuedouble;
cJSON *apparent_temperature = cJSON_GetObjectItem(current, "apparent_temperature");
current_weather.apparent_temperature = apparent_temperature->valuedouble;

cJSON *daily_forecasts = cJSON_GetObjectItem(parsed_response, "daily");

cJSON *weather_code_list = cJSON_GetObjectItem(daily_forecasts, "weather_code");
cJSON *temperature_2m_max_list = cJSON_GetObjectItem(daily_forecasts, "temperature_2m_max");
cJSON *temperature_2m_min_list = cJSON_GetObjectItem(daily_forecasts, "temperature_2m_min");
cJSON *precipitation_probability_max_list = cJSON_GetObjectItem(daily_forecasts, "precipitation_probability_max");

for (int i = 0; i < cJSON_GetArraySize(weather_code_list); i++) {
forecasts[i].temperature = cJSON_GetArrayItem(temperature_2m_max_list, i)->valuedouble;
forecasts[i].low_temp = cJSON_GetArrayItem(temperature_2m_min_list, i)->valuedouble;
forecasts[i].high_temp = cJSON_GetArrayItem(temperature_2m_max_list, i)->valuedouble;
forecasts[i].rain_percent = cJSON_GetArrayItem(precipitation_probability_max_list, i)->valueint;
forecasts[i].icon = zsw_ui_utils_icon_from_wmo_weather_code(cJSON_GetArrayItem(weather_code_list, i)->valueint,
&forecasts[i].color, &forecasts[i].text);
sprintf(forecasts[i].day, "%s", days[(time_now.tm.tm_wday + i) % 7]);
}

weather_ui_set_weather_data(current_weather, forecasts, cJSON_GetArraySize(weather_code_list));

cJSON_Delete(parsed_response);
last_update_weather_time = k_uptime_get();
ble_comm_request_gps_status(false);
} else {
LOG_ERR("HTTP request failed\n");
}
}

static void fetch_weather_data(double lat, double lon)
{
char weather_url[512];
snprintf(weather_url, sizeof(weather_url), HTTP_REQUEST_URL_FMT, lat, lon, WEATHER_UI_NUM_FORECASTS);
zsw_ble_http_get(weather_url, http_rsp_cb);
// TODO Handle if HTTP requests are not enabled or supported on phone.
}

static void on_zbus_ble_data_callback(const struct zbus_channel *chan)
{
const struct ble_data_event *event = zbus_chan_const_msg(chan);

if (event->data.type == BLE_COMM_DATA_TYPE_GPS) {
last_update_gps_time = k_uptime_get();
LOG_DBG("Got GPS data, fetch weather\n");
LOG_DBG("Latitude: %f\n", event->data.data.gps.lat);
LOG_DBG("Longitude: %f\n", event->data.data.gps.lon);
last_lat = event->data.data.gps.lat;
last_lon = event->data.data.gps.lon;
fetch_weather_data(event->data.data.gps.lat, event->data.data.gps.lon);
}
}

static void weather_app_start(lv_obj_t *root, lv_group_t *group)
{
if (last_update_gps_time == 0 || k_uptime_delta(&last_update_gps_time) > MAX_GPS_AGED_TIME_MS) {
LOG_DBG("GPS data is too old, request GPS\n");
int res = ble_comm_request_gps_status(true);
if (res != 0) {
LOG_ERR("Failed to request GPS data\n");
}
// TODO Show GPS fetching in progress in app
} else {
fetch_weather_data(last_lat, last_lon);
}
weather_ui_show(root);

zsw_timeval_t time;
zsw_clock_get_time(&time);
weather_ui_set_time(time.tm.tm_hour, time.tm.tm_min, time.tm.tm_sec);
active = true;
}

static void weather_app_stop(void)
{
active = false;
weather_ui_remove();
ble_comm_request_gps_status(false);
}

static int weather_app_add(void)
{
zsw_app_manager_add_application(&app);

// TODO Add periodic GPS and weather polling in backgrund.

return 0;
}

SYS_INIT(weather_app_add, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
Loading