change to docker

This commit is contained in:
anlicheng 2025-11-12 15:49:03 +08:00
parent 8181d1af19
commit 25bb8b0514
54 changed files with 2874 additions and 5017 deletions

954
HTTP_API_README.md Normal file
View File

@ -0,0 +1,954 @@
# 🧩 IoT 容器管理接口文档
**模块**`container_handler`
**作者**licheng5
**创建时间**2020-04-26
**说明**:提供容器的部署、配置、启动、停止、查询等管理 API 接口。
## 服务器地址
http://127.0.0.1:18090
---
## 📦 模块结构
| 模块 | 说明 |
|------|------|
| `container_handler` | 提供容器管理的 HTTP 接口处理 |
| `iot_util` | 工具模块,用于生成标准化 JSON 响应 |
---
## ⚙️ `iot_util` 模块函数声明
```erlang
%%--------------------------------------------------------------------
%% @doc
%% 将数据封装为标准 JSON 响应:
%% {"result": Data}
%%--------------------------------------------------------------------
json_data(Data) ->
jiffy:encode(#{
<<"result">> => Data
}, [force_utf8]).
%%--------------------------------------------------------------------
%% @doc
%% 生成错误响应 JSON
%% {
%% "error": {
%% "code": ErrCode,
%% "message": ErrMessage
%% }
%% }
%%--------------------------------------------------------------------
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
jiffy:encode(#{
<<"error">> => #{
<<"code">> => ErrCode,
<<"message">> => ErrMessage
}
}, [force_utf8]).
```
### 📘 返回格式说明
| 字段 | 说明 |
|------|------|
| `result` | 正常返回数据 |
| `error.code` | 错误代码 |
| `error.message` | 错误信息 |
---
## 🌐 HTTP API 接口列表
以下为 `container_handler` 模块导出的全部 HTTP 接口。
---
### 1⃣ 获取容器列表
**URL**`/container/get_all`
**Method**`GET`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | list | 容器信息列表 |
#### 示例响应
```json
{
"result": [
{"name": "container_1", "status": "running"},
{"name": "container_2", "status": "stopped"}
]
}
```
#### 错误响应
```json
{
"error": {
"code": -1,
"message": "host not found"
}
}
```
---
### 2⃣ 下发配置文件
**URL**`/container/push_config`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
| container_name | binary (string) | ✅ | 容器名称 |
| config | binary (string, JSON) | ✅ | 容器配置内容JSON 字符串) |
| timeout | integer | ✅ | 超时时间(秒) |
### 请求示例
```json
{"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm", "container_name": "my_nginx_new", "config": "{\"application\":{\"namexyz\":\"RandomConfigApp\",\"version\":\"1.2.7\",\"environment\":{\"debug_mode\":true,\"log_level\":\"info\",\"max_log_files\":10}},\"server\":{\"host\":\"127.0.0.1\",\"port\":8080,\"ssl_enabled\":false,\"allowed_origins\":[\"https:\\/\\/example.com\",\"http:\\/\\/localhost:3000\"]},\"database\":{\"type\":\"postgresql\",\"host\":\"db.example.com\",\"port\":5432,\"username\":\"admin_7xq9f\",\"password\":\"p@ssw0rd!r4nd\",\"connection_pool\":15,\"timeout_seconds\":30},\"features\":{\"enable_analytics\":true,\"enable_cache\":false,\"experimental_features\":[\"ai_enhancement\",\"realtime_sync\"]},\"third_party\":{\"api_key\":\"a3b8c2d4e5f6g7h8i9j0k1l2m3n4o5p\",\"weather_service_url\":\"https:\\/\\/api.weather.example\\/v3\",\"payment_gateway\":{\"endpoint\":\"https:\\/\\/pay.example.com\",\"merchant_id\":\"M123456789\"}},\"scheduled_tasks\":[{\"name\":\"nightly_backup\",\"cron\":\"0 3 * * *\",\"enabled\":true},{\"name\":\"log_cleanup\",\"interval_minutes\":1440,\"retention_days\":7}],\"admins\":[{\"username\":\"alice_dev\",\"email\":\"alice@example.com\",\"permissions\":[\"read\",\"write\",\"admin\"]},{\"username\":\"bob_ops\",\"email\":\"bob@example.org\",\"permissions\":[\"read\",\"audit\"]}]}", "timeout": 10}
```
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | map | 配置结果 |
#### 示例响应
```json
{
"result": {
"container_name": "sensor_service",
"status": "config pushed"
}
}
```
#### 错误响应
```json
{
"error": {
"code": -1,
"message": "host not found"
}
}
```
---
### 3⃣ 部署容器服务
**URL**`/container/deploy`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
| task_id | integer | ✅ | 任务 ID |
| config | map | ✅ | 部署配置内容 |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | map | 部署结果 |
### 请求示例
```json
{
"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm",
"task_id": 1,
"timeout": 10,
"config": {
"image": "docker.1ms.run/library/nginx:latest",
"container_name": "my_nginx_new",
"command": [
"nginx",
"-g",
"daemon off;"
],
"entrypoint": [
"/docker-entrypoint.sh"
],
"envs": [
"ENV1=val1",
"ENV2=val2"
],
"env_file": [
"./env.list"
],
"ports": [
"8080:80",
"443:443"
],
"expose": [
"80",
"443"
],
"volumes": [
"/host/data:/data",
"/host/log:/var/log"
],
"networks": [
"mynet"
],
"labels": {
"role": "web",
"env": "prod"
},
"restart": "always",
"user": "www-data",
"working_dir": "/app",
"hostname": "myhost",
"privileged": true,
"cap_add": [
"NET_ADMIN"
],
"cap_drop": [
"MKNOD"
],
"devices": [
"/dev/snd:/dev/snd"
],
"mem_limit": "512m",
"mem_reservation": "256m",
"cpu_shares": 512,
"cpus": 1.5,
"ulimits": {
"nofile": "1024:2048"
},
"sysctls": {
"net.ipv4.ip_forward": "1"
},
"tmpfs": [
"/tmp"
],
"extra_hosts": [
"host1:192.168.0.1"
],
"healthcheck": {
"test": [
"CMD-SHELL",
"curl -f http://localhost || exit 1"
],
"interval": "30s",
"timeout": "10s",
"retries": 3
}
}
}
```
#### 示例响应
```json
{
"result": {
"task_id": 1001,
"status": "deployed"
}
}
```
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "host not found"
}
}
```
---
### 4⃣ 启动容器服务
**URL**`/container/start`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
| container_name | binary (string) | ✅ | 容器名称 |
### 请求示例
```json
{"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm", "container_name": "my_nginx_new"}
```
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | map | 启动结果 |
#### 示例响应
```json
{
"result": {
"container_name": "sensor_service",
"status": "started"
}
}
```
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "host not found"
}
}
```
---
### 5⃣ 停止容器服务
**URL**`/container/stop`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
| container_name | binary (string) | ✅ | 容器名称 |
### 请求示例
```json
{"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm", "container_name": "my_nginx_new"}
```
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | map | 停止结果 |
#### 示例响应
```json
{
"result": {
"container_name": "sensor_service",
"status": "stopped"
}
}
```
### 6 强制停止容器服务
**URL**`/container/kill`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
| container_name | binary (string) | ✅ | 容器名称 |
### 请求示例
```json
{"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm", "container_name": "my_nginx_new"}
```
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | map | 停止结果 |
#### 示例响应
```json
{
"result": {
"container_name": "sensor_service",
"status": "stopped"
}
}
```
### 7 删除容器
**URL**`/container/remove`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
| container_name | binary (string) | ✅ | 容器名称 |
### 请求示例
```json
{"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm", "container_name": "my_nginx_new"}
```
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | map | 停止结果 |
#### 示例响应
```json
{
"result": {
"container_name": "sensor_service",
"status": "stopped"
}
}
```
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "host not found"
}
}
```
---
### 6⃣ 未知路径处理
**说明**:如果请求路径未匹配任何定义接口,将返回错误信息。
#### 示例响应
```json
{
"error": {
"code": -1,
"message": "url: /unknown/path not found"
}
}
```
---
## 🧾 返回约定总结
| 类型 | 说明 |
|------|------|
| `{"result": Data}` | 表示请求成功 |
| `{"error": {"code": N, "message": Text}}` | 表示请求失败 |
# 🧩 IoT Endpoint 管理接口文档
**模块**`endpoint_handler`
**作者**licheng5
**创建时间**2020-04-26
**说明**:用于管理 IoT Endpoint 的运行状态,包括启动、停止、重启、状态查询等。
---
## 📦 模块结构
| 模块 | 说明 |
|------|------|
| `endpoint_handler` | 提供 Endpoint 管理的 HTTP 接口处理 |
| `iot_util` | 工具模块,用于生成标准化 JSON 响应 |
---
## ⚙️ `iot_util` 模块函数声明
```erlang
%%--------------------------------------------------------------------
%% @doc
%% 将数据封装为标准 JSON 响应:
%% {"result": Data}
%%--------------------------------------------------------------------
json_data(Data) ->
jiffy:encode(#{
<<"result">> => Data
}, [force_utf8]).
%%--------------------------------------------------------------------
%% @doc
%% 生成错误响应 JSON
%% {
%% "error": {
%% "code": ErrCode,
%% "message": ErrMessage
%% }
%% }
%%--------------------------------------------------------------------
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
jiffy:encode(#{
<<"error">> => #{
<<"code">> => ErrCode,
<<"message">> => ErrMessage
}
}, [force_utf8]).
```
### 📘 返回格式说明
| 字段 | 说明 |
|------|------|
| `result` | 正常返回数据 |
| `error.code` | 错误代码 |
| `error.message` | 错误信息 |
---
## 🌐 HTTP API 接口列表
以下为 `endpoint_handler` 模块导出的全部 HTTP 接口。
---
### 1⃣ 获取 Endpoint 运行状态
**URL**`/endpoint/run_statuses`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| Ids | list | ✅ | Endpoint ID 列表 |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | list | 每个 ID 对应状态:`0` 未运行,`1` 运行中 |
#### 示例请求
```json
[1, 2, 3]
```
#### 示例响应
```json
{
"result": [1, 0, 1]
}
```
---
### 2⃣ 启动 Endpoint
**URL**`/endpoint/start`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | integer | ✅ | Endpoint 唯一 ID |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | string | 启动结果,如 `"success"` |
#### 示例响应
```json
{
"result": "success"
}
```
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "endpoint not found"
}
}
```
---
### 3⃣ 停止 Endpoint
**URL**`/endpoint/stop`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | integer | ✅ | Endpoint 唯一 ID |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | string | 停止结果,如 `"success"` |
#### 示例响应
```json
{
"result": "success"
}
```
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "stop endpoint error"
}
}
```
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "stop endpoint error"
}
}
```
---
### 4⃣ 重启 Endpoint
**URL**`/endpoint/restart`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | integer | ✅ | Endpoint 唯一 ID |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | string | 重启结果,如 `"success"` |
#### 示例响应
```json
{
"result": "success"
}
```
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "restart endpoint error"
}
}
```
---
### 5⃣ 未知路径处理
**说明**:如果请求路径未匹配任何定义接口,将返回错误信息。
#### 示例响应
```json
{
"error": {
"code": -1,
"message": "url: /unknown/path not found"
}
}
```
---
## 🧾 返回约定总结
| 类型 | 说明 |
|------|------|
| `{"result": Data}` | 表示请求成功 |
| `{"error": {"code": N, "message": Text}}` | 表示请求失败 |
# 🧩 IoT Host 管理接口文档
**模块**`host_handler`
**作者**licheng5
**创建时间**2020-04-26
**说明**:用于管理 IoT 主机,包括主机指标、状态查询、激活、删除、发布事件等接口。
---
## 📦 模块结构
| 模块 | 说明 |
|------|------|
| `host_handler` | 提供 Host 管理的 HTTP 接口处理 |
| `iot_util` | 工具模块,用于生成标准化 JSON 响应 |
---
## ⚙️ `iot_util` 模块函数声明
```erlang
%%--------------------------------------------------------------------
%% @doc
%% 将数据封装为标准 JSON 响应:
%% {"result": Data}
%%--------------------------------------------------------------------
json_data(Data) ->
jiffy:encode(#{
<<"result">> => Data
}, [force_utf8]).
%%--------------------------------------------------------------------
%% @doc
%% 生成错误响应 JSON
%% {
%% "error": {
%% "code": ErrCode,
%% "message": ErrMessage
%% }
%% }
%%--------------------------------------------------------------------
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
jiffy:encode(#{
<<"error">> => #{
<<"code">> => ErrCode,
<<"message">> => ErrMessage
}
}, [force_utf8]).
```
### 📘 返回格式说明
| 字段 | 说明 |
|------|------|
| `result` | 正常返回数据 |
| `error.code` | 错误代码 |
| `error.message` | 错误信息 |
---
## 🌐 HTTP API 接口列表
以下为 `host_handler` 模块导出的全部 HTTP 接口。
---
### 1⃣ 获取主机指标
**URL**`/host/metric`
**Method**`GET`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | map | 主机指标信息 |
#### 示例响应
```json
{
"result": {"cpu": 20, "memory": 1024, "disk": 51200}
}
```
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "host not found"
}
}
```
#### 无指标信息响应
```json
{
"error": {
"code": 404,
"message": "no metric info"
}
}
```
---
### 2⃣ 查询主机状态
**URL**`/host/status`
**Method**`GET`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | map | 主机状态信息 |
#### 示例响应
```json
{
"result": {"authorize_status": 1, "active": true}
}
```
---
### 3⃣ 重新加载主机信息
**URL**`/host/reload`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | string | 重新加载结果,如 `"success"` |
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "reload error"
}
}
```
---
### 4⃣ 删除主机
**URL**`/host/delete`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | string | 删除结果,如 `"success"` |
#### 错误响应
```json
{
"error": {
"code": 404,
"message": "error"
}
}
```
---
### 5⃣ 激活主机
**URL**`/host/activate`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
| auth | boolean | ✅ | `true` 激活, `false` 取消激活 |
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | string | 激活结果,如 `"success"` |
#### 错误响应
```json
{
"error": {
"code": 400,
"message": "host not found"
}
}
```
---
### 6⃣ 发布主机事件
**URL**`/host/pub`
**Method**`POST`
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| uuid | binary (string) | ✅ | 主机唯一标识符 |
| topic | binary (string) | ✅ | 事件主题 |
| content | binary (string) | ✅ | 发布内容 |
### 请求示例
```json
{"uuid": "qbxmjyzrkpntfgswaevodhluicqzxplkm", "topic": "/device/1234/all", "content": "this is a topic payload", "timeout": 10}
```
#### 响应参数
| 字段 | 类型 | 说明 |
|------|------|------|
| result | string | 发布结果,如 `"success"` |
#### 错误响应
```json
{
"error": {
"code": 400,
"message": "host not found"
}
}
```
---
### 7⃣ 未知路径处理
**说明**:如果请求路径未匹配任何定义接口,将返回错误信息。
#### 示例响应
```json
{
"error": {
"code": -1,
"message": "url: /unknown/path not found"
}
}
```
---
## 🧾 返回约定总结
| 类型 | 说明 |
|------|------|
| `{"result": Data}` | 表示请求成功 |
| `{"error": {"code": N, "message": Text}}` | 表示请求失败 |

View File

@ -1,12 +0,0 @@
iot
=====
An OTP application
## erlang client sdk
https://github.com/emqx/emqtt
Build
-----
$ rebar3 compile

View File

@ -33,7 +33,7 @@
-record(endpoint, {
id :: integer(),
%%
name :: binary(),
matcher :: binary(),
%%
title = <<>> :: binary(),
%% , : #{<<"protocol">> => <<"http|https|ws|kafka|mqtt">>, <<"args">> => #{}}

View File

@ -23,51 +23,10 @@
-define(TASK_STATUS_FAILED, 0). %% 线
-define(TASK_STATUS_OK, 1). %% 线
%% efka主动发起的消息体类型,
-define(PACKET_REQUEST, 16#01).
-define(PACKET_RESPONSE, 16#02).
%% pub/sub的消息,
-define(PACKET_PUB, 16#03).
%% push调用不需要返回,
-define(PACKET_COMMAND, 16#04).
%%
-define(PACKET_ASYNC_CALL, 16#05).
-define(PACKET_ASYNC_CALL_REPLY, 16#06).
%% ping包
-define(PACKET_PING, 16#FF).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
-define(METHOD_AUTH, 16#01).
-define(METHOD_DATA, 16#02).
-define(METHOD_PING, 16#03).
-define(METHOD_INFORM, 16#04).
-define(METHOD_EVENT, 16#05).
-define(METHOD_PHASE, 16#06).
-define(METHOD_REQUEST_SERVICE_CONFIG, 16#07).
%%%% ,
%%
-define(COMMAND_AUTH, 16#08).
%%%% ,
-define(PUSH_DEPLOY, 16#01).
-define(PUSH_START_SERVICE, 16#02).
-define(PUSH_STOP_SERVICE, 16#03).
-define(PUSH_SERVICE_CONFIG, 16#04).
-define(PUSH_INVOKE, 16#05).
-define(PUSH_TASK_LOG, 16#06).
%%
-record(kv, {
key :: binary(),

View File

@ -0,0 +1,93 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%% , 1: topic的pub/sub机制; 2. target的单点通讯和广播
%%% @end
%%% Created : 21. 4 2025 17:28
%%%-------------------------------------------------------------------
-author("anlicheng").
%% efka主动发起的消息体类型,
-define(PACKET_REQUEST, 16#01).
-define(PACKET_RESPONSE, 16#02).
%% efka主动发起不需要返回的数据
-define(PACKET_CAST, 16#03).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
-define(MESSAGE_AUTH_REQUEST, 16#01).
-define(MESSAGE_AUTH_REPLY, 16#02).
-define(MESSAGE_COMMAND, 16#03).
-define(MESSAGE_DEPLOY, 16#04).
-define(MESSAGE_PUB, 16#05).
-define(MESSAGE_DATA, 16#06).
-define(MESSAGE_EVENT, 16#07).
%% efka主动上报的event-stream流, : docker-create的实时处理逻辑上报
-define(MESSAGE_EVENT_STREAM, 16#08).
-define(MESSAGE_JSONRPC_REQUEST, 16#F0).
-define(MESSAGE_JSONRPC_REPLY, 16#F1).
%%%% ,
%%
-define(COMMAND_AUTH, 16#08).
-record(auth_request, {
uuid :: binary(),
username :: binary(),
salt :: binary(),
token :: binary(),
timestamp :: integer()
}).
-record(auth_reply, {
code :: integer(),
payload :: binary()
}).
-record(pub, {
topic :: binary(),
content :: binary()
}).
-record(command, {
command_type :: integer(),
command :: binary()
}).
-record(jsonrpc_request, {
method :: binary(),
params = <<>> :: any()
}).
-record(jsonrpc_reply, {
result :: any() | undefined,
error :: any() | undefined
}).
-record(data, {
service_id :: binary(),
device_uuid :: binary(),
route_key :: binary(),
metric :: binary()
}).
-record(event, {
service_id :: binary(),
event_type :: integer(),
params :: binary()
}).
-record(task_event_stream, {
task_id :: integer(),
type :: binary(),
stream :: binary()
}).

View File

@ -1,133 +0,0 @@
%% -*- coding: utf-8 -*-
%% Automatically generated, do not edit
%% Generated by gpb_compile version 4.21.1
-ifndef(message_pb).
-define(message_pb, true).
-define(message_pb_gpb_version, "4.21.1").
-ifndef('AUTH_REQUEST_PB_H').
-define('AUTH_REQUEST_PB_H', true).
-record(auth_request,
{uuid = <<>> :: unicode:chardata() | undefined, % = 1, optional
username = <<>> :: unicode:chardata() | undefined, % = 2, optional
salt = <<>> :: unicode:chardata() | undefined, % = 4, optional
token = <<>> :: unicode:chardata() | undefined, % = 5, optional
timestamp = 0 :: non_neg_integer() | undefined % = 6, optional, 32 bits
}).
-endif.
-ifndef('AUTH_REPLY_PB_H').
-define('AUTH_REPLY_PB_H', true).
-record(auth_reply,
{code = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
message = <<>> :: unicode:chardata() | undefined % = 2, optional
}).
-endif.
-ifndef('PUB_PB_H').
-define('PUB_PB_H', true).
-record(pub,
{topic = <<>> :: unicode:chardata() | undefined, % = 1, optional
content = <<>> :: iodata() | undefined % = 2, optional
}).
-endif.
-ifndef('COMMAND_PB_H').
-define('COMMAND_PB_H', true).
-record(command,
{command_type = <<>> :: unicode:chardata() | undefined, % = 1, optional
command = <<>> :: iodata() | undefined % = 2, optional
}).
-endif.
-ifndef('RPC_DEPLOY_PB_H').
-define('RPC_DEPLOY_PB_H', true).
-record(rpc_deploy,
{packet_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
task_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
config = <<>> :: unicode:chardata() | undefined % = 3, optional
}).
-endif.
-ifndef('RPC_START_CONTAINER_PB_H').
-define('RPC_START_CONTAINER_PB_H', true).
-record(rpc_start_container,
{packet_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
container_name = <<>> :: unicode:chardata() | undefined % = 2, optional
}).
-endif.
-ifndef('RPC_STOP_CONTAINER_PB_H').
-define('RPC_STOP_CONTAINER_PB_H', true).
-record(rpc_stop_container,
{packet_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
container_name = <<>> :: unicode:chardata() | undefined % = 2, optional
}).
-endif.
-ifndef('RPC_CONFIG_CONTAINER_PB_H').
-define('RPC_CONFIG_CONTAINER_PB_H', true).
-record(rpc_config_container,
{packet_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
container_name = <<>> :: unicode:chardata() | undefined, % = 2, optional
config = <<>> :: iodata() | undefined % = 3, optional
}).
-endif.
-ifndef('FETCH_TASK_LOG_PB_H').
-define('FETCH_TASK_LOG_PB_H', true).
-record(fetch_task_log,
{task_id = 0 :: non_neg_integer() | undefined % = 1, optional, 32 bits
}).
-endif.
-ifndef('CONTAINER_CONFIG_PB_H').
-define('CONTAINER_CONFIG_PB_H', true).
-record(container_config,
{container_name = <<>> :: unicode:chardata() | undefined, % = 1, optional
config = <<>> :: iodata() | undefined % = 2, optional
}).
-endif.
-ifndef('DATA_PB_H').
-define('DATA_PB_H', true).
-record(data,
{service_id = <<>> :: unicode:chardata() | undefined, % = 1, optional
device_uuid = <<>> :: unicode:chardata() | undefined, % = 2, optional
route_key = <<>> :: unicode:chardata() | undefined, % = 3, optional
metric = <<>> :: iodata() | undefined % = 4, optional
}).
-endif.
-ifndef('EVENT_PB_H').
-define('EVENT_PB_H', true).
-record(event,
{service_id = <<>> :: unicode:chardata() | undefined, % = 1, optional
event_type = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
params = <<>> :: unicode:chardata() | undefined % = 3, optional
}).
-endif.
-ifndef('PING_PB_H').
-define('PING_PB_H', true).
-record(ping,
{adcode = <<>> :: unicode:chardata() | undefined, % = 1, optional
boot_time = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
province = <<>> :: unicode:chardata() | undefined, % = 3, optional
city = <<>> :: unicode:chardata() | undefined, % = 4, optional
efka_version = <<>> :: unicode:chardata() | undefined, % = 5, optional
kernel_arch = <<>> :: unicode:chardata() | undefined, % = 6, optional
ips = [] :: [unicode:chardata()] | undefined, % = 7, repeated
cpu_core = 0 :: non_neg_integer() | undefined, % = 8, optional, 32 bits
cpu_load = 0 :: non_neg_integer() | undefined, % = 9, optional, 32 bits
cpu_temperature = 0.0 :: float() | integer() | infinity | '-infinity' | nan | undefined, % = 10, optional
disk = [] :: [integer()] | undefined, % = 11, repeated, 32 bits
memory = [] :: [integer()] | undefined, % = 12, repeated, 32 bits
interfaces = <<>> :: unicode:chardata() | undefined % = 13, optional
}).
-endif.
-endif.

View File

@ -1,30 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 16. 5 2023 12:48
%%%-------------------------------------------------------------------
-module(ai_event_logs_bo).
-author("aresei").
-include("iot.hrl").
-export([insert/6]).
%% API
-spec insert(HostUUID :: binary(), DeviceUUID :: binary(), SceneId :: integer(), MicroId :: integer(), EventType :: integer(), Content :: binary()) ->
ok | {ok, InsertId :: integer()} | {error, Reason :: any()}.
insert(HostUUID, DeviceUUID, SceneId, MicroId, EventType, Content)
when is_integer(EventType), is_binary(HostUUID), is_binary(DeviceUUID), is_integer(SceneId), is_integer(MicroId), is_binary(Content) ->
mysql_pool:insert(mysql_iot, <<"ai_event_logs">>, #{
<<"event_type">> => EventType,
<<"host_uuid">> => HostUUID,
<<"device_uuid">> => DeviceUUID,
<<"scene_id">> => SceneId,
<<"micro_id">> => MicroId,
<<"content">> => Content,
<<"created_at">> => calendar:local_time()
}, true).

View File

@ -1,43 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 16. 5 2023 12:48
%%%-------------------------------------------------------------------
-module(device_bo).
-author("aresei").
-include("iot.hrl").
%% API
-export([get_all_devices/0, get_host_devices/1, get_device_by_uuid/1, change_status/2]).
-spec get_all_devices() -> {ok, Devices :: [map()]} | {error, Reason :: any()}.
get_all_devices() ->
mysql_pool:get_all(mysql_iot, <<"SELECT * FROM device WHERE device_uuid != ''">>).
-spec get_host_devices(HostId :: integer()) -> {ok, Devices :: [map()]} | {error, Reason::any()}.
get_host_devices(HostId) when is_integer(HostId) ->
mysql_pool:get_all(mysql_iot, <<"SELECT device_uuid FROM device WHERE host_id = ? AND device_uuid != ''">>, [HostId]).
-spec get_device_by_uuid(DeviceUUID :: binary()) -> {ok, DeviceInfo :: map()} | undefined.
get_device_by_uuid(DeviceUUID) when is_binary(DeviceUUID) ->
mysql_pool:get_row(mysql_iot, <<"SELECT * FROM device WHERE device_uuid = ? LIMIT 1">>, [DeviceUUID]).
%%
-spec change_status(DeviceUUID :: binary(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
change_status(DeviceUUID, NStatus) when is_binary(DeviceUUID), is_integer(NStatus) ->
change_status0(DeviceUUID, NStatus).
change_status0(DeviceUUID, ?DEVICE_ONLINE) when is_binary(DeviceUUID) ->
Timestamp = calendar:local_time(),
case mysql_pool:get_row(mysql_iot, <<"SELECT status FROM device WHERE device_uuid = ? LIMIT 1">>, [DeviceUUID]) of
{ok, #{<<"status">> := -1}} ->
mysql_pool:update_by(mysql_iot, <<"UPDATE device SET status = ?, access_at = ?, updated_at = ? WHERE device_uuid = ? LIMIT 1">>, [?DEVICE_ONLINE, Timestamp, Timestamp, DeviceUUID]);
{ok, _} ->
mysql_pool:update_by(mysql_iot, <<"UPDATE device SET status = ?, updated_at = ? WHERE device_uuid = ? LIMIT 1">>, [?DEVICE_ONLINE, Timestamp, DeviceUUID]);
undefined ->
{error, <<"device not found">>}
end;
change_status0(DeviceUUID, ?DEVICE_OFFLINE) when is_binary(DeviceUUID) ->
mysql_pool:update_by(mysql_iot, <<"UPDATE device SET status = ? WHERE device_uuid = ? LIMIT 1">>, [?DEVICE_OFFLINE, DeviceUUID]).

View File

@ -1,97 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 16. 5 2023 12:48
%%%-------------------------------------------------------------------
-module(endpoint_bo).
-author("aresei").
-include("endpoint.hrl").
%% API
-export([get_all_endpoints/0, get_endpoint/1]).
-export([endpoint_record/1]).
-spec get_all_endpoints() -> [Endpoint :: map()].
get_all_endpoints() ->
case mysql_pool:get_all(mysql_iot, <<"SELECT * FROM endpoint where status = 1">>) of
{ok, Endpoints} ->
Endpoints;
{error, _} ->
[]
end.
-spec get_endpoint(Id :: integer()) -> undefined | {ok, EndpointInfo :: map()}.
get_endpoint(Id) when is_integer(Id) ->
mysql_pool:get_row(mysql_iot, <<"SELECT * FROM endpoint WHERE id = ? and status = 1 LIMIT 1">>, [Id]).
-spec endpoint_record(EndpointInfo :: #{}) -> error | {ok, Endpoint :: #endpoint{}}.
endpoint_record(#{<<"id">> := Id, <<"name">> := Name, <<"title">> := Title, <<"type">> := Type, <<"config_json">> := ConfigJson,
<<"status">> := Status, <<"updated_at">> := UpdatedAt, <<"created_at">> := CreatedAt}) ->
try
Config = parse_config(Type, catch jiffy:decode(ConfigJson, [return_maps])),
{ok, #endpoint {
id = Id,
name = Name,
title = Title,
config = Config,
status = Status,
updated_at = UpdatedAt,
created_at = CreatedAt
}}
catch throw:_ ->
error
end.
parse_config(<<"mqtt">>, #{<<"host">> := Host, <<"port">> := Port, <<"client_id">> := ClientId, <<"username">> := Username, <<"password">> := Password, <<"topic">> := Topic, <<"qos">> := Qos}) ->
#mqtt_endpoint{
host = Host,
port = Port,
client_id = ClientId,
username = Username,
password = Password,
topic = Topic,
qos = Qos
};
parse_config(<<"http">>, #{<<"url">> := Url, <<"pool_size">> := PoolSize}) ->
#http_endpoint{
url = Url,
pool_size = PoolSize
};
parse_config(<<"kafka">>, #{<<"sasl_config">> := #{<<"username">> := Username, <<"password">> := Password, <<"mechanism">> := Mechanism0}, <<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
Mechanism = case Mechanism0 of
<<"sha_256">> ->
scram_sha_256;
<<"sha_512">> ->
scram_sha_512;
<<"plain">> ->
plain;
_ ->
plain
end,
#kafka_endpoint{
sasl_config = {Mechanism, Username, Password},
bootstrap_servers = parse_bootstrap_servers(BootstrapServers),
topic = Topic
};
parse_config(<<"kafka">>, #{<<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
#kafka_endpoint{
sasl_config = undefined,
bootstrap_servers = parse_bootstrap_servers(BootstrapServers),
topic = Topic
};
parse_config(_, _) ->
throw(invalid_config).
parse_bootstrap_servers(BootstrapServers) when is_list(BootstrapServers) ->
lists:filtermap(fun(S) ->
case binary:split(S, <<":">>) of
[Host0, Port0] ->
{true, {binary_to_list(Host0), binary_to_integer(Port0)}};
_ ->
false
end
end, BootstrapServers).

View File

@ -1,25 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 16. 5 2023 12:48
%%%-------------------------------------------------------------------
-module(event_logs_bo).
-author("aresei").
-include("iot.hrl").
-export([insert/3]).
%% API
-spec insert(EventType :: integer(), AssocUUID :: binary(), Status :: integer()) ->
{ok, InsertId :: integer()} | {error, Reason :: any()}.
insert(EventType, AssocUUID, Status) when is_integer(EventType), is_binary(AssocUUID), is_integer(Status) ->
mysql_pool:insert(mysql_iot, <<"event_logs">>, #{
<<"event_type">> => EventType,
<<"assoc_uuid">> => AssocUUID,
<<"status">> => Status,
<<"created_at">> => calendar:local_time()
}, true).

View File

@ -1,49 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 16. 5 2023 12:48
%%%-------------------------------------------------------------------
-module(host_bo).
-author("aresei").
-include("iot.hrl").
%% API
-export([get_all_hosts/0, change_status/2, get_host_by_uuid/1, get_host_by_id/1]).
-spec get_all_hosts() -> UUIDList :: [binary()].
get_all_hosts() ->
case mysql_pool:get_all(mysql_iot, <<"SELECT uuid FROM host where uuid != '' limit 10">>) of
{ok, Hosts} ->
lists:map(fun(#{<<"uuid">> := UUID}) -> UUID end, Hosts);
{error, _} ->
[]
end.
-spec get_host_by_uuid(UUID :: binary()) -> undefined | {ok, HostInfo :: map()}.
get_host_by_uuid(UUID) when is_binary(UUID) ->
mysql_pool:get_row(mysql_iot, <<"SELECT * FROM host WHERE uuid = ? LIMIT 1">>, [UUID]).
-spec get_host_by_id(HostId :: integer()) -> undefined | {ok, HostInfo :: map()}.
get_host_by_id(HostId) when is_integer(HostId) ->
mysql_pool:get_row(mysql_iot, <<"SELECT * FROM host WHERE id = ? LIMIT 1">>, [HostId]).
%%
-spec change_status(UUID :: binary(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
change_status(UUID, NStatus) when is_binary(UUID), is_integer(NStatus) ->
change_status0(UUID, NStatus).
change_status0(UUID, ?HOST_ONLINE) when is_binary(UUID) ->
Timestamp = calendar:local_time(),
case mysql_pool:get_row(mysql_iot, <<"SELECT status FROM host WHERE uuid = ? LIMIT 1">>, [UUID]) of
%%
{ok, #{<<"status">> := -1}} ->
mysql_pool:update_by(mysql_iot, <<"UPDATE host SET status = ?, access_at = ?, updated_at = ? WHERE uuid = ? LIMIT 1">>, [?HOST_ONLINE, Timestamp, Timestamp, UUID]);
{ok, _} ->
mysql_pool:update_by(mysql_iot, <<"UPDATE host SET status = ?, updated_at = ? WHERE uuid = ? LIMIT 1">>, [?HOST_ONLINE, Timestamp, UUID]);
undefined ->
{error, <<"host not found">>}
end;
change_status0(UUID, ?HOST_OFFLINE) when is_binary(UUID) ->
mysql_pool:update_by(mysql_iot, <<"UPDATE host SET status = ? WHERE uuid = ? LIMIT 1">>, [?HOST_OFFLINE, UUID]).

View File

@ -1,17 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 16. 5 2023 12:48
%%%-------------------------------------------------------------------
-module(micro_inform_log).
-author("aresei").
-include("iot.hrl").
%% API
-export([insert/1]).
insert(Fields) when is_map(Fields) ->
mysql_pool:insert(mysql_iot, <<"micro_inform_log">>, Fields, true).

View File

@ -1,23 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 16. 5 2023 12:48
%%%-------------------------------------------------------------------
-module(micro_service_bo).
-author("aresei").
-export([get_service_config/1]).
%% API
%% TODO
-spec get_service_config(ServiceId :: binary()) -> {ok, ConfigJson :: binary()} | error.
get_service_config(ServiceId) when is_binary(ServiceId) ->
case mysql_pool:get_row(mysql_iot, <<"SELECT * FROM micro_service WHERE id = ? LIMIT 1">>, [ServiceId]) of
undefined ->
error;
{ok, #{<<"config">> := Config}} ->
{ok, Config}
end.

View File

@ -1,19 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 16. 5 2023 12:48
%%%-------------------------------------------------------------------
-module(micro_set_bo).
-author("aresei").
-include("iot.hrl").
%% API
-export([change_status/4]).
%%
-spec change_status(HostId :: integer(), SceneId :: integer(), MircoId :: integer(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
change_status(HostId, SceneId, MircoId, Status) when is_integer(HostId), is_integer(SceneId), is_integer(MircoId), is_integer(Status) ->
mysql_pool:update_by(mysql_iot, <<"UPDATE micro_set SET status = ? WHERE host_id = ? AND scene_id = ? AND micro_id = ? LIMIT 1">>, [Status, HostId, SceneId, MircoId]).

View File

@ -1,19 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 16. 5 2023 12:48
%%%-------------------------------------------------------------------
-module(task_logs_bo).
-author("aresei").
-include("iot.hrl").
%% API
-export([change_status/2]).
%%
-spec change_status(TaskId :: integer(), Status :: integer()) -> {ok, AffectedRow :: integer()} | {error, Reason :: any()}.
change_status(TaskId, Status) when is_integer(TaskId), is_integer(Status) ->
mysql_pool:update_by(mysql_iot, <<"UPDATE task_logs SET status = ? WHERE id = ? LIMIT 1">>, [Status, TaskId]).

View File

@ -13,25 +13,23 @@
%% API
-export([start_link/1]).
-export([get_name/1, get_pid/1, forward/3, reload/2, clean_up/1]).
-export([get_alias_pid/1]).
-export([get_alias_pid/1, is_support/1, get_protocol/1]).
-export([endpoint_record/1]).
%%%===================================================================
%%% API
%%%===================================================================
-spec start_link(Endpoint :: #endpoint{}) -> {'ok', pid()} | 'ignore' | {'error', term()}.
start_link(Endpoint = #endpoint{id = Id, name = Name, config = #http_endpoint{}}) ->
start_link(Endpoint = #endpoint{id = Id, config = #http_endpoint{}}) ->
LocalName = get_name(Id),
AliasName = get_alias_name(Name),
endpoint_http:start_link(LocalName, AliasName, Endpoint);
start_link(Endpoint = #endpoint{id = Id, name = Name, config = #mqtt_endpoint{}}) ->
endpoint_http:start_link(LocalName, Endpoint);
start_link(Endpoint = #endpoint{id = Id, config = #mqtt_endpoint{}}) ->
LocalName = get_name(Id),
AliasName = get_alias_name(Name),
endpoint_mqtt:start_link(LocalName, AliasName, Endpoint);
start_link(Endpoint = #endpoint{id = Id, name = Name, config = #kafka_endpoint{}}) ->
endpoint_mqtt:start_link(LocalName, Endpoint);
start_link(Endpoint = #endpoint{id = Id, config = #kafka_endpoint{}}) ->
LocalName = get_name(Id),
AliasName = get_alias_name(Name),
endpoint_kafka:start_link(LocalName, AliasName, Endpoint).
endpoint_kafka:start_link(LocalName, Endpoint).
-spec get_name(Id :: integer()) -> atom().
get_name(Id) when is_integer(Id) ->
@ -58,4 +56,88 @@ reload(Pid, NEndpoint = #endpoint{}) when is_pid(Pid) ->
-spec clean_up(Pid :: pid()) -> ok.
clean_up(Pid) when is_pid(Pid) ->
gen_server:call(Pid, clean_up, 5000).
gen_server:call(Pid, clean_up, 5000).
-spec get_protocol(Endpoint :: #endpoint{}) -> atom().
get_protocol(#endpoint{config = #http_endpoint{}}) ->
http;
get_protocol(#endpoint{config = #mqtt_endpoint{}}) ->
mqtt;
get_protocol(#endpoint{config = #kafka_endpoint{}}) ->
kafka.
-spec is_support(Protocol :: atom()) -> boolean().
is_support(Protocol) when is_atom(Protocol) ->
{ok, Props} = application:get_env(iot, endpoints),
SupportProtocols = proplists:get_value(support_protocols, Props, []),
lists:member(Protocol, SupportProtocols).
-spec endpoint_record(EndpointInfo :: #{}) -> error | {ok, Endpoint :: #endpoint{}}.
endpoint_record(#{<<"id">> := Id, <<"matcher">> := Matcher, <<"title">> := Title, <<"type">> := Type, <<"config">> := ConfigJson,
<<"status">> := Status, <<"updated_at">> := UpdatedAt, <<"created_at">> := CreatedAt}) ->
try
Config = parse_config(Type, ConfigJson),
{ok, #endpoint {
id = Id,
matcher = Matcher,
title = Title,
config = Config,
status = Status,
updated_at = UpdatedAt,
created_at = CreatedAt
}}
catch throw:_ ->
error
end.
parse_config(<<"mqtt">>, #{<<"host">> := Host, <<"port">> := Port0, <<"client_id">> := ClientId, <<"username">> := Username, <<"password">> := Password, <<"topic">> := Topic, <<"qos">> := Qos}) ->
Port = if is_binary(Port0) -> binary_to_integer(Port0); is_integer(Port0) -> Port0 end,
#mqtt_endpoint{
host = Host,
port = Port,
client_id = ClientId,
username = Username,
password = Password,
topic = Topic,
qos = Qos
};
parse_config(<<"http">>, #{<<"url">> := Url, <<"pool_size">> := PoolSize}) ->
#http_endpoint{
url = Url,
pool_size = PoolSize
};
parse_config(<<"kafka">>, #{<<"sasl_config">> := #{<<"username">> := Username, <<"password">> := Password, <<"mechanism">> := Mechanism0}, <<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
Mechanism = case Mechanism0 of
<<"sha_256">> ->
scram_sha_256;
<<"sha_512">> ->
scram_sha_512;
<<"plain">> ->
plain;
_ ->
plain
end,
#kafka_endpoint{
sasl_config = {Mechanism, Username, Password},
bootstrap_servers = parse_bootstrap_servers(BootstrapServers),
topic = Topic
};
parse_config(<<"kafka">>, #{<<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
#kafka_endpoint{
sasl_config = undefined,
bootstrap_servers = parse_bootstrap_servers(BootstrapServers),
topic = Topic
};
parse_config(_, _) ->
throw(invalid_config).
parse_bootstrap_servers(BootstrapServers) when is_list(BootstrapServers) ->
lists:filtermap(fun(S) ->
case binary:split(S, <<":">>) of
[Host0, Port0] ->
{true, {binary_to_list(Host0), binary_to_integer(Port0)}};
_ ->
false
end
end, BootstrapServers).

View File

@ -13,7 +13,7 @@
-behaviour(gen_server).
%% API
-export([start_link/3]).
-export([start_link/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
@ -30,10 +30,10 @@
%%%===================================================================
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link(LocalName :: atom(), AliasName :: atom(), Endpoint :: #endpoint{}) ->
-spec(start_link(LocalName :: atom(), Endpoint :: #endpoint{}) ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(LocalName, AliasName, Endpoint = #endpoint{config = #http_endpoint{}}) when is_atom(LocalName), is_atom(AliasName) ->
gen_server:start_link({local, LocalName}, ?MODULE, [AliasName, Endpoint], []).
start_link(LocalName, Endpoint = #endpoint{config = #http_endpoint{}}) when is_atom(LocalName) ->
gen_server:start_link({local, LocalName}, ?MODULE, [Endpoint], []).
%%%===================================================================
%%% gen_server callbacks
@ -44,9 +44,9 @@ start_link(LocalName, AliasName, Endpoint = #endpoint{config = #http_endpoint{}}
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([AliasName, Endpoint]) ->
init([Endpoint = #endpoint{matcher = Matcher}]) ->
endpoint_subscription:subscribe(Matcher, self()),
Buffer = endpoint_buffer:new(Endpoint, 10),
true = gproc:reg({n, l, AliasName}),
{ok, #state{endpoint = Endpoint, buffer = Buffer}}.
%% @private

View File

@ -13,7 +13,7 @@
-behaviour(gen_server).
%% API
-export([start_link/3]).
-export([start_link/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
@ -39,8 +39,8 @@
%% @doc Creates a gen_statem process which calls Module:init/1 to
%% initialize. To ensure a synchronized start-up procedure, this
%% function does not return until Module:init/1 has returned.
start_link(LocalName, AliasName, Endpoint = #endpoint{}) when is_atom(LocalName), is_atom(AliasName) ->
gen_server:start_link({local, LocalName}, ?MODULE, [AliasName, Endpoint], []).
start_link(LocalName, Endpoint = #endpoint{}) when is_atom(LocalName) ->
gen_server:start_link({local, LocalName}, ?MODULE, [Endpoint], []).
%%%===================================================================
%%% gen_statem callbacks
@ -50,10 +50,10 @@ start_link(LocalName, AliasName, Endpoint = #endpoint{}) when is_atom(LocalName)
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
%% gen_statem:start_link/[3,4], this function is called by the new
%% process to initialize.
init([AliasName, Endpoint = #endpoint{id = Id}]) ->
erlang:process_flag(trap_exit, true),
true = gproc:reg({n, l, AliasName}),
init([Endpoint = #endpoint{id = Id, matcher = Matcher}]) ->
endpoint_subscription:subscribe(Matcher, self()),
erlang:process_flag(trap_exit, true),
%% ,
erlang:start_timer(0, self(), connect),
%%
@ -108,13 +108,19 @@ handle_info({timeout, _, connect}, State = #state{buffer = Buffer, status = ?DIS
BaseConfig
end,
case brod:start_link_client(BootstrapServers, ClientId, ClientConfig) of
case catch brod:start_link_client(BootstrapServers, ClientId, ClientConfig) of
{ok, ClientPid} ->
ok = brod:start_producer(ClientId, Topic, _ProducerConfig = []),
NBuffer = endpoint_buffer:trigger_next(Buffer),
{noreply, State#state{buffer = NBuffer, client_pid = ClientPid, status = ?CONNECTED}};
{error, Reason} ->
lager:debug("[endpoint_kafka] start_client: ~p, get error: ~p", [ClientId, Reason]),
case brod:start_producer(ClientId, Topic, _ProducerConfig = []) of
ok ->
NBuffer = endpoint_buffer:trigger_next(Buffer),
{noreply, State#state{buffer = NBuffer, client_pid = ClientPid, status = ?CONNECTED}};
{error, Reason} ->
lager:debug("[endpoint_kafka] start_producer: ~p, get error: ~p", [ClientId, Reason]),
retry_connect(),
{noreply, State#state{status = ?DISCONNECTED, client_pid = undefined}}
end;
Error ->
lager:debug("[endpoint_kafka] start_client: ~p, get error: ~p", [ClientId, Error]),
retry_connect(),
{noreply, State#state{status = ?DISCONNECTED, client_pid = undefined}}
end;
@ -174,12 +180,4 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
%%%===================================================================
retry_connect() ->
erlang:start_timer(?RETRY_INTERVAL, self(), connect).
check_produce_result(ok) ->
true;
check_produce_result({ok, _}) ->
true;
check_produce_result({ok, _}) ->
false.
erlang:start_timer(?RETRY_INTERVAL, self(), connect).

View File

@ -13,7 +13,7 @@
-behaviour(gen_server).
%% API
-export([start_link/3]).
-export([start_link/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
@ -41,8 +41,8 @@
%% @doc Creates a gen_statem process which calls Module:init/1 to
%% initialize. To ensure a synchronized start-up procedure, this
%% function does not return until Module:init/1 has returned.
start_link(LocalName, AliasName, Endpoint = #endpoint{}) when is_atom(LocalName), is_atom(AliasName) ->
gen_server:start_link({local, LocalName}, ?MODULE, [AliasName, Endpoint], []).
start_link(LocalName, Endpoint = #endpoint{}) when is_atom(LocalName) ->
gen_server:start_link({local, LocalName}, ?MODULE, [Endpoint], []).
%%%===================================================================
%%% gen_statem callbacks
@ -52,9 +52,10 @@ start_link(LocalName, AliasName, Endpoint = #endpoint{}) when is_atom(LocalName)
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
%% gen_statem:start_link/[3,4], this function is called by the new
%% process to initialize.
init([AliasName, Endpoint]) ->
erlang:process_flag(trap_exit, true),
true = gproc:reg({n, l, AliasName}),
init([Endpoint = #endpoint{matcher = Matcher}]) ->
% erlang:process_flag(trap_exit, true),
endpoint_subscription:subscribe(Matcher, self()),
%% ,
erlang:start_timer(0, self(), create_postman),
%%
@ -94,7 +95,7 @@ handle_cast({forward, ServiceId, Metric}, State = #state{buffer = Buffer}) ->
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({timeout, _, create_postman}, State = #state{buffer = Buffer, status = ?DISCONNECTED,
endpoint = #endpoint{title = Title, config = #mqtt_endpoint{host = Host, port = Port, username = Username, password = Password, client_id = ClientId}}}) ->
lager:debug("[endpoint_mqtt] endpoint: ~p, create postman", [Title]),
lager:debug("[endpoint_mqtt] endpoint: ~ts, create postman", [Title]),
Opts = [
{owner, self()},
{clientid, ClientId},
@ -112,7 +113,7 @@ handle_info({timeout, _, create_postman}, State = #state{buffer = Buffer, status
{ok, ConnPid} = emqtt:start_link(Opts),
lager:debug("[endpoint_mqtt] start connect, options: ~p", [Opts]),
case emqtt:connect(ConnPid, 5000) of
case catch emqtt:connect(ConnPid, 5000) of
{ok, _} ->
lager:debug("[endpoint_mqtt] connect success, pid: ~p", [ConnPid]),
NBuffer = endpoint_buffer:trigger_n(Buffer),
@ -120,6 +121,10 @@ handle_info({timeout, _, create_postman}, State = #state{buffer = Buffer, status
{error, Reason} ->
lager:warning("[endpoint_mqtt] connect get error: ~p", [Reason]),
erlang:start_timer(5000, self(), create_postman),
{noreply, State};
Error ->
lager:warning("[endpoint_mqtt] connect get error: ~p", [Error]),
erlang:start_timer(5000, self(), create_postman),
{noreply, State}
end;

View File

@ -0,0 +1,200 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 07. 11 2025 16:27
%%%-------------------------------------------------------------------
-module(endpoint_subscription).
-author("anlicheng").
-behaviour(gen_server).
%% API
-export([start_link/0]).
-export([subscribe/2, publish/3]).
-export([match_components/2, is_valid_components/1, of_components/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
%%
-record(subscriber, {
topic :: binary(),
subscriber_pid :: pid(),
components = [],
%%
%% 1. topic优先级别最高
%% 2. *
%% 3. +
order :: integer()
}).
-record(state, {
subscribers = []
}).
%%%===================================================================
%%% API
%%%===================================================================
-spec subscribe(Topic :: binary(), SubscriberPid :: pid()) -> ok | {error, Reason :: binary()}.
subscribe(Topic, SubscriberPid) when is_binary(Topic), is_pid(SubscriberPid) ->
gen_server:call(?SERVER, {subscribe, Topic, SubscriberPid}).
-spec publish(RouteKey :: binary(), ServiceId :: binary(), Content :: binary()) -> no_return().
publish(RouteKey, ServiceId, Content) when is_binary(RouteKey), is_binary(Content) ->
gen_server:cast(?SERVER, {publish, RouteKey, ServiceId, Content}).
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([]) ->
{ok, #state{}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
%% SubscriberPid只能订阅同一个topic一次
handle_call({subscribe, Topic, SubscriberPid}, _From, State = #state{subscribers = Subscribers}) ->
Components = of_components(Topic),
case is_valid_components(Components) of
true ->
Sub = #subscriber{topic = Topic, subscriber_pid = SubscriberPid, components = Components, order = order_num(Components)},
%% SubscriberPid的monitor退
erlang:monitor(process, SubscriberPid),
{reply, ok, State#state{subscribers = Subscribers ++ [Sub]}};
false ->
{reply, {error, <<"invalid topic name">>}, State}
end.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
%%
handle_cast({publish, RouteKey, ServiceId, Metric}, State = #state{subscribers = Subscribers}) ->
MatchedSubscribers = match_subscribers(Subscribers, RouteKey),
lists:foreach(fun(#subscriber{subscriber_pid = SubscriberPid}) ->
endpoint:forward(SubscriberPid, ServiceId, Metric)
end, MatchedSubscribers),
lager:debug("[efka_subscription] route_key: ~p, metric: ~p, match subscribers: ~p", [RouteKey, Metric, MatchedSubscribers]),
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({'DOWN', _Ref, process, SubscriberPid, Reason}, State = #state{subscribers = Subscribers}) ->
lager:debug("[efka_subscription] subscriber: ~p, down with reason: ~p", [SubscriberPid, Reason]),
NSubscribers = lists:filter(fun(#subscriber{subscriber_pid = Pid0}) -> SubscriberPid /= Pid0 end, Subscribers),
{noreply, State#state{subscribers = NSubscribers}};
handle_info(Info, State = #state{}) ->
lager:debug("[efka_subscription] get unknown info: ~p", [Info]),
{noreply, State}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
%%
-spec match_subscribers(Subscribers :: [#subscriber{}], Topic :: binary()) -> [#subscriber{}].
match_subscribers(Subscribers, Topic) when is_list(Subscribers), is_binary(Topic) ->
Components = of_components(Topic),
lists:foldl(fun(S = #subscriber{components = Components0, subscriber_pid = Pid0}, Acc) ->
case match_components(Components0, Components) andalso not contain_channel(Pid0, Acc) of
true ->
[S|Acc];
false ->
Acc
end
end, [], Subscribers).
-spec contain_channel(Pid :: pid(), Subscribers :: list()) -> boolean().
contain_channel(Pid, Subscribers) when is_pid(Pid), is_list(Subscribers) ->
lists:search(fun(#subscriber{subscriber_pid = Pid0}) -> Pid == Pid0 end, Subscribers) /= false.
%% topic和发布的topic的Components信息
%% *++
-spec match_components(list(), list()) -> boolean().
match_components(A, B) when is_list(A), is_list(B) ->
match_components(A, B, false).
match_components([<<"+">>], [_|_], _) ->
true;
match_components([], [], _) ->
true;
match_components([<<"*">>|T0], [_|T1], _) ->
match_components(T0, T1, false);
match_components([C0|T0], [C0|T1], _) ->
match_components(T0, T1, false);
match_components(_, _, _) ->
false.
-spec of_components(Topic :: binary()) -> [binary()].
of_components(Topic) when is_binary(Topic) ->
binary:split(Topic, <<$/>>, [global]).
is_valid_components([]) ->
true;
is_valid_components([<<$+>>|T]) ->
length(T) =:= 0;
is_valid_components([<<$*>>|T]) ->
is_valid_components(T);
is_valid_components([_|T]) ->
is_valid_components(T).
-spec order_num(Components :: list()) -> integer().
order_num([]) ->
1;
order_num([<<$*>>|_]) ->
2;
order_num([<<$+>>|_]) ->
3;
order_num([_|Tail]) ->
order_num(Tail).

View File

@ -12,7 +12,6 @@
-export([ensured_endpoint_started/1, delete_endpoint/1]).
-export([init/1]).
-export([kafka_test/0]).
-define(SERVER, ?MODULE).
@ -30,59 +29,22 @@ start_link() ->
%% modules => modules()} % optional
init([]) ->
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
Endpoints = endpoint_bo:get_all_endpoints(),
ChildSpecs = lists:flatmap(fun(EndpointInfo) ->
case endpoint_bo:endpoint_record(EndpointInfo) of
Endpoints = iot_api:get_all_endpoints(),
ChildSpecs = lists:filtermap(fun(EndpointInfo) ->
case endpoint:endpoint_record(EndpointInfo) of
error ->
[];
false;
{ok, Endpoint} ->
[Endpoint]
case endpoint:is_support(endpoint:get_protocol(Endpoint)) of
true ->
{true, child_spec(Endpoint)};
false ->
false
end
end
end, Endpoints),
end, Endpoints),
{ok, {SupFlags, ChildSpecs}}.
%% internal functions
kafka_test() ->
Endpoint = #endpoint{
id = 1,
%%
name = <<"kafka_test">>,
%%
title = <<"kafka测试"/utf8>>,
%% , : #{<<"protocol">> => <<"http|https|ws|kafka|mqtt">>, <<"args">> => #{}}
config = #kafka_endpoint{
%sasl_config = {
% scram_sha_256,
% <<"admin">>,
% <<"lz4rP5UavRTiGZEZK8G51mxHcM5iPC">>
%},
sasl_config = undefined,
bootstrap_servers = [
{"127.0.0.1", 19092}
],
topic = <<"metric">>
},
status = 0,
updated_at = 0,
created_at = 0
},
{ok, Pid} = ensured_endpoint_started(Endpoint),
ServiceId = <<"service_id_123">>,
Metric = <<"this is a test">>,
endpoint:forward(Pid, ServiceId, Metric),
endpoint:forward(Pid, ServiceId, Metric),
endpoint:forward(Pid, ServiceId, Metric),
endpoint:forward(Pid, ServiceId, Metric),
endpoint:forward(Pid, ServiceId, Metric),
endpoint:forward(Pid, ServiceId, Metric),
endpoint:forward(Pid, ServiceId, Metric),
endpoint:forward(Pid, ServiceId, Metric),
endpoint:forward(Pid, ServiceId, Metric),
endpoint:forward(Pid, ServiceId, Metric).
-spec ensured_endpoint_started(Endpoint :: #endpoint{}) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
ensured_endpoint_started(Endpoint = #endpoint{}) ->
case supervisor:start_child(?MODULE, child_spec(Endpoint)) of

View File

@ -0,0 +1,49 @@
%%%-------------------------------------------------------------------
%% @doc endpoint top level supervisor.
%% @end
%%%-------------------------------------------------------------------
-module(endpoint_sup_sup).
-behaviour(supervisor).
-include("endpoint.hrl").
-export([start_link/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
%% sup_flags() = #{strategy => strategy(), % optional
%% intensity => non_neg_integer(), % optional
%% period => pos_integer()} % optional
%% child_spec() = #{id => child_id(), % mandatory
%% start => mfargs(), % mandatory
%% restart => restart(), % optional
%% shutdown => shutdown(), % optional
%% type => worker(), % optional
%% modules => modules()} % optional
init([]) ->
SupFlags = #{strategy => one_for_all, intensity => 1000, period => 3600},
ChildSpecs = [
#{
id => endpoint_subscription,
start => {'endpoint_subscription', start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => ['endpoint_subscription']
},
#{
id => 'endpoint_sup',
start => {'endpoint_sup', start_link, []},
restart => permanent,
shutdown => 2000,
type => supervisor,
modules => ['endpoint_sup']
}
],
{ok, {SupFlags, ChildSpecs}}.

View File

@ -0,0 +1,309 @@
%%%-------------------------------------------------------------------
%%% @author licheng5
%%% @copyright (C) 2020, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 26. 4 2020 3:36
%%%-------------------------------------------------------------------
-module(container_handler).
-author("licheng5").
-include("iot.hrl").
-define(REQ_TIMEOUT, 10000).
%% API
-export([handle_request/4]).
handle_request("GET", "/container/get_all", #{<<"uuid">> := UUID}, _) when is_binary(UUID) ->
%% ConfigJson是否是合法的json字符串
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(-1, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:get_containers(Pid) of
{ok, Ref} ->
case iot_host:await_reply(Ref, ?REQ_TIMEOUT) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(-1, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(-1, Reason)}
end
end;
%% config.json,
handle_request("POST", "/container/push_config", _,
#{<<"uuid">> := UUID, <<"container_name">> := ContainerName, <<"config">> := Config, <<"timeout">> := Timeout0})
when is_binary(UUID), is_binary(ContainerName), is_binary(Config), is_integer(Timeout0) ->
%% ConfigJson是否是合法的json字符串
true = iot_util:is_json(Config),
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(-1, <<"host not found">>)};
Pid when is_pid(Pid) ->
Timeout = Timeout0 * 1000,
case iot_host:config_container(Pid, ContainerName, Config) of
{ok, Ref} ->
case iot_host:await_reply(Ref, Timeout) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(-1, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(-1, Reason)}
end
end;
%%
handle_request("POST", "/container/deploy", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"config">> := Config})
when is_binary(UUID), is_integer(TaskId), is_map(Config) ->
case validate_config(Config) of
ok ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:deploy_container(Pid, TaskId, Config) of
{ok, Ref} ->
case iot_host:await_reply(Ref, ?REQ_TIMEOUT) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
{error, Errors} ->
Reason = iolist_to_binary(lists:join(<<"|||">>, Errors)),
{ok, 200, iot_util:json_error(400, Reason)}
end;
%%
handle_request("POST", "/container/start", _, #{<<"uuid">> := UUID, <<"container_name">> := ContainerName}) when is_binary(UUID), is_binary(ContainerName) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:start_container(Pid, ContainerName) of
{ok, Ref} ->
case iot_host:await_reply(Ref, ?REQ_TIMEOUT) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
%%
handle_request("POST", "/container/stop", _, #{<<"uuid">> := UUID, <<"container_name">> := ContainerName}) when is_binary(UUID), is_binary(ContainerName) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:stop_container(Pid, ContainerName) of
{ok, Ref} ->
case iot_host:await_reply(Ref, ?REQ_TIMEOUT) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
handle_request("POST", "/container/kill", _, #{<<"uuid">> := UUID, <<"container_name">> := ContainerName}) when is_binary(UUID), is_binary(ContainerName) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:kill_container(Pid, ContainerName) of
{ok, Ref} ->
case iot_host:await_reply(Ref, ?REQ_TIMEOUT) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
%%
handle_request("POST", "/container/remove", _, #{<<"uuid">> := UUID, <<"container_name">> := ContainerName}) when is_binary(UUID), is_binary(ContainerName) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:remove_container(Pid, ContainerName) of
{ok, Ref} ->
case iot_host:await_reply(Ref, ?REQ_TIMEOUT) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
handle_request(_, Path, _, _) ->
Path1 = list_to_binary(Path),
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% helper methods
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
validate_config(Config) when is_map(Config) ->
%%
Required = [
{<<"image">>, binary},
{<<"container_name">>, binary},
{<<"command">>, {list, binary}},
{<<"restart">>, binary}
],
%%
Optional = [
{<<"privileged">>, boolean},
{<<"envs">>, {list, binary}},
{<<"ports">>, {list, binary}},
{<<"expose">>, {list, binary}},
{<<"volumes">>, {list, binary}},
{<<"networks">>, {list, binary}},
{<<"labels">>, {map, {binary, binary}}},
{<<"user">>, binary},
{<<"working_dir">>, binary},
{<<"hostname">>, binary},
{<<"cap_add">>, {list, binary}},
{<<"cap_drop">>, {list, binary}},
{<<"devices">>, {list, binary}},
{<<"mem_limit">>, binary},
{<<"mem_reservation">>, binary},
{<<"cpu_shares">>, integer},
{<<"cpus">>, number},
{<<"ulimits">>, {map, {binary, binary}}},
{<<"sysctls">>, {map, {binary, binary}}},
{<<"tmpfs">>, {list, binary}},
{<<"extra_hosts">>, {list, binary}},
{<<"healthcheck">>, {map, {binary, any}}}
],
Errors1 = check_required(Config, Required),
Errors2 = check_optional(Config, Optional),
Errors = Errors1 ++ Errors2,
case Errors of
[] ->
ok;
_ ->
{error, lists:map(fun erlang:iolist_to_binary/1, Errors)}
end.
%%------------------------------------------------------------------------------
%%
%%------------------------------------------------------------------------------
check_required(Config, Fields) ->
lists:foldl(
fun({Key, Type}, ErrAcc) ->
case maps:get(Key, Config, undefined) of
undefined ->
[io_lib:format("miss requied parameter: ~p", [Key]) | ErrAcc];
Value ->
case check_type(Value, Type) of
true ->
ErrAcc;
false ->
[io_lib:format("required parameter: ~p, type must be: ~p", [Key, type_name(Type)]) | ErrAcc]
end
end
end,
[], Fields).
%%------------------------------------------------------------------------------
%%
%%------------------------------------------------------------------------------
check_optional(Config, Fields) ->
lists:foldl(
fun({Key, Type}, ErrAcc) ->
case maps:get(Key, Config, undefined) of
undefined ->
ErrAcc;
Value ->
case check_type(Value, Type) of
true ->
ErrAcc;
false ->
[io_lib:format("optional parameter: ~p, type must be: ~p", [Key, type_name(Type)]) | ErrAcc]
end
end
end,
[], Fields).
%%------------------------------------------------------------------------------
%% binary版
%%------------------------------------------------------------------------------
-spec type_name(tuple() | atom()) -> binary().
type_name(binary) ->
<<"string">>;
type_name(integer) ->
<<"integer">>;
type_name(number) ->
<<"number">>;
type_name(list) ->
<<"list">>;
type_name({list, binary}) ->
<<"list of string">>;
type_name({list, number}) ->
<<"list of number">>;
type_name({list, integer}) ->
<<"list of integer">>;
type_name(map) ->
<<"map">>;
type_name({map, {binary, binary}}) ->
<<"map of string:string">>;
type_name({map, {binary, any}}) ->
<<"map of string:any">>;
type_name(boolean) ->
<<"boolean">>.
-spec check_type(Value :: any(), any()) -> boolean().
check_type(Value, binary) ->
is_binary(Value);
check_type(Value, integer) ->
is_integer(Value);
check_type(Value, number) ->
is_number(Value);
check_type(Value, list) when is_list(Value) ->
true;
check_type(Value, {list, binary}) when is_list(Value) ->
lists:all(fun(E) -> is_binary(E) end, Value);
check_type(Value, {list, number}) when is_list(Value) ->
lists:all(fun(E) -> is_number(E) end, Value);
check_type(Value, {list, integer}) when is_list(Value) ->
lists:all(fun(E) -> is_integer(E) end, Value);
check_type(Value, map) when is_map(Value) ->
true;
check_type(Value, {map, {binary, binary}}) when is_map(Value) ->
lists:all(fun({K, V}) -> is_binary(K) andalso is_binary(V) end, maps:to_list(Value));
check_type(Value, {map, {binary, any}}) when is_map(Value) ->
lists:all(fun({K, _}) -> is_binary(K) end, maps:to_list(Value));
check_type(Value, boolean) ->
is_boolean(Value);
check_type(_, _) ->
false.

View File

@ -45,24 +45,6 @@ handle_request("POST", "/device/delete", _, #{<<"host_id">> := HostId, <<"device
{ok, 200, iot_util:json_data(<<"success">>)}
end;
%%
handle_request("POST", "/device/activate", _, #{<<"host_id">> := HostId, <<"device_uuid">> := DeviceUUID, <<"auth">> := Auth})
when is_integer(HostId), is_binary(DeviceUUID), is_boolean(Auth) ->
AliasName = iot_host:get_alias_name(HostId),
case global:whereis_name(AliasName) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"activate device failed">>)};
HostPid when is_pid(HostPid) ->
case iot_host:activate_device(HostPid, DeviceUUID, Auth) of
ok ->
{ok, 200, iot_util:json_data(<<"success">>)};
{error, Reason} ->
lager:debug("[device_handler] activate device: ~p, get error: ~p", [DeviceUUID, Reason]),
{ok, 200, iot_util:json_error(404, <<"activate device failed">>)}
end
end;
handle_request(_, Path, _, _) ->
Path1 = list_to_binary(Path),
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.

View File

@ -30,17 +30,17 @@ handle_request("POST", "/endpoint/run_statuses", _, Ids) when is_list(Ids) ->
{ok, 200, iot_util:json_data(Statuses)};
handle_request("POST", "/endpoint/start", _, #{<<"id">> := Id}) when is_integer(Id) ->
case endpoint_bo:get_endpoint(Id) of
case iot_api:get_endpoint(Id) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
{ok, EndpointInfo} ->
case endpoint_bo:endpoint_record(EndpointInfo) of
{ok, Endpoint = #endpoint{name = Name}} ->
case endpoint:endpoint_record(EndpointInfo) of
{ok, Endpoint = #endpoint{title = Title}} ->
case endpoint_sup:ensured_endpoint_started(Endpoint) of
{ok, Pid} when is_pid(Pid) ->
{ok, 200, iot_util:json_data(<<"success">>)};
{error, Reason} ->
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Name, Reason]),
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Title, Reason]),
{ok, 200, iot_util:json_error(404, <<"start endpoint error">>)}
end;
error ->
@ -49,7 +49,7 @@ handle_request("POST", "/endpoint/start", _, #{<<"id">> := Id}) when is_integer(
end;
handle_request("POST", "/endpoint/stop", _, #{<<"id">> := Id}) when is_integer(Id) ->
case endpoint_bo:get_endpoint(Id) of
case iot_api:get_endpoint(Id) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
{ok, _} ->
@ -63,19 +63,19 @@ handle_request("POST", "/endpoint/stop", _, #{<<"id">> := Id}) when is_integer(I
end;
handle_request("POST", "/endpoint/restart", _, #{<<"id">> := Id}) when is_integer(Id) ->
case endpoint_bo:get_endpoint(Id) of
case iot_api:get_endpoint(Id) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
{ok, EndpointInfo} ->
case endpoint_bo:endpoint_record(EndpointInfo) of
{ok, Endpoint = #endpoint{name = Name}} ->
case endpoint:endpoint_record(EndpointInfo) of
{ok, Endpoint = #endpoint{title = Title}} ->
case endpoint:get_pid(Id) of
undefined ->
case endpoint_sup:ensured_endpoint_started(Endpoint) of
{ok, Pid} when is_pid(Pid) ->
{ok, 200, iot_util:json_data(<<"success">>)};
{error, Reason} ->
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Name, Reason]),
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Title, Reason]),
{ok, 200, iot_util:json_error(404, <<"restart endpoint error">>)}
end;
Pid ->
@ -85,11 +85,11 @@ handle_request("POST", "/endpoint/restart", _, #{<<"id">> := Id}) when is_intege
{ok, Pid} when is_pid(Pid) ->
{ok, 200, iot_util:json_data(<<"success">>)};
{error, Reason} ->
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Name, Reason]),
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Title, Reason]),
{ok, 200, iot_util:json_error(404, <<"restart endpoint error">>)}
end;
{error, Reason} ->
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Name, Reason]),
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Title, Reason]),
{ok, 200, iot_util:json_error(404, <<"stop endpoint error">>)}
end
end;

View File

@ -0,0 +1,47 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 08. 5 2025 13:00
%%%-------------------------------------------------------------------
-module(event_stream_handler).
-author("anlicheng").
%% API
-export([init/2]).
init(Req0, Opts) ->
Method = binary_to_list(cowboy_req:method(Req0)),
Path = binary_to_list(cowboy_req:path(Req0)),
GetParams0 = cowboy_req:parse_qs(Req0),
GetParams = maps:from_list(GetParams0),
#{<<"task_id">> := TaskId0} = GetParams,
TaskId = binary_to_integer(TaskId0),
lager:debug("method: ~p, path: ~p, get: ~p", [Method, Path, GetParams]),
Req1 = cowboy_req:stream_reply(200, #{
<<"Content-Type">> => <<"text/event-stream">>,
<<"Cache-Control">> => <<"no-cache">>,
<<"Connection">> => <<"keep-alive">>
}, Req0),
ok = iot_event_stream_observer:add_listener(self(), TaskId),
receiver_events(TaskId, Req1),
{ok, Req1, Opts}.
receiver_events(TaskId, Req) ->
receive
{stream_data, TaskId, Type, Stream} ->
Data = jiffy:encode(#{<<"type">> => Type, <<"stream">> => Stream}, [force_utf8]),
Body = iolist_to_binary([<<"event: message\n">>, <<"data: ", Data/binary, "\n">>, <<"\n">>]),
ok = cowboy_req:stream_body(Body, nofin, Req),
receiver_events(TaskId, Req);
{stream_close, TaskId, Reason} ->
CloseFrame = iolist_to_binary([<<"event: close\n">>, <<"data: ", Reason/binary, "\n">>, <<"\n">>]),
ok = cowboy_req:stream_body(CloseFrame, fin, Req)
end.

View File

@ -42,20 +42,6 @@ handle_request("GET", "/host/status", #{<<"uuid">> := UUID}, _) when is_binary(U
{ok, 200, iot_util:json_data(StatusInfo)}
end;
%%
handle_request("POST", "/host/reload", _, #{<<"uuid">> := UUID}) when is_binary(UUID) ->
lager:debug("[host_handler] will reload host uuid: ~p", [UUID]),
case iot_host_sup:ensured_host_started(UUID) of
{ok, Pid} when is_pid(Pid) ->
{ok, #{<<"authorize_status">> := AuthorizeStatus}} = host_bo:get_host_by_uuid(UUID),
ok = iot_host:activate(Pid, AuthorizeStatus =:= 1),
lager:debug("[host_handler] already_started reload host uuid: ~p, success", [UUID]),
{ok, 200, iot_util:json_data(<<"success">>)};
Error ->
lager:debug("[host_handler] reload host uuid: ~p, error: ~p", [UUID, Error]),
{ok, 200, iot_util:json_error(404, <<"reload error">>)}
end;
%%
handle_request("POST", "/host/delete", _, #{<<"uuid">> := UUID}) when is_binary(UUID) ->
case iot_host_sup:delete_host(UUID) of

View File

@ -6,39 +6,12 @@
%%% @end
%%% Created : 08. 5 2025 13:00
%%%-------------------------------------------------------------------
-module(http_server).
-module(http_protocol).
-author("anlicheng").
%% API
-export([start/0]).
-export([init/2]).
%% http服务
start() ->
{ok, Props} = application:get_env(iot, http_server),
Acceptors = proplists:get_value(acceptors, Props, 50),
MaxConnections = proplists:get_value(max_connections, Props, 10240),
Backlog = proplists:get_value(backlog, Props, 1024),
Port = proplists:get_value(port, Props),
Dispatcher = cowboy_router:compile([
{'_', [
{"/host/[...]", ?MODULE, [host_handler]},
{"/service/[...]", ?MODULE, [service_handler]},
{"/device/[...]", ?MODULE, [device_handler]}
]}
]),
TransOpts = [
{port, Port},
{num_acceptors, Acceptors},
{backlog, Backlog},
{max_connections, MaxConnections}
],
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
lager:debug("[http_server] the http server start at: ~p, pid is: ~p", [Port, Pid]).
init(Req0, Opts = [Mod|_]) ->
Method = binary_to_list(cowboy_req:method(Req0)),
Path = binary_to_list(cowboy_req:path(Req0)),

View File

@ -1,146 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author licheng5
%%% @copyright (C) 2020, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 26. 4 2020 3:36
%%%-------------------------------------------------------------------
-module(service_handler).
-author("licheng5").
-include("iot.hrl").
%% API
-export([handle_request/4]).
%% config.json,
handle_request("POST", "/service/push_config", _,
#{<<"uuid">> := UUID, <<"service_id">> := ServiceId, <<"config_json">> := ConfigJson, <<"timeout">> := Timeout0})
when is_binary(UUID), is_binary(ServiceId), is_binary(ConfigJson), is_integer(Timeout0) ->
%% ConfigJson是否是合法的json字符串
true = iot_util:is_json(ConfigJson),
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(-1, <<"host not found">>)};
Pid when is_pid(Pid) ->
Timeout = Timeout0 * 1000,
case iot_host:async_service_config(Pid, ServiceId, ConfigJson, Timeout) of
{ok, Ref} ->
case iot_host:await_reply(Ref, Timeout) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(-1, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(-1, Reason)}
end
end;
%%
handle_request("POST", "/service/deploy", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"service_id">> := ServiceId, <<"tar_url">> := TarUrl})
when is_binary(UUID), is_integer(TaskId), is_binary(ServiceId), is_binary(TarUrl) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:deploy_service(Pid, TaskId, ServiceId, TarUrl) of
{ok, Ref} ->
case iot_host:await_reply(Ref, 5000) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
%%
handle_request("POST", "/service/start", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId}) when is_binary(UUID), is_binary(ServiceId) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:start_service(Pid, ServiceId) of
{ok, Ref} ->
case iot_host:await_reply(Ref, 5000) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
%%
handle_request("POST", "/service/stop", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId}) when is_binary(UUID), is_binary(ServiceId) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:stop_service(Pid, ServiceId) of
{ok, Ref} ->
case iot_host:await_reply(Ref, 5000) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
%% , json
handle_request("POST", "/service/invoke", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId, <<"payload">> := Payload, <<"timeout">> := Timeout0})
when is_binary(UUID), is_binary(ServiceId), is_binary(Payload), is_integer(Timeout0) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
Timeout = Timeout0 * 1000,
case iot_host:invoke_service(Pid, ServiceId, Payload, Timeout) of
{ok, Ref} ->
case iot_host:await_reply(Ref, Timeout) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
handle_request("POST", "/service/task_log", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId}) when is_binary(UUID), is_integer(TaskId) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:task_log(Pid, TaskId) of
{ok, Ref} ->
case iot_host:await_reply(Ref, 5000) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
handle_request(_, Path, _, _) ->
Path1 = list_to_binary(Path),
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% helper methods
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

View File

@ -19,7 +19,6 @@
mysql,
gproc,
% gpb,
esockd,
mnesia,
crypto,
public_key,

View File

@ -9,130 +9,191 @@
-module(iot_api).
-author("anlicheng").
-behaviour(gen_server).
%% API
-export([start_link/0]).
-export([ai_event/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(API_TOKEN, <<"wv6fGyBhl*7@AsD9">>).
-record(state, {
}).
-export([get_all_hosts/0, get_host_by_id/1, get_host_by_uuid/1, change_host_status/2]).
-export([get_host_devices/1, get_device_by_uuid/1, change_device_status/2]).
-export([get_all_endpoints/0, get_endpoint/1]).
%%%===================================================================
%%% API
%%%===================================================================
-spec get_all_hosts() -> [HostUUID :: binary()].
get_all_hosts() ->
case do_get("/get_all_hosts", []) of
{ok, Ids} ->
Ids;
_ ->
[]
end.
-spec get_host_by_uuid(UUID :: binary()) -> undefined | {ok, HostInfo :: map()}.
get_host_by_uuid(UUID) when is_binary(UUID) ->
case do_get("/get_host_by_uuid", [{<<"uuid">>, UUID}]) of
{ok, HostInfo} ->
{ok, HostInfo};
_ ->
undefined
end.
-spec get_host_by_id(HostId :: integer()) -> undefined | {ok, HostInfo :: map()}.
get_host_by_id(HostId) when is_integer(HostId) ->
case do_get("/get_host_by_id", [{<<"host_id">>, integer_to_binary(HostId)}]) of
{ok, HostInfo} ->
{ok, HostInfo};
_ ->
undefined
end.
%%
-spec change_host_status(UUID :: binary(), Status :: integer()) -> {ok, Result :: any()} | {error, Reason :: any()}.
change_host_status(UUID, NStatus) when is_binary(UUID), is_integer(NStatus) ->
do_post("/change_host_status", #{<<"uuid">> => UUID, <<"new_status">> => NStatus}).
-spec get_host_devices(HostId :: integer()) -> {ok, Devices :: [map()]} | {error, Reason::any()}.
get_host_devices(HostId) when is_integer(HostId) ->
do_get("/get_host_devices", [{<<"host_id">>, integer_to_binary(HostId)}]).
-spec get_device_by_uuid(DeviceUUID :: binary()) -> {ok, DeviceInfo :: map()} | undefined.
get_device_by_uuid(DeviceUUID) when is_binary(DeviceUUID) ->
case do_get("/get_device_by_uuid", [{<<"device_uuid">>, DeviceUUID}]) of
{ok, DeviceInfo} ->
{ok, DeviceInfo};
_ ->
undefined
end.
%%
-spec change_device_status(DeviceUUID :: binary(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
change_device_status(DeviceUUID, NStatus) when is_binary(DeviceUUID), is_integer(NStatus) ->
do_post("/change_device_status", #{<<"device_uuid">> => DeviceUUID, <<"new_status">> => NStatus}).
%%%-------------------------------------------------------------------
%% endpoint相关的api
%%%-------------------------------------------------------------------
%% API
-spec get_all_endpoints() -> [Endpoint :: map()].
get_all_endpoints() ->
case do_get("/get_all_endpoints", []) of
{ok, Endpoints} ->
Endpoints;
_ ->
[]
end.
-spec get_endpoint(Id :: integer()) -> undefined | {ok, EndpointInfo :: map()}.
get_endpoint(Id) when is_integer(Id) ->
case do_get("/get_endpoint", [{<<"id">>, integer_to_binary(Id)}]) of
{ok, EndpointInfo} when is_map(EndpointInfo) ->
{ok, EndpointInfo};
_ ->
undefined
end.
ai_event(Id) when is_integer(Id) ->
gen_server:cast(?MODULE, {ai_event, Id}).
Token = iot_util:md5(<<?API_TOKEN/binary, (integer_to_binary(Id))/binary, ?API_TOKEN/binary>>),
{ok, Url} = application:get_env(iot, api_url),
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
Headers = [
{<<"content-type">>, <<"application/json">>}
],
ReqData = #{
<<"token">> => Token,
<<"id">> => Id
},
Body = iolist_to_binary(jiffy:encode(ReqData, [force_utf8])),
case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
{ok, 200, _, ClientRef} ->
{ok, RespBody} = hackney:body(ClientRef),
lager:debug("[iot_api] send body: ~p, get error is: ~p", [Body, RespBody]),
hackney:close(ClientRef);
{ok, HttpCode, _, ClientRef} ->
{ok, RespBody} = hackney:body(ClientRef),
hackney:close(ClientRef),
lager:warning("[iot_api] send body: ~p, get error is: ~p", [Body, {HttpCode, RespBody}]);
{error, Reason} ->
lager:warning("[iot_api] send body: ~p, get error is: ~p", [Body, Reason])
end.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%%%-------------------------------------------------------------------
%% helper methods
%%%-------------------------------------------------------------------
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([]) ->
{ok, #state{}}.
-spec do_post(Path :: string(), Params :: map()) -> {ok, Resp :: any()} | {error, Reason :: any()}.
do_post(Path, Params) when is_list(Path), is_map(Params) ->
{ok, BaseUrl} = application:get_env(iot, api_url),
Headers = [
{<<"content-type">>, <<"application/json">>},
{<<"Accept">>, <<"application/json">>}
],
Url = BaseUrl ++ Path,
Body = iolist_to_binary(jiffy:encode(Params, [force_utf8])),
case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
{ok, 200, _, ClientRef} ->
{ok, RespBody} = hackney:body(ClientRef),
lager:debug("[iot_api] request url: ~p, send body: ~p, get error is: ~p", [Url, Body, RespBody]),
hackney:close(ClientRef),
case catch jiffy:decode(RespBody, [return_maps]) of
#{<<"result">> := Result} ->
{ok, Result};
#{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}} ->
{error, {Code, Message}};
{error, Reason} ->
{error, Reason};
Other ->
{error, Other}
end;
{ok, HttpCode, _, ClientRef} ->
{ok, RespBody} = hackney:body(ClientRef),
hackney:close(ClientRef),
lager:warning("[iot_api] request url: ~p, send body: ~p, get error is: ~p", [Url, Body, {HttpCode, RespBody}]),
{error, {HttpCode, RespBody}};
{error, Reason} ->
lager:warning("[iot_api] request url: ~p, send body: ~p, get error is: ~p", [Url, Body, Reason]),
{error, Reason}
end.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call(_Request, _From, State = #state{}) ->
{reply, ok, State}.
-spec do_get(Path :: string(), Params :: [{Key :: binary(), Val :: binary()}]) -> {ok, Resp :: any()} | {error, Reason :: any()}.
do_get(Path, Params) when is_list(Path), is_list(Params) ->
{ok, BaseUrl} = application:get_env(iot, api_url),
Headers = [
{<<"Accept">>, <<"application/json">>}
],
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast({ai_event, Id}, State = #state{}) ->
spawn_monitor(fun() ->
Token = iot_util:md5(<<?API_TOKEN/binary, (integer_to_binary(Id))/binary, ?API_TOKEN/binary>>),
{ok, Url} = application:get_env(iot, api_url),
Url = case length(Params) > 0 of
true ->
QS = binary_to_list(uri_string:compose_query(Params)),
BaseUrl ++ Path ++ "?" ++ QS;
false ->
BaseUrl ++ Path
end,
Headers = [
{<<"content-type">>, <<"application/json">>}
],
ReqData = #{
<<"token">> => Token,
<<"id">> => Id
},
Body = iolist_to_binary(jiffy:encode(ReqData, [force_utf8])),
case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
{ok, 200, _, ClientRef} ->
{ok, RespBody} = hackney:body(ClientRef),
lager:debug("[iot_api] send body: ~p, get error is: ~p", [Body, RespBody]),
hackney:close(ClientRef);
{ok, HttpCode, _, ClientRef} ->
{ok, RespBody} = hackney:body(ClientRef),
hackney:close(ClientRef),
lager:warning("[iot_api] send body: ~p, get error is: ~p", [Body, {HttpCode, RespBody}]);
{error, Reason} ->
lager:warning("[iot_api] send body: ~p, get error is: ~p", [Body, Reason])
end
end),
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
%% Task进程挂掉
handle_info({'DOWN', _MRef, process, _Pid, normal}, State) ->
{noreply, State};
handle_info({'DOWN', _MRef, process, _Pid, Reason}, State) ->
lager:notice("[iot_api] task process down with reason: ~p", [Reason]),
{noreply, State};
handle_info(_Info, State = #state{}) ->
{noreply, State}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) ->
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
case hackney:request(get, Url, Headers, <<>>, [{pool, false}]) of
{ok, 200, _, ClientRef} ->
{ok, RespBody} = hackney:body(ClientRef),
hackney:close(ClientRef),
lager:debug("[iot_api] url: ~p, get response is: ~p", [Url, RespBody]),
case catch jiffy:decode(RespBody, [return_maps]) of
#{<<"result">> := Result} ->
{ok, Result};
#{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}} ->
{error, {Code, Message}};
{error, Reason} ->
{error, Reason};
Other ->
{error, Other}
end;
{ok, HttpCode, _, ClientRef} ->
{ok, RespBody} = hackney:body(ClientRef),
hackney:close(ClientRef),
lager:warning("[iot_api] request url: ~p, get error is: ~p", [Url, {HttpCode, RespBody}]),
{error, {HttpCode, RespBody}};
{error, Reason} ->
lager:warning("[iot_api] request url: ~p, get error is: ~p", [Url, Reason]),
{error, Reason}
end.

View File

@ -16,10 +16,10 @@ start(_StartType, _StartArgs) ->
start_mnesia(),
%% http服务
http_server:start(),
start_http_server(),
%% tcp服务
tcp_server:start(),
start_tcp_server(),
iot_sup:start_link().
@ -38,6 +38,56 @@ start_mnesia() ->
%%
ok.
start_http_server() ->
{ok, Props} = application:get_env(iot, http_server),
Acceptors = proplists:get_value(acceptors, Props, 50),
MaxConnections = proplists:get_value(max_connections, Props, 10240),
Backlog = proplists:get_value(backlog, Props, 1024),
Port = proplists:get_value(port, Props),
Dispatcher = cowboy_router:compile([
{'_', [
{"/host/[...]", http_protocol, [host_handler]},
{"/container/[...]", http_protocol, [container_handler]},
{"/device/[...]", http_protocol, [device_handler]},
{"/event_stream", event_stream_handler, []}
]}
]),
TransOpts = #{
max_connections => MaxConnections,
num_acceptors => Acceptors,
shutdown => brutal_kill,
socket_opts => [
{backlog, Backlog},
{port, Port}
]
},
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
lager:debug("[http_server] the http server start at: ~p, pid is: ~p", [Port, Pid]).
%% tcp服务
start_tcp_server() ->
{ok, Props} = application:get_env(iot, tcp_server),
Acceptors = proplists:get_value(acceptors, Props, 50),
MaxConnections = proplists:get_value(max_connections, Props, 10240),
Backlog = proplists:get_value(backlog, Props, 1024),
Port = proplists:get_value(port, Props),
TransOpts = #{
max_connections => MaxConnections,
num_acceptors => Acceptors,
shutdown => brutal_kill,
socket_opts => [
{nodelay, false},
{backlog, Backlog},
{port, Port}
]
},
{ok, _} = ranch:start_listener(tcp_server, ranch_tcp, TransOpts, tcp_channel, []),
lager:debug("[iot_app] the tcp server start at: ~p", [Port]).
-spec ensure_mnesia_schema() -> any().
ensure_mnesia_schema() ->
case mnesia:system_info(use_dir) of

View File

@ -10,7 +10,7 @@
-include("iot.hrl").
%% API
-export([new/1, is_activated/1, change_status/2, reload/1, auth/2]).
-export([new/1, change_status/2, reload/1]).
%%
-define(DEVICE_AUTH_DENIED, 0).
@ -22,7 +22,6 @@
-record(device, {
device_uuid :: binary(),
auth_state = ?STATE_DENIED,
status = ?DEVICE_OFFLINE
}).
@ -32,29 +31,24 @@
-spec new(DeviceInfo :: binary() | map()) -> error | {ok, Device :: #device{}}.
new(DeviceUUID) when is_binary(DeviceUUID) ->
case device_bo:get_device_by_uuid(DeviceUUID) of
{ok, #{<<"device_uuid">> := DeviceUUID, <<"authorize_status">> := AuthorizeStatus, <<"status">> := Status}} ->
{ok, #device{device_uuid = DeviceUUID, status = Status, auth_state = auth_state(AuthorizeStatus)}};
case iot_api:get_device_by_uuid(DeviceUUID) of
{ok, #{<<"device_uuid">> := DeviceUUID, <<"status">> := Status}} ->
{ok, #device{device_uuid = DeviceUUID, status = Status}};
undefined ->
lager:warning("[iot_device] device uuid: ~p, loaded from mysql failed", [DeviceUUID]),
error
end;
new(#{<<"device_uuid">> := DeviceUUID, <<"authorize_status">> := AuthorizeStatus, <<"status">> := Status}) ->
{ok, #device{device_uuid = DeviceUUID, status = Status, auth_state = auth_state(AuthorizeStatus)}}.
-spec is_activated(Device :: #device{}) -> boolean().
is_activated(#device{auth_state = AuthState}) ->
AuthState =:= ?STATE_ACTIVATED.
new(#{<<"device_uuid">> := DeviceUUID, <<"status">> := Status}) ->
{ok, #device{device_uuid = DeviceUUID, status = Status}}.
-spec change_status(Device :: #device{}, NewStatus :: integer()) -> NDevice :: #device{}.
change_status(Device = #device{status = Status}, NewStatus) when is_integer(NewStatus), Status =:= NewStatus ->
Device;
change_status(Device = #device{device_uuid = DeviceUUID}, ?DEVICE_ONLINE) ->
{ok, _} = device_bo:change_status(DeviceUUID, ?DEVICE_ONLINE),
report_event(DeviceUUID, ?DEVICE_ONLINE),
iot_api:change_device_status(DeviceUUID, ?DEVICE_ONLINE),
Device#device{status = ?DEVICE_ONLINE};
change_status(Device = #device{device_uuid = DeviceUUID}, ?DEVICE_OFFLINE) ->
{ok, #{<<"status">> := Status}} = device_bo:get_device_by_uuid(DeviceUUID),
{ok, #{<<"status">> := Status}} = iot_api:get_device_by_uuid(DeviceUUID),
case Status of
?DEVICE_NOT_JOINED ->
lager:debug("[iot_device] device: ~p, device_maybe_offline, not joined, can not change to offline", [DeviceUUID]),
@ -63,64 +57,17 @@ change_status(Device = #device{device_uuid = DeviceUUID}, ?DEVICE_OFFLINE) ->
lager:debug("[iot_device] device: ~p, device_maybe_offline, is offline, do nothing", [DeviceUUID]),
Device#device{status = ?DEVICE_OFFLINE};
?DEVICE_ONLINE ->
{ok, _} = device_bo:change_status(DeviceUUID, ?DEVICE_OFFLINE),
report_event(DeviceUUID, ?DEVICE_OFFLINE),
iot_api:change_device_status(DeviceUUID, ?DEVICE_OFFLINE),
Device#device{status = ?DEVICE_OFFLINE}
end.
-spec reload(Device :: #device{}) -> error | {ok, NDevice :: #device{}}.
reload(Device = #device{device_uuid = DeviceUUID}) ->
lager:debug("[iot_device] will reload: ~p", [DeviceUUID]),
case device_bo:get_device_by_uuid(DeviceUUID) of
{ok, #{<<"authorize_status">> := AuthorizeStatus, <<"status">> := Status}} ->
{ok, Device#device{device_uuid = DeviceUUID, status = Status, auth_state = auth_state(AuthorizeStatus)}};
case iot_api:get_device_by_uuid(DeviceUUID) of
{ok, #{<<"status">> := Status}} ->
{ok, Device#device{device_uuid = DeviceUUID, status = Status}};
undefined ->
lager:warning("[iot_device] device uuid: ~p, loaded from mysql failed", [DeviceUUID]),
error
end.
-spec auth(Device :: #device{}, Auth :: boolean()) -> NDevice :: #device{}.
auth(Device = #device{auth_state = StateName, device_uuid = DeviceUUID}, Auth) when is_boolean(Auth) ->
case {StateName, Auth} of
{?STATE_DENIED, false} ->
lager:debug("[iot_device] device_uuid: ~p, auth: false, will keep state_name: ~p", [DeviceUUID, ?STATE_DENIED]),
Device;
{?STATE_DENIED, true} ->
Device#device{auth_state = ?STATE_ACTIVATED};
{?STATE_ACTIVATED, false} ->
lager:debug("[iot_device] device_uuid: ~p, auth: false, state_name from: ~p, to: ~p", [DeviceUUID, ?STATE_ACTIVATED, ?STATE_DENIED]),
Device#device{auth_state = ?STATE_DENIED};
{?STATE_ACTIVATED, true} ->
lager:debug("[iot_device] device_uuid: ~p, auth: true, will keep state_name: ~p", [DeviceUUID, ?STATE_ACTIVATED]),
Device
end.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec auth_state(integer()) -> atom().
auth_state(?DEVICE_AUTH_AUTHED) ->
?STATE_ACTIVATED;
auth_state(?DEVICE_AUTH_DENIED) ->
?STATE_DENIED.
-spec report_event(DeviceUUID :: binary(), NewStatus :: integer()) -> no_return().
report_event(DeviceUUID, NewStatus) when is_binary(DeviceUUID), is_integer(NewStatus) ->
TextMap = #{
0 => <<"离线"/utf8>>,
1 => <<"在线"/utf8>>
},
%%
Timestamp = iot_util:timestamp_of_seconds(),
FieldsList = [#{
<<"key">> => <<"device_status">>,
<<"value">> => NewStatus,
<<"value_text">> => maps:get(NewStatus, TextMap),
<<"unit">> => 0,
<<"type">> => <<"DI">>,
<<"name">> => <<"设备状态"/utf8>>,
<<"timestamp">> => Timestamp
}],
iot_router:route_uuid(DeviceUUID, FieldsList, Timestamp),
lager:debug("[iot_device] device_uuid: ~p, route fields: ~p", [DeviceUUID, FieldsList]).
end.

View File

@ -4,50 +4,41 @@
%%% @doc
%%%
%%% @end
%%% Created : 17. 8 2025 00:26
%%% Created : 26. 9 2025 12:19
%%%-------------------------------------------------------------------
-module(iot_name_server).
-module(iot_event_stream_observer).
-author("anlicheng").
-behaviour(gen_server).
%% API
-export([start_link/0]).
-export([whereis_alias/1, register/2]).
-export([add_listener/2, stream_data/3, stream_close/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(TAB, iot_name_server).
-record(state, {
%% #{Pid => Name}
pid_names = #{},
refs = []
listeners = #{}
}).
%%%===================================================================
%%% API
%%%===================================================================
-spec register(Name :: atom(), Pid :: pid()) -> ok.
register(Name, Pid) when is_atom(Name), is_pid(Pid) ->
gen_server:call(?SERVER, {register, Name, Pid}).
-spec add_listener(ListenerPid :: pid(), TaskId :: integer()) -> ok.
add_listener(ListenerPid, TaskId) when is_pid(ListenerPid), is_integer(TaskId) ->
gen_server:call(?SERVER, {add_listener, ListenerPid, TaskId}).
-spec whereis_alias(Name :: atom()) -> undefined | pid().
whereis_alias(Name) when is_atom(Name) ->
case ets:lookup(?TAB, Name) of
[] ->
undefined;
[{Name, Pid}|_] ->
case is_process_alive(Pid) of
true ->
Pid;
false ->
undefined
end
end.
-spec stream_data(TaskId :: integer(), Type :: binary(), Stream :: binary()) -> no_return().
stream_data(TaskId, Type, Stream) when is_integer(TaskId), is_binary(Type), is_binary(Stream) ->
gen_server:cast(?SERVER, {stream_data, TaskId, Type, Stream}).
-spec stream_close(TaskId :: integer(), Reason :: binary()) -> no_return().
stream_close(TaskId, Reason) when is_integer(TaskId), is_binary(Reason) ->
gen_server:cast(?SERVER, {stream_close, TaskId, Reason}).
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
@ -65,8 +56,6 @@ start_link() ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([]) ->
%%
ets:new(?TAB, [named_table, ordered_set, public, {keypos, 1}]),
{ok, #state{}}.
%% @private
@ -79,10 +68,9 @@ init([]) ->
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call({register, Name, Pid}, _From, State = #state{refs = Refs, pid_names = PidNames}) ->
true = ets:insert(?TAB, {Name, Pid}),
MRef = erlang:monitor(process, Pid),
{reply, ok, State#state{refs = [MRef|Refs], pid_names = maps:put(Pid, Name, PidNames)}}.
handle_call({add_listener, ListenerPid, TaskId}, _From, State = #state{listeners = Listeners}) ->
erlang:monitor(process, ListenerPid),
{reply, ok, State#state{listeners = maps:put(TaskId, ListenerPid, Listeners)}}.
%% @private
%% @doc Handling cast messages
@ -90,7 +78,21 @@ handle_call({register, Name, Pid}, _From, State = #state{refs = Refs, pid_names
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast(_Request, State = #state{}) ->
handle_cast({stream_data, TaskId, Type, Stream}, State = #state{listeners = Listeners}) ->
case maps:find(TaskId, Listeners) of
error ->
ok;
{ok, ListenerPid} ->
is_process_alive(ListenerPid) andalso ListenerPid ! {stream_data, TaskId, Type, Stream}
end,
{noreply, State};
handle_cast({stream_close, TaskId, Reason}, State = #state{listeners = Listeners}) ->
case maps:find(TaskId, Listeners) of
error ->
ok;
{ok, ListenerPid} ->
is_process_alive(ListenerPid) andalso ListenerPid ! {stream_close, TaskId, Reason}
end,
{noreply, State}.
%% @private
@ -99,20 +101,9 @@ handle_cast(_Request, State = #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({'DOWN', MRef, process, Pid, Reason}, State = #state{refs = Refs, pid_names = PidNames}) ->
% lager:debug("[iot_name_server] pid: ~p, down with reason: ~p", [Reason]),
case lists:member(MRef, Refs) of
true ->
case maps:take(Pid, PidNames) of
error ->
{noreply, State#state{refs = lists:delete(MRef, Refs)}};
{Name, NPidNames} ->
true = ets:delete(?TAB, Name),
{noreply, State#state{pid_names = NPidNames, refs = lists:delete(MRef, Refs)}}
end;
false ->
{noreply, State}
end.
handle_info({'DOWN', _Ref, process, Pid, _Reason}, State = #state{listeners = Listeners}) ->
NListeners = maps:filter(fun(_, ListenerPid) -> ListenerPid /= Pid end, Listeners),
{noreply, State#state{listeners = NListeners}}.
%% @private
%% @doc This function is called by a gen_server when it is about to

View File

@ -9,7 +9,7 @@
-module(iot_host).
-author("aresei").
-include("iot.hrl").
-include("message_pb.hrl").
-include("message.hrl").
-behaviour(gen_statem).
@ -25,9 +25,9 @@
-export([get_metric/1, get_status/1]).
%%
-export([pub/3, attach_channel/2, command/3]).
-export([deploy_service/4, start_service/2, stop_service/2, invoke_service/4, async_service_config/4, task_log/2, await_reply/2]).
-export([deploy_container/3, start_container/2, stop_container/2, remove_container/2, kill_container/2, config_container/3, get_containers/1, await_reply/2]).
%%
-export([reload_device/2, delete_device/2, activate_device/3]).
-export([reload_device/2, delete_device/2]).
-export([heartbeat/1]).
%% gen_statem callbacks
@ -68,7 +68,7 @@ get_alias_name(HostId0) when is_integer(HostId0) ->
binary_to_atom(<<"iot_host_id:", HostId/binary>>).
%%
-spec handle(Pid :: pid(), Packet :: {atom(), binary()} | {atom(), {binary(), binary()}}) -> no_return().
-spec handle(Pid :: pid(), Packet :: {atom(), any()}) -> no_return().
handle(Pid, Packet) when is_pid(Pid) ->
gen_statem:cast(Pid, {handle, Packet}).
@ -89,40 +89,54 @@ get_metric(Pid) when is_pid(Pid) ->
attach_channel(Pid, ChannelPid) when is_pid(Pid), is_pid(ChannelPid) ->
gen_statem:call(Pid, {attach_channel, ChannelPid}).
-spec async_service_config(Pid :: pid(), ServiceId :: binary(), ConfigJson :: binary(), Timeout :: integer()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
async_service_config(Pid, ServiceId, ConfigJson, Timeout) when is_pid(Pid), is_binary(ServiceId), is_binary(ConfigJson), is_integer(Timeout) ->
ConfigBin = message_pb:encode_msg(#push_service_config{service_id = ServiceId, config_json = ConfigJson, timeout = Timeout}),
gen_statem:call(Pid, {async_call, self(), ?PUSH_SERVICE_CONFIG, ConfigBin}).
-spec get_containers(Pid :: pid()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
get_containers(Pid) when is_pid(Pid) ->
Request = #jsonrpc_request{method = <<"get_containers">>, params = #{}},
EncConfigBin = message_codec:encode(?MESSAGE_JSONRPC_REQUEST, Request),
gen_statem:call(Pid, {jsonrpc_call, self(), EncConfigBin}).
-spec deploy_service(Pid :: pid(), TaskId :: integer(), ServiceId :: binary(), TarUrl :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
deploy_service(Pid, TaskId, ServiceId, TarUrl) when is_pid(Pid), is_integer(TaskId), is_binary(ServiceId), is_binary(TarUrl) ->
PushBin = message_pb:encode_msg(#deploy{task_id = TaskId, service_id = ServiceId, tar_url = TarUrl}),
gen_statem:call(Pid, {async_call, self(), ?PUSH_DEPLOY, PushBin}).
-spec config_container(Pid :: pid(), ContainerName :: binary(), ConfigJson :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
config_container(Pid, ContainerName, ConfigJson) when is_pid(Pid), is_binary(ContainerName), is_binary(ConfigJson) ->
Request = #jsonrpc_request{method = <<"config_container">>, params = #{<<"container_name">> => ContainerName, <<"config">> => ConfigJson}},
EncConfigBin = message_codec:encode(?MESSAGE_JSONRPC_REQUEST, Request),
gen_statem:call(Pid, {jsonrpc_call, self(), EncConfigBin}).
-spec start_service(Pid :: pid(), ServiceId :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
start_service(Pid, ServiceId) when is_pid(Pid), is_binary(ServiceId) ->
gen_statem:call(Pid, {async_call, self(), ?PUSH_START_SERVICE, ServiceId}).
-spec deploy_container(Pid :: pid(), TaskId :: integer(), Config :: map()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
deploy_container(Pid, TaskId, Config) when is_pid(Pid), is_integer(TaskId), is_map(Config) ->
Request = #jsonrpc_request{method = <<"deploy">>, params = #{<<"task_id">> => TaskId, <<"config">> => Config}},
EncDeployBin = message_codec:encode(?MESSAGE_JSONRPC_REQUEST, Request),
gen_statem:call(Pid, {jsonrpc_call, self(), EncDeployBin}).
-spec stop_service(Pid :: pid(), ServiceId :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
stop_service(Pid, ServiceId) when is_pid(Pid), is_binary(ServiceId) ->
gen_statem:call(Pid, {async_call, self(), ?PUSH_STOP_SERVICE, ServiceId}).
-spec start_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
start_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
Request = #jsonrpc_request{method = <<"start_container">>, params = #{<<"container_name">> => ContainerName}},
EncCallBin = message_codec:encode(?MESSAGE_JSONRPC_REQUEST, Request),
gen_statem:call(Pid, {jsonrpc_call, self(), EncCallBin}).
-spec invoke_service(Pid :: pid(), ServiceId :: binary(), Payload :: binary(), Timeout :: integer()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
invoke_service(Pid, ServiceId, Payload, Timeout) when is_pid(Pid), is_binary(ServiceId), is_binary(Payload), is_integer(Timeout) ->
InvokeBin = message_pb:encode_msg(#invoke{service_id = ServiceId, payload = Payload, timeout = Timeout}),
gen_statem:call(Pid, {async_call, self(), ?PUSH_INVOKE, InvokeBin}).
-spec stop_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
stop_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
Request = #jsonrpc_request{method = <<"stop_container">>, params = #{<<"container_name">> => ContainerName}},
EncCallBin = message_codec:encode(?MESSAGE_JSONRPC_REQUEST, Request),
gen_statem:call(Pid, {jsonrpc_call, self(), EncCallBin}).
-spec task_log(Pid :: pid(), TaskId :: integer()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
task_log(Pid, TaskId) when is_pid(Pid), is_integer(TaskId) ->
TaskLogBin = message_pb:encode_msg(#fetch_task_log{task_id = TaskId}),
gen_statem:call(Pid, {async_call, self(), ?PUSH_TASK_LOG, TaskLogBin}).
-spec kill_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
kill_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
Request = #jsonrpc_request{method = <<"kill_container">>, params = #{<<"container_name">> => ContainerName}},
EncCallBin = message_codec:encode(?MESSAGE_JSONRPC_REQUEST, Request),
gen_statem:call(Pid, {jsonrpc_call, self(), EncCallBin}).
-spec remove_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
remove_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
Request = #jsonrpc_request{method = <<"remove_container">>, params = #{<<"container_name">> => ContainerName}},
EncCallBin = message_codec:encode(?MESSAGE_JSONRPC_REQUEST, Request),
gen_statem:call(Pid, {jsonrpc_call, self(), EncCallBin}).
-spec await_reply(Ref :: reference(), Timeout :: integer()) -> {ok, Result :: binary()} | {error, Reason :: binary()}.
await_reply(Ref, Timeout) when is_reference(Ref), is_integer(Timeout) ->
receive
{async_call_reply, Ref, #async_call_reply{code = 1, result = Result}} ->
{jsonrpc_reply, Ref, #jsonrpc_reply{result = Result, error = undefined}} ->
{ok, Result};
{async_call_reply, Ref, #async_call_reply{code = 0, message = Message}} ->
{jsonrpc_reply, Ref, #jsonrpc_reply{result = undefined, error = #{<<"message">> := Message}}} ->
{error, Message}
after Timeout ->
{error, <<"timeout">>}
@ -145,10 +159,6 @@ reload_device(Pid, DeviceUUID) when is_pid(Pid), is_binary(DeviceUUID) ->
delete_device(Pid, DeviceUUID) when is_pid(Pid), is_binary(DeviceUUID) ->
gen_statem:call(Pid, {delete_device, DeviceUUID}).
-spec activate_device(Pid :: pid(), DeviceUUID :: binary(), Auth :: boolean()) -> ok | {error, Reason :: any()}.
activate_device(Pid, DeviceUUID, Auth) when is_pid(Pid), is_binary(DeviceUUID), is_boolean(Auth) ->
gen_statem:call(Pid, {activate_device, DeviceUUID, Auth}).
-spec heartbeat(Pid :: pid()) -> no_return().
heartbeat(undefined) ->
ok;
@ -170,7 +180,7 @@ start_link(Name, UUID) when is_atom(Name), is_binary(UUID) ->
%% gen_statem:start_link/[3,4], this function is called by the new
%% process to initialize.
init([UUID]) ->
case host_bo:get_host_by_uuid(UUID) of
case iot_api:get_host_by_uuid(UUID) of
{ok, #{<<"id">> := HostId, <<"authorize_status">> := AuthorizeStatus}} ->
%% host_id注册别名, HostPid
AliasName = get_alias_name(HostId),
@ -185,7 +195,7 @@ init([UUID]) ->
end,
%%
{ok, DeviceInfos} = device_bo:get_host_devices(HostId),
{ok, DeviceInfos} = iot_api:get_host_devices(HostId),
Devices = lists:filtermap(fun(DeviceInfo = #{<<"device_uuid">> := DeviceUUID}) ->
case iot_device:new(DeviceInfo) of
error ->
@ -227,14 +237,14 @@ handle_event({call, From}, get_status, _, State = #state{channel_pid = ChannelPi
{keep_state, State, [{reply, From, {ok, Reply}}]};
%% channel存在
handle_event({call, From}, {async_call, ReceiverPid, PushType, PushBin}, _, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
handle_event({call, From}, {jsonrpc_call, ReceiverPid, RpcCall}, _, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
case HasSession andalso is_pid(ChannelPid) of
true ->
%% websocket发送请求
Ref = tcp_channel:async_call(ChannelPid, ReceiverPid, PushType, PushBin),
Ref = tcp_channel:jsonrpc_call(ChannelPid, ReceiverPid, RpcCall),
{keep_state, State, [{reply, From, {ok, Ref}}]};
false ->
lager:debug("[iot_host] uuid: ~p, publish_type: ~p, invalid state: ~p", [UUID, PushType, state_map(State)]),
lager:debug("[iot_host] uuid: ~p, invalid state: ~p", [UUID, state_map(State)]),
{keep_state, State, [{reply, From, {error, <<"主机离线,发送请求失败"/utf8>>}}]}
end;
@ -294,9 +304,8 @@ handle_event({call, From}, {attach_channel, ChannelPid}, StateName, State = #sta
?STATE_ACTIVATED ->
erlang:monitor(process, ChannelPid),
%% 线
{ok, AffectedRow} = host_bo:change_status(UUID, ?HOST_ONLINE),
report_event(UUID, ?HOST_ONLINE),
lager:debug("[iot_host] host_id(attach_channel) uuid: ~p, will change status, affected_row: ~p", [UUID, AffectedRow]),
ChangeResult = iot_api:change_host_status(UUID, ?HOST_ONLINE),
lager:debug("[iot_host] host_id(attach_channel) uuid: ~p, will change status, result: ~p", [UUID, ChangeResult]),
{keep_state, State#state{channel_pid = ChannelPid, has_session = true}, [{reply, From, ok}]};
%%
?STATE_DENIED ->
@ -327,44 +336,20 @@ handle_event({call, From}, {reload_device, DeviceUUID}, _, State = #state{device
handle_event({call, From}, {delete_device, DeviceUUID}, _, State = #state{device_map = DeviceMap}) ->
{keep_state, State#state{device_map = maps:remove(DeviceUUID, DeviceMap)}, [{reply, From, ok}]};
%%
handle_event({call, From}, {activate_device, DeviceUUID, Auth}, _, State = #state{device_map = DeviceMap}) ->
%% todo
handle_event(cast, {handle, {data, #data{service_id = ServiceId, device_uuid = DeviceUUID, route_key = RouteKey0, metric = Metric}}}, ?STATE_ACTIVATED,
State = #state{uuid = UUID, has_session = true, device_map = DeviceMap}) ->
lager:debug("[iot_host] metric_data host: ~p, service_id: ~p, device_uuid: ~p, route_key: ~p, metric: ~p", [UUID, ServiceId, DeviceUUID, RouteKey0, Metric]),
case maps:find(DeviceUUID, DeviceMap) of
error ->
{keep_state, State, [{reply, From, {error, <<"device not found">>}}]};
lager:warning("[iot_host] host uuid: ~p, device uuid: ~p not found, metric: ~p", [UUID, DeviceUUID, Metric]),
{keep_state, State};
{ok, Device} ->
NDevice = iot_device:auth(Device, Auth),
{keep_state, State#state{device_map = maps:put(DeviceUUID, NDevice, DeviceMap)}, [{reply, From, ok}]}
end;
%% todo
handle_event(cast, {handle, {data, #data{service_id = ServiceId, device_uuid = DeviceUUID, route_key = RouteKey0, metric = Metric}}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, has_session = true, device_map = DeviceMap}) ->
lager:debug("[iot_host] metric_data host: ~p, service_id: ~p, device_uuid: ~p, route_key: ~p, metric: ~p", [UUID, ServiceId, DeviceUUID, RouteKey0, Metric]),
case DeviceUUID =/= <<"">> of
true ->
case maps:find(DeviceUUID, DeviceMap) of
error ->
lager:warning("[iot_host] host uuid: ~p, device uuid: ~p not found, metric: ~p", [UUID, DeviceUUID, Metric]),
{keep_state, State};
{ok, Device} ->
case iot_device:is_activated(Device) of
true ->
RouteKey = get_route_key(RouteKey0),
case endpoint:get_alias_pid(RouteKey) of
undefined ->
ok;
EndpointPid ->
endpoint:forward(EndpointPid, ServiceId, Metric)
end,
NDevice = iot_device:change_status(Device, ?DEVICE_ONLINE),
{keep_state, State#state{device_map = maps:put(DeviceUUID, NDevice, DeviceMap)}};
false ->
lager:warning("[iot_host] host uuid: ~p, device_uuid: ~p not activated, metric: ~p", [UUID, DeviceUUID, Metric]),
{keep_state, State}
end
end;
false ->
{keep_state, State}
RouteKey = get_route_key(RouteKey0),
endpoint_subscription:publish(RouteKey, ServiceId, Metric),
NDevice = iot_device:change_status(Device, ?DEVICE_ONLINE),
{keep_state, State#state{device_map = maps:put(DeviceUUID, NDevice, DeviceMap)}}
end;
%% ping的数据是通过aes加密后的
@ -372,10 +357,6 @@ handle_event(cast, {handle, {ping, Metrics}}, ?STATE_ACTIVATED, State = #state{u
lager:debug("[iot_host] ping host_id uuid: ~p, get ping: ~p", [UUID, Metrics]),
{keep_state, State#state{metrics = Metrics}};
handle_event(cast, {handle, {inform, #service_inform{service_id = ServiceId, status = Status, timestamp = Timestamp}}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, has_session = true}) ->
lager:debug("[iot_host] inform host: ~p, service_id: ~p, status: ~p, timestamp: ~p", [UUID, ServiceId, Status, Timestamp]),
{keep_state, State};
handle_event(cast, {handle, {event, #event{service_id = ServiceId, event_type = EventType, params = Params}}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, has_session = true}) ->
lager:debug("[iot_host] event uuid: ~p, service_id: ~p, event_type: ~p, params: ~p", [UUID, ServiceId, EventType, Params]),
%DevicePid = iot_device:get_pid(DeviceUUID),
@ -390,15 +371,14 @@ handle_event(cast, heartbeat, _, State = #state{heartbeat_counter = HeartbeatCou
%% 线,
handle_event(info, {timeout, _, heartbeat_ticker}, _, State = #state{uuid = UUID, heartbeat_counter = 0, channel_pid = ChannelPid}) ->
lager:warning("[iot_host] uuid: ~p, heartbeat lost, devices will unknown", [UUID]),
{ok, #{<<"status">> := Status}} = host_bo:get_host_by_uuid(UUID),
{ok, #{<<"status">> := Status}} = iot_api:get_host_by_uuid(UUID),
case Status of
?HOST_NOT_JOINED ->
lager:debug("[iot_host] host: ~p, host_maybe_offline, host not joined, can not change to offline", [UUID]);
?HOST_OFFLINE ->
lager:debug("[iot_host] host: ~p, host_maybe_offline, host now is offline, do nothing", [UUID]);
?HOST_ONLINE ->
{ok, _} = host_bo:change_status(UUID, ?HOST_OFFLINE),
report_event(UUID, ?HOST_OFFLINE)
iot_api:change_host_status(UUID, ?HOST_OFFLINE)
end,
%% channel
@ -443,33 +423,12 @@ code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
%%% Internal functions
%%%===================================================================
-spec get_route_key(binary()) -> binary().
get_route_key(<<"">>) ->
<<"default">>;
<<"/">>;
get_route_key(RouteKey) when is_binary(RouteKey) ->
RouteKey.
-spec report_event(UUID :: binary(), NewStatus :: integer()) -> no_return().
report_event(UUID, NewStatus) when is_binary(UUID), is_integer(NewStatus) ->
TextMap = #{
0 => <<"离线"/utf8>>,
1 => <<"在线"/utf8>>
},
%%
Timestamp = iot_util:timestamp_of_seconds(),
FieldsList = [#{
<<"key">> => <<"host_status">>,
<<"value">> => NewStatus,
<<"value_text">> => maps:get(NewStatus, TextMap),
<<"unit">> => 0,
<<"type">> => <<"DI">>,
<<"name">> => <<"主机状态"/utf8>>,
<<"timestamp">> => Timestamp
}],
%% todo
% iot_router:route_uuid(UUID, FieldsList, Timestamp),
lager:debug("[iot_host] host_uuid: ~p, route fields: ~p", [UUID, FieldsList]).
%% state转换成map
state_map(#state{host_id = HostId, uuid = UUID, has_session = HasSession, heartbeat_counter = HeartbeatCounter, channel_pid = ChannelPid, metrics = Metrics}) ->
#{

View File

@ -15,7 +15,7 @@ start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
Specs = lists:map(fun child_spec/1, host_bo:get_all_hosts()),
Specs = lists:map(fun child_spec/1, iot_api:get_all_hosts()),
{ok, {#{strategy => one_for_one, intensity => 1000, period => 3600}, Specs}}.

View File

@ -1,26 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 04. 7 2023 11:30
%%%-------------------------------------------------------------------
-module(iot_router).
-author("aresei").
-include("iot.hrl").
%% API
-export([route_uuid/3]).
-spec route_uuid(RouterUUID :: binary(), Fields :: list(), Timestamp :: integer()) -> no_return().
route_uuid(RouterUUID, Fields, Timestamp) when is_binary(RouterUUID), is_list(Fields), is_integer(Timestamp) ->
%%
case redis_client:hget(RouterUUID, <<"location_code">>) of
{ok, undefined} ->
lager:warning("[iot_host] the north_data hget location_code, uuid: ~p, not found, fields: ~p", [RouterUUID, Fields]);
{ok, LocationCode} when is_binary(LocationCode) ->
iot_zd_endpoint:forward(LocationCode, Fields, Timestamp);
{error, Reason} ->
lager:warning("[iot_host] the north_data hget location_code uuid: ~p, get error: ~p, fields: ~p", [RouterUUID, Reason, Fields])
end.

View File

@ -29,21 +29,21 @@ init([]) ->
Specs = [
#{
id => 'iot_name_server',
start => {'iot_name_server', start_link, []},
id => 'iot_event_stream_observer',
start => {'iot_event_stream_observer', start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => ['iot_name_server']
modules => ['iot_event_stream_observer']
},
#{
id => 'endpoint_sup',
start => {'endpoint_sup', start_link, []},
id => endpoint_sup_sup,
start => {'endpoint_sup_sup', start_link, []},
restart => permanent,
shutdown => 2000,
type => supervisor,
modules => ['endpoint_sup']
modules => ['endpoint_sup_sup']
},
#{
@ -61,7 +61,11 @@ init([]) ->
%% internal functions
pools() ->
{ok, Pools} = application:get_env(iot, pools),
lists:map(fun({Name, PoolArgs, WorkerArgs}) ->
poolboy:child_spec(Name, [{name, {local, Name}}|PoolArgs], WorkerArgs)
end, Pools).
case application:get_env(iot, pools) of
undefined ->
[];
{ok, Pools} ->
lists:map(fun({Name, PoolArgs, WorkerArgs}) ->
poolboy:child_spec(Name, [{name, {local, Name}}|PoolArgs], WorkerArgs)
end, Pools)
end.

View File

@ -0,0 +1,134 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 17. 9 2025 16:05
%%%-------------------------------------------------------------------
-module(message_codec).
-author("anlicheng").
-include("message.hrl").
-define(I32, 1).
-define(Bytes, 2).
%% API
-export([encode/2, decode/1]).
-spec encode(MessageType :: integer(), Message :: any()) -> binary().
encode(MessageType, Message) when is_integer(MessageType) ->
Bin = encode0(Message),
<<MessageType, Bin/binary>>.
encode0(#auth_request{uuid = UUID, username = Username, salt = Salt, token = Token, timestamp = Timestamp}) ->
iolist_to_binary([
marshal(?Bytes, UUID),
marshal(?Bytes, Username),
marshal(?Bytes, Salt),
marshal(?Bytes, Token),
marshal(?I32, Timestamp)
]);
encode0(#auth_reply{code = Code, payload = Payload}) ->
iolist_to_binary([
marshal(?I32, Code),
marshal(?Bytes, Payload)
]);
encode0(#jsonrpc_reply{result = Result, error = undefined}) ->
ResultBin = erlang:term_to_binary(#{<<"result">> => Result}),
iolist_to_binary([marshal(?Bytes, ResultBin)]);
encode0(#jsonrpc_reply{result = undefined, error = Error}) ->
ResultBin = erlang:term_to_binary(#{<<"error">> => Error}),
iolist_to_binary([marshal(?Bytes, ResultBin)]);
encode0(#pub{topic = Topic, content = Content}) ->
iolist_to_binary([
marshal(?Bytes, Topic),
marshal(?Bytes, Content)
]);
encode0(#command{command_type = CommandType, command = Command}) ->
iolist_to_binary([
marshal(?I32, CommandType),
marshal(?Bytes, Command)
]);
encode0(#jsonrpc_request{method = Method, params = Params}) ->
ReqBody = erlang:term_to_binary(#{<<"method">> => Method, <<"params">> => Params}),
marshal(?Bytes, ReqBody);
encode0(#data{service_id = ServiceId, device_uuid = DeviceUUID, route_key = RouteKey, metric = Metric}) ->
iolist_to_binary([
marshal(?Bytes, ServiceId),
marshal(?Bytes, DeviceUUID),
marshal(?Bytes, RouteKey),
marshal(?Bytes, Metric)
]);
encode0(#event{service_id = ServiceId, event_type = EventType, params = Params}) ->
iolist_to_binary([
marshal(?Bytes, ServiceId),
marshal(?I32, EventType),
marshal(?Bytes, Params)
]);
encode0(#task_event_stream{task_id = TaskId, type = Type, stream = Stream}) ->
iolist_to_binary([
marshal(?I32, TaskId),
marshal(?Bytes, Type),
marshal(?Bytes, Stream)
]).
-spec decode(Bin :: binary()) -> {ok, Message :: any()} | error.
decode(<<PacketType:8, Packet/binary>>) ->
case unmarshal(Packet) of
{ok, Fields} ->
decode0(PacketType, Fields);
error ->
error
end.
decode0(?MESSAGE_AUTH_REQUEST, [UUID, Username, Salt, Token, Timestamp]) ->
{ok, #auth_request{uuid = UUID, username = Username, salt = Salt, token = Token, timestamp = Timestamp}};
decode0(?MESSAGE_JSONRPC_REPLY, [ReplyBin]) ->
case erlang:binary_to_term(ReplyBin) of
#{<<"result">> := Result} ->
{ok, #jsonrpc_reply{result = Result}};
#{<<"error">> := Error} ->
{ok, #jsonrpc_reply{error = Error}};
_ ->
error
end;
decode0(?MESSAGE_PUB, [Topic, Content]) ->
{ok, #pub{topic = Topic, content = Content}};
decode0(?MESSAGE_COMMAND, [CommandType, Command]) ->
{ok, #command{command_type = CommandType, command = Command}};
decode0(?MESSAGE_AUTH_REPLY, [Code, Payload]) ->
{ok, #auth_reply{code = Code, payload = Payload}};
decode0(?MESSAGE_JSONRPC_REQUEST, [ReqBody]) ->
#{<<"method">> := Method, <<"params">> := Params} = erlang:binary_to_term(ReqBody),
{ok, #jsonrpc_request{method = Method, params = Params}};
decode0(?MESSAGE_DATA, [ServiceId, DeviceUUID, RouteKey, Metric]) ->
{ok, #data{service_id = ServiceId, device_uuid = DeviceUUID, route_key = RouteKey, metric = Metric}};
decode0(?MESSAGE_EVENT, [ServiceId, EventType, Params]) ->
{ok, #event{service_id = ServiceId, event_type = EventType, params = Params}};
decode0(?MESSAGE_EVENT_STREAM, [TaskId, Type, Stream]) ->
{ok, #task_event_stream{task_id = TaskId, type = Type, stream = Stream}};
decode0(_, _) ->
error.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% helper methods
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec marshal(Type :: integer(), Field :: any()) -> binary().
marshal(?I32, Field) when is_integer(Field) ->
<<?I32, Field:32>>;
marshal(?Bytes, Field) when is_binary(Field) ->
Len = byte_size(Field),
<<?Bytes, Len:16, Field/binary>>.
-spec unmarshal(Bin :: binary()) -> {ok, Components :: [any()]} | error.
unmarshal(Bin) when is_binary(Bin) ->
unmarshal(Bin, []).
unmarshal(<<>>, Acc) ->
{ok, lists:reverse(Acc)};
unmarshal(<<?I32, F:32, Rest/binary>>, Acc) ->
unmarshal(Rest, [F|Acc]);
unmarshal(<<?Bytes, Len:16, F:Len/binary, Rest/binary>>, Acc) ->
unmarshal(Rest, [F|Acc]);
unmarshal(_, _) ->
error.

View File

@ -13,7 +13,7 @@
%% API
-export([rsa_encode/1]).
-export([insert_services/1]).
-export([test_mqtt/0, test_influxdb/0]).
-export([test_influxdb/0]).
test_influxdb() ->
UUID = <<"device123123">>,
@ -29,13 +29,6 @@ test_influxdb() ->
end)
end, lists:seq(1, 100)).
test_mqtt() ->
iot_zd_endpoint:forward(<<"location_code_test123">>, [
#{<<"key">> => <<"name">>, <<"value">> => <<"anlicheng">>},
#{<<"key">> => <<"age">>, <<"value">> => 30},
#{<<"key">> => <<"flow">>, <<"value">> => 30}
], iot_util:timestamp_of_seconds()).
insert_services(Num) ->
lists:foreach(fun(Id) ->
Res = mysql_pool:insert(mysql_iot, <<"micro_service">>,

View File

@ -1,281 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%% 1.
%%% 2. host进程不能直接去监听topic线
%%% @end
%%% Created : 12. 3 2023 21:27
%%%-------------------------------------------------------------------
-module(iot_mqtt_consumer).
-author("aresei").
-include("iot.hrl").
-behaviour(gen_server).
%% API
-export([start_link/0]).
-export([mock/5]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(RETRY_INTERVAL, 5000).
%%
-define(EXECUTE_TIMEOUT, 10 * 1000).
%%
-define(Topics,[
{<<"CET/NX/download">>, 2}
]).
-record(state, {
conn_pid :: undefined | pid(),
logger_pid :: pid(),
mqtt_props :: list(),
%%
flight_num = 0
}).
%%%===================================================================
%%% API
%%%===================================================================
mock(LocationCode, Para, SType, CType, Value) when is_binary(LocationCode), is_integer(SType), is_integer(CType), is_integer(Para) ->
Req = #{
<<"version">> => <<"1.0">>,
<<"ts">> => iot_util:current_time(),
<<"properties">> => #{
<<"type">> => <<"ctrl">>,
<<"para">> => Para,
<<"stype">> => SType,
<<"ctype">> => CType,
<<"value">> => Value,
<<"timestamp">> => iot_util:current_time()
},
<<"location_code">> => LocationCode
},
gen_server:call(?MODULE, {mock, Req}).
%% @doc Spawns the server and registers the local name (unique)
-spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @private
%% @doc Initializes the server
-spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore).
init([]) ->
erlang:process_flag(trap_exit, true),
{ok, Props} = application:get_env(iot, zhongdian),
%% ,
erlang:start_timer(0, self(), create_consumer),
%%
{ok, LoggerPid} = iot_logger:start_link("zd_directive_data"),
{ok, #state{mqtt_props = Props, conn_pid = undefined, logger_pid = LoggerPid}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call({mock, Request}, _From, State = #state{conn_pid = ConnPid, flight_num = FlightNum}) when is_pid(ConnPid) ->
publish_directive(Request, jiffy:encode(Request, [force_utf8])),
{reply, ok, State#state{flight_num = FlightNum + 1}}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast(_Request, State = #state{}) ->
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({disconnect, ReasonCode, Properties}, State) ->
lager:debug("[iot_zd_consumer] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
{stop, disconnected, State};
%% json反序列需要在host进程进行
handle_info({publish, #{packet_id := _PacketId, payload := Payload, qos := 2, topic := Topic}}, State = #state{flight_num = FlightNum}) ->
lager:debug("[iot_zd_consumer] Recv a topic: ~p, publish packet: ~ts, qos: 2", [Topic, Payload]),
Request = catch jiffy:decode(Payload, [return_maps]),
publish_directive(Request, Payload),
{noreply, State#state{flight_num = FlightNum + 1}};
handle_info({publish, #{packet_id := _PacketId, payload := Payload, qos := Qos, topic := Topic}}, State) ->
lager:notice("[iot_zd_consumer] Recv a topic: ~p, publish packet: ~ts, qos: ~p, qos is error", [Topic, Payload, Qos]),
{noreply, State};
handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) ->
lager:debug("[iot_zd_consumer] receive puback packet: ~p", [Packet]),
{noreply, State};
handle_info({timeout, _, create_consumer}, State = #state{mqtt_props = Props, conn_pid = undefined}) ->
try
{ok, ConnPid} = create_consumer(Props),
{noreply, State#state{conn_pid = ConnPid}}
catch _:Error:Stack ->
lager:warning("[iot_zd_consumer] config: ~p, create consumer get error: ~p, stack: ~p", [Props, Error, Stack]),
erlang:start_timer(?RETRY_INTERVAL, self(), create_consumer),
{noreply, State#state{conn_pid = undefined}}
end;
%% postman进程挂掉时
handle_info({'EXIT', ConnPid, Reason}, State = #state{conn_pid = ConnPid}) ->
lager:warning("[iot_zd_consumer] consumer exited with reason: ~p", [Reason]),
erlang:start_timer(?RETRY_INTERVAL, self(), create_consumer),
{noreply, State#state{conn_pid = undefined}};
handle_info({'EXIT', LoggerPid, Reason}, State = #state{logger_pid = LoggerPid}) ->
lager:warning("[iot_zd_consumer] logger exited with reason: ~p", [Reason]),
{ok, LoggerPid} = iot_logger:start_link("zd_directive_data"),
{noreply, State#state{logger_pid = LoggerPid}};
handle_info({directive_reply, Reply}, State = #state{logger_pid = LoggerPid, flight_num = FlightNum}) ->
FlightInfo = <<"flight_num: ", (integer_to_binary(FlightNum - 1))/binary>>,
case Reply of
{ok, RawReq, DirectiveResult} ->
case DirectiveResult of
ok ->
iot_logger:write(LoggerPid, [<<"[success]">>, RawReq, <<"OK">>, FlightInfo]);
{ok, Response} when is_binary(Response) ->
iot_logger:write(LoggerPid, [<<"[success]">>, RawReq, Response, FlightInfo]);
{error, Reason0} ->
Reason = if
is_atom(Reason0) -> atom_to_binary(Reason0);
is_binary(Reason0) -> Reason0;
true -> <<"Unknow error">>
end,
iot_logger:write(LoggerPid, [<<"[error]">>, RawReq, Reason, FlightInfo])
end;
{error, RawReq, Error} when is_binary(Error) ->
iot_logger:write(LoggerPid, [<<"[error]">>, RawReq, Error, FlightInfo])
end,
{noreply, State#state{flight_num = FlightNum - 1}};
handle_info(Info, State = #state{}) ->
lager:notice("[iot_zd_consumer] get a unknown info: ~p", [Info]),
{noreply, State}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(Reason, _State = #state{conn_pid = ConnPid}) when is_pid(ConnPid) ->
%% topic的订阅
TopicNames = lists:map(fun({Name, _}) -> Name end, ?Topics),
{ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, TopicNames),
ok = emqtt:disconnect(ConnPid),
lager:debug("[iot_zd_consumer] terminate with reason: ~p", [Reason]),
ok;
terminate(Reason, _State) ->
lager:debug("[iot_zd_consumer] terminate with reason: ~p", [Reason]),
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
publish_directive(#{<<"version">> := Version, <<"location_code">> := LocationCode, <<"properties">> := DirectiveParams}, RawReq) ->
%% LocationCode查找到主机和Device_uuid
ReceiverPid = self(),
case redis_client:hgetall(LocationCode) of
{ok, #{<<"host_uuid">> := HostUUID, <<"device_uuid">> := DeviceUUID}} ->
case iot_host:get_pid(HostUUID) of
undefined ->
ReceiverPid ! {directive_reply, {error, RawReq, <<"host uuid: ", HostUUID/binary, " not found">>}};
Pid when is_pid(Pid) ->
ok
end;
{ok, Map} when is_map(Map) ->
RedisData = iolist_to_binary(jiffy:encode(Map, [force_utf8])),
ReceiverPid ! {directive_reply, {error, RawReq, <<"invalid redis data: ", RedisData/binary>>}};
_ ->
ReceiverPid ! {directive_reply, {error, RawReq, <<"location_code: ", LocationCode/binary, " not found in redis">>}}
end;
publish_directive(Other, RawReq) ->
lager:warning("[iot_zd_consumer] get a error message: ~p", [Other]),
self() ! {directive_reply, {error, RawReq, <<"unknown directive">>}}.
-spec create_consumer(Props :: list()) -> {ok, ConnPid :: pid()} | {error, Reason :: any()}.
create_consumer(Props) when is_list(Props) ->
Node = atom_to_binary(node()),
ClientId = <<"mqtt-client-", Node/binary, "-zhongdian_mqtt_consumer">>,
%% emqx服务器的连接
Host = proplists:get_value(host, Props),
Port = proplists:get_value(port, Props, 18080),
Username = proplists:get_value(username, Props),
Password = proplists:get_value(password, Props),
Keepalive = proplists:get_value(keepalive, Props, 86400),
Opts = [
{clientid, ClientId},
{host, Host},
{port, Port},
{owner, self()},
{tcp_opts, []},
{username, Username},
{password, Password},
{keepalive, Keepalive},
{auto_ack, true},
{connect_timeout, 5000},
{proto_ver, v5},
{retry_interval, 5000}
],
%% emqx服务器的连接
lager:debug("[iot_zd_consumer] opts is: ~p", [Opts]),
case emqtt:start_link(Opts) of
{ok, ConnPid} ->
%% host相关的全部事件
lager:debug("[iot_zd_consumer] start conntecting, pid: ~p", [ConnPid]),
{ok, _} = emqtt:connect(ConnPid),
lager:debug("[iot_zd_consumer] connect success, pid: ~p", [ConnPid]),
SubscribeResult = emqtt:subscribe(ConnPid, ?Topics),
lager:debug("[iot_zd_consumer] subscribe topics: ~p, result is: ~p", [?Topics, SubscribeResult]),
{ok, ConnPid};
ignore ->
{error, ignore};
{error, Reason} ->
{error, Reason}
end.

File diff suppressed because it is too large Load Diff

View File

@ -8,16 +8,15 @@
%%%-------------------------------------------------------------------
-module(tcp_channel).
-author("licheng5").
-include("iot.hrl").
-include("message_pb.hrl").
-include("message.hrl").
-behaviour(ranch_protocol).
%% API
-export([pub/3, async_call/4, command/3]).
-export([stop/2]).
-export([pub/3, jsonrpc_call/3, command/3]).
-export([start_link/2]).
-export([start_link/3, stop/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]).
-export([init/3, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]).
-record(state, {
transport,
@ -44,10 +43,10 @@ command(Pid, CommandType, Command) when is_pid(Pid), is_integer(CommandType), is
gen_server:cast(Pid, {command, CommandType, Command}).
%%
-spec async_call(Pid :: pid(), ReceiverPid :: pid(), CallType :: integer(), CallBin :: binary()) -> Ref :: reference().
async_call(Pid, ReceiverPid, CallType, CallBin) when is_pid(Pid), is_pid(ReceiverPid), is_integer(CallType), is_binary(CallBin) ->
-spec jsonrpc_call(Pid :: pid(), ReceiverPid :: pid(), CallBin :: binary()) -> Ref :: reference().
jsonrpc_call(Pid, ReceiverPid, CallBin) when is_pid(Pid), is_pid(ReceiverPid), is_binary(CallBin) ->
Ref = make_ref(),
gen_server:cast(Pid, {async_call, ReceiverPid, Ref, CallType, CallBin}),
gen_server:cast(Pid, {jsonrpc_call, ReceiverPid, Ref, CallBin}),
Ref.
%%
@ -61,46 +60,43 @@ stop(Pid, Reason) when is_pid(Pid) ->
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
start_link(Transport, Sock) ->
{ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock]])}.
start_link(Ref, Transport, Opts) ->
{ok, proc_lib:spawn_link(?MODULE, init, [Ref, Transport, Opts])}.
init([Transport, Sock]) ->
lager:debug("[sdlan_channel] get a new connection: ~p", [Sock]),
case Transport:wait(Sock) of
{ok, NewSock} ->
Transport:setopts(Sock, [{active, true}]),
% erlang:start_timer(?PING_TICKER, self(), ping_ticker),
gen_server:enter_loop(?MODULE, [], #state{transport = Transport, socket = NewSock});
{error, Reason} ->
{stop, Reason}
end.
init(Ref, Transport, _Opts = []) ->
{ok, Socket} = ranch:handshake(Ref),
lager:debug("[sdlan_channel] get a new connection: ~p", [Socket]),
Transport:setopts(Socket, [binary, {active, true}, {packet, 4}]),
% erlang:start_timer(?PING_TICKER, self(), ping_ticker),
gen_server:enter_loop(?MODULE, [], #state{transport = Transport, socket = Socket}).
handle_call(_Request, _From, State) ->
{reply, ok, State}.
%% , pub/sub机制
handle_cast({pub, Topic, Content}, State = #state{transport = Transport, socket = Socket}) ->
PubBin = message_pb:encode_msg(#pub{topic = Topic, content = Content}),
Transport:send(Socket, <<?PACKET_PUB, PubBin/binary>>),
EncPub = message_codec:encode(?MESSAGE_PUB, #pub{topic = Topic, content = Content}),
Transport:send(Socket, <<?PACKET_CAST, EncPub/binary>>),
{noreply, State};
%% Command消息
handle_cast({command, CommandType, Command}, State = #state{transport = Transport, socket = Socket}) ->
Transport:send(Socket, <<?PACKET_COMMAND, CommandType:8, Command/binary>>),
EncCommand = message_codec:encode(?MESSAGE_COMMAND, #command{command_type = CommandType, command = Command}),
Transport:send(Socket, <<?PACKET_CAST, EncCommand/binary>>),
{noreply, State};
%%
handle_cast({async_call, ReceiverPid, Ref, CallType, CallBin}, State = #state{transport = Transport, socket = Socket, packet_id = PacketId, inflight = Inflight}) ->
Transport:send(Socket, <<?PACKET_ASYNC_CALL, PacketId:32, CallType:8, CallBin/binary>>),
handle_cast({jsonrpc_call, ReceiverPid, Ref, CallBin}, State = #state{transport = Transport, socket = Socket, packet_id = PacketId, inflight = Inflight}) ->
Transport:send(Socket, <<?PACKET_REQUEST, PacketId:32, CallBin/binary>>),
{noreply, State#state{packet_id = PacketId + 1, inflight = maps:put(PacketId, {ReceiverPid, Ref}, Inflight)}}.
%% auth验证
handle_info({tcp, Socket, <<?PACKET_REQUEST, PacketId:32, ?METHOD_AUTH:8, AuthRequestBin/binary>>}, State = #state{transport = Transport, socket = Socket}) ->
#auth_request{ uuid = UUID, username = Username, token = Token, salt = Salt, timestamp = Timestamp } = message_pb:decode_msg(AuthRequestBin, auth_request),
handle_info({tcp, Socket, <<?PACKET_REQUEST, PacketId:32, RequestBin/binary>>}, State = #state{transport = Transport, socket = Socket}) ->
{ok, #auth_request{uuid = UUID, username = Username, token = Token, salt = Salt, timestamp = Timestamp}} = message_codec:decode(RequestBin),
lager:debug("[ws_channel] auth uuid: ~p", [UUID]),
case iot_auth:check(Username, Token, UUID, Salt, Timestamp) of
true ->
case host_bo:get_host_by_uuid(UUID) of
case iot_api:get_host_by_uuid(UUID) of
undefined ->
lager:warning("[ws_channel] uuid: ~p, user: ~p, host not found", [UUID, Username]),
{stop, State};
@ -111,20 +107,20 @@ handle_info({tcp, Socket, <<?PACKET_REQUEST, PacketId:32, ?METHOD_AUTH:8, AuthRe
ok ->
%% host的monitor
erlang:monitor(process, HostPid),
AuthReplyBin = message_pb:encode_msg(#auth_reply{code = 0, message = <<"ok">>}),
AuthReplyBin = message_codec:encode(?MESSAGE_AUTH_REPLY, #auth_reply{code = 0, payload = <<"ok">>}),
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32, AuthReplyBin/binary>>),
{noreply, State#state{uuid = UUID, host_pid = HostPid}};
{denied, Reason} when is_binary(Reason) ->
erlang:monitor(process, HostPid),
AuthReplyBin = message_pb:encode_msg(#auth_reply{code = 1, message = Reason}),
AuthReplyBin = message_codec:encode(?MESSAGE_AUTH_REPLY, #auth_reply{code = 1, payload = Reason}),
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32, AuthReplyBin/binary>>),
lager:debug("[ws_channel] uuid: ~p, attach channel get error: ~p, stop channel", [UUID, Reason]),
{noreply, State#state{uuid = UUID, host_pid = HostPid}};
{error, Reason} when is_binary(Reason) ->
AuthReplyBin = message_pb:encode_msg(#auth_reply{code = 2, message = Reason}),
AuthReplyBin = message_codec:encode(?MESSAGE_AUTH_REPLY, #auth_reply{code = 2, payload = Reason}),
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32, AuthReplyBin/binary>>),
lager:debug("[ws_channel] uuid: ~p, attach channel get error: ~p, stop channel", [UUID, Reason]),
@ -136,59 +132,42 @@ handle_info({tcp, Socket, <<?PACKET_REQUEST, PacketId:32, ?METHOD_AUTH:8, AuthRe
{stop, State}
end;
%%
handle_info({tcp, Socket, <<?PACKET_REQUEST, PacketId:32, ?METHOD_REQUEST_SERVICE_CONFIG:8, ServiceId/binary>>}, State = #state{transport = Transport, socket = Socket}) ->
lager:debug("[ws_channel] service_config request service_id: ~p", [ServiceId]),
case micro_service_bo:get_service_config(ServiceId) of
error ->
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32>>);
{ok, ConfigJson} when is_binary(ConfigJson) ->
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32, ConfigJson/binary>>)
handle_info({tcp, Socket, <<?PACKET_CAST, CastBin/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
{ok, CastMessage} = message_codec:decode(CastBin),
case CastMessage of
#data{} = Data ->
iot_host:handle(HostPid, {data, Data});
#event{} = Event ->
iot_host:handle(HostPid, {event, Event});
#task_event_stream{task_id = TaskId, type = <<"close">>, stream = Reason} ->
iot_event_stream_observer:stream_close(TaskId, Reason);
#task_event_stream{task_id = TaskId, type = Type, stream = Stream} ->
lager:debug("[tcp_channel] get task_id: ~p, type: ~ts, stream: ~ts", [TaskId, Type, Stream]),
iot_event_stream_observer:stream_data(TaskId, Type, Stream)
end,
{noreply, State};
handle_info({tcp, Socket, <<?PACKET_REQUEST, ?METHOD_DATA:8, Data0/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
Data = message_pb:decode_msg(Data0, data),
iot_host:handle(HostPid, {data, Data}),
{noreply, State};
handle_info({tcp, Socket, <<?PACKET_REQUEST, ?METHOD_PING:8, PingData/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
Ping = message_pb:decode_msg(PingData, ping),
iot_host:handle(HostPid, {ping, Ping}),
{noreply, State};
handle_info({tcp, Socket, <<?PACKET_REQUEST, ?METHOD_INFORM:8, InformData/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
ServiceInform = message_pb:decode_msg(InformData, service_inform),
iot_host:handle(HostPid, {inform, ServiceInform}),
{noreply, State};
handle_info({tcp, Socket, <<?PACKET_REQUEST, ?METHOD_EVENT:8, EventData/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
Event = message_pb:decode_msg(EventData, event),
iot_host:handle(HostPid, {event, Event}),
{noreply, State};
%handle_info({tcp, Socket, <<?PACKET_PING, PingData/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
% Ping = message_pb:decode_msg(PingData, ping),
% iot_host:handle(HostPid, {ping, Ping}),
% {noreply, State};
%%
handle_info({tcp, Socket, <<?PACKET_ASYNC_CALL_REPLY, PacketId:32, ResponseBin/binary>>}, State = #state{socket = Socket, uuid = UUID, inflight = Inflight}) when PacketId > 0 ->
AsyncCallReply = message_pb:decode_msg(ResponseBin, async_call_reply),
lager:debug("[ws_channel] uuid: ~p, get async_call_reply: ~p, packet_id: ~p", [UUID, AsyncCallReply, PacketId]),
handle_info({tcp, Socket, <<?PACKET_RESPONSE, PacketId:32, ResponseBin/binary>>}, State = #state{socket = Socket, uuid = UUID, inflight = Inflight}) when PacketId > 0 ->
{ok, RpcReply} = message_codec:decode(ResponseBin),
case maps:take(PacketId, Inflight) of
error ->
lager:warning("[ws_channel] get unknown async_call_reply message: ~p, packet_id: ~p", [AsyncCallReply, PacketId]),
{noreply, State};
{{ReceiverPid, Ref}, NInflight} ->
case is_pid(ReceiverPid) andalso is_process_alive(ReceiverPid) of
true ->
ReceiverPid ! {async_call_reply, Ref, AsyncCallReply};
ReceiverPid ! {jsonrpc_reply, Ref, RpcReply};
false ->
lager:warning("[ws_channel] get async_call_reply message: ~p, packet_id: ~p, but receiver_pid is deaded", [AsyncCallReply, PacketId])
lager:warning("[ws_channel] get async_call_reply message: ~p, packet_id: ~p, but receiver_pid is deaded", [RpcReply, PacketId])
end,
{noreply, State#state{inflight = NInflight}}
end;
%% efka的ping包
handle_info({tcp, Socket, <<?PACKET_PING>>}, State = #state{socket = Socket}) ->
{noreply, State};
handle_info({tcp_error, Sock, Reason}, State = #state{socket = Sock}) ->
lager:notice("[sdlan_channel] tcp_error: ~p", [Reason]),
{stop, normal, State};
@ -207,7 +186,7 @@ handle_info({'DOWN', _, process, HostPid, Reason}, State = #state{uuid = UUID, h
{stop, State};
handle_info(Info, State) ->
lager:warning("[sdlan_channel] get a unknown message: ~p, channel will closed", [Info]),
lager:warning("[sdlan_channel] get a unknown message: ~p, channel will closed, state: ~p", [Info, State]),
{noreply, State}.
terminate(Reason, #state{}) ->

View File

@ -1,37 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 08. 5 2025 12:58
%%%-------------------------------------------------------------------
-module(tcp_server).
-author("anlicheng").
%% API
-export([start/0]).
%% tcp服务
start() ->
{ok, Props} = application:get_env(iot, tcp_server),
Acceptors = proplists:get_value(acceptors, Props, 50),
MaxConnections = proplists:get_value(max_connections, Props, 10240),
Backlog = proplists:get_value(backlog, Props, 1024),
Port = proplists:get_value(port, Props),
TransOpts = [
{tcp_options, [
binary,
{reuseaddr, true},
{active, false},
{packet, 4},
{nodelay, false},
{backlog, Backlog}
]},
{acceptors, Acceptors},
{max_connections, MaxConnections}
],
{ok, _} = esockd:open('iot/tcp_server', Port, TransOpts, {tcp_channel, start_link, []}),
lager:debug("[iot_app] the tcp server start at: ~p", [Port]).

View File

@ -1,97 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 12. 8 2025 15:12
%%%-------------------------------------------------------------------
-module(endpoint_mnesia).
-author("aresei").
-include("endpoint.hrl").
-include_lib("stdlib/include/qlc.hrl").
-define(TAB, endpoint).
%% API
-export([create_table/0]).
-export([insert/1, delete/1, check_name/1]).
-export([get_endpoint/1]).
-export([as_map/1]).
create_table() ->
%% id生成器
mnesia:create_table(endpoint, [
{attributes, record_info(fields, endpoint)},
{record_name, endpoint},
{disc_copies, [node()]},
{type, ordered_set}
]).
-spec check_name(Name :: binary()) -> boolean() | {error, Reason :: any()}.
check_name(Name) when is_binary(Name) ->
Fun = fun() ->
Q = qlc:q([E || E <- mnesia:table(?TAB), E#endpoint.name =:= Name]),
case qlc:e(Q) of
[] ->
false;
[_|_] ->
true
end
end,
case mnesia:transaction(Fun) of
{'atomic', Res} ->
Res;
{'aborted', Reason} ->
{error, Reason}
end.
-spec get_endpoint(Id :: integer()) -> error | {ok, Endpoint :: #endpoint{}}.
get_endpoint(Id) when is_integer(Id) ->
case mnesia:dirty_read(?TAB, Id) of
[] ->
error;
[Endpoint | _] ->
{ok, Endpoint}
end.
-spec insert(Endpoint :: #endpoint{}) -> ok | {error, Reason :: term()}.
insert(Endpoint = #endpoint{}) ->
case mnesia:transaction(fun() -> mnesia:write(?TAB, Endpoint, write) end) of
{'atomic', ok} ->
ok;
{'aborted', Reason} ->
{error, Reason}
end.
-spec delete(Id :: integer()) -> ok | {error, Reason :: any()}.
delete(Id) when is_integer(Id) ->
case mnesia:transaction(fun() -> mnesia:delete(?TAB, Id, write) end) of
{'atomic', ok} ->
ok;
{'aborted', Reason} ->
{error, Reason}
end.
-spec as_map(Endpoint :: #endpoint{}) -> map().
as_map(#endpoint{id = Id, name = Name, title = Title, config = Config, updated_at = UpdateTs, created_at = CreateTs}) ->
{ConfigKey, ConfigMap} =
case Config of
#http_endpoint{url = Url, pool_size = PoolSize} ->
{<<"http">>, #{<<"url">> => Url, <<"pool_size">> => PoolSize}};
#mqtt_endpoint{host = Host, port = Port, client_id = ClientId, username = Username, password = Password, topic = Topic, qos = Qos} ->
{<<"mqtt">>, #{<<"host">> => Host, <<"port">> => Port, <<"client_id">> => ClientId, <<"username">> => Username, <<"password">> => Password, <<"topic">> => Topic, <<"qos">> => Qos}};
#kafka_endpoint{username = Username, password = Password, bootstrap_servers = BootstrapServers, topic = Topic} ->
{<<"kafka">>, #{<<"username">> => Username, <<"password">> => Password, <<"bootstrap_servers">> => BootstrapServers, <<"topic">> => Topic}};
#mysql_endpoint{host = Host, port = Port, username = Username, password = Password, database = Database, table_name = TableName} ->
{<<"mysql">>, #{<<"host">> => Host, <<"port">> => Port, <<"username">> => Username, <<"password">> => Password, <<"database">> => Database, <<"table_name">> => TableName}}
end,
Map = #{
<<"id">> => Id,
<<"name">> => Name,
<<"title">> => Title,
<<"update_ts">> => UpdateTs,
<<"create_ts">> => CreateTs
},
Map#{ConfigKey => ConfigMap}.

View File

@ -1,214 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 06. 7 2023 12:02
%%%-------------------------------------------------------------------
-module(endpoint_mysql).
-include("endpoint.hrl").
-behaviour(gen_server).
%% API
-export([start_link/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
%%
-define(RETRY_INTERVAL, 5000).
-define(DISCONNECTED, disconnected).
-define(CONNECTED, connected).
-record(state, {
endpoint :: #endpoint{},
buffer :: endpoint_buffer:buffer(),
pool_pid :: undefined | pid(),
status = ?DISCONNECTED
}).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Creates a gen_statem process which calls Module:init/1 to
%% initialize. To ensure a synchronized start-up procedure, this
%% function does not return until Module:init/1 has returned.
start_link(LocalName, AliasName, Endpoint = #endpoint{}) when is_atom(LocalName), is_atom(AliasName) ->
gen_statem:start_link({local, LocalName}, ?MODULE, [AliasName, Endpoint], []).
%%%===================================================================
%%% gen_statem callbacks
%%%===================================================================
%% @private
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
%% gen_statem:start_link/[3,4], this function is called by the new
%% process to initialize.
init([AliasName, Endpoint]) ->
iot_name_server:register(AliasName, self()),
erlang:process_flag(trap_exit, true),
%% ,
erlang:start_timer(0, self(), create_postman),
%%
Buffer = endpoint_buffer:new(Endpoint, 10),
{ok, #state{endpoint = Endpoint, buffer = Buffer, status = ?DISCONNECTED}}.
%% @private
%% @doc Handling call messages
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
State :: #state{}) ->
{reply, Reply :: term(), NewState :: #state{}} |
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_call(get_stat, _From, State = #state{buffer = Buffer}) ->
Stat = endpoint_buffer:stat(Buffer),
{reply, {ok, Stat}, State}.
%% @private
%% @doc Handling cast messages
-spec(handle_cast(Request :: term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_cast({forward, ServiceId, Format, Metric}, State = #state{buffer = Buffer}) ->
NBuffer = endpoint_buffer:append({ServiceId, Format, Metric}, Buffer),
{noreply, State#state{buffer = NBuffer}};
handle_cast(cleanup, State = #state{buffer = Buffer}) ->
endpoint_buffer:cleanup(Buffer),
{noreply, State}.
%% @private
%% @doc Handling all non call/cast messages
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
{noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}).
handle_info({timeout, _, create_postman}, State = #state{status = ?DISCONNECTED, buffer = Buffer,
endpoint = #endpoint{title = Title, config = #mysql_endpoint{host = Host, port = Port, username = Username, password = Password, database = Database}}}) ->
lager:debug("[iot_endpoint] endpoint: ~p, create postman", [Title]),
WorkerArgs = [
{host, binary_to_list(Host)},
{port, Port},
{user, binary_to_list(Username)},
{password, binary_to_list(Password)},
{keep_alive, true},
{database, binary_to_list(Database)},
{queries, [<<"set names utf8">>]}
],
%% 线
PoolSize = 5,
case poolboy:start_link([{size, PoolSize}, {max_overflow, PoolSize}, {worker_module, mysql}], WorkerArgs) of
{ok, PoolPid} ->
NBuffer = endpoint_buffer:trigger_n(Buffer),
{noreply, State#state{pool_pid = PoolPid, buffer = NBuffer, status = ?CONNECTED}};
ignore ->
retry_connect(),
{noreply, State};
{error, Reason} ->
lager:warning("[mqtt_postman] start connect pool, get error: ~p", [Reason]),
retry_connect(),
{noreply, State}
end;
%% 线
handle_info({next_data, _Id, _Tuple}, State = #state{status = ?DISCONNECTED}) ->
{noreply, State};
%% mqtt服务器
handle_info({next_data, Id, {ServiceId, Metric}}, State = #state{status = ?CONNECTED, pool_pid = PoolPid, buffer = Buffer,
endpoint = #endpoint{title = Title, config = #mysql_endpoint{table_name = Table, fields_map = FieldsMap}}}) ->
case insert_sql(Table, ServiceId, FieldsMap, Metric) of
{ok, InsertSql, Values} ->
case poolboy:transaction(PoolPid, fun(ConnPid) -> mysql:query(ConnPid, InsertSql, Values) end) of
ok ->
NBuffer = endpoint_buffer:ack(Id, Buffer),
{noreply, State#state{buffer = NBuffer}};
Error ->
lager:warning("[endpoint_mysql] endpoint: ~p, insert mysql get error: ~p", [Title, Error]),
{noreply, State}
end;
error ->
lager:debug("[endpoint_mysql] endpoint: ~p, make sql error", [Title]),
{noreply, State}
end;
%% postman进程挂掉时
handle_info({'EXIT', PoolPid, Reason}, State = #state{endpoint = #endpoint{title = Title}, pool_pid = PoolPid}) ->
lager:warning("[enpoint_mqtt] endpoint: ~p, conn pid exit with reason: ~p", [Title, Reason]),
retry_connect(),
{noreply, disconnected, State#state{pool_pid = undefined, status = ?DISCONNECTED}};
handle_info(Info, State = #state{status = Status}) ->
lager:warning("[iot_endpoint] unknown message: ~p, status: ~p", [Info, Status]),
{noreply, State}.
%% @private
%% @doc This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()).
terminate(Reason, #state{endpoint = #endpoint{title = Title}, buffer = Buffer}) ->
lager:debug("[iot_endpoint] endpoint: ~p, terminate with reason: ~p", [Title, Reason]),
endpoint_buffer:cleanup(Buffer),
ok.
%% @private
%% @doc Convert process state when code is changed
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
Extra :: term()) ->
{ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
retry_connect() ->
erlang:start_timer(?RETRY_INTERVAL, self(), create_postman).
-spec insert_sql(Table :: binary(), ServiceId :: binary(), FieldsMap :: map(), Metric :: binary()) ->
error | {ok, Sql :: binary(), Values :: list()}.
insert_sql(Table, ServiceId, FieldsMap, Metric) when is_binary(Table), is_binary(ServiceId), is_binary(Metric) ->
case line_format:parse(Metric) of
error ->
error;
{ok, #{<<"measurement">> := Measurement, <<"tags">> := Tags, <<"fields">> := Fields, <<"timestamp">> := Timestamp}} ->
Map = maps:merge(Tags, Fields),
NMap = Map#{<<"measurement">> => Measurement, <<"timestamp">> => Timestamp},
TableFields = lists:flatmap(fun({TableField, F}) ->
case maps:find(F, NMap) of
error ->
[];
{ok, Val} ->
[{TableField, Val}]
end
end, maps:to_list(FieldsMap)),
{Keys, Values} = kvs(TableFields),
FieldSql = iolist_to_binary(lists:join(<<", ">>, Keys)),
Placeholders = lists:duplicate(length(Keys), <<"?">>),
ValuesPlaceholder = iolist_to_binary(lists:join(<<", ">>, Placeholders)),
{ok, <<"INSERT INTO ", Table/binary, "(", FieldSql/binary, ") VALUES(", ValuesPlaceholder/binary, ")">>, Values}
end.
-spec kvs(Fields :: list()) -> {Keys :: list(), Values :: list()}.
kvs(Fields) when is_list(Fields) ->
{Keys0, Values0} = lists:foldl(fun({K, V}, {Acc0, Acc1}) -> {[K|Acc0], [V|Acc1]} end, {[], []}, Fields),
{lists:reverse(Keys0), lists:reverse(Values0)}.

View File

@ -1,121 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author aresei
%%% @copyright (C) 2023, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 04. 7 2023 12:31
%%%-------------------------------------------------------------------
-module(service_config_model).
-author("aresei").
-include("iot_tables.hrl").
-include_lib("stdlib/include/qlc.hrl").
-define(TAB, service_config).
%% API
-export([create_table/0]).
-export([insert/4, update/4, get_config/1, delete/1]).
-export([as_map/1]).
create_table() ->
%% id生成器
mnesia:create_table(service_config, [
{attributes, record_info(fields, service_config)},
{record_name, service_config},
{disc_copies, [node()]},
{type, ordered_set}
]).
-spec insert(ServiceId :: binary(), HostUUID :: binary(), ConfigJson :: binary(), LastEditUser :: integer()) -> ok | {error, Reason :: term()}.
insert(ServiceId, HostUUID, ConfigJson, LastEditUser) when is_binary(ServiceId), is_binary(HostUUID), is_binary(ConfigJson), is_integer(LastEditUser) ->
ServiceConfig = #service_config{
service_id = ServiceId,
host_uuid = HostUUID,
config_json = ConfigJson,
last_config_json = <<>>,
last_edit_user = LastEditUser,
create_ts = iot_util:current_time(),
update_ts = iot_util:current_time()
},
case mnesia:transaction(fun() -> mnesia:write(?TAB, ServiceConfig, write) end) of
{'atomic', ok} ->
ok;
{'aborted', Reason} ->
{error, Reason}
end.
-spec update(ServiceId :: binary(), HostUUID :: binary(), ConfigJson :: binary(), LastEditUser :: integer()) -> ok | {error, Reason :: term()}.
update(ServiceId, HostUUID, ConfigJson, LastEditUser) when is_binary(ServiceId), is_binary(HostUUID), is_binary(ConfigJson), is_integer(LastEditUser) ->
Fun = fun() ->
case mnesia:read(?TAB, ServiceId, write) of
[] ->
ServiceConfig = #service_config{
service_id = ServiceId,
host_uuid = HostUUID,
config_json = ConfigJson,
last_config_json = <<>>,
last_edit_user = LastEditUser,
create_ts = iot_util:current_time(),
update_ts = iot_util:current_time()
},
mnesia:write(?TAB, ServiceConfig, write);
[ServiceConfig0 = #service_config{config_json = OldConfigJson}] ->
NServiceConfig = ServiceConfig0#service_config{
config_json = ConfigJson,
last_config_json = OldConfigJson,
last_edit_user = LastEditUser,
update_ts = iot_util:current_time()
},
mnesia:write(?TAB, NServiceConfig, write)
end
end,
case mnesia:transaction(Fun) of
{'atomic', ok} ->
ok;
{'aborted', Reason} ->
{error, Reason}
end.
-spec get_config(ServiceId :: any()) -> error | {ok, Config :: #service_config{}}.
get_config(ServiceId) when is_binary(ServiceId) ->
case mnesia:dirty_read(?TAB, ServiceId) of
[] ->
error;
[Config] ->
{ok, Config}
end.
-spec delete(ServiceId :: binary()) -> ok | {error, Reason :: any()}.
delete(ServiceId) when is_binary(ServiceId) ->
Fun = fun() ->
case mnesia:read(?TAB, ServiceId, write) of
[] ->
ok;
[ServiceConfig0 = #service_config{config_json = OldConfigJson}] ->
NServiceConfig = ServiceConfig0#service_config{
config_json = <<"">>,
last_config_json = OldConfigJson,
update_ts = iot_util:current_time()
},
mnesia:write(?TAB, NServiceConfig, write)
end
end,
case mnesia:transaction(Fun) of
{'atomic', ok} ->
ok;
{'aborted', Reason} ->
{error, Reason}
end.
-spec as_map(ServiceConfig :: #service_config{}) -> map().
as_map(#service_config{service_id = ServiceId, config_json = ConfigJson, last_config_json = LastConfigJson, last_edit_user = LastEditUser, update_ts = UpdateTs, create_ts = CreateTs}) ->
#{
<<"service_id">> => ServiceId,
<<"config_json">> => ConfigJson,
<<"last_config_json">> => LastConfigJson,
<<"last_edit_user">> => LastEditUser,
<<"update_ts">> => UpdateTs,
<<"create_ts">> => CreateTs
}.

View File

@ -1,176 +0,0 @@
%%%-------------------------------------------------------------------
%%% @author licheng5
%%% @copyright (C) 2020, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 26. 4 2020 3:36
%%%-------------------------------------------------------------------
-module(service_handler).
-author("licheng5").
-include("iot.hrl").
%% API
-export([handle_request/4]).
%% config.json,
handle_request("POST", "/service/push_config", _,
#{<<"uuid">> := UUID, <<"service_id">> := ServiceId, <<"last_edit_user">> := LastEditUser, <<"config_json">> := ConfigJson, <<"timeout">> := Timeout0})
when is_binary(UUID), is_binary(ServiceId), is_binary(ConfigJson), is_integer(Timeout0) ->
%% ConfigJson是否是合法的json字符串
case iot_util:is_json(ConfigJson) of
true ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(-1, <<"host not found">>)};
Pid when is_pid(Pid) ->
Timeout = Timeout0 * 1000,
case iot_host:async_service_config(Pid, ServiceId, ConfigJson, Timeout) of
{ok, Ref} ->
case iot_host:await_reply(Ref, Timeout) of
{ok, Result} ->
%%
case service_config_model:update(ServiceId, UUID, ConfigJson, LastEditUser) of
ok ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
lager:debug("[service_handler] set_config service_id: ~p, get error: ~p", [ServiceId, Reason]),
{ok, 200, iot_util:json_error(-1, <<"set service config failed">>)}
end;
{error, Reason} ->
{ok, 200, iot_util:json_error(-1, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(-1, Reason)}
end
end;
false ->
{ok, 200, iot_util:json_error(-1, <<"config is invalid json">>)}
end;
%%
handle_request("GET", "/service/get_config", #{<<"service_id">> := ServiceId}, _) when is_binary(ServiceId) ->
case service_config_model:get_config(ServiceId) of
error ->
{ok, 200, iot_util:json_error(-1, <<"service config not found">>)};
{ok, Config} ->
{ok, 200, iot_util:json_data(service_config_model:as_map(Config))}
end;
%%
handle_request("POST", "/service/delete_config", _, #{<<"service_id">> := ServiceId}) when is_binary(ServiceId) ->
case service_config_model:delete(ServiceId) of
ok ->
{ok, 200, iot_util:json_data(<<"success">>)};
{error, Reason} ->
lager:debug("[service_handler] delete config of service_id: ~p, error: ~p", [ServiceId, Reason]),
{ok, 200, iot_util:json_error(-1, <<"delete service config errror">>)}
end;
%%
handle_request("POST", "/service/deploy", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"service_id">> := ServiceId, <<"tar_url">> := TarUrl})
when is_binary(UUID), is_integer(TaskId), is_binary(ServiceId), is_binary(TarUrl) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:deploy_service(Pid, TaskId, ServiceId, TarUrl) of
{ok, Ref} ->
case iot_host:await_reply(Ref, 5000) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
%%
handle_request("POST", "/service/start", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId}) when is_binary(UUID), is_binary(ServiceId) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:start_service(Pid, ServiceId) of
{ok, Ref} ->
case iot_host:await_reply(Ref, 5000) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
%%
handle_request("POST", "/service/stop", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId}) when is_binary(UUID), is_binary(ServiceId) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:stop_service(Pid, ServiceId) of
{ok, Ref} ->
case iot_host:await_reply(Ref, 5000) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
%% , json
handle_request("POST", "/service/invoke", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId, <<"payload">> := Payload, <<"timeout">> := Timeout0})
when is_binary(UUID), is_binary(ServiceId), is_binary(Payload), is_integer(Timeout0) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
Timeout = Timeout0 * 1000,
case iot_host:invoke_service(Pid, ServiceId, Payload, Timeout) of
{ok, Ref} ->
case iot_host:await_reply(Ref, Timeout) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
handle_request("POST", "/service/task_log", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId}) when is_binary(UUID), is_integer(TaskId) ->
case iot_host:get_pid(UUID) of
undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
Pid when is_pid(Pid) ->
case iot_host:task_log(Pid, TaskId) of
{ok, Ref} ->
case iot_host:await_reply(Ref, 5000) of
{ok, Result} ->
{ok, 200, iot_util:json_data(Result)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end;
{error, Reason} when is_binary(Reason) ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end;
handle_request(_, Path, _, _) ->
Path1 = list_to_binary(Path),
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% helper methods
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

View File

@ -25,7 +25,13 @@
{port, 18080}
]},
{api_url, "http://39.98.184.67:8800/api/v1/taskLog"},
{api_url, "http://100.123.0.4/api/v1"},
%% 支持的协议
{endpoints, [
{support_protocols, [
http
]}
]},
%% 目标服务器地址
{emqx_server, [
@ -41,34 +47,34 @@
%% 权限检验时的预埋token
{pre_tokens, [
{<<"test">>, <<"iot2023">>}
]},
{pools, [
%% mysql连接池配置
{mysql_iot,
[{size, 10}, {max_overflow, 20}, {worker_module, mysql}],
[
{host, "47.111.101.3"},
{port, 3306},
{user, "root"},
{connect_mode, synchronous},
{keep_alive, true},
{password, "r3a-7Qrh#3Q"},
{database, "nannong_demo"}
]
},
%% redis连接池
{redis_pool,
[{size, 10}, {max_overflow, 20}, {worker_module, eredis}],
[
{host, "127.0.0.1"},
{port, 6379},
{database, 1}
]
}
]}
%{pools, [
% %% mysql连接池配置
% {mysql_iot,
% [{size, 10}, {max_overflow, 20}, {worker_module, mysql}],
% [
% {host, "47.111.101.3"},
% {port, 3306},
% {user, "root"},
% {connect_mode, synchronous},
% {keep_alive, true},
% {password, "r3a-7Qrh#3Q"},
% {database, "nannong_demo"}
% ]
% },
% %% redis连接池
% {redis_pool,
% [{size, 10}, {max_overflow, 20}, {worker_module, eredis}],
% [
% {host, "127.0.0.1"},
% {port, 6379},
% {database, 1}
% ]
% }
%]}
]},

View File

@ -29,21 +29,21 @@
{host, "172.19.0.4"},
{port, 8086},
{token, <<"A-ZRjqMK_7NR45lXXEiR7AEtYCd1ETzq9Z61FTMQLb5O4-1hSf8sCrjdPB84e__xsrItKHL3qjJALgbYN-H_VQ==">>}
]},
{pools, [
%% redis连接池
{redis_pool,
[{size, 10}, {max_overflow, 20}, {worker_module, eredis}],
[
{host, "172.30.6.175"},
{port, 26379},
{database, 1}
]
}
]}
%{pools, [
% %% redis连接池
% {redis_pool,
% [{size, 10}, {max_overflow, 20}, {worker_module, eredis}],
% [
% {host, "172.30.6.175"},
% {port, 26379},
% {database, 1}
% ]
% }
%]}
]},

View File

@ -1,63 +1,69 @@
## Endpoint管理
## Endpoint数据结构
### 获取全部的Endpoint
### 数据库表结构
```html
method: GET
url: /endpoint/all
返回数据:
[
{
"name": "名称",
"title": "中电集团"
"matcher": "匹配的正则表达式",
"protocol": "http|https|websocket|mqtt|kafka",
"config": "参考config格式说明"
}
]
```mysql
CREATE TABLE `endpoint` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名称,路由时基于名称',
`title` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '序列号',
`type` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '类型',
`config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '配置信息基于json格式存储',
`status` smallint NOT NULL DEFAULT '-1',
`creator` smallint NOT NULL DEFAULT '0' COMMENT '创建人',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
```
### 创建Endpoint
### config_json中的数据配置
```html
method: POST
url: /endpoint/create
body: (content-type: application/json)
{"name": $name, "matcher": $matcher, "title": $title, "protocol": "http|https|websocket|kafka|mqtt", "config": "参考config格式说明"}
#### http方式: type=http
```json
说明:
name是唯一的不同的终端名称代表不同的接受端
{
"url": "http(s)://www.test.com/api",
"pool_size": 10
}
```
### 删除Endpoint
```html
method: POST
url: /endpoint/delete
body: (content-type: application/json)
{"name": $name}
#### mqtt方式, type=mqtt
```json
{
"host": "127.0.0.1",
"port": 3361,
"client_id": "ClientIdOfMqtt",
"username": "root",
"password": "Password1234",
"topic": "mqtt_topic",
"qos": 0
}
```
### config格式说明
```html
#### kafka方式, type=kafka
其中sasl_config可以不配置
```json
http|https
{"url": "http(s)://xx.com"}
{
"bootstrap_servers": ["127.0.0.1:9090", "192.168.1.1:9090"],
"topic": "KafkaTopic",
"sasl_config": {
"username": "root",
"password": "password1234",
"mechanism": "sha_256|sha_512|plain"
}
}
```
websocket
{"url": "ws://xx.com/ws"}
### 关于name的规则说明
```text
边缘端的微服务在数据上传的时候需要指定routing_key, 服务器端收到数据后会根据 routing_key = name 对数据进行路由
kafka:
{"bootstrap_server": ["localhost:9092"], "topic": "test", "username": "test", "password": "password1234"}
mysql:
{"host": "localhost", port: 3306, "username": "test", "password": "test1234", "database": "iot", "table_name": "north_data"}
mqtt:
{"host": "localhost", port: 1883, "username": "test", "password": "test1234", "topic": "CET/NX/${location_code}/upload", "qos": 0|1|2}
topic中支持预定义变量: ${location_code}; 发送的时候会替换成对应的点位编码
```
```

409
docs/iot_api.md Normal file
View File

@ -0,0 +1,409 @@
---
```markdown
# 📘 IoT API 接口文档
> 模块:`iot_api`
> 作者:**anlicheng**
> 创建时间2023-12-24
> 数据格式:`application/json`
> 认证方式:内置 `API_TOKEN = "wv6fGyBhl*7@AsD9"`
---
## 🔐 通用请求头
| Header | 值 | 说明 |
|--------|----|------|
| Content-Type | application/json | 请求体格式 |
| Accept | application/json | 响应体格式 |
---
## 🧩 主机Host相关接口
### 1. 获取所有主机列表
**接口:**
```
GET /get_all_hosts
````
**参数:**
**返回示例:**
```json
{"result":
[
"uuid-1",
"uuid-2",
"uuid-3"
]
}
````
---
### 2. 通过 UUID 获取主机信息
**接口:**
```
GET /get_host_by_uuid?uuid=<uuid>
```
**参数:**
| 参数名 | 类型 | 必填 | 说明 |
| ---- | ------ | -- | ------- |
| uuid | string | ✅ | 主机 UUID |
**返回示例:(包含host的全部字段)**
```json
{
"result": {
"id": 1,
"uuid": "uuid-1",
"name": "HostA",
"status": 1,
"authorize_status": 1,
"created_at": "2024-01-01T00:00:00Z"
}
}
```
---
### 3. 通过主机 ID 获取主机信息
**接口:**
```
GET /get_host_by_id?host_id=<id>
```
**参数:**
| 参数名 | 类型 | 必填 | 说明 |
| ------- | ------- | -- | ----- |
| host_id | integer | ✅ | 主机 ID |
**返回示例:(包含host的全部字段)**
```json
{
"result": {
"id": 1,
"uuid": "uuid-1",
"name": "HostA",
"authorize_status": 1
}
}
```
---
### 4. 修改主机状态
**接口:**
```
POST /change_host_status
```
**请求体:**
```json
{
"uuid": "uuid-1",
"new_status": 1
}
```
**返回示例:**
```json
{
"result": "ok"
}
```
---
### 5. 获取主机下的设备列表
**接口:**
```
GET /get_host_devices?host_id=<id>
```
**参数:**
| 参数名 | 类型 | 必填 | 说明 |
| ------- | ------- | -- | ----- |
| host_id | integer | ✅ | 主机 ID |
**返回示例:(包含device的全部字段)**
```json
{
"result": [
{ "device_uuid": "dev-1", "status": 1 },
{ "device_uuid": "dev-2", "status": 0 }
]
}
```
---
## 🔧 设备Device相关接口
### 6. 获取设备详情
**接口:**
```
GET /get_device_by_uuid?device_uuid=<uuid>
```
**参数:**
| 参数名 | 类型 | 必填 | 说明 |
| ----------- | ------ | -- | ------- |
| device_uuid | string | ✅ | 设备 UUID |
**返回示例:(包含device的全部字段)**
```json
{
"result": {
"device_uuid": "dev-1",
"type": "sensor",
"status": 1
}
}
```
---
### 7. 修改设备状态
**接口:**
```
POST /change_device_status
```
**请求体:**
```json
{
"device_uuid": "dev-1",
"new_status": 1
}
```
**返回示例:**
```json
{
"result": "ok"
}
```
---
## 🌐 Endpoint数据终端相关接口
### 8. 获取所有 Endpoint
**接口:**
```
GET /get_all_endpoints
```
**参数:** 无
**返回示例:**
```json
{
"result": [
{
"id": 1,
"matcher": "/device/+",
"title": "MQTT 接入节点",
"type": "mqtt",
"status": 1,
"creator": 1,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-05-01T12:00:00Z",
"config": {
"host": "mqtt.broker.local",
"port": 1883,
"client_id": "iot-client-1",
"username": "iot_user",
"password": "123456",
"topic": "iot/device/data",
"qos": 1
}
},
{
"id": 2,
"matcher": "/device/*",
"title": "HTTP 推送接口",
"type": "http",
"status": 1,
"creator": 2,
"created_at": "2024-02-15T10:00:00Z",
"updated_at": "2024-04-01T09:30:00Z",
"config": {
"url": "https://webhook.example.com/data",
"pool_size": 10
}
},
{
"id": 3,
"matcher": "/device/*",
"title": "Kafka 接入(认证)",
"type": "kafka",
"status": 1,
"creator": 3,
"created_at": "2024-03-10T08:00:00Z",
"updated_at": "2024-06-20T09:00:00Z",
"config": {
"bootstrap_servers": [
"kafka1:9092",
"kafka2:9092"
],
"topic": "iot_topic",
"sasl_config": {
"username": "user_a",
"password": "p@ssw0rd",
"mechanism": "sha_256"
}
}
},
{
"id": 4,
"matcher": "/device/*",
"title": "Kafka 接入(无认证)",
"type": "kafka",
"status": 1,
"creator": 3,
"created_at": "2024-03-15T09:30:00Z",
"updated_at": "2024-06-25T10:15:00Z",
"config": {
"bootstrap_servers": [
"kafka1:9092"
],
"topic": "iot_noauth_topic"
}
}
]
}
```
---
### 9. 获取指定 Endpoint 信息
**接口:**
```
GET /get_endpoint?id=<id>
```
**参数:**
| 参数名 | 类型 | 必填 | 说明 |
| --- | ------- | -- | ----------- |
| id | integer | ✅ | Endpoint ID |
**返回示例:()**
```json
{
"result": {
"id": 1,
"matcher": "/device/+",
"title": "MQTT接口",
"type": "mqtt",
"config": {
"url": "https://webhook.example.com/data",
"pool_size": 10
}
}
}
```
## 🧱 Endpoint 配置结构说明
根据 `type` 不同,`config_json` 结构如下:
### MQTT
```json
{
"host": "broker.example.com",
"port": 1883,
"client_id": "client-1",
"username": "user",
"password": "pass",
"topic": "iot/topic",
"qos": 1
}
```
### HTTP
```json
{
"url": "https://api.example.com",
"pool_size": 5
}
```
### Kafka带认证
```json
{
"bootstrap_servers": ["kafka1:9092", "kafka2:9092"],
"topic": "iot_topic",
"sasl_config": {
"username": "user",
"password": "pass",
"mechanism": "sha_512"
}
}
```
### Kafka无认证
```json
{
"bootstrap_servers": ["kafka1:9092"],
"topic": "iot_topic"
}
```
---
## ⚠️ 错误响应格式
统一错误返回结构:
```json
{
"error": {
"code": 400,
"message": "Invalid parameter"
}
}
```

