Skip to content

Commit 199232b

Browse files
committed
Add more boilerplate code for the actual api
1 parent cf2cc4f commit 199232b

File tree

13 files changed

+279
-177
lines changed

13 files changed

+279
-177
lines changed

cppesphomeapi/api.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
syntax = "proto3";
2-
2+
package cppesphomeapi.proto;
33
import "api_options.proto";
44

55
service APIConnection {

cppesphomeapi/api_options.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
syntax = "proto2";
2+
package cppesphomeapi.proto;
23
import "google/protobuf/descriptor.proto";
34

45
enum APISourceType {

cppesphomeapi/include/cppesphomeapi/api_client.hpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,27 @@
44
#include <memory>
55
#include <string>
66
#include <cppesphomeapi/cppesphomeapi_export.hpp>
7+
#include "async_result.hpp"
78

89
namespace cppesphomeapi
910
{
11+
class ApiConnection;
12+
1013
class CPPESPHOMEAPI_EXPORT ApiClient
1114
{
1215
public:
13-
ApiClient(std::string hostname, std::uint16_t port = 6053, std::string password = "");
16+
explicit ApiClient(const boost::asio::any_io_executor &executor, std::string hostname, std::uint16_t port = 6053, std::string password = "");
1417
~ApiClient();
1518

19+
AsyncResult<void> connect();
20+
1621
ApiClient(const ApiClient &) = delete;
1722
ApiClient(ApiClient &&) = delete;
1823
ApiClient &operator=(const ApiClient &) = delete;
1924
ApiClient &operator=(ApiClient &&) = delete;
2025

2126
private:
22-
class Impl;
23-
std::unique_ptr<Impl> impl_;
27+
std::unique_ptr<ApiConnection> connection_;
2428
};
2529
} // namespace cppesphomeapi
2630
#endif

cppesphomeapi/include/cppesphomeapi/result.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ namespace cppesphomeapi
88

99
enum ApiErrorCode
1010
{
11+
SerializeError,
1112
ParseError,
12-
UnexpectedMessage
13+
UnexpectedMessage,
14+
SendError
1315
};
1416

1517
struct ApiError

cppesphomeapi/src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
target_sources(cppesphomeapi PRIVATE
22
api_client.cpp
33
make_unexpected_result.cpp
4+
plain_text_protocol.cpp
5+
api_connection.cpp
46
)

cppesphomeapi/src/api_client.cpp

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
#include "cppesphomeapi/api_client.hpp"
2+
#include "api_connection.hpp"
23
namespace cppesphomeapi
34
{
4-
class ApiClient::Impl
5-
{};
6-
7-
ApiClient::ApiClient(std::string hostname, std::uint16_t port, std::string password)
8-
: impl_{std::make_unique<Impl>()}
5+
ApiClient::ApiClient(const boost::asio::any_io_executor &executor,
6+
std::string hostname,
7+
std::uint16_t port,
8+
std::string password)
9+
: connection_{std::make_unique<ApiConnection>(std::move(hostname), port, std::move(password), executor)}
910
{}
1011

1112
ApiClient::~ApiClient() = default;
13+
14+
AsyncResult<void> ApiClient::connect()
15+
{
16+
co_return co_await connection_->connect();
17+
}
1218
} // namespace cppesphomeapi
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#include "api_connection.hpp"
2+
#include <print>
3+
#include "api.pb.h"
4+
#include "make_unexpected_result.hpp"
5+
6+
namespace asio = boost::asio;
7+
namespace this_coro = asio::this_coro;
8+
9+
namespace cppesphomeapi
10+
{
11+
ApiConnection::ApiConnection(std::string hostname,
12+
std::uint16_t port,
13+
std::string password,
14+
const asio::any_io_executor &executor)
15+
: hostname_{std::move(hostname)}
16+
, port_{port}
17+
, password_{std::move(password)}
18+
, strand_{asio::make_strand(executor)}
19+
, socket_{strand_}
20+
{}
21+
22+
AsyncResult<void> ApiConnection::connect()
23+
{
24+
auto executor = co_await this_coro::executor;
25+
tcp::resolver resolver{executor};
26+
const auto resolved = co_await resolver.async_resolve(hostname_, std::to_string(port_));
27+
28+
co_await socket_.async_connect(resolved->endpoint());
29+
socket_.set_option(asio::socket_base::keep_alive{true});
30+
31+
auto send_result = co_await send_message_hello();
32+
33+
co_return send_result;
34+
}
35+
36+
AsyncResult<void> ApiConnection::send_message_hello()
37+
{
38+
cppesphomeapi::proto::HelloRequest hello_request;
39+
40+
hello_request.ParseFromArray(nullptr, 0);
41+
hello_request.set_client_info(std::string{"cppapi"});
42+
co_await send_message(hello_request);
43+
const auto msg_promise = co_await receive_message<cppesphomeapi::proto::HelloResponse>();
44+
45+
std::println("Got esphome device \"{}\": Version {}.{}",
46+
msg_promise->name(),
47+
msg_promise->api_version_major(),
48+
msg_promise->api_version_minor());
49+
// todo: do something with the message.
50+
co_return Result<void>{};
51+
}
52+
53+
AsyncResult<void> ApiConnection::send_message(const google::protobuf::Message &message)
54+
{
55+
const auto packet = plain_text_serialize(message);
56+
if (packet.has_value())
57+
{
58+
const auto written = co_await socket_.async_write_some(asio::buffer(packet.value()));
59+
if (written != packet->size())
60+
{
61+
co_return make_unexpected_result(
62+
ApiErrorCode::SendError,
63+
std::format("Could not send message. Bytes written are different: expected={}, written={}",
64+
packet->size(),
65+
written));
66+
}
67+
co_return Result<void>{};
68+
}
69+
co_return std::unexpected(packet.error());
70+
}
71+
} // namespace cppesphomeapi
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#pragma once
2+
#include <cstdint>
3+
#include <string>
4+
#include <boost/asio/executor.hpp>
5+
#include <boost/asio/strand.hpp>
6+
#include <google/protobuf/message.h>
7+
#include "cppesphomeapi/async_result.hpp"
8+
#include "make_unexpected_result.hpp"
9+
#include "plain_text_protocol.hpp"
10+
#include "tcp.hpp"
11+
namespace cppesphomeapi
12+
{
13+
class ApiConnection
14+
{
15+
public:
16+
explicit ApiConnection(std::string hostname,
17+
std::uint16_t port,
18+
std::string password,
19+
const boost::asio::any_io_executor &executor);
20+
21+
AsyncResult<void> connect();
22+
AsyncResult<void> send_message_hello();
23+
24+
private:
25+
AsyncResult<void> send_message(const google::protobuf::Message &message);
26+
27+
template <typename TMsg>
28+
AsyncResult<TMsg> receive_message()
29+
{
30+
namespace asio = boost::asio;
31+
std::array<std::uint8_t, 512> buffer{};
32+
const auto received_bytes = co_await socket_.async_receive(asio::buffer(buffer));
33+
if (received_bytes < 3)
34+
{
35+
co_return make_unexpected_result(ApiErrorCode::ParseError,
36+
"response does not contain enough bytes for the header");
37+
}
38+
co_return plain_text_decode<TMsg>(std::span{buffer.begin(), received_bytes});
39+
}
40+
41+
private:
42+
std::string hostname_;
43+
std::uint16_t port_;
44+
std::string password_;
45+
boost::asio::strand<boost::asio::any_io_executor> strand_;
46+
tcp::socket socket_;
47+
};
48+
} // namespace cppesphomeapi
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include "plain_text_protocol.hpp"
2+
#include <google/protobuf/io/coded_stream.h>
3+
#include <google/protobuf/io/zero_copy_stream.h>
4+
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
5+
#include "api_options.pb.h"
6+
#include "make_unexpected_result.hpp"
7+
8+
namespace cppesphomeapi
9+
{
10+
Result<void> plain_text_decode(std::span<const std::uint8_t> received_data, ::google::protobuf::Message &message)
11+
{
12+
if (received_data.size() < 3)
13+
{
14+
return make_unexpected_result(ApiErrorCode::ParseError,
15+
"response does not contain enough bytes for the header");
16+
}
17+
google::protobuf::io::CodedInputStream stream{received_data.data(), static_cast<int>(received_data.size())};
18+
std::uint32_t preamble{};
19+
if (not stream.ReadVarint32(&preamble) or preamble != 0x00)
20+
{
21+
return make_unexpected_result(ApiErrorCode::ParseError, "response does contain an invalid preamble");
22+
}
23+
24+
std::uint32_t message_size{};
25+
if (not stream.ReadVarint32(&message_size))
26+
{
27+
return make_unexpected_result(ApiErrorCode::ParseError, "got no message size");
28+
}
29+
30+
std::uint32_t message_type{};
31+
if (not stream.ReadVarint32(&message_type))
32+
{
33+
return make_unexpected_result(ApiErrorCode::ParseError, "got no message type");
34+
}
35+
36+
const auto expected_message_id = message.GetDescriptor()->options().GetExtension(proto::id);
37+
if (expected_message_id != message_type)
38+
{
39+
return make_unexpected_result(
40+
ApiErrorCode::UnexpectedMessage,
41+
std::format("Expected {} but got message id {}", message.GetDescriptor()->name(), message_type));
42+
}
43+
if (stream.BytesUntilLimit() != message_size)
44+
{
45+
return make_unexpected_result(
46+
ApiErrorCode::ParseError,
47+
std::format("Received message size does not match the remaining bytes. Expected {} bytes got {} bytes.",
48+
message_size,
49+
stream.BytesUntilLimit()));
50+
}
51+
const bool parsed = message.ParseFromCodedStream(std::addressof(stream));
52+
if (not parsed)
53+
{
54+
return make_unexpected_result(
55+
ApiErrorCode::ParseError,
56+
std::format("Could not parse message \"{}\" from bytes.", message.GetDescriptor()->name()));
57+
}
58+
return {};
59+
}
60+
61+
Result<std::vector<std::uint8_t>> plain_text_serialize(const ::google::protobuf::Message &message)
62+
{
63+
constexpr std::uint8_t kPlainTextPreamble = 0x00;
64+
auto &&msg_options = message.GetDescriptor()->options();
65+
if (not msg_options.HasExtension(proto::id))
66+
{
67+
return make_unexpected_result(
68+
ApiErrorCode::SerializeError,
69+
std::format("message \"{}\" does not contain the id field", message.GetDescriptor()->name()));
70+
}
71+
72+
constexpr auto kMaxHeaderLen = 5;
73+
std::vector<std::uint8_t> buffer;
74+
buffer.resize(message.ByteSizeLong() + kMaxHeaderLen);
75+
76+
buffer[0] = kPlainTextPreamble;
77+
78+
google::protobuf::io::ArrayOutputStream zstream{std::next(buffer.data(), 1), static_cast<int>(buffer.size() - 1)};
79+
google::protobuf::io::CodedOutputStream output_stream{std::addressof(zstream)};
80+
81+
output_stream.WriteVarint32(message.ByteSizeLong());
82+
output_stream.WriteVarint32(msg_options.GetExtension(proto::id));
83+
84+
if(output_stream.ByteCount() != (kMaxHeaderLen - 1)) {
85+
buffer.resize(1 + message.ByteSizeLong() + output_stream.ByteCount());
86+
}
87+
88+
const bool serialized = message.SerializeToCodedStream(std::addressof(output_stream));
89+
if (not serialized)
90+
{
91+
return make_unexpected_result(
92+
ApiErrorCode::SerializeError,
93+
std::format("could not serialize message \"{}\"", message.GetDescriptor()->name()));
94+
}
95+
return buffer;
96+
}
97+
} // namespace cppesphomeapi
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#pragma once
2+
#include <cstdint>
3+
#include <span>
4+
#include <vector>
5+
#include <google/protobuf/message.h>
6+
#include "cppesphomeapi/result.hpp"
7+
8+
namespace cppesphomeapi
9+
{
10+
Result<void> plain_text_decode(std::span<const std::uint8_t> received_data, ::google::protobuf::Message &message);
11+
12+
template <typename TMsg>
13+
Result<TMsg> plain_text_decode(std::span<const std::uint8_t> received_data)
14+
{
15+
TMsg received_message;
16+
return plain_text_decode(received_data, received_message)
17+
.transform([received_message = std::move(received_message)]() { return received_message; });
18+
}
19+
20+
Result<std::vector<std::uint8_t>> plain_text_serialize(const ::google::protobuf::Message &message);
21+
} // namespace cppesphomeapi

0 commit comments

Comments
 (0)