change to docker
This commit is contained in:
parent
8181d1af19
commit
25bb8b0514
954
HTTP_API_README.md
Normal file
954
HTTP_API_README.md
Normal 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}}` | 表示请求失败 |
|
||||
12
README.md
12
README.md
@ -1,12 +0,0 @@
|
||||
iot
|
||||
=====
|
||||
|
||||
An OTP application
|
||||
|
||||
## erlang client sdk
|
||||
https://github.com/emqx/emqtt
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
$ rebar3 compile
|
||||
@ -33,7 +33,7 @@
|
||||
-record(endpoint, {
|
||||
id :: integer(),
|
||||
%% 全局唯一,在路由规则中通过名称来指定
|
||||
name :: binary(),
|
||||
matcher :: binary(),
|
||||
%% 标题描述
|
||||
title = <<>> :: binary(),
|
||||
%% 配置项, 格式: #{<<"protocol">> => <<"http|https|ws|kafka|mqtt">>, <<"args">> => #{}}
|
||||
|
||||
@ -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(),
|
||||
|
||||
93
apps/iot/include/message.hrl
Normal file
93
apps/iot/include/message.hrl
Normal 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()
|
||||
}).
|
||||
@ -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.
|
||||
@ -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).
|
||||
@ -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]).
|
||||
@ -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).
|
||||
@ -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).
|
||||
@ -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]).
|
||||
@ -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).
|
||||
@ -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.
|
||||
@ -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]).
|
||||
@ -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]).
|
||||
@ -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).
|
||||
@ -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
|
||||
|
||||
@ -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).
|
||||
@ -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;
|
||||
|
||||
|
||||
200
apps/iot/src/endpoint/endpoint_subscription.erl
Normal file
200
apps/iot/src/endpoint/endpoint_subscription.erl
Normal 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).
|
||||
@ -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
|
||||
|
||||
49
apps/iot/src/endpoint/endpoint_sup_sup.erl
Normal file
49
apps/iot/src/endpoint/endpoint_sup_sup.erl
Normal 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}}.
|
||||
309
apps/iot/src/http_handlers/container_handler.erl
Normal file
309
apps/iot/src/http_handlers/container_handler.erl
Normal 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.
|
||||
@ -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">>)}.
|
||||
@ -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;
|
||||
|
||||
47
apps/iot/src/http_handlers/event_stream_handler.erl
Normal file
47
apps/iot/src/http_handlers/event_stream_handler.erl
Normal 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.
|
||||
@ -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
|
||||
|
||||
@ -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)),
|
||||
@ -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
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
@ -19,7 +19,6 @@
|
||||
mysql,
|
||||
gproc,
|
||||
% gpb,
|
||||
esockd,
|
||||
mnesia,
|
||||
crypto,
|
||||
public_key,
|
||||
|
||||
@ -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.
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
@ -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
|
||||
@ -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}) ->
|
||||
#{
|
||||
|
||||
@ -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}}.
|
||||
|
||||
|
||||
@ -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.
|
||||
@ -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.
|
||||
|
||||
134
apps/iot/src/message/message_codec.erl
Normal file
134
apps/iot/src/message/message_codec.erl
Normal 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.
|
||||
@ -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">>,
|
||||
|
||||
@ -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
@ -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{}) ->
|
||||
|
||||
@ -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]).
|
||||
@ -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}.
|
||||
@ -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)}.
|
||||
@ -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
|
||||
}.
|
||||
@ -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
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
@ -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}
|
||||
% ]
|
||||
% }
|
||||
%]}
|
||||
|
||||
]},
|
||||
|
||||
|
||||
|
||||
@ -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}
|
||||
% ]
|
||||
% }
|
||||
|
||||
%]}
|
||||
|
||||
]},
|
||||
|
||||
|
||||
|
||||
100
docs/endpoint.md
100
docs/endpoint.md
@ -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
409
docs/iot_api.md
Normal 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"
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -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"}}},
|
||||
|
||||
52
rebar.lock
52
rebar.lock
@ -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">>}]}
|
||||
].
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user