mirror of
https://github.com/sweidac/kidsbox.git
synced 2026-10-28 06:52:57 +01:00
Implement full NFC-URL streaming architecture
- 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
This commit is contained in:
19 files changed
+1172
-318
No files matched your search
+148
-9
@@ -1,13 +1,152 @@
|
||||
console.log('hi');
|
||||
'use strict';
|
||||
|
||||
const elements = {};
|
||||
/* ── helpers ──────────────────────────────────────────────────────────── */
|
||||
|
||||
elements.rfidId = document.querySelector('.rfid-id');
|
||||
elements.rfidRefresh = document.querySelector('.rfid-refresh');
|
||||
function fmt_bytes(n) {
|
||||
if (n < 1024) return n + ' B';
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
||||
return (n / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
elements.rfidRefresh.addEventListener('click', async () => {
|
||||
const response = await fetch('/rfid'),
|
||||
rfidId = await response.text();
|
||||
async function api(method, path, body) {
|
||||
const opts = { method };
|
||||
if (body) {
|
||||
opts.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||
opts.body = body;
|
||||
}
|
||||
const res = await fetch(path, opts);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
elements.rfidId.innerText = rfidId;
|
||||
});
|
||||
/* ── device info ──────────────────────────────────────────────────────── */
|
||||
|
||||
async function refreshInfo() {
|
||||
try {
|
||||
const d = await api('GET', '/api/info');
|
||||
document.getElementById('info-ip').textContent = d.ip ?? '—';
|
||||
document.getElementById('info-playing').textContent = d.playing ? 'Playing' : 'Stopped';
|
||||
document.getElementById('info-cache').textContent = fmt_bytes(d.cacheBytes ?? 0);
|
||||
document.getElementById('info-heap').textContent = fmt_bytes(d.freeHeap ?? 0);
|
||||
|
||||
document.getElementById('track-uri').textContent = d.currentUri || '—';
|
||||
document.getElementById('btn-pause').disabled = !d.playing;
|
||||
document.getElementById('btn-stop').disabled = !d.playing;
|
||||
|
||||
const vol = d.volume ?? 50;
|
||||
document.getElementById('volume-slider').value = vol;
|
||||
document.getElementById('volume-label').textContent = vol;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch /api/info', e);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── storage ──────────────────────────────────────────────────────────── */
|
||||
|
||||
async function refreshStorage() {
|
||||
try {
|
||||
const entries = await api('GET', '/api/storage');
|
||||
const list = document.getElementById('cache-list');
|
||||
const summary = document.getElementById('storage-summary');
|
||||
|
||||
list.innerHTML = '';
|
||||
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
summary.textContent = 'No cached files';
|
||||
return;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
entries.forEach(e => {
|
||||
total += e.size ?? 0;
|
||||
const li = document.createElement('li');
|
||||
const key = document.createElement('span');
|
||||
key.className = 'cache-key';
|
||||
key.textContent = e.key;
|
||||
const sz = document.createElement('span');
|
||||
sz.className = 'cache-size';
|
||||
sz.textContent = fmt_bytes(e.size ?? 0);
|
||||
const del = document.createElement('button');
|
||||
del.className = 'btn btn-danger';
|
||||
del.style.cssText = 'padding:.2rem .5rem;font-size:.75rem';
|
||||
del.textContent = '✕';
|
||||
del.addEventListener('click', () => deleteEntry(e.key));
|
||||
li.append(key, sz, del);
|
||||
list.appendChild(li);
|
||||
});
|
||||
|
||||
summary.textContent =
|
||||
`${entries.length} file${entries.length !== 1 ? 's' : ''} — ${fmt_bytes(total)}`;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch /api/storage', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEntry(key) {
|
||||
await api('DELETE', `/api/storage?key=${encodeURIComponent(key)}`);
|
||||
refreshStorage();
|
||||
}
|
||||
|
||||
async function clearCache() {
|
||||
if (!confirm('Delete all cached files?')) return;
|
||||
await api('DELETE', '/api/storage');
|
||||
refreshStorage();
|
||||
}
|
||||
|
||||
/* ── playback ─────────────────────────────────────────────────────────── */
|
||||
|
||||
async function sendPlayback(action, value) {
|
||||
const body = value !== undefined
|
||||
? `action=${action}&value=${encodeURIComponent(value)}`
|
||||
: `action=${action}`;
|
||||
await api('POST', '/api/playback', body);
|
||||
refreshInfo();
|
||||
}
|
||||
|
||||
/* ── bluetooth ────────────────────────────────────────────────────────── */
|
||||
|
||||
async function refreshBt() {
|
||||
try {
|
||||
const d = await api('GET', '/api/bluetooth');
|
||||
const status = document.getElementById('bt-status');
|
||||
const btn = document.getElementById('btn-bt-toggle');
|
||||
if (d.enabled) {
|
||||
status.textContent = 'Enabled';
|
||||
status.style.color = '#48bb78';
|
||||
btn.textContent = 'Disable';
|
||||
} else {
|
||||
status.textContent = 'Disabled';
|
||||
status.style.color = '#a0aec0';
|
||||
btn.textContent = 'Enable';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch /api/bluetooth', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleBt() {
|
||||
const enabled = document.getElementById('bt-status').textContent === 'Enabled';
|
||||
await api('POST', '/api/bluetooth', `enabled=${!enabled}`);
|
||||
refreshBt();
|
||||
}
|
||||
|
||||
/* ── init ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshInfo();
|
||||
refreshStorage();
|
||||
refreshBt();
|
||||
|
||||
document.getElementById('btn-refresh-info').addEventListener('click', refreshInfo);
|
||||
document.getElementById('btn-clear-cache').addEventListener('click', clearCache);
|
||||
document.getElementById('btn-bt-toggle').addEventListener('click', toggleBt);
|
||||
document.getElementById('btn-pause').addEventListener('click', () => sendPlayback('pause'));
|
||||
document.getElementById('btn-stop').addEventListener('click', () => sendPlayback('stop'));
|
||||
|
||||
const slider = document.getElementById('volume-slider');
|
||||
const label = document.getElementById('volume-label');
|
||||
slider.addEventListener('input', () => { label.textContent = slider.value; });
|
||||
slider.addEventListener('change', () => sendPlayback('volume', slider.value));
|
||||
|
||||
/* Auto-refresh info every 5 s while page is visible */
|
||||
setInterval(() => { if (!document.hidden) refreshInfo(); }, 5000);
|
||||
});
|
||||
Reference in new issue
Block a user