mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 21:26:33 +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"] }
|
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
url = "2"
|
url = "2"
|
||||||
|
webkit6 = "0.4"
|
||||||
|
|
|
||||||
|
|
@ -148,14 +148,18 @@ pub fn fetch_bundle(server: &str, key: &str) -> KioskBundle {
|
||||||
resp.json().expect("bad bundle JSON")
|
resp.json().expect("bad bundle JSON")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send heartbeat.
|
/// Send heartbeat with display geometry.
|
||||||
pub fn heartbeat(server: &str, key: &str) {
|
pub fn heartbeat(server: &str, key: &str, displays: &[(String, u32, u32)]) {
|
||||||
let client = reqwest::blocking::Client::new();
|
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
|
let _ = client
|
||||||
.post(format!("{server}/api/kiosk/heartbeat"))
|
.post(format!("{server}/api/kiosk/heartbeat"))
|
||||||
.header("Authorization", format!("Bearer {key}"))
|
.header("Authorization", format!("Bearer {key}"))
|
||||||
.json(&serde_json::json!({
|
.json(&serde_json::json!({
|
||||||
"kiosk_app_version": env!("CARGO_PKG_VERSION"),
|
"kiosk_app_version": env!("CARGO_PKG_VERSION"),
|
||||||
|
"displays": display_info,
|
||||||
}))
|
}))
|
||||||
.timeout(Duration::from_secs(5))
|
.timeout(Duration::from_secs(5))
|
||||||
.send();
|
.send();
|
||||||
|
|
|
||||||
|
|
@ -92,10 +92,11 @@ fn activate(app: &Application) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Heartbeat loop
|
// Heartbeat loop — also reports display geometry
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(60));
|
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),
|
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) {
|
fn show_pairing_code(window: &ApplicationWindow, code: &str) {
|
||||||
let vbox = GtkBox::new(Orientation::Vertical, 20);
|
let vbox = GtkBox::new(Orientation::Vertical, 20);
|
||||||
vbox.set_valign(gtk::Align::Center);
|
vbox.set_valign(gtk::Align::Center);
|
||||||
|
|
@ -212,16 +240,20 @@ fn render_bundle(window: &ApplicationWindow, bundle: KioskBundle) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"html" => {
|
"html" => {
|
||||||
let html = cell.html_content.as_deref().unwrap_or("HTML");
|
let html = cell.html_content.as_deref().unwrap_or("");
|
||||||
let label = Label::new(Some(&html.chars().take(100).collect::<String>()));
|
let webview = webkit6::WebView::new();
|
||||||
add_css(&label, "label { color: #888; background-color: #111; }");
|
webkit6::prelude::WebViewExt::load_html(&webview, html, None);
|
||||||
label.set_vexpand(true);
|
webview.set_vexpand(true);
|
||||||
label.set_hexpand(true);
|
webview.set_hexpand(true);
|
||||||
label.upcast()
|
webview.upcast()
|
||||||
}
|
}
|
||||||
"web" => {
|
"web" => {
|
||||||
let url = cell.web_url.as_deref().unwrap_or("about:blank");
|
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"),
|
_ => placeholder("Unknown content"),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ function registerKioskRoutes(
|
||||||
bundle_version?: string;
|
bundle_version?: string;
|
||||||
kiosk_app_version?: string;
|
kiosk_app_version?: string;
|
||||||
os_version?: string;
|
os_version?: string;
|
||||||
|
displays?: Array<{ name: string; width_px: number; height_px: number }>;
|
||||||
}>(event);
|
}>(event);
|
||||||
|
|
||||||
repo.touchKiosk(kiosk.id, {
|
repo.touchKiosk(kiosk.id, {
|
||||||
|
|
@ -234,6 +235,29 @@ function registerKioskRoutes(
|
||||||
os_version: body?.os_version ?? null,
|
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() };
|
return { ok: true, now: new Date().toISOString() };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue