Skip to content
Merged
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ set(NEURON_SOURCES
plugins/restful/metric_handle.c
plugins/restful/normal_handle.c
plugins/restful/rw_handle.c
plugins/restful/read_response.c
plugins/restful/adapter_handle.c
plugins/restful/datatag_handle.c
plugins/restful/global_config_handle.c
Expand Down
117 changes: 117 additions & 0 deletions docs/api/cn/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,123 @@ HTTP 200 响应:
}
```

## 读取点位值

*POST* /api/v2/read

### 请求头部

**Content-Type** application/json

**Authorization** Bearer \<token\>

### 请求体

```json
{
"node": "modbus-tcp-1",
"group": "group-1",
"sync": false
}
```

`sync` 为可选布尔值,默认为 `false`,读取缓存;`true` 请求同步读取。

### 响应(200 OK)

```json
{
"tags": [
{
"name": "temperature",
"value": 25.5,
"transferPrecision": 2,
"timestamp": 1700000000000,
"quality": 192,
"source_timestamp": 1700000000123,
"server_timestamp": 1700000000456
},
{
"name": "pressure",
"error": 3002,
"quality": 0
}
]
}
```

成功点位返回 `value`,错误点位返回 `error`;浮点值附带 `transferPrecision`。以下采样属性对成功和错误点位均可用,位于点位对象顶层:

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| timestamp | 整数(int64) | 驱动提供的原有点位时间戳,原样传递,不转换单位 |
| quality | 整数(int32) | 驱动提供的质量值 |
| source_timestamp | 整数(int64) | 源时间戳,Unix epoch 毫秒 |
| server_timestamp | 整数(int64) | 服务器时间戳,Unix epoch 毫秒 |

四个字段独立可选:仅在当前样本具有对应属性时返回,显式 `0` 会保留;缺失时省略,不返回 `null`,不生成时间戳,也不使用其他时间戳替代。

原有点位元数据(metas)仍在点位对象顶层展开。如果与采样属性同名,存在的采样属性覆盖同名元数据;采样属性缺失时保留原元数据。

## 分页读取点位值

*POST* /api/v2/read/paginate

请求头部与读取点位值接口相同。

### 请求体

```json
{
"node": "modbus-tcp-1",
"group": "group-1",
"sync": false,
"query": {
"currentPage": 1,
"pageSize": 10,
"isError": false
}
}
```

`query` 可选。分页参数位于 `query` 中,`currentPage` 从 1 开始;`isError: true` 仅返回错误点位,`false` 不启用错误筛选。

### 响应(200 OK)

```json
{
"meta": {
"currentPage": 1,
"pageSize": 10,
"total": 1
},
"items": [
{
"name": "temperature",
"type": 10,
"address": "1!400001",
"attribute": 1,
"description": "",
"precision": 2,
"decimal": 0,
"bias": 0,
"unit": "°C",
"value": 25.5,
"transferPrecision": 2,
"timestamp": 1700000000000,
"quality": 192,
"source_timestamp": 1700000000123,
"server_timestamp": 1700000000456,
"attributes": {}
}
]
}
```

`items[]` 中四个可选采样属性的规则与 `/api/v2/read` 相同。原有 metas 放在 `attributes` 对象中,与顶层采样属性独立;`attributes` 不是采样属性容器。`type` 仍为配置类型(AUTO 为 `0`),不因运行时识别而改变。空结果或超出范围的页返回空 `items` 数组。

这些采样属性属于读取结果,`GET /api/v2/tags` 的配置响应不增加这些字段。

## 添加插件

*POST* /api/v2/plugin
Expand Down
117 changes: 117 additions & 0 deletions docs/api/english/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,123 @@ To update both group name and interval:
}
```

## Read Tag Values

*POST* /api/v2/read

### Request Headers

**Content-Type** application/json

**Authorization** Bearer \<token\>

### Body

```json
{
"node": "modbus-tcp-1",
"group": "group-1",
"sync": false
}
```

`sync` is an optional boolean, defaulting to `false` to read cached values. Set it to `true` to request a synchronous read.

### Response (200 OK)

```json
{
"tags": [
{
"name": "temperature",
"value": 25.5,
"transferPrecision": 2,
"timestamp": 1700000000000,
"quality": 192,
"source_timestamp": 1700000000123,
"server_timestamp": 1700000000456
},
{
"name": "pressure",
"error": 3002,
"quality": 0
}
]
}
```