View File

@ -1,15 +1,14 @@
{erl_opts, [debug_info]}.
{deps, [
{poolboy, ".*", {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.1"}}},
{hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.16.0"}}},
{hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.25.0"}}},
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}},
{cowboy, ".*", {git, "https://github.com/ninenines/cowboy.git", {tag, "2.10.0"}}},
{esockd, ".*", {git, "https://github.com/emqx/esockd.git", {tag, "v5.7.3"}}},
{cowboy, ".*", {git, "https://github.com/ninenines/cowboy.git", {tag, "2.14.0"}}},
{ranch, ".*", {git, "https://github.com/ninenines/ranch.git", {tag, "2.2.0"}}},
{brod, ".*", {git, "https://github.com/kafka4beam/brod.git", {tag, "4.4.5"}}},
{jiffy, ".*", {git, "https://github.com/davisp/jiffy.git", {tag, "1.1.1"}}},
{mysql, ".*", {git, "https://github.com/mysql-otp/mysql-otp", {tag, "1.8.0"}}},
{eredis, ".*", {git, "https://github.com/wooga/eredis.git", {tag, "v1.2.0"}}},
{gpb, ".*", {git, "https://github.com/tomas-abrahamsson/gpb.git", {tag, "4.20.0"}}},
{emqtt, ".*", {git, "https://gitea.s5s8.com/anlicheng/emqtt.git", {branch, "main"}}},
{gproc, ".*", {git, "https://github.com/uwiger/gproc.git", {tag, "0.9.1"}}},
{parse_trans, ".*", {git, "https://github.com/uwiger/parse_trans", {tag, "3.0.0"}}},

View File

@ -3,14 +3,14 @@
{git,"https://github.com/kafka4beam/brod.git",
{ref,"877852a175f6051b604ea7986bdb8da04ce19e76"}},
0},
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.2">>},1},
{<<"certifi">>,{pkg,<<"certifi">>,<<"2.15.0">>},1},
{<<"cowboy">>,
{git,"https://github.com/ninenines/cowboy.git",
{ref,"9e600f6c1df3c440bc196b66ebbc005d70107217"}},
{ref,"e713a630f384f861fa396048f9c881ca183aeda9"}},
0},
{<<"cowlib">>,
{git,"https://github.com/ninenines/cowlib",
{ref,"cc04201c1d0e1d5603cd1cde037ab729b192634c"}},
{ref,"aca0ad953417b29bab2c41eeb4c37c98606c848b"}},
1},
{<<"crc32cer">>,{pkg,<<"crc32cer">>,<<"1.0.3">>},2},
{<<"emqtt">>,
@ -21,25 +21,17 @@
{git,"https://github.com/wooga/eredis.git",
{ref,"9ad91f149310a7d002cb966f62b7e2c3330abb04"}},
0},
{<<"esockd">>,
{git,"https://github.com/emqx/esockd.git",
{ref,"d9ce4024cc42a65e9a05001997031e743442f955"}},
0},
{<<"fs">>,{pkg,<<"fs">>,<<"6.1.1">>},1},
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1},
{<<"gpb">>,
{git,"https://github.com/tomas-abrahamsson/gpb.git",
{ref,"edda1006d863a09509673778c455d33d88e6edbc"}},
0},
{<<"gproc">>,
{git,"https://github.com/uwiger/gproc.git",
{ref,"4ca45e0a97722a418a31eb1753f4e3b953f7fb1d"}},
0},
{<<"hackney">>,
{git,"https://github.com/benoitc/hackney.git",
{ref,"f3e9292db22c807e73f57a8422402d6b423ddf5f"}},
{ref,"8c00789e411d7c09a9808d720232098da1f19d69"}},
0},
{<<"idna">>,{pkg,<<"idna">>,<<"6.0.1">>},1},
{<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},1},
{<<"jiffy">>,
{git,"https://github.com/davisp/jiffy.git",
{ref,"9ea1b35b6e60ba21dfd4adbd18e7916a831fd7d4"}},
@ -50,7 +42,7 @@
{ref,"459a3b2cdd9eadd29e5a7ce5c43932f5ccd6eb88"}},
0},
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},1},
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.4.0">>},1},
{<<"mysql">>,
{git,"https://github.com/mysql-otp/mysql-otp",
{ref,"caf5ff96c677a8fe0ce6f4082bc036c8fd27dd62"}},
@ -64,36 +56,36 @@
{ref,"3bb48a893ff5598f7c73731ac17545206d259fac"}},
0},
{<<"ranch">>,
{git,"https://github.com/ninenines/ranch",
{ref,"a692f44567034dacf5efcaa24a24183788594eb7"}},
1},
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},1},
{git,"https://github.com/ninenines/ranch.git",
{ref,"9c8520ab8e9c6f3890ac3251d04fbe0b9514940f"}},
0},
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},1},
{<<"sync">>,
{git,"https://github.com/rustyio/sync.git",
{ref,"f13e61a79623290219d7c10dff1dd94d91eee963"}},
{ref,"4e909f69d3d0db21a6d7128b20748819e415c9eb"}},
0},
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.5.0">>},2}]}.
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},1}]}.
[
{pkg_hash,[
{<<"certifi">>, <<"B7CFEAE9D2ED395695DD8201C57A2D019C0C43ECAF8B8BCB9320B40D6662F340">>},
{<<"certifi">>, <<"0E6E882FCDAAA0A5A9F2B3DB55B1394DBA07E8D6D9BCAD08318FB604C6839712">>},
{<<"crc32cer">>, <<"AD0E42BED8603F2C72DE2A00F1B5063FFE12D5988615CAD984096900431D1C1A">>},
{<<"fs">>, <<"9D147B944D60CFA48A349F12D06C8EE71128F610C90870BDF9A6773206452ED0">>},
{<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>},
{<<"idna">>, <<"1D038FB2E7668CE41FBF681D2C45902E52B3CB9E9C77B55334353B222C2EE50C">>},
{<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>},
{<<"kafka_protocol">>, <<"6F53B15CD6F6A12C1D0010DB074B4A15985C71BC7F594BC2D67D9837B3B378A1">>},
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
{<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
{<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>},
{<<"unicode_util_compat">>, <<"8516502659002CEC19E244EBD90D312183064BE95025A319A6C7E89F4BCCD65B">>}]},
{<<"mimerl">>, <<"3882A5CA67FBBE7117BA8947F27643557ADEC38FA2307490C4C4207624CB213B">>},
{<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>},
{<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]},
{pkg_hash_ext,[
{<<"certifi">>, <<"3B3B5F36493004AC3455966991EAF6E768CE9884693D9968055AEEEB1E575040">>},
{<<"certifi">>, <<"B147ED22CE71D72EAFDAD94F055165C1C182F61A2FF49DF28BCC71D1D5B94A60">>},
{<<"crc32cer">>, <<"08FDCD5CE51ACD839A12E98742F0F0EDA19A2A679FC9FBFAF6AAB958310FB70E">>},
{<<"fs">>, <<"EF94E95FFE79916860649FED80AC62B04C322B0BB70F5128144C026B4D171F8B">>},
{<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>},
{<<"idna">>, <<"A02C8A1C4FD601215BB0B0324C8A6986749F807CE35F25449EC9E69758708122">>},
{<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>},
{<<"kafka_protocol">>, <<"1D5E9597AD3C0776C86DC5E08D3BAAEA7DB805A52E5FD35E3F071AAD7789FC4C">>},
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
{<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
{<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>},
{<<"unicode_util_compat">>, <<"D48D002E15F5CC105A696CF2F1BBB3FC72B4B770A184D8420C8DB20DA2674B38">>}]}
{<<"mimerl">>, <<"13AF15F9F68C65884ECCA3A3891D50A7B57D82152792F3E19D88650AA126B144">>},
{<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>},
{<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]}
].