diff --git a/.gitignore b/.gitignore index 20f9f8fc..5757089e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,4 @@ dist.zip scripts/git-token etc/*.seg _upgrade_base/ -apps/dgiot_sinmahe + diff --git a/apps/dgiot_api/priv/swagger/erlang_handler.src b/apps/dgiot_api/priv/swagger/erlang_handler.src index 94366c12..815e1828 100644 --- a/apps/dgiot_api/priv/swagger/erlang_handler.src +++ b/apps/dgiot_api/priv/swagger/erlang_handler.src @@ -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.">>}. \ No newline at end of file + {error, <<"Not Allowed.">>}. diff --git a/apps/dgiot_api/src/handler/dgiot_role_handler.erl b/apps/dgiot_api/src/handler/dgiot_role_handler.erl index 4edf7f80..d1d4c639 100644 --- a/apps/dgiot_api/src/handler/dgiot_role_handler.erl +++ b/apps/dgiot_api/src/handler/dgiot_role_handler.erl @@ -15,7 +15,7 @@ %%-------------------------------------------------------------------- -module(dgiot_role_handler). --author("shuwa"). +-author("dgiot"). -include_lib("dgiot/include/logger.hrl"). -behavior(dgiot_rest). -dgiot_rest(all). diff --git a/apps/dgiot_api/src/handler/dgiot_system_handler.erl b/apps/dgiot_api/src/handler/dgiot_system_handler.erl index f9ed8a6e..1bbc74b8 100644 --- a/apps/dgiot_api/src/handler/dgiot_system_handler.erl +++ b/apps/dgiot_api/src/handler/dgiot_system_handler.erl @@ -15,7 +15,7 @@ %%-------------------------------------------------------------------- -module(dgiot_system_handler). --author("shuwa"). +-author("dgiot"). -include_lib("dgiot/include/logger.hrl"). -behavior(dgiot_rest). diff --git a/apps/dgiot_bridge/src/dgiot_bridge.erl b/apps/dgiot_bridge/src/dgiot_bridge.erl index 120163cb..c2fd9d90 100644 --- a/apps/dgiot_bridge/src/dgiot_bridge.erl +++ b/apps/dgiot_bridge/src/dgiot_bridge.erl @@ -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. diff --git a/apps/dgiot_bridge/src/dgiot_bridge_app.erl b/apps/dgiot_bridge/src/dgiot_bridge_app.erl index bf6ac927..100a3a2d 100644 --- a/apps/dgiot_bridge/src/dgiot_bridge_app.erl +++ b/apps/dgiot_bridge/src/dgiot_bridge_app.erl @@ -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}. diff --git a/apps/dgiot_bridge/src/dgiot_bridge_server.erl b/apps/dgiot_bridge/src/dgiot_bridge_server.erl index f7550f16..c260711a 100644 --- a/apps/dgiot_bridge/src/dgiot_bridge_server.erl +++ b/apps/dgiot_bridge/src/dgiot_bridge_server.erl @@ -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() }), diff --git a/apps/dgiot_device/src/dgiot_device.erl b/apps/dgiot_device/src/dgiot_device.erl index 4a0dcc05..f730b2da 100644 --- a/apps/dgiot_device/src/dgiot_device.erl +++ b/apps/dgiot_device/src/dgiot_device.erl @@ -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. diff --git a/apps/dgiot_modbus/.gitignore b/apps/dgiot_modbus/.gitignore new file mode 100644 index 00000000..0c20ff0d --- /dev/null +++ b/apps/dgiot_modbus/.gitignore @@ -0,0 +1,6 @@ +.eunit +deps +*.o +*.beam +*.plt +erl_crash.dump diff --git a/apps/dgiot_modbus/LICENSE b/apps/dgiot_modbus/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/apps/dgiot_modbus/LICENSE @@ -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. diff --git a/apps/dgiot_modbus/Makefile b/apps/dgiot_modbus/Makefile new file mode 100644 index 00000000..f45c42c0 --- /dev/null +++ b/apps/dgiot_modbus/Makefile @@ -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 \ No newline at end of file diff --git a/apps/dgiot_modbus/README.md b/apps/dgiot_modbus/README.md new file mode 100644 index 00000000..586c6d79 --- /dev/null +++ b/apps/dgiot_modbus/README.md @@ -0,0 +1,4 @@ +## dgiot_modbus + +dgiot_modbus + diff --git a/apps/dgiot_modbus/etc/dgiot_modbus.conf b/apps/dgiot_modbus/etc/dgiot_modbus.conf new file mode 100644 index 00000000..4f574ecf --- /dev/null +++ b/apps/dgiot_modbus/etc/dgiot_modbus.conf @@ -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 + diff --git a/apps/dgiot_modbus/include/dgiot_modbus.hrl b/apps/dgiot_modbus/include/dgiot_modbus.hrl new file mode 100644 index 00000000..806babdc --- /dev/null +++ b/apps/dgiot_modbus/include/dgiot_modbus.hrl @@ -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 diff --git a/apps/dgiot_modbus/priv/dgiot_modbus.schema b/apps/dgiot_modbus/priv/dgiot_modbus.schema new file mode 100644 index 00000000..14aaea3d --- /dev/null +++ b/apps/dgiot_modbus/priv/dgiot_modbus.schema @@ -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} +]}. diff --git a/apps/dgiot_modbus/rebar.config b/apps/dgiot_modbus/rebar.config new file mode 100644 index 00000000..9780e572 --- /dev/null +++ b/apps/dgiot_modbus/rebar.config @@ -0,0 +1,2 @@ +{deps, [ +]}. diff --git a/apps/dgiot_modbus/src/dgiot_modbus.app.src b/apps/dgiot_modbus/src/dgiot_modbus.app.src new file mode 100644 index 00000000..76db0a4d --- /dev/null +++ b/apps/dgiot_modbus/src/dgiot_modbus.app.src @@ -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, []}} + ]}. diff --git a/apps/dgiot_modbus/src/dgiot_modbus.app.src.script b/apps/dgiot_modbus/src/dgiot_modbus.app.src.script new file mode 100644 index 00000000..d49efe8a --- /dev/null +++ b/apps/dgiot_modbus/src/dgiot_modbus.app.src.script @@ -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. + diff --git a/apps/dgiot_modbus/src/dgiot_modbus_app.erl b/apps/dgiot_modbus/src/dgiot_modbus_app.erl new file mode 100644 index 00000000..cec60320 --- /dev/null +++ b/apps/dgiot_modbus/src/dgiot_modbus_app.erl @@ -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. diff --git a/apps/dgiot_modbus/src/dgiot_modbus_sup.erl b/apps/dgiot_modbus/src/dgiot_modbus_sup.erl new file mode 100644 index 00000000..810c0c4c --- /dev/null +++ b/apps/dgiot_modbus/src/dgiot_modbus_sup.erl @@ -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}, []}}. + + diff --git a/apps/dgiot_modbus/src/modbus/modbus.erl b/apps/dgiot_modbus/src/modbus/modbus.erl new file mode 100644 index 00000000..78ef910d --- /dev/null +++ b/apps/dgiot_modbus/src/modbus/modbus.erl @@ -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, + <>}, 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, + <>}, N). + +read_coils(Pid, Addr, N) + when is_integer(Addr), Addr >= 0 -> + send_read_pdu(Pid, {pdu, ?READ_COILS, <>}, N). +read_coils(Pid, UnitId, Addr, N) + when is_integer(UnitId), is_integer(Addr), Addr >= 0 -> + send_read_pdu(Pid, {pdu, UnitId, ?READ_COILS, <>}, 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, 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, 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, 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, N). + +read_input_registers(Pid, Addr, N) + when is_integer(Addr), Addr >= 0 -> + send_read_reg_pdu(Pid, {pdu, ?READ_INPUT_REGISTERS, <>}). +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, + <>}). + +read_holding_registers(Pid, Addr, N) + when is_integer(Addr), Addr >= 0 -> + send_read_reg_pdu(Pid, {pdu, ?READ_HOLDING_REGISTERS, <>}). +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, + <>}). + +write_single_holding_register(Pid, Addr, Value) + when is_integer(Addr), Addr >= 0 -> + send_write_pdu(Pid, {pdu, ?WRITE_SINGLE_HOLDING_REGISTER, + <>}, + 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, 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, 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, N). + +send_read_pdu(Pid, Msg, N) -> + try gen_server:call(Pid, Msg) of + {ok, <>} 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, <>} -> + ok; + Error -> Error + catch + _Type:Reason -> + {error, Reason} + end. + +send_read_reg_pdu(Pid, Msg) -> + try gen_server:call(Pid, Msg) of + {ok, <>} -> + {ok, [ Reg || <> <= 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 <- 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||<> <= <> ]) || + <> <= 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, <>); +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), + <>. diff --git a/apps/dgiot_modbus/src/modbus/modbus_demo_callback.erl b/apps/dgiot_modbus/src/modbus/modbus_demo_callback.erl new file mode 100644 index 00000000..af386fe0 --- /dev/null +++ b/apps/dgiot_modbus/src/modbus/modbus_demo_callback.erl @@ -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). diff --git a/apps/dgiot_modbus/src/modbus/modbus_device.erl b/apps/dgiot_modbus/src/modbus/modbus_device.erl new file mode 100644 index 00000000..aab10604 --- /dev/null +++ b/apps/dgiot_modbus/src/modbus/modbus_device.erl @@ -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) -> + <> = 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 = <>, + + Size = byte_size(Message), + <>; + +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 = <>, + + Size = byte_size(Message), + <>; + +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 = <>, + + Size = size(Message), + <>; + +generate_request(#tcp_request{tid = Tid, address = Address, function = Code, start = Start, data = Data}) -> + Message = <>, + Size = size(Message), + <>. + + +%% @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, <>} -> + 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, <>} -> + {ok, Data}; + {ok, <>} -> + {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. diff --git a/apps/dgiot_modbus/src/modbus/modbus_rtu.erl b/apps/dgiot_modbus/src/modbus/modbus_rtu.erl new file mode 100644 index 00000000..ea10ad11 --- /dev/null +++ b/apps/dgiot_modbus/src/modbus/modbus_rtu.erl @@ -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 = <>, + 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, + + <> = dgiot_utils:hex_to_binary(is16(Address)), + <> = 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(<> = Buff, Acc, + #{<<"addr">> := DtuAddr} = State) -> + CheckCrc = dgiot_utils:crc16(<>), + 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(<> = 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(<> = 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) -> + <> = Buff, + {SizeOfData, DataBytes} = + case FunCode of + ?FC_READ_COILS -> + <> = ResponseData, + {Size, Data}; + ?FC_READ_INPUTS -> + <> = ResponseData, + {Size, Data}; + ?FC_READ_HREGS -> + <> = ResponseData, + {Size, Data}; + ?FC_READ_IREGS -> + <> = 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 -> + <> = DataBytes, + CheckBuf = <>, + 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, + <> = Rest1, + CheckBuf = <>, + 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), + <>. + +%%%% ---------------- +%%%% 解析数据值 +%%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) -> +%% <> = <>, +%% 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) -> +%% <> = <>, +%% 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, [<> | Res]). + +list_word16_to_binary(Values) when is_list(Values) -> + list_to_binary( + lists:map( + fun(X) -> + RoundedValue = round(X), + <> + 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">> + }} -> + <> = dgiot_utils:hex_to_binary(modbus_rtu:is16(OldSlaveid)), + <> = 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, + <> = Buff, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"short16_AB">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(2, IntLen) * 8, + <> = Buff, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"short16_BA">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(2, IntLen) * 8, + <> = Buff, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"ushort16_AB">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(2, IntLen) * 8, + <> = Buff, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"ushort16_BA">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(2, IntLen) * 8, + <> = Buff, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"long32_ABCD">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(4, IntLen) * 8, + <> = Buff, + <> = <>, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"long32_CDAB">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(4, IntLen) * 8, + <> = Buff, + <> = <>, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"ulong32_ABCD">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(4, IntLen) * 8, + <> = Buff, + <> = <>, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"ulong32_CDAB">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(4, IntLen) * 8, + <> = Buff, + <> = <>, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"float32_ABCD">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(4, IntLen) * 8, + <> = Buff, + <> = <>, + {Value, Rest}; + +format_value(Buff, #{<<"dataForm">> := #{ + <<"data">> := Len, + <<"originaltype">> := <<"float32_CDAB">> +}}) -> + IntLen = dgiot_utils:to_int(Len), + Size = max(4, IntLen) * 8, + <> = Buff, + <> = <>, + {Value, Rest}; + +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"big">>, +%% <<"originaltype">> := <<"uint16">> +%%}}) -> +%% Size = max(2, Len) * 8, +%% <> = Buff, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"little">>, +%% <<"originaltype">> := <<"uint16">>}}) -> +%% Size = max(2, Len) * 8, +%% <> = Buff, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"big">>, +%% <<"originaltype">> := <<"int16">>}}) -> +%% Size = max(2, Len) * 8, +%% <> = Buff, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"little">>, +%% <<"originaltype">> := <<"int16">>}}) -> +%% Size = max(2, Len) * 8, +%% <> = Buff, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"big">>, +%% <<"originaltype">> := <<"uint32">>} +%%}) -> +%% Size = max(4, Len) * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"little">>, +%% <<"originaltype">> := <<"uint32">>} +%%}) -> +%% Size = max(4, Len) * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"big">>, +%% <<"originaltype">> := <<"int32">>} +%%}) -> +%% Size = max(4, Len) * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"little">>, +%% <<"originaltype">> := <<"int32">>}}) -> +%% Size = max(4, Len) * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"big">>, +%% <<"originaltype">> := <<"float">>}}) -> +%% Size = max(4, Len) * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"little">>, +%% <<"originaltype">> := <<"float">>}}) -> +%% Size = max(4, Len) * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"big">>, +%% <<"originaltype">> := <<"double">>}}) -> +%% Size = max(4, Len) * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"little">>, +%% <<"originaltype">> := <<"double">>}}) -> +%% Size = max(4, Len) * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"big">>, +%% <<"originaltype">> := <<"string">>}}) -> +%% Size = Len * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"little">>, +%% <<"originaltype">> := <<"string">>}}) -> +%% Size = Len * 8, +%% <> = Buff, +%% <> = <>, +%% {Value, Rest}; +%% +%%%% customized data(按大端顺序返回hex data) +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"big">>}}) -> +%% <> = Buff, +%% {Value, Rest}; +%% +%%format_value(Buff, #{<<"dataForm">> := #{ +%% <<"quantity">> := Len, +%% <<"byteorder">> := <<"little">>}}) -> +%% <> = Buff, +%% {Value, Rest}; + +%% @todo 其它类型处理 +format_value(_, #{<<"identifier">> := Field}) -> + ?LOG(info,"Field ~p", [Field]), + throw({field_error, <>}). diff --git a/apps/dgiot_modbus/src/modbus/modbus_tcp_client.erl b/apps/dgiot_modbus/src/modbus/modbus_tcp_client.erl new file mode 100644 index 00000000..631faad9 --- /dev/null +++ b/apps/dgiot_modbus/src/modbus/modbus_tcp_client.erl @@ -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? +%% <> -> +%% State1 = handle_reply_pdu(TransID, UnitID, 16#80+Func, +%% Params, State#state { buf = Buf1 }), +%% {noreply, State1}; +%% <> -> +%% case Data1 of +%% <> -> +%% 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 = <>, +%% ?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 +%% <> 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). diff --git a/apps/dgiot_modbus/src/modbus/modbus_tcp_server.erl b/apps/dgiot_modbus/src/modbus/modbus_tcp_server.erl new file mode 100644 index 00000000..ffe395fa --- /dev/null +++ b/apps/dgiot_modbus/src/modbus/modbus_tcp_server.erl @@ -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}. diff --git a/apps/dgiot_modbus/src/modbus/modbus_tcp_server1.erl b/apps/dgiot_modbus/src/modbus/modbus_tcp_server1.erl new file mode 100644 index 00000000..3b321492 --- /dev/null +++ b/apps/dgiot_modbus/src/modbus/modbus_tcp_server1.erl @@ -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 +%% <> -> +%% case Data1 of +%% <> -> +%% ?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), +%% <>), +%% {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, +%% <>, 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, <>), +%% State; +%%handle_pdu(Socket, UnitID, TransID, ?READ_COILS, +%% <>, 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, <>), +%% State; +%%handle_pdu(Socket, UnitID, TransID, ?WRITE_SINGLE_COIL, +%% <>, State) -> +%% Value1 = apply(State#state.callback, write_single_coil, [Addr,Value]), +%% send(Socket, TransID, State#state.proto_id, UnitID, +%% ?WRITE_SINGLE_COIL, <>), +%% State; +%%handle_pdu(Socket, UnitID, TransID, ?WRITE_MULTIPLE_COILS, +%% <>, 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, <>), +%% State; +%%handle_pdu(Socket, UnitID, TransID, ?READ_INPUT_REGISTERS, +%% <>, State) -> +%% Regs = apply(State#state.callback, read_input_registers, [Addr,N]), +%% RegData = << <> || Reg <- Regs >>, +%% Len = byte_size(RegData), +%% send(Socket, TransID, State#state.proto_id,UnitID, +%% ?READ_INPUT_REGISTERS,<>), +%% State; +%%handle_pdu(Socket, UnitID, TransID, ?READ_HOLDING_REGISTERS, +%% <>, State) -> +%% Regs = apply(State#state.callback, read_holding_registers, [Addr,N]), +%% RegData = << <> || Reg <- Regs >>, +%% Len = byte_size(RegData), +%% send(Socket, TransID, State#state.proto_id,UnitID, +%% ?READ_HOLDING_REGISTERS,<>), +%% State; +%%handle_pdu(Socket, UnitID, TransID, ?WRITE_SINGLE_HOLDING_REGISTER, +%% <>, State) -> +%% Value1 = apply(State#state.callback, write_single_holding_register, +%% [Addr,Value]), +%% send(Socket, TransID, State#state.proto_id, UnitID, +%% ?WRITE_SINGLE_HOLDING_REGISTER, <>), +%% State; +%%handle_pdu(Socket, UnitID, TransID, ?WRITE_MULTIPLE_HOLDING_REGISTERS, +%% <>, State) -> +%% Values = [ V || <> <= 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,<>), +%% State; +%%handle_pdu(Socket, UnitID, TransID, Func,<<_/binary>>, State) -> +%% send(Socket,TransID,State#state.proto_id, UnitID, +%% 16#80 + (Func band 16#7f), <>). +%% +%%send(Socket,TransID,ProtoID,UnitID,Func,Bin) -> +%% Length = byte_size(Bin) + 2, +%% Data = <>, +%% exo_socket:send(Socket, Data). diff --git a/apps/dgiot_modbus/src/modbus/modbus_util.erl b/apps/dgiot_modbus/src/modbus/modbus_util.erl new file mode 100644 index 00000000..464af0e6 --- /dev/null +++ b/apps/dgiot_modbus/src/modbus/modbus_util.erl @@ -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 || <> <= <>]) || <> <= Bin]). + +%% @doc Function to convert bytes to 16bits integer. +%% @end +-spec binary_to_int16(Bin::binary()) -> [integer()]. +binary_to_int16(Bin) -> + [ X || <> <= Bin ]. + +%% @doc Function to convert bytes to 16bits signed integer. +%% @end +-spec binary_to_int16s(Bin::binary()) -> [integer()]. +binary_to_int16s(Bin) -> + [ X || <> <= Bin ]. + +%% @doc Function to convert bytes to 32bits integer. +%% @end +-spec binary_to_int32(Bin::binary()) -> [integer()]. +binary_to_int32(Bin) -> + [ X || <> <= Bin ]. + +%% @doc Function to convert bytes to 32bits signed integer. +%% @end +-spec binary_to_int32s(Bin::binary()) -> [integer()]. +binary_to_int32s(Bin) -> + [ X || <> <= Bin ]. + +%% @doc Function to convert bytes to 32bits float number. +%% @end +-spec binary_to_float32(Bin::binary()) -> [float()]. +binary_to_float32(Bin) -> + [ X || <> <= 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, <>); +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 <- Values >>. diff --git a/apps/dgiot_modbus_tcp/.gitignore b/apps/dgiot_modbus_tcp/.gitignore new file mode 100644 index 00000000..218b2226 --- /dev/null +++ b/apps/dgiot_modbus_tcp/.gitignore @@ -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/ diff --git a/apps/dgiot_modbus_tcp/LICENSE b/apps/dgiot_modbus_tcp/LICENSE new file mode 100644 index 00000000..89b26d4e --- /dev/null +++ b/apps/dgiot_modbus_tcp/LICENSE @@ -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. diff --git a/apps/dgiot_modbus_tcp/Makefile b/apps/dgiot_modbus_tcp/Makefile new file mode 100644 index 00000000..94a901db --- /dev/null +++ b/apps/dgiot_modbus_tcp/Makefile @@ -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 diff --git a/apps/dgiot_modbus_tcp/README.md b/apps/dgiot_modbus_tcp/README.md new file mode 100644 index 00000000..bd5dded6 --- /dev/null +++ b/apps/dgiot_modbus_tcp/README.md @@ -0,0 +1,2 @@ +# dgiot_modbus_tcp + diff --git a/apps/dgiot_modbus_tcp/erlang.mk b/apps/dgiot_modbus_tcp/erlang.mk new file mode 100644 index 00000000..8930dfc4 --- /dev/null +++ b/apps/dgiot_modbus_tcp/erlang.mk @@ -0,0 +1 @@ +include ../../erlang.mk diff --git a/apps/dgiot_modbus_tcp/etc/dgiot_modbus_tcp.conf b/apps/dgiot_modbus_tcp/etc/dgiot_modbus_tcp.conf new file mode 100644 index 00000000..e69de29b diff --git a/apps/dgiot_modbus_tcp/include/dgiot_modbus_tcp.hrl b/apps/dgiot_modbus_tcp/include/dgiot_modbus_tcp.hrl new file mode 100644 index 00000000..cdf74c22 --- /dev/null +++ b/apps/dgiot_modbus_tcp/include/dgiot_modbus_tcp.hrl @@ -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 = <<>> +}). + diff --git a/apps/dgiot_modbus_tcp/priv/swagger/swagger_modbus_tcp.json b/apps/dgiot_modbus_tcp/priv/swagger/swagger_modbus_tcp.json new file mode 100644 index 00000000..044505fb --- /dev/null +++ b/apps/dgiot_modbus_tcp/priv/swagger/swagger_modbus_tcp.json @@ -0,0 +1,11 @@ +{ + "definitions": {}, + "paths": { + }, + "tags": [ + { + "description": "dgiot_modbus_tcp", + "name": "dgiot_modbus_tcp" + } + ] +} diff --git a/apps/dgiot_modbus_tcp/rebar.config b/apps/dgiot_modbus_tcp/rebar.config new file mode 100644 index 00000000..6d6bfe1a --- /dev/null +++ b/apps/dgiot_modbus_tcp/rebar.config @@ -0,0 +1,3 @@ +{deps, [ + +]}. diff --git a/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src new file mode 100644 index 00000000..624ae838 --- /dev/null +++ b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src @@ -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,[]}}]}. diff --git a/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src.script b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src.script new file mode 100644 index 00000000..d49efe8a --- /dev/null +++ b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.app.src.script @@ -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. + diff --git a/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.erl b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.erl new file mode 100644 index 00000000..773e433a --- /dev/null +++ b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp.erl @@ -0,0 +1,192 @@ +%%%------------------------------------------------------------------- +%%% @author stoneliu +%%% @copyright (C) 2020, +%%% @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)]), + <> = dgiot_utils:hex_to_binary(modbus_rtu:is16(Di)), + <> = 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. diff --git a/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_app.erl b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_app.erl new file mode 100644 index 00000000..2bce510c --- /dev/null +++ b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_app.erl @@ -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. diff --git a/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_channel.erl b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_channel.erl new file mode 100644 index 00000000..bca0e614 --- /dev/null +++ b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_channel.erl @@ -0,0 +1,170 @@ +%%%------------------------------------------------------------------- +%%% @copyright (C) 2019, +%%% @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}])). diff --git a/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_sup.erl b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_sup.erl new file mode 100644 index 00000000..61d23d99 --- /dev/null +++ b/apps/dgiot_modbus_tcp/src/dgiot_modbus_tcp_sup.erl @@ -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}, []}}. + diff --git a/apps/dgiot_modbus_tcp/src/handler/dgiot_modbus_tcp_handler.erl b/apps/dgiot_modbus_tcp/src/handler/dgiot_modbus_tcp_handler.erl new file mode 100644 index 00000000..74dca2b5 --- /dev/null +++ b/apps/dgiot_modbus_tcp/src/handler/dgiot_modbus_tcp_handler.erl @@ -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.">>}. diff --git a/apps/dgiot_opc/priv/opc_topo.json b/apps/dgiot_opc/priv/opc_topo.json index 5558213a..61810569 100644 --- a/apps/dgiot_opc/priv/opc_topo.json +++ b/apps/dgiot_opc/priv/opc_topo.json @@ -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" } } diff --git a/apps/dgiot_opc/src/dgiot_opc_channel.erl b/apps/dgiot_opc/src/dgiot_opc_channel.erl index 493fee1e..b3e9bfac 100644 --- a/apps/dgiot_opc/src/dgiot_opc_channel.erl +++ b/apps/dgiot_opc/src/dgiot_opc_channel.erl @@ -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>> diff --git a/apps/dgiot_parse/etc/dgiot_parse.conf b/apps/dgiot_parse/etc/dgiot_parse.conf index 7709144f..29d58533 100644 --- a/apps/dgiot_parse/etc/dgiot_parse.conf +++ b/apps/dgiot_parse/etc/dgiot_parse.conf @@ -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 diff --git a/apps/dgiot_parse/src/dgiot_parse_rest.erl b/apps/dgiot_parse/src/dgiot_parse_rest.erl index 08770426..0105337b 100644 --- a/apps/dgiot_parse/src/dgiot_parse_rest.erl +++ b/apps/dgiot_parse/src/dgiot_parse_rest.erl @@ -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 diff --git a/apps/dgiot_task/src/dgiot_task_channel.erl b/apps/dgiot_task/src/dgiot_task_channel.erl index 69884bb4..05f8a8c1 100644 --- a/apps/dgiot_task/src/dgiot_task_channel.erl +++ b/apps/dgiot_task/src/dgiot_task_channel.erl @@ -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>> diff --git a/apps/dgiot_tdengine/src/dgiot_tdengine_channel.erl b/apps/dgiot_tdengine/src/dgiot_tdengine_channel.erl index 2628a2c9..39acce48 100644 --- a/apps/dgiot_tdengine/src/dgiot_tdengine_channel.erl +++ b/apps/dgiot_tdengine/src/dgiot_tdengine_channel.erl @@ -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>> diff --git a/apps/dgiot_topo/src/dgiot_topo_channel.erl b/apps/dgiot_topo/src/dgiot_topo_channel.erl index b98e8665..170febce 100644 --- a/apps/dgiot_topo/src/dgiot_topo_channel.erl +++ b/apps/dgiot_topo/src/dgiot_topo_channel.erl @@ -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>> diff --git a/data/loaded_plugins.tmpl b/data/loaded_plugins.tmpl index ddd81ed8..76556405 100644 --- a/data/loaded_plugins.tmpl +++ b/data/loaded_plugins.tmpl @@ -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}}}. diff --git a/rebar.config.erl b/rebar.config.erl index c4d9a7a9..ff277c10 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -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) -> [].