智慧卫生间
Hypergryph 智能卫生间
系统结构图
系统硬件概述
门锁
技术参数:
名称 | 参数值 |
---|---|
产品尺寸 | 75mm x 53 mm |
产品材质 | 304不锈钢 |
信号输出 | 干接点开关量输出 |
引脚 | GND/OUT |
适用场景 | 内开门 |
用途 | 开关信号输出 |
产品图:
网关
技术参数:
名称 | 参数值 |
---|---|
输入输出通道 | 6路DI开关量 |
通讯接口 | 1路RJ45网口 |
工作电压 | DC9~30V |
用途 | 数据发布者(publisher) |
物联网平台-Broker
技术参数:
名称 | 参数值 |
---|---|
云平台 | 阿里云 |
区域 | 上海 |
规格 | 1000连接/1000TPS(5,000 连接 / 10,000 TPS,10,000 连接 / 20,000 TPS等可选) |
用途 | 消息订阅与转发的代理 |
软件架构与系统集成
软件架构概述
名词说明:
名称 | 功能概述 |
---|---|
用户终端 | Subcriber, 订阅者,飞书app,web页面,显示每个厕所的实时状态 |
API服务器 | 阿里云平台,用作接收认证请求与返回EMQX平台的的认证信息 |
物联网平台 | Broker, EMQX物联网平台,作消息订阅与发布的代理 |
流程:
- 认证请求 :飞书app与API服务器通信,请求临时认证
- web页面访问:当用户终端第一步获取到认证以后,将参数填入web页面的url后访问
- 物联网平台数据获取:web页面作为订阅者向物联网平台订阅数据
认证请求
飞书app在打开网页链接时,需要传入三个参数,gender、username、pw,其中username、pw为物联网平台认证,需要从API服务器获取,所有认证有效期按天算,每天0点清空所有认证,参数说明如下:
名称 | 值 | 类型 |
---|---|---|
gender | 取值为male和female, 用来直接跳转到当前性别的网页 | string |
username | 物联网平台认证,用户名称, 需要飞书app 向API服务器发送post请求 (每天0点失效) | string |
pw | 物联网平台认证,对应的用户密码, 需要飞书app 向API服务器发送post请求 (每天0点失效) | string |
加密请求:
考虑到数据安全性,物联网平台认证均在飞书app内进行,并采用rsa加密方式:
-
事先将公钥文件.pem 保存到飞书app内
-
读取pem文件,获取当前系统时间,格式为"%Y-%m-%d %H:%M:%S",采用PKCS#1 v1.5加密当前系统时间为encrypted_time
-
向api服务器地址发送post请求:
名称 | 值 |
---|---|
url | “http://139.196.xxx.xxx/api/decrypt”(临时演示) |
data | {“encrypted_time” : “加密后的系统时间”} |
header | {“Content-Type”: “application/json”} |
- 返回值说明:
字段 | 值 | 类型 |
---|---|---|
createat | 当前日期,例:2023-12-18 | string |
username | 一段随机的字符串(每天0点失效) | string |
password | 一段随机的字符串(每天0点失效) | string |
web页面访问
访问流程与示例代码:
- 解析认证请求步骤返回的json,提取username 和 password 并组合成网页 url = f"http://139.196.xxx.xxx/?gender={gender}&username={username}&pw={password}" 后打开浏览器访问 用户终端显示的网页
示例:http://139.196.xxx.xxx/?gender=male&username=kF91WFKve3&pw=LLOcTxHGeb
- 示例脚本:
python
import base64
import time
import requests
import json
from datetime import datetime
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from concurrent.futures import ThreadPoolExecutor
import statistics
# 加载公钥
def load_public_key():
with open("public_key.pem", "rb") as key_file:
return serialization.load_pem_public_key(key_file.read(), backend=default_backend())
# RSA加密字符串
def encrypt_string(data, public_key):
return public_key.encrypt(
data.encode(),
padding.PKCS1v15()
)
# 发送POST请求
def send_post_request(url, encrypted_data):
headers = {'Content-Type': 'application/json'}
data = json.dumps({"encryptedString": base64.b64encode(encrypted_data).decode('utf-8')})
try:
response = requests.post(url, headers=headers, data=data)
if response.status_code == 200:
return response.json(), None
else:
return None, f"Error {response.status_code}: {response.text}"
except requests.RequestException as e:
return None, str(e)
# 获取当前时间并格式化
def get_current_time():
now = datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S")
def test_request(url, encrypted_time):
start_time = time.time()
response_data, error = send_post_request(url, encrypted_time)
end_time = time.time()
# 转化为毫秒
response_time = (end_time - start_time) * 1000
if error:
print(f"Request failed: {error}")
return False, response_time
# 提取 username 和 password
username = response_data.get("username")
password = response_data.get("password")
gender = 'male'
url = f"http://139.196.xxx.xxx/?gender={gender}&username={username}&pw={password}"
webbrowser.open(url)
print(response_data)
return True, response_time
def main():
public_key = load_public_key()
current_time = get_current_time()
encrypted_time = encrypt_string(current_time, public_key)
test_request("http://139.196.xxx.xxx/api/decrypt", encrypted_time)
if __name__ == "__main__":
main()
Unity3d & C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Networking;
using UnityEngine.UI;
public class ApiManager : MonoBehaviour
{
public Text debugText;
private string publicKeyXML;
[System.Serializable]
public class EncryptedData
{
public string encryptedString;
}
[System.Serializable]
public class User
{
public string username;
public string password;
}
private string username;
private string password;
private string txt;
private string apiUrl;
private string htmlUrl;
private string gender;
//获取系统时间,请求时,发送加密后的系统时间
private string GetDateTime()
{
// 获取当前的系统时间
DateTime now = DateTime.Now;
// 格式化时间。例如:"yyyy-MM-dd HH:mm:ss" 表示 "年-月-日 时:分:秒"
string formattedTime = now.ToString("yyyy-MM-dd HH:mm:ss");
// 打印格式化后的时间
Debug.Log("Current Time: " + formattedTime);
return formattedTime;
}
private IEnumerator SendPostRequest(string url, string encryptedString)
{
// 创建JSON数据
string jsonData = JsonUtility.ToJson(new EncryptedData { encryptedString = encryptedString });
// 创建UnityWebRequest对象
using (UnityWebRequest www = new UnityWebRequest(url, "POST"))
{
byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonData);
www.uploadHandler = new UploadHandlerRaw(bodyRaw);
www.downloadHandler = new DownloadHandlerBuffer();
www.SetRequestHeader("Content-Type", "application/json");
// 发送请求并等待返回
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
string responseText = www.downloadHandler.text;
debugText.text = "Response: " + responseText + "\n-------------------------------\n"; ;
}
else
{
debugText.text += "Response: " + www.downloadHandler.text + "\n-------------------------------\n"; ;
string recjsonData = www.downloadHandler.text;
// 将 JSON 字符串解析为 User 类的实例
User user = JsonUtility.FromJson<User>(recjsonData);
txt = debugText.text;
// 访问解析后的数据
username = user.username;
password = user.password;
// 打开终端网页
htmlUrl = "http://139.196.xxx.xxx/?gender=" + gender + "&username=" + username + "&pw=" + password;
Application.OpenURL(htmlUrl);
debugText.text = txt + "打开链接: " + htmlUrl + "\n-------------------------------\n"; ;
}
}
}
//利用公钥加密系统时间
public string EncryptString(string textToEncrypt, string publicKeyXML)
{
byte[] bytesToEncrypt = Encoding.UTF8.GetBytes(textToEncrypt);
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.FromXmlString(publicKeyXML);
byte[] encryptedData = rsa.Encrypt(bytesToEncrypt, false);
return Convert.ToBase64String(encryptedData);
}
}
//请入账户密并打开网页
public void OpenHtml()
{
string encryptedString = EncryptString(GetDateTime(), publicKeyXML);
debugText.text += encryptedString + "\n-------------------------------\n"; ;
StartCoroutine(SendPostRequest(apiUrl, encryptedString));
}
//绑定按钮,点击触发请求时间 btn.name 为 male 和 female
public void BtnClick()
{
GameObject btn = EventSystem.current.currentSelectedGameObject;
if (btn != null)
{
debugText.text = publicKeyXML + "\n-------------------------------\n";
gender = btn.name;
OpenHtml();
}
}
public void Start()
{
apiUrl = "http://139.196.xxx.xxx/api/decrypt";
publicKeyXML = Resources.Load<TextAsset>("public_key").text;
debugText.text = publicKeyXML+"\n-------------------------------\n";
}
}
web页面说明:
绿色代表未被占用的
橙色代表被占用
红色代表网关失联或者未设置网关
下方有两个按钮,Male 和 Female, 用来切换界面
物联网平台数据获取
数据获取流程:
-
门锁数据采集:
所有门锁采用干接点方式集中接入到网关DI口上
-
数据发布:
网关采用RJ45接口有线接入公网,或者采用ap模式,无线接入公网
采用MQTT TCP协议,将门锁数据发布到物联网平台,发布模式分两种:
主动上报:当有门锁数据变化时,立马发布数据,或者 设置 10s 间隔自动循环发布
按需上报:web页面发送订阅请求,无论是否有门锁数据变化,或者是否到达10s间隔,网关都会立马发布一条数据
-
数据订阅:
web页面根据网关的TopicName向物联网平台订阅, 此时物联网平台则会向网关订阅,如果网关发布有数据,物联网平台则将获取到的数据发布给web页面,物联网平台在其中扮演一个Broker的角色
负载测试
web页面服务器负载测试
名称 | 规格 |
---|---|
测试工具 | Apache JMeter |
测试服务器规格 | 最低配 2核CPU 2GB运行内存 带宽 3Mbps |
总线程数 | 100 |
总线程启动时间 | 1s 1s内启动100个线程 |
测试结果如下:
Label | # Samples | Average | Median | 90% Line | 95% Line | 99% Line | Min | Max | Error % | Throughput | Received KB/sec | Sent KB/sec |
---|---|---|---|---|---|---|---|---|---|---|---|---|
HTTP Request | 100 | 39 | 38 | 43 | 56 | 74 | 32 | 78 | 0.00% | 97.08738 | 764.37 | 15.55 |
TOTAL | 100 | 39 | 38 | 43 | 56 | 74 | 32 | 78 | 0.00% | 97.08738 | 764.37 | 15.55 |
字段说明:
名称 | 值 | 说明 |
---|---|---|
Samples | 100 | 总共进行了100次请求 |
Average | 39 | 平均响应时间为39ms |
Median | 38 | 响应时间的中位数为38ms |
90% Line | 43 | 90%的请求都在43ms内响应 |
95% Line | 56 | 95%的请求都在56ms内响应 |
99% Line | 74 | 99%的请求都在74ms内响应 |
Min | 32 | 最小的响应时间为32ms |
Max | 78 | 最大的响应时间为78ms |
Error % | 0.00% | 错误率为0% |
Throughput | 97.08738 | 吞吐量,服务器每秒可以处理97.08738个请求 |
Received KB/sec | 764.37 | 每秒接收764.37KB |
Sent KB/sec | 15.55 | 每秒发送15.55KB |
API服务器负载测试
第一组测试:
名称 | 规格 |
---|---|
测试工具 | 自定义python脚本 |
测试服务器规格 | 最低配 2核CPU 2GB运行内存 带宽 3Mbps |
总线程数 | 100 |
线程池 | 10 |
测试结果如下:
字段说明:
名称 | 值 | 说明 |
---|---|---|
Total Time | 0.584909 seconds | 100个请求的总耗时0.584909 秒 |
Total requests | 100 | 总共测试了100个请求 |
Concurrent requests | 10 | 同时发送10个请求 |
Successful | 100 | 100个请求都成功了 |
Failed | 0 | 0个请求失败 |
Successful Rate | 100.0 | 错误率为0% |
Average response time | 55.6159 | 平均响应时间为55ms |
Median response time | 54.273 | 响应时间中位数为54ms |
第二组测试:
名称 | 规格 |
---|---|
测试工具 | 自定义python脚本 |
测试服务器规格 | 2核CPU 2GB运行内存 带宽 3Mbps |
总线程数 | 1000 |
线程池 | 100 |
测试结果如下:
物联网平台负载测试
名称 | 规格 |
---|---|
测试工具 | XMeter |
物联网平台规格 | 基础款最低配,1000 连接/1000TPS ,最多支持1000个客户端连接和1000个数据每秒的通信 |
发布者数量 | 20 也就是有20个网关 |
客户端数量 | 500 500个订阅 |
测试时长 | 1分钟 |
测试类型 | 连接及消息吞吐测试 |
测试结果如下:
page | 运行次数 | 最大响应时间(ms) | 最小响应时间(ms) | 平均响应时间(ms) | 平均成功响应时间(ms) | 平均吞吐量(/s) | 平均成功吞吐量(/s) | 平均请求大小(KiB) | 响应码成功率 | 验证点成功率 | 验证点错误率 | 平均标准差 | 90分位响应时间(ms) | 90%平均响应时间(ms) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MQTT Connect - pub | 20 | 79 | 23 | 32 | 32 | 2 | 2 | 0.0107 | 100.00% | 100.00% | 0.00% | 14.3279 | 33 | 29.6 |
MQTT Connect - sub | 500 | 111 | 20 | 31.4 | 31.4 | 50 | 50 | 0.0107 | 100.00% | 100.00% | 0.00% | 16.885 | 53 | 28.5 |
MQTT Pub Sampler | 60 | 1 | 0 | 0.1 | 0.1 | 1.2 | 1.2 | 0.0195 | 100.00% | 100.00% | 0.00% | 0.2613 | 0 | 0 |
MQTT Sub Sampler | 26952 | 16462 | 11 | 4396.4 | 4396.4 | 414.6462 | 414.6462 | 1.0234 | 100.00% | 100.00% | 0.00% | 4868.8762 | 13463 | 3996.2 |
核心性能指标说明:
名称 | 说明 |
---|---|
吞吐量 | 每秒完成的操作总数。如 MQTT 连接测试中的吞吐量,指每秒新建的连接数。MQTT 消息吞吐测试中的消息吞吐量,指每秒发布和订阅的总数。 |
响应时间 | 一次操作从发起到完成的时间 |
90% 平均响应时间 | 所有操作的响应时间中前 90% 数据的平均值。90% 平均响应时间排除了部分波动数据对整体响应时间可能造成的影响 |
虚拟用户数 | 每一个虚拟用户代表一个模拟客户端。如 MQTT 连接测试中的虚拟用户数,指模拟的连接数。MQTT 消息吞吐测试中的虚拟用户数,指发布模拟客户端或订阅模拟客户端的数量 |
响应码成功率 | 所有操作中成功操作所占的比例。如 MQTT 连接测试中的响应成功率,指成功连接数占所有连接的比例。 |