Skip to content
Closed
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
170 changes: 170 additions & 0 deletions usermods/usermod_v2_bartdepart/bart_station_model.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#include "bart_station_model.h"
#include "util.h"

#include <algorithm>
#include <cstdio>
#include <cstring>

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<JsonArray>()) {
for (JsonObject station : root["station"].as<JsonArray>()) {
if (!station["etd"].is<JsonArray>()) continue;
for (JsonObject etd : station["etd"].as<JsonArray>()) {
if (!etd["estimate"].is<JsonArray>()) continue;
bool matches = false;
for (JsonObject est : etd["estimate"].as<JsonArray>()) {
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::ETDBatch>&
BartStationModel::Platform::history() const {
return history_;
}

const std::vector<String>& 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<String> BartStationModel::destinationsForPlatform(const String& platformId) const {
for (auto const& p : platforms) {
if (p.platformId() == platformId) {
return p.destinations();
}
}
return {};
}
47 changes: 47 additions & 0 deletions usermods/usermod_v2_bartdepart/bart_station_model.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once

#include "wled.h"

#include <deque>
#include <vector>
#include <ctime>

#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<ETD> 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<ETDBatch>& history() const;
const std::vector<String>& destinations() const;
String toString() const;

private:
String platformId_;
std::deque<ETDBatch> history_;
std::vector<String> destinations_;

// return UTC tstamp
time_t parseHeaderTimestamp(const char* dateStr, const char* timeStr) const;
};

std::vector<Platform> platforms;

void update(std::time_t now, BartStationModel&& delta);
time_t oldest() const;
std::vector<String> destinationsForPlatform(const String& platformId) const;
};
59 changes: 59 additions & 0 deletions usermods/usermod_v2_bartdepart/interfaces.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once

#include <ctime>
#include <memory>
#include <string>
#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<typename ModelType>
class IDataSourceT : public IConfigurable {
public:
virtual ~IDataSourceT() = default;

/// Fetch new data, nullptr if no new data
virtual std::unique_ptr<ModelType> fetch(std::time_t now) = 0;

/// Backfill older history if needed, nullptr if no new data
virtual std::unique_ptr<ModelType> 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<typename ModelType>
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);
}
};
95 changes: 95 additions & 0 deletions usermods/usermod_v2_bartdepart/legacy_bart_source.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#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<BartStationModel> 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 < 200 || httpCode >= 300) {
https_.end();
DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: HTTP status not OK: %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;
}
Comment on lines +41 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Stream JSON instead of building a String to cut peak RAM and avoid OOM.

Current flow holds payload String and DynamicJsonDocument simultaneously. Stream-deserialize from the HTTPClient stream, size the doc from Content-Length, and log parse errors.

Apply:

-  String payload = https_.getString();
-  https_.end();
-
-  size_t jsonSz = payload.length() * 2;
-  DynamicJsonDocument doc(jsonSz);
-  auto err = deserializeJson(doc, payload);
+  size_t contentLen = https_.getSize();
+  size_t jsonSz = contentLen > 0 ? (size_t)(contentLen + 512) : 4096; // heuristic
+  DynamicJsonDocument doc(jsonSz);
+  auto err = deserializeJson(doc, https_.getStream());
+  https_.end();
   if (err) {
+    DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: JSON parse error: %s\n", err.c_str());
     nextFetch_ = now + updateSecs_ * backoffMult_;
     if (backoffMult_ < 16) backoffMult_ *= 2;
     return nullptr;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
// Stream‐deserialize JSON instead of buffering in a String to reduce peak RAM usage
size_t contentLen = https_.getSize();
size_t jsonSz = contentLen > 0
? static_cast<size_t>(contentLen + 512) // add slack
: 4096; // fallback heuristic
DynamicJsonDocument doc(jsonSz);
auto err = deserializeJson(doc, https_.getStream());
https_.end();
if (err) {
DEBUG_PRINTF("BartDepart: LegacyBartSource::fetch: JSON parse error: %s\n", err.c_str());
nextFetch_ = now + updateSecs_ * backoffMult_;
if (backoffMult_ < 16) backoffMult_ *= 2;
return nullptr;
}


JsonObject root = doc["root"].as<JsonObject>();
if (root.isNull()) {
nextFetch_ = now + updateSecs_ * backoffMult_;
if (backoffMult_ < 16) backoffMult_ *= 2;
return nullptr;
}

std::unique_ptr<BartStationModel> 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;
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["ApiStation"], apiStation_, apiStation_);

// Only invalidate when source identity changes (base/station)
invalidate_history |= (apiBase_ != prevBase) || (apiStation_ != prevStation);
return ok;
}
Loading