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:
commit
eeb1244cc4
18 changed files with 548 additions and 63 deletions
2
common
2
common
|
@ -1 +1 @@
|
|||
Subproject commit 25cd53ee6882c3f66746d6d8c27790ef22d18322
|
||||
Subproject commit 950857f1d792e2dad81d6e1f2e5b2d8cab60fd71
|
|
@ -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
|
||||
|
|
|
@ -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
102
etc/examples/api.conf
Normal 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"
|
||||
)
|
||||
}
|
||||
)
|
|
@ -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
2
fpga
|
@ -1 +1 @@
|
|||
Subproject commit 36be2ddb52e968e9fd7d22f7548c1f3b539ca845
|
||||
Subproject commit 8b994bfb84ea8cb96e070f7f7bd3bf84b587bfcf
|
|
@ -6,6 +6,8 @@
|
|||
* @license Apache 2.0
|
||||
*********************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
#include <villas/api/request.hpp>
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
* @license Apache 2.0
|
||||
*********************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <villas/nodes/api.hpp>
|
||||
#include <villas/api/requests/node.hpp>
|
||||
|
||||
|
|
57
include/villas/api/universal.hpp
Normal file
57
include/villas/api/universal.hpp
Normal 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 */
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
using namespace villas::node;
|
||||
using namespace villas::node::api;
|
||||
using namespace villas::node::api::universal;
|
||||
|
||||
void UniversalRequest::prepare()
|
||||
{
|
||||
|
|
|
@ -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", ×tamp,
|
||||
"value", &value
|
||||
"value", &json_value,
|
||||
"validity", &validity,
|
||||
"timesource", ×ource,
|
||||
"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 */
|
|
@ -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 */
|
|
@ -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
145
lib/api/universal.cpp
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue