#include "server/api_server.hpp" #include "mission/mission_enqueue.hpp" #include "util/http_util.hpp" namespace lm { std::optional ApiServer::enqueueMission(const nlohmann::json& request, std::string& err) { nlohmann::json payload; if (!MissionEnqueue::buildPayload(mission_store_, request, payload, err)) return std::nullopt; return mission_queue_.enqueue(payload, err); } bool ApiServer::enqueueRequest(const nlohmann::json& request, httplib::Response& res, int status_code) { std::string err; const auto entry = enqueueMission(request, err); if (!entry) { HttpUtil::jsonError(res, 400, err); return false; } HttpUtil::addCors(res); res.status = status_code; res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = entry->dump(); return true; } nlohmann::json ApiServer::toMirQueueEntry(const nlohmann::json& entry) const { nlohmann::json out = nlohmann::json::object(); if (entry.contains("id")) out["id"] = entry["id"]; out["mission_id"] = entry.value("mission_id", std::string("")); out["state"] = entry.contains("status") ? entry["status"] : nlohmann::json("pending"); out["message"] = entry.value("mission_name", std::string("")); if (entry.contains("priority") && entry["priority"].is_number()) out["priority"] = entry["priority"]; else out["priority"] = 0; out["robot_id"] = entry.value("robot_id", std::string("default")); return out; } void ApiServer::registerMissionRoutes(httplib::Server& svr) { svr.Get("/api/missions", [this](const httplib::Request&, httplib::Response& res) { HttpUtil::addCors(res); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = mission_store_.snapshot().dump(); }); svr.Put("/api/missions", [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; if (!mission_store_.replace(payload, err)) return HttpUtil::jsonError(res, 400, err); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = mission_store_.snapshot().dump(); }); } void ApiServer::registerIntegrationRoutes(httplib::Server& svr) { svr.Get("/api/triggers", [this](const httplib::Request&, httplib::Response& res) { HttpUtil::addCors(res); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = mission_store_.listTriggers().dump(); }); svr.Post("/api/triggers", [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 trigger = mission_store_.addTrigger(payload, err); if (!trigger) return HttpUtil::jsonError(res, 400, err); res.status = 201; res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = trigger->dump(); }); svr.Delete(R"(/api/triggers/([0-9a-fA-F]+))", [this](const httplib::Request& req, httplib::Response& res) { HttpUtil::addCors(res); std::string err; if (!mission_store_.deleteTrigger(req.matches[1].str(), err)) return HttpUtil::jsonError(res, 400, err); res.status = 204; }); svr.Get("/api/modbus/coils", [this](const httplib::Request&, httplib::Response& res) { HttpUtil::addCors(res); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = modbus_.coilStates().dump(); }); svr.Put(R"(/api/modbus/coils/([0-9]+))", [this](const httplib::Request& req, httplib::Response& res) { HttpUtil::addCors(res); nlohmann::json payload; try { payload = nlohmann::json::parse(req.body.empty() ? "{}" : req.body); } catch (...) { return HttpUtil::jsonError(res, 400, "invalid JSON"); } const int coil_id = std::stoi(req.matches[1].str()); const bool value = !payload.contains("value") || payload["value"].get(); std::string err; if (!modbus_.writeCoil(coil_id, value, err)) return HttpUtil::jsonError(res, 400, err); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = nlohmann::json({{"coil_id", coil_id}, {"value", value}}).dump(); }); svr.Post(R"(/api/modbus/coils/([0-9]+)/trigger)", [this](const httplib::Request& req, httplib::Response& res) { HttpUtil::addCors(res); const int coil_id = std::stoi(req.matches[1].str()); std::string err; if (!modbus_.fireCoil(coil_id, err)) return HttpUtil::jsonError(res, 400, err); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = nlohmann::json({{"ok", true}, {"coil_id", coil_id}}).dump(); }); svr.Get("/api/fleet/robots", [this](const httplib::Request&, httplib::Response& res) { HttpUtil::addCors(res); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = mission_store_.listRobots().dump(); }); svr.Get("/api/fleet/schedules", [this](const httplib::Request&, httplib::Response& res) { HttpUtil::addCors(res); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = mission_store_.listSchedules().dump(); }); svr.Post("/api/fleet/schedules", [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 schedule = mission_store_.addSchedule(payload, err); if (!schedule) return HttpUtil::jsonError(res, 400, err); if (schedule->value("start_mode", "asap") == "asap" && schedule->value("enabled", true)) scheduler_.runScheduleNow(schedule->value("id", ""), err); res.status = 201; res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = schedule->dump(); }); svr.Put(R"(/api/fleet/schedules/([0-9a-fA-F]+))", [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; if (!mission_store_.updateSchedule(req.matches[1].str(), payload, 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/fleet/schedules/([0-9a-fA-F]+))", [this](const httplib::Request& req, httplib::Response& res) { HttpUtil::addCors(res); std::string err; if (!mission_store_.deleteSchedule(req.matches[1].str(), err)) return HttpUtil::jsonError(res, 400, err); res.status = 204; }); svr.Post(R"(/api/fleet/schedules/([0-9a-fA-F]+)/run)", [this](const httplib::Request& req, httplib::Response& res) { HttpUtil::addCors(res); std::string err; if (!scheduler_.runScheduleNow(req.matches[1].str(), err)) return HttpUtil::jsonError(res, 400, err); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = nlohmann::json({{"ok", true}}).dump(); }); } void ApiServer::registerMirV2Routes(httplib::Server& svr) { svr.Get("/api/v2.0.0/missions", [this](const httplib::Request&, httplib::Response& res) { HttpUtil::addCors(res); res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = mission_store_.listMissions().dump(); }); svr.Get("/api/v2.0.0/mission_queue", [this](const httplib::Request&, httplib::Response& res) { HttpUtil::addCors(res); nlohmann::json out = nlohmann::json::array(); const nlohmann::json queue = mission_queue_.list(); if (queue.is_array()) { for (const auto& item : queue) { if (item.is_object()) out.push_back(toMirQueueEntry(item)); } } res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = out.dump(); }); svr.Post("/api/v2.0.0/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"); } if (!payload.contains("source")) payload["source"] = "rest_api_v2"; std::string err; const auto entry = enqueueMission(payload, err); if (!entry) return HttpUtil::jsonError(res, 400, err); HttpUtil::addCors(res); res.status = 201; res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = toMirQueueEntry(*entry).dump(); }); svr.Delete("/api/v2.0.0/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.Get("/api/v2.0.0/status", [this](const httplib::Request&, httplib::Response& res) { HttpUtil::addCors(res); const auto runner = mission_queue_.runnerStatus(); nlohmann::json body = {{"state_id", runner.value("state", "idle") == "running" ? 3 : runner.value("state", "") == "paused" ? 4 : 1}, {"state_text", runner.value("state", "idle")}, {"message", runner.value("message", "")}}; res.set_header("Content-Type", "application/json; charset=utf-8"); res.body = body.dump(); }); } } // namespace lm