A successful tag returns `value`; an error tag returns `error`. Floating-point values also include `transferPrecision`. Both successful and error samples may include these top-level sample attributes:

| Field | Type | Description |
| --- | --- | --- |
| timestamp | Integer (int64) | Existing driver-provided tag timestamp, passed through without unit conversion |
| quality | Integer (int32) | Driver-provided quality value |
| source_timestamp | Integer (int64) | Source timestamp in Unix epoch milliseconds |
| server_timestamp | Integer (int64) | Server timestamp in Unix epoch milliseconds |

The four fields are independently optional. A field is returned only when the current sample supplies it; explicit `0` is preserved. Missing fields are omitted, not returned as `null`. No timestamps are generated or substituted from another timestamp field.

Existing tag metadata (metas) remains flattened into the tag object. A present sample attribute replaces a metadata field with the same name; when the sample attribute is absent, the original metadata field is preserved.

## Read Tag Values with Pagination

*POST* /api/v2/read/paginate

Request headers are the same as for reading tag values.

### Body

```json
{
"node": "modbus-tcp-1",
"group": "group-1",
"sync": false,
"query": {
"currentPage": 1,
"pageSize": 10,
"isError": false
}
}
```

`query` is optional. Pagination parameters belong inside `query`, and `currentPage` starts at 1. Set `isError` to `true` to return only error tags; `false` does not enable error filtering.

### Response (200 OK)

```json
{
"meta": {
"currentPage": 1,
"pageSize": 10,
"total": 1
},
"items": [
{
"name": "temperature",
"type": 10,
"address": "1!400001",
"attribute": 1,
"description": "",
"precision": 2,
"decimal": 0,
"bias": 0,
"unit": "°C",
"value": 25.5,
"transferPrecision": 2,
"timestamp": 1700000000000,
"quality": 192,
"source_timestamp": 1700000000123,
"server_timestamp": 1700000000456,
"attributes": {}
}
]
}
```

The four optional sample attributes in `items[]` follow the same rules as `/api/v2/read`. Existing metas remain inside `attributes`, separate from the top-level sample attributes; `attributes` is not a sample-attribute container. `type` remains the configured type (including `0` for AUTO), not the inferred runtime type. Empty results and out-of-range pages return an empty `items` array.

These sample attributes belong to read responses; the configuration response from `GET /api/v2/tags` is unchanged.

## Add Plugin

*POST* /api/v2/plugin
Expand Down
2 changes: 2 additions & 0 deletions plugins/restful/adapter_handle.c
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ static int json_to_gdatatags(neu_json_gtag_array_t *gtag_array,
gdatatags[i].tags[j].bias = gtag_array->gtags[i].tags[j].bias;
gdatatags[i].tags[j].address = gtag_array->gtags[i].tags[j].address;
gdatatags[i].tags[j].name = gtag_array->gtags[i].tags[j].name;
gdatatags[i].tags[j].unit = gtag_array->gtags[i].tags[j].unit;
if (gtag_array->gtags[i].tags[j].description != NULL) {
gdatatags[i].tags[j].description =
gtag_array->gtags[i].tags[j].description;
Expand All @@ -560,6 +561,7 @@ static int json_to_gdatatags(neu_json_gtag_array_t *gtag_array,
gtag_array->gtags[i].tags[j].address = NULL; // moved
gtag_array->gtags[i].tags[j].name = NULL; // moved
gtag_array->gtags[i].tags[j].description = NULL; // moved
gtag_array->gtags[i].tags[j].unit = NULL; // moved
}
}

Expand Down
8 changes: 6 additions & 2 deletions plugins/restful/global_config_handle.c
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,7 @@ static int get_tags_resp(context_t *ctx, neu_resp_get_tag_t *tags)
tags_res.tags[index].precision = tag->precision;
tags_res.tags[index].decimal = tag->decimal;
tags_res.tags[index].bias = tag->bias;
tags_res.tags[index].unit = tag->unit;
tags_res.tags[index].t = NEU_JSON_UNDEFINE;
}

Expand Down Expand Up @@ -1228,10 +1229,12 @@ static int get_driver_tags_resp(context_t *ctx, neu_resp_get_tag_t *tags)
gtag->tags[index].precision = tag->precision;
gtag->tags[index].decimal = tag->decimal;
gtag->tags[index].bias = tag->bias;
gtag->tags[index].unit = tag->unit;
gtag->tags[index].t = NEU_JSON_UNDEFINE;
tag->name = NULL; // moved
tag->address = NULL; // moved
tag->description = NULL; // moved
tag->unit = NULL; // moved
}

