From b4deb47c4a2afc8920ade5b579eb2035e23659ae Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Thu, 28 Aug 2025 11:23:37 -0700 Subject: [PATCH 01/14] Add BARTdepart transit departure usermod --- .../bart_station_model.cpp | 170 ++++++++++++++++++ .../bart_station_model.h | 47 +++++ usermods/usermod_v2_bartdepart/interfaces.h | 59 ++++++ .../legacy_bart_source.cpp | 88 +++++++++ .../legacy_bart_source.h | 39 ++++ usermods/usermod_v2_bartdepart/library.json | 4 + .../usermod_v2_bartdepart/platform_view.cpp | 130 ++++++++++++++ .../usermod_v2_bartdepart/platform_view.h | 28 +++ usermods/usermod_v2_bartdepart/readme.md | 72 ++++++++ .../usermod_v2_bartdepart/train_color.cpp | 13 ++ usermods/usermod_v2_bartdepart/train_color.h | 25 +++ .../usermod_v2_bartdepart.cpp | 155 ++++++++++++++++ .../usermod_v2_bartdepart.h | 53 ++++++ usermods/usermod_v2_bartdepart/util.cpp | 35 ++++ usermods/usermod_v2_bartdepart/util.h | 89 +++++++++ 15 files changed, 1007 insertions(+) create mode 100644 usermods/usermod_v2_bartdepart/bart_station_model.cpp create mode 100644 usermods/usermod_v2_bartdepart/bart_station_model.h create mode 100644 usermods/usermod_v2_bartdepart/interfaces.h create mode 100644 usermods/usermod_v2_bartdepart/legacy_bart_source.cpp create mode 100644 usermods/usermod_v2_bartdepart/legacy_bart_source.h create mode 100644 usermods/usermod_v2_bartdepart/library.json create mode 100644 usermods/usermod_v2_bartdepart/platform_view.cpp create mode 100644 usermods/usermod_v2_bartdepart/platform_view.h create mode 100644 usermods/usermod_v2_bartdepart/readme.md create mode 100644 usermods/usermod_v2_bartdepart/train_color.cpp create mode 100644 usermods/usermod_v2_bartdepart/train_color.h create mode 100644 usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp create mode 100644 usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.h create mode 100644 usermods/usermod_v2_bartdepart/util.cpp create mode 100644 usermods/usermod_v2_bartdepart/util.h diff --git a/usermods/usermod_v2_bartdepart/bart_station_model.cpp b/usermods/usermod_v2_bartdepart/bart_station_model.cpp new file mode 100644 index 0000000000..138a36e38e --- /dev/null +++ b/usermods/usermod_v2_bartdepart/bart_station_model.cpp @@ -0,0 +1,170 @@ +#include "bart_station_model.h" +#include "util.h" + +#include +#include +#include + +BartStationModel::Platform::Platform(const String& platformId) + : platformId_(platformId) {} + +void BartStationModel::Platform::update(const JsonObject& root) { + if (platformId_.isEmpty()) return; + + ETDBatch batch; + const char* dateStr = root["date"] | ""; + const char* timeStr = root["time"] | ""; + batch.apiTs = parseHeaderTimestamp(dateStr, timeStr); + batch.ourTs = bartdepart::util::time_now(); + + if (root["station"].is()) { + for (JsonObject station : root["station"].as()) { + if (!station["etd"].is()) continue; + for (JsonObject etd : station["etd"].as()) { + if (!etd["estimate"].is()) continue; + bool matches = false; + for (JsonObject est : etd["estimate"].as()) { + if (String(est["platform"] | "0") != platformId_) continue; + matches = true; + int mins = atoi(est["minutes"] | "0"); + time_t dep = batch.apiTs + mins * 60; + TrainColor col = parseTrainColor(est["color"] | ""); + batch.etds.push_back(ETD{dep, col}); + } + if (matches) { + String dest = etd["destination"] | ""; + if (!dest.isEmpty()) { + auto it = std::find(destinations_.begin(), destinations_.end(), dest); + if (it == destinations_.end()) destinations_.push_back(dest); + } + } + } + } + } + + std::sort(batch.etds.begin(), batch.etds.end(), + [](const ETD& a, const ETD& b){ return a.estDep < b.estDep; }); + + history_.push_back(std::move(batch)); + while (history_.size() > 5) { + history_.pop_front(); + } + + DEBUG_PRINTF("BartDepart::update platform %s: %s\n", + platformId_.c_str(), toString().c_str()); +} + +void BartStationModel::Platform::merge(const Platform& other) { + for (auto const& b : other.history_) { + history_.push_back(b); + if (history_.size() > 5) history_.pop_front(); + } + + for (auto const& d : other.destinations_) { + auto it = std::find(destinations_.begin(), destinations_.end(), d); + if (it == destinations_.end()) destinations_.push_back(d); + } +} + +time_t BartStationModel::Platform::oldest() const { + if (history_.empty()) return 0; + return history_.front().ourTs; +} + +const String& BartStationModel::Platform::platformId() const { + return platformId_; +} + +const std::deque& +BartStationModel::Platform::history() const { + return history_; +} + +const std::vector& BartStationModel::Platform::destinations() const { + return destinations_; +} + +String BartStationModel::Platform::toString() const { + if (history_.empty()) return String(); + + const ETDBatch& batch = history_.back(); + const auto& etds = batch.etds; + if (etds.empty()) return String(); + + char nowBuf[20]; + bartdepart::util::fmt_local(nowBuf, sizeof(nowBuf), batch.ourTs, "%H:%M:%S"); + int lagSecs = batch.ourTs - batch.apiTs; + + String out; + out += nowBuf; + out += ": lag "; + out += lagSecs; + out += ":"; + + time_t prevTs = batch.ourTs; + + for (const auto& e : etds) { + out += " +"; + int diff = e.estDep - prevTs; + out += diff / 60; + out += " ("; + prevTs = e.estDep; + + char depBuf[20]; + bartdepart::util::fmt_local(depBuf, sizeof(depBuf), e.estDep, "%H:%M:%S"); + out += depBuf; + out += ":"; + out += ::toString(e.color); + out += ")"; + } + return out; +} + +time_t BartStationModel::Platform::parseHeaderTimestamp(const char* dateStr, + const char* timeStr) const { + int month=0, day=0, year=0; + int hour=0, min=0, sec=0; + char ampm[3] = {0}; + sscanf(dateStr, "%d/%d/%d", &month, &day, &year); + sscanf(timeStr, "%d:%d:%d %2s", &hour, &min, &sec, ampm); + if (strcasecmp(ampm, "PM") == 0 && hour < 12) hour += 12; + if (strcasecmp(ampm, "AM") == 0 && hour == 12) hour = 0; + struct tm tm{}; + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = min; + tm.tm_sec = sec; + return mktime(&tm) - bartdepart::util::current_offset(); // return UTC +} + +void BartStationModel::update(std::time_t now, BartStationModel&& delta) { + for (auto &p : delta.platforms) { + auto it = std::find_if(platforms.begin(), platforms.end(), + [&](const Platform& x){ return x.platformId() == p.platformId(); }); + if (it != platforms.end()) { + it->merge(p); + } else { + platforms.push_back(std::move(p)); + } + } +} + +time_t BartStationModel::oldest() const { + time_t oldest = 0; + for (auto const& p : platforms) { + time_t o = p.oldest(); + if (!oldest || (o && o < oldest)) oldest = o; + } + return oldest; +} + +std::vector BartStationModel::destinationsForPlatform(const String& platformId) const { + for (auto const& p : platforms) { + if (p.platformId() == platformId) { + return p.destinations(); + } + } + return {}; +} diff --git a/usermods/usermod_v2_bartdepart/bart_station_model.h b/usermods/usermod_v2_bartdepart/bart_station_model.h new file mode 100644 index 0000000000..2bf604d9a4 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/bart_station_model.h @@ -0,0 +1,47 @@ +#pragma once + +#include "wled.h" + +#include +#include +#include + +#include "train_color.h" + +struct BartStationModel { + struct Platform { + struct ETD { + time_t estDep; + TrainColor color; + }; + struct ETDBatch { + time_t apiTs; + time_t ourTs; + std::vector etds; + }; + + explicit Platform(const String& platformId); + + void update(const JsonObject& root); + void merge(const Platform& other); + time_t oldest() const; + const String& platformId() const; + const std::deque& history() const; + const std::vector& destinations() const; + String toString() const; + + private: + String platformId_; + std::deque history_; + std::vector destinations_; + + // return UTC tstamp + time_t parseHeaderTimestamp(const char* dateStr, const char* timeStr) const; + }; + + std::vector platforms; + + void update(std::time_t now, BartStationModel&& delta); + time_t oldest() const; + std::vector destinationsForPlatform(const String& platformId) const; +}; diff --git a/usermods/usermod_v2_bartdepart/interfaces.h b/usermods/usermod_v2_bartdepart/interfaces.h new file mode 100644 index 0000000000..422afa9105 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/interfaces.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include "wled.h" + +/// Config interface +struct IConfigurable { + virtual ~IConfigurable() = default; + virtual void addToConfig(JsonObject& root) = 0; + virtual void appendConfigData(Print& s) {} + virtual bool readFromConfig(JsonObject& root, + bool startup_complete, + bool& invalidate_history) = 0; + virtual const char* configKey() const = 0; +}; + +/// Templated data source interface +/// @tparam ModelType The concrete data model type +template +class IDataSourceT : public IConfigurable { +public: + virtual ~IDataSourceT() = default; + + /// Fetch new data, nullptr if no new data + virtual std::unique_ptr fetch(std::time_t now) = 0; + + /// Backfill older history if needed, nullptr if no new data + virtual std::unique_ptr checkhistory(std::time_t now, std::time_t oldestTstamp) = 0; + + /// Force the internal schedule to fetch ASAP (e.g. after ON or re-enable) + virtual void reload(std::time_t now) = 0; + + /// Identify the source (optional) + virtual std::string name() const = 0; +}; + +/// Templated data view interface +/// @tparam ModelType The concrete data model type +template +class IDataViewT : public IConfigurable { +public: + virtual ~IDataViewT() = default; + + /// Render the model to output (LEDs, serial, etc.) + virtual void view(std::time_t now, const ModelType& model, int16_t dbgPixelIndex) = 0; + + /// Identify the view (optional) + virtual std::string name() const = 0; + + /// Append DebugPixel info + virtual void appendDebugPixel(Print& s) const = 0; + + /// Append config page info, optionally using the latest model data + virtual void appendConfigData(Print& s, const ModelType* model) { + IConfigurable::appendConfigData(s); + } +}; diff --git a/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp b/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp new file mode 100644 index 0000000000..59ac7acb3d --- /dev/null +++ b/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp @@ -0,0 +1,88 @@ +#include "legacy_bart_source.h" +#include "util.h" + +LegacyBartSource::LegacyBartSource() { + client_.setInsecure(); +} + +void LegacyBartSource::reload(std::time_t now) { + nextFetch_ = now; + backoffMult_ = 1; +} + +static String composeUrl(const String& base, const String& key, const String& station) { + String url = base; + url += "&key="; url += key; + url += "&orig="; url += station; + return url; +} + +std::unique_ptr LegacyBartSource::fetch(std::time_t now) { + if (now == 0 || now < nextFetch_) return nullptr; + + String url = composeUrl(apiBase_, apiKey_, apiStation_); + if (!https_.begin(client_, url)) { + https_.end(); + DEBUG_PRINTLN(F("BartDepart: LegacyBartSource::fetch: trouble initiating request")); + nextFetch_ = now + updateSecs_ * backoffMult_; + if (backoffMult_ < 16) backoffMult_ *= 2; + return nullptr; + } + DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: free heap before GET: %u\n", + ESP.getFreeHeap()); + int httpCode = https_.GET(); + if (httpCode <= 0) { + https_.end(); + DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: https get error code: %d\n", httpCode); + nextFetch_ = now + updateSecs_ * backoffMult_; + if (backoffMult_ < 16) backoffMult_ *= 2; + return nullptr; + } + String payload = https_.getString(); + https_.end(); + + size_t jsonSz = payload.length() * 2; + DynamicJsonDocument doc(jsonSz); + auto err = deserializeJson(doc, payload); + if (err) { + nextFetch_ = now + updateSecs_ * backoffMult_; + if (backoffMult_ < 16) backoffMult_ *= 2; + return nullptr; + } + + JsonObject root = doc["root"].as(); + if (root.isNull()) { + nextFetch_ = now + updateSecs_ * backoffMult_; + if (backoffMult_ < 16) backoffMult_ *= 2; + return nullptr; + } + + std::unique_ptr model(new BartStationModel()); + for (const String& pid : platformIds()) { + if (pid.isEmpty()) continue; + BartStationModel::Platform tp(pid); + tp.update(root); + model->platforms.push_back(std::move(tp)); + } + + nextFetch_ = now + updateSecs_; + backoffMult_ = 1; + return model; +} + +void LegacyBartSource::addToConfig(JsonObject& root) { + root["UpdateSecs"] = updateSecs_; + root["ApiBase"] = apiBase_; + root["ApiKey"] = apiKey_; + root["ApiStation"] = apiStation_; +} + +bool LegacyBartSource::readFromConfig(JsonObject& root, bool startup_complete, bool& invalidate_history) { + bool ok = true; + ok &= getJsonValue(root["UpdateSecs"], updateSecs_, 60); + ok &= getJsonValue(root["ApiBase"], apiBase_, apiBase_); + ok &= getJsonValue(root["ApiKey"], apiKey_, apiKey_); + ok &= getJsonValue(root["ApiStation"], apiStation_, apiStation_); + invalidate_history = true; + return ok; +} diff --git a/usermods/usermod_v2_bartdepart/legacy_bart_source.h b/usermods/usermod_v2_bartdepart/legacy_bart_source.h new file mode 100644 index 0000000000..24f2a96783 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/legacy_bart_source.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include "interfaces.h" +#include "bart_station_model.h" +#include "wled.h" +#include "WiFiClientSecure.h" +#if defined(ARDUINO_ARCH_ESP8266) + #include +#else + #include +#endif + +class LegacyBartSource : public IDataSourceT { +private: + uint16_t updateSecs_ = 60; + String apiBase_ = "https://api.bart.gov/api/etd.aspx?cmd=etd&json=y"; + String apiKey_ = "MW9S-E7SL-26DU-VV8V"; + String apiStation_ = "19th"; + time_t nextFetch_ = 0; + uint8_t backoffMult_ = 1; + WiFiClientSecure client_; + HTTPClient https_; + +public: + LegacyBartSource(); + std::unique_ptr fetch(std::time_t now) override; + std::unique_ptr checkhistory(std::time_t now, std::time_t oldestTstamp) override { return nullptr; } + void reload(std::time_t now) override; + std::string name() const override { return "LegacyBartSource"; } + + void addToConfig(JsonObject& root) override; + bool readFromConfig(JsonObject& root, bool startup_complete, bool& invalidate_history) override; + const char* configKey() const override { return "LegacyBartSource"; } + + uint16_t updateSecs() const { return updateSecs_; } + std::vector platformIds() const { return { "1", "2", "3", "4" }; } +}; diff --git a/usermods/usermod_v2_bartdepart/library.json b/usermods/usermod_v2_bartdepart/library.json new file mode 100644 index 0000000000..f63ffd468c --- /dev/null +++ b/usermods/usermod_v2_bartdepart/library.json @@ -0,0 +1,4 @@ +{ + "name": "usermod_v2_bartdepart", + "build": { "libArchive": false } +} diff --git a/usermods/usermod_v2_bartdepart/platform_view.cpp b/usermods/usermod_v2_bartdepart/platform_view.cpp new file mode 100644 index 0000000000..d640c471d7 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/platform_view.cpp @@ -0,0 +1,130 @@ +#include "platform_view.h" +#include "util.h" +#include "wled.h" + +// helper to map TrainColor enum → CRGB +static CRGB colorFromTrainColor(TrainColor tc) { + switch (tc) { + case TrainColor::Red: + return CRGB(255, 0, 0); + case TrainColor::Orange: + return CRGB(255, 150, 30); + case TrainColor::Yellow: + return CRGB(255, 255, 0); + case TrainColor::Green: + return CRGB(0, 255, 0); + case TrainColor::Blue: + return CRGB(0, 0, 255); + case TrainColor::White: + return CRGB(255, 255, 255); + default: + return CRGB(0, 0, 0); + } +} + +void PlatformView::view(std::time_t now, const BartStationModel &model, + int16_t dbgPixelIndex) { + if (segmentId_ == -1) + return; // disabled + + // find matching platform data first + const BartStationModel::Platform* match = nullptr; + for (const auto &platform : model.platforms) { + if (platform.platformId() == platformId_) { + match = &platform; + break; + } + } + if (!match) + return; + + const auto &history = match->history(); + if (match->platformId().isEmpty()) + return; + if (history.empty()) + return; + + static uint8_t frameCnt = 0; // 0..99 + bool preferFirst = frameCnt < 50; // true for first 0.5 s + frameCnt = (frameCnt + 1) % 100; + + const auto &batch = history.back(); + + if (segmentId_ < 0 || segmentId_ >= strip.getMaxSegments()) + return; + Segment &seg = strip.getSegment((uint8_t)segmentId_); + int len = seg.virtualLength(); + if (len <= 0) + return; + + // Ensure mapping is initialized (mirroring, grouping, reverse, etc.) + seg.beginDraw(); + // Do not alter freeze state during overlay draws + bartdepart::util::FreezeGuard freezeGuard(seg, false); + + auto brightness = [&](uint32_t c) { + return (uint16_t)(((c >> 16) & 0xFF) + ((c >> 8) & 0xFF) + (c & 0xFF)); + }; + + for (int i = 0; i < len; ++i) { + uint32_t bestColor = 0; + uint16_t bestB = 0; + + for (auto &e : batch.etds) { + float diff = float(updateSecs_ + e.estDep - now) * len / 3600.0f; + if (diff < 0.0f || diff >= len) + continue; + + int idx = int(floor(diff)); + float frac = diff - float(idx); + + uint8_t b = 0; + if (i == idx) { + b = uint8_t((1.0f - frac) * 255); + } else if (i == idx + 1) { + b = uint8_t(frac * 255); + } else { + continue; + } + + CRGB col = colorFromTrainColor(e.color); + uint32_t newColor = (((uint32_t)col.r * b / 255) << 16) | + (((uint32_t)col.g * b / 255) << 8) | + ((uint32_t)col.b * b / 255); + + uint16_t newB = brightness(newColor); + if ((preferFirst && (bestColor == 0 || newB > 2 * bestB)) || + (!preferFirst && newB * 2 >= bestB)) { + bestColor = newColor; + bestB = newB; + } + } + + seg.setPixelColor(i, bartdepart::util::blinkDebug(i, dbgPixelIndex, bestColor)); + } +} + +void PlatformView::appendConfigData(Print &s, const BartStationModel *model) { + // 4th arg (pre) = BEFORE input -> keep (-1 disables) right next to the field + // 3rd arg (post) = AFTER input -> show the destinations note, visually separated + s.print(F("addInfo('BartDepart:")); + s.print(configKey()); // e.g. "PlatformView1" + s.print(F(":SegmentId',1,'
")); + if (model) { + auto dests = model->destinationsForPlatform(platformId_); + if (!dests.empty()) { + s.print(F("Destinations: ")); + for (size_t i = 0; i < dests.size(); ++i) { + if (i) s.print(F(", ")); + s.print(dests[i]); + } + } else { + s.print(F("No destinations known")); + } + } else { + s.print(F("No destinations known")); + } + s.print(F("
',")); + s.print(F("' (-1 disables)'")); + s.println(F(");")); +} diff --git a/usermods/usermod_v2_bartdepart/platform_view.h b/usermods/usermod_v2_bartdepart/platform_view.h new file mode 100644 index 0000000000..37c5076f89 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/platform_view.h @@ -0,0 +1,28 @@ +#pragma once + +#include "interfaces.h" +#include "bart_station_model.h" +#include "util.h" + +class PlatformView : public IDataViewT { +private: + uint16_t updateSecs_ = 60; + String platformId_; + int16_t segmentId_ = -1; + std::string configKey_; +public: + PlatformView(const String& platformId) + : platformId_(platformId), segmentId_(-1), + configKey_(std::string("PlatformView") + platformId.c_str()) {} + + void view(std::time_t now, const BartStationModel& model, int16_t dbgPixelIndex) override; + void appendDebugPixel(Print& s) const override { /* empty */ } + void addToConfig(JsonObject& root) override { root["SegmentId"] = segmentId_; } + void appendConfigData(Print& s) override { appendConfigData(s, nullptr); } + void appendConfigData(Print& s, const BartStationModel* model) override; + bool readFromConfig(JsonObject& root, bool startup_complete, bool& invalidate_history) override { + return getJsonValue(root["SegmentId"], segmentId_, segmentId_); + } + const char* configKey() const override { return configKey_.c_str(); } + std::string name() const override { return configKey_; } +}; diff --git a/usermods/usermod_v2_bartdepart/readme.md b/usermods/usermod_v2_bartdepart/readme.md new file mode 100644 index 0000000000..ca1a07163b --- /dev/null +++ b/usermods/usermod_v2_bartdepart/readme.md @@ -0,0 +1,72 @@ +# BARTdepart + +Render upcoming Bay Area Rapid Transit (BART) departures on LED segments. +Each platform gets its own segment; colored markers sweep toward “now” as +departure times approach. + +## Features + +- Virtual‑segment aware rendering (reverse, mirroring, grouping honored). +- Configurable per‑platform segment mapping; disable any platform with `-1`. +- Graceful overlay behavior (no freezing the segment while drawing). +- Simple debug tooling via a blinking debug pixel index. + +## Installation + +Add `usermod_v2_bartdepart` to your PlatformIO environment in +`platformio_override.ini`: + +``` +custom_usermods = usermod_v2_bartdepart +``` + +Build and flash as usual: +- Build: `pio run -e ` +- Upload: `pio run -e -t upload` + +## Configuration + +Open the WLED web UI → Config → Usermods → “BartDepart”. The following keys are +available. + +- BartDepart.Enabled: enable/disable the module. + +Source: LegacyBartSource (BART ETD API) +- UpdateSecs: fetch interval in seconds (default 60). +- ApiBase: base URL for the ETD endpoint. +- ApiKey: API key. The default demo key works for testing; consider providing + your own key for reliability. +- ApiStation: station abbreviation (e.g., `19th`, `embr`, `mont`). + +Views: PlatformView1 … PlatformView4 +- SegmentId: WLED segment index to render that platform (`-1` disables). + +Notes +- Virtual Segments: Rendering uses `virtualLength()` and `beginDraw()`, so + segment reverse/mirror/group settings are respected automatically. If the + displayed direction seems wrong, toggle the segment’s Reverse or adjust + mapping settings in WLED. +- Multiple Segments: Map different platforms to different segments to visualize + each platform independently. You can also map multiple PlatformViews to the + same segment if you prefer, but overlapping markers will blend. + +## Usage + +1) Assign `SegmentId` for the platform views you want (set others to `-1`). +2) Set `ApiStation` to your station abbreviation; keep `UpdateSecs` at 60 to + start. +3) Save and reboot. After a short boot safety delay, the module fetches ETDs + and starts rendering. Dots represent upcoming trains; their color reflects + the train line. + +## Troubleshooting + +- Nothing shows: Ensure `BartDepart.Enabled = true`, a valid `SegmentId` is set + (and the segment has non‑zero length), and the station abbreviation is + correct. Confirm the controller has Wi‑Fi and time (NTP) sync. +- Direction looks reversed: Toggle the segment’s Reverse setting in WLED. +- Flicker or odd blending: Multiple departures can overlap on adjacent bins; the + renderer favors the brighter color on a short cadence. Increase pixel count or + split platforms across separate segments for more resolution. +- API errors/backoff: On fetch errors the module exponentially backs off up to + 16× `UpdateSecs`. Verify connectivity and API settings. diff --git a/usermods/usermod_v2_bartdepart/train_color.cpp b/usermods/usermod_v2_bartdepart/train_color.cpp new file mode 100644 index 0000000000..4f7b46e382 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/train_color.cpp @@ -0,0 +1,13 @@ +#include + +#include "train_color.h" + +TrainColor parseTrainColor(const char * name) { + if (strcmp(name, "RED")==0) return TrainColor::Red; + else if (strcmp(name, "ORANGE")==0) return TrainColor::Orange; + else if (strcmp(name, "YELLOW")==0) return TrainColor::Yellow; + else if (strcmp(name, "GREEN")==0) return TrainColor::Green; + else if (strcmp(name, "BLUE")==0) return TrainColor::Blue; + else if (strcmp(name, "WHITE")==0) return TrainColor::White; + return TrainColor::Unknown; +} diff --git a/usermods/usermod_v2_bartdepart/train_color.h b/usermods/usermod_v2_bartdepart/train_color.h new file mode 100644 index 0000000000..e5d6a35886 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/train_color.h @@ -0,0 +1,25 @@ +#pragma once + +enum class TrainColor { + Red, + Orange, + Yellow, + Green, + Blue, + White, + Unknown +}; + +inline const char * toString(TrainColor c) { + switch (c) { + case TrainColor::Red: return "RED"; + case TrainColor::Orange: return "ORANGE"; + case TrainColor::Yellow: return "YELLOW"; + case TrainColor::Green: return "GREEN"; + case TrainColor::Blue: return "BLUE"; + case TrainColor::White: return "WHITE"; + default: return "UNKNOWN"; + } +} + +TrainColor parseTrainColor(const char * name); diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp new file mode 100644 index 0000000000..22ac380f99 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp @@ -0,0 +1,155 @@ +#include +#include +#include + +#include "usermod_v2_bartdepart.h" +#include "interfaces.h" +#include "util.h" + +#include "bart_station_model.h" +#include "legacy_bart_source.h" +#include "platform_view.h" + +const char CFG_NAME[] PROGMEM = "BartDepart"; +const char CFG_ENABLED[] PROGMEM = "Enabled"; +const char CFG_DBG_PIXEL_INDEX[] PROGMEM = "DebugPixelIndex"; + +static BartDepart bartdepart_usermod; +REGISTER_USERMOD(bartdepart_usermod); + +// Delay after boot (milliseconds) to allow disabling before heavy work +const uint32_t SAFETY_DELAY_MS = 10u * 1000u; + +BartDepart::BartDepart() { + sources_.push_back(::make_unique()); + model_ = ::make_unique(); + views_.push_back(::make_unique("1")); + views_.push_back(::make_unique("2")); + views_.push_back(::make_unique("3")); + views_.push_back(::make_unique("4")); +} + +void BartDepart::setup() { + DEBUG_PRINTLN(F("BartDepart::setup starting")); + uint32_t now_ms = millis(); + safeToStart_ = now_ms + SAFETY_DELAY_MS; + showBooting(); + state_ = BartDepartState::Setup; + DEBUG_PRINTLN(F("BartDepart::setup finished")); +} + +void BartDepart::loop() { + uint32_t now_ms = millis(); + if (!edgeInit_) { + lastOff_ = offMode; + lastEnabled_ = enabled_; + edgeInit_ = true; + } + + time_t const now = bartdepart::util::time_now_utc(); + + if (state_ == BartDepartState::Setup) { + if (now_ms < safeToStart_) return; + state_ = BartDepartState::Running; + doneBooting(); + reloadSources(now); + } + + bool becameOn = (lastOff_ && !offMode); + bool becameEnabled = (!lastEnabled_ && enabled_); + if (becameOn || becameEnabled) { + reloadSources(now); + } + lastOff_ = offMode; + lastEnabled_ = enabled_; + + if (!enabled_ || offMode || strip.isUpdating()) return; + + for (auto& src : sources_) { + if (auto data = src->fetch(now)) { + model_->update(now, std::move(*data)); + } + if (auto hist = src->checkhistory(now, model_->oldest())) { + model_->update(now, std::move(*hist)); + } + } +} + +void BartDepart::handleOverlayDraw() { + time_t now = bartdepart::util::time_now_utc(); + for (auto& view : views_) { + view->view(now, *model_, dbgPixelIndex_); + } +} + +void BartDepart::addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(CFG_NAME)); + top[FPSTR(CFG_ENABLED)] = enabled_; + top[FPSTR(CFG_DBG_PIXEL_INDEX)] = dbgPixelIndex_; + for (auto& src : sources_) { + JsonObject sub = top.createNestedObject(src->configKey()); + src->addToConfig(sub); + } + for (auto& vw : views_) { + JsonObject sub = top.createNestedObject(vw->configKey()); + vw->addToConfig(sub); + } +} + +void BartDepart::appendConfigData(Print& s) { + for (auto& src : sources_) { + src->appendConfigData(s); + } + + for (auto& vw : views_) { + vw->appendConfigData(s, model_.get()); + } +} + +bool BartDepart::readFromConfig(JsonObject& root) { + JsonObject top = root[FPSTR(CFG_NAME)]; + if (top.isNull()) return false; + + bool ok = true; + bool invalidate_history = false; + bool startup_complete = state_ == BartDepartState::Running; + + ok &= getJsonValue(top[FPSTR(CFG_ENABLED)], enabled_, false); + ok &= getJsonValue(top[FPSTR(CFG_DBG_PIXEL_INDEX)], dbgPixelIndex_, -1); + + for (auto& src : sources_) { + JsonObject sub = top[src->configKey()]; + ok &= src->readFromConfig(sub, startup_complete, invalidate_history); + } + for (auto& vw : views_) { + JsonObject sub = top[vw->configKey()]; + ok &= vw->readFromConfig(sub, startup_complete, invalidate_history); + } + + if (invalidate_history) { + model_->platforms.clear(); + if (startup_complete) reloadSources(bartdepart::util::time_now_utc()); + } + + return ok; +} + +void BartDepart::showBooting() { + Segment& seg = strip.getMainSegment(); + seg.setMode(28); + seg.speed = 200; + seg.setPalette(128); + seg.setColor(0, 0x404060); + seg.setColor(1, 0x000000); + seg.setColor(2, 0x303040); +} + +void BartDepart::doneBooting() { + Segment& seg = strip.getMainSegment(); + seg.freeze = true; + seg.setMode(0); +} + +void BartDepart::reloadSources(std::time_t now) { + for (auto& src : sources_) src->reload(now); +} diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.h b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.h new file mode 100644 index 0000000000..82bb1c2fd4 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.h @@ -0,0 +1,53 @@ +#pragma once +#include + +#include "interfaces.h" +#include "wled.h" + +#define BARTDEPART2_VERSION "0.0.1" + +#define USERMOD_ID_BARTDEPART 560 + +class BartStationModel; +class LegacyBartSource; +class PlatformView; + +enum class BartDepartState { + Initial, + Setup, + Running +}; + +class BartDepart : public Usermod { +private: + bool enabled_ = false; + int16_t dbgPixelIndex_ = -1; + BartDepartState state_ = BartDepartState::Initial; + uint32_t safeToStart_ = 0; + bool edgeInit_ = false; + bool lastOff_ = false; + bool lastEnabled_ = false; + + std::vector>> sources_; + std::unique_ptr model_; + std::vector>> views_; + +public: + BartDepart(); + ~BartDepart() override = default; + void setup() override; + void loop() override; + void handleOverlayDraw() override; + void addToConfig(JsonObject &obj) override; + void appendConfigData(Print& s) override; + bool readFromConfig(JsonObject& obj) override; + uint16_t getId() override { return USERMOD_ID_BARTDEPART; } + + inline void enable(bool en) { enabled_ = en; } + inline bool isEnabled() { return enabled_; } + +protected: + void showBooting(); + void doneBooting(); + void reloadSources(std::time_t now); +}; diff --git a/usermods/usermod_v2_bartdepart/util.cpp b/usermods/usermod_v2_bartdepart/util.cpp new file mode 100644 index 0000000000..91a72348f4 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/util.cpp @@ -0,0 +1,35 @@ +#include "util.h" + +namespace bartdepart { +namespace util { + +uint32_t hsv2rgb(float h, float s, float v) { + // Normalize inputs to safe ranges + if (s < 0.f) s = 0.f; else if (s > 1.f) s = 1.f; + if (v < 0.f) v = 0.f; else if (v > 1.f) v = 1.f; + float hh = fmodf(h, 360.f); + if (hh < 0.f) hh += 360.f; + + float c = v * s; + hh = hh / 60.f; + float x = c * (1.f - fabsf(fmodf(hh, 2.f) - 1.f)); + float r1, g1, b1; + if (hh < 1.f) { r1 = c; g1 = x; b1 = 0.f; } + else if (hh < 2.f) { r1 = x; g1 = c; b1 = 0.f; } + else if (hh < 3.f) { r1 = 0.f; g1 = c; b1 = x; } + else if (hh < 4.f) { r1 = 0.f; g1 = x; b1 = c; } + else if (hh < 5.f) { r1 = x; g1 = 0.f; b1 = c; } + else { r1 = c; g1 = 0.f; b1 = x; } + float m = v - c; + // Clamp final channels into [0,1] to avoid rounding drift outside range + float rf = r1 + m; if (rf < 0.f) rf = 0.f; else if (rf > 1.f) rf = 1.f; + float gf = g1 + m; if (gf < 0.f) gf = 0.f; else if (gf > 1.f) gf = 1.f; + float bf = b1 + m; if (bf < 0.f) bf = 0.f; else if (bf > 1.f) bf = 1.f; + uint8_t r = uint8_t(lrintf(rf * 255.f)); + uint8_t g = uint8_t(lrintf(gf * 255.f)); + uint8_t b = uint8_t(lrintf(bf * 255.f)); + return RGBW32(r, g, b, 0); +} + +} // namespace util +} // namespace bartdepart diff --git a/usermods/usermod_v2_bartdepart/util.h b/usermods/usermod_v2_bartdepart/util.h new file mode 100644 index 0000000000..dcdc497413 --- /dev/null +++ b/usermods/usermod_v2_bartdepart/util.h @@ -0,0 +1,89 @@ +#pragma once + +#include "wled.h" +#include +#include +#include + +namespace bartdepart { +namespace util { + +// RAII guard to temporarily freeze a segment during rendering and always +// restore its original freeze state on exit (including early returns). +struct FreezeGuard { + Segment &seg; + bool prev; + explicit FreezeGuard(Segment &s, bool freezeNow = true) : seg(s), prev(s.freeze) { + seg.freeze = freezeNow; + } + ~FreezeGuard() { seg.freeze = prev; } + FreezeGuard(const FreezeGuard &) = delete; + FreezeGuard &operator=(const FreezeGuard &) = delete; +}; + +inline time_t time_now_utc() { return (time_t)toki.getTime().sec; } + +inline time_t time_now() { return time_now_utc(); } + +inline long current_offset() { + long off = (long)localTime - (long)toki.getTime().sec; + if (off < -54000 || off > 54000) + off = 0; + return off; +} + +inline void fmt_local(char *out, size_t n, time_t utc_ts, + const char *fmt = "%m-%d %H:%M") { + const time_t local_sec = utc_ts + current_offset(); + struct tm tmLocal; + gmtime_r(&local_sec, &tmLocal); + strftime(out, n, fmt, &tmLocal); +} + +template inline T clamp01(T v) { + return v < T(0) ? T(0) : (v > T(1) ? T(1) : v); +} + +inline double lerp(double a, double b, double t) { return a + (b - a) * t; } + +uint32_t hsv2rgb(float h, float s, float v); + +inline uint32_t applySaturation(uint32_t col, float sat) { + if (sat < 0.f) + sat = 0.f; + else if (sat > 1.f) + sat = 1.f; + + const float r = float((col >> 16) & 0xFF); + const float g = float((col >> 8) & 0xFF); + const float b = float((col) & 0xFF); + + const float y = 0.2627f * r + 0.6780f * g + 0.0593f * b; + + auto mixc = [&](float c) { + float v = y + sat * (c - y); + if (v < 0.f) + v = 0.f; + if (v > 255.f) + v = 255.f; + return v; + }; + + const uint8_t r2 = uint8_t(lrintf(mixc(r))); + const uint8_t g2 = uint8_t(lrintf(mixc(g))); + const uint8_t b2 = uint8_t(lrintf(mixc(b))); + return RGBW32(r2, g2, b2, 0); +} + +// Blink a pixel between its color and a gray debug color to mark dbgPixelIndex. +inline uint32_t blinkDebug(int i, int16_t dbgPixelIndex, uint32_t col) { + if (dbgPixelIndex >= 0 && i == dbgPixelIndex) { + static const uint32_t dbgCol = hsv2rgb(0.f, 0.f, 0.4f); + if ((millis() / 1000) & 1) + return dbgCol; + } + return col; +} + +} // namespace util +} // namespace bartdepart From 104f01640eee56f6bb3027cb25acda3ce2d8d5c5 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Fri, 29 Aug 2025 12:57:44 -0700 Subject: [PATCH 02/14] bartdepart: Handle non-2xx HTTP responses --- usermods/usermod_v2_bartdepart/legacy_bart_source.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp b/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp index 59ac7acb3d..0b8d050ff7 100644 --- a/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp +++ b/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp @@ -31,9 +31,9 @@ std::unique_ptr LegacyBartSource::fetch(std::time_t now) { DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: free heap before GET: %u\n", ESP.getFreeHeap()); int httpCode = https_.GET(); - if (httpCode <= 0) { + if (httpCode < 200 || httpCode >= 300) { https_.end(); - DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: https get error code: %d\n", httpCode); + DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: HTTP status not OK: %d\n", httpCode); nextFetch_ = now + updateSecs_ * backoffMult_; if (backoffMult_ < 16) backoffMult_ *= 2; return nullptr; From 754db27ae75c1403af30763cf41167cf0a7e0769 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Fri, 29 Aug 2025 13:01:40 -0700 Subject: [PATCH 03/14] bartdepart: Improve documentation and comments --- usermods/usermod_v2_bartdepart/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/usermod_v2_bartdepart/readme.md b/usermods/usermod_v2_bartdepart/readme.md index ca1a07163b..e06756a7ee 100644 --- a/usermods/usermod_v2_bartdepart/readme.md +++ b/usermods/usermod_v2_bartdepart/readme.md @@ -16,7 +16,7 @@ departure times approach. Add `usermod_v2_bartdepart` to your PlatformIO environment in `platformio_override.ini`: -``` +```ini custom_usermods = usermod_v2_bartdepart ``` From 353d70894540aab66dc675bda353108e291ef082 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Fri, 29 Aug 2025 13:03:37 -0700 Subject: [PATCH 04/14] bartdepart: Guard against null and accept case-insensitive input --- usermods/usermod_v2_bartdepart/train_color.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/usermods/usermod_v2_bartdepart/train_color.cpp b/usermods/usermod_v2_bartdepart/train_color.cpp index 4f7b46e382..c570f11db7 100644 --- a/usermods/usermod_v2_bartdepart/train_color.cpp +++ b/usermods/usermod_v2_bartdepart/train_color.cpp @@ -1,13 +1,15 @@ #include +#include // for strcasecmp #include "train_color.h" TrainColor parseTrainColor(const char * name) { - if (strcmp(name, "RED")==0) return TrainColor::Red; - else if (strcmp(name, "ORANGE")==0) return TrainColor::Orange; - else if (strcmp(name, "YELLOW")==0) return TrainColor::Yellow; - else if (strcmp(name, "GREEN")==0) return TrainColor::Green; - else if (strcmp(name, "BLUE")==0) return TrainColor::Blue; - else if (strcmp(name, "WHITE")==0) return TrainColor::White; + if (!name) return TrainColor::Unknown; + if (strcasecmp(name, "RED")==0) return TrainColor::Red; + else if (strcasecmp(name, "ORANGE")==0) return TrainColor::Orange; + else if (strcasecmp(name, "YELLOW")==0) return TrainColor::Yellow; + else if (strcasecmp(name, "GREEN")==0) return TrainColor::Green; + else if (strcasecmp(name, "BLUE")==0) return TrainColor::Blue; + else if (strcasecmp(name, "WHITE")==0) return TrainColor::White; return TrainColor::Unknown; } From 2a111985e44eda2bb38f217655fc3e7b76b116d4 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Fri, 29 Aug 2025 13:05:12 -0700 Subject: [PATCH 05/14] bartdepart: Return true when config section is absent --- usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp index 22ac380f99..0e8d63abd0 100644 --- a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp @@ -108,7 +108,7 @@ void BartDepart::appendConfigData(Print& s) { bool BartDepart::readFromConfig(JsonObject& root) { JsonObject top = root[FPSTR(CFG_NAME)]; - if (top.isNull()) return false; + if (top.isNull()) return true; bool ok = true; bool invalidate_history = false; From e6bbcf3654269639e6ec50790481f548970b413b Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Fri, 29 Aug 2025 13:06:36 -0700 Subject: [PATCH 06/14] bartdepart: Add missing includes --- usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.h | 1 + 1 file changed, 1 insertion(+) diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.h b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.h index 82bb1c2fd4..05fd49590d 100644 --- a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.h +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "interfaces.h" #include "wled.h" From c323ab574c3c1e23f7904e7cff922f2ca91640b3 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Fri, 29 Aug 2025 14:13:31 -0700 Subject: [PATCH 07/14] bartdepart: Don't render when disabled or LEDs are off --- usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp index 0e8d63abd0..cb41beec83 100644 --- a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp @@ -76,6 +76,7 @@ void BartDepart::loop() { } void BartDepart::handleOverlayDraw() { + if (!enabled_ || offMode) return; time_t now = bartdepart::util::time_now_utc(); for (auto& view : views_) { view->view(now, *model_, dbgPixelIndex_); From e34cf5ee2e89e922f7fd71d38f6644f1f4339bd1 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Tue, 2 Sep 2025 14:34:23 -0700 Subject: [PATCH 08/14] =?UTF-8?q?bartdepart:=20Don=E2=80=99t=20invalidate?= =?UTF-8?q?=20history=20on=20every=20config=20read?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usermod_v2_bartdepart/legacy_bart_source.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp b/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp index 0b8d050ff7..ee4a89e858 100644 --- a/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp +++ b/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp @@ -79,10 +79,17 @@ void LegacyBartSource::addToConfig(JsonObject& root) { bool LegacyBartSource::readFromConfig(JsonObject& root, bool startup_complete, bool& invalidate_history) { bool ok = true; + uint16_t prevUpdate = updateSecs_; + String prevBase = apiBase_; + String prevKey = apiKey_; + String prevStation= apiStation_; + ok &= getJsonValue(root["UpdateSecs"], updateSecs_, 60); - ok &= getJsonValue(root["ApiBase"], apiBase_, apiBase_); - ok &= getJsonValue(root["ApiKey"], apiKey_, apiKey_); + ok &= getJsonValue(root["ApiBase"], apiBase_, apiBase_); + ok &= getJsonValue(root["ApiKey"], apiKey_, apiKey_); ok &= getJsonValue(root["ApiStation"], apiStation_, apiStation_); - invalidate_history = true; + + // Only invalidate when source identity changes (base/station) + invalidate_history = (apiBase_ != prevBase) || (apiStation_ != prevStation); return ok; } From 0236850ceada5e4a2235ef3aaaa74104f8d87870 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Tue, 2 Sep 2025 14:36:17 -0700 Subject: [PATCH 09/14] bartdepart: Avoid forcing permanent freeze after boot --- usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp index cb41beec83..056f2dd01e 100644 --- a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp @@ -147,7 +147,6 @@ void BartDepart::showBooting() { void BartDepart::doneBooting() { Segment& seg = strip.getMainSegment(); - seg.freeze = true; seg.setMode(0); } From df82eb314509578f5759aa41449ddebc08683b82 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Tue, 2 Sep 2025 14:45:26 -0700 Subject: [PATCH 10/14] bartdepart: Remove partial debugPixelIndex implementation till we need --- usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp index 056f2dd01e..26a0ea9e46 100644 --- a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp @@ -12,7 +12,6 @@ const char CFG_NAME[] PROGMEM = "BartDepart"; const char CFG_ENABLED[] PROGMEM = "Enabled"; -const char CFG_DBG_PIXEL_INDEX[] PROGMEM = "DebugPixelIndex"; static BartDepart bartdepart_usermod; REGISTER_USERMOD(bartdepart_usermod); @@ -86,7 +85,6 @@ void BartDepart::handleOverlayDraw() { void BartDepart::addToConfig(JsonObject& root) { JsonObject top = root.createNestedObject(FPSTR(CFG_NAME)); top[FPSTR(CFG_ENABLED)] = enabled_; - top[FPSTR(CFG_DBG_PIXEL_INDEX)] = dbgPixelIndex_; for (auto& src : sources_) { JsonObject sub = top.createNestedObject(src->configKey()); src->addToConfig(sub); @@ -116,7 +114,6 @@ bool BartDepart::readFromConfig(JsonObject& root) { bool startup_complete = state_ == BartDepartState::Running; ok &= getJsonValue(top[FPSTR(CFG_ENABLED)], enabled_, false); - ok &= getJsonValue(top[FPSTR(CFG_DBG_PIXEL_INDEX)], dbgPixelIndex_, -1); for (auto& src : sources_) { JsonObject sub = top[src->configKey()]; From 350caef0b1efeb9c19ea126a75f015802b2a8981 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Tue, 2 Sep 2025 14:49:11 -0700 Subject: [PATCH 11/14] bartdepart: Gate boot animation behind enabled_ to avoid surprising users when disabled --- usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp index 26a0ea9e46..15ed0d03c0 100644 --- a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp @@ -32,7 +32,7 @@ void BartDepart::setup() { DEBUG_PRINTLN(F("BartDepart::setup starting")); uint32_t now_ms = millis(); safeToStart_ = now_ms + SAFETY_DELAY_MS; - showBooting(); + if (enabled_) showBooting(); state_ = BartDepartState::Setup; DEBUG_PRINTLN(F("BartDepart::setup finished")); } @@ -50,7 +50,7 @@ void BartDepart::loop() { if (state_ == BartDepartState::Setup) { if (now_ms < safeToStart_) return; state_ = BartDepartState::Running; - doneBooting(); + if (enabled_) doneBooting(); reloadSources(now); } From 964c9e9f1d229ba368ff30f317d410096429cb35 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Tue, 2 Sep 2025 14:51:54 -0700 Subject: [PATCH 12/14] bartdepart: Avoid reloading sources until system time is valid --- usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp index 15ed0d03c0..5d4a89d367 100644 --- a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp @@ -51,13 +51,13 @@ void BartDepart::loop() { if (now_ms < safeToStart_) return; state_ = BartDepartState::Running; if (enabled_) doneBooting(); - reloadSources(now); + if (now > 0) reloadSources(now); } bool becameOn = (lastOff_ && !offMode); bool becameEnabled = (!lastEnabled_ && enabled_); if (becameOn || becameEnabled) { - reloadSources(now); + if (now > 0) reloadSources(now); } lastOff_ = offMode; lastEnabled_ = enabled_; From e6797686d4701a94e2f31efb2a08ef4bfd18de08 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Tue, 2 Sep 2025 14:59:42 -0700 Subject: [PATCH 13/14] bartdepart: Pre-reserve vector capacity to reduce heap churn on MCUs --- usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp index 5d4a89d367..a5dc8eff6e 100644 --- a/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp +++ b/usermods/usermod_v2_bartdepart/usermod_v2_bartdepart.cpp @@ -20,8 +20,12 @@ REGISTER_USERMOD(bartdepart_usermod); const uint32_t SAFETY_DELAY_MS = 10u * 1000u; BartDepart::BartDepart() { + sources_.reserve(1); sources_.push_back(::make_unique()); + model_ = ::make_unique(); + + views_.reserve(4); views_.push_back(::make_unique("1")); views_.push_back(::make_unique("2")); views_.push_back(::make_unique("3")); From 03f718b429b6c9c283bb8b67d54c347bc9ea386f Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Tue, 2 Sep 2025 15:09:49 -0700 Subject: [PATCH 14/14] bartdepart: OR-accumulate invalidate_history across readers --- usermods/usermod_v2_bartdepart/legacy_bart_source.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp b/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp index ee4a89e858..3f8b713c90 100644 --- a/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp +++ b/usermods/usermod_v2_bartdepart/legacy_bart_source.cpp @@ -90,6 +90,6 @@ bool LegacyBartSource::readFromConfig(JsonObject& root, bool startup_complete, b ok &= getJsonValue(root["ApiStation"], apiStation_, apiStation_); // Only invalidate when source identity changes (base/station) - invalidate_history = (apiBase_ != prevBase) || (apiStation_ != prevStation); + invalidate_history |= (apiBase_ != prevBase) || (apiStation_ != prevStation); return ok; }