feat(emulator): implement gRPC control server and emulator commands

- Added `AgentControlServer` to manage gRPC connections for emulator control.
- Introduced `EmulatorServiceImpl` with methods for starting, stopping, and controlling the emulator, including button presses and memory operations.
- Created new command handlers for pressing, releasing, and holding emulator buttons.
- Updated CMake configuration to include new source files and proto definitions for the emulator service.

Benefits:
- Enhanced control over the emulator through gRPC, allowing for remote interactions.
- Improved modularity and maintainability of the emulator's command handling.
- Streamlined integration of new features for emulator control and state inspection.
This commit is contained in:
scawful
2025-10-11 13:57:07 -04:00
parent 9ffb7803f5
commit aacc7795c3
10 changed files with 1054 additions and 181 deletions

View File

@@ -1,35 +1,256 @@
#include "cli/handlers/tools/emulator_commands.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_format.h"
#include <grpcpp/grpcpp.h>
#include "protos/emulator_service.grpc.pb.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "absl/time/time.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
namespace yaze {
namespace cli {
namespace handlers {
namespace {
// A simple client for the EmulatorService
class EmulatorClient {
public:
EmulatorClient() {
auto channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
stub_ = agent::EmulatorService::NewStub(channel);
}
template <typename TRequest, typename TResponse>
absl::StatusOr<TResponse> CallRpc(
grpc::Status (agent::EmulatorService::Stub::*rpc_method)(grpc::ClientContext*, const TRequest&, TResponse*),
const TRequest& request) {
TResponse response;
grpc::ClientContext context;
auto deadline = std::chrono::system_clock::now() + std::chrono::seconds(5);
context.set_deadline(deadline);
grpc::Status status = (stub_.get()->*rpc_method)(&context, request, &response);
if (!status.ok()) {
return absl::UnavailableError(absl::StrFormat(
"RPC failed: (%d) %s", status.error_code(), status.error_message()));
}
return response;
}
private:
std::unique_ptr<agent::EmulatorService::Stub> stub_;
};
// Helper to parse button from string
absl::StatusOr<agent::Button> StringToButton(absl::string_view s) {
if (s == "A") return agent::Button::A;
if (s == "B") return agent::Button::B;
if (s == "X") return agent::Button::X;
if (s == "Y") return agent::Button::Y;
if (s == "L") return agent::Button::L;
if (s == "R") return agent::Button::R;
if (s == "SELECT") return agent::Button::SELECT;
if (s == "START") return agent::Button::START;
if (s == "UP") return agent::Button::UP;
if (s == "DOWN") return agent::Button::DOWN;
if (s == "LEFT") return agent::Button::LEFT;
if (s == "RIGHT") return agent::Button::RIGHT;
return absl::InvalidArgumentError(absl::StrCat("Unknown button: ", s));
}
} // namespace
// --- Command Implementations ---
absl::Status EmulatorResetCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::Empty request;
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::Reset, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("EmulatorReset");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorGetStateCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::GameStateRequest request;
request.set_include_screenshot(parser.HasFlag("screenshot"));
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::GetGameState, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("EmulatorState");
formatter.AddField("game_mode", static_cast<uint64_t>(response.game_mode()));
formatter.AddField("link_state", static_cast<uint64_t>(response.link_state()));
formatter.AddField("link_pos_x", static_cast<uint64_t>(response.link_pos_x()));
formatter.AddField("link_pos_y", static_cast<uint64_t>(response.link_pos_y()));
formatter.AddField("link_health", static_cast<uint64_t>(response.link_health()));
if (!response.screenshot_png().empty()) {
formatter.AddField("screenshot_size", static_cast<uint64_t>(response.screenshot_png().size()));
}
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorReadMemoryCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::MemoryRequest request;
uint32_t address;
if (!absl::SimpleHexAtoi(parser.GetString("address").value(), &address)) {
return absl::InvalidArgumentError("Invalid address format.");
}
request.set_address(address);
request.set_size(parser.GetInt("length").value_or(16));
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::ReadMemory, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("MemoryRead");
formatter.AddHexField("address", response.address());
formatter.AddField("data_hex", absl::BytesToHexString(response.data()));
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorWriteMemoryCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::MemoryWriteRequest request;
uint32_t address;
if (!absl::SimpleHexAtoi(parser.GetString("address").value(), &address)) {
return absl::InvalidArgumentError("Invalid address format.");
}
request.set_address(address);
std::string data_hex = parser.GetString("data").value();
request.set_data(absl::HexStringToBytes(data_hex));
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::WriteMemory, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("MemoryWrite");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorPressButtonsCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::ButtonRequest request;
std::vector<std::string> buttons = absl::StrSplit(parser.GetString("buttons").value(), ',');
for (const auto& btn_str : buttons) {
auto button_or = StringToButton(btn_str);
if (!button_or.ok()) return button_or.status();
request.add_buttons(button_or.value());
}
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::PressButtons, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("PressButtons");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorReleaseButtonsCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::ButtonRequest request;
std::vector<std::string> buttons = absl::StrSplit(parser.GetString("buttons").value(), ',');
for (const auto& btn_str : buttons) {
auto button_or = StringToButton(btn_str);
if (!button_or.ok()) return button_or.status();
request.add_buttons(button_or.value());
}
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::ReleaseButtons, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("ReleaseButtons");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorHoldButtonsCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
EmulatorClient client;
agent::ButtonHoldRequest request;
std::vector<std::string> buttons = absl::StrSplit(parser.GetString("buttons").value(), ',');
for (const auto& btn_str : buttons) {
auto button_or = StringToButton(btn_str);
if (!button_or.ok()) return button_or.status();
request.add_buttons(button_or.value());
}
request.set_duration_ms(parser.GetInt("duration").value());
auto response_or = client.CallRpc(&agent::EmulatorService::Stub::HoldButtons, request);
if (!response_or.ok()) {
return response_or.status();
}
auto response = response_or.value();
formatter.BeginObject("HoldButtons");
formatter.AddField("success", response.success());
formatter.AddField("message", response.message());
formatter.EndObject();
return absl::OkStatus();
}
// --- Placeholder Implementations for commands not yet migrated to gRPC ---
absl::Status EmulatorStepCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto count = parser.GetInt("count").value_or(1);
formatter.BeginObject("Emulator Step");
formatter.AddField("steps_executed", count);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator stepping requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorRunCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto until_breakpoint = parser.GetString("until").value_or("");
formatter.BeginObject("Emulator Run");
formatter.AddField("until_breakpoint", until_breakpoint);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator running requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
@@ -37,134 +258,31 @@ absl::Status EmulatorPauseCommandHandler::Execute(Rom* rom, const resources::Arg
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Pause");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator pause requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorResetCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Reset");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator reset requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorGetStateCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator State");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Emulator state requires emulator integration");
formatter.BeginObject("state");
formatter.AddField("running", false);
formatter.AddField("paused", true);
formatter.AddField("pc", "0x000000");
formatter.EndObject();
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorSetBreakpointCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto address_str = parser.GetString("address").value();
auto condition = parser.GetString("condition").value_or("");
uint32_t address;
if (!absl::SimpleHexAtoi(address_str, &address)) {
return absl::InvalidArgumentError(
"Invalid address format. Must be hex.");
}
formatter.BeginObject("Emulator Breakpoint Set");
formatter.AddHexField("address", address, 6);
formatter.AddField("condition", condition);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Breakpoint setting requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorClearBreakpointCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto address_str = parser.GetString("address").value();
uint32_t address;
if (!absl::SimpleHexAtoi(address_str, &address)) {
return absl::InvalidArgumentError(
"Invalid address format. Must be hex.");
}
formatter.BeginObject("Emulator Breakpoint Cleared");
formatter.AddHexField("address", address, 6);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Breakpoint clearing requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorListBreakpointsCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Breakpoints");
formatter.AddField("total_breakpoints", 0);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Breakpoint listing requires emulator integration");
formatter.BeginArray("breakpoints");
formatter.EndArray();
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorReadMemoryCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto address_str = parser.GetString("address").value();
auto length = parser.GetInt("length").value_or(16);
uint32_t address;
if (!absl::SimpleHexAtoi(address_str, &address)) {
return absl::InvalidArgumentError(
"Invalid address format. Must be hex.");
}
formatter.BeginObject("Emulator Memory Read");
formatter.AddHexField("address", address, 6);
formatter.AddField("length", length);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Memory reading requires emulator integration");
formatter.BeginArray("data");
formatter.EndArray();
formatter.EndObject();
return absl::OkStatus();
}
absl::Status EmulatorWriteMemoryCommandHandler::Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) {
auto address_str = parser.GetString("address").value();
auto data_str = parser.GetString("data").value();
uint32_t address;
if (!absl::SimpleHexAtoi(address_str, &address)) {
return absl::InvalidArgumentError(
"Invalid address format. Must be hex.");
}
formatter.BeginObject("Emulator Memory Write");
formatter.AddHexField("address", address, 6);
formatter.AddField("data", data_str);
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Memory writing requires emulator integration");
formatter.EndObject();
return absl::OkStatus();
}
@@ -172,19 +290,7 @@ absl::Status EmulatorGetRegistersCommandHandler::Execute(Rom* rom, const resourc
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Registers");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Register reading requires emulator integration");
formatter.BeginObject("registers");
formatter.AddField("A", std::string("0x0000"));
formatter.AddField("X", std::string("0x0000"));
formatter.AddField("Y", std::string("0x0000"));
formatter.AddField("PC", std::string("0x000000"));
formatter.AddField("SP", std::string("0x01FF"));
formatter.AddField("DB", std::string("0x00"));
formatter.AddField("DP", std::string("0x0000"));
formatter.EndObject();
formatter.EndObject();
return absl::OkStatus();
}
@@ -192,16 +298,7 @@ absl::Status EmulatorGetMetricsCommandHandler::Execute(Rom* rom, const resources
resources::OutputFormatter& formatter) {
formatter.BeginObject("Emulator Metrics");
formatter.AddField("status", "not_implemented");
formatter.AddField("message", "Metrics require emulator integration");
formatter.BeginObject("metrics");
formatter.AddField("instructions_per_second", 0);
formatter.AddField("total_instructions", 0);
formatter.AddField("cycles_per_frame", 0);
formatter.AddField("frame_rate", 0);
formatter.EndObject();
formatter.EndObject();
return absl::OkStatus();
}

