This commit is contained in:
188
www/dashboard.js
188
www/dashboard.js
@@ -56,6 +56,7 @@
|
||||
userGroups: [],
|
||||
maps: [],
|
||||
mapsLoaded: false,
|
||||
ioModules: [],
|
||||
robotPose: null,
|
||||
robotPoseAt: 0,
|
||||
mapPollTimer: null,
|
||||
@@ -277,6 +278,167 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureIoModulesLoaded() {
|
||||
if (store.ioModules.length) return store.ioModules;
|
||||
try {
|
||||
if (window.IoModulesCatalog?.refresh) {
|
||||
store.ioModules = await window.IoModulesCatalog.refresh();
|
||||
} else {
|
||||
const res = await fetch("/api/io_modules", { credentials: "include" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
store.ioModules = Array.isArray(data.io_modules) ? data.io_modules : [];
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
store.ioModules = [];
|
||||
}
|
||||
return store.ioModules;
|
||||
}
|
||||
|
||||
function ioModuleOptions(selectedId) {
|
||||
const list = store.ioModules || [];
|
||||
if (!list.length) return `<option value="">${escapeHtml(t("ioModules.empty"))}</option>`;
|
||||
return list
|
||||
.map(
|
||||
(m) =>
|
||||
`<option value="${escapeHtml(m.id)}" ${m.id === selectedId ? "selected" : ""}>${escapeHtml(m.name || m.id)}</option>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function ioModuleById(id) {
|
||||
return (store.ioModules || []).find((m) => m.id === id) || null;
|
||||
}
|
||||
|
||||
async function setIoModuleConnected(id, connect) {
|
||||
const path = connect ? "connect" : "disconnect";
|
||||
const res = await fetch(`/api/io_modules/${encodeURIComponent(id)}/${path}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || res.statusText);
|
||||
}
|
||||
const data = await res.json();
|
||||
const idx = store.ioModules.findIndex((m) => m.id === id);
|
||||
if (idx >= 0) store.ioModules[idx] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
function renderIoConnectWidget(widget, bodyEl) {
|
||||
if (!bodyEl) return;
|
||||
const mod = ioModuleById(widget.io_module_id);
|
||||
const connected = !!mod?.connected;
|
||||
const label = widget.title || mod?.name || t("dashboard.widget.io_connect");
|
||||
bodyEl.innerHTML = `
|
||||
<button type="button" class="dashboardMirMissionBtn dashboardIoConnectBtn" data-io-connect="${escapeHtml(widget.io_module_id || "")}">
|
||||
${escapeHtml(connected ? t("dashboard.widget.io_disconnect") : t("dashboard.widget.io_connectAction"))}
|
||||
</button>
|
||||
<p class="mutedNote dashboardIoWidgetMeta">${escapeHtml(label)} · ${escapeHtml(mod ? (mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected")) : "—")}</p>`;
|
||||
const btn = bodyEl.querySelector("[data-io-connect]");
|
||||
btn?.addEventListener("click", async () => {
|
||||
if (!widget.io_module_id) return;
|
||||
try {
|
||||
await setIoModuleConnected(widget.io_module_id, !connected);
|
||||
renderIoConnectWidget(widget, bodyEl);
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderIoStatusWidget(widget, bodyEl) {
|
||||
if (!bodyEl) return;
|
||||
const mod = ioModuleById(widget.io_module_id);
|
||||
const label = widget.title || mod?.name || t("dashboard.widget.io_status");
|
||||
bodyEl.innerHTML = `
|
||||
<div class="dashboardIoStatusCard">
|
||||
<div class="dashboardIoStatusName">${escapeHtml(label)}</div>
|
||||
<div class="dashboardIoStatusLine">${escapeHtml(mod?.module_type ? t(`ioModules.type.${mod.module_type}`) : "—")} · ${escapeHtml(mod?.ip_address || "—")}</div>
|
||||
<div class="dashboardIoStatusBadge ${mod?.connected ? "is-on" : "is-off"}">${escapeHtml(mod ? (mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected")) : "—")}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function fetchIoRuntime(moduleId) {
|
||||
const res = await fetch(`/api/io_modules/${encodeURIComponent(moduleId)}/runtime`, { credentials: "include" });
|
||||
if (!res.ok) throw new Error(res.statusText);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function setIoOutput(moduleId, port, value, timeoutMs = 0) {
|
||||
const res = await fetch(`/api/io_modules/${encodeURIComponent(moduleId)}/output`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ output: port, value, timeout_ms: timeoutMs }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || res.statusText);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function ioPortRange(mod) {
|
||||
return mod?.module_type === "wise" ? [0, 1, 2, 3] : [1, 2, 3, 4];
|
||||
}
|
||||
|
||||
function renderIoConfigurationWidget(widget, bodyEl) {
|
||||
if (!bodyEl) return;
|
||||
const mod = ioModuleById(widget.io_module_id);
|
||||
const label = widget.title || mod?.name || t("dashboard.widget.io_configuration");
|
||||
if (!mod) {
|
||||
bodyEl.innerHTML = `<p class="mutedNote">${escapeHtml(t("dashboard.widget.configHint"))}</p>`;
|
||||
return;
|
||||
}
|
||||
const ports = ioPortRange(mod);
|
||||
bodyEl.innerHTML = `
|
||||
<div class="dashboardIoConfigHead">
|
||||
<div class="dashboardIoStatusName">${escapeHtml(label)}</div>
|
||||
<div class="dashboardIoStatusBadge ${mod.connected ? "is-on" : "is-off"}">${escapeHtml(mod.connected ? t("ioModules.status.connected") : t("ioModules.status.disconnected"))}</div>
|
||||
</div>
|
||||
<div class="dashboardIoConfigPorts" data-io-config-ports></div>`;
|
||||
const portsEl = bodyEl.querySelector("[data-io-config-ports]");
|
||||
ports.forEach((port) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "dashboardIoConfigRow";
|
||||
row.innerHTML = `
|
||||
<span class="dashboardIoConfigPortLabel">${escapeHtml(t("dashboard.widget.ioPort", { port }))}</span>
|
||||
<button type="button" class="dashboardIoConfigToggle" data-port="${port}" data-value="off">${escapeHtml(t("dashboard.widget.ioOff"))}</button>
|
||||
<button type="button" class="dashboardIoConfigToggle is-primary" data-port="${port}" data-value="on">${escapeHtml(t("dashboard.widget.ioOn"))}</button>`;
|
||||
portsEl.appendChild(row);
|
||||
});
|
||||
const refresh = async () => {
|
||||
try {
|
||||
const rt = await fetchIoRuntime(mod.id);
|
||||
portsEl.querySelectorAll("[data-port]").forEach((btn) => {
|
||||
const port = Number(btn.dataset.port);
|
||||
const outputs = Array.isArray(rt.outputs) ? rt.outputs : [];
|
||||
const idx = mod.module_type === "wise" ? port : port;
|
||||
const on = !!outputs[idx];
|
||||
btn.classList.toggle("is-active", (btn.dataset.value === "on") === on);
|
||||
});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
portsEl.addEventListener("click", async (evt) => {
|
||||
const btn = evt.target.closest("[data-port]");
|
||||
if (!btn || !widget.io_module_id) return;
|
||||
const port = Number(btn.dataset.port);
|
||||
const on = btn.dataset.value === "on";
|
||||
try {
|
||||
await setIoOutput(widget.io_module_id, port, on);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
});
|
||||
void refresh();
|
||||
}
|
||||
|
||||
function widgetTypeLabel(type) {
|
||||
return t(`dashboard.widget.${type}`) || type;
|
||||
}
|
||||
@@ -986,6 +1148,16 @@
|
||||
<label>${t("dashboard.widget.field.title")}</label>
|
||||
<input data-field="title" type="text" value="${escapeHtml(widget.title || t("dashboard.widget.robot_summary"))}" />
|
||||
</div>`;
|
||||
} else if (type === "io_connect" || type === "io_status" || type === "io_configuration") {
|
||||
container.innerHTML = `
|
||||
<div class="row rowWide">
|
||||
<label>${t("dashboard.widget.field.ioModule")}</label>
|
||||
<select data-field="io_module_id">${ioModuleOptions(widget.io_module_id || "")}</select>
|
||||
</div>
|
||||
<div class="row rowWide">
|
||||
<label>${t("dashboard.widget.field.title")}</label>
|
||||
<input data-field="title" type="text" value="${escapeHtml(widget.title || "")}" placeholder="${escapeHtml(widgetTypeLabel(type))}" />
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1209,6 +1381,15 @@
|
||||
case "robot_summary":
|
||||
renderRobotSummaryWidget(widget, bodyEl);
|
||||
break;
|
||||
case "io_connect":
|
||||
renderIoConnectWidget(widget, bodyEl);
|
||||
break;
|
||||
case "io_status":
|
||||
renderIoStatusWidget(widget, bodyEl);
|
||||
break;
|
||||
case "io_configuration":
|
||||
renderIoConfigurationWidget(widget, bodyEl);
|
||||
break;
|
||||
default:
|
||||
bodyEl.innerHTML = `<p class="mutedNote">${t("dashboard.widget.unsupported")}</p>`;
|
||||
}
|
||||
@@ -1245,6 +1426,9 @@
|
||||
if (widget.type === "mission_queue") refreshQueueWidget(bodyEl);
|
||||
if (widget.type === "pause_continue") renderPauseContinueWidget(widget, bodyEl);
|
||||
if (widget.type === "mission_action_log") renderMissionActionLogWidget(widget, bodyEl);
|
||||
if (widget.type === "io_connect") renderIoConnectWidget(widget, bodyEl);
|
||||
if (widget.type === "io_status") renderIoStatusWidget(widget, bodyEl);
|
||||
if (widget.type === "io_configuration") renderIoConfigurationWidget(widget, bodyEl);
|
||||
});
|
||||
if (hasMapWidget()) refreshMapWidgets();
|
||||
}
|
||||
@@ -1260,6 +1444,8 @@
|
||||
};
|
||||
if (widget.type === "map" || widget.type === "map_locked") {
|
||||
void ensureMapsLoaded().then(open);
|
||||
} else if (widget.type === "io_connect" || widget.type === "io_status" || widget.type === "io_configuration") {
|
||||
void ensureIoModulesLoaded().then(open);
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
@@ -1484,6 +1670,7 @@
|
||||
async function init() {
|
||||
await loadStoreFromBackend();
|
||||
await loadUserGroups();
|
||||
await ensureIoModulesLoaded();
|
||||
bindEvents();
|
||||
setView("list");
|
||||
}
|
||||
@@ -1493,6 +1680,7 @@
|
||||
getNavItems,
|
||||
handleNav,
|
||||
onPageShow() {
|
||||
void ensureIoModulesLoaded();
|
||||
if (store.view === "designer") {
|
||||
syncDesignerEditMode();
|
||||
renderDesignerChrome();
|
||||
|
||||
Reference in New Issue
Block a user