add new project

This commit is contained in:
lsxredrain 2021-05-22 03:06:00 +09:00
parent 006ef08483
commit 789fe8333a
255 changed files with 23519 additions and 2716 deletions

View File

@ -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:
@ -79,4 +79,4 @@ Just as in the **subject**, use the imperative, present tense: "change" not "cha
The footer should contain any information about **Breaking Changes** and is also the place to reference GitHub issues that this commit **Closes**.
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.

View File

@ -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)

View File

@ -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 规范

View File

@ -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).

View File

@ -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).
### Разработка плагинов

View File

@ -1,35 +1,35 @@
# 数蛙工业互联网持续集成平台
## 前言
数蛙团队2016年之前在互联网和移动互联网爬坑多年2016年开始进入物联网爬坑希望通过这个开源平台把多年爬坑经验共享出来让多学科交叉的工业互联网项目变得更简单。
- 让丰富工程人员可以通过视窗交互可以完成需求较简单的工业互联网项目
- 让广大的初级前端工程师通过serverless的方式可以承接需求较复杂的工业互联网项目
- 让Python、Java、Go、C初级后台工程师通过web编程开发通道来承接复杂的工业互联网项目
工业互联网落地过程是IT和OT持续融合的过程也是技术领域和业务领域持续融合的过程。会者不难难着不会要把工业互联网项目变得简单一些主要还是需要依赖于各个领域的专家围绕业务场景进行信息流的无缝路由构建一张多学科融合的知识路由表。
基于数蛙工业物联网持续集成平台可以让学者多学科交叉的知识路由表中发布学术论文,技者在实战中练就快速消化项目的本领和收入回报,商者围绕实际项目找到盈利商机。
# 愿景
数蛙团队希望通过数蛙工业互联网持续集成平台达成下面一些愿景:
+ 通过工程人员、前端工程师、初级后台工程师在不超过1个月的实际完成中小型的工业互联网项目
+ 通过代码开源、软件免费、文档共享、技术认证、产品认证、运维托管等多种方式保证高质量的交付
+ 技术领域专家不断持续集成业界优秀技术框架、业务领域专家不断持续优化业务模型和流程、构建多学科交叉的开放平台
# 产品
+ 边缘侧系列产品
+ 云端系统产品
# 构建
+ Linux/Unix/Mac构建
@ -40,7 +40,7 @@ make
+ windows构建
```
下载msys64
git clone https://hub.fastgit.org/dgiot/dgiot_server.git
make
``
@ -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) |

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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).

View File

View 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.

View 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()]}).

View File

43
apps/dgiot/rebar.config Normal file
View 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]}
]}
]}.

View 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.

View 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).

View 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"}
]}
]}.

View 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.

View 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
%%--------------------------------------------------------------------

View 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
View 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"}.

View File

@ -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
View 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
View 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
View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
## applications 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 Mozillas 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

View 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}).

View 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"

View File

@ -0,0 +1,8 @@
Integration test for emq-coap
======
execute following command
```
make
```

View 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()

View 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}.

View File

@ -0,0 +1,4 @@
{deps,
[
{gen_coap, {git, "https://hub.fastgit.org/fastdgiot/gen_coap", {tag, "v0.3.2"}}}
]}.

View 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"}
]}
]}.

View 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)).

View 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
}.

View 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=~pContent=~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.

View 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)}]).

View 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]).

View 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.

View 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]).

View 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]}}.

View 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.

View 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)).

View 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 successfullygot {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.

View File

@ -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)

View File

@ -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)
## 设计

View File

@ -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"}}}
]}
]}
]}.

View File

@ -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

View File

@ -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
##

View File

@ -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"}}}
]}
]}
]}.

View File

@ -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.

View File

@ -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}]}.

View File

@ -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
View 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
View 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.

View 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
## applications 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 Mozillas 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

View 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).

View 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"

View 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()

View 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()

View 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()

View 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()

View 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;
}

View 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>

View File

@ -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>

View 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 Clients 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>

View 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>

View 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>

View 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 Clients 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 Servers or LwM2M Bootstrap-Servers 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>

View 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 doesnt 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>

View 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}.

View 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}]}]}.

View 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>>).

View 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,[]}}]}.

View 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.

View 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.

View 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.

View 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}].

View 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).

View 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.

View 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]).

View 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)] }}.

View 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}.

View 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.

View 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.

View 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.

File diff suppressed because it is too large Load Diff

View 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).

View 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).

View File

@ -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"}
]}
]}.

View File

@ -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, []}
]}
]
}.

View File

@ -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.

Some files were not shown because too many files have changed in this diff Show More