This commit is contained in:
208
www/missions.js
208
www/missions.js
@@ -12,6 +12,7 @@
|
||||
],
|
||||
Logic: [
|
||||
{ type: "if", label: "If" },
|
||||
{ type: "while", label: "While", isLoop: true },
|
||||
{ type: "loop", label: "Loop", isLoop: true },
|
||||
{ type: "break", label: "Break" },
|
||||
{ type: "continue", label: "Continue" },
|
||||
@@ -20,6 +21,8 @@
|
||||
"I/O": [
|
||||
{ type: "set_digital_output", label: "Set digital output" },
|
||||
{ type: "wait_digital_input", label: "Wait for digital input" },
|
||||
{ type: "connect_bluetooth", label: "Connect Bluetooth module" },
|
||||
{ type: "disconnect_bluetooth", label: "Disconnect Bluetooth module" },
|
||||
{ type: "set_plc_register", label: "Set PLC register" },
|
||||
],
|
||||
Cart: [
|
||||
@@ -37,6 +40,42 @@
|
||||
const SAMPLE_POSITIONS = ["Charging station", "Warehouse", "Production line 1", "Dock A"];
|
||||
const SAMPLE_MARKERS = ["Marker 1", "Marker 2", "Home"];
|
||||
const SAMPLE_IO_MODULES = ["GPIO module 1", "PLC I/O 1"];
|
||||
let ioModuleCatalog = [];
|
||||
|
||||
function ioModuleNames() {
|
||||
const names = ioModuleCatalog.map((m) => m.name).filter(Boolean);
|
||||
return names.length ? names : SAMPLE_IO_MODULES;
|
||||
}
|
||||
|
||||
function ioModuleByName(name) {
|
||||
return ioModuleCatalog.find((m) => m.name === name || m.id === name) || null;
|
||||
}
|
||||
|
||||
function ioOutputPorts(moduleName) {
|
||||
const mod = ioModuleByName(moduleName);
|
||||
return mod?.module_type === "wise" ? [0, 1, 2, 3] : [1, 2, 3, 4];
|
||||
}
|
||||
|
||||
function ioInputPorts(moduleName) {
|
||||
return ioOutputPorts(moduleName);
|
||||
}
|
||||
|
||||
async function loadIoModuleCatalog() {
|
||||
try {
|
||||
if (window.IoModulesCatalog?.refresh) {
|
||||
ioModuleCatalog = await window.IoModulesCatalog.refresh();
|
||||
} else {
|
||||
const res = await fetch("/api/io_modules", { credentials: "include" });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
ioModuleCatalog = Array.isArray(data.io_modules) ? data.io_modules : [];
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
ioModuleCatalog = [];
|
||||
}
|
||||
return ioModuleCatalog;
|
||||
}
|
||||
const SAMPLE_CARTS = ["Any valid cart", "Cart A", "Cart B"];
|
||||
|
||||
let missionPositionCatalog = [];
|
||||
@@ -170,13 +209,18 @@
|
||||
case "switch_map":
|
||||
return { map_id: "", entry_position_id: "" };
|
||||
case "if":
|
||||
return { condition: "position_free", position: positionIds()[0] || SAMPLE_POSITIONS[0] };
|
||||
return { condition: "io_input", module: ioModuleNames()[0] || "", input: 1, expected: true };
|
||||
case "while":
|
||||
return { condition: "io_input", module: ioModuleNames()[0] || "", input: 1, expected: true };
|
||||
case "loop":
|
||||
return { count: 1, mode: "count" };
|
||||
case "set_digital_output":
|
||||
return { module: SAMPLE_IO_MODULES[0], pin: 1, value: true };
|
||||
return { module: ioModuleNames()[0] || "", output: 1, value: true, operation: "set", timeout_ms: 0 };
|
||||
case "wait_digital_input":
|
||||
return { module: SAMPLE_IO_MODULES[0], pin: 1, expected: true, timeout_s: 30 };
|
||||
return { module: ioModuleNames()[0] || "", input: 1, expected: true, timeout_s: 30 };
|
||||
case "connect_bluetooth":
|
||||
case "disconnect_bluetooth":
|
||||
return { module: ioModuleNames()[0] || "" };
|
||||
case "set_plc_register":
|
||||
return { register: 1, action: "set", value: 0 };
|
||||
case "pick_cart":
|
||||
@@ -344,12 +388,28 @@
|
||||
return `Speed: ${p.speed}`;
|
||||
case "loop":
|
||||
return p.mode === "endless" ? "Lặp vô hạn" : `Lặp ${p.count} lần • ${action.children?.length || 0} bước`;
|
||||
case "if":
|
||||
case "if": {
|
||||
if (p.condition === "io_input") {
|
||||
const port = p.input != null ? p.input : p.pin;
|
||||
return `If I/O ${p.module} in ${port} = ${p.expected ? "ON" : "OFF"}`;
|
||||
}
|
||||
return `If ${p.condition} @ ${positionLabel(p.position)}`;
|
||||
case "set_digital_output":
|
||||
return `${p.module} pin ${p.pin} → ${p.value ? "ON" : "OFF"}`;
|
||||
case "wait_digital_input":
|
||||
return `${p.module} pin ${p.pin} = ${p.expected ? "ON" : "OFF"}`;
|
||||
}
|
||||
case "while":
|
||||
return `While I/O ${p.module} in ${p.input != null ? p.input : p.pin} = ${p.expected ? "ON" : "OFF"} • ${action.children?.length || 0} bước`;
|
||||
case "set_digital_output": {
|
||||
const port = p.output != null ? p.output : p.pin;
|
||||
const op = p.operation === "timed" ? ` (${p.timeout_ms || 0}ms)` : "";
|
||||
return `${p.module} out ${port} → ${p.value ? "ON" : "OFF"}${op}`;
|
||||
}
|
||||
case "wait_digital_input": {
|
||||
const port = p.input != null ? p.input : p.pin;
|
||||
return `${p.module} in ${port} = ${p.expected ? "ON" : "OFF"} (${p.timeout_s}s)`;
|
||||
}
|
||||
case "connect_bluetooth":
|
||||
return `Connect ${p.module || "?"}`;
|
||||
case "disconnect_bluetooth":
|
||||
return `Disconnect ${p.module || "?"}`;
|
||||
case "set_plc_register":
|
||||
return `Reg ${p.register}: ${p.action} ${p.value}`;
|
||||
case "pick_cart":
|
||||
@@ -374,10 +434,15 @@
|
||||
function normalizeActionTree(actions) {
|
||||
if (!Array.isArray(actions)) return;
|
||||
actions.forEach((action) => {
|
||||
if (action.type === "loop" && !Array.isArray(action.children)) {
|
||||
if ((action.type === "loop" || action.type === "while") && !Array.isArray(action.children)) {
|
||||
action.children = [];
|
||||
}
|
||||
if (action.type === "if") {
|
||||
if (!Array.isArray(action.children)) action.children = [];
|
||||
if (!Array.isArray(action.else_children)) action.else_children = [];
|
||||
}
|
||||
if (Array.isArray(action.children)) normalizeActionTree(action.children);
|
||||
if (Array.isArray(action.else_children)) normalizeActionTree(action.else_children);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -387,9 +452,24 @@
|
||||
if (path === "root") return draft.actions;
|
||||
const parts = path.split(".").filter((p) => p !== "root");
|
||||
let list = draft.actions;
|
||||
for (const part of parts) {
|
||||
let i = 0;
|
||||
while (i < parts.length) {
|
||||
const part = parts[i];
|
||||
if (part === "else") return null;
|
||||
const node = list.find((a) => a.id === part);
|
||||
if (!node || !Array.isArray(node.children)) return null;
|
||||
if (!node) return null;
|
||||
i += 1;
|
||||
if (i < parts.length && parts[i] === "else") {
|
||||
if (!Array.isArray(node.else_children)) return null;
|
||||
list = node.else_children;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (i >= parts.length) {
|
||||
if (!Array.isArray(node.children)) return null;
|
||||
return node.children;
|
||||
}
|
||||
if (!Array.isArray(node.children)) return null;
|
||||
list = node.children;
|
||||
}
|
||||
return list;
|
||||
@@ -402,7 +482,11 @@
|
||||
if (action.id === actionId) return { action, list, index: i, path, parent };
|
||||
if (Array.isArray(action.children)) {
|
||||
const hit = findActionWithParent(actionId, action.children, `${path}.${action.id}`, action);
|
||||
if (hit) return { ...hit, label: t(`missions.action.${type}`) || hit.label };
|
||||
if (hit) return hit;
|
||||
}
|
||||
if (Array.isArray(action.else_children)) {
|
||||
const hit = findActionWithParent(actionId, action.else_children, `${path}.${action.id}.else`, action);
|
||||
if (hit) return hit;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -492,6 +576,7 @@
|
||||
actions.forEach((action) => {
|
||||
visit(action, depth);
|
||||
if (Array.isArray(action.children)) walkActions(action.children, visit, depth + 1);
|
||||
if (Array.isArray(action.else_children)) walkActions(action.else_children, visit, depth + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -504,6 +589,9 @@
|
||||
if (Array.isArray(copy.children)) {
|
||||
copy.children = copy.children.map((child) => resolveActionSnapshot(child, depth));
|
||||
}
|
||||
if (Array.isArray(copy.else_children)) {
|
||||
copy.else_children = copy.else_children.map((child) => resolveActionSnapshot(child, depth));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -949,8 +1037,8 @@
|
||||
row.dataset.index = String(index);
|
||||
|
||||
const iconClass =
|
||||
action.kind === "mission" ? "kind-mission" : action.type === "loop" ? "kind-loop" : "";
|
||||
const iconChar = action.kind === "mission" ? "◎" : action.type === "loop" ? "↻" : "▶";
|
||||
action.kind === "mission" ? "kind-mission" : action.type === "loop" || action.type === "while" ? "kind-loop" : action.type === "if" ? "kind-if" : "";
|
||||
const iconChar = action.kind === "mission" ? "◎" : action.type === "loop" || action.type === "while" ? "↻" : action.type === "if" ? "?" : "▶";
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="missionDragHandle" draggable="true" title="Kéo để sắp xếp" aria-label="Kéo để sắp xếp">↕</div>
|
||||
@@ -968,17 +1056,17 @@
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (action.type === "loop" && Array.isArray(action.children)) {
|
||||
if ((action.type === "loop" || action.type === "while") && Array.isArray(action.children)) {
|
||||
const loop = document.createElement("div");
|
||||
loop.className = "missionLoopBlock";
|
||||
loop.innerHTML = `<div class="missionLoopLabel">Loop body — kéo action vào đây</div>`;
|
||||
loop.innerHTML = `<div class="missionLoopLabel">${action.type === "while" ? "While body" : "Loop body"} — kéo action vào đây</div>`;
|
||||
const drop = document.createElement("div");
|
||||
drop.className = "missionLoopDrop";
|
||||
drop.dataset.loopPath = `${listPath}.${action.id}`;
|
||||
if (!action.children.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "missionLoopEmpty";
|
||||
empty.textContent = "Kéo action hoặc mission vào loop";
|
||||
empty.textContent = "Kéo action hoặc mission vào đây";
|
||||
drop.appendChild(empty);
|
||||
} else {
|
||||
renderActionRows(action.children, `${listPath}.${action.id}`, drop);
|
||||
@@ -987,6 +1075,31 @@
|
||||
row.appendChild(loop);
|
||||
}
|
||||
|
||||
if (action.type === "if" && Array.isArray(action.children)) {
|
||||
const makeBranch = (label, branchPath, children) => {
|
||||
const block = document.createElement("div");
|
||||
block.className = "missionLoopBlock missionIfBlock";
|
||||
block.innerHTML = `<div class="missionLoopLabel">${label}</div>`;
|
||||
const drop = document.createElement("div");
|
||||
drop.className = "missionLoopDrop";
|
||||
drop.dataset.loopPath = branchPath;
|
||||
if (!children.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "missionLoopEmpty";
|
||||
empty.textContent = "Kéo action vào nhánh này";
|
||||
drop.appendChild(empty);
|
||||
} else {
|
||||
renderActionRows(children, branchPath, drop);
|
||||
}
|
||||
block.appendChild(drop);
|
||||
return block;
|
||||
};
|
||||
row.appendChild(makeBranch("Then", `${listPath}.${action.id}`, action.children));
|
||||
row.appendChild(
|
||||
makeBranch("Else", `${listPath}.${action.id}.else`, Array.isArray(action.else_children) ? action.else_children : [])
|
||||
);
|
||||
}
|
||||
|
||||
row.querySelector("[data-config]").addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
openActionConfig(action.id);
|
||||
@@ -1282,13 +1395,35 @@
|
||||
addField("Số lần lặp", textInput("count", p.count, "number"));
|
||||
break;
|
||||
case "if":
|
||||
addField("Điều kiện", selectInput("condition", p.condition, ["position_free", "position_occupied", "register_equals"]));
|
||||
addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
|
||||
addVariableToggle("position", "Position");
|
||||
addField("Điều kiện", selectInput("condition", p.condition, ["io_input", "position_free", "position_occupied", "register_equals"]));
|
||||
if (p.condition === "io_input") {
|
||||
addField("Module", selectInput("module", p.module, ioModuleNames()));
|
||||
addField("Input", selectInput("input", String(p.input != null ? p.input : p.pin || 1), ioInputPorts(p.module).map(String)));
|
||||
{
|
||||
const chk = document.createElement("label");
|
||||
chk.innerHTML = `<input type="checkbox" data-param="expected" ${p.expected ? "checked" : ""} /> ${t("missions.action.waitOnLevel")}`;
|
||||
addField("Kỳ vọng", chk);
|
||||
}
|
||||
} else {
|
||||
addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
|
||||
addVariableToggle("position", "Position");
|
||||
}
|
||||
break;
|
||||
case "while":
|
||||
addField("Điều kiện", selectInput("condition", p.condition || "io_input", ["io_input"]));
|
||||
addField("Module", selectInput("module", p.module, ioModuleNames()));
|
||||
addField("Input", selectInput("input", String(p.input != null ? p.input : p.pin || 1), ioInputPorts(p.module).map(String)));
|
||||
{
|
||||
const chk = document.createElement("label");
|
||||
chk.innerHTML = `<input type="checkbox" data-param="expected" ${p.expected ? "checked" : ""} /> ${t("missions.action.waitOnLevel")}`;
|
||||
addField("Kỳ vọng", chk);
|
||||
}
|
||||
break;
|
||||
case "set_digital_output":
|
||||
addField("Module", selectInput("module", p.module, SAMPLE_IO_MODULES));
|
||||
addField("Pin", textInput("pin", p.pin, "number"));
|
||||
addField("Module", selectInput("module", p.module, ioModuleNames()));
|
||||
addField("Output", selectInput("output", String(p.output != null ? p.output : p.pin || 1), ioOutputPorts(p.module).map(String)));
|
||||
addField("Operation", selectInput("operation", p.operation || "set", ["set", "timed"]));
|
||||
addField("Timeout (ms)", textInput("timeout_ms", p.timeout_ms != null ? p.timeout_ms : 0, "number"));
|
||||
{
|
||||
const chk = document.createElement("label");
|
||||
chk.innerHTML = `<input type="checkbox" data-param="value" ${p.value ? "checked" : ""} /> Bật (ON)`;
|
||||
@@ -1296,8 +1431,8 @@
|
||||
}
|
||||
break;
|
||||
case "wait_digital_input":
|
||||
addField("Module", selectInput("module", p.module, SAMPLE_IO_MODULES));
|
||||
addField("Pin", textInput("pin", p.pin, "number"));
|
||||
addField("Module", selectInput("module", p.module, ioModuleNames()));
|
||||
addField("Input", selectInput("input", String(p.input != null ? p.input : p.pin || 1), ioInputPorts(p.module).map(String)));
|
||||
addField("Timeout (s)", textInput("timeout_s", p.timeout_s, "number"));
|
||||
{
|
||||
const chk = document.createElement("label");
|
||||
@@ -1305,6 +1440,10 @@
|
||||
addField("Kỳ vọng", chk);
|
||||
}
|
||||
break;
|
||||
case "connect_bluetooth":
|
||||
case "disconnect_bluetooth":
|
||||
addField("Module", selectInput("module", p.module, ioModuleNames()));
|
||||
break;
|
||||
case "set_plc_register":
|
||||
addField("Register", textInput("register", p.register, "number"));
|
||||
addField("Hành động", selectInput("action", p.action, ["set", "add", "subtract"]));
|
||||
@@ -1364,6 +1503,8 @@
|
||||
else params[key] = node.value;
|
||||
});
|
||||
hit.action.params = params;
|
||||
if (params.input != null && params.input !== "") params.input = Number(params.input);
|
||||
if (params.output != null && params.output !== "") params.output = Number(params.output);
|
||||
setDirty(true);
|
||||
renderMissionEditor();
|
||||
}
|
||||
@@ -1464,6 +1605,7 @@
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
await loadIoModuleCatalog();
|
||||
try {
|
||||
const maps = await fetch("/api/maps", { credentials: "include" });
|
||||
if (maps.ok) {
|
||||
@@ -1503,11 +1645,13 @@
|
||||
startQueuePoll,
|
||||
stopQueuePoll,
|
||||
onPageShow() {
|
||||
if (!missionEditorViewEl?.hidden) renderMissionEditor();
|
||||
else {
|
||||
renderMissionList();
|
||||
startQueuePoll();
|
||||
}
|
||||
void loadIoModuleCatalog().then(() => {
|
||||
if (!missionEditorViewEl?.hidden) renderMissionEditor();
|
||||
else {
|
||||
renderMissionList();
|
||||
startQueuePoll();
|
||||
}
|
||||
});
|
||||
},
|
||||
onPageHide() {
|
||||
stopQueuePoll();
|
||||
@@ -1527,6 +1671,12 @@
|
||||
}
|
||||
window.addEventListener("lm:locale-change", onLocaleChange);
|
||||
|
||||
window.addEventListener("lm:io-modules-changed", () => {
|
||||
void loadIoModuleCatalog().then(() => {
|
||||
if (!missionEditorViewEl?.hidden) renderMissionEditor();
|
||||
});
|
||||
});
|
||||
|
||||
if (window.AuthApp?.isReady()) boot();
|
||||
else window.addEventListener("lm:auth-ready", boot, { once: true });
|
||||
window.addEventListener("lm:auth-logout", stopQueuePollForce);
|
||||
|
||||
Reference in New Issue
Block a user