mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 19:06:34 +00:00
feat: WebKit for web/html cells + display auto-discovery via heartbeat
Rust kiosk: - web cells now use webkit6 WebView (load_uri) - html cells use WebView.load_html (full HTML rendering) - query_displays() reads /sys/class/drm/ for connected HDMI/DP outputs - Heartbeat reports display geometry every 60s Server: - /api/kiosk/heartbeat accepts displays array - Syncs kiosk-reported displays to display records - Updates dimensions when changed, creates new displays for new ports
This commit is contained in:
parent
722ddcfb12
commit
766bf8dee0
4 changed files with 72 additions and 11 deletions
|
|
@ -32,3 +32,4 @@ hostname = "0.4"
|
|||
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
||||
futures-util = "0.3"
|
||||
url = "2"
|
||||
webkit6 = "0.4"
|
||||
|
|
|
|||
|
|
@ -148,14 +148,18 @@ pub fn fetch_bundle(server: &str, key: &str) -> KioskBundle {
|
|||
resp.json().expect("bad bundle JSON")
|
||||
}
|
||||
|
||||
/// Send heartbeat.
|
||||
pub fn heartbeat(server: &str, key: &str) {
|
||||
/// Send heartbeat with display geometry.
|
||||
pub fn heartbeat(server: &str, key: &str, displays: &[(String, u32, u32)]) {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let display_info: Vec<_> = displays.iter().map(|(name, w, h)| {
|
||||
serde_json::json!({ "name": name, "width_px": w, "height_px": h })
|
||||
}).collect();
|
||||
let _ = client
|
||||
.post(format!("{server}/api/kiosk/heartbeat"))
|
||||
.header("Authorization", format!("Bearer {key}"))
|
||||
.json(&serde_json::json!({
|
||||
"kiosk_app_version": env!("CARGO_PKG_VERSION"),
|
||||
"displays": display_info,
|
||||
}))
|
||||
.timeout(Duration::from_secs(5))
|
||||
.send();
|
||||
|
|
|
|||
|
|
@ -92,10 +92,11 @@ fn activate(app: &Application) {
|
|||
}
|
||||
});
|
||||
|
||||
// Heartbeat loop
|
||||
// Heartbeat loop — also reports display geometry
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(60));
|
||||
server::heartbeat(&server, &key);
|
||||
let displays = query_displays();
|
||||
server::heartbeat(&server, &key, &displays);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -117,6 +118,33 @@ enum WorkerMsg {
|
|||
RenderBundle(KioskBundle),
|
||||
}
|
||||
|
||||
/// Query connected HDMI displays from sysfs. Returns (name, width, height).
|
||||
/// Reads /sys/class/drm/*/status and /sys/class/drm/*/modes.
|
||||
fn query_displays() -> Vec<(String, u32, u32)> {
|
||||
let mut out = Vec::new();
|
||||
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return out };
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
// Skip non-HDMI connectors and the "card" parents
|
||||
if !name.contains("-HDMI-") && !name.contains("-DP-") { continue; }
|
||||
let path = entry.path();
|
||||
let status = std::fs::read_to_string(path.join("status")).unwrap_or_default();
|
||||
if status.trim() != "connected" { continue; }
|
||||
let modes = std::fs::read_to_string(path.join("modes")).unwrap_or_default();
|
||||
// First line = preferred mode
|
||||
let mode = modes.lines().next().unwrap_or("");
|
||||
let parts: Vec<&str> = mode.split('x').collect();
|
||||
if parts.len() != 2 { continue; }
|
||||
let w: u32 = parts[0].parse().unwrap_or(0);
|
||||
let h: u32 = parts[1].trim().parse().unwrap_or(0);
|
||||
if w == 0 || h == 0 { continue; }
|
||||
// Strip "cardN-" prefix for cleaner name
|
||||
let clean_name = name.split_once('-').map(|(_, rest)| rest.to_string()).unwrap_or(name);
|
||||
out.push((clean_name, w, h));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn show_pairing_code(window: &ApplicationWindow, code: &str) {
|
||||
let vbox = GtkBox::new(Orientation::Vertical, 20);
|
||||
vbox.set_valign(gtk::Align::Center);
|
||||
|
|
@ -212,16 +240,20 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
|||
}
|
||||
}
|
||||
"html" => {
|
||||
let html = cell.html_content.as_deref().unwrap_or("HTML");
|
||||
let label = Label::new(Some(&html.chars().take(100).collect::<String>()));
|
||||
add_css(&label, "label { color: #888; background-color: #111; }");
|
||||
label.set_vexpand(true);
|
||||
label.set_hexpand(true);
|
||||
label.upcast()
|
||||
let html = cell.html_content.as_deref().unwrap_or("");
|
||||
let webview = webkit6::WebView::new();
|
||||
webkit6::prelude::WebViewExt::load_html(&webview, html, None);
|
||||
webview.set_vexpand(true);
|
||||
webview.set_hexpand(true);
|
||||
webview.upcast()
|
||||
}
|
||||
"web" => {
|
||||
let url = cell.web_url.as_deref().unwrap_or("about:blank");
|
||||
placeholder(&format!("Web: {url}"))
|
||||
let webview = webkit6::WebView::new();
|
||||
webkit6::prelude::WebViewExt::load_uri(&webview, url);
|
||||
webview.set_vexpand(true);
|
||||
webview.set_hexpand(true);
|
||||
webview.upcast()
|
||||
}
|
||||
_ => placeholder("Unknown content"),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@ function registerKioskRoutes(
|
|||
bundle_version?: string;
|
||||
kiosk_app_version?: string;
|
||||
os_version?: string;
|
||||
displays?: Array<{ name: string; width_px: number; height_px: number }>;
|
||||
}>(event);
|
||||
|
||||
repo.touchKiosk(kiosk.id, {
|
||||
|
|
@ -234,6 +235,29 @@ function registerKioskRoutes(
|
|||
os_version: body?.os_version ?? null,
|
||||
});
|
||||
|
||||
// Sync displays reported by the kiosk
|
||||
if (Array.isArray(body?.displays)) {
|
||||
const existing = repo.listDisplaysForKiosk(kiosk.id);
|
||||
for (const reported of body.displays) {
|
||||
const match = existing.find((d) => d.name.endsWith(reported.name));
|
||||
if (match) {
|
||||
if (match.width_px !== reported.width_px || match.height_px !== reported.height_px) {
|
||||
repo.updateDisplay(match.id, {
|
||||
width_px: reported.width_px,
|
||||
height_px: reported.height_px,
|
||||
} as any);
|
||||
}
|
||||
} else {
|
||||
// New display — create it
|
||||
repo.createDisplayForKiosk(kiosk.id, {
|
||||
name: reported.name,
|
||||
width_px: reported.width_px,
|
||||
height_px: reported.height_px,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, now: new Date().toISOString() };
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue