1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/node/ synced 2025-03-09 00:00:00 +01:00

Merge pull request #627 from VILLASframework/node-api-v2

implement v2 of the universal data-exchange API node-type
This commit is contained in:
Steffen Vogel 2022-12-16 23:42:55 +01:00 committed by GitHub
commit eeb1244cc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 548 additions and 63 deletions

2
common

@ -1 +1 @@
Subproject commit 25cd53ee6882c3f66746d6d8c27790ef22d18322
Subproject commit 950857f1d792e2dad81d6e1f2e5b2d8cab60fd71

View file

@ -2,14 +2,26 @@
---
allOf:
- type: object
additionalProperties: true
description: |
Additional properties can be retrieved via Rest API call:
```bash
curl http://localhost:80/api/v2/universal/api_node_name/config
```
- $ref: ../node_signals.yaml
- $ref: ../node.yaml
- type: object
properties:
in:
type: object
required:
- signals
properties:
signals:
type: array
items:
$ref: ./signals/api_signal.yaml
out:
type: object
required:
- signals
properties:
signals:
type: array
items:
$ref: ./signals/api_signal.yaml

View file

@ -0,0 +1,41 @@
# yaml-language-server: $schema=http://json-schema.org/draft-07/schema
---
allOf:
- type: object
properties:
description:
type: string
description: A human readable description of the channel.
payload:
description: |
Describes the type of information which is exchanged over the channel.
type: string
enum:
- events
- samples
range:
oneOf:
- type: object
description: Limits for numeric datatypes
properties:
min:
type: number
max:
type: number
- type: array
description: A list of allowed string values for string datatype
items:
type: string
rate:
optional: true
type: number
description: |
Expected refresh-rate in Hertz of this channel
Does not apply channels which have event payloads.
- $ref: ../../signal.yaml

102
etc/examples/api.conf Normal file
View file

@ -0,0 +1,102 @@
http = {
port = 8080
}
nodes = {
api_node = {
type = "api"
in = {
signals = (
{
name = "sig1_in",
type = "float",
unit = "V",
description = "Signal 1",
rate = 100,
readable = true,
writable = false,
payload = "samples"
},
{
name = "sig2_in",
type = "float",
unit = "A",
description = "Signal 1",
rate = 100,
readable = true,
writable = false,
payload = "samples"
},
{
name = "sig3_in",
type = "float",
unit = "A",
description = "Signal 1",
rate = 100,
readable = true,
writable = false,
payload = "samples"
}
)
}
out = {
signals = (
# Output signals have no name, type and unit settings as those are implicitly
# derived from the signals which are routed to this node
{
description = "Signal 1",
rate = 100,
readable = true,
writable = false,
payload = "samples"
},
{
description = "Signal 1",
rate = 100,
readable = true,
writable = false,
payload = "samples"
},
{
description = "Signal 1",
rate = 100,
readable = true,
writable = false,
payload = "samples"
}
)
}
}
signal_node = {
type = "signal"
signal = "mixed"
values = 5
rate = 1.0
}
}
paths = (
{
in = [
"api_node"
],
hooks = (
"print"
)
},
{
in = [
"signal_node"
]
out = [
"api_node"
]
hooks = (
"print"
)
}
)

View file

@ -2,11 +2,29 @@ nodes = {
api_node = {
type = "api"
// Additional configuration can be retrieved via Rest API call:
// curl http://localhost:80/api/v2/universal/api_node/config
my_setting = "my_value"
a = {
b = false
in = {
signals = (
{
name = "" # Same as 'id' in uAPI context
description = "Volts on Bus A" # A human readable description of the channel
type = "float" # Same as 'datatype' in uAPI context
unit = "V"
payload = "events" # or 'samples'
rate = 100.0 # An expected refresh/sample rate of the signal
range = {
min = 20.0
max = 100.0
}
readable = true
writable = false
}
)
}
out = {
signals = (
# Similar to above
)
}
}
}

2
fpga

@ -1 +1 @@
Subproject commit 36be2ddb52e968e9fd7d22f7548c1f3b539ca845
Subproject commit 8b994bfb84ea8cb96e070f7f7bd3bf84b587bfcf

View file

@ -6,6 +6,8 @@
* @license Apache 2.0
*********************************************************************************/
#pragma once
#include <uuid/uuid.h>
#include <villas/api/request.hpp>

View file

@ -6,6 +6,8 @@
* @license Apache 2.0
*********************************************************************************/
#pragma once
#include <villas/nodes/api.hpp>
#include <villas/api/requests/node.hpp>

View file

@ -0,0 +1,57 @@
/** Universal Data-exchange API request.
*
* @file
* @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
* @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
* @license Apache 2.0
*********************************************************************************/
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <jansson.h>
#include <villas/signal.hpp>
namespace villas {
namespace node {
namespace api {
namespace universal {
enum PayloadType {
SAMPLES = 0,
EVENTS = 1
};
// Channel (uAPI) is a synonym for signal (VILLAS)
class Channel {
public:
std::string description;
PayloadType payload;
double range_min;
double range_max;
std::vector<std::string> range_options;
double rate;
bool readable;
bool writable;
using Ptr = std::shared_ptr<Channel>;
void parse(json_t *json);
json_t * toJson(Signal::Ptr sig) const;
};
class ChannelList : public std::vector<Channel::Ptr> {
public:
void parse(json_t *json, bool readable, bool writable);
};
} /* namespace universal */
} /* namespace api */
} /* namespace node */
} /* namespace villas */

View file

@ -1,5 +1,6 @@
/** Node type: Universal Data-exchange API
/** Node type: Universal Data-exchange API (v2)
*
* @see https://github.com/ERIGrid2/JRA-3.1-api
* @file
* @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
* @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
@ -9,6 +10,7 @@
#pragma once
#include <villas/node.hpp>
#include <villas/api/universal.hpp>
#include <pthread.h>
namespace villas {
@ -24,6 +26,7 @@ public:
struct Direction {
Sample *sample;
api::universal::ChannelList channels;
pthread_cond_t cv;
pthread_mutex_t mutex;
};
@ -34,7 +37,12 @@ public:
virtual
int prepare();
virtual
int check();
protected:
virtual
int parse(json_t *json, const uuid_t sn_uuid);
virtual
int _read(struct Sample *smps[], unsigned cnt);

View file

@ -10,6 +10,7 @@ set(API_SRC
session.cpp
request.cpp
response.cpp
universal.cpp
requests/node.cpp
requests/path.cpp
@ -30,10 +31,10 @@ set(API_SRC
requests/path_info.cpp
requests/path_action.cpp
requests/universal/config.cpp
requests/universal/status.cpp
requests/universal/info.cpp
requests/universal/signal.cpp
requests/universal/signals.cpp
requests/universal/channel.cpp
requests/universal/channels.cpp
)
if(WITH_GRAPHVIZ)

View file

@ -10,6 +10,7 @@
using namespace villas::node;
using namespace villas::node::api;
using namespace villas::node::api::universal;
void UniversalRequest::prepare()
{

View file

@ -14,11 +14,11 @@ namespace node {
namespace api {
namespace universal {
class SignalRequest : public UniversalRequest {
class ChannelRequest : public UniversalRequest {
public:
using UniversalRequest::UniversalRequest;
Response * executeGet(const std::string &signalName)
Response * executeGet(const std::string &signalName, PayloadType payload)
{
if (body != nullptr)
throw BadRequest("This endpoint does not accept any body data");
@ -38,9 +38,17 @@ public:
}
auto sig = smp->signals->getByIndex(idx);
auto *json_signal = json_pack("{ s: f, s: o }",
auto ch = api_node->write.channels.at(idx);
if (payload != ch->payload)
throw BadRequest("Mismatching payload type");
auto *json_signal = json_pack("{ s: f, s: o, s: s, s: s, s: s }",
"timestamp", time_to_double(&smp->ts.origin),
"value", smp->data[idx].toJson(sig->type)
"value", smp->data[idx].toJson(sig->type),
"validity", "unknown",
"source", "unknown",
"timesource", "unknown"
);
if (smp->length <= (unsigned) idx)
@ -53,7 +61,7 @@ public:
return new JsonResponse(session, HTTP_STATUS_OK, json_signal);
}
Response * executePut(const std::string &signalName)
Response * executePut(const std::string &signalName, PayloadType payload)
{
int ret;
@ -72,22 +80,46 @@ public:
}
auto sig = smp->signals->getByIndex(idx);
auto ch = api_node->read.channels.at(idx);
if (payload != ch->payload)
throw BadRequest("Mismatching payload type");
double timestamp = 0;
double value = 0;
json_t *json_value;
const char *validity = nullptr;
const char *source = nullptr;
const char *timesource = nullptr;
json_error_t err;
ret = json_unpack_ex(body, &err, 0, "{ s: F, s: F }",
ret = json_unpack_ex(body, &err, 0, "{ s: F, s: o, s?: s, s?: s, s?: s }",
"timestamp", &timestamp,
"value", &value
"value", &json_value,
"validity", &validity,
"timesource", &timesource,
"source", &source
);
if (ret) {
pthread_mutex_unlock(&api_node->read.mutex);
throw BadRequest("Malformed body: {}", err.text);
}
if (validity)
logger->warn("Attribute 'validity' is not supported by VILLASnode");
if (source)
logger->warn("Attribute 'source' is not supported by VILLASnode");
if (timesource)
logger->warn("Attribute 'timesource' is not supported by VILLASnode");
ret = smp->data[idx].parseJson(sig->type, json_value);
if (ret) {
pthread_mutex_unlock(&api_node->read.mutex);
throw BadRequest("Malformed value");
}
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);
@ -98,12 +130,21 @@ public:
virtual Response * execute()
{
auto const &signalName = matches[2];
auto const &subResource = matches[3];
PayloadType payload;
if (subResource == "event")
payload = PayloadType::EVENTS;
else if (subResource == "sample")
payload = PayloadType::EVENTS;
else
throw BadRequest("Unsupported sub-resource: {}", subResource);
switch (method) {
case Session::Method::GET:
return executeGet(signalName);
return executeGet(signalName, payload);
case Session::Method::PUT:
return executePut(signalName);
return executePut(signalName, payload);
default:
throw InvalidMethod(this);
}
@ -111,10 +152,10 @@ public:
};
/* 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<SignalRequest, n, r, d> p;
static char n[] = "universal/channel/sample";
static char r[] = "/universal/(" RE_NODE_NAME ")/channel/([a-z0-9_-]+)/(sample|event)";
static char d[] = "retrieve or send samples via universal data-exchange API";
static RequestPlugin<ChannelRequest, n, r, d> p;
} /* namespace universal */
} /* namespace api */

