diff --git a/include/villas/api.hpp b/include/villas/api.hpp index 819a9d26d..266c484bb 100644 --- a/include/villas/api.hpp +++ b/include/villas/api.hpp @@ -109,7 +109,7 @@ protected: void worker(); public: - /** Initalize the API. + /** Initialize the API. * * Save references to list of paths / nodes for command execution. */ diff --git a/include/villas/api/requests/universal.hpp b/include/villas/api/requests/universal.hpp new file mode 100644 index 000000000..dbaf496d2 --- /dev/null +++ b/include/villas/api/requests/universal.hpp @@ -0,0 +1,49 @@ +/** Universal Data-exchange API request. + * + * @file + * @author Steffen Vogel + * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include +#include + +namespace villas { +namespace node { + +/* Forward declarations */ +class Node; + +namespace api { + +class UniversalRequest : public NodeRequest { + +protected: + APINode *api_node; + +public: + using NodeRequest::NodeRequest; + + virtual void + prepare(); +}; + +} /* namespace api */ +} /* namespace node */ +} /* namespace villas */ diff --git a/include/villas/nodes/api.hpp b/include/villas/nodes/api.hpp new file mode 100644 index 000000000..fcc63a507 --- /dev/null +++ b/include/villas/nodes/api.hpp @@ -0,0 +1,62 @@ +/** Node type: Universal Data-exchange API + * + * @file + * @author Steffen Vogel + * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#pragma once + +#include +#include + +namespace villas { +namespace node { + +/* Forward declarations */ +struct Sample; + +class APINode : public Node { + +public: + APINode(const std::string &name = ""); + + struct Direction { + Sample *sample; + pthread_cond_t cv; + pthread_mutex_t mutex; + }; + + // Accessed by api::universal::SignalRequest + Direction read, write; + + virtual + int prepare(); + +protected: + + virtual + int _read(struct Sample *smps[], unsigned cnt); + + virtual + int _write(struct Sample *smps[], unsigned cnt); +}; + +} /* namespace node */ +} /* namespace villas */ diff --git a/include/villas/path.hpp b/include/villas/path.hpp index 41b73ee77..103a44a7e 100644 --- a/include/villas/path.hpp +++ b/include/villas/path.hpp @@ -129,13 +129,13 @@ public: os << CLR_MAG("=>"); - if (p.destinations.size() > 1) + if (p.destinations.size() != 1) os << " ["; for (auto pd : p.destinations) os << " " << pd->getNode()->getNameShort(); - if (p.destinations.size() > 1) + if (p.destinations.size() != 1) os << " ]"; return os; diff --git a/lib/api/CMakeLists.txt b/lib/api/CMakeLists.txt index 7de37ef0e..c112114c7 100644 --- a/lib/api/CMakeLists.txt +++ b/lib/api/CMakeLists.txt @@ -28,6 +28,7 @@ set(API_SRC requests/node.cpp requests/path.cpp + requests/universal.cpp requests/status.cpp requests/capabiltities.cpp @@ -43,6 +44,11 @@ set(API_SRC requests/paths.cpp requests/path_info.cpp requests/path_action.cpp + + requests/universal/config.cpp + requests/universal/info.cpp + requests/universal/signal.cpp + requests/universal/signals.cpp ) if(WITH_GRAPHVIZ) diff --git a/lib/api/requests/universal.cpp b/lib/api/requests/universal.cpp new file mode 100644 index 000000000..90ccb10fd --- /dev/null +++ b/lib/api/requests/universal.cpp @@ -0,0 +1,36 @@ +/** Universal Data-exchange API request + * + * @file + * @author Steffen Vogel + * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include + +using namespace villas::node; +using namespace villas::node::api; + +void UniversalRequest::prepare() +{ + NodeRequest::prepare(); + + api_node = dynamic_cast(node); + if (!api_node) + throw BadRequest("Node {} is not an univeral API node!", node->getNameShort()); +} diff --git a/lib/api/requests/universal/config.cpp b/lib/api/requests/universal/config.cpp new file mode 100644 index 000000000..440b33b29 --- /dev/null +++ b/lib/api/requests/universal/config.cpp @@ -0,0 +1,57 @@ +/** The Universal Data-exchange API. + * + * @author Steffen Vogel + * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include +#include +#include + +namespace villas { +namespace node { +namespace api { +namespace universal { + +class ConfigRequest : public UniversalRequest { +public: + using UniversalRequest::UniversalRequest; + + virtual Response * execute() + { + if (method != Session::Method::GET) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("This endpoint does not accept any body data"); + + return new JsonResponse(session, HTTP_STATUS_OK, node->getConfig()); + } +}; + +/* Register API requests */ +static char n[] = "universal/config"; +static char r[] = "/universal/(" RE_NODE_NAME ")/config"; +static char d[] = "get configuration of universal data-exchange API"; +static RequestPlugin p; + +} /* namespace universal */ +} /* namespace api */ +} /* namespace node */ +} /* namespace villas */ diff --git a/lib/api/requests/universal/info.cpp b/lib/api/requests/universal/info.cpp new file mode 100644 index 000000000..81c98cdd6 --- /dev/null +++ b/lib/api/requests/universal/info.cpp @@ -0,0 +1,74 @@ +/** The Universal Data-exchange API. + * + * @author Steffen Vogel + * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include + +#include +#include +#include + +namespace villas { +namespace node { +namespace api { +namespace universal { + +class InfoRequest : public UniversalRequest { +public: + using UniversalRequest::UniversalRequest; + + virtual Response * execute() + { + if (method != Session::Method::GET) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("This endpoint does not accept any body data"); + + auto uid = node->getUuid(); + + char uid_str[UUID_STR_LEN]; + uuid_unparse(uid, uid_str); + + auto *info = json_pack("{ s: s, s: s, s: { s: s, s: s, s: s } }", + "id", node->getNameShort().c_str(), + "uuid", uid_str, + + "transport", + "type", "villas", + "version", PROJECT_VERSION, + "build", PROJECT_BUILD_ID + ); + + return new JsonResponse(session, HTTP_STATUS_OK, info); + } +}; + +/* Register API requests */ +static char n[] = "universal/info"; +static char r[] = "/universal/(" RE_NODE_NAME ")/info"; +static char d[] = "get infos of universal data-exchange API"; +static RequestPlugin p; + +} /* namespace universal */ +} /* namespace api */ +} /* namespace node */ +} /* namespace villas */ diff --git a/lib/api/requests/universal/signal.cpp b/lib/api/requests/universal/signal.cpp new file mode 100644 index 000000000..ca3666245 --- /dev/null +++ b/lib/api/requests/universal/signal.cpp @@ -0,0 +1,139 @@ +/** The Universal Data-exchange API. + * + * @author Steffen Vogel + * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include +#include +#include + +namespace villas { +namespace node { +namespace api { +namespace universal { + +class SignalRequest : public UniversalRequest { +public: + using UniversalRequest::UniversalRequest; + + Response * executeGet(const std::string &signalName) + { + if (body != nullptr) + throw BadRequest("This endpoint does not accept any body data"); + + pthread_mutex_lock(&api_node->write.mutex); + + auto *smp = api_node->write.sample; + if (!smp) { + pthread_mutex_unlock(&api_node->write.mutex); + throw Error(HTTP_STATUS_NOT_FOUND, "No data available"); + } + + auto idx = smp->signals->getIndexByName(signalName); + if (idx < 0) { + pthread_mutex_unlock(&api_node->write.mutex); + throw Error(HTTP_STATUS_NOT_FOUND, "Unknown signal id: {}", signalName); + } + + auto sig = smp->signals->getByIndex(idx); + auto *json_signal = json_pack("{ s: f, s: o }", + "timestamp", time_to_double(&smp->ts.origin), + "value", smp->data[idx].toJson(sig->type) + ); + + if (smp->length <= (unsigned) idx) + smp->length = idx + 1; + + smp->flags |= (int) SampleFlags::HAS_TS_ORIGIN | (int) SampleFlags::HAS_DATA; + + pthread_mutex_unlock(&api_node->write.mutex); + + logger->info("resp: {}", (void *) json_signal); + + return new JsonResponse(session, HTTP_STATUS_OK, json_signal); + } + + Response * executePut(const std::string &signalName) + { + int ret; + + pthread_mutex_lock(&api_node->read.mutex); + + auto *smp = api_node->read.sample; + if (!smp) { + pthread_mutex_unlock(&api_node->read.mutex); + throw Error(HTTP_STATUS_INTERNAL_SERVER_ERROR, "Not initialized yet"); + } + + auto idx = smp->signals->getIndexByName(signalName); + if (idx < 0) { + pthread_mutex_unlock(&api_node->read.mutex); + throw BadRequest("Unknown signal id: {}", signalName); + } + + auto sig = smp->signals->getByIndex(idx); + + double timestamp = 0; + double value = 0; + + json_error_t err; + ret = json_unpack_ex(body, &err, 0, "{ s: f, s: f }", + "timestamp", ×tamp, + "value", &value + ); + if (ret) { + pthread_mutex_unlock(&api_node->read.mutex); + throw BadRequest("Malformed body: {}", err.text); + } + + smp->ts.origin = time_from_double(timestamp); + smp->data[idx].f = value; + + pthread_cond_signal(&api_node->read.cv); + pthread_mutex_unlock(&api_node->read.mutex); + + return new JsonResponse(session, HTTP_STATUS_OK, json_object()); + } + + virtual Response * execute() + { + auto const &signalName = matches[2]; + + switch (method) { + case Session::Method::GET: + return executeGet(signalName); + case Session::Method::PUT: + return executePut(signalName); + default: + throw InvalidMethod(this); + } + } +}; + +/* Register API requests */ +static char n[] = "universal/signal"; +static char r[] = "/universal/(" RE_NODE_NAME ")/signal/([a-z0-9_-]+)/state"; +static char d[] = "get or set signal of universal data-exchange API"; +static RequestPlugin p; + +} /* namespace universal */ +} /* namespace api */ +} /* namespace node */ +} /* namespace villas */ diff --git a/lib/api/requests/universal/signals.cpp b/lib/api/requests/universal/signals.cpp new file mode 100644 index 000000000..e27664346 --- /dev/null +++ b/lib/api/requests/universal/signals.cpp @@ -0,0 +1,81 @@ +/** The Universal Data-exchange API. + * + * @author Steffen Vogel + * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include +#include + +namespace villas { +namespace node { +namespace api { +namespace universal { + +class SignalsRequest : public UniversalRequest { +public: + using UniversalRequest::UniversalRequest; + + virtual Response * execute() + { + if (method != Session::Method::GET) + throw InvalidMethod(this); + + if (body != nullptr) + throw BadRequest("This endpoint does not accept any body data"); + + auto *json_sigs = json_array(); + for (auto const &sig : *node->getOutputSignals()) { + auto *json_sig = json_pack("{ s: s, s: s, s: b, s: b, s: o }", + "id", fmt::format("out/{}", sig->name).c_str(), + "source", node->getNameShort().c_str(), + "readable", true, + "writable", false, + "value", json_null() + ); + + json_array_append(json_sigs, json_sig); + } + + for (auto const &sig : *node->getInputSignals()) { + auto *json_sig = json_pack("{ s: s, s: s, s: b, s: b, s: o }", + "id", fmt::format("in/{}", sig->name).c_str(), + "source", node->getNameShort().c_str(), + "readable", false, + "writable", true, + "value", json_null() + ); + + json_array_append(json_sigs, json_sig); + } + + return new JsonResponse(session, HTTP_STATUS_OK, json_sigs); + } +}; + +/* Register API requests */ +static char n[] = "universal/signals"; +static char r[] = "/universal/(" RE_NODE_NAME ")/signals"; +static char d[] = "get signals of universal data-exchange API node"; +static RequestPlugin p; + +} /* namespace universal */ +} /* namespace api */ +} /* namespace node */ +} /* namespace villas */ diff --git a/lib/nodes/CMakeLists.txt b/lib/nodes/CMakeLists.txt index 90999bf51..ff0bcb5bb 100644 --- a/lib/nodes/CMakeLists.txt +++ b/lib/nodes/CMakeLists.txt @@ -24,6 +24,10 @@ set(NODE_SRC loopback_internal.cpp ) +if(WITH_WEB) + list(APPEND NODE_SRC api.cpp) +endif() + # Enable Golang support if(WITH_NODE_GO) list(APPEND NODE_SRC go.cpp) diff --git a/lib/nodes/api.cpp b/lib/nodes/api.cpp new file mode 100644 index 000000000..30ff33a1c --- /dev/null +++ b/lib/nodes/api.cpp @@ -0,0 +1,93 @@ +/** Node type: Universal Data-exchange API + * + * @author Steffen Vogel + * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC + * @license GNU General Public License (version 3) + * + * VILLASnode + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *********************************************************************************/ + +#include +#include +#include + +using namespace villas; +using namespace villas::node; + +APINode::APINode(const std::string &name) : + Node(name) +{ + int ret; + auto dirs = std::vector{&read, &write}; + + for (auto dir : dirs) { + ret = pthread_mutex_init(&dir->mutex, nullptr); + if (ret) + throw RuntimeError("failed to initialize mutex"); + + ret = pthread_cond_init(&dir->cv, nullptr); + if (ret) + throw RuntimeError("failed to initialize mutex"); + } +} + +int APINode::prepare() +{ + auto signals_in = getInputSignals(false); + + read.sample = sample_alloc_mem(signals_in->size()); + if (!read.sample) + throw MemoryAllocationError(); + + write.sample = sample_alloc_mem(64); + if (!write.sample) + throw MemoryAllocationError(); + + unsigned j = 0; + for (auto sig : *signals_in) + read.sample->data[j++] = sig->init; + + read.sample->length = j; + read.sample->signals = signals_in; + + return Node::prepare(); +} + +int APINode::_read(struct Sample *smps[], unsigned cnt) +{ + assert(cnt == 1); + + pthread_cond_wait(&read.cv, &read.mutex); + + sample_copy(smps[0], read.sample); + + return 1; +} + +int APINode::_write(struct Sample *smps[], unsigned cnt) +{ + assert(cnt == 1); + + sample_copy(write.sample, smps[0]); + + pthread_cond_signal(&write.cv); + + return 1; +} + +static char n[] = "api"; +static char d[] = "A node providing a HTTP REST interface"; +static NodePlugin p;