mirror of
https://github.com/sweidac/kidsbox.git
synced 2026-10-27 22:52:57 +01:00
- rc522: add NDEF URL reading via WUPA+cascade anticoll+SELECT+TLV parse
- AudioPlayer: route http:// URIs to esp_player_http_music_play()
- CacheManager: download URLs async to /sdcard/cache/{hash}.{ext}
- TagPlayerControl: NDEF URL → cache-or-stream; fallback to UID local mapping
- RFIDInterface: wire ndef_callback, expose combined uid+url callback
- WebInterface: add /api/info, /api/storage, /api/playback, /api/bluetooth
- BluetoothInterface: stub for A2DP speaker mode
- Networking: WiFi connect with 30s timeout instead of blocking forever
- main.cpp: re-enable WiFi + web server, fix init order, add CacheManager
- www/: full SPA — playback controls, device info, storage, BT pairing
- .gitmodules: switch rc522 submodule URL to HTTPS
https://claude.ai/code/session_01MhAnh68DPNnTjcKqjNYDmk
206 lines
5.9 KiB
C++
206 lines
5.9 KiB
C++
#include "CacheManager.hpp"
|
|
|
|
#include <esp_log.h>
|
|
#include <esp_http_client.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/task.h>
|
|
#include <sys/stat.h>
|
|
#include <dirent.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <string>
|
|
#include <functional>
|
|
|
|
static const char* TAG = "CacheManager";
|
|
|
|
struct DownloadArgs {
|
|
std::string url;
|
|
std::function<void(bool)> onComplete;
|
|
};
|
|
|
|
CacheManager::CacheManager() {
|
|
mkdir(CACHE_DIR, 0777);
|
|
}
|
|
|
|
/* djb2 hash of the URL, returned as 8-char hex string */
|
|
std::string CacheManager::urlToKey(const std::string& url) const {
|
|
uint32_t hash = 5381;
|
|
for(char c : url) {
|
|
hash = ((hash << 5) + hash) ^ (uint8_t)c;
|
|
}
|
|
char buf[9];
|
|
snprintf(buf, sizeof(buf), "%08lx", (unsigned long)hash);
|
|
return std::string(buf);
|
|
}
|
|
|
|
std::string CacheManager::urlExtension(const std::string& url) const {
|
|
/* Strip query/fragment before looking for extension */
|
|
size_t qs = url.find('?');
|
|
std::string path = (qs != std::string::npos) ? url.substr(0, qs) : url;
|
|
|
|
size_t dot = path.rfind('.');
|
|
size_t slash = path.rfind('/');
|
|
|
|
if(dot != std::string::npos && (slash == std::string::npos || dot > slash)) {
|
|
std::string ext = path.substr(dot); /* includes the dot */
|
|
if(ext.size() <= 5) return ext; /* ignore suspiciously long "extensions" */
|
|
}
|
|
return ".mp3"; /* sensible default */
|
|
}
|
|
|
|
std::string CacheManager::keyToPath(const std::string& key, const std::string& ext) const {
|
|
return std::string(CACHE_DIR) + "/" + key + ext;
|
|
}
|
|
|
|
/* Find any file in CACHE_DIR whose name starts with key */
|
|
std::string CacheManager::findFileByKey(const std::string& key) const {
|
|
DIR* dir = opendir(CACHE_DIR);
|
|
if(!dir) return "";
|
|
|
|
struct dirent* entry;
|
|
while((entry = readdir(dir)) != NULL) {
|
|
std::string name(entry->d_name);
|
|
if(name.rfind(key, 0) == 0) {
|
|
closedir(dir);
|
|
return std::string(CACHE_DIR) + "/" + name;
|
|
}
|
|
}
|
|
closedir(dir);
|
|
return "";
|
|
}
|
|
|
|
std::string CacheManager::getCachedPath(const std::string& url) const {
|
|
if(url.empty()) return "";
|
|
std::string key = urlToKey(url);
|
|
std::string path = findFileByKey(key);
|
|
if(!path.empty()) {
|
|
ESP_LOGI(TAG, "Cache hit: %s -> %s", url.c_str(), path.c_str());
|
|
}
|
|
return path;
|
|
}
|
|
|
|
void CacheManager::downloadTask(void* arg) {
|
|
auto* dl = static_cast<DownloadArgs*>(arg);
|
|
|
|
ESP_LOGI(TAG, "Downloading: %s", dl->url.c_str());
|
|
|
|
CacheManager cm;
|
|
std::string key = cm.urlToKey(dl->url);
|
|
std::string ext = cm.urlExtension(dl->url);
|
|
std::string path = cm.keyToPath(key, ext);
|
|
|
|
esp_http_client_config_t cfg = {};
|
|
cfg.url = dl->url.c_str();
|
|
cfg.timeout_ms = 30000;
|
|
cfg.buffer_size = 4096;
|
|
|
|
esp_http_client_handle_t client = esp_http_client_init(&cfg);
|
|
esp_err_t err = esp_http_client_open(client, 0);
|
|
|
|
bool success = false;
|
|
|
|
if(err == ESP_OK) {
|
|
int content_len = esp_http_client_fetch_headers(client);
|
|
(void)content_len;
|
|
|
|
FILE* f = fopen(path.c_str(), "wb");
|
|
if(f) {
|
|
char buf[4096];
|
|
int read_len;
|
|
while((read_len = esp_http_client_read(client, buf, sizeof(buf))) > 0) {
|
|
fwrite(buf, 1, read_len, f);
|
|
}
|
|
fclose(f);
|
|
success = (read_len == 0); /* 0 = clean EOF */
|
|
if(!success) {
|
|
remove(path.c_str());
|
|
ESP_LOGE(TAG, "Download error, partial file deleted");
|
|
} else {
|
|
ESP_LOGI(TAG, "Cached to %s", path.c_str());
|
|
}
|
|
} else {
|
|
ESP_LOGE(TAG, "Cannot open cache file: %s (errno %d)", path.c_str(), errno);
|
|
}
|
|
} else {
|
|
ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err));
|
|
}
|
|
|
|
esp_http_client_cleanup(client);
|
|
|
|
if(dl->onComplete) dl->onComplete(success);
|
|
|
|
delete dl;
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
void CacheManager::downloadAsync(const std::string& url, std::function<void(bool)> onComplete) {
|
|
if(url.empty()) return;
|
|
|
|
auto* args = new DownloadArgs{ url, onComplete };
|
|
xTaskCreate(downloadTask, "cache_dl", 8192, args, 3, NULL);
|
|
}
|
|
|
|
std::vector<CacheEntry> CacheManager::listEntries() const {
|
|
std::vector<CacheEntry> entries;
|
|
|
|
DIR* dir = opendir(CACHE_DIR);
|
|
if(!dir) return entries;
|
|
|
|
struct dirent* entry;
|
|
while((entry = readdir(dir)) != NULL) {
|
|
if(entry->d_name[0] == '.') continue;
|
|
|
|
std::string name(entry->d_name);
|
|
std::string full = std::string(CACHE_DIR) + "/" + name;
|
|
|
|
struct stat st;
|
|
size_t size = 0;
|
|
if(stat(full.c_str(), &st) == 0) size = st.st_size;
|
|
|
|
/* Key is filename without extension */
|
|
size_t dot = name.rfind('.');
|
|
std::string key = (dot != std::string::npos) ? name.substr(0, dot) : name;
|
|
|
|
entries.push_back({ key, /*url unknown*/"", full, size });
|
|
}
|
|
closedir(dir);
|
|
return entries;
|
|
}
|
|
|
|
bool CacheManager::deleteEntry(const std::string& key) {
|
|
std::string path = findFileByKey(key);
|
|
if(path.empty()) return false;
|
|
return remove(path.c_str()) == 0;
|
|
}
|
|
|
|
void CacheManager::deleteAll() {
|
|
DIR* dir = opendir(CACHE_DIR);
|
|
if(!dir) return;
|
|
|
|
struct dirent* entry;
|
|
while((entry = readdir(dir)) != NULL) {
|
|
if(entry->d_name[0] == '.') continue;
|
|
std::string full = std::string(CACHE_DIR) + "/" + entry->d_name;
|
|
remove(full.c_str());
|
|
}
|
|
closedir(dir);
|
|
ESP_LOGI(TAG, "Cache cleared");
|
|
}
|
|
|
|
size_t CacheManager::totalBytes() const {
|
|
size_t total = 0;
|
|
DIR* dir = opendir(CACHE_DIR);
|
|
if(!dir) return 0;
|
|
|
|
struct dirent* entry;
|
|
while((entry = readdir(dir)) != NULL) {
|
|
if(entry->d_name[0] == '.') continue;
|
|
std::string full = std::string(CACHE_DIR) + "/" + entry->d_name;
|
|
struct stat st;
|
|
if(stat(full.c_str(), &st) == 0) total += st.st_size;
|
|
}
|
|
closedir(dir);
|
|
return total;
|
|
}
|