mirror of
https://gitee.com/dgiiot/dgiot.git
synced 2024-11-29 18:57:41 +08:00
add new project
This commit is contained in:
parent
006ef08483
commit
789fe8333a
@ -23,7 +23,7 @@ The **header** with **type** is mandatory. The **scope** of the header is option
|
||||
|
||||
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier to read on GitHub as well as in various git tools.
|
||||
|
||||
The footer should contain a [closing reference to an issue](https://help.hub.fastgit.org/articles/closing-issues-via-commit-messages/) if any.
|
||||
The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
|
||||
|
||||
Example 1:
|
||||
|
||||
|
9
Makefile
9
Makefile
@ -5,10 +5,15 @@ BUILD = $(CURDIR)/build
|
||||
SCRIPTS = $(CURDIR)/scripts
|
||||
export PKG_VSN ?= $(shell $(CURDIR)/pkg-vsn.sh)
|
||||
export EMQX_DESC ?= EMQ X
|
||||
export EMQX_CE_DASHBOARD_VERSION ?= v4.3.1
|
||||
export EMQX_CE_DASHBOARD_VERSION ?= v4.3.0
|
||||
ifeq ($(OS),Windows_NT)
|
||||
export REBAR_COLOR=none
|
||||
endif
|
||||
ifeq ($(OS),Windows_NT)
|
||||
GET_DASHBOARD=$(SCRIPTS)/get-windashboard.sh
|
||||
else
|
||||
GET_DASHBOARD=$(SCRIPTS)/get-dashboard.sh
|
||||
endif
|
||||
|
||||
PROFILE ?= emqx
|
||||
REL_PROFILES := emqx emqx-edge
|
||||
@ -32,7 +37,7 @@ $(REBAR): ensure-rebar3
|
||||
|
||||
.PHONY: get-dashboard
|
||||
get-dashboard:
|
||||
@$(SCRIPTS)/get-dashboard.sh
|
||||
@$(GET_DASHBOARD)
|
||||
|
||||
.PHONY: eunit
|
||||
eunit: $(REBAR)
|
||||
|
14
README-CN.md
14
README-CN.md
@ -1,6 +1,6 @@
|
||||
# EMQ X Broker
|
||||
|
||||
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://hub.fastgit.org/emqx/emqx/releases)
|
||||
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases)
|
||||
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
从 3.0 版本开始,*EMQ X* 完整支持 MQTT V5.0 协议规范,向下兼容 MQTT V3.1 和 V3.1.1,并支持 MQTT-SN、CoAP、LwM2M、WebSocket 和 STOMP 等通信协议。EMQ X 3.0 单集群可支持千万级别的 MQTT 并发连接。
|
||||
|
||||
- 新功能的完整列表,请参阅 [EMQ X Release Notes](https://hub.fastgit.org/emqx/emqx/releases)。
|
||||
- 新功能的完整列表,请参阅 [EMQ X Release Notes](https://github.com/emqx/emqx/releases)。
|
||||
- 获取更多信息,请访问 [EMQ X 官网](https://www.emqx.cn/)。
|
||||
|
||||
## 安装
|
||||
@ -45,7 +45,7 @@ docker run -d --name emqx -p 1883:1883 -p 8081:8081 -p 8083:8083 -p 8883:8883 -p
|
||||
4.3 及以后的版本:
|
||||
|
||||
```bash
|
||||
git clone https://hub.fastgit.org/emqx/emqx.git
|
||||
git clone https://github.com/emqx/emqx.git
|
||||
cd emqx
|
||||
make
|
||||
_build/emqx/rel/emqx/bin console
|
||||
@ -54,7 +54,7 @@ _build/emqx/rel/emqx/bin console
|
||||
对于 4.3 之前的版本,通过另外一个仓库构建:
|
||||
|
||||
```bash
|
||||
git clone https://hub.fastgit.org/emqx/emqx-rel.git
|
||||
git clone https://github.com/emqx/emqx-rel.git
|
||||
cd emqx-rel
|
||||
make
|
||||
_build/emqx/rel/emqx/bin/emqx console
|
||||
@ -111,12 +111,12 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer
|
||||
|
||||
### 问答
|
||||
|
||||
[GitHub Discussions](https://hub.fastgit.org/emqx/emqx/discussions)
|
||||
[GitHub Discussions](https://github.com/emqx/emqx/discussions)
|
||||
[EMQ 中文问答社区](https://askemq.com)
|
||||
|
||||
### 参与设计
|
||||
|
||||
如果对 EMQ X 有改进建议,可以向[EIP](https://hub.fastgit.org/emqx/eip) 提交 PR 和 ISSUE
|
||||
如果对 EMQ X 有改进建议,可以向[EIP](https://github.com/emqx/eip) 提交 PR 和 ISSUE
|
||||
|
||||
### 插件开发
|
||||
|
||||
@ -134,7 +134,7 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer
|
||||
- [Weibo](https://weibo.com/emqtt)
|
||||
- [Blog](https://www.emqx.cn/blog)
|
||||
|
||||
欢迎你将任何 bug、问题和功能请求提交到 [emqx/emqx](https://hub.fastgit.org/emqx/emqx/issues)。
|
||||
欢迎你将任何 bug、问题和功能请求提交到 [emqx/emqx](https://github.com/emqx/emqx/issues)。
|
||||
|
||||
## MQTT 规范
|
||||
|
||||
|
12
README-JP.md
12
README-JP.md
@ -1,6 +1,6 @@
|
||||
# EMQ X Broker
|
||||
|
||||
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://hub.fastgit.org/emqx/emqx/releases)
|
||||
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases)
|
||||
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
|
||||
@ -16,7 +16,7 @@
|
||||
version 3.0 以降、*EMQ X* は MQTT V5.0 の仕様を完全にサポートしており、MQTT V3.1およびV3.1.1とも下位互換性があります。
|
||||
MQTT-SN、CoAP、LwM2M、WebSocket、STOMPなどの通信プロトコルをサポートしています。 MQTTの同時接続数は1つのクラスター上で1,000万以上にまでスケールできます。
|
||||
|
||||
- 新機能の一覧については、[EMQ Xリリースノート](https://hub.fastgit.org/emqx/emqx/releases)を参照してください。
|
||||
- 新機能の一覧については、[EMQ Xリリースノート](https://github.com/emqx/emqx/releases)を参照してください。
|
||||
- 詳細はこちら[EMQ X公式ウェブサイト](https://www.emqx.io/)をご覧ください。
|
||||
|
||||
## インストール
|
||||
@ -46,7 +46,7 @@ version 3.0 以降の *EMQ X* をビルドするには Erlang/OTP R21+ が必要
|
||||
version 4.3 以降の場合:
|
||||
|
||||
```bash
|
||||
git clone https://hub.fastgit.org/emqx/emqx-rel.git
|
||||
git clone https://github.com/emqx/emqx-rel.git
|
||||
cd emqx-rel
|
||||
make
|
||||
_build/emqx/rel/emqx/bin/emqx console
|
||||
@ -105,11 +105,11 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer
|
||||
|
||||
### 質問する
|
||||
|
||||
質問や知識共有の場として[GitHub Discussions](https://hub.fastgit.org/emqx/emqx/discussions)を用意しています。
|
||||
質問や知識共有の場として[GitHub Discussions](https://github.com/emqx/emqx/discussions)を用意しています。
|
||||
|
||||
### 提案
|
||||
|
||||
大規模な改善のご提案がある場合は、[EIP](https://hub.fastgit.org/emqx/eip)にPRをどうぞ。
|
||||
大規模な改善のご提案がある場合は、[EIP](https://github.com/emqx/eip)にPRをどうぞ。
|
||||
|
||||
### 自作プラグイン
|
||||
|
||||
@ -128,4 +128,4 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0, see [LICENSE](https://hub.fastgit.org/emqx/MQTTX/blob/master/LICENSE).
|
||||
Apache License 2.0, see [LICENSE](https://github.com/emqx/MQTTX/blob/master/LICENSE).
|
||||
|
14
README-RU.md
14
README-RU.md
@ -1,12 +1,12 @@
|
||||
# Брокер EMQ X
|
||||
|
||||
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://hub.fastgit.org/emqx/emqx/releases)
|
||||
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://github.com/emqx/emqx/releases)
|
||||
[![Build Status](https://travis-ci.org/emqx/emqx.svg)](https://travis-ci.org/emqx/emqx)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
|
||||
[![Slack Invite](<https://slack-invite.emqx.io/badge.svg>)](https://slack-invite.emqx.io)
|
||||
[![Twitter](https://img.shields.io/badge/Follow-EMQ-1DA1F2?logo=twitter)](https://twitter.com/EMQTech)
|
||||
[![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow?logo=github)](https://hub.fastgit.org/emqx/emqx/discussions)
|
||||
[![Community](https://img.shields.io/badge/Community-EMQ%20X-yellow?logo=github)](https://github.com/emqx/emqx/discussions)
|
||||
|
||||
[![The best IoT MQTT open source team looks forward to your joining](https://www.emqx.io/static/img/github_readme_en_bg.png)](https://www.emqx.io/careers)
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
Начиная с релиза 3.0, брокер *EMQ X* полностью поддерживает протокол MQTT версии 5.0, и обратно совместим с версиями 3.1 и 3.1.1, а также протоколами MQTT-SN, CoAP, LwM2M, WebSocket и STOMP. Начиная с релиза 3.0, брокер *EMQ X* может масштабироваться до более чем 10 миллионов одновременных MQTT соединений на один кластер.
|
||||
|
||||
- Полный список возможностей доступен по ссылке: [EMQ X Release Notes](https://hub.fastgit.org/emqx/emqx/releases).
|
||||
- Полный список возможностей доступен по ссылке: [EMQ X Release Notes](https://github.com/emqx/emqx/releases).
|
||||
- Более подробная информация доступна на нашем сайте: [EMQ X homepage](https://www.emqx.io).
|
||||
|
||||
## Установка
|
||||
@ -46,7 +46,7 @@ docker run -d --name emqx -p 1883:1883 -p 8081:8081 -p 8083:8083 -p 8883:8883 -p
|
||||
Инструкция для сборки версии 4.3 и выше:
|
||||
|
||||
```bash
|
||||
git clone https://hub.fastgit.org/emqx/emqx.git
|
||||
git clone https://github.com/emqx/emqx.git
|
||||
cd emqx
|
||||
make
|
||||
_build/emqx/rel/emqx/bin console
|
||||
@ -55,7 +55,7 @@ _build/emqx/rel/emqx/bin console
|
||||
Более ранние релизы могут быть собраны с помощью другого репозитория:
|
||||
|
||||
```bash
|
||||
git clone https://hub.fastgit.org/emqx/emqx-rel.git
|
||||
git clone https://github.com/emqx/emqx-rel.git
|
||||
cd emqx-rel
|
||||
make
|
||||
_build/emqx/rel/emqx/bin/emqx console
|
||||
@ -115,11 +115,11 @@ DIALYZER_ANALYSE_APP=emqx_lwm2m,emqx_auth_jwt,emqx_auth_ldap make dialyzer
|
||||
|
||||
### Вопросы
|
||||
|
||||
Задать вопрос или поделиться идеей можно в [GitHub Discussions](https://hub.fastgit.org/emqx/emqx/discussions).
|
||||
Задать вопрос или поделиться идеей можно в [GitHub Discussions](https://github.com/emqx/emqx/discussions).
|
||||
|
||||
### Предложения
|
||||
|
||||
Более масштабные предложения можно присылать в виде pull request в репозиторий [EIP](https://hub.fastgit.org/emqx/eip).
|
||||
Более масштабные предложения можно присылать в виде pull request в репозиторий [EIP](https://github.com/emqx/eip).
|
||||
|
||||
### Разработка плагинов
|
||||
|
||||
|
@ -61,7 +61,7 @@ make
|
||||
|
||||
| 联系方式 | 地址 |
|
||||
| -------------- | ----------------------------------------------------------------------------------------- |
|
||||
| github | [https://hub.fastgit.org/dgiot](https://hub.fastgit.org/dgiot?from=git) |
|
||||
| github | [https://github.com/dgiot](https://github.com/dgiot?from=git) |
|
||||
| gitee | [https://gitee.com/dgiot](https://gitee.com/dgiiot?from=git) |
|
||||
| 官网 | [https://www.iotn2n.com](https://www.iotn2n.com?from=git) |
|
||||
| 博客 | [https://tech.iotn2n.com](https://tech.iotn2n.com?from=git) |
|
||||
|
@ -13,7 +13,7 @@ install Visual Studio.
|
||||
Visual Studio 2019 is used in our tests.
|
||||
If you are like me (@zmstone), do not know where to start,
|
||||
please follow this OTP guide:
|
||||
https://hub.fastgit.org/erlang/otp/blob/master/HOWTO/INSTALL-WIN32.md
|
||||
https://github.com/erlang/otp/blob/master/HOWTO/INSTALL-WIN32.md
|
||||
|
||||
NOTE: To avoid surprises, you may need to add below two paths to `Path` environment variable
|
||||
and order them before other paths.
|
||||
@ -86,7 +86,7 @@ scoop install git curl make jq zip unzip
|
||||
|
||||
## Build EMQ X source code
|
||||
|
||||
* Clone the repo: `git clone https://hub.fastgit.org/emqx/emqx.git`
|
||||
* Clone the repo: `git clone https://github.com/emqx/emqx.git`
|
||||
|
||||
* Start CMD or Powershell
|
||||
|
||||
|
27
apps/dgiot/.editorconfig
Normal file
27
apps/dgiot/.editorconfig
Normal file
@ -0,0 +1,27 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{erl, src, hrl}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Tab indentation (no size specified)
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
43
apps/dgiot/.gitignore
vendored
Normal file
43
apps/dgiot/.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
.eunit
|
||||
deps
|
||||
!deps/.placeholder
|
||||
*.o
|
||||
*.beam
|
||||
*.plt
|
||||
erl_crash.dump
|
||||
ebin
|
||||
!ebin/.placeholder
|
||||
.concrete/DEV_MODE
|
||||
.rebar
|
||||
test/ebin/*.beam
|
||||
.exrc
|
||||
plugins/*/ebin
|
||||
log/
|
||||
*.swp
|
||||
*.so
|
||||
.erlang.mk/
|
||||
cover/
|
||||
emqx.d
|
||||
eunit.coverdata
|
||||
test/ct.cover.spec
|
||||
logs
|
||||
ct.coverdata
|
||||
.idea/
|
||||
emqx.iml
|
||||
_rel/
|
||||
data/
|
||||
_build
|
||||
.rebar3
|
||||
rebar3.crashdump
|
||||
.DS_Store
|
||||
emqx.iml
|
||||
bbmustache/
|
||||
etc/gen.emqx.conf
|
||||
compile_commands.json
|
||||
cuttlefish
|
||||
rebar.lock
|
||||
xrefr
|
||||
erlang.mk
|
||||
*.coverdata
|
||||
etc/emqx.conf.rendered
|
||||
Mnesia.*/
|
201
apps/dgiot/LICENSE
Normal file
201
apps/dgiot/LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
139
apps/dgiot/Makefile
Normal file
139
apps/dgiot/Makefile
Normal file
@ -0,0 +1,139 @@
|
||||
## shallow clone for speed
|
||||
|
||||
REBAR_GIT_CLONE_OPTIONS += --depth 1
|
||||
export REBAR_GIT_CLONE_OPTIONS
|
||||
|
||||
SUITES_FILES := $(shell find test -name '*_SUITE.erl' | sort)
|
||||
|
||||
CT_SUITES := $(foreach value,$(SUITES_FILES),$(shell val=$$(basename $(value) .erl); echo $${val%_*}))
|
||||
|
||||
CT_NODE_NAME = emqxct@127.0.0.1
|
||||
|
||||
RUN_NODE_NAME = emqxdebug@127.0.0.1
|
||||
|
||||
.PHONY: all
|
||||
all: compile
|
||||
|
||||
.PHONY: tests
|
||||
tests: eunit ct
|
||||
|
||||
.PHONY: run
|
||||
run: run_setup unlock
|
||||
@rebar3 as test get-deps
|
||||
@rebar3 as test auto --name $(RUN_NODE_NAME) --script scripts/run_dgiot.escript
|
||||
|
||||
.PHONY: run_setup
|
||||
run_setup:
|
||||
@erl -noshell -eval \
|
||||
"{ok, [[HOME]]} = init:get_argument(home), \
|
||||
FilePath = HOME ++ \"/.config/rebar3/rebar.config\", \
|
||||
case file:consult(FilePath) of \
|
||||
{ok, Term} -> \
|
||||
NewTerm = case lists:keyfind(plugins, 1, Term) of \
|
||||
false -> [{plugins, [rebar3_auto]} | Term]; \
|
||||
{plugins, OldPlugins} -> \
|
||||
NewPlugins0 = OldPlugins -- [rebar3_auto], \
|
||||
NewPlugins = [rebar3_auto | NewPlugins0], \
|
||||
lists:keyreplace(plugins, 1, Term, {plugins, NewPlugins}) \
|
||||
end, \
|
||||
ok = file:write_file(FilePath, [io_lib:format(\"~p.\n\", [I]) || I <- NewTerm]); \
|
||||
_Enoent -> \
|
||||
os:cmd(\"mkdir -p ~/.config/rebar3/ \"), \
|
||||
NewTerm=[{plugins, [rebar3_auto]}], \
|
||||
ok = file:write_file(FilePath, [io_lib:format(\"~p.\n\", [I]) || I <- NewTerm]) \
|
||||
end, \
|
||||
halt(0)."
|
||||
|
||||
.PHONY: shell
|
||||
shell:
|
||||
@rebar3 as test auto
|
||||
|
||||
compile: unlock
|
||||
@rebar3 compile
|
||||
|
||||
unlock:
|
||||
@rebar3 unlock
|
||||
|
||||
clean: distclean
|
||||
|
||||
## Cuttlefish escript is built by default when cuttlefish app (as dependency) was built
|
||||
CUTTLEFISH_SCRIPT := _build/default/lib/cuttlefish/cuttlefish
|
||||
|
||||
.PHONY: cover
|
||||
cover:
|
||||
@rebar3 cover
|
||||
|
||||
.PHONY: coveralls
|
||||
coveralls:
|
||||
@rebar3 as test coveralls send
|
||||
|
||||
.PHONY: xref
|
||||
xref:
|
||||
@rebar3 xref
|
||||
|
||||
.PHONY: dialyzer
|
||||
dialyzer:
|
||||
@rebar3 dialyzer
|
||||
|
||||
.PHONY: proper
|
||||
proper:
|
||||
@rebar3 proper -d test/props -c
|
||||
|
||||
.PHONY: deps
|
||||
deps:
|
||||
@rebar3 get-deps
|
||||
|
||||
.PHONY: eunit
|
||||
eunit:
|
||||
@rebar3 eunit -v
|
||||
|
||||
.PHONY: ct_setup
|
||||
ct_setup:
|
||||
rebar3 as test compile
|
||||
@mkdir -p data
|
||||
@if [ ! -f data/loaded_plugins ]; then touch data/loaded_plugins; fi
|
||||
@ln -s -f '../../../../etc' _build/test/lib/emqx/
|
||||
@ln -s -f '../../../../data' _build/test/lib/emqx/
|
||||
|
||||
.PHONY: ct
|
||||
ct: ct_setup
|
||||
@rebar3 ct -v --name $(CT_NODE_NAME) --suite=$(shell echo $(foreach var,$(CT_SUITES),test/$(var)_SUITE) | tr ' ' ',')
|
||||
|
||||
## Run one single CT with rebar3
|
||||
## e.g. make ct-one-suite suite=dgiot_bridge
|
||||
.PHONY: $(SUITES:%=ct-%)
|
||||
$(CT_SUITES:%=ct-%): ct_setup
|
||||
@rebar3 ct -v --readable=false --name $(CT_NODE_NAME) --suite=$(@:ct-%=%)_SUITE --cover
|
||||
|
||||
.PHONY: app.config
|
||||
app.config: $(CUTTLEFISH_SCRIPT) etc/gen.dgiot.conf
|
||||
$(CUTTLEFISH_SCRIPT) -l info -e etc/ -c etc/gen.dgiot.conf -i priv/dgiot.schema -d data/
|
||||
|
||||
$(CUTTLEFISH_SCRIPT):
|
||||
@rebar3 get-deps
|
||||
@if [ ! -f cuttlefish ]; then make -C _build/default/lib/cuttlefish; fi
|
||||
|
||||
bbmustache:
|
||||
@git clone https://hub.fastgit.org/soranoba/bbmustache.git && cd bbmustache && ./rebar3 compile && cd ..
|
||||
|
||||
# This hack is to generate a conf file for testing
|
||||
# relx overlay is used for release
|
||||
etc/gen.dgiot.conf: bbmustache etc/dgiot.conf
|
||||
@erl -noshell -pa bbmustache/_build/default/lib/bbmustache/ebin -eval \
|
||||
"{ok, Temp} = file:read_file('etc/dgiot.conf'), \
|
||||
{ok, Vars0} = file:consult('vars'), \
|
||||
Vars = [{atom_to_list(N), list_to_binary(V)} || {N, V} <- Vars0], \
|
||||
Targ = bbmustache:render(Temp, Vars), \
|
||||
ok = file:write_file('etc/gen.dgiot.conf', Targ), \
|
||||
halt(0)."
|
||||
|
||||
.PHONY: gen-clean
|
||||
gen-clean:
|
||||
@rm -rf bbmustache
|
||||
@rm -f etc/gen.dgiot.conf etc/dgiot.conf.rendered
|
||||
|
||||
.PHONY: distclean
|
||||
distclean: gen-clean
|
||||
@rm -rf Mnesia.*
|
||||
@rm -rf _build cover deps logs log data
|
||||
@rm -f rebar.lock compile_commands.json cuttlefish erl_crash.dump
|
99
apps/dgiot/README-CN.md
Normal file
99
apps/dgiot/README-CN.md
Normal file
@ -0,0 +1,99 @@
|
||||
# EMQ X Broker
|
||||
|
||||
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://hub.fastgit.org/emqx/emqx/releases)
|
||||
[![Build Status](https://travis-ci.org/emqx/dgiot.svg)](https://travis-ci.org/emqx/emqx)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg)](https://coveralls.io/github/emqx/emqx)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
|
||||
[![Slack Invite](<https://slack-invite.dgiot.io/badge.svg>)](https://slack-invite.dgiot.io)
|
||||
[![Twitter](https://img.shields.io/badge/Twitter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt)
|
||||
|
||||
[![最棒的物联网 MQTT 开源团队期待您的加入](https://www.dgiot.io/static/img/github_readme_cn_bg.png)](https://www.dgiot.io/cn/careers)
|
||||
|
||||
[English](./README.md) | 简体中文
|
||||
|
||||
*EMQ X* 是一款完全开源,高度可伸缩,高可用的分布式 MQTT 消息服务器,适用于 IoT、M2M 和移动应用程序,可处理千万级别的并发客户端。
|
||||
|
||||
从 3.0 版本开始,*EMQ X* 完整支持 MQTT V5.0 协议规范,向下兼容 MQTT V3.1 和 V3.1.1,并支持 MQTT-SN、CoAP、LwM2M、WebSocket 和 STOMP 等通信协议。EMQ X 3.0 单集群可支持千万级别的 MQTT 并发连接。
|
||||
|
||||
- 新功能的完整列表,请参阅 [EMQ X Release Notes](https://hub.fastgit.org/emqx/emqx/releases)。
|
||||
- 获取更多信息,请访问 [EMQ X 官网](https://www.dgiot.io/cn/)。
|
||||
|
||||
## 安装
|
||||
|
||||
*EMQ X* 是跨平台的,支持 Linux、Unix、macOS 以及 Windows。这意味着 *EMQ X* 可以部署在 x86_64 架构的服务器上,也可以部署在 Raspberry Pi 这样的 ARM 设备上。
|
||||
|
||||
#### EMQ X Docker 镜像安装
|
||||
|
||||
```
|
||||
docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx
|
||||
```
|
||||
|
||||
#### 二进制软件包安装
|
||||
|
||||
需从 [EMQ X 下载](https://www.dgiot.io/cn/downloads) 页面获取相应操作系统的二进制软件包。
|
||||
|
||||
- [单节点安装文档](https://docs.dgiot.io/broker/latest/cn/getting-started/install.html)
|
||||
- [集群配置文档](https://docs.dgiot.io/broker/latest/cn/advanced/cluster.html)
|
||||
|
||||
## 从源码构建
|
||||
|
||||
3.0 版本开始,构建 *EMQ X* 需要 Erlang/OTP R21+。
|
||||
|
||||
```
|
||||
git clone https://hub.fastgit.org/emqx/emqx-rel.git
|
||||
|
||||
cd emqx-rel && make
|
||||
|
||||
cd _rel/emqx && ./bin/emqx console
|
||||
```
|
||||
|
||||
## 快速入门
|
||||
|
||||
```
|
||||
# Start emqx
|
||||
./bin/emqx start
|
||||
|
||||
# Check Status
|
||||
./bin/dgiot_ctl status
|
||||
|
||||
# Stop emqx
|
||||
./bin/emqx stop
|
||||
```
|
||||
|
||||
*EMQ X* 启动,可以使用浏览器访问 http://localhost:18083 来查看 Dashboard。
|
||||
|
||||
## FAQ
|
||||
|
||||
访问 [EMQ X FAQ](https://docs.dgiot.io/broker/latest/cn/faq/faq.html) 以获取常见问题的帮助。
|
||||
|
||||
## 产品路线
|
||||
|
||||
通过 [EMQ X Roadmap uses Github milestones](https://hub.fastgit.org/emqx/emqx/milestones) 参与跟踪项目进度。
|
||||
|
||||
## 社区、讨论、贡献和支持
|
||||
|
||||
你可通过以下途径与 EMQ 社区及开发者联系:
|
||||
|
||||
- [Slack](https://slack-invite.dgiot.io)
|
||||
- [Twitter](https://twitter.com/emqtt)
|
||||
- [Facebook](https://www.facebook.com/emqxmqtt)
|
||||
- [Reddit](https://www.reddit.com/r/emqx/)
|
||||
- [Forum](https://groups.google.com/d/forum/emqtt)
|
||||
- [Weibo](https://weibo.com/emqtt)
|
||||
- [Blog](https://www.dgiot.io/cn/blog)
|
||||
|
||||
欢迎你将任何 bug、问题和功能请求提交到 [emqx/emqx](https://hub.fastgit.org/emqx/emqx/issues)。
|
||||
|
||||
## MQTT 规范
|
||||
|
||||
你可以通过以下链接了解与查阅 MQTT 协议:
|
||||
|
||||
[MQTT Version 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
|
||||
|
||||
[MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html)
|
||||
|
||||
[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf)
|
||||
|
||||
## 开源许可
|
||||
|
||||
Apache License 2.0, 详见 [LICENSE](./LICENSE)。
|
99
apps/dgiot/README.md
Normal file
99
apps/dgiot/README.md
Normal file
@ -0,0 +1,99 @@
|
||||
# EMQ X Broker
|
||||
|
||||
[![GitHub Release](https://img.shields.io/github/release/emqx/emqx?color=brightgreen)](https://hub.fastgit.org/emqx/emqx/releases)
|
||||
[![Build Status](https://travis-ci.org/emqx/dgiot.svg)](https://travis-ci.org/emqx/emqx)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/emqx/emqx/badge.svg?branch=master)](https://coveralls.io/github/emqx/emqx?branch=master)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/emqx/emqx)](https://hub.docker.com/r/emqx/emqx)
|
||||
[![Slack Invite](<https://slack-invite.dgiot.io/badge.svg>)](https://slack-invite.dgiot.io)
|
||||
[![Twitter](https://img.shields.io/badge/Twitter-EMQ%20X-1DA1F2?logo=twitter)](https://twitter.com/emqtt)
|
||||
|
||||
[![The best IoT MQTT open source team looks forward to your joining](https://www.dgiot.io/static/img/github_readme_en_bg.png)](https://www.dgiot.io/careers)
|
||||
|
||||
English | [简体中文](./README-CN.md)
|
||||
|
||||
*EMQ X* broker is a fully open source, highly scalable, highly available distributed MQTT messaging broker for IoT, M2M and Mobile applications that can handle tens of millions of concurrent clients.
|
||||
|
||||
Starting from 3.0 release, *EMQ X* broker fully supports MQTT V5.0 protocol specifications and backward compatible with MQTT V3.1 and V3.1.1, as well as other communication protocols such as MQTT-SN, CoAP, LwM2M, WebSocket and STOMP. The 3.0 release of the *EMQ X* broker can scaled to 10+ million concurrent MQTT connections on one cluster.
|
||||
|
||||
- For full list of new features, please read [EMQ X Release Notes](https://hub.fastgit.org/emqx/emqx/releases).
|
||||
- For more information, please visit [EMQ X homepage](https://www.dgiot.io).
|
||||
|
||||
## Installation
|
||||
|
||||
The *EMQ X* broker is cross-platform, which supports Linux, Unix, macOS and Windows. It means *EMQ X* can be deployed on x86_64 architecture servers and ARM devices like Raspberry Pi.
|
||||
|
||||
#### Installing via EMQ X Docker Image
|
||||
|
||||
```
|
||||
docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx
|
||||
```
|
||||
|
||||
#### Installing via Binary Package
|
||||
|
||||
Get the binary package of the corresponding OS from [EMQ X Download](https://www.dgiot.io/downloads) page.
|
||||
|
||||
- [Single Node Install](https://docs.dgiot.io/broker/latest/en/getting-started/installation.html)
|
||||
- [Multi Node Install](https://docs.dgiot.io/broker/latest/en/advanced/cluster.html)
|
||||
|
||||
|
||||
## Build From Source
|
||||
|
||||
The *EMQ X* broker requires Erlang/OTP R21+ to build since 3.0 release.
|
||||
|
||||
```
|
||||
git clone -b v4.0.0 https://hub.fastgit.org/emqx/emqx-rel.git
|
||||
|
||||
cd emqx-rel && make
|
||||
|
||||
cd _build/emqx/rel/emqx && ./bin/emqx console
|
||||
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```
|
||||
# Start emqx
|
||||
./bin/emqx start
|
||||
|
||||
# Check Status
|
||||
./bin/dgiot_ctl status
|
||||
|
||||
# Stop emqx
|
||||
./bin/emqx stop
|
||||
```
|
||||
|
||||
To view the dashboard after running, use your browser to open: http://localhost:18083
|
||||
|
||||
## FAQ
|
||||
|
||||
Visiting [EMQ X FAQ](https://docs.dgiot.io/broker/latest/en/faq/faq.html) to get help of common problems.
|
||||
|
||||
## Roadmap
|
||||
|
||||
The [EMQ X Roadmap uses Github milestones](https://hub.fastgit.org/emqx/emqx/milestones) to track the progress of the project.
|
||||
|
||||
## Community, discussion, contribution, and support
|
||||
|
||||
You can reach the EMQ community and developers via the following channels:
|
||||
- [Slack](https://slack-invite.dgiot.io/)
|
||||
- [Twitter](https://twitter.com/emqtt)
|
||||
- [Facebook](https://www.facebook.com/emqxmqtt)
|
||||
- [Reddit](https://www.reddit.com/r/emqx/)
|
||||
- [Forum](https://groups.google.com/d/forum/emqtt)
|
||||
- [Blog](https://medium.com/@emqtt)
|
||||
|
||||
Please submit any bugs, issues, and feature requests to [emqx/emqx](https://hub.fastgit.org/emqx/emqx/issues).
|
||||
|
||||
## MQTT Specifications
|
||||
|
||||
You can read the mqtt protocol via the following links:
|
||||
|
||||
[MQTT Version 3.1.1](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
|
||||
|
||||
[MQTT Version 5.0](https://docs.oasis-open.org/mqtt/mqtt/v5.0/cs02/mqtt-v5.0-cs02.html)
|
||||
|
||||
[MQTT SN](http://mqtt.org/new/wp-content/uploads/2009/06/MQTT-SN_spec_v1.2.pdf)
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0, see [LICENSE](./LICENSE).
|
0
apps/dgiot/etc/dgiot.conf
Normal file
0
apps/dgiot/etc/dgiot.conf
Normal file
80
apps/dgiot/include/dgiot.hrl
Normal file
80
apps/dgiot/include/dgiot.hrl
Normal file
@ -0,0 +1,80 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020 EMQ 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.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-ifndef(DGIOT_HRL).
|
||||
-define(DGIOT_HRL, true).
|
||||
|
||||
-define(APP, dgiot).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Common
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(Otherwise, true).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Banner
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(ERTS_MINIMUM_REQUIRED, "10.0").
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Configs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(NO_PRIORITY_TABLE, none).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Alarm
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-record(alarm, {
|
||||
id :: binary(),
|
||||
severity :: notice | warning | error | critical,
|
||||
title :: iolist(),
|
||||
summary :: iolist(),
|
||||
%% Timestamp (Unit: millisecond)
|
||||
timestamp :: integer() | undefined
|
||||
}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Plugin
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-record(plugin, {
|
||||
name :: atom(),
|
||||
dir :: string() | undefined,
|
||||
descr :: string(),
|
||||
vendor :: string() | undefined,
|
||||
active = false :: boolean(),
|
||||
info = #{} :: map(),
|
||||
type :: atom()
|
||||
}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Command
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-record(command, {
|
||||
name :: atom(),
|
||||
action :: atom(),
|
||||
args = [] :: list(),
|
||||
opts = [] :: list(),
|
||||
usage :: string(),
|
||||
descr :: string()
|
||||
}).
|
||||
|
||||
-endif.
|
26
apps/dgiot/include/types.hrl
Normal file
26
apps/dgiot/include/types.hrl
Normal file
@ -0,0 +1,26 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2019-2021 EMQ 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.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-type(maybe(T) :: undefined | T).
|
||||
|
||||
-type(startlink_ret() :: {ok, pid()} | ignore | {error, term()}).
|
||||
|
||||
-type(ok_or_error(Reason) :: ok | {error, Reason}).
|
||||
|
||||
-type(ok_or_error(Value, Reason) :: {ok, Value} | {error, Reason}).
|
||||
|
||||
-type(mfargs() :: {module(), atom(), [term()]}).
|
||||
|
0
apps/dgiot/priv/dgiot.schema
Normal file
0
apps/dgiot/priv/dgiot.schema
Normal file
43
apps/dgiot/rebar.config
Normal file
43
apps/dgiot/rebar.config
Normal file
@ -0,0 +1,43 @@
|
||||
{minimum_otp_vsn, "21.3"}.
|
||||
|
||||
{plugins, [rebar3_proper]}.
|
||||
|
||||
{deps,
|
||||
[ {emqx, {git, "https://hub.fastgit.org/fastdgiot/emqx", {tag, "v4.3.1"}}}
|
||||
]}.
|
||||
|
||||
{erl_opts, [warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
debug_info,
|
||||
compressed %% for edge
|
||||
]}.
|
||||
|
||||
{overrides, [{add, [{erl_opts, [compressed]}]}]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
|
||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||
locals_not_used, deprecated_function_calls,
|
||||
warnings_as_errors, deprecated_functions
|
||||
]}.
|
||||
|
||||
{cover_enabled, true}.
|
||||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
||||
|
||||
{erl_first_files, ["src/dgiot_logger.erl"]}.
|
||||
|
||||
{profiles,
|
||||
[{test,
|
||||
[{plugins, [{coveralls, {git, "https://hub.fastgit.org/fastdgiot/coveralls-erl", {branch, "github"}}}]},
|
||||
{deps,
|
||||
[{bbmustache, "1.7.0"},
|
||||
{emqtt, {git, "https://hub.fastgit.org/fastdgiot/emqtt", {tag, "1.2.0"}}},
|
||||
{dgiot_ct_helpers, {git, "https://hub.fastgit.org/fastdgiot/emqx-ct-helpers", {tag, "1.3.0"}}}
|
||||
]},
|
||||
{erl_opts, [debug_info]}
|
||||
]}
|
||||
]}.
|
||||
|
20
apps/dgiot/rebar.config.script
Normal file
20
apps/dgiot/rebar.config.script
Normal file
@ -0,0 +1,20 @@
|
||||
%% -*-: erlang -*-
|
||||
|
||||
case {os:getenv("GITHUB_ACTIONS"), os:getenv("GITHUB_TOKEN")} of
|
||||
{"true", Token} when is_list(Token) ->
|
||||
CONFIG1 = [{coveralls_repo_token, Token},
|
||||
{coveralls_service_job_id, os:getenv("GITHUB_RUN_ID")},
|
||||
{coveralls_commit_sha, os:getenv("GITHUB_SHA")},
|
||||
{coveralls_service_number, os:getenv("GITHUB_RUN_NUMBER")},
|
||||
{coveralls_coverdata, "_build/test/cover/*.coverdata"},
|
||||
{coveralls_service_name, "github"} | CONFIG],
|
||||
case os:getenv("GITHUB_EVENT_NAME") =:= "pull_request"
|
||||
andalso string:tokens(os:getenv("GITHUB_REF"), "/") of
|
||||
[_, "pull", PRNO, _] ->
|
||||
[{coveralls_service_pull_request, PRNO} | CONFIG1];
|
||||
_ ->
|
||||
CONFIG1
|
||||
end;
|
||||
_ ->
|
||||
CONFIG
|
||||
end.
|
17
apps/dgiot/scripts/run_dgiot.escript
Normal file
17
apps/dgiot/scripts/run_dgiot.escript
Normal file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env escript
|
||||
|
||||
main(_) ->
|
||||
start().
|
||||
|
||||
start() ->
|
||||
ok = application:load(mnesia),
|
||||
MnesiaName = lists:concat(["Mnesia.", atom_to_list(node())]),
|
||||
MnesiaDir = filename:join(["_build", "data", MnesiaName]),
|
||||
ok = application:set_env(mnesia, dir, MnesiaDir),
|
||||
SpecEmqxConfig = fun(_) -> ok end,
|
||||
start(SpecDgiotConfig).
|
||||
|
||||
start(SpecEmqxConfig) ->
|
||||
SchemaPath = filename:join(["priv", "dgiot.schema"]),
|
||||
ConfPath = filename:join(["etc", "dgiot.conf"]),
|
||||
dgiot_ct_helpers:start_app(dgiot, SchemaPath, ConfPath, SpecDgiotConfig).
|
15
apps/dgiot/src/dgiot.app.src
Normal file
15
apps/dgiot/src/dgiot.app.src
Normal file
@ -0,0 +1,15 @@
|
||||
{application, dgiot,
|
||||
[{description, "dgiot vm"},
|
||||
{id, "dgiot"},
|
||||
{vsn, "git"},
|
||||
{modules, []},
|
||||
{registered, []},
|
||||
{applications, [kernel,stdlib,gproc,gen_rpc, sasl,os_mon]},
|
||||
{mod, {dgiot_app,[]}},
|
||||
{env, []},
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["DGIOT Team "]},
|
||||
{links, [{"Homepage", "https://www.iotn2n.com/"},
|
||||
{"Github", "https://hub.fastgit.org/dgiot/dgiot"}
|
||||
]}
|
||||
]}.
|
34
apps/dgiot/src/dgiot.app.src.script
Normal file
34
apps/dgiot/src/dgiot.app.src.script
Normal file
@ -0,0 +1,34 @@
|
||||
%%-*- mode: erlang -*-
|
||||
%% .app.src.script
|
||||
|
||||
Config = case os:getenv("DGIOT_DESC") of
|
||||
false -> CONFIG; % env var not defined
|
||||
[] -> CONFIG; % env var set to empty string
|
||||
Desc ->
|
||||
[begin
|
||||
AppConf0 = lists:keystore(description, 1, AppConf, {description, Desc}),
|
||||
{application, App, AppConf0}
|
||||
end || Conf = {application, App, AppConf} <- CONFIG]
|
||||
end,
|
||||
|
||||
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("dgiot_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.
|
59
apps/dgiot/src/dgiot_app.erl
Normal file
59
apps/dgiot/src/dgiot_app.erl
Normal file
@ -0,0 +1,59 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020 EMQ 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_app).
|
||||
|
||||
-emqx_plugin(?MODULE).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-export([ start/2
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
-define(APP, dgiot).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Application callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start(_Type, _Args) ->
|
||||
print_banner(),
|
||||
{ok, Sup} = dgiot_sup:start_link(),
|
||||
register(dgiot, self()),
|
||||
print_vsn(),
|
||||
{ok, Sup}.
|
||||
|
||||
-spec(stop(State :: term()) -> term()).
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Print Banner
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
print_banner() ->
|
||||
io:format("Starting ~s on node ~s~n", [?APP, node()]).
|
||||
|
||||
print_vsn() ->
|
||||
{ok, Descr} = application:get_key(description),
|
||||
{ok, Vsn} = application:get_key(vsn),
|
||||
io:format("~s ~s is running now!~n", [Descr, Vsn]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Autocluster
|
||||
%%--------------------------------------------------------------------
|
||||
|
114
apps/dgiot/src/dgiot_sup.erl
Normal file
114
apps/dgiot/src/dgiot_sup.erl
Normal file
@ -0,0 +1,114 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020 EMQ 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_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-include("types.hrl").
|
||||
-include("dgiot.hrl").
|
||||
|
||||
-logger_header("[Supervisor]").
|
||||
|
||||
-export([start_link/0
|
||||
, start_child/1
|
||||
, start_child/2
|
||||
, stop_child/1
|
||||
]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
-type(startchild_ret() :: {ok, supervisor:child()}
|
||||
| {ok, supervisor:child(), term()}
|
||||
| {error, term()}).
|
||||
|
||||
-define(SUP, ?MODULE).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-spec(start_link() -> startlink_ret()).
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SUP}, ?MODULE, []).
|
||||
|
||||
-spec(start_child(supervisor:child_spec()) -> startchild_ret()).
|
||||
start_child(ChildSpec) when is_map(ChildSpec) ->
|
||||
supervisor:start_child(?SUP, ChildSpec).
|
||||
|
||||
-spec(start_child(module(), worker | supervisor) -> startchild_ret()).
|
||||
start_child(Mod, Type) ->
|
||||
start_child(child_spec(Mod, Type)).
|
||||
|
||||
-spec(stop_child(supervisor:child_id()) -> ok | {error, term()}).
|
||||
stop_child(ChildId) ->
|
||||
case supervisor:terminate_child(?SUP, ChildId) of
|
||||
ok -> supervisor:delete_child(?SUP, ChildId);
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Supervisor callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
Childs = get_child_supervisor() ,
|
||||
io:format(" ~p ~n", [Childs]),
|
||||
SupFlags = #{strategy => one_for_all,
|
||||
intensity => 0,
|
||||
period => 1
|
||||
},
|
||||
{ok, {SupFlags, Childs}}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
child_spec(Mod, supervisor) ->
|
||||
#{id => Mod,
|
||||
start => {Mod, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => infinity,
|
||||
type => supervisor,
|
||||
modules => [Mod]
|
||||
};
|
||||
|
||||
child_spec(Mod, worker) ->
|
||||
#{id => Mod,
|
||||
start => {Mod, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 15000,
|
||||
type => worker,
|
||||
modules => [Mod]
|
||||
}.
|
||||
|
||||
get_child_supervisor() ->
|
||||
Specs = lists:foldl(fun({_Name, Mod, [Order | _]}, Acc) ->
|
||||
case lists:member({behaviour, [supervisor]}, Mod:module_info(attributes)) of
|
||||
true ->
|
||||
Acc ++ [{Order, child_spec(Mod, supervisor)}];
|
||||
_ ->
|
||||
case lists:member({behaviour, [worker]}, Mod:module_info(attributes)) of
|
||||
true -> Acc ++ [{Order, child_spec(Mod, supervisor)}];
|
||||
false -> Acc
|
||||
end
|
||||
end
|
||||
end, [], lists:sort(ekka_boot:all_module_attributes(dgiot_supervisor))),
|
||||
lists:foldl(fun({_,Spec}, Acc1) ->
|
||||
Acc1 ++ [Spec]
|
||||
end, [], lists:keysort(1, Specs)).
|
||||
|
||||
|
8
apps/dgiot/vars
Normal file
8
apps/dgiot/vars
Normal file
@ -0,0 +1,8 @@
|
||||
%% vars here are for test only, not intended for release
|
||||
|
||||
{platform_bin_dir, "bin"}.
|
||||
{platform_data_dir, "data"}.
|
||||
{platform_etc_dir, "etc"}.
|
||||
{platform_lib_dir, "lib"}.
|
||||
{platform_log_dir, "log"}.
|
||||
{platform_plugins_dir, "plugins"}.
|
@ -9,6 +9,6 @@
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||
{links, [{"Homepage", "https://emqx.io/"},
|
||||
{"Github", "https://hub.fastgit.org/emqx/emqx-bridge-mqtt"}
|
||||
{"Github", "https://hub.fastgit.org/fastdgiot/emqx-bridge-mqtt"}
|
||||
]}
|
||||
]}.
|
||||
|
25
apps/emqx_coap/.gitignore
vendored
Normal file
25
apps/emqx_coap/.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
deps/
|
||||
ebin/
|
||||
_rel/
|
||||
.erlang.mk/
|
||||
*.d
|
||||
*.o
|
||||
*.exe
|
||||
data/
|
||||
*.iml
|
||||
.idea/
|
||||
logs/
|
||||
*.beam
|
||||
emqx_coap.d
|
||||
intergration_test/emqx-rel/
|
||||
intergration_test/libcoap/
|
||||
intergration_test/case*.txt
|
||||
.DS_Store
|
||||
_build/
|
||||
rebar.lock
|
||||
rebar3.crashdump
|
||||
*.swp
|
||||
erlang.mk
|
||||
.rebar3/
|
||||
etc/emqx_coap.conf.rendered
|
||||
.tags*
|
256
apps/emqx_coap/README.md
Normal file
256
apps/emqx_coap/README.md
Normal file
@ -0,0 +1,256 @@
|
||||
|
||||
# emqx-coap
|
||||
|
||||
emqx-coap is a CoAP Gateway for EMQ X Broker. It translates CoAP messages into MQTT messages and make it possible to communiate between CoAP clients and MQTT clients.
|
||||
|
||||
### Client Usage Example
|
||||
libcoap is an excellent coap library which has a simple client tool. It is recommended to use libcoap as a coap client.
|
||||
|
||||
To compile libcoap, do following steps:
|
||||
|
||||
```
|
||||
git clone http://github.com/obgm/libcoap
|
||||
cd libcoap
|
||||
./autogen.sh
|
||||
./configure --enable-documentation=no --enable-tests=no
|
||||
make
|
||||
```
|
||||
|
||||
### Publish example:
|
||||
```
|
||||
libcoap/examples/coap-client -m put -e 1234 "coap://127.0.0.1/mqtt/topic1?c=client1&u=tom&p=secret"
|
||||
```
|
||||
- topic name is "topic1", NOT "/topic1"
|
||||
- client id is client1
|
||||
- username is tom
|
||||
- password is secret
|
||||
- payload is a text string "1234"
|
||||
|
||||
A mqtt message with topic="topic1", payload="1234" has been published. Any mqtt client or coap client, who has subscribed this topic could receive this message immediately.
|
||||
|
||||
### Subscribe example:
|
||||
|
||||
```
|
||||
libcoap/examples/coap-client -m get -s 10 "coap://127.0.0.1/mqtt/topic1?c=client1&u=tom&p=secret"
|
||||
```
|
||||
- topic name is "topic1", NOT "/topic1"
|
||||
- client id is client1
|
||||
- username is tom
|
||||
- password is secret
|
||||
- subscribe time is 10 seconds
|
||||
|
||||
And you will get following result if any mqtt client or coap client sent message with text "1234567" to "topic1":
|
||||
|
||||
```
|
||||
v:1 t:CON c:GET i:31ae {} [ ]
|
||||
1234567v:1 t:CON c:GET i:31af {} [ Observe:1, Uri-Path:mqtt, Uri-Path:topic1, Uri-Query:c=client1, Uri-Query:u=tom, Uri-Query:p=secret ]
|
||||
```
|
||||
The output message is not well formatted which hide "1234567" at the head of the 2nd line.
|
||||
|
||||
### Configure
|
||||
|
||||
#### Common
|
||||
|
||||
File: etc/emqx_coap.conf
|
||||
|
||||
```properties
|
||||
|
||||
## The UDP port that CoAP is listening on.
|
||||
##
|
||||
## Value: Port
|
||||
coap.port = 5683
|
||||
|
||||
## Interval for keepalive, specified in seconds.
|
||||
##
|
||||
## Value: Duration
|
||||
## -s: seconds
|
||||
## -m: minutes
|
||||
## -h: hours
|
||||
coap.keepalive = 120s
|
||||
|
||||
## Whether to enable statistics for CoAP clients.
|
||||
##
|
||||
## Value: on | off
|
||||
coap.enable_stats = off
|
||||
|
||||
```
|
||||
|
||||
#### DTLS
|
||||
|
||||
emqx_coap enable one-way authentication by default.
|
||||
|
||||
If you want to disable it, comment these lines.
|
||||
|
||||
File: etc/emqx_coap.conf
|
||||
|
||||
```properties
|
||||
|
||||
## The DTLS port that CoAP is listening on.
|
||||
##
|
||||
## Value: Port
|
||||
coap.dtls.port = 5684
|
||||
|
||||
## Private key file for DTLS
|
||||
##
|
||||
## Value: File
|
||||
coap.dtls.keyfile = {{ platform_etc_dir }}/certs/key.pem
|
||||
|
||||
## Server certificate for DTLS.
|
||||
##
|
||||
## Value: File
|
||||
coap.dtls.certfile = {{ platform_etc_dir }}/certs/cert.pem
|
||||
|
||||
```
|
||||
|
||||
##### Enable two-way autentication
|
||||
|
||||
For two-way autentication:
|
||||
|
||||
```properties
|
||||
|
||||
## A server only does x509-path validation in mode verify_peer,
|
||||
## as it then sends a certificate request to the client (this
|
||||
## message is not sent if the verify option is verify_none).
|
||||
## You can then also want to specify option fail_if_no_peer_cert.
|
||||
## More information at: http://erlang.org/doc/man/ssl.html
|
||||
##
|
||||
## Value: verify_peer | verify_none
|
||||
## coap.dtls.verify = verify_peer
|
||||
|
||||
## PEM-encoded CA certificates for DTLS
|
||||
##
|
||||
## Value: File
|
||||
## coap.dtls.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem
|
||||
|
||||
## Used together with {verify, verify_peer} by an SSL server. If set to true,
|
||||
## the server fails if the client does not have a certificate to send, that is,
|
||||
## sends an empty certificate.
|
||||
##
|
||||
## Value: true | false
|
||||
## coap.dtls.fail_if_no_peer_cert = false
|
||||
|
||||
```
|
||||
|
||||
### Load emqx-coap
|
||||
|
||||
```bash
|
||||
./bin/emqx_ctl plugins load emqx_coap
|
||||
```
|
||||
|
||||
CoAP Client Observe Operation (subscribe topic)
|
||||
-----------------------------------------------
|
||||
To subscribe any topic, issue following command:
|
||||
|
||||
```
|
||||
GET coap://localhost/mqtt/{topicname}?c={clientid}&u={username}&p={password} with OBSERVE=0
|
||||
```
|
||||
|
||||
- "mqtt" in the path is mandatory.
|
||||
- replace {topicname}, {clientid}, {username} and {password} with your true values.
|
||||
- {topicname} and {clientid} is mandatory.
|
||||
- if clientid is absent, a "bad_request" will be returned.
|
||||
- {topicname} in URI should be percent-encoded to prevent special characters, such as + and #.
|
||||
- {username} and {password} are optional.
|
||||
- if {username} or {password} is incorrect, the error code `unauthorized` will be returned.
|
||||
- topic is subscribed with qos1.
|
||||
- if the subscription failed due to ACL deny, the error code `forbidden` will be returned.
|
||||
|
||||
CoAP Client Unobserve Operation (unsubscribe topic)
|
||||
---------------------------------------------------
|
||||
To cancel observation, issue following command:
|
||||
|
||||
```
|
||||
GET coap://localhost/mqtt/{topicname}?c={clientid}&u={username}&p={password} with OBSERVE=1
|
||||
```
|
||||
|
||||
- "mqtt" in the path is mandatory.
|
||||
- replace {topicname}, {clientid}, {username} and {password} with your true values.
|
||||
- {topicname} and {clientid} is mandatory.
|
||||
- if clientid is absent, a "bad_request" will be returned.
|
||||
- {topicname} in URI should be percent-encoded to prevent special characters, such as + and #.
|
||||
- {username} and {password} are optional.
|
||||
- if {username} or {password} is incorrect, the error code `unauthorized` will be returned.
|
||||
|
||||
CoAP Client Notification Operation (subscribed Message)
|
||||
-------------------------------------------------------
|
||||
Server will issue an observe-notification as a subscribed message.
|
||||
|
||||
- Its payload is exactly the mqtt payload.
|
||||
- payload data type is "application/octet-stream".
|
||||
|
||||
CoAP Client Publish Operation
|
||||
-----------------------------
|
||||
Issue a coap put command to publish messages. For example:
|
||||
|
||||
```
|
||||
PUT coap://localhost/mqtt/{topicname}?c={clientid}&u={username}&p={password}
|
||||
```
|
||||
|
||||
- "mqtt" in the path is mandatory.
|
||||
- replace {topicname}, {clientid}, {username} and {password} with your true values.
|
||||
- {topicname} and {clientid} is mandatory.
|
||||
- if clientid is absent, a "bad_request" will be returned.
|
||||
- {topicname} in URI should be percent-encoded to prevent special characters, such as + and #.
|
||||
- {username} and {password} are optional.
|
||||
- if {username} or {password} is incorrect, the error code `unauthorized` will be returned.
|
||||
- payload could be any binary data.
|
||||
- payload data type is "application/octet-stream".
|
||||
- publish message will be sent with qos0.
|
||||
- if the publishing failed due to ACL deny, the error code `forbidden` will be returned.
|
||||
|
||||
CoAP Client Keep Alive
|
||||
----------------------
|
||||
Device should issue a get command periodically, serve as a ping to keep mqtt session online.
|
||||
|
||||
```
|
||||
GET coap://localhost/mqtt/{any_topicname}?c={clientid}&u={username}&p={password}
|
||||
```
|
||||
|
||||
- "mqtt" in the path is mandatory.
|
||||
- replace {any_topicname}, {clientid}, {username} and {password} with your true values.
|
||||
- {any_topicname} is optional, and should be percent-encoded to prevent special characters.
|
||||
- {clientid} is mandatory. If clientid is absent, a "bad_request" will be returned.
|
||||
- {username} and {password} are optional.
|
||||
- if {username} or {password} is incorrect, the error code `unauthorized` will be returned.
|
||||
- coap client should do keepalive work periodically to keep mqtt session online, especially those devices in a NAT network.
|
||||
|
||||
|
||||
CoAP Client NOTES
|
||||
-----------------
|
||||
emqx-coap gateway does not accept POST and DELETE requests.
|
||||
|
||||
Topics in URI should be percent-encoded, but corresponding uri_path option has percent-encoding converted. Please refer to RFC 7252 section 6.4, "Decomposing URIs into Options":
|
||||
|
||||
> Note that these rules completely resolve any percent-encoding.
|
||||
|
||||
That implies coap client is responsible to convert any percert-encoding into true character while assembling coap packet.
|
||||
|
||||
|
||||
ClientId, Username, Password and Topic
|
||||
--------------------------------------
|
||||
ClientId/username/password/topic in the coap URI are the concepts in mqtt. That is to say, emqx-coap is trying to fit coap message into mqtt system, by borrowing the client/username/password/topic from mqtt.
|
||||
|
||||
The Auth/ACL/Hook features in mqtt also applies on coap stuff. For example:
|
||||
- If username/password is not authorized, coap client will get an unauthorized error.
|
||||
- If username or clientid is not allowed to published specific topic, coap message will be dropped in fact, although coap client will get an acknoledgement from emqx-coap.
|
||||
- If a coap message is published, a 'message.publish' hook is able to capture this message as well.
|
||||
|
||||
well-known locations
|
||||
--------------------
|
||||
Discovery always return "</mqtt>,</ps>"
|
||||
|
||||
For example
|
||||
```
|
||||
libcoap/examples/coap-client -m get "coap://127.0.0.1/.well-known/core"
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Apache License Version 2.0
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
EMQ X Team.
|
||||
|
13
apps/emqx_coap/TODO
Normal file
13
apps/emqx_coap/TODO
Normal file
@ -0,0 +1,13 @@
|
||||
1. Remove the test/test_mqtt_broker and use emqx-ct-helpers -> Done!
|
||||
- Enhance all test case
|
||||
|
||||
2. Remove the mqtt adaptor
|
||||
3. Remove the emqx_coap_pubsub_topics.erl
|
||||
|
||||
|
||||
### Problems
|
||||
|
||||
1. The coap-client of libcoap does not support Fragment DTLS handshake frame
|
||||
* So, the connection will be established failed, if the 'Server Hello' frame is too big
|
||||
* Why is the 'Server Hello' too big when enable the 'coap.dtls.cacertfile' option?
|
||||
2.
|
BIN
apps/emqx_coap/docs/rfc7049.pdf
Normal file
BIN
apps/emqx_coap/docs/rfc7049.pdf
Normal file
Binary file not shown.
BIN
apps/emqx_coap/docs/rfc7228.pdf
Normal file
BIN
apps/emqx_coap/docs/rfc7228.pdf
Normal file
Binary file not shown.
BIN
apps/emqx_coap/docs/rfc7252.pdf
Normal file
BIN
apps/emqx_coap/docs/rfc7252.pdf
Normal file
Binary file not shown.
82
apps/emqx_coap/etc/emqx_coap.conf
Normal file
82
apps/emqx_coap/etc/emqx_coap.conf
Normal file
@ -0,0 +1,82 @@
|
||||
##--------------------------------------------------------------------
|
||||
## CoAP Gateway
|
||||
##--------------------------------------------------------------------
|
||||
|
||||
## The IP and UDP port that CoAP bind with.
|
||||
##
|
||||
## Default: 0.0.0.0:5683
|
||||
##
|
||||
## Examples:
|
||||
## coap.bind.udp.x = 0.0.0.0:5683 | :::5683 | 127.0.0.1:5683 | ::1:5683
|
||||
##
|
||||
coap.bind.udp.1 = 0.0.0.0:5683
|
||||
##coap.bind.udp.2 = 0.0.0.0:6683
|
||||
|
||||
## Whether to enable statistics for CoAP clients.
|
||||
##
|
||||
## Value: on | off
|
||||
coap.enable_stats = off
|
||||
|
||||
|
||||
##------------------------------------------------------------------------------
|
||||
## DTLS options
|
||||
|
||||
## The DTLS port that CoAP is listening on.
|
||||
##
|
||||
## Default: 0.0.0.0:5684
|
||||
##
|
||||
## Examples:
|
||||
## coap.bind.dtls.x = 0.0.0.0:5684 | :::5684 | 127.0.0.1:5684 | ::1:5684
|
||||
##
|
||||
coap.bind.dtls.1 = 0.0.0.0:5684
|
||||
##coap.bind.dtls.2 = 0.0.0.0:6684
|
||||
|
||||
## A server only does x509-path validation in mode verify_peer,
|
||||
## as it then sends a certificate request to the client (this
|
||||
## message is not sent if the verify option is verify_none).
|
||||
## You can then also want to specify option fail_if_no_peer_cert.
|
||||
## More information at: http://erlang.org/doc/man/ssl.html
|
||||
##
|
||||
## Value: verify_peer | verify_none
|
||||
## coap.dtls.verify = verify_peer
|
||||
|
||||
## Private key file for DTLS
|
||||
##
|
||||
## Value: File
|
||||
coap.dtls.keyfile = {{ platform_etc_dir }}/certs/key.pem
|
||||
|
||||
## Server certificate for DTLS.
|
||||
##
|
||||
## Value: File
|
||||
coap.dtls.certfile = {{ platform_etc_dir }}/certs/cert.pem
|
||||
|
||||
## PEM-encoded CA certificates for DTLS
|
||||
##
|
||||
## Value: File
|
||||
## coap.dtls.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem
|
||||
|
||||
## Used together with {verify, verify_peer} by an SSL server. If set to true,
|
||||
## the server fails if the client does not have a certificate to send, that is,
|
||||
## sends an empty certificate.
|
||||
##
|
||||
## Value: true | false
|
||||
## coap.dtls.fail_if_no_peer_cert = false
|
||||
|
||||
## This is the single most important configuration option of an Erlang SSL
|
||||
## application. Ciphers (and their ordering) define the way the client and
|
||||
## server encrypt information over the wire, from the initial Diffie-Helman
|
||||
## key exchange, the session key encryption ## algorithm and the message
|
||||
## digest algorithm. Selecting a good cipher suite is critical for the
|
||||
## application’s data security, confidentiality and performance.
|
||||
##
|
||||
## The cipher list above offers:
|
||||
##
|
||||
## A good balance between compatibility with older browsers.
|
||||
## It can get stricter for Machine-To-Machine scenarios.
|
||||
## Perfect Forward Secrecy.
|
||||
## No old/insecure encryption and HMAC algorithms
|
||||
##
|
||||
## Most of it was copied from Mozilla’s Server Side TLS article
|
||||
##
|
||||
## Value: Ciphers
|
||||
coap.dtls.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA
|
20
apps/emqx_coap/include/emqx_coap.hrl
Normal file
20
apps/emqx_coap/include/emqx_coap.hrl
Normal file
@ -0,0 +1,20 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(APP, emqx_coap).
|
||||
|
||||
-record(coap_mqtt_auth, {clientid, username, password}).
|
||||
|
129
apps/emqx_coap/intergration_test/Makefile
Normal file
129
apps/emqx_coap/intergration_test/Makefile
Normal file
@ -0,0 +1,129 @@
|
||||
.PHONY: clean, clean_result, start_broker stop_broker case1 case2 case3
|
||||
|
||||
RELX_CONF = emqx-rel/relx.config
|
||||
LIBCOAP_GIT = libcoap/README.md
|
||||
|
||||
all: clean_result $(RELX_CONF) $(LIBCOAP_GIT) start_broker clean_result case1 case2 case3 case4 stop_broker
|
||||
@echo " "
|
||||
@echo " test complete"
|
||||
@echo " "
|
||||
|
||||
clean_result:
|
||||
-rm -f case*.txt
|
||||
|
||||
|
||||
start_broker:
|
||||
-rm -f emqx-rel/_rel/emqx/log/*
|
||||
-emqx-rel/_rel/emqx/bin/emqx stop
|
||||
sleep 1
|
||||
emqx-rel/_rel/emqx/bin/emqx start
|
||||
sleep 1
|
||||
emqx-rel/_rel/emqx/bin/emqx_ctl plugins load emqx_coap
|
||||
|
||||
stop_broker:
|
||||
-emqx-rel/_rel/emqx/bin/emqx stop
|
||||
|
||||
case1:
|
||||
libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/topic1?c=client1&u=tom&p=secret" > case1_output.txt &
|
||||
sleep 1
|
||||
libcoap/examples/coap-client -m put -e w123G45 "coap://127.0.0.1/mqtt/topic1?c=client2&u=mike&p=pw12"
|
||||
sleep 6
|
||||
python check_result.py case1 case1_output.txt==w123G45
|
||||
|
||||
case2:
|
||||
# subscribe to topic="x/y"
|
||||
libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/x%2Fy?c=client3&u=tom&p=secret" > case2_output1.txt &
|
||||
# subscribe to topic="+/z"
|
||||
libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/%2B%2Fz?c=client4&u=mike&p=pw12" > case2_output2.txt &
|
||||
sleep 1
|
||||
# publish to topic="x/y"
|
||||
libcoap/examples/coap-client -m put -e big9wolf "coap://127.0.0.1/mqtt/x%2Fy?c=client5&u=sun&p=pw3"
|
||||
# publish to topic="p/z"
|
||||
libcoap/examples/coap-client -m put -e black2ant "coap://127.0.0.1/mqtt/p%2Fz?c=client5&u=sun&p=pw3"
|
||||
sleep 6
|
||||
python check_result.py case2 case2_output1.txt==big9wolf case2_output1.txt!=black2ant case2_output2.txt!=big9wolf case2_output2.txt==black2ant
|
||||
|
||||
case3:
|
||||
libcoap/examples/coap-client -m get -T tk12 -s 5 "coap://127.0.0.1/mqtt/a%2Fb?c=client3&u=tom&p=secret" > case3_output1.txt &
|
||||
libcoap/examples/coap-client -m get -T tk34 -s 5 "coap://127.0.0.1/mqtt/c%2Fd?c=client3&u=tom&p=secret" > case3_output2.txt &
|
||||
sleep 1
|
||||
libcoap/examples/coap-client -m put -e big9wolf "coap://127.0.0.1/mqtt/c%2Fd?c=client5&u=sun&p=pw3"
|
||||
libcoap/examples/coap-client -m put -e black2ant "coap://127.0.0.1/mqtt/a%2Fb?c=client5&u=sun&p=pw3"
|
||||
sleep 6
|
||||
python check_result.py case3 case3_output1.txt==black2ant case3_output2.txt==big9wolf case3_output2.txt!=black2ant
|
||||
|
||||
|
||||
|
||||
case4:
|
||||
# reload emqx_coap, does it work as expected?
|
||||
sleep 1
|
||||
emqx-rel/_rel/emqx/bin/emqx_ctl plugins unload emqx_coap
|
||||
sleep 1
|
||||
emqx-rel/_rel/emqx/bin/emqx_ctl plugins load emqx_coap
|
||||
sleep 1
|
||||
libcoap/examples/coap-client -m get -s 5 "coap://127.0.0.1/mqtt/topic1?c=client1&u=tom&p=secret" > case4_output.txt &
|
||||
sleep 1
|
||||
libcoap/examples/coap-client -m put -e w6J3G45 "coap://127.0.0.1/mqtt/topic1?c=client2&u=mike&p=pw12"
|
||||
sleep 6
|
||||
python check_result.py case4 case4_output.txt==w6J3G45
|
||||
|
||||
|
||||
|
||||
|
||||
$(RELX_CONF):
|
||||
git clone https://hub.fastgit.org/fastdgiot/emqx-rel.git
|
||||
git clone https://hub.fastgit.org/fastdgiot/emq-coap.git
|
||||
@echo "update emq-coap with this development code"
|
||||
mv emq-coap emqx_coap
|
||||
-rm -rf emqx_coap/etc
|
||||
-rm -rf emqx_coap/include
|
||||
-rm -rf emqx_coap/priv
|
||||
-rm -rf emqx_coap/src
|
||||
-rm -rf emqx_coap/Makefile
|
||||
cp -rf ../etc emqx_coap/
|
||||
cp -rf ../include emqx_coap/
|
||||
cp -rf ../priv emqx_coap/
|
||||
cp -rf ../src emqx_coap/
|
||||
cp -rf ../Makefile emqx_coap/Makefile
|
||||
-mkdir emqx-rel/deps
|
||||
mv emqx_coap emqx-rel/deps/
|
||||
@echo "start building ..."
|
||||
make -C emqx-rel -f Makefile
|
||||
|
||||
|
||||
coap: $(LIBCOAP_GIT)
|
||||
@echo "make coap"
|
||||
|
||||
$(LIBCOAP_GIT):
|
||||
git clone -b v4.1.2 http://github.com/obgm/libcoap
|
||||
cd libcoap && ./autogen.sh && ./configure --enable-documentation=no --enable-tests=no
|
||||
make -C libcoap -f Makefile
|
||||
|
||||
r: rebuild_emq
|
||||
# r short for rebuild_emq
|
||||
@echo " rebuild complete "
|
||||
|
||||
rebuild_emq:
|
||||
-emqx-rel/_rel/emqx/bin/emqx stop
|
||||
-rm -rf emqx-rel/deps/emqx_coap/etc
|
||||
-rm -rf emqx-rel/deps/emqx_coap/include
|
||||
-rm -rf emqx-rel/deps/emqx_coap/priv
|
||||
-rm -rf emqx-rel/deps/emqx_coap/src
|
||||
-rm -rf emqx-rel/deps/emqx_coap/Makefile
|
||||
cp -rf ../etc emqx-rel/deps/emqx_coap/
|
||||
cp -rf ../include emqx-rel/deps/emqx_coap/
|
||||
cp -rf ../priv emqx-rel/deps/emqx_coap/
|
||||
cp -rf ../src emqx-rel/deps/emqx_coap/
|
||||
cp -rf ../Makefile emqx-rel/deps/emqx_coap/Makefile
|
||||
make -C emqx-rel -f Makefile
|
||||
|
||||
clean: clean_result
|
||||
-rm -f client/*.exe
|
||||
-rm -f client/*.o
|
||||
-rm -rf emqx-rel
|
||||
-rm -rf libcoap
|
||||
|
||||
lazy: clean_result start_broker case2 stop_broker
|
||||
# custom your command here
|
||||
@echo "you are so lazy"
|
||||
|
8
apps/emqx_coap/intergration_test/README.md
Normal file
8
apps/emqx_coap/intergration_test/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
Integration test for emq-coap
|
||||
======
|
||||
|
||||
execute following command
|
||||
```
|
||||
make
|
||||
```
|
||||
|
52
apps/emqx_coap/intergration_test/check_result.py
Normal file
52
apps/emqx_coap/intergration_test/check_result.py
Normal file
@ -0,0 +1,52 @@
|
||||
import sys
|
||||
|
||||
|
||||
def have_string(filename, text):
|
||||
data = open(filename, "rb").read()
|
||||
if data.find(text) > 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def mark(case_number, result, description):
|
||||
if result:
|
||||
f = open(case_number+"_PASS.txt", "wb")
|
||||
f.close()
|
||||
print("\n\n"+case_number+" PASS\n\n")
|
||||
else:
|
||||
f = open(case_number+"_FAIL.txt", "wb")
|
||||
f.write(description)
|
||||
f.close()
|
||||
print("\n\n"+case_number+" FAIL\n\n")
|
||||
|
||||
def parse_condition(condition):
|
||||
if condition.find("==") > 0:
|
||||
r = condition.split("==")
|
||||
return r[0], r[1], True
|
||||
elif condition.find("!=") > 0:
|
||||
r = condition.split("!=")
|
||||
return r[0], r[1], False
|
||||
else:
|
||||
print("\ncondition syntax error\n\n\n")
|
||||
sys.exit("condition syntax error")
|
||||
|
||||
|
||||
def main():
|
||||
case_number = sys.argv[1]
|
||||
description = ""
|
||||
conclustion = True
|
||||
for condition in sys.argv[2:]:
|
||||
filename, text, result = parse_condition(condition)
|
||||
if have_string(filename, text) == result:
|
||||
pass
|
||||
else:
|
||||
conclustion = False
|
||||
description = description + "\n" + condition + " failed\n"
|
||||
|
||||
mark(case_number, conclustion, description)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
90
apps/emqx_coap/priv/emqx_coap.schema
Normal file
90
apps/emqx_coap/priv/emqx_coap.schema
Normal file
@ -0,0 +1,90 @@
|
||||
%%-*- mode: erlang -*-
|
||||
%% emqx_coap config mapping
|
||||
{mapping, "coap.bind.udp.$number", "emqx_coap.bind_udp", [
|
||||
{datatype, ip},
|
||||
{default, "0.0.0.0:5683"}
|
||||
]}.
|
||||
|
||||
{mapping, "coap.enable_stats", "emqx_coap.enable_stats", [
|
||||
{datatype, flag}
|
||||
]}.
|
||||
|
||||
{mapping, "coap.bind.dtls.$number", "emqx_coap.bind_dtls", [
|
||||
{datatype, ip},
|
||||
{default, "0.0.0.0:5684"}
|
||||
]}.
|
||||
|
||||
{mapping, "coap.dtls.keyfile", "emqx_coap.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "coap.dtls.certfile", "emqx_coap.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "coap.dtls.verify", "emqx_coap.dtls_opts", [
|
||||
{default, verify_none},
|
||||
{datatype, {enum, [verify_none, verify_peer]}}
|
||||
]}.
|
||||
|
||||
{mapping, "coap.dtls.cacertfile", "emqx_coap.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "coap.dtls.fail_if_no_peer_cert", "emqx_coap.dtls_opts", [
|
||||
{datatype, {enum, [true, false]}}
|
||||
]}.
|
||||
|
||||
{mapping, "coap.dtls.ciphers", "emqx_coap.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_coap.bind_udp", fun(Conf) ->
|
||||
Options = cuttlefish_variable:filter_by_prefix("coap.bind.udp", Conf),
|
||||
lists:map(fun({_, Bind}) ->
|
||||
{Ip, Port} = cuttlefish_datatypes:from_string(Bind, ip),
|
||||
Opts = case inet:parse_address(Ip) of
|
||||
{ok, {_,_,_,_} = Address} ->
|
||||
[inet, {ip, Address}];
|
||||
{ok, {_,_,_,_,_,_,_,_} = Address} ->
|
||||
[inet6, {ip, Address}]
|
||||
end,
|
||||
{Port, Opts}
|
||||
end, Options)
|
||||
end}.
|
||||
|
||||
{translation, "emqx_coap.bind_dtls", fun(Conf) ->
|
||||
Options = cuttlefish_variable:filter_by_prefix("coap.bind.dtls", Conf),
|
||||
lists:map(fun({_, Bind}) ->
|
||||
{Ip, Port} = cuttlefish_datatypes:from_string(Bind, ip),
|
||||
Opts = case inet:parse_address(Ip) of
|
||||
{ok, {_,_,_,_} = Address} ->
|
||||
[inet, {ip, Address}];
|
||||
{ok, {_,_,_,_,_,_,_,_} = Address} ->
|
||||
[inet6, {ip, Address}]
|
||||
end,
|
||||
{Port, Opts}
|
||||
end, Options)
|
||||
end}.
|
||||
|
||||
{translation, "emqx_coap.dtls_opts", fun(Conf) ->
|
||||
Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
|
||||
|
||||
%% Ciphers
|
||||
SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end,
|
||||
Ciphers =
|
||||
case cuttlefish:conf_get("coap.dtls.ciphers", Conf, undefined) of
|
||||
undefined ->
|
||||
lists:append([ssl:cipher_suites(all, V, openssl) || V <- ['dtlsv1.2', 'dtlsv1']]);
|
||||
C ->
|
||||
SplitFun(C)
|
||||
end,
|
||||
|
||||
Filter([{verify, cuttlefish:conf_get("coap.dtls.verify", Conf, undefined)},
|
||||
{keyfile, cuttlefish:conf_get("coap.dtls.keyfile", Conf, undefined)},
|
||||
{certfile, cuttlefish:conf_get("coap.dtls.certfile", Conf, undefined)},
|
||||
{cacertfile, cuttlefish:conf_get("coap.dtls.cacertfile", Conf, undefined)},
|
||||
{fail_if_no_peer_cert, cuttlefish:conf_get("coap.dtls.fail_if_no_peer_cert", Conf, undefined)},
|
||||
{ciphers, Ciphers}])
|
||||
end}.
|
||||
|
4
apps/emqx_coap/rebar.config
Normal file
4
apps/emqx_coap/rebar.config
Normal file
@ -0,0 +1,4 @@
|
||||
{deps,
|
||||
[
|
||||
{gen_coap, {git, "https://hub.fastgit.org/fastdgiot/gen_coap", {tag, "v0.3.2"}}}
|
||||
]}.
|
14
apps/emqx_coap/src/emqx_coap.app.src
Normal file
14
apps/emqx_coap/src/emqx_coap.app.src
Normal file
@ -0,0 +1,14 @@
|
||||
{application, emqx_coap,
|
||||
[{description, "EMQ X CoAP Gateway"},
|
||||
{vsn, "4.3.0"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, []},
|
||||
{applications, [kernel,stdlib,gen_coap]},
|
||||
{mod, {emqx_coap_app, []}},
|
||||
{env, []},
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||
{links, [{"Homepage", "https://emqx.io/"},
|
||||
{"Github", "https://hub.fastgit.org/fastdgiot/emqx-coap"}
|
||||
]}
|
||||
]}.
|
40
apps/emqx_coap/src/emqx_coap_app.erl
Normal file
40
apps/emqx_coap/src/emqx_coap_app.erl
Normal file
@ -0,0 +1,40 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(protocol).
|
||||
|
||||
-include("emqx_coap.hrl").
|
||||
|
||||
-export([ start/2
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
start(_Type, _Args) ->
|
||||
{ok, Sup} = emqx_coap_sup:start_link(),
|
||||
coap_server_registry:add_handler([<<"mqtt">>], emqx_coap_resource, undefined),
|
||||
coap_server_registry:add_handler([<<"ps">>], emqx_coap_pubsub_resource, undefined),
|
||||
_ = emqx_coap_pubsub_topics:start_link(),
|
||||
emqx_coap_server:start(application:get_all_env(?APP)),
|
||||
{ok,Sup}.
|
||||
|
||||
stop(_State) ->
|
||||
coap_server_registry:remove_handler([<<"mqtt">>], emqx_coap_resource, undefined),
|
||||
coap_server_registry:remove_handler([<<"ps">>], emqx_coap_pubsub_resource, undefined),
|
||||
emqx_coap_server:stop(application:get_all_env(?APP)).
|
387
apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl
Normal file
387
apps/emqx_coap/src/emqx_coap_mqtt_adapter.erl
Normal file
@ -0,0 +1,387 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_mqtt_adapter).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("emqx_coap.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
-logger_header("[CoAP-Adpter]").
|
||||
|
||||
%% API.
|
||||
-export([ subscribe/2
|
||||
, unsubscribe/2
|
||||
, publish/3
|
||||
]).
|
||||
|
||||
-export([ client_pid/4
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
-export([ call/2
|
||||
, call/3
|
||||
]).
|
||||
|
||||
%% gen_server.
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
|
||||
-record(state, {peername, clientid, username, password, sub_topics = [], connected_at}).
|
||||
|
||||
-define(ALIVE_INTERVAL, 20000).
|
||||
|
||||
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
|
||||
|
||||
-define(SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0, is_new => false}).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
client_pid(undefined, _Username, _Password, _Channel) ->
|
||||
{error, bad_request};
|
||||
client_pid(ClientId, Username, Password, Channel) ->
|
||||
% check authority
|
||||
case start(ClientId, Username, Password, Channel) of
|
||||
{ok, Pid1} -> {ok, Pid1};
|
||||
{error, {already_started, Pid2}} -> {ok, Pid2};
|
||||
{error, auth_failure} -> {error, auth_failure};
|
||||
Other -> {error, Other}
|
||||
end.
|
||||
|
||||
start(ClientId, Username, Password, Channel) ->
|
||||
% DO NOT use start_link, since multiple coap_reponsder may have relation with one mqtt adapter,
|
||||
% one coap_responder crashes should not make mqtt adapter crash too
|
||||
% And coap_responder is not a system process
|
||||
% it is dangerous to link mqtt adapter to coap_responder
|
||||
gen_server:start({via, emqx_coap_registry, {ClientId, Username, Password}},
|
||||
?MODULE, {ClientId, Username, Password, Channel}, []).
|
||||
|
||||
stop(Pid) ->
|
||||
gen_server:stop(Pid).
|
||||
|
||||
subscribe(Pid, Topic) ->
|
||||
gen_server:call(Pid, {subscribe, Topic, self()}).
|
||||
|
||||
unsubscribe(Pid, Topic) ->
|
||||
gen_server:call(Pid, {unsubscribe, Topic, self()}).
|
||||
|
||||
publish(Pid, Topic, Payload) ->
|
||||
gen_server:call(Pid, {publish, Topic, Payload}).
|
||||
|
||||
%% For emqx_management plugin
|
||||
call(Pid, Msg) ->
|
||||
call(Pid, Msg, infinity).
|
||||
|
||||
call(Pid, Msg, _) ->
|
||||
Pid ! Msg, ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% gen_server Callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init({ClientId, Username, Password, Channel}) ->
|
||||
?LOG(debug, "try to start adapter ClientId=~p, Username=~p, Password=~p, "
|
||||
"Channel=~0p", [ClientId, Username, Password, Channel]),
|
||||
State0 = #state{peername = Channel,
|
||||
clientid = ClientId,
|
||||
username = Username,
|
||||
password = Password},
|
||||
_ = run_hooks('client.connect', [conninfo(State0)], undefined),
|
||||
case emqx_access_control:authenticate(clientinfo(State0)) of
|
||||
{ok, _AuthResult} ->
|
||||
ok = emqx_cm:discard_session(ClientId),
|
||||
|
||||
_ = run_hooks('client.connack', [conninfo(State0), success], undefined),
|
||||
|
||||
State = State0#state{connected_at = erlang:system_time(millisecond)},
|
||||
|
||||
run_hooks('client.connected', [clientinfo(State), conninfo(State)]),
|
||||
|
||||
Self = self(),
|
||||
erlang:send_after(?ALIVE_INTERVAL, Self, check_alive),
|
||||
_ = emqx_cm_locker:trans(ClientId, fun(_) ->
|
||||
emqx_cm:register_channel(ClientId, Self, conninfo(State))
|
||||
end),
|
||||
emqx_cm:insert_channel_info(ClientId, info(State), stats(State)),
|
||||
{ok, State};
|
||||
{error, Reason} ->
|
||||
?LOG(debug, "authentication faild: ~p", [Reason]),
|
||||
_ = run_hooks('client.connack', [conninfo(State0), not_authorized], undefined),
|
||||
{stop, {shutdown, Reason}}
|
||||
end.
|
||||
|
||||
handle_call({subscribe, Topic, CoapPid}, _From, State=#state{sub_topics = TopicList}) ->
|
||||
NewTopics = proplists:delete(Topic, TopicList),
|
||||
IsWild = emqx_topic:wildcard(Topic),
|
||||
{reply, chann_subscribe(Topic, State), State#state{sub_topics =
|
||||
[{Topic, {IsWild, CoapPid}}|NewTopics]}, hibernate};
|
||||
|
||||
handle_call({unsubscribe, Topic, _CoapPid}, _From, State=#state{sub_topics = TopicList}) ->
|
||||
NewTopics = proplists:delete(Topic, TopicList),
|
||||
chann_unsubscribe(Topic, State),
|
||||
{reply, ok, State#state{sub_topics = NewTopics}, hibernate};
|
||||
|
||||
handle_call({publish, Topic, Payload}, _From, State) ->
|
||||
{reply, chann_publish(Topic, Payload, State), State};
|
||||
|
||||
handle_call(info, _From, State) ->
|
||||
{reply, info(State), State};
|
||||
|
||||
handle_call(stats, _From, State) ->
|
||||
{reply, stats(State), State, hibernate};
|
||||
|
||||
handle_call(kick, _From, State) ->
|
||||
{stop, {shutdown, kick}, ok, State};
|
||||
|
||||
handle_call({set_rate_limit, _Rl}, _From, State) ->
|
||||
?LOG(error, "set_rate_limit is not support", []),
|
||||
{reply, ok, State};
|
||||
|
||||
handle_call(get_rate_limit, _From, State) ->
|
||||
?LOG(error, "get_rate_limit is not support", []),
|
||||
{reply, ok, State};
|
||||
|
||||
handle_call(Request, _From, State) ->
|
||||
?LOG(error, "adapter unexpected call ~p", [Request]),
|
||||
{reply, ignored, State, hibernate}.
|
||||
|
||||
handle_cast(Msg, State) ->
|
||||
?LOG(error, "broker_api unexpected cast ~p", [Msg]),
|
||||
{noreply, State, hibernate}.
|
||||
|
||||
handle_info({deliver, _Topic, #message{topic = Topic, payload = Payload}},
|
||||
State = #state{sub_topics = Subscribers}) ->
|
||||
deliver([{Topic, Payload}], Subscribers),
|
||||
{noreply, State, hibernate};
|
||||
|
||||
handle_info(check_alive, State = #state{sub_topics = []}) ->
|
||||
{stop, {shutdown, check_alive}, State};
|
||||
handle_info(check_alive, State) ->
|
||||
erlang:send_after(?ALIVE_INTERVAL, self(), check_alive),
|
||||
{noreply, State, hibernate};
|
||||
|
||||
handle_info({shutdown, Error}, State) ->
|
||||
{stop, {shutdown, Error}, State};
|
||||
|
||||
handle_info({shutdown, conflict, {ClientId, NewPid}}, State) ->
|
||||
?LOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid]),
|
||||
{stop, {shutdown, conflict}, State};
|
||||
|
||||
handle_info(discard, State) ->
|
||||
?LOG(warning, "the connection is discarded. " ++
|
||||
"possibly there is another client with the same clientid", []),
|
||||
{stop, {shutdown, discarded}, State};
|
||||
|
||||
handle_info(kick, State) ->
|
||||
?LOG(info, "Kicked", []),
|
||||
{stop, {shutdown, kick}, State};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?LOG(error, "adapter unexpected info ~p", [Info]),
|
||||
{noreply, State, hibernate}.
|
||||
|
||||
terminate(Reason, State = #state{clientid = ClientId, sub_topics = SubTopics}) ->
|
||||
?LOG(debug, "unsubscribe ~p while exiting for ~p", [SubTopics, Reason]),
|
||||
[chann_unsubscribe(Topic, State) || {Topic, _} <- SubTopics],
|
||||
emqx_cm:unregister_channel(ClientId),
|
||||
|
||||
ConnInfo0 = conninfo(State),
|
||||
ConnInfo = ConnInfo0#{disconnected_at => erlang:system_time(millisecond)},
|
||||
run_hooks('client.disconnected', [clientinfo(State), Reason, ConnInfo]).
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Channel adapter functions
|
||||
|
||||
chann_subscribe(Topic, State = #state{clientid = ClientId}) ->
|
||||
?LOG(debug, "subscribe Topic=~p", [Topic]),
|
||||
case emqx_access_control:check_acl(clientinfo(State), subscribe, Topic) of
|
||||
allow ->
|
||||
emqx_broker:subscribe(Topic, ClientId, ?SUBOPTS),
|
||||
emqx_hooks:run('session.subscribed', [clientinfo(State), Topic, ?SUBOPTS]),
|
||||
ok;
|
||||
deny ->
|
||||
?LOG(warning, "subscribe to ~p by clientid ~p failed due to acl check.",
|
||||
[Topic, ClientId]),
|
||||
{error, forbidden}
|
||||
end.
|
||||
|
||||
chann_unsubscribe(Topic, State) ->
|
||||
?LOG(debug, "unsubscribe Topic=~p", [Topic]),
|
||||
Opts = #{rh => 0, rap => 0, nl => 0, qos => 0},
|
||||
emqx_broker:unsubscribe(Topic),
|
||||
emqx_hooks:run('session.unsubscribed', [clientinfo(State), Topic, Opts]).
|
||||
|
||||
chann_publish(Topic, Payload, State = #state{clientid = ClientId}) ->
|
||||
?LOG(debug, "publish Topic=~p, Payload=~p", [Topic, Payload]),
|
||||
case emqx_access_control:check_acl(clientinfo(State), publish, Topic) of
|
||||
allow ->
|
||||
_ = emqx_broker:publish(
|
||||
emqx_message:set_flag(retain, false,
|
||||
emqx_message:make(ClientId, ?QOS_0, Topic, Payload))),
|
||||
ok;
|
||||
deny ->
|
||||
?LOG(warning, "publish to ~p by clientid ~p failed due to acl check.",
|
||||
[Topic, ClientId]),
|
||||
{error, forbidden}
|
||||
end.
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Deliver
|
||||
|
||||
deliver([], _) -> ok;
|
||||
deliver([Pub | More], Subscribers) ->
|
||||
ok = do_deliver(Pub, Subscribers),
|
||||
deliver(More, Subscribers).
|
||||
|
||||
do_deliver({Topic, Payload}, Subscribers) ->
|
||||
%% handle PUBLISH packet from broker
|
||||
?LOG(debug, "deliver message from broker Topic=~p, Payload=~p", [Topic, Payload]),
|
||||
deliver_to_coap(Topic, Payload, Subscribers),
|
||||
ok.
|
||||
|
||||
deliver_to_coap(_TopicName, _Payload, []) ->
|
||||
ok;
|
||||
deliver_to_coap(TopicName, Payload, [{TopicFilter, {IsWild, CoapPid}}|T]) ->
|
||||
Matched = case IsWild of
|
||||
true -> emqx_topic:match(TopicName, TopicFilter);
|
||||
false -> TopicName =:= TopicFilter
|
||||
end,
|
||||
%?LOG(debug, "deliver_to_coap Matched=~p, CoapPid=~p, TopicName=~p, Payload=~p, T=~p",
|
||||
% [Matched, CoapPid, TopicName, Payload, T]),
|
||||
Matched andalso (CoapPid ! {dispatch, TopicName, Payload}),
|
||||
deliver_to_coap(TopicName, Payload, T).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helper funcs
|
||||
|
||||
-compile({inline, [run_hooks/2, run_hooks/3]}).
|
||||
run_hooks(Name, Args) ->
|
||||
ok = emqx_metrics:inc(Name), emqx_hooks:run(Name, Args).
|
||||
|
||||
run_hooks(Name, Args, Acc) ->
|
||||
ok = emqx_metrics:inc(Name), emqx_hooks:run_fold(Name, Args, Acc).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Info & Stats
|
||||
|
||||
info(State) ->
|
||||
ChannInfo = chann_info(State),
|
||||
ChannInfo#{sockinfo => sockinfo(State)}.
|
||||
|
||||
%% copies from emqx_connection:info/1
|
||||
sockinfo(#state{peername = Peername}) ->
|
||||
#{socktype => udp,
|
||||
peername => Peername,
|
||||
sockname => {{127, 0, 0, 1}, 5683}, %% FIXME: Sock?
|
||||
sockstate => running,
|
||||
active_n => 1
|
||||
}.
|
||||
|
||||
%% copies from emqx_channel:info/1
|
||||
chann_info(State) ->
|
||||
#{conninfo => conninfo(State),
|
||||
conn_state => connected,
|
||||
clientinfo => clientinfo(State),
|
||||
session => maps:from_list(session_info(State)),
|
||||
will_msg => undefined
|
||||
}.
|
||||
|
||||
conninfo(#state{peername = Peername,
|
||||
clientid = ClientId,
|
||||
connected_at = ConnectedAt}) ->
|
||||
#{socktype => udp,
|
||||
sockname => {{127, 0, 0, 1}, 5683},
|
||||
peername => Peername,
|
||||
peercert => nossl, %% TODO: dtls
|
||||
conn_mod => ?MODULE,
|
||||
proto_name => <<"CoAP">>,
|
||||
proto_ver => 1,
|
||||
clean_start => true,
|
||||
clientid => ClientId,
|
||||
username => undefined,
|
||||
conn_props => undefined,
|
||||
connected => true,
|
||||
connected_at => ConnectedAt,
|
||||
keepalive => 0,
|
||||
receive_maximum => 0,
|
||||
expiry_interval => 0
|
||||
}.
|
||||
|
||||
%% copies from emqx_session:info/1
|
||||
session_info(#state{sub_topics = SubTopics, connected_at = ConnectedAt}) ->
|
||||
Subs = lists:foldl(
|
||||
fun({Topic, _}, Acc) ->
|
||||
Acc#{Topic => ?SUBOPTS}
|
||||
end, #{}, SubTopics),
|
||||
[{subscriptions, Subs},
|
||||
{upgrade_qos, false},
|
||||
{retry_interval, 0},
|
||||
{await_rel_timeout, 0},
|
||||
{created_at, ConnectedAt}
|
||||
].
|
||||
|
||||
%% The stats keys copied from emqx_connection:stats/1
|
||||
stats(#state{sub_topics = SubTopics}) ->
|
||||
SockStats = [{recv_oct, 0}, {recv_cnt, 0}, {send_oct, 0}, {send_cnt, 0}, {send_pend, 0}],
|
||||
ConnStats = emqx_pd:get_counters(?CONN_STATS),
|
||||
ChanStats = [{subscriptions_cnt, length(SubTopics)},
|
||||
{subscriptions_max, length(SubTopics)},
|
||||
{inflight_cnt, 0},
|
||||
{inflight_max, 0},
|
||||
{mqueue_len, 0},
|
||||
{mqueue_max, 0},
|
||||
{mqueue_dropped, 0},
|
||||
{next_pkt_id, 0},
|
||||
{awaiting_rel_cnt, 0},
|
||||
{awaiting_rel_max, 0}
|
||||
],
|
||||
ProcStats = emqx_misc:proc_stats(),
|
||||
lists:append([SockStats, ConnStats, ChanStats, ProcStats]).
|
||||
|
||||
clientinfo(#state{peername = {PeerHost, _},
|
||||
clientid = ClientId,
|
||||
username = Username,
|
||||
password = Password}) ->
|
||||
#{zone => undefined,
|
||||
protocol => coap,
|
||||
peerhost => PeerHost,
|
||||
sockport => 5683, %% FIXME:
|
||||
clientid => ClientId,
|
||||
username => Username,
|
||||
password => Password,
|
||||
peercert => nossl,
|
||||
is_bridge => false,
|
||||
is_superuser => false,
|
||||
mountpoint => undefined,
|
||||
ws_cookie => undefined
|
||||
}.
|
||||
|
322
apps/emqx_coap/src/emqx_coap_pubsub_resource.erl
Normal file
322
apps/emqx_coap/src/emqx_coap_pubsub_resource.erl
Normal file
@ -0,0 +1,322 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_pubsub_resource).
|
||||
|
||||
-behaviour(coap_resource).
|
||||
|
||||
-include("emqx_coap.hrl").
|
||||
-include_lib("gen_coap/include/coap.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-logger_header("[CoAP-PS-RES]").
|
||||
|
||||
-export([ coap_discover/2
|
||||
, coap_get/5
|
||||
, coap_post/4
|
||||
, coap_put/4
|
||||
, coap_delete/3
|
||||
, coap_observe/5
|
||||
, coap_unobserve/1
|
||||
, handle_info/2
|
||||
, coap_ack/2
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-export([topic/1]).
|
||||
-endif.
|
||||
|
||||
-define(PS_PREFIX, [<<"ps">>]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Resource Callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
coap_discover(_Prefix, _Args) ->
|
||||
[{absolute, [<<"ps">>], []}].
|
||||
|
||||
coap_get(ChId, ?PS_PREFIX, TopicPath, Query, Content=#coap_content{format = Format}) when TopicPath =/= [] ->
|
||||
Topic = topic(TopicPath),
|
||||
?LOG(debug, "coap_get() Topic=~p, Query=~p~n", [Topic, Query]),
|
||||
#coap_mqtt_auth{clientid = Clientid, username = Usr, password = Passwd} = get_auth(Query),
|
||||
case emqx_coap_mqtt_adapter:client_pid(Clientid, Usr, Passwd, ChId) of
|
||||
{ok, Pid} ->
|
||||
put(mqtt_client_pid, Pid),
|
||||
case Format of
|
||||
<<"application/link-format">> ->
|
||||
Content;
|
||||
_Other ->
|
||||
%% READ the topic info
|
||||
read_last_publish_message(emqx_topic:wildcard(Topic), Topic, Content)
|
||||
end;
|
||||
{error, auth_failure} ->
|
||||
put(mqtt_client_pid, undefined),
|
||||
{error, unauthorized};
|
||||
{error, bad_request} ->
|
||||
put(mqtt_client_pid, undefined),
|
||||
{error, bad_request};
|
||||
{error, _Other} ->
|
||||
put(mqtt_client_pid, undefined),
|
||||
{error, internal_server_error}
|
||||
end;
|
||||
coap_get(ChId, Prefix, TopicPath, Query, _Content) ->
|
||||
?LOG(error, "ignore bad get request ChId=~p, Prefix=~p, TopicPath=~p, Query=~p", [ChId, Prefix, TopicPath, Query]),
|
||||
{error, bad_request}.
|
||||
|
||||
coap_post(_ChId, ?PS_PREFIX, TopicPath, #coap_content{format = Format, payload = Payload, max_age = MaxAge}) when TopicPath =/= [] ->
|
||||
Topic = topic(TopicPath),
|
||||
?LOG(debug, "coap_post() Topic=~p, MaxAge=~p, Format=~p~n", [Topic, MaxAge, Format]),
|
||||
case Format of
|
||||
%% We treat ct of "application/link-format" as CREATE message
|
||||
<<"application/link-format">> ->
|
||||
handle_received_create(Topic, MaxAge, Payload);
|
||||
%% We treat ct of other values as PUBLISH message
|
||||
Other ->
|
||||
?LOG(debug, "coap_post() receive payload format=~p, will process as PUBLISH~n", [Format]),
|
||||
handle_received_publish(Topic, MaxAge, Other, Payload)
|
||||
end;
|
||||
|
||||
coap_post(_ChId, _Prefix, _TopicPath, _Content) ->
|
||||
{error, method_not_allowed}.
|
||||
|
||||
coap_put(_ChId, ?PS_PREFIX, TopicPath, #coap_content{max_age = MaxAge, format = Format, payload = Payload}) when TopicPath =/= [] ->
|
||||
Topic = topic(TopicPath),
|
||||
?LOG(debug, "put message, Topic=~p, Payload=~p~n", [Topic, Payload]),
|
||||
handle_received_publish(Topic, MaxAge, Format, Payload);
|
||||
|
||||
coap_put(_ChId, Prefix, TopicPath, Content) ->
|
||||
?LOG(error, "put has error, Prefix=~p, TopicPath=~p, Content=~p", [Prefix, TopicPath, Content]),
|
||||
{error, bad_request}.
|
||||
|
||||
coap_delete(_ChId, ?PS_PREFIX, TopicPath) ->
|
||||
delete_topic_info(topic(TopicPath));
|
||||
|
||||
coap_delete(_ChId, _Prefix, _TopicPath) ->
|
||||
{error, method_not_allowed}.
|
||||
|
||||
coap_observe(ChId, ?PS_PREFIX, TopicPath, Ack, Content) when TopicPath =/= [] ->
|
||||
Topic = topic(TopicPath),
|
||||
?LOG(debug, "observe Topic=~p, Ack=~p,Content=~p", [Topic, Ack, Content]),
|
||||
Pid = get(mqtt_client_pid),
|
||||
case emqx_coap_mqtt_adapter:subscribe(Pid, Topic) of
|
||||
ok ->
|
||||
Code = case emqx_coap_pubsub_topics:is_topic_timeout(Topic) of
|
||||
true -> nocontent;
|
||||
false-> content
|
||||
end,
|
||||
{ok, {state, ChId, ?PS_PREFIX, [Topic]}, Code, Content};
|
||||
{error, Code} ->
|
||||
{error, Code}
|
||||
end;
|
||||
|
||||
coap_observe(ChId, Prefix, TopicPath, Ack, _Content) ->
|
||||
?LOG(error, "unknown observe request ChId=~p, Prefix=~p, TopicPath=~p, Ack=~p", [ChId, Prefix, TopicPath, Ack]),
|
||||
{error, bad_request}.
|
||||
|
||||
coap_unobserve({state, _ChId, ?PS_PREFIX, TopicPath}) when TopicPath =/= [] ->
|
||||
Topic = topic(TopicPath),
|
||||
?LOG(debug, "unobserve ~p", [Topic]),
|
||||
Pid = get(mqtt_client_pid),
|
||||
emqx_coap_mqtt_adapter:unsubscribe(Pid, Topic),
|
||||
ok;
|
||||
coap_unobserve({state, ChId, Prefix, TopicPath}) ->
|
||||
?LOG(error, "ignore unknown unobserve request ChId=~p, Prefix=~p, TopicPath=~p", [ChId, Prefix, TopicPath]),
|
||||
ok.
|
||||
|
||||
handle_info({dispatch, Topic, Payload}, State) ->
|
||||
?LOG(debug, "dispatch Topic=~p, Payload=~p", [Topic, Payload]),
|
||||
{ok, Ret} = emqx_coap_pubsub_topics:reset_topic_info(Topic, Payload),
|
||||
?LOG(debug, "Updated publish info of topic=~p, the Ret is ~p", [Topic, Ret]),
|
||||
{notify, [], #coap_content{format = <<"application/octet-stream">>, payload = Payload}, State};
|
||||
handle_info(Message, State) ->
|
||||
?LOG(error, "Unknown Message ~p", [Message]),
|
||||
{noreply, State}.
|
||||
|
||||
coap_ack(_Ref, State) -> {ok, State}.
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal Functions
|
||||
%%--------------------------------------------------------------------
|
||||
get_auth(Query) ->
|
||||
get_auth(Query, #coap_mqtt_auth{}).
|
||||
|
||||
get_auth([], Auth=#coap_mqtt_auth{}) ->
|
||||
Auth;
|
||||
get_auth([<<$c, $=, Rest/binary>>|T], Auth=#coap_mqtt_auth{}) ->
|
||||
get_auth(T, Auth#coap_mqtt_auth{clientid = Rest});
|
||||
get_auth([<<$u, $=, Rest/binary>>|T], Auth=#coap_mqtt_auth{}) ->
|
||||
get_auth(T, Auth#coap_mqtt_auth{username = Rest});
|
||||
get_auth([<<$p, $=, Rest/binary>>|T], Auth=#coap_mqtt_auth{}) ->
|
||||
get_auth(T, Auth#coap_mqtt_auth{password = Rest});
|
||||
get_auth([Param|T], Auth=#coap_mqtt_auth{}) ->
|
||||
?LOG(error, "ignore unknown parameter ~p", [Param]),
|
||||
get_auth(T, Auth).
|
||||
|
||||
add_topic_info(publish, Topic, MaxAge, Format, Payload) when is_binary(Topic), Topic =/= <<>> ->
|
||||
case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of
|
||||
[{_, StoredMaxAge, StoredCT, _, _}] ->
|
||||
?LOG(debug, "publish topic=~p already exists, need reset the topic info", [Topic]),
|
||||
%% check whether the ct value stored matches the ct option in this POST message
|
||||
case Format =:= StoredCT of
|
||||
true ->
|
||||
{ok, Ret} =
|
||||
case StoredMaxAge =:= MaxAge of
|
||||
true ->
|
||||
emqx_coap_pubsub_topics:reset_topic_info(Topic, Payload);
|
||||
false ->
|
||||
emqx_coap_pubsub_topics:reset_topic_info(Topic, MaxAge, Payload)
|
||||
end,
|
||||
{changed, Ret};
|
||||
false ->
|
||||
?LOG(debug, "ct values of topic=~p do not match, stored ct=~p, new ct=~p, ignore the PUBLISH", [Topic, StoredCT, Format]),
|
||||
{changed, false}
|
||||
end;
|
||||
[] ->
|
||||
?LOG(debug, "publish topic=~p will be created", [Topic]),
|
||||
{ok, Ret} = emqx_coap_pubsub_topics:add_topic_info(Topic, MaxAge, Format, Payload),
|
||||
{created, Ret}
|
||||
end;
|
||||
|
||||
add_topic_info(create, Topic, MaxAge, Format, _Payload) when is_binary(Topic), Topic =/= <<>> ->
|
||||
case emqx_coap_pubsub_topics:is_topic_existed(Topic) of
|
||||
true ->
|
||||
%% Whether we should support CREATE to an existed topic is TBD!!
|
||||
?LOG(debug, "create topic=~p already exists, need reset the topic info", [Topic]),
|
||||
{ok, Ret} = emqx_coap_pubsub_topics:reset_topic_info(Topic, MaxAge, Format, <<>>);
|
||||
false ->
|
||||
?LOG(debug, "create topic=~p will be created", [Topic]),
|
||||
{ok, Ret} = emqx_coap_pubsub_topics:add_topic_info(Topic, MaxAge, Format, <<>>)
|
||||
end,
|
||||
{created, Ret};
|
||||
|
||||
add_topic_info(_, Topic, _MaxAge, _Format, _Payload) ->
|
||||
?LOG(debug, "create topic=~p info failed", [Topic]),
|
||||
{badarg, false}.
|
||||
|
||||
concatenate_location_path(List = [TopicPart1, TopicPart2, TopicPart3]) when is_binary(TopicPart1), is_binary(TopicPart2), is_binary(TopicPart3) ->
|
||||
list_to_binary(lists:foldl( fun (Element, AccIn) when Element =/= <<>> ->
|
||||
AccIn ++ "/" ++ binary_to_list(Element);
|
||||
(_Element, AccIn) ->
|
||||
AccIn
|
||||
end, [], List)).
|
||||
|
||||
format_string_to_int(<<"application/octet-stream">>) ->
|
||||
<<"42">>;
|
||||
format_string_to_int(<<"application/exi">>) ->
|
||||
<<"47">>;
|
||||
format_string_to_int(<<"application/json">>) ->
|
||||
<<"50">>.
|
||||
|
||||
handle_received_publish(Topic, MaxAge, Format, Payload) ->
|
||||
case add_topic_info(publish, Topic, MaxAge, format_string_to_int(Format), Payload) of
|
||||
{Ret, true} ->
|
||||
Pid = get(mqtt_client_pid),
|
||||
case emqx_coap_mqtt_adapter:publish(Pid, topic(Topic), Payload) of
|
||||
ok ->
|
||||
{ok, Ret, case Ret of
|
||||
changed -> #coap_content{};
|
||||
created ->
|
||||
#coap_content{location_path = [
|
||||
concatenate_location_path([<<"ps">>, Topic, <<>>])]}
|
||||
end};
|
||||
{error, Code} ->
|
||||
{error, Code}
|
||||
end;
|
||||
{_, false} ->
|
||||
?LOG(debug, "add_topic_info failed, will return bad_request", []),
|
||||
{error, bad_request}
|
||||
end.
|
||||
|
||||
handle_received_create(TopicPrefix, MaxAge, Payload) ->
|
||||
case core_link:decode(Payload) of
|
||||
[{rootless, [Topic], [{ct, CT}]}] when is_binary(Topic), Topic =/= <<>> ->
|
||||
TrueTopic = emqx_http_lib:uri_decode(Topic),
|
||||
?LOG(debug, "decoded link-format payload, the Topic=~p, CT=~p~n", [TrueTopic, CT]),
|
||||
LocPath = concatenate_location_path([<<"ps">>, TopicPrefix, TrueTopic]),
|
||||
FullTopic = binary:part(LocPath, 4, byte_size(LocPath)-4),
|
||||
?LOG(debug, "the location path is ~p, the full topic is ~p~n", [LocPath, FullTopic]),
|
||||
case add_topic_info(create, FullTopic, MaxAge, CT, <<>>) of
|
||||
{_, true} ->
|
||||
?LOG(debug, "create topic info successfully, will return LocPath=~p", [LocPath]),
|
||||
{ok, created, #coap_content{location_path = [LocPath]}};
|
||||
{_, false} ->
|
||||
?LOG(debug, "create topic info failed, will return bad_request", []),
|
||||
{error, bad_request}
|
||||
end;
|
||||
Other ->
|
||||
?LOG(debug, "post with bad payload of link-format ~p, will return bad_request", [Other]),
|
||||
{error, bad_request}
|
||||
end.
|
||||
|
||||
%% When topic is timeout, server should return nocontent here,
|
||||
%% but gen_coap only receive return value of #coap_content from coap_get, so temporarily we can't give the Code 2.07 {ok, nocontent} out.TBC!!!
|
||||
return_resource(Topic, Payload, MaxAge, TimeStamp, Content) ->
|
||||
TimeElapsed = trunc((erlang:system_time(millisecond) - TimeStamp) / 1000),
|
||||
case TimeElapsed < MaxAge of
|
||||
true ->
|
||||
LeftTime = (MaxAge - TimeElapsed),
|
||||
?LOG(debug, "topic=~p has max age left time is ~p", [Topic, LeftTime]),
|
||||
Content#coap_content{max_age = LeftTime, payload = Payload};
|
||||
false ->
|
||||
?LOG(debug, "topic=~p has been timeout, will return empty content", [Topic]),
|
||||
#coap_content{}
|
||||
end.
|
||||
|
||||
read_last_publish_message(false, Topic, Content=#coap_content{format = QueryFormat}) when is_binary(QueryFormat)->
|
||||
?LOG(debug, "the QueryFormat=~p", [QueryFormat]),
|
||||
case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[{_, MaxAge, CT, Payload, TimeStamp}] ->
|
||||
case CT =:= format_string_to_int(QueryFormat) of
|
||||
true ->
|
||||
return_resource(Topic, Payload, MaxAge, TimeStamp, Content);
|
||||
false ->
|
||||
?LOG(debug, "format value does not match, the queried format=~p, the stored format=~p", [QueryFormat, CT]),
|
||||
{error, bad_request}
|
||||
end
|
||||
end;
|
||||
|
||||
read_last_publish_message(false, Topic, Content) ->
|
||||
case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[{_, MaxAge, _, Payload, TimeStamp}] ->
|
||||
return_resource(Topic, Payload, MaxAge, TimeStamp, Content)
|
||||
end;
|
||||
|
||||
read_last_publish_message(true, Topic, _Content) ->
|
||||
?LOG(debug, "the topic=~p is illegal wildcard topic", [Topic]),
|
||||
{error, bad_request}.
|
||||
|
||||
delete_topic_info(Topic) ->
|
||||
case emqx_coap_pubsub_topics:lookup_topic_info(Topic) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[{_, _, _, _, _}] ->
|
||||
emqx_coap_pubsub_topics:delete_sub_topics(Topic)
|
||||
end.
|
||||
|
||||
topic(Topic) when is_binary(Topic) -> Topic;
|
||||
topic([]) -> <<>>;
|
||||
topic([Path | TopicPath]) ->
|
||||
case topic(TopicPath) of
|
||||
<<>> -> Path;
|
||||
RemTopic ->
|
||||
<<Path/binary, $/, RemTopic/binary>>
|
||||
end.
|
185
apps/emqx_coap/src/emqx_coap_pubsub_topics.erl
Normal file
185
apps/emqx_coap/src/emqx_coap_pubsub_topics.erl
Normal file
@ -0,0 +1,185 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_pubsub_topics).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("emqx_coap.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-logger_header("[CoAP-PS-TOPICS]").
|
||||
|
||||
-export([ start_link/0
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
-export([ add_topic_info/4
|
||||
, delete_topic_info/1
|
||||
, delete_sub_topics/1
|
||||
, is_topic_existed/1
|
||||
, is_topic_timeout/1
|
||||
, reset_topic_info/2
|
||||
, reset_topic_info/3
|
||||
, reset_topic_info/4
|
||||
, lookup_topic_info/1
|
||||
, lookup_topic_payload/1
|
||||
]).
|
||||
|
||||
%% gen_server.
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
|
||||
-record(state, {}).
|
||||
|
||||
-define(COAP_TOPIC_TABLE, coap_topic).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
stop(Pid) ->
|
||||
gen_server:stop(Pid).
|
||||
|
||||
add_topic_info(Topic, MaxAge, CT, Payload) when is_binary(Topic), is_integer(MaxAge), is_binary(CT), is_binary(Payload) ->
|
||||
gen_server:call(?MODULE, {add_topic, {Topic, MaxAge, CT, Payload}}).
|
||||
|
||||
delete_topic_info(Topic) when is_binary(Topic) ->
|
||||
gen_server:call(?MODULE, {remove_topic, Topic}).
|
||||
|
||||
delete_sub_topics(Topic) when is_binary(Topic) ->
|
||||
gen_server:cast(?MODULE, {remove_sub_topics, Topic}).
|
||||
|
||||
reset_topic_info(Topic, Payload) ->
|
||||
gen_server:call(?MODULE, {reset_topic, {Topic, Payload}}).
|
||||
|
||||
reset_topic_info(Topic, MaxAge, Payload) ->
|
||||
gen_server:call(?MODULE, {reset_topic, {Topic, MaxAge, Payload}}).
|
||||
|
||||
reset_topic_info(Topic, MaxAge, CT, Payload) ->
|
||||
gen_server:call(?MODULE, {reset_topic, {Topic, MaxAge, CT, Payload}}).
|
||||
|
||||
is_topic_existed(Topic) ->
|
||||
ets:member(?COAP_TOPIC_TABLE, Topic).
|
||||
|
||||
is_topic_timeout(Topic) when is_binary(Topic) ->
|
||||
[{Topic, MaxAge, _, _, TimeStamp}] = ets:lookup(?COAP_TOPIC_TABLE, Topic),
|
||||
%% MaxAge: x seconds
|
||||
MaxAge < ((erlang:system_time(millisecond) - TimeStamp) / 1000).
|
||||
|
||||
lookup_topic_info(Topic) ->
|
||||
ets:lookup(?COAP_TOPIC_TABLE, Topic).
|
||||
|
||||
lookup_topic_payload(Topic) ->
|
||||
try ets:lookup_element(?COAP_TOPIC_TABLE, Topic, 4)
|
||||
catch
|
||||
error:badarg -> undefined
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% gen_server callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
_ = ets:new(?COAP_TOPIC_TABLE, [set, named_table, protected]),
|
||||
?LOG(debug, "Create the coap_topic table", []),
|
||||
{ok, #state{}}.
|
||||
|
||||
handle_call({add_topic, {Topic, MaxAge, CT, Payload}}, _From, State) ->
|
||||
Ret = create_table_element(Topic, MaxAge, CT, Payload),
|
||||
{reply, {ok, Ret}, State, hibernate};
|
||||
|
||||
handle_call({reset_topic, {Topic, Payload}}, _From, State) ->
|
||||
Ret = update_table_element(Topic, Payload),
|
||||
{reply, {ok, Ret}, State, hibernate};
|
||||
|
||||
handle_call({reset_topic, {Topic, MaxAge, Payload}}, _From, State) ->
|
||||
Ret = update_table_element(Topic, MaxAge, Payload),
|
||||
{reply, {ok, Ret}, State, hibernate};
|
||||
|
||||
handle_call({reset_topic, {Topic, MaxAge, CT, Payload}}, _From, State) ->
|
||||
Ret = update_table_element(Topic, MaxAge, CT, Payload),
|
||||
{reply, {ok, Ret}, State, hibernate};
|
||||
|
||||
handle_call({remove_topic, {Topic, _Content}}, _From, State) ->
|
||||
ets:delete(?COAP_TOPIC_TABLE, Topic),
|
||||
?LOG(debug, "Remove topic ~p in the coap_topic table", [Topic]),
|
||||
{reply, ok, State, hibernate};
|
||||
|
||||
handle_call(Request, _From, State) ->
|
||||
?LOG(error, "adapter unexpected call ~p", [Request]),
|
||||
{reply, ignored, State, hibernate}.
|
||||
|
||||
handle_cast({remove_sub_topics, TopicPrefix}, State) ->
|
||||
DeletedTopicNum = ets:foldl(fun ({Topic, _, _, _, _}, AccIn) ->
|
||||
case binary:match(Topic, TopicPrefix) =/= nomatch of
|
||||
true ->
|
||||
?LOG(debug, "Remove topic ~p in the coap_topic table", [Topic]),
|
||||
ets:delete(?COAP_TOPIC_TABLE, Topic),
|
||||
AccIn + 1;
|
||||
false ->
|
||||
AccIn
|
||||
end
|
||||
end, 0, ?COAP_TOPIC_TABLE),
|
||||
?LOG(debug, "Remove number of ~p topics with prefix=~p in the coap_topic table", [DeletedTopicNum, TopicPrefix]),
|
||||
{noreply, State, hibernate};
|
||||
|
||||
handle_cast(Msg, State) ->
|
||||
?LOG(error, "broker_api unexpected cast ~p", [Msg]),
|
||||
{noreply, State, hibernate}.
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?LOG(error, "adapter unexpected info ~p", [Info]),
|
||||
{noreply, State, hibernate}.
|
||||
|
||||
terminate(Reason, #state{}) ->
|
||||
ets:delete(?COAP_TOPIC_TABLE),
|
||||
?LOG(error, "the ~p terminate for reason ~p", [?MODULE, Reason]),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal Functions
|
||||
%%--------------------------------------------------------------------
|
||||
create_table_element(Topic, MaxAge, CT, Payload) ->
|
||||
TopicInfo = {Topic, MaxAge, CT, Payload, erlang:system_time(millisecond)},
|
||||
?LOG(debug, "Insert ~p in the coap_topic table", [TopicInfo]),
|
||||
ets:insert_new(?COAP_TOPIC_TABLE, TopicInfo).
|
||||
|
||||
update_table_element(Topic, Payload) ->
|
||||
?LOG(debug, "Update the topic=~p only with Payload", [Topic]),
|
||||
ets:update_element(?COAP_TOPIC_TABLE, Topic, [{4, Payload}, {5, erlang:system_time(millisecond)}]).
|
||||
|
||||
update_table_element(Topic, MaxAge, Payload) ->
|
||||
?LOG(debug, "Update the topic=~p info of MaxAge=~p and Payload", [Topic, MaxAge]),
|
||||
ets:update_element(?COAP_TOPIC_TABLE, Topic, [{2, MaxAge}, {4, Payload}, {5, erlang:system_time(millisecond)}]).
|
||||
|
||||
update_table_element(Topic, MaxAge, CT, <<>>) ->
|
||||
?LOG(debug, "Update the topic=~p info of MaxAge=~p, CT=~p, payload=<<>>", [Topic, MaxAge, CT]),
|
||||
ets:update_element(?COAP_TOPIC_TABLE, Topic, [{2, MaxAge}, {3, CT}, {5, erlang:system_time(millisecond)}]).
|
154
apps/emqx_coap/src/emqx_coap_registry.erl
Normal file
154
apps/emqx_coap/src/emqx_coap_registry.erl
Normal file
@ -0,0 +1,154 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_registry).
|
||||
|
||||
-author("Feng Lee <feng@emqx.io>").
|
||||
|
||||
-include("emqx_coap.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
|
||||
-logger_header("[CoAP-Registry]").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API.
|
||||
-export([ start_link/0
|
||||
, register_name/2
|
||||
, unregister_name/1
|
||||
, whereis_name/1
|
||||
, send/2
|
||||
, stop/0
|
||||
]).
|
||||
|
||||
%% gen_server.
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
|
||||
-record(state, {}).
|
||||
|
||||
-define(RESPONSE_TAB, coap_response_process).
|
||||
-define(RESPONSE_REF_TAB, coap_response_process_ref).
|
||||
|
||||
%% ------------------------------------------------------------------
|
||||
%% API Function Definitions
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
register_name(Name, Pid) ->
|
||||
gen_server:call(?MODULE, {register_name, Name, Pid}).
|
||||
|
||||
unregister_name(Name) ->
|
||||
gen_server:call(?MODULE, {unregister_name, Name}).
|
||||
|
||||
whereis_name(Name) ->
|
||||
case ets:lookup(?RESPONSE_TAB, Name) of
|
||||
[] -> undefined;
|
||||
[{Name, Pid, _MRef}] -> Pid
|
||||
end.
|
||||
|
||||
send(Name, Msg) ->
|
||||
case whereis_name(Name) of
|
||||
undefined ->
|
||||
exit({badarg, {Name, Msg}});
|
||||
Pid when is_pid(Pid) ->
|
||||
Pid ! Msg,
|
||||
Pid
|
||||
end.
|
||||
|
||||
stop() ->
|
||||
gen_server:stop(?MODULE).
|
||||
|
||||
|
||||
%% ------------------------------------------------------------------
|
||||
%% gen_server Function Definitions
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
_ = ets:new(?RESPONSE_TAB, [set, named_table, protected]),
|
||||
_ = ets:new(?RESPONSE_REF_TAB, [set, named_table, protected]),
|
||||
{ok, #state{}}.
|
||||
|
||||
handle_call({register_name, Name, Pid}, _From, State) ->
|
||||
case ets:member(?RESPONSE_TAB, Name) of
|
||||
false ->
|
||||
MRef = monitor_client(Pid),
|
||||
ets:insert(?RESPONSE_TAB, {Name, Pid, MRef}),
|
||||
ets:insert(?RESPONSE_REF_TAB, {MRef, Name, Pid}),
|
||||
{reply, yes, State};
|
||||
true -> {reply, no, State}
|
||||
end;
|
||||
|
||||
handle_call({unregister_name, Name}, _From, State) ->
|
||||
case ets:lookup(?RESPONSE_TAB, Name) of
|
||||
[] ->
|
||||
ok;
|
||||
[{Name, _Pid, MRef}] ->
|
||||
erase_monitor(MRef),
|
||||
ets:delete(?RESPONSE_TAB, Name),
|
||||
ets:delete(?RESPONSE_REF_TAB, MRef)
|
||||
end,
|
||||
{reply, ok, State};
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ignored, State}.
|
||||
|
||||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
|
||||
handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) ->
|
||||
case ets:lookup(?RESPONSE_REF_TAB, MRef) of
|
||||
[{MRef, Name, _Pid}] ->
|
||||
ets:delete(?RESPONSE_TAB, Name),
|
||||
ets:delete(?RESPONSE_REF_TAB, MRef),
|
||||
erase_monitor(MRef);
|
||||
[] ->
|
||||
?LOG(error, "MRef of client ~p not found", [DownPid])
|
||||
end,
|
||||
{noreply, State};
|
||||
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
ets:delete(?RESPONSE_TAB),
|
||||
ets:delete(?RESPONSE_REF_TAB),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
monitor_client(Pid) ->
|
||||
erlang:monitor(process, Pid).
|
||||
|
||||
erase_monitor(MRef) ->
|
||||
catch erlang:demonitor(MRef, [flush]).
|
137
apps/emqx_coap/src/emqx_coap_resource.erl
Normal file
137
apps/emqx_coap/src/emqx_coap_resource.erl
Normal file
@ -0,0 +1,137 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_resource).
|
||||
|
||||
-behaviour(coap_resource).
|
||||
|
||||
-include("emqx_coap.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("gen_coap/include/coap.hrl").
|
||||
|
||||
-logger_header("[CoAP-RES]").
|
||||
|
||||
-export([ coap_discover/2
|
||||
, coap_get/5
|
||||
, coap_post/4
|
||||
, coap_put/4
|
||||
, coap_delete/3
|
||||
, coap_observe/5
|
||||
, coap_unobserve/1
|
||||
, handle_info/2
|
||||
, coap_ack/2
|
||||
]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-export([topic/1]).
|
||||
-endif.
|
||||
|
||||
-define(MQTT_PREFIX, [<<"mqtt">>]).
|
||||
|
||||
% resource operations
|
||||
coap_discover(_Prefix, _Args) ->
|
||||
[{absolute, [<<"mqtt">>], []}].
|
||||
|
||||
coap_get(ChId, ?MQTT_PREFIX, Path, Query, _Content) ->
|
||||
?LOG(debug, "coap_get() Path=~p, Query=~p~n", [Path, Query]),
|
||||
#coap_mqtt_auth{clientid = Clientid, username = Usr, password = Passwd} = get_auth(Query),
|
||||
case emqx_coap_mqtt_adapter:client_pid(Clientid, Usr, Passwd, ChId) of
|
||||
{ok, Pid} ->
|
||||
put(mqtt_client_pid, Pid),
|
||||
#coap_content{};
|
||||
{error, auth_failure} ->
|
||||
put(mqtt_client_pid, undefined),
|
||||
{error, unauthorized};
|
||||
{error, bad_request} ->
|
||||
put(mqtt_client_pid, undefined),
|
||||
{error, bad_request};
|
||||
{error, _Other} ->
|
||||
put(mqtt_client_pid, undefined),
|
||||
{error, internal_server_error}
|
||||
end;
|
||||
coap_get(ChId, Prefix, Path, Query, _Content) ->
|
||||
?LOG(error, "ignore bad get request ChId=~p, Prefix=~p, Path=~p, Query=~p", [ChId, Prefix, Path, Query]),
|
||||
{error, bad_request}.
|
||||
|
||||
coap_post(_ChId, _Prefix, _Topic, _Content) ->
|
||||
{error, method_not_allowed}.
|
||||
|
||||
coap_put(_ChId, ?MQTT_PREFIX, Topic, #coap_content{payload = Payload}) when Topic =/= [] ->
|
||||
?LOG(debug, "put message, Topic=~p, Payload=~p~n", [Topic, Payload]),
|
||||
Pid = get(mqtt_client_pid),
|
||||
emqx_coap_mqtt_adapter:publish(Pid, topic(Topic), Payload);
|
||||
coap_put(_ChId, Prefix, Topic, Content) ->
|
||||
?LOG(error, "put has error, Prefix=~p, Topic=~p, Content=~p", [Prefix, Topic, Content]),
|
||||
{error, bad_request}.
|
||||
|
||||
coap_delete(_ChId, _Prefix, _Topic) ->
|
||||
{error, method_not_allowed}.
|
||||
|
||||
coap_observe(ChId, ?MQTT_PREFIX, Topic, Ack, Content) when Topic =/= [] ->
|
||||
TrueTopic = topic(Topic),
|
||||
?LOG(debug, "observe Topic=~p, Ack=~p", [TrueTopic, Ack]),
|
||||
Pid = get(mqtt_client_pid),
|
||||
case emqx_coap_mqtt_adapter:subscribe(Pid, TrueTopic) of
|
||||
ok -> {ok, {state, ChId, ?MQTT_PREFIX, [TrueTopic]}, content, Content};
|
||||
{error, Code} -> {error, Code}
|
||||
end;
|
||||
coap_observe(ChId, Prefix, Topic, Ack, _Content) ->
|
||||
?LOG(error, "unknown observe request ChId=~p, Prefix=~p, Topic=~p, Ack=~p", [ChId, Prefix, Topic, Ack]),
|
||||
{error, bad_request}.
|
||||
|
||||
coap_unobserve({state, _ChId, ?MQTT_PREFIX, Topic}) when Topic =/= [] ->
|
||||
?LOG(debug, "unobserve ~p", [Topic]),
|
||||
Pid = get(mqtt_client_pid),
|
||||
emqx_coap_mqtt_adapter:unsubscribe(Pid, topic(Topic)),
|
||||
ok;
|
||||
coap_unobserve({state, ChId, Prefix, Topic}) ->
|
||||
?LOG(error, "ignore unknown unobserve request ChId=~p, Prefix=~p, Topic=~p", [ChId, Prefix, Topic]),
|
||||
ok.
|
||||
|
||||
handle_info({dispatch, Topic, Payload}, State) ->
|
||||
?LOG(debug, "dispatch Topic=~p, Payload=~p", [Topic, Payload]),
|
||||
{notify, [], #coap_content{format = <<"application/octet-stream">>, payload = Payload}, State};
|
||||
handle_info(Message, State) ->
|
||||
emqx_coap_mqtt_adapter:handle_info(Message, State).
|
||||
|
||||
coap_ack(_Ref, State) -> {ok, State}.
|
||||
|
||||
get_auth(Query) ->
|
||||
get_auth(Query, #coap_mqtt_auth{}).
|
||||
|
||||
get_auth([], Auth=#coap_mqtt_auth{}) ->
|
||||
Auth;
|
||||
get_auth([<<$c, $=, Rest/binary>>|T], Auth=#coap_mqtt_auth{}) ->
|
||||
get_auth(T, Auth#coap_mqtt_auth{clientid = Rest});
|
||||
get_auth([<<$u, $=, Rest/binary>>|T], Auth=#coap_mqtt_auth{}) ->
|
||||
get_auth(T, Auth#coap_mqtt_auth{username = Rest});
|
||||
get_auth([<<$p, $=, Rest/binary>>|T], Auth=#coap_mqtt_auth{}) ->
|
||||
get_auth(T, Auth#coap_mqtt_auth{password = Rest});
|
||||
get_auth([Param|T], Auth=#coap_mqtt_auth{}) ->
|
||||
?LOG(error, "ignore unknown parameter ~p", [Param]),
|
||||
get_auth(T, Auth).
|
||||
|
||||
topic(Topic) when is_binary(Topic) -> Topic;
|
||||
topic([]) -> <<>>;
|
||||
topic([Path | TopicPath]) ->
|
||||
case topic(TopicPath) of
|
||||
<<>> -> Path;
|
||||
RemTopic ->
|
||||
<<Path/binary, $/, RemTopic/binary>>
|
||||
end.
|
||||
|
106
apps/emqx_coap/src/emqx_coap_server.erl
Normal file
106
apps/emqx_coap/src/emqx_coap_server.erl
Normal file
@ -0,0 +1,106 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_server).
|
||||
|
||||
-include("emqx_coap.hrl").
|
||||
|
||||
-export([ start/1
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
-export([ start_listener/1
|
||||
, start_listener/3
|
||||
, stop_listener/1
|
||||
, stop_listener/2
|
||||
]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start(Envs) ->
|
||||
{ok, _} = application:ensure_all_started(gen_coap),
|
||||
start_listeners(Envs).
|
||||
|
||||
stop(Envs) ->
|
||||
stop_listeners(Envs).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal funcs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start_listeners(Envs) ->
|
||||
lists:foreach(fun start_listener/1, listeners_confs(Envs)).
|
||||
|
||||
stop_listeners(Envs) ->
|
||||
lists:foreach(fun stop_listener/1, listeners_confs(Envs)).
|
||||
|
||||
start_listener({Proto, ListenOn, Opts}) ->
|
||||
case start_listener(Proto, ListenOn, Opts) of
|
||||
{ok, _Pid} ->
|
||||
io:format("Start coap:~s listener on ~s successfully.~n",
|
||||
[Proto, format(ListenOn)]);
|
||||
{error, Reason} ->
|
||||
io:format(standard_error, "Failed to start coap:~s listener on ~s: ~0p~n",
|
||||
[Proto, format(ListenOn), Reason]),
|
||||
error(Reason)
|
||||
end.
|
||||
|
||||
start_listener(udp, ListenOn, Opts) ->
|
||||
coap_server:start_udp('coap:udp', ListenOn, Opts);
|
||||
start_listener(dtls, ListenOn, Opts) ->
|
||||
coap_server:start_dtls('coap:dtls', ListenOn, Opts).
|
||||
|
||||
stop_listener({Proto, ListenOn, _Opts}) ->
|
||||
Ret = stop_listener(Proto, ListenOn),
|
||||
case Ret of
|
||||
ok -> io:format("Stop coap:~s listener on ~s successfully.~n",
|
||||
[Proto, format(ListenOn)]);
|
||||
{error, Reason} ->
|
||||
io:format(standard_error, "Failed to stop coap:~s listener on ~s: ~0p~n.",
|
||||
[Proto, format(ListenOn), Reason])
|
||||
end,
|
||||
Ret.
|
||||
|
||||
stop_listener(udp, ListenOn) ->
|
||||
coap_server:stop_udp('coap:udp', ListenOn);
|
||||
stop_listener(dtls, ListenOn) ->
|
||||
coap_server:stop_dtls('coap:dtls', ListenOn).
|
||||
|
||||
%% XXX: It is a temporary func to convert conf format for esockd
|
||||
listeners_confs(Envs) ->
|
||||
listeners_confs(udp, Envs) ++ listeners_confs(dtls, Envs).
|
||||
|
||||
listeners_confs(udp, Envs) ->
|
||||
Udps = proplists:get_value(bind_udp, Envs, []),
|
||||
[{udp, Port, [{udp_options, InetOpts}]} || {Port, InetOpts} <- Udps];
|
||||
|
||||
listeners_confs(dtls, Envs) ->
|
||||
case proplists:get_value(dtls_opts, Envs, []) of
|
||||
[] -> [];
|
||||
DtlsOpts ->
|
||||
BindDtls = proplists:get_value(bind_dtls, Envs, []),
|
||||
[{dtls, Port, [{dtls_options, InetOpts ++ DtlsOpts}]} || {Port, InetOpts} <- BindDtls]
|
||||
end.
|
||||
|
||||
format(Port) when is_integer(Port) ->
|
||||
io_lib:format("0.0.0.0:~w", [Port]);
|
||||
format({Addr, Port}) when is_list(Addr) ->
|
||||
io_lib:format("~s:~w", [Addr, Port]);
|
||||
format({Addr, Port}) when is_tuple(Addr) ->
|
||||
io_lib:format("~s:~w", [inet:ntoa(Addr), Port]).
|
||||
|
42
apps/emqx_coap/src/emqx_coap_sup.erl
Normal file
42
apps/emqx_coap/src/emqx_coap_sup.erl
Normal file
@ -0,0 +1,42 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([ start_link/0
|
||||
, init/1
|
||||
]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init(_Args) ->
|
||||
Registry = #{id => emqx_coap_registry,
|
||||
start => {emqx_coap_registry, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 5000,
|
||||
type => worker,
|
||||
modules => [emqx_coap_registry]},
|
||||
PsTopics = #{id => emqx_coap_pubsub_topics,
|
||||
start => {emqx_coap_pubsub_topics, start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 5000,
|
||||
type => worker,
|
||||
modules => [emqx_coap_pubsub_topics]},
|
||||
{ok, {{one_for_all, 10, 3600}, [Registry, PsTopics]}}.
|
||||
|
59
apps/emqx_coap/src/emqx_coap_timer.erl
Normal file
59
apps/emqx_coap/src/emqx_coap_timer.erl
Normal file
@ -0,0 +1,59 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_timer).
|
||||
|
||||
-include("emqx_coap.hrl").
|
||||
|
||||
-export([ cancel_timer/1
|
||||
, start_timer/2
|
||||
, restart_timer/1
|
||||
, kick_timer/1
|
||||
, is_timeout/1
|
||||
, get_timer_length/1
|
||||
]).
|
||||
|
||||
-record(timer_state, {interval, kickme, tref, message}).
|
||||
|
||||
-define(LOG(Level, Format, Args),
|
||||
emqx_logger:Level("CoAP-Timer: " ++ Format, Args)).
|
||||
|
||||
cancel_timer(#timer_state{tref = TRef}) when is_reference(TRef) ->
|
||||
catch erlang:cancel_timer(TRef),
|
||||
ok;
|
||||
cancel_timer(_) ->
|
||||
ok.
|
||||
|
||||
kick_timer(State=#timer_state{kickme = false}) ->
|
||||
State#timer_state{kickme = true};
|
||||
kick_timer(State=#timer_state{kickme = true}) ->
|
||||
State.
|
||||
|
||||
start_timer(Sec, Msg) ->
|
||||
?LOG(debug, "emqx_coap_timer:start_timer ~p", [Sec]),
|
||||
TRef = erlang:send_after(timer:seconds(Sec), self(), Msg),
|
||||
#timer_state{interval = Sec, kickme = false, tref = TRef, message = Msg}.
|
||||
|
||||
restart_timer(State=#timer_state{interval = Sec, message = Msg}) ->
|
||||
?LOG(debug, "emqx_coap_timer:restart_timer ~p", [Sec]),
|
||||
TRef = erlang:send_after(timer:seconds(Sec), self(), Msg),
|
||||
State#timer_state{kickme = false, tref = TRef}.
|
||||
|
||||
is_timeout(#timer_state{kickme = Bool}) ->
|
||||
not Bool.
|
||||
|
||||
get_timer_length(#timer_state{interval = Interval}) ->
|
||||
Interval.
|
311
apps/emqx_coap/test/emqx_coap_SUITE.erl
Normal file
311
apps/emqx_coap/test/emqx_coap_SUITE.erl
Normal file
@ -0,0 +1,311 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("gen_coap/include/coap.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
|
||||
-define(LOGT(Format, Args), ct:pal(Format, Args)).
|
||||
|
||||
all() -> emqx_ct:all(?MODULE).
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_ct_helpers:start_apps([emqx_coap], fun set_sepecial_cfg/1),
|
||||
Config.
|
||||
|
||||
set_sepecial_cfg(emqx_coap) ->
|
||||
Opts = application:get_env(emqx_coap, dtls_opts,[]),
|
||||
Opts2 = [{keyfile, emqx_ct_helpers:deps_path(emqx, "etc/certs/key.pem")},
|
||||
{certfile, emqx_ct_helpers:deps_path(emqx, "etc/certs/cert.pem")}],
|
||||
application:set_env(emqx_coap, dtls_opts, emqx_misc:merge_opts(Opts, Opts2)),
|
||||
application:set_env(emqx_coap, enable_stats, true);
|
||||
set_sepecial_cfg(_) ->
|
||||
ok.
|
||||
|
||||
end_per_suite(Config) ->
|
||||
emqx_ct_helpers:stop_apps([emqx_coap]),
|
||||
Config.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Test Cases
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
t_publish(_Config) ->
|
||||
Topic = <<"abc">>, Payload = <<"123">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
URI = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
|
||||
%% Sub topic first
|
||||
emqx:subscribe(Topic),
|
||||
|
||||
Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
{ok, changed, _} = Reply,
|
||||
|
||||
receive
|
||||
{deliver, Topic, Msg} ->
|
||||
?assertEqual(Topic, Msg#message.topic),
|
||||
?assertEqual(Payload, Msg#message.payload)
|
||||
after
|
||||
500 ->
|
||||
?assert(false)
|
||||
end.
|
||||
|
||||
t_publish_acl_deny(_Config) ->
|
||||
Topic = <<"abc">>, Payload = <<"123">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
URI = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
|
||||
%% Sub topic first
|
||||
emqx:subscribe(Topic),
|
||||
|
||||
ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]),
|
||||
ok = meck:expect(emqx_access_control, check_acl, 3, deny),
|
||||
Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?assertEqual({error,forbidden}, Reply),
|
||||
ok = meck:unload(emqx_access_control),
|
||||
receive
|
||||
{deliver, Topic, Msg} -> ct:fail({unexpected, {Topic, Msg}})
|
||||
after
|
||||
500 -> ok
|
||||
end.
|
||||
|
||||
t_observe(_Config) ->
|
||||
Topic = <<"abc">>, TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"123">>,
|
||||
Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
{ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri),
|
||||
?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]),
|
||||
|
||||
[SubPid] = emqx:subscribers(Topic),
|
||||
?assert(is_pid(SubPid)),
|
||||
|
||||
%% Publish a message
|
||||
emqx:publish(emqx_message:make(Topic, Payload)),
|
||||
|
||||
Notif = receive_notification(),
|
||||
?LOGT("observer get Notif=~p", [Notif]),
|
||||
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif,
|
||||
?assertEqual(Payload, PayloadRecv),
|
||||
|
||||
er_coap_observer:stop(Pid),
|
||||
timer:sleep(100),
|
||||
|
||||
[] = emqx:subscribers(Topic).
|
||||
|
||||
t_observe_acl_deny(_Config) ->
|
||||
Topic = <<"abc">>, TopicStr = binary_to_list(Topic),
|
||||
Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
ok = meck:new(emqx_access_control, [non_strict, passthrough, no_history]),
|
||||
ok = meck:expect(emqx_access_control, check_acl, 3, deny),
|
||||
?assertEqual({error,forbidden}, er_coap_observer:observe(Uri)),
|
||||
[] = emqx:subscribers(Topic),
|
||||
ok = meck:unload(emqx_access_control).
|
||||
|
||||
t_observe_wildcard(_Config) ->
|
||||
Topic = <<"+/b">>, TopicStr = http_uri:encode(binary_to_list(Topic)),
|
||||
Payload = <<"123">>,
|
||||
Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
{ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri),
|
||||
?LOGT("observer Uri=~p, Pid=~p, N=~p, Code=~p, Content=~p", [Uri, Pid, N, Code, Content]),
|
||||
|
||||
[SubPid] = emqx:subscribers(Topic),
|
||||
?assert(is_pid(SubPid)),
|
||||
|
||||
%% Publish a message
|
||||
emqx:publish(emqx_message:make(<<"a/b">>, Payload)),
|
||||
|
||||
Notif = receive_notification(),
|
||||
?LOGT("observer get Notif=~p", [Notif]),
|
||||
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif,
|
||||
?assertEqual(Payload, PayloadRecv),
|
||||
|
||||
er_coap_observer:stop(Pid),
|
||||
timer:sleep(100),
|
||||
|
||||
[] = emqx:subscribers(Topic).
|
||||
|
||||
t_observe_pub(_Config) ->
|
||||
Topic = <<"+/b">>, TopicStr = http_uri:encode(binary_to_list(Topic)),
|
||||
Uri = "coap://127.0.0.1/mqtt/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
{ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri),
|
||||
?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]),
|
||||
|
||||
[SubPid] = emqx:subscribers(Topic),
|
||||
?assert(is_pid(SubPid)),
|
||||
|
||||
Topic2 = <<"a/b">>, Payload2 = <<"UFO">>,
|
||||
TopicStr2 = http_uri:encode(binary_to_list(Topic2)),
|
||||
URI2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret",
|
||||
|
||||
Reply2 = er_coap_client:request(put, URI2, #coap_content{format = <<"application/octet-stream">>, payload = Payload2}),
|
||||
{ok,changed, _} = Reply2,
|
||||
|
||||
Notif2 = receive_notification(),
|
||||
?LOGT("observer get Notif2=~p", [Notif2]),
|
||||
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv2}} = Notif2,
|
||||
?assertEqual(Payload2, PayloadRecv2),
|
||||
|
||||
Topic3 = <<"j/b">>, Payload3 = <<"ET629">>,
|
||||
TopicStr3 = http_uri:encode(binary_to_list(Topic3)),
|
||||
URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?c=client2&u=mike&p=guess",
|
||||
Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
|
||||
{ok,changed, _} = Reply3,
|
||||
|
||||
Notif3 = receive_notification(),
|
||||
?LOGT("observer get Notif3=~p", [Notif3]),
|
||||
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv3}} = Notif3,
|
||||
?assertEqual(Payload3, PayloadRecv3),
|
||||
|
||||
er_coap_observer:stop(Pid).
|
||||
|
||||
t_one_clientid_sub_2_topics(_Config) ->
|
||||
Topic1 = <<"abc">>, TopicStr1 = binary_to_list(Topic1),
|
||||
Payload1 = <<"123">>,
|
||||
Uri1 = "coap://127.0.0.1/mqtt/"++TopicStr1++"?c=client1&u=tom&p=secret",
|
||||
{ok, Pid1, N1, Code1, Content1} = er_coap_observer:observe(Uri1),
|
||||
?LOGT("observer 1 Pid=~p, N=~p, Code=~p, Content=~p", [Pid1, N1, Code1, Content1]),
|
||||
|
||||
[SubPid] = emqx:subscribers(Topic1),
|
||||
?assert(is_pid(SubPid)),
|
||||
|
||||
Topic2 = <<"x/y">>, TopicStr2 = http_uri:encode(binary_to_list(Topic2)),
|
||||
Payload2 = <<"456">>,
|
||||
Uri2 = "coap://127.0.0.1/mqtt/"++TopicStr2++"?c=client1&u=tom&p=secret",
|
||||
{ok, Pid2, N2, Code2, Content2} = er_coap_observer:observe(Uri2),
|
||||
?LOGT("observer 2 Pid=~p, N=~p, Code=~p, Content=~p", [Pid2, N2, Code2, Content2]),
|
||||
|
||||
[SubPid] = emqx:subscribers(Topic2),
|
||||
?assert(is_pid(SubPid)),
|
||||
|
||||
emqx:publish(emqx_message:make(Topic1, Payload1)),
|
||||
|
||||
Notif1 = receive_notification(),
|
||||
?LOGT("observer 1 get Notif=~p", [Notif1]),
|
||||
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv1}} = Notif1,
|
||||
?assertEqual(Payload1, PayloadRecv1),
|
||||
|
||||
emqx:publish(emqx_message:make(Topic2, Payload2)),
|
||||
|
||||
Notif2 = receive_notification(),
|
||||
?LOGT("observer 2 get Notif=~p", [Notif2]),
|
||||
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv2}} = Notif2,
|
||||
?assertEqual(Payload2, PayloadRecv2),
|
||||
|
||||
er_coap_observer:stop(Pid1),
|
||||
er_coap_observer:stop(Pid2).
|
||||
|
||||
t_invalid_parameter(_Config) ->
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% "cid=client2" is invaid
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
Topic3 = <<"a/b">>, Payload3 = <<"ET629">>,
|
||||
TopicStr3 = http_uri:encode(binary_to_list(Topic3)),
|
||||
URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?cid=client2&u=tom&p=simple",
|
||||
Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
|
||||
?assertMatch({error,bad_request}, Reply3),
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% "what=hello" is invaid
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
URI4 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?what=hello",
|
||||
Reply4 = er_coap_client:request(put, URI4, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
|
||||
?assertMatch({error, bad_request}, Reply4).
|
||||
|
||||
t_invalid_topic(_Config) ->
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% "a/b" is a valid topic string
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
Topic3 = <<"a/b">>, Payload3 = <<"ET629">>,
|
||||
TopicStr3 = binary_to_list(Topic3),
|
||||
URI3 = "coap://127.0.0.1/mqtt/"++TopicStr3++"?c=client2&u=tom&p=simple",
|
||||
Reply3 = er_coap_client:request(put, URI3, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
|
||||
?assertMatch({ok,changed,_Content}, Reply3),
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% "+?#" is invaid topic string
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
URI4 = "coap://127.0.0.1/mqtt/"++"+?#"++"?what=hello",
|
||||
Reply4 = er_coap_client:request(put, URI4, #coap_content{format = <<"application/octet-stream">>, payload = Payload3}),
|
||||
?assertMatch({error,bad_request}, Reply4).
|
||||
|
||||
% mqtt connection kicked by coap with same client id
|
||||
t_kick_1(_Config) ->
|
||||
URI = "coap://127.0.0.1/mqtt/abc?c=clientid&u=tom&p=secret",
|
||||
% workaround: emqx:subscribe does not kick same client id.
|
||||
spawn_monitor(fun() ->
|
||||
{ok, C} = emqtt:start_link([{host, "localhost"},
|
||||
{clientid, <<"clientid">>},
|
||||
{username, <<"plain">>},
|
||||
{password, <<"plain">>}]),
|
||||
{ok, _} = emqtt:connect(C) end),
|
||||
er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>,
|
||||
payload = <<"123">>}),
|
||||
receive
|
||||
{'DOWN', _, _, _, _} -> ok
|
||||
after 2000 ->
|
||||
?assert(false)
|
||||
end.
|
||||
|
||||
% mqtt connection kicked by coap with same client id
|
||||
t_acl(Config) ->
|
||||
%% Update acl file and reload mod_acl_internal
|
||||
Path = filename:join([testdir(proplists:get_value(data_dir, Config)), "deny.conf"]),
|
||||
ok = file:write_file(Path, <<"{deny, {user, \"coap\"}, publish, [\"abc\"]}.">>),
|
||||
OldPath = emqx:get_env(acl_file),
|
||||
emqx_mod_acl_internal:reload([{acl_file, Path}]),
|
||||
|
||||
emqx:subscribe(<<"abc">>),
|
||||
URI = "coap://127.0.0.1/mqtt/adbc?c=client1&u=coap&p=secret",
|
||||
er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>,
|
||||
payload = <<"123">>}),
|
||||
receive
|
||||
_Something -> ?assert(false)
|
||||
after 2000 ->
|
||||
ok
|
||||
end,
|
||||
|
||||
application:set_env(emqx, acl_file, OldPath),
|
||||
file:delete(Path),
|
||||
emqx_mod_acl_internal:reload([{acl_file, OldPath}]).
|
||||
|
||||
t_stats(_) ->
|
||||
ok.
|
||||
|
||||
t_auth_failure(_) ->
|
||||
ok.
|
||||
|
||||
t_qos_supprot(_) ->
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helpers
|
||||
|
||||
receive_notification() ->
|
||||
receive
|
||||
{coap_notify, Pid, N2, Code2, Content2} ->
|
||||
{coap_notify, Pid, N2, Code2, Content2}
|
||||
after 2000 ->
|
||||
receive_notification_timeout
|
||||
end.
|
||||
|
||||
testdir(DataPath) ->
|
||||
Ls = filename:split(DataPath),
|
||||
filename:join(lists:sublist(Ls, 1, length(Ls) - 1)).
|
677
apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl
Normal file
677
apps/emqx_coap/test/emqx_coap_pubsub_SUITE.erl
Normal file
@ -0,0 +1,677 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_coap_pubsub_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-include_lib("gen_coap/include/coap.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
|
||||
-define(LOGT(Format, Args), ct:pal(Format, Args)).
|
||||
|
||||
all() -> emqx_ct:all(?MODULE).
|
||||
|
||||
init_per_suite(Config) ->
|
||||
emqx_ct_helpers:start_apps([emqx_coap], fun set_sepecial_cfg/1),
|
||||
Config.
|
||||
|
||||
set_sepecial_cfg(emqx_coap) ->
|
||||
application:set_env(emqx_coap, enable_stats, true);
|
||||
set_sepecial_cfg(_) ->
|
||||
ok.
|
||||
|
||||
end_per_suite(Config) ->
|
||||
emqx_ct_helpers:stop_apps([emqx_coap]),
|
||||
Config.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Test Cases
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
t_update_max_age(_Config) ->
|
||||
TopicInPayload = <<"topic1">>,
|
||||
Payload = <<"<topic1>;ct=42">>,
|
||||
Payload1 = <<"<topic1>;ct=50">>,
|
||||
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
|
||||
URI2 = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
|
||||
?LOGT("lookup topic info=~p", [TopicInfo]),
|
||||
?assertEqual(60, MaxAge1),
|
||||
?assertEqual(<<"42">>, CT1),
|
||||
|
||||
timer:sleep(50),
|
||||
|
||||
%% post to create the same topic but with different max age and ct value in payload
|
||||
Reply1 = er_coap_client:request(post, URI, #coap_content{max_age = 70, format = <<"application/link-format">>, payload = Payload1}),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply1,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
[{TopicInPayload, MaxAge2, CT2, _ResPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
|
||||
?assertEqual(70, MaxAge2),
|
||||
?assertEqual(<<"50">>, CT2),
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2).
|
||||
|
||||
t_create_subtopic(_Config) ->
|
||||
TopicInPayload = <<"topic1">>,
|
||||
TopicInPayloadStr = "topic1",
|
||||
Payload = <<"<topic1>;ct=42">>,
|
||||
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
|
||||
RealURI = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret",
|
||||
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
|
||||
?LOGT("lookup topic info=~p", [TopicInfo]),
|
||||
?assertEqual(60, MaxAge1),
|
||||
?assertEqual(<<"42">>, CT1),
|
||||
|
||||
timer:sleep(50),
|
||||
|
||||
%% post to create the a sub topic
|
||||
SubPayload = <<"<subtopic>;ct=42">>,
|
||||
SubTopicInPayloadStr = "subtopic",
|
||||
SubURI = "coap://127.0.0.1/ps/"++TopicInPayloadStr++"?c=client1&u=tom&p=secret",
|
||||
SubRealURI = "coap://127.0.0.1/ps/"++TopicInPayloadStr++"/"++SubTopicInPayloadStr++"?c=client1&u=tom&p=secret",
|
||||
FullTopic = list_to_binary(TopicInPayloadStr++"/"++SubTopicInPayloadStr),
|
||||
Reply1 = er_coap_client:request(post, SubURI, #coap_content{format = <<"application/link-format">>, payload = SubPayload}),
|
||||
?LOGT("Reply =~p", [Reply1]),
|
||||
{ok,created, #coap_content{location_path = LocPath1}} = Reply1,
|
||||
?assertEqual([<<"/ps/topic1/subtopic">>] ,LocPath1),
|
||||
[{FullTopic, MaxAge2, CT2, _ResPayload, _}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
|
||||
?assertEqual(60, MaxAge2),
|
||||
?assertEqual(<<"42">>, CT2),
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, SubRealURI),
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, RealURI).
|
||||
|
||||
t_over_max_age(_Config) ->
|
||||
TopicInPayload = <<"topic1">>,
|
||||
Payload = <<"<topic1>;ct=42">>,
|
||||
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 2, format = <<"application/link-format">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
|
||||
?LOGT("lookup topic info=~p", [TopicInfo]),
|
||||
?assertEqual(2, MaxAge1),
|
||||
?assertEqual(<<"42">>, CT1),
|
||||
|
||||
timer:sleep(3000),
|
||||
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(TopicInPayload)).
|
||||
|
||||
t_refreash_max_age(_Config) ->
|
||||
TopicInPayload = <<"topic1">>,
|
||||
Payload = <<"<topic1>;ct=42">>,
|
||||
Payload1 = <<"<topic1>;ct=50">>,
|
||||
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
|
||||
RealURI = "coap://127.0.0.1/ps/topic1"++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/link-format">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
TopicInfo = [{TopicInPayload, MaxAge1, CT1, _ResPayload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
|
||||
?LOGT("lookup topic info=~p", [TopicInfo]),
|
||||
?LOGT("TimeStamp=~p", [TimeStamp]),
|
||||
?assertEqual(5, MaxAge1),
|
||||
?assertEqual(<<"42">>, CT1),
|
||||
|
||||
timer:sleep(3000),
|
||||
|
||||
%% post to create the same topic, the max age timer will be restarted with the new max age value
|
||||
Reply1 = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/link-format">>, payload = Payload1}),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply1,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
[{TopicInPayload, MaxAge2, CT2, _ResPayload, TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(TopicInPayload),
|
||||
?LOGT("TimeStamp1=~p", [TimeStamp1]),
|
||||
?assertEqual(5, MaxAge2),
|
||||
?assertEqual(<<"50">>, CT2),
|
||||
|
||||
timer:sleep(3000),
|
||||
?assertEqual(false, emqx_coap_pubsub_topics:is_topic_timeout(TopicInPayload)),
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, RealURI).
|
||||
|
||||
t_case01_publish_post(_Config) ->
|
||||
timer:sleep(100),
|
||||
MainTopic = <<"maintopic">>,
|
||||
TopicInPayload = <<"topic1">>,
|
||||
Payload = <<"<topic1>;ct=42">>,
|
||||
MainTopicStr = binary_to_list(MainTopic),
|
||||
|
||||
%% post to create topic maintopic/topic1
|
||||
URI1 = "coap://127.0.0.1/ps/"++MainTopicStr++"?c=client1&u=tom&p=secret",
|
||||
FullTopic = list_to_binary(MainTopicStr++"/"++binary_to_list(TopicInPayload)),
|
||||
Reply1 = er_coap_client:request(post, URI1, #coap_content{format = <<"application/link-format">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply1]),
|
||||
{ok,created, #coap_content{location_path = LocPath1}} = Reply1,
|
||||
?assertEqual([<<"/ps/maintopic/topic1">>] ,LocPath1),
|
||||
[{FullTopic, MaxAge, CT2, <<>>, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
|
||||
?assertEqual(60, MaxAge),
|
||||
?assertEqual(<<"42">>, CT2),
|
||||
|
||||
%% post to publish message to topic maintopic/topic1
|
||||
FullTopicStr = http_uri:encode(binary_to_list(FullTopic)),
|
||||
URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret",
|
||||
PubPayload = <<"PUBLISH">>,
|
||||
|
||||
%% Sub topic first
|
||||
emqx:subscribe(FullTopic),
|
||||
|
||||
Reply2 = er_coap_client:request(post, URI2, #coap_content{format = <<"application/octet-stream">>, payload = PubPayload}),
|
||||
?LOGT("Reply =~p", [Reply2]),
|
||||
{ok,changed, _} = Reply2,
|
||||
TopicInfo = [{FullTopic, MaxAge, CT2, PubPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
|
||||
?LOGT("the topic info =~p", [TopicInfo]),
|
||||
|
||||
assert_recv(FullTopic, PubPayload),
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2).
|
||||
|
||||
t_case02_publish_post(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"payload">>,
|
||||
|
||||
%% Sub topic first
|
||||
emqx:subscribe(Topic),
|
||||
|
||||
%% post to publish a new topic "topic1", and the topic is created
|
||||
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?assertEqual(60, MaxAge),
|
||||
?assertEqual(<<"42">>, CT),
|
||||
|
||||
assert_recv(Topic, Payload),
|
||||
|
||||
%% post to publish a new message to the same topic "topic1" with different payload
|
||||
NewPayload = <<"newpayload">>,
|
||||
Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = NewPayload}),
|
||||
?LOGT("Reply =~p", [Reply1]),
|
||||
{ok,changed, _} = Reply1,
|
||||
[{Topic, MaxAge, CT, NewPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
|
||||
assert_recv(Topic, NewPayload),
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
|
||||
|
||||
t_case03_publish_post(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"payload">>,
|
||||
|
||||
%% Sub topic first
|
||||
emqx:subscribe(Topic),
|
||||
|
||||
%% post to publish a new topic "topic1", and the topic is created
|
||||
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?assertEqual(60, MaxAge),
|
||||
?assertEqual(<<"42">>, CT),
|
||||
|
||||
assert_recv(Topic, Payload),
|
||||
|
||||
%% post to publish a new message to the same topic "topic1", but the ct is not same as created
|
||||
NewPayload = <<"newpayload">>,
|
||||
Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/exi">>, payload = NewPayload}),
|
||||
?LOGT("Reply =~p", [Reply1]),
|
||||
?assertEqual({error,bad_request}, Reply1),
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
|
||||
|
||||
t_case04_publish_post(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"payload">>,
|
||||
|
||||
%% post to publish a new topic "topic1", and the topic is created
|
||||
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?assertEqual(5, MaxAge),
|
||||
?assertEqual(<<"42">>, CT),
|
||||
|
||||
%% after max age timeout, the topic still exists but the status is timeout
|
||||
timer:sleep(6000),
|
||||
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
|
||||
|
||||
t_case01_publish_put(_Config) ->
|
||||
MainTopic = <<"maintopic">>,
|
||||
TopicInPayload = <<"topic1">>,
|
||||
Payload = <<"<topic1>;ct=42">>,
|
||||
MainTopicStr = binary_to_list(MainTopic),
|
||||
|
||||
%% post to create topic maintopic/topic1
|
||||
URI1 = "coap://127.0.0.1/ps/"++MainTopicStr++"?c=client1&u=tom&p=secret",
|
||||
FullTopic = list_to_binary(MainTopicStr++"/"++binary_to_list(TopicInPayload)),
|
||||
Reply1 = er_coap_client:request(post, URI1, #coap_content{format = <<"application/link-format">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply1]),
|
||||
{ok,created, #coap_content{location_path = LocPath1}} = Reply1,
|
||||
?assertEqual([<<"/ps/maintopic/topic1">>] ,LocPath1),
|
||||
[{FullTopic, MaxAge, CT2, <<>>, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
|
||||
?assertEqual(60, MaxAge),
|
||||
?assertEqual(<<"42">>, CT2),
|
||||
|
||||
%% put to publish message to topic maintopic/topic1
|
||||
FullTopicStr = http_uri:encode(binary_to_list(FullTopic)),
|
||||
URI2 = "coap://127.0.0.1/ps/"++FullTopicStr++"?c=client1&u=tom&p=secret",
|
||||
PubPayload = <<"PUBLISH">>,
|
||||
|
||||
%% Sub topic first
|
||||
emqx:subscribe(FullTopic),
|
||||
|
||||
Reply2 = er_coap_client:request(put, URI2, #coap_content{format = <<"application/octet-stream">>, payload = PubPayload}),
|
||||
?LOGT("Reply =~p", [Reply2]),
|
||||
{ok,changed, _} = Reply2,
|
||||
[{FullTopic, MaxAge, CT2, PubPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(FullTopic),
|
||||
|
||||
assert_recv(FullTopic, PubPayload),
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI2).
|
||||
|
||||
t_case02_publish_put(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"payload">>,
|
||||
|
||||
%% Sub topic first
|
||||
emqx:subscribe(Topic),
|
||||
|
||||
%% put to publish a new topic "topic1", and the topic is created
|
||||
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?assertEqual(60, MaxAge),
|
||||
?assertEqual(<<"42">>, CT),
|
||||
|
||||
assert_recv(Topic, Payload),
|
||||
|
||||
%% put to publish a new message to the same topic "topic1" with different payload
|
||||
NewPayload = <<"newpayload">>,
|
||||
Reply1 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = NewPayload}),
|
||||
?LOGT("Reply =~p", [Reply1]),
|
||||
{ok,changed, _} = Reply1,
|
||||
[{Topic, MaxAge, CT, NewPayload, _TimeStamp1}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
|
||||
assert_recv(Topic, NewPayload),
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
|
||||
|
||||
t_case03_publish_put(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"payload">>,
|
||||
|
||||
%% Sub topic first
|
||||
emqx:subscribe(Topic),
|
||||
|
||||
%% put to publish a new topic "topic1", and the topic is created
|
||||
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?assertEqual(60, MaxAge),
|
||||
?assertEqual(<<"42">>, CT),
|
||||
|
||||
assert_recv(Topic, Payload),
|
||||
|
||||
%% put to publish a new message to the same topic "topic1", but the ct is not same as created
|
||||
NewPayload = <<"newpayload">>,
|
||||
Reply1 = er_coap_client:request(put, URI, #coap_content{format = <<"application/exi">>, payload = NewPayload}),
|
||||
?LOGT("Reply =~p", [Reply1]),
|
||||
?assertEqual({error,bad_request}, Reply1),
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
|
||||
|
||||
t_case04_publish_put(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"payload">>,
|
||||
|
||||
%% put to publish a new topic "topic1", and the topic is created
|
||||
URI = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(put, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/topic1">>] ,LocPath),
|
||||
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?assertEqual(5, MaxAge),
|
||||
?assertEqual(<<"42">>, CT),
|
||||
|
||||
%% after max age timeout, no publish message to the same topic, the topic info will be deleted
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
% but there is one thing to do is we don't count in the publish message received from emqx(from other node).TBD!!!!!!!!!!!!!
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
timer:sleep(6000),
|
||||
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
|
||||
|
||||
t_case01_subscribe(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
Payload1 = <<"<topic1>;ct=42">>,
|
||||
timer:sleep(100),
|
||||
|
||||
%% First post to create a topic "topic1"
|
||||
Uri = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/link-format">>, payload = Payload1}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = [LocPath]}} = Reply,
|
||||
?assertEqual(<<"/ps/topic1">> ,LocPath),
|
||||
TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?LOGT("lookup topic info=~p", [TopicInfo]),
|
||||
?assertEqual(60, MaxAge1),
|
||||
?assertEqual(<<"42">>, CT1),
|
||||
|
||||
%% Subscribe the topic
|
||||
Uri1 = "coap://127.0.0.1"++binary_to_list(LocPath)++"?c=client1&u=tom&p=secret",
|
||||
{ok, Pid, N, Code, Content} = er_coap_observer:observe(Uri1),
|
||||
?LOGT("observer Pid=~p, N=~p, Code=~p, Content=~p", [Pid, N, Code, Content]),
|
||||
|
||||
[SubPid] = emqx:subscribers(Topic),
|
||||
?assert(is_pid(SubPid)),
|
||||
|
||||
%% Publish a message
|
||||
Payload = <<"123">>,
|
||||
emqx:publish(emqx_message:make(Topic, Payload)),
|
||||
|
||||
Notif = receive_notification(),
|
||||
?LOGT("observer get Notif=~p", [Notif]),
|
||||
{coap_notify, _, _, {ok,content}, #coap_content{payload = PayloadRecv}} = Notif,
|
||||
|
||||
?assertEqual(Payload, PayloadRecv),
|
||||
|
||||
%% GET to read the publish message of the topic
|
||||
Reply1 = er_coap_client:request(get, Uri1),
|
||||
?LOGT("Reply=~p", [Reply1]),
|
||||
{ok,content, #coap_content{payload = <<"123">>}} = Reply1,
|
||||
|
||||
er_coap_observer:stop(Pid),
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri1).
|
||||
|
||||
t_case02_subscribe(_Config) ->
|
||||
Topic = <<"a/b">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
PercentEncodedTopic = http_uri:encode(TopicStr),
|
||||
Payload = <<"payload">>,
|
||||
|
||||
%% post to publish a new topic "a/b", and the topic is created
|
||||
URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/a/b">>] ,LocPath),
|
||||
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?assertEqual(5, MaxAge),
|
||||
?assertEqual(<<"42">>, CT),
|
||||
|
||||
%% Wait for the max age of the timer expires
|
||||
timer:sleep(6000),
|
||||
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
|
||||
|
||||
%% Subscribe to the timeout topic "a/b", still successfully,got {ok, nocontent} Method
|
||||
Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
|
||||
Reply1 = {ok, Pid, _N, nocontent, _} = er_coap_observer:observe(Uri),
|
||||
?LOGT("Subscribe Reply=~p", [Reply1]),
|
||||
|
||||
[SubPid] = emqx:subscribers(Topic),
|
||||
?assert(is_pid(SubPid)),
|
||||
|
||||
%% put to publish to topic "a/b"
|
||||
Reply2 = er_coap_client:request(put, URI, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
{ok,changed, #coap_content{}} = Reply2,
|
||||
[{Topic, MaxAge1, CT, Payload, TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?assertEqual(60, MaxAge1),
|
||||
?assertEqual(<<"42">>, CT),
|
||||
?assertEqual(false, TimeStamp =:= timeout),
|
||||
|
||||
%% Publish a message
|
||||
emqx:publish(emqx_message:make(Topic, Payload)),
|
||||
|
||||
Notif = receive_notification(),
|
||||
?LOGT("observer get Notif=~p", [Notif]),
|
||||
{coap_notify, _, _, {ok,content}, #coap_content{payload = Payload}} = Notif,
|
||||
|
||||
er_coap_observer:stop(Pid),
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
|
||||
|
||||
t_case03_subscribe(_Config) ->
|
||||
%% Subscribe to the unexisted topic "a/b", got not_found
|
||||
Topic = <<"a/b">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
PercentEncodedTopic = http_uri:encode(TopicStr),
|
||||
Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
|
||||
{error, not_found} = er_coap_observer:observe(Uri),
|
||||
|
||||
[] = emqx:subscribers(Topic).
|
||||
|
||||
t_case04_subscribe(_Config) ->
|
||||
%% Subscribe to the wildcad topic "+/b", got bad_request
|
||||
Topic = <<"+/b">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
PercentEncodedTopic = http_uri:encode(TopicStr),
|
||||
Uri = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
|
||||
{error, bad_request} = er_coap_observer:observe(Uri),
|
||||
|
||||
[] = emqx:subscribers(Topic).
|
||||
|
||||
t_case01_read(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"PubPayload">>,
|
||||
timer:sleep(100),
|
||||
|
||||
%% First post to create a topic "topic1"
|
||||
Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = [LocPath]}} = Reply,
|
||||
?assertEqual(<<"/ps/topic1">> ,LocPath),
|
||||
TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?LOGT("lookup topic info=~p", [TopicInfo]),
|
||||
?assertEqual(60, MaxAge1),
|
||||
?assertEqual(<<"42">>, CT1),
|
||||
|
||||
%% GET to read the publish message of the topic
|
||||
timer:sleep(1000),
|
||||
Reply1 = er_coap_client:request(get, Uri),
|
||||
?LOGT("Reply=~p", [Reply1]),
|
||||
{ok,content, #coap_content{payload = Payload}} = Reply1,
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
|
||||
|
||||
t_case02_read(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"PubPayload">>,
|
||||
timer:sleep(100),
|
||||
|
||||
%% First post to publish a topic "topic1"
|
||||
Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = [LocPath]}} = Reply,
|
||||
?assertEqual(<<"/ps/topic1">> ,LocPath),
|
||||
TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?LOGT("lookup topic info=~p", [TopicInfo]),
|
||||
?assertEqual(60, MaxAge1),
|
||||
?assertEqual(<<"42">>, CT1),
|
||||
|
||||
%% GET to read the publish message of unmatched format, got bad_request
|
||||
Reply1 = er_coap_client:request(get, Uri, #coap_content{format = <<"application/json">>}),
|
||||
?LOGT("Reply=~p", [Reply1]),
|
||||
{error, bad_request} = Reply1,
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
|
||||
|
||||
t_case03_read(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
timer:sleep(100),
|
||||
|
||||
%% GET to read the nexisted topic "topic1", got not_found
|
||||
Reply = er_coap_client:request(get, Uri),
|
||||
?LOGT("Reply=~p", [Reply]),
|
||||
{error, not_found} = Reply.
|
||||
|
||||
t_case04_read(_Config) ->
|
||||
Topic = <<"topic1">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
Payload = <<"PubPayload">>,
|
||||
timer:sleep(100),
|
||||
|
||||
%% First post to publish a topic "topic1"
|
||||
Uri = "coap://127.0.0.1/ps/"++TopicStr++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, Uri, #coap_content{format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = [LocPath]}} = Reply,
|
||||
?assertEqual(<<"/ps/topic1">> ,LocPath),
|
||||
TopicInfo = [{Topic, MaxAge1, CT1, _ResPayload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?LOGT("lookup topic info=~p", [TopicInfo]),
|
||||
?assertEqual(60, MaxAge1),
|
||||
?assertEqual(<<"42">>, CT1),
|
||||
|
||||
%% GET to read the publish message of wildcard topic, got bad_request
|
||||
WildTopic = binary_to_list(<<"+/topic1">>),
|
||||
Uri1 = "coap://127.0.0.1/ps/"++WildTopic++"?c=client1&u=tom&p=secret",
|
||||
Reply1 = er_coap_client:request(get, Uri1, #coap_content{format = <<"application/json">>}),
|
||||
?LOGT("Reply=~p", [Reply1]),
|
||||
{error, bad_request} = Reply1,
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, Uri).
|
||||
|
||||
t_case05_read(_Config) ->
|
||||
Topic = <<"a/b">>,
|
||||
TopicStr = binary_to_list(Topic),
|
||||
PercentEncodedTopic = http_uri:encode(TopicStr),
|
||||
Payload = <<"payload">>,
|
||||
|
||||
%% post to publish a new topic "a/b", and the topic is created
|
||||
URI = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{max_age = 5, format = <<"application/octet-stream">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/a/b">>] ,LocPath),
|
||||
[{Topic, MaxAge, CT, Payload, _TimeStamp}] = emqx_coap_pubsub_topics:lookup_topic_info(Topic),
|
||||
?assertEqual(5, MaxAge),
|
||||
?assertEqual(<<"42">>, CT),
|
||||
|
||||
%% Wait for the max age of the timer expires
|
||||
timer:sleep(6000),
|
||||
?assertEqual(true, emqx_coap_pubsub_topics:is_topic_timeout(Topic)),
|
||||
|
||||
%% GET to read the expired publish message, supposed to get {ok, nocontent}, but now got {ok, content}
|
||||
Reply1 = er_coap_client:request(get, URI),
|
||||
?LOGT("Reply=~p", [Reply1]),
|
||||
{ok, content, #coap_content{payload = <<>>}}= Reply1,
|
||||
|
||||
{ok, deleted, #coap_content{}} = er_coap_client:request(delete, URI).
|
||||
|
||||
t_case01_delete(_Config) ->
|
||||
TopicInPayload = <<"a/b">>,
|
||||
TopicStr = binary_to_list(TopicInPayload),
|
||||
PercentEncodedTopic = http_uri:encode(TopicStr),
|
||||
Payload = list_to_binary("<"++PercentEncodedTopic++">;ct=42"),
|
||||
URI = "coap://127.0.0.1/ps/"++"?c=client1&u=tom&p=secret",
|
||||
|
||||
%% Client post to CREATE topic "a/b"
|
||||
Reply = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload}),
|
||||
?LOGT("Reply =~p", [Reply]),
|
||||
{ok,created, #coap_content{location_path = LocPath}} = Reply,
|
||||
?assertEqual([<<"/ps/a/b">>] ,LocPath),
|
||||
|
||||
%% Client post to CREATE topic "a/b/c"
|
||||
TopicInPayload1 = <<"a/b/c">>,
|
||||
PercentEncodedTopic1 = http_uri:encode(binary_to_list(TopicInPayload1)),
|
||||
Payload1 = list_to_binary("<"++PercentEncodedTopic1++">;ct=42"),
|
||||
Reply1 = er_coap_client:request(post, URI, #coap_content{format = <<"application/link-format">>, payload = Payload1}),
|
||||
?LOGT("Reply =~p", [Reply1]),
|
||||
{ok,created, #coap_content{location_path = LocPath1}} = Reply1,
|
||||
?assertEqual([<<"/ps/a/b/c">>] ,LocPath1),
|
||||
|
||||
timer:sleep(50),
|
||||
|
||||
%% DELETE the topic "a/b"
|
||||
UriD = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
|
||||
ReplyD = er_coap_client:request(delete, UriD),
|
||||
?LOGT("Reply=~p", [ReplyD]),
|
||||
{ok, deleted, #coap_content{}}= ReplyD,
|
||||
|
||||
timer:sleep(300), %% Waiting gen_server:cast/2 for deleting operation
|
||||
?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload)),
|
||||
?assertEqual(false, emqx_coap_pubsub_topics:is_topic_existed(TopicInPayload1)).
|
||||
|
||||
t_case02_delete(_Config) ->
|
||||
TopicInPayload = <<"a/b">>,
|
||||
TopicStr = binary_to_list(TopicInPayload),
|
||||
PercentEncodedTopic = http_uri:encode(TopicStr),
|
||||
|
||||
%% DELETE the unexisted topic "a/b"
|
||||
Uri1 = "coap://127.0.0.1/ps/"++PercentEncodedTopic++"?c=client1&u=tom&p=secret",
|
||||
Reply1 = er_coap_client:request(delete, Uri1),
|
||||
?LOGT("Reply=~p", [Reply1]),
|
||||
{error, not_found} = Reply1.
|
||||
|
||||
t_case13_emit_stats_test(_Config) ->
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
|
||||
receive_notification() ->
|
||||
receive
|
||||
{coap_notify, Pid, N2, Code2, Content2} ->
|
||||
{coap_notify, Pid, N2, Code2, Content2}
|
||||
after 2000 ->
|
||||
receive_notification_timeout
|
||||
end.
|
||||
|
||||
assert_recv(Topic, Payload) ->
|
||||
receive
|
||||
{deliver, _, Msg} ->
|
||||
?assertEqual(Topic, Msg#message.topic),
|
||||
?assertEqual(Payload, Msg#message.payload)
|
||||
after
|
||||
500 ->
|
||||
?assert(false)
|
||||
end.
|
||||
|
@ -32,8 +32,8 @@ See: `priv/protos/exhook.proto`
|
||||
|
||||
## Recommended gRPC Framework
|
||||
|
||||
See: https://hub.fastgit.org/grpc-ecosystem/awesome-grpc
|
||||
See: https://github.com/grpc-ecosystem/awesome-grpc
|
||||
|
||||
## Thanks
|
||||
|
||||
- [grpcbox](https://hub.fastgit.org/tsloughter/grpcbox)
|
||||
- [grpcbox](https://github.com/tsloughter/grpcbox)
|
||||
|
@ -19,7 +19,7 @@
|
||||
2. 将 `emqx-extension-hook` 重命名为 `emqx-exhook`
|
||||
|
||||
|
||||
旧版本的设计:[emqx-extension-hook design in v4.2.0](https://hub.fastgit.org/emqx/emqx-exhook/blob/v4.2.0/docs/design.md)
|
||||
旧版本的设计:[emqx-extension-hook design in v4.2.0](https://hub.fastgit.org/fastdgiot/emqx-exhook/blob/v4.2.0/docs/design.md)
|
||||
|
||||
## 设计
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
%%-*- mode: erlang -*-
|
||||
{plugins,
|
||||
[rebar3_proper,
|
||||
{grpc_plugin, {git, "https://hub.fastgit.org/HJianBo/grpc_plugin", {tag, "v0.10.2"}}}
|
||||
{grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}}
|
||||
]}.
|
||||
|
||||
{deps,
|
||||
[{grpc, {git, "https://hub.fastgit.org/emqx/grpc-erl", {tag, "0.6.2"}}}
|
||||
[{grpc, {git, "https://hub.fastgit.org/fastdgiot/grpc-erl", {tag, "0.6.2"}}}
|
||||
]}.
|
||||
|
||||
{grpc,
|
||||
@ -43,7 +43,7 @@
|
||||
{profiles,
|
||||
[{test,
|
||||
[{deps,
|
||||
[{emqx_ct_helper, {git, "https://hub.fastgit.org/emqx/emqx-ct-helpers", {tag, "v1.3.1"}}}
|
||||
[{emqx_ct_helper, {git, "https://hub.fastgit.org/fastdgiot/emqx-ct-helpers", {tag, "v1.3.1"}}}
|
||||
]}
|
||||
]}
|
||||
]}.
|
||||
|
@ -21,4 +21,4 @@ See: `priv/protos/exproto.proto`
|
||||
|
||||
## Recommended gRPC Framework
|
||||
|
||||
See: https://hub.fastgit.org/grpc-ecosystem/awesome-grpc
|
||||
See: https://github.com/grpc-ecosystem/awesome-grpc
|
||||
|
@ -58,7 +58,7 @@ exproto.listener.protoname.idle_timeout = 30s
|
||||
|
||||
## The access control rules for the MQTT/TCP listener.
|
||||
##
|
||||
## See: https://hub.fastgit.org/emqtt/esockd#allowdeny
|
||||
## See: https://github.com/emqtt/esockd#allowdeny
|
||||
##
|
||||
## Value: ACL Rule
|
||||
##
|
||||
|
@ -9,11 +9,11 @@
|
||||
{parse_transform}]}.
|
||||
{plugins,
|
||||
[rebar3_proper,
|
||||
{grpc_plugin, {git, "https://hub.fastgit.org/HJianBo/grpc_plugin", {tag, "v0.10.2"}}}
|
||||
{grpc_plugin, {git, "https://github.com/HJianBo/grpc_plugin", {tag, "v0.10.2"}}}
|
||||
]}.
|
||||
|
||||
{deps,
|
||||
[{grpc, {git, "https://hub.fastgit.org/emqx/grpc-erl", {tag, "0.6.2"}}}
|
||||
[{grpc, {git, "https://hub.fastgit.org/fastdgiot/grpc-erl", {tag, "0.6.2"}}}
|
||||
]}.
|
||||
|
||||
{grpc,
|
||||
@ -46,7 +46,7 @@
|
||||
{profiles,
|
||||
[{test,
|
||||
[{deps,
|
||||
[{emqx_ct_helper, {git, "https://hub.fastgit.org/emqx/emqx-ct-helpers", {tag, "v1.3.0"}}}
|
||||
[{emqx_ct_helper, {git, "https://hub.fastgit.org/fastdgiot/emqx-ct-helpers", {tag, "v1.3.0"}}}
|
||||
]}
|
||||
]}
|
||||
]}.
|
||||
|
@ -3,13 +3,13 @@
|
||||
|
||||
This plugin makes it possible to write hooks in lua scripts.
|
||||
|
||||
Lua virtual machine is implemented by [luerl](https://hub.fastgit.org/rvirding/luerl) which supports Lua 5.2. Following features may not work properly:
|
||||
Lua virtual machine is implemented by [luerl](https://github.com/rvirding/luerl) which supports Lua 5.2. Following features may not work properly:
|
||||
* label and goto
|
||||
* tail-call optimisation in return
|
||||
* only limited standard libraries
|
||||
* proper handling of `__metatable`
|
||||
|
||||
For the supported functions, please refer to luerl's [project page](https://hub.fastgit.org/rvirding/luerl).
|
||||
For the supported functions, please refer to luerl's [project page](https://github.com/rvirding/luerl).
|
||||
|
||||
Lua scripts are stored in 'data/scripts' directory, and will be loaded automatically. If a script is changed during runtime, it should be reloaded to take effect.
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{deps,
|
||||
[{luerl, {git, "https://hub.fastgit.org/emqx/luerl", {tag, "v0.3.1"}}}
|
||||
[{luerl, {git, "https://hub.fastgit.org/fastdgiot/luerl", {tag, "v0.3.1"}}}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
|
@ -9,6 +9,6 @@
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||
{links, [{"Homepage", "https://emqx.io/"},
|
||||
{"Github", "https://hub.fastgit.org/emqx/emqx-lua-hook"}
|
||||
{"Github", "https://hub.fastgit.org/fastdgiot/emqx-lua-hook"}
|
||||
]}
|
||||
]}.
|
||||
|
25
apps/emqx_lwm2m/.gitignore
vendored
Normal file
25
apps/emqx_lwm2m/.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
deps/
|
||||
ebin/
|
||||
_rel/
|
||||
.erlang.mk/
|
||||
*.d
|
||||
*.o
|
||||
*.exe
|
||||
data/
|
||||
*.iml
|
||||
.idea/
|
||||
logs/
|
||||
*.beam
|
||||
emqx_coap.d
|
||||
erlang.mk
|
||||
integration_test/emqx-rel/
|
||||
integration_test/build_wakaama/
|
||||
integration_test/case*.txt
|
||||
integration_test/paho/
|
||||
integration_test/wakaama/
|
||||
_build/
|
||||
rebar.lock
|
||||
rebar3.crashdump
|
||||
*.conf.rendered
|
||||
.rebar3/
|
||||
*.swp
|
357
apps/emqx_lwm2m/README.md
Normal file
357
apps/emqx_lwm2m/README.md
Normal file
@ -0,0 +1,357 @@
|
||||
|
||||
# LwM2M Gateway for the EMQ X Broker.
|
||||
|
||||
[The LwM2M Specifications](http://www.openmobilealliance.org/release/LightweightM2M) is a Lightweight Machine to Machine protocol.
|
||||
|
||||
With `emqx_lwm2m`, user is able to send LwM2M commands(READ/WRITE/EXECUTE/...) and get LwM2M response in MQTT way. `emqx_lwm2m` transforms data between MQTT and LwM2M protocol.
|
||||
|
||||
emqx_lwm2m needs object definitions to parse data from lwm2m devices. Object definitions are declared by organizations in XML format, you could find those XMLs from [LwM2MRegistry](http://www.openmobilealliance.org/wp/OMNA/LwM2M/LwM2MRegistry.html), download and put them into the directory specified by `lwm2m.xml_dir`. If no associated object definition is found, response from device will be discarded and report an error message in log.
|
||||
|
||||
## Load emqx_lwm2m
|
||||
|
||||
```
|
||||
./bin/emqx_ctl plugins load emqx_lwm2m
|
||||
```
|
||||
|
||||
## Test emqx-lwm2m using *wakaama*
|
||||
|
||||
[wakaama](https://github.com/eclipse/wakaama) is an easy-to-use lwm2m client command line tool.
|
||||
|
||||
Start *lwm2mclient* using an endpoint name `ep1`:
|
||||
```
|
||||
./lwm2mclient -n ep1 -h 127.0.0.1 -p 5683 -4
|
||||
```
|
||||
|
||||
To send an LwM2M DISCOVER command to *lwm2mclient*, publish an MQTT message to topic `lwm2m/<epn>/dn` (where `<epn>` is the endpoint name of the client), with following payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"reqID": "2",
|
||||
"msgType": "discover",
|
||||
"data": {
|
||||
"path": "/3/0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The MQTT message will be translated to an LwM2M DISCOVER command and sent to the *lwm2mclient*. Then the response of *lwm2mclient* will be in turn translated to an MQTT message, with topic `lwm2m/<epn>/up/resp`, with following payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"reqID": "2",
|
||||
"msgType": "discover",
|
||||
"data": {
|
||||
"code":"2.05",
|
||||
"codeMsg": "content",
|
||||
"content": [
|
||||
"</3/0>;dim=8",
|
||||
"</3/0/0>",
|
||||
"</3/0/1>",
|
||||
"</3/0/4>",
|
||||
"</3/0/16>"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## LwM2M <--> MQTT Mapping
|
||||
|
||||
### Register/Update (LwM2M Client Registration Interface)
|
||||
|
||||
- **LwM2M Register and Update message will be converted to following MQTT message:**
|
||||
|
||||
- **Method:** PUBLISH
|
||||
- **Topic:** `lwm2m/{?EndpointName}/up/resp` (configurable)
|
||||
- **Payload**:
|
||||
- MsgType **register** and **update**:
|
||||
```json
|
||||
{
|
||||
"msgType": {?MsgType},
|
||||
"data": {
|
||||
"ep": {?EndpointName},
|
||||
"lt": {?LifeTime},
|
||||
"sms": {?MSISDN},
|
||||
"lwm2m": {?Lwm2mVersion},
|
||||
"b": {?Binding},
|
||||
"alternatePath": {?AlternatePath},
|
||||
"objectList": {?ObjectList}
|
||||
}
|
||||
}
|
||||
```
|
||||
- {?EndpointName}: String, the endpoint name of the LwM2M client
|
||||
- {?MsgType}: String, could be:
|
||||
- "register": LwM2M Register
|
||||
- "update": LwM2M Update
|
||||
- "data" contains the query options and the object-list of the register message
|
||||
- The *update* message is only published if the object-list changed.
|
||||
|
||||
### Downlink Command and Uplink Response (LwM2M Device Management & Service Enablement Interface)
|
||||
|
||||
- **To send a downlink command to device, publish following MQTT message:**
|
||||
- **Method:** PUBLISH
|
||||
- **Topic:** `lwm2m/{?EndpointName}/dn`
|
||||
- **Request Payload**:
|
||||
```json
|
||||
{
|
||||
"reqID": {?ReqID},
|
||||
"msgType": {?MsgType},
|
||||
"data": {?Data}
|
||||
}
|
||||
```
|
||||
- {?ReqID}: Integer, request-id, used for matching the response to the request
|
||||
- {?MsgType}: String, can be one of the following:
|
||||
- "read": LwM2M Read
|
||||
- "discover": LwM2M Discover
|
||||
- "write": LwM2M Write
|
||||
- "write-attr": LwM2M Write Attributes
|
||||
- "execute": LwM2M Execute
|
||||
- "create": LwM2M Create
|
||||
- "delete": LwM2M Delete
|
||||
- {?Data}: Json Object, its value depends on the {?MsgType}:
|
||||
- **If {?MsgType} = "read" or "discover"**:
|
||||
```json
|
||||
{
|
||||
"path": {?ResourcePath}
|
||||
}
|
||||
```
|
||||
- {?ResourcePath}: String, LwM2M full resource path. e.g. "3/0", "/3/0/0", "/3/0/6/0"
|
||||
- **If {?MsgType} = "write" (single write)**:
|
||||
```json
|
||||
{
|
||||
"path": {?ResourcePath},
|
||||
"type": {?ValueType},
|
||||
"value": {?Value}
|
||||
}
|
||||
```
|
||||
- {?ValueType}: String, can be: "Time", "String", "Integer", "Float", "Boolean", "Opaque", "Objlnk"
|
||||
- {?Value}: Value of the resource, depends on "type".
|
||||
- **If {?MsgType} = "write" (batch write)**:
|
||||
```json
|
||||
{
|
||||
"basePath": {?BasePath},
|
||||
"content": [
|
||||
{
|
||||
"path": {?ResourcePath},
|
||||
"type": {?ValueType},
|
||||
"value": {?Value}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- The full path is concatenation of "basePath" and "path".
|
||||
- **If {?MsgType} = "write-attr"**:
|
||||
```json
|
||||
{
|
||||
"path": {?ResourcePath},
|
||||
"pmin": {?PeriodMin},
|
||||
"pmax": {?PeriodMax},
|
||||
"gt": {?GreaterThan},
|
||||
"lt": {?LessThan},
|
||||
"st": {?Step}
|
||||
}
|
||||
```
|
||||
- {?PeriodMin}: Number, LwM2M Notification Class Attribute - Minimum Period.
|
||||
- {?PeriodMax}: Number, LwM2M Notification Class Attribute - Maximum Period.
|
||||
- {?GreaterThan}: Number, LwM2M Notification Class Attribute - Greater Than.
|
||||
- {?LessThan}: Number, LwM2M Notification Class Attribute - Less Than.
|
||||
- {?Step}: Number, LwM2M Notification Class Attribute - Step.
|
||||
|
||||
- **If {?MsgType} = "execute"**:
|
||||
```json
|
||||
{
|
||||
"path": {?ResourcePath},
|
||||
"args": {?Arguments}
|
||||
}
|
||||
```
|
||||
- {?Arguments}: String, LwM2M Execute Arguments.
|
||||
|
||||
- **If {?MsgType} = "create"**:
|
||||
```json
|
||||
{
|
||||
"basePath": "/{?ObjectID}",
|
||||
"content": [
|
||||
{
|
||||
"path": {?ResourcePath},
|
||||
"type": {?ValueType},
|
||||
"value": {?Value}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- {?ObjectID}: Integer, LwM2M Object ID
|
||||
|
||||
- **If {?MsgType} = "delete"**:
|
||||
```json
|
||||
{
|
||||
"path": "{?ObjectID}/{?ObjectInstanceID}"
|
||||
}
|
||||
```
|
||||
- {?ObjectInstanceID}: Integer, LwM2M Object Instance ID
|
||||
|
||||
- **The response of LwM2M will be converted to following MQTT message:**
|
||||
- **Method:** PUBLISH
|
||||
- **Topic:** `"lwm2m/{?EndpointName}/up/resp"`
|
||||
- **Response Payload:**
|
||||
|
||||
```json
|
||||
{
|
||||
"reqID": {?ReqID},
|
||||
"imei": {?IMEI},
|
||||
"imsi": {?IMSI},
|
||||
"msgType": {?MsgType},
|
||||
"data": {?Data}
|
||||
}
|
||||
```
|
||||
|
||||
- {?MsgType}: String, can be:
|
||||
- "read": LwM2M Read
|
||||
- "discover": LwM2M Discover
|
||||
- "write": LwM2M Write
|
||||
- "write-attr": LwM2M Write Attributes
|
||||
- "execute": LwM2M Execute
|
||||
- "create": LwM2M Create
|
||||
- "delete": LwM2M Delete
|
||||
- **"ack"**: [CoAP Empty ACK](https://tools.ietf.org/html/rfc7252#section-5.2.2)
|
||||
- {?Data}: Json Object, its value depends on {?MsgType}:
|
||||
- **If {?MsgType} = "write", "write-attr", "execute", "create", "delete", or "read"(when response without content)**:
|
||||
```json
|
||||
{
|
||||
"code": {?StatusCode},
|
||||
"codeMsg": {?CodeMsg},
|
||||
"reqPath": {?RequestPath}
|
||||
}
|
||||
```
|
||||
- {?StatusCode}: String, LwM2M status code, e.g. "2.01", "4.00", etc.
|
||||
- {?CodeMsg}: String, LwM2M response message, e.g. "content", "bad_request"
|
||||
- {?RequestPath}: String, the requested "path" or "basePath"
|
||||
|
||||
- **If {?MsgType} = "discover"**:
|
||||
```json
|
||||
{
|
||||
"code": {?StatusCode},
|
||||
"codeMsg": {?CodeMsg},
|
||||
"reqPath": {?RequestPath},
|
||||
"content": [
|
||||
{?Link},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
- {?Link}: String(LwM2M link format) e.g. `"</3>"`, `"<3/0/1>;dim=8"`
|
||||
|
||||
- **If {?MsgType} = "read"(when response with content)**:
|
||||
```json
|
||||
{
|
||||
"code": {?StatusCode},
|
||||
"codeMsg": {?CodeMsg},
|
||||
"content": {?Content}
|
||||
}
|
||||
```
|
||||
- {?Content}
|
||||
```json
|
||||
[
|
||||
{
|
||||
"path": {?ResourcePath},
|
||||
"value": {?Value}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- **If {?MsgType} = "ack", "data" does not exists**
|
||||
|
||||
### Observe (Information Reporting Interface - Observe/Cancel-Observe)
|
||||
|
||||
- **To observe/cancel-observe LwM2M client, send following MQTT PUBLISH:**
|
||||
- **Method:** PUBLISH
|
||||
- **Topic:** `lwm2m/{?EndpointName}/dn`
|
||||
- **Request Payload**:
|
||||
```json
|
||||
{
|
||||
"reqID": {?ReqID},
|
||||
"msgType": {?MsgType},
|
||||
"data": {
|
||||
"path": {?ResourcePath}
|
||||
}
|
||||
}
|
||||
```
|
||||
- {?ResourcePath}: String, the LwM2M resource to be observed/cancel-observed.
|
||||
- {?MsgType}: String, can be:
|
||||
- "observe": LwM2M Observe
|
||||
- "cancel-observe": LwM2M Cancel Observe
|
||||
- {?ReqID}: Integer, request-id, is the {?ReqID} in the request
|
||||
|
||||
- **Responses will be converted to following MQTT message:**
|
||||
- **Method:** PUBLISH
|
||||
- **Topic:** `lwm2m/{?EndpointName}/up/resp`
|
||||
- **Response Payload**:
|
||||
```json
|
||||
{
|
||||
"reqID": {?ReqID},
|
||||
"msgType": {?MsgType},
|
||||
"data": {
|
||||
"code": {?StatusCode},
|
||||
"codeMsg": {?CodeMsg},
|
||||
"reqPath": {?RequestPath},
|
||||
"content": [
|
||||
{
|
||||
"path": {?ResourcePath},
|
||||
"value": {?Value}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
- {?MsgType}: String, can be:
|
||||
- "observe": LwM2M Observe
|
||||
- "cancel-observe": LwM2M Cancel Observe
|
||||
- **"ack"**: [CoAP Empty ACK](https://tools.ietf.org/html/rfc7252#section-5.2.2)
|
||||
|
||||
### Notification (Information Reporting Interface - Notify)
|
||||
|
||||
- **The notifications from LwM2M clients will be converted to MQTT PUBLISH:**
|
||||
- **Method:** PUBLISH
|
||||
- **Topic:** `lwm2m/{?EndpiontName}/up/notify`
|
||||
- **Notification Payload**:
|
||||
```json
|
||||
{
|
||||
"reqID": {?ReqID},
|
||||
"msgType": {?MsgType},
|
||||
"seqNum": {?ObserveSeqNum},
|
||||
"data": {
|
||||
"code": {?StatusCode},
|
||||
"codeMsg": {?CodeMsg},
|
||||
"reqPath": {?RequestPath},
|
||||
"content": [
|
||||
{
|
||||
"path": {?ResourcePath},
|
||||
"value": {?Value}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
- {?MsgType}: String, must be "notify"
|
||||
- {?ObserveSeqNum}: Number, value of "Observe" option in CoAP message
|
||||
- "content": same to the "content" field contains in the response of "read" command
|
||||
|
||||
## Feature limitations
|
||||
|
||||
- emqx_lwm2m implements LwM2M gateway to EMQX, not a full-featured and independent LwM2M server.
|
||||
- emqx_lwm2m does not include LwM2M bootstrap server.
|
||||
- emqx_lwm2m supports UDP binding, no SMS binding yet.
|
||||
- Firmware object is not fully supported now since mqtt to coap block-wise transfer is not available.
|
||||
- Object Versioning is not supported now.
|
||||
|
||||
## DTLS
|
||||
|
||||
emqx-lwm2m support DTLS to secure UDP data.
|
||||
|
||||
Please config lwm2m.certfile and lwm2m.keyfile in emqx_lwm2m.conf. If certfile or keyfile are invalid, DTLS will be turned off and you could read a error message in the log.
|
||||
|
||||
## License
|
||||
|
||||
Apache License Version 2.0
|
||||
|
||||
## Author
|
||||
|
||||
EMQ X-Men Team.
|
145
apps/emqx_lwm2m/etc/emqx_lwm2m.conf
Normal file
145
apps/emqx_lwm2m/etc/emqx_lwm2m.conf
Normal file
@ -0,0 +1,145 @@
|
||||
##--------------------------------------------------------------------
|
||||
## LwM2M Gateway
|
||||
##--------------------------------------------------------------------
|
||||
|
||||
##--------------------------------------------------------------------
|
||||
## Protocols
|
||||
|
||||
# To Limit the range of lifetime, in seconds
|
||||
lwm2m.lifetime_min = 1s
|
||||
lwm2m.lifetime_max = 86400s
|
||||
|
||||
# The time window for Q Mode, indicating that after how long time
|
||||
# the downlink commands sent to the client will be cached.
|
||||
#lwm2m.qmode_time_window = 22
|
||||
|
||||
# Auto send observer command to device
|
||||
# on | off
|
||||
#lwm2m.auto_observe = off
|
||||
|
||||
# The topic subscribed by the lwm2m client after it is connected
|
||||
# Placeholders supported:
|
||||
# '%e': Endpoint Name
|
||||
# '%a': IP Address
|
||||
lwm2m.mountpoint = lwm2m/%e/
|
||||
|
||||
# The topic subscribed by the lwm2m client after it is connected
|
||||
# Placeholders supported:
|
||||
# '%e': Endpoint Name
|
||||
# '%a': IP Address
|
||||
lwm2m.topics.command = dn/#
|
||||
|
||||
# The topic to which the lwm2m client's response is published
|
||||
lwm2m.topics.response = up/resp
|
||||
|
||||
# The topic to which the lwm2m client's notify message is published
|
||||
lwm2m.topics.notify = up/notify
|
||||
|
||||
# The topic to which the lwm2m client's register message is published
|
||||
lwm2m.topics.register = up/resp
|
||||
|
||||
# The topic to which the lwm2m client's update message is published
|
||||
lwm2m.topics.update = up/resp
|
||||
|
||||
# When publish the update message.
|
||||
#
|
||||
# Can be one of:
|
||||
# - contains_object_list: only if the update message contains object list
|
||||
# - always: always publish the update message
|
||||
#
|
||||
# Defaults to contains_object_list
|
||||
#lwm2m.update_msg_publish_condition = contains_object_list
|
||||
|
||||
# Dir where the object definition files can be found
|
||||
lwm2m.xml_dir = {{ platform_etc_dir }}/lwm2m_xml
|
||||
|
||||
##--------------------------------------------------------------------
|
||||
## UDP Listener options
|
||||
|
||||
## The IP and port of the LwM2M Gateway
|
||||
##
|
||||
## Default: 0.0.0.0:5683
|
||||
## Examples:
|
||||
## lwm2m.bind.udp.x = 0.0.0.0:5683 | :::5683 | 127.0.0.1:5683 | ::1:5683
|
||||
lwm2m.bind.udp.1 = 0.0.0.0:5683
|
||||
#lwm2m.bind.udp.2 = 0.0.0.0:6683
|
||||
|
||||
## Socket options, used for performance tuning
|
||||
##
|
||||
## Examples:
|
||||
## lwm2m.opts.$name = $value
|
||||
## See: https://erlang.org/doc/man/gen_udp.html#type-option
|
||||
lwm2m.opts.buffer = 1024KB
|
||||
lwm2m.opts.recbuf = 1024KB
|
||||
lwm2m.opts.sndbuf = 1024KB
|
||||
lwm2m.opts.read_packets = 20
|
||||
|
||||
##--------------------------------------------------------------------
|
||||
## DTLS Listener Options
|
||||
|
||||
## The DTLS port that LwM2M is listening on.
|
||||
##
|
||||
## Default: 0.0.0.0:5684
|
||||
##
|
||||
## Examples:
|
||||
## lwm2m.bind.dtls.x = 0.0.0.0:5684 | :::5684 | 127.0.0.1:5684 | ::1:5684
|
||||
##
|
||||
lwm2m.bind.dtls.1 = 0.0.0.0:5684
|
||||
#lwm2m.bind.dtls.2 = 0.0.0.0:6684
|
||||
|
||||
## A server only does x509-path validation in mode verify_peer,
|
||||
## as it then sends a certificate request to the client (this
|
||||
## message is not sent if the verify option is verify_none).
|
||||
## You can then also want to specify option fail_if_no_peer_cert.
|
||||
## More information at: http://erlang.org/doc/man/ssl.html
|
||||
##
|
||||
## Value: verify_peer | verify_none
|
||||
#lwm2m.dtls.verify = verify_peer
|
||||
|
||||
## Private key file for DTLS
|
||||
##
|
||||
## Value: File
|
||||
lwm2m.dtls.keyfile = {{ platform_etc_dir }}/certs/key.pem
|
||||
|
||||
## Server certificate for DTLS.
|
||||
##
|
||||
## Value: File
|
||||
lwm2m.dtls.certfile = {{ platform_etc_dir }}/certs/cert.pem
|
||||
|
||||
## PEM-encoded CA certificates for DTLS
|
||||
##
|
||||
## Value: File
|
||||
#lwm2m.dtls.cacertfile = {{ platform_etc_dir }}/certs/cacert.pem
|
||||
|
||||
## Used together with {verify, verify_peer} by an SSL server. If set to true,
|
||||
## the server fails if the client does not have a certificate to send, that is,
|
||||
## sends an empty certificate.
|
||||
##
|
||||
## Value: true | false
|
||||
#lwm2m.dtls.fail_if_no_peer_cert = false
|
||||
|
||||
## This is the single most important configuration option of an Erlang SSL
|
||||
## application. Ciphers (and their ordering) define the way the client and
|
||||
## server encrypt information over the wire, from the initial Diffie-Helman
|
||||
## key exchange, the session key encryption ## algorithm and the message
|
||||
## digest algorithm. Selecting a good cipher suite is critical for the
|
||||
## application’s data security, confidentiality and performance.
|
||||
##
|
||||
## The cipher list above offers:
|
||||
##
|
||||
## A good balance between compatibility with older browsers.
|
||||
## It can get stricter for Machine-To-Machine scenarios.
|
||||
## Perfect Forward Secrecy.
|
||||
## No old/insecure encryption and HMAC algorithms
|
||||
##
|
||||
## Most of it was copied from Mozilla’s Server Side TLS article
|
||||
##
|
||||
## Value: Ciphers
|
||||
lwm2m.dtls.ciphers = ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-AES256-SHA384,ECDHE-RSA-AES256-SHA384,ECDHE-ECDSA-DES-CBC3-SHA,ECDH-ECDSA-AES256-GCM-SHA384,ECDH-RSA-AES256-GCM-SHA384,ECDH-ECDSA-AES256-SHA384,ECDH-RSA-AES256-SHA384,DHE-DSS-AES256-GCM-SHA384,DHE-DSS-AES256-SHA256,AES256-GCM-SHA384,AES256-SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES128-SHA256,ECDHE-RSA-AES128-SHA256,ECDH-ECDSA-AES128-GCM-SHA256,ECDH-RSA-AES128-GCM-SHA256,ECDH-ECDSA-AES128-SHA256,ECDH-RSA-AES128-SHA256,DHE-DSS-AES128-GCM-SHA256,DHE-DSS-AES128-SHA256,AES128-GCM-SHA256,AES128-SHA256,ECDHE-ECDSA-AES256-SHA,ECDHE-RSA-AES256-SHA,DHE-DSS-AES256-SHA,ECDH-ECDSA-AES256-SHA,ECDH-RSA-AES256-SHA,AES256-SHA,ECDHE-ECDSA-AES128-SHA,ECDHE-RSA-AES128-SHA,DHE-DSS-AES128-SHA,ECDH-ECDSA-AES128-SHA,ECDH-RSA-AES128-SHA,AES128-SHA
|
||||
|
||||
## Ciphers for TLS PSK.
|
||||
##
|
||||
## Note that 'lwm2m.dtls.ciphers' and 'lwm2m.dtls.psk_ciphers' cannot
|
||||
## be configured at the same time.
|
||||
## See 'https://tools.ietf.org/html/rfc4279#section-2'.
|
||||
#lwm2m.dtls.psk_ciphers = PSK-AES128-CBC-SHA,PSK-AES256-CBC-SHA,PSK-3DES-EDE-CBC-SHA,PSK-RC4-SHA
|
51
apps/emqx_lwm2m/include/emqx_lwm2m.hrl
Normal file
51
apps/emqx_lwm2m/include/emqx_lwm2m.hrl
Normal file
@ -0,0 +1,51 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(APP, emqx_lwm2m).
|
||||
|
||||
-record(coap_mqtt_auth, { clientid
|
||||
, username
|
||||
, password
|
||||
}).
|
||||
-record(lwm2m_context, { epn
|
||||
, location
|
||||
}).
|
||||
|
||||
-define(OMA_ALTER_PATH_RT, <<"\"oma.lwm2m\"">>).
|
||||
|
||||
-define(MQ_COMMAND_ID, <<"CmdID">>).
|
||||
-define(MQ_COMMAND, <<"requestID">>).
|
||||
-define(MQ_BASENAME, <<"BaseName">>).
|
||||
-define(MQ_ARGS, <<"Arguments">>).
|
||||
|
||||
-define(MQ_VALUE_TYPE, <<"ValueType">>).
|
||||
-define(MQ_VALUE, <<"Value">>).
|
||||
-define(MQ_ERROR, <<"Error">>).
|
||||
-define(MQ_RESULT, <<"Result">>).
|
||||
|
||||
-define(ERR_NO_XML, <<"No XML Definition">>).
|
||||
-define(ERR_NOT_ACCEPTABLE, <<"Not Acceptable">>).
|
||||
-define(ERR_METHOD_NOT_ALLOWED, <<"Method Not Allowed">>).
|
||||
-define(ERR_NOT_FOUND, <<"Not Found">>).
|
||||
-define(ERR_UNAUTHORIZED, <<"Unauthorized">>).
|
||||
-define(ERR_BAD_REQUEST, <<"Bad Request">>).
|
||||
|
||||
|
||||
-define(LWM2M_FORMAT_PLAIN_TEXT, 0).
|
||||
-define(LWM2M_FORMAT_LINK, 40).
|
||||
-define(LWM2M_FORMAT_OPAQUE, 42).
|
||||
-define(LWM2M_FORMAT_TLV, 11542).
|
||||
-define(LWMWM_FORMAT_JSON, 11543).
|
128
apps/emqx_lwm2m/integration_test/Makefile
Normal file
128
apps/emqx_lwm2m/integration_test/Makefile
Normal file
@ -0,0 +1,128 @@
|
||||
|
||||
.PHONY: clean, clean_result, start_broker stop_broker case1 case2 case3
|
||||
|
||||
EMQX_DIR = emqx-enterprise-rel
|
||||
EMQ = $(EMQX_DIR)/relx.config
|
||||
WAKAAMA = build_wakaama/lightclient
|
||||
PAHO_PYTHON = paho/mqtt/client.py
|
||||
|
||||
all: clean_result $(EMQ) $(WAKAAMA) $(PAHO_PYTHON) start_broker clean_result case1 case2 case3 stop_broker
|
||||
@echo " "
|
||||
@echo " test complete"
|
||||
@echo " "
|
||||
|
||||
clean_result:
|
||||
-rm -f case*.txt
|
||||
|
||||
start_broker:
|
||||
-rm -f $(EMQX_DIR)/_rel/emqx/log/*
|
||||
-$(EMQX_DIR)/_rel/emqx/bin/emqx stop
|
||||
sleep 3
|
||||
$(EMQX_DIR)/_rel/emqx/bin/emqx start
|
||||
sleep 1
|
||||
$(EMQX_DIR)/_rel/emqx/bin/emqx_ctl plugins load emqx_lwm2m
|
||||
|
||||
|
||||
stop_broker:
|
||||
-$(EMQX_DIR)/_rel/emqx/bin/emqx stop
|
||||
|
||||
|
||||
case1:
|
||||
-build_wakaama/lightclient -4 -n jXtestlwm2m &
|
||||
python case1.py
|
||||
-ps aux|grep lightclient|awk '{print $$2}'|xargs kill -2
|
||||
|
||||
case2:
|
||||
-build_wakaama/lightclient -4 -n jXtestlwm2m &
|
||||
python case2.py
|
||||
-ps aux|grep lightclient|awk '{print $$2}'|xargs kill -2
|
||||
|
||||
case3:
|
||||
-build_wakaama/lightclient -4 -n jXtestlwm2m &
|
||||
python case3.py
|
||||
-ps aux|grep lightclient|awk '{print $$2}'|xargs kill -2
|
||||
|
||||
|
||||
$(EMQ):
|
||||
git clone https://hub.fastgit.org/fastdgiot/emqx-enterprise-rel
|
||||
git clone https://hub.fastgit.org/fastdgiot/emqx-lwm2m.git
|
||||
@echo "update emqx-lwm2m with this development code"
|
||||
mv emqx-lwm2m emqx_lwm2m
|
||||
-rm -rf emqx_lwm2m/etc
|
||||
-rm -rf emqx_lwm2m/include
|
||||
-rm -rf emqx_lwm2m/priv
|
||||
-rm -rf emqx_lwm2m/src
|
||||
-rm -rf emqx_lwm2m/Makefile
|
||||
-rm -rf emqx_lwm2m/erlang.mk
|
||||
cp -rf ../etc emqx_lwm2m/
|
||||
cp -rf ../include emqx_lwm2m/
|
||||
cp -rf ../priv emqx_lwm2m/
|
||||
cp -rf ../src emqx_lwm2m/
|
||||
cp -rf ../Makefile emqx_lwm2m/Makefile
|
||||
cp -rf ../erlang.mk emqx_lwm2m/erlang.mk
|
||||
-mkdir $(EMQX_DIR)/deps
|
||||
mv emqx_lwm2m $(EMQX_DIR)/deps/
|
||||
@echo "start building ..."
|
||||
python insert_lwm2m_plugin.py
|
||||
make -C emqx-rel -f Makefile
|
||||
-cp -rf ../lwm2m_xml $(EMQX_DIR)/_rel/emqx/etc/
|
||||
|
||||
|
||||
w:
|
||||
cd build_wakaama && cmake -DCMAKE_BUILD_TYPE=Debug ../wakaama/examples/lightclient && make
|
||||
|
||||
$(WAKAAMA):
|
||||
git clone https://github.com/eclipse/wakaama
|
||||
-mkdir build_wakaama
|
||||
# replace lightclient's source code, change port from 5683 to 5683, since 5683 is the default port of emqx-lwm2m
|
||||
cp -f object_security.c wakaama/examples/lightclient/object_security.c
|
||||
cd build_wakaama && cmake -DCMAKE_BUILD_TYPE=Debug ../wakaama/examples/lightclient && make
|
||||
|
||||
|
||||
mqtt: $(PAHO_PYTHON)
|
||||
# short for paho python client
|
||||
|
||||
$(PAHO_PYTHON):
|
||||
git clone https://github.com/eclipse/paho.mqtt.python.git
|
||||
mv paho.mqtt.python/src/paho ./
|
||||
rm -rf paho.mqtt.python
|
||||
|
||||
|
||||
r: rebuild_emq
|
||||
# r short for rebuild_emq
|
||||
@echo " rebuild complete "
|
||||
|
||||
|
||||
rebuild_emq:
|
||||
-$(EMQX_DIR)/_rel/emqx/bin/emqx stop
|
||||
-mkdir $(EMQX_DIR)/deps
|
||||
-rm -rf $(EMQX_DIR)/deps/emqx_lwm2m/etc
|
||||
-rm -rf $(EMQX_DIR)/deps/emqx_lwm2m/include
|
||||
-rm -rf $(EMQX_DIR)/deps/emqx_lwm2m/priv
|
||||
-rm -rf $(EMQX_DIR)/deps/emqx_lwm2m/src
|
||||
-rm -rf $(EMQX_DIR)/deps/emqx_lwm2m/Makefile
|
||||
-rm -rf $(EMQX_DIR)/deps/emqx_lwm2m/erlang.mk
|
||||
cp -rf ../etc $(EMQX_DIR)/deps/emqx_lwm2m/
|
||||
cp -rf ../include $(EMQX_DIR)/deps/emqx_lwm2m/
|
||||
cp -rf ../priv $(EMQX_DIR)/deps/emqx_lwm2m/
|
||||
cp -rf ../src $(EMQX_DIR)/deps/emqx_lwm2m/
|
||||
cp -rf ../Makefile $(EMQX_DIR)/deps/emqx_lwm2m/Makefile
|
||||
cp -rf ../erlang.mk $(EMQX_DIR)/deps/emqx_lwm2m/erlang.mk
|
||||
make -C $(EMQX_DIR) -f Makefile
|
||||
|
||||
|
||||
clean: clean_result
|
||||
-rm -f client/*.exe
|
||||
-rm -f client/*.o
|
||||
-rm -rf emqx-rel
|
||||
-rm -rf build_wakaama
|
||||
-rm -rf wakaama
|
||||
|
||||
|
||||
|
||||
lazy: clean_result start_broker case1 case2 case3 stop_broker
|
||||
# custom your command here
|
||||
@echo "you are so lazy"
|
||||
|
||||
|
||||
|
65
apps/emqx_lwm2m/integration_test/case1.py
Normal file
65
apps/emqx_lwm2m/integration_test/case1.py
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
|
||||
|
||||
import sys, time
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
|
||||
quit_now = False
|
||||
DeviceId = "jXtestlwm2m"
|
||||
test_step = 0
|
||||
|
||||
def on_connect(mqttc, userdata, flags, rc):
|
||||
global DeviceId
|
||||
json = "{\"CmdID\":5,\"Command\":\"Discover\",\"BaseName\":\"/3/0\"}"
|
||||
mqttc.publish("lwm2m/"+DeviceId+"/command", json)
|
||||
|
||||
def on_message(mqttc, userdata, msg):
|
||||
global quit_now, conclusion, test_step
|
||||
if msg:
|
||||
print(["incoming message topic ", msg.topic, " payload ", msg.payload])
|
||||
|
||||
if msg.topic == 'lwm2m/jXtestlwm2m/response':
|
||||
if test_step == 0:
|
||||
if msg.payload == '{"CmdID":5,"Command":"Discover","Result":"</3/0>,</3/0/0>,</3/0/1>,</3/0/16>,</3/0/4>"}':
|
||||
json = "{\"CmdID\":6,\"Command\":\"Read\",\"BaseName\":\"/3/0\"}"
|
||||
mqttc.publish("lwm2m/"+DeviceId+"/command", json)
|
||||
test_step = 1
|
||||
elif test_step == 1:
|
||||
if msg.payload == '{"CmdID":6,"Command":"Read","Result":{"bn":"/3/0","e":[{"n":"0","sv":"Open Mobile Alliance"},{"n":"1","sv":"Lightweight M2M Client"},{"n":"16","sv":"U"}]}}':
|
||||
test_step = 2
|
||||
quit_now = True
|
||||
|
||||
|
||||
def on_publish(mqttc, userdata, mid):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
global DeviceId, test_step
|
||||
timeout = 7
|
||||
mqttc = mqtt.Client("test_coap_lwm2m_c02334")
|
||||
mqttc.on_message = on_message
|
||||
mqttc.on_publish = on_publish
|
||||
mqttc.on_connect = on_connect
|
||||
|
||||
mqttc.connect("127.0.0.1", 1883, 120)
|
||||
mqttc.subscribe("lwm2m/"+DeviceId+"/response", qos=1)
|
||||
mqttc.loop_start()
|
||||
while quit_now == False and timeout > 0:
|
||||
time.sleep(1)
|
||||
timeout = timeout - 1
|
||||
mqttc.disconnect()
|
||||
mqttc.loop_stop()
|
||||
if test_step == 2:
|
||||
print("\n\n CASE1 PASS\n\n")
|
||||
else:
|
||||
print("\n\n CASE1 FAIL\n\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
60
apps/emqx_lwm2m/integration_test/case2.py
Normal file
60
apps/emqx_lwm2m/integration_test/case2.py
Normal file
@ -0,0 +1,60 @@
|
||||
|
||||
|
||||
|
||||
import sys, time
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
|
||||
quit_now = False
|
||||
DeviceId = "jXtestlwm2m"
|
||||
test_step = 0
|
||||
|
||||
def on_connect(mqttc, userdata, flags, rc):
|
||||
global DeviceId
|
||||
json = "{\"CmdID\":5,\"Command\":\"Execute\",\"BaseName\":\"/3/0/4\"}"
|
||||
mqttc.publish("lwm2m/"+DeviceId+"/command", json)
|
||||
|
||||
def on_message(mqttc, userdata, msg):
|
||||
global quit_now, conclusion, test_step
|
||||
if msg:
|
||||
print(["incoming message topic ", msg.topic, " payload ", msg.payload])
|
||||
|
||||
if msg.topic == 'lwm2m/jXtestlwm2m/response':
|
||||
if test_step == 0:
|
||||
if msg.payload == '{"CmdID":5,"Command":"Execute","Result":"Changed"}':
|
||||
test_step = 1
|
||||
quit_now = True
|
||||
|
||||
|
||||
def on_publish(mqttc, userdata, mid):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
global DeviceId, test_step
|
||||
timeout = 7
|
||||
mqttc = mqtt.Client("test_coap_lwm2m_c02334")
|
||||
mqttc.on_message = on_message
|
||||
mqttc.on_publish = on_publish
|
||||
mqttc.on_connect = on_connect
|
||||
|
||||
mqttc.connect("127.0.0.1", 1883, 120)
|
||||
mqttc.subscribe("lwm2m/"+DeviceId+"/response", qos=1)
|
||||
mqttc.loop_start()
|
||||
while quit_now == False and timeout > 0:
|
||||
time.sleep(1)
|
||||
timeout = timeout - 1
|
||||
mqttc.disconnect()
|
||||
mqttc.loop_stop()
|
||||
if test_step == 1:
|
||||
print("\n\n CASE2 PASS\n\n")
|
||||
else:
|
||||
print("\n\n CASE2 FAIL\n\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
60
apps/emqx_lwm2m/integration_test/case3.py
Normal file
60
apps/emqx_lwm2m/integration_test/case3.py
Normal file
@ -0,0 +1,60 @@
|
||||
|
||||
|
||||
|
||||
import sys, time
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
|
||||
quit_now = False
|
||||
DeviceId = "jXtestlwm2m"
|
||||
test_step = 0
|
||||
|
||||
def on_connect(mqttc, userdata, flags, rc):
|
||||
global DeviceId
|
||||
json = '{"CmdID":5,"Command":"Write","Value":{"bn":"/1/0/1","e":[{"v":123}]}}'
|
||||
mqttc.publish("lwm2m/"+DeviceId+"/command", json)
|
||||
|
||||
def on_message(mqttc, userdata, msg):
|
||||
global quit_now, conclusion, test_step
|
||||
if msg:
|
||||
print(["incoming message topic ", msg.topic, " payload ", msg.payload])
|
||||
|
||||
if msg.topic == 'lwm2m/jXtestlwm2m/response':
|
||||
if test_step == 0:
|
||||
if msg.payload == '{"CmdID":5,"Command":"Write","Result":"Changed"}':
|
||||
test_step = 1
|
||||
quit_now = True
|
||||
|
||||
|
||||
def on_publish(mqttc, userdata, mid):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
global DeviceId, test_step
|
||||
timeout = 7
|
||||
mqttc = mqtt.Client("test_coap_lwm2m_c02334")
|
||||
mqttc.on_message = on_message
|
||||
mqttc.on_publish = on_publish
|
||||
mqttc.on_connect = on_connect
|
||||
|
||||
mqttc.connect("127.0.0.1", 1883, 120)
|
||||
mqttc.subscribe("lwm2m/"+DeviceId+"/response", qos=1)
|
||||
mqttc.loop_start()
|
||||
while quit_now == False and timeout > 0:
|
||||
time.sleep(1)
|
||||
timeout = timeout - 1
|
||||
mqttc.disconnect()
|
||||
mqttc.loop_stop()
|
||||
if test_step == 1:
|
||||
print("\n\n CASE3 PASS\n\n")
|
||||
else:
|
||||
print("\n\n CASE3 FAIL\n\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
51
apps/emqx_lwm2m/integration_test/insert_lwm2m_plugin.py
Normal file
51
apps/emqx_lwm2m/integration_test/insert_lwm2m_plugin.py
Normal file
@ -0,0 +1,51 @@
|
||||
|
||||
|
||||
def change_makefile():
|
||||
f = open("emqx-rel/Makefile", "rb")
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
if data.find("emqx_lwm2m") < 0:
|
||||
data = data.replace("emqx_auth_pgsql emqx_auth_mongo", "emqx_auth_pgsql emqx_auth_mongo emqx_lwm2m\n\ndep_emqx_lwm2m = git https://hub.fastgit.org/fastdgiot/emqx-lwm2m\n\n")
|
||||
f = open("emqx-rel/Makefile", "wb")
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
|
||||
f = open("emqx-rel/relx.config", "rb")
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
if data.find("emq_lwm2m") < 0:
|
||||
f = open("emqx-rel/relx.config", "wb")
|
||||
data = data.replace("{emqx_auth_mongo, load}", "{emqx_auth_mongo, load},\n{emqx_lwm2m, load}")
|
||||
data = data.replace('{template, "rel/conf/emqx.conf", "etc/emqx.conf"},', \
|
||||
'{template, "rel/conf/emqx.conf", "etc/emqx.conf"},'+ \
|
||||
'\n {template, "rel/conf/plugins/emqx_lwm2m.conf", "etc/plugins/emqx_lwm2m.conf"},'+ \
|
||||
'\n {copy, "deps/emqx_lwm2m/lwm2m_xml", "etc/"},')
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
|
||||
|
||||
def change_lwm2m_config():
|
||||
f = open("emqx-rel/deps/emqx_lwm2m/etc/emqx_lwm2m.conf", "rb")
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
if data.find("5683") > 0:
|
||||
data = data.replace("5683", "5683")
|
||||
f = open("emqx-rel/deps/emqx_lwm2m/etc/emqx_lwm2m.conf", "wb")
|
||||
f.write(data)
|
||||
f.close()
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
change_makefile()
|
||||
change_lwm2m_config()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
253
apps/emqx_lwm2m/integration_test/object_security.c
Normal file
253
apps/emqx_lwm2m/integration_test/object_security.c
Normal file
@ -0,0 +1,253 @@
|
||||
/*******************************************************************************
|
||||
*
|
||||
* Copyright (c) 2013, 2014, 2015 Intel Corporation and others.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* and Eclipse Distribution License v1.0 which accompany this distribution.
|
||||
*
|
||||
* The Eclipse Public License is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
* The Eclipse Distribution License is available at
|
||||
* http://www.eclipse.org/org/documents/edl-v10.php.
|
||||
*
|
||||
* Contributors:
|
||||
* David Navarro, Intel Corporation - initial API and implementation
|
||||
* Bosch Software Innovations GmbH - Please refer to git log
|
||||
* Pascal Rieux - Please refer to git log
|
||||
*
|
||||
*******************************************************************************/
|
||||
|
||||
/*
|
||||
* Resources:
|
||||
*
|
||||
* Name | ID | Operations | Instances | Mandatory | Type | Range | Units |
|
||||
* Server URI | 0 | | Single | Yes | String | | |
|
||||
* Bootstrap Server | 1 | | Single | Yes | Boolean | | |
|
||||
* Security Mode | 2 | | Single | Yes | Integer | 0-3 | |
|
||||
* Public Key or ID | 3 | | Single | Yes | Opaque | | |
|
||||
* Server Public Key or ID | 4 | | Single | Yes | Opaque | | |
|
||||
* Secret Key | 5 | | Single | Yes | Opaque | | |
|
||||
* SMS Security Mode | 6 | | Single | Yes | Integer | 0-255 | |
|
||||
* SMS Binding Key Param. | 7 | | Single | Yes | Opaque | 6 B | |
|
||||
* SMS Binding Secret Keys | 8 | | Single | Yes | Opaque | 32-48 B | |
|
||||
* Server SMS Number | 9 | | Single | Yes | Integer | | |
|
||||
* Short Server ID | 10 | | Single | No | Integer | 1-65535 | |
|
||||
* Client Hold Off Time | 11 | | Single | Yes | Integer | | s |
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Here we implement a very basic LWM2M Security Object which only knows NoSec security mode.
|
||||
*/
|
||||
|
||||
#include "liblwm2m.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
|
||||
typedef struct _security_instance_
|
||||
{
|
||||
struct _security_instance_ * next; // matches lwm2m_list_t::next
|
||||
uint16_t instanceId; // matches lwm2m_list_t::id
|
||||
char * uri;
|
||||
bool isBootstrap;
|
||||
uint16_t shortID;
|
||||
uint32_t clientHoldOffTime;
|
||||
} security_instance_t;
|
||||
|
||||
static uint8_t prv_get_value(lwm2m_data_t * dataP,
|
||||
security_instance_t * targetP)
|
||||
{
|
||||
|
||||
switch (dataP->id)
|
||||
{
|
||||
case LWM2M_SECURITY_URI_ID:
|
||||
lwm2m_data_encode_string(targetP->uri, dataP);
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_BOOTSTRAP_ID:
|
||||
lwm2m_data_encode_bool(targetP->isBootstrap, dataP);
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_SECURITY_ID:
|
||||
lwm2m_data_encode_int(LWM2M_SECURITY_MODE_NONE, dataP);
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_PUBLIC_KEY_ID:
|
||||
// Here we return an opaque of 1 byte containing 0
|
||||
{
|
||||
uint8_t value = 0;
|
||||
|
||||
lwm2m_data_encode_opaque(&value, 1, dataP);
|
||||
}
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_SERVER_PUBLIC_KEY_ID:
|
||||
// Here we return an opaque of 1 byte containing 0
|
||||
{
|
||||
uint8_t value = 0;
|
||||
|
||||
lwm2m_data_encode_opaque(&value, 1, dataP);
|
||||
}
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_SECRET_KEY_ID:
|
||||
// Here we return an opaque of 1 byte containing 0
|
||||
{
|
||||
uint8_t value = 0;
|
||||
|
||||
lwm2m_data_encode_opaque(&value, 1, dataP);
|
||||
}
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_SMS_SECURITY_ID:
|
||||
lwm2m_data_encode_int(LWM2M_SECURITY_MODE_NONE, dataP);
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_SMS_KEY_PARAM_ID:
|
||||
// Here we return an opaque of 6 bytes containing a buggy value
|
||||
{
|
||||
char * value = "12345";
|
||||
lwm2m_data_encode_opaque((uint8_t *)value, 6, dataP);
|
||||
}
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_SMS_SECRET_KEY_ID:
|
||||
// Here we return an opaque of 32 bytes containing a buggy value
|
||||
{
|
||||
char * value = "1234567890abcdefghijklmnopqrstu";
|
||||
lwm2m_data_encode_opaque((uint8_t *)value, 32, dataP);
|
||||
}
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_SMS_SERVER_NUMBER_ID:
|
||||
lwm2m_data_encode_int(0, dataP);
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_SHORT_SERVER_ID:
|
||||
lwm2m_data_encode_int(targetP->shortID, dataP);
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
case LWM2M_SECURITY_HOLD_OFF_ID:
|
||||
lwm2m_data_encode_int(targetP->clientHoldOffTime, dataP);
|
||||
return COAP_205_CONTENT;
|
||||
|
||||
default:
|
||||
return COAP_404_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t prv_security_read(uint16_t instanceId,
|
||||
int * numDataP,
|
||||
lwm2m_data_t ** dataArrayP,
|
||||
lwm2m_object_t * objectP)
|
||||
{
|
||||
security_instance_t * targetP;
|
||||
uint8_t result;
|
||||
int i;
|
||||
|
||||
targetP = (security_instance_t *)lwm2m_list_find(objectP->instanceList, instanceId);
|
||||
if (NULL == targetP) return COAP_404_NOT_FOUND;
|
||||
|
||||
// is the server asking for the full instance ?
|
||||
if (*numDataP == 0)
|
||||
{
|
||||
uint16_t resList[] = {LWM2M_SECURITY_URI_ID,
|
||||
LWM2M_SECURITY_BOOTSTRAP_ID,
|
||||
LWM2M_SECURITY_SECURITY_ID,
|
||||
LWM2M_SECURITY_PUBLIC_KEY_ID,
|
||||
LWM2M_SECURITY_SERVER_PUBLIC_KEY_ID,
|
||||
LWM2M_SECURITY_SECRET_KEY_ID,
|
||||
LWM2M_SECURITY_SMS_SECURITY_ID,
|
||||
LWM2M_SECURITY_SMS_KEY_PARAM_ID,
|
||||
LWM2M_SECURITY_SMS_SECRET_KEY_ID,
|
||||
LWM2M_SECURITY_SMS_SERVER_NUMBER_ID,
|
||||
LWM2M_SECURITY_SHORT_SERVER_ID,
|
||||
LWM2M_SECURITY_HOLD_OFF_ID};
|
||||
int nbRes = sizeof(resList)/sizeof(uint16_t);
|
||||
|
||||
*dataArrayP = lwm2m_data_new(nbRes);
|
||||
if (*dataArrayP == NULL) return COAP_500_INTERNAL_SERVER_ERROR;
|
||||
*numDataP = nbRes;
|
||||
for (i = 0 ; i < nbRes ; i++)
|
||||
{
|
||||
(*dataArrayP)[i].id = resList[i];
|
||||
}
|
||||
}
|
||||
|
||||
i = 0;
|
||||
do
|
||||
{
|
||||
result = prv_get_value((*dataArrayP) + i, targetP);
|
||||
i++;
|
||||
} while (i < *numDataP && result == COAP_205_CONTENT);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
lwm2m_object_t * get_security_object()
|
||||
{
|
||||
lwm2m_object_t * securityObj;
|
||||
|
||||
securityObj = (lwm2m_object_t *)lwm2m_malloc(sizeof(lwm2m_object_t));
|
||||
|
||||
if (NULL != securityObj)
|
||||
{
|
||||
security_instance_t * targetP;
|
||||
|
||||
memset(securityObj, 0, sizeof(lwm2m_object_t));
|
||||
|
||||
securityObj->objID = 0;
|
||||
|
||||
// Manually create an hardcoded instance
|
||||
targetP = (security_instance_t *)lwm2m_malloc(sizeof(security_instance_t));
|
||||
if (NULL == targetP)
|
||||
{
|
||||
lwm2m_free(securityObj);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memset(targetP, 0, sizeof(security_instance_t));
|
||||
targetP->instanceId = 0;
|
||||
targetP->uri = strdup("coap://localhost:5683");
|
||||
targetP->isBootstrap = false;
|
||||
targetP->shortID = 123;
|
||||
targetP->clientHoldOffTime = 10;
|
||||
|
||||
securityObj->instanceList = LWM2M_LIST_ADD(securityObj->instanceList, targetP);
|
||||
|
||||
securityObj->readFunc = prv_security_read;
|
||||
}
|
||||
|
||||
return securityObj;
|
||||
}
|
||||
|
||||
void free_security_object(lwm2m_object_t * objectP)
|
||||
{
|
||||
while (objectP->instanceList != NULL)
|
||||
{
|
||||
security_instance_t * securityInstance = (security_instance_t *)objectP->instanceList;
|
||||
objectP->instanceList = objectP->instanceList->next;
|
||||
if (NULL != securityInstance->uri)
|
||||
{
|
||||
lwm2m_free(securityInstance->uri);
|
||||
}
|
||||
lwm2m_free(securityInstance);
|
||||
}
|
||||
lwm2m_free(objectP);
|
||||
}
|
||||
|
||||
char * get_server_uri(lwm2m_object_t * objectP,
|
||||
uint16_t secObjInstID)
|
||||
{
|
||||
security_instance_t * targetP = (security_instance_t *)LWM2M_LIST_FIND(objectP->instanceList, secObjInstID);
|
||||
|
||||
if (NULL != targetP)
|
||||
{
|
||||
return lwm2m_strdup(targetP->uri);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
128
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Access_Control-v1_0_1.xml
Normal file
128
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Access_Control-v1_0_1.xml
Normal file
@ -0,0 +1,128 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
FILE INFORMATION
|
||||
|
||||
OMA Permanent Document
|
||||
File: OMA-SUP-XML_LWM2M_Access_Control-V1_0_1-20170704-A
|
||||
Type: xml
|
||||
|
||||
Public Reachable Information
|
||||
Path: http://www.openmobilealliance.org/tech/profiles
|
||||
Name: LWM2M_Access_Control-v1_0_1.xml
|
||||
|
||||
NORMATIVE INFORMATION
|
||||
|
||||
Information about this file can be found in the latest revision of
|
||||
|
||||
OMA-TS-LightweightM2M-V1_0_1
|
||||
|
||||
This is available at http://www.openmobilealliance.org/
|
||||
|
||||
Send comments to technical-comments@mail.openmobilealliance.org
|
||||
|
||||
CHANGE HISTORY
|
||||
|
||||
08022017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0009-INP_LightweightM2M-V1_0_ERP_for_Final_Approval
|
||||
04072017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0029R01-INP_LightweightM2M_V1.0.1_ERP_for_Final_Approval_Notification
|
||||
|
||||
LEGAL DISCLAIMER
|
||||
|
||||
Copyright 2017 Open Mobile Alliance All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The above license is used as a license under copyright only. Please
|
||||
reference the OMA IPR Policy for patent licensing terms:
|
||||
http://www.openmobilealliance.org/ipr.html
|
||||
-->
|
||||
|
||||
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://openmobilealliance.org/tech/profiles/LWM2M.xsd">
|
||||
<Object ObjectType="MODefinition">
|
||||
<Name>LwM2M Access Control</Name>
|
||||
<Description1><![CDATA[Access Control Object is used to check whether the LwM2M Server has access right for performing an operation.]]></Description1>
|
||||
<ObjectID>2</ObjectID>
|
||||
<ObjectURN>urn:oma:lwm2m:oma:2</ObjectURN>
|
||||
<LWM2MVersion>1.0</LWM2MVersion>
|
||||
<ObjectVersion>1.0</ObjectVersion>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Resources>
|
||||
<Item ID="0">
|
||||
<Name>Object ID</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>1-65534</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Resources 0 and 1 point to the Object Instance for which the Instances of the ACL Resource of that Access Control Object Instance are applicable.]]></Description>
|
||||
</Item>
|
||||
<Item ID="1">
|
||||
<Name>Object Instance ID</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-65535</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[See above]]></Description>
|
||||
</Item>
|
||||
<Item ID="2">
|
||||
<Name>ACL</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>16-bit</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[The Resource Instance ID MUST be the Short Server ID of a certain LwM2M Server for which associated access rights are contained in the Resource Instance value.
|
||||
The Resource Instance ID 0 is a specific ID, determining the ACL Instance which contains the default access rights.
|
||||
Each bit set in the Resource Instance value, grants an access right to the LwM2M Server to the corresponding operation.
|
||||
The bit order is specified as below.
|
||||
1st LSB: R(Read, Observe, Write-Attributes)
|
||||
2nd LSB: W(Write)
|
||||
3rd LSB: E(Execute)
|
||||
4th LSB: D(Delete)
|
||||
5th LSB: C(Create)
|
||||
Other bits are reserved for future use.]]></Description>
|
||||
</Item>
|
||||
<Item ID="3">
|
||||
<Name>Access Control Owner</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-65535</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Short Server ID of a certain LwM2M Server; only such an LwM2M Server can manage the Resources of this Object Instance.
|
||||
The specific value MAX_ID=65535 means this Access Control Object Instance is created and modified during a Bootstrap phase only.]]></Description>
|
||||
</Item>
|
||||
</Resources>
|
||||
<Description2><![CDATA[]]></Description2>
|
||||
</Object>
|
||||
</LWM2M>
|
@ -0,0 +1,164 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
FILE INFORMATION
|
||||
|
||||
OMA Permanent Document
|
||||
File: OMA-SUP-XML_LWM2M_Connectivity_Statistics-V1_0_1-20170704-A
|
||||
Type: xml
|
||||
|
||||
Public Reachable Information
|
||||
Path: http://www.openmobilealliance.org/tech/profiles
|
||||
Name: LWM2M_Connectivity_Statistics-v1_0_1.xml
|
||||
|
||||
NORMATIVE INFORMATION
|
||||
|
||||
Information about this file can be found in the latest revision of
|
||||
|
||||
OMA-TS-LightweightM2M-V1_0_1
|
||||
|
||||
This is available at http://www.openmobilealliance.org/
|
||||
|
||||
Send comments to technical-comments@mail.openmobilealliance.org
|
||||
|
||||
CHANGE HISTORY
|
||||
|
||||
08022017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0009-INP_LightweightM2M-V1_0_ERP_for_Final_Approval
|
||||
04072017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0029R01-INP_LightweightM2M_V1.0.1_ERP_for_Final_Approval_Notification
|
||||
|
||||
LEGAL DISCLAIMER
|
||||
|
||||
Copyright 2017 Open Mobile Alliance All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The above license is used as a license under copyright only. Please
|
||||
reference the OMA IPR Policy for patent licensing terms:
|
||||
http://www.openmobilealliance.org/ipr.html
|
||||
-->
|
||||
|
||||
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://openmobilealliance.org/tech/profiles/LWM2M.xsd">
|
||||
<Object ObjectType="MODefinition">
|
||||
<Name>Connectivity Statistics</Name>
|
||||
<Description1><![CDATA[This LwM2M Objects enables client to collect statistical information and enables the LwM2M Server to retrieve these information, set the collection duration and reset the statistical parameters.]]></Description1>
|
||||
<ObjectID>7</ObjectID>
|
||||
<ObjectURN>urn:oma:lwm2m:oma:7</ObjectURN>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Resources>
|
||||
<Item ID="0">
|
||||
<Name>SMS Tx Counter</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Indicate the total number of SMS successfully transmitted during the collection period.]]></Description>
|
||||
</Item>
|
||||
<Item ID="1">
|
||||
<Name>SMS Rx Counter</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Indicate the total number of SMS successfully received during the collection period.]]></Description>
|
||||
</Item>
|
||||
<Item ID="2">
|
||||
<Name>Tx Data</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>Kilo-Bytes</Units>
|
||||
<Description><![CDATA[Indicate the total amount of IP data transmitted during the collection period.]]></Description>
|
||||
</Item>
|
||||
<Item ID="3">
|
||||
<Name>Rx Data</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>Kilo-Bytes</Units>
|
||||
<Description><![CDATA[Indicate the total amount of IP data received during the collection period.]]></Description>
|
||||
</Item>
|
||||
<Item ID="4">
|
||||
<Name>Max Message Size</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>Byte</Units>
|
||||
<Description><![CDATA[The maximum IP message size that is used during the collection period.]]></Description>
|
||||
</Item>
|
||||
<Item ID="5">
|
||||
<Name>Average Message Size</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>Byte</Units>
|
||||
<Description><![CDATA[The average IP message size that is used during the collection period.]]></Description>
|
||||
</Item>
|
||||
<Item ID="6">
|
||||
<Name>Start</Name>
|
||||
<Operations>E</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type></Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Reset resources 0-5 to 0 and start to collect information, If resource 8 (Collection Period) value is 0, the client will keep collecting information until resource 7 (Stop) is executed, otherwise the client will stop collecting information after specified period ended.
|
||||
Note:When reporting the Tx Data or Rx Data, the LwM2M Client reports the total KB transmitted/received over IP bearer(s), including all protocol header bytes up to and including the IP header. This does not include lower level retransmissions/optimizations (e.g. RAN, header compression) or SMS messages.]]></Description>
|
||||
</Item>
|
||||
<Item ID="7"><Name>Stop</Name>
|
||||
<Operations>E</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type></Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Stop collecting information, but do not reset resources 0-5.]]></Description>
|
||||
</Item>
|
||||
<Item ID="8"><Name>Collection Period</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>Seconds</Units>
|
||||
<Description><![CDATA[The default collection period in seconds. The value 0 indicates that the collection period is not set.]]></Description>
|
||||
</Item></Resources>
|
||||
<Description2><![CDATA[Note: When reporting the Tx Data or Rx Data, the LwM2M Client reports the total KB transmitted/received over IP bearer(s), including all protocol header bytes up to and including the IP header. This does not include lower level retransmissions/optimizations (e.g. RAN, header compression) or SMS messages.]]></Description2>
|
||||
</Object>
|
||||
</LWM2M>
|
330
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Device-v1_0_1.xml
Normal file
330
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Device-v1_0_1.xml
Normal file
@ -0,0 +1,330 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
FILE INFORMATION
|
||||
|
||||
OMA Permanent Document
|
||||
File: OMA-SUP-XML_LWM2M_Device-V1_0_1-20170704-A
|
||||
Type: xml
|
||||
|
||||
Public Reachable Information
|
||||
Path: http://www.openmobilealliance.org/tech/profiles
|
||||
Name: LWM2M_Device-v1_0_1.xml
|
||||
|
||||
NORMATIVE INFORMATION
|
||||
|
||||
Information about this file can be found in the latest revision of
|
||||
|
||||
OMA-TS-LightweightM2M-V1_0_1
|
||||
|
||||
This is available at http://www.openmobilealliance.org/
|
||||
|
||||
Send comments to technical-comments@mail.openmobilealliance.org
|
||||
|
||||
CHANGE HISTORY
|
||||
|
||||
08022017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0009-INP_LightweightM2M-V1_0_ERP_for_Final_Approval
|
||||
04072017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0029R01-INP_LightweightM2M_V1.0.1_ERP_for_Final_Approval_Notification
|
||||
|
||||
LEGAL DISCLAIMER
|
||||
|
||||
Copyright 2017 Open Mobile Alliance All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The above license is used as a license under copyright only. Please
|
||||
reference the OMA IPR Policy for patent licensing terms:
|
||||
http://www.openmobilealliance.org/ipr.html
|
||||
-->
|
||||
|
||||
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://openmobilealliance.org/tech/profiles/LWM2M.xsd">
|
||||
<Object ObjectType="MODefinition">
|
||||
<Name>Device</Name>
|
||||
<Description1><![CDATA[This LwM2M Object provides a range of device related information which can be queried by the LwM2M Server, and a device reboot and factory reset function.]]></Description1>
|
||||
<ObjectID>3</ObjectID>
|
||||
<ObjectURN>urn:oma:lwm2m:oma:3</ObjectURN>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Resources>
|
||||
<Item ID="0">
|
||||
<Name>Manufacturer</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Human readable manufacturer name]]></Description>
|
||||
</Item>
|
||||
<Item ID="1">
|
||||
<Name>Model Number</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[A model identifier (manufacturer specified string)]]></Description>
|
||||
</Item>
|
||||
<Item ID="2">
|
||||
<Name>Serial Number</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Serial Number]]></Description>
|
||||
</Item>
|
||||
<Item ID="3">
|
||||
<Name>Firmware Version</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Current firmware version of the Device.The Firmware Management function could rely on this resource.]]></Description>
|
||||
</Item>
|
||||
<Item ID="4">
|
||||
<Name>Reboot</Name>
|
||||
<Operations>E</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type></Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Reboot the LwM2M Device to restore the Device from unexpected firmware failure.]]></Description>
|
||||
</Item>
|
||||
<Item ID="5">
|
||||
<Name>Factory Reset</Name>
|
||||
<Operations>E</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type></Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Perform factory reset of the LwM2M Device to make the LwM2M Device to go through initial deployment sequence where provisioning and bootstrap sequence is performed. This requires client ensuring post factory reset to have minimal information to allow it to carry out one of the bootstrap methods specified in section 5.2.3.
|
||||
When this Resource is executed, “De-register” operation MAY be sent to the LwM2M Server(s) before factory reset of the LwM2M Device.]]></Description>
|
||||
</Item>
|
||||
<Item ID="6">
|
||||
<Name>Available Power Sources</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-7</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[0 – DC power
|
||||
1 – Internal Battery
|
||||
2 – External Battery
|
||||
4 – Power over Ethernet
|
||||
5 – USB
|
||||
6 – AC (Mains) power
|
||||
7 – Solar
|
||||
The same Resource Instance ID MUST be used to associate a given Power Source (Resource ID:6) with its Present Voltage (Resource ID:7) and its Present Current (Resource ID:8)]]></Description>
|
||||
</Item>
|
||||
<Item ID="7">
|
||||
<Name>Power Source Voltage</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>mV</Units>
|
||||
<Description><![CDATA[Present voltage for each Available Power Sources Resource Instance.]]></Description>
|
||||
</Item>
|
||||
<Item ID="8">
|
||||
<Name>Power Source Current</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>mA</Units>
|
||||
<Description><![CDATA[Present current for each Available Power Source.]]></Description>
|
||||
</Item>
|
||||
<Item ID="9">
|
||||
<Name>Battery Level</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-100</RangeEnumeration>
|
||||
<Units>%</Units>
|
||||
<Description><![CDATA[Contains the current battery level as a percentage (with a range from 0 to 100). This value is only valid for the Device internal Battery if present (one Available Power Sources Resource Instance is 1).]]></Description>
|
||||
</Item>
|
||||
<Item ID="10">
|
||||
<Name>Memory Free</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>KB</Units>
|
||||
<Description><![CDATA[Estimated current available amount of storage space which can store data and software in the LwM2M Device (expressed in kilobytes).]]></Description>
|
||||
</Item>
|
||||
<Item ID="11">
|
||||
<Name>Error Code</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-8</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[0=No error
|
||||
1=Low battery power
|
||||
2=External power supply off
|
||||
3=GPS module failure
|
||||
4=Low received signal strength
|
||||
5=Out of memory
|
||||
6=SMS failure
|
||||
7=IP connectivity failure
|
||||
8=Peripheral malfunction
|
||||
|
||||
When the single Device Object Instance is initiated, there is only one error code Resource Instance whose value is equal to 0 that means no error. When the first error happens, the LwM2M Client changes error code Resource Instance to any non-zero value to indicate the error type. When any other error happens, a new error code Resource Instance is created. When an error associated with a Resource Instance is no longer present, that Resource Instance is deleted. When the single existing error is no longer present, the LwM2M Client returns to the original no error state where Instance 0 has value 0.
|
||||
This error code Resource MAY be observed by the LwM2M Server. How to deal with LwM2M Client’s error report depends on the policy of the LwM2M Server.]]></Description>
|
||||
</Item>
|
||||
<Item ID="12">
|
||||
<Name>Reset Error Code</Name>
|
||||
<Operations>E</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type></Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Delete all error code Resource Instances and create only one zero-value error code that implies no error, then re-evaluate all error conditions and update and create Resources Instances to capture all current error conditions.]]></Description>
|
||||
</Item>
|
||||
<Item ID="13">
|
||||
<Name>Current Time</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Time</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Current UNIX time of the LwM2M Client.
|
||||
The LwM2M Client should be responsible to increase this time value as every second elapses.
|
||||
The LwM2M Server is able to write this Resource to make the LwM2M Client synchronized with the LwM2M Server.]]></Description>
|
||||
</Item>
|
||||
<Item ID="14">
|
||||
<Name>UTC Offset</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Indicates the UTC offset currently in effect for this LwM2M Device. UTC+X [ISO 8601].]]></Description>
|
||||
</Item>
|
||||
<Item ID="15">
|
||||
<Name>Timezone</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Indicates in which time zone the LwM2M Device is located, in IANA Timezone (TZ) database format.]]></Description>
|
||||
</Item>
|
||||
<Item ID="16">
|
||||
<Name>Supported Binding and Modes</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Indicates which bindings and modes are supported in the LwM2M Client. The possible values of Resource are combination of "U" or "UQ" and "S" or "SQ".]]></Description>
|
||||
</Item>
|
||||
<Item ID="17"><Name>Device Type</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Type of the device (manufacturer specified string: e.g., smart meters / dev Class…)]]></Description>
|
||||
</Item>
|
||||
<Item ID="18"><Name>Hardware Version</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Current hardware version of the device]]></Description>
|
||||
</Item>
|
||||
<Item ID="19"><Name>Software Version</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Current software version of the device (manufacturer specified string). On elaborated LwM2M device, SW could be split in 2 parts: a firmware one and a higher level software on top.
|
||||
Both pieces of Software are together managed by LwM2M Firmware Update Object (Object ID 5)]]></Description>
|
||||
</Item>
|
||||
<Item ID="20"><Name>Battery Status</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-6</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[This value is only valid for the Device Internal Battery if present (one Available Power Sources Resource Instance value is 1).
|
||||
Battery
|
||||
Status Meaning Description
|
||||
0 Normal The battery is operating normally and not on power.
|
||||
1 Charging The battery is currently charging.
|
||||
2 Charge Complete The battery is fully charged and still on power.
|
||||
3 Damaged The battery has some problem.
|
||||
4 Low Battery The battery is low on charge.
|
||||
5 Not Installed The battery is not installed.
|
||||
6 Unknown The battery information is not available.]]></Description>
|
||||
</Item>
|
||||
<Item ID="21"><Name>Memory Total</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Total amount of storage space which can store data and software in the LwM2M Device (expressed in kilobytes).]]></Description>
|
||||
</Item>
|
||||
<Item ID="22"><Name>ExtDevInfo</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Objlnk</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Reference to external “Device” object instance containing information. For example, such an external device can be a Host Device, which is a device into which the Device containing the LwM2M client is embedded. This Resource may be used to retrieve information about the Host Device.]]></Description>
|
||||
</Item></Resources>
|
||||
<Description2></Description2>
|
||||
</Object>
|
||||
</LWM2M>
|
202
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Firmware_Update-v1_0_1.xml
Normal file
202
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Firmware_Update-v1_0_1.xml
Normal file
@ -0,0 +1,202 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
FILE INFORMATION
|
||||
|
||||
OMA Permanent Document
|
||||
File: OMA-SUP-XML_LWM2M_Firmware_Update-V1_0_1-20170704-A
|
||||
Type: xml
|
||||
|
||||
Public Reachable Information
|
||||
Path: http://www.openmobilealliance.org/tech/profiles
|
||||
Name: LWM2M_Firmware_Update-v1_0_1.xml
|
||||
|
||||
NORMATIVE INFORMATION
|
||||
|
||||
Information about this file can be found in the latest revision of
|
||||
|
||||
OMA-TS-LightweightM2M-V1_0_1
|
||||
|
||||
This is available at http://www.openmobilealliance.org/
|
||||
|
||||
Send comments to technical-comments@mail.openmobilealliance.org
|
||||
|
||||
CHANGE HISTORY
|
||||
|
||||
08022017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0009-INP_LightweightM2M-V1_0_ERP_for_Final_Approval
|
||||
04072017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0029R01-INP_LightweightM2M_V1.0.1_ERP_for_Final_Approval_Notification
|
||||
|
||||
LEGAL DISCLAIMER
|
||||
|
||||
Copyright 2017 Open Mobile Alliance All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The above license is used as a license under copyright only. Please
|
||||
reference the OMA IPR Policy for patent licensing terms:
|
||||
http://www.openmobilealliance.org/ipr.html
|
||||
-->
|
||||
|
||||
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://openmobilealliance.org/tech/profiles/LWM2M.xsd">
|
||||
<Object ObjectType="MODefinition">
|
||||
<Name>Firmware Update</Name>
|
||||
<Description1><![CDATA[This LwM2M Object enables management of firmware which is to be updated. This Object includes installing firmware package, updating firmware, and performing actions after updating firmware. The firmware update MAY require to reboot the device; it will depend on a number of factors, such as the operating system architecture and the extent of the updated software.
|
||||
The envisioned functionality with LwM2M version 1.0 is to allow a LwM2M Client to connect to any LwM2M version 1.0 compliant Server to obtain a firmware imagine using the object and resource structure defined in this section experiencing communication security protection using DTLS. There are, however, other design decisions that need to be taken into account to allow a manufacturer of a device to securely install firmware on a device. Examples for such design decisions are how to manage the firmware update repository at the server side (which may include user interface considerations), the techniques to provide additional application layer security protection of the firmware image, how many versions of firmware imagines to store on the device, and how to execute the firmware update process considering the hardware specific details of a given IoT hardware product. These aspects are considered to be outside the scope of the LwM2M version 1.0 specification.
|
||||
A LwM2M Server may also instruct a LwM2M Client to fetch a firmware image from a dedicated server (instead of pushing firmware imagines to the LwM2M Client). The Package URI resource is contained in the Firmware object and can be used for this purpose.
|
||||
A LwM2M Client MUST support block-wise transfer [CoAP_Blockwise] if it implements the Firmware Update object.
|
||||
A LwM2M Server MUST support block-wise transfer. Other protocols, such as HTTP/HTTPs, MAY also be used for downloading firmware updates (via the Package URI resource). For constrained devices it is, however, RECOMMENDED to use CoAP for firmware downloads to avoid the need for additional protocol implementations.]]></Description1>
|
||||
<ObjectID>5</ObjectID>
|
||||
<ObjectURN>urn:oma:lwm2m:oma:5</ObjectURN>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Resources>
|
||||
<Item ID="0">
|
||||
<Name>Package</Name>
|
||||
<Operations>W</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Opaque</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Firmware package]]></Description>
|
||||
</Item>
|
||||
<Item ID="1">
|
||||
<Name>Package URI</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration>0-255 bytes</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[URI from where the device can download the firmware package by an alternative mechanism. As soon the device has received the Package URI it performs the download at the next practical opportunity.
|
||||
The URI format is defined in RFC 3986. For example, coaps://example.org/firmware is a syntactically valid URI. The URI scheme determines the protocol to be used. For CoAP this endpoint MAY be a LwM2M Server but does not necessarily need to be. A CoAP server implementing block-wise transfer is sufficient as a server hosting a firmware repository and the expectation is that this server merely serves as a separate file server making firmware images available to LwM2M Clients.]]></Description>
|
||||
</Item>
|
||||
<Item ID="2">
|
||||
<Name>Update</Name>
|
||||
<Operations>E</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type></Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Updates firmware by using the firmware package stored in Package, or, by using the firmware downloaded from the Package URI.
|
||||
This Resource is only executable when the value of the State Resource is Downloaded.]]></Description>
|
||||
</Item>
|
||||
<Item ID="3">
|
||||
<Name>State</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-3</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Indicates current state with respect to this firmware update. This value is set by the LwM2M Client.
|
||||
0: Idle (before downloading or after successful updating)
|
||||
1: Downloading (The data sequence is on the way)
|
||||
2: Downloaded
|
||||
3: Updating
|
||||
If writing the firmware package to Package Resource is done, or, if the device has downloaded the firmware package from the Package URI the state changes to Downloaded.
|
||||
Writing an empty string to Package URI Resource or setting the Package Resource to NULL (‘\0’), resets the Firmware Update State Machine: the State Resource value is set to Idle and the Update Result Resource value is set to 0.
|
||||
When in Downloaded state, and the executable Resource Update is triggered, the state changes to Updating.
|
||||
If the Update Resource failed, the state returns at Downloaded.
|
||||
If performing the Update Resource was successful, the state changes from Updating to Idle.
|
||||
Firmware Update mechanisms are illustrated below in Figure 29 of the LwM2M version 1.0 specification.]]></Description>
|
||||
</Item>
|
||||
<Item ID="5">
|
||||
<Name>Update Result</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-9</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Contains the result of downloading or updating the firmware
|
||||
0: Initial value. Once the updating process is initiated (Download /Update), this Resource MUST be reset to Initial value.
|
||||
1: Firmware updated successfully,
|
||||
2: Not enough flash memory for the new firmware package.
|
||||
3. Out of RAM during downloading process.
|
||||
4: Connection lost during downloading process.
|
||||
5: Integrity check failure for new downloaded package.
|
||||
6: Unsupported package type.
|
||||
7: Invalid URI
|
||||
8: Firmware update failed
|
||||
9: Unsupported protocol. A LwM2M client indicates the failure to retrieve the firmware image using the URI provided in the Package URI resource by writing the value 9 to the /5/0/5 (Update Result resource) when the URI contained a URI scheme unsupported by the client. Consequently, the LwM2M Client is unable to retrieve the firmware image using the URI provided by the LwM2M Server in the Package URI when it refers to an unsupported protocol.]]></Description>
|
||||
</Item>
|
||||
<Item ID="6">
|
||||
<Name>PkgName</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration>0-255 bytes</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Name of the Firmware Package]]></Description>
|
||||
</Item>
|
||||
<Item ID="7">
|
||||
<Name>PkgVersion</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration>0-255 bytes</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Version of the Firmware package]]></Description>
|
||||
</Item>
|
||||
<Item ID="8">
|
||||
<Name>Firmware Update Protocol Support</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[This resource indicates what protocols the LwM2M Client implements to retrieve firmware images. The LwM2M server uses this information to decide what URI to include in the Package URI. A LwM2M Server MUST NOT include a URI in the Package URI object that uses a protocol that is unsupported by the LwM2M client.
|
||||
For example, if a LwM2M client indicates that it supports CoAP and CoAPS then a LwM2M Server must not provide an HTTP URI in the Packet URI.
|
||||
The following values are defined by this version of the specification:
|
||||
0 – CoAP (as defined in RFC 7252) with the additional support for block-wise transfer. CoAP is the default setting.
|
||||
1 – CoAPS (as defined in RFC 7252) with the additional support for block-wise transfer
|
||||
2 – HTTP 1.1 (as defined in RFC 7230)
|
||||
3 – HTTPS 1.1 (as defined in RFC 7230)
|
||||
Additional values MAY be defined in the future. Any value not understood by the LwM2M Server MUST be ignored.]]></Description>
|
||||
</Item>
|
||||
<Item ID="9">
|
||||
<Name>Firmware Update Delivery Method</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[The LwM2M Client uses this resource to indicate its support for transferring firmware images to the client either via the Package Resource (=push) or via the Package URI Resource (=pull) mechanism.
|
||||
0 – Pull only
|
||||
1 – Push only
|
||||
2 – Both. In this case the LwM2M Server MAY choose the preferred mechanism for conveying the firmware image to the LwM2M Client.]]></Description>
|
||||
</Item>
|
||||
</Resources>
|
||||
<Description2><![CDATA[]]></Description2>
|
||||
</Object>
|
||||
</LWM2M>
|
143
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Location-v1_0.xml
Normal file
143
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Location-v1_0.xml
Normal file
@ -0,0 +1,143 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
FILE INFORMATION
|
||||
|
||||
OMA Permanent Document
|
||||
File: OMA-SUP-XML_LWM2M_Location-V1_0-20170208-A
|
||||
Type: xml
|
||||
|
||||
Public Reachable Information
|
||||
Path: http://www.openmobilealliance.org/tech/profiles
|
||||
Name: LWM2M_Location-v1_0.xml
|
||||
|
||||
NORMATIVE INFORMATION
|
||||
|
||||
Information about this file can be found in the latest revision of
|
||||
|
||||
OMA-TS-LightweightM2M-V1_0
|
||||
|
||||
This is available at http://www.openmobilealliance.org/
|
||||
|
||||
Send comments to technical-comments@mail.openmobilealliance.org
|
||||
|
||||
CHANGE HISTORY
|
||||
|
||||
08022017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0009-INP_LightweightM2M-V1_0_ERP_for_Final_Approval
|
||||
|
||||
LEGAL DISCLAIMER
|
||||
|
||||
Copyright 2017 Open Mobile Alliance All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The above license is used as a license under copyright only. Please
|
||||
reference the OMA IPR Policy for patent licensing terms:
|
||||
http://www.openmobilealliance.org/ipr.html
|
||||
-->
|
||||
|
||||
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://openmobilealliance.org/tech/profiles/LWM2M.xsd">
|
||||
<Object ObjectType="MODefinition">
|
||||
<Name>Location</Name>
|
||||
<Description1><![CDATA[This LwM2M Objects provide a range of device related information which can be queried by the LwM2M Server, and a device reboot and factory reset function.]]></Description1>
|
||||
<ObjectID>6</ObjectID>
|
||||
<ObjectURN>urn:oma:lwm2m:oma:6</ObjectURN>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Resources>
|
||||
<Item ID="0">
|
||||
<Name>Latitude</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Float</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>Deg</Units>
|
||||
<Description><![CDATA[The decimal notation of latitude, e.g., -43.5723 [World Geodetic System 1984].]]></Description>
|
||||
</Item>
|
||||
<Item ID="1">
|
||||
<Name>Longitude</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Float</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>Deg</Units>
|
||||
<Description><![CDATA[The decimal notation of longitude, e.g., 153.21760 [World Geodetic System 1984].]]></Description>
|
||||
</Item>
|
||||
<Item ID="2">
|
||||
<Name>Altitude</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Float</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>m</Units>
|
||||
<Description><![CDATA[The decimal notation of altitude in meters above sea level.]]></Description>
|
||||
</Item>
|
||||
<Item ID="3">
|
||||
<Name>Radius</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Float</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>m</Units>
|
||||
<Description><![CDATA[The value in the Radius Resource indicates the size in meters of a circular area around a point of geometry.]]></Description>
|
||||
</Item>
|
||||
<Item ID="4">
|
||||
<Name>Velocity</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Opaque</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[The velocity in the LwM2M Client is defined in [3GPP-TS_23.032].]]></Description>
|
||||
</Item>
|
||||
<Item ID="5">
|
||||
<Name>Timestamp</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Time</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[The timestamp of when the location measurement was performed.]]></Description>
|
||||
</Item>
|
||||
<Item ID="6"><Name>Speed</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Float</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>Meters per second</Units>
|
||||
<Description><![CDATA[Speed is the time rate of change in position of a LwM2M Client without regard for direction: the scalar component of velocity.]]></Description>
|
||||
</Item></Resources>
|
||||
<Description2></Description2>
|
||||
</Object>
|
||||
</LWM2M>
|
225
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Security-v1_0.xml
Normal file
225
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Security-v1_0.xml
Normal file
@ -0,0 +1,225 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
FILE INFORMATION
|
||||
|
||||
OMA Permanent Document
|
||||
File: OMA-SUP-XML_LWM2M_Security-V1_0-20170208-A
|
||||
Type: xml
|
||||
|
||||
Public Reachable Information
|
||||
Path: http://www.openmobilealliance.org/tech/profiles
|
||||
Name: LWM2M_Security-v1_0.xml
|
||||
|
||||
NORMATIVE INFORMATION
|
||||
|
||||
Information about this file can be found in the latest revision of
|
||||
|
||||
OMA-TS-LightweightM2M-V1_0
|
||||
|
||||
This is available at http://www.openmobilealliance.org/
|
||||
|
||||
Send comments to technical-comments@mail.openmobilealliance.org
|
||||
|
||||
CHANGE HISTORY
|
||||
|
||||
08022017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0009-INP_LightweightM2M-V1_0_ERP_for_Final_Approval
|
||||
|
||||
LEGAL DISCLAIMER
|
||||
|
||||
Copyright 2017 Open Mobile Alliance All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The above license is used as a license under copyright only. Please
|
||||
reference the OMA IPR Policy for patent licensing terms:
|
||||
http://www.openmobilealliance.org/ipr.html
|
||||
-->
|
||||
|
||||
<LWM2M xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://openmobilealliance.org/tech/profiles/LWM2M.xsd" >
|
||||
<Object ObjectType="MODefinition">
|
||||
<Name>LWM2M Security</Name>
|
||||
<Description1><![CDATA[This LwM2M Object provides the keying material of a LwM2M Client appropriate to access a specified LwM2M Server. One Object Instance SHOULD address a LwM2M Bootstrap-Server.
|
||||
These LwM2M Object Resources MUST only be changed by a LwM2M Bootstrap-Server or Bootstrap from Smartcard and MUST NOT be accessible by any other LwM2M Server.]]></Description1>
|
||||
<ObjectID>0</ObjectID>
|
||||
<ObjectURN>urn:oma:lwm2m:oma:0</ObjectURN>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Resources>
|
||||
<Item ID="0">
|
||||
<Name>LWM2M Server URI</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration>0-255 bytes</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Uniquely identifies the LwM2M Server or LwM2M Bootstrap-Server. The format of the CoAP URI is defined in Section 6 of RFC 7252.]]></Description>
|
||||
</Item>
|
||||
<Item ID="1">
|
||||
<Name>Bootstrap-Server</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Boolean</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Determines if the current instance concerns a LwM2M Bootstrap-Server (true) or a standard LwM2M Server (false)]]></Description>
|
||||
</Item>
|
||||
<Item ID="2">
|
||||
<Name>Security Mode</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-4</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Determines which UDP payload security mode is used
|
||||
0: Pre-Shared Key mode
|
||||
1: Raw Public Key mode
|
||||
2: Certificate mode
|
||||
3: NoSec mode
|
||||
4: Certificate mode with EST]]></Description>
|
||||
</Item>
|
||||
<Item ID="3">
|
||||
<Name>Public Key or Identity</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Opaque</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Stores the LwM2M Client’s Certificate (Certificate mode), public key (RPK mode) or PSK Identity (PSK mode). The format is defined in Section E.1.1 of the LwM2M version 1.0 specification.]]></Description>
|
||||
</Item>
|
||||
<Item ID="4">
|
||||
<Name>Server Public Key</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Opaque</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Stores the LwM2M Server’s or LwM2M Bootstrap-Server’s Certificate (Certificate mode), public key (RPK mode). The format is defined in Section E.1.1 of the LwM2M version 1.0 specification.]]></Description>
|
||||
</Item>
|
||||
<Item ID="5">
|
||||
<Name>Secret Key</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Opaque</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Stores the secret key or private key of the security mode. The format of the keying material is defined by the security mode in Section E.1.1 of the LwM2M version 1.0 specification. This Resource MUST only be changed by a bootstrap-server and MUST NOT be readable by any server.]]></Description>
|
||||
</Item>
|
||||
<Item ID="6">
|
||||
<Name>SMS Security Mode</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>0-255</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Determines which SMS security mode is used (see section 7.2 of the LwM2M version 1.0 specification)
|
||||
0: Reserved for future use
|
||||
1: DTLS mode (Device terminated) PSK mode assumed
|
||||
2: Secure Packet Structure mode (Smartcard terminated)
|
||||
3: NoSec mode
|
||||
4: Reserved mode (DTLS mode with multiplexing Security Association support)
|
||||
5-203 : Reserved for future use
|
||||
204-255: Proprietary modes]]></Description>
|
||||
</Item>
|
||||
<Item ID="7">
|
||||
<Name>SMS Binding Key Parameters</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Opaque</Type>
|
||||
<RangeEnumeration>6 bytes</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Stores the KIc, KID, SPI and TAR. The format is defined in Section E.1.2 of the LwM2M version 1.0 specification.]]></Description>
|
||||
</Item>
|
||||
<Item ID="8">
|
||||
<Name>SMS Binding Secret Key(s)</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Opaque</Type>
|
||||
<RangeEnumeration>16-32-48 bytes</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Stores the values of the key(s) for the SMS binding.
|
||||
This resource MUST only be changed by a bootstrap-server and MUST NOT be readable by any server.]]></Description>
|
||||
</Item>
|
||||
<Item ID="9">
|
||||
<Name>LwM2M Server SMS Number</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[MSISDN used by the LwM2M Client to send messages to the LwM2M Server via the SMS binding.
|
||||
The LwM2M Client SHALL silently ignore any SMS originated from unknown MSISDN]]></Description>
|
||||
</Item>
|
||||
<Item ID="10">
|
||||
<Name>Short Server ID</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>1-65534</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[This identifier uniquely identifies each LwM2M Server configured for the LwM2M Client.
|
||||
This Resource MUST be set when the Bootstrap-Server Resource has false value.
|
||||
Specific ID:0 and ID:65535 values MUST NOT be used for identifying the LwM2M Server (Section 6.3 of the LwM2M version 1.0 specification).]]></Description>
|
||||
</Item>
|
||||
<Item ID="11">
|
||||
<Name>Client Hold Off Time</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>s</Units>
|
||||
<Description><![CDATA[Relevant information for a Bootstrap-Server only.
|
||||
The number of seconds to wait before initiating a Client Initiated Bootstrap once the LwM2M Client has determined it should initiate this bootstrap mode.
|
||||
In case client initiated bootstrap is supported by the LwM2M Client, this resource MUST be supported.]]></Description>
|
||||
</Item>
|
||||
<Item ID="12">
|
||||
<Name>Bootstrap-Server Account Timeout</Name>
|
||||
<Operations></Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>s</Units>
|
||||
<Description><![CDATA[The LwM2M Client MUST purge the LwM2M Bootstrap-Server Account after the timeout value given by this resource. The lowest timeout value is 1.
|
||||
If the value is set to 0, or if this resource is not instantiated, the Bootstrap-Server Account lifetime is infinite.]]></Description>
|
||||
</Item>
|
||||
</Resources>
|
||||
<Description2><![CDATA[]]></Description2>
|
||||
</Object>
|
||||
</LWM2M>
|
171
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Server-v1_0.xml
Normal file
171
apps/emqx_lwm2m/lwm2m_xml/LWM2M_Server-v1_0.xml
Normal file
@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
FILE INFORMATION
|
||||
|
||||
OMA Permanent Document
|
||||
File: OMA-SUP-XML_LWM2M_Server-V1_0-20170208-A
|
||||
Type: xml
|
||||
|
||||
Public Reachable Information
|
||||
Path: http://www.openmobilealliance.org/tech/profiles
|
||||
Name: LWM2M_Server-v1_0.xml
|
||||
|
||||
NORMATIVE INFORMATION
|
||||
|
||||
Information about this file can be found in the latest revision of
|
||||
|
||||
OMA-TS-LightweightM2M-V1_0
|
||||
|
||||
This is available at http://www.openmobilealliance.org/
|
||||
|
||||
Send comments to technical-comments@mail.openmobilealliance.org
|
||||
|
||||
CHANGE HISTORY
|
||||
|
||||
08022017 Status changed to Approved by TP, TP Ref # OMA-TP-2017-0009-INP_LightweightM2M-V1_0_ERP_for_Final_Approval
|
||||
|
||||
LEGAL DISCLAIMER
|
||||
|
||||
Copyright 2017 Open Mobile Alliance All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The above license is used as a license under copyright only. Please
|
||||
reference the OMA IPR Policy for patent licensing terms:
|
||||
http://www.openmobilealliance.org/ipr.html
|
||||
-->
|
||||
|
||||
<LWM2M xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://openmobilealliance.org/tech/profiles/LWM2M.xsd">
|
||||
<Object ObjectType="MODefinition">
|
||||
<Name>LwM2M Server</Name>
|
||||
<Description1><![CDATA[This LwM2M Objects provides the data related to a LwM2M Server. A Bootstrap-Server has no such an Object Instance associated to it.]]></Description1>
|
||||
<ObjectID>1</ObjectID>
|
||||
<ObjectURN>urn:oma:lwm2m:oma:1</ObjectURN>
|
||||
<MultipleInstances>Multiple</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Resources>
|
||||
<Item ID="0">
|
||||
<Name>Short Server ID</Name>
|
||||
<Operations>R</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration>1-65535</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[Used as link to associate server Object Instance.]]></Description>
|
||||
</Item>
|
||||
<Item ID="1">
|
||||
<Name>Lifetime</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>s</Units>
|
||||
<Description><![CDATA[Specify the lifetime of the registration in seconds (see Section 5.3 Registration).]]></Description>
|
||||
</Item>
|
||||
<Item ID="2">
|
||||
<Name>Default Minimum Period</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>s</Units>
|
||||
<Description><![CDATA[The default value the LwM2M Client should use for the Minimum Period of an Observation in the absence of this parameter being included in an Observation.
|
||||
If this Resource doesn’t exist, the default value is 0.]]></Description>
|
||||
</Item>
|
||||
<Item ID="3">
|
||||
<Name>Default Maximum Period</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>s</Units>
|
||||
<Description><![CDATA[The default value the LwM2M Client should use for the Maximum Period of an Observation in the absence of this parameter being included in an Observation.]]></Description>
|
||||
</Item>
|
||||
<Item ID="4">
|
||||
<Name>Disable</Name>
|
||||
<Operations>E</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type></Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[If this Resource is executed, this LwM2M Server Object is disabled for a certain period defined in the Disabled Timeout Resource. After receiving “Execute” operation, LwM2M Client MUST send response of the operation and perform de-registration process, and underlying network connection between the Client and Server MUST be disconnected to disable the LwM2M Server account.
|
||||
After the above process, the LwM2M Client MUST NOT send any message to the Server and ignore all the messages from the LwM2M Server for the period.]]></Description>
|
||||
</Item>
|
||||
<Item ID="5">
|
||||
<Name>Disable Timeout</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Optional</Mandatory>
|
||||
<Type>Integer</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units>s</Units>
|
||||
<Description><![CDATA[A period to disable the Server. After this period, the LwM2M Client MUST perform registration process to the Server. If this Resource is not set, a default timeout value is 86400 (1 day).]]></Description>
|
||||
</Item>
|
||||
<Item ID="6">
|
||||
<Name>Notification Storing When Disabled or Offline</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>Boolean</Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[If true, the LwM2M Client stores “Notify” operations to the LwM2M Server while the LwM2M Server account is disabled or the LwM2M Client is offline. After the LwM2M Server account is enabled or the LwM2M Client is online, the LwM2M Client reports the stored “Notify” operations to the Server.
|
||||
If false, the LwM2M Client discards all the “Notify” operations or temporarily disables the Observe function while the LwM2M Server is disabled or the LwM2M Client is offline.
|
||||
The default value is true.
|
||||
The maximum number of storing Notifications per Server is up to the implementation.]]></Description>
|
||||
</Item>
|
||||
<Item ID="7">
|
||||
<Name>Binding</Name>
|
||||
<Operations>RW</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type>String</Type>
|
||||
<RangeEnumeration>The possible values of Resource are listed in 5.3.1.1</RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[This Resource defines the transport binding configured for the LwM2M Client.
|
||||
If the LwM2M Client supports the binding specified in this Resource, the LwM2M Client MUST use that transport for the Current Binding Mode.]]></Description>
|
||||
</Item>
|
||||
<Item ID="8">
|
||||
<Name>Registration Update Trigger</Name>
|
||||
<Operations>E</Operations>
|
||||
<MultipleInstances>Single</MultipleInstances>
|
||||
<Mandatory>Mandatory</Mandatory>
|
||||
<Type></Type>
|
||||
<RangeEnumeration></RangeEnumeration>
|
||||
<Units></Units>
|
||||
<Description><![CDATA[If this Resource is executed the LwM2M Client MUST perform an “Update” operation with this LwM2M Server using that transport for the Current Binding Mode.]]></Description>
|
||||
</Item>
|
||||
</Resources>
|
||||
<Description2></Description2>
|
||||
</Object>
|
||||
</LWM2M>
|
212
apps/emqx_lwm2m/priv/emqx_lwm2m.schema
Normal file
212
apps/emqx_lwm2m/priv/emqx_lwm2m.schema
Normal file
@ -0,0 +1,212 @@
|
||||
%% -*-: erlang -*-
|
||||
|
||||
{mapping, "lwm2m.bind.udp.$number", "emqx_lwm2m.bind_udp", [
|
||||
{datatype, ip},
|
||||
{default, "0.0.0.0:5683"}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.bind.dtls.$number", "emqx_lwm2m.bind_dtls", [
|
||||
{datatype, ip},
|
||||
{default, "0.0.0.0:5684"}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.lifetime_min", "emqx_lwm2m.lifetime_min", [
|
||||
{datatype, {duration, s}},
|
||||
{default, 0}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.lifetime_max", "emqx_lwm2m.lifetime_max", [
|
||||
{datatype, {duration, s}},
|
||||
{default, 315360000} %% 10 years
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.qmode_time_window", "emqx_lwm2m.qmode_time_window", [
|
||||
{datatype, integer},
|
||||
{default, 0}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.auto_observe", "emqx_lwm2m.auto_observe", [
|
||||
{datatype, flag},
|
||||
{default, off}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.lb", "emqx_lwm2m.options", [
|
||||
{datatype, atom},
|
||||
{default, undefined}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.opts.$name", "emqx_lwm2m.options", [
|
||||
{datatype, bytesize}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_lwm2m.bind_udp", fun(Conf) ->
|
||||
Options = cuttlefish_variable:filter_by_prefix("lwm2m.bind.udp", Conf),
|
||||
lists:map(fun({_, Bind}) ->
|
||||
{Ip, Port} = cuttlefish_datatypes:from_string(Bind, ip),
|
||||
Opts = case inet:parse_address(Ip) of
|
||||
{ok, {_,_,_,_} = Address} ->
|
||||
[inet, {ip, Address}];
|
||||
{ok, {_,_,_,_,_,_,_,_} = Address} ->
|
||||
[inet6, {ip, Address}]
|
||||
end,
|
||||
{Port, Opts}
|
||||
end, Options)
|
||||
end}.
|
||||
|
||||
{translation, "emqx_lwm2m.bind_dtls", fun(Conf) ->
|
||||
Options = cuttlefish_variable:filter_by_prefix("lwm2m.bind.dtls", Conf),
|
||||
lists:map(fun({_, Bind}) ->
|
||||
{Ip, Port} = cuttlefish_datatypes:from_string(Bind, ip),
|
||||
Opts = case inet:parse_address(Ip) of
|
||||
{ok, {_,_,_,_} = Address} ->
|
||||
[inet, {ip, Address}];
|
||||
{ok, {_,_,_,_,_,_,_,_} = Address} ->
|
||||
[inet6, {ip, Address}]
|
||||
end,
|
||||
{Port, Opts}
|
||||
end, Options)
|
||||
end}.
|
||||
|
||||
{translation, "emqx_lwm2m.options", fun(Conf) ->
|
||||
Options = cuttlefish_variable:filter_by_prefix("lwm2m.opts", Conf),
|
||||
Opts = lists:map(fun({[_,_, Key], Value}) ->
|
||||
{list_to_atom(Key), Value}
|
||||
end, Options),
|
||||
|
||||
case cuttlefish:conf_get("lwm2m.lb", Conf, undefined) of
|
||||
undefined -> ignore;
|
||||
_ ->
|
||||
cuttlefish:warn("The 'lwm2m.lb' option has removed from v4.2.0!")
|
||||
end,
|
||||
|
||||
Opts
|
||||
end}.
|
||||
|
||||
{mapping, "lwm2m.mountpoint", "emqx_lwm2m.mountpoint", [
|
||||
{datatype, string},
|
||||
{default, ""}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.topics.command", "emqx_lwm2m.topics", [
|
||||
{datatype, string},
|
||||
{default, "lwm2m/%e/dn/#"}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.topics.response", "emqx_lwm2m.topics", [
|
||||
{datatype, string},
|
||||
{default, "lwm2m/%e/up/resp"}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.topics.notify", "emqx_lwm2m.topics", [
|
||||
{datatype, string},
|
||||
{default, "lwm2m/%e/up/notify"}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.topics.register", "emqx_lwm2m.topics", [
|
||||
{datatype, string},
|
||||
{default, "lwm2m/%e/up/resp"}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.topics.update", "emqx_lwm2m.topics", [
|
||||
{datatype, string},
|
||||
{default, "lwm2m/%e/up/resp"}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.update_msg_publish_condition", "emqx_lwm2m.update_msg_publish_condition", [
|
||||
{datatype, {enum, [contains_object_list, always]}},
|
||||
{default, contains_object_list}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_lwm2m.topics", fun(Conf) ->
|
||||
Topics = cuttlefish_variable:filter_by_prefix("lwm2m.topics", Conf),
|
||||
Opts = lists:map(fun({[_,_, Key], Value}) ->
|
||||
{list_to_atom(Key), Value}
|
||||
end, Topics),
|
||||
Opts
|
||||
end}.
|
||||
|
||||
{mapping, "lwm2m.xml_dir", "emqx_lwm2m.xml_dir", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
%% Plan to remove v5.0-alpha.1, please use lwm2m.dtls_opts.keyfile instead
|
||||
{mapping, "lwm2m.keyfile", "emqx_lwm2m.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
%% Plan to remove v5.0-alpha.1, please use lwm2m.dtls_opts.certfile instead
|
||||
{mapping, "lwm2m.certfile", "emqx_lwm2m.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.dtls.keyfile", "emqx_lwm2m.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.dtls.certfile", "emqx_lwm2m.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.dtls.verify", "emqx_lwm2m.dtls_opts", [
|
||||
{default, verify_none},
|
||||
{datatype, {enum, [verify_none, verify_peer]}}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.dtls.cacertfile", "emqx_lwm2m.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.dtls.fail_if_no_peer_cert", "emqx_lwm2m.dtls_opts", [
|
||||
{datatype, {enum, [true, false]}}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.dtls.ciphers", "emqx_lwm2m.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{mapping, "lwm2m.dtls.psk_ciphers", "emqx_lwm2m.dtls_opts", [
|
||||
{datatype, string}
|
||||
]}.
|
||||
|
||||
{translation, "emqx_lwm2m.dtls_opts", fun(Conf) ->
|
||||
Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end,
|
||||
|
||||
%% compatible with before v4.2
|
||||
%% It plan to remove at v5.0-alpha.1
|
||||
OldKey = cuttlefish:conf_get("lwm2m.keyfile", Conf, undefined),
|
||||
OldCert = cuttlefish:conf_get("lwm2m.certfile", Conf, undefined),
|
||||
|
||||
%% Ciphers
|
||||
SplitFun = fun(undefined) -> undefined; (S) -> string:tokens(S, ",") end,
|
||||
Ciphers =
|
||||
case cuttlefish:conf_get("lwm2m.dtls.ciphers", Conf, undefined) of
|
||||
undefined ->
|
||||
[];
|
||||
C ->
|
||||
[{ciphers, SplitFun(C)}]
|
||||
end,
|
||||
PskCiphers =
|
||||
case cuttlefish:conf_get("lwm2m.dtls.psk_ciphers", Conf, undefined) of
|
||||
undefined ->
|
||||
[];
|
||||
C2 ->
|
||||
Psk = lists:map(fun("PSK-AES128-CBC-SHA") -> {psk, aes_128_cbc, sha};
|
||||
("PSK-AES256-CBC-SHA") -> {psk, aes_256_cbc, sha};
|
||||
("PSK-3DES-EDE-CBC-SHA") -> {psk, '3des_ede_cbc', sha};
|
||||
("PSK-RC4-SHA") -> {psk, rc4_128, sha}
|
||||
end, SplitFun(C2)),
|
||||
[{ciphers, Psk}, {user_lookup_fun, {fun emqx_psk:lookup/3, <<>>}}]
|
||||
end,
|
||||
Ciphers /= []
|
||||
andalso PskCiphers /= []
|
||||
andalso cuttlefish:invalid("The 'lwm2m.dtls.ciphers' and 'lwm2m.dtls.psk_ciphers' cannot exist simultaneously."),
|
||||
|
||||
NCiphers = Ciphers ++ PskCiphers,
|
||||
|
||||
Filter([{verify, cuttlefish:conf_get("lwm2m.dtls.verify", Conf, undefined)},
|
||||
{keyfile, cuttlefish:conf_get("lwm2m.dtls.keyfile", Conf, OldKey)},
|
||||
{certfile, cuttlefish:conf_get("lwm2m.dtls.certfile", Conf, OldCert)},
|
||||
{cacertfile, cuttlefish:conf_get("lwm2m.dtls.cacertfile", Conf, undefined)},
|
||||
{fail_if_no_peer_cert, cuttlefish:conf_get("lwm2m.dtls.fail_if_no_peer_cert", Conf, undefined)} | NCiphers])
|
||||
end}.
|
||||
|
28
apps/emqx_lwm2m/rebar.config
Normal file
28
apps/emqx_lwm2m/rebar.config
Normal file
@ -0,0 +1,28 @@
|
||||
{deps,
|
||||
[{lwm2m_coap, {git, "https://hub.fastgit.org/fastdgiot/lwm2m-coap", {tag, "v1.1.2"}}}
|
||||
]}.
|
||||
|
||||
{profiles,
|
||||
[{test,
|
||||
[{deps, [{er_coap_client, {git, "https://hub.fastgit.org/fastdgiot/er_coap_client", {tag, "v1.0"}}},
|
||||
{emqx_ct_helpers, {git, "https://hub.fastgit.org/fastdgiot/emqx-ct-helpers", {tag, "1.2.2"}}},
|
||||
{emqtt, {git, "https://hub.fastgit.org/fastdgiot/emqtt", {tag, "1.2.0"}}}
|
||||
]}
|
||||
]}
|
||||
]}.
|
||||
|
||||
{edoc_opts, [{preprocess, true}]}.
|
||||
{erl_opts, [warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_obsolete_guard,
|
||||
debug_info,
|
||||
{parse_transform}]}.
|
||||
|
||||
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||
locals_not_used, deprecated_function_calls,
|
||||
warnings_as_errors, deprecated_functions]}.
|
||||
{cover_enabled, true}.
|
||||
{cover_opts, [verbose]}.
|
||||
{cover_export_enabled, true}.
|
||||
{extra_src_dirs, [{"lwm2m_xml", [{recursive,true}]}]}.
|
230
apps/emqx_lwm2m/src/binary_util.erl
Normal file
230
apps/emqx_lwm2m/src/binary_util.erl
Normal file
@ -0,0 +1,230 @@
|
||||
-module(binary_util).
|
||||
|
||||
%% copied from https://github.com/arcusfelis/binary2
|
||||
|
||||
%% Bytes
|
||||
-export([ reverse/1
|
||||
, join/2
|
||||
, duplicate/2
|
||||
, suffix/2
|
||||
, prefix/2
|
||||
]).
|
||||
|
||||
%% Bits
|
||||
-export([ union/2
|
||||
, subtract/2
|
||||
, intersection/2
|
||||
, inverse/1
|
||||
]).
|
||||
|
||||
%% Trimming
|
||||
-export([ rtrim/1
|
||||
, rtrim/2
|
||||
, ltrim/1
|
||||
, ltrim/2
|
||||
, trim/1
|
||||
, trim/2
|
||||
]).
|
||||
|
||||
%% Parsing
|
||||
-export([ bin_to_int/1]).
|
||||
|
||||
%% Matching
|
||||
-export([ optimize_patterns/1]).
|
||||
|
||||
%% CoAP
|
||||
-export([ join_path/1]).
|
||||
|
||||
|
||||
trim(B) -> trim(B, 0).
|
||||
ltrim(B) -> ltrim(B, 0).
|
||||
rtrim(B) -> rtrim(B, 0).
|
||||
|
||||
|
||||
rtrim(B, X) when is_binary(B), is_integer(X) ->
|
||||
S = byte_size(B),
|
||||
do_rtrim(S, B, X);
|
||||
rtrim(B, [_|_]=Xs) when is_binary(B) ->
|
||||
S = byte_size(B),
|
||||
do_mrtrim(S, B, Xs).
|
||||
|
||||
|
||||
ltrim(B, X) when is_binary(B), is_integer(X) ->
|
||||
do_ltrim(B, X);
|
||||
ltrim(B, [_|_]=Xs) when is_binary(B) ->
|
||||
do_mltrim(B, Xs).
|
||||
|
||||
|
||||
%% @doc The second element is a single integer element or an ordset of elements.
|
||||
trim(B, X) when is_binary(B), is_integer(X) ->
|
||||
From = ltrimc(B, X, 0),
|
||||
case byte_size(B) of
|
||||
From ->
|
||||
<<>>;
|
||||
S ->
|
||||
To = do_rtrimc(S, B, X),
|
||||
binary:part(B, From, To - From)
|
||||
end;
|
||||
trim(B, [_|_]=Xs) when is_binary(B) ->
|
||||
From = mltrimc(B, Xs, 0),
|
||||
case byte_size(B) of
|
||||
From ->
|
||||
<<>>;
|
||||
S ->
|
||||
To = do_mrtrimc(S, B, Xs),
|
||||
binary:part(B, From, To - From)
|
||||
end.
|
||||
|
||||
do_ltrim(<<X, B/binary>>, X) ->
|
||||
do_ltrim(B, X);
|
||||
do_ltrim(B, _X) ->
|
||||
B.
|
||||
|
||||
%% multi, left trimming.
|
||||
do_mltrim(<<X, B/binary>> = XB, Xs) ->
|
||||
case ordsets:is_element(X, Xs) of
|
||||
true -> do_mltrim(B, Xs);
|
||||
false -> XB
|
||||
end;
|
||||
do_mltrim(<<>>, _Xs) ->
|
||||
<<>>.
|
||||
|
||||
do_rtrim(0, _B, _X) ->
|
||||
<<>>;
|
||||
do_rtrim(S, B, X) ->
|
||||
S2 = S - 1,
|
||||
case binary:at(B, S2) of
|
||||
X -> do_rtrim(S2, B, X);
|
||||
_ -> binary_part(B, 0, S)
|
||||
end.
|
||||
|
||||
%% Multiple version of do_rtrim.
|
||||
do_mrtrim(0, _B, _Xs) ->
|
||||
<<>>;
|
||||
do_mrtrim(S, B, Xs) ->
|
||||
S2 = S - 1,
|
||||
X = binary:at(B, S2),
|
||||
case ordsets:is_element(X, Xs) of
|
||||
true -> do_mrtrim(S2, B, Xs);
|
||||
false -> binary_part(B, 0, S)
|
||||
end.
|
||||
|
||||
|
||||
ltrimc(<<X, B/binary>>, X, C) ->
|
||||
ltrimc(B, X, C+1);
|
||||
ltrimc(_B, _X, C) ->
|
||||
C.
|
||||
|
||||
%% multi, left trimming, returns a count of matched bytes from the left.
|
||||
mltrimc(<<X, B/binary>>, Xs, C) ->
|
||||
case ordsets:is_element(X, Xs) of
|
||||
true -> mltrimc(B, Xs, C+1);
|
||||
false -> C
|
||||
end;
|
||||
mltrimc(<<>>, _Xs, C) ->
|
||||
C.
|
||||
|
||||
% This clause will never be matched.
|
||||
%do_rtrimc(0, _B, _X) ->
|
||||
% 0;
|
||||
do_rtrimc(S, B, X) ->
|
||||
S2 = S - 1,
|
||||
case binary:at(B, S2) of
|
||||
X -> do_rtrimc(S2, B, X);
|
||||
_ -> S
|
||||
end.
|
||||
|
||||
do_mrtrimc(S, B, Xs) ->
|
||||
S2 = S - 1,
|
||||
X = binary:at(B, S2),
|
||||
case ordsets:is_element(X, Xs) of
|
||||
true -> do_mrtrimc(S2, B, Xs);
|
||||
false -> S
|
||||
end.
|
||||
|
||||
%% @doc Reverse the bytes' order.
|
||||
reverse(Bin) when is_binary(Bin) ->
|
||||
S = bit_size(Bin),
|
||||
<<V:S/integer-little>> = Bin,
|
||||
<<V:S/integer-big>>.
|
||||
|
||||
join([B|Bs], Sep) when is_binary(Sep) ->
|
||||
iolist_to_binary([B|add_separator(Bs, Sep)]);
|
||||
|
||||
join([], _Sep) ->
|
||||
<<>>.
|
||||
|
||||
add_separator([B|Bs], Sep) ->
|
||||
[Sep, B | add_separator(Bs, Sep)];
|
||||
add_separator([], _) ->
|
||||
[].
|
||||
|
||||
%% @doc Repeat the binary `B' `C' times.
|
||||
duplicate(C, B) ->
|
||||
iolist_to_binary(lists:duplicate(C, B)).
|
||||
|
||||
prefix(B, L) when is_binary(B), is_integer(L), L > 0 ->
|
||||
binary:part(B, 0, L).
|
||||
|
||||
suffix(B, L) when is_binary(B), is_integer(L), L > 0 ->
|
||||
S = byte_size(B),
|
||||
binary:part(B, S-L, L).
|
||||
|
||||
|
||||
union(B1, B2) ->
|
||||
S = bit_size(B1),
|
||||
<<V1:S>> = B1,
|
||||
<<V2:S>> = B2,
|
||||
V3 = V1 bor V2,
|
||||
<<V3:S>>.
|
||||
|
||||
subtract(B1, B2) ->
|
||||
S = bit_size(B1),
|
||||
<<V1:S>> = B1,
|
||||
<<V2:S>> = B2,
|
||||
V3 = (V1 bxor V2) band V1,
|
||||
<<V3:S>>.
|
||||
|
||||
intersection(B1, B2) ->
|
||||
S = bit_size(B1),
|
||||
<<V1:S>> = B1,
|
||||
<<V2:S>> = B2,
|
||||
V3 = V1 band V2,
|
||||
<<V3:S>>.
|
||||
|
||||
inverse(B1) ->
|
||||
S = bit_size(B1),
|
||||
<<V1:S>> = B1,
|
||||
V2 = bnot V1,
|
||||
<<V2:S>>.
|
||||
|
||||
%% @doc string:to_integer/1 for binaries
|
||||
bin_to_int(Bin) ->
|
||||
bin_to_int(Bin, 0).
|
||||
|
||||
bin_to_int(<<H, T/binary>>, X) when $0 =< H, H =< $9 ->
|
||||
bin_to_int(T, (X*10)+(H-$0));
|
||||
bin_to_int(Bin, X) ->
|
||||
{X, Bin}.
|
||||
|
||||
%% Remove longer patterns if shorter pattern matches
|
||||
%% Useful to run before binary:compile_pattern/1
|
||||
optimize_patterns(Patterns) ->
|
||||
Sorted = lists:usort(Patterns),
|
||||
remove_long_duplicates(Sorted).
|
||||
|
||||
remove_long_duplicates([H|T]) ->
|
||||
%% match(Subject, Pattern)
|
||||
DedupT = [X || X <- T, binary:match(X, H) =:= nomatch],
|
||||
[H|remove_long_duplicates(DedupT)];
|
||||
remove_long_duplicates([]) ->
|
||||
[].
|
||||
|
||||
join_path(PathList) ->
|
||||
join_path(PathList, <<>>).
|
||||
|
||||
join_path([], Result) -> Result;
|
||||
join_path([<<>> | PathList], Result) ->
|
||||
join_path(PathList, Result);
|
||||
join_path([Path | PathList], Result) ->
|
||||
join_path(PathList, <<Result/binary, "/", Path/binary>>).
|
7
apps/emqx_lwm2m/src/emqx_lwm2m.app.src
Normal file
7
apps/emqx_lwm2m/src/emqx_lwm2m.app.src
Normal file
@ -0,0 +1,7 @@
|
||||
{application,emqx_lwm2m,
|
||||
[{description,"EMQ X LwM2M Gateway"},
|
||||
{vsn, "4.3.0"}, % strict semver, bump manually!
|
||||
{modules,[]},
|
||||
{registered,[emqx_lwm2m_sup]},
|
||||
{applications,[kernel,stdlib,lwm2m_coap]},
|
||||
{mod,{emqx_lwm2m_app,[]}}]}.
|
44
apps/emqx_lwm2m/src/emqx_lwm2m_app.erl
Normal file
44
apps/emqx_lwm2m/src/emqx_lwm2m_app.erl
Normal file
@ -0,0 +1,44 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-emqx_plugin(protocol).
|
||||
|
||||
-export([ start/2
|
||||
, stop/1
|
||||
, prep_stop/1
|
||||
]).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
|
||||
|
||||
start(_Type, _Args) ->
|
||||
Pid = emqx_lwm2m_sup:start_link(),
|
||||
_ = lwm2m_coap_server:start_registry(),
|
||||
lwm2m_coap_server_registry:add_handler([<<"rd">>], emqx_lwm2m_coap_resource, undefined),
|
||||
emqx_lwm2m_coap_server:start(application:get_all_env(?APP)),
|
||||
Pid.
|
||||
|
||||
prep_stop(State) ->
|
||||
lwm2m_coap_server_registry:remove_handler([<<"rd">>], emqx_lwm2m_coap_resource, undefined),
|
||||
emqx_lwm2m_coap_server:stop(application:get_all_env(?APP)),
|
||||
State.
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
309
apps/emqx_lwm2m/src/emqx_lwm2m_cmd_handler.erl
Normal file
309
apps/emqx_lwm2m/src/emqx_lwm2m_cmd_handler.erl
Normal file
@ -0,0 +1,309 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_cmd_handler).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
|
||||
-include_lib("lwm2m_coap/include/coap.hrl").
|
||||
|
||||
-export([ mqtt2coap/2
|
||||
, coap2mqtt/4
|
||||
, ack2mqtt/1
|
||||
]).
|
||||
|
||||
-export([path_list/1]).
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-CMD: " ++ Format, Args)).
|
||||
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"create">>, <<"data">> := Data}) ->
|
||||
PathList = path_list(maps:get(<<"basePath">>, Data, <<"/">>)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, maps:get(<<"content">>, Data)),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
CoapRequest = lwm2m_coap_message:request(con, post, Payload, [{uri_path, FullPathList},
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}]),
|
||||
{CoapRequest, InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"delete">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
{lwm2m_coap_message:request(con, delete, <<>>, [{uri_path, FullPathList}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"read">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
{lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">> := Data}) ->
|
||||
Encoding = maps:get(<<"encoding">>, InputCmd, <<"plain">>),
|
||||
CoapRequest =
|
||||
case maps:get(<<"basePath">>, Data, <<"/">>) of
|
||||
<<"/">> ->
|
||||
single_write_request(AlternatePath, Data, Encoding);
|
||||
BasePath ->
|
||||
batch_write_request(AlternatePath, BasePath, maps:get(<<"content">>, Data), Encoding)
|
||||
end,
|
||||
{CoapRequest, InputCmd};
|
||||
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
Args =
|
||||
case maps:get(<<"args">>, Data, <<>>) of
|
||||
<<"undefined">> -> <<>>;
|
||||
undefined -> <<>>;
|
||||
Arg1 -> Arg1
|
||||
end,
|
||||
{lwm2m_coap_message:request(con, post, Args, [{uri_path, FullPathList}, {content_format, <<"text/plain">>}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
{lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {'accept', ?LWM2M_FORMAT_LINK}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write-attr">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
Query = attr_query_list(Data),
|
||||
{lwm2m_coap_message:request(con, put, <<>>, [{uri_path, FullPathList}, {uri_query, Query}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"observe">>, <<"data">> := Data}) ->
|
||||
PathList = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {observe, 0}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}) ->
|
||||
PathList = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {observe, 1}]), InputCmd}.
|
||||
|
||||
coap2mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"create">>}) ->
|
||||
make_response(Code, Ref);
|
||||
coap2mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"delete">>}) ->
|
||||
make_response(Code, Ref);
|
||||
coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"read">>}) ->
|
||||
coap_read_to_mqtt(Method, CoapPayload, data_format(Options), Ref);
|
||||
coap2mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write">>}) ->
|
||||
coap_write_to_mqtt(Method, Ref);
|
||||
coap2mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"execute">>}) ->
|
||||
coap_execute_to_mqtt(Method, Ref);
|
||||
coap2mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"discover">>}) ->
|
||||
coap_discover_to_mqtt(Method, CoapPayload, Ref);
|
||||
coap2mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write-attr">>}) ->
|
||||
coap_writeattr_to_mqtt(Method, CoapPayload, Ref);
|
||||
coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"observe">>}) ->
|
||||
coap_observe_to_mqtt(Method, CoapPayload, data_format(Options), observe_seq(Options), Ref);
|
||||
coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"cancel-observe">>}) ->
|
||||
coap_cancel_observe_to_mqtt(Method, CoapPayload, data_format(Options), Ref).
|
||||
|
||||
coap_read_to_mqtt({error, ErrorCode}, _CoapPayload, _Format, Ref) ->
|
||||
make_response(ErrorCode, Ref);
|
||||
coap_read_to_mqtt({ok, SuccessCode}, CoapPayload, Format, Ref) ->
|
||||
try
|
||||
Result = coap_content_to_mqtt_payload(CoapPayload, Format, Ref),
|
||||
make_response(SuccessCode, Ref, Format, Result)
|
||||
catch
|
||||
error:not_implemented -> make_response(not_implemented, Ref);
|
||||
C:R:Stack ->
|
||||
?LOG(error, "~p, bad payload format: ~p, stacktrace: ~p", [{C, R}, CoapPayload, Stack]),
|
||||
make_response(bad_request, Ref)
|
||||
end.
|
||||
|
||||
ack2mqtt(Ref) ->
|
||||
make_base_response(Ref).
|
||||
|
||||
coap_content_to_mqtt_payload(CoapPayload, <<"text/plain">>, Ref) ->
|
||||
emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload);
|
||||
coap_content_to_mqtt_payload(CoapPayload, <<"application/octet-stream">>, Ref) ->
|
||||
emqx_lwm2m_message:opaque_to_json(extract_path(Ref), CoapPayload);
|
||||
coap_content_to_mqtt_payload(CoapPayload, <<"application/vnd.oma.lwm2m+tlv">>, Ref) ->
|
||||
emqx_lwm2m_message:tlv_to_json(extract_path(Ref), CoapPayload);
|
||||
coap_content_to_mqtt_payload(CoapPayload, <<"application/vnd.oma.lwm2m+json">>, _Ref) ->
|
||||
emqx_lwm2m_message:translate_json(CoapPayload).
|
||||
|
||||
coap_write_to_mqtt({ok, changed}, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
coap_write_to_mqtt({error, Error}, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
coap_execute_to_mqtt({ok, changed}, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
coap_execute_to_mqtt({error, Error}, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
coap_discover_to_mqtt({ok, content}, CoapPayload, Ref) ->
|
||||
Links = binary:split(CoapPayload, <<",">>),
|
||||
make_response(content, Ref, <<"application/link-format">>, Links);
|
||||
coap_discover_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
coap_writeattr_to_mqtt({ok, changed}, _CoapPayload, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
coap_writeattr_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
coap_observe_to_mqtt({error, Error}, _CoapPayload, _Format, _ObserveSeqNum, Ref) ->
|
||||
make_response(Error, Ref);
|
||||
coap_observe_to_mqtt({ok, content}, CoapPayload, Format, 0, Ref) ->
|
||||
coap_read_to_mqtt({ok, content}, CoapPayload, Format, Ref);
|
||||
coap_observe_to_mqtt({ok, content}, CoapPayload, Format, ObserveSeqNum, Ref) ->
|
||||
RefWithObserve = maps:put(<<"seqNum">>, ObserveSeqNum, Ref),
|
||||
RefNotify = maps:put(<<"msgType">>, <<"notify">>, RefWithObserve),
|
||||
coap_read_to_mqtt({ok, content}, CoapPayload, Format, RefNotify).
|
||||
|
||||
coap_cancel_observe_to_mqtt({ok, content}, CoapPayload, Format, Ref) ->
|
||||
coap_read_to_mqtt({ok, content}, CoapPayload, Format, Ref);
|
||||
coap_cancel_observe_to_mqtt({error, Error}, _CoapPayload, _Format, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
make_response(Code, Ref=#{}) ->
|
||||
BaseRsp = make_base_response(Ref),
|
||||
make_data_response(BaseRsp, Code).
|
||||
make_response(Code, Ref=#{}, _Format, Result) ->
|
||||
BaseRsp = make_base_response(Ref),
|
||||
make_data_response(BaseRsp, Code, _Format, Result).
|
||||
|
||||
%% The base response format is what included in the request:
|
||||
%%
|
||||
%% #{
|
||||
%% <<"seqNum">> => SeqNum,
|
||||
%% <<"requestID">> => maps:get(<<"requestID">>, Ref, null),
|
||||
%% <<"cacheID">> => maps:get(<<"cacheID">>, Ref, null),
|
||||
%% <<"msgType">> => maps:get(<<"msgType">>, Ref, null)
|
||||
%% }
|
||||
|
||||
make_base_response(Ref=#{}) ->
|
||||
remove_tmp_fields(Ref).
|
||||
|
||||
make_data_response(BaseRsp, Code) ->
|
||||
BaseRsp#{
|
||||
<<"data">> => #{
|
||||
<<"reqPath">> => extract_path(BaseRsp),
|
||||
<<"code">> => code(Code),
|
||||
<<"codeMsg">> => Code
|
||||
}
|
||||
}.
|
||||
make_data_response(BaseRsp, Code, _Format, Result) ->
|
||||
BaseRsp#{
|
||||
<<"data">> => #{
|
||||
<<"reqPath">> => extract_path(BaseRsp),
|
||||
<<"code">> => code(Code),
|
||||
<<"codeMsg">> => Code,
|
||||
<<"content">> => Result
|
||||
}
|
||||
}.
|
||||
|
||||
remove_tmp_fields(Ref) ->
|
||||
maps:remove(observe_type, Ref).
|
||||
|
||||
path_list(Path) ->
|
||||
case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, ResId, ResInstId] -> [ObjId, ObjInsId, ResId, ResInstId];
|
||||
[ObjId, ObjInsId, ResId] -> [ObjId, ObjInsId, ResId];
|
||||
[ObjId, ObjInsId] -> [ObjId, ObjInsId];
|
||||
[ObjId] -> [ObjId]
|
||||
end.
|
||||
|
||||
attr_query_list(Data) ->
|
||||
attr_query_list(Data, valid_attr_keys(), []).
|
||||
attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) ->
|
||||
maps:fold(
|
||||
fun
|
||||
(_K, null, Acc) -> Acc;
|
||||
(K, V, Acc) ->
|
||||
case lists:member(K, ValidAttrKeys) of
|
||||
true ->
|
||||
Val = bin(V),
|
||||
KV = <<K/binary, "=", Val/binary>>,
|
||||
Acc ++ [KV];
|
||||
false ->
|
||||
Acc
|
||||
end
|
||||
end, QueryList, QueryJson).
|
||||
|
||||
valid_attr_keys() ->
|
||||
[<<"pmin">>, <<"pmax">>, <<"gt">>, <<"lt">>, <<"st">>].
|
||||
|
||||
data_format(Options) ->
|
||||
proplists:get_value(content_format, Options, <<"text/plain">>).
|
||||
observe_seq(Options) ->
|
||||
proplists:get_value(observe, Options, rand:uniform(1000000) + 1 ).
|
||||
|
||||
add_alternate_path_prefix(<<"/">>, PathList) ->
|
||||
PathList;
|
||||
add_alternate_path_prefix(AlternatePath, PathList) ->
|
||||
[binary_util:trim(AlternatePath, $/) | PathList].
|
||||
|
||||
extract_path(Ref = #{}) ->
|
||||
case Ref of
|
||||
#{<<"data">> := Data} ->
|
||||
case maps:get(<<"path">>, Data, nil) of
|
||||
nil -> maps:get(<<"basePath">>, Data, undefined);
|
||||
Path -> Path
|
||||
end;
|
||||
#{<<"path">> := Path} ->
|
||||
Path
|
||||
end.
|
||||
|
||||
batch_write_request(AlternatePath, BasePath, Content, Encoding) ->
|
||||
PathList = path_list(BasePath),
|
||||
Method = case length(PathList) of
|
||||
2 -> post;
|
||||
3 -> put
|
||||
end,
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
Content1 = decoding(Content, Encoding),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Content1),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
lwm2m_coap_message:request(con, Method, Payload, [{uri_path, FullPathList}, {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]).
|
||||
|
||||
single_write_request(AlternatePath, Data, Encoding) ->
|
||||
PathList = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
Datas = decoding([Data], Encoding),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Datas),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
lwm2m_coap_message:request(con, put, Payload, [{uri_path, FullPathList}, {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]).
|
||||
|
||||
|
||||
code(get) -> <<"0.01">>;
|
||||
code(post) -> <<"0.02">>;
|
||||
code(put) -> <<"0.03">>;
|
||||
code(delete) -> <<"0.04">>;
|
||||
code(created) -> <<"2.01">>;
|
||||
code(deleted) -> <<"2.02">>;
|
||||
code(valid) -> <<"2.03">>;
|
||||
code(changed) -> <<"2.04">>;
|
||||
code(content) -> <<"2.05">>;
|
||||
code(continue) -> <<"2.31">>;
|
||||
code(bad_request) -> <<"4.00">>;
|
||||
code(uauthorized) -> <<"4.01">>;
|
||||
code(bad_option) -> <<"4.02">>;
|
||||
code(forbidden) -> <<"4.03">>;
|
||||
code(not_found) -> <<"4.04">>;
|
||||
code(method_not_allowed) -> <<"4.05">>;
|
||||
code(not_acceptable) -> <<"4.06">>;
|
||||
code(request_entity_incomplete) -> <<"4.08">>;
|
||||
code(precondition_failed) -> <<"4.12">>;
|
||||
code(request_entity_too_large) -> <<"4.13">>;
|
||||
code(unsupported_content_format) -> <<"4.15">>;
|
||||
code(internal_server_error) -> <<"5.00">>;
|
||||
code(not_implemented) -> <<"5.01">>;
|
||||
code(bad_gateway) -> <<"5.02">>;
|
||||
code(service_unavailable) -> <<"5.03">>;
|
||||
code(gateway_timeout) -> <<"5.04">>;
|
||||
code(proxying_not_supported) -> <<"5.05">>.
|
||||
|
||||
bin(Bin) when is_binary(Bin) -> Bin;
|
||||
bin(Str) when is_list(Str) -> list_to_binary(Str);
|
||||
bin(Int) when is_integer(Int) -> integer_to_binary(Int);
|
||||
bin(Float) when is_float(Float) -> float_to_binary(Float).
|
||||
|
||||
decoding(Datas, <<"hex">>) ->
|
||||
lists:map(fun(Data = #{<<"value">> := Value}) ->
|
||||
Data#{<<"value">> => emqx_misc:hexstr2bin(Value)}
|
||||
end, Datas);
|
||||
decoding(Datas, _) ->
|
||||
Datas.
|
383
apps/emqx_lwm2m/src/emqx_lwm2m_coap_resource.erl
Normal file
383
apps/emqx_lwm2m/src/emqx_lwm2m_coap_resource.erl
Normal file
@ -0,0 +1,383 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_coap_resource).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
-include_lib("lwm2m_coap/include/coap.hrl").
|
||||
|
||||
-behaviour(lwm2m_coap_resource).
|
||||
|
||||
-export([ coap_discover/2
|
||||
, coap_get/5
|
||||
, coap_post/5
|
||||
, coap_put/5
|
||||
, coap_delete/4
|
||||
, coap_observe/5
|
||||
, coap_unobserve/1
|
||||
, coap_response/7
|
||||
, coap_ack/3
|
||||
, handle_info/2
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, terminate/2
|
||||
]).
|
||||
|
||||
-export([parse_object_list/1]).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
|
||||
-define(PREFIX, <<"rd">>).
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-RESOURCE: " ++ Format, Args)).
|
||||
|
||||
-dialyzer([{nowarn_function, [coap_discover/2]}]).
|
||||
% we use {'absolute', string(), [{atom(), binary()}]} as coap_uri()
|
||||
% https://hub.fastgit.org/fastdgiot/lwm2m-coap/blob/258e9bd3762124395e83c1e68a1583b84718230f/src/lwm2m_coap_resource.erl#L61
|
||||
% resource operations
|
||||
coap_discover(_Prefix, _Args) ->
|
||||
[{absolute, "mqtt", []}].
|
||||
|
||||
coap_get(ChId, [?PREFIX], Query, Content, Lwm2mState) ->
|
||||
?LOG(debug, "~p ~p GET Query=~p, Content=~p", [self(),ChId, Query, Content]),
|
||||
{ok, #coap_content{}, Lwm2mState};
|
||||
coap_get(ChId, Prefix, Query, Content, Lwm2mState) ->
|
||||
?LOG(error, "ignore bad put request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]),
|
||||
{error, bad_request, Lwm2mState}.
|
||||
|
||||
% LWM2M REGISTER COMMAND
|
||||
coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = [?PREFIX]}, Lwm2mState) ->
|
||||
?LOG(debug, "~p ~p REGISTER command Query=~p, Content=~p", [self(), ChId, Query, Content]),
|
||||
case parse_options(Query) of
|
||||
{error, {bad_opt, _CustomOption}} ->
|
||||
?LOG(error, "Reject REGISTER from ~p due to wrong option", [ChId]),
|
||||
{error, bad_request, Lwm2mState};
|
||||
{ok, LwM2MQuery} ->
|
||||
process_register(ChId, LwM2MQuery, Content#coap_content.payload, Lwm2mState)
|
||||
end;
|
||||
|
||||
% LWM2M UPDATE COMMAND
|
||||
coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = LocationPath}, Lwm2mState) ->
|
||||
?LOG(debug, "~p ~p UPDATE command location=~p, Query=~p, Content=~p", [self(), ChId, LocationPath, Query, Content]),
|
||||
case parse_options(Query) of
|
||||
{error, {bad_opt, _CustomOption}} ->
|
||||
?LOG(error, "Reject UPDATE from ~p due to wrong option, Query=~p", [ChId, Query]),
|
||||
{error, bad_request, Lwm2mState};
|
||||
{ok, LwM2MQuery} ->
|
||||
process_update(ChId, LwM2MQuery, LocationPath, Content#coap_content.payload, Lwm2mState)
|
||||
end;
|
||||
|
||||
coap_post(ChId, Prefix, Query, Content, Lwm2mState) ->
|
||||
?LOG(error, "bad post request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]),
|
||||
{error, bad_request, Lwm2mState}.
|
||||
|
||||
coap_put(_ChId, Prefix, Query, Content, Lwm2mState) ->
|
||||
?LOG(error, "put has error, Prefix=~p, Query=~p, Content=~p", [Prefix, Query, Content]),
|
||||
{error, bad_request, Lwm2mState}.
|
||||
|
||||
% LWM2M DE-REGISTER COMMAND
|
||||
coap_delete(ChId, [?PREFIX], #coap_content{uri_path = Location}, Lwm2mState) ->
|
||||
LocationPath = binary_util:join_path(Location),
|
||||
?LOG(debug, "~p ~p DELETE command location=~p", [self(), ChId, LocationPath]),
|
||||
case get(lwm2m_context) of
|
||||
#lwm2m_context{location = LocationPath} ->
|
||||
lwm2m_coap_responder:stop(deregister),
|
||||
{ok, Lwm2mState};
|
||||
undefined ->
|
||||
?LOG(error, "Reject DELETE from ~p, Location: ~p not found", [ChId, Location]),
|
||||
{error, forbidden, Lwm2mState};
|
||||
TrueLocation ->
|
||||
?LOG(error, "Reject DELETE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]),
|
||||
{error, not_found, Lwm2mState}
|
||||
end;
|
||||
coap_delete(_ChId, _Prefix, _Content, Lwm2mState) ->
|
||||
{error, forbidden, Lwm2mState}.
|
||||
|
||||
coap_observe(ChId, Prefix, Name, Ack, Lwm2mState) ->
|
||||
?LOG(error, "unsupported observe request ChId=~p, Prefix=~p, Name=~p, Ack=~p", [ChId, Prefix, Name, Ack]),
|
||||
{error, method_not_allowed, Lwm2mState}.
|
||||
|
||||
coap_unobserve(Lwm2mState) ->
|
||||
?LOG(error, "unsupported unobserve request: ~p", [Lwm2mState]),
|
||||
{ok, Lwm2mState}.
|
||||
|
||||
coap_response(ChId, Ref, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Lwm2mState) ->
|
||||
?LOG(info, "~p, RCV CoAP response, CoapMsgType: ~p, CoapMsgMethod: ~p, CoapMsgPayload: ~p,
|
||||
CoapMsgOpts: ~p, Ref: ~p",
|
||||
[ChId, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref]),
|
||||
MqttPayload = emqx_lwm2m_cmd_handler:coap2mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref),
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState),
|
||||
{noreply, Lwm2mState2}.
|
||||
|
||||
coap_ack(_ChId, Ref, Lwm2mState) ->
|
||||
?LOG(info, "~p, RCV CoAP Empty ACK, Ref: ~p", [_ChId, Ref]),
|
||||
AckRef = maps:put(<<"msgType">>, <<"ack">>, Ref),
|
||||
MqttPayload = emqx_lwm2m_cmd_handler:ack2mqtt(AckRef),
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState),
|
||||
{ok, Lwm2mState2}.
|
||||
|
||||
%% Batch deliver
|
||||
handle_info({deliver, Topic, Msgs}, Lwm2mState) when is_list(Msgs) ->
|
||||
{noreply, lists:foldl(fun(Msg, NewState) ->
|
||||
element(2, handle_info({deliver, Topic, Msg}, NewState))
|
||||
end, Lwm2mState, Msgs)};
|
||||
%% Handle MQTT Message
|
||||
handle_info({deliver, _Topic, MqttMsg}, Lwm2mState) ->
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:deliver(MqttMsg, Lwm2mState),
|
||||
{noreply, Lwm2mState2};
|
||||
|
||||
%% Deliver Coap Message to Device
|
||||
handle_info({deliver_to_coap, CoapRequest, Ref}, Lwm2mState) ->
|
||||
{send_request, CoapRequest, Ref, Lwm2mState};
|
||||
|
||||
handle_info({'EXIT', _Pid, Reason}, Lwm2mState) ->
|
||||
?LOG(info, "~p, received exit from: ~p, reason: ~p, quit now!", [self(), _Pid, Reason]),
|
||||
{stop, Reason, Lwm2mState};
|
||||
|
||||
handle_info(post_init, Lwm2mState) ->
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:post_init(Lwm2mState),
|
||||
{noreply, Lwm2mState2};
|
||||
|
||||
handle_info(auto_observe, Lwm2mState) ->
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:auto_observe(Lwm2mState),
|
||||
{noreply, Lwm2mState2};
|
||||
|
||||
handle_info({life_timer, expired}, Lwm2mState) ->
|
||||
?LOG(debug, "lifetime expired, shutdown", []),
|
||||
{stop, life_timer_expired, Lwm2mState};
|
||||
|
||||
handle_info({shutdown, Error}, Lwm2mState) ->
|
||||
{stop, Error, Lwm2mState};
|
||||
|
||||
handle_info({shutdown, conflict, {ClientId, NewPid}}, Lwm2mState) ->
|
||||
?LOG(warning, "lwm2m '~s' conflict with ~p, shutdown", [ClientId, NewPid]),
|
||||
{stop, conflict, Lwm2mState};
|
||||
|
||||
handle_info({suback, _MsgId, [_GrantedQos]}, Lwm2mState) ->
|
||||
{noreply, Lwm2mState};
|
||||
|
||||
handle_info(emit_stats, Lwm2mState) ->
|
||||
{noreply, Lwm2mState};
|
||||
|
||||
handle_info(Message, Lwm2mState) ->
|
||||
?LOG(error, "Unknown Message ~p", [Message]),
|
||||
{noreply, Lwm2mState}.
|
||||
|
||||
|
||||
handle_call(info, _From, Lwm2mState) ->
|
||||
{Info, Lwm2mState2} = emqx_lwm2m_protocol:get_info(Lwm2mState),
|
||||
{reply, Info, Lwm2mState2};
|
||||
|
||||
handle_call(stats, _From, Lwm2mState) ->
|
||||
{Stats, Lwm2mState2} = emqx_lwm2m_protocol:get_stats(Lwm2mState),
|
||||
{reply, Stats, Lwm2mState2};
|
||||
|
||||
handle_call(kick, _From, Lwm2mState) ->
|
||||
{stop, kick, Lwm2mState};
|
||||
|
||||
handle_call({set_rate_limit, _Rl}, _From, Lwm2mState) ->
|
||||
?LOG(error, "set_rate_limit is not support", []),
|
||||
{reply, ok, Lwm2mState};
|
||||
|
||||
handle_call(get_rate_limit, _From, Lwm2mState) ->
|
||||
?LOG(error, "get_rate_limit is not support", []),
|
||||
{reply, ok, Lwm2mState};
|
||||
|
||||
handle_call(session, _From, Lwm2mState) ->
|
||||
?LOG(error, "get_session is not support", []),
|
||||
{reply, ok, Lwm2mState};
|
||||
|
||||
handle_call(Request, _From, Lwm2mState) ->
|
||||
?LOG(error, "adapter unexpected call ~p", [Request]),
|
||||
{reply, ok, Lwm2mState}.
|
||||
|
||||
handle_cast(Msg, Lwm2mState) ->
|
||||
?LOG(error, "unexpected cast ~p", [Msg]),
|
||||
{noreply, Lwm2mState, hibernate}.
|
||||
|
||||
terminate(Reason, Lwm2mState) ->
|
||||
emqx_lwm2m_protocol:terminate(Reason, Lwm2mState).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% Internal Functions
|
||||
%%%%%%%%%%%%%%%%%%%%%%
|
||||
process_register(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState) ->
|
||||
Epn = maps:get(<<"ep">>, LwM2MQuery, undefined),
|
||||
LifeTime = maps:get(<<"lt">>, LwM2MQuery, undefined),
|
||||
Ver = maps:get(<<"lwm2m">>, LwM2MQuery, undefined),
|
||||
case check_lwm2m_version(Ver) of
|
||||
false ->
|
||||
?LOG(error, "Reject REGISTER from ~p due to unsupported version: ~p", [ChId, Ver]),
|
||||
lwm2m_coap_responder:stop(invalid_version),
|
||||
{error, precondition_failed, Lwm2mState};
|
||||
true ->
|
||||
case check_epn(Epn) andalso check_lifetime(LifeTime) of
|
||||
true ->
|
||||
init_lwm2m_emq_client(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState);
|
||||
false ->
|
||||
?LOG(error, "Reject REGISTER from ~p due to wrong parameters, epn=~p, lifetime=~p", [ChId, Epn, LifeTime]),
|
||||
lwm2m_coap_responder:stop(invalid_query_params),
|
||||
{error, bad_request, Lwm2mState}
|
||||
end
|
||||
end.
|
||||
|
||||
process_update(ChId, LwM2MQuery, Location, LwM2MPayload, Lwm2mState) ->
|
||||
LocationPath = binary_util:join_path(Location),
|
||||
case get(lwm2m_context) of
|
||||
#lwm2m_context{location = LocationPath} ->
|
||||
RegInfo = append_object_list(LwM2MQuery, LwM2MPayload),
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:update_reg_info(RegInfo, Lwm2mState),
|
||||
?LOG(info, "~p, UPDATE Success, assgined location: ~p", [ChId, LocationPath]),
|
||||
{ok, changed, #coap_content{}, Lwm2mState2};
|
||||
undefined ->
|
||||
?LOG(error, "Reject UPDATE from ~p, Location: ~p not found", [ChId, Location]),
|
||||
{error, forbidden, Lwm2mState};
|
||||
TrueLocation ->
|
||||
?LOG(error, "Reject UPDATE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]),
|
||||
{error, not_found, Lwm2mState}
|
||||
end.
|
||||
|
||||
init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, _Lwm2mState = undefined) ->
|
||||
RegInfo = append_object_list(LwM2MQuery, LwM2MPayload),
|
||||
case emqx_lwm2m_protocol:init(self(), Epn, ChId, RegInfo) of
|
||||
{ok, Lwm2mState} ->
|
||||
LocationPath = assign_location_path(Epn),
|
||||
?LOG(info, "~p, REGISTER Success, assgined location: ~p", [ChId, LocationPath]),
|
||||
{ok, created, #coap_content{location_path = LocationPath}, Lwm2mState};
|
||||
{error, Error} ->
|
||||
lwm2m_coap_responder:stop(Error),
|
||||
?LOG(error, "~p, REGISTER Failed, error: ~p", [ChId, Error]),
|
||||
{error, forbidden, undefined}
|
||||
end;
|
||||
init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, Lwm2mState) ->
|
||||
RegInfo = append_object_list(LwM2MQuery, LwM2MPayload),
|
||||
LocationPath = assign_location_path(Epn),
|
||||
?LOG(info, "~p, RE-REGISTER Success, location: ~p", [ChId, LocationPath]),
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:replace_reg_info(RegInfo, Lwm2mState),
|
||||
{ok, created, #coap_content{location_path = LocationPath}, Lwm2mState2}.
|
||||
|
||||
append_object_list(LwM2MQuery, <<>>) when map_size(LwM2MQuery) == 0 -> #{};
|
||||
append_object_list(LwM2MQuery, <<>>) -> LwM2MQuery;
|
||||
append_object_list(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) ->
|
||||
{AlterPath, ObjList} = parse_object_list(LwM2MPayload),
|
||||
LwM2MQuery#{
|
||||
<<"alternatePath">> => AlterPath,
|
||||
<<"objectList">> => ObjList
|
||||
}.
|
||||
|
||||
parse_options(InputQuery) ->
|
||||
parse_options(InputQuery, maps:new()).
|
||||
|
||||
parse_options([], Query) -> {ok, Query};
|
||||
parse_options([<<"ep=", Epn/binary>>|T], Query) ->
|
||||
parse_options(T, maps:put(<<"ep">>, Epn, Query));
|
||||
parse_options([<<"lt=", Lt/binary>>|T], Query) ->
|
||||
parse_options(T, maps:put(<<"lt">>, binary_to_integer(Lt), Query));
|
||||
parse_options([<<"lwm2m=", Ver/binary>>|T], Query) ->
|
||||
parse_options(T, maps:put(<<"lwm2m">>, Ver, Query));
|
||||
parse_options([<<"b=", Binding/binary>>|T], Query) ->
|
||||
parse_options(T, maps:put(<<"b">>, Binding, Query));
|
||||
parse_options([CustomOption|T], Query) ->
|
||||
case binary:split(CustomOption, <<"=">>) of
|
||||
[OptKey, OptValue] when OptKey =/= <<>> ->
|
||||
?LOG(debug, "non-standard option: ~p", [CustomOption]),
|
||||
parse_options(T, maps:put(OptKey, OptValue, Query));
|
||||
_BadOpt ->
|
||||
?LOG(error, "bad option: ~p", [CustomOption]),
|
||||
{error, {bad_opt, CustomOption}}
|
||||
end.
|
||||
|
||||
parse_object_list(<<>>) -> {<<"/">>, <<>>};
|
||||
parse_object_list(ObjLinks) when is_binary(ObjLinks) ->
|
||||
parse_object_list(binary:split(ObjLinks, <<",">>, [global]));
|
||||
|
||||
parse_object_list(FullObjLinkList) when is_list(FullObjLinkList) ->
|
||||
case drop_attr(FullObjLinkList) of
|
||||
{<<"/">>, _} = RootPrefixedLinks ->
|
||||
RootPrefixedLinks;
|
||||
{AlterPath, ObjLinkList} ->
|
||||
LenAlterPath = byte_size(AlterPath),
|
||||
WithOutPrefix =
|
||||
lists:map(
|
||||
fun
|
||||
(<<Prefix:LenAlterPath/binary, Link/binary>>) when Prefix =:= AlterPath ->
|
||||
trim(Link);
|
||||
(Link) -> Link
|
||||
end, ObjLinkList),
|
||||
{AlterPath, WithOutPrefix}
|
||||
end.
|
||||
|
||||
drop_attr(LinkList) ->
|
||||
lists:foldr(
|
||||
fun(Link, {AlternatePath, LinkAcc}) ->
|
||||
{MainLink, LinkAttrs} = parse_link(Link),
|
||||
case is_alternate_path(LinkAttrs) of
|
||||
false -> {AlternatePath, [MainLink | LinkAcc]};
|
||||
true -> {MainLink, LinkAcc}
|
||||
end
|
||||
end, {<<"/">>, []}, LinkList).
|
||||
|
||||
is_alternate_path(#{<<"rt">> := ?OMA_ALTER_PATH_RT}) -> true;
|
||||
is_alternate_path(_) -> false.
|
||||
|
||||
parse_link(Link) ->
|
||||
[MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]),
|
||||
{delink(trim(MainLink)), parse_link_attrs(Attrs)}.
|
||||
|
||||
parse_link_attrs(LinkAttrs) when is_list(LinkAttrs) ->
|
||||
lists:foldl(
|
||||
fun(Attr, Acc) ->
|
||||
case binary:split(trim(Attr), <<"=">>) of
|
||||
[AttrKey, AttrValue] when AttrKey =/= <<>> ->
|
||||
maps:put(AttrKey, AttrValue, Acc);
|
||||
_BadAttr -> throw({bad_attr, _BadAttr})
|
||||
end
|
||||
end, maps:new(), LinkAttrs).
|
||||
|
||||
trim(Str)-> binary_util:trim(Str, $ ).
|
||||
delink(Str) ->
|
||||
Ltrim = binary_util:ltrim(Str, $<),
|
||||
binary_util:rtrim(Ltrim, $>).
|
||||
|
||||
check_lwm2m_version(<<"1">>) -> true;
|
||||
check_lwm2m_version(<<"1.", _PatchVerNum/binary>>) -> true;
|
||||
check_lwm2m_version(_) -> false.
|
||||
|
||||
check_epn(undefined) -> false;
|
||||
check_epn(_) -> true.
|
||||
|
||||
check_lifetime(undefined) -> false;
|
||||
check_lifetime(LifeTime) when is_integer(LifeTime) ->
|
||||
Max = proplists:get_value(lifetime_max, lwm2m_coap_responder:options(), 315360000),
|
||||
Min = proplists:get_value(lifetime_min, lwm2m_coap_responder:options(), 0),
|
||||
if
|
||||
LifeTime >= Min, LifeTime =< Max ->
|
||||
true;
|
||||
true ->
|
||||
false
|
||||
end;
|
||||
check_lifetime(_) -> false.
|
||||
|
||||
|
||||
assign_location_path(Epn) ->
|
||||
%Location = list_to_binary(io_lib:format("~.16B", [rand:uniform(65535)])),
|
||||
%LocationPath = <<"/rd/", Location/binary>>,
|
||||
Location = [<<"rd">>, Epn],
|
||||
put(lwm2m_context, #lwm2m_context{epn = Epn, location = binary_util:join_path(Location)}),
|
||||
Location.
|
118
apps/emqx_lwm2m/src/emqx_lwm2m_coap_server.erl
Normal file
118
apps/emqx_lwm2m/src/emqx_lwm2m_coap_server.erl
Normal file
@ -0,0 +1,118 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_coap_server).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
|
||||
-export([ start/1
|
||||
, stop/1
|
||||
]).
|
||||
|
||||
-export([ start_listener/1
|
||||
, start_listener/3
|
||||
, stop_listener/1
|
||||
, stop_listener/2
|
||||
]).
|
||||
|
||||
-define(LOG(Level, Format, Args),
|
||||
logger:Level("LwM2M: " ++ Format, Args)).
|
||||
|
||||
start(Envs) ->
|
||||
start_listeners(Envs).
|
||||
|
||||
stop(Envs) ->
|
||||
stop_listeners(Envs).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal funcs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
start_listeners(Envs) ->
|
||||
lists:foreach(fun start_listener/1, listeners_confs(Envs)).
|
||||
|
||||
stop_listeners(Envs) ->
|
||||
lists:foreach(fun stop_listener/1, listeners_confs(Envs)).
|
||||
|
||||
start_listener({Proto, ListenOn, Opts}) ->
|
||||
case start_listener(Proto, ListenOn, Opts) of
|
||||
{ok, _Pid} ->
|
||||
io:format("Start lwm2m:~s listener on ~s successfully.~n",
|
||||
[Proto, format(ListenOn)]);
|
||||
{error, Reason} ->
|
||||
io:format(standard_error, "Failed to start lwm2m:~s listener on ~s: ~0p~n",
|
||||
[Proto, format(ListenOn), Reason]),
|
||||
error(Reason)
|
||||
end.
|
||||
|
||||
start_listener(udp, ListenOn, Opts) ->
|
||||
lwm2m_coap_server:start_udp('lwm2m:udp', ListenOn, Opts);
|
||||
start_listener(dtls, ListenOn, Opts) ->
|
||||
lwm2m_coap_server:start_dtls('lwm2m:dtls', ListenOn, Opts).
|
||||
|
||||
stop_listener({Proto, ListenOn, _Opts}) ->
|
||||
Ret = stop_listener(Proto, ListenOn),
|
||||
case Ret of
|
||||
ok -> io:format("Stop lwm2m:~s listener on ~s successfully.~n",
|
||||
[Proto, format(ListenOn)]);
|
||||
{error, Reason} ->
|
||||
io:format(standard_error, "Failed to stop lwm2m:~s listener on ~s: ~0p~n",
|
||||
[Proto, format(ListenOn), Reason])
|
||||
end,
|
||||
Ret.
|
||||
|
||||
stop_listener(udp, ListenOn) ->
|
||||
lwm2m_coap_server:stop_udp('lwm2m:udp', ListenOn);
|
||||
stop_listener(dtls, ListenOn) ->
|
||||
lwm2m_coap_server:stop_dtls('lwm2m:dtls', ListenOn).
|
||||
|
||||
listeners_confs(Envs) ->
|
||||
listeners_confs(udp, Envs) ++ listeners_confs(dtls, Envs).
|
||||
|
||||
listeners_confs(udp, Envs) ->
|
||||
Udps = proplists:get_value(bind_udp, Envs, []),
|
||||
Opts = proplists:get_value(options, Envs, []),
|
||||
[{udp, Port, [{udp_options, InetOpts ++ Opts}] ++ get_lwm2m_opts(Envs)} || {Port, InetOpts} <- Udps];
|
||||
|
||||
listeners_confs(dtls, Envs) ->
|
||||
Dtls = proplists:get_value(bind_dtls, Envs, []),
|
||||
Opts = proplists:get_value(dtls_opts, Envs, []),
|
||||
[{dtls, Port, [{dtls_options, InetOpts ++ Opts}] ++ get_lwm2m_opts(Envs)} || {Port, InetOpts} <- Dtls].
|
||||
|
||||
format(Port) when is_integer(Port) ->
|
||||
io_lib:format("0.0.0.0:~w", [Port]);
|
||||
format({Addr, Port}) when is_list(Addr) ->
|
||||
io_lib:format("~s:~w", [Addr, Port]);
|
||||
format({Addr, Port}) when is_tuple(Addr) ->
|
||||
io_lib:format("~s:~w", [inet:ntoa(Addr), Port]).
|
||||
|
||||
get_lwm2m_opts(Envs) ->
|
||||
LifetimeMax = proplists:get_value(lifetime_max, Envs, 315360000),
|
||||
LifetimeMin = proplists:get_value(lifetime_min, Envs, 0),
|
||||
Mountpoint = proplists:get_value(mountpoint, Envs, ""),
|
||||
Sockport = proplists:get_value(port, Envs, 5683),
|
||||
AutoObserve = proplists:get_value(auto_observe, Envs, []),
|
||||
QmodeTimeWindow = proplists:get_value(qmode_time_window, Envs, []),
|
||||
Topics = proplists:get_value(topics, Envs, []),
|
||||
PublishCondition = proplists:get_value(update_msg_publish_condition, Envs, contains_object_list),
|
||||
[{lifetime_max, LifetimeMax},
|
||||
{lifetime_min, LifetimeMin},
|
||||
{mountpoint, list_to_binary(Mountpoint)},
|
||||
{port, Sockport},
|
||||
{auto_observe, AutoObserve},
|
||||
{qmode_time_window, QmodeTimeWindow},
|
||||
{update_msg_publish_condition, PublishCondition},
|
||||
{topics, Topics}].
|
351
apps/emqx_lwm2m/src/emqx_lwm2m_json.erl
Normal file
351
apps/emqx_lwm2m/src/emqx_lwm2m_json.erl
Normal file
@ -0,0 +1,351 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_json).
|
||||
|
||||
-export([ tlv_to_json/2
|
||||
, json_to_tlv/2
|
||||
, text_to_json/2
|
||||
, opaque_to_json/2
|
||||
]).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-JSON: " ++ Format, Args)).
|
||||
|
||||
tlv_to_json(BaseName, TlvData) ->
|
||||
DecodedTlv = emqx_lwm2m_tlv:parse(TlvData),
|
||||
ObjectId = object_id(BaseName),
|
||||
ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true),
|
||||
case DecodedTlv of
|
||||
[#{tlv_resource_with_value:=Id, value:=Value}] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, Id, 3),
|
||||
encode_json(TrueBaseName, tlv_single_resource(Id, Value, ObjDefinition));
|
||||
List1 = [#{tlv_resource_with_value:=_Id}, _|_] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2),
|
||||
encode_json(TrueBaseName, tlv_level2(<<>>, List1, ObjDefinition, []));
|
||||
List2 = [#{tlv_multiple_resource:=_Id}|_] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2),
|
||||
encode_json(TrueBaseName, tlv_level2(<<>>, List2, ObjDefinition, []));
|
||||
[#{tlv_object_instance:=Id, value:=Value}] ->
|
||||
TrueBaseName = basename(BaseName, undefined, Id, undefined, 2),
|
||||
encode_json(TrueBaseName, tlv_level2(<<>>, Value, ObjDefinition, []));
|
||||
List3=[#{tlv_object_instance:=Id, value:=_Value}, _|_] ->
|
||||
TrueBaseName = basename(BaseName, Id, undefined, undefined, 1),
|
||||
encode_json(TrueBaseName, tlv_level1(List3, ObjDefinition, []))
|
||||
end.
|
||||
|
||||
|
||||
tlv_level1([], _ObjDefinition, Acc) ->
|
||||
Acc;
|
||||
tlv_level1([#{tlv_object_instance:=Id, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
New = tlv_level2(integer_to_binary(Id), Value, ObjDefinition, []),
|
||||
tlv_level1(T, ObjDefinition, Acc++New).
|
||||
|
||||
tlv_level2(_, [], _, Acc) ->
|
||||
Acc;
|
||||
tlv_level2(RelativePath, [#{tlv_resource_with_value:=ResourceId, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
{K, V} = value(Value, ResourceId, ObjDefinition),
|
||||
Name = name(RelativePath, ResourceId),
|
||||
New = #{n => Name, K => V},
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc++[New]);
|
||||
tlv_level2(RelativePath, [#{tlv_multiple_resource:=ResourceId, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
NewRelativePath = name(RelativePath, ResourceId),
|
||||
SubList = tlv_level3(NewRelativePath, Value, ResourceId, ObjDefinition, []),
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc++SubList).
|
||||
|
||||
tlv_level3(_RelativePath, [], _Id, _ObjDefinition, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
tlv_level3(RelativePath, [#{tlv_resource_instance:=InsId, value:=Value}|T], ResourceId, ObjDefinition, Acc) ->
|
||||
{K, V} = value(Value, ResourceId, ObjDefinition),
|
||||
Name = name(RelativePath, InsId),
|
||||
New = #{n => Name, K => V},
|
||||
tlv_level3(RelativePath, T, ResourceId, ObjDefinition, [New|Acc]).
|
||||
|
||||
tlv_single_resource(Id, Value, ObjDefinition) ->
|
||||
{K, V} = value(Value, Id, ObjDefinition),
|
||||
[#{K=>V}].
|
||||
|
||||
basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 3) ->
|
||||
?LOG(debug, "basename3 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]),
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, ResId/binary>>;
|
||||
[ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, (integer_to_binary(ResourceId))/binary>>;
|
||||
[ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary, $/, (integer_to_binary(ResourceId))/binary>>
|
||||
end;
|
||||
basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 2) ->
|
||||
?LOG(debug, "basename2 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]),
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, _ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>;
|
||||
[ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>;
|
||||
[ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary>>
|
||||
end;
|
||||
basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 1) ->
|
||||
?LOG(debug, "basename1 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]),
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId, _ObjInsId, _ResId] -> <<$/, ObjId/binary>>;
|
||||
[ObjId, _ObjInsId] -> <<$/, ObjId/binary>>;
|
||||
[ObjId] -> <<$/, ObjId/binary>>
|
||||
end.
|
||||
|
||||
|
||||
name(RelativePath, Id) ->
|
||||
case RelativePath of
|
||||
<<>> -> integer_to_binary(Id);
|
||||
_ -> <<RelativePath/binary, $/, (integer_to_binary(Id))/binary>>
|
||||
end.
|
||||
|
||||
|
||||
object_id(BaseName) ->
|
||||
case binary:split(binary_util:trim(BaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId] -> binary_to_integer(ObjId);
|
||||
[ObjId, _] -> binary_to_integer(ObjId);
|
||||
[ObjId, _, _] -> binary_to_integer(ObjId);
|
||||
[ObjId, _, _, _] -> binary_to_integer(ObjId)
|
||||
end.
|
||||
|
||||
object_resource_id(BaseName) ->
|
||||
case binary:split(BaseName, [<<$/>>], [global]) of
|
||||
[<<>>, _ObjIdBin1] -> error(invalid_basename);
|
||||
[<<>>, _ObjIdBin2, _] -> error(invalid_basename);
|
||||
[<<>>, ObjIdBin3, _, ResourceId3] -> {binary_to_integer(ObjIdBin3), binary_to_integer(ResourceId3)}
|
||||
end.
|
||||
|
||||
% TLV binary to json text
|
||||
value(Value, ResourceId, ObjDefinition) ->
|
||||
case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of
|
||||
"String" ->
|
||||
{sv, Value}; % keep binary type since it is same as a string for jsx
|
||||
"Integer" ->
|
||||
Size = byte_size(Value)*8,
|
||||
<<IntResult:Size>> = Value,
|
||||
{v, IntResult};
|
||||
"Float" ->
|
||||
Size = byte_size(Value)*8,
|
||||
<<FloatResult:Size/float>> = Value,
|
||||
{v, FloatResult};
|
||||
"Boolean" ->
|
||||
B = case Value of
|
||||
<<0>> -> false;
|
||||
<<1>> -> true
|
||||
end,
|
||||
{bv, B};
|
||||
"Opaque" ->
|
||||
{sv, base64:decode(Value)};
|
||||
"Time" ->
|
||||
Size = byte_size(Value)*8,
|
||||
<<IntResult:Size>> = Value,
|
||||
{v, IntResult};
|
||||
"Objlnk" ->
|
||||
<<ObjId:16, ObjInsId:16>> = Value,
|
||||
{ov, list_to_binary(io_lib:format("~b:~b", [ObjId, ObjInsId]))}
|
||||
end.
|
||||
|
||||
|
||||
encode_json(BaseName, E) ->
|
||||
?LOG(debug, "encode_json BaseName=~p, E=~p", [BaseName, E]),
|
||||
#{bn=>BaseName, e=>E}.
|
||||
|
||||
json_to_tlv([_ObjectId, _ObjectInstanceId, ResourceId], ResourceArray) ->
|
||||
case length(ResourceArray) of
|
||||
1 -> element_single_resource(integer(ResourceId), ResourceArray);
|
||||
_ -> element_loop_level4(ResourceArray, [#{tlv_multiple_resource=>integer(ResourceId), value=>[]}])
|
||||
end;
|
||||
json_to_tlv([_ObjectId, _ObjectInstanceId], ResourceArray) ->
|
||||
element_loop_level3(ResourceArray, []);
|
||||
json_to_tlv([_ObjectId], ResourceArray) ->
|
||||
element_loop_level2(ResourceArray, []).
|
||||
|
||||
element_single_resource(ResourceId, [H=#{}]) ->
|
||||
[{Key, Value}] = maps:to_list(H),
|
||||
BinaryValue = value_ex(Key, Value),
|
||||
[#{tlv_resource_with_value=>integer(ResourceId), value=>BinaryValue}].
|
||||
|
||||
element_loop_level2([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level2([H|T], Acc) ->
|
||||
NewAcc = insert(object, H, Acc),
|
||||
element_loop_level2(T, NewAcc).
|
||||
|
||||
element_loop_level3([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level3([H|T], Acc) ->
|
||||
NewAcc = insert(object_instance, H, Acc),
|
||||
element_loop_level3(T, NewAcc).
|
||||
|
||||
element_loop_level4([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level4([H|T], Acc) ->
|
||||
NewAcc = insert(resource, H, Acc),
|
||||
element_loop_level4(T, NewAcc).
|
||||
|
||||
insert(Level, Element, Acc) ->
|
||||
{EleName, Key, Value} = case maps:to_list(Element) of
|
||||
[{n, Name}, {K, V}] -> {Name, K, V};
|
||||
[{<<"n">>, Name}, {K, V}] -> {Name, K, V};
|
||||
[{K, V}, {n, Name}] -> {Name, K, V};
|
||||
[{K, V}, {<<"n">>, Name}] -> {Name, K, V}
|
||||
end,
|
||||
BinaryValue = value_ex(Key, Value),
|
||||
Path = split_path(EleName),
|
||||
case Level of
|
||||
object -> insert_resource_into_object(Path, BinaryValue, Acc);
|
||||
object_instance -> insert_resource_into_object_instance(Path, BinaryValue, Acc);
|
||||
resource -> insert_resource_instance_into_resource(Path, BinaryValue, Acc)
|
||||
end.
|
||||
|
||||
|
||||
% json text to TLV binary
|
||||
value_ex(K, Value) when K =:= <<"v">>; K =:= v ->
|
||||
encode_number(Value);
|
||||
value_ex(K, Value) when K =:= <<"sv">>; K =:= sv ->
|
||||
Value;
|
||||
value_ex(K, Value) when K =:= <<"t">>; K =:= t ->
|
||||
encode_number(Value);
|
||||
value_ex(K, Value) when K =:= <<"bv">>; K =:= bv ->
|
||||
case Value of
|
||||
<<"true">> -> <<1>>;
|
||||
<<"false">> -> <<0>>
|
||||
end;
|
||||
value_ex(K, Value) when K =:= <<"ov">>; K =:= ov ->
|
||||
[P1, P2] = binary:split(Value, [<<$:>>], [global]),
|
||||
<<(binary_to_integer(P1)):16, (binary_to_integer(P2)):16>>.
|
||||
|
||||
insert_resource_into_object([ObjectInstanceId|OtherIds], Value, Acc) ->
|
||||
?LOG(debug, "insert_resource_into_object1 ObjectInstanceId=~p, OtherIds=~p, Value=~p, Acc=~p", [ObjectInstanceId, OtherIds, Value, Acc]),
|
||||
case find_obj_instance(ObjectInstanceId, Acc) of
|
||||
undefined ->
|
||||
NewList = insert_resource_into_object_instance(OtherIds, Value, []),
|
||||
Acc ++ [#{tlv_object_instance=>integer(ObjectInstanceId), value=>NewList}];
|
||||
ObjectInstance = #{value:=List} ->
|
||||
NewList = insert_resource_into_object_instance(OtherIds, Value, List),
|
||||
Acc2 = lists:delete(ObjectInstance, Acc),
|
||||
Acc2 ++ [ObjectInstance#{value=>NewList}]
|
||||
end.
|
||||
|
||||
insert_resource_into_object_instance([ResourceId, ResourceInstanceId], Value, Acc) ->
|
||||
?LOG(debug, "insert_resource_into_object_instance1() ResourceId=~p, ResourceInstanceId=~p, Value=~p, Acc=~p", [ResourceId, ResourceInstanceId, Value, Acc]),
|
||||
case find_resource(ResourceId, Acc) of
|
||||
undefined ->
|
||||
NewList = insert_resource_instance_into_resource([ResourceInstanceId], Value, []),
|
||||
Acc++[#{tlv_multiple_resource=>integer(ResourceId), value=>NewList}];
|
||||
Resource = #{value:=List}->
|
||||
NewList = insert_resource_instance_into_resource([ResourceInstanceId], Value, List),
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [Resource#{value=>NewList}]
|
||||
end;
|
||||
insert_resource_into_object_instance([ResourceId], Value, Acc) ->
|
||||
?LOG(debug, "insert_resource_into_object_instance2() ResourceId=~p, Value=~p, Acc=~p", [ResourceId, Value, Acc]),
|
||||
NewMap = #{tlv_resource_with_value=>integer(ResourceId), value=>Value},
|
||||
case find_resource(ResourceId, Acc) of
|
||||
undefined ->
|
||||
Acc ++ [NewMap];
|
||||
Resource ->
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [NewMap]
|
||||
end.
|
||||
|
||||
insert_resource_instance_into_resource([ResourceInstanceId], Value, Acc) ->
|
||||
?LOG(debug, "insert_resource_instance_into_resource() ResourceInstanceId=~p, Value=~p, Acc=~p", [ResourceInstanceId, Value, Acc]),
|
||||
NewMap = #{tlv_resource_instance=>integer(ResourceInstanceId), value=>Value},
|
||||
case find_resource_instance(ResourceInstanceId, Acc) of
|
||||
undefined ->
|
||||
Acc ++ [NewMap];
|
||||
Resource ->
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [NewMap]
|
||||
end.
|
||||
|
||||
|
||||
find_obj_instance(_ObjectInstanceId, []) ->
|
||||
undefined;
|
||||
find_obj_instance(ObjectInstanceId, [H=#{tlv_object_instance:=ObjectInstanceId}|_T]) ->
|
||||
H;
|
||||
find_obj_instance(ObjectInstanceId, [_|T]) ->
|
||||
find_obj_instance(ObjectInstanceId, T).
|
||||
|
||||
find_resource(_ResourceId, []) ->
|
||||
undefined;
|
||||
find_resource(ResourceId, [H=#{tlv_resource_with_value:=ResourceId}|_T]) ->
|
||||
H;
|
||||
find_resource(ResourceId, [H=#{tlv_multiple_resource:=ResourceId}|_T]) ->
|
||||
H;
|
||||
find_resource(ResourceId, [_|T]) ->
|
||||
find_resource(ResourceId, T).
|
||||
|
||||
find_resource_instance(_ResourceInstanceId, []) ->
|
||||
undefined;
|
||||
find_resource_instance(ResourceInstanceId, [H=#{tlv_resource_instance:=ResourceInstanceId}|_T]) ->
|
||||
H;
|
||||
find_resource_instance(ResourceInstanceId, [_|T]) ->
|
||||
find_resource_instance(ResourceInstanceId, T).
|
||||
|
||||
split_path(Path) ->
|
||||
List = binary:split(Path, [<<$/>>], [global]),
|
||||
path(List, []).
|
||||
|
||||
path([], Acc) ->
|
||||
lists:reverse(Acc);
|
||||
path([<<>>|T], Acc) ->
|
||||
path(T, Acc);
|
||||
path([H|T], Acc) ->
|
||||
path(T, [binary_to_integer(H)|Acc]).
|
||||
|
||||
|
||||
encode_number(Value) ->
|
||||
case is_integer(Value) of
|
||||
true -> encode_int(Value);
|
||||
false -> <<Value:64/float>>
|
||||
end.
|
||||
|
||||
encode_int(Int) -> binary:encode_unsigned(Int).
|
||||
|
||||
text_to_json(BaseName, Text) ->
|
||||
{ObjectId, ResourceId} = object_resource_id(BaseName),
|
||||
ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true),
|
||||
{K, V} = text_value(Text, ResourceId, ObjDefinition),
|
||||
#{bn=>BaseName, e=>[#{K=>V}]}.
|
||||
|
||||
|
||||
% text to json
|
||||
text_value(Text, ResourceId, ObjDefinition) ->
|
||||
case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of
|
||||
"String" ->
|
||||
{sv, Text}; % keep binary type since it is same as a string for jsx
|
||||
"Integer" ->
|
||||
{v, binary_to_integer(Text)};
|
||||
"Float" ->
|
||||
{v, binary_to_float(Text)};
|
||||
"Boolean" ->
|
||||
B = case Text of
|
||||
<<"true">> -> false;
|
||||
<<"false">> -> true
|
||||
end,
|
||||
{bv, B};
|
||||
"Opaque" ->
|
||||
% keep the base64 string
|
||||
{sv, Text};
|
||||
"Time" ->
|
||||
{v, binary_to_integer(Text)};
|
||||
"Objlnk" ->
|
||||
{ov, Text}
|
||||
end.
|
||||
|
||||
opaque_to_json(BaseName, Binary) ->
|
||||
#{bn=>BaseName, e=>[#{sv=>base64:encode(Binary)}]}.
|
||||
|
||||
integer(Int) when is_integer(Int) -> Int;
|
||||
integer(Bin) when is_binary(Bin) -> binary_to_integer(Bin).
|
387
apps/emqx_lwm2m/src/emqx_lwm2m_message.erl
Normal file
387
apps/emqx_lwm2m/src/emqx_lwm2m_message.erl
Normal file
@ -0,0 +1,387 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_message).
|
||||
|
||||
-export([ tlv_to_json/2
|
||||
, json_to_tlv/2
|
||||
, text_to_json/2
|
||||
, opaque_to_json/2
|
||||
, translate_json/1
|
||||
]).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-JSON: " ++ Format, Args)).
|
||||
|
||||
tlv_to_json(BaseName, TlvData) ->
|
||||
DecodedTlv = emqx_lwm2m_tlv:parse(TlvData),
|
||||
ObjectId = object_id(BaseName),
|
||||
ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true),
|
||||
case DecodedTlv of
|
||||
[#{tlv_resource_with_value:=Id, value:=Value}] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, Id, 3),
|
||||
tlv_single_resource(TrueBaseName, Id, Value, ObjDefinition);
|
||||
List1 = [#{tlv_resource_with_value:=_Id}, _|_] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2),
|
||||
tlv_level2(TrueBaseName, List1, ObjDefinition, []);
|
||||
List2 = [#{tlv_multiple_resource:=_Id}|_] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2),
|
||||
tlv_level2(TrueBaseName, List2, ObjDefinition, []);
|
||||
[#{tlv_object_instance:=Id, value:=Value}] ->
|
||||
TrueBaseName = basename(BaseName, undefined, Id, undefined, 2),
|
||||
tlv_level2(TrueBaseName, Value, ObjDefinition, []);
|
||||
List3=[#{tlv_object_instance:=_Id}, _|_] ->
|
||||
tlv_level1(integer_to_binary(ObjectId), List3, ObjDefinition, [])
|
||||
end.
|
||||
|
||||
|
||||
tlv_level1(_Path, [], _ObjDefinition, Acc) ->
|
||||
Acc;
|
||||
tlv_level1(Path, [#{tlv_object_instance:=Id, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
New = tlv_level2(make_path(Path, Id), Value, ObjDefinition, []),
|
||||
tlv_level1(Path, T, ObjDefinition, Acc++New).
|
||||
|
||||
tlv_level2(_, [], _, Acc) ->
|
||||
Acc;
|
||||
tlv_level2(RelativePath, [#{tlv_resource_with_value:=ResourceId, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
Val = value(Value, ResourceId, ObjDefinition),
|
||||
New = #{path => make_path(RelativePath, ResourceId),
|
||||
value=>Val},
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc++[New]);
|
||||
tlv_level2(RelativePath, [#{tlv_multiple_resource:=ResourceId, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
SubList = tlv_level3(make_path(RelativePath, ResourceId),
|
||||
Value, ResourceId, ObjDefinition, []),
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc++SubList).
|
||||
|
||||
tlv_level3(_RelativePath, [], _Id, _ObjDefinition, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
tlv_level3(RelativePath, [#{tlv_resource_instance:=InsId, value:=Value}|T], ResourceId, ObjDefinition, Acc) ->
|
||||
Val = value(Value, ResourceId, ObjDefinition),
|
||||
New = #{path => make_path(RelativePath, InsId),
|
||||
value=>Val},
|
||||
tlv_level3(RelativePath, T, ResourceId, ObjDefinition, [New|Acc]).
|
||||
|
||||
tlv_single_resource(BaseName, Id, Value, ObjDefinition) ->
|
||||
Val = value(Value, Id, ObjDefinition),
|
||||
[#{path=>BaseName, value=>Val}].
|
||||
|
||||
basename(OldBaseName, _ObjectId, ObjectInstanceId, ResourceId, 3) ->
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, ResId/binary>>;
|
||||
[ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, (integer_to_binary(ResourceId))/binary>>;
|
||||
[ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary, $/, (integer_to_binary(ResourceId))/binary>>
|
||||
end;
|
||||
basename(OldBaseName, _ObjectId, ObjectInstanceId, _ResourceId, 2) ->
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, _ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>;
|
||||
[ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>;
|
||||
[ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary>>
|
||||
end.
|
||||
% basename(OldBaseName, _ObjectId, _ObjectInstanceId, _ResourceId, 1) ->
|
||||
% case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
% [ObjId, _ObjInsId, _ResId] -> <<$/, ObjId/binary>>;
|
||||
% [ObjId, _ObjInsId] -> <<$/, ObjId/binary>>;
|
||||
% [ObjId] -> <<$/, ObjId/binary>>
|
||||
% end.
|
||||
|
||||
make_path(RelativePath, Id) ->
|
||||
<<RelativePath/binary, $/, (integer_to_binary(Id))/binary>>.
|
||||
|
||||
object_id(BaseName) ->
|
||||
case binary:split(binary_util:trim(BaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId] -> binary_to_integer(ObjId);
|
||||
[ObjId, _] -> binary_to_integer(ObjId);
|
||||
[ObjId, _, _] -> binary_to_integer(ObjId);
|
||||
[ObjId, _, _, _] -> binary_to_integer(ObjId)
|
||||
end.
|
||||
|
||||
object_resource_id(BaseName) ->
|
||||
case binary:split(binary_util:trim(BaseName, $/), [<<$/>>], [global]) of
|
||||
[_ObjIdBin1] -> error({invalid_basename, BaseName});
|
||||
[_ObjIdBin2, _] -> error({invalid_basename, BaseName});
|
||||
[ObjIdBin3, _, ResourceId3] ->
|
||||
{binary_to_integer(ObjIdBin3), binary_to_integer(ResourceId3)};
|
||||
[ObjIdBin3, _, ResourceId3, _] ->
|
||||
{binary_to_integer(ObjIdBin3), binary_to_integer(ResourceId3)}
|
||||
end.
|
||||
|
||||
% TLV binary to json text
|
||||
value(Value, ResourceId, ObjDefinition) ->
|
||||
case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of
|
||||
"String" ->
|
||||
Value; % keep binary type since it is same as a string for jsx
|
||||
"Integer" ->
|
||||
Size = byte_size(Value)*8,
|
||||
<<IntResult:Size/signed>> = Value,
|
||||
IntResult;
|
||||
"Float" ->
|
||||
Size = byte_size(Value)*8,
|
||||
<<FloatResult:Size/float>> = Value,
|
||||
FloatResult;
|
||||
"Boolean" ->
|
||||
B = case Value of
|
||||
<<0>> -> false;
|
||||
<<1>> -> true
|
||||
end,
|
||||
B;
|
||||
"Opaque" ->
|
||||
base64:encode(Value);
|
||||
"Time" ->
|
||||
Size = byte_size(Value)*8,
|
||||
<<IntResult:Size>> = Value,
|
||||
IntResult;
|
||||
"Objlnk" ->
|
||||
<<ObjId:16, ObjInsId:16>> = Value,
|
||||
list_to_binary(io_lib:format("~b:~b", [ObjId, ObjInsId]))
|
||||
end.
|
||||
|
||||
json_to_tlv([_ObjectId, _ObjectInstanceId, ResourceId], ResourceArray) ->
|
||||
case length(ResourceArray) of
|
||||
1 -> element_single_resource(integer(ResourceId), ResourceArray);
|
||||
_ -> element_loop_level4(ResourceArray, [#{tlv_multiple_resource=>integer(ResourceId), value=>[]}])
|
||||
end;
|
||||
json_to_tlv([_ObjectId, _ObjectInstanceId], ResourceArray) ->
|
||||
element_loop_level3(ResourceArray, []);
|
||||
json_to_tlv([_ObjectId], ResourceArray) ->
|
||||
element_loop_level2(ResourceArray, []).
|
||||
|
||||
element_single_resource(ResourceId, [#{<<"type">> := Type, <<"value">> := Value}]) ->
|
||||
BinaryValue = value_ex(Type, Value),
|
||||
[#{tlv_resource_with_value=>integer(ResourceId), value=>BinaryValue}].
|
||||
|
||||
element_loop_level2([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level2([H|T], Acc) ->
|
||||
NewAcc = insert(object, H, Acc),
|
||||
element_loop_level2(T, NewAcc).
|
||||
|
||||
element_loop_level3([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level3([H|T], Acc) ->
|
||||
NewAcc = insert(object_instance, H, Acc),
|
||||
element_loop_level3(T, NewAcc).
|
||||
|
||||
element_loop_level4([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level4([H|T], Acc) ->
|
||||
NewAcc = insert(resource, H, Acc),
|
||||
element_loop_level4(T, NewAcc).
|
||||
|
||||
insert(Level, #{<<"path">> := EleName, <<"type">> := Type, <<"value">> := Value}, Acc) ->
|
||||
BinaryValue = value_ex(Type, Value),
|
||||
Path = split_path(EleName),
|
||||
case Level of
|
||||
object -> insert_resource_into_object(Path, BinaryValue, Acc);
|
||||
object_instance -> insert_resource_into_object_instance(Path, BinaryValue, Acc);
|
||||
resource -> insert_resource_instance_into_resource(hd(Path), BinaryValue, Acc)
|
||||
end.
|
||||
|
||||
% json text to TLV binary
|
||||
value_ex(K, Value) when K =:= <<"Integer">>; K =:= <<"Float">>; K =:= <<"Time">> ->
|
||||
encode_number(Value);
|
||||
|
||||
value_ex(K, Value) when K =:= <<"String">> ->
|
||||
Value;
|
||||
value_ex(K, Value) when K =:= <<"Opaque">> ->
|
||||
Value;
|
||||
value_ex(K, <<"true">>) when K =:= <<"Boolean">> -> <<1>>;
|
||||
value_ex(K, <<"false">>) when K =:= <<"Boolean">> -> <<0>>;
|
||||
|
||||
value_ex(K, Value) when K =:= <<"Objlnk">>; K =:= ov ->
|
||||
[P1, P2] = binary:split(Value, [<<$:>>], [global]),
|
||||
<<(binary_to_integer(P1)):16, (binary_to_integer(P2)):16>>.
|
||||
|
||||
insert_resource_into_object([ObjectInstanceId|OtherIds], Value, Acc) ->
|
||||
case find_obj_instance(ObjectInstanceId, Acc) of
|
||||
undefined ->
|
||||
NewList = insert_resource_into_object_instance(OtherIds, Value, []),
|
||||
Acc ++ [#{tlv_object_instance=>integer(ObjectInstanceId), value=>NewList}];
|
||||
ObjectInstance = #{value:=List} ->
|
||||
NewList = insert_resource_into_object_instance(OtherIds, Value, List),
|
||||
Acc2 = lists:delete(ObjectInstance, Acc),
|
||||
Acc2 ++ [ObjectInstance#{value=>NewList}]
|
||||
end.
|
||||
|
||||
insert_resource_into_object_instance([ResourceId, ResourceInstanceId], Value, Acc) ->
|
||||
case find_resource(ResourceId, Acc) of
|
||||
undefined ->
|
||||
NewList = insert_resource_instance_into_resource(ResourceInstanceId, Value, []),
|
||||
Acc++[#{tlv_multiple_resource=>integer(ResourceId), value=>NewList}];
|
||||
Resource = #{value:=List}->
|
||||
NewList = insert_resource_instance_into_resource(ResourceInstanceId, Value, List),
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [Resource#{value=>NewList}]
|
||||
end;
|
||||
insert_resource_into_object_instance([ResourceId], Value, Acc) ->
|
||||
NewMap = #{tlv_resource_with_value=>integer(ResourceId), value=>Value},
|
||||
case find_resource(ResourceId, Acc) of
|
||||
undefined ->
|
||||
Acc ++ [NewMap];
|
||||
Resource ->
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [NewMap]
|
||||
end.
|
||||
|
||||
insert_resource_instance_into_resource(ResourceInstanceId, Value, Acc) ->
|
||||
NewMap = #{tlv_resource_instance=>integer(ResourceInstanceId), value=>Value},
|
||||
case find_resource_instance(ResourceInstanceId, Acc) of
|
||||
undefined ->
|
||||
Acc ++ [NewMap];
|
||||
Resource ->
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [NewMap]
|
||||
end.
|
||||
|
||||
|
||||
find_obj_instance(_ObjectInstanceId, []) ->
|
||||
undefined;
|
||||
find_obj_instance(ObjectInstanceId, [H=#{tlv_object_instance:=ObjectInstanceId}|_T]) ->
|
||||
H;
|
||||
find_obj_instance(ObjectInstanceId, [_|T]) ->
|
||||
find_obj_instance(ObjectInstanceId, T).
|
||||
|
||||
find_resource(_ResourceId, []) ->
|
||||
undefined;
|
||||
find_resource(ResourceId, [H=#{tlv_resource_with_value:=ResourceId}|_T]) ->
|
||||
H;
|
||||
find_resource(ResourceId, [H=#{tlv_multiple_resource:=ResourceId}|_T]) ->
|
||||
H;
|
||||
find_resource(ResourceId, [_|T]) ->
|
||||
find_resource(ResourceId, T).
|
||||
|
||||
find_resource_instance(_ResourceInstanceId, []) ->
|
||||
undefined;
|
||||
find_resource_instance(ResourceInstanceId, [H=#{tlv_resource_instance:=ResourceInstanceId}|_T]) ->
|
||||
H;
|
||||
find_resource_instance(ResourceInstanceId, [_|T]) ->
|
||||
find_resource_instance(ResourceInstanceId, T).
|
||||
|
||||
split_path(Path) ->
|
||||
List = binary:split(Path, [<<$/>>], [global]),
|
||||
path(List, []).
|
||||
|
||||
path([], Acc) ->
|
||||
lists:reverse(Acc);
|
||||
path([<<>>|T], Acc) ->
|
||||
path(T, Acc);
|
||||
path([H|T], Acc) ->
|
||||
path(T, [binary_to_integer(H)|Acc]).
|
||||
|
||||
text_to_json(BaseName, Text) ->
|
||||
{ObjectId, ResourceId} = object_resource_id(BaseName),
|
||||
ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true),
|
||||
Val = text_value(Text, ResourceId, ObjDefinition),
|
||||
[#{path => BaseName, value => Val}].
|
||||
|
||||
% text to json
|
||||
text_value(Text, ResourceId, ObjDefinition) ->
|
||||
case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of
|
||||
"String" ->
|
||||
Text;
|
||||
"Integer" ->
|
||||
binary_to_number(Text);
|
||||
"Float" ->
|
||||
binary_to_number(Text);
|
||||
"Boolean" ->
|
||||
B = case Text of
|
||||
<<"true">> -> false;
|
||||
<<"false">> -> true
|
||||
end,
|
||||
B;
|
||||
"Opaque" ->
|
||||
% should we keep the base64 string ?
|
||||
base64:encode(Text);
|
||||
"Time" ->
|
||||
binary_to_number(Text);
|
||||
"Objlnk" ->
|
||||
Text
|
||||
end.
|
||||
|
||||
opaque_to_json(BaseName, Binary) ->
|
||||
[#{path => BaseName, value => base64:encode(Binary)}].
|
||||
|
||||
translate_json(JSONBin) ->
|
||||
JSONTerm = emqx_json:decode(JSONBin, [return_maps]),
|
||||
BaseName = maps:get(<<"bn">>, JSONTerm, <<>>),
|
||||
ElementList = maps:get(<<"e">>, JSONTerm, []),
|
||||
translate_element(BaseName, ElementList, []).
|
||||
|
||||
translate_element(_BaseName, [], Acc) ->
|
||||
lists:reverse(Acc);
|
||||
translate_element(BaseName, [Element | ElementList], Acc) ->
|
||||
RelativePath = maps:get(<<"n">>, Element, <<>>),
|
||||
FullPath = full_path(BaseName, RelativePath),
|
||||
NewAcc = [
|
||||
#{path => FullPath,
|
||||
value => get_element_value(Element)
|
||||
} | Acc],
|
||||
translate_element(BaseName, ElementList, NewAcc).
|
||||
|
||||
full_path(BaseName, RelativePath) ->
|
||||
Prefix = binary_util:rtrim(BaseName, $/),
|
||||
Path = binary_util:ltrim(RelativePath, $/),
|
||||
<<Prefix/binary, $/, Path/binary>>.
|
||||
|
||||
get_element_value(#{ <<"t">> := Value}) -> Value;
|
||||
get_element_value(#{ <<"v">> := Value}) -> Value;
|
||||
get_element_value(#{ <<"bv">> := Value}) -> Value;
|
||||
get_element_value(#{ <<"ov">> := Value}) -> Value;
|
||||
get_element_value(#{ <<"sv">> := Value}) -> Value;
|
||||
get_element_value(_) -> null.
|
||||
|
||||
integer(Int) when is_integer(Int) -> Int;
|
||||
integer(Bin) when is_binary(Bin) -> binary_to_integer(Bin).
|
||||
|
||||
%% encode number to its binary representation
|
||||
encode_number(NumStr) when is_binary(NumStr) ->
|
||||
try
|
||||
Int = binary_to_integer(NumStr),
|
||||
encode_int(Int)
|
||||
catch
|
||||
error:badarg ->
|
||||
Float = binary_to_float(NumStr),
|
||||
<<Float:64/float>>
|
||||
end;
|
||||
encode_number(Int) when is_integer(Int) ->
|
||||
encode_int(Int);
|
||||
encode_number(Float) when is_float(Float) ->
|
||||
<<Float:64/float>>.
|
||||
|
||||
encode_int(Int) when Int >= 0 ->
|
||||
binary:encode_unsigned(Int);
|
||||
encode_int(Int) when Int < 0 ->
|
||||
Size = byte_size_of_signed(-Int) * 8,
|
||||
<<Int:Size/signed>>.
|
||||
|
||||
byte_size_of_signed(UInt) ->
|
||||
byte_size_of_signed(UInt, 0).
|
||||
|
||||
byte_size_of_signed(UInt, N) ->
|
||||
BitSize = (8*N - 1),
|
||||
Max = (1 bsl BitSize),
|
||||
if
|
||||
UInt =< Max -> N;
|
||||
UInt > Max -> byte_size_of_signed(UInt, N+1)
|
||||
end.
|
||||
|
||||
binary_to_number(NumStr) ->
|
||||
try
|
||||
binary_to_integer(NumStr)
|
||||
catch
|
||||
error:badarg ->
|
||||
binary_to_float(NumStr)
|
||||
end.
|
511
apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl
Normal file
511
apps/emqx_lwm2m/src/emqx_lwm2m_protocol.erl
Normal file
@ -0,0 +1,511 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_protocol).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
%% API.
|
||||
-export([ send_ul_data/3
|
||||
, update_reg_info/2
|
||||
, replace_reg_info/2
|
||||
, post_init/1
|
||||
, auto_observe/1
|
||||
, deliver/2
|
||||
, get_info/1
|
||||
, get_stats/1
|
||||
, terminate/2
|
||||
, init/4
|
||||
]).
|
||||
|
||||
%% For Mgmt
|
||||
-export([ call/2
|
||||
, call/3
|
||||
]).
|
||||
|
||||
-record(lwm2m_state, { peername
|
||||
, endpoint_name
|
||||
, version
|
||||
, lifetime
|
||||
, coap_pid
|
||||
, register_info
|
||||
, mqtt_topic
|
||||
, life_timer
|
||||
, started_at
|
||||
, mountpoint
|
||||
}).
|
||||
|
||||
-define(DEFAULT_KEEP_ALIVE_DURATION, 60*2).
|
||||
|
||||
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
|
||||
|
||||
-define(SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => 0, is_new => true}).
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-PROTO: " ++ Format, Args)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
call(Pid, Msg) ->
|
||||
call(Pid, Msg, 5000).
|
||||
|
||||
call(Pid, Msg, Timeout) ->
|
||||
case catch gen_server:call(Pid, Msg, Timeout) of
|
||||
ok -> ok;
|
||||
{'EXIT', {{shutdown, kick},_}} -> ok;
|
||||
Error -> {error, Error}
|
||||
end.
|
||||
|
||||
init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">> := LifeTime, <<"lwm2m">> := Ver}) ->
|
||||
Mountpoint = proplists:get_value(mountpoint, lwm2m_coap_responder:options(), ""),
|
||||
Lwm2mState = #lwm2m_state{peername = Peername,
|
||||
endpoint_name = EndpointName,
|
||||
version = Ver,
|
||||
lifetime = LifeTime,
|
||||
coap_pid = CoapPid,
|
||||
register_info = RegInfo,
|
||||
mountpoint = Mountpoint},
|
||||
ClientInfo = clientinfo(Lwm2mState),
|
||||
_ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined),
|
||||
case emqx_access_control:authenticate(ClientInfo) of
|
||||
{ok, AuthResult} ->
|
||||
_ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined),
|
||||
|
||||
ClientInfo1 = maps:merge(ClientInfo, AuthResult),
|
||||
Sockport = proplists:get_value(port, lwm2m_coap_responder:options(), 5683),
|
||||
ClientInfo2 = maps:put(sockport, Sockport, ClientInfo1),
|
||||
Lwm2mState1 = Lwm2mState#lwm2m_state{started_at = time_now(),
|
||||
mountpoint = maps:get(mountpoint, ClientInfo2)},
|
||||
run_hooks('client.connected', [ClientInfo2, conninfo(Lwm2mState1)]),
|
||||
|
||||
erlang:send(CoapPid, post_init),
|
||||
erlang:send_after(2000, CoapPid, auto_observe),
|
||||
|
||||
_ = emqx_cm_locker:trans(EndpointName, fun(_) ->
|
||||
emqx_cm:register_channel(EndpointName, CoapPid, conninfo(Lwm2mState1))
|
||||
end),
|
||||
emqx_cm:insert_channel_info(EndpointName, info(Lwm2mState1), stats(Lwm2mState1)),
|
||||
|
||||
{ok, Lwm2mState1#lwm2m_state{life_timer = emqx_lwm2m_timer:start_timer(LifeTime, {life_timer, expired})}};
|
||||
{error, Error} ->
|
||||
_ = run_hooks('client.connack', [conninfo(Lwm2mState), not_authorized], undefined),
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
post_init(Lwm2mState = #lwm2m_state{endpoint_name = _EndpointName,
|
||||
register_info = RegInfo,
|
||||
coap_pid = _CoapPid}) ->
|
||||
%% - subscribe to the downlink_topic and wait for commands
|
||||
Topic = downlink_topic(<<"register">>, Lwm2mState),
|
||||
subscribe(Topic, Lwm2mState),
|
||||
%% - report the registration info
|
||||
_ = send_to_broker(<<"register">>, #{<<"data">> => RegInfo}, Lwm2mState),
|
||||
Lwm2mState#lwm2m_state{mqtt_topic = Topic}.
|
||||
|
||||
update_reg_info(NewRegInfo, Lwm2mState = #lwm2m_state{
|
||||
life_timer = LifeTimer, register_info = RegInfo,
|
||||
coap_pid = CoapPid}) ->
|
||||
|
||||
UpdatedRegInfo = maps:merge(RegInfo, NewRegInfo),
|
||||
|
||||
_ = case proplists:get_value(update_msg_publish_condition,
|
||||
lwm2m_coap_responder:options(), contains_object_list) of
|
||||
always ->
|
||||
send_to_broker(<<"update">>, #{<<"data">> => UpdatedRegInfo}, Lwm2mState);
|
||||
contains_object_list ->
|
||||
%% - report the registration info update, but only when objectList is updated.
|
||||
case NewRegInfo of
|
||||
#{<<"objectList">> := _} ->
|
||||
send_to_broker(<<"update">>, #{<<"data">> => UpdatedRegInfo}, Lwm2mState);
|
||||
_ -> ok
|
||||
end
|
||||
end,
|
||||
|
||||
%% - flush cached donwlink commands
|
||||
_ = flush_cached_downlink_messages(CoapPid),
|
||||
|
||||
%% - update the life timer
|
||||
UpdatedLifeTimer = emqx_lwm2m_timer:refresh_timer(
|
||||
maps:get(<<"lt">>, UpdatedRegInfo), LifeTimer),
|
||||
|
||||
?LOG(debug, "Update RegInfo to: ~p", [UpdatedRegInfo]),
|
||||
Lwm2mState#lwm2m_state{life_timer = UpdatedLifeTimer,
|
||||
register_info = UpdatedRegInfo}.
|
||||
|
||||
replace_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer,
|
||||
coap_pid = CoapPid}) ->
|
||||
_ = send_to_broker(<<"register">>, #{<<"data">> => NewRegInfo}, Lwm2mState),
|
||||
|
||||
%% - flush cached donwlink commands
|
||||
_ = flush_cached_downlink_messages(CoapPid),
|
||||
|
||||
%% - update the life timer
|
||||
UpdatedLifeTimer = emqx_lwm2m_timer:refresh_timer(
|
||||
maps:get(<<"lt">>, NewRegInfo), LifeTimer),
|
||||
|
||||
_ = send_auto_observe(CoapPid, NewRegInfo),
|
||||
|
||||
?LOG(debug, "Replace RegInfo to: ~p", [NewRegInfo]),
|
||||
Lwm2mState#lwm2m_state{life_timer = UpdatedLifeTimer,
|
||||
register_info = NewRegInfo}.
|
||||
|
||||
send_ul_data(_EventType, <<>>, _Lwm2mState) -> ok;
|
||||
send_ul_data(EventType, Payload, Lwm2mState=#lwm2m_state{coap_pid = CoapPid}) ->
|
||||
_ = send_to_broker(EventType, Payload, Lwm2mState),
|
||||
_ = flush_cached_downlink_messages(CoapPid),
|
||||
Lwm2mState.
|
||||
|
||||
auto_observe(Lwm2mState = #lwm2m_state{register_info = RegInfo,
|
||||
coap_pid = CoapPid}) ->
|
||||
_ = send_auto_observe(CoapPid, RegInfo),
|
||||
Lwm2mState.
|
||||
|
||||
deliver(#message{topic = Topic, payload = Payload}, Lwm2mState = #lwm2m_state{coap_pid = CoapPid, register_info = RegInfo, started_at = StartedAt}) ->
|
||||
IsCacheMode = is_cache_mode(RegInfo, StartedAt),
|
||||
?LOG(debug, "Get MQTT message from broker, IsCacheModeNow?: ~p, Topic: ~p, Payload: ~p", [IsCacheMode, Topic, Payload]),
|
||||
AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>),
|
||||
deliver_to_coap(AlternatePath, Payload, CoapPid, IsCacheMode),
|
||||
Lwm2mState.
|
||||
|
||||
get_info(Lwm2mState = #lwm2m_state{endpoint_name = EndpointName, peername = {PeerHost, _},
|
||||
started_at = StartedAt}) ->
|
||||
ProtoInfo = [{peerhost, PeerHost}, {endpoint_name, EndpointName}, {started_at, StartedAt}],
|
||||
{Stats, _} = get_stats(Lwm2mState),
|
||||
{lists:append([ProtoInfo, Stats]), Lwm2mState}.
|
||||
|
||||
get_stats(Lwm2mState) ->
|
||||
Stats = emqx_misc:proc_stats(),
|
||||
{Stats, Lwm2mState}.
|
||||
|
||||
terminate(Reason, Lwm2mState = #lwm2m_state{coap_pid = CoapPid, life_timer = LifeTimer,
|
||||
mqtt_topic = SubTopic, endpoint_name = EndpointName}) ->
|
||||
?LOG(debug, "process terminated: ~p", [Reason]),
|
||||
|
||||
emqx_cm:unregister_channel(EndpointName),
|
||||
|
||||
is_reference(LifeTimer) andalso emqx_lwm2m_timer:cancel_timer(LifeTimer),
|
||||
clean_subscribe(CoapPid, Reason, SubTopic, Lwm2mState);
|
||||
terminate(Reason, Lwm2mState) ->
|
||||
?LOG(error, "process terminated: ~p, lwm2m_state: ~p", [Reason, Lwm2mState]).
|
||||
|
||||
clean_subscribe(_CoapPid, _Error, undefined, _Lwm2mState) -> ok;
|
||||
clean_subscribe(CoapPid, {shutdown, Error}, SubTopic, Lwm2mState) ->
|
||||
do_clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState);
|
||||
clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState) ->
|
||||
do_clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState).
|
||||
|
||||
do_clean_subscribe(_CoapPid, Error, SubTopic, Lwm2mState) ->
|
||||
?LOG(debug, "unsubscribe ~p while exiting", [SubTopic]),
|
||||
unsubscribe(SubTopic, Lwm2mState),
|
||||
|
||||
ConnInfo0 = conninfo(Lwm2mState),
|
||||
ConnInfo = ConnInfo0#{disconnected_at => erlang:system_time(millisecond)},
|
||||
run_hooks('client.disconnected', [clientinfo(Lwm2mState), Error, ConnInfo]).
|
||||
|
||||
subscribe(Topic, Lwm2mState = #lwm2m_state{endpoint_name = EndpointName}) ->
|
||||
emqx_broker:subscribe(Topic, EndpointName, ?SUBOPTS),
|
||||
emqx_hooks:run('session.subscribed', [clientinfo(Lwm2mState), Topic, ?SUBOPTS]).
|
||||
|
||||
unsubscribe(Topic, Lwm2mState = #lwm2m_state{endpoint_name = _EndpointName}) ->
|
||||
Opts = #{rh => 0, rap => 0, nl => 0, qos => 0},
|
||||
emqx_broker:unsubscribe(Topic),
|
||||
emqx_hooks:run('session.unsubscribed', [clientinfo(Lwm2mState), Topic, Opts]).
|
||||
|
||||
publish(Topic, Payload, Qos, EndpointName) ->
|
||||
emqx_broker:publish(emqx_message:set_flag(retain, false, emqx_message:make(EndpointName, Qos, Topic, Payload))).
|
||||
|
||||
time_now() -> erlang:system_time(millisecond).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Deliver downlink message to coap
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
deliver_to_coap(AlternatePath, JsonData, CoapPid, CacheMode) when is_binary(JsonData)->
|
||||
try
|
||||
TermData = emqx_json:decode(JsonData, [return_maps]),
|
||||
deliver_to_coap(AlternatePath, TermData, CoapPid, CacheMode)
|
||||
catch
|
||||
C:R:Stack ->
|
||||
?LOG(error, "deliver_to_coap - Invalid JSON: ~p, Exception: ~p, stacktrace: ~p",
|
||||
[JsonData, {C, R}, Stack])
|
||||
end;
|
||||
|
||||
deliver_to_coap(AlternatePath, TermData, CoapPid, CacheMode) when is_map(TermData) ->
|
||||
?LOG(info, "SEND To CoAP, AlternatePath=~p, Data=~p", [AlternatePath, TermData]),
|
||||
{CoapRequest, Ref} = emqx_lwm2m_cmd_handler:mqtt2coap(AlternatePath, TermData),
|
||||
|
||||
case CacheMode of
|
||||
false ->
|
||||
do_deliver_to_coap(CoapPid, CoapRequest, Ref);
|
||||
true ->
|
||||
cache_downlink_message(CoapRequest, Ref)
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Send uplink message to broker
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
send_to_broker(EventType, Payload = #{}, Lwm2mState) ->
|
||||
do_send_to_broker(EventType, Payload, Lwm2mState).
|
||||
|
||||
do_send_to_broker(EventType, Payload, Lwm2mState) ->
|
||||
NewPayload = maps:put(<<"msgType">>, EventType, Payload),
|
||||
Topic = uplink_topic(EventType, Lwm2mState),
|
||||
publish(Topic, emqx_json:encode(NewPayload), _Qos = 0, Lwm2mState#lwm2m_state.endpoint_name).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Auto Observe
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
send_auto_observe(CoapPid, RegInfo) ->
|
||||
%% - auto observe the objects
|
||||
case proplists:get_value(auto_observe, lwm2m_coap_responder:options(), false) of
|
||||
true ->
|
||||
AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>),
|
||||
auto_observe(AlternatePath, maps:get(<<"objectList">>, RegInfo, []), CoapPid);
|
||||
_ -> ?LOG(info, "Auto Observe Disabled", [])
|
||||
end.
|
||||
|
||||
auto_observe(AlternatePath, ObjectList, CoapPid) ->
|
||||
?LOG(info, "Auto Observe on: ~p", [ObjectList]),
|
||||
erlang:spawn(fun() ->
|
||||
observe_object_list(AlternatePath, ObjectList, CoapPid)
|
||||
end).
|
||||
|
||||
observe_object_list(AlternatePath, ObjectList, CoapPid) ->
|
||||
lists:foreach(fun(ObjectPath) ->
|
||||
observe_object_slowly(AlternatePath, ObjectPath, CoapPid, 100)
|
||||
end, ObjectList).
|
||||
|
||||
observe_object_slowly(AlternatePath, ObjectPath, CoapPid, Interval) ->
|
||||
observe_object(AlternatePath, ObjectPath, CoapPid),
|
||||
timer:sleep(Interval).
|
||||
|
||||
observe_object(AlternatePath, ObjectPath, CoapPid) ->
|
||||
Payload = #{
|
||||
<<"msgType">> => <<"observe">>,
|
||||
<<"data">> => #{
|
||||
<<"path">> => ObjectPath
|
||||
}
|
||||
},
|
||||
?LOG(info, "Observe ObjectPath: ~p", [ObjectPath]),
|
||||
deliver_to_coap(AlternatePath, Payload, CoapPid, false).
|
||||
|
||||
do_deliver_to_coap_slowly(CoapPid, CoapRequestList, Interval) ->
|
||||
erlang:spawn(fun() ->
|
||||
lists:foreach(fun({CoapRequest, Ref}) ->
|
||||
_ = do_deliver_to_coap(CoapPid, CoapRequest, Ref),
|
||||
timer:sleep(Interval)
|
||||
end, lists:reverse(CoapRequestList))
|
||||
end).
|
||||
|
||||
do_deliver_to_coap(CoapPid, CoapRequest, Ref) ->
|
||||
?LOG(debug, "Deliver To CoAP(~p), CoapRequest: ~p", [CoapPid, CoapRequest]),
|
||||
CoapPid ! {deliver_to_coap, CoapRequest, Ref}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Queue Mode
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
cache_downlink_message(CoapRequest, Ref) ->
|
||||
?LOG(debug, "Cache downlink coap request: ~p, Ref: ~p", [CoapRequest, Ref]),
|
||||
put(dl_msg_cache, [{CoapRequest, Ref} | get_cached_downlink_messages()]).
|
||||
|
||||
flush_cached_downlink_messages(CoapPid) ->
|
||||
case erase(dl_msg_cache) of
|
||||
CachedMessageList when is_list(CachedMessageList)->
|
||||
do_deliver_to_coap_slowly(CoapPid, CachedMessageList, 100);
|
||||
undefined -> ok
|
||||
end.
|
||||
|
||||
get_cached_downlink_messages() ->
|
||||
case get(dl_msg_cache) of
|
||||
undefined -> [];
|
||||
CachedMessageList -> CachedMessageList
|
||||
end.
|
||||
|
||||
is_cache_mode(RegInfo, StartedAt) ->
|
||||
case is_psm(RegInfo) orelse is_qmode(RegInfo) of
|
||||
true ->
|
||||
QModeTimeWind = proplists:get_value(qmode_time_window, lwm2m_coap_responder:options(), 22),
|
||||
Now = time_now(),
|
||||
if (Now - StartedAt) >= QModeTimeWind -> true;
|
||||
true -> false
|
||||
end;
|
||||
false -> false
|
||||
end.
|
||||
|
||||
is_psm(_) -> false.
|
||||
|
||||
is_qmode(#{<<"b">> := Binding}) when Binding =:= <<"UQ">>;
|
||||
Binding =:= <<"SQ">>;
|
||||
Binding =:= <<"UQS">>
|
||||
-> true;
|
||||
is_qmode(_) -> false.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Construct downlink and uplink topics
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
downlink_topic(EventType, Lwm2mState = #lwm2m_state{mountpoint = Mountpoint}) ->
|
||||
Topics = proplists:get_value(topics, lwm2m_coap_responder:options(), []),
|
||||
DnTopic = proplists:get_value(downlink_topic_key(EventType), Topics,
|
||||
default_downlink_topic(EventType)),
|
||||
take_place(mountpoint(iolist_to_binary(DnTopic), Mountpoint), Lwm2mState).
|
||||
|
||||
uplink_topic(EventType, Lwm2mState = #lwm2m_state{mountpoint = Mountpoint}) ->
|
||||
Topics = proplists:get_value(topics, lwm2m_coap_responder:options(), []),
|
||||
UpTopic = proplists:get_value(uplink_topic_key(EventType), Topics,
|
||||
default_uplink_topic(EventType)),
|
||||
take_place(mountpoint(iolist_to_binary(UpTopic), Mountpoint), Lwm2mState).
|
||||
|
||||
downlink_topic_key(EventType) when is_binary(EventType) ->
|
||||
command.
|
||||
|
||||
uplink_topic_key(<<"notify">>) -> notify;
|
||||
uplink_topic_key(<<"register">>) -> register;
|
||||
uplink_topic_key(<<"update">>) -> update;
|
||||
uplink_topic_key(EventType) when is_binary(EventType) ->
|
||||
response.
|
||||
|
||||
default_downlink_topic(Type) when is_binary(Type)->
|
||||
<<"dn/#">>.
|
||||
|
||||
default_uplink_topic(<<"notify">>) ->
|
||||
<<"up/notify">>;
|
||||
default_uplink_topic(Type) when is_binary(Type) ->
|
||||
<<"up/resp">>.
|
||||
|
||||
take_place(Text, Lwm2mState) ->
|
||||
{IPAddr, _} = Lwm2mState#lwm2m_state.peername,
|
||||
IPAddrBin = iolist_to_binary(inet:ntoa(IPAddr)),
|
||||
take_place(take_place(Text, <<"%a">>, IPAddrBin),
|
||||
<<"%e">>, Lwm2mState#lwm2m_state.endpoint_name).
|
||||
|
||||
take_place(Text, Placeholder, Value) ->
|
||||
binary:replace(Text, Placeholder, Value, [global]).
|
||||
|
||||
clientinfo(#lwm2m_state{peername = {PeerHost, _},
|
||||
endpoint_name = EndpointName,
|
||||
mountpoint = Mountpoint}) ->
|
||||
#{zone => undefined,
|
||||
protocol => lwm2m,
|
||||
peerhost => PeerHost,
|
||||
sockport => 5683, %% FIXME:
|
||||
clientid => EndpointName,
|
||||
username => undefined,
|
||||
password => undefined,
|
||||
peercert => nossl,
|
||||
is_bridge => false,
|
||||
is_superuser => false,
|
||||
mountpoint => Mountpoint,
|
||||
ws_cookie => undefined
|
||||
}.
|
||||
|
||||
mountpoint(Topic, <<>>) ->
|
||||
Topic;
|
||||
mountpoint(Topic, Mountpoint) ->
|
||||
<<Mountpoint/binary, Topic/binary>>.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helper funcs
|
||||
|
||||
-compile({inline, [run_hooks/2, run_hooks/3]}).
|
||||
run_hooks(Name, Args) ->
|
||||
ok = emqx_metrics:inc(Name), emqx_hooks:run(Name, Args).
|
||||
|
||||
run_hooks(Name, Args, Acc) ->
|
||||
ok = emqx_metrics:inc(Name), emqx_hooks:run_fold(Name, Args, Acc).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Info & Stats
|
||||
|
||||
info(State) ->
|
||||
ChannInfo = chann_info(State),
|
||||
ChannInfo#{sockinfo => sockinfo(State)}.
|
||||
|
||||
%% copies from emqx_connection:info/1
|
||||
sockinfo(#lwm2m_state{peername = Peername}) ->
|
||||
#{socktype => udp,
|
||||
peername => Peername,
|
||||
sockname => {{127,0,0,1}, 5683}, %% FIXME: Sock?
|
||||
sockstate => running,
|
||||
active_n => 1
|
||||
}.
|
||||
|
||||
%% copies from emqx_channel:info/1
|
||||
chann_info(State) ->
|
||||
#{conninfo => conninfo(State),
|
||||
conn_state => connected,
|
||||
clientinfo => clientinfo(State),
|
||||
session => maps:from_list(session_info(State)),
|
||||
will_msg => undefined
|
||||
}.
|
||||
|
||||
conninfo(#lwm2m_state{peername = Peername,
|
||||
version = Ver,
|
||||
started_at = StartedAt,
|
||||
endpoint_name = Epn}) ->
|
||||
#{socktype => udp,
|
||||
sockname => {{127,0,0,1}, 5683},
|
||||
peername => Peername,
|
||||
peercert => nossl, %% TODO: dtls
|
||||
conn_mod => ?MODULE,
|
||||
proto_name => <<"LwM2M">>,
|
||||
proto_ver => Ver,
|
||||
clean_start => true,
|
||||
clientid => Epn,
|
||||
username => undefined,
|
||||
conn_props => undefined,
|
||||
connected => true,
|
||||
connected_at => StartedAt,
|
||||
keepalive => 0,
|
||||
receive_maximum => 0,
|
||||
expiry_interval => 0
|
||||
}.
|
||||
|
||||
%% copies from emqx_session:info/1
|
||||
session_info(#lwm2m_state{mqtt_topic = SubTopic, started_at = StartedAt}) ->
|
||||
[{subscriptions, #{SubTopic => ?SUBOPTS}},
|
||||
{upgrade_qos, false},
|
||||
{retry_interval, 0},
|
||||
{await_rel_timeout, 0},
|
||||
{created_at, StartedAt}
|
||||
].
|
||||
|
||||
%% The stats keys copied from emqx_connection:stats/1
|
||||
stats(_State) ->
|
||||
SockStats = [{recv_oct,0}, {recv_cnt,0}, {send_oct,0}, {send_cnt,0}, {send_pend,0}],
|
||||
ConnStats = emqx_pd:get_counters(?CONN_STATS),
|
||||
ChanStats = [{subscriptions_cnt, 1},
|
||||
{subscriptions_max, 1},
|
||||
{inflight_cnt, 0},
|
||||
{inflight_max, 0},
|
||||
{mqueue_len, 0},
|
||||
{mqueue_max, 0},
|
||||
{mqueue_dropped, 0},
|
||||
{next_pkt_id, 0},
|
||||
{awaiting_rel_cnt, 0},
|
||||
{awaiting_rel_max, 0}
|
||||
],
|
||||
ProcStats = emqx_misc:proc_stats(),
|
||||
lists:append([SockStats, ConnStats, ChanStats, ProcStats]).
|
||||
|
32
apps/emqx_lwm2m/src/emqx_lwm2m_sup.erl
Normal file
32
apps/emqx_lwm2m/src/emqx_lwm2m_sup.erl
Normal file
@ -0,0 +1,32 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([ start_link/0
|
||||
, init/1
|
||||
]).
|
||||
|
||||
-define(CHILD(M), {M, {M, start_link, []}, permanent, 5000, worker, [M]}).
|
||||
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init(_Args) ->
|
||||
{ok, { {one_for_all, 10, 3600}, [?CHILD(emqx_lwm2m_xml_object_db)] }}.
|
47
apps/emqx_lwm2m/src/emqx_lwm2m_timer.erl
Normal file
47
apps/emqx_lwm2m/src/emqx_lwm2m_timer.erl
Normal file
@ -0,0 +1,47 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_timer).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
|
||||
-export([ cancel_timer/1
|
||||
, start_timer/2
|
||||
, refresh_timer/1
|
||||
, refresh_timer/2
|
||||
]).
|
||||
|
||||
-record(timer_state, { interval
|
||||
, tref
|
||||
, message
|
||||
}).
|
||||
|
||||
-define(LOG(Level, Format, Args),
|
||||
logger:Level("LWM2M-TIMER: " ++ Format, Args)).
|
||||
|
||||
cancel_timer(#timer_state{tref = TRef}) when is_reference(TRef) ->
|
||||
_ = erlang:cancel_timer(TRef), ok.
|
||||
|
||||
refresh_timer(State=#timer_state{interval = Interval, message = Msg}) ->
|
||||
cancel_timer(State), start_timer(Interval, Msg).
|
||||
refresh_timer(NewInterval, State=#timer_state{message = Msg}) ->
|
||||
cancel_timer(State), start_timer(NewInterval, Msg).
|
||||
|
||||
%% start timer in seconds
|
||||
start_timer(Interval, Msg) ->
|
||||
?LOG(debug, "start_timer of ~p secs", [Interval]),
|
||||
TRef = erlang:send_after(timer:seconds(Interval), self(), Msg),
|
||||
#timer_state{interval = Interval, tref = TRef, message = Msg}.
|
165
apps/emqx_lwm2m/src/emqx_lwm2m_tlv.erl
Normal file
165
apps/emqx_lwm2m/src/emqx_lwm2m_tlv.erl
Normal file
@ -0,0 +1,165 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_tlv).
|
||||
|
||||
-export([ parse/1
|
||||
, encode/1
|
||||
]).
|
||||
|
||||
|
||||
-ifdef(TEST).
|
||||
-export([binary_to_hex_string/1]).
|
||||
-endif.
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-TLV: " ++ Format, Args)).
|
||||
|
||||
-define(TLV_TYPE_OBJECT_INSTANCE, 0).
|
||||
-define(TLV_TYPE_RESOURCE_INSTANCE, 1).
|
||||
-define(TLV_TYPE_MULTIPLE_RESOURCE, 2).
|
||||
-define(TLV_TYPE_RESOURCE_WITH_VALUE, 3).
|
||||
|
||||
-define(TLV_NO_LENGTH_FIELD, 0).
|
||||
-define(TLV_LEGNTH_8_BIT, 1).
|
||||
-define(TLV_LEGNTH_16_BIT, 2).
|
||||
-define(TLV_LEGNTH_24_BIT, 3).
|
||||
|
||||
|
||||
|
||||
%----------------------------------------------------------------------------------------------------------------------------------------
|
||||
% [#{tlv_object_instance := Id11, value := Value11}, #{tlv_object_instance := Id12, value := Value12}, ...]
|
||||
% where Value11 and Value12 is a list:
|
||||
% [#{tlv_resource_with_value => Id21, value => Value21}, #{tlv_multiple_resource => Id22, value = Value22}, ...]
|
||||
% where Value21 is a binary
|
||||
% Value22 is a list:
|
||||
% [#{tlv_resource_instance => Id31, value => Value31}, #{tlv_resource_instance => Id32, value => Value32}, ...]
|
||||
% where Value31 and Value32 is a binary
|
||||
%
|
||||
% correspond to three levels:
|
||||
% 1) Object Instance Level
|
||||
% 2) Resource Level
|
||||
% 3) Resource Instance Level
|
||||
%
|
||||
% NOTE: TLV does not has object level, only has object instance level. It implies TLV can not represent multiple objects
|
||||
%----------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
parse(Data) ->
|
||||
parse_loop(Data, []).
|
||||
|
||||
parse_loop(<<>>, Acc)->
|
||||
lists:reverse(Acc);
|
||||
parse_loop(Data, Acc) ->
|
||||
{New, Rest} = parse_step1(Data),
|
||||
parse_loop(Rest, [New|Acc]).
|
||||
|
||||
parse_step1(<<?TLV_TYPE_OBJECT_INSTANCE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>) ->
|
||||
{Id, Value, Rest2} = parse_step2(id_length_bit_width(IdLength), LengthType, InlineLength, Rest),
|
||||
{#{tlv_object_instance => Id, value => parse_loop(Value, [])}, Rest2};
|
||||
parse_step1(<<?TLV_TYPE_RESOURCE_INSTANCE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>) ->
|
||||
{Id, Value, Rest2} = parse_step2(id_length_bit_width(IdLength), LengthType, InlineLength, Rest),
|
||||
{#{tlv_resource_instance => Id, value => Value}, Rest2};
|
||||
parse_step1(<<?TLV_TYPE_MULTIPLE_RESOURCE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>) ->
|
||||
{Id, Value, Rest2} = parse_step2(id_length_bit_width(IdLength), LengthType, InlineLength, Rest),
|
||||
{#{tlv_multiple_resource => Id, value => parse_loop(Value, [])}, Rest2};
|
||||
parse_step1(<<?TLV_TYPE_RESOURCE_WITH_VALUE:2, IdLength:1, LengthType:2, InlineLength:3, Rest/binary>>) ->
|
||||
{Id, Value, Rest2} = parse_step2(id_length_bit_width(IdLength), LengthType, InlineLength, Rest),
|
||||
{#{tlv_resource_with_value => Id, value => Value}, Rest2}.
|
||||
|
||||
parse_step2(IdLength, ?TLV_NO_LENGTH_FIELD, Length, Data) ->
|
||||
<<Id:IdLength, Value:Length/binary, Rest/binary>> = Data,
|
||||
{Id, Value, Rest};
|
||||
parse_step2(IdLength, ?TLV_LEGNTH_8_BIT, _, Data) ->
|
||||
<<Id:IdLength, Length:8, Rest/binary>> = Data,
|
||||
parse_step3(Id, Length, Rest);
|
||||
parse_step2(IdLength, ?TLV_LEGNTH_16_BIT, _, Data) ->
|
||||
<<Id:IdLength, Length:16, Rest/binary>> = Data,
|
||||
parse_step3(Id, Length, Rest);
|
||||
parse_step2(IdLength, ?TLV_LEGNTH_24_BIT, _, Data) ->
|
||||
<<Id:IdLength, Length:24, Rest/binary>> = Data,
|
||||
parse_step3(Id, Length, Rest).
|
||||
|
||||
parse_step3(Id, Length, Data) ->
|
||||
<<Value:Length/binary, Rest/binary>> = Data,
|
||||
{Id, Value, Rest}.
|
||||
|
||||
id_length_bit_width(0) -> 8;
|
||||
id_length_bit_width(1) -> 16.
|
||||
|
||||
|
||||
encode(TlvList) ->
|
||||
encode(TlvList, <<>>).
|
||||
|
||||
encode([], Acc) ->
|
||||
Acc;
|
||||
encode([#{tlv_object_instance := Id, value := Value}|T], Acc) ->
|
||||
SubItems = encode(Value, <<>>),
|
||||
NewBinary = encode_body(?TLV_TYPE_OBJECT_INSTANCE, Id, SubItems),
|
||||
encode(T, <<Acc/binary, NewBinary/binary>>);
|
||||
encode([#{tlv_resource_instance := Id, value := Value}|T], Acc) ->
|
||||
ValBinary = encode_value(Value),
|
||||
NewBinary = encode_body(?TLV_TYPE_RESOURCE_INSTANCE, Id, ValBinary),
|
||||
encode(T, <<Acc/binary, NewBinary/binary>>);
|
||||
encode([#{tlv_multiple_resource := Id, value := Value}|T], Acc) ->
|
||||
SubItems = encode(Value, <<>>),
|
||||
NewBinary = encode_body(?TLV_TYPE_MULTIPLE_RESOURCE, Id, SubItems),
|
||||
encode(T, <<Acc/binary, NewBinary/binary>>);
|
||||
encode([#{tlv_resource_with_value := Id, value := Value}|T], Acc) ->
|
||||
ValBinary = encode_value(Value),
|
||||
NewBinary = encode_body(?TLV_TYPE_RESOURCE_WITH_VALUE, Id, ValBinary),
|
||||
encode(T, <<Acc/binary, NewBinary/binary>>).
|
||||
|
||||
encode_body(Type, Id, Value) ->
|
||||
Size = byte_size(Value),
|
||||
{IdLength, IdBinarySize, IdBinary} = if
|
||||
Id < 256 -> {0, 1, <<Id:8>>};
|
||||
true -> {1, 2, <<Id:16>>}
|
||||
end,
|
||||
if
|
||||
Size < 8 -> <<Type:2, IdLength:1, ?TLV_NO_LENGTH_FIELD:2, Size:3, IdBinary:IdBinarySize/binary, Value:Size/binary>>;
|
||||
Size < 256 -> <<Type:2, IdLength:1, ?TLV_LEGNTH_8_BIT:2, 0:3, IdBinary:IdBinarySize/binary, Size:8, Value:Size/binary>>;
|
||||
Size < 65536 -> <<Type:2, IdLength:1, ?TLV_LEGNTH_16_BIT:2, 0:3, IdBinary:IdBinarySize/binary, Size:16, Value:Size/binary>>;
|
||||
true -> <<Type:2, IdLength:1, ?TLV_LEGNTH_24_BIT:2, 0:3, IdBinary:IdBinarySize/binary, Size:24, Value:Size/binary>>
|
||||
end.
|
||||
|
||||
encode_value(Value) when is_binary(Value) ->
|
||||
Value;
|
||||
encode_value(Value) when is_list(Value) ->
|
||||
list_to_binary(Value);
|
||||
encode_value(true) ->
|
||||
<<1>>;
|
||||
encode_value(false) ->
|
||||
<<0>>;
|
||||
encode_value(Value) when is_integer(Value) ->
|
||||
if
|
||||
Value > -128, Value < 128 -> <<Value>>;
|
||||
Value > -32768, Value < 32768 -> <<Value:16>>;
|
||||
true -> <<Value:24>>
|
||||
end;
|
||||
encode_value(Value) when is_float(Value) ->
|
||||
<<Value:32/float>>;
|
||||
encode_value(Value) ->
|
||||
error(io_lib:format("unsupport format ~p", [Value])).
|
||||
|
||||
|
||||
|
||||
-ifdef(TEST).
|
||||
binary_to_hex_string(Data) ->
|
||||
lists:flatten([io_lib:format("~2.16.0B ",[X]) || <<X:8>> <= Data ]).
|
||||
-endif.
|
||||
|
62
apps/emqx_lwm2m/src/emqx_lwm2m_xml_object.erl
Normal file
62
apps/emqx_lwm2m/src/emqx_lwm2m_xml_object.erl
Normal file
@ -0,0 +1,62 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_xml_object).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
-include_lib("xmerl/include/xmerl.hrl").
|
||||
|
||||
-export([ get_obj_def/2
|
||||
, get_object_id/1
|
||||
, get_object_and_resource_id/2
|
||||
, get_resource_type/2
|
||||
, get_resource_name/2
|
||||
]).
|
||||
|
||||
-define(LOG(Level, Format, Args),
|
||||
logger:Level("LWM2M-OBJ: " ++ Format, Args)).
|
||||
|
||||
% This module is for future use. Disabled now.
|
||||
|
||||
get_obj_def(ObjectIdInt, true) ->
|
||||
emqx_lwm2m_xml_object_db:find_objectid(ObjectIdInt);
|
||||
get_obj_def(ObjectNameStr, false) ->
|
||||
emqx_lwm2m_xml_object_db:find_name(ObjectNameStr).
|
||||
|
||||
|
||||
|
||||
get_object_id(ObjDefinition) ->
|
||||
[#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition),
|
||||
ObjectId.
|
||||
|
||||
|
||||
get_object_and_resource_id(ResourceNameBinary, ObjDefinition) ->
|
||||
ResourceNameString = binary_to_list(ResourceNameBinary),
|
||||
[#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition),
|
||||
[#xmlAttribute{value=ResourceId}] = xmerl_xpath:string("Resources/Item/Name[.=\""++ResourceNameString++"\"]/../@ID", ObjDefinition),
|
||||
?LOG(debug, "get_object_and_resource_id ObjectId=~p, ResourceId=~p", [ObjectId, ResourceId]),
|
||||
{ObjectId, ResourceId}.
|
||||
|
||||
|
||||
get_resource_type(ResourceIdInt, ObjDefinition) ->
|
||||
ResourceIdString = integer_to_list(ResourceIdInt),
|
||||
[#xmlText{value=DataType}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Type/text()", ObjDefinition),
|
||||
DataType.
|
||||
|
||||
get_resource_name(ResourceIdInt, ObjDefinition) ->
|
||||
ResourceIdString = integer_to_list(ResourceIdInt),
|
||||
[#xmlText{value=Name}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Name/text()", ObjDefinition),
|
||||
Name.
|
145
apps/emqx_lwm2m/src/emqx_lwm2m_xml_object_db.erl
Normal file
145
apps/emqx_lwm2m/src/emqx_lwm2m_xml_object_db.erl
Normal file
@ -0,0 +1,145 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_lwm2m_xml_object_db).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
-include_lib("xmerl/include/xmerl.hrl").
|
||||
|
||||
% This module is for future use. Disabled now.
|
||||
|
||||
%% API
|
||||
-export([ start_link/0
|
||||
, stop/0
|
||||
, find_name/1
|
||||
, find_objectid/1
|
||||
]).
|
||||
|
||||
%% gen_server.
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
|
||||
-define(LOG(Level, Format, Args),
|
||||
logger:Level("LWM2M-OBJ-DB: " ++ Format, Args)).
|
||||
|
||||
-define(LWM2M_OBJECT_DEF_TAB, lwm2m_object_def_tab).
|
||||
-define(LWM2M_OBJECT_NAME_TO_ID_TAB, lwm2m_object_name_to_id_tab).
|
||||
|
||||
-record(state, {}).
|
||||
|
||||
%% ------------------------------------------------------------------
|
||||
%% API Function Definitions
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
find_objectid(ObjectId) ->
|
||||
ObjectIdInt = case is_list(ObjectId) of
|
||||
true -> list_to_integer(ObjectId);
|
||||
false -> ObjectId
|
||||
end,
|
||||
case ets:lookup(?LWM2M_OBJECT_DEF_TAB, ObjectIdInt) of
|
||||
[] -> error(no_xml_definition);
|
||||
[{ObjectId, Xml}] -> Xml
|
||||
end.
|
||||
|
||||
find_name(Name) ->
|
||||
NameBinary = case is_list(Name) of
|
||||
true -> list_to_binary(Name);
|
||||
false -> Name
|
||||
end,
|
||||
case ets:lookup(?LWM2M_OBJECT_NAME_TO_ID_TAB, NameBinary) of
|
||||
[] ->
|
||||
undefined;
|
||||
[{NameBinary, ObjectId}] ->
|
||||
case ets:lookup(?LWM2M_OBJECT_DEF_TAB, ObjectId) of
|
||||
[] -> undefined;
|
||||
[{ObjectId, Xml}] -> Xml
|
||||
end
|
||||
end.
|
||||
|
||||
stop() ->
|
||||
gen_server:stop(?MODULE).
|
||||
|
||||
|
||||
%% ------------------------------------------------------------------
|
||||
%% gen_server Function Definitions
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
_ = ets:new(?LWM2M_OBJECT_DEF_TAB, [set, named_table, protected]),
|
||||
_ = ets:new(?LWM2M_OBJECT_NAME_TO_ID_TAB, [set, named_table, protected]),
|
||||
PluginsEtcDir = emqx:get_env(plugins_etc_dir),
|
||||
DefBaseDir = re:replace(PluginsEtcDir, "plugins", "lwm2m_xml", [{return, list}]),
|
||||
BaseDir = application:get_env(emqx_lwm2m, xml_dir, DefBaseDir),
|
||||
load(BaseDir),
|
||||
{ok, #state{}}.
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ignored, State}.
|
||||
|
||||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
ets:delete(?LWM2M_OBJECT_DEF_TAB),
|
||||
ets:delete(?LWM2M_OBJECT_NAME_TO_ID_TAB),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
load(BaseDir) ->
|
||||
Wild = case lists:last(BaseDir) == $/ of
|
||||
true -> BaseDir++"*.xml";
|
||||
false -> BaseDir++"/*.xml"
|
||||
end,
|
||||
AllXmlFiles = filelib:wildcard(Wild),
|
||||
load_loop(AllXmlFiles).
|
||||
|
||||
load_loop([]) ->
|
||||
ok;
|
||||
load_loop([FileName|T]) ->
|
||||
ObjectXml = load_xml(FileName),
|
||||
[#xmlText{value=ObjectIdString}] = xmerl_xpath:string("ObjectID/text()", ObjectXml),
|
||||
[#xmlText{value=Name}] = xmerl_xpath:string("Name/text()", ObjectXml),
|
||||
ObjectId = list_to_integer(ObjectIdString),
|
||||
NameBinary = list_to_binary(Name),
|
||||
?LOG(debug, "load_loop FileName=~p, ObjectId=~p, Name=~p", [FileName, ObjectId, NameBinary]),
|
||||
ets:insert(?LWM2M_OBJECT_DEF_TAB, {ObjectId, ObjectXml}),
|
||||
ets:insert(?LWM2M_OBJECT_NAME_TO_ID_TAB, {NameBinary, ObjectId}),
|
||||
load_loop(T).
|
||||
|
||||
|
||||
load_xml(FileName) ->
|
||||
{Xml, _Rest} = xmerl_scan:file(FileName),
|
||||
[ObjectXml] = xmerl_xpath:string("/LWM2M/Object", Xml),
|
||||
ObjectXml.
|
||||
|
1947
apps/emqx_lwm2m/test/emqx_lwm2m_SUITE.erl
Normal file
1947
apps/emqx_lwm2m/test/emqx_lwm2m_SUITE.erl
Normal file
File diff suppressed because it is too large
Load Diff
240
apps/emqx_lwm2m/test/emqx_tlv_SUITE.erl
Normal file
240
apps/emqx_lwm2m/test/emqx_tlv_SUITE.erl
Normal file
@ -0,0 +1,240 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(emqx_tlv_SUITE).
|
||||
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-define(LOGT(Format, Args), logger:debug("TEST_SUITE: " ++ Format, Args)).
|
||||
|
||||
-include("emqx_lwm2m.hrl").
|
||||
-include_lib("lwm2m_coap/include/coap.hrl").
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
|
||||
all() -> [case01, case02, case03, case03_0, case04, case05, case06, case07, case08, case09].
|
||||
|
||||
|
||||
|
||||
init_per_suite(Config) ->
|
||||
Config.
|
||||
|
||||
end_per_suite(Config) ->
|
||||
Config.
|
||||
|
||||
|
||||
case01(_Config) ->
|
||||
Data = <<16#C8, 16#00, 16#14, 16#4F, 16#70, 16#65, 16#6E, 16#20, 16#4D, 16#6F, 16#62, 16#69, 16#6C, 16#65, 16#20, 16#41, 16#6C, 16#6C, 16#69, 16#61, 16#6E, 16#63, 16#65>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_resource_with_value => 16#00, value => <<"Open Mobile Alliance">>}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
||||
case02(_Config) ->
|
||||
Data = <<16#86, 16#06, 16#41, 16#00, 16#01, 16#41, 16#01, 16#05>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_multiple_resource => 16#06, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<1>>},
|
||||
#{tlv_resource_instance => 16#01, value => <<5>>}
|
||||
]}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
||||
case03(_Config) ->
|
||||
Data = <<16#C8, 16#00, 16#14, 16#4F, 16#70, 16#65, 16#6E, 16#20, 16#4D, 16#6F, 16#62, 16#69, 16#6C, 16#65, 16#20, 16#41, 16#6C, 16#6C, 16#69, 16#61, 16#6E, 16#63, 16#65, 16#C8, 16#01, 16#16, 16#4C, 16#69, 16#67, 16#68, 16#74, 16#77, 16#65, 16#69, 16#67, 16#68, 16#74, 16#20, 16#4D, 16#32, 16#4D, 16#20, 16#43, 16#6C, 16#69, 16#65, 16#6E, 16#74, 16#C8, 16#02, 16#09, 16#33, 16#34, 16#35, 16#30, 16#30, 16#30, 16#31, 16#32, 16#33>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_resource_with_value => 16#00, value => <<"Open Mobile Alliance">>},
|
||||
#{tlv_resource_with_value => 16#01, value => <<"Lightweight M2M Client">>},
|
||||
#{tlv_resource_with_value => 16#02, value => <<"345000123">>}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
||||
case03_0(_Config) ->
|
||||
Data = <<16#87, 16#02, 16#41, 16#7F, 16#07, 16#61, 16#01, 16#36, 16#01>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_multiple_resource => 16#02, value => [
|
||||
#{tlv_resource_instance => 16#7F, value => <<16#07>>},
|
||||
#{tlv_resource_instance => 16#0136, value => <<16#01>>}
|
||||
]}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
||||
case04(_Config) ->
|
||||
% 6.4.3.1 Single Object Instance Request Example
|
||||
Data = <<16#C8, 16#00, 16#14, 16#4F, 16#70, 16#65, 16#6E, 16#20, 16#4D, 16#6F, 16#62, 16#69, 16#6C, 16#65, 16#20, 16#41, 16#6C, 16#6C, 16#69, 16#61, 16#6E, 16#63, 16#65, 16#C8, 16#01, 16#16, 16#4C, 16#69, 16#67, 16#68, 16#74, 16#77, 16#65, 16#69, 16#67, 16#68, 16#74, 16#20, 16#4D, 16#32, 16#4D, 16#20, 16#43, 16#6C, 16#69, 16#65, 16#6E, 16#74, 16#C8, 16#02, 16#09, 16#33, 16#34, 16#35, 16#30, 16#30, 16#30, 16#31, 16#32, 16#33, 16#C3, 16#03, 16#31, 16#2E, 16#30, 16#86, 16#06, 16#41, 16#00, 16#01, 16#41, 16#01, 16#05, 16#88, 16#07, 16#08, 16#42, 16#00, 16#0E, 16#D8, 16#42, 16#01, 16#13, 16#88, 16#87, 16#08, 16#41, 16#00, 16#7D, 16#42, 16#01, 16#03, 16#84, 16#C1, 16#09, 16#64, 16#C1, 16#0A, 16#0F, 16#83, 16#0B, 16#41, 16#00, 16#00, 16#C4, 16#0D, 16#51, 16#82, 16#42, 16#8F, 16#C6, 16#0E, 16#2B, 16#30, 16#32, 16#3A, 16#30, 16#30, 16#C1, 16#10, 16#55>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_resource_with_value => 16#00, value => <<"Open Mobile Alliance">>},
|
||||
#{tlv_resource_with_value => 16#01, value => <<"Lightweight M2M Client">>},
|
||||
#{tlv_resource_with_value => 16#02, value => <<"345000123">>},
|
||||
#{tlv_resource_with_value => 16#03, value => <<"1.0">>},
|
||||
#{tlv_multiple_resource => 16#06, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<1>>},
|
||||
#{tlv_resource_instance => 16#01, value => <<5>>}
|
||||
]},
|
||||
#{tlv_multiple_resource => 16#07, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<16#0ED8:16>>},
|
||||
#{tlv_resource_instance => 16#01, value => <<16#1388:16>>}
|
||||
]},
|
||||
#{tlv_multiple_resource => 16#08, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<16#7d>>},
|
||||
#{tlv_resource_instance => 16#01, value => <<16#0384:16>>}
|
||||
]},
|
||||
#{tlv_resource_with_value => 16#09, value => <<16#64>>},
|
||||
#{tlv_resource_with_value => 16#0A, value => <<16#0F>>},
|
||||
#{tlv_multiple_resource => 16#0B, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<16#00>>}
|
||||
]},
|
||||
#{tlv_resource_with_value => 16#0D, value => <<16#5182428F:32>>},
|
||||
#{tlv_resource_with_value => 16#0E, value => <<"+02:00">>},
|
||||
#{tlv_resource_with_value => 16#10, value => <<"U">>}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
||||
case05(_Config) ->
|
||||
% 6.4.3.2 Multiple Object Instance Request Examples
|
||||
% A) Request on Single-Instance Object
|
||||
Data = <<16#08, 16#00, 16#79, 16#C8, 16#00, 16#14, 16#4F, 16#70, 16#65, 16#6E, 16#20, 16#4D, 16#6F, 16#62, 16#69, 16#6C, 16#65, 16#20, 16#41, 16#6C, 16#6C, 16#69, 16#61, 16#6E, 16#63, 16#65, 16#C8, 16#01, 16#16, 16#4C, 16#69, 16#67, 16#68, 16#74, 16#77, 16#65, 16#69, 16#67, 16#68, 16#74, 16#20, 16#4D, 16#32, 16#4D, 16#20, 16#43, 16#6C, 16#69, 16#65, 16#6E, 16#74, 16#C8, 16#02, 16#09, 16#33, 16#34, 16#35, 16#30, 16#30, 16#30, 16#31, 16#32, 16#33, 16#C3, 16#03, 16#31, 16#2E, 16#30, 16#86, 16#06, 16#41, 16#00, 16#01, 16#41, 16#01, 16#05, 16#88, 16#07, 16#08, 16#42, 16#00, 16#0E, 16#D8, 16#42, 16#01, 16#13, 16#88, 16#87, 16#08, 16#41, 16#00, 16#7D, 16#42, 16#01, 16#03, 16#84, 16#C1, 16#09, 16#64, 16#C1, 16#0A, 16#0F, 16#83, 16#0B, 16#41, 16#00, 16#00, 16#C4, 16#0D, 16#51, 16#82, 16#42, 16#8F, 16#C6, 16#0E, 16#2B, 16#30, 16#32, 16#3A, 16#30, 16#30, 16#C1, 16#10, 16#55>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_object_instance => 16#00, value => [
|
||||
#{tlv_resource_with_value => 16#00, value => <<"Open Mobile Alliance">>},
|
||||
#{tlv_resource_with_value => 16#01, value => <<"Lightweight M2M Client">>},
|
||||
#{tlv_resource_with_value => 16#02, value => <<"345000123">>},
|
||||
#{tlv_resource_with_value => 16#03, value => <<"1.0">>},
|
||||
#{tlv_multiple_resource => 16#06, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<1>>},
|
||||
#{tlv_resource_instance => 16#01, value => <<5>>}
|
||||
]},
|
||||
#{tlv_multiple_resource => 16#07, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<16#0ED8:16>>},
|
||||
#{tlv_resource_instance => 16#01, value => <<16#1388:16>>}
|
||||
]},
|
||||
#{tlv_multiple_resource => 16#08, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<16#7d>>},
|
||||
#{tlv_resource_instance => 16#01, value => <<16#0384:16>>}
|
||||
]},
|
||||
#{tlv_resource_with_value => 16#09, value => <<16#64>>},
|
||||
#{tlv_resource_with_value => 16#0A, value => <<16#0F>>},
|
||||
#{tlv_multiple_resource => 16#0B, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<16#00>>}
|
||||
]},
|
||||
#{tlv_resource_with_value => 16#0D, value => <<16#5182428F:32>>},
|
||||
#{tlv_resource_with_value => 16#0E, value => <<"+02:00">>},
|
||||
#{tlv_resource_with_value => 16#10, value => <<"U">>}
|
||||
]}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
||||
case06(_Config) ->
|
||||
% 6.4.3.2 Multiple Object Instance Request Examples
|
||||
% B) Request on Multiple-Instances Object having 2 instances
|
||||
Data = <<16#08, 16#00, 16#0E, 16#C1, 16#00, 16#01, 16#C1, 16#01, 16#00, 16#83, 16#02, 16#41, 16#7F, 16#07, 16#C1, 16#03, 16#7F, 16#08, 16#02, 16#12, 16#C1, 16#00, 16#03, 16#C1, 16#01, 16#00, 16#87, 16#02, 16#41, 16#7F, 16#07, 16#61, 16#01, 16#36, 16#01, 16#C1, 16#03, 16#7F>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_object_instance => 16#00, value => [
|
||||
#{tlv_resource_with_value => 16#00, value => <<16#01>>},
|
||||
#{tlv_resource_with_value => 16#01, value => <<16#00>>},
|
||||
#{tlv_multiple_resource => 16#02, value => [
|
||||
#{tlv_resource_instance => 16#7F, value => <<16#07>>}
|
||||
]},
|
||||
#{tlv_resource_with_value => 16#03, value => <<16#7F>>}
|
||||
]},
|
||||
#{tlv_object_instance => 16#02, value => [
|
||||
#{tlv_resource_with_value => 16#00, value => <<16#03>>},
|
||||
#{tlv_resource_with_value => 16#01, value => <<16#00>>},
|
||||
#{tlv_multiple_resource => 16#02, value => [
|
||||
#{tlv_resource_instance => 16#7F, value => <<16#07>>},
|
||||
#{tlv_resource_instance => 16#0136, value => <<16#01>>}
|
||||
]},
|
||||
#{tlv_resource_with_value => 16#03, value => <<16#7F>>}
|
||||
]}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
||||
case07(_Config) ->
|
||||
% 6.4.3.2 Multiple Object Instance Request Examples
|
||||
% C) Request on Multiple-Instances Object having 1 instance only
|
||||
Data = <<16#08, 16#00, 16#0F, 16#C1, 16#00, 16#01, 16#C4, 16#01, 16#00, 16#01, 16#51, 16#80, 16#C1, 16#06, 16#01, 16#C1, 16#07, 16#55>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_object_instance => 16#00, value => [
|
||||
#{tlv_resource_with_value => 16#00, value => <<16#01>>},
|
||||
#{tlv_resource_with_value => 16#01, value => <<86400:32>>},
|
||||
#{tlv_resource_with_value => 16#06, value => <<16#01>>},
|
||||
#{tlv_resource_with_value => 16#07, value => <<$U>>}]}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
||||
case08(_Config) ->
|
||||
% 6.4.3.3 Example of Request on an Object Instance containing an Object Link Resource
|
||||
% Example 1) request to Object 65 Instance 0: Read /65/0
|
||||
Data = <<16#88, 16#00, 16#0C, 16#44, 16#00, 16#00, 16#42, 16#00, 16#00, 16#44, 16#01, 16#00, 16#42, 16#00, 16#01, 16#C8, 16#01, 16#0D, 16#38, 16#36, 16#31, 16#33, 16#38, 16#30, 16#30, 16#37, 16#35, 16#35, 16#35, 16#30, 16#30, 16#C4, 16#02, 16#12, 16#34, 16#56, 16#78>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_multiple_resource => 16#00, value => [
|
||||
#{tlv_resource_instance => 16#00, value => <<16#00, 16#42, 16#00, 16#00>>},
|
||||
#{tlv_resource_instance => 16#01, value => <<16#00, 16#42, 16#00, 16#01>>}
|
||||
]},
|
||||
#{tlv_resource_with_value => 16#01, value => <<"8613800755500">>},
|
||||
#{tlv_resource_with_value => 16#02, value => <<16#12345678:32>>}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
||||
case09(_Config) ->
|
||||
% 6.4.3.3 Example of Request on an Object Instance containing an Object Link Resource
|
||||
% Example 2) request to Object 66: Read /66: TLV payload will contain 2 Object Instances
|
||||
Data = <<16#08, 16#00, 16#26, 16#C8, 16#00, 16#0B, 16#6D, 16#79, 16#53, 16#65, 16#72, 16#76, 16#69, 16#63, 16#65, 16#20, 16#31, 16#C8, 16#01, 16#0F, 16#49, 16#6E, 16#74, 16#65, 16#72, 16#6E, 16#65, 16#74, 16#2E, 16#31, 16#35, 16#2E, 16#32, 16#33, 16#34, 16#C4, 16#02, 16#00, 16#43, 16#00, 16#00, 16#08, 16#01, 16#26, 16#C8, 16#00, 16#0B, 16#6D, 16#79, 16#53, 16#65, 16#72, 16#76, 16#69, 16#63, 16#65, 16#20, 16#32, 16#C8, 16#01, 16#0F, 16#49, 16#6E, 16#74, 16#65, 16#72, 16#6E, 16#65, 16#74, 16#2E, 16#31, 16#35, 16#2E, 16#32, 16#33, 16#35, 16#C4, 16#02, 16#FF, 16#FF, 16#FF, 16#FF>>,
|
||||
R = emqx_lwm2m_tlv:parse(Data),
|
||||
Exp = [
|
||||
#{tlv_object_instance => 16#00, value => [
|
||||
#{tlv_resource_with_value => 16#00, value => <<"myService 1">>},
|
||||
#{tlv_resource_with_value => 16#01, value => <<"Internet.15.234">>},
|
||||
#{tlv_resource_with_value => 16#02, value => <<16#00, 16#43, 16#00, 16#00>>}
|
||||
]},
|
||||
#{tlv_object_instance => 16#01, value => [
|
||||
#{tlv_resource_with_value => 16#00, value => <<"myService 2">>},
|
||||
#{tlv_resource_with_value => 16#01, value => <<"Internet.15.235">>},
|
||||
#{tlv_resource_with_value => 16#02, value => <<16#FF, 16#FF, 16#FF, 16#FF>>}
|
||||
]}
|
||||
],
|
||||
?assertEqual(Exp, R),
|
||||
EncodedBinary = emqx_lwm2m_tlv:encode(Exp),
|
||||
?assertEqual(EncodedBinary, Data).
|
||||
|
171
apps/emqx_lwm2m/test/test_mqtt_broker.erl
Normal file
171
apps/emqx_lwm2m/test/test_mqtt_broker.erl
Normal file
@ -0,0 +1,171 @@
|
||||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ 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(test_mqtt_broker).
|
||||
|
||||
-compile(nowarn_export_all).
|
||||
-compile(export_all).
|
||||
|
||||
-define(LOGT(Format, Args), logger:debug("TEST_BROKER: " ++ Format, Args)).
|
||||
|
||||
-record(state, {subscriber}).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
start(_, <<"attacker">>, _, _, _) ->
|
||||
{stop, auth_failure};
|
||||
start(ClientId, Username, Password, _Channel, KeepaliveInterval) ->
|
||||
true = is_binary(ClientId),
|
||||
(true = ( is_binary(Username)) orelse (Username == undefined) ),
|
||||
(true = ( is_binary(Password)) orelse (Password == undefined) ),
|
||||
self() ! {keepalive, start, KeepaliveInterval},
|
||||
{ok, []}.
|
||||
|
||||
publish(Topic, Payload, Qos) ->
|
||||
ClientId = <<"lwm2m_test_suite">>,
|
||||
Msg = emqx_message:make(ClientId, Qos, Topic, Payload),
|
||||
emqx:publish(Msg).
|
||||
|
||||
subscribe(Topic) ->
|
||||
gen_server:call(?MODULE, {subscribe, Topic, self()}).
|
||||
|
||||
unsubscribe(Topic) ->
|
||||
gen_server:call(?MODULE, {unsubscribe, Topic}).
|
||||
|
||||
get_subscrbied_topics() ->
|
||||
[Topic || {_Client, Topic} <- ets:tab2list(emqx_subscription)].
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
stop() ->
|
||||
gen_server:stop(?MODULE).
|
||||
|
||||
init(_Param) ->
|
||||
{ok, #state{subscriber = []}}.
|
||||
|
||||
handle_call({subscribe, Topic, Proc}, _From, State=#state{subscriber = SubList}) ->
|
||||
?LOGT("test broker subscribe Topic=~p, Pid=~p~n", [Topic, Proc]),
|
||||
is_binary(Topic) orelse error("Topic should be a binary"),
|
||||
{reply, {ok, []}, State#state{subscriber = [{Topic, Proc}|SubList]}};
|
||||
|
||||
handle_call(get_subscribed_topics, _From, State=#state{subscriber = SubList}) ->
|
||||
Response = subscribed_topics(SubList, []),
|
||||
?LOGT("test broker get subscribed topics=~p~n", [Response]),
|
||||
{reply, Response, State};
|
||||
|
||||
handle_call({unsubscribe, Topic}, _From, State=#state{subscriber = SubList}) ->
|
||||
?LOGT("test broker unsubscribe Topic=~p~n", [Topic]),
|
||||
is_binary(Topic) orelse error("Topic should be a binary"),
|
||||
NewSubList = proplists:delete(Topic, SubList),
|
||||
{reply, {ok, []}, State#state{subscriber = NewSubList}};
|
||||
|
||||
|
||||
handle_call({publish, {Topic, Msg, MatchedTopicFilter}}, _From, State=#state{subscriber = SubList}) ->
|
||||
(is_binary(Topic) and is_binary(Msg)) orelse error("Topic and Msg should be binary"),
|
||||
Pid = proplists:get_value(MatchedTopicFilter, SubList),
|
||||
?LOGT("test broker publish topic=~p, Msg=~p, Pid=~p, MatchedTopicFilter=~p, SubList=~p~n", [Topic, Msg, Pid, MatchedTopicFilter, SubList]),
|
||||
(Pid == undefined) andalso ?LOGT("!!!!! this topic ~p has never been subscribed, please specify a valid topic filter", [MatchedTopicFilter]),
|
||||
?assertNotEqual(undefined, Pid),
|
||||
Pid ! {deliver, #message{topic = Topic, payload = Msg}},
|
||||
{reply, ok, State};
|
||||
|
||||
handle_call(stop, _From, State) ->
|
||||
{stop, normal, stopped, State};
|
||||
|
||||
handle_call(Req, _From, State) ->
|
||||
?LOGT("test_broker_server: ignore call Req=~p~n", [Req]),
|
||||
{reply, {error, badreq}, State}.
|
||||
|
||||
|
||||
handle_cast(Msg, State) ->
|
||||
?LOGT("test_broker_server: ignore cast msg=~p~n", [Msg]),
|
||||
{noreply, State}.
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?LOGT("test_broker_server: ignore info=~p~n", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
terminate(Reason, _State) ->
|
||||
?LOGT("test_broker_server: terminate Reason=~p~n", [Reason]),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
|
||||
|
||||
|
||||
subscribed_topics([], Acc) ->
|
||||
Acc;
|
||||
subscribed_topics([{Topic,_Pid}|T], Acc) ->
|
||||
subscribed_topics(T, [Topic|Acc]).
|
||||
|
||||
|
||||
|
||||
|
||||
-record(keepalive, {statfun, statval, tsec, tmsg, tref, repeat = 0}).
|
||||
|
||||
-type(keepalive() :: #keepalive{}).
|
||||
|
||||
%% @doc Start a keepalive
|
||||
-spec(start(fun(), integer(), any()) -> undefined | keepalive()).
|
||||
start(_, 0, _) ->
|
||||
undefined;
|
||||
start(StatFun, TimeoutSec, TimeoutMsg) ->
|
||||
{ok, StatVal} = StatFun(),
|
||||
#keepalive{statfun = StatFun, statval = StatVal,
|
||||
tsec = TimeoutSec, tmsg = TimeoutMsg,
|
||||
tref = timer(TimeoutSec, TimeoutMsg)}.
|
||||
|
||||
%% @doc Check keepalive, called when timeout.
|
||||
-spec(check(keepalive()) -> {ok, keepalive()} | {error, any()}).
|
||||
check(KeepAlive = #keepalive{statfun = StatFun, statval = LastVal, repeat = Repeat}) ->
|
||||
case StatFun() of
|
||||
{ok, NewVal} ->
|
||||
if NewVal =/= LastVal ->
|
||||
{ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = 0})};
|
||||
Repeat < 1 ->
|
||||
{ok, resume(KeepAlive#keepalive{statval = NewVal, repeat = Repeat + 1})};
|
||||
true ->
|
||||
{error, timeout}
|
||||
end;
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
resume(KeepAlive = #keepalive{tsec = TimeoutSec, tmsg = TimeoutMsg}) ->
|
||||
KeepAlive#keepalive{tref = timer(TimeoutSec, TimeoutMsg)}.
|
||||
|
||||
%% @doc Cancel Keepalive
|
||||
-spec(cancel(keepalive()) -> ok).
|
||||
cancel(#keepalive{tref = TRef}) ->
|
||||
cancel(TRef);
|
||||
cancel(undefined) ->
|
||||
ok;
|
||||
cancel(TRef) ->
|
||||
catch erlang:cancel_timer(TRef).
|
||||
|
||||
timer(Sec, Msg) ->
|
||||
erlang:send_after(timer:seconds(Sec), self(), Msg).
|
||||
|
||||
|
||||
log(Format, Args) ->
|
||||
logger:debug(Format, Args).
|
@ -1,6 +1,6 @@
|
||||
{application, emqx_management,
|
||||
[{description, "EMQ X Management API and CLI"},
|
||||
{vsn, "4.3.1"}, % strict semver, bump manually!
|
||||
{vsn, "4.3.0"}, % strict semver, bump manually!
|
||||
{modules, []},
|
||||
{registered, [emqx_management_sup]},
|
||||
{applications, [kernel,stdlib,minirest]},
|
||||
@ -9,6 +9,6 @@
|
||||
{licenses, ["Apache-2.0"]},
|
||||
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||
{links, [{"Homepage", "https://emqx.io/"},
|
||||
{"Github", "https://hub.fastgit.org/emqx/emqx-management"}
|
||||
{"Github", "https://hub.fastgit.org/fastdgiot/emqx-management"}
|
||||
]}
|
||||
]}.
|
||||
|
@ -1,12 +0,0 @@
|
||||
%% -*-: erlang -*-
|
||||
{"4.3.1",
|
||||
[ {"4.3.0",
|
||||
[ {load_module, emqx_mgmt_data_backup, brutal_purge, soft_purge, []}
|
||||
]}
|
||||
],
|
||||
[
|
||||
{"4.3.0",
|
||||
[ {load_module, emqx_mgmt_data_backup, brutal_purge, soft_purge, []}
|
||||
]}
|
||||
]
|
||||
}.
|
@ -514,7 +514,7 @@ import_modules(Modules) ->
|
||||
<<"enabled">> := Enabled,
|
||||
<<"created_at">> := CreatedAt,
|
||||
<<"description">> := Description}) ->
|
||||
_ = emqx_modules:import_module({Id, any_to_atom(Type), Config, Enabled, CreatedAt, Description})
|
||||
emqx_modules:import_module({Id, any_to_atom(Type), Config, Enabled, CreatedAt, Description})
|
||||
end, Modules)
|
||||
end.
|
||||
|
||||
|
0
apps/emqx_management/test/emqx_auth_mnesia_migration_SUITE_data/make_data.sh
Normal file → Executable file
0
apps/emqx_management/test/emqx_auth_mnesia_migration_SUITE_data/make_data.sh
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user