Skip to content

Commit 9410dce

Browse files
committed
feat:add modbus-tcp simulator
1 parent 811ba04 commit 9410dce

File tree

14 files changed

+1729
-3
lines changed

14 files changed

+1729
-3
lines changed

CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ set(NEURON_SOURCES
169169
plugins/restful/system_handle.c
170170
plugins/restful/datalayers_handle.c
171171
plugins/restful/rest.c
172-
plugins/restful/user.c)
172+
plugins/restful/user.c
173+
plugins/restful/simulator_handle.c
174+
simulator/modbus_tcp_simulator.c)
173175

174176
set(CMAKE_BUILD_RPATH ./)
175177
add_executable(neuron)

include/neuron/errcodes.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ typedef enum {
5353
NEU_ERR_USER_NOT_EXISTS = 1020,
5454
NEU_ERR_INVALID_USER_LEN = 1021,
5555
NEU_ERR_USER_NO_PERMISSION = 1022,
56+
NEU_ERR_PORT_IN_USE = 1023,
5657

5758
NEU_ERR_NODE_EXIST = 2002,
5859
NEU_ERR_NODE_NOT_EXIST = 2003,
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* NEURON IIoT System for Industry 4.0
3+
* Copyright (C) 2020-2025 EMQ Technologies Co., Ltd All rights reserved.
4+
*
5+
* This program is free software; you can redistribute it and/or
6+
* modify it under the terms of the GNU Lesser General Public
7+
* License as published by the Free Software Foundation; either
8+
* version 3 of the License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Lesser General Public License
16+
* along with this program; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18+
**/
19+
#ifndef NEU_MODBUS_TCP_SIMULATOR_H
20+
#define NEU_MODBUS_TCP_SIMULATOR_H
21+
22+
#include <stdbool.h>
23+
#include <stdint.h>
24+
25+
int neu_modbus_simulator_init(const char *config_dir);
26+
void neu_modbus_simulator_fini(void);
27+
28+
int neu_modbus_simulator_start(const char *ip, uint16_t port);
29+
void neu_modbus_simulator_stop(void);
30+
void neu_modbus_simulator_apply_persist(void);
31+
32+
typedef enum {
33+
NEU_SIM_TAG_NONE = 0,
34+
NEU_SIM_TAG_SINE = 1, // float, [-100,100], period 60s
35+
NEU_SIM_TAG_SAW = 2, // int16, [0,100], period 100s
36+
NEU_SIM_TAG_SQUARE = 3, // int16, [-10,10], period 10s
37+
NEU_SIM_TAG_RANDOM = 4 // int16, [0,100]
38+
} neu_sim_tag_type_e;
39+
40+
int neu_modbus_simulator_config_tags(const char **names, const char **addr_strs,
41+
const uint16_t * addresses,
42+
const neu_sim_tag_type_e *types,
43+
int tag_count);
44+
45+
char *neu_modbus_simulator_export_drivers_json(void);
46+
char *neu_modbus_simulator_list_tags_json(void);
47+
48+
typedef struct {
49+
bool running;
50+
char ip[64];
51+
uint16_t port;
52+
int tag_count;
53+
int error;
54+
} neu_modbus_simulator_status_t;
55+
56+
void neu_modbus_simulator_get_status(neu_modbus_simulator_status_t *out);
57+
58+
#endif

neuron.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"disable_auth": 0,
55
"syslog_host": "",
66
"syslog_port": 541,
7-
"sub_filter_error": 0
8-
}
7+
"sub_filter_error": 0,
8+
"modbus_simulator": { "port":502 }
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE IF NOT EXISTS modbus_tcp_simulator (
2+
id INTEGER PRIMARY KEY CHECK (id = 1),
3+
enabled INTEGER NOT NULL DEFAULT 0,
4+
tags_json TEXT NOT NULL DEFAULT '[]',
5+
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
6+
);

plugins/restful/handle.c

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#include "version_handle.h"
4545

4646
#include "handle.h"
47+
#include "simulator_handle.h"
4748

4849
struct neu_rest_handle_ctx {
4950
void *plugin;
@@ -190,6 +191,24 @@ static struct neu_http_handler cors_handler[] = {
190191
{
191192
.url = "/api/v2/auth/basic/security_policies",
192193
},
194+
{
195+
.url = "/api/v2/simulator/status",
196+
},
197+
{
198+
.url = "/api/v2/simulator/start",
199+
},
200+
{
201+
.url = "/api/v2/simulator/stop",
202+
},
203+
{
204+
.url = "/api/v2/simulator/config",
205+
},
206+
{
207+
.url = "/api/v2/simulator/tags",
208+
},
209+
{
210+
.url = "/api/v2/simulator/export",
211+
},
193212
};
194213

195214
static struct neu_http_handler rest_handlers[] = {
@@ -686,6 +705,42 @@ static struct neu_http_handler rest_handlers[] = {
686705
.url = "/api/v2/auth/basic/security_policies",
687706
.value.handler = handle_server_security_policy,
688707
},
708+
{
709+
.method = NEU_HTTP_METHOD_GET,
710+
.type = NEU_HTTP_HANDLER_FUNCTION,
711+
.url = "/api/v2/simulator/status",
712+
.value.handler = handle_simulator_status,
713+
},
714+
{
715+
.method = NEU_HTTP_METHOD_GET,
716+
.type = NEU_HTTP_HANDLER_FUNCTION,
717+
.url = "/api/v2/simulator/tags",
718+
.value.handler = handle_simulator_list_tags,
719+
},
720+
{
721+
.method = NEU_HTTP_METHOD_POST,
722+
.type = NEU_HTTP_HANDLER_FUNCTION,
723+
.url = "/api/v2/simulator/start",
724+
.value.handler = handle_simulator_start,
725+
},
726+
{
727+
.method = NEU_HTTP_METHOD_POST,
728+
.type = NEU_HTTP_HANDLER_FUNCTION,
729+
.url = "/api/v2/simulator/stop",
730+
.value.handler = handle_simulator_stop,
731+
},
732+
{
733+
.method = NEU_HTTP_METHOD_POST,
734+
.type = NEU_HTTP_HANDLER_FUNCTION,
735+
.url = "/api/v2/simulator/config",
736+
.value.handler = handle_simulator_set_config,
737+
},
738+
{
739+
.method = NEU_HTTP_METHOD_GET,
740+
.type = NEU_HTTP_HANDLER_FUNCTION,
741+
.url = "/api/v2/simulator/export",
742+
.value.handler = handle_simulator_export,
743+
},
689744
};
690745

691746
void neu_rest_handler(const struct neu_http_handler **handlers, uint32_t *size)

plugins/restful/simulator_handle.c

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
#include <ctype.h>
2+
#include <jansson.h>
3+
#include <stdlib.h>
4+
#include <string.h>
5+
6+
#include "handle.h"
7+
#include "modbus_tcp_simulator.h"
8+
#include "utils/http.h"
9+
#include "utils/log.h"
10+
#include "json/neu_json_fn.h"
11+
12+
static int extract_hold_start_from_addr(const char *addr_str,
13+
uint16_t * out_idx0)
14+
{
15+
if (!addr_str || !out_idx0)
16+
return -1;
17+
const char *p = strstr(addr_str, "!4");
18+
if (!p)
19+
return -1;
20+
p += 2;
21+
if (!isdigit((unsigned char) *p))
22+
return -1;
23+
long v = strtol(p, NULL, 10);
24+
if (v <= 0 || v > 65535)
25+
return -1;
26+
*out_idx0 = (uint16_t)(v - 1);
27+
return 0;
28+
}
29+
30+
void handle_simulator_status(nng_aio *aio)
31+
{
32+
NEU_VALIDATE_JWT(aio);
33+
neu_modbus_simulator_status_t st = { 0 };
34+
neu_modbus_simulator_get_status(&st);
35+
char buf[256] = { 0 };
36+
snprintf(buf, sizeof(buf),
37+
"{\"running\":%d,\"port\":%u,\"tag_count\":%d,\"error\":%d}",
38+
st.running ? 1 : 0, st.port, st.tag_count, st.error);
39+
neu_http_ok(aio, strdup(buf));
40+
}
41+
42+
void handle_simulator_start(nng_aio *aio)
43+
{
44+
NEU_VALIDATE_JWT(aio);
45+
int rc = neu_modbus_simulator_start(NULL, 0);
46+
47+
neu_modbus_simulator_status_t st = { 0 };
48+
neu_modbus_simulator_get_status(&st);
49+
if (rc != 0 || (!st.running && st.error != 0)) {
50+
char buf[64] = { 0 };
51+
snprintf(buf, sizeof(buf), "{\"error\":%d}", st.error);
52+
neu_http_ok(aio, strdup(buf));
53+
return;
54+
}
55+
neu_http_ok(aio, strdup("{\"error\":0}"));
56+
}
57+
58+
void handle_simulator_list_tags(nng_aio *aio)
59+
{
60+
NEU_VALIDATE_JWT(aio);
61+
char *out = neu_modbus_simulator_list_tags_json();
62+
if (!out) {
63+
NEU_JSON_RESPONSE_ERROR(NEU_ERR_EINTERNAL, {
64+
neu_http_response(aio, NEU_ERR_EINTERNAL, result_error);
65+
});
66+
return;
67+
}
68+
neu_http_ok(aio, out);
69+
}
70+
71+
void handle_simulator_stop(nng_aio *aio)
72+
{
73+
NEU_VALIDATE_JWT(aio);
74+
neu_modbus_simulator_stop();
75+
neu_http_ok(aio, strdup("{\"error\":0}"));
76+
}
77+
78+
void handle_simulator_set_config(nng_aio *aio)
79+
{
80+
NEU_VALIDATE_JWT(aio);
81+
char * body = NULL;
82+
size_t sz = 0;
83+
if (neu_http_get_body(aio, (void **) &body, &sz) != 0 || !body) {
84+
NEU_JSON_RESPONSE_ERROR(NEU_ERR_PARAM_IS_WRONG, {
85+
neu_http_response(aio, NEU_ERR_PARAM_IS_WRONG, result_error);
86+
});
87+
return;
88+
}
89+
90+
json_error_t jerr;
91+
json_t * root = json_loads(body, 0, &jerr);
92+
free(body);
93+
if (!root) {
94+
NEU_JSON_RESPONSE_ERROR(NEU_ERR_PARAM_IS_WRONG, {
95+
neu_http_response(aio, NEU_ERR_PARAM_IS_WRONG, result_error);
96+
});
97+
return;
98+
}
99+
json_t *tags = json_object_get(root, "tags");
100+
if (!tags || !json_is_array(tags)) {
101+
json_decref(root);
102+
NEU_JSON_RESPONSE_ERROR(NEU_ERR_PARAM_IS_WRONG, {
103+
neu_http_response(aio, NEU_ERR_PARAM_IS_WRONG, result_error);
104+
});
105+
return;
106+
}
107+
108+
int count = 0;
109+
for (size_t i = 0; i < json_array_size(tags); ++i) {
110+
json_t *obj = json_array_get(tags, i);
111+
if (!obj || !json_is_object(obj))
112+
continue;
113+
json_t *jaddr = json_object_get(obj, "address");
114+
json_t *jtype = json_object_get(obj, "type");
115+
if (!jaddr || !json_is_string(jaddr) || !jtype)
116+
continue;
117+
const char *addr_str = json_string_value(jaddr);
118+
uint16_t idx0 = 0;
119+
if (extract_hold_start_from_addr(addr_str, &idx0) != 0)
120+
continue;
121+
count++;
122+
}
123+
124+
char ** names = NULL;
125+
char ** addresses = NULL;
126+
uint16_t * indices = NULL;
127+
neu_sim_tag_type_e *types = NULL;
128+
if (count > 0) {
129+
names = calloc((size_t) count, sizeof(char *));
130+
addresses = calloc((size_t) count, sizeof(char *));
131+
indices = calloc((size_t) count, sizeof(uint16_t));
132+
types = calloc((size_t) count, sizeof(neu_sim_tag_type_e));
133+
if (!names || !addresses || !indices || !types) {
134+
free(names);
135+
free(addresses);
136+
free(indices);
137+
free(types);
138+
json_decref(root);
139+
NEU_JSON_RESPONSE_ERROR(NEU_ERR_EINTERNAL, {
140+
neu_http_response(aio, NEU_ERR_EINTERNAL, result_error);
141+
});
142+
return;
143+
}
144+
}
145+
146+
int m = 0;
147+
for (size_t i = 0; i < json_array_size(tags); ++i) {
148+
json_t *obj = json_array_get(tags, i);
149+
if (!obj || !json_is_object(obj))
150+
continue;
151+
json_t *jname = json_object_get(obj, "name");
152+
json_t *jaddr = json_object_get(obj, "address");
153+
json_t *jtype = json_object_get(obj, "type");
154+
if (!jaddr || !json_is_string(jaddr) || !jtype)
155+
continue;
156+
const char *addr_str = json_string_value(jaddr);
157+
uint16_t idx0 = 0;
158+
if (extract_hold_start_from_addr(addr_str, &idx0) != 0)
159+
continue;
160+
neu_sim_tag_type_e tp = NEU_SIM_TAG_NONE;
161+
if (json_is_string(jtype)) {
162+
const char *ts = json_string_value(jtype);
163+
if (ts) {
164+
if (strcmp(ts, "sine") == 0)
165+
tp = NEU_SIM_TAG_SINE;
166+
else if (strcmp(ts, "saw") == 0)
167+
tp = NEU_SIM_TAG_SAW;
168+
else if (strcmp(ts, "square") == 0)
169+
tp = NEU_SIM_TAG_SQUARE;
170+
else if (strcmp(ts, "random") == 0)
171+
tp = NEU_SIM_TAG_RANDOM;
172+
}
173+
} else if (json_is_integer(jtype)) {
174+
tp = (neu_sim_tag_type_e) json_integer_value(jtype);
175+
}
176+
if (tp == NEU_SIM_TAG_NONE || idx0 >= 1000)
177+
continue;
178+
indices[m] = idx0;
179+
types[m] = tp;
180+
names[m] = jname && json_is_string(jname)
181+
? strdup(json_string_value(jname))
182+
: strdup("");
183+
addresses[m] = strdup(addr_str);
184+
m++;
185+
}
186+
json_decref(root);
187+
188+
neu_modbus_simulator_config_tags(
189+
(const char **) names, (const char **) addresses, indices, types, m);
190+
191+
for (int i = 0; i < m; ++i) {
192+
free(names[i]);
193+
free(addresses[i]);
194+
}
195+
free(names);
196+
free(addresses);
197+
free(indices);
198+
free(types);
199+
neu_http_ok(aio, strdup("{\"error\":0}"));
200+
}
201+
202+
void handle_simulator_export(nng_aio *aio)
203+
{
204+
NEU_VALIDATE_JWT(aio);
205+
char *drivers = neu_modbus_simulator_export_drivers_json();
206+
if (!drivers) {
207+
NEU_JSON_RESPONSE_ERROR(NEU_ERR_EINTERNAL, {
208+
neu_http_response(aio, NEU_ERR_EINTERNAL, result_error);
209+
});
210+
return;
211+
}
212+
neu_http_response_file(
213+
aio, drivers, strlen(drivers),
214+
"attachment; filename=\"simulator_modbus_south.json\"");
215+
free(drivers);
216+
}

0 commit comments

Comments
 (0)