Skip to content

Commit

Permalink
Merge pull request #705 from SignalK/async_configurables
Browse files Browse the repository at this point in the history
Async Configurables
  • Loading branch information
mairas authored Aug 2, 2024
2 parents a48c188 + caa08c9 commit ad96f0f
Show file tree
Hide file tree
Showing 12 changed files with 2,524 additions and 2,089 deletions.
4,029 changes: 2,030 additions & 1,999 deletions src/sensesp/net/web/autogen/web_ui_files.h

Large diffs are not rendered by default.

132 changes: 113 additions & 19 deletions src/sensesp/net/web/config_handler.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
#include "config_handler.h"

#include "sensesp.h"

#include "config_handler.h"

namespace sensesp {

const char* poll_status_strings[] = {
"error",
"ok",
"pending",
};

esp_err_t handle_config_list(httpd_req_t* req) {
JsonDocument json_doc;
JsonArray arr = json_doc["keys"].to<JsonArray>();
Expand Down Expand Up @@ -31,11 +37,24 @@ void add_config_list_handler(HTTPServer* server) {
void add_config_get_handler(HTTPServer* server) {
server->add_handler(new HTTPRequestHandler(
1 << HTTP_GET, "/api/config/*", [](httpd_req_t* req) {
ESP_LOGD("ConfigHandler", "GET request to URL %s", req->uri);
String url_tail = String(req->uri).substring(11);
char url_tail_cstr[url_tail.length() + 1];
urldecode2(url_tail_cstr, url_tail.c_str());
url_tail = String(url_tail_cstr);
if (url_tail.length() == 0) {
String path;
String query = "";
if (url_tail.indexOf('?') != -1) {
path = url_tail.substring(0, url_tail.indexOf('?'));
query = url_tail.substring(url_tail.indexOf('?') + 1);
} else {
path = url_tail;
}
char path_cstr[path.length() + 1];
urldecode2(path_cstr, path.c_str());
url_tail = String(path_cstr);

ESP_LOGV("ConfigHandler", "URL parts: %s, %s", path.c_str(),
query.c_str());

if (path.length() == 0) {
// return a list of all Configurable objects
return handle_config_list(req);
}
Expand All @@ -48,14 +67,69 @@ void add_config_get_handler(HTTPServer* server) {
return ESP_FAIL;
}

// get the configuration
JsonDocument doc;
String response;
JsonObject config = doc["config"].to<JsonObject>();
JsonObject config;
config = doc["config"].to<JsonObject>();

// Should return a JSON object with the following keys:
// - status: "ok", "pending" or "error"
// - config, optional: the configuration object
// - schema, optional: the configuration schema
// - description, optional: the configuration description

ConfigurableResult status = ConfigurableResult::kOk;

if (confable->is_async()) {
if (query.startsWith("poll_get_result")) {
// Poll the status of a previous async GET (get) request
status = confable->poll_get_result(config);
if (status == ConfigurableResult::kPending) {
// Set result code 202
httpd_resp_set_status(req, "202 Accepted");
}
} else
if (query.startsWith("poll_put_result")) {
// Poll the status of a previous async PUT (set) request
status = confable->poll_set_result();
if (status == ConfigurableResult::kPending) {
// Set result code 202
httpd_resp_set_status(req, "202 Accepted");
}
} else {
// Initiate an async GET (get) request
status = confable->async_get_configuration();
if (status == ConfigurableResult::kOk) {
// Return the resulting config object immediately
confable->poll_get_result(config);
} else {
// Set result code 202
httpd_resp_set_status(req, "202 Accepted");
}
doc["schema"] = serialized(confable->get_config_schema());
doc["description"] = confable->get_description();
}
doc["status"] = poll_status_strings[static_cast<int>(status)];
} else {
// Synchronous GET (get) request
if (query.startsWith("poll")) {
// Polling is not supported for synchronous requests
httpd_resp_send_err(
req, HTTPD_400_BAD_REQUEST,
"Polling is not supported for synchronous requests");
}

doc["status"] =
poll_status_strings[static_cast<int>(ConfigurableResult::kOk)];

doc["schema"] = serialized(confable->get_config_schema());
doc["description"] = confable->get_description();
}

confable->get_configuration(config);
doc["schema"] = serialized(confable->get_config_schema());
doc["description"] = confable->get_description();

String response;
serializeJson(doc, response);
ESP_LOGV("ConfigHandler", "Response: %s", response.c_str());
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, response.c_str());
return ESP_OK;
Expand Down Expand Up @@ -114,17 +188,37 @@ void add_config_put_handler(HTTPServer* server) {
return ESP_FAIL;
}

// set the configuration
if (!confable->set_configuration(doc.as<JsonObject>())) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"Error setting configuration");
return ESP_FAIL;
String response;
if (confable->is_async()) {
// Initiate an async PUT (set) request
ConfigurableResult result =
confable->async_set_configuration(doc.as<JsonObject>());
if (result == ConfigurableResult::kError) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"Error setting configuration");
return ESP_FAIL;
}
confable->save_configuration();
String result_string = poll_status_strings[static_cast<int>(result)];
response = "{\"status\":\"" + result_string + "\"}";
// Set result code 202
httpd_resp_set_status(req, "202 Accepted");
} else {
// Synchronous PUT (set) request
bool result = confable->set_configuration(doc.as<JsonObject>());
if (!result) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"Error setting configuration");
return ESP_FAIL;
}
confable->save_configuration();
response = "{\"status\":\"ok\"}";
}

// save the configuration
confable->save_configuration();
httpd_resp_sendstr(req, "OK");
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, response.c_str());
return ESP_OK;

}));
}

