feat(travel): Implement settings UI and web server for MTR route configuration

- Added MainUIHandler class to manage the main UI and polling for arrival data.
- Introduced SettingsUI class for displaying QR code and configuration options.
- Created SettingsUIHandler to manage settings UI lifecycle and web server interactions.
- Developed WebHandler to handle HTTP requests for MTR route settings, including adding and removing routes.
- Implemented web endpoints for fetching MTR lines, routes, and saving settings.
- Enhanced UI with responsive design for e-ink displays and added error handling for web interactions.
This commit is contained in:
GW_MC
2026-02-03 19:26:53 +08:00
parent 0672a5fb74
commit c4635948e4
24 changed files with 2324 additions and 22 deletions

View File

@@ -0,0 +1,170 @@
#include "ui/apps/travel/settings/settings_handler.h"
#include "cJSON.h"
#include "esp_log.h"
static const char* TAG = "TravelSettings";
namespace travel {
SettingHandler::SettingHandler(std::unique_ptr<NVSStorageHandler> storage)
: routes_()
, polling_interval_sec_(DEFAULT_POLLING_INTERVAL)
, storage_(std::move(storage)) {
}
esp_err_t SettingHandler::init(const EventGroupHandle_t& system_event_group) {
storage_->init(system_event_group);
return ESP_OK;
}
void SettingHandler::load_settings() {
// Load polling interval
std::string poll_str = storage_->get(NVS_KEY_POLLING);
if (!poll_str.empty()) {
polling_interval_sec_ = std::stoul(poll_str);
if (polling_interval_sec_ < MIN_POLLING_INTERVAL ||
polling_interval_sec_ > MAX_POLLING_INTERVAL) {
polling_interval_sec_ = DEFAULT_POLLING_INTERVAL;
}
}
// Load routes
std::string routes_json = storage_->get(NVS_KEY_ROUTES);
if (!routes_json.empty()) {
routes_from_json(routes_json);
}
ESP_LOGI(TAG, "Loaded %d routes, polling interval: %d seconds",
static_cast<int>(routes_.size()), static_cast<int>(polling_interval_sec_));
}
void SettingHandler::save_settings() {
// Save polling interval
storage_->put(NVS_KEY_POLLING, std::to_string(polling_interval_sec_));
// Save routes
std::string routes_json = routes_to_json();
storage_->put(NVS_KEY_ROUTES, routes_json);
ESP_LOGI(TAG, "Saved %d routes, polling interval: %d seconds",
static_cast<int>(routes_.size()), static_cast<int>(polling_interval_sec_));
}
void SettingHandler::add_route(const RoutePair& route) {
if (routes_.size() >= MAX_ROUTES) {
ESP_LOGW(TAG, "Maximum number of routes reached (%d)", static_cast<int>(MAX_ROUTES));
return;
}
// Check for duplicates
for (const auto& existing : routes_) {
if (existing == route) {
ESP_LOGW(TAG, "Route already exists");
return;
}
}
routes_.push_back(route);
ESP_LOGI(TAG, "Added route: %s -> %s", route.station_name.c_str(), route.dest_name.c_str());
}
void SettingHandler::remove_route(size_t index) {
if (index < routes_.size()) {
ESP_LOGI(TAG, "Removing route at index %d", static_cast<int>(index));
routes_.erase(routes_.begin() + index);
}
}
void SettingHandler::clear_routes() {
routes_.clear();
ESP_LOGI(TAG, "Cleared all routes");
}
void SettingHandler::set_polling_interval(uint32_t seconds) {
if (seconds < MIN_POLLING_INTERVAL) {
seconds = MIN_POLLING_INTERVAL;
} else if (seconds > MAX_POLLING_INTERVAL) {
seconds = MAX_POLLING_INTERVAL;
}
polling_interval_sec_ = seconds;
ESP_LOGI(TAG, "Set polling interval to %d seconds", static_cast<int>(seconds));
}
std::string SettingHandler::routes_to_json() const {
cJSON* root = cJSON_CreateArray();
for (const auto& route : routes_) {
cJSON* route_obj = cJSON_CreateObject();
cJSON_AddStringToObject(route_obj, "line_code", route.line_code.c_str());
cJSON_AddStringToObject(route_obj, "line_name", route.line_name.c_str());
cJSON_AddStringToObject(route_obj, "line_color", route.line_color.c_str());
cJSON_AddStringToObject(route_obj, "station_code", route.station_code.c_str());
cJSON_AddStringToObject(route_obj, "station_name", route.station_name.c_str());
cJSON_AddStringToObject(route_obj, "dest_code", route.dest_code.c_str());
cJSON_AddStringToObject(route_obj, "dest_name", route.dest_name.c_str());
cJSON_AddItemToArray(root, route_obj);
}
char* json_str = cJSON_PrintUnformatted(root);
std::string result(json_str ? json_str : "[]");
if (json_str) {
free(json_str);
}
cJSON_Delete(root);
return result;
}
void SettingHandler::routes_from_json(const std::string& json) {
routes_.clear();
cJSON* root = cJSON_Parse(json.c_str());
if (!root || !cJSON_IsArray(root)) {
ESP_LOGE(TAG, "Failed to parse routes JSON");
if (root) {
cJSON_Delete(root);
}
return;
}
int array_size = cJSON_GetArraySize(root);
for (int i = 0; i < array_size && i < static_cast<int>(MAX_ROUTES); i++) {
cJSON* route_obj = cJSON_GetArrayItem(root, i);
if (!route_obj || !cJSON_IsObject(route_obj)) {
continue;
}
RoutePair route;
cJSON* item;
item = cJSON_GetObjectItem(route_obj, "line_code");
if (item && cJSON_IsString(item)) route.line_code = item->valuestring;
item = cJSON_GetObjectItem(route_obj, "line_name");
if (item && cJSON_IsString(item)) route.line_name = item->valuestring;
item = cJSON_GetObjectItem(route_obj, "line_color");
if (item && cJSON_IsString(item)) route.line_color = item->valuestring;
item = cJSON_GetObjectItem(route_obj, "station_code");
if (item && cJSON_IsString(item)) route.station_code = item->valuestring;
item = cJSON_GetObjectItem(route_obj, "station_name");
if (item && cJSON_IsString(item)) route.station_name = item->valuestring;
item = cJSON_GetObjectItem(route_obj, "dest_code");
if (item && cJSON_IsString(item)) route.dest_code = item->valuestring;
item = cJSON_GetObjectItem(route_obj, "dest_name");
if (item && cJSON_IsString(item)) route.dest_name = item->valuestring;
if (!route.line_code.empty() && !route.station_code.empty() && !route.dest_code.empty()) {
routes_.push_back(route);
}
}
cJSON_Delete(root);
ESP_LOGI(TAG, "Loaded %d routes from JSON", static_cast<int>(routes_.size()));
}
} // namespace travel