mirror of
https://gitee.com/dgiiot/dgiot.git
synced 2024-11-29 18:57:41 +08:00
bridge
This commit is contained in:
parent
6159414e8e
commit
e394422f2d
2
.gitignore
vendored
2
.gitignore
vendored
@ -47,4 +47,4 @@ dist.zip
|
||||
scripts/git-token
|
||||
etc/*.seg
|
||||
_upgrade_base/
|
||||
apps/dgiot_sinmahe
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author dgiot
|
||||
%%% @copyright (C) 2019, shuwa
|
||||
%%% @copyright (C) 2019, dgiot
|
||||
%%% @doc
|
||||
%%% API 处理模块 产生时间: {% now "r" %}
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(dgiot_{{ mod }}_handler).
|
||||
-author("shuwa").
|
||||
-author("dgiot").
|
||||
-behavior(dgiot_rest).
|
||||
-compile([{parse_transform, lager_transform}]).
|
||||
|
||||
@ -80,4 +80,4 @@ do_request({{ api.operationid }}, _Args, Context, Req) ->
|
||||
|
||||
%% 服务器不支持的API接口
|
||||
do_request(_OperationId, _Args, _Context, _Req) ->
|
||||
{error, <<"Not Allowed.">>}.
|
||||
{error, <<"Not Allowed.">>}.
|
||||
|
@ -15,7 +15,7 @@
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(dgiot_role_handler).
|
||||
-author("shuwa").
|
||||
-author("dgiot").
|
||||
-include_lib("dgiot/include/logger.hrl").
|
||||
-behavior(dgiot_rest).
|
||||
-dgiot_rest(all).
|
||||
|
@ -15,7 +15,7 @@
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(dgiot_system_handler).
|
||||
-author("shuwa").
|
||||
-author("dgiot").
|
||||
-include_lib("dgiot/include/logger.hrl").
|
||||
-behavior(dgiot_rest).
|
||||
|
||||
|
@ -190,10 +190,12 @@ load_channel() ->
|
||||
false ->
|
||||
?LOG(error, "~p is not json.", [Json]);
|
||||
Filter ->
|
||||
?LOG(error, "Filter:~p", [Filter]),
|
||||
start_channel(dgiot_bridge, Filter)
|
||||
end
|
||||
end, Filters);
|
||||
_ ->
|
||||
_Other ->
|
||||
?LOG(error, "_Other:~p", [_Other]),
|
||||
ok
|
||||
end.
|
||||
|
||||
|
@ -29,7 +29,6 @@
|
||||
%%====================================================================
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
?LOG(error,"_StartType ~p, _StartArgs ~p",[_StartType, _StartArgs]),
|
||||
{ok, Sup} = dgiot_bridge_sup:start_link(),
|
||||
dgiot_bridge:start(),
|
||||
{ok, Sup}.
|
||||
|
@ -80,6 +80,7 @@ handle_info({start_channel, Module, #{<<"objectId">> := ChannelId, <<"type">> :=
|
||||
{noreply, State};
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
?LOG(error, "_Info ~p, State:~p", [_Info, State]),
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
@ -153,7 +154,6 @@ do_handle(#{<<"channelId">> := ChannelId, <<"action">> := <<"start_logger">>} =
|
||||
{ok, _Type, ProductIds} = dgiot_bridge:get_products(ChannelId),
|
||||
Fmt = "Channel[~s] is Running, Products:~s, Log is ~s",
|
||||
Args = [ChannelId, jsx:encode(ProductIds), true],
|
||||
dgiot_logger:info(Fmt, Args),
|
||||
dgiot_data:insert(?ETS, {ChannelId, log}, NewFilter#{
|
||||
<<"time">> => dgiot_datetime:nowstamp()
|
||||
}),
|
||||
|
@ -26,7 +26,7 @@
|
||||
|
||||
%% 存储产品
|
||||
%%-define(SMART_PROD, mnesia_smartprod).
|
||||
%%-record(shuwa_prod, {
|
||||
%%-record(dgiot_prod, {
|
||||
%% key, % [ProductId], [产品ID]
|
||||
%% product % 产品基本数据,map类型
|
||||
%%}).
|
||||
@ -64,9 +64,9 @@ lookup_prod(ProductId) ->
|
||||
case dgiot_mnesia:lookup(ProductId) of
|
||||
{atomic, []} ->
|
||||
{error, not_find};
|
||||
{aborted, Reason} ->
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
{atomic, [{mnesia, _K, V}]} ->
|
||||
{ok, [{mnesia, _K, V}]} ->
|
||||
{Product, _} = V,
|
||||
{ok, Product}
|
||||
end.
|
||||
|
6
apps/dgiot_modbus/.gitignore
vendored
Normal file
6
apps/dgiot_modbus/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.eunit
|
||||
deps
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
201
apps/dgiot_modbus/LICENSE
Normal file
201
apps/dgiot_modbus/LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
24
apps/dgiot_modbus/Makefile
Normal file
24
apps/dgiot_modbus/Makefile
Normal file
@ -0,0 +1,24 @@
|
||||
PROJECT = shuwa_modbus
|
||||
PROJECT_DESCRIPTION = shuwa_modbus Protocol Plugin
|
||||
PROJECT_VERSION = 1.5.4
|
||||
|
||||
CUR_BRANCH := $(shell git branch | grep -e "^*" | cut -d' ' -f 2)
|
||||
BRANCH := $(if $(filter $(CUR_BRANCH), master develop), $(CUR_BRANCH), develop)
|
||||
|
||||
BUILD_DEPS = emqx cuttlefish lager
|
||||
dep_emqx = git-emqx https://github.com/emqx/emqx $(BRANCH)
|
||||
dep_cuttlefish = git-emqx https://github.com/emqx/cuttlefish v2.2.1
|
||||
dep_lager = git https://github.com/basho/lager master
|
||||
|
||||
DIALYZER_DIRS := ebin/
|
||||
DIALYZER_OPTS := --verbose --statistics -Werror_handling \
|
||||
-Wrace_conditions #-Wunmatched_returns
|
||||
|
||||
ERLC_OPTS += +'{parse_transform, lager_transform}'
|
||||
|
||||
include erlang.mk
|
||||
|
||||
app:: rebar.config
|
||||
|
||||
app.config::
|
||||
./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/shuwa_modbus.conf -d data
|
4
apps/dgiot_modbus/README.md
Normal file
4
apps/dgiot_modbus/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
## dgiot_modbus
|
||||
|
||||
dgiot_modbus
|
||||
|
10
apps/dgiot_modbus/etc/dgiot_modbus.conf
Normal file
10
apps/dgiot_modbus/etc/dgiot_modbus.conf
Normal file
@ -0,0 +1,10 @@
|
||||
modbus.ip = 127.0.0.1
|
||||
|
||||
modbus.port = 502
|
||||
|
||||
modbus.protocol = tcp
|
||||
|
||||
modbus.unit_id = 255
|
||||
|
||||
modbus.callback = modbus_demo_callback
|
||||
|
118
apps/dgiot_modbus/include/dgiot_modbus.hrl
Normal file
118
apps/dgiot_modbus/include/dgiot_modbus.hrl
Normal file
@ -0,0 +1,118 @@
|
||||
-define(READ_DISCRETE_INPUTS, 2).
|
||||
-define(READ_COILS, 1).
|
||||
-define(WRITE_SINGLE_COIL, 5).
|
||||
-define(WRITE_MULTIPLE_COILS, 15).
|
||||
-define(READ_INPUT_REGISTERS, 4).
|
||||
-define(READ_HOLDING_REGISTERS, 3).
|
||||
-define(WRITE_SINGLE_HOLDING_REGISTER, 6).
|
||||
-define(WRITE_MULTIPLE_HOLDING_REGISTERS, 16).
|
||||
|
||||
-define(ILLEGAL_FUNCTION, 1).
|
||||
-define(ILLEGAL_DATA_ADDRESS, 2).
|
||||
-define(ILLEGAL_DATA_VALUE, 3).
|
||||
-define(SLAVE_DEVICE_FAILURE, 4).
|
||||
-define(ACKNOWLEDGE, 5).
|
||||
-define(SLAVE_DEVICE_BUSY, 6).
|
||||
-define(NEGATIVE_ACKNOWLEDGE, 7).
|
||||
-define(MEMORY_PARITY_ERROR, 8).
|
||||
-define(GATEWAY_PATH_UNAVAILABLE, 10).
|
||||
-define(GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND, 11).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
-define(FC_READ_COILS, 16#01). %读线圈寄存器
|
||||
-define(FC_READ_INPUTS, 16#02). %读离散输入寄存器
|
||||
-define(FC_READ_HREGS, 16#03). %读保持寄存器
|
||||
-define(FC_READ_IREGS, 16#04). %读输入寄存器
|
||||
|
||||
-define(FC_WRITE_COIL, 16#05). %写单个线圈寄存器
|
||||
-define(FC_WRITE_HREG, 16#06). %写单个保持寄存
|
||||
-define(FC_WRITE_COILS, 16#0f). %写多个线圈寄存器
|
||||
-define(FC_WRITE_HREGS, 16#10). %写多个保持寄存器
|
||||
|
||||
%%_____________________________________________
|
||||
%%表2 ModBus功能码与数据类型对应表 |
|
||||
%%|代码 |功能 |数据类型 |
|
||||
%%|01 |读 |位 |
|
||||
%%|02 |读 |位 |
|
||||
%%|03 |读 |整型、字符型、状态字、浮点型 |
|
||||
%%|04 |读 |整型、状态字、浮点型 |
|
||||
%%|05 |写 |位 |
|
||||
%%|06 |写 |整型、字符型、状态字、浮点型 |
|
||||
%%|08 |N/A |重复“回路反馈”信息 |
|
||||
%%|15 |写 |位 |
|
||||
%%|16 |写 |整型、字符型、状态字、浮点型 |
|
||||
%%——————————————————————————————————————————————
|
||||
|
||||
-record(rtu_req, {slaveId, funcode, address, quality}).
|
||||
-record(rtu_pdu, {slaveId, funcode, dataByteSize, data}).
|
||||
-record(tcp_request, {sock, tid = 1, address = 1, function, start, data }).
|
||||
|
||||
-record(tcp_request1, {
|
||||
id,
|
||||
tid = 1,
|
||||
address = 1,
|
||||
function,
|
||||
start,
|
||||
data,
|
||||
env = #{}
|
||||
}).
|
||||
|
||||
%%_________________________________________________________________________________________________________________________________________________________________
|
||||
%%表1 ModBus功能码
|
||||
%%_________________________________________________________________________________________________________________________________________________________________
|
||||
%%|功能码 | 名称 | 作用
|
||||
%%|01 |读取线圈状态 |取得一组逻辑线圈的当前状态(ON/OFF)
|
||||
%%|02 |读取输入状态 |取得一组开关输入的当前状态(ON/OFF)
|
||||
%%|03 |读取保持寄存器 | 在一个或多个保持寄存器中取得当前的二进制值
|
||||
%%|04 |读取输入寄存器 | 在一个或多个输入寄存器中取得当前的二进制值
|
||||
%%|05 |强置单线圈 |强置一个逻辑线圈的通断状态
|
||||
%%|06 |预置单寄存器 |把具体二进值装入一个保持寄存器
|
||||
%%|07 |读取异常状态 |取得8个内部线圈的通断状态,这8个线圈的地址由控制器决定,用户逻辑可以将这些线圈定义,以说明从机状态,短报文适宜于迅速读取状态
|
||||
%%|08 |回送诊断校验 |把诊断校验报文送从机,以对通信处理进行评鉴
|
||||
%%|09 |编程(只用于484) | 使主机模拟编程器作用,修改PC从机逻辑
|
||||
%%|10 |控询(只用于484) | 可使主机与一台正在执行长程序任务从机通信,探询该从机是否已完成其操作任务,仅在含有功能码9的报文发送后,本功能码才发送
|
||||
%%|11 |读取事件计数 |可使主机发出单询问,并随即判定操作是否成功,尤其是该命令或其他应答产生通信错误时
|
||||
%%|12 |读取通信事件记录 |可是主机检索每台从机的ModBus事务处理通信事件记录。如果某项事务处理完成,记录会给出有关错误
|
||||
%%|13 |编程(184/384 484 584) |可使主机模拟编程器功能修改PC从机逻辑
|
||||
%%|14 |探询(184/384 484 584) |可使主机与正在执行任务的从机通信,定期控询该从机是否已完成其程序操作,仅在含有功能13的报文发送后,本功能码才得发送
|
||||
%%|15 |强置多线圈 |强置一串连续逻辑线圈的通断
|
||||
%%|16 |预置多寄存器 |把具体的二进制值装入一串连续的保持寄存器
|
||||
%%|17 |报告从机标识 |可使主机判断编址从机的类型及该从机运行指示灯的状态
|
||||
%%|18 |(884和MICRO 84) |可使主机模拟编程功能,修改PC状态逻辑
|
||||
%%|19 |重置通信链路 |发生非可修改错误后,是从机复位于已知状态,可重置顺序字节
|
||||
%%|20 |读取通用参数(584L) |显示扩展存储器文件中的数据信息
|
||||
%%|21 |写入通用参数(584L) |把通用参数写入扩展存储文件,或修改之
|
||||
%%|22~64 | |保留作扩展功能备用
|
||||
%%|65~72 |保留以备用户功能所用 |留作用户功能的扩展编码
|
||||
%%|73~119 |非法功能 |
|
||||
%%|120~127| 保留 |留作内部作用
|
||||
%%|128~255| 保留 |用于异常应答
|
||||
%%__________________________________________________________________________________________________________________________________________________________________
|
||||
|
||||
%%modbus完整支持很多功能码,但是实际在应用的时候常用的也就那么几个。具体如下:
|
||||
%%
|
||||
%%0x01: 读线圈寄存器
|
||||
%%0x02: 读离散输入寄存器
|
||||
%%0x03: 读保持寄存器
|
||||
%%0x04: 读输入寄存器
|
||||
%%0x05: 写单个线圈寄存器
|
||||
%%0x06: 写单个保持寄存器
|
||||
%%0x0f: 写多个线圈寄存器
|
||||
%%0x10: 写多个保持寄存器
|
||||
%%如上所示一共8种功能码。这其中有涉及到线圈、离散输入、保持、输入四种寄存器。这名字也不知道谁起的,让人看了一点不通俗易懂,搞得晕晕乎乎。实际上你要是看清他的本质就很简单了。下面分别解释一下:
|
||||
%%
|
||||
%%线圈寄存器:实际上就可以类比为开关量,每个bit都对应一个信号的开关状态。所以一个byte就可以同时控制8路的信号。比如控制外部8路io的高低。 线圈寄存器支持读也支持写,写在功能码里面又分为写单个线圈寄存器和写多个线圈寄存器。对应上面的功能码也就是:0x01 0x05 0x0f
|
||||
%%
|
||||
%%离散输入寄存器:如果线圈寄存器理解了这个自然也明白了。离散输入寄存器就相当于线圈寄存器的只读模式,他也是每个bit表示一个开关量,而他的开关量只能读取输入的开关信号,是不能够写的。比如我读取外部按键的按下还是松开。所以功能码也简单就一个读的 0x02
|
||||
%%
|
||||
%%保持寄存器:这个寄存器的单位不再是bit而是两个byte,也就是可以存放具体的数据量的,并且是可读写的。比如我我设置时间年月日,不但可以写也可以读出来现在的时间。写也分为单个写和多个写,所以功能码有对应的三个:0x03 0x06 0x10
|
||||
%%
|
||||
%%输入寄存器:只剩下这最后一个了,这个和保持寄存器类似,但是也是只支持读而不能写。一个寄存器也是占据两个byte的空间。类比我我通过读取输入寄存器获取现在的AD采集值。对应的功能码也就一个 0x04
|
||||
%%
|
||||
%%对应的错误返回:
|
||||
%%在对应功能码基础上加上0x80
|
||||
%%
|
||||
%%1、“01”读取线圈状态发送:
|
||||
%%————————————————
|
||||
%%版权声明:本文为CSDN博主「JiaoCL」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
|
||||
%%原文链接:https://blog.csdn.net/liboxiu/article/details/86473516
|
24
apps/dgiot_modbus/priv/dgiot_modbus.schema
Normal file
24
apps/dgiot_modbus/priv/dgiot_modbus.schema
Normal file
@ -0,0 +1,24 @@
|
||||
{mapping, "modbus.ip", "dgiot_modbus.ip", [
|
||||
{default, "127.0.0.1"},
|
||||
{datatype, [ip, string]}
|
||||
]}.
|
||||
|
||||
{mapping, "modbus.port", "dgiot_modbus.port", [
|
||||
{default, 502},
|
||||
{datatype, integer}
|
||||
]}.
|
||||
|
||||
{mapping, "modbus.protocol", "dgiot_modbus.protocol", [
|
||||
{default, "tcp"},
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "modbus.unit_id", "dgiot_modbus.unit_id", [
|
||||
{default, 255},
|
||||
{datatype, integer}
|
||||
]}.
|
||||
|
||||
{mapping, "modbus.callback", "dgiot_modbus.modbus_demo_callback", [
|
||||
{default, "modbus_demo_callback"},
|
||||
{datatype, string}
|
||||
]}.
|
2
apps/dgiot_modbus/rebar.config
Normal file
2
apps/dgiot_modbus/rebar.config
Normal file
@ -0,0 +1,2 @@
|
||||
{deps, [
|
||||
]}.
|
8
apps/dgiot_modbus/src/dgiot_modbus.app.src
Normal file
8
apps/dgiot_modbus/src/dgiot_modbus.app.src
Normal file
@ -0,0 +1,8 @@
|
||||
{application, dgiot_modbus,
|
||||
[{description, "数蛙工业MODBUS"},
|
||||
{vsn, "1.6.4"},
|
||||
{modules, []},
|
||||
{registered, [dgiot_modbus_sup]},
|
||||
{applications, [kernel, stdlib, dgiot]},
|
||||
{mod, {dgiot_modbus_app, []}}
|
||||
]}.
|
25
apps/dgiot_modbus/src/dgiot_modbus.app.src.script
Normal file
25
apps/dgiot_modbus/src/dgiot_modbus.app.src.script
Normal file
@ -0,0 +1,25 @@
|
||||
%%-*- mode: erlang -*-
|
||||
%% .app.src.script
|
||||
|
||||
RemoveLeadingV =
|
||||
fun(Tag) ->
|
||||
case re:run(Tag, "v\[0-9\]+\.\[0-9\]+\.*") of
|
||||
nomatch ->
|
||||
Tag;
|
||||
{match, _} ->
|
||||
%% if it is a version number prefixed by 'v' then remove the 'v'
|
||||
"v" ++ Vsn = Tag,
|
||||
Vsn
|
||||
end
|
||||
end,
|
||||
|
||||
case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
|
||||
false -> CONFIG; % env var not defined
|
||||
[] -> CONFIG; % env var set to empty string
|
||||
Tag ->
|
||||
[begin
|
||||
AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
|
||||
{application, App, AppConf0}
|
||||
end || Conf = {application, App, AppConf} <- CONFIG]
|
||||
end.
|
||||
|
31
apps/dgiot_modbus/src/dgiot_modbus_app.erl
Normal file
31
apps/dgiot_modbus/src/dgiot_modbus_app.erl
Normal file
@ -0,0 +1,31 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(dgiot_modbus_app).
|
||||
|
||||
-emqx_plugin(?MODULE).
|
||||
|
||||
-behaviour(application).
|
||||
-include("dgiot_modbus.hrl").
|
||||
|
||||
%% Application callbacks
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
dgiot_modbus_sup:start_link().
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
34
apps/dgiot_modbus/src/dgiot_modbus_sup.erl
Normal file
34
apps/dgiot_modbus/src/dgiot_modbus_sup.erl
Normal file
@ -0,0 +1,34 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(dgiot_modbus_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-include("dgiot_modbus.hrl").
|
||||
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
{ok, {{one_for_one, 5, 10}, []}}.
|
||||
|
||||
|
194
apps/dgiot_modbus/src/modbus/modbus.erl
Normal file
194
apps/dgiot_modbus/src/modbus/modbus.erl
Normal file
@ -0,0 +1,194 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(modbus).
|
||||
|
||||
-include("dgiot_modbus.hrl").
|
||||
|
||||
%% preset unit id
|
||||
-export([read_discrete_inputs/3,
|
||||
read_coils/3,
|
||||
write_single_coil/3,
|
||||
write_multiple_coils/3,
|
||||
read_input_registers/3,
|
||||
read_holding_registers/3,
|
||||
write_single_holding_register/3,
|
||||
write_multiple_holding_registers/3]).
|
||||
|
||||
%%unit id as parameter
|
||||
-export([read_discrete_inputs/4,
|
||||
read_coils/4,
|
||||
write_single_coil/4,
|
||||
write_multiple_coils/4,
|
||||
read_input_registers/4,
|
||||
read_holding_registers/4,
|
||||
write_single_holding_register/4,
|
||||
write_multiple_holding_registers/4]).
|
||||
|
||||
-export([bits_to_coils/2]).
|
||||
-export([coils_to_bin/1]).
|
||||
|
||||
read_discrete_inputs(Pid, Addr, N)
|
||||
when is_integer(Addr), Addr >= 0 ->
|
||||
send_read_pdu(Pid, {pdu, ?READ_DISCRETE_INPUTS,
|
||||
<<Addr:16, N:16>>}, N).
|
||||
read_discrete_inputs(Pid, UnitId, Addr, N)
|
||||
when is_integer(UnitId), is_integer(Addr), Addr >= 0 ->
|
||||
send_read_pdu(Pid, {pdu, UnitId, ?READ_DISCRETE_INPUTS,
|
||||
<<Addr:16, N:16>>}, N).
|
||||
|
||||
read_coils(Pid, Addr, N)
|
||||
when is_integer(Addr), Addr >= 0 ->
|
||||
send_read_pdu(Pid, {pdu, ?READ_COILS, <<Addr:16, N:16>>}, N).
|
||||
read_coils(Pid, UnitId, Addr, N)
|
||||
when is_integer(UnitId), is_integer(Addr), Addr >= 0 ->
|
||||
send_read_pdu(Pid, {pdu, UnitId, ?READ_COILS, <<Addr:16, N:16>>}, N).
|
||||
|
||||
write_single_coil(Pid, Addr, Value)
|
||||
when is_integer(Addr), Addr >= 0,
|
||||
is_integer(Value), Value >= 0 ->
|
||||
Value1 = if Value =/= 0 -> 16#FF00; true -> 0 end,
|
||||
send_write_pdu(Pid, {pdu, ?WRITE_SINGLE_COIL,
|
||||
<<Addr:16, Value1:16>>},
|
||||
Addr, Value1).
|
||||
write_single_coil(Pid, UnitId, Addr, Value)
|
||||
when is_integer(UnitId),
|
||||
is_integer(Addr), Addr >= 0,
|
||||
is_integer(Value), Value >= 0 ->
|
||||
Value1 = if Value =/= 0 -> 16#FF00; true -> 0 end,
|
||||
send_write_pdu(Pid, {pdu, UnitId, ?WRITE_SINGLE_COIL,
|
||||
<<Addr:16, Value1:16>>},
|
||||
Addr, Value1).
|
||||
|
||||
write_multiple_coils(Pid, Addr, BitList)
|
||||
when is_integer(Addr), Addr >= 0 ->
|
||||
{N, Data, M} = bitlist_to_params(BitList),
|
||||
send_write_pdu(Pid, {pdu, ?WRITE_MULTIPLE_COILS,
|
||||
<<Addr:16, N:16, M, Data/binary>>},
|
||||
Addr, N).
|
||||
write_multiple_coils(Pid, UnitId, Addr, BitList)
|
||||
when is_integer(Addr), Addr >= 0 ->
|
||||
{N, Data, M} = bitlist_to_params(BitList),
|
||||
send_write_pdu(Pid, {pdu, UnitId, ?WRITE_MULTIPLE_COILS,
|
||||
<<Addr:16, N:16, M, Data/binary>>},
|
||||
Addr, N).
|
||||
|
||||
read_input_registers(Pid, Addr, N)
|
||||
when is_integer(Addr), Addr >= 0 ->
|
||||
send_read_reg_pdu(Pid, {pdu, ?READ_INPUT_REGISTERS, <<Addr:16, N:16>>}).
|
||||
read_input_registers(Pid, UnitId, Addr, N)
|
||||
when is_integer(UnitId), is_integer(Addr), Addr >= 0 ->
|
||||
send_read_reg_pdu(Pid, {pdu, UnitId, ?READ_INPUT_REGISTERS,
|
||||
<<Addr:16, N:16>>}).
|
||||
|
||||
read_holding_registers(Pid, Addr, N)
|
||||
when is_integer(Addr), Addr >= 0 ->
|
||||
send_read_reg_pdu(Pid, {pdu, ?READ_HOLDING_REGISTERS, <<Addr:16, N:16>>}).
|
||||
read_holding_registers(Pid, UnitId, Addr, N)
|
||||
when is_integer(UnitId), is_integer(Addr), Addr >= 0 ->
|
||||
send_read_reg_pdu(Pid, {pdu, UnitId, ?READ_HOLDING_REGISTERS,
|
||||
<<Addr:16, N:16>>}).
|
||||
|
||||
write_single_holding_register(Pid, Addr, Value)
|
||||
when is_integer(Addr), Addr >= 0 ->
|
||||
send_write_pdu(Pid, {pdu, ?WRITE_SINGLE_HOLDING_REGISTER,
|
||||
<<Addr:16, Value:16>>},
|
||||
Addr, Value).
|
||||
write_single_holding_register(Pid, UnitId, Addr, Value)
|
||||
when is_integer(UnitId), is_integer(Addr), Addr >= 0 ->
|
||||
send_write_pdu(Pid, {pdu, UnitId, ?WRITE_SINGLE_HOLDING_REGISTER,
|
||||
<<Addr:16, Value:16>>},
|
||||
Addr, Value).
|
||||
|
||||
write_multiple_holding_registers(Pid, Addr, Values)
|
||||
when is_integer(Addr), Addr >= 0 ->
|
||||
{N, Data, M} = valuelist_to_params(Values),
|
||||
send_write_pdu(Pid, {pdu, ?WRITE_MULTIPLE_HOLDING_REGISTERS,
|
||||
<<Addr:16,N:16,M,Data/binary>>},
|
||||
Addr, N).
|
||||
write_multiple_holding_registers(Pid, UnitId, Addr, Values)
|
||||
when is_integer(UnitId), is_integer(Addr), Addr >= 0 ->
|
||||
{N, Data, M} = valuelist_to_params(Values),
|
||||
send_write_pdu(Pid, {pdu, UnitId, ?WRITE_MULTIPLE_HOLDING_REGISTERS,
|
||||
<<Addr:16,N:16,M,Data/binary>>},
|
||||
Addr, N).
|
||||
|
||||
send_read_pdu(Pid, Msg, N) ->
|
||||
try gen_server:call(Pid, Msg) of
|
||||
{ok, <<Len,Bin:Len/binary>>} when N =< Len*8 ->
|
||||
{ok, bits_to_coils(N, Bin)};
|
||||
Error ->
|
||||
Error
|
||||
catch
|
||||
_Type:Reason ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
send_write_pdu(Pid, Msg, Address, Value) ->
|
||||
try gen_server:call(Pid, Msg) of
|
||||
{ok, <<Address:16, Value:16>>} ->
|
||||
ok;
|
||||
Error -> Error
|
||||
catch
|
||||
_Type:Reason ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
send_read_reg_pdu(Pid, Msg) ->
|
||||
try gen_server:call(Pid, Msg) of
|
||||
{ok, <<Len, RegData:Len/binary>>} ->
|
||||
{ok, [ Reg || <<Reg:16>> <= RegData ]};
|
||||
Error ->
|
||||
Error
|
||||
catch
|
||||
_Type:Reason ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
bitlist_to_params(BitList) ->
|
||||
N = length(BitList),
|
||||
Data = coils_to_bin(BitList),
|
||||
M = byte_size(Data),
|
||||
{N, Data, M}.
|
||||
|
||||
valuelist_to_params(Values) ->
|
||||
N = length(Values),
|
||||
Data = << <<V:16>> || V <- Values >>,
|
||||
M = byte_size(Data),
|
||||
{N, Data, M}.
|
||||
|
||||
|
||||
%% utils
|
||||
%% bits are stored lsb
|
||||
bits_to_coils(N, Bin) when is_integer(N), N >= 0,
|
||||
is_binary(Bin) ->
|
||||
Bits = lists:append([ lists:reverse([B||<<B:1>> <= <<Byte>> ]) ||
|
||||
<<Byte>> <= Bin]),
|
||||
{BitList,_} = lists:split(N, Bits),
|
||||
BitList.
|
||||
|
||||
coils_to_bin(Bits) ->
|
||||
coils_to_bin(Bits, <<>>).
|
||||
|
||||
coils_to_bin([B0,B1,B2,B3,B4,B5,B6,B7|Bits], Acc) ->
|
||||
coils_to_bin(Bits, <<Acc/binary, B7:1,B6:1,B5:1,B4:1,
|
||||
B3:1,B2:1,B1:1,B0:1>>);
|
||||
coils_to_bin([], Acc) ->
|
||||
Acc;
|
||||
coils_to_bin(Bits, Acc) ->
|
||||
M = length(Bits),
|
||||
[B0,B1,B2,B3,B4,B5,B6,B7] = Bits++lists:duplicate(8-M, 0),
|
||||
<<Acc/binary, B7:1,B6:1,B5:1,B4:1,B3:1,B2:1,B1:1,B0:1>>.
|
61
apps/dgiot_modbus/src/modbus/modbus_demo_callback.erl
Normal file
61
apps/dgiot_modbus/src/modbus/modbus_demo_callback.erl
Normal file
@ -0,0 +1,61 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(modbus_demo_callback).
|
||||
|
||||
-export([read_discrete_inputs/2,
|
||||
read_coils/2,
|
||||
write_single_coil/2,
|
||||
write_multiple_coils/2,
|
||||
read_input_registers/2,
|
||||
read_holding_registers/2,
|
||||
write_single_holding_register/2,
|
||||
write_multiple_holding_registers/2]).
|
||||
|
||||
read_discrete_inputs(Addr,N) ->
|
||||
io:format("demo_callback: read_discrete_inputs: addr=~w, n=~w\n", [Addr,N]),
|
||||
[ (rand:uniform(2)-1) || _ <- lists:seq(1, N)].
|
||||
|
||||
read_coils(Addr,N) ->
|
||||
io:format("demo_callback: read_coils: addr=~w, n=~w\n", [Addr,N]),
|
||||
[ (rand:uniform(2)-1) || _ <- lists:seq(1, N)].
|
||||
|
||||
write_single_coil(Addr, Value) ->
|
||||
io:format("demo_callback: write_single_coil: addr=~w, value=~w\n",
|
||||
[Addr,Value]),
|
||||
Value.
|
||||
|
||||
write_multiple_coils(Addr,BitList) ->
|
||||
io:format("demo_callback: write_multiple_coils: addr=~w, values=~w\n",
|
||||
[Addr,BitList]),
|
||||
length(BitList).
|
||||
|
||||
read_input_registers(Addr, N) ->
|
||||
io:format("demo_callback: read_input_registers: addr=~w, n=~w\n", [Addr,N]),
|
||||
[ (rand:uniform(16#10000)-1) || _ <- lists:seq(1, N)].
|
||||
|
||||
read_holding_registers(Addr, N) ->
|
||||
io:format("demo_callback: read_holding_registers: addr=~w, n=~w\n",
|
||||
[Addr,N]),
|
||||
[ (rand:uniform(16#10000)-1) || _ <- lists:seq(1, N)].
|
||||
|
||||
write_single_holding_register(Addr, Value) ->
|
||||
io:format("demo_callback: write_single_holding_register: addr=~w, value=~w\n", [Addr,Value]),
|
||||
Value.
|
||||
|
||||
write_multiple_holding_registers(Addr, Values) ->
|
||||
io:format("demo_callback: write_multiple_holding_registers: addr=~w, values=~w\n", [Addr,Values]),
|
||||
length(Values).
|
263
apps/dgiot_modbus/src/modbus/modbus_device.erl
Normal file
263
apps/dgiot_modbus/src/modbus/modbus_device.erl
Normal file
@ -0,0 +1,263 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(modbus_device).
|
||||
-behavior(gen_server).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]).
|
||||
|
||||
-include("dgiot_modbus.hrl").
|
||||
|
||||
|
||||
%%% %%% -------------------------------------------------------------------
|
||||
%% ModbusTCP gen_server API
|
||||
%%% %%% -------------------------------------------------------------------
|
||||
|
||||
init([Host, Port, DeviceAddr]) ->
|
||||
Retval = gen_tcp:connect(Host, Port, [binary, {active,false}, {packet, 0}]),
|
||||
|
||||
case Retval of
|
||||
{ok, Sock} ->
|
||||
State = #tcp_request{sock = Sock, address = DeviceAddr},
|
||||
{ok, State};
|
||||
{error,ErrorType} ->
|
||||
{stop, {error, ErrorType}}
|
||||
end.
|
||||
|
||||
handle_call({read_coils, Start, Offset, Opts}, _From, State) ->
|
||||
NewState = State#tcp_request{
|
||||
tid = State#tcp_request.tid +1,
|
||||
function = ?FC_READ_COILS,
|
||||
start = Start,
|
||||
data = Offset
|
||||
},
|
||||
|
||||
{ok, Data} = send_and_receive(NewState),
|
||||
FinalData = case output(Data, Opts, coils) of
|
||||
Result when length(Result) > Offset ->
|
||||
{ResultHead, _} = lists:split(Offset, Result),
|
||||
ResultHead;
|
||||
Result -> Result
|
||||
end,
|
||||
{reply, FinalData, NewState};
|
||||
|
||||
handle_call({read_inputs, Start, Offset, Opts}, _From, State) ->
|
||||
NewState = State#tcp_request{
|
||||
tid = State#tcp_request.tid +1,
|
||||
function = ?FC_READ_INPUTS,
|
||||
start = Start,
|
||||
data = Offset
|
||||
},
|
||||
|
||||
{ok, Data} = send_and_receive(NewState),
|
||||
FinalData = case output(Data, Opts, coils) of
|
||||
Result when length(Result) > Offset ->
|
||||
{ResultHead, _} = lists:split(Offset, Result),
|
||||
ResultHead;
|
||||
Result -> Result
|
||||
end,
|
||||
{reply, FinalData, NewState};
|
||||
|
||||
|
||||
handle_call({read_hregs, Start, Offset, Opts}, _From, State) ->
|
||||
NewState = State#tcp_request{
|
||||
tid = State#tcp_request.tid +1,
|
||||
function = ?FC_READ_HREGS,
|
||||
start = Start,
|
||||
data = Offset
|
||||
},
|
||||
|
||||
{ok, Data} = send_and_receive(NewState),
|
||||
FinalData = output(Data, Opts, int16),
|
||||
{reply, FinalData, NewState};
|
||||
|
||||
handle_call({read_iregs,Start, Offset, Opts}, _From, State) ->
|
||||
NewState = State#tcp_request{
|
||||
tid = State#tcp_request.tid +1,
|
||||
function = ?FC_READ_IREGS,
|
||||
start = Start,
|
||||
data = Offset
|
||||
},
|
||||
|
||||
{ok, Data} = send_and_receive(NewState),
|
||||
FinalData = output(Data, Opts, int16),
|
||||
{reply, FinalData, NewState};
|
||||
|
||||
handle_call({write_coil, Start, Data}, _From, State) ->
|
||||
<<NewData:16>> = case Data of
|
||||
0 -> <<16#0000:16>>;
|
||||
1 -> <<16#ff00:16>>
|
||||
end,
|
||||
|
||||
NewState = State#tcp_request{
|
||||
tid = State#tcp_request.tid +1,
|
||||
function = ?FC_WRITE_COIL,
|
||||
start = Start,
|
||||
data = NewData
|
||||
},
|
||||
|
||||
{ok, NewData} = send_and_receive(NewState),
|
||||
{reply, ok, NewState};
|
||||
|
||||
handle_call({write_coils, Start, Data}, _From, State) ->
|
||||
NewState = State#tcp_request{
|
||||
tid = State#tcp_request.tid +1,
|
||||
function = ?FC_WRITE_COILS,
|
||||
start = Start,
|
||||
data = Data
|
||||
},
|
||||
|
||||
Length = if
|
||||
is_list(Data) -> length(Data);
|
||||
is_binary(Data) -> bit_size(Data)
|
||||
end,
|
||||
{ok, Length} = send_and_receive(NewState),
|
||||
{reply, ok, NewState};
|
||||
|
||||
handle_call({write_hreg, Start, Data}, _From, State) ->
|
||||
NewState = State#tcp_request{
|
||||
tid = State#tcp_request.tid +1,
|
||||
function = ?FC_WRITE_HREG,
|
||||
start = Start,
|
||||
data = Data
|
||||
},
|
||||
|
||||
{ok, Data} = send_and_receive(NewState),
|
||||
{reply, ok, NewState};
|
||||
|
||||
handle_call({write_hregs, Start, Data}, _From, State) ->
|
||||
NewState = State#tcp_request{
|
||||
tid = State#tcp_request.tid +1,
|
||||
function = ?FC_WRITE_HREGS,
|
||||
start = Start,
|
||||
data = Data
|
||||
},
|
||||
|
||||
Length = length(Data),
|
||||
{ok, Length} = send_and_receive(NewState),
|
||||
{reply, ok, NewState}.
|
||||
|
||||
|
||||
handle_cast(stop, State) ->
|
||||
{stop, normal, State};
|
||||
|
||||
handle_cast(_From, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_, State) ->
|
||||
gen_tcp:close(State#tcp_request.sock),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
|
||||
%%% %%% -------------------------------------------------------------------
|
||||
%% Util
|
||||
%%% %%% -------------------------------------------------------------------
|
||||
|
||||
%% @doc Function to send the request and get the response.
|
||||
%% @end
|
||||
-spec send_and_receive(State::#tcp_request{}) -> {ok, binary()}.
|
||||
send_and_receive(State) ->
|
||||
Message = generate_request(State),
|
||||
ok = gen_tcp:send(State#tcp_request.sock, Message),
|
||||
{ok, _Data} = get_response(State).
|
||||
|
||||
%% @doc Function to generate the request message from State.
|
||||
%% @end
|
||||
-spec generate_request(State::#tcp_request{}) -> binary().
|
||||
generate_request(#tcp_request{tid = Tid, address = Address, function = ?FC_WRITE_COILS,
|
||||
start = Start, data = Data}) when is_list(Data) ->
|
||||
Length = length(Data),
|
||||
NewData = modbus_util:coils_to_binary(Data),
|
||||
ByteSize = byte_size(NewData),
|
||||
Message = <<Address:8, ?FC_WRITE_COILS:8, Start:16, Length:16, ByteSize:8, NewData/binary>>,
|
||||
|
||||
Size = byte_size(Message),
|
||||
<<Tid:16, 0:16, Size:16, Message/binary>>;
|
||||
|
||||
generate_request(#tcp_request{tid = Tid, address = Address, function = ?FC_WRITE_COILS,
|
||||
start = Start, data = Data}) when is_binary(Data) ->
|
||||
Length = bit_size(Data),
|
||||
ByteSize = byte_size(Data),
|
||||
Message = <<Address:8, ?FC_WRITE_COILS:8, Start:16, Length:16, ByteSize:8, Data/binary>>,
|
||||
|
||||
Size = byte_size(Message),
|
||||
<<Tid:16, 0:16, Size:16, Message/binary>>;
|
||||
|
||||
generate_request(#tcp_request{tid = Tid, address = Address, function = ?FC_WRITE_HREGS, start = Start, data = Data}) ->
|
||||
Length = length(Data),
|
||||
NewData = modbus_util:int16_to_binary(Data),
|
||||
ByteSize = byte_size(NewData),
|
||||
Message = <<Address:8, ?FC_WRITE_HREGS:8, Start:16, Length:16, ByteSize:8, NewData/binary>>,
|
||||
|
||||
Size = size(Message),
|
||||
<<Tid:16, 0:16, Size:16, Message/binary>>;
|
||||
|
||||
generate_request(#tcp_request{tid = Tid, address = Address, function = Code, start = Start, data = Data}) ->
|
||||
Message = <<Address:8, Code:8, Start:16, Data:16>>,
|
||||
Size = size(Message),
|
||||
<<Tid:16, 0:16, Size:16, Message/binary>>.
|
||||
|
||||
|
||||
%% @doc Function to validate the response header and get the data from the tcp socket.
|
||||
%% @end
|
||||
-spec get_response(State::#tcp_request{}) -> ok | {error, term()}.
|
||||
get_response(#tcp_request{sock = Socket, tid = Tid, address = Address, function = Code, start = Start}) ->
|
||||
BadCode = Code + 128,
|
||||
|
||||
case gen_tcp:recv(Socket, 0) of
|
||||
{ok, <<Tid:16, 0:16,_TcpSize:16, Address, BadCode, ErrorCode>>} ->
|
||||
case ErrorCode of
|
||||
1 -> {error, illegal_function};
|
||||
2 -> {error, illegal_data_address};
|
||||
3 -> {error, illegal_data_value};
|
||||
4 -> {error, slave_device_failure};
|
||||
5 -> {error, acknowledge};
|
||||
6 -> {error, slave_device_busy};
|
||||
7 -> {error, negative_ack};
|
||||
8 -> {error, memory_parity};
|
||||
10 -> {error, path_unavailable};
|
||||
11 -> {error, failed_to_response};
|
||||
_ -> {error, unknown_response_code}
|
||||
end;
|
||||
{ok, <<Tid:16, 0:16,_TcpSize:16, Address, Code, Start:16, Data:16>>} ->
|
||||
{ok, Data};
|
||||
{ok, <<Tid:16, 0:16,_TcpSize:16, Address, Code, Size, Data:Size/binary>>} ->
|
||||
{ok, Data};
|
||||
Junk -> io:format("Junk: ~w~n", [Junk]), {error,junk}
|
||||
end.
|
||||
|
||||
%% @doc Function convert data to the selected output.
|
||||
%% @end
|
||||
-spec output(Data::binary(), Opts::list(), Default::atom()) -> list().
|
||||
output(Data, Opts, Default) ->
|
||||
Output = proplists:get_value(output, Opts, Default),
|
||||
Signed = proplists:get_value(signed, Opts, false),
|
||||
case {Output, Signed} of
|
||||
{int16, false} -> modbus_util:binary_to_int16(Data);
|
||||
{int16, true} -> modbus_util:binary_to_int16s(Data);
|
||||
{int32, false} -> modbus_util:binary_to_int32(Data);
|
||||
{int32, true} -> modbus_util:binary_to_int32s(Data);
|
||||
{float32, _} -> modbus_util:binary_to_float32(Data);
|
||||
{coils, _} -> modbus_util:binary_to_coils(Data);
|
||||
{ascii, _} -> modbus_util:binary_to_ascii(Data);
|
||||
{binary, _} -> Data;
|
||||
_ -> Data
|
||||
end.
|
656
apps/dgiot_modbus/src/modbus/modbus_rtu.erl
Normal file
656
apps/dgiot_modbus/src/modbus/modbus_rtu.erl
Normal file
@ -0,0 +1,656 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(modbus_rtu).
|
||||
-author("jonhl").
|
||||
|
||||
-include("dgiot_modbus.hrl").
|
||||
-include_lib("dgiot/include/logger.hrl").
|
||||
|
||||
-export([
|
||||
init/1,
|
||||
login/1,
|
||||
parse_frame/3,
|
||||
to_frame/1,
|
||||
build_req_message/1]
|
||||
).
|
||||
|
||||
-export([modbus_encoder/4, modbus_decoder/4, is16/1]).
|
||||
|
||||
init(State) ->
|
||||
State#{<<"req">> => [], <<"ts">> => dgiot_datetime:now_ms(), <<"interval">> => 300}.
|
||||
|
||||
login(#{<<"devaddr">> := DTUAddr, <<"product">> := ProductId, <<"ip">> := Ip,
|
||||
<<"channelId">> := ChannelId} = State) ->
|
||||
Topic = <<ProductId/binary, "/", ChannelId/binary, "/", DTUAddr/binary>>,
|
||||
dgiot_mqtt:subscribe(Topic),
|
||||
dgiot_device:register(ProductId, DTUAddr, ChannelId, #{<<"ip">> => Ip}),
|
||||
{ok, State};
|
||||
|
||||
login(State) ->
|
||||
{ok, State}.
|
||||
|
||||
to_frame(#{
|
||||
<<"value">> := Data,
|
||||
<<"addr">> := SlaveId,
|
||||
<<"productid">> := ProductId,
|
||||
<<"di">> := Address
|
||||
}) ->
|
||||
encode_data(Data, Address, SlaveId, ProductId);
|
||||
|
||||
%%<<"cmd">> => Cmd,
|
||||
%%<<"gateway">> => DtuAddr,
|
||||
%%<<"addr">> => SlaveId,
|
||||
%%<<"di">> => Address
|
||||
to_frame(#{
|
||||
<<"value">> := Value,
|
||||
<<"gateway">> := DtuAddr,
|
||||
<<"addr">> := SlaveId,
|
||||
<<"di">> := Address
|
||||
}) ->
|
||||
case dgiot_device:lookup_hub(DtuAddr, SlaveId) of
|
||||
{error, not_find} -> [];
|
||||
[ProductId, _DevAddr] ->
|
||||
encode_data(Value, Address, SlaveId, ProductId)
|
||||
end.
|
||||
|
||||
encode_data(Data, Address, SlaveId, ProductId) ->
|
||||
lists:foldl(fun({_Cmd, Quality, OperateType}, Acc) ->
|
||||
FunCode =
|
||||
case OperateType of
|
||||
<<"readCoils">> -> ?FC_READ_COILS;
|
||||
<<"readInputs">> -> ?FC_READ_INPUTS;
|
||||
<<"readHregs">> -> ?FC_READ_HREGS;
|
||||
<<"readIregs">> -> ?FC_READ_IREGS;
|
||||
<<"writeCoil">> -> ?FC_WRITE_COIL;
|
||||
<<"writeHreg">> -> ?FC_WRITE_COILS; %%需要校验,写多个线圈是什么状态
|
||||
<<"writeCoils">> -> ?FC_WRITE_HREG;
|
||||
<<"writeHregs">> -> ?FC_WRITE_HREGS; %%需要校验,写多个保持寄存器是什么状态
|
||||
_ -> ?FC_READ_HREGS
|
||||
end,
|
||||
|
||||
<<H:8, L:8>> = dgiot_utils:hex_to_binary(is16(Address)),
|
||||
<<Sh:8, Sl:8>> = dgiot_utils:hex_to_binary(is16(SlaveId)),
|
||||
|
||||
RtuReq = #rtu_req{
|
||||
slaveId = Sh * 256 + Sl,
|
||||
funcode = dgiot_utils:to_int(FunCode),
|
||||
address = H * 256 + L,
|
||||
quality = dgiot_utils:to_int(Quality)
|
||||
},
|
||||
Acc ++ [build_req_message(RtuReq)]
|
||||
end, [], modbus_encoder(ProductId, SlaveId, Address, Data)).
|
||||
|
||||
is16(<<"0X", Data/binary>>) ->
|
||||
<<"00", Data/binary>>;
|
||||
|
||||
is16(Data) ->
|
||||
<<"00", Data/binary>>.
|
||||
|
||||
%rtu modbus
|
||||
parse_frame(<<>>, Acc, _State) -> {<<>>, Acc};
|
||||
|
||||
parse_frame(<<MbAddr:8, BadCode:8, ErrorCode:8, Crc:2/binary>> = Buff, Acc,
|
||||
#{<<"addr">> := DtuAddr} = State) ->
|
||||
CheckCrc = dgiot_utils:crc16(<<MbAddr:8, BadCode:8, ErrorCode:8>>),
|
||||
case CheckCrc =:= Crc of
|
||||
true ->
|
||||
Error = case ErrorCode of
|
||||
?ILLEGAL_FUNCTION -> {error, illegal_function};
|
||||
?ILLEGAL_DATA_ADDRESS -> {error, illegal_data_address};
|
||||
?ILLEGAL_DATA_VALUE -> {error, illegal_data_value};
|
||||
?SLAVE_DEVICE_FAILURE -> {error, slave_device_failure};
|
||||
?ACKNOWLEDGE -> {error, acknowledge};
|
||||
?SLAVE_DEVICE_BUSY -> {error, slave_device_busy};
|
||||
?NEGATIVE_ACKNOWLEDGE -> {error, negative_acknowledge};
|
||||
?MEMORY_PARITY_ERROR -> {error, memory_parity_error};
|
||||
?GATEWAY_PATH_UNAVAILABLE -> {error, gateway_path_unavailable};
|
||||
?GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND -> {error, gateway_target_device_failed_to_respond};
|
||||
_ -> {error, unknown_response_code}
|
||||
end,
|
||||
?LOG(info,"DtuAddr ~p Modbus ~p, BadCode ~p, Error ~p", [DtuAddr, MbAddr, BadCode, Error]),
|
||||
{<<>>, Acc};
|
||||
false ->
|
||||
parse_frame(Buff, Acc, State)
|
||||
end;
|
||||
|
||||
%% 传感器直接做为dtu物模型的一个指标
|
||||
parse_frame(<<SlaveId:8, _/binary>> = Buff, Acc, #{<<"dtuproduct">> := ProductId, <<"slaveId">> := SlaveId, <<"dtuaddr">> := DtuAddr, <<"address">> := Address} = State) ->
|
||||
case decode_data(Buff, ProductId, DtuAddr, Address, Acc) of
|
||||
{Rest1, Acc1} ->
|
||||
parse_frame(Rest1, Acc1, State);
|
||||
[Buff, Acc] ->
|
||||
[Buff, Acc]
|
||||
end;
|
||||
|
||||
%% 传感器独立建产品,做为子设备挂载到dtu上面
|
||||
parse_frame(<<SlaveId:8, _/binary>> = Buff, Acc, #{<<"dtuaddr">> := DtuAddr, <<"slaveId">> := SlaveId, <<"address">> := Address} = State) ->
|
||||
case dgiot_device:lookup_hub(DtuAddr, dgiot_utils:to_binary(SlaveId)) of
|
||||
{error, _} ->
|
||||
[<<>>, Acc];
|
||||
[ProductId, _DevAddr] ->
|
||||
case decode_data(Buff, ProductId, DtuAddr, Address, Acc) of
|
||||
{Rest1, Acc1} ->
|
||||
parse_frame(Rest1, Acc1, State);
|
||||
[Buff, Acc] ->
|
||||
{Buff, Acc}
|
||||
end
|
||||
end.
|
||||
|
||||
decode_data(Buff, ProductId, DtuAddr, Address, Acc) ->
|
||||
<<SlaveId:8, FunCode:8, ResponseData/binary>> = Buff,
|
||||
{SizeOfData, DataBytes} =
|
||||
case FunCode of
|
||||
?FC_READ_COILS ->
|
||||
<<Size:8, Data/binary>> = ResponseData,
|
||||
{Size, Data};
|
||||
?FC_READ_INPUTS ->
|
||||
<<Size:8, Data/binary>> = ResponseData,
|
||||
{Size, Data};
|
||||
?FC_READ_HREGS ->
|
||||
<<Size:8, Data/binary>> = ResponseData,
|
||||
{Size, Data};
|
||||
?FC_READ_IREGS ->
|
||||
<<Size:8, Data/binary>> = ResponseData,
|
||||
{Size, Data};
|
||||
?FC_WRITE_COIL -> {0, []};
|
||||
?FC_WRITE_HREG -> {0, []};
|
||||
?FC_WRITE_COILS -> {0, []};
|
||||
?FC_WRITE_HREGS -> {0, []};
|
||||
_ -> {0, []}
|
||||
end,
|
||||
case SizeOfData > 0 of
|
||||
true ->
|
||||
<<UserZone:SizeOfData/bytes, Crc:2/binary, Rest1/binary>> = DataBytes,
|
||||
CheckBuf = <<SlaveId:8, FunCode:8, SizeOfData:8, UserZone/binary>>,
|
||||
CheckCrc = dgiot_utils:crc16(CheckBuf),
|
||||
case CheckCrc =:= Crc of
|
||||
true ->
|
||||
Acc1 = Acc ++ modbus_decoder(ProductId, SlaveId, Address, UserZone),
|
||||
{Rest1, Acc1};
|
||||
false ->
|
||||
{Rest1, Acc}
|
||||
end;
|
||||
false ->
|
||||
case FunCode of
|
||||
?FC_WRITE_COIL ->
|
||||
get_write(ResponseData, SlaveId, FunCode, DtuAddr, ProductId, Address, Acc);
|
||||
?FC_WRITE_HREG ->
|
||||
get_write(ResponseData, SlaveId, FunCode, DtuAddr, ProductId, Address, Acc);
|
||||
?FC_WRITE_COILS ->
|
||||
[Buff, Acc];
|
||||
?FC_WRITE_HREGS ->
|
||||
[Buff, Acc];
|
||||
_ -> [Buff, Acc]
|
||||
end
|
||||
end.
|
||||
|
||||
get_write(ResponseData, SlaveId, FunCode, _DtuAddr, ProductId, Address, Acc) ->
|
||||
<<_Addr:2/binary, Rest1/binary>> = ResponseData,
|
||||
Size1 = byte_size(Rest1) - 2,
|
||||
<<UserZone:Size1/bytes, Crc:2/binary>> = Rest1,
|
||||
CheckBuf = <<SlaveId:8, FunCode:8, _Addr:2/binary, UserZone/binary>>,
|
||||
CheckCrc = dgiot_utils:crc16(CheckBuf),
|
||||
case CheckCrc =:= Crc of
|
||||
true ->
|
||||
Acc1 = Acc ++ modbus_decoder(ProductId, SlaveId, Address, UserZone),
|
||||
{<<>>, Acc1};
|
||||
false ->
|
||||
{<<>>, Acc}
|
||||
end.
|
||||
|
||||
build_req_message(Req) when is_record(Req, rtu_req) ->
|
||||
% validate
|
||||
if
|
||||
(Req#rtu_req.slaveId < 0) or (Req#rtu_req.slaveId > 247) ->
|
||||
throw({argumentError, Req#rtu_req.slaveId});
|
||||
true -> ok
|
||||
end,
|
||||
if
|
||||
(Req#rtu_req.funcode < 0) or (Req#rtu_req.funcode > 255) ->
|
||||
throw({argumentError, Req#rtu_req.funcode});
|
||||
true -> ok
|
||||
end,
|
||||
if
|
||||
(Req#rtu_req.address < 0) or (Req#rtu_req.address > 65535) ->
|
||||
throw({argumentError, Req#rtu_req.address});
|
||||
true -> ok
|
||||
end,
|
||||
if
|
||||
(Req#rtu_req.quality < 1) or (Req#rtu_req.quality > 2000) ->
|
||||
throw({argumentError, Req#rtu_req.quality});
|
||||
true -> ok
|
||||
end,
|
||||
Message =
|
||||
case Req#rtu_req.funcode of
|
||||
?FC_READ_COILS ->
|
||||
<<(Req#rtu_req.slaveId):8, (Req#rtu_req.funcode):8, (Req#rtu_req.address):16, (Req#rtu_req.quality):16>>;
|
||||
?FC_READ_INPUTS ->
|
||||
<<(Req#rtu_req.slaveId):8, (Req#rtu_req.funcode):8, (Req#rtu_req.address):16, (Req#rtu_req.quality):16>>;
|
||||
?FC_READ_HREGS ->
|
||||
<<(Req#rtu_req.slaveId):8, (Req#rtu_req.funcode):8, (Req#rtu_req.address):16, (Req#rtu_req.quality):16>>;
|
||||
?FC_READ_IREGS ->
|
||||
<<(Req#rtu_req.slaveId):8, (Req#rtu_req.funcode):8, (Req#rtu_req.address):16, (Req#rtu_req.quality):16>>;
|
||||
?FC_WRITE_COIL ->
|
||||
ValuesBin = case Req#rtu_req.quality of
|
||||
1 ->
|
||||
<<16#ff, 16#00>>;
|
||||
_ ->
|
||||
<<16#00, 16#00>>
|
||||
end,
|
||||
<<(Req#rtu_req.slaveId):8, (Req#rtu_req.funcode):8, (Req#rtu_req.address):16, ValuesBin/binary>>;
|
||||
?FC_WRITE_COILS ->
|
||||
Quantity = length(Req#rtu_req.quality),
|
||||
ValuesBin = list_bit_to_binary(Req#rtu_req.quality),
|
||||
ByteCount = length(binary_to_list(ValuesBin)),
|
||||
<<(Req#rtu_req.slaveId):8, (Req#rtu_req.funcode):8, (Req#rtu_req.address):16, Quantity:16, ByteCount:8, ValuesBin/binary>>;
|
||||
?FC_WRITE_HREG ->
|
||||
ValueBin = list_word16_to_binary([Req#rtu_req.quality]),
|
||||
<<(Req#rtu_req.slaveId):8, (Req#rtu_req.funcode):8, (Req#rtu_req.address):16, ValueBin/binary>>;
|
||||
?FC_WRITE_HREGS ->
|
||||
Quantity = length(Req#rtu_req.quality),
|
||||
ValuesBin = list_word16_to_binary(Req#rtu_req.quality),
|
||||
ByteCount = length(binary_to_list(ValuesBin)),
|
||||
<<(Req#rtu_req.slaveId):8, (Req#rtu_req.funcode):8, (Req#rtu_req.address):16, Quantity:16, ByteCount:8, ValuesBin/binary>>;
|
||||
_ ->
|
||||
erlang:error(function_not_implemented)
|
||||
end,
|
||||
Checksum = dgiot_utils:crc16(Message),
|
||||
<<Message/binary, Checksum/binary>>.
|
||||
|
||||
%%%% ----------------
|
||||
%%%% 解析数据值
|
||||
%%parseData(FunCode, DataByteSize, Data) ->
|
||||
%% case FunCode of
|
||||
%% T when T =:= 1; T =:= 2 ->
|
||||
%% % 离散量
|
||||
%% parseCoilData(Data, DataByteSize);
|
||||
%% T when T =:= 3; T =:= 4 ->
|
||||
%% % 线圈
|
||||
%% parseRegisterData(Data)
|
||||
%% end.
|
||||
|
||||
%% ----------------
|
||||
%% 解析离散量数据值, 两个字节(16bit)组成一个值
|
||||
%%parseRegisterData(Data) ->
|
||||
%% parseRegisterData_2(Data, []).
|
||||
|
||||
%%parseRegisterData_2([], Rdata) ->
|
||||
%% lists:reverse(Rdata);
|
||||
%%parseRegisterData_2([Hi, Lo | T], Rdata) ->
|
||||
%% <<D:16>> = <<Hi:8, Lo:8>>,
|
||||
%% parseRegisterData_2(T, [D | Rdata]).
|
||||
|
||||
%% ----------------
|
||||
%% 解析线圈数据值, 一个字节(8bit)拆为8个值
|
||||
%%parseCoilData(Data, DataByteSize) ->
|
||||
%% lists:sublist(parseCoilData_2(Data, []), DataByteSize * 8).
|
||||
|
||||
%%parseCoilData_2([], Rdata) ->
|
||||
%% lists:reverse(Rdata);
|
||||
%%parseCoilData_2([H | T], Rdata) ->
|
||||
%% <<D8:1, D7:1, D6:1, D5:1, D4:1, D3:1, D2:1, D1:1>> = <<H:8>>,
|
||||
%% parseCoilData_2(T, [D8, D7, D6, D5, D4, D3, D2, D1 | Rdata]).
|
||||
|
||||
|
||||
list_bit_to_binary(Values) when is_list(Values) ->
|
||||
L = length(Values),
|
||||
AlignedValues = case L rem 8 of
|
||||
0 ->
|
||||
Values;
|
||||
Remainder ->
|
||||
Values ++ [0 || _ <- lists:seq(1, 8 - Remainder)]
|
||||
end,
|
||||
list_to_binary(
|
||||
bit_as_bytes(AlignedValues)
|
||||
).
|
||||
|
||||
bit_as_bytes(L) when is_list(L) ->
|
||||
bit_as_bytes(L, []).
|
||||
|
||||
bit_as_bytes([], Res) ->
|
||||
lists:reverse(Res);
|
||||
bit_as_bytes([B0, B1, B2, B3, B4, B5, B6, B7 | Rest], Res) ->
|
||||
bit_as_bytes(Rest, [<<B7:1, B6:1, B5:1, B4:1, B3:1, B2:1, B1:1, B0:1>> | Res]).
|
||||
|
||||
list_word16_to_binary(Values) when is_list(Values) ->
|
||||
list_to_binary(
|
||||
lists:map(
|
||||
fun(X) ->
|
||||
RoundedValue = round(X),
|
||||
<<RoundedValue:16>>
|
||||
end,
|
||||
Values
|
||||
)
|
||||
).
|
||||
|
||||
modbus_decoder(ProductId, SlaveId, Address, Data) ->
|
||||
case dgiot_device:lookup_prod(ProductId) of
|
||||
{ok, #{<<"thing">> := #{<<"properties">> := Props}}} ->
|
||||
lists:foldl(fun(X, Acc) ->
|
||||
case X of
|
||||
#{<<"identifier">> := Identifier,
|
||||
<<"dataForm">> := #{
|
||||
<<"slaveid">> := OldSlaveid,
|
||||
<<"address">> := OldAddress,
|
||||
<<"protocol">> := <<"modbus">>
|
||||
}} ->
|
||||
<<H:8, L:8>> = dgiot_utils:hex_to_binary(modbus_rtu:is16(OldSlaveid)),
|
||||
<<Sh:8, Sl:8>> = dgiot_utils:hex_to_binary(modbus_rtu:is16(OldAddress)),
|
||||
NewSlaveid = H * 256 + L,
|
||||
NewAddress = Sh * 256 + Sl,
|
||||
case {SlaveId, Address} of
|
||||
{NewSlaveid, NewAddress} ->
|
||||
case format_value(Data, X) of
|
||||
{Value, _Rest} ->
|
||||
Acc#{Identifier => Value};
|
||||
_ -> Acc
|
||||
end;
|
||||
_ ->
|
||||
Acc
|
||||
end;
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end, #{}, Props);
|
||||
_ -> []
|
||||
end.
|
||||
|
||||
modbus_encoder(ProductId, SlaveId, Address, Value) ->
|
||||
BinSlaveId = dgiot_utils:to_binary(SlaveId),
|
||||
case dgiot_device:lookup_prod(ProductId) of
|
||||
{ok, #{<<"thing">> := #{<<"properties">> := Props}}} ->
|
||||
lists:foldl(fun(X, Acc) ->
|
||||
case X of
|
||||
#{<<"accessMode">> := <<"r">>, <<"dataForm">> := #{<<"address">> := Address, <<"protocol">> := <<"modbus">>,
|
||||
<<"data">> := Data, <<"slaveid">> := BinSlaveId, <<"operatetype">> := Operatetype}} ->
|
||||
Acc ++ [{<<"r">>, Data, Operatetype}];
|
||||
#{<<"accessMode">> := Cmd, <<"dataForm">> := #{<<"address">> := Address, <<"protocol">> := <<"modbus">>,
|
||||
<<"data">> := _Quantity, <<"slaveid">> := BinSlaveId, <<"operatetype">> := Operatetype}} ->
|
||||
Acc ++ [{Cmd, Value, Operatetype}];
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end, [], Props);
|
||||
Error ->
|
||||
?LOG(info,"~p", [Error]),
|
||||
[]
|
||||
end.
|
||||
|
||||
%% 1)大端模式:Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
|
||||
%% (其实大端模式才是我们直观上认为的模式,和字符串存储的模式差类似)
|
||||
%% 低地址 --------------------> 高地址
|
||||
%% 0x12 | 0x34 | 0x56 | 0x78
|
||||
%% 2)小端模式:Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
|
||||
%% 低地址 --------------------> 高地址
|
||||
%% 0x78 | 0x56 | 0x34 | 0x12
|
||||
|
||||
format_value(Buff, #{
|
||||
<<"accessMode">> := <<"rw">>,
|
||||
<<"dataForm">> := DataForm} = X) ->
|
||||
format_value(Buff, X#{<<"accessMode">> => <<"r">>,
|
||||
<<"dataForm">> => DataForm#{<<"data">> => byte_size(Buff)}
|
||||
});
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"bit">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(2, IntLen) * 8,
|
||||
<<Value:Size, Rest/binary>> = Buff,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"short16_AB">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(2, IntLen) * 8,
|
||||
<<Value:Size/signed-big-integer, Rest/binary>> = Buff,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"short16_BA">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(2, IntLen) * 8,
|
||||
<<Value:Size/signed-little-integer, Rest/binary>> = Buff,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"ushort16_AB">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(2, IntLen) * 8,
|
||||
<<Value:Size/unsigned-big-integer, Rest/binary>> = Buff,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"ushort16_BA">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(2, IntLen) * 8,
|
||||
<<Value:Size/unsigned-little-integer, Rest/binary>> = Buff,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"long32_ABCD">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(4, IntLen) * 8,
|
||||
<<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
<<Value:Size/signed-big-integer>> = <<H:2/binary, L:2/binary>>,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"long32_CDAB">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(4, IntLen) * 8,
|
||||
<<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
<<Value:Size/signed-little-integer>> = <<L:2/binary, H:2/binary>>,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"ulong32_ABCD">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(4, IntLen) * 8,
|
||||
<<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
<<Value:Size/unsigned-big-integer>> = <<H:2/binary, L:2/binary>>,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"ulong32_CDAB">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(4, IntLen) * 8,
|
||||
<<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
<<Value:Size/unsigned-little-integer>> = <<L:2/binary, H:2/binary>>,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"float32_ABCD">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(4, IntLen) * 8,
|
||||
<<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
<<Value:Size/unsigned-big-float>> = <<H:2/binary, L:2/binary>>,
|
||||
{Value, Rest};
|
||||
|
||||
format_value(Buff, #{<<"dataForm">> := #{
|
||||
<<"data">> := Len,
|
||||
<<"originaltype">> := <<"float32_CDAB">>
|
||||
}}) ->
|
||||
IntLen = dgiot_utils:to_int(Len),
|
||||
Size = max(4, IntLen) * 8,
|
||||
<<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
<<Value:Size/unsigned-little-float>> = <<L:2/binary, H:2/binary>>,
|
||||
{Value, Rest};
|
||||
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"big">>,
|
||||
%% <<"originaltype">> := <<"uint16">>
|
||||
%%}}) ->
|
||||
%% Size = max(2, Len) * 8,
|
||||
%% <<Value:Size/unsigned-big-integer, Rest/binary>> = Buff,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"little">>,
|
||||
%% <<"originaltype">> := <<"uint16">>}}) ->
|
||||
%% Size = max(2, Len) * 8,
|
||||
%% <<Value:Size/unsigned-little-integer, Rest/binary>> = Buff,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"big">>,
|
||||
%% <<"originaltype">> := <<"int16">>}}) ->
|
||||
%% Size = max(2, Len) * 8,
|
||||
%% <<Value:Size/signed-big-integer, Rest/binary>> = Buff,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"little">>,
|
||||
%% <<"originaltype">> := <<"int16">>}}) ->
|
||||
%% Size = max(2, Len) * 8,
|
||||
%% <<Value:Size/signed-little-integer, Rest/binary>> = Buff,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"big">>,
|
||||
%% <<"originaltype">> := <<"uint32">>}
|
||||
%%}) ->
|
||||
%% Size = max(4, Len) * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/unsigned-big-integer>> = <<H:2/binary, L:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"little">>,
|
||||
%% <<"originaltype">> := <<"uint32">>}
|
||||
%%}) ->
|
||||
%% Size = max(4, Len) * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/unsigned-big-integer>> = <<L:2/binary, H:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"big">>,
|
||||
%% <<"originaltype">> := <<"int32">>}
|
||||
%%}) ->
|
||||
%% Size = max(4, Len) * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/signed-big-integer>> = <<H:2/binary, L:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"little">>,
|
||||
%% <<"originaltype">> := <<"int32">>}}) ->
|
||||
%% Size = max(4, Len) * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/signed-big-integer>> = <<L:2/binary, H:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"big">>,
|
||||
%% <<"originaltype">> := <<"float">>}}) ->
|
||||
%% Size = max(4, Len) * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/unsigned-big-float>> = <<H:2/binary, L:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"little">>,
|
||||
%% <<"originaltype">> := <<"float">>}}) ->
|
||||
%% Size = max(4, Len) * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/unsigned-big-float>> = <<L:2/binary, H:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"big">>,
|
||||
%% <<"originaltype">> := <<"double">>}}) ->
|
||||
%% Size = max(4, Len) * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/unsigned-big-float>> = <<H:2/binary, L:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"little">>,
|
||||
%% <<"originaltype">> := <<"double">>}}) ->
|
||||
%% Size = max(4, Len) * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/unsigned-big-float>> = <<L:2/binary, H:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"big">>,
|
||||
%% <<"originaltype">> := <<"string">>}}) ->
|
||||
%% Size = Len * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/big-binary>> = <<H:2/binary, L:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"little">>,
|
||||
%% <<"originaltype">> := <<"string">>}}) ->
|
||||
%% Size = Len * 8,
|
||||
%% <<H:2/binary, L:2/binary, Rest/binary>> = Buff,
|
||||
%% <<Value:Size/big-binary>> = <<L:2/binary, H:2/binary>>,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%%% customized data(按大端顺序返回hex data)
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"big">>}}) ->
|
||||
%% <<Value:Len/big-binary, Rest/binary>> = Buff,
|
||||
%% {Value, Rest};
|
||||
%%
|
||||
%%format_value(Buff, #{<<"dataForm">> := #{
|
||||
%% <<"quantity">> := Len,
|
||||
%% <<"byteorder">> := <<"little">>}}) ->
|
||||
%% <<Value:Len/little-binary, Rest/binary>> = Buff,
|
||||
%% {Value, Rest};
|
||||
|
||||
%% @todo 其它类型处理
|
||||
format_value(_, #{<<"identifier">> := Field}) ->
|
||||
?LOG(info,"Field ~p", [Field]),
|
||||
throw({field_error, <<Field/binary, " is not validate">>}).
|
345
apps/dgiot_modbus/src/modbus/modbus_tcp_client.erl
Normal file
345
apps/dgiot_modbus/src/modbus/modbus_tcp_client.erl
Normal file
@ -0,0 +1,345 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(modbus_tcp_client).
|
||||
|
||||
%%-include("dgiot_modbus.hrl").
|
||||
%%
|
||||
%%-behaviour(gen_server).
|
||||
%%
|
||||
%%%% API
|
||||
%%-export([start/1]).
|
||||
%%-export([start_link/1]).
|
||||
%%-export([stop/1]).
|
||||
%%
|
||||
%%%% gen_server callbacks
|
||||
%%-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
%% terminate/2, code_change/3]).
|
||||
%%
|
||||
%%-define(DEFAULT_TCP_PORT, 502).
|
||||
%%-define(DEFAULT_TIMEOUT, 5000).
|
||||
%%-define(DEFAULT_RECONNECT_INTERVAL, 3000).
|
||||
%%
|
||||
%%-record(exo_tags,
|
||||
%%{
|
||||
%% data=tcp,
|
||||
%% closed=tcp_closed,
|
||||
%% error=tcp_error
|
||||
%%}).
|
||||
%%
|
||||
%%-record(state,
|
||||
%%{
|
||||
%% socket,
|
||||
%% is_active = false,
|
||||
%% options = [],
|
||||
%% tags = #exo_tags{},
|
||||
%% reconnect = true,
|
||||
%% reconnect_interval = ?DEFAULT_RECONNECT_INTERVAL,
|
||||
%% reconnect_timer,
|
||||
%% proto_id = 0,
|
||||
%% trans_id = 1,
|
||||
%% default_unit_id, %% Used when no unit id in request
|
||||
%% requests = [],
|
||||
%% buf = <<>>
|
||||
%%}).
|
||||
%%
|
||||
%%%%%===================================================================
|
||||
%%%%% API
|
||||
%%%%%===================================================================
|
||||
%%
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%%% @doc
|
||||
%%%% Starts the server
|
||||
%%%%
|
||||
%%%% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
|
||||
%%%% @end
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%
|
||||
%%start_link(Opts) -> do_start(Opts, true).
|
||||
%%start(Opts) -> do_start(Opts, false).
|
||||
%%
|
||||
%%do_start(Opts, Link) when is_list(Opts), is_boolean(Link) ->
|
||||
%% application:ensure_all_started(exo),
|
||||
%% case connect(Opts) of
|
||||
%% {ok,Socket} ->
|
||||
%% case gen_server:start_link(?MODULE, [Socket|Opts], []) of
|
||||
%% {ok, Pid} ->
|
||||
%% ok = exo_socket:controlling_process(Socket, Pid),
|
||||
%% activate(Pid),
|
||||
%% if Link -> ok;
|
||||
%% true -> unlink(Pid)
|
||||
%% end,
|
||||
%% {ok,Pid};
|
||||
%% Error ->
|
||||
%% Error
|
||||
%% end;
|
||||
%% Error ->
|
||||
%% Error
|
||||
%% end.
|
||||
%%
|
||||
%%stop(Pid) ->
|
||||
%% gen_server:call(Pid, stop).
|
||||
%%
|
||||
%%activate(Pid) ->
|
||||
%% gen_server:call(Pid, activate).
|
||||
%%
|
||||
%%%%%===================================================================
|
||||
%%%%% gen_server callbacks
|
||||
%%%%%===================================================================
|
||||
%%
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%%% @private
|
||||
%%%% @doc
|
||||
%%%% Initializes the server
|
||||
%%%%
|
||||
%%%% @spec init(Args) -> {ok, State} |
|
||||
%%%% {ok, State, Timeout} |
|
||||
%%%% ignore |
|
||||
%%%% {stop, Reason}
|
||||
%%%% @end
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%init([Socket | Opts]) ->
|
||||
%% IVal = proplists:get_value(reconnect_interval,Opts,
|
||||
%% ?DEFAULT_RECONNECT_INTERVAL),
|
||||
%% ?LOG(debug,"init options ~p", [Opts]),
|
||||
%% {ok, #state{ is_active = false,
|
||||
%% default_unit_id = proplists:get_value(unit_id, Opts, 255),
|
||||
%% reconnect = proplists:get_value(reconnect, Opts, true),
|
||||
%% reconnect_interval = IVal,
|
||||
%% socket=Socket,
|
||||
%% options = Opts,
|
||||
%% tags = exo_tags(Socket)
|
||||
%% }}.
|
||||
%%
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%%% @private
|
||||
%%%% @doc
|
||||
%%%% Handling call messages
|
||||
%%%%
|
||||
%%%% @spec handle_call(Request, From, State) ->
|
||||
%%%% {reply, Reply, State} |
|
||||
%%%% {reply, Reply, State, Timeout} |
|
||||
%%%% {noreply, State} |
|
||||
%%%% {noreply, State, Timeout} |
|
||||
%%%% {stop, Reason, Reply, State} |
|
||||
%%%% {stop, Reason, State}
|
||||
%%%% @end
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%handle_call({pdu,Func,Params}, From, State)
|
||||
%% when is_binary(Params), State#state.is_active ->
|
||||
%% UnitID = State#state.default_unit_id,
|
||||
%% NewState = handle_send_pdu(UnitID, Func, Params, From, State),
|
||||
%% {noreply, NewState};
|
||||
%%handle_call({pdu,UnitID,Func,Params}, From, State)
|
||||
%% when is_binary(Params), State#state.is_active ->
|
||||
%% NewState = handle_send_pdu(UnitID, Func, Params, From, State),
|
||||
%% {noreply, NewState};
|
||||
%%
|
||||
%%handle_call({pdu,_Func,Params}, _From, State)
|
||||
%% when is_binary(Params), not State#state.is_active ->
|
||||
%% {reply, {error,not_connected}, State};
|
||||
%%handle_call({pdu,_UnitId,_Func,Params}, _From, State)
|
||||
%% when is_binary(Params), not State#state.is_active ->
|
||||
%% {reply, {error,not_connected}, State};
|
||||
%%
|
||||
%%handle_call(activate, _From, State) ->
|
||||
%% exo_socket:setopts(State#state.socket, [{active, once}]),
|
||||
%% {reply, ok, State#state { is_active = true }};
|
||||
%%handle_call(stop, _From, State) ->
|
||||
%% {stop, normal, ok, State};
|
||||
%%handle_call(_Request, _From, State) ->
|
||||
%% {reply, {error,badarg}, State}.
|
||||
%%
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%%% @private
|
||||
%%%% @doc
|
||||
%%%% Handling cast messages
|
||||
%%%%
|
||||
%%%% @spec handle_cast(Msg, State) -> {noreply, State} |
|
||||
%%%% {noreply, State, Timeout} |
|
||||
%%%% {stop, Reason, State}
|
||||
%%%% @end
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%handle_cast(_Msg, State) ->
|
||||
%% {noreply, State}.
|
||||
%%
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%%% @private
|
||||
%%%% @doc
|
||||
%%%% Handling all non call/cast messages
|
||||
%%%%
|
||||
%%%% @spec handle_info(Info, State) -> {noreply, State} |
|
||||
%%%% {noreply, State, Timeout} |
|
||||
%%%% {stop, Reason, State}
|
||||
%%%% @end
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%handle_info({Tag,_Socket,Data}, State)
|
||||
%% when Tag =:= (State#state.tags)#exo_tags.data ->
|
||||
%% exo_socket:setopts(State#state.socket, [{active, once}]),
|
||||
%% Buf = <<(State#state.buf)/binary, Data/binary>>,
|
||||
%% ?LOG(debug,"got data ~p", [Buf]),
|
||||
%% case Buf of
|
||||
%% %% victron bug for error codes?
|
||||
%% <<TransID:16,_ProtoID:16,2:16,UnitID,1:1,Func:7,Params:1/binary,
|
||||
%% Buf1/binary>> ->
|
||||
%% State1 = handle_reply_pdu(TransID, UnitID, 16#80+Func,
|
||||
%% Params, State#state { buf = Buf1 }),
|
||||
%% {noreply, State1};
|
||||
%% <<TransID:16,_ProtoID:16,Length:16,Data1:Length/binary,Buf1/binary>> ->
|
||||
%% case Data1 of
|
||||
%% <<UnitID,Func,Params/binary>> ->
|
||||
%% State1 = handle_reply_pdu(TransID, UnitID, Func,
|
||||
%% Params, State#state { buf = Buf1 }),
|
||||
%% {noreply, State1};
|
||||
%% _ ->
|
||||
%% ?LOG(debug,"data too short ~p", [Buf]),
|
||||
%% {noreply, State#state { buf = Buf1 }}
|
||||
%% end;
|
||||
%% _ ->
|
||||
%% {noreply, State#state { buf = Buf }}
|
||||
%% end;
|
||||
%%handle_info({Tag,_Socket},State) when
|
||||
%% Tag =:= (State#state.tags)#exo_tags.closed ->
|
||||
%% if State#state.reconnect ->
|
||||
%% State1 = State#state { socket = undefined, is_active = false },
|
||||
%% {noreply, handle_reconnect({error,closed}, State1 )};
|
||||
%% true ->
|
||||
%% {stop, closed, State}
|
||||
%% end;
|
||||
%%handle_info({Tag,_Socket,Error},State)
|
||||
%% when Tag =:= (State#state.tags)#exo_tags.error ->
|
||||
%% if State#state.reconnect ->
|
||||
%% State1 = State#state { socket = undefined, is_active = false },
|
||||
%% {noreply, handle_reconnect({error,Error}, State1)};
|
||||
%% true ->
|
||||
%% {stop, error, State}
|
||||
%% end;
|
||||
%%handle_info({timeout,T,reconnect}, State)
|
||||
%% when T =:= State#state.reconnect_timer ->
|
||||
%% if State#state.socket =:= undefined ->
|
||||
%% case connect(State#state.options) of
|
||||
%% {ok,Socket} ->
|
||||
%% exo_socket:setopts(Socket, [{active, once}]),
|
||||
%% {noreply, State#state { socket=Socket,
|
||||
%% is_active = true}};
|
||||
%% Error ->
|
||||
%% ?LOG(debug,"unable to open socket ~p", [Error]),
|
||||
%% Timer = start_timer(State#state.reconnect_interval,
|
||||
%% reconnect),
|
||||
%% {noreply, State#state { reconnect_timer = Timer }}
|
||||
%% end;
|
||||
%% true ->
|
||||
%% ?LOG(warning,"reconnect timeout while socket open",[]),
|
||||
%% {noreply,State}
|
||||
%% end;
|
||||
%%handle_info(_Info, State) ->
|
||||
%% ?LOG(warning,"got info ~p", [_Info]),
|
||||
%% {noreply, State}.
|
||||
%%
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%%% @private
|
||||
%%%% @doc
|
||||
%%%% This function is called by a gen_server when it is about to
|
||||
%%%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%%%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%%%% with Reason. The return value is ignored.
|
||||
%%%%
|
||||
%%%% @spec terminate(Reason, State) -> void()
|
||||
%%%% @end
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%terminate(_Reason, _State) ->
|
||||
%% ok.
|
||||
%%
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%%% @private
|
||||
%%%% @doc
|
||||
%%%% Convert process state when code is changed
|
||||
%%%%
|
||||
%%%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
|
||||
%%%% @end
|
||||
%%%%--------------------------------------------------------------------
|
||||
%%code_change(_OldVsn, State, _Extra) ->
|
||||
%% {ok, State}.
|
||||
%%
|
||||
%%%%%===================================================================
|
||||
%%%%% Internal functions
|
||||
%%%%%===================================================================
|
||||
%%handle_send_pdu(UnitID, Func, Params, From, State) ->
|
||||
%% TransID = State#state.trans_id,
|
||||
%% ProtoID = State#state.proto_id,
|
||||
%% Length = byte_size(Params)+2, %% func,unitid,params
|
||||
%% Data = <<TransID:16,ProtoID:16,Length:16,UnitID,Func,Params/binary>>,
|
||||
%% ?LOG(debug,"send data ~p", [Data]),
|
||||
%% exo_socket:send(State#state.socket, Data),
|
||||
%% Req = {TransID, UnitID, Func, From },
|
||||
%% State#state { trans_id = (TransID+1) band 16#ffff,
|
||||
%% requests = [Req | State#state.requests]}.
|
||||
%%
|
||||
%%handle_reply_pdu(TransID, UnitID1, Func1, Pdu, State) ->
|
||||
%% case lists:keytake(TransID, 1, State#state.requests) of
|
||||
%% false ->
|
||||
%% ?LOG(warning,"transaction ~p not found", [TransID]),
|
||||
%% State;
|
||||
%% {value,{_,UnitID,Func,From},Reqs} when
|
||||
%% UnitID =:= UnitID1; UnitID =:= 255 ->
|
||||
%% ?LOG(debug,"unit_id=~p,func=~p, matched unit_id=~p,func=~p",
|
||||
%% [UnitID1,Func1,UnitID,Func]),
|
||||
%% case Pdu of
|
||||
%% <<ErrorCode>> when Func + 16#80 =:= Func1 ->
|
||||
%% gen_server:reply(From, {error, ErrorCode}),
|
||||
%% State#state { requests = Reqs };
|
||||
%% Data when Func =:= Func1 ->
|
||||
%% %% FIXME: some more cases here
|
||||
%% gen_server:reply(From, {ok,Data}),
|
||||
%% State#state { requests = Reqs };
|
||||
%% _ ->
|
||||
%% ?LOG(warning,"unmatched response pdu ~p", [Pdu]),
|
||||
%% gen_server:reply(From, {error, internal}),
|
||||
%% State#state { requests = Reqs }
|
||||
%% end;
|
||||
%% _ ->
|
||||
%% ?LOG(warning,"reply from other unit ~p", [Pdu]),
|
||||
%% State
|
||||
%% end.
|
||||
%%
|
||||
%%exo_tags(Socket) ->
|
||||
%% {Data,Closed,Error} = exo_socket:tags(Socket),
|
||||
%% #exo_tags { data = Data,
|
||||
%% closed = Closed,
|
||||
%% error = Error }.
|
||||
%%
|
||||
%%handle_reconnect(Error, State) ->
|
||||
%% lists:foreach(
|
||||
%% fun({_,_UnitID,_Func,From}) ->
|
||||
%% gen_server:reply(From, Error)
|
||||
%% end, State#state.requests),
|
||||
%% Timer = start_timer(State#state.reconnect_interval, reconnect),
|
||||
%% State#state { reconnect_timer = Timer, requests = [], buf = <<>> }.
|
||||
%%
|
||||
%%connect(Opts0) ->
|
||||
%% Opts = Opts0 ++ application:get_all_env(modbus),
|
||||
%% ?LOG(debug,"connect options ~p", [Opts]),
|
||||
%% Host = proplists:get_value(host, Opts, "localhost"),
|
||||
%% Port = proplists:get_value(port, Opts, ?DEFAULT_TCP_PORT),
|
||||
%% Timeout = proplists:get_value(timeout, Opts, ?DEFAULT_TIMEOUT),
|
||||
%% Protocol = proplists:get_value(protocol, Opts, [tcp]),
|
||||
%% SocketOptions = [{mode,binary},{active,false},{nodelay,true},{packet,0}],
|
||||
%% exo_socket:connect(Host, Port, Protocol, SocketOptions, Timeout).
|
||||
%%
|
||||
%%start_timer(undefined, _Tag) ->
|
||||
%% undefined;
|
||||
%%start_timer(Timeout, Tag) ->
|
||||
%% erlang:start_timer(Timeout, self(), Tag).
|
115
apps/dgiot_modbus/src/modbus/modbus_tcp_server.erl
Normal file
115
apps/dgiot_modbus/src/modbus/modbus_tcp_server.erl
Normal file
@ -0,0 +1,115 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(modbus_tcp_server).
|
||||
%%-author("johnliu").
|
||||
%%-include_lib("dgiot/include/dgiot_socket.hrl").
|
||||
%%-include("dgiot_meter.hrl").
|
||||
%%%% API
|
||||
%%-export([start/2]).
|
||||
%%
|
||||
%%%% TCP callback
|
||||
%%-export([init/1, handle_info/2, handle_cast/2, handle_call/3, terminate/2, code_change/3]).
|
||||
%%
|
||||
%%start(Port, State) ->
|
||||
%% dgiot_tcp_server:child_spec(?MODULE, dgiot_utils:to_int(Port), State).
|
||||
%%
|
||||
%%%% =======================
|
||||
%%%% tcp server start
|
||||
%%%% {ok, State} | {stop, Reason}
|
||||
%%init(TCPState) ->
|
||||
%% {ok, TCPState}.
|
||||
%%
|
||||
%%%%设备登录报文,登陆成功后,开始搜表
|
||||
%%handle_info({tcp, DtuAddr}, #tcp{socket = Socket, state = #state{id = ChannelId, dtuaddr = <<>>} = State} = TCPState) when byte_size(DtuAddr) == 15 ->
|
||||
%% ?LOG(info,"DevAddr ~p ChannelId ~p", [DtuAddr, ChannelId]),
|
||||
%% DTUIP = dgiot_utils:get_ip(Socket),
|
||||
%%%% dgiot_meter:create_dtu(DtuAddr, ChannelId, DTUIP),
|
||||
%%%% {Ref, Step} = dgiot_smartmeter:search_meter(tcp, undefined, TCPState, 1),
|
||||
%% {noreply, TCPState#tcp{buff = <<>>, state = State#state{dtuaddr = DtuAddr, ref = Ref, step = Step}}};
|
||||
%%
|
||||
%%%%设备登录异常报文丢弃
|
||||
%%handle_info({tcp, ErrorBuff}, #tcp{state = #state{dtuaddr = <<>>}} = TCPState) ->
|
||||
%% ?LOG(info,"ErrorBuff ~p ", [ErrorBuff]),
|
||||
%% {noreply, TCPState#tcp{buff = <<>>}};
|
||||
%%
|
||||
%%
|
||||
%%%%定时器触发搜表
|
||||
%%handle_info(search_meter, #tcp{state = #state{ref = Ref} = State} = TCPState) ->
|
||||
%% {NewRef, Step} = dgiot_smartmeter:search_meter(tcp, Ref, TCPState, 1),
|
||||
%% {noreply, TCPState#tcp{buff = <<>>, state = State#state{ref = NewRef, step = Step}}};
|
||||
%%
|
||||
%%%%ACK报文触发搜表
|
||||
%%handle_info({tcp, Buff}, #tcp{socket = Socket, state = #state{id = ChannelId, dtuaddr = DtuAddr, ref = Ref, step = search_meter} = State} = TCPState) ->
|
||||
%% ?LOG(info,"from_dev: search_meter Buff ~p", [dgiot_utils:binary_to_hex(Buff)]),
|
||||
%% lists:map(fun(X) ->
|
||||
%% case X of
|
||||
%% #{<<"addr">> := Addr} ->
|
||||
%% ?LOG(info,"from_dev: search_meter Addr ~p", [Addr]),
|
||||
%% DTUIP = dgiot_utils:get_ip(Socket),
|
||||
%% dgiot_meter:create_meter(dgiot_utils:binary_to_hex(Addr), ChannelId, DTUIP, DtuAddr);
|
||||
%% _ ->
|
||||
%% pass %%异常报文丢弃
|
||||
%% end
|
||||
%% end, dgiot_smartmeter:parse_frame(dlt645, Buff, [])),
|
||||
%% {NewRef, Step} = dgiot_smartmeter:search_meter(tcp, Ref, TCPState, 1),
|
||||
%% {noreply, TCPState#tcp{buff = <<>>, state = State#state{ref = NewRef, step = Step}}};
|
||||
%%
|
||||
%%%%接受抄表任务命令抄表
|
||||
%%handle_info({deliver, _Topic, Msg}, #tcp{state = #state{id = ChannelId, step = read_meter}} = TCPState) ->
|
||||
%% case binary:split(dgiot_mqtt:get_topic(Msg), <<$/>>, [global, trim]) of
|
||||
%% [<<"thing">>, _ProductId, _DevAddr] ->
|
||||
%% #{<<"thingdata">> := ThingData} = jsx:decode(dgiot_mqtt:get_payload(Msg), [{labels, binary}, return_maps]),
|
||||
%% Payload = dgiot_smartmeter:to_frame(ThingData),
|
||||
%% dgiot_bridge:send_log(ChannelId, "from_task: ~ts: ~ts ", [_Topic, unicode:characters_to_list(dgiot_mqtt:get_payload(Msg))]),
|
||||
%% ?LOG(info,"task->dev: Payload ~p", [dgiot_utils:binary_to_hex(Payload)]),
|
||||
%% dgiot_tcp_server:send(TCPState, Payload);
|
||||
%% _ ->
|
||||
%% pass
|
||||
%% end,
|
||||
%% {noreply, TCPState};
|
||||
%%
|
||||
%%%% 接收抄表任务的ACK报文
|
||||
%%handle_info({tcp, Buff}, #tcp{state = #state{id = ChannelId, step = read_meter}} = TCPState) ->
|
||||
%% case dgiot_smartmeter:parse_frame(dlt645, Buff, []) of
|
||||
%% [#{<<"addr">> := Addr, <<"value">> := Value} | _] ->
|
||||
%% case dgiot_data:get({meter, ChannelId}) of
|
||||
%% {ProductId, _ACL, _Properties} -> DevAddr = dgiot_utils:binary_to_hex(Addr),
|
||||
%% Topic = <<"thing/", ProductId/binary, "/", DevAddr/binary, "/post">>,
|
||||
%% dgiot_mqtt:publish(DevAddr, Topic, jsx:encode(Value));
|
||||
%% _ -> pass
|
||||
%% end;
|
||||
%% _ -> pass
|
||||
%% end,
|
||||
%% {noreply, TCPState#tcp{buff = <<>>}};
|
||||
%%
|
||||
%%%% 异常报文丢弃
|
||||
%%%% {stop, TCPState} | {stop, Reason} | {ok, TCPState} | ok | stop
|
||||
%%handle_info(_Info, TCPState) ->
|
||||
%% {noreply, TCPState}.
|
||||
%%
|
||||
%%handle_call(_Msg, _From, TCPState) ->
|
||||
%% {reply, ok, TCPState}.
|
||||
%%
|
||||
%%handle_cast(_Msg, TCPState) ->
|
||||
%% {noreply, TCPState}.
|
||||
%%
|
||||
%%terminate(_Reason, _TCPState) ->
|
||||
%% dgiot_metrics:dec(dgiot_meter, <<"dtu_login">>, 1),
|
||||
%% ok.
|
||||
%%
|
||||
%%code_change(_OldVsn, TCPState, _Extra) ->
|
||||
%% {ok, TCPState}.
|
199
apps/dgiot_modbus/src/modbus/modbus_tcp_server1.erl
Normal file
199
apps/dgiot_modbus/src/modbus/modbus_tcp_server1.erl
Normal file
@ -0,0 +1,199 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(modbus_tcp_server1).
|
||||
%%-include("dgiot_modbus.hrl").
|
||||
%%-behaviour(exo_socket_server).
|
||||
%%-export([init/2, data/3, close/2, error/3, control/4]).
|
||||
%%
|
||||
%%-export([start_link/1]).
|
||||
%%-export([stop/1]).
|
||||
%%
|
||||
%%-record(state,
|
||||
%%{
|
||||
%% proto_id = 0,
|
||||
%% unit_ids = [255],
|
||||
%% buf = <<>>,
|
||||
%% callback
|
||||
%%}).
|
||||
%%
|
||||
%%-define(DEFAULT_TCP_PORT, 502).
|
||||
%%
|
||||
%%start_link(Opts0) ->
|
||||
%% Opts = Opts0 ++ application:get_all_env(modbus),
|
||||
%% ?LOG(debug,"start options ~p", [Opts]),
|
||||
%% Port = proplists:get_value(port, Opts, ?DEFAULT_TCP_PORT),
|
||||
%% Callback = proplists:get_value(callback, Opts),
|
||||
%% UnitIDs = case proplists:get_value(unit_ids, Opts) of
|
||||
%% List when is_list(List) -> List;
|
||||
%% I when is_integer(I) -> [I];
|
||||
%% undefined ->
|
||||
%% %% old syntax
|
||||
%% case proplists:get_value(unit_id, Opts, 255) of
|
||||
%% List when is_list(List) -> List;
|
||||
%% I when is_integer(I) -> [I]
|
||||
%% end
|
||||
%% end,
|
||||
%% exo_app:start(),
|
||||
%% exo_socket_server:start_link(Port, [tcp],
|
||||
%% [{active,once},{mode,binary},
|
||||
%% {reuseaddr,true},{nodelay,true}],
|
||||
%% ?MODULE, [Callback,UnitIDs]).
|
||||
%%
|
||||
%%stop(Pid) when is_pid(Pid) ->
|
||||
%% exo_socket_server:stop(Pid).
|
||||
%%
|
||||
%%%% init(Socket::socket(), Args::[term()]
|
||||
%%%% -> {ok,state()} | {stop,reason(),state()}
|
||||
%%init(_Socket, [Callback,UnitIDs]) ->
|
||||
%% ?LOG(debug,"unit ids ~p", [UnitIDs]),
|
||||
%% {ok,
|
||||
%% #state{
|
||||
%% callback = Callback,
|
||||
%% unit_ids = UnitIDs
|
||||
%% }}.
|
||||
%%
|
||||
%%%% data(Socket::socket(), Data::io_list(), State::state())
|
||||
%%%% -> {ok,state()}|{close,state()}|{stop,reason(),state()}
|
||||
%%data(Socket, Data, State) ->
|
||||
%% Buf = <<(State#state.buf)/binary, Data/binary>>,
|
||||
%% case Buf of
|
||||
%% <<TransID:16,_ProtoID:16,Length:16,Data1:Length/binary, Buf1/binary>> ->
|
||||
%% case Data1 of
|
||||
%% <<UnitID, Func, Params/binary>> ->
|
||||
%% ?LOG(debug,"unit_id ~p received, check list ~p",
|
||||
%% [UnitID, State#state.unit_ids]),
|
||||
%% case lists:member(UnitID,State#state.unit_ids) of
|
||||
%% true ->
|
||||
%% try handle_pdu(Socket,UnitID,TransID,Func,Params,
|
||||
%% State#state { buf = Buf1 }) of
|
||||
%% State1 ->
|
||||
%% {ok, State1}
|
||||
%% catch
|
||||
%% error:_Reason ->
|
||||
%% send(Socket,TransID,State#state.proto_id,
|
||||
%% UnitID,
|
||||
%% 16#80 + (Func band 16#7f),
|
||||
%% <<?SLAVE_DEVICE_FAILURE>>),
|
||||
%% {ok, State}
|
||||
%% end;
|
||||
%% false ->
|
||||
%% ?LOG(warning,"pdu not for us", []),
|
||||
%% {ok, State#state { buf = Buf1 }}
|
||||
%% end;
|
||||
%% _ ->
|
||||
%% ?LOG(warning,"pdu too short", []),
|
||||
%% {ok, State#state { buf = Buf1 }}
|
||||
%% end;
|
||||
%% _ ->
|
||||
%% %% FIXME: throw if too big
|
||||
%% {ok, State#state { buf = Buf }}
|
||||
%% end.
|
||||
%%
|
||||
%%%% close(Socket::socket(), State::state())
|
||||
%%%% -> {ok,state()}
|
||||
%%close(_Socket, State) ->
|
||||
%% {ok, State}.
|
||||
%%
|
||||
%%%% error(Socket::socket(),Error::error(), State:state())
|
||||
%%%% -> {ok,state()} | {stop,reason(),state()}
|
||||
%%
|
||||
%%error(_Socket, Error,State) ->
|
||||
%% {stop, Error, State}.
|
||||
%%
|
||||
%%%% control(Socket::socket(), Request::term(),
|
||||
%%%% From::term(), State:state())
|
||||
%%%% -> {reply, Reply::term(),state() [,Timeout]} |
|
||||
%%%% {noreply,state() [,Timeout]} |
|
||||
%%%% {ignore,state()[,Timeout]} |
|
||||
%%%% {send, Bin::binary(),state()[,Timeout]} |
|
||||
%%%% {data, Data::term()[,Timeout]} |
|
||||
%%%% {stop,reason(), Reply::term(),state()]}
|
||||
%%
|
||||
%%control(_Socket, _Request, _From, State) ->
|
||||
%% {reply, {error, no_control}, State}.
|
||||
%%
|
||||
%%%%
|
||||
%%%% Handle modbus command
|
||||
%%%%
|
||||
%%handle_pdu(Socket, UnitID, TransID, ?READ_DISCRETE_INPUTS,
|
||||
%% <<Addr:16,N:16>>, State) ->
|
||||
%% Coils = apply(State#state.callback,read_discrete_inputs,[Addr,N]),
|
||||
%% Bin = modbus:coils_to_bin(Coils),
|
||||
%% Len = byte_size(Bin),
|
||||
%% send(Socket, TransID, State#state.proto_id, UnitID,
|
||||
%% ?READ_DISCRETE_INPUTS, <<Len, Bin/binary>>),
|
||||
%% State;
|
||||
%%handle_pdu(Socket, UnitID, TransID, ?READ_COILS,
|
||||
%% <<Addr:16,N:16>>, State) ->
|
||||
%% Coils = apply(State#state.callback,read_coils,[Addr,N]),
|
||||
%% Bin = modbus:coils_to_bin(Coils),
|
||||
%% Len = byte_size(Bin),
|
||||
%% send(Socket, TransID, State#state.proto_id, UnitID,
|
||||
%% ?READ_COILS, <<Len, Bin/binary>>),
|
||||
%% State;
|
||||
%%handle_pdu(Socket, UnitID, TransID, ?WRITE_SINGLE_COIL,
|
||||
%% <<Addr:16,Value:16>>, State) ->
|
||||
%% Value1 = apply(State#state.callback, write_single_coil, [Addr,Value]),
|
||||
%% send(Socket, TransID, State#state.proto_id, UnitID,
|
||||
%% ?WRITE_SINGLE_COIL, <<Addr:16, Value1:16>>),
|
||||
%% State;
|
||||
%%handle_pdu(Socket, UnitID, TransID, ?WRITE_MULTIPLE_COILS,
|
||||
%% <<Addr:16, N:16, _M, Data/binary>>, State) ->
|
||||
%% Coils = modbus:bits_to_coils(N, Data),
|
||||
%% N1 = apply(State#state.callback, write_multiple_coils, [Addr,Coils]),
|
||||
%% send(Socket, TransID, State#state.proto_id, UnitID,
|
||||
%% ?WRITE_MULTIPLE_COILS, <<Addr:16, N1:16>>),
|
||||
%% State;
|
||||
%%handle_pdu(Socket, UnitID, TransID, ?READ_INPUT_REGISTERS,
|
||||
%% <<Addr:16,N:16>>, State) ->
|
||||
%% Regs = apply(State#state.callback, read_input_registers, [Addr,N]),
|
||||
%% RegData = << <<Reg:16>> || Reg <- Regs >>,
|
||||
%% Len = byte_size(RegData),
|
||||
%% send(Socket, TransID, State#state.proto_id,UnitID,
|
||||
%% ?READ_INPUT_REGISTERS,<<Len, RegData/binary>>),
|
||||
%% State;
|
||||
%%handle_pdu(Socket, UnitID, TransID, ?READ_HOLDING_REGISTERS,
|
||||
%% <<Addr:16,N:16>>, State) ->
|
||||
%% Regs = apply(State#state.callback, read_holding_registers, [Addr,N]),
|
||||
%% RegData = << <<Reg:16>> || Reg <- Regs >>,
|
||||
%% Len = byte_size(RegData),
|
||||
%% send(Socket, TransID, State#state.proto_id,UnitID,
|
||||
%% ?READ_HOLDING_REGISTERS,<<Len, RegData/binary>>),
|
||||
%% State;
|
||||
%%handle_pdu(Socket, UnitID, TransID, ?WRITE_SINGLE_HOLDING_REGISTER,
|
||||
%% <<Addr:16,Value:16>>, State) ->
|
||||
%% Value1 = apply(State#state.callback, write_single_holding_register,
|
||||
%% [Addr,Value]),
|
||||
%% send(Socket, TransID, State#state.proto_id, UnitID,
|
||||
%% ?WRITE_SINGLE_HOLDING_REGISTER, <<Addr:16, Value1:16>>),
|
||||
%% State;
|
||||
%%handle_pdu(Socket, UnitID, TransID, ?WRITE_MULTIPLE_HOLDING_REGISTERS,
|
||||
%% <<Addr:16,_N:16,_M,Data/binary>>, State) ->
|
||||
%% Values = [ V || <<V:16>> <= Data ], %% check M? and check N!
|
||||
%% N1 = apply(State#state.callback, write_multiple_holding_registers,
|
||||
%% [Addr,Values]),
|
||||
%% send(Socket,TransID, State#state.proto_id,UnitID,
|
||||
%% ?WRITE_MULTIPLE_HOLDING_REGISTERS,<<Addr:16, N1:16>>),
|
||||
%% State;
|
||||
%%handle_pdu(Socket, UnitID, TransID, Func,<<_/binary>>, State) ->
|
||||
%% send(Socket,TransID,State#state.proto_id, UnitID,
|
||||
%% 16#80 + (Func band 16#7f), <<?ILLEGAL_FUNCTION>>).
|
||||
%%
|
||||
%%send(Socket,TransID,ProtoID,UnitID,Func,Bin) ->
|
||||
%% Length = byte_size(Bin) + 2,
|
||||
%% Data = <<TransID:16, ProtoID:16, Length:16, UnitID, Func, Bin/binary>>,
|
||||
%% exo_socket:send(Socket, Data).
|
89
apps/dgiot_modbus/src/modbus/modbus_util.erl
Normal file
89
apps/dgiot_modbus/src/modbus/modbus_util.erl
Normal file
@ -0,0 +1,89 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 DGIOT Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(modbus_util).
|
||||
-export([
|
||||
binary_to_coils/1,
|
||||
binary_to_int16/1,
|
||||
binary_to_int16s/1,
|
||||
binary_to_int32/1,
|
||||
binary_to_int32s/1,
|
||||
binary_to_float32/1,
|
||||
binary_to_ascii/1,
|
||||
coils_to_binary/1,
|
||||
int16_to_binary/1
|
||||
]).
|
||||
|
||||
%% @doc Function to convert bytes to coils.
|
||||
%% @end
|
||||
-spec binary_to_coils(Bin::binary()) -> [0|1].
|
||||
binary_to_coils(Bin) ->
|
||||
lists:append([ lists:reverse([ Y || <<Y:1>> <= <<X>>]) || <<X:8>> <= Bin]).
|
||||
|
||||
%% @doc Function to convert bytes to 16bits integer.
|
||||
%% @end
|
||||
-spec binary_to_int16(Bin::binary()) -> [integer()].
|
||||
binary_to_int16(Bin) ->
|
||||
[ X || <<X:16/integer>> <= Bin ].
|
||||
|
||||
%% @doc Function to convert bytes to 16bits signed integer.
|
||||
%% @end
|
||||
-spec binary_to_int16s(Bin::binary()) -> [integer()].
|
||||
binary_to_int16s(Bin) ->
|
||||
[ X || <<X:16/signed-integer>> <= Bin ].
|
||||
|
||||
%% @doc Function to convert bytes to 32bits integer.
|
||||
%% @end
|
||||
-spec binary_to_int32(Bin::binary()) -> [integer()].
|
||||
binary_to_int32(Bin) ->
|
||||
[ X || <<X:32/integer>> <= Bin ].
|
||||
|
||||
%% @doc Function to convert bytes to 32bits signed integer.
|
||||
%% @end
|
||||
-spec binary_to_int32s(Bin::binary()) -> [integer()].
|
||||
binary_to_int32s(Bin) ->
|
||||
[ X || <<X:32/signed-integer>> <= Bin ].
|
||||
|
||||
%% @doc Function to convert bytes to 32bits float number.
|
||||
%% @end
|
||||
-spec binary_to_float32(Bin::binary()) -> [float()].
|
||||
binary_to_float32(Bin) ->
|
||||
[ X || <<X:32/float>> <= Bin ].
|
||||
|
||||
%% @doc Function to convert bytes to ASCII.
|
||||
%% @end
|
||||
-spec binary_to_ascii(Bin::binary()) -> list().
|
||||
binary_to_ascii(Bin) ->
|
||||
erlang:binary_to_list(Bin).
|
||||
|
||||
%% @doc Function to convert a list of coils to binary.
|
||||
%% @end
|
||||
-spec coils_to_binary(Values::list()) -> binary().
|
||||
coils_to_binary(Values) ->
|
||||
coils_to_binary(Values, <<>>).
|
||||
|
||||
coils_to_binary([], Acc) ->
|
||||
Acc;
|
||||
coils_to_binary([B0, B1, B2, B3, B4, B5, B6, B7 | T], Acc) ->
|
||||
coils_to_binary(T, <<Acc/binary, B7:1, B6:1, B5:1, B4:1, B3:1, B2:1, B1:1, B0:1>>);
|
||||
coils_to_binary(Values, Acc) ->
|
||||
coils_to_binary(Values ++ [0], Acc).
|
||||
|
||||
%% @doc Function to convert a list of 16bits integer to binary.
|
||||
%% @end
|
||||
-spec int16_to_binary(Values::list()) -> binary().
|
||||
int16_to_binary(Values) ->
|
||||
<< <<X:16>> || X <- Values >>.
|
49
apps/dgiot_modbus_tcp/.gitignore
vendored
Normal file
49
apps/dgiot_modbus_tcp/.gitignore
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
.eunit
|
||||
test-data/
|
||||
deps
|
||||
!deps/.placeholder
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
!ebin/.placeholder
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
test/ebin/*.beam
|
||||
.exrc
|
||||
plugins/*/ebin
|
||||
*.swp
|
||||
*.so
|
||||
.erlang.mk/
|
||||
cover/
|
||||
eunit.coverdata
|
||||
test/ct.cover.spec
|
||||
ct.coverdata
|
||||
.idea/
|
||||
_build
|
||||
.rebar3
|
||||
rebar3.crashdump
|
||||
.DS_Store
|
||||
etc/gen.emqx.conf
|
||||
compile_commands.json
|
||||
cuttlefish
|
||||
xrefr
|
||||
*.coverdata
|
||||
etc/emqx.conf.rendered
|
||||
Mnesia.*/
|
||||
*.DS_Store
|
||||
_checkouts
|
||||
rebar.config.rendered
|
||||
/rebar3
|
||||
rebar.lock
|
||||
.stamp
|
||||
tmp/
|
||||
_packages
|
||||
elvis
|
||||
emqx_dialyzer_*_plt
|
||||
*/emqx_dashboard/priv/www
|
||||
dist.zip
|
||||
scripts/git-token
|
||||
etc/*.seg
|
||||
_upgrade_base/
|
21
apps/dgiot_modbus_tcp/LICENSE
Normal file
21
apps/dgiot_modbus_tcp/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 dgiot
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
28
apps/dgiot_modbus_tcp/Makefile
Normal file
28
apps/dgiot_modbus_tcp/Makefile
Normal file
@ -0,0 +1,28 @@
|
||||
PROJECT = dgiot_acrel
|
||||
PROJECT_DESCRIPTION = dgiot_acrel
|
||||
|
||||
CUR_BRANCH := $(shell git branch | grep -e "^*" | cut -d' ' -f 2)
|
||||
BRANCH := $(if $(filter $(CUR_BRANCH), master develop), $(CUR_BRANCH), develop)
|
||||
|
||||
BUILD_DEPS = emqx cuttlefish ekaf jsx brod supervisor3
|
||||
dep_emqx = git-emqx https://github.com/emqx/emqx $(BRANCH)
|
||||
dep_cuttlefish = git-emqx https://github.com/emqx/cuttlefish v2.2.1
|
||||
|
||||
ERLC_OPTS += +debug_info
|
||||
|
||||
ERLC_OPTS += +'{parse_transform, lager_transform}'
|
||||
|
||||
NO_AUTOPATCH = cuttlefish
|
||||
|
||||
COVER = true
|
||||
|
||||
ERLC_OPTS += +'{parse_transform, lager_transform}'
|
||||
|
||||
$(shell [ -f erlang.mk ] || curl -s -o erlang.mk https://raw.githubusercontent.com/emqx/erlmk/master/erlang.mk)
|
||||
|
||||
include erlang.mk
|
||||
|
||||
app:: rebar.config
|
||||
|
||||
app.config::
|
||||
./deps/cuttlefish/cuttlefish -l info -e etc/ -c etc/dgiot_acrel.conf -i priv/dgiot_acrel.schema -d data
|
2
apps/dgiot_modbus_tcp/README.md
Normal file
2
apps/dgiot_modbus_tcp/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# dgiot_modbus_tcp
|
||||
|
1
apps/dgiot_modbus_tcp/erlang.mk
vendored
Normal file
1
apps/dgiot_modbus_tcp/erlang.mk
vendored
Normal file
@ -0,0 +1 @@
|
||||
include ../../erlang.mk
|
0
apps/dgiot_modbus_tcp/etc/dgiot_modbus_tcp.conf
Normal file
0
apps/dgiot_modbus_tcp/etc/dgiot_modbus_tcp.conf
Normal file
48
apps/dgiot_modbus_tcp/include/dgiot_modbus_tcp.hrl
Normal file
48
apps/dgiot_modbus_tcp/include/dgiot_modbus_tcp.hrl
Normal file
@ -0,0 +1,48 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2016-2017 John liu <34489690@qq.com>.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @doc dgiot_modbus_tcp COMMAND.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Frame Size Limits
|
||||
%%
|
||||
%% To prevent malicious clients from exploiting memory allocation in a server,
|
||||
%% servers MAY place maximum limits on:
|
||||
%%
|
||||
%% the number of frame headers allowed in a single frame
|
||||
%% the maximum length of header lines
|
||||
%% the maximum size of a frame body
|
||||
%%
|
||||
%% If these limits are exceeded the server SHOULD send the client an ERROR frame
|
||||
%% and then close the connection.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(dgiot_modbus_tcp_DTU, dgiot_modbus_tcp_dtu_ets).
|
||||
-record(state, {
|
||||
id,
|
||||
devaddr = <<>>,
|
||||
heartcount = 0,
|
||||
regtype = <<>>,
|
||||
head = "xxxxxx0eee",
|
||||
len = 0,
|
||||
app = <<>>,
|
||||
product = <<>>,
|
||||
deviceId = <<>>,
|
||||
scale = 10,
|
||||
temperature = 0,
|
||||
env = <<>>
|
||||
}).
|
||||
|
11
apps/dgiot_modbus_tcp/priv/swagger/swagger_modbus_tcp.json
Normal file
11
apps/dgiot_modbus_tcp/priv/swagger/swagger_modbus_tcp.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"definitions": {},
|
||||
"paths": {
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"description": "dgiot_modbus_tcp",
|
||||
"name": "dgiot_modbus_tcp"
|
||||
}
|
||||
]
|
||||
}
|
3
apps/dgiot_modbus_tcp/rebar.config
Normal file
3
apps/dgiot_modbus_tcp/rebar.config
Normal file
@ -0,0 +1,3 @@
|
||||
{deps, [
|
||||
|
||||
]}.
|
7
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src
Normal file
7
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src
Normal file
@ -0,0 +1,7 @@
|
||||
{application,dgiot_modbus_tcp,
|
||||
[{description,"modbus_tcp"},
|
||||
{vsn,"1.0.0"},
|
||||
{modules,[]},
|
||||
{registered,[dgiot_modbus_tcp_sup]},
|
||||
{applications,[kernel,stdlib,dgiot]},
|
||||
{mod,{dgiot_modbus_tcp_app,[]}}]}.
|
25
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src.script
Normal file
25
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src.script
Normal file
@ -0,0 +1,25 @@
|
||||
%%-*- mode: erlang -*-
|
||||
%% .app.src.script
|
||||
|
||||
RemoveLeadingV =
|
||||
fun(Tag) ->
|
||||
case re:run(Tag, "v\[0-9\]+\.\[0-9\]+\.*") of
|
||||
nomatch ->
|
||||
Tag;
|
||||
{match, _} ->
|
||||
%% if it is a version number prefixed by 'v' then remove the 'v'
|
||||
"v" ++ Vsn = Tag,
|
||||
Vsn
|
||||
end
|
||||
end,
|
||||
|
||||
case os:getenv("EMQX_DEPS_DEFAULT_VSN") of
|
||||
false -> CONFIG; % env var not defined
|
||||
[] -> CONFIG; % env var set to empty string
|
||||
Tag ->
|
||||
[begin
|
||||
AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}),
|
||||
{application, App, AppConf0}
|
||||
end || Conf = {application, App, AppConf} <- CONFIG]
|
||||
end.
|
||||
|
192
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.erl
Normal file
192
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.erl
Normal file
@ -0,0 +1,192 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author stoneliu
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 20. 三月 2021 12:00
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(dgiot_modbus_tcp).
|
||||
-author("stoneliu").
|
||||
-include("dgiot_modbus_tcp.hrl").
|
||||
-include_lib("dgiot/include/dgiot_socket.hrl").
|
||||
-include_lib("dgiot/include/logger.hrl").
|
||||
|
||||
-define(MAX_BUFF_SIZE, 1024).
|
||||
|
||||
-export([
|
||||
get_deviceid/2,
|
||||
start/2
|
||||
]).
|
||||
|
||||
%% TCP callback
|
||||
-export([init/1, handle_info/2, handle_cast/2, handle_call/3, terminate/2, code_change/3]).
|
||||
|
||||
start(Port, State) ->
|
||||
dgiot_tcp_server:child_spec(?MODULE, dgiot_utils:to_int(Port), State).
|
||||
|
||||
%% =======================
|
||||
%% {ok, State} | {stop, Reason}
|
||||
%%init(TCPState) ->
|
||||
%% erlang:send_after(5 * 1000, self(), login),.
|
||||
%% {ok, TCPState}.
|
||||
|
||||
init(#tcp{state = #state{id = ChannelId}} = TCPState) ->
|
||||
?LOG(info,"ChannelId ~p", [ChannelId]),
|
||||
case dgiot_bridge:get_products(ChannelId) of
|
||||
{ok, _TYPE, _ProductIds} ->
|
||||
{ok, TCPState};
|
||||
{error, not_find} ->
|
||||
{stop, not_find_channel}
|
||||
end.
|
||||
|
||||
%% 9C A5 25 CD 00 DB
|
||||
%% 11 04 02 06 92 FA FE
|
||||
handle_info({tcp, Buff}, #tcp{socket = Socket, state = #state{id = ChannelId, devaddr = <<>>, head = Head, len = Len, product = ProductId} = State} = TCPState) ->
|
||||
dgiot_bridge:send_log(ChannelId, "DTU revice from ~p", [dgiot_utils:binary_to_hex(Buff)]),
|
||||
DTUIP = dgiot_utils:get_ip(Socket),
|
||||
DtuAddr = dgiot_utils:binary_to_hex(Buff),
|
||||
List = dgiot_utils:to_list(DtuAddr),
|
||||
List1 = dgiot_utils:to_list(Buff),
|
||||
#{<<"objectId">> := DeviceId} =
|
||||
dgiot_parse:get_objectid(<<"Device">>, #{<<"product">> => ProductId, <<"devaddr">> => DtuAddr}),
|
||||
case re:run(DtuAddr, Head, [{capture, first, list}]) of
|
||||
{match, [Head]} when length(List) == Len ->
|
||||
{DevId, Devaddr} =
|
||||
case create_device(DeviceId, ProductId, DtuAddr, DTUIP) of
|
||||
{<<>>, <<>>} ->
|
||||
{<<>>, <<>>};
|
||||
{DevId1, Devaddr1} ->
|
||||
{DevId1, Devaddr1}
|
||||
end,
|
||||
{noreply, TCPState#tcp{buff = <<>>, state = State#state{devaddr = Devaddr, deviceId = DevId}}};
|
||||
_Error ->
|
||||
case re:run(Buff, Head, [{capture, first, list}]) of
|
||||
{match, [Head]} when length(List1) == Len ->
|
||||
create_device(DeviceId, ProductId, Buff, DTUIP),
|
||||
{noreply, TCPState#tcp{buff = <<>>, state = State#state{devaddr = Buff}}};
|
||||
Error1 ->
|
||||
?LOG(info,"Error1 ~p Buff ~p ", [Error1, dgiot_utils:to_list(Buff)]),
|
||||
{noreply, TCPState#tcp{buff = <<>>}}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_info({tcp, Buff}, #tcp{state = #state{id = ChannelId, devaddr = DtuAddr, env = #{product := ProductId, pn := Pn, di := Di}, product = DtuProductId} = State} = TCPState) ->
|
||||
dgiot_bridge:send_log(ChannelId, "revice from ~p", [dgiot_utils:binary_to_hex(Buff)]),
|
||||
<<H:8, L:8>> = dgiot_utils:hex_to_binary(modbus_rtu:is16(Di)),
|
||||
<<Sh:8, Sl:8>> = dgiot_utils:hex_to_binary(modbus_rtu:is16(Pn)),
|
||||
case modbus_rtu:parse_frame(Buff, [], #{
|
||||
<<"dtuproduct">> => ProductId,
|
||||
<<"channel">> => ChannelId,
|
||||
<<"dtuaddr">> => DtuAddr,
|
||||
<<"slaveId">> => Sh * 256 + Sl,
|
||||
<<"address">> => H * 256 + L}) of
|
||||
{_, Things} ->
|
||||
NewTopic = <<"thing/", DtuProductId/binary, "/", DtuAddr/binary, "/post">>,
|
||||
dgiot_bridge:send_log(ChannelId, "end to_task: ~p: ~p ~n", [NewTopic, jsx:encode(Things)]),
|
||||
dgiot_mqtt:publish(DtuAddr, NewTopic, jsx:encode(Things));
|
||||
Other ->
|
||||
?LOG(info,"Other ~p", [Other]),
|
||||
pass
|
||||
end,
|
||||
{noreply, TCPState#tcp{buff = <<>>, state = State#state{env = <<>>}}};
|
||||
|
||||
handle_info({deliver, _Topic, Msg}, #tcp{state = #state{id = ChannelId} = State} = TCPState) ->
|
||||
case binary:split(dgiot_mqtt:get_topic(Msg), <<$/>>, [global, trim]) of
|
||||
[<<"thing">>, _ProductId, _DevAddr] ->
|
||||
[#{<<"thingdata">> := ThingData} | _] = jsx:decode(dgiot_mqtt:get_payload(Msg), [{labels, binary}, return_maps]),
|
||||
case ThingData of
|
||||
#{<<"command">> := <<"r">>,
|
||||
<<"data">> := Value,
|
||||
<<"di">> := Di,
|
||||
<<"pn">> := SlaveId,
|
||||
<<"product">> := ProductId,
|
||||
<<"protocol">> := <<"modbus">>
|
||||
} ->
|
||||
Datas = modbus_rtu:to_frame(#{
|
||||
<<"addr">> => SlaveId,
|
||||
<<"value">> => Value,
|
||||
<<"productid">> => ProductId,
|
||||
<<"di">> => Di}),
|
||||
lists:map(fun(X) ->
|
||||
dgiot_bridge:send_log(ChannelId, "to_device: ~p ", [dgiot_utils:binary_to_hex(X)]),
|
||||
dgiot_tcp_server:send(TCPState, X)
|
||||
end, Datas),
|
||||
{noreply, TCPState#tcp{state = State#state{env = #{product => ProductId, pn => SlaveId, di => Di}}}};
|
||||
_ ->
|
||||
{noreply, TCPState}
|
||||
end;
|
||||
_Other ->
|
||||
{noreply, TCPState}
|
||||
end;
|
||||
|
||||
%% {stop, TCPState} | {stop, Reason} | {ok, TCPState} | ok | stop
|
||||
handle_info(_Info, TCPState) ->
|
||||
?LOG(info,"TCPState ~p", [TCPState]),
|
||||
{noreply, TCPState}.
|
||||
|
||||
handle_call(_Msg, _From, TCPState) ->
|
||||
{reply, ok, TCPState}.
|
||||
|
||||
handle_cast(_Msg, TCPState) ->
|
||||
{noreply, TCPState}.
|
||||
|
||||
terminate(_Reason, _TCPState) ->
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, TCPState, _Extra) ->
|
||||
{ok, TCPState}.
|
||||
|
||||
get_deviceid(ProdcutId, DevAddr) ->
|
||||
#{<<"objectId">> := DeviceId} =
|
||||
dgiot_parse:get_objectid(<<"Device">>, #{<<"product">> => ProdcutId, <<"devaddr">> => DevAddr}),
|
||||
DeviceId.
|
||||
|
||||
create_device(DeviceId, ProductId, DTUMAC, DTUIP) ->
|
||||
case dgiot_parse:get_object(<<"Product">>, ProductId) of
|
||||
{ok, #{<<"ACL">> := Acl, <<"devType">> := DevType}} ->
|
||||
case dgiot_parse:get_object(<<"Device">>, DeviceId) of
|
||||
{ok, #{<<"results">> := [#{<<"devaddr">> := _GWAddr} | _] = _Result}} ->
|
||||
dgiot_parse:update_object(<<"Device">>, DeviceId, #{<<"ip">> => DTUIP, <<"status">> => <<"ONLINE">>}),
|
||||
dgiot_task:save_pnque(ProductId, DTUMAC, ProductId, DTUMAC),
|
||||
create_instruct(Acl, ProductId, DeviceId),
|
||||
{DeviceId, DTUMAC};
|
||||
_ ->
|
||||
dgiot_device:create_device(#{
|
||||
<<"devaddr">> => DTUMAC,
|
||||
<<"name">> => <<"USRDTU", DTUMAC/binary>>,
|
||||
<<"ip">> => DTUIP,
|
||||
<<"isEnable">> => true,
|
||||
<<"product">> => ProductId,
|
||||
<<"ACL">> => Acl,
|
||||
<<"status">> => <<"ONLINE">>,
|
||||
<<"location">> => #{<<"__type">> => <<"GeoPoint">>, <<"longitude">> => 120.161324, <<"latitude">> => 30.262441},
|
||||
<<"brand">> => <<"USRDTU">>,
|
||||
<<"devModel">> => DevType
|
||||
}),
|
||||
dgiot_task:save_pnque(ProductId, DTUMAC, ProductId, DTUMAC),
|
||||
create_instruct(Acl, ProductId, DeviceId),
|
||||
{DeviceId, DTUMAC}
|
||||
end;
|
||||
Error2 ->
|
||||
?LOG(info,"Error2 ~p ", [Error2]),
|
||||
{<<>>, <<>>}
|
||||
end.
|
||||
|
||||
create_instruct(ACL, DtuProductId, DtuDevId) ->
|
||||
case dgiot_device:lookup_prod(DtuProductId) of
|
||||
{ok, #{<<"thing">> := #{<<"properties">> := Properties}}} ->
|
||||
lists:map(fun(Y) ->
|
||||
case Y of
|
||||
#{<<"dataForm">> := #{<<"slaveid">> := 256}} -> %%不做指令
|
||||
pass;
|
||||
#{<<"dataForm">> := #{<<"slaveid">> := SlaveId}} ->
|
||||
Pn = dgiot_utils:to_binary(SlaveId),
|
||||
%% ?LOG(info,"DtuProductId ~p DtuDevId ~p Pn ~p ACL ~p", [DtuProductId, DtuDevId, Pn, ACL]),
|
||||
%% ?LOG(info,"Y ~p", [Y]),
|
||||
dgiot_instruct:create(DtuProductId, DtuDevId, Pn, ACL, <<"all">>, #{<<"properties">> => [Y]});
|
||||
_ -> pass
|
||||
end
|
||||
end, Properties);
|
||||
_ -> pass
|
||||
end.
|
34
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_app.erl
Normal file
34
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_app.erl
Normal file
@ -0,0 +1,34 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2016-2017 John liu <34489690@qq.com>.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @doc dgiot_modbus_tcp Application
|
||||
-module(dgiot_modbus_tcp_app).
|
||||
-behaviour(application).
|
||||
-emqx_plugin(?MODULE).
|
||||
|
||||
%% Application callbacks
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Application callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
dgiot_modbus_tcp_sup:start_link().
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
170
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_channel.erl
Normal file
170
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_channel.erl
Normal file
@ -0,0 +1,170 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @copyright (C) 2019, <COMPANY>
|
||||
%%% @doc
|
||||
%%% 前置机客户端
|
||||
%%% @end
|
||||
%%% Created : 20. 三月 2021 12:00
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(dgiot_modbus_tcp_channel).
|
||||
-behavior(dgiot_channelx).
|
||||
-author("johnliu").
|
||||
-include("dgiot_modbus_tcp.hrl").
|
||||
-define(TYPE, <<"MODBUS_TCP">>).
|
||||
%% API
|
||||
-export([start/2]).
|
||||
|
||||
%% Channel callback
|
||||
-export([init/3, handle_init/1, handle_event/3, handle_message/2, stop/3]).
|
||||
|
||||
%% 注册通道类型
|
||||
-channel(?TYPE).
|
||||
-channel_type(#{
|
||||
type => 1,
|
||||
title => #{
|
||||
zh => <<"MODBUS_TCP通道"/utf8>>
|
||||
},
|
||||
description => #{
|
||||
zh => <<"MODBUS_TCP通道"/utf8>>
|
||||
}
|
||||
}).
|
||||
%% 注册通道参数
|
||||
-params(#{
|
||||
<<"port">> => #{
|
||||
order => 1,
|
||||
type => integer,
|
||||
required => true,
|
||||
default => 20110,
|
||||
title => #{
|
||||
zh => <<"端口"/utf8>>
|
||||
},
|
||||
description => #{
|
||||
zh => <<"侦听端口"/utf8>>
|
||||
}
|
||||
},
|
||||
<<"regtype">> => #{
|
||||
order => 2,
|
||||
type => string,
|
||||
required => true,
|
||||
default => <<"上传Mac"/utf8>>,
|
||||
title => #{
|
||||
zh => <<"注册类型"/utf8>>
|
||||
},
|
||||
description => #{
|
||||
zh => <<"上传Mac"/utf8>>
|
||||
}
|
||||
},
|
||||
<<"regular">> => #{
|
||||
order => 3,
|
||||
type => string,
|
||||
required => true,
|
||||
default => <<"9C-A5-25-**-**-**">>,
|
||||
title => #{
|
||||
zh => <<"登录报文帧头"/utf8>>
|
||||
},
|
||||
description => #{
|
||||
zh => <<"填写正则表达式匹配login"/utf8>>
|
||||
}
|
||||
},
|
||||
<<"DTUTYPE">> => #{
|
||||
order => 4,
|
||||
type => string,
|
||||
required => true,
|
||||
default => <<"usr">>,
|
||||
title => #{
|
||||
zh => <<"控制器厂商"/utf8>>
|
||||
},
|
||||
description => #{
|
||||
zh => <<"控制器厂商"/utf8>>
|
||||
}
|
||||
},
|
||||
<<"heartbeat">> => #{
|
||||
order => 5,
|
||||
type => integer,
|
||||
required => true,
|
||||
default => 10,
|
||||
title => #{
|
||||
zh => <<"心跳周期"/utf8>>
|
||||
},
|
||||
description => #{
|
||||
zh => <<"心跳周期"/utf8>>
|
||||
}
|
||||
},
|
||||
<<"ico">> => #{
|
||||
order => 102,
|
||||
type => string,
|
||||
required => false,
|
||||
default => <<"http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/shuwa_tech/zh/product/dgiot/channel/MODBUS_TCP%E9%80%9A%E9%81%93.jpg">>,
|
||||
title => #{
|
||||
en => <<"channel ICO">>,
|
||||
zh => <<"通道ICO"/utf8>>
|
||||
},
|
||||
description => #{
|
||||
en => <<"channel ICO">>,
|
||||
zh => <<"通道ICO"/utf8>>
|
||||
}
|
||||
}
|
||||
}).
|
||||
|
||||
start(ChannelId, ChannelArgs) ->
|
||||
dgiot_channelx:add(?TYPE, ChannelId, ?MODULE, ChannelArgs).
|
||||
|
||||
%% 通道初始化
|
||||
init(?TYPE, ChannelId, #{
|
||||
<<"port">> := Port,
|
||||
<<"heartbeat">> := Heartbeat,
|
||||
<<"regtype">> := Type,
|
||||
<<"regular">> := Regular,
|
||||
<<"product">> := Products
|
||||
} = _Args) ->
|
||||
[{ProdcutId, App} | _] = get_app(Products),
|
||||
{Header, Len} = get_header(Regular),
|
||||
State = #state{
|
||||
id = ChannelId,
|
||||
regtype = Type,
|
||||
head = Header,
|
||||
len = Len,
|
||||
app = App,
|
||||
product = ProdcutId
|
||||
},
|
||||
|
||||
dgiot_data:insert({ChannelId, heartbeat}, {Heartbeat, Port}),
|
||||
{ok, State, dgiot_modbus_tcp:start(Port, State)};
|
||||
|
||||
init(?TYPE, _ChannelId, _Args) ->
|
||||
{ok, #{}, #{}}.
|
||||
|
||||
handle_init(State) ->
|
||||
{ok, State}.
|
||||
|
||||
%% 通道消息处理,注意:进程池调用
|
||||
handle_event(_EventId, _Event, State) ->
|
||||
{ok, State}.
|
||||
|
||||
handle_message(_Message, State) ->
|
||||
{ok, State}.
|
||||
|
||||
stop(_ChannelType, _ChannelId, _State) ->
|
||||
ok.
|
||||
|
||||
get_app(Products) ->
|
||||
lists:map(fun({ProdcutId, #{<<"ACL">> := Acl}}) ->
|
||||
Predicate = fun(E) ->
|
||||
case E of
|
||||
<<"role:", _/binary>> -> true;
|
||||
_ -> false
|
||||
end
|
||||
end,
|
||||
[<<"role:", App/binary>> | _] = lists:filter(Predicate, maps:keys(Acl)),
|
||||
{ProdcutId, App}
|
||||
end, Products).
|
||||
|
||||
|
||||
|
||||
get_header(Regular) ->
|
||||
lists:foldl(fun(X, {Header, Len}) ->
|
||||
case X of
|
||||
"**" -> {Header, Len + length(X)};
|
||||
_ -> {Header ++ X, Len + length(X)}
|
||||
end
|
||||
end, {[], 0},
|
||||
re:split(dgiot_utils:to_list(Regular), "-", [{return, list}])).
|
44
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_sup.erl
Normal file
44
apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_sup.erl
Normal file
@ -0,0 +1,44 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2016-2017 John liu <34489690@qq.com>.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @doc dgiot_modbus_tcp supervisor
|
||||
-module(dgiot_modbus_tcp_sup).
|
||||
-include("dgiot_modbus_tcp.hrl").
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
||||
%% Helper macro for declaring children of supervisor
|
||||
-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Supervisor callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
{ok, { {one_for_one, 5, 10}, []}}.
|
||||
|
@ -0,0 +1,78 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author stoneliu
|
||||
%%% @copyright (C) 2019, dgiot
|
||||
%%% @doc
|
||||
%%% API 处理模块
|
||||
%%% Created : 20. 三月 2021 12:00
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(dgiot_modbus_tcp_handler).
|
||||
-author("stoneliu").
|
||||
-behavior(dgiot_rest).
|
||||
-dgiot_rest(all).
|
||||
-include_lib("dgiot/include/logger.hrl").
|
||||
|
||||
%% API
|
||||
-export([swagger_modbus_tcp/0]).
|
||||
-export([handle/4]).
|
||||
|
||||
%% API描述
|
||||
%% 支持二种方式导入
|
||||
%% 示例:
|
||||
%% 1. Metadata为map表示的JSON,
|
||||
%% dgiot_http_server:bind(<<"/pump">>, ?MODULE, [], Metadata)
|
||||
%% 2. 从模块的priv/swagger/下导入
|
||||
%% dgiot_http_server:bind(<<"/swagger_feeders.json">>, ?MODULE, [], priv)
|
||||
swagger_modbus_tcp() ->
|
||||
[
|
||||
dgiot_http_server:bind(<<"/swagger_modbus_tcp.json">>, ?MODULE, [], priv)
|
||||
].
|
||||
|
||||
|
||||
%%%===================================================================
|
||||
%%% 请求处理
|
||||
%%% 如果登录, Context 内有 <<"user">>, version
|
||||
%%%===================================================================
|
||||
|
||||
-spec handle(OperationID :: atom(), Args :: map(), Context :: map(), Req :: dgiot_req:req()) ->
|
||||
{Status :: dgiot_req:http_status(), Body :: map()} |
|
||||
{Status :: dgiot_req:http_status(), Headers :: map(), Body :: map()} |
|
||||
{Status :: dgiot_req:http_status(), Headers :: map(), Body :: map(), Req :: dgiot_req:req()}.
|
||||
|
||||
handle(OperationID, Args, Context, Req) ->
|
||||
Headers = #{},
|
||||
case catch do_request(OperationID, Args, Context, Req) of
|
||||
{ErrType, Reason} when ErrType == 'EXIT'; ErrType == error ->
|
||||
?LOG(info, "do request: ~p, ~p, ~p~n", [OperationID, Args, Reason]),
|
||||
Err = case is_binary(Reason) of
|
||||
true -> Reason;
|
||||
false ->
|
||||
dgiot_ctl:format("~p", [Reason])
|
||||
end,
|
||||
{500, Headers, #{<<"error">> => Err}};
|
||||
ok ->
|
||||
%% ?LOG(debug,"do request: ~p, ~p ->ok ~n", [OperationID, Args]),
|
||||
{200, Headers, #{}, Req};
|
||||
{ok, Res} ->
|
||||
%% ?LOG(info,"do request: ~p, ~p ->~p~n", [OperationID, Args, Res]),
|
||||
{200, Headers, Res, Req};
|
||||
{Status, Res} ->
|
||||
%% ?LOG(info,"do request: ~p, ~p ->~p~n", [OperationID, Args, Res]),
|
||||
{Status, Headers, Res, Req};
|
||||
{Status, NewHeaders, Res} ->
|
||||
%% ?LOG(info,"do request: ~p, ~p ->~p~n", [OperationID, Args, Res]),
|
||||
{Status, maps:merge(Headers, NewHeaders), Res, Req}
|
||||
end.
|
||||
|
||||
|
||||
%%%===================================================================
|
||||
%%% 内部函数 Version:API版本
|
||||
%%%===================================================================
|
||||
|
||||
|
||||
%% PumpTemplet 概要: 新增报告模板 描述:新增报告模板
|
||||
%% OperationId:post_pump_templet
|
||||
%% 请求:get /iotapi/pump/templet
|
||||
%% 服务器不支持的API接口
|
||||
do_request(_OperationId, _Args, _Context, _Req) ->
|
||||
{error, <<"Not Allowed.">>}.
|
@ -141,6 +141,6 @@
|
||||
],
|
||||
"className": "Stage"
|
||||
},
|
||||
"background": "http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/dgiot_tech/zh/blog/study/opc/black_xbs.png"
|
||||
"background": "http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/shuwa_tech/zh/blog/study/opc/black_xbs.png"
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@
|
||||
order => 102,
|
||||
type => string,
|
||||
required => false,
|
||||
default => <<"http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/dgiot_tech/zh/product/dgiot/channel/OPC_ICO.png">>,
|
||||
default => <<"http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/shuwa_tech/zh/product/dgiot/channel/OPC_ICO.png">>,
|
||||
title => #{
|
||||
en => <<"channel ICO">>,
|
||||
zh => <<"通道ICO"/utf8>>
|
||||
|
@ -6,7 +6,7 @@ parse.delete_field = ACL,objectId,updatedAt,createdAt
|
||||
##--------------------------------------------------------------------
|
||||
## parse config
|
||||
##--------------------------------------------------------------------
|
||||
parse.parse_server = http://47.105.106.54:1337
|
||||
parse.parse_server = http://1.117.219.8:1337
|
||||
parse.parse_path = /parse/
|
||||
parse.parse_appid = 1uqZbbdd_JMyQ45YLsUzYezMRPerMa80
|
||||
parse.parse_master_key = PADbN7p973quWLngikp6XvrDbL97u_vM
|
||||
|
@ -22,7 +22,7 @@
|
||||
-define(JSON_DECODE(Data), jsx:decode(Data, [{labels, binary}, return_maps])).
|
||||
-define(HTTPOption(Option), [{timeout, 60000}, {connect_timeout, 60000}] ++ Option).
|
||||
-define(REQUESTOption(Option), [{body_format, binary} | Option]).
|
||||
-define(HEAD_CFG, [{"content-length", del}, {"referer", del}, {"user-agent", "SHUWA"}]).
|
||||
-define(HEAD_CFG, [{"content-length", del}, {"referer", del}, {"user-agent", "dgiot"}]).
|
||||
|
||||
|
||||
%% API
|
||||
|
@ -153,7 +153,7 @@
|
||||
order => 102,
|
||||
type => string,
|
||||
required => false,
|
||||
default => <<"http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/dgiot_tech/zh/product/dgiot/channel/%E6%8C%87%E4%BB%A4%E4%BB%BB%E5%8A%A1%E5%9B%BE%E6%A0%87.png">>,
|
||||
default => <<"http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/shuwa_tech/zh/product/dgiot/channel/%E6%8C%87%E4%BB%A4%E4%BB%BB%E5%8A%A1%E5%9B%BE%E6%A0%87.png">>,
|
||||
title => #{
|
||||
en => <<"channel ICO">>,
|
||||
zh => <<"通道ICO"/utf8>>
|
||||
|
@ -127,7 +127,7 @@
|
||||
order => 102,
|
||||
type => string,
|
||||
required => false,
|
||||
default => <<"http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/dgiot_tech/zh/product/dgiot/channel/TD%E5%9B%BE%E6%A0%87.png">>,
|
||||
default => <<"http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/shuwa_tech/zh/product/dgiot/channel/TD%E5%9B%BE%E6%A0%87.png">>,
|
||||
title => #{
|
||||
en => <<"channel ICO">>,
|
||||
zh => <<"通道ICO"/utf8>>
|
||||
|
@ -67,7 +67,7 @@
|
||||
order => 102,
|
||||
type => string,
|
||||
required => false,
|
||||
default => <<"http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/dgiot_tech/zh/product/dgiot/channel/TOPO%E7%BB%84%E6%80%81%E9%80%9A%E9%81%93%E5%9B%BE%E6%A0%87.png">>,
|
||||
default => <<"http://dgiot-1253666439.cos.ap-shanghai-fsi.myqcloud.com/shuwa_tech/zh/product/dgiot/channel/TOPO%E7%BB%84%E6%80%81%E9%80%9A%E9%81%93%E5%9B%BE%E6%A0%87.png">>,
|
||||
title => #{
|
||||
en => <<"channel ICO">>,
|
||||
zh => <<"通道ICO"/utf8>>
|
||||
|
@ -19,3 +19,5 @@
|
||||
{dgiot_topo, {{enable_plugin_dgiot_topo}}}.
|
||||
{dgiot_opc, {{enable_plugin_dgiot_opc}}}.
|
||||
{dgiot_niisten, {{enable_plugin_dgiot_niisten}}}.
|
||||
{dgiot_modbus, {{enable_plugin_dgiot_modbus}}}.
|
||||
{dgiot_modbus, {{enable_plugin_dgiot_modbus_tcp}}}.
|
||||
|
@ -197,6 +197,8 @@ overlay_vars_rel(RelType) ->
|
||||
, {enable_plugin_dgiot_topo, true}
|
||||
, {enable_plugin_dgiot_opc, true}
|
||||
, {enable_plugin_dgiot_niisten, true}
|
||||
, {enable_plugin_dgiot_modbus, true}
|
||||
, {enable_plugin_dgiot_modbus_tcp, true}
|
||||
, {vm_args_file, VmArgs}
|
||||
].
|
||||
|
||||
@ -312,12 +314,13 @@ relx_plugin_apps_per_rel(cloud) ->
|
||||
, dgiot_tdengine
|
||||
, dgiot_evidence
|
||||
, dgiot_license
|
||||
, dgiot_niisten
|
||||
, dgiot_task
|
||||
, dgiot_http
|
||||
, dgiot_topo
|
||||
, dgiot_opc
|
||||
, dgiot_niisten
|
||||
, dgiot_modbus
|
||||
, dgiot_modbus_tcp
|
||||
];
|
||||
relx_plugin_apps_per_rel(edge) ->
|
||||
[].
|
||||
|
Loading…
Reference in New Issue
Block a user