监测平台开发文档

智能监测平台 TJAI-Platform

开发文档

平台结构概述

监测平台(下文简称平台)采用 NextJS 作为开发技术栈。基于 NextJS 的前后端一体化和服务器端预渲染(Server Side Rendering) 特性,可以方便地进行设备端传感器数据上传和客户端页面实时刷新。

NextJS 依赖 Node.js 环境和 ReactJS 前端框架,因此开发环境上需要安装好 Node.js 和 npm 包管理工具。在 Ubuntu 20.04 LTS 版本下安装nodejs 方法如下

1
2
$ curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
$ sudo apt install -y nodejs

Nodejs 环境安装完毕后,就可以直接用 npm 包管理工具创建 NextJS项目了

1
$ npx create-next-app tjai-platform --use-npm

平台项目目录大致分为五个部分:

  • /components 组件目录:前端页面的组件 jsx 文件存放在该目录中,在前端页面可以直接调用。

  • /lib 库函数目录:组件或前端页中需要多次调用的函数或方法在该目录中定义。

  • /models 数据库结构目录:平台采用 mongoose 库管理 mongodb 的数据模型结构。该目录用于定义各模型的 Schema 结构。

  • /pages 前端页面目录:平台网站的页面目录。该目录下的脚本文件通过 nextjs 的编译后可以直接通过 base_url/[pages] 访问。页面内容采用 react 函数和 jsx 语法编写。脚本格式可以使用 .js, .ts, 或 .jsx,非常灵活。

  • /pages/api 服务器端接口目录:服务器端响应外部或网站内部请求的 http 接口在该目录中定义。

其中的核心是 /pages前端页面和/pages/api服务端接口。客户端页面会调用/component组件。前端和接口都会调用/lib库中定义好的函数。

平台使用 MongoDB 作为处理客户信息和传感器数据的数据库。并使用 Mongoose 库作为数据库管理工具:

1
2
3
4
5
# 安装 MongoDB Enterprise Edition
$ wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | sudo apt-key add -
$ echo "deb [ arch=amd64,arm64 ] http://repo.mongodb.com/apt/ubuntu focal/mongodb-enterprise/5.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-enterprise.list
$ sudo apt update
$ sudo apt-get install -y mongodb-enterprise

/etc/mongod.conf配置好之后,用mongosh设置管理员名称和密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ mongosh

> use admin
> db.createUser({
user: "admin",
pwd: "password",
roles: [
{ role: "userAdminAnyDatabase", db:"admin" },
{ role: "readWriteAnyDatabase", db: "admin" }
]
})
> db.adminCommand( { shutdown: 1 } )

$ sudo nano /etc/mongod.conf
# 修改以下配置
security:
authorization: enabled

.env.local文件中设置好环境变量

1
2
3
MONGODB_URI=mongodb://admin:password@hostname:port/db
MONGODB_DB=db
DB_NAME=db

即可完成数据库跟平台的初步连接。

前端页面

NextJS 的前端框架是 ReactJS,在创建 NextJS 项目时会自动安装好 React 依赖,不必再手动导入bablereact-dom等脚本库。

前端主要页面有两个:主页和仪表页。主页即/pages/indes.js页是单纯的展示页,除了平台介绍和登录按钮以外没什么别的功能内容。仪表页是客户使用账户密码登录后查看设备、传感器、实时画面和警告信息的综合页面,因此大部分功能组件都在仪表页中。

CSS 设计

前端整体选用 NextJS 推荐的 Tailwind CSS 作为 CSS 工具库。安装方法参考 官方教程

1
2
3
4
$ cd ~/Projects/tjai-platform
$ npm install -D tailwindcss postcss autoprefixer
$ npx tailwindcss init -p
$ nano tailwind.config.js

./tailwind.config.js

1
2
3
4
5
6
7
8
9
10
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

./style/global.css

1
2
3
@tailwind base;
@tailwind components;
@tailwind utilities;

./pages/_app.js

1
import "../style/global.css"

之后即可在className中直接使用tailwindcss预设样式。