Expand Down
101 changes: 101 additions & 0 deletions src/sensesp/system/async_response_handler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#ifndef SENSESP_SRC_SENSESP_SIGNALK_SYSTEM_ASYNC_RESPONSE_HANDLER_H_
#define SENSESP_SRC_SENSESP_SIGNALK_SYSTEM_ASYNC_RESPONSE_HANDLER_H_

#include "sensesp.h"

#include <Arduino.h>
#include <elapsedMillis.h>

#include "valueproducer.h"

namespace sensesp {

enum class AsyncResponseStatus {
kReady,
kPending,
kSuccess,
kFailure,
kTimeout,
};

/**
* @brief Handle async command responses from remote systems.
*
* This class is used to report responses to commands sent to a remote system.
* The class should consume a boolean value when a response is received. The
* boolean value indicates whether the command was successful or not.
* The response handler also emits the response status to its subscribers.
*
*/
class AsyncResponseHandler : public ValueConsumer<bool>,
public ValueProducer<AsyncResponseStatus> {
public:
AsyncResponseHandler()
: ValueConsumer<bool>(), ValueProducer<AsyncResponseStatus>() {}
AsyncResponseHandler(int timeout)
: ValueConsumer<bool>(),
ValueProducer<AsyncResponseStatus>(),
timeout_{timeout} {}

/**
* @brief Activate must be called when a command is sent to the remote system.
*
* @param value
*/
void activate() {
ESP_LOGV("AsyncResponseHandler", "Activating response handler");
elapsed_millis_ = 0;
status_ = AsyncResponseStatus::kPending;
this->emit(status_);

if (timeout_reaction_ != nullptr) {
ReactESP::app->remove(timeout_reaction_);
timeout_reaction_ = nullptr;
}
timeout_reaction_ = ReactESP::app->onDelay(timeout_, [this]() {
if (status_ == AsyncResponseStatus::kPending) {
ESP_LOGV("AsyncResponseHandler", "Timeout");
status_ = AsyncResponseStatus::kTimeout;
this->emit(status_);
}
this->timeout_reaction_ = nullptr;
});
}

void set(const bool& success) override {
ESP_LOGV("AsyncResponseHandler", "status: %d", status_);
if (status_ != AsyncResponseStatus::kPending) {
ESP_LOGV("AsyncResponseHandler",
"Received response when not in pending state");
return;
}

// Clear the timeout reaction
if (timeout_reaction_ != nullptr) {
ReactESP::app->remove(timeout_reaction_);
timeout_reaction_ = nullptr;
}

ESP_LOGV("AsyncResponseHandler", "Received response: %d", success);

if (success) {
status_ = AsyncResponseStatus::kSuccess;
} else {
status_ = AsyncResponseStatus::kFailure;
}
this->emit(status_);
}

const AsyncResponseStatus get_status() const { return status_; }

protected:
DelayReaction* timeout_reaction_ = nullptr;
AsyncResponseStatus status_ = AsyncResponseStatus::kReady;
String result_message_;
int timeout_ = 3000; // Default timeout in ms
elapsedMillis elapsed_millis_;
};

} // namespace sensesp

#endif // SENSESP_SRC_SENSESP_SIGNALK_SYSTEM_ASYNC_RESPONSE_HANDLER_H_
7 changes: 2 additions & 5 deletions src/sensesp/system/configurable.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#include "sensesp.h"

#include "configurable.h"

#include "SPIFFS.h"
#include "sensesp.h"
#include "sensesp/system/hash.h"

namespace sensesp {
Expand All @@ -22,10 +23,6 @@ Configurable::Configurable(String config_path, String description,
}
}

void Configurable::get_configuration(JsonObject& doc) {
debugW("WARNING: get_configuration not defined");
}

// Sets and saves the configuration
bool Configurable::set_configuration(const JsonObject& config) {
debugW("WARNING: set_configuration not defined for this Class");
Expand Down
Loading

0 comments on commit ad96f0f

Please sign in to comment.