0.背景
对于以嵌入式应用为代表的物联网项目,很多工作需要数据推送来调试后面的业务逻辑。如果完全依赖硬件环境调试,会非常麻烦,而且,对于大中专院校的学生,还有一些其他数据部署会推迟的项目,先行构建一个好用的测试桩是必要的。
1.需求限定
我们现在假定认为最终后续接口模块的输入是一组Json表示的结构化数据。比如这个:
sensor/monitor/realtime
{
"ver": "1.0",
"type": "monitor",
"time": "2024-01-09 13:51:03",
"sn": 22,
"speed1": 0, //电机1转速,单位:转/分
"speed2": 0, //电机2转速,单位:转/分
"speed3": 0, //电机转速,单位:转/分
}
类似这样的数据是一组。比如还有类似这样的:
sensor/monitor/state
{
"ver": "1.0",
"type": "state",
"time": "2023-08-31 18:25:34",
"net": 1 //采集设备网络状态,1-在线,0-离线
"dmiStat":1 //dmi工作状态,1-正常,0-异常。
}
我们现在的需求是,希望能够周期性模拟生成一些数据帧,然后把这些数据发送出去。
2.接口设计 - 配置文件
基本的设计理念是数据生成、表示、处理、分发,完全独立。进程独立,然后通过管道互相连接。让数据通过stdin->stdout的管道互相联通。
这些需要发送的数据帧,每一个帧,一个配置文件,放置在一个独立的cfg目录。上层应用遍历这个文件夹的json文件,就可以自行完成数据的填报,装配,发送。
fake_data.json
/*
* JSON 文件说明:
* topic: 表示传感器监测的数据
* data_template: 监测数据的字段名称和数据类型
* ver: 版本号
* type: 监测类型
* time: 数据生成时间
* sn: 系统序列号
* speed1: 电机1的转速,单位: 转/分
* speed2: 电机2的转速,单位: 转/分
* speed3: 电机3的转速,单位: 转/分
* monitor_data_engine.py <topic> <tag>
* postman.py <topic> <content>"
*/
{
"topic": "sensor/monitor/data",
"data_template": {
"ver": "1.0",
"type": "monitor",
"time": "2024-01-09 13:51:03",
"sn": 22,
"speed1": 0,
"speed2": 0,
"speed3": 0
},
"cmd_library": "python3 ./monitor_data_engine.py %s %s",
"cmd_postman": "python3 ./postman.py %s %s"
}
3.构建data_library程序 - V1.0
对于单独的数据帧可以考虑用这一版简化版的数据自动生成器,因为各帧数据之间需要加入起伏或者随机扰动。我们使用两个文件,来暂存sn和数据批次,来保证同一批次的几组数据是同步的。这个程序很像一个纯粹的查询器,这一版设计时,我打算直接把它做成能独立运行的进程。依据命令行参数,stdin = >stdout。完全使用文件,管道来处理一个流水线式的操作是可能的。
python
import sys
from datetime import datetime
import random
import math
import os
SN_FILE = 'sn.cache'
TAG_FILE = 'tag.cache'
def RefreshCurrentSn(tag):
if(os.path.exists(SN_FILE)):
with open(SN_FILE, 'r') as f:
sn = f.read()
else:
sn = 1
# 从文件中读取数据
if(os.path.exists(TAG_FILE)):
with open(TAG_FILE, 'r') as f:
data = f.read()
if(data == tag): return sn;
# 写入数据到文件中
with open(TAG_FILE, 'w') as f:
f.write(tag)
with open(SN_FILE, 'w') as f:
dd = int(sn)+1
sn = str(dd)
f.write(sn)
return int(sn)
def GenDateTime():
# 获取当前的日期和时间
now = datetime.now()
# 将日期和时间格式化成所需的字符串格式
date_time_str = now.strftime("%Y-%m-%d %H:%M:%S")
ret = date_time_str
return ret;
def generate_next_point(amplitude, period, current_position):
# 计算弧度
angle = (2 * math.pi / period) * current_position
# 计算下一个点的位置
next_point = amplitude * math.sin(angle) + amplitude;
return next_point
def LookupDataField(data_field, tag):
sn = int(RefreshCurrentSn(tag))
print(sn)
if(data_field == "ver"): print("\"1.0\"");
if(data_field == "type"): print("\"monitor\"");
if(data_field == "time"): print("\"%s\"" %(GenDateTime()))
if(data_field == "sn"): print("%d" %(sn))
if(data_field == "speed1"): print("%d" %(int(generate_next_point(10,25,sn))))
if(data_field == "speed2"): print("%d" %(int(generate_next_point(7,20,sn))))
if(data_field == "speed3"): print("%d" %(int(generate_next_point(5,10,sn))))
if(data_field == "hight"): print("%d" %(generate_next_point(0,20,sn)))
if(data_field == "carPos"): print("%d" %(generate_next_point(0,20,sn)))
if(data_field == "bigCarPos"): print("%d" %(generate_next_point(0,50,sn)))
if(data_field == "pitch"): print("%d" %(generate_next_point(0,2,sn)))
if(data_field == "weight"): print("%d" %(generate_next_point(0,20,sn)))
if(data_field == "wind"): print("%d" %(random.randint(0,13)))
return;
# 打印脚本名称
#print('脚本名称:', sys.argv[0])
# 打印传递给脚本的参数
if len(sys.argv) == 3:
data_field = sys.argv[1]
tag = sys.argv[2]
LookupDataField(data_field, tag)
else:
print('sys.argv[0] <data_field> <tag>')
4.构建顶层接口程序fake_data_engine
昨晚因为其他事务牵绊,写到这里时已经到22:30左右,最终没有实现管道型的工作模式,仍然是走了直接硬逻辑连接的老路,回头我把它再整理一下。整理完,连同git日志,发在这里:
python
import sys
from datetime import datetime
import random
import math
import os
import json
import data_library
import uuid
import postman
def getfilecountByExt(dirOfFile, extWithDot):
# 指定目录路径
directory = dirOfFile
# 初始化计数器
csv_count = 0
# 遍历目录下所有文件
for filename in os.listdir(directory):
if filename.endswith(extWithDot): # 判断文件是否以.csv结尾
csv_count += 1
return csv_count;
def ReadAllCfgOfJsonPayloadIn(dirOfCfgFile):
# 定义要遍历的路径
path_to_json_files = dirOfCfgFile
rawJsonCfg=[]
# 遍历路径下的所有文件
for root, dirs, files in os.walk(path_to_json_files):
for file in files:
print(file)
if file.endswith('.json'):
# 读取JSON文件
with open(os.path.join(root, file)) as json_file:
data = json.load(json_file)
rawJsonCfg.append(data)
return rawJsonCfg;
def ReadCfgOfJsonPayload(jsonRawObj):
topic = jsonRawObj["topic"]
payload = jsonRawObj["payload"]
dataEngineCmd = jsonRawObj["data_engine_cmd"]
postmanCmd = jsonRawObj["postman_cmd"]
return (topic, payload, dataEngineCmd, postmanCmd);
def GenCompleteFakePayloadFromDirOf(dirOfCfg):
arrayOfJsonPayloadToSent = ReadAllCfgOfJsonPayloadIn(dirOfCfg);
tagOfThisTurn = str(uuid.uuid4())
payloads=[]
for payloadCfgItem in arrayOfJsonPayloadToSent:
(topic, payload, dataEngineCmd, postmanCmd) = ReadCfgOfJsonPayload(payloadCfgItem)
new_payload =json.loads('{}')
for payloadField in payload:
new_value = data_library.LookupDataField(topic, payloadField, tagOfThisTurn)
print(new_value)
new_payload[payloadField] = new_value;
payload = new_payload
payloads.append((topic, payload))
postman.sendMqttMsg(topic, json.dumps(payload), "240318999")
GenCompleteFakePayloadFromDirOf("cfg")
注意倒数第二行那个postman,它是负责把组装好的消息发出去的。很清晰,对吧?
5.最终的调用
@echo off
:loop
REM 在这里输入您要执行的命令行命令
REM 例如,执行一个示例命令: echo Hello, World!
python fake_data_engine.py
REM 等待15秒
timeout /t 15 /nobreak >nul
REM 继续循环
goto loop
你可能不会相信,我用.bat完成了 这个定时循环调用。这个dos窗口必须保持打开,如果想要变换一下,需要加入服务,但是还有可能的权限问题,我在这里曾经讨论过它。linux下面的脱离终端串口的执行命令非常清晰,对吧:
nohup ./app_daemon_240302999 ./shake_240302999 > /dev/null 2>&1 &