主页组件

  • <Layout>

    主页的上半部分,包括顶部菜单栏和正中平台名称。文件路径/components/layouts/Layout.jsx<Layout>组件中简单导入了以下两个组件:

  • < Header>

    顶部菜单栏。目前菜单栏仅作为外观和占位作用,真正可以点击进入的只有Dashboard和登录验证按钮。Documentation用于将来编写使用教程和文档,配置了相应的下拉菜单。Purchase是预留的在线商店页面。Contact预留用于放置社交账户。

  • <Hero>
    主页正中平台名称展示。

  • <Tabs>
    主页下半部分。使用标签展示形式。鼠标指针点击标签时自动切换文本内容。实现方式是定义一个页面 state 变量 openTab 以及对应的设置函数 setOpenTab。每个标签设置 onClick()事件处理函数:onClick=( () => { setOpenTab(1/2/3) } )。然后在文本tag的 className 中加入条件判断 className = { openTab === 1/2/3? "block":"hidden" } 作为文本显示脚本。

仪表页组件

  • <Admin>
    仪表页框架组件,包含标题组件<Head>、侧边栏组件<Sidebar>和用户导航栏组件<AdminNavbar>

  • <AdminNavBar>
    仪表页顶部导航栏。包含隐藏侧栏按钮和用户名邮箱公司名称的显示,最右边是等处按钮。侧栏隐藏显示按钮是绑定了

    1
    onClick={ () => { props.setOpenSidebar(!props.openSidebar); } }

    事件处理函数,并在<Admin>组件中将openSidebar状态变量用于控制侧边栏的显示和隐藏:

    1
    2
    3
    <div className={`relative ${openSidebar ? "md:ml-64" : ""} transition-all` } >
    // children
    </div>

    用户名邮箱公司名则根据登入用户的注册信息在数据库中进行提取。利用 NextJS 的SSR特性,在 dashboard.js脚本中定义 getServerSideProps(context) 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    export async function getServerSidePrps(context) {
    // ...Query mongodb for user info.
    return {
    props: {
    username: session.user.name,
    email: session.user.email,
    corp: camera[0]?.user.corp,
    camera: camera
    }
    }
    }

    dashboard页中把 props 以{...props}的形式传递给<Admin>控件后即可在<AdminNavbar>中以props.username的形式直接获取用户信息显示在DOM中。

  • <Sidebar>
    <ul>标签以列表依次显示「首页」、「设备列表」、「警告通知」、「日志图表」、「危险行为分析」和「Signout」。
    这些列表项点击事件并非页面跳转,而是组件隐藏或显示。
    列表项的onClick事件处理函数绑定了从dashboard.js传递过来的props.setXXX()等函数,用于控制各控件的显示和隐藏。
    Sidebar.jsx中定义一个linkLabel对象,将各个链接的文本存储在该队对象中。

  • linkLabel.home:地图页链接。点击后通过设置mapshow状态变量控制地图显示。

    1
    2
    3
    4
     onClick = {() => {
    props.hideAll();
    props.setMapshow(true);
    }}
  • linkLabel.devices:设备列表。利用CSS实现下拉及隐藏。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <Link href="/dashboard">
    <a
    href="#devicesSubmenu"
    data-bs-toggle="collapse"
    aria-expanded="false"
    aria-controls="devicesSubmenu"
    className="dropdown-toggle text-blueGray-700 hover:text-blueGray-500 text-xs uppercase py-3 font-bold block"
    >
    <i className="fas fa-fingerprint text-blueGray-400 mr-2 text-sm"></i>{" "}
    {linkLabel.devices + " "} {"(" + props.camera.length + ")"}
    </a>
    </Link>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      .dropdown-toggle::after {
    display: inline-block;
    margin-left: 0.255em;
    vertical-align: 0.255em;
    content: "";
    border-top: 0.3em solid;
    border-right: 0.3em solid transparent;
    border-bottom: 0;
    border-left: 0.3em solid transparent;
    }

    利用前文提到的dashboardjs中传递来的props.camera获取设备名称并映射成设备名称显示在列表中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {props.camera.map((item) => (
    <li className="items-center" key={item._id}>
    <a
    href="#"
    onClick={() => {
    props.setCamSource(item.device.deviceid);
    props.setDevId(item.device._id);
    props.hideAll();
    props.setVideoshow(true);
    props.setRtdata(true);
    props.socket?.emit("leave");
    props.socket?.emit("find",item.device.deviceid);
    }}
    className="text-blueGray-700 hover:text-blueGray-500 text-xs uppercase py-3 font-bold block"
    >
    <i className="fas fa-fingerprint text-blueGray-400 mr-2 text-sm"></i>
    {item.device.deviceid}
    </a>
    </li>
    ))}

    其中onClickprops.socket?.emit()两行是通过向SocketIO server发送事件来唤起webrtc连接。具体在视频传输方案一节论述。

  • linkLabel.notification:警告通知
    调用/lib/useAlert中定义的useAlert("all",userid)方法获取未读警告通知数量,并在「警告通知」按钮右上用红圈数字标识显示。
    (待补充)

  • <FooterAdmin>
    页面底部的版权链接和预留的备案号信息。简单地以<ul>标签形式横排列出。

  • <CardLineChart>
    日志图表页的实时数据图表控件。使用recharts库作为图表绘制工具库(该库专为React框架设计)。

  • <CardRealtimeData>
    设备页的实时数据图表控件。同样使用recharts库作为图表绘制工具库。

  • <ServoControl>
    舵机操作按钮控件。

  • <CardAlerts>
    警告通知页的消息列表控件。

  • <RTCPlayer>
    WebRTC实时视频显示控件。视频播放器使用react-player库作为wrapper。通过props变量传入props.remoteStreamprops.isFull参数。前者作为控件的url属性,url={props.remoteStream}。后者用于判断是否有其他客户端正在查看视频图像。

