Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/better streaming protocol #75

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions ESP/lib/src/data/CommandManager/BaseCommand.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include "BaseCommand.hpp"

CommandResult PingCommand::execute() {
return CommandResult::getSuccessResult("pong");
}

CommandResult ToggleStreamCommand::validate() {
if (!data.containsKey("state"))
return CommandResult::getErrorResult("Missing state field");

return CommandResult::getSuccessResult("");
}

CommandResult ToggleStreamCommand::execute() {
this->streamServer.toggleTCPStream(data["state"].as<bool>());

return CommandResult::getSuccessResult("TCP Stream state set to:" +
data["state"].as<std::string>());
}
72 changes: 72 additions & 0 deletions ESP/lib/src/data/CommandManager/BaseCommand.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#ifndef COMMAND_HPP
#define COMMAND_HPP
#include <ArduinoJson.h>

#include <optional>
#include <string>
#include <variant>

#include "data/config/project_config.hpp"
#include "network/stream/streamServer.hpp"

class CommandResult {
private:
std::optional<std::string> successMessage;
std::optional<std::string> errorMessage;

public:
CommandResult(std::optional<std::string> success_message,
std::optional<std::string> error_message) {
if (success_message.has_value()) {
this->successMessage =
"{\"message\":\"" + success_message.value() + "\"}";
} else
this->successMessage = std::nullopt;

if (error_message.has_value())
this->errorMessage = "{\"error\":\"" + error_message.value() + "\"}";
else
this->errorMessage = std::nullopt;
}

bool isSuccess() const { return successMessage.has_value(); }

static CommandResult getSuccessResult(std::string message) {
return CommandResult(message, std::nullopt);
}

static CommandResult getErrorResult(std::string message) {
return CommandResult(std::nullopt, message);
}

std::string getSuccessMessage() const { return successMessage.value(); };
std::string getErrorMessage() const { return errorMessage.value(); }
};

class ICommand {
public:
virtual CommandResult validate() = 0;
virtual CommandResult execute() = 0;
virtual ~ICommand() = default;
};

class PingCommand : public ICommand {
public:
CommandResult validate() override {
return CommandResult::getSuccessResult("");
};
CommandResult execute() override;
};

class ToggleStreamCommand : public ICommand {
StreamServer& streamServer;
JsonVariant data;

public:
ToggleStreamCommand(StreamServer& streamServer, JsonVariant data)
: streamServer(streamServer), data(data) {}

CommandResult validate() override;
CommandResult execute() override;
};
#endif
158 changes: 113 additions & 45 deletions ESP/lib/src/data/CommandManager/CommandManager.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
#include "CommandManager.hpp"

CommandManager::CommandManager(ProjectConfig* deviceConfig)
: deviceConfig(deviceConfig) {}
std::unique_ptr<ICommand> CommandManager::createCommand(CommandType commandType,
JsonVariant& data) {
switch (commandType) {
case CommandType::PING:
return std::make_unique<PingCommand>();
case CommandType::SET_WIFI:
return std::make_unique<SetWiFiCommand>(this->projectConfig, data);
case CommandType::SET_MDNS:
return std::make_unique<SetMDNSCommand>(this->projectConfig, data);
case CommandType::SAVE_CONFIG:
return std::make_unique<SaveConfigCommand>(this->projectConfig);
case CommandType::SET_FPS:
return std::make_unique<SetFPSCommand>(this->projectConfig, data);
case CommandType::TOGGLE_STREAM:
return std::make_unique<ToggleStreamCommand>(this->streamServer, data);
default:
return nullptr;
}
}

