Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
67d6b1b
feature(wind): add wind speed graph, Clay options, clamp to max
Feb 2, 2026
993eab3
Add show_wind_graph toggle; revert UUID and displayName to match master
Feb 20, 2026
743fead
Fix crash: move show_wind_graph to end of Config struct for persist c…
Feb 20, 2026
fd21273
Fix crash on upgrade: proper ConfigV1->Config migration in config.c
Feb 20, 2026
7e35a20
Bump version to 1.25.0
Feb 20, 2026
bd14bd4
Fix crash on upgrade: move WIND_TREND to end of persist enum key
Feb 21, 2026
c320c63
Fix stale OWM weather cache causing inaccurate wind/temp data
Mar 12, 2026
fc87c6f
Merge feature/wind-speed-graph into updated main (v1.30.0)
Mar 30, 2026
0773212
Remove unnecessary ConfigV2 migration (wind config is dev-only)
Mar 30, 2026
0e85273
fix: use temp file in prepare-package.sh to avoid Volta chicken-and-egg
Mar 30, 2026
063d59f
feat: add wind speed graph overlay to forecast
Mar 30, 2026
44659df
fix: use temp file in prepare-package.sh to avoid Volta chicken-and-egg
Mar 30, 2026
f71bf5b
Merge remote-tracking branch 'origin/main' into feature/wind-speed-gr…
Apr 4, 2026
1a7189a
Merge branch 'feature/wind-speed-graph-updated' into feature/wind-spe…
Apr 5, 2026
f364e93
Merge branch 'main' into feature/wind-speed-graph-updated
Apr 7, 2026
19e1e33
feat: remove auto wind max, default to 20 mph/kph fixed scale
Apr 7, 2026
e2ce8bd
feat: improve wind max config — better labels, kph-friendly options, …
Apr 7, 2026
79676ff
perf: wind graph — move VLAs inside guard, integer math, add memory p…
Apr 7, 2026
ed4a834
fix: refresh watchface on appear to prevent partial screen after noti…
Apr 7, 2026
ef27cc3
Merge branch 'feature/wind-speed-graph-updated' into feature/wind-spe…
Apr 7, 2026
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
6 changes: 5 additions & 1 deletion package.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"messageKeys": [
"TEMP_TREND_INT16",
"PRECIP_TREND_UINT8",
"WIND_TREND_UINT8",
"FORECAST_START",
"NUM_ENTRIES",
"CURRENT_TEMP",
Expand All @@ -86,7 +87,10 @@
"CLAY_COLOR_SUNDAY",
"CLAY_COLOR_US_FEDERAL",
"CLAY_COLOR_TIME",
"CLAY_DAY_NIGHT_SHADING"
"CLAY_DAY_NIGHT_SHADING",
"CLAY_WIND_UNIT",
"CLAY_WIND_MAX",
"CLAY_SHOW_WIND_GRAPH"
]
}
}
3 changes: 2 additions & 1 deletion scripts/prepare-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ if [[ ! -f "$profile_file" ]]; then
exit 1
fi

npx --yes mustache "$profile_file" "$template_file" > "$output_file"
npx --yes mustache "$profile_file" "$template_file" > "${output_file}.tmp"
mv "${output_file}.tmp" "${output_file}"
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
Expand Down
17 changes: 15 additions & 2 deletions src/c/appendix/app_message.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ static void inbox_received_callback(DictionaryIterator *iterator, void *context)
// Weather data
Tuple *temp_trend_tuple = dict_find(iterator, MESSAGE_KEY_TEMP_TREND_INT16);
Tuple *precip_trend_tuple = dict_find(iterator, MESSAGE_KEY_PRECIP_TREND_UINT8);
Tuple *wind_trend_tuple = dict_find(iterator, MESSAGE_KEY_WIND_TREND_UINT8);
Tuple *forecast_start_tuple = dict_find(iterator, MESSAGE_KEY_FORECAST_START);
Tuple *num_entries_tuple = dict_find(iterator, MESSAGE_KEY_NUM_ENTRIES);
Tuple *current_temp_tuple = dict_find(iterator, MESSAGE_KEY_CURRENT_TEMP);
Expand All @@ -38,8 +39,11 @@ static void inbox_received_callback(DictionaryIterator *iterator, void *context)
Tuple *clay_color_us_federal_tuple = dict_find(iterator, MESSAGE_KEY_CLAY_COLOR_US_FEDERAL);
Tuple *clay_color_time_tuple = dict_find(iterator, MESSAGE_KEY_CLAY_COLOR_TIME);
Tuple *clay_day_night_shading_tuple = dict_find(iterator, MESSAGE_KEY_CLAY_DAY_NIGHT_SHADING);
Tuple *clay_wind_unit_tuple = dict_find(iterator, MESSAGE_KEY_CLAY_WIND_UNIT);
Tuple *clay_wind_max_tuple = dict_find(iterator, MESSAGE_KEY_CLAY_WIND_MAX);
Tuple *clay_show_wind_graph_tuple = dict_find(iterator, MESSAGE_KEY_CLAY_SHOW_WIND_GRAPH);