end:
Expand Down Expand Up @@ -1500,9 +1503,10 @@ static int add_tag(context_t *ctx, neu_json_add_tags_req_t *data)
cmd.tags[i].name = strdup(data->tags[i].name);
cmd.tags[i].description =
strdup(data->tags[i].description ? data->tags[i].description : "");
cmd.tags[i].unit = strdup(data->tags[i].unit ? data->tags[i].unit : "");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve units in global-config exports

When PUT /api/v2/global/config receives a non-empty tag unit, this line now preserves it in the live tag, but the corresponding GET path builds its response in get_tags_resp without assigning tags_res.tags[index].unit (the assignments at lines 1143-1151 omit it). neu_json_encode_tag consequently serializes the zero-initialized pointer as "", so the new nonempty round-trip assertion fails and exporting then re-importing the configuration silently clears the unit; copy tag->unit into the response tags in this sibling export path.

Useful? React with 👍 / 👎.


if (NULL == cmd.tags[i].address || NULL == cmd.tags[i].name ||
NULL == cmd.tags[i].description) {
NULL == cmd.tags[i].description || NULL == cmd.tags[i].unit) {
ret = NEU_ERR_EINTERNAL;
goto error;
}
Expand All @@ -1516,7 +1520,7 @@ static int add_tag(context_t *ctx, neu_json_add_tags_req_t *data)
return ret;

error:
for (int j = 0; j < i && j < cmd.n_tag; ++j) {
for (int j = 0; j < cmd.n_tag; ++j) {
neu_tag_fini(&cmd.tags[j]);
}
free(cmd.tags);
Expand Down
80 changes: 80 additions & 0 deletions plugins/restful/read_response.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#include "msg.h"

#include "read_response.h"

static int encode_attrs(json_t *object, const neu_tag_sample_attrs_t *attrs)
{
const struct {
const char *name;
bool present;
int64_t value;
} fields[] = {
{ "timestamp", attrs->has_timestamp, attrs->timestamp },
{ "quality", attrs->has_quality, attrs->quality },
{ "source_timestamp", attrs->has_source_timestamp,
attrs->source_timestamp },
{ "server_timestamp", attrs->has_server_timestamp,
attrs->server_timestamp },
};

for (size_t i = 0; i < sizeof(fields) / sizeof(fields[0]); i++) {
if (fields[i].present &&
json_object_set_new(object, fields[i].name,
json_integer(fields[i].value)) != 0) {
return -1;
}
}
return 0;
}

int neu_http_encode_read_response(void *json_object, void *param)
{
neu_http_read_response_t *ctx = param;
json_t * object = json_object;
if (ctx == NULL || ctx->response == NULL) {
return -1;
}

int count = ctx->paginate
? ((neu_json_read_paginate_resp_t *) ctx->response)->n_tag
: ((neu_json_read_resp_t *) ctx->response)->n_tag;
unsigned int length = ctx->tags ? utarray_len(ctx->tags) : 0;
if (!json_is_object(object) || count < 0 ||
(count > 0 &&
(ctx->offset > length ||
(unsigned int) count > length - ctx->offset))) {
if (ctx->paginate) {
neu_json_read_paginate_resp_t *response = ctx->response;
for (int i = 0; response->tags && i < count; i++) {
free(response->tags[i].datatag.name);
free(response->tags[i].datatag.address);
free(response->tags[i].datatag.description);
free(response->tags[i].datatag.unit);
}
}
return -1;
}

int ret = ctx->paginate
? neu_json_encode_read_paginate_resp(json_object, ctx->response)
: neu_json_encode_read_resp(json_object, ctx->response);
if (ret != 0) {
return ret;
}

json_t *items =
json_object_get(json_object, ctx->paginate ? "items" : "tags");
if (!json_is_array(items) || json_array_size(items) != (size_t) count) {
return -1;
}
for (int i = 0; i < count; i++) {
void *tag = utarray_eltptr(ctx->tags, ctx->offset + (unsigned int) i);
const neu_tag_sample_attrs_t *attrs = ctx->paginate
? &((neu_resp_tag_value_meta_paginate_t *) tag)->value.attrs
: &((neu_resp_tag_value_meta_t *) tag)->value.attrs;
if (encode_attrs(json_array_get(items, i), attrs) != 0) {
return -1;
}
}
return 0;
}
Loading
Loading