# EdgeManager（SCADA系统）

- [EdgeManager（SCADA系统）](#edgemanagerscada系统)
  - [API](#api)
    - [class `EDataCapture`](#class-edatacapture)
      - [0. HTTP POST: `set_node_data`](#0-http-post-set_node_data)
  - [使用指北](#使用指北)
    - [后端](#后端)
    - [前端](#前端)
  - [开发环境](#开发环境)
  - [调试环境](#调试环境)
  - [技术细节](#技术细节)
    - [0. `EdgeManager\EDataCapture\EDataCapture -> set_data()`为什么是以`6710885`为大小chunked的？](#0-edgemanageredatacaptureedatacapture---set_data为什么是以6710885为大小chunked的)

## API

> API设计最大程度遵循了已有的MES规范。

### class `EDataCapture`

#### 0. HTTP POST: `set_node_data`

**请求**

> 上位机程序中请单次传入尽量多的数据使得性能最大化。
> 
> 但仍需兼顾传输速率和超时时间。
>
> 无需指定数值类型，服务端会自动根据已添加的节点信息检查，若不符则会报错。


**上传设备状态**

> 数据节点的数据类型在后台设定为设备状态时，该节点上传的值只能上传int或string两种类型。
> 
> 并且int或string这两种类型，只能上传下方表格中int或string指定的值，若不符则会报错。
> 
> 上传对应的设备状态parent_device_code为必传字段

|  int类型   | string类型  | 描述  |
|  -----  | -----  | -----  | 
| 1  | IDLE  | 设备已经初始化，再等待工作  |
| 2  | RUN   | 设备在工作  |
| 3  | FINISH  | 工序结束后，托盘还没取出时，设备为FINISH，取出后变为IDLE状态  |
| 4  | TROUBLE   | 设备有问题，需要维修，这时msg有相应的关键字  |
| 5  | PAUSE   |  设备通过手工操作变成暂停状态  |
| 6  | OFFLINE   | 设备处于离线状态  |

```json
{
    "action": "set_node_data",
    "param": {
        "working_subclass": <string>,  // 可选
        "data": [
            {
                "code": <string>,
                // value的类型需与type对应
                "value": <string | int | float | bool>,
                "device_code": [string],  // 必需字段
                "parent_device_code": [string],  // 可选字段，对应MES中的code
                "batch": [string]  // 可选字段
            },
            {
                //code的值在后台的数据类别为设备状态类型时
                "code": <string>,
                // value的类型需与type对应
                "value": <string | int >,
                "device_code": [string],  // 必需字段
                "parent_device_code": [string],  // 必需字段，对应MES中的code
                "batch": [string]  // 可选字段
            },
            {
                "code": <string>,
                "value": <string | int | float | bool>,
                "device_code": [string],
                "parent_device_code": [string],
                "batch": [string]
            },
            ...
        ]
    }
}
```

**返回**

操作成功：

```json
{
    "action": "result_set_node_data",
    "errcode": 0,
    "errmsg": ""
}
```

操作失败（**工序单元尚未登记**）：

```json
{
    "action": "result_set_node_data",
    "errcode": 4002,
    "errmsg": "未登记过的工序单元！"
}
```

操作失败（**数值类型错误**）：

```json
{
    "action": "result_set_node_data",
    "errcode": 4002,
    "errmsg": "节点编码和数值类型不匹配！"
}
```

操作失败（不明原因）：

```json
{
    "action": "result_set_node_data",
    "errcode": 4002,
    "errmsg": "ROLLBACKed: Bad data received (structure and/or values)"
}
```


## 使用指北

本项目可独立运行，也可作为MES的插件使用。

下文简要介绍如何将代码合并入MES中。

需要保证`timescaledb`的版本在**2.2.0或以上**。

### 后端

添加pg扩展（Ubuntu）：

```bash
sudo apt install php-pgsql
```

添加pg扩展（CentOS 7）：

```bash
sudo yum install php-pgsql
```

添加pg扩展（CentOS8）：

```bash
sudo dnf install php-pgsql
```

目录结构：

```pre
📦EdgeManager
 ┣ 📂EDataCapture
 ┃ ┣ 📜EDataCapture.php
 ┃ ┗ 📜ENodeConfigure.php
 ┣ 📜Init.php
 ┗ 📜Utils.php
```

建议使用composer autoload引入。

将目录`EdgeManager`复制至项目后在composer.json内写入：

```json
"autoload": {
    "psr-4": {
        "EdgeManager\\": "EdgeManager/"
    },
    "files": [
        "EdgeManager/Utils.php",
        "EdgeManager/Init.php"
    ]
},
```

运行`composer dump-autoload`即可正常调用EdgeManager的代码。

### 前端

添加依赖（**使用npm**）：

```bash
npm i element-ui \
    @d2-projects/d2-crud \
    vue-cheetah-grid \
    @d2-projects/vue-table-export \
    @d2-projects/vue-table-import \
    github-markdown-css \
    marked@^2.0.0 \
    jschardet -S
```

添加依赖（**使用yarn**）：
```bash
yarn add element-ui \
    @d2-projects/d2-crud \
    vue-cheetah-grid \
    @d2-projects/vue-table-export \
    @d2-projects/vue-table-import \
    github-markdown-css \
    marked@^2.0.0 \
    jschardet
```

需要在`main.js`中增加（全局引入组件）：

```js
// D2-Crud
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import D2Crud from '@d2-projects/d2-crud'
// Cheetah-Grid
import vueCheetahGrid from 'vue-cheetah-grid'
// 表格导出插件
import pluginExport from '@d2-projects/vue-table-export'
import pluginImport from '@d2-projects/vue-table-import'

Vue.use(ElementUI)
Vue.use(D2Crud)
Vue.use(vueCheetahGrid)
Vue.use(pluginExport)
Vue.use(pluginImport)
```

*（可选）* 在`package.json`里更改`element-ui`版本：

```json
"element-ui": ">2.15.9 || 2.15.8",
```

`2.15.9`有一个小[bug](https://github.com/ElemeFE/element/issues/21941)，会导致性能下降。

此外请参照目录结构中注释进行代码合并：

```pre
📦src
 ┣ 📂api
 ┃ ┣ 📂modules
 ┃ ┃ ┣ 📜scada.configure.api.js  # 增添Axios请求
 ┃ ┃ ┗ 📜sys.user.api.js
 ┃ ┣ 📜index.js
 ┃ ┣ 📜service.js
 ┃ ┗ 📜tools.js
 ┣ 📂assets
 ┣ 📂components
 ┃ ┣ 📂d2-markdown  # 渲染markdown所需组件（在D2Admin的基础上精简了功能）
 ┃ ┃ ┗ 📜index.vue
 ┃ ┗ 📜index.js  # 在此处注册d2-markdown
 ┣ 📂libs
 ┣ 📂locales
 ┣ 📂menu
 ┃ ┗ 📜index.js  # 增添菜单
 ┣ 📂plugin
 ┣ 📂router
 ┃ ┣ 📜index.js
 ┃ ┗ 📜routes.js  # 增添路由
 ┣ 📂store
 ┣ 📂views
 ┃ ┣ 📂scada  # 增添页面
 ┃ ┃ ┣ 📂scadaConfigure
 ┃ ┃ ┃ ┗ 📜index.vue
 ┃ ┃ ┗ 📂scadaQuery
 ┃ ┃ ┃ ┗ 📜index.vue
 ┃ ┗ 📂system
 ┣ 📜App.vue
 ┣ 📜i18n.js
 ┣ 📜main.js
 ┗ 📜setting.js
```

## 开发环境

拉取代码：

```bash
# 建议先配置SSH key pair
git clone ssh://git@118.195.187.246:10022/ysun/EdgeManager.git
cd EdgeManager
```

一键部署PHP workerman和TimescaleDB环境：

```bash
# docker build --network host -t edge_manager .
docker compose up -d
```

进入交互式Prompt：

```bash
docker exec -it edge_manager bash
```

后端调试：

```bash
# In container
# --no_dup_code：禁止code在不同的working subclass间复用
# --relay_device_status：不判断是否是设备状态并转发到MES接口
php EdgeManager.php --no_dup_code --relay_device_status --server_name=GPU-server-01 --user=postgres --password=big_dick start
```

前端调试：

```bash
# In host
# yarn
# yarn watch
yarn serve
```

客户端连接：

```bash
# sudo apt install postgresql-client
# 登入
psql -h localhost -U postgres
# 显示数据库列表
\l
```

## 调试环境

不使用`docker compose`创建两个container分别运行EdgeManager和pg：

```bash
# cd EdgeManager  # 先定位到项目目录，方便创建image和挂载
# 创建EdgeManager的image
docker build --network host -t edge_manager .
# 创建container运行EdgeManager
docker run -d --name edge_manager_test -v $PWD:/EdgeManager --network host --ipc host -it edge_manager
# 创建container运行pg，将端口映射到host的55432
docker run -d --name pg_test -v $PWD/config/postgresql.conf:/etc/postgresql/postgresql.conf -p 55432:5432 -e POSTGRES_PASSWORD=big_dick -it timescale/timescaledb-ha:pg14-latest postgres -c 'config_file=/etc/postgresql/postgresql.conf'
# 进入交互式Prompt
docker exec -it edge_manager_test bash
# 启动EdgeManager（workerman）命令省略...

# 常用命令
# 查看全部container
docker ps -a
# 启动已停止的container
docker start [container]
```

## 技术细节

### 0. `EdgeManager\EDataCapture\EDataCapture -> set_data()`为什么是以`6710885`为大小chunked的？

首先明确一点，根据PostgreSQL的技术架构，条件允许的情况下，**一次性插入多条记录是效率最高的**，比如：

```sql
INSERT INTO films (code, title, did, date_prod, kind) VALUES
    ('B6717', 'Tampopo', 110, '1985-02-10', 'Comedy'),
    ('HG120', 'The Dinner Game', 140, DEFAULT, 'Comedy');
```

这其中，仅插入部分字段的值，比如：

```sql
INSERT INTO films (code, title, did, date_prod, kind)
    VALUES ('T_601', 'Yojimbo', 106, '1961-06-16', 'Drama');
```

的效率又远远不如插入全部列，但使用特殊变量DEFAULT替代缺少的字段，比如：

```sql
INSERT INTO films VALUES
    ('UA502', 'Bananas', DEFAULT, '1971-07-13', 'Comedy', '82 minutes');
```

PostgreSQL的文档里没有提及这个话题，因为源码里为SQL语句的内存分配[指定了一个最大长度常量`MaxAllocSize`](https://github.com/postgres/postgres/blob/2373fe78dfc9d4aa2348a86fffdf8eb9d757e9d5/src/common/stringinfo.c#L28)，其大小为比1GB小1字节。这个值可以在编译前手动修改，所以理论上每个PG运行的实例都可以不一样，如果我们使用的PG并非修改过源码手动编译的，那么SQL语句的最大长度为`1024 ** 3 - 1 = 1073741823` bytes。

> 安全起见，下述计算假定working_subclass等字段的最大长度均为我们以往约定的45 bytes。

而此处我源码中的SQL语句头长度为138 bytes（注意末尾空格）：

![](imgs/sql_head.png)

单个语句体的长度为160 bytes：

![](imgs/sql_body.png)

所以其最多可以一次性插入`(1073741823 - 138) / 160`条记录，向下取整后即为`6710885`。
