add top bar
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
#include "mission/mission_scheduler.hpp"
|
||||
#include "mission/mission_store.hpp"
|
||||
#include "mission/modbus_trigger_service.hpp"
|
||||
#include "robot/robot_runtime.hpp"
|
||||
#include "server/api_server.hpp"
|
||||
#include "server/static_file_server.hpp"
|
||||
#include "storage/state_repository.hpp"
|
||||
@@ -31,6 +32,7 @@ int LidarManagerApp::run()
|
||||
const std::filesystem::path missions_store_path = data_path_.parent_path() / "missions.json";
|
||||
MissionQueue mission_queue(mission_queue_path);
|
||||
MissionStore mission_store(missions_store_path);
|
||||
RobotRuntime robot_runtime(data_path_.parent_path() / "robot_runtime.json", mission_queue);
|
||||
|
||||
const auto enqueue_fn = [&mission_store, &mission_queue](const nlohmann::json& request, std::string& err) -> bool {
|
||||
nlohmann::json payload;
|
||||
@@ -49,7 +51,7 @@ int LidarManagerApp::run()
|
||||
return auth.preRoute(req, res);
|
||||
});
|
||||
|
||||
ApiServer api(repo, mission_queue, mission_store, modbus, scheduler);
|
||||
ApiServer api(repo, mission_queue, mission_store, modbus, scheduler, robot_runtime);
|
||||
api.registerRoutes(svr);
|
||||
auth.registerRoutes(svr);
|
||||
StaticFileServer::mount(svr, www_root_);
|
||||
|
||||
@@ -157,7 +157,8 @@ std::optional<std::string> AuthService::resourceForApiPath(const std::string& pa
|
||||
return std::nullopt;
|
||||
if (path.rfind("/api/users", 0) == 0 || path.rfind("/api/user_groups", 0) == 0)
|
||||
return "users";
|
||||
if (path.rfind("/api/missions", 0) == 0 || path.rfind("/api/mission_queue", 0) == 0)
|
||||
if (path.rfind("/api/missions", 0) == 0 || path.rfind("/api/mission_queue", 0) == 0 ||
|
||||
path.rfind("/api/robot", 0) == 0)
|
||||
return "missions";
|
||||
if (path.rfind("/api/triggers", 0) == 0 || path.rfind("/api/schedules", 0) == 0 ||
|
||||
path.rfind("/api/robots", 0) == 0 || path.rfind("/api/fleet", 0) == 0 ||
|
||||
@@ -410,6 +411,47 @@ bool AuthService::changePassword(const std::string& token,
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<nlohmann::json> AuthService::changeProfile(const std::string& token,
|
||||
const nlohmann::json& payload,
|
||||
std::string& err)
|
||||
{
|
||||
if (!payload.is_object())
|
||||
{
|
||||
err = "invalid payload";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
const auto it = sessions_.find(token);
|
||||
if (it == sessions_.end())
|
||||
{
|
||||
err = "not authenticated";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
for (auto& user : data_["users"])
|
||||
{
|
||||
if (user.value("id", "") != it->second.user_id)
|
||||
continue;
|
||||
if (payload.contains("display_name") && payload["display_name"].is_string())
|
||||
{
|
||||
const std::string name = StringUtil::trimCopy(payload["display_name"].get<std::string>());
|
||||
if (name.empty())
|
||||
{
|
||||
err = "display_name cannot be empty";
|
||||
return std::nullopt;
|
||||
}
|
||||
user["display_name"] = name;
|
||||
}
|
||||
saveUnlocked();
|
||||
const auto* group = findGroupByIdUnlocked(user.value("group_id", ""));
|
||||
return userPublicView(user, group ? *group : nlohmann::json::object());
|
||||
}
|
||||
|
||||
err = "user not found";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
nlohmann::json AuthService::listGroups() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
@@ -704,6 +746,31 @@ void AuthService::registerRoutes(httplib::Server& svr)
|
||||
HttpUtil::addCors(res);
|
||||
});
|
||||
|
||||
svr.Put("/api/auth/profile", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
nlohmann::json body;
|
||||
try
|
||||
{
|
||||
body = nlohmann::json::parse(req.body);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
HttpUtil::jsonError(res, 400, "invalid json");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string token = extractToken(req);
|
||||
std::string err;
|
||||
const auto profile = changeProfile(token, body, err);
|
||||
if (!profile)
|
||||
{
|
||||
HttpUtil::jsonError(res, 400, err);
|
||||
return;
|
||||
}
|
||||
nlohmann::json out = {{"user", *profile}};
|
||||
res.set_content(out.dump(), "application/json; charset=utf-8");
|
||||
HttpUtil::addCors(res);
|
||||
});
|
||||
|
||||
svr.Get("/api/user_groups", [this](const httplib::Request&, httplib::Response& res) {
|
||||
nlohmann::json out = {{"groups", listGroups()}};
|
||||
res.set_content(out.dump(), "application/json; charset=utf-8");
|
||||
|
||||
@@ -40,6 +40,9 @@ public:
|
||||
const std::string& current_password,
|
||||
const std::string& new_password,
|
||||
std::string& err);
|
||||
std::optional<nlohmann::json> changeProfile(const std::string& token,
|
||||
const nlohmann::json& payload,
|
||||
std::string& err);
|
||||
|
||||
nlohmann::json listGroups() const;
|
||||
nlohmann::json listUsers() const;
|
||||
|
||||
265
src/robot/robot_runtime.cpp
Normal file
265
src/robot/robot_runtime.cpp
Normal file
@@ -0,0 +1,265 @@
|
||||
#include "robot/robot_runtime.hpp"
|
||||
|
||||
#include "mission/mission_queue.hpp"
|
||||
#include "util/file_util.hpp"
|
||||
#include "util/id_util.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
namespace lm {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kDefaultMessage = "Waiting for new missions...";
|
||||
|
||||
} // namespace
|
||||
|
||||
RobotRuntime::RobotRuntime(std::filesystem::path runtime_path, MissionQueue& mission_queue)
|
||||
: runtime_path_(std::move(runtime_path)), mission_queue_(mission_queue)
|
||||
{
|
||||
load();
|
||||
ensureDefaultsUnlocked();
|
||||
}
|
||||
|
||||
void RobotRuntime::load()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
state_ = nlohmann::json::object();
|
||||
if (!std::filesystem::exists(runtime_path_))
|
||||
return;
|
||||
try
|
||||
{
|
||||
const auto parsed = nlohmann::json::parse(FileUtil::readBinary(runtime_path_));
|
||||
if (parsed.is_object())
|
||||
state_ = parsed;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
state_ = nlohmann::json::object();
|
||||
}
|
||||
}
|
||||
|
||||
void RobotRuntime::saveUnlocked() const
|
||||
{
|
||||
FileUtil::writeBinaryAtomic(runtime_path_, state_.dump(2));
|
||||
}
|
||||
|
||||
void RobotRuntime::ensureDefaultsUnlocked()
|
||||
{
|
||||
if (!state_.is_object())
|
||||
state_ = nlohmann::json::object();
|
||||
if (!state_.contains("motion"))
|
||||
state_["motion"] = "paused";
|
||||
if (!state_.contains("health"))
|
||||
state_["health"] = "ok";
|
||||
if (!state_.contains("message"))
|
||||
state_["message"] = kDefaultMessage;
|
||||
if (!state_.contains("error"))
|
||||
state_["error"] = nullptr;
|
||||
if (!state_.contains("battery_percent"))
|
||||
state_["battery_percent"] = 54;
|
||||
if (!state_.contains("battery_charging"))
|
||||
state_["battery_charging"] = false;
|
||||
if (!state_.contains("joystick_engaged"))
|
||||
state_["joystick_engaged"] = false;
|
||||
if (!state_.contains("joystick_speed"))
|
||||
state_["joystick_speed"] = "fast";
|
||||
if (!state_.contains("cmd_linear"))
|
||||
state_["cmd_linear"] = 0.0;
|
||||
if (!state_.contains("cmd_angular"))
|
||||
state_["cmd_angular"] = 0.0;
|
||||
if (!state_.contains("updated_at"))
|
||||
state_["updated_at"] = IdUtil::nowIso8601();
|
||||
saveUnlocked();
|
||||
}
|
||||
|
||||
nlohmann::json RobotRuntime::buildStatusUnlocked() const
|
||||
{
|
||||
const auto runner = mission_queue_.runnerStatus();
|
||||
const auto queue = mission_queue_.list();
|
||||
|
||||
int pending = 0;
|
||||
if (queue.is_array())
|
||||
{
|
||||
for (const auto& item : queue)
|
||||
{
|
||||
if (item.value("status", "") == "pending")
|
||||
++pending;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string motion = state_.value("motion", "paused");
|
||||
const std::string health = state_.value("health", "ok");
|
||||
const std::string runner_state = runner.value("state", "idle");
|
||||
|
||||
std::string message = state_.value("message", kDefaultMessage);
|
||||
if (health == "ok")
|
||||
{
|
||||
if (!runner.value("message", "").empty())
|
||||
message = runner.value("message", "");
|
||||
else if (runner_state == "idle" && motion == "paused" && pending == 0)
|
||||
message = kDefaultMessage;
|
||||
}
|
||||
|
||||
nlohmann::json body = {{"motion", motion},
|
||||
{"health", health},
|
||||
{"message", message},
|
||||
{"error", state_.contains("error") ? state_["error"] : nullptr},
|
||||
{"battery_percent", state_.value("battery_percent", 54)},
|
||||
{"battery_charging", state_.value("battery_charging", false)},
|
||||
{"joystick_engaged", state_.value("joystick_engaged", false)},
|
||||
{"joystick_speed", state_.value("joystick_speed", "fast")},
|
||||
{"cmd_linear", state_.value("cmd_linear", 0.0)},
|
||||
{"cmd_angular", state_.value("cmd_angular", 0.0)},
|
||||
{"runner", runner},
|
||||
{"queue_pending", pending},
|
||||
{"updated_at", state_.value("updated_at", "")}};
|
||||
return body;
|
||||
}
|
||||
|
||||
nlohmann::json RobotRuntime::status() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return buildStatusUnlocked();
|
||||
}
|
||||
|
||||
bool RobotRuntime::start(std::string& err)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (state_.value("health", "ok") == "error")
|
||||
{
|
||||
err = "cannot start while robot is in error state — reset error first";
|
||||
return false;
|
||||
}
|
||||
|
||||
state_["motion"] = "running";
|
||||
state_["message"] = "Robot running";
|
||||
state_["updated_at"] = IdUtil::nowIso8601();
|
||||
saveUnlocked();
|
||||
|
||||
const std::string runner_state = mission_queue_.runnerStatus().value("state", "idle");
|
||||
if (runner_state == "paused")
|
||||
return mission_queue_.resume(err);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RobotRuntime::pause(std::string& err)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
state_["motion"] = "paused";
|
||||
state_["message"] = "Robot paused";
|
||||
state_["joystick_engaged"] = false;
|
||||
state_["cmd_linear"] = 0.0;
|
||||
state_["cmd_angular"] = 0.0;
|
||||
state_["updated_at"] = IdUtil::nowIso8601();
|
||||
saveUnlocked();
|
||||
|
||||
const std::string runner_state = mission_queue_.runnerStatus().value("state", "idle");
|
||||
if (runner_state == "running" || runner_state == "paused")
|
||||
return mission_queue_.pause(err);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RobotRuntime::resetError(std::string& err)
|
||||
{
|
||||
(void)err;
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
state_["health"] = "ok";
|
||||
state_["error"] = nullptr;
|
||||
state_["message"] = kDefaultMessage;
|
||||
state_["updated_at"] = IdUtil::nowIso8601();
|
||||
saveUnlocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RobotRuntime::setCmdVel(double linear, double angular, std::string& err)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (state_.value("health", "ok") == "error")
|
||||
{
|
||||
err = "robot is in error state";
|
||||
return false;
|
||||
}
|
||||
if (!state_.value("joystick_engaged", false))
|
||||
{
|
||||
err = "joystick is not engaged";
|
||||
return false;
|
||||
}
|
||||
if (state_.value("motion", "paused") != "running")
|
||||
{
|
||||
err = "robot motion is paused — press START first";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string speed = state_.value("joystick_speed", "fast");
|
||||
double scale = 1.0;
|
||||
if (speed == "medium")
|
||||
scale = 0.55;
|
||||
else if (speed == "slow")
|
||||
scale = 0.25;
|
||||
|
||||
linear = std::clamp(linear, -1.0, 1.0) * scale;
|
||||
angular = std::clamp(angular, -1.0, 1.0) * scale;
|
||||
|
||||
state_["cmd_linear"] = linear;
|
||||
state_["cmd_angular"] = angular;
|
||||
state_["updated_at"] = IdUtil::nowIso8601();
|
||||
saveUnlocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RobotRuntime::setJoystick(bool engaged, const std::string& speed, std::string& err)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (engaged && state_.value("health", "ok") == "error")
|
||||
{
|
||||
err = "cannot engage joystick while robot is in error state";
|
||||
return false;
|
||||
}
|
||||
if (engaged && state_.value("motion", "paused") != "running")
|
||||
{
|
||||
err = "start the robot before engaging the joystick";
|
||||
return false;
|
||||
}
|
||||
|
||||
state_["joystick_engaged"] = engaged;
|
||||
if (!speed.empty())
|
||||
{
|
||||
if (speed != "fast" && speed != "medium" && speed != "slow")
|
||||
{
|
||||
err = "speed must be fast, medium, or slow";
|
||||
return false;
|
||||
}
|
||||
state_["joystick_speed"] = speed;
|
||||
}
|
||||
if (!engaged)
|
||||
{
|
||||
state_["cmd_linear"] = 0.0;
|
||||
state_["cmd_angular"] = 0.0;
|
||||
}
|
||||
state_["updated_at"] = IdUtil::nowIso8601();
|
||||
saveUnlocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RobotRuntime::tick()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
const bool running = state_.value("motion", "paused") == "running";
|
||||
const bool joy = state_.value("joystick_engaged", false);
|
||||
double battery = state_.value("battery_percent", 54.0);
|
||||
|
||||
if (running && (joy || std::abs(state_.value("cmd_linear", 0.0)) > 0.01))
|
||||
battery = std::max(0.0, battery - 0.02);
|
||||
else if (state_.value("battery_charging", false))
|
||||
battery = std::min(100.0, battery + 0.05);
|
||||
else if (!running)
|
||||
battery = std::min(100.0, battery + 0.005);
|
||||
|
||||
state_["battery_percent"] = static_cast<int>(std::lround(battery));
|
||||
saveUnlocked();
|
||||
}
|
||||
|
||||
} // namespace lm
|
||||
38
src/robot/robot_runtime.hpp
Normal file
38
src/robot/robot_runtime.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace lm {
|
||||
|
||||
class MissionQueue;
|
||||
|
||||
class RobotRuntime
|
||||
{
|
||||
public:
|
||||
explicit RobotRuntime(std::filesystem::path runtime_path, MissionQueue& mission_queue);
|
||||
|
||||
nlohmann::json status() const;
|
||||
bool start(std::string& err);
|
||||
bool pause(std::string& err);
|
||||
bool resetError(std::string& err);
|
||||
bool setCmdVel(double linear, double angular, std::string& err);
|
||||
bool setJoystick(bool engaged, const std::string& speed, std::string& err);
|
||||
void tick();
|
||||
|
||||
private:
|
||||
std::filesystem::path runtime_path_;
|
||||
MissionQueue& mission_queue_;
|
||||
mutable std::mutex mu_;
|
||||
nlohmann::json state_;
|
||||
|
||||
void load();
|
||||
void saveUnlocked() const;
|
||||
void ensureDefaultsUnlocked();
|
||||
nlohmann::json buildStatusUnlocked() const;
|
||||
};
|
||||
|
||||
} // namespace lm
|
||||
85
src/server/api_robot_routes.cpp
Normal file
85
src/server/api_robot_routes.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#include "server/api_server.hpp"
|
||||
|
||||
#include "util/http_util.hpp"
|
||||
|
||||
namespace lm {
|
||||
|
||||
void ApiServer::registerRobotRoutes(httplib::Server& svr)
|
||||
{
|
||||
svr.Get("/api/robot/status", [this](const httplib::Request&, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
robot_runtime_.tick();
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = robot_runtime_.status().dump();
|
||||
});
|
||||
|
||||
svr.Post("/api/robot/start", [this](const httplib::Request&, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
std::string err;
|
||||
if (!robot_runtime_.start(err))
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = robot_runtime_.status().dump();
|
||||
});
|
||||
|
||||
svr.Post("/api/robot/pause", [this](const httplib::Request&, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
std::string err;
|
||||
if (!robot_runtime_.pause(err))
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = robot_runtime_.status().dump();
|
||||
});
|
||||
|
||||
svr.Post("/api/robot/errors/reset", [this](const httplib::Request&, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
std::string err;
|
||||
if (!robot_runtime_.resetError(err))
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = robot_runtime_.status().dump();
|
||||
});
|
||||
|
||||
svr.Post("/api/robot/cmd_vel", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
nlohmann::json body;
|
||||
try
|
||||
{
|
||||
body = nlohmann::json::parse(req.body);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return HttpUtil::jsonError(res, 400, "invalid JSON");
|
||||
}
|
||||
const double linear = body.value("linear", body.value("vx", 0.0));
|
||||
const double angular = body.value("angular", body.value("omega", 0.0));
|
||||
std::string err;
|
||||
if (!robot_runtime_.setCmdVel(linear, angular, err))
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = robot_runtime_.status().dump();
|
||||
});
|
||||
|
||||
svr.Post("/api/robot/joystick", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
HttpUtil::addCors(res);
|
||||
nlohmann::json body;
|
||||
try
|
||||
{
|
||||
body = nlohmann::json::parse(req.body);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return HttpUtil::jsonError(res, 400, "invalid JSON");
|
||||
}
|
||||
if (!body.contains("engaged") || !body["engaged"].is_boolean())
|
||||
return HttpUtil::jsonError(res, 400, "engaged (boolean) is required");
|
||||
const std::string speed = body.value("speed", "");
|
||||
std::string err;
|
||||
if (!robot_runtime_.setJoystick(body["engaged"].get<bool>(), speed, err))
|
||||
return HttpUtil::jsonError(res, 400, err);
|
||||
res.set_header("Content-Type", "application/json; charset=utf-8");
|
||||
res.body = robot_runtime_.status().dump();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace lm
|
||||
@@ -14,12 +14,14 @@ ApiServer::ApiServer(StateRepository& repo,
|
||||
MissionQueue& mission_queue,
|
||||
MissionStore& mission_store,
|
||||
ModbusTriggerService& modbus,
|
||||
MissionScheduler& scheduler)
|
||||
MissionScheduler& scheduler,
|
||||
RobotRuntime& robot_runtime)
|
||||
: repo_(repo),
|
||||
mission_queue_(mission_queue),
|
||||
mission_store_(mission_store),
|
||||
modbus_(modbus),
|
||||
scheduler_(scheduler)
|
||||
scheduler_(scheduler),
|
||||
robot_runtime_(robot_runtime)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -538,6 +540,7 @@ void ApiServer::registerRoutes(httplib::Server& svr)
|
||||
registerMissionRoutes(svr);
|
||||
registerIntegrationRoutes(svr);
|
||||
registerMirV2Routes(svr);
|
||||
registerRobotRoutes(svr);
|
||||
}
|
||||
|
||||
} // namespace lm
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "mission/mission_scheduler.hpp"
|
||||
#include "mission/mission_store.hpp"
|
||||
#include "mission/modbus_trigger_service.hpp"
|
||||
#include "robot/robot_runtime.hpp"
|
||||
#include "storage/state_repository.hpp"
|
||||
|
||||
namespace lm {
|
||||
@@ -17,7 +18,8 @@ public:
|
||||
MissionQueue& mission_queue,
|
||||
MissionStore& mission_store,
|
||||
ModbusTriggerService& modbus,
|
||||
MissionScheduler& scheduler);
|
||||
MissionScheduler& scheduler,
|
||||
RobotRuntime& robot_runtime);
|
||||
|
||||
void registerRoutes(httplib::Server& svr);
|
||||
|
||||
@@ -27,6 +29,7 @@ private:
|
||||
MissionStore& mission_store_;
|
||||
ModbusTriggerService& modbus_;
|
||||
MissionScheduler& scheduler_;
|
||||
RobotRuntime& robot_runtime_;
|
||||
|
||||
bool enqueueRequest(const nlohmann::json& request, httplib::Response& res, int status_code = 201);
|
||||
std::optional<nlohmann::json> enqueueMission(const nlohmann::json& request, std::string& err);
|
||||
@@ -34,6 +37,7 @@ private:
|
||||
void registerMissionRoutes(httplib::Server& svr);
|
||||
void registerMirV2Routes(httplib::Server& svr);
|
||||
void registerIntegrationRoutes(httplib::Server& svr);
|
||||
void registerRobotRoutes(httplib::Server& svr);
|
||||
};
|
||||
|
||||
} // namespace lm
|
||||
|
||||
Reference in New Issue
Block a user