View file

@ -5,6 +5,7 @@
* @license Apache 2.0
*********************************************************************************/
#include <villas/utils.hpp>
#include <villas/api/requests/universal.hpp>
#include <villas/api/response.hpp>
@ -26,26 +27,19 @@ public:
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()
);
for (size_t i = 0; i < MIN(api_node->getOutputSignals()->size(), api_node->write.channels.size()); i++) {
auto sig = api_node->getOutputSignals()->at(i);
auto ch = api_node->write.channels.at(i);
auto *json_sig = ch->toJson(sig);
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()
);
for (size_t i = 0; i < MIN(api_node->getInputSignals()->size(), api_node->read.channels.size()); i++) {
auto sig = api_node->getInputSignals()->at(i);
auto ch = api_node->read.channels.at(i);
auto *json_sig = ch->toJson(sig);
json_array_append(json_sigs, json_sig);
}
@ -55,9 +49,9 @@ public:
};
/* 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 char n[] = "universal/channels";
static char r[] = "/universal/(" RE_NODE_NAME ")/channels";
static char d[] = "get channels of universal data-exchange API node";
static RequestPlugin<SignalsRequest, n, r, d> p;
} /* namespace universal */

View file

@ -14,7 +14,7 @@ namespace node {
namespace api {
namespace universal {
class ConfigRequest : public UniversalRequest {
class StatusRequest : public UniversalRequest {
public:
using UniversalRequest::UniversalRequest;
@ -26,15 +26,20 @@ public:
if (body != nullptr)
throw BadRequest("This endpoint does not accept any body data");
return new JsonResponse(session, HTTP_STATUS_OK, node->getConfig());
auto *json_response = json_pack("{ s: s }",
// TODO: Add connectivity check or heuristic here.
"connected", "unknown"
);
return new JsonResponse(session, HTTP_STATUS_OK, json_response);
}
};
/* 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<ConfigRequest, n, r, d> p;
static char n[] = "universal/status";
static char r[] = "/universal/(" RE_NODE_NAME ")/status";
static char d[] = "get status of universal data-exchange API";
static RequestPlugin<StatusRequest, n, r, d> p;
} /* namespace universal */
} /* namespace api */

145
lib/api/universal.cpp Normal file
View file

@ -0,0 +1,145 @@
/** API Response.
*
* @file
* @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
* @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
* @license Apache 2.0
*********************************************************************************/
#include <limits>
#include <cmath>
#include <villas/exceptions.hpp>
#include <villas/api/universal.hpp>
using namespace villas::node::api::universal;
void ChannelList::parse(json_t *json, bool readable, bool writable)
{
if (!json_is_array(json))
throw ConfigError(json, "node-config-node-api-signals", "Signal list of API node must be an array");
clear();
size_t i;
json_t *json_channel;
json_array_foreach(json, i, json_channel) {
auto channel = std::make_shared<Channel>();
channel->parse(json_channel);
channel->readable = readable;
channel->writable = writable;
push_back(channel);
}
}
void Channel::parse(json_t *json)
{
const char *desc = nullptr;
const char *pl = nullptr;
json_t *json_range = nullptr;
json_error_t err;
int ret = json_unpack_ex(json, &err, 0, "{ s?: s, s?: s, s?: o, s?: F }",
"description", &desc,
"payload", &pl,
"range", &json_range,
"rate", &rate
);
if (ret)
throw ConfigError(json, err, "node-config-node-api-signals");
if (desc)
description = desc;
if (pl) {
if (!strcmp(pl, "samples"))
payload = PayloadType::SAMPLES;
else if (!strcmp(pl, "events"))
payload = PayloadType::EVENTS;
else
throw ConfigError(json, "node-config-node-api-signals-payload", "Invalid payload type: {}", pl);
}
range_min = std::numeric_limits<double>::quiet_NaN();
range_max = std::numeric_limits<double>::quiet_NaN();
if (json_range) {
if (json_is_array(json_range)) {
ret = json_unpack_ex(json, &err, 0, "{ s?: F, s?: F }",
"min", &range_min,
"max", &range_max
);
if (ret)
throw ConfigError(json, err, "node-config-node-api-signals-range", "Failed to parse channel range");
} else if (json_is_object(json_range)) {
size_t i;
json_t *json_option;
range_options.clear();
json_array_foreach(json_range, i, json_option) {
if (!json_is_string(json_option))
throw ConfigError(json, err, "node-config-node-api-signals-range", "Channel range options must be strings");
auto *option = json_string_value(json_option);
range_options.push_back(option);
}
} else
throw ConfigError(json, "node-config-node-api-signals-range", "Channel range must be an array or object");
}
}
json_t * Channel::toJson(Signal::Ptr sig) const
{
json_error_t err;
json_t *json_ch = json_pack_ex(&err, 0, "{ s: s, s: s, s: b, s: b }",
"id", sig->name.c_str(),
"datatype", signalTypeToString(sig->type).c_str(),
"readable", (int) readable,
"writable", (int) writable
);
if (!description.empty())
json_object_set(json_ch, "description", json_string(description.c_str()));
if (!sig->unit.empty())
json_object_set(json_ch, "unit", json_string(sig->unit.c_str()));
if (rate > 0)
json_object_set(json_ch, "rate", json_real(rate));
switch (payload) {
case PayloadType::EVENTS:
json_object_set(json_ch, "payload", json_string("events"));
break;
case PayloadType::SAMPLES:
json_object_set(json_ch, "payload", json_string("samples"));
break;
default: {}
}
switch (sig->type) {
case SignalType::FLOAT: {
if (std::isnan(range_min) && std::isnan(range_max))
break;
json_t *json_range = json_object();
if (!std::isnan(range_min))
json_object_set(json_range, "min", json_real(range_min));
if (!std::isnan(range_max))
json_object_set(json_range, "max", json_real(range_max));
json_object_set(json_ch, "range", json_range);
break;
}
default: {}
}
return json_ch;
}

View file

@ -1,16 +1,20 @@
/** Node type: Universal Data-exchange API
/** Node type: Universal Data-exchange API (v2)
*
* @see https://github.com/ERIGrid2/JRA-3.1-api
* @author Steffen Vogel <svogel2@eonerc.rwth-aachen.de>
* @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
* @license Apache 2.0
*********************************************************************************/
#include <vector>
#include <villas/exceptions.hpp>
#include <villas/api/universal.hpp>
#include <villas/nodes/api.hpp>
using namespace villas;
using namespace villas::node;
using namespace villas::node::api::universal;
APINode::APINode(const std::string &name) :
Node(name),
@ -53,6 +57,21 @@ int APINode::prepare()
return Node::prepare();
}
int APINode::check()
{
for (auto &ch : read.channels) {
if (ch->payload != PayloadType::SAMPLES)
return -1;
}
for (auto &ch : write.channels) {
if (ch->payload != PayloadType::SAMPLES)
return -1;
}
return 0;
}
int APINode::_read(struct Sample *smps[], unsigned cnt)
{
assert(cnt == 1);
@ -75,6 +94,34 @@ int APINode::_write(struct Sample *smps[], unsigned cnt)
return 1;
}
int APINode::parse(json_t *json, const uuid_t sn_uuid)
{
int ret = Node::parse(json, sn_uuid);
if (ret)
return ret;
json_t *json_signals_in = nullptr;
json_t *json_signals_out = nullptr;
json_error_t err;
ret = json_unpack_ex(json, &err, 0, "{ s?: { s?: o }, s?: { s?: o } }",
"in",
"signals", &json_signals_in,
"out",
"signals", &json_signals_out
);
if (ret)
throw ConfigError(json, err, "node-config-node-api");
if (json_signals_in)
read.channels.parse(json_signals_in, false, true);
if (json_signals_out)
write.channels.parse(json_signals_out, true, false);
return 0;
}
static char n[] = "api";
static char d[] = "A node providing a HTTP REST interface";
static NodePlugin<APINode, n , d, (int) NodeFactory::Flags::SUPPORTS_READ | (int) NodeFactory::Flags::SUPPORTS_WRITE, 1> p;

View file

@ -187,14 +187,23 @@ int SignalData::parseJson(enum SignalType type, json_t *json)
switch (type) {
case SignalType::FLOAT:
if (!json_is_number(json))
return -1;
this->f = json_number_value(json);
break;
case SignalType::INTEGER:
if (!json_is_integer(json))
return -1;
this->i = json_integer_value(json);
break;
case SignalType::BOOLEAN:
if (!json_is_boolean(json))
return -1;
this->b = json_boolean_value(json);
break;
@ -207,14 +216,14 @@ int SignalData::parseJson(enum SignalType type, json_t *json)
"imag", &imag
);
if (ret)
return -2;
return -1;
this->z = std::complex<float>(real, imag);
break;
}
case SignalType::INVALID:
return -1;
return -2;
}
return 0;