if(temp_trend_tuple && temp_trend_tuple && forecast_start_tuple && num_entries_tuple && city_tuple && sun_events_tuple) {
if(temp_trend_tuple && forecast_start_tuple && num_entries_tuple && city_tuple && sun_events_tuple && precip_trend_tuple && wind_trend_tuple) {
// Weather data received
APP_LOG(APP_LOG_LEVEL_INFO, "All tuples received!");
persist_set_forecast_start((time_t)forecast_start_tuple->value->int32);
Expand All @@ -55,6 +59,8 @@ static void inbox_received_callback(DictionaryIterator *iterator, void *context)
persist_set_temp_trend(temp_data, num_entries);
uint8_t *precip_data = (uint8_t*) precip_trend_tuple->value->data;
persist_set_precip_trend(precip_data, num_entries);
uint8_t *wind_data = (uint8_t*) wind_trend_tuple->value->data;
persist_set_wind_trend(wind_data, num_entries);
persist_set_city((char*)city_tuple->value->cstring);
int lo, hi;
min_max(temp_data, num_entries, &lo, &hi);
Expand All @@ -74,7 +80,8 @@ static void inbox_received_callback(DictionaryIterator *iterator, void *context)
else if (clay_celsius_tuple && clay_time_lead_zero_tuple && clay_axis_12h_tuple && clay_start_mon_tuple && clay_prev_week_tuple
&& clay_color_today_tuple && clay_time_font_tuple && clay_vibe_tuple && clay_show_qt_tuple && clay_show_bt_tuple
&& clay_show_bt_disconnect_tuple && clay_show_am_pm_tuple && clay_color_saturday_tuple && clay_color_sunday_tuple
&& clay_color_us_federal_tuple && clay_color_time_tuple && clay_day_night_shading_tuple) {
&& clay_color_us_federal_tuple && clay_color_time_tuple && clay_day_night_shading_tuple
&& clay_wind_unit_tuple && clay_wind_max_tuple && clay_show_wind_graph_tuple) {
// Clay config data received
bool clay_celsius = (bool) (clay_celsius_tuple->value->int16);
bool time_lead_zero = (bool) (clay_time_lead_zero_tuple->value->int16);
Expand All @@ -93,13 +100,19 @@ static void inbox_received_callback(DictionaryIterator *iterator, void *context)
GColor color_sunday = GColorFromHEX(clay_color_sunday_tuple->value->int32);
GColor color_us_federal = GColorFromHEX(clay_color_us_federal_tuple->value->int32);
GColor color_time = GColorFromHEX(clay_color_time_tuple->value->int32);
int16_t wind_unit = clay_wind_unit_tuple->value->int16;
int16_t wind_max = clay_wind_max_tuple->value->int16;
bool show_wind_graph = (bool) (clay_show_wind_graph_tuple->value->int16);
Config config = (Config) {
.celsius = clay_celsius,
.time_lead_zero = time_lead_zero,
.axis_12h = axis_12h,
.start_mon = start_mon,
.prev_week = prev_week,
.time_font = time_font,
.wind_unit = wind_unit,
.wind_max = wind_max,
.show_wind_graph = show_wind_graph,
.color_today = color_today,
.vibe = vibe,
.show_qt = show_qt,
Expand Down
61 changes: 60 additions & 1 deletion src/c/appendix/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,77 @@ static Config config_defaults(void) {
.vibe = false,
.show_am_pm = false,
.time_font = TIME_FONT_ROBOTO,
.wind_unit = 0,
.wind_max = 20,
.color_today = GColorBlack,
.color_saturday = GColorWhite,
.color_sunday = GColorWhite,
.color_us_federal = GColorWhite,
.color_time = GColorWhite,
.show_wind_graph = true,
.day_night_shading = true
};
}

// Config layout as it existed in master (before wind fields were added).
// Used for migration when upgrading from master -> this branch.
typedef struct {
bool celsius;
bool time_lead_zero;
bool axis_12h;
bool start_mon;
bool prev_week;
bool show_qt;
bool show_bt;
bool show_bt_disconnect;
bool vibe;
bool show_am_pm;
int16_t time_font;
GColor color_today;
GColor color_saturday;
GColor color_sunday;
GColor color_us_federal;
GColor color_time;
} ConfigV1;

static void config_migrate(int bytes_read) {
if (bytes_read == (int)sizeof(ConfigV1)) {
// Upgrading from master: persist_read_data loaded 17 bytes at the start
// of the new struct. The 5 GColor fields were at raw offsets
// 12-16 in ConfigV1, but in Config they are at 16-20 (shifted by the
// two new int16_t wind fields). Extract the colors from their raw byte
// positions before we overwrite anything.
uint8_t *raw = (uint8_t*) g_config;
GColor old_color_today = (GColor){ .argb = raw[12] };
GColor old_color_saturday = (GColor){ .argb = raw[13] };
GColor old_color_sunday = (GColor){ .argb = raw[14] };
GColor old_color_us_federal = (GColor){ .argb = raw[15] };
GColor old_color_time = (GColor){ .argb = raw[16] };
// Wind fields: safe defaults
g_config->wind_unit = 0; // mph
g_config->wind_max = 20;
// Restore colors at their correct new offsets
g_config->color_today = old_color_today;
g_config->color_saturday = old_color_saturday;
g_config->color_sunday = old_color_sunday;
g_config->color_us_federal = old_color_us_federal;
g_config->color_time = old_color_time;
g_config->show_wind_graph = true;
g_config->day_night_shading = true;
} else if (bytes_read < (int)sizeof(Config)) {
// Unknown layout smaller than current — apply safe defaults for all new fields
g_config->wind_unit = 0;
g_config->wind_max = 20;
g_config->show_wind_graph = true;
g_config->day_night_shading = true;
}
// bytes_read == sizeof(Config): current format, nothing to do
}

static void config_read_or_default(Config *config) {
*config = config_defaults();
persist_get_config(config);
int bytes_read = persist_get_config(config);
config_migrate(bytes_read);
}

void config_load() {
Expand Down
4 changes: 4 additions & 0 deletions src/c/appendix/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ typedef struct {
bool vibe;
bool show_am_pm;
int16_t time_font;
int16_t wind_unit; // 0 = mph, 1 = kph
int16_t wind_max; // 0 = auto, otherwise fixed max value (same unit as wind_unit)
GColor color_today;
GColor color_saturday;
GColor color_sunday;
GColor color_us_federal;
GColor color_time;
// Added after v1 — must stay at end for backward-compat persist reads
bool show_wind_graph;
bool day_night_shading;
} Config;

Expand Down
19 changes: 17 additions & 2 deletions src/c/appendix/persist.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

enum key {
TEMP_LO, TEMP_HI, TEMP_TREND, PRECIP_TREND, FORECAST_START, CITY, SUN_EVENT_START_TYPE, SUN_EVENT_TIMES, NUM_ENTRIES,
CURRENT_TEMP, BATTERY_LEVEL, CONFIG
CURRENT_TEMP, BATTERY_LEVEL, CONFIG, WIND_TREND
}; // Deprecated: BATTERY_LEVEL

void persist_init() {
Expand All @@ -19,7 +19,11 @@ void persist_init() {
}
if (!persist_exists(PRECIP_TREND)) {
uint8_t data[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
persist_write_data(TEMP_TREND, (void*) data, 12*sizeof(uint8_t));
persist_write_data(PRECIP_TREND, (void*) data, 12*sizeof(uint8_t));
}
if (!persist_exists(WIND_TREND)) {
uint8_t data_w[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
persist_write_data(WIND_TREND, (void*) data_w, 12*sizeof(uint8_t));
}
if (!persist_exists(FORECAST_START)) {
persist_write_int(FORECAST_START, 0);
Expand Down Expand Up @@ -48,6 +52,9 @@ void persist_init() {
.start_mon = false,
.prev_week = true,
.time_font = TIME_FONT_ROBOTO,
.wind_unit = 0,
.wind_max = 0,
.show_wind_graph = true,
.color_today = GColorBlack,
.show_qt = true,
.show_bt = true,
Expand Down Expand Up @@ -80,6 +87,10 @@ int persist_get_precip_trend(uint8_t *buffer, const size_t buffer_size) {
return persist_read_data(PRECIP_TREND, (void*) buffer, buffer_size * sizeof(uint8_t));
}

int persist_get_wind_trend(uint8_t *buffer, const size_t buffer_size) {
return persist_read_data(WIND_TREND, (void*) buffer, buffer_size * sizeof(uint8_t));
}

time_t persist_get_forecast_start() {
return (time_t) persist_read_int(FORECAST_START);
}
Expand Down Expand Up @@ -124,6 +135,10 @@ void persist_set_precip_trend(uint8_t *data, const size_t size) {
persist_write_data(PRECIP_TREND, (void*) data, size * sizeof(uint8_t));
}

void persist_set_wind_trend(uint8_t *data, const size_t size) {
persist_write_data(WIND_TREND, (void*) data, size * sizeof(uint8_t));
}

void persist_set_forecast_start(time_t val) {
persist_write_int(FORECAST_START, (int) val);
}
Expand Down
4 changes: 4 additions & 0 deletions src/c/appendix/persist.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ int persist_get_temp_trend(int16_t *buffer, const size_t buffer_size);

int persist_get_precip_trend(uint8_t *buffer, const size_t buffer_size);

int persist_get_wind_trend(uint8_t *buffer, const size_t buffer_size);

time_t persist_get_forecast_start();

int persist_get_num_entries();
Expand All @@ -36,6 +38,8 @@ void persist_set_temp_trend(int16_t *data, const size_t size);

void persist_set_precip_trend(uint8_t *data, const size_t size);

void persist_set_wind_trend(uint8_t *data, const size_t size);

void persist_set_forecast_start(time_t val);

void persist_set_num_entries(int val);
Expand Down
62 changes: 62 additions & 0 deletions src/c/layers/forecast_layer.c
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,68 @@ static void forecast_update_proc(Layer *layer, GContext *ctx)
gpath_destroy(path_temp);
MEMORY_HEAP_PROBE_SAMPLE("after_temp_path_destroy", &redraw_probe);

// Prepare and draw the wind speed line (scaled independently)
if (g_config && g_config->show_wind_graph) {
uint8_t winds[num_entries];
GPoint points_wind[num_entries];
persist_get_wind_trend(winds, num_entries);
int max_wind = (g_config->wind_max > 0) ? g_config->wind_max : 20;
for (int i = 0; i < num_entries; ++i) {
int entry_x = graph_bounds.origin.x + i * entry_w;
int wind = winds[i];
if (wind > max_wind) {
wind = max_wind; // Clamp to configured max so line sticks to top
}
int wind_h = (int32_t)wind * temp_plot_h / max_wind;
points_wind[i] = GPoint(entry_x, h - wind_h - MARGIN_TEMP_H - BOTTOM_AXIS_H);
}
GPathInfo path_info_wind = {
.num_points = num_entries,
.points = points_wind
};
MEMORY_HEAP_PROBE_SAMPLE("before_wind_path_create", &redraw_probe);
GPath *path_wind = gpath_create(&path_info_wind);
MEMORY_HEAP_PROBE_SAMPLE("after_wind_path_create", &redraw_probe);
graphics_context_set_stroke_color(ctx, PBL_IF_COLOR_ELSE(GColorYellow, GColorWhite));
graphics_context_set_stroke_width(ctx, 1);
gpath_draw_outline_open(ctx, path_wind);
MEMORY_HEAP_PROBE_SAMPLE("before_wind_path_destroy", &redraw_probe);
gpath_destroy(path_wind);
MEMORY_HEAP_PROBE_SAMPLE("after_wind_path_destroy", &redraw_probe);
} // end show_wind_graph

// Prepare and draw the wind speed line (scaled independently)
if (g_config && g_config->show_wind_graph) {
int max_wind = 0;
// If a fixed max is configured, use it (in same units as persisted wind data)
if (g_config && g_config->wind_max > 0) {
max_wind = g_config->wind_max;
} else {
for (int i = 0; i < num_entries; ++i) {
if ((int)winds[i] > max_wind) max_wind = winds[i];
}
if (max_wind == 0) max_wind = 1; // avoid divide by zero
}
for (int i = 0; i < num_entries; ++i) {
int entry_x = graph_bounds.origin.x + i * entry_w;
int wind = winds[i];
if (wind > max_wind) {
wind = max_wind; // Clamp to configured max so line sticks to top
}
int wind_h = (float) wind / max_wind * (h - MARGIN_TEMP_H * 2 - BOTTOM_AXIS_H);
points_wind[i] = GPoint(entry_x, h - wind_h - MARGIN_TEMP_H - BOTTOM_AXIS_H);
}
GPathInfo path_info_wind = {
.num_points = num_entries,
.points = points_wind
};
GPath *path_wind = gpath_create(&path_info_wind);
graphics_context_set_stroke_color(ctx, PBL_IF_COLOR_ELSE(GColorYellow, GColorWhite));
graphics_context_set_stroke_width(ctx, 1);
gpath_draw_outline_open(ctx, path_wind);
gpath_destroy(path_wind);
} // end show_wind_graph

// Draw a line for the bottom axis
graphics_context_set_stroke_color(ctx, render_spec.axis_color);
graphics_context_set_stroke_width(ctx, 1);
Expand Down
7 changes: 7 additions & 0 deletions src/c/windows/main_window.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ static void main_window_load(Window *window) {
MEMORY_LOG_HEAP("after_window_load");
}

static void main_window_appear(Window *window) {
main_window_refresh();
loading_layer_refresh();
}

static void main_window_unload(Window *window) {
MEMORY_LOG_HEAP("before_window_unload");
time_layer_destroy();
Expand Down Expand Up @@ -76,6 +81,7 @@ void main_window_create() {
// Set handlers to manage the elements inside the Window
window_set_window_handlers(s_main_window, (WindowHandlers) {
.load = main_window_load,
.appear = main_window_appear,
.unload = main_window_unload
});

Expand All @@ -93,6 +99,7 @@ void main_window_refresh() {
forecast_layer_refresh();
calendar_layer_refresh();
calendar_status_layer_refresh();
loading_layer_refresh();
}

void main_window_destroy() {
Expand Down
31 changes: 31 additions & 0 deletions src/pkjs/clay/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,37 @@ module.exports = [
"defaultValue": true,
"description": "Show hatch shading between sunset and sunrise to distinguish day and night on the forecast graph."
},
{
"type": "select",
"defaultValue": "mph",
"messageKey": "windUnit",
"label": "Wind Units",
"options": [
{ "label": "mph", "value": "mph" },
{ "label": "kph", "value": "kph" }
]
},
{
"type": "select",
"defaultValue": "20",
"messageKey": "windMax",
"label": "Wind graph max speed",
"description": "Sets the top of the wind speed scale. Default 20 for mph / 30 for kph.",
"options": [
{ "label": "10", "value": "10" },
{ "label": "20", "value": "20" },
{ "label": "30", "value": "30" },
{ "label": "40", "value": "40" },
{ "label": "60", "value": "60" },
{ "label": "80", "value": "80" }
]
},
{
"type": "toggle",
"label": "Show wind speed graph",
"messageKey": "showWindGraph",
"defaultValue": true
},
{
"type": "radiogroup",
"label": "Provider",
Expand Down
Loading