excuting misstion from queue
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "app/lidar_manager_app.hpp"
|
||||
|
||||
#include "mission/mission_queue.hpp"
|
||||
#include "server/api_server.hpp"
|
||||
#include "server/static_file_server.hpp"
|
||||
#include "storage/state_repository.hpp"
|
||||
@@ -21,8 +22,11 @@ int LidarManagerApp::run()
|
||||
StateRepository repo(data_path_);
|
||||
repo.load();
|
||||
|
||||
const std::filesystem::path mission_queue_path = data_path_.parent_path() / "mission_queue.json";
|
||||
MissionQueue mission_queue(mission_queue_path);
|
||||
|
||||
httplib::Server svr;
|
||||
ApiServer api(repo);
|
||||
ApiServer api(repo, mission_queue);
|
||||
api.registerRoutes(svr);
|
||||
StaticFileServer::mount(svr, www_root_);
|
||||
|
||||
|
||||
446
src/mission/mission_queue.cpp
Normal file
446
src/mission/mission_queue.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
#include "mission/mission_queue.hpp"
|
||||
|
||||
#include "util/file_util.hpp"
|
||||
#include "util/id_util.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace lm {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string paramValue(const std::string& action_id,
|
||||
const nlohmann::json& params,
|
||||
const std::string& key,
|
||||
const nlohmann::json& parameters)
|
||||
{
|
||||
const std::string lookup = action_id + ":" + key;
|
||||
if (parameters.is_object() && parameters.contains(lookup))
|
||||
return parameters[lookup].get<std::string>();
|
||||
if (params.contains(key) && params[key].is_string())
|
||||
return params[key].get<std::string>();
|
||||
return "";
|
||||
}
|
||||
|
||||
double paramNumber(const nlohmann::json& params, const std::string& key, double fallback)
|
||||
{
|
||||
if (params.contains(key) && params[key].is_number())
|
||||
return params[key].get<double>();
|
||||
return fallback;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MissionQueue::MissionQueue(std::filesystem::path queue_path) : queue_path_(std::move(queue_path))
|
||||
{
|
||||
load();
|
||||
ensureRunnerDefaults();
|
||||
startWorkerIfNeeded();
|
||||
}
|
||||
|
||||
MissionQueue::~MissionQueue()
|
||||
{
|
||||
stop_ = true;
|
||||
wake_ = true;
|
||||
if (worker_.joinable())
|
||||
worker_.join();
|
||||
}
|
||||
|
||||
void MissionQueue::load()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
queue_ = nlohmann::json::array();
|
||||
runner_ = nlohmann::json::object();
|
||||
if (!std::filesystem::exists(queue_path_))
|
||||
return;
|
||||
try
|
||||
{
|
||||
const auto parsed = nlohmann::json::parse(FileUtil::readBinary(queue_path_));
|
||||
if (parsed.is_object())
|
||||
{
|
||||
if (parsed.contains("queue") && parsed["queue"].is_array())
|
||||
queue_ = parsed["queue"];
|
||||
if (parsed.contains("runner") && parsed["runner"].is_object())
|
||||
runner_ = parsed["runner"];
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
queue_ = nlohmann::json::array();
|
||||
}
|
||||
ensureRunnerDefaults();
|
||||
}
|
||||
|
||||
void MissionQueue::saveUnlocked() const
|
||||
{
|
||||
const nlohmann::json payload = {{"queue", queue_}, {"runner", runner_}};
|
||||
FileUtil::writeBinaryAtomic(queue_path_, payload.dump(2));
|
||||
}
|
||||
|
||||
void MissionQueue::ensureRunnerDefaults()
|
||||
{
|
||||
if (!runner_.is_object())
|
||||
runner_ = nlohmann::json::object();
|
||||
if (!runner_.contains("state"))
|
||||
runner_["state"] = "idle";
|
||||
if (!runner_.contains("message"))
|
||||
runner_["message"] = "";
|
||||
if (!runner_.contains("current_queue_id"))
|
||||
runner_["current_queue_id"] = nullptr;
|
||||
if (!runner_.contains("current_action"))
|
||||
runner_["current_action"] = nullptr;
|
||||
}
|
||||
|
||||
void MissionQueue::startWorkerIfNeeded()
|
||||
{
|
||||
if (worker_.joinable())
|
||||
return;
|
||||
worker_ = std::thread([this] { workerLoop(); });
|
||||
}
|
||||
|
||||
nlohmann::json MissionQueue::list() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return queue_;
|
||||
}
|
||||
|
||||
nlohmann::json MissionQueue::runnerStatus() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return runner_;
|
||||
}
|
||||
|
||||
std::optional<nlohmann::json> MissionQueue::enqueue(const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
if (!payload.is_object())
|
||||
{
|
||||
err = "payload must be an object";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!payload.contains("mission") || !payload["mission"].is_object())
|
||||
{
|
||||
err = "mission is required";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
nlohmann::json entry = nlohmann::json::object();
|
||||
entry["id"] = IdUtil::newId();
|
||||
entry["mission_id"] = payload["mission"].value("id", "");
|
||||
entry["mission_name"] = payload["mission"].value("name", "Mission");
|
||||
entry["mission_group"] = payload["mission"].value("group", "Missions");
|
||||
entry["mission"] = payload["mission"];
|
||||
entry["parameters"] = payload.contains("parameters") && payload["parameters"].is_object() ? payload["parameters"]
|
||||
: nlohmann::json::object();
|
||||
entry["status"] = "pending";
|
||||
entry["created_at"] = IdUtil::nowIso8601();
|
||||
entry["started_at"] = nullptr;
|
||||
entry["finished_at"] = nullptr;
|
||||
entry["log"] = nlohmann::json::array();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
queue_.push_back(entry);
|
||||
saveUnlocked();
|
||||
}
|
||||
|
||||
wake_ = true;
|
||||
return entry;
|
||||
}
|
||||
|
||||
bool MissionQueue::removeById(const std::string& id, std::string& err)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (!queue_.is_array())
|
||||
{
|
||||
err = "queue unavailable";
|
||||
return false;
|
||||
}
|
||||
const auto before = queue_.size();
|
||||
nlohmann::json next = nlohmann::json::array();
|
||||
for (const auto& item : queue_)
|
||||
{
|
||||
if (!item.is_object())
|
||||
continue;
|
||||
if (item.value("id", "") == id)
|
||||
{
|
||||
const std::string status = item.value("status", "");
|
||||
if (status == "executing")
|
||||
{
|
||||
err = "cannot remove executing mission";
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
next.push_back(item);
|
||||
}
|
||||
if (next.size() == before)
|
||||
{
|
||||
err = "queue item not found";
|
||||
return false;
|
||||
}
|
||||
queue_ = std::move(next);
|
||||
saveUnlocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MissionQueue::clearAll(std::string& err)
|
||||
{
|
||||
(void)err;
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
nlohmann::json next = nlohmann::json::array();
|
||||
for (const auto& item : queue_)
|
||||
{
|
||||
if (!item.is_object())
|
||||
continue;
|
||||
if (item.value("status", "") == "executing")
|
||||
next.push_back(item);
|
||||
}
|
||||
queue_ = std::move(next);
|
||||
setRunnerState(next.empty() ? "idle" : "running", next.empty() ? "" : "Đang thực thi mission");
|
||||
saveUnlocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MissionQueue::reorder(const nlohmann::json& ordered_ids, std::string& err)
|
||||
{
|
||||
if (!ordered_ids.is_array())
|
||||
{
|
||||
err = "ordered_ids must be an array";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (!queue_.is_array())
|
||||
{
|
||||
err = "queue unavailable";
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json by_id = nlohmann::json::object();
|
||||
for (auto& item : queue_)
|
||||
{
|
||||
if (item.is_object())
|
||||
by_id[item.value("id", "")] = item;
|
||||
}
|
||||
|
||||
nlohmann::json next = nlohmann::json::array();
|
||||
std::unordered_set<std::string> seen;
|
||||
for (const auto& id_json : ordered_ids)
|
||||
{
|
||||
if (!id_json.is_string())
|
||||
continue;
|
||||
const std::string id = id_json.get<std::string>();
|
||||
if (!by_id.contains(id))
|
||||
continue;
|
||||
next.push_back(by_id[id]);
|
||||
seen.insert(id);
|
||||
}
|
||||
|
||||
for (const auto& item : queue_)
|
||||
{
|
||||
if (!item.is_object())
|
||||
continue;
|
||||
const std::string id = item.value("id", "");
|
||||
if (seen.count(id))
|
||||
continue;
|
||||
next.push_back(item);
|
||||
}
|
||||
|
||||
queue_ = std::move(next);
|
||||
saveUnlocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
void MissionQueue::workerLoop()
|
||||
{
|
||||
while (!stop_)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
processQueueUnlocked();
|
||||
}
|
||||
for (int i = 0; i < 20 && !wake_; ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
wake_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MissionQueue::processQueueUnlocked()
|
||||
{
|
||||
if (!queue_.is_array())
|
||||
return;
|
||||
|
||||
for (auto& item : queue_)
|
||||
{
|
||||
if (!item.is_object())
|
||||
continue;
|
||||
if (item.value("status", "") != "pending")
|
||||
continue;
|
||||
executeMissionUnlocked(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (runner_.value("state", "") == "running")
|
||||
setRunnerState("idle", "Queue trống");
|
||||
}
|
||||
|
||||
void MissionQueue::executeMissionUnlocked(nlohmann::json& entry)
|
||||
{
|
||||
entry["status"] = "executing";
|
||||
entry["started_at"] = IdUtil::nowIso8601();
|
||||
runner_["current_queue_id"] = entry.value("id", "");
|
||||
runner_["current_action"] = nullptr;
|
||||
setRunnerState("running", "Đang chạy: " + entry.value("mission_name", "Mission"));
|
||||
saveUnlocked();
|
||||
|
||||
try
|
||||
{
|
||||
nlohmann::json log = nlohmann::json::array();
|
||||
const auto& mission = entry["mission"];
|
||||
const auto& parameters = entry["parameters"];
|
||||
const auto& actions =
|
||||
mission.contains("actions") && mission["actions"].is_array() ? mission["actions"] : nlohmann::json::array();
|
||||
executeActionsUnlocked(actions, parameters, log, 0);
|
||||
entry["log"] = log;
|
||||
entry["status"] = "completed";
|
||||
entry["finished_at"] = IdUtil::nowIso8601();
|
||||
setRunnerState("idle", "Hoàn thành: " + entry.value("mission_name", "Mission"));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
entry["status"] = "failed";
|
||||
entry["finished_at"] = IdUtil::nowIso8601();
|
||||
setRunnerState("error", "Lỗi khi chạy: " + entry.value("mission_name", "Mission"));
|
||||
}
|
||||
|
||||
runner_["current_queue_id"] = nullptr;
|
||||
runner_["current_action"] = nullptr;
|
||||
saveUnlocked();
|
||||
wake_ = true;
|
||||
}
|
||||
|
||||
void MissionQueue::executeActionsUnlocked(const nlohmann::json& actions,
|
||||
const nlohmann::json& parameters,
|
||||
nlohmann::json& log,
|
||||
int loop_depth)
|
||||
{
|
||||
if (loop_depth > 8)
|
||||
throw std::runtime_error("loop depth exceeded");
|
||||
|
||||
for (const auto& action : actions)
|
||||
{
|
||||
if (!action.is_object())
|
||||
continue;
|
||||
|
||||
const std::string action_id = action.value("id", "");
|
||||
const std::string kind = action.value("kind", "action");
|
||||
const std::string type = action.value("type", "");
|
||||
const std::string label = action.value("label", type);
|
||||
const auto& params = action.contains("params") && action["params"].is_object() ? action["params"]
|
||||
: nlohmann::json::object();
|
||||
|
||||
runner_["current_action"] = label;
|
||||
saveUnlocked();
|
||||
|
||||
if (kind == "mission")
|
||||
{
|
||||
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);
|
||||
}
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == "wait")
|
||||
{
|
||||
const int ms = static_cast<int>(paramNumber(params, "seconds", 1) * 1000);
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "info"}, {"message", "Wait " + std::to_string(ms) + "ms"}});
|
||||
sleepMs(ms);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == "move_to_position" || type == "adjust_localization" || type == "pick_cart" || type == "drop_cart")
|
||||
{
|
||||
const std::string pos = paramValue(action_id, params, "position", parameters);
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()},
|
||||
{"level", "info"},
|
||||
{"message", label + " → " + (pos.empty() ? "?" : pos)}});
|
||||
sleepMs(1200);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == "move_to_marker")
|
||||
{
|
||||
const std::string marker = paramValue(action_id, params, "marker", parameters);
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()},
|
||||
{"level", "info"},
|
||||
{"message", label + " → " + (marker.empty() ? "?" : marker)}});
|
||||
sleepMs(1200);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == "user_log")
|
||||
{
|
||||
const std::string message = params.value("message", "Mission step");
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "user"}, {"message", message}});
|
||||
sleepMs(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == "pause")
|
||||
{
|
||||
log.push_back({{"ts", IdUtil::nowIso8601()}, {"level", "warn"}, {"message", "Pause (simulated)"}});
|
||||
sleepMs(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
log.push_back(
|
||||
{{"ts", IdUtil::nowIso8601()}, {"level", "info"}, {"message", label + " (" + type + ") simulated"}});
|
||||
sleepMs(400);
|
||||
}
|
||||
}
|
||||
|
||||
void MissionQueue::sleepMs(int ms)
|
||||
{
|
||||
if (ms <= 0)
|
||||
return;
|
||||
const int step = 100;
|
||||
for (int elapsed = 0; elapsed < ms && !stop_; elapsed += step)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(std::min(step, ms - elapsed)));
|
||||
}
|
||||
|
||||
void MissionQueue::setRunnerState(const std::string& state, const std::string& message)
|
||||
{
|
||||
runner_["state"] = state;
|
||||
runner_["message"] = message;
|
||||
runner_["updated_at"] = IdUtil::nowIso8601();
|
||||
}
|
||||
|
||||
} // namespace lm
|
||||
56
src/mission/mission_queue.hpp
Normal file
56
src/mission/mission_queue.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace lm {
|
||||
|
||||
class MissionQueue
|
||||
{
|
||||
public:
|
||||
explicit MissionQueue(std::filesystem::path queue_path);
|
||||
~MissionQueue();
|
||||
|
||||
MissionQueue(const MissionQueue&) = delete;
|
||||
MissionQueue& operator=(const MissionQueue&) = delete;
|
||||
|
||||
nlohmann::json list() const;
|
||||
nlohmann::json runnerStatus() const;
|
||||
|
||||
std::optional<nlohmann::json> enqueue(const nlohmann::json& payload, std::string& err);
|
||||
bool removeById(const std::string& id, std::string& err);
|
||||
bool clearAll(std::string& err);
|
||||
bool reorder(const nlohmann::json& ordered_ids, std::string& err);
|
||||
|
||||
private:
|
||||
std::filesystem::path queue_path_;
|
||||
mutable std::mutex mu_;
|
||||
nlohmann::json queue_;
|
||||
nlohmann::json runner_;
|
||||
|
||||
std::thread worker_;
|
||||
std::atomic<bool> stop_{false};
|
||||
std::atomic<bool> wake_{false};
|
||||
|
||||
void load();
|
||||
void saveUnlocked() const;
|
||||
void ensureRunnerDefaults();
|
||||
void startWorkerIfNeeded();
|
||||
void workerLoop();
|
||||
void processQueueUnlocked();
|
||||
void executeMissionUnlocked(nlohmann::json& entry);
|
||||
void 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 = "");
|
||||
};
|
||||
|
||||
} // namespace lm
|
||||
@@ -1,723 +0,0 @@
|
||||
#include "mission/mission_service.hpp"
|
||||
|
||||
#include "util/file_util.hpp"
|
||||
#include "util/id_util.hpp"
|
||||
#include "util/string_util.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
namespace lm {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kLogMax = 200;
|
||||
|
||||
nlohmann::json defaultDoc()
|
||||
{
|
||||
return nlohmann::json{{"version", 1},
|
||||
{"missions", nlohmann::json::array()},
|
||||
{"groups", nlohmann::json::array({"Missions", "Move", "Logic", "I/O", "Cart", "Misc"})},
|
||||
{"triggers", nlohmann::json::array()},
|
||||
{"dashboard", nlohmann::json{{"widgets", nlohmann::json::array()}}}};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MissionService::MissionService(std::filesystem::path data_dir)
|
||||
: data_dir_(std::move(data_dir)), missions_path_(data_dir_ / "missions.json")
|
||||
{
|
||||
load();
|
||||
worker_ = std::thread([this] { workerLoop(); });
|
||||
}
|
||||
|
||||
MissionService::~MissionService()
|
||||
{
|
||||
stop_thread_ = true;
|
||||
cv_.notify_all();
|
||||
if (worker_.joinable())
|
||||
worker_.join();
|
||||
}
|
||||
|
||||
void MissionService::load()
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
const auto raw = FileUtil::readBinary(missions_path_);
|
||||
if (raw.empty())
|
||||
{
|
||||
doc_ = defaultDoc();
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
doc_ = nlohmann::json::parse(raw);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
doc_ = defaultDoc();
|
||||
}
|
||||
ensureDocSchema();
|
||||
queue_ = nlohmann::json::array();
|
||||
action_log_ = nlohmann::json::array();
|
||||
}
|
||||
|
||||
void MissionService::ensureDocSchema()
|
||||
{
|
||||
if (!doc_.is_object())
|
||||
doc_ = defaultDoc();
|
||||
if (!doc_.contains("version"))
|
||||
doc_["version"] = 1;
|
||||
if (!doc_.contains("missions") || !doc_["missions"].is_array())
|
||||
doc_["missions"] = nlohmann::json::array();
|
||||
if (!doc_.contains("groups") || !doc_["groups"].is_array())
|
||||
doc_["groups"] = defaultDoc()["groups"];
|
||||
if (!doc_.contains("triggers") || !doc_["triggers"].is_array())
|
||||
doc_["triggers"] = nlohmann::json::array();
|
||||
if (!doc_.contains("dashboard") || !doc_["dashboard"].is_object())
|
||||
doc_["dashboard"] = nlohmann::json{{"widgets", nlohmann::json::array()}};
|
||||
if (!doc_["dashboard"].contains("widgets") || !doc_["dashboard"]["widgets"].is_array())
|
||||
doc_["dashboard"]["widgets"] = nlohmann::json::array();
|
||||
}
|
||||
|
||||
bool MissionService::saveDocument() const
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(data_dir_, ec);
|
||||
auto body = doc_.dump(2);
|
||||
body.push_back('\n');
|
||||
return FileUtil::writeBinaryAtomic(missions_path_, body);
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::snapshot() const
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
return doc_;
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::runnerStatus() const
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
return nlohmann::json{{"mode", mode_},
|
||||
{"state_id", state_id_},
|
||||
{"queue", queue_},
|
||||
{"current", current_ ? *current_ : nlohmann::json(nullptr)},
|
||||
{"action_log", action_log_}};
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::listMissions() const
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
return doc_;
|
||||
}
|
||||
|
||||
std::optional<nlohmann::json> MissionService::getMission(const std::string& id) const
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
for (const auto& m : doc_["missions"])
|
||||
{
|
||||
if (m.contains("id") && m["id"].get<std::string>() == id)
|
||||
return m;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::findMissionUnlocked(const std::string& id) const
|
||||
{
|
||||
for (const auto& m : doc_["missions"])
|
||||
{
|
||||
if (m.contains("id") && m["id"].get<std::string>() == id)
|
||||
return m;
|
||||
}
|
||||
return nlohmann::json();
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::createMission(const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
if (!payload.is_object() || !payload.contains("name") || !payload["name"].is_string())
|
||||
{
|
||||
err = "name is required";
|
||||
return {};
|
||||
}
|
||||
const std::string name = StringUtil::trimCopy(payload["name"].get<std::string>());
|
||||
if (name.empty())
|
||||
{
|
||||
err = "name is required";
|
||||
return {};
|
||||
}
|
||||
for (const auto& m : doc_["missions"])
|
||||
{
|
||||
if (m.contains("name") && m["name"].get<std::string>() == name)
|
||||
{
|
||||
err = "mission name already exists";
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json mission = payload;
|
||||
if (payload.contains("id") && payload["id"].is_string() && !payload["id"].get<std::string>().empty())
|
||||
mission["id"] = payload["id"].get<std::string>();
|
||||
else
|
||||
mission["id"] = IdUtil::newId();
|
||||
mission["name"] = name;
|
||||
if (!mission.contains("group") || !mission["group"].is_string())
|
||||
mission["group"] = "Missions";
|
||||
if (!mission.contains("description"))
|
||||
mission["description"] = "";
|
||||
if (!mission.contains("actions") || !mission["actions"].is_array())
|
||||
mission["actions"] = nlohmann::json::array();
|
||||
mission["updated_at"] = IdUtil::nowIso8601();
|
||||
|
||||
const std::string group = mission["group"].get<std::string>();
|
||||
if (std::find(doc_["groups"].begin(), doc_["groups"].end(), group) == doc_["groups"].end())
|
||||
doc_["groups"].push_back(group);
|
||||
|
||||
doc_["missions"].push_back(mission);
|
||||
return mission;
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::updateMission(const std::string& id, const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
for (auto& m : doc_["missions"])
|
||||
{
|
||||
if (!m.contains("id") || m["id"].get<std::string>() != id)
|
||||
continue;
|
||||
if (payload.contains("name") && payload["name"].is_string())
|
||||
{
|
||||
const std::string name = StringUtil::trimCopy(payload["name"].get<std::string>());
|
||||
if (name.empty())
|
||||
{
|
||||
err = "name is required";
|
||||
return {};
|
||||
}
|
||||
for (const auto& other : doc_["missions"])
|
||||
{
|
||||
if (other.contains("id") && other["id"].get<std::string>() != id && other.contains("name") &&
|
||||
other["name"].get<std::string>() == name)
|
||||
{
|
||||
err = "mission name already exists";
|
||||
return {};
|
||||
}
|
||||
}
|
||||
m["name"] = name;
|
||||
}
|
||||
if (payload.contains("group") && payload["group"].is_string())
|
||||
{
|
||||
m["group"] = payload["group"];
|
||||
const std::string group = m["group"].get<std::string>();
|
||||
if (std::find(doc_["groups"].begin(), doc_["groups"].end(), group) == doc_["groups"].end())
|
||||
doc_["groups"].push_back(group);
|
||||
}
|
||||
if (payload.contains("description") && payload["description"].is_string())
|
||||
m["description"] = payload["description"];
|
||||
if (payload.contains("actions") && payload["actions"].is_array())
|
||||
m["actions"] = payload["actions"];
|
||||
m["updated_at"] = IdUtil::nowIso8601();
|
||||
return m;
|
||||
}
|
||||
err = "mission not found";
|
||||
return {};
|
||||
}
|
||||
|
||||
bool MissionService::deleteMission(const std::string& id, std::string& err)
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
auto& missions = doc_["missions"];
|
||||
for (auto it = missions.begin(); it != missions.end(); ++it)
|
||||
{
|
||||
if (it->contains("id") && (*it)["id"].get<std::string>() == id)
|
||||
{
|
||||
missions.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
err = "mission not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::listQueue() const
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
return queue_;
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::enqueueMission(const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
if (!payload.is_object() || !payload.contains("mission_id") || !payload["mission_id"].is_string())
|
||||
{
|
||||
err = "mission_id is required";
|
||||
return {};
|
||||
}
|
||||
const std::string mission_id = payload["mission_id"].get<std::string>();
|
||||
const auto mission = findMissionUnlocked(mission_id);
|
||||
if (mission.is_null())
|
||||
{
|
||||
err = "mission not found";
|
||||
return {};
|
||||
}
|
||||
if (mode_ == "stop")
|
||||
{
|
||||
err = "robot is stopped; cannot queue mission";
|
||||
return {};
|
||||
}
|
||||
if (mode_ != "autonomous")
|
||||
{
|
||||
err = "robot must be in autonomous mode to queue missions";
|
||||
return {};
|
||||
}
|
||||
|
||||
nlohmann::json parameters = nlohmann::json::object();
|
||||
if (payload.contains("parameters") && payload["parameters"].is_object())
|
||||
parameters = payload["parameters"];
|
||||
|
||||
nlohmann::json entry = {{"id", IdUtil::newId()},
|
||||
{"queue_num", next_queue_num_++},
|
||||
{"mission_id", mission_id},
|
||||
{"mission_name", mission["name"]},
|
||||
{"parameters", parameters},
|
||||
{"state", "pending"},
|
||||
{"created_at", IdUtil::nowIso8601()}};
|
||||
queue_.push_back(entry);
|
||||
appendLogUnlocked("info", "Mission queued: " + mission["name"].get<std::string>(), {{"queue_id", entry["id"]}});
|
||||
cv_.notify_all();
|
||||
return entry;
|
||||
}
|
||||
|
||||
bool MissionService::dequeueEntry(const std::string& queue_id, std::string& err)
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
for (auto it = queue_.begin(); it != queue_.end(); ++it)
|
||||
{
|
||||
if ((*it).contains("id") && (*it)["id"].get<std::string>() == queue_id)
|
||||
{
|
||||
if ((*it).value("state", "") == "executing")
|
||||
{
|
||||
err = "cannot remove executing mission";
|
||||
return false;
|
||||
}
|
||||
queue_.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
err = "queue entry not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MissionService::clearQueue(std::string& err)
|
||||
{
|
||||
(void)err;
|
||||
std::lock_guard lock(mu_);
|
||||
nlohmann::json kept = nlohmann::json::array();
|
||||
for (auto& e : queue_)
|
||||
{
|
||||
if (e.value("state", "") == "executing")
|
||||
kept.push_back(e);
|
||||
}
|
||||
queue_ = kept;
|
||||
if (state_id_ == 3)
|
||||
cv_.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MissionService::reorderQueue(const std::string& queue_id, int new_index, std::string& err)
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
if (new_index < 0)
|
||||
{
|
||||
err = "invalid index";
|
||||
return false;
|
||||
}
|
||||
nlohmann::json item;
|
||||
int found = -1;
|
||||
for (size_t i = 0; i < queue_.size(); ++i)
|
||||
{
|
||||
if (queue_[i].contains("id") && queue_[i]["id"].get<std::string>() == queue_id)
|
||||
{
|
||||
if (queue_[i].value("state", "") != "pending")
|
||||
{
|
||||
err = "only pending missions can be reordered";
|
||||
return false;
|
||||
}
|
||||
item = queue_[i];
|
||||
found = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found < 0)
|
||||
{
|
||||
err = "queue entry not found";
|
||||
return false;
|
||||
}
|
||||
queue_.erase(queue_.begin() + found);
|
||||
if (new_index > static_cast<int>(queue_.size()))
|
||||
new_index = static_cast<int>(queue_.size());
|
||||
queue_.insert(queue_.begin() + new_index, item);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MissionService::setMode(const std::string& mode, std::string& err)
|
||||
{
|
||||
if (mode != "autonomous" && mode != "manual" && mode != "stop")
|
||||
{
|
||||
err = "mode must be autonomous, manual, or stop";
|
||||
return false;
|
||||
}
|
||||
std::lock_guard lock(mu_);
|
||||
mode_ = mode;
|
||||
if (mode == "stop")
|
||||
state_id_ = 4;
|
||||
appendLogUnlocked("info", "Operating mode: " + mode);
|
||||
cv_.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MissionService::setRunnerState(int state_id, std::string& err)
|
||||
{
|
||||
if (state_id != 3 && state_id != 4)
|
||||
{
|
||||
err = "state_id must be 3 (play) or 4 (pause)";
|
||||
return false;
|
||||
}
|
||||
std::lock_guard lock(mu_);
|
||||
state_id_ = state_id;
|
||||
runner_paused_ = (state_id == 4);
|
||||
appendLogUnlocked("info", state_id == 3 ? "Runner play" : "Runner pause");
|
||||
cv_.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::listTriggers() const
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
return doc_["triggers"];
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::createTrigger(const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
if (!payload.is_object() || !payload.contains("name") || !payload.contains("coil_id") ||
|
||||
!payload.contains("mission_id"))
|
||||
{
|
||||
err = "name, coil_id, mission_id required";
|
||||
return {};
|
||||
}
|
||||
const int coil_id = payload["coil_id"].get<int>();
|
||||
if (coil_id < 1001 || coil_id > 2000)
|
||||
{
|
||||
err = "coil_id must be between 1001 and 2000";
|
||||
return {};
|
||||
}
|
||||
for (const auto& t : doc_["triggers"])
|
||||
{
|
||||
if (t.contains("coil_id") && t["coil_id"].get<int>() == coil_id)
|
||||
{
|
||||
err = "coil_id already used";
|
||||
return {};
|
||||
}
|
||||
}
|
||||
const std::string mission_id = payload["mission_id"].get<std::string>();
|
||||
if (findMissionUnlocked(mission_id).is_null())
|
||||
{
|
||||
err = "mission not found";
|
||||
return {};
|
||||
}
|
||||
nlohmann::json trigger = {{"id", IdUtil::newId()},
|
||||
{"name", StringUtil::trimCopy(payload["name"].get<std::string>())},
|
||||
{"coil_id", coil_id},
|
||||
{"mission_id", mission_id}};
|
||||
doc_["triggers"].push_back(trigger);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
bool MissionService::deleteTrigger(const std::string& id, std::string& err)
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
auto& triggers = doc_["triggers"];
|
||||
for (auto it = triggers.begin(); it != triggers.end(); ++it)
|
||||
{
|
||||
if (it->contains("id") && (*it)["id"].get<std::string>() == id)
|
||||
{
|
||||
triggers.erase(it);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
err = "trigger not found";
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::fireTriggerByCoil(int coil_id, std::string& err)
|
||||
{
|
||||
nlohmann::json trigger;
|
||||
std::string mission_id;
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
for (const auto& t : doc_["triggers"])
|
||||
{
|
||||
if (t.contains("coil_id") && t["coil_id"].get<int>() == coil_id)
|
||||
{
|
||||
trigger = t;
|
||||
mission_id = t["mission_id"].get<std::string>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mission_id.empty())
|
||||
{
|
||||
err = "trigger not found for coil";
|
||||
return {};
|
||||
}
|
||||
return enqueueMission(nlohmann::json{{"mission_id", mission_id}, {"parameters", nlohmann::json::object()}}, err);
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::getDashboard() const
|
||||
{
|
||||
std::lock_guard lock(mu_);
|
||||
return doc_["dashboard"];
|
||||
}
|
||||
|
||||
bool MissionService::setDashboard(const nlohmann::json& payload, std::string& err)
|
||||
{
|
||||
if (!payload.is_object() || !payload.contains("widgets") || !payload["widgets"].is_array())
|
||||
{
|
||||
err = "widgets array required";
|
||||
return false;
|
||||
}
|
||||
std::lock_guard lock(mu_);
|
||||
doc_["dashboard"] = payload;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MissionService::appendLogUnlocked(const std::string& level,
|
||||
const std::string& message,
|
||||
const nlohmann::json& extra)
|
||||
{
|
||||
nlohmann::json row = {{"ts", IdUtil::nowIso8601()}, {"level", level}, {"message", message}};
|
||||
if (!extra.empty())
|
||||
row["extra"] = extra;
|
||||
action_log_.push_back(row);
|
||||
while (action_log_.size() > kLogMax)
|
||||
action_log_.erase(action_log_.begin());
|
||||
}
|
||||
|
||||
nlohmann::json MissionService::paramValue(const nlohmann::json& v)
|
||||
{
|
||||
if (v.is_object() && v.contains("kind") && v["kind"].get<std::string>() == "variable")
|
||||
return v.value("default", "");
|
||||
return v;
|
||||
}
|
||||
|
||||
void MissionService::resolveParamsUnlocked(nlohmann::json& params, const nlohmann::json& runtime) const
|
||||
{
|
||||
if (!params.is_object())
|
||||
return;
|
||||
for (auto it = params.begin(); it != params.end(); ++it)
|
||||
{
|
||||
if (it.value().is_object() && it.value().contains("kind") &&
|
||||
it.value()["kind"].get<std::string>() == "variable")
|
||||
{
|
||||
const std::string key = it.key();
|
||||
if (runtime.contains(key))
|
||||
params[key] = runtime[key];
|
||||
else
|
||||
params[key] = it.value().value("default", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MissionService::waitWhilePausedUnlocked()
|
||||
{
|
||||
while (!stop_thread_ && (state_id_ == 4 || runner_paused_))
|
||||
{
|
||||
mu_.unlock();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
mu_.lock();
|
||||
}
|
||||
}
|
||||
|
||||
void MissionService::sleepMs(int ms)
|
||||
{
|
||||
if (ms <= 0)
|
||||
return;
|
||||
mu_.unlock();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
||||
mu_.lock();
|
||||
}
|
||||
|
||||
void MissionService::workerLoop()
|
||||
{
|
||||
while (!stop_thread_)
|
||||
{
|
||||
{
|
||||
std::unique_lock lock(mu_);
|
||||
cv_.wait_for(lock, std::chrono::milliseconds(200), [this] {
|
||||
if (stop_thread_)
|
||||
return true;
|
||||
if (mode_ != "autonomous" || state_id_ != 3)
|
||||
return false;
|
||||
if (current_)
|
||||
return true;
|
||||
for (const auto& e : queue_)
|
||||
{
|
||||
if (e.value("state", "") == "pending")
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (stop_thread_)
|
||||
break;
|
||||
tickRunnerUnlocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MissionService::tickRunnerUnlocked()
|
||||
{
|
||||
if (mode_ != "autonomous" || state_id_ != 3 || current_)
|
||||
return;
|
||||
|
||||
for (auto& entry : queue_)
|
||||
{
|
||||
if (entry.value("state", "") != "pending")
|
||||
continue;
|
||||
entry["state"] = "executing";
|
||||
entry["started_at"] = IdUtil::nowIso8601();
|
||||
current_ = entry;
|
||||
executeQueueEntryUnlocked(entry);
|
||||
entry["state"] = "finished";
|
||||
entry["finished_at"] = IdUtil::nowIso8601();
|
||||
appendLogUnlocked("info", "Mission finished: " + entry["mission_name"].get<std::string>());
|
||||
current_ = std::nullopt;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MissionService::executeQueueEntryUnlocked(nlohmann::json& entry)
|
||||
{
|
||||
const std::string mission_id = entry["mission_id"].get<std::string>();
|
||||
const auto mission = findMissionUnlocked(mission_id);
|
||||
if (mission.is_null())
|
||||
{
|
||||
entry["state"] = "failed";
|
||||
entry["error"] = "mission definition missing";
|
||||
appendLogUnlocked("error", "Mission missing: " + mission_id);
|
||||
return;
|
||||
}
|
||||
nlohmann::json parameters = entry.value("parameters", nlohmann::json::object());
|
||||
if (!parameters.is_object())
|
||||
parameters = nlohmann::json::object();
|
||||
current_ = entry;
|
||||
current_->value("progress", nlohmann::json::object())["mission_name"] = mission["name"];
|
||||
executeActionsUnlocked(mission.value("actions", nlohmann::json::array()), parameters, 0);
|
||||
}
|
||||
|
||||
void MissionService::executeActionsUnlocked(const nlohmann::json& actions,
|
||||
const nlohmann::json& parameters,
|
||||
int depth)
|
||||
{
|
||||
if (depth > 32)
|
||||
return;
|
||||
for (const auto& action : actions)
|
||||
{
|
||||
waitWhilePausedUnlocked();
|
||||
if (stop_thread_ || mode_ != "autonomous")
|
||||
return;
|
||||
if (!executeActionUnlocked(action, parameters, depth))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool MissionService::executeActionUnlocked(const nlohmann::json& action,
|
||||
const nlohmann::json& parameters,
|
||||
int depth)
|
||||
{
|
||||
if (!action.is_object())
|
||||
return true;
|
||||
|
||||
const std::string kind = action.value("kind", "action");
|
||||
if (kind == "mission")
|
||||
{
|
||||
const std::string ref = action.value("refId", "");
|
||||
const auto sub = findMissionUnlocked(ref);
|
||||
if (sub.is_null())
|
||||
{
|
||||
appendLogUnlocked("error", "Embedded mission not found");
|
||||
return false;
|
||||
}
|
||||
appendLogUnlocked("info", "Run sub-mission: " + sub["name"].get<std::string>());
|
||||
executeActionsUnlocked(sub.value("actions", nlohmann::json::array()), parameters, depth + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string type = action.value("type", "");
|
||||
nlohmann::json params = action.value("params", nlohmann::json::object());
|
||||
resolveParamsUnlocked(params, parameters);
|
||||
|
||||
if (current_)
|
||||
{
|
||||
(*current_)["current_action"] = {{"type", type}, {"label", action.value("label", type)}, {"params", params}};
|
||||
}
|
||||
|
||||
appendLogUnlocked("action", action.value("label", type), {{"type", type}, {"params", params}});
|
||||
|
||||
if (type == "pause")
|
||||
{
|
||||
runner_paused_ = true;
|
||||
state_id_ = 4;
|
||||
appendLogUnlocked("info", "Pause action — waiting for continue");
|
||||
waitWhilePausedUnlocked();
|
||||
return true;
|
||||
}
|
||||
if (type == "break" || type == "continue")
|
||||
return type != "break";
|
||||
|
||||
if (type == "wait")
|
||||
{
|
||||
const int sec = params.value("seconds", 1);
|
||||
sleepMs(sec * 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == "loop" && action.contains("children") && action["children"].is_array())
|
||||
{
|
||||
const std::string loop_mode = params.value("mode", "count");
|
||||
const int count = params.value("count", 1);
|
||||
if (loop_mode == "endless")
|
||||
{
|
||||
int guard = 0;
|
||||
while (!stop_thread_ && mode_ == "autonomous" && guard < 1000)
|
||||
{
|
||||
waitWhilePausedUnlocked();
|
||||
executeActionsUnlocked(action["children"], parameters, depth + 1);
|
||||
++guard;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < count && !stop_thread_; ++i)
|
||||
{
|
||||
waitWhilePausedUnlocked();
|
||||
executeActionsUnlocked(action["children"], parameters, depth + 1);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == "move_to_position" || type == "move_to_marker" || type == "adjust_localization")
|
||||
sleepMs(1500);
|
||||
else if (type == "pick_cart" || type == "drop_cart")
|
||||
sleepMs(2000);
|
||||
else if (type == "set_digital_output" || type == "wait_digital_input" || type == "set_plc_register")
|
||||
sleepMs(800);
|
||||
else if (type == "user_log" || type == "play_sound")
|
||||
sleepMs(300);
|
||||
else
|
||||
sleepMs(500);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace lm
|
||||
@@ -1,92 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace lm {
|
||||
|
||||
class MissionService
|
||||
{
|
||||
public:
|
||||
explicit MissionService(std::filesystem::path data_dir);
|
||||
|
||||
~MissionService();
|
||||
|
||||
MissionService(const MissionService&) = delete;
|
||||
MissionService& operator=(const MissionService&) = delete;
|
||||
|
||||
nlohmann::json snapshot() const;
|
||||
nlohmann::json runnerStatus() const;
|
||||
|
||||
bool saveDocument() const;
|
||||
|
||||
nlohmann::json listMissions() const;
|
||||
std::optional<nlohmann::json> getMission(const std::string& id) const;
|
||||
nlohmann::json createMission(const nlohmann::json& payload, std::string& err);
|
||||
nlohmann::json updateMission(const std::string& id, const nlohmann::json& payload, std::string& err);
|
||||
bool deleteMission(const std::string& id, std::string& err);
|
||||
|
||||
nlohmann::json listQueue() const;
|
||||
nlohmann::json enqueueMission(const nlohmann::json& payload, std::string& err);
|
||||
bool dequeueEntry(const std::string& queue_id, std::string& err);
|
||||
bool clearQueue(std::string& err);
|
||||
bool reorderQueue(const std::string& queue_id, int new_index, std::string& err);
|
||||
|
||||
bool setMode(const std::string& mode, std::string& err);
|
||||
bool setRunnerState(int state_id, std::string& err);
|
||||
|
||||
nlohmann::json listTriggers() const;
|
||||
nlohmann::json createTrigger(const nlohmann::json& payload, std::string& err);
|
||||
bool deleteTrigger(const std::string& id, std::string& err);
|
||||
nlohmann::json fireTriggerByCoil(int coil_id, std::string& err);
|
||||
|
||||
nlohmann::json getDashboard() const;
|
||||
bool setDashboard(const nlohmann::json& payload, std::string& err);
|
||||
|
||||
private:
|
||||
std::filesystem::path data_dir_;
|
||||
std::filesystem::path missions_path_;
|
||||
|
||||
mutable std::mutex mu_;
|
||||
nlohmann::json doc_;
|
||||
nlohmann::json queue_;
|
||||
nlohmann::json action_log_;
|
||||
std::string mode_{"autonomous"};
|
||||
int state_id_{3};
|
||||
std::optional<nlohmann::json> current_;
|
||||
int next_queue_num_{1};
|
||||
|
||||
std::atomic<bool> stop_thread_{false};
|
||||
std::atomic<bool> runner_paused_{false};
|
||||
std::condition_variable cv_;
|
||||
std::thread worker_;
|
||||
|
||||
void load();
|
||||
void ensureDocSchema();
|
||||
nlohmann::json findMissionUnlocked(const std::string& id) const;
|
||||
void workerLoop();
|
||||
void tickRunnerUnlocked();
|
||||
void executeQueueEntryUnlocked(nlohmann::json& entry);
|
||||
void executeActionsUnlocked(const nlohmann::json& actions,
|
||||
const nlohmann::json& parameters,
|
||||
int depth);
|
||||
bool executeActionUnlocked(const nlohmann::json& action,
|
||||
const nlohmann::json& parameters,
|
||||
int depth);
|
||||
void appendLogUnlocked(const std::string& level,
|
||||
const std::string& message,
|
||||
const nlohmann::json& extra = nlohmann::json::object());
|
||||
void resolveParamsUnlocked(nlohmann::json& params, const nlohmann::json& runtime) const;
|
||||
static nlohmann::json paramValue(const nlohmann::json& v);
|
||||
void waitWhilePausedUnlocked();
|
||||
void sleepMs(int ms);
|
||||
};
|
||||
|
||||
} // namespace lm
|
||||
@@ -9,7 +9,10 @@
|
||||
|
||||
namespace lm {
|
||||
|
||||
ApiServer::ApiServer(StateRepository& repo) : repo_(repo) {}
|
||||
ApiServer::ApiServer(StateRepository& repo, MissionQueue& mission_queue)
|
||||
: repo_(repo), mission_queue_(mission_queue)
|
||||
{
|
||||
}
|
||||
|
||||
void ApiServer::registerRoutes(httplib::Server& svr)
|
||||
{
|
||||
@@ -436,6 +439,69 @@ void ApiServer::registerRoutes(httplib::Server& svr)
|
||||
return HttpUtil::jsonError(res, 500, "failed to save layout");
|
||||
res.status = 204;
|
||||
});
|
||||
|
||||
svr.Get("/api/mission_queue", [this](const httplib::Request&, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = nlohmann::json({{"queue", mission_queue_.list()}, {"runner", mission_queue_.runnerStatus()}}).dump();
|
||||
});
|
||||
|
||||
svr.Post("/api/mission_queue", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
nlohmann::json payload;
|
||||
try
|
||||
{
|
||||
payload = nlohmann::json::parse(req.body);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return HttpUtil::jsonError(res, 400, "invalid JSON");
|
||||
}
|
||||
std::string err;
|
||||
const auto entry = mission_queue_.enqueue(payload, err);
|
||||
if (!entry)
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.status = 201;
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = entry->dump();
|
||||
});
|
||||
|
||||
svr.Delete("/api/mission_queue", [this](const httplib::Request&, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
std::string err;
|
||||
if (!mission_queue_.clearAll(err))
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.status = 204;
|
||||
});
|
||||
|
||||
svr.Put("/api/mission_queue/reorder", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
nlohmann::json payload;
|
||||
try
|
||||
{
|
||||
payload = nlohmann::json::parse(req.body);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return HttpUtil::jsonError(res, 400, "invalid JSON");
|
||||
}
|
||||
if (!payload.contains("ordered_ids") || !payload["ordered_ids"].is_array())
|
||||
return HttpUtil::jsonError(res, 400, "ordered_ids is required");
|
||||
std::string err;
|
||||
if (!mission_queue_.reorder(payload["ordered_ids"], err))
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = nlohmann::json({{"ok", true}}).dump();
|
||||
});
|
||||
|
||||
svr.Delete(R"(/api/mission_queue/([0-9a-fA-F]+))", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
const std::string id = req.matches[1].str();
|
||||
std::string err;
|
||||
if (!mission_queue_.removeById(id, err))
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.status = 204;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace lm
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
#include "mission/mission_queue.hpp"
|
||||
#include "storage/state_repository.hpp"
|
||||
|
||||
namespace lm {
|
||||
@@ -9,12 +10,13 @@ namespace lm {
|
||||
class ApiServer
|
||||
{
|
||||
public:
|
||||
explicit ApiServer(StateRepository& repo);
|
||||
ApiServer(StateRepository& repo, MissionQueue& mission_queue);
|
||||
|
||||
void registerRoutes(httplib::Server& svr);
|
||||
|
||||
private:
|
||||
StateRepository& repo_;
|
||||
MissionQueue& mission_queue_;
|
||||
};
|
||||
|
||||
} // namespace lm
|
||||
|
||||
Reference in New Issue
Block a user