Skip to content

Commit 315b5ab

Browse files
committed
Fix CodeRabbit issues:
- Avoid leaving segments permanently frozen; restore previous freeze state - Fix negative modulo in local-time marker logic - Handle invalid segments safely - Clarify duplicate "N" table row - Fix country normalizeLocation bug - Don't include the API key in logging - Tighten wording; avoid hardcoding OWM plan limits - Fix rate-limit reset logic - Harden rate-limit check and keep informative logging - Add network timeout to avoid long blocking on bad links - Check non-2xx status codes before parsing - Right-size JSON buffer per target - Fix potential buffer overrun in emitSeriesMDHM - Use float instead of double for DataPoint.value - Restore seg.freeze after rendering (and avoid freezing on zero-length) - use PROGMEM to reduce RAM use - Sanitize HSV inputs - Clamp HSV values in conversion to RGB - Clarify SAFETY_DELAY_MS type - Clamp and wrap HSV inputs to prevent overflow/underflow artifacts - Normalize wind direction to [0,360) before mapping to hue - fixed compiler warning about shadowed variables - delay initial checkhistory after fetch
1 parent 2df807d commit 315b5ab

15 files changed

+161
-84
lines changed

usermods/usermod_v2_skystrip/FAQ.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The mapping between wind direction and hue can be approximated as:
3939
| SW | 90 | Lime |
4040
| W | 120 | Green |
4141
| NW | 180 | Cyan |
42-
| N | 240 | Blue |
42+
| N | 240 | Blue | (wraps around)
4343

4444

4545
## Temperature View (TV)

usermods/usermod_v2_skystrip/cloud_view.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#include <limits>
88

99
static constexpr int16_t DEFAULT_SEG_ID = -1; // -1 means disabled
10-
const char CFG_SEG_ID[] = "SegmentId";
10+
const char CFG_SEG_ID[] PROGMEM = "SegmentId";
1111

