This commit is contained in:
@@ -1,33 +1,66 @@
|
||||
{
|
||||
"queue": [
|
||||
{
|
||||
"created_at": "2026-06-13T07:03:00Z",
|
||||
"finished_at": "2026-06-13T07:03:01Z",
|
||||
"id": "c636ad0d89937cd2",
|
||||
"created_at": "2026-06-13T07:19:31Z",
|
||||
"finished_at": "2026-06-13T07:19:32Z",
|
||||
"id": "3a520bd342883c75",
|
||||
"log": [
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Loop 1/1",
|
||||
"ts": "2026-06-13T07:19:31Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-13T07:19:31Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-13T07:03:00Z"
|
||||
"ts": "2026-06-13T07:19:31Z"
|
||||
}
|
||||
],
|
||||
"mission": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "a1",
|
||||
"children": [
|
||||
{
|
||||
"id": "c6c40563-0755-4e97-a48a-bb91ac8b0a9c",
|
||||
"kind": "action",
|
||||
"label": "Set PLC register",
|
||||
"params": {
|
||||
"action": "set",
|
||||
"register": 1,
|
||||
"value": 0
|
||||
},
|
||||
"type": "set_plc_register"
|
||||
},
|
||||
{
|
||||
"id": "a1",
|
||||
"kind": "action",
|
||||
"label": "Wait",
|
||||
"params": {
|
||||
"seconds": 1
|
||||
},
|
||||
"type": "wait"
|
||||
}
|
||||
],
|
||||
"id": "65f3cf0b-73fa-4f51-8774-1c5d4c83d8c4",
|
||||
"kind": "action",
|
||||
"label": "Wait",
|
||||
"label": "Loop",
|
||||
"params": {
|
||||
"seconds": 1
|
||||
"count": 1,
|
||||
"mode": "count"
|
||||
},
|
||||
"type": "wait"
|
||||
"type": "loop"
|
||||
}
|
||||
],
|
||||
"description": "",
|
||||
"group": "Missions",
|
||||
"id": "5ae9dbcb0722dffb",
|
||||
"name": "Test run",
|
||||
"updated_at": "2026-06-13T04:44:03Z"
|
||||
"updated_at": "2026-06-13T07:19:17.185Z"
|
||||
},
|
||||
"mission_group": "Missions",
|
||||
"mission_id": "5ae9dbcb0722dffb",
|
||||
@@ -36,37 +69,70 @@
|
||||
"priority": 0,
|
||||
"robot_id": "default",
|
||||
"source": "ui",
|
||||
"started_at": "2026-06-13T07:03:00Z",
|
||||
"started_at": "2026-06-13T07:19:31Z",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"created_at": "2026-06-13T07:03:01Z",
|
||||
"finished_at": "2026-06-13T07:03:02Z",
|
||||
"id": "06048341b549f0ac",
|
||||
"created_at": "2026-06-13T07:19:41Z",
|
||||
"finished_at": "2026-06-13T07:19:43Z",
|
||||
"id": "51de82d74cb4b1cd",
|
||||
"log": [
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Loop 1/1",
|
||||
"ts": "2026-06-13T07:19:41Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Set PLC register (set_plc_register) simulated",
|
||||
"ts": "2026-06-13T07:19:41Z"
|
||||
},
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-13T07:03:01Z"
|
||||
"ts": "2026-06-13T07:19:42Z"
|
||||
}
|
||||
],
|
||||
"mission": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "a1",
|
||||
"children": [
|
||||
{
|
||||
"id": "c6c40563-0755-4e97-a48a-bb91ac8b0a9c",
|
||||
"kind": "action",
|
||||
"label": "Set PLC register",
|
||||
"params": {
|
||||
"action": "set",
|
||||
"register": 1,
|
||||
"value": 0
|
||||
},
|
||||
"type": "set_plc_register"
|
||||
},
|
||||
{
|
||||
"id": "a1",
|
||||
"kind": "action",
|
||||
"label": "Wait",
|
||||
"params": {
|
||||
"seconds": 1
|
||||
},
|
||||
"type": "wait"
|
||||
}
|
||||
],
|
||||
"id": "65f3cf0b-73fa-4f51-8774-1c5d4c83d8c4",
|
||||
"kind": "action",
|
||||
"label": "Wait",
|
||||
"label": "Loop",
|
||||
"params": {
|
||||
"seconds": 1
|
||||
"count": 1,
|
||||
"mode": "count"
|
||||
},
|
||||
"type": "wait"
|
||||
"type": "loop"
|
||||
}
|
||||
],
|
||||
"description": "",
|
||||
"group": "Missions",
|
||||
"id": "5ae9dbcb0722dffb",
|
||||
"name": "Test run",
|
||||
"updated_at": "2026-06-13T04:44:03Z"
|
||||
"updated_at": "2026-06-13T07:19:17.185Z"
|
||||
},
|
||||
"mission_group": "Missions",
|
||||
"mission_id": "5ae9dbcb0722dffb",
|
||||
@@ -75,85 +141,7 @@
|
||||
"priority": 0,
|
||||
"robot_id": "default",
|
||||
"source": "ui",
|
||||
"started_at": "2026-06-13T07:03:01Z",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"created_at": "2026-06-13T07:03:01Z",
|
||||
"finished_at": "2026-06-13T07:03:03Z",
|
||||
"id": "887245afd51df357",
|
||||
"log": [
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-13T07:03:02Z"
|
||||
}
|
||||
],
|
||||
"mission": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "a1",
|
||||
"kind": "action",
|
||||
"label": "Wait",
|
||||
"params": {
|
||||
"seconds": 1
|
||||
},
|
||||
"type": "wait"
|
||||
}
|
||||
],
|
||||
"description": "",
|
||||
"group": "Missions",
|
||||
"id": "5ae9dbcb0722dffb",
|
||||
"name": "Test run",
|
||||
"updated_at": "2026-06-13T04:44:03Z"
|
||||
},
|
||||
"mission_group": "Missions",
|
||||
"mission_id": "5ae9dbcb0722dffb",
|
||||
"mission_name": "Test run",
|
||||
"parameters": {},
|
||||
"priority": 0,
|
||||
"robot_id": "default",
|
||||
"source": "modbus:1005",
|
||||
"started_at": "2026-06-13T07:03:02Z",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"created_at": "2026-06-13T07:03:01Z",
|
||||
"finished_at": "2026-06-13T07:03:04Z",
|
||||
"id": "4365bd4d8beedfd2",
|
||||
"log": [
|
||||
{
|
||||
"level": "info",
|
||||
"message": "Wait 1000ms",
|
||||
"ts": "2026-06-13T07:03:03Z"
|
||||
}
|
||||
],
|
||||
"mission": {
|
||||
"actions": [
|
||||
{
|
||||
"id": "a1",
|
||||
"kind": "action",
|
||||
"label": "Wait",
|
||||
"params": {
|
||||
"seconds": 1
|
||||
},
|
||||
"type": "wait"
|
||||
}
|
||||
],
|
||||
"description": "",
|
||||
"group": "Missions",
|
||||
"id": "5ae9dbcb0722dffb",
|
||||
"name": "Test run",
|
||||
"updated_at": "2026-06-13T04:44:03Z"
|
||||
},
|
||||
"mission_group": "Missions",
|
||||
"mission_id": "5ae9dbcb0722dffb",
|
||||
"mission_name": "Test run",
|
||||
"parameters": {},
|
||||
"priority": 0,
|
||||
"robot_id": "default",
|
||||
"source": "fleet:pytest-schedule",
|
||||
"started_at": "2026-06-13T07:03:03Z",
|
||||
"started_at": "2026-06-13T07:19:41Z",
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
@@ -163,6 +151,6 @@
|
||||
"message": "Hoàn thành: Test run",
|
||||
"paused": false,
|
||||
"state": "idle",
|
||||
"updated_at": "2026-06-13T07:03:04Z"
|
||||
"updated_at": "2026-06-13T07:19:43Z"
|
||||
}
|
||||
}
|
||||
@@ -14,20 +14,43 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"id": "a1",
|
||||
"children": [
|
||||
{
|
||||
"id": "c6c40563-0755-4e97-a48a-bb91ac8b0a9c",
|
||||
"kind": "action",
|
||||
"label": "Set PLC register",
|
||||
"params": {
|
||||
"action": "set",
|
||||
"register": 1,
|
||||
"value": 0
|
||||
},
|
||||
"type": "set_plc_register"
|
||||
},
|
||||
{
|
||||
"id": "a1",
|
||||
"kind": "action",
|
||||
"label": "Wait",
|
||||
"params": {
|
||||
"seconds": 1
|
||||
},
|
||||
"type": "wait"
|
||||
}
|
||||
],
|
||||
"id": "65f3cf0b-73fa-4f51-8774-1c5d4c83d8c4",
|
||||
"kind": "action",
|
||||
"label": "Wait",
|
||||
"label": "Loop",
|
||||
"params": {
|
||||
"seconds": 1
|
||||
"count": 1,
|
||||
"mode": "count"
|
||||
},
|
||||
"type": "wait"
|
||||
"type": "loop"
|
||||
}
|
||||
],
|
||||
"description": "",
|
||||
"group": "Missions",
|
||||
"id": "5ae9dbcb0722dffb",
|
||||
"name": "Test run",
|
||||
"updated_at": "2026-06-13T04:44:03Z"
|
||||
"updated_at": "2026-06-13T07:19:17.185Z"
|
||||
},
|
||||
{
|
||||
"actions": [
|
||||
|
||||
@@ -411,7 +411,7 @@ void MissionQueue::runMissionActions(nlohmann::json& entry)
|
||||
}
|
||||
}
|
||||
|
||||
void MissionQueue::executeActionsUnlocked(const nlohmann::json& actions,
|
||||
MissionQueue::LoopControl MissionQueue::executeActionsUnlocked(const nlohmann::json& actions,
|
||||
const nlohmann::json& parameters,
|
||||
nlohmann::json& log,
|
||||
int loop_depth)
|
||||
@@ -426,7 +426,7 @@ void MissionQueue::executeActionsUnlocked(const nlohmann::json& actions,
|
||||
while (paused_ && !stop_)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
if (stop_)
|
||||
return;
|
||||
return LoopControl::None;
|
||||
|
||||
const std::string action_id = action.value("id", "");
|
||||
const std::string kind = action.value("kind", "action");
|
||||
@@ -445,30 +445,58 @@ void MissionQueue::executeActionsUnlocked(const nlohmann::json& actions,
|
||||
{
|
||||
const std::string ref_id = action.value("refId", "");
|
||||
nlohmann::json ref_mission = nlohmann::json::object();
|
||||
// Nested mission snapshot should be resolved by frontend before queueing.
|
||||
(void)ref_id;
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "info"}, {"message", "Sub-mission: " + label}});
|
||||
if (action.contains("resolved_mission") && action["resolved_mission"].is_object())
|
||||
{
|
||||
const auto& nested_actions = action["resolved_mission"]["actions"];
|
||||
executeActionsUnlocked(nested_actions, parameters, log, loop_depth);
|
||||
const LoopControl nested = executeActionsUnlocked(nested_actions, parameters, log, loop_depth);
|
||||
if (nested == LoopControl::Break)
|
||||
return LoopControl::Break;
|
||||
if (nested == LoopControl::Continue)
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == "break")
|
||||
{
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "info"}, {"message", "Break loop"}});
|
||||
return LoopControl::Break;
|
||||
}
|
||||
|
||||
if (type == "continue")
|
||||
{
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "info"}, {"message", "Continue loop"}});
|
||||
return LoopControl::Continue;
|
||||
}
|
||||
|
||||
if (type == "loop")
|
||||
{
|
||||
const std::string mode = params.value("mode", "count");
|
||||
const int count = static_cast<int>(paramNumber(params, "count", 1));
|
||||
const auto& children =
|
||||
action.contains("children") && action["children"].is_array() ? action["children"] : nlohmann::json::array();
|
||||
const int iterations = mode == "endless" ? 1 : std::max(1, count);
|
||||
for (int i = 0; i < iterations; ++i)
|
||||
const int iterations = mode == "endless" ? 10000 : std::max(1, count);
|
||||
for (int i = 0; i < iterations && !stop_; ++i)
|
||||
{
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()},
|
||||
{"level", "info"},
|
||||
{"message", "Loop " + std::to_string(i + 1) + "/" + std::to_string(iterations)}});
|
||||
executeActionsUnlocked(children, parameters, log, loop_depth + 1);
|
||||
if (mode == "endless" && i == 0)
|
||||
{
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()},
|
||||
{"level", "info"},
|
||||
{"message", "Loop endless (simulated, max " + std::to_string(iterations) + ")"}});
|
||||
}
|
||||
else if (mode != "endless")
|
||||
{
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()},
|
||||
{"level", "info"},
|
||||
{"message", "Loop " + std::to_string(i + 1) + "/" + std::to_string(iterations)}});
|
||||
}
|
||||
const LoopControl ctrl = executeActionsUnlocked(children, parameters, log, loop_depth + 1);
|
||||
if (ctrl == LoopControl::Break)
|
||||
break;
|
||||
if (ctrl == LoopControl::Continue)
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -520,6 +548,7 @@ void MissionQueue::executeActionsUnlocked(const nlohmann::json& actions,
|
||||
{{"ts", IdUtil::nowIso8601()}, {"level", "info"}, {"message", label + " (" + type + ") simulated"}});
|
||||
sleepMs(400);
|
||||
}
|
||||
return LoopControl::None;
|
||||
}
|
||||
|
||||
void MissionQueue::sleepMs(int ms)
|
||||
|
||||
@@ -31,6 +31,8 @@ public:
|
||||
bool resume(std::string& err);
|
||||
|
||||
private:
|
||||
enum class LoopControl { None, Break, Continue };
|
||||
|
||||
std::filesystem::path queue_path_;
|
||||
mutable std::recursive_mutex mu_;
|
||||
nlohmann::json queue_;
|
||||
@@ -47,10 +49,10 @@ private:
|
||||
void startWorkerIfNeeded();
|
||||
void workerLoop();
|
||||
void runMissionActions(nlohmann::json& entry);
|
||||
void executeActionsUnlocked(const nlohmann::json& actions,
|
||||
const nlohmann::json& parameters,
|
||||
nlohmann::json& log,
|
||||
int loop_depth);
|
||||
LoopControl executeActionsUnlocked(const nlohmann::json& actions,
|
||||
const nlohmann::json& parameters,
|
||||
nlohmann::json& log,
|
||||
int loop_depth);
|
||||
void sleepMs(int ms);
|
||||
void setRunnerState(const std::string& state, const std::string& message = "");
|
||||
void insertByPriorityUnlocked(nlohmann::json& entry);
|
||||
|
||||
@@ -302,11 +302,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeActionTree(actions) {
|
||||
if (!Array.isArray(actions)) return;
|
||||
actions.forEach((action) => {
|
||||
if (action.type === "loop" && !Array.isArray(action.children)) {
|
||||
action.children = [];
|
||||
}
|
||||
if (Array.isArray(action.children)) normalizeActionTree(action.children);
|
||||
});
|
||||
}
|
||||
|
||||
function findActionList(path) {
|
||||
const draft = getDraft();
|
||||
if (!draft) return null;
|
||||
if (path === "root") return draft.actions;
|
||||
const parts = path.split(".");
|
||||
const parts = path.split(".").filter((p) => p !== "root");
|
||||
let list = draft.actions;
|
||||
for (const part of parts) {
|
||||
const node = list.find((a) => a.id === part);
|
||||
@@ -321,7 +331,7 @@
|
||||
for (let i = 0; i < list.length; i += 1) {
|
||||
const action = list[i];
|
||||
if (action.id === actionId) return { action, list, index: i, path, parent };
|
||||
if (action.children?.length) {
|
||||
if (Array.isArray(action.children)) {
|
||||
const hit = findActionWithParent(actionId, action.children, `${path}.${action.id}`, action);
|
||||
if (hit) return hit;
|
||||
}
|
||||
@@ -765,12 +775,17 @@
|
||||
const item = document.createElement("button");
|
||||
item.type = "button";
|
||||
item.className = "missionPaletteItem";
|
||||
item.draggable = true;
|
||||
item.innerHTML = `<span class="missionActionIcon">${def.isLoop ? "↻" : "▶"}</span><span>${escapeHtml(def.label)}</span>`;
|
||||
item.addEventListener("click", () => {
|
||||
addActionToList(def.type, "root");
|
||||
menu.hidden = true;
|
||||
btn.classList.remove("open");
|
||||
});
|
||||
item.addEventListener("dragstart", (evt) => {
|
||||
onPaletteDragStart(evt, { actionType: def.type });
|
||||
});
|
||||
item.addEventListener("dragend", onDragEnd);
|
||||
menu.appendChild(item);
|
||||
});
|
||||
|
||||
@@ -785,12 +800,17 @@
|
||||
const item = document.createElement("button");
|
||||
item.type = "button";
|
||||
item.className = "missionPaletteItem missionRef";
|
||||
item.draggable = true;
|
||||
item.innerHTML = `<span class="missionActionIcon kind-mission">◎</span><span>${escapeHtml(m.name)}</span>`;
|
||||
item.addEventListener("click", () => {
|
||||
addMissionRefToList(m.id, "root");
|
||||
menu.hidden = true;
|
||||
btn.classList.remove("open");
|
||||
});
|
||||
item.addEventListener("dragstart", (evt) => {
|
||||
onPaletteDragStart(evt, { missionId: m.id });
|
||||
});
|
||||
item.addEventListener("dragend", onDragEnd);
|
||||
menu.appendChild(item);
|
||||
});
|
||||
}
|
||||
@@ -891,15 +911,13 @@
|
||||
renderActionPalette();
|
||||
missionActionListEl.innerHTML = "";
|
||||
renderActionRows(draft.actions, "root", missionActionListEl);
|
||||
bindDragPaletteItems();
|
||||
}
|
||||
|
||||
function bindDragPaletteItems() {
|
||||
document.querySelectorAll(".missionLoopDrop").forEach((drop) => {
|
||||
drop.addEventListener("dragover", onLoopDragOver);
|
||||
drop.addEventListener("dragleave", onLoopDragLeave);
|
||||
drop.addEventListener("drop", onLoopDrop);
|
||||
});
|
||||
function onPaletteDragStart(evt, payload) {
|
||||
store.drag = { mode: "palette", ...payload };
|
||||
evt.dataTransfer.effectAllowed = "copy";
|
||||
evt.dataTransfer.setData("text/plain", payload.actionType || payload.missionId || "palette");
|
||||
evt.currentTarget.classList.add("dragging");
|
||||
}
|
||||
|
||||
function onDragStart(evt, actionId, listPath) {
|
||||
@@ -913,6 +931,7 @@
|
||||
document.querySelectorAll(".missionActionRow.dragging, .missionActionRow.dropBefore, .missionActionRow.dropAfter").forEach((n) => {
|
||||
n.classList.remove("dragging", "dropBefore", "dropAfter");
|
||||
});
|
||||
document.querySelectorAll(".missionPaletteItem.dragging").forEach((n) => n.classList.remove("dragging"));
|
||||
document.querySelectorAll(".missionLoopDrop.dragOver").forEach((n) => n.classList.remove("dragOver"));
|
||||
store.drag = null;
|
||||
}
|
||||
@@ -980,6 +999,7 @@
|
||||
if (!mission) return;
|
||||
store.editingId = missionId;
|
||||
store.draft = JSON.parse(JSON.stringify(mission));
|
||||
normalizeActionTree(store.draft.actions);
|
||||
setDirty(false);
|
||||
missionsListViewEl.hidden = true;
|
||||
missionEditorViewEl.hidden = false;
|
||||
|
||||
@@ -718,6 +718,7 @@ canvas {
|
||||
color: var(--text);
|
||||
}
|
||||
.missionPaletteItem:hover { background: #eff6ff; }
|
||||
.missionPaletteItem.dragging { opacity: 0.45; }
|
||||
.missionPaletteItem.missionRef .missionActionIcon { background: rgba(16, 185, 129, 0.15); color: #059669; }
|
||||
|
||||
.missionEditorBody { padding: 16px 18px 20px; }
|
||||
|
||||
Reference in New Issue
Block a user