View File

@@ -7,9 +7,6 @@ namespace yaze {
namespace cli {
namespace handlers {
/**
* @brief Command handler for emulator step execution
*/
class EmulatorStepCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-step"; }
@@ -21,16 +18,13 @@ class EmulatorStepCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for emulator run execution
*/
class EmulatorRunCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-run"; }
@@ -42,16 +36,13 @@ class EmulatorRunCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for emulator pause
*/
class EmulatorPauseCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-pause"; }
@@ -63,16 +54,13 @@ class EmulatorPauseCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for emulator reset
*/
class EmulatorResetCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-reset"; }
@@ -84,16 +72,13 @@ class EmulatorResetCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for getting emulator state
*/
class EmulatorGetStateCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-get-state"; }
@@ -105,16 +90,13 @@ class EmulatorGetStateCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for setting breakpoints
*/
class EmulatorSetBreakpointCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-set-breakpoint"; }
@@ -133,9 +115,6 @@ class EmulatorSetBreakpointCommandHandler : public resources::CommandHandler {
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for clearing breakpoints
*/
class EmulatorClearBreakpointCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-clear-breakpoint"; }
@@ -154,9 +133,6 @@ class EmulatorClearBreakpointCommandHandler : public resources::CommandHandler {
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for listing breakpoints
*/
class EmulatorListBreakpointsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-list-breakpoints"; }
@@ -168,16 +144,13 @@ class EmulatorListBreakpointsCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for reading emulator memory
*/
class EmulatorReadMemoryCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-read-memory"; }
@@ -196,9 +169,6 @@ class EmulatorReadMemoryCommandHandler : public resources::CommandHandler {
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for writing emulator memory
*/
class EmulatorWriteMemoryCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-write-memory"; }
@@ -217,9 +187,6 @@ class EmulatorWriteMemoryCommandHandler : public resources::CommandHandler {
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for getting emulator registers
*/
class EmulatorGetRegistersCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-get-registers"; }
@@ -231,16 +198,13 @@ class EmulatorGetRegistersCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
/**
* @brief Command handler for getting emulator metrics
*/
class EmulatorGetMetricsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-get-metrics"; }
@@ -252,13 +216,65 @@ class EmulatorGetMetricsCommandHandler : public resources::CommandHandler {
}
absl::Status ValidateArgs(const resources::ArgumentParser& parser) override {
return absl::OkStatus(); // No required args
return absl::OkStatus();
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
class EmulatorPressButtonsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-press-buttons"; }
std::string GetDescription() const {
return "Press and release emulator buttons";
}
std::string GetUsage() const {
return "emulator-press-buttons --buttons <button1>,<button2>,...";
}
absl::Status ValidateArgs(
const resources::ArgumentParser& parser) override {
return parser.RequireArgs({"buttons"});
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
class EmulatorReleaseButtonsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-release-buttons"; }
std::string GetDescription() const {
return "Release currently held emulator buttons";
}
std::string GetUsage() const {
return "emulator-release-buttons --buttons <button1>,<button2>,...";
}
absl::Status ValidateArgs(
const resources::ArgumentParser& parser) override {
return parser.RequireArgs({"buttons"});
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
class EmulatorHoldButtonsCommandHandler : public resources::CommandHandler {
public:
std::string GetName() const { return "emulator-hold-buttons"; }
std::string GetDescription() const {
return "Hold emulator buttons for a specified duration";
}
std::string GetUsage() const {
return "emulator-hold-buttons --buttons <button1>,<button2>,... --duration "
"<ms>";
}
absl::Status ValidateArgs(
const resources::ArgumentParser& parser) override {
return parser.RequireArgs({"buttons", "duration"});
}
absl::Status Execute(Rom* rom, const resources::ArgumentParser& parser,
resources::OutputFormatter& formatter) override;
};
} // namespace handlers
} // namespace cli
} // namespace yaze