1212
static bool isDay(const SkyModel &m, time_t t) {
1313
const time_t MAXTT = std::numeric_limits<time_t>::max();
@@ -50,12 +50,12 @@ void CloudView::view(time_t now, SkyModel const &model, int16_t dbgPixelIndex) {
5050
return;
5151

5252
Segment &seg = strip.getSegment((uint8_t)segId_);
53-
seg.freeze = true;
5453
int start = seg.start;
5554
int end = seg.stop - 1;
5655
int len = end - start + 1;
57-
if (len == 0)
56+
if (len <= 0)
5857
return;
58+
skystrip::util::FreezeGuard freezeGuard(seg);
5959

6060
constexpr double kHorizonSec = 48.0 * 3600.0;
6161
const double step = (len > 1) ? (kHorizonSec / double(len - 1)) : 0.0;
@@ -73,9 +73,9 @@ void CloudView::view(time_t now, SkyModel const &model, int16_t dbgPixelIndex) {
7373
time_t sunriseTOD = 0;
7474
time_t sunsetTOD = 0;
7575
if (useSunrise)
76-
sunriseTOD = (sunrise + offset) % DAY;
76+
sunriseTOD = (((sunrise + offset) % DAY) + DAY) % DAY; // normalize to [0, DAY)
7777
if (useSunset)
78-
sunsetTOD = (sunset + offset) % DAY;
78+
sunsetTOD = (((sunset + offset) % DAY) + DAY) % DAY; // normalize to [0, DAY)
7979

8080
auto nearTOD = [&](time_t a, time_t b) {
8181
time_t diff = (a >= b) ? (a - b) : (b - a);
@@ -87,7 +87,7 @@ void CloudView::view(time_t now, SkyModel const &model, int16_t dbgPixelIndex) {
8787
auto isMarker = [&](time_t t) {
8888
if (!useSunrise && !useSunset)
8989
return false;
90-
time_t tod = (t + offset) % DAY;
90+
time_t tod = (((t + offset) % DAY) + DAY) % DAY; // normalize to [0, DAY)
9191
if (useSunrise && nearTOD(tod, sunriseTOD))
9292
return true;
9393
if (useSunset && nearTOD(tod, sunsetTOD))

usermods/usermod_v2_skystrip/delta_view.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#include "wled.h"
88

99
static constexpr int16_t DEFAULT_SEG_ID = -1; // -1 means disabled
10-
const char CFG_SEG_ID[] = "SegmentId";
10+
const char CFG_SEG_ID[] PROGMEM = "SegmentId";
1111

1212
struct Stop {
1313
double f;
@@ -74,12 +74,12 @@ void DeltaView::view(time_t now, SkyModel const &model, int16_t dbgPixelIndex) {
7474
return;
7575

7676
Segment &seg = strip.getSegment((uint8_t)segId_);
77-
seg.freeze = true;
7877
int start = seg.start;
7978
int end = seg.stop - 1;
8079
int len = end - start + 1;
81-
if (len == 0)
80+
if (len <= 0)
8281
return;
82+
skystrip::util::FreezeGuard freezeGuard(seg);
8383

8484
constexpr double kHorizonSec = 48.0 * 3600.0;
8585
const double step = (len > 1) ? (kHorizonSec / double(len - 1)) : 0.0;

usermods/usermod_v2_skystrip/open_weather_map_source.cpp

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ static constexpr unsigned DEFAULT_INTERVAL_SEC = 3600; // 1 hour
2020
// - these are user visible in the webapp settings UI
2121
// - they are scoped to this module, don't need to be globally unique
2222
//
23-
const char CFG_API_BASE[] = "ApiBase";
24-
const char CFG_API_KEY[] = "ApiKey";
25-
const char CFG_LATITUDE[] = "Latitude";
26-
const char CFG_LONGITUDE[] = "Longitude";
27-
const char CFG_INTERVAL_SEC[] = "IntervalSec";
28-
const char CFG_LOCATION[] = "Location";
23+
const char CFG_API_BASE[] PROGMEM = "ApiBase";
24+
const char CFG_API_KEY[] PROGMEM = "ApiKey";
25+
const char CFG_LATITUDE[] PROGMEM = "Latitude";
26+
const char CFG_LONGITUDE[] PROGMEM = "Longitude";
27+
const char CFG_INTERVAL_SEC[] PROGMEM = "IntervalSec";
28+
const char CFG_LOCATION[] PROGMEM = "Location";
2929

3030
// keep commas; encode spaces etc.
3131
static void urlEncode(const char* src, char* dst, size_t dstSize) {
@@ -46,6 +46,27 @@ static void urlEncode(const char* src, char* dst, size_t dstSize) {
4646
dst[di] = '\0';
4747
}
4848

49+
// Redact the API key in a URL by replacing the value after "appid=" with '*'.
50+
static void redactApiKeyInUrl(const char* in, char* out, size_t outLen) {
51+
if (!in || !out || outLen == 0) return;
52+
const char* p = strstr(in, "appid=");
53+
if (!p) {
54+
strncpy(out, in, outLen);
55+
out[outLen - 1] = '\0';
56+
return;
57+
}
58+
size_t prefixLen = (size_t)(p - in) + 6; // include "appid="
59+
if (prefixLen >= outLen) {
60+
// Not enough space; best effort copy and terminate
61+
strncpy(out, in, outLen);
62+
out[outLen - 1] = '\0';
63+
return;
64+
}
65+
memcpy(out, in, prefixLen);
66+
out[prefixLen] = '*';
67+
out[prefixLen + 1] = '\0';
68+
}
69+
4970
// Normalize "Oakland, CA, USA" → "Oakland,CA,US" in-place
5071
static void normalizeLocation(char* q) {
5172
// trim spaces and commas
@@ -57,9 +78,8 @@ static void normalizeLocation(char* q) {
5778
*out = '\0';
5879
len = strlen(q);
5980
if (len >= 4 && strcasecmp(q + len - 4, ",USA") == 0) {
60-
q[len - 2] = 'U';
61-
q[len - 1] = 'S';
62-
q[len] = '\0';
81+
// Truncate the trailing 'A' so ",USA" → ",US" without corrupting chars
82+
q[len - 1] = '\0';
6383
}
6484
}
6585

@@ -200,11 +220,14 @@ std::unique_ptr<SkyModel> OpenWeatherMapSource::fetch(std::time_t now) {
200220
return nullptr;
201221

202222
lastFetch_ = now;
223+
lastHistFetch_ = now; // history fetches should wait
203224

204225
// Fetch JSON
205226
char url[256];
206227
composeApiUrl(url, sizeof(url));
207-
DEBUG_PRINTF("SkyStrip: %s::fetch URL: %s\n", name().c_str(), url);
228+
char redacted[256];
229+
redactApiKeyInUrl(url, redacted, sizeof(redacted));
230+
DEBUG_PRINTF("SkyStrip: %s::fetch URL: %s\n", name().c_str(), redacted);
208231

209232
auto doc = getJson(url);
210233
if (!doc) {
@@ -253,12 +276,12 @@ std::unique_ptr<SkyModel> OpenWeatherMapSource::fetch(std::time_t now) {
253276
model->sunset_ = sunset;
254277
for (JsonObject hour : hourly) {
255278
time_t dt = hour["dt"].as<time_t>();
256-
model->temperature_forecast.push_back({ dt, hour["temp"].as<double>() });
257-
model->dew_point_forecast.push_back({ dt, hour["dew_point"].as<double>() });
258-
model->wind_speed_forecast.push_back({ dt, hour["wind_speed"].as<double>() });
259-
model->wind_dir_forecast.push_back({ dt, hour["wind_deg"].as<double>() });
260-
model->wind_gust_forecast.push_back({ dt, hour["wind_gust"].as<double>() });
261-
model->cloud_cover_forecast.push_back({ dt, hour["clouds"].as<double>() });
279+
model->temperature_forecast.push_back({ dt, (float)hour["temp"].as<double>() });
280+
model->dew_point_forecast.push_back({ dt, (float)hour["dew_point"].as<double>() });
281+
model->wind_speed_forecast.push_back({ dt, (float)hour["wind_speed"].as<double>() });
282+
model->wind_dir_forecast.push_back({ dt, (float)hour["wind_deg"].as<double>() });
283+
model->wind_gust_forecast.push_back({ dt, (float)hour["wind_gust"].as<double>() });
284+
model->cloud_cover_forecast.push_back({ dt, (float)hour["clouds"].as<double>() });
262285
JsonArray wArr = hour["weather"].as<JsonArray>();
263286
bool hasRain = false, hasSnow = false;
264287
if (hour.containsKey("rain")) {
@@ -278,10 +301,13 @@ std::unique_ptr<SkyModel> OpenWeatherMapSource::fetch(std::time_t now) {
278301
hasSnow = true;
279302
}
280303
int ptype = hasRain && hasSnow ? 3 : (hasSnow ? 2 : (hasRain ? 1 : 0));
281-
model->precip_type_forecast.push_back({ dt, double(ptype) });
282-
model->precip_prob_forecast.push_back({ dt, hour["pop"].as<double>() });
304+
model->precip_type_forecast.push_back({ dt, (float)ptype });
305+
model->precip_prob_forecast.push_back({ dt, (float)hour["pop"].as<double>() });
283306
}
284307

308+
// Stagger history fetch to avoid back-to-back GETs in same loop iteration
309+
// and reduce risk of watchdog resets. Enforce at least 15s before history.
310+
lastHistFetch_ = skystrip::util::time_now_utc();
285311
return model;
286312
}
287313

@@ -298,7 +324,9 @@ std::unique_ptr<SkyModel> OpenWeatherMapSource::checkhistory(time_t now, std::ti
298324
snprintf(url, sizeof(url),
299325
"%s/data/3.0/onecall/timemachine?lat=%.6f&lon=%.6f&dt=%ld&units=imperial&appid=%s",
300326
apiBase_.c_str(), latitude_, longitude_, (long)fetchDt, apiKey_.c_str());
301-
DEBUG_PRINTF("SkyStrip: %s::checkhistory URL: %s\n", name().c_str(), url);
327+
char redacted[256];
328+
redactApiKeyInUrl(url, redacted, sizeof(redacted));
329+
DEBUG_PRINTF("SkyStrip: %s::checkhistory URL: %s\n", name().c_str(), redacted);
302330

303331
auto doc = getJson(url);
304332
if (!doc) {
@@ -321,12 +349,12 @@ std::unique_ptr<SkyModel> OpenWeatherMapSource::checkhistory(time_t now, std::ti
321349
for (JsonObject hour : hourly) {
322350
time_t dt = hour["dt"].as<time_t>();
323351
if (dt >= oldestTstamp) continue;
324-
model->temperature_forecast.push_back({ dt, hour["temp"].as<double>() });
325-
model->dew_point_forecast.push_back({ dt, hour["dew_point"].as<double>() });
326-
model->wind_speed_forecast.push_back({ dt, hour["wind_speed"].as<double>() });
327-
model->wind_dir_forecast.push_back({ dt, hour["wind_deg"].as<double>() });
328-
model->wind_gust_forecast.push_back({ dt, hour["wind_gust"].as<double>() });
329-
model->cloud_cover_forecast.push_back({ dt, hour["clouds"].as<double>() });
352+
model->temperature_forecast.push_back({ dt, (float)hour["temp"].as<double>() });
353+
model->dew_point_forecast.push_back({ dt, (float)hour["dew_point"].as<double>() });
354+
model->wind_speed_forecast.push_back({ dt, (float)hour["wind_speed"].as<double>() });
355+
model->wind_dir_forecast.push_back({ dt, (float)hour["wind_deg"].as<double>() });
356+
model->wind_gust_forecast.push_back({ dt, (float)hour["wind_gust"].as<double>() });
357+
model->cloud_cover_forecast.push_back({ dt, (float)hour["clouds"].as<double>() });
330358
JsonArray wArr = hour["weather"].as<JsonArray>();
331359
bool hasRain = false, hasSnow = false;
332360
if (hour.containsKey("rain")) {
@@ -346,8 +374,8 @@ std::unique_ptr<SkyModel> OpenWeatherMapSource::checkhistory(time_t now, std::ti
346374
hasSnow = true;
347375
}
348376
int ptype = hasRain && hasSnow ? 3 : (hasSnow ? 2 : (hasRain ? 1 : 0));
349-
model->precip_type_forecast.push_back({ dt, double(ptype) });
350-
model->precip_prob_forecast.push_back({ dt, hour["pop"].as<double>() });
377+
model->precip_type_forecast.push_back({ dt, (float)ptype });
378+
model->precip_prob_forecast.push_back({ dt, (float)hour["pop"].as<double>() });
351379
}
352380

353381
if (model->temperature_forecast.empty()) return nullptr;
@@ -384,7 +412,9 @@ bool OpenWeatherMapSource::geocodeOWM(std::string const & rawQuery,
384412
snprintf(url, sizeof(url),
385413
"%s/geo/1.0/direct?q=%s&limit=5&appid=%s",
386414
apiBase_.c_str(), enc, apiKey_.c_str());
387-
DEBUG_PRINTF("SkyStrip: %s::geocodeOWM URL: %s\n", name().c_str(), url);
415+
char redacted[512];
416+
redactApiKeyInUrl(url, redacted, sizeof(redacted));
417+
DEBUG_PRINTF("SkyStrip: %s::geocodeOWM URL: %s\n", name().c_str(), redacted);
388418

389419
auto doc = getJson(url);
390420
resetRateLimit(); // we want to do a fetch immediately after ...

usermods/usermod_v2_skystrip/readme.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ Add `usermod_v2_skystrip` to `custom_usermods` in your PlatformIO environment.
1010
## Configuration
1111

1212
Acquire an API key from
13-
[OpenWeatherMap](https://openweathermap.org/api/one-call-3). The skystrip
14-
module makes one API call per hour plus 24 calls when initially started
15-
up; it should remain comfortably under the free-tier limit of 1000 per
16-
day.
13+
[OpenWeatherMap](https://openweathermap.org/api/one-call-3). The SkyStrip
14+
module makes one API call per hour, plus up to 24 calls on first startup.
15+
This typically stays within free-tier limits, but check your current plan.
1716

1817
Enter the latitude and longitude for the desired forecast. There are
1918
several ways to do this:

usermods/usermod_v2_skystrip/rest_json_client.cpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@ RestJsonClient::RestJsonClient()
88
}
99

1010
void RestJsonClient::resetRateLimit() {
11-
// pretend we just made the last fetch RATE_LIMIT_MS ago
12-
lastFetchMs_ = millis() - static_cast<unsigned long>(-static_cast<long>(RATE_LIMIT_MS));
11+
// pretend we fetched RATE_LIMIT_MS ago (allow immediate next call)
12+
lastFetchMs_ = millis() - RATE_LIMIT_MS;
1313
}
1414

1515
DynamicJsonDocument* RestJsonClient::getJson(const char* url) {
1616
// enforce a basic rate limit to prevent runaway software from making bursts
1717
// of API calls (looks like DoS and get's our API key turned off ...)
1818
unsigned long now_ms = millis();
19-
if (now_ms - lastFetchMs_ < RATE_LIMIT_MS) {
20-
DEBUG_PRINTLN("SkyStrip: RestJsonClient::getJson: RATE LIMITED");
19+
// compute elapsed using unsigned arithmetic to avoid signed underflow
20+
unsigned long elapsed = now_ms - lastFetchMs_;
21+
if (elapsed < RATE_LIMIT_MS) {
22+
unsigned long remaining = RATE_LIMIT_MS - elapsed;
23+
DEBUG_PRINTF("SkyStrip: RestJsonClient::getJson: RATE LIMITED (%lu ms remaining)\n", remaining);
2124
return nullptr;
2225
}
2326
lastFetchMs_ = now_ms;
@@ -42,9 +45,11 @@ DynamicJsonDocument* RestJsonClient::getJson(const char* url) {
4245
}
4346
DEBUG_PRINTF("SkyStrip: RestJsonClient::getJson: free heap before GET: %u\n", ESP.getFreeHeap());
4447
int code = http_.GET();
45-
if (code <= 0) {
48+
// Treat network errors (<=0) and non-2xx statuses as failures.
49+
// Optionally consider 204 (No Content) as failure since there is no body to parse.
50+
if (code <= 0 || code < 200 || code >= 300 || code == 204) {
4651
http_.end();
47-
DEBUG_PRINTF("SkyStrip: RestJsonClient::getJson: http get error code: %d\n", code);
52+
DEBUG_PRINTF("SkyStrip: RestJsonClient::getJson: HTTP error/status: %d\n", code);
4853
return nullptr;
4954
}
5055

usermods/usermod_v2_skystrip/rest_json_client.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ class RestJsonClient {
2525

2626
protected:
2727
static constexpr unsigned RATE_LIMIT_MS = 10u * 1000u; // 10 seconds
28-
static constexpr size_t MAX_JSON_SIZE = 32 * 1024; // 32kB fixed buffer
29-
28+
#if defined(ARDUINO_ARCH_ESP8266)
29+
static constexpr size_t MAX_JSON_SIZE = 16 * 1024; // 16kB on 8266
30+
#else
31+
static constexpr size_t MAX_JSON_SIZE = 32 * 1024; // 32kB on ESP32
32+
#endif
33+
3034
private:
3135
HTTPClient http_;
3236
unsigned long lastFetchMs_;

usermods/usermod_v2_skystrip/skymodel.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,25 @@ static inline void emitSeriesMDHM(Print &out, time_t now, const char *label,
122122

123123
size_t i = 0;
124124
size_t off = 0;
125+
const size_t cap = sizeof(line);
125126
for (const auto& dp : s) {
126127
if (i % 6 == 0) {
127-
off = snprintf(line, sizeof(line), "SkyModel:");
128+
int n = snprintf(line, cap, "SkyModel:");
129+
off = (n < 0) ? 0u : ((size_t)n >= cap ? cap - 1 : (size_t)n);
128130
}
129131
skystrip::util::fmt_local(tb, sizeof(tb), dp.tstamp);
130-
off += snprintf(line + off, sizeof(line) - off,
131-
" (%s, %6.2f)", tb, dp.value);
132+
if (off < cap) {
133+
size_t rem = cap - off;
134+
int n = snprintf(line + off, rem, " (%s, %6.2f)", tb, dp.value);
135+
if (n > 0) off += ((size_t)n >= rem ? rem - 1 : (size_t)n);
136+
}
132137
if (i % 6 == 5 || i == s.size() - 1) {
133-
if (i == s.size() - 1) off += snprintf(line + off, sizeof(line) - off, " ]");
138+
if (i == s.size() - 1 && off < cap) {
139+
size_t rem = cap - off;
140+
int n = snprintf(line + off, rem, " ]");
141+
if (n > 0) off += ((size_t)n >= rem ? rem - 1 : (size_t)n);
142+
}
143+
if (off >= cap) off = cap - 1; // ensure space for newline
134144
line[off++] = '\n';
135145
out.write((const uint8_t*)line, off);
136146
}

usermods/usermod_v2_skystrip/skymodel.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Print;
1010

1111
struct DataPoint {
1212
time_t tstamp;
13-
double value;
13+
float value;
1414
};
1515

1616
class SkyModel {

usermods/usermod_v2_skystrip/temperature_view.cpp

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ static constexpr int16_t DEFAULT_SEG_ID = -1; // -1 means disabled
1010
// - these are user visible in the webapp settings UI
1111
// - they are scoped to this module, don't need to be globally unique
1212
//
13-
const char CFG_SEG_ID[] = "SegmentId";
13+
const char CFG_SEG_ID[] PROGMEM = "SegmentId";
1414

1515
// Map dew-point depression (°F) -> saturation multiplier.
1616
// dd<=2°F -> minSat ; dd>=25°F -> 1.0 ; smooth in between.
@@ -77,12 +77,12 @@ void TemperatureView::view(time_t now, SkyModel const &model,
7777
if (segId_ < 0 || segId_ >= strip.getMaxSegments())
7878
return;
7979
Segment &seg = strip.getSegment((uint8_t)segId_);
80-
seg.freeze = true;
8180
int start = seg.start;
8281
int end = seg.stop - 1; // inclusive
8382
int len = end - start + 1;
84-
if (len == 0)
83+
if (len <= 0)
8584
return;
85+
skystrip::util::FreezeGuard freezeGuard(seg);
8686

8787
constexpr double kHorizonSec = 48.0 * 3600.0;
8888
const double step = (len > 1) ? (kHorizonSec / double(len - 1)) : 0.0;
@@ -97,9 +97,7 @@ void TemperatureView::view(time_t now, SkyModel const &model,
9797
return 0.f;
9898

9999
time_t local = t + tzOffset; // convert to local seconds
100-
time_t s = local % DAY; // seconds since local midnight
101-
if (s < 0)
102-
s += DAY;
100+
time_t s = (((local % DAY) + DAY) % DAY); // seconds since local midnight (normalized)
103101

104102
// Seconds-of-day for markers + per-marker width multipliers.
105103
static const time_t kMarkers[] = {0 * 3600, 3 * 3600, 6 * 3600,

0 commit comments

Comments
 (0)