const CommandType CommandManager::getCommandType(JsonVariant& command) {
CommandType CommandManager::getCommandType(JsonVariant& command) {
if (!command.containsKey("command"))
return CommandType::None;

Expand All @@ -18,64 +35,115 @@ bool CommandManager::hasDataField(JsonVariant& command) {
return command.containsKey("data");
}

void CommandManager::handleCommands(CommandsPayload commandsPayload) {
CommandResult CommandManager::handleBatchCommands(
CommandsPayload commandsPayload) {
std::vector<std::unique_ptr<ICommand>> commands;
std::vector<std::string> results = {};
std::vector<std::string> errors = {};

if (!commandsPayload.data.containsKey("commands")) {
log_e("Json data sent not supported, lacks commands field");
return;
std::string error = "Json data sent not supported, lacks commands field";
log_e("%s", error.c_str());
return CommandResult::getErrorResult(error);
}

// we first try to create a command based on the payload
// if it's not supported, we register that as an error
// then, we try to validate the command, if it's succeful
// we add it to the list of commands to execute
// otherwise - you guessed it, error
// we only execute them if no errors were registered
for (JsonVariant commandData :
commandsPayload.data["commands"].as<JsonArray>()) {
this->handleCommand(commandData);
auto command_or_result = this->createCommandFromJsonVariant(commandData);
if (auto command_ptr =
std::get_if<std::unique_ptr<ICommand>>(&command_or_result)) {
auto validation_result = (*command_ptr)->validate();
if (validation_result.isSuccess())
commands.emplace_back(std::move((*command_ptr)));
else
errors.push_back(validation_result.getErrorMessage());
} else {
errors.push_back(
std::get<CommandResult>(command_or_result).getErrorMessage());
continue;
}
}

this->deviceConfig->save();
// if we have any errors, consolidate them into a single message and return
if (errors.size() > 0)
return CommandResult::getErrorResult(
Helpers::format_string("\"[%s]\"", this->join_strings(errors, ", ")));

for (auto& valid_command : commands) {
auto result = valid_command->execute();
if (result.isSuccess()) {
results.push_back(result.getSuccessMessage());
} else {
// since we're executing them already, and we've encountered an error
// we should add it to regular results
results.push_back(result.getErrorMessage());
}
}

return CommandResult::getErrorResult(
Helpers::format_string("\"[%s]\"", this->join_strings(results, ", ")));
;
}

void CommandManager::handleCommand(JsonVariant command) {
auto command_type = this->getCommandType(command);
CommandResult CommandManager::handleSingleCommand(
CommandsPayload commandsPayload) {
if (!commandsPayload.data.containsKey("command")) {
std::string error = "Json data sent not supported, lacks commands field";
log_e("%s", error.c_str());

CommandResult::getErrorResult(error);
}

switch (command_type) {
case CommandType::SET_WIFI: {
if (!this->hasDataField(command))
// malformed command, lacked data field
break;
JsonVariant commandData = commandsPayload.data;
auto command_or_result = this->createCommandFromJsonVariant(commandData);

if (!command["data"].containsKey("ssid") ||
!command["data"].containsKey("password"))
break;
if (std::holds_alternative<CommandResult>(command_or_result)) {
return std::get<CommandResult>(command_or_result);
}

std::string customNetworkName = "main";
if (command["data"].containsKey("network_name"))
customNetworkName = command["data"]["network_name"].as<std::string>();
auto command =
std::move(std::get<std::unique_ptr<ICommand>>(command_or_result));

this->deviceConfig->setWifiConfig(customNetworkName,
command["data"]["ssid"],
command["data"]["password"],
0, // channel, should this be zero?
0, // power, should this be zero?
false, false);
auto validation_result = command->validate();
if (!validation_result.isSuccess()) {
return validation_result;
};

break;
}
case CommandType::SET_MDNS: {
if (!this->hasDataField(command))
break;
return command->execute();
}

if (!command["data"].containsKey("hostname") ||
!strlen(command["data"]["hostname"]))
break;
std::variant<std::unique_ptr<ICommand>, CommandResult>
CommandManager::createCommandFromJsonVariant(JsonVariant& command) {
auto command_type = this->getCommandType(command);
if (command_type == CommandType::None) {
std::string error =
Helpers::format_string("Command not supported: %s", command["command"]);
log_e("%s", error.c_str());
return CommandResult::getErrorResult(error);
}

this->deviceConfig->setMDNSConfig(command["data"]["hostname"],
"openiristracker", false);
if (!this->hasDataField(command)) {
std::string error = Helpers::format_string(
"Command is missing data field: %s", command["command"]);
log_e("%s", error.c_str());
return CommandResult::getErrorResult(error);
}

break;
}
case CommandType::PING: {
Serial.println("PONG \n\r");
break;
}
default:
break;
auto command_data = command["data"].as<JsonVariant>();
auto command_ptr = this->createCommand(command_type, command_data);

if (!command_ptr) {
std::string error = Helpers::format_string("Command is not supported: %s",
command["command"]);
log_e("%s", error.c_str());
return CommandResult::getErrorResult(error);
}

return command_ptr;
}
67 changes: 52 additions & 15 deletions ESP/lib/src/data/CommandManager/CommandManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,74 @@
#ifndef TASK_MANAGER_HPP
#define TASK_MANAGER_HPP
#include <ArduinoJson.h>

#include <iostream>
#include <iterator>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <unordered_map>
#include <variant>
#include <vector>

#include "data/CommandManager/BaseCommand.hpp"
#include "data/config/project_config.hpp"
#include "network/stream/streamServer.hpp"

struct CommandsPayload {
JsonVariant data;
};

enum CommandType {
None,
PING,
SET_WIFI,
SET_MDNS,
SET_FPS,
TOGGLE_STREAM,
SAVE_CONFIG,
};

struct CommandsPayload {
JsonDocument data;
};
const std::unordered_map<std::string, CommandType> commandMap = {
{"ping", CommandType::PING},
{"set_wifi", CommandType::SET_WIFI},
{"set_mdns", CommandType::SET_MDNS},
{"set_fps", CommandType::SET_FPS},
{"toggle_stream", CommandType::TOGGLE_STREAM},
{"save_config", CommandType::SAVE_CONFIG}};

class CommandManager {
private:
const std::unordered_map<std::string, CommandType> commandMap = {
{"ping", CommandType::PING},
{"set_wifi", CommandType::SET_WIFI},
{"set_mdns", CommandType::SET_MDNS},
};
ProjectConfig &projectConfig;
StreamServer &streamServer;

std::string join_strings(std::vector<std::string> const &strings,
std::string delim) {
std::stringstream ss;
std::copy(strings.begin(), strings.end(),
std::ostream_iterator<std::string>(ss, delim.c_str()));
return ss.str();
}

bool hasDataField(JsonVariant &command);
std::unique_ptr<ICommand> createCommand(CommandType commandType,
JsonVariant &data);

ProjectConfig* deviceConfig;
std::variant<std::unique_ptr<ICommand>, CommandResult>
createCommandFromJsonVariant(JsonVariant &command);

bool hasDataField(JsonVariant& command);
void handleCommand(JsonVariant command);
const CommandType getCommandType(JsonVariant& command);
CommandType getCommandType(JsonVariant &command);

// // TODO rewrite the API
// // TODO add FPS/ Freq / cropping to the API
// // TODO rewrite camera handler to be simpler and easier to change

public:
CommandManager(ProjectConfig* deviceConfig);
void handleCommands(CommandsPayload commandsPayload);
};
CommandManager(ProjectConfig &projectConfig, StreamServer &streamServer)
: projectConfig(projectConfig), streamServer(streamServer) {};

CommandResult handleSingleCommand(CommandsPayload commandsPayload);
CommandResult handleBatchCommands(CommandsPayload commandsPayload);
};
#endif
Loading
Loading