This commit is contained in:
156
www/missions.js
156
www/missions.js
@@ -6,6 +6,7 @@
|
||||
{ type: "move_to_position", label: "Go to position" },
|
||||
{ type: "move_to_marker", label: "Go to marker" },
|
||||
{ type: "adjust_localization", label: "Adjust localization" },
|
||||
{ type: "switch_map", label: "Switch map" },
|
||||
{ type: "wait", label: "Wait" },
|
||||
{ type: "set_speed", label: "Set speed" },
|
||||
],
|
||||
@@ -38,6 +39,60 @@
|
||||
const SAMPLE_IO_MODULES = ["GPIO module 1", "PLC I/O 1"];
|
||||
const SAMPLE_CARTS = ["Any valid cart", "Cart A", "Cart B"];
|
||||
|
||||
let missionPositionCatalog = [];
|
||||
|
||||
function buildPositionCatalogFromMaps(maps) {
|
||||
const out = [];
|
||||
(maps || []).forEach((map) => {
|
||||
if (!map?.id) return;
|
||||
const zones = Array.isArray(map.zones) ? map.zones : [];
|
||||
zones
|
||||
.filter((z) => z && z.type === "position" && typeof z.id === "string" && z.id)
|
||||
.forEach((z) => {
|
||||
const mapName = map.name || map.id;
|
||||
const pname = z.name || z.id;
|
||||
out.push({
|
||||
position_id: z.id,
|
||||
map_id: map.id,
|
||||
name: pname,
|
||||
label: `${mapName} / ${pname}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function positionOptions() {
|
||||
return missionPositionCatalog.map((p) => ({ value: p.position_id, label: p.label }));
|
||||
}
|
||||
|
||||
function positionIds() {
|
||||
return missionPositionCatalog.map((p) => p.position_id);
|
||||
}
|
||||
|
||||
function positionLabel(ref) {
|
||||
if (!ref) return "—";
|
||||
const hit = missionPositionCatalog.find((p) => p.position_id === ref || p.name === ref);
|
||||
return hit?.label || ref;
|
||||
}
|
||||
|
||||
function positionSelectOptions() {
|
||||
const opts = positionOptions();
|
||||
if (opts.length) return opts;
|
||||
return SAMPLE_POSITIONS.map((n) => ({ value: n, label: n }));
|
||||
}
|
||||
|
||||
function syncPositionFieldDefs() {
|
||||
const ids = positionIds();
|
||||
["move_to_position", "adjust_localization", "if", "pick_cart", "drop_cart"].forEach((type) => {
|
||||
const defs = VARIABLE_FIELD_DEFS[type];
|
||||
if (!defs) return;
|
||||
defs.forEach((def) => {
|
||||
if (def.key === "position") def.options = ids.length ? ids : SAMPLE_POSITIONS;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const el = (id) => document.getElementById(id);
|
||||
const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
|
||||
|
||||
@@ -75,6 +130,7 @@
|
||||
{ key: "cart", label: "Cart", options: SAMPLE_CARTS },
|
||||
],
|
||||
drop_cart: [{ key: "position", label: "Position", options: SAMPLE_POSITIONS }],
|
||||
switch_map: [{ key: "map_id", label: "Map", options: [] }],
|
||||
};
|
||||
|
||||
const store = {
|
||||
@@ -102,17 +158,19 @@
|
||||
function defaultParams(type) {
|
||||
switch (type) {
|
||||
case "move_to_position":
|
||||
return { position: SAMPLE_POSITIONS[0], check_free: true };
|
||||
return { position: positionIds()[0] || SAMPLE_POSITIONS[0], check_free: true };
|
||||
case "move_to_marker":
|
||||
return { marker: SAMPLE_MARKERS[0] };
|
||||
case "adjust_localization":
|
||||
return { position: SAMPLE_POSITIONS[0] };
|
||||
return { position: positionIds()[0] || SAMPLE_POSITIONS[0] };
|
||||
case "wait":
|
||||
return { seconds: 1 };
|
||||
case "set_speed":
|
||||
return { speed: "normal" };
|
||||
case "switch_map":
|
||||
return { map_id: "", entry_position_id: "" };
|
||||
case "if":
|
||||
return { condition: "position_free", position: SAMPLE_POSITIONS[0] };
|
||||
return { condition: "position_free", position: positionIds()[0] || SAMPLE_POSITIONS[0] };
|
||||
case "loop":
|
||||
return { count: 1, mode: "count" };
|
||||
case "set_digital_output":
|
||||
@@ -122,13 +180,13 @@
|
||||
case "set_plc_register":
|
||||
return { register: 1, action: "set", value: 0 };
|
||||
case "pick_cart":
|
||||
return { position: SAMPLE_POSITIONS[0], cart: SAMPLE_CARTS[0] };
|
||||
return { position: positionIds()[0] || SAMPLE_POSITIONS[0], cart: SAMPLE_CARTS[0] };
|
||||
case "drop_cart":
|
||||
return { position: SAMPLE_POSITIONS[0], collision_check: true };
|
||||
return { position: positionIds()[0] || SAMPLE_POSITIONS[0], collision_check: true };
|
||||
case "user_log":
|
||||
return { message: "Mission step" };
|
||||
case "play_sound":
|
||||
return { sound: "beep" };
|
||||
return { sound: "sys_beep", volume: 100 };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -277,7 +335,7 @@
|
||||
const fmtVar = (key, val) => (p[`${key}_var`] ? `${val} (biến)` : val);
|
||||
switch (action.type) {
|
||||
case "move_to_position":
|
||||
return `Position: ${fmtVar("position", p.position)}${p.check_free ? " • kiểm tra trống" : ""}`;
|
||||
return `Position: ${fmtVar("position", positionLabel(p.position))}${p.check_free ? " • kiểm tra trống" : ""}`;
|
||||
case "move_to_marker":
|
||||
return `Marker: ${fmtVar("marker", p.marker)}`;
|
||||
case "wait":
|
||||
@@ -287,7 +345,7 @@
|
||||
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":
|
||||
return `If ${p.condition} @ ${p.position || "—"}`;
|
||||
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":
|
||||
@@ -296,11 +354,18 @@
|
||||
return `Reg ${p.register}: ${p.action} ${p.value}`;
|
||||
case "pick_cart":
|
||||
case "drop_cart":
|
||||
return `${action.type === "pick_cart" ? "Pick" : "Drop"} @ ${fmtVar("position", p.position)}${action.type === "pick_cart" ? ` • ${fmtVar("cart", p.cart)}` : ""}`;
|
||||
return `${action.type === "pick_cart" ? "Pick" : "Drop"} @ ${fmtVar("position", positionLabel(p.position))}${action.type === "pick_cart" ? ` • ${fmtVar("cart", p.cart)}` : ""}`;
|
||||
case "user_log":
|
||||
return p.message || "—";
|
||||
case "play_sound":
|
||||
return p.sound || "—";
|
||||
case "switch_map":
|
||||
return `${p.map_id || "—"}`;
|
||||
case "play_sound": {
|
||||
const soundName = (() => {
|
||||
const s = (window.SoundsApp?.getSounds?.() || []).find((x) => x.id === p.sound);
|
||||
return s?.name || p.sound || "—";
|
||||
})();
|
||||
return `${soundName} · ${p.volume != null ? p.volume : 100}%`;
|
||||
}
|
||||
default:
|
||||
return action.label;
|
||||
}
|
||||
@@ -679,9 +744,11 @@
|
||||
sel.dataset.varKey = v.key;
|
||||
v.options.forEach((opt) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = opt;
|
||||
o.textContent = opt;
|
||||
if (opt === v.default) o.selected = true;
|
||||
const value = opt?.value ?? opt;
|
||||
const label = opt?.label ?? positionLabel(value);
|
||||
o.value = value;
|
||||
o.textContent = label;
|
||||
if (value === v.default) o.selected = true;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
row.appendChild(lab);
|
||||
@@ -1170,6 +1237,19 @@
|
||||
return select;
|
||||
};
|
||||
|
||||
const selectInputLabeled = (key, value, options) => {
|
||||
const select = document.createElement("select");
|
||||
select.dataset.param = key;
|
||||
options.forEach((opt) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = opt.value;
|
||||
o.textContent = opt.label;
|
||||
if (opt.value === value) o.selected = true;
|
||||
select.appendChild(o);
|
||||
});
|
||||
return select;
|
||||
};
|
||||
|
||||
const addVariableToggle = (paramKey, fieldLabel) => {
|
||||
const chk = document.createElement("label");
|
||||
chk.innerHTML = `<input type="checkbox" data-param="${paramKey}_var" ${p[`${paramKey}_var`] ? "checked" : ""} /> Biến — hỏi khi thêm vào queue`;
|
||||
@@ -1179,7 +1259,7 @@
|
||||
switch (action.type) {
|
||||
case "move_to_position":
|
||||
case "adjust_localization":
|
||||
addField("Position", selectInput("position", p.position, SAMPLE_POSITIONS));
|
||||
addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
|
||||
addVariableToggle("position", "Position");
|
||||
if (action.type === "move_to_position") {
|
||||
const chk = document.createElement("label");
|
||||
@@ -1203,7 +1283,7 @@
|
||||
break;
|
||||
case "if":
|
||||
addField("Điều kiện", selectInput("condition", p.condition, ["position_free", "position_occupied", "register_equals"]));
|
||||
addField("Position", selectInput("position", p.position, SAMPLE_POSITIONS));
|
||||
addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
|
||||
addVariableToggle("position", "Position");
|
||||
break;
|
||||
case "set_digital_output":
|
||||
@@ -1231,13 +1311,13 @@
|
||||
addField("Giá trị", textInput("value", p.value, "number"));
|
||||
break;
|
||||
case "pick_cart":
|
||||
addField("Position", selectInput("position", p.position, SAMPLE_POSITIONS));
|
||||
addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
|
||||
addVariableToggle("position", "Position");
|
||||
addField("Cart", selectInput("cart", p.cart, SAMPLE_CARTS));
|
||||
addVariableToggle("cart", "Cart");
|
||||
break;
|
||||
case "drop_cart":
|
||||
addField("Position", selectInput("position", p.position, SAMPLE_POSITIONS));
|
||||
addField("Position", selectInputLabeled("position", p.position, positionSelectOptions()));
|
||||
addVariableToggle("position", "Position");
|
||||
{
|
||||
const chk = document.createElement("label");
|
||||
@@ -1248,9 +1328,25 @@
|
||||
case "user_log":
|
||||
addField("Message", textInput("message", p.message));
|
||||
break;
|
||||
case "play_sound":
|
||||
addField("Sound", selectInput("sound", p.sound, ["beep", "horn", "chime"]));
|
||||
case "play_sound": {
|
||||
const catalog = (window.SoundsApp?.getSounds?.() || []).filter((s) => s.enabled !== false);
|
||||
const options = catalog.length
|
||||
? catalog.map((s) => ({ value: s.id, label: s.name }))
|
||||
: [
|
||||
{ value: "sys_beep", label: "Beep" },
|
||||
{ value: "sys_horn", label: "Horn" },
|
||||
{ value: "sys_chime", label: "Chime" },
|
||||
];
|
||||
addField("Sound", selectInputLabeled("sound", p.sound || options[0].value, options));
|
||||
addField("Volume (0–100)", textInput("volume", p.volume != null ? p.volume : 100, "number"));
|
||||
break;
|
||||
}
|
||||
case "switch_map": {
|
||||
const options = (VARIABLE_FIELD_DEFS.switch_map?.[0]?.options || []).map((id) => ({ value: id, label: id }));
|
||||
addField("Map", selectInputLabeled("map_id", p.map_id || options[0]?.value || "", options));
|
||||
addField("Entry position id", textInput("entry_position_id", p.entry_position_id || ""));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
grid.innerHTML = `<p class="mutedNote">Action này không có tham số cấu hình.</p>`;
|
||||
}
|
||||
@@ -1361,6 +1457,26 @@
|
||||
|
||||
async function init() {
|
||||
loadStore();
|
||||
if (window.SoundsApp?.refreshSounds) {
|
||||
try {
|
||||
await window.SoundsApp.refreshSounds();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
try {
|
||||
const maps = await fetch("/api/maps", { credentials: "include" });
|
||||
if (maps.ok) {
|
||||
const data = await maps.json();
|
||||
const list = Array.isArray(data.maps) ? data.maps : [];
|
||||
missionPositionCatalog = buildPositionCatalogFromMaps(list);
|
||||
syncPositionFieldDefs();
|
||||
const ids = list.map((m) => m.id).filter(Boolean);
|
||||
VARIABLE_FIELD_DEFS.switch_map[0].options = ids;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
await loadStoreFromBackend();
|
||||
bindEvents();
|
||||
renderMissionList();
|
||||
|
||||
Reference in New Issue
Block a user