服务器端接口

按照NextJS的项目目录结构编写以下API接口对公网开放。

  • /api/alert
    警告信息查询接口,分为[...alertquery].js, read.js两个接口。前者查询警告信息的具体内容,后者用于获取警告信息的已读状态。

  • /api/auth
    用户登录认证接口,详见 登录认证方案

  • /api/device
    设备端用于向平台发送请求获取认证token或上传实时数据到数据库的接口。分为cameraSource, deviceInfo, getToken, realtimeData, sensorInfoUpdate五个接口。

  • /api/getdata
    平台客户端页面从数据库调用查询历史数据的接口。

  • /api/socketio
    SocketIO的服务器端接口。用于WebRTC的Signaling和舵机控制。

数据库结构

平台使用Mongodb作为数据库,并且用mongoose库作为数据库处理工具,在/models目录中以json格式定义了 mongoose 模组,即 mongodb collection 的数据格式。

  • Alert
    警告通知的数据结构。一级键名有alertdata, unit, sensorinfo, device, user, isRead, createdAt。其中alertdata以列表形式存放警告数据,包含气体浓度和警告时间两个二级键名。

  • Camera
    摄像机信息数据结构。一级键名有cameraid, url, user, device

  • Device
    探测器设备信息数据结构。一级键名有deviceid, serialnumber, coordinate, user

  • Realtimedata
    实时气体浓度数据的数据结构。一级键名有sensordata, device, user, createdAt

  • Sensorinfo
    气体传感器信息的数据结构。一级键名有code, name, type, upperthreshold, bottomthreshold, user, device

  • User
    用户信息的数据结构。一级键名有name, email, password, corp

登录认证方案

平台认证使用nextauth作为验证框架,bcryptjs作为加密算法库。

视频传输方案

平台使用WebRTC作为实时视频传输方案。配合设备端的aiortc脚本进行设备端摄像头画面传输。

舵机控制方案

设备页舵机控制采用网页客户端发送socketioservo-control事件到服务器端再转发到设备端,并在rtc.py脚本中使用python-pigpio库对树莓派gpio pin脚占控比设置的方式进行。