[base] hail vibe-coding
Minimal API functionality with unit tests, could be used for integration tests with Unity server. For internal testing used RPC_server implementation
This commit is contained in:
commit
cbe56d9193
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
build/
|
||||
.cache/
|
||||
subprojects/asio-1.36.0/
|
||||
subprojects/glog/
|
||||
subprojects/googletest-*
|
||||
subprojects/nlohmann_json/
|
||||
subprojects/packagecache/
|
||||
subprojects/yaml-cpp-0.8.0
|
||||
125
API.md
Normal file
125
API.md
Normal file
@ -0,0 +1,125 @@
|
||||
# JSON-RPC API Documentation
|
||||
|
||||
The Cloud Point RPC server implements the **JSON-RPC 2.0** protocol over TCP.
|
||||
|
||||
## General Format
|
||||
|
||||
All requests and responses are JSON objects.
|
||||
|
||||
### Request
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "<method_name>",
|
||||
"params": {},
|
||||
"id": <integer|string>
|
||||
}
|
||||
```
|
||||
*Note: `params` is currently ignored by the implemented methods but is part of the standard.*
|
||||
|
||||
### Response (Success)
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": <method_specific_result>,
|
||||
"id": <matching_request_id>
|
||||
}
|
||||
```
|
||||
|
||||
### Response (Error)
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": <integer>,
|
||||
"message": "<string>"
|
||||
},
|
||||
"id": <matching_request_id>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Methods
|
||||
|
||||
### `get-intrinsic-params`
|
||||
|
||||
Retrieves the intrinsic camera parameters as a flat 3x3 matrix (row-major).
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get-intrinsic-params",
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": [
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 1.0
|
||||
],
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
*Type: `vector<double>` (size 9)*
|
||||
|
||||
### `get-extrinsic-params`
|
||||
|
||||
Retrieves the extrinsic camera parameters as a flat 4x4 matrix (row-major).
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get-extrinsic-params",
|
||||
"id": 2
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": [
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0
|
||||
],
|
||||
"id": 2
|
||||
}
|
||||
```
|
||||
*Type: `vector<double>` (size 16)*
|
||||
|
||||
### `get-cloud-point`
|
||||
|
||||
Retrieves the current field of view point cloud.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "get-cloud-point",
|
||||
"id": 3
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"result": [
|
||||
[0.1, 0.2, 0.3],
|
||||
[1.1, 1.2, 1.3],
|
||||
[5.5, 6.6, 7.7]
|
||||
],
|
||||
"id": 3
|
||||
}
|
||||
```
|
||||
*Type: `vector<vector<double>>` (List of [x, y, z] points)*
|
||||
40
Dockerfile
Normal file
40
Dockerfile
Normal file
@ -0,0 +1,40 @@
|
||||
# Use Ubuntu 24.04 as base (matching development environment)
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# Avoid interactive prompts during package installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install dependencies
|
||||
# - build-essential: Compiler (gcc/g++)
|
||||
# - meson/ninja-build: Build system
|
||||
# - git: For fetching subprojects
|
||||
# - pkg-config, cmake: For dependency resolution
|
||||
# - libssl-dev: Often needed for cmake fetches/networking
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
meson \
|
||||
ninja-build \
|
||||
git \
|
||||
pkg-config \
|
||||
cmake \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Setup build directory and compile
|
||||
# We allow git to fetch subprojects (glog, gtest, asio, etc.)
|
||||
RUN meson setup build && \
|
||||
meson compile -C build
|
||||
|
||||
# Expose the default port (can be changed in config.yaml)
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the server by default
|
||||
# We assume the config.yaml is in the root /app or we copy it.
|
||||
# The build output is in build/src/cloud_point_rpc_server
|
||||
CMD ["./build/src/cloud_point_rpc_server", "config.yaml"]
|
||||
56
README.md
Normal file
56
README.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Cloud Point RPC
|
||||
|
||||
Communication JSON RPC protocol and implementation with Unity Scene.
|
||||
|
||||
## API Documentation
|
||||
|
||||
See [API.md](API.md) for detailed request/response formats.
|
||||
|
||||
## Development
|
||||
|
||||
The project uses **Meson** build system and **C++20**.
|
||||
|
||||
### Dependencies
|
||||
- Meson, Ninja
|
||||
- GCC/Clang (C++20 support)
|
||||
- Git (for subprojects)
|
||||
|
||||
### Build & Run
|
||||
```bash
|
||||
meson setup build
|
||||
meson compile -C build
|
||||
./build/src/cloud_point_rpc_server config.yaml
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
meson test -C build -v
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
You can build and run the server using Docker.
|
||||
|
||||
### 1. Build Image
|
||||
```bash
|
||||
docker build -t cloud-point-rpc .
|
||||
```
|
||||
|
||||
### 2. Run Container
|
||||
The server runs on port **8080** by default (defined in `config.yaml` inside the image).
|
||||
|
||||
**Option A: Port Mapping**
|
||||
```bash
|
||||
docker run -p 8080:8080 cloud-point-rpc
|
||||
```
|
||||
|
||||
**Option B: Host Network (For Simplicity/Performance)**
|
||||
```bash
|
||||
docker run --network host cloud-point-rpc
|
||||
```
|
||||
|
||||
**Custom Configuration:**
|
||||
You can mount your own `config.yaml` to override the default settings:
|
||||
```bash
|
||||
docker run -p 8080:8080 -v $(pwd)/my_config.yaml:/app/config.yaml cloud-point-rpc
|
||||
```
|
||||
11
config.yaml
Normal file
11
config.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
server:
|
||||
ip: "127.0.0.1"
|
||||
port: 8085
|
||||
|
||||
test_data:
|
||||
intrinsic_params: [1.1, 0.0, 0.0, 0.0, 1.1, 0.0, 0.0, 0.0, 1.0]
|
||||
extrinsic_params: [1.0, 0.0, 0.0, 0.5, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]
|
||||
cloud_point:
|
||||
- [0.1, 0.2, 0.3]
|
||||
- [1.1, 1.2, 1.3]
|
||||
- [5.5, 6.6, 7.7]
|
||||
66
include/cloud_point_rpc/config.hpp
Normal file
66
include/cloud_point_rpc/config.hpp
Normal file
@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
#include <glog/logging.h>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
struct ServerConfig {
|
||||
std::string ip;
|
||||
int port;
|
||||
};
|
||||
|
||||
struct TestData {
|
||||
std::vector<double> intrinsic_params;
|
||||
std::vector<double> extrinsic_params;
|
||||
std::vector<std::vector<double>> cloud_point;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
ServerConfig server;
|
||||
TestData test_data;
|
||||
};
|
||||
|
||||
class ConfigLoader {
|
||||
public:
|
||||
static Config load(const std::string& path) {
|
||||
try {
|
||||
YAML::Node config = YAML::LoadFile(path);
|
||||
Config c;
|
||||
|
||||
// Server
|
||||
if (config["server"]) {
|
||||
c.server.ip = config["server"]["ip"].as<std::string>("127.0.0.1");
|
||||
c.server.port = config["server"]["port"].as<int>(8080);
|
||||
} else {
|
||||
c.server.ip = "127.0.0.1";
|
||||
c.server.port = 8080;
|
||||
LOG(WARNING) << "No 'server' section, using defaults.";
|
||||
}
|
||||
|
||||
// Test Data
|
||||
if (config["test_data"]) {
|
||||
c.test_data.intrinsic_params = config["test_data"]["intrinsic_params"].as<std::vector<double>>();
|
||||
c.test_data.extrinsic_params = config["test_data"]["extrinsic_params"].as<std::vector<double>>();
|
||||
|
||||
// Parse cloud_point (list of lists)
|
||||
auto cp_node = config["test_data"]["cloud_point"];
|
||||
for (const auto& point_node : cp_node) {
|
||||
c.test_data.cloud_point.push_back(point_node.as<std::vector<double>>());
|
||||
}
|
||||
} else {
|
||||
LOG(WARNING) << "No 'test_data' section, using empty/defaults.";
|
||||
}
|
||||
|
||||
return c;
|
||||
} catch (const YAML::Exception& e) {
|
||||
LOG(ERROR) << "Failed to load config: " << e.what();
|
||||
throw std::runtime_error("Config load failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace cloud_point_rpc
|
||||
80
include/cloud_point_rpc/rpc_client.hpp
Normal file
80
include/cloud_point_rpc/rpc_client.hpp
Normal file
@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include <glog/logging.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <asio.hpp>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
class RpcClient {
|
||||
public:
|
||||
RpcClient() : socket_(io_context_) {}
|
||||
|
||||
void connect(const std::string& ip, int port) {
|
||||
try {
|
||||
LOG(INFO) << "Client connecting to " << ip << ":" << port;
|
||||
asio::ip::tcp::endpoint endpoint(asio::ip::make_address(ip), port);
|
||||
socket_.connect(endpoint);
|
||||
LOG(INFO) << "Client connected";
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(std::string("Connection Failed: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<double> get_intrinsic_params() {
|
||||
return call_method<std::vector<double>>("get-intrinsic-params");
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<double> get_extrinsic_params() {
|
||||
return call_method<std::vector<double>>("get-extrinsic-params");
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<std::vector<double>> get_cloud_point() {
|
||||
return call_method<std::vector<std::vector<double>>>("get-cloud-point");
|
||||
}
|
||||
|
||||
~RpcClient() {
|
||||
// Socket closes automatically
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename T>
|
||||
T call_method(const std::string& method) {
|
||||
using json = nlohmann::json;
|
||||
|
||||
// Create Request
|
||||
json request = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"method", method},
|
||||
{"id", ++id_counter_}
|
||||
};
|
||||
std::string request_str = request.dump();
|
||||
|
||||
// Send
|
||||
LOG(INFO) << "Client sending: " << request_str;
|
||||
asio::write(socket_, asio::buffer(request_str));
|
||||
|
||||
// Read Response
|
||||
LOG(INFO) << "Client reading response...";
|
||||
char buffer[4096];
|
||||
size_t len = socket_.read_some(asio::buffer(buffer, 4096));
|
||||
LOG(INFO) << "Client read " << len << " bytes";
|
||||
|
||||
json response = json::parse(std::string(buffer, len));
|
||||
|
||||
if (response.contains("error")) {
|
||||
throw std::runtime_error(response["error"]["message"].get<std::string>());
|
||||
}
|
||||
|
||||
return response["result"].get<T>();
|
||||
}
|
||||
|
||||
asio::io_context io_context_;
|
||||
asio::ip::tcp::socket socket_;
|
||||
int id_counter_ = 0;
|
||||
};
|
||||
|
||||
} // namespace cloud_point_rpc
|
||||
21
include/cloud_point_rpc/rpc_server.hpp
Normal file
21
include/cloud_point_rpc/rpc_server.hpp
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
class RpcServer {
|
||||
public:
|
||||
using Handler = std::function<nlohmann::json(const nlohmann::json&)>;
|
||||
|
||||
void register_method(const std::string& name, Handler handler);
|
||||
[[nodiscard]] std::string process(const std::string& request_str);
|
||||
|
||||
private:
|
||||
std::map<std::string, Handler> handlers_;
|
||||
};
|
||||
|
||||
} // namespace cloud_point_rpc
|
||||
20
include/cloud_point_rpc/service.hpp
Normal file
20
include/cloud_point_rpc/service.hpp
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "cloud_point_rpc/config.hpp"
|
||||
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
class Service {
|
||||
public:
|
||||
explicit Service(const TestData& data = {});
|
||||
|
||||
[[nodiscard]] std::vector<double> get_intrinsic_params() const;
|
||||
[[nodiscard]] std::vector<double> get_extrinsic_params() const;
|
||||
[[nodiscard]] std::vector<std::vector<double>> get_cloud_point() const;
|
||||
|
||||
private:
|
||||
TestData data_;
|
||||
};
|
||||
|
||||
} // namespace cloud_point_rpc
|
||||
129
include/cloud_point_rpc/tcp_server.hpp
Normal file
129
include/cloud_point_rpc/tcp_server.hpp
Normal file
@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <glog/logging.h>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <asio.hpp>
|
||||
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
class TcpServer {
|
||||
public:
|
||||
using RequestProcessor = std::function<std::string(const std::string&)>;
|
||||
|
||||
TcpServer(const std::string& ip, int port, RequestProcessor processor)
|
||||
: ip_(ip), port_(port), processor_(std::move(processor)),
|
||||
acceptor_(io_context_), running_(false) {}
|
||||
|
||||
~TcpServer() { stop(); }
|
||||
|
||||
void start() {
|
||||
try {
|
||||
asio::ip::tcp::endpoint endpoint(asio::ip::make_address(ip_), port_);
|
||||
acceptor_.open(endpoint.protocol());
|
||||
acceptor_.set_option(asio::ip::tcp::acceptor::reuse_address(true));
|
||||
acceptor_.bind(endpoint);
|
||||
acceptor_.listen();
|
||||
|
||||
running_ = true;
|
||||
LOG(INFO) << "Server listening on " << ip_ << ":" << port_;
|
||||
|
||||
accept_thread_ = std::jthread([this]() {
|
||||
LOG(INFO) << "Accept thread started";
|
||||
while (running_) {
|
||||
try {
|
||||
auto socket = std::make_shared<asio::ip::tcp::socket>(io_context_);
|
||||
acceptor_.accept(*socket);
|
||||
|
||||
LOG(INFO) << "New connection from " << socket->remote_endpoint().address().to_string();
|
||||
|
||||
std::jthread([this, socket]() {
|
||||
handle_client(socket);
|
||||
}).detach();
|
||||
} catch (const std::system_error& e) {
|
||||
LOG(INFO) << "Accept exception: " << e.what();
|
||||
if (running_) {
|
||||
LOG(WARNING) << "Accept failed: " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG(INFO) << "Accept thread exiting";
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
LOG(ERROR) << "Server start failed: " << e.what();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (!running_) return;
|
||||
LOG(INFO) << "Stopping server...";
|
||||
running_ = false;
|
||||
// Closing acceptor unblocks accept() call usually, but sometimes we need to prod it
|
||||
asio::error_code ec;
|
||||
acceptor_.close(ec);
|
||||
|
||||
// Ensure accept unblocks by connecting a dummy socket
|
||||
try {
|
||||
asio::ip::tcp::endpoint endpoint(asio::ip::make_address(ip_), port_);
|
||||
asio::ip::tcp::socket dummy_sock(io_context_);
|
||||
asio::error_code connect_ec;
|
||||
dummy_sock.connect(endpoint, connect_ec);
|
||||
} catch (...) {
|
||||
// Ignore
|
||||
}
|
||||
LOG(INFO) << "Acceptor closed";
|
||||
}
|
||||
|
||||
void join() {
|
||||
if (accept_thread_.joinable()) {
|
||||
LOG(INFO) << "Joining accept thread...";
|
||||
accept_thread_.join();
|
||||
LOG(INFO) << "Accept thread joined";
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void handle_client(std::shared_ptr<asio::ip::tcp::socket> socket) {
|
||||
try {
|
||||
asio::streambuf buffer;
|
||||
// Read until newline or EOF/error
|
||||
// Note: This matches the client implementation which should send a newline
|
||||
// However, previous implementation read 4096 bytes raw.
|
||||
// Let's emulate "read some" to match previous simple behavior, or use read_until if we enforce framing.
|
||||
// Given this is a prototype, let's read once.
|
||||
|
||||
LOG(INFO) << "Server reading from client...";
|
||||
char data[4096];
|
||||
size_t length = socket->read_some(asio::buffer(data, 4096)); // Error will throw
|
||||
LOG(INFO) << "Server read " << length << " bytes";
|
||||
|
||||
if (length > 0) {
|
||||
std::string request(data, length);
|
||||
LOG(INFO) << "Server processing request: " << request;
|
||||
std::string response = processor_(request);
|
||||
response += "\n";
|
||||
LOG(INFO) << "Server sending response: " << response;
|
||||
asio::write(*socket, asio::buffer(response));
|
||||
LOG(INFO) << "Server sent response";
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG(WARNING) << "Client handling error: " << e.what();
|
||||
}
|
||||
// Socket closes when shared_ptr dies
|
||||
}
|
||||
|
||||
std::string ip_;
|
||||
int port_;
|
||||
RequestProcessor processor_;
|
||||
|
||||
asio::io_context io_context_;
|
||||
asio::ip::tcp::acceptor acceptor_;
|
||||
|
||||
std::atomic<bool> running_;
|
||||
std::jthread accept_thread_;
|
||||
};
|
||||
|
||||
} // namespace cloud_point_rpc
|
||||
28
meson.build
Normal file
28
meson.build
Normal file
@ -0,0 +1,28 @@
|
||||
project('cloud_point_rpc', 'cpp',
|
||||
version : '0.1',
|
||||
default_options : ['warning_level=3', 'cpp_std=c++20'])
|
||||
|
||||
# Dependencies
|
||||
json_dep = dependency('nlohmann_json', fallback : ['nlohmann_json', 'nlohmann_json_dep'])
|
||||
thread_dep = dependency('threads')
|
||||
asio_dep = dependency('asio', fallback : ['asio', 'asio_dep'])
|
||||
|
||||
# GLog via CMake fallback
|
||||
cmake = import('cmake')
|
||||
glog_opt = cmake.subproject_options()
|
||||
glog_opt.add_cmake_defines({'WITH_GFLAGS': 'OFF', 'WITH_GTEST': 'OFF'})
|
||||
glog_proj = cmake.subproject('glog', options: glog_opt)
|
||||
glog_dep = glog_proj.dependency('glog')
|
||||
|
||||
yaml_dep = dependency('yaml-cpp', fallback : ['yaml-cpp', 'yaml_cpp_dep'])
|
||||
|
||||
# GTest & GMock via explicit subproject
|
||||
gtest_proj = subproject('gtest')
|
||||
gtest_dep = gtest_proj.get_variable('gtest_dep')
|
||||
gtest_main_dep = gtest_proj.get_variable('gtest_main_dep')
|
||||
gmock_dep = gtest_proj.get_variable('gmock_dep')
|
||||
|
||||
inc = include_directories('include')
|
||||
|
||||
subdir('src')
|
||||
subdir('tests')
|
||||
33
src/main.cpp
Normal file
33
src/main.cpp
Normal file
@ -0,0 +1,33 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "cloud_point_rpc/rpc_server.hpp"
|
||||
#include "cloud_point_rpc/service.hpp"
|
||||
|
||||
int main() {
|
||||
cloud_point_rpc::Service service;
|
||||
cloud_point_rpc::RpcServer server;
|
||||
|
||||
// Bind service methods to RPC handlers
|
||||
server.register_method("get-intrinsic-params", [&](const nlohmann::json&) {
|
||||
return service.get_intrinsic_params();
|
||||
});
|
||||
|
||||
server.register_method("get-extrinsic-params", [&](const nlohmann::json&) {
|
||||
return service.get_extrinsic_params();
|
||||
});
|
||||
|
||||
server.register_method("get-cloud-point", [&](const nlohmann::json&) {
|
||||
return service.get_cloud_point();
|
||||
});
|
||||
|
||||
std::cout << "Cloud Point RPC Server initialized." << std::endl;
|
||||
std::cout << "Enter JSON-RPC requests (Ctrl+D to exit):" << std::endl;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(std::cin, line)) {
|
||||
if (line.empty()) continue;
|
||||
std::cout << server.process(line) << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
27
src/meson.build
Normal file
27
src/meson.build
Normal file
@ -0,0 +1,27 @@
|
||||
cloud_point_rpc_sources = files(
|
||||
'rpc_server.cpp',
|
||||
'service.cpp'
|
||||
)
|
||||
|
||||
libcloud_point_rpc = static_library('cloud_point_rpc',
|
||||
cloud_point_rpc_sources,
|
||||
include_directories : inc,
|
||||
dependencies : [json_dep, thread_dep, glog_dep, yaml_dep, asio_dep],
|
||||
install : true)
|
||||
|
||||
cloud_point_rpc_dep = declare_dependency(
|
||||
include_directories : inc,
|
||||
link_with : libcloud_point_rpc,
|
||||
dependencies : [json_dep, glog_dep, yaml_dep, asio_dep])
|
||||
|
||||
# Client/CLI tool (legacy stdin/stdout)
|
||||
executable('cloud_point_rpc_cli',
|
||||
'main.cpp',
|
||||
dependencies : cloud_point_rpc_dep,
|
||||
install : true)
|
||||
|
||||
# Server executable (TCP)
|
||||
executable('cloud_point_rpc_server',
|
||||
'server_main.cpp',
|
||||
dependencies : cloud_point_rpc_dep,
|
||||
install : true)
|
||||
61
src/rpc_server.cpp
Normal file
61
src/rpc_server.cpp
Normal file
@ -0,0 +1,61 @@
|
||||
#include "cloud_point_rpc/rpc_server.hpp"
|
||||
#include <iostream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
namespace {
|
||||
json create_error(int code, const std::string& message, const json& id = nullptr) {
|
||||
return {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"error", {{"code", code}, {"message", message}}},
|
||||
{"id", id}
|
||||
};
|
||||
}
|
||||
|
||||
json create_success(const json& result, const json& id) {
|
||||
return {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"result", result},
|
||||
{"id", id}
|
||||
};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void RpcServer::register_method(const std::string& name, Handler handler) {
|
||||
handlers_[name] = std::move(handler);
|
||||
}
|
||||
|
||||
std::string RpcServer::process(const std::string& request_str) {
|
||||
json request;
|
||||
try {
|
||||
request = json::parse(request_str);
|
||||
} catch (const json::parse_error&) {
|
||||
return create_error(-32700, "Parse error").dump();
|
||||
}
|
||||
|
||||
// Batch requests are not supported in this minimal version, assume single object
|
||||
if (!request.contains("jsonrpc") || request["jsonrpc"] != "2.0" ||
|
||||
!request.contains("method") || !request.contains("id")) {
|
||||
return create_error(-32600, "Invalid Request", request.value("id", json(nullptr))).dump();
|
||||
}
|
||||
|
||||
std::string method = request["method"];
|
||||
json id = request["id"];
|
||||
json params = request.value("params", json::object());
|
||||
|
||||
auto it = handlers_.find(method);
|
||||
if (it == handlers_.end()) {
|
||||
return create_error(-32601, "Method not found", id).dump();
|
||||
}
|
||||
|
||||
try {
|
||||
json result = it->second(params);
|
||||
return create_success(result, id).dump();
|
||||
} catch (const std::exception& e) {
|
||||
return create_error(-32000, e.what(), id).dump(); // Server error
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace cloud_point_rpc
|
||||
58
src/server_main.cpp
Normal file
58
src/server_main.cpp
Normal file
@ -0,0 +1,58 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <glog/logging.h>
|
||||
#include "cloud_point_rpc/rpc_server.hpp"
|
||||
#include "cloud_point_rpc/service.hpp"
|
||||
#include "cloud_point_rpc/config.hpp"
|
||||
#include "cloud_point_rpc/tcp_server.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
google::InstallFailureSignalHandler();
|
||||
FLAGS_alsologtostderr = 1;
|
||||
|
||||
std::string config_path = "config.yaml";
|
||||
if (argc > 1) {
|
||||
config_path = argv[1];
|
||||
}
|
||||
|
||||
LOG(INFO) << "Starting Cloud Point RPC Server (Test Mock)...";
|
||||
|
||||
try {
|
||||
auto config = cloud_point_rpc::ConfigLoader::load(config_path);
|
||||
LOG(INFO) << "Loaded config from " << config_path;
|
||||
|
||||
// Inject test data into service
|
||||
cloud_point_rpc::Service service(config.test_data);
|
||||
cloud_point_rpc::RpcServer rpc_server;
|
||||
|
||||
rpc_server.register_method("get-intrinsic-params", [&](const json&) {
|
||||
return service.get_intrinsic_params();
|
||||
});
|
||||
|
||||
rpc_server.register_method("get-extrinsic-params", [&](const json&) {
|
||||
return service.get_extrinsic_params();
|
||||
});
|
||||
|
||||
rpc_server.register_method("get-cloud-point", [&](const json&) {
|
||||
return service.get_cloud_point();
|
||||
});
|
||||
|
||||
cloud_point_rpc::TcpServer server(config.server.ip, config.server.port,
|
||||
[&](const std::string& request) {
|
||||
return rpc_server.process(request);
|
||||
}
|
||||
);
|
||||
|
||||
server.start();
|
||||
server.join();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
LOG(ERROR) << "Fatal error: " << e.what();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
38
src/service.cpp
Normal file
38
src/service.cpp
Normal file
@ -0,0 +1,38 @@
|
||||
#include "cloud_point_rpc/service.hpp"
|
||||
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
Service::Service(const TestData& data) : data_(data) {}
|
||||
|
||||
std::vector<double> Service::get_intrinsic_params() const {
|
||||
if (data_.intrinsic_params.empty()) {
|
||||
// Fallback if no config loaded
|
||||
return {1.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 1.0};
|
||||
}
|
||||
return data_.intrinsic_params;
|
||||
}
|
||||
|
||||
std::vector<double> Service::get_extrinsic_params() const {
|
||||
if (data_.extrinsic_params.empty()) {
|
||||
return {1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0};
|
||||
}
|
||||
return data_.extrinsic_params;
|
||||
}
|
||||
|
||||
std::vector<std::vector<double>> Service::get_cloud_point() const {
|
||||
if (data_.cloud_point.empty()) {
|
||||
return {
|
||||
{0.1, 0.2, 0.3},
|
||||
{1.1, 1.2, 1.3},
|
||||
{2.1, 2.2, 2.3}
|
||||
};
|
||||
}
|
||||
return data_.cloud_point;
|
||||
}
|
||||
|
||||
} // namespace cloud_point_rpc
|
||||
13
subprojects/asio.wrap
Normal file
13
subprojects/asio.wrap
Normal file
@ -0,0 +1,13 @@
|
||||
[wrap-file]
|
||||
directory = asio-1.36.0
|
||||
source_url = https://sourceforge.net/projects/asio/files/asio/1.36.0%20%28Stable%29/asio-1.36.0.tar.gz/download
|
||||
source_filename = asio-1.36.0.tar.gz
|
||||
source_hash = 55d5c64e78b1bedd0004423e695c2cfc191fc71914eaaa7f042329ff99ee6155
|
||||
patch_filename = asio_1.36.0-2_patch.zip
|
||||
patch_url = https://wrapdb.mesonbuild.com/v2/asio_1.36.0-2/get_patch
|
||||
patch_hash = 0562360a9d6ef6fc5316f9a0b1d433b799259c70e0ce8f109e949b0b79797cd9
|
||||
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/asio_1.36.0-2/asio-1.36.0.tar.gz
|
||||
wrapdb_version = 1.36.0-2
|
||||
|
||||
[provide]
|
||||
asio = asio_dep
|
||||
7
subprojects/glog.wrap
Normal file
7
subprojects/glog.wrap
Normal file
@ -0,0 +1,7 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/google/glog.git
|
||||
revision = v0.6.0
|
||||
depth = 1
|
||||
|
||||
[provide]
|
||||
glog = glog_dep
|
||||
16
subprojects/gtest.wrap
Normal file
16
subprojects/gtest.wrap
Normal file
@ -0,0 +1,16 @@
|
||||
[wrap-file]
|
||||
directory = googletest-1.17.0
|
||||
source_url = https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz
|
||||
source_filename = googletest-1.17.0.tar.gz
|
||||
source_hash = 65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c
|
||||
patch_filename = gtest_1.17.0-4_patch.zip
|
||||
patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.17.0-4/get_patch
|
||||
patch_hash = 3abf7662d09db706453a5b064a1e914678c74b9d9b0b19382747ca561d0d8750
|
||||
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.17.0-4/googletest-1.17.0.tar.gz
|
||||
wrapdb_version = 1.17.0-4
|
||||
|
||||
[provide]
|
||||
gtest = gtest_dep
|
||||
gtest_main = gtest_main_dep
|
||||
gmock = gmock_dep
|
||||
gmock_main = gmock_main_dep
|
||||
7
subprojects/nlohmann_json.wrap
Normal file
7
subprojects/nlohmann_json.wrap
Normal file
@ -0,0 +1,7 @@
|
||||
[wrap-git]
|
||||
url = https://github.com/nlohmann/json.git
|
||||
revision = v3.11.2
|
||||
depth = 1
|
||||
|
||||
[provide]
|
||||
nlohmann_json = nlohmann_json_dep
|
||||
13
subprojects/yaml-cpp.wrap
Normal file
13
subprojects/yaml-cpp.wrap
Normal file
@ -0,0 +1,13 @@
|
||||
[wrap-file]
|
||||
directory = yaml-cpp-0.8.0
|
||||
source_url = https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.zip
|
||||
source_filename = yaml-cpp-0.8.0.zip
|
||||
source_hash = 334e80ab7b52e14c23f94e041c74bab0742f2281aad55f66be2f19f4b7747071
|
||||
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/yaml-cpp_0.8.0-2/yaml-cpp-0.8.0.zip
|
||||
patch_filename = yaml-cpp_0.8.0-2_patch.zip
|
||||
patch_url = https://wrapdb.mesonbuild.com/v2/yaml-cpp_0.8.0-2/get_patch
|
||||
patch_hash = e7424f2804f2bb9e99f8ecea0c3c53e6de813f93043130243a27adfef3526573
|
||||
wrapdb_version = 0.8.0-2
|
||||
|
||||
[provide]
|
||||
dependency_names = yaml-cpp
|
||||
10
tests/meson.build
Normal file
10
tests/meson.build
Normal file
@ -0,0 +1,10 @@
|
||||
test_sources = files(
|
||||
'test_rpc.cpp',
|
||||
'test_integration.cpp'
|
||||
)
|
||||
|
||||
test_exe = executable('unit_tests',
|
||||
test_sources,
|
||||
dependencies : [cloud_point_rpc_dep, gtest_dep, gtest_main_dep, gmock_dep])
|
||||
|
||||
test('unit_tests', test_exe)
|
||||
100
tests/test_integration.cpp
Normal file
100
tests/test_integration.cpp
Normal file
@ -0,0 +1,100 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
#include "cloud_point_rpc/config.hpp"
|
||||
#include "cloud_point_rpc/rpc_server.hpp"
|
||||
#include "cloud_point_rpc/service.hpp"
|
||||
#include "cloud_point_rpc/tcp_server.hpp"
|
||||
#include "cloud_point_rpc/rpc_client.hpp"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
using namespace cloud_point_rpc;
|
||||
|
||||
class IntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a temporary config file for testing
|
||||
std::ofstream config_file("config.yaml");
|
||||
config_file << "server:\n"
|
||||
<< " ip: \"127.0.0.1\"\n"
|
||||
<< " port: 9095\n"
|
||||
<< "test_data:\n"
|
||||
<< " intrinsic_params: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]\n"
|
||||
<< " extrinsic_params: [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]\n"
|
||||
<< " cloud_point:\n"
|
||||
<< " - [0.1, 0.2, 0.3]\n";
|
||||
config_file.close();
|
||||
|
||||
// Setup Mock Server
|
||||
try {
|
||||
config_ = ConfigLoader::load("config.yaml");
|
||||
} catch (...) {
|
||||
// If config fails, we can't proceed, but we should avoid crashing in TearDown
|
||||
throw;
|
||||
}
|
||||
|
||||
service_ = std::make_unique<Service>(config_.test_data);
|
||||
rpc_server_ = std::make_unique<RpcServer>();
|
||||
|
||||
rpc_server_->register_method("get-intrinsic-params", [&](const nlohmann::json&) {
|
||||
return service_->get_intrinsic_params();
|
||||
});
|
||||
|
||||
// Start Server Thread
|
||||
tcp_server_ = std::make_unique<TcpServer>(config_.server.ip, config_.server.port,
|
||||
[&](const std::string& req) {
|
||||
return rpc_server_->process(req);
|
||||
}
|
||||
);
|
||||
|
||||
server_thread_ = std::thread([&]() {
|
||||
tcp_server_->start();
|
||||
});
|
||||
|
||||
// Give server time to start
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (tcp_server_) {
|
||||
tcp_server_->stop();
|
||||
}
|
||||
if (server_thread_.joinable()) {
|
||||
server_thread_.join();
|
||||
}
|
||||
// Clean up
|
||||
std::remove("config.yaml");
|
||||
}
|
||||
|
||||
Config config_;
|
||||
std::unique_ptr<Service> service_;
|
||||
std::unique_ptr<RpcServer> rpc_server_;
|
||||
std::unique_ptr<TcpServer> tcp_server_;
|
||||
std::thread server_thread_;
|
||||
};
|
||||
|
||||
TEST_F(IntegrationTest, ClientCanConnectAndRetrieveValues) {
|
||||
RpcClient client;
|
||||
|
||||
// Act: Connect
|
||||
ASSERT_NO_THROW(client.connect(config_.server.ip, config_.server.port));
|
||||
|
||||
// Act: Call Method
|
||||
auto params = client.get_intrinsic_params();
|
||||
|
||||
// Assert: Values match config
|
||||
const auto& expected = config_.test_data.intrinsic_params;
|
||||
ASSERT_EQ(params.size(), expected.size());
|
||||
for (size_t i = 0; i < params.size(); ++i) {
|
||||
EXPECT_DOUBLE_EQ(params[i], expected[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(IntegrationTest, ClientHandlesConnectionError) {
|
||||
RpcClient client;
|
||||
// Try connecting to a closed port
|
||||
EXPECT_THROW(client.connect("127.0.0.1", 9999), std::runtime_error);
|
||||
}
|
||||
61
tests/test_rpc.cpp
Normal file
61
tests/test_rpc.cpp
Normal file
@ -0,0 +1,61 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "cloud_point_rpc/rpc_server.hpp"
|
||||
#include "cloud_point_rpc/service.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using namespace cloud_point_rpc;
|
||||
|
||||
class RpcServerTest : public ::testing::Test {
|
||||
protected:
|
||||
RpcServer server;
|
||||
Service service;
|
||||
|
||||
void SetUp() override {
|
||||
server.register_method("get-intrinsic-params", [&](const json&) {
|
||||
return service.get_intrinsic_params();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(RpcServerTest, GetIntrinsicParamsReturnsMatrix) {
|
||||
std::string request = R"({"jsonrpc": "2.0", "method": "get-intrinsic-params", "id": 1})";
|
||||
std::string response_str = server.process(request);
|
||||
|
||||
json response = json::parse(response_str);
|
||||
|
||||
ASSERT_EQ(response["jsonrpc"], "2.0");
|
||||
ASSERT_EQ(response["id"], 1);
|
||||
ASSERT_TRUE(response.contains("result"));
|
||||
|
||||
auto result = response["result"].get<std::vector<double>>();
|
||||
EXPECT_EQ(result.size(), 9);
|
||||
// Verify Identity Matrix
|
||||
EXPECT_EQ(result[0], 1.0);
|
||||
EXPECT_EQ(result[4], 1.0);
|
||||
EXPECT_EQ(result[8], 1.0);
|
||||
}
|
||||
|
||||
TEST_F(RpcServerTest, MethodNotFoundReturnsError) {
|
||||
std::string request = R"({"jsonrpc": "2.0", "method": "unknown-method", "id": 99})";
|
||||
std::string response_str = server.process(request);
|
||||
|
||||
json response = json::parse(response_str);
|
||||
|
||||
ASSERT_TRUE(response.contains("error"));
|
||||
EXPECT_EQ(response["error"]["code"], -32601);
|
||||
EXPECT_EQ(response["error"]["message"], "Method not found");
|
||||
}
|
||||
|
||||
TEST_F(RpcServerTest, InvalidJsonReturnsParseError) {
|
||||
std::string request = R"({"jsonrpc": "2.0", "method": "broken-json...)";
|
||||
std::string response_str = server.process(request);
|
||||
|
||||
json response = json::parse(response_str);
|
||||
|
||||
ASSERT_TRUE(response.contains("error"));
|
||||
EXPECT_EQ(response["error"]["code"], -32700);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user