用于制动器整车测试的无线物联系统

2019/05/05 Proj

一个实现汽车数据采集、解析、存储、分析、固件升级的软硬结合的无线物联系统,方便测试人员远程进行汽车调试

1、项目介绍

基于wifi通信协议的无线传感节点采集汽车的CAN和ADC数据,发送到局域网内的上位机和远程的私有云服务器。可以在上位机上实时查看数据的波形图,还可以通过拉取私有云服务器的数据库内的数据,进行远程数据查看。进而为远程调试汽车制动器参数提供数据参考,进而可以通过ota项目通过TCP来给ECU修改参数。

2、后端难点

2.1 TCP拆包和组包的问题

服务器

通过Netty自带的解码器,实现特定分隔符的拆包和组包。上位机发送命令和配置文件时,都要在末尾加上分隔符,比如\t

上位机

1、关于接收数据库中的ADC和CAN数据

下发数据都存放在一个循环队列中。

根据ADC/CAN数据中的长度位来提取特定的数据(解决特定数据长度提取问题);根据下发数据包的OVER指令来判断是否发送完毕(解决拆包和组包问题)。

2、关于接收数据库中的配置文件

根据下发数据包的OVER指令判断是否发送完毕(解决拆包和组包问题)。

2.2 数据库具体问题

2.2.1 测试名称查询速率过慢

起始方案

从所有数据中查询不同的实验名称,使用mongodbdistinct方法

问题:查询时间长

改进方案

查询数据库的配置文件集合(config),每个实验都会对应一个配置文件。从该集合中获取信息就会达到较快的查询速率。

进一步改进(提高ADC/CAN数据查询速度)

MongoDB索引可以提高文档的查询、更新、删除、排序操作,所以结合业务需求,适当创建索引。

在ADC/CAN数据库初始化 init函数中加入创建单下降索引(配置文件数据库也可以添加改index),

if(this.getIndexName().equals("")) {
				collection.createIndex(Indexes.descending(this.indexName), new SingleResultCallback<String>() {
					@Override
					public void onResult(String result, Throwable t) {
						logger.info(String.format("db.col create index by \"%s\"(indexName_-1)", result));
					}
				});
			}

进一步改进(返回doc中需要的信息,节省流量)

MongoDB的映射(Projection)可以实现只返回我们需要的内容。以查询ADC/CAN数据为例,

projections = new BasicDBObject();
//只返回裸数据,不需要_id
projections.append(DataProcessor.MONGODB_KEY_RAW_DATA, 1).append("_id", 0);
FindIterable<Document> docIter = mongodb.collection.find(filterDocs).projection(projections) ;
docIter.forEach(new Block<Document>() {
    @Override
    public void apply(final Document document) {//每个doc所做的操作
        try {
                                                                           ctx.write(Unpooled.copiedBuffer(TCP_ServerHandler4PC.MONGODB_FIND_DOCS+":",CharsetUtil.UTF_8));//加入抬头
            Binary rawDataBin = (Binary)document.get(DataProcessor.MONGODB_KEY_RAW_DATA); 
            byte[] rawDataByte = rawDataBin.getData();            TCP_ServerHandler4PC.writeFlushFuture(ctx,Unpooled.wrappedBuffer(rawDataByte));//发给上位机原始数据
        }catch(Exception e) {
            logger.error("",e);
        }						    	
    }}, new SingleResultCallback<Void>() {//所有操作完成后的工作 	
    @Override
    public void onResult(final Void result, final Throwable t) {
        TCP_ServerHandler4PC.writeFlushFuture(ctx,TCP_ServerHandler4PC.MONGODB_FIND_DOCS+
                                              TCP_ServerHandler4PC.SEG_CMD_DONE_SIGNAL+TCP_ServerHandler4PC.DONE_SIGNAL_OVER);        logger.debug(TCP_ServerHandler4PC.MONGODB_FIND_DOCS+TCP_ServerHandler4PC.SEG_CMD_DONE_SIGNAL+TCP_ServerHandler4PC.DONE_SIGNAL_OVER);
    }			    	
});

2.2.2 历史数据自动清空(该方案抛弃,不希望程序擅自删除数据)

方案1

使用MongoDB自带的TTL索引,指定文档的过期时间,索引必须为时间或者包含时间的字符串。

方案2

使用Linux的计划任务服务程序——crontab

方案3

使用quartz库实现后台程序自动每天执行清空指令。

方案4

使用spring自带的定时任务实现周期性清空数据库,实现linux中类似crontab的功能(语法方面和crontabquartz类似)。

注意1:操作两个collecions时候,启动事务。但是MongoDB的事务只支持集群模式和分片模式。这边可以增加冗余措施,即判断返回的ClientSession是否是null(null说明不支持事务),则不启动事务(start和commit均不进行)

注意2:异步操作时,如果多个异步操作需要对同一个对象进行调用/操作,则需要已同步方式实现。比如异步操作1,需要使用a;异步操作2,会改变a;如果两者同时进行,则无法确定在操作1进行时,操作2是否已经改变了a。所以需要在操作1的onResult后再调用操作2

最终方案

1.使用spring自带的定时任务实现周期性清空数据库(方案4);

2.插入文档时,同时插入创建时间,周期性判断该时间来删除

2.2.3 数据库分集合存储

目的

提高数据查询速率,而不想删除历史数据。

数据量计算

注意:单个节点

ADC:480bytes/pck,20pcks/s,1000SPS

CAN:480bytes/pck,6.25pcks/s(CAN : 1pck/10ms)

size:2270bytes/pcks

一分钟:1575pcks

一小时:94500pcks,204.6MB

单个节点全速运行一个小时,需要204.6MB存储空间(94500包,ADC每个通道数据数 = 94500*50 = 472500)存储ADC/CAN数据。

数据库结构

1.配置文件存储集合

2.数据存储集合

该类集合以月为单位进行分离。

eg.

2019-012019-022019-032019-04

操作对象

建立三个MongoClient

1.配置文件插入/查询对象MongoClient1

2.通用数据查询对象MongoClient2

该数据查询对象可以随时改变指向的集合。

3.当前月份数据集合数据插入对象MongoClient3

工作流程

1.测试开始

配置文件内包含本次实验所指向的集合名称yyyy_MM(可能会出现跨越月末凌晨的问题,从而使得数据到达下一个月的数据集合)

2.数据插入

从节点发送过来的数据插入到数据集合(MongoClient3集合指向更新时候发生在定时任务中,每个月凌晨0点)

bean.xml内定义了col后是可以改变的,在一个线程内修改col后,相应引用实例的对象都会相应改变。

注意1:每次初始化DataProccessor的时候,需要重新根据当前日期初始化连接的结合

注意2MongoClient3集合指向更新时候发生在定时任务中,每个月凌晨0点。而且同时需要为该集合建立索引

3.数据查询

3.1 查询配置文件数据库config,得到所有名称

3.2 指定测试名称,查询具体数据

服务器端首先根据测试名称拿到config集合中该测试指向的集合,从该集合中获取所有数据(下面一个月的也可以查询一下)

另外

对于2.2.2,进行改进,分别进行两步操作:

1.每个月根据insertIsodate删除config内配置文件和数据集合内数据

Mongodatabase具有查询集合的方法,一个foreach即可根据filter(这里针对insertIsodate)删除

2.每天根据config集合中的isodate找到数据集合,删除数据集合,再删除配置文件

config文件中包含了数据对应的集合,如果数据跨越两个集合,则由第1个措施来删除。

2.3 数据库比较和选择

2.3.1 物联网数据特点

特点

1.异构——数据库需要进行异构数据解析或者直接存储(交给下面的上位机/边缘处理)

2.时间序列——数据库对时间序列有较好的支持

3.海量——数据库需要面对大量的数据

4.关联——数据库需要对不同维度的数据进行关联分析

描述同一个实体的数据在时间上具有关联性;描述不同实体的数据在空间上会有关联性;描述实体的不同维度之间也具有关联性。

5.读多写少——数据库不需要支持事务

物联网数据一般写入后,不需要再更改,只需要读取。

数据类型

1)RFID:射频识别

2)地址/唯一标识符

3)过程,系统和对象的描述性数据

4)普遍的环境数据和位置数据

5)传感器数据:多维时间序列数据

6)历史数据

7)物理模型:模型是现实的模板

8)用于控制的执行器和命令数据的状态

数据库选型注意点

1)尺寸,比例和索引

2)处理大量数据时的有效性

3)用户友好的模式

4)便携性

5)查询语言

6)流程建模和交易

7)异质性和一体化

8)时间序列聚合

9)存档

10)安全性和成本

2.3.2 数据库选择

  MySQL MongoDB
丰富的数据模型
动态Schema
数据类型
数据本地化
字段更新
易于编程
复杂事务
审计
自动分片

MongoDB最常见的用例包括单视图,物联网,移动,实时分析,个性化,目录和内容管理。

2.4 周期运行一个线程

起始方案

使用sleep,线程包括{打印5s的数据包个数 -> sleep 5s}

改进方案1

使用ScheduledExecutorService实现预定执行或者重复执行任务。Executors类的newSingleThreadScheduledExecutornewScheduledThreadPool方法将返回实现了ScheduledExecutorService接口的对象。具体见《Java核心技术卷I》——14.9.2 预定执行

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

ScheduledExecutorService scheduledExecutorService =
                Executors.newSingleThreadScheduledExecutor();
    	scheduledExecutorService.scheduleAtFixedRate((TestTools)context.getBean("testTools"),
                5, 5, TimeUnit.SECONDS);

改进方案2

Spring自带有定时任务,实现linux中类似crontab的功能(语法方面和crontabquartz类似)。

2.5 加密

问题

如何实现测试人员的安全登录?

解决方案

测试人员名称使用明文传输;

密码使用MD5加密后,结合服务器发送过来的随机码,再次进行MD5加密。

2.6 Netty数据发送方式

问题

需要发送好多个byte[],但是writeAndFlush不支持发送byte[]

方案1

零拷贝方式Unpooled.wrappedBuffer

public static ByteBuf wrappedBuffer(byte[] arrays)

方案2

深度拷贝方式Unpooled.copiedBuffer()

public static ByteBuf copiedBuffer(byte[] arrays)

方案1的零拷贝不需要从一个区域搬运到另外一个区域,速度根更快。

2.6 数据库异步和Netty发送问题

异步查询两个数据库(见上方数据库分集合存储),在全部查询完后,通过flush发送

问题

部分数据发送两遍,如

|---------------------------------------------|   接收到的数据1
       |--------------------------------------|   接收到的数据2

分析

异步操作,速度很快在第二个集合(第二个集合一般都是空的)结束查询后,第一个数据集合的flush过程还没有进行完,第二个马上触发,导致后面的数据发送两遍。

解决方法

方案1.放弃查询两个数据库

方案2.两个集合读出来的数据先分别放到两个Buffer中,然后通过零拷贝发送。

2.7 服务器资源不足

使用发送SMTP邮件的方式提醒管理员进行数据归档、CPU负载过高、内存使用率过高。

方案

crontab周期性运行磁盘、CPU、内存使用率的shell脚本,在数据库服务器中写入到MongoDB中,在应用服务器中存放到特定的文件夹。

2.8 部署问题

自动化部署工具,设计的shell脚本仍然友好度不高,需要改进。

本项目采用数据库和应用服务器分开部署的方式实现,在数据库端安装MySQL(ota项目)、MongoDB(ota和rcloud项目),在应用服务器端安装Docker(ota和rcloud项目的部署为容器)、Redis(ota项目在用户验证时,需要用到Redis)。

2.9 项目参数配置

一个项目的参数不是一成不变的,比如smtp的host、邮箱、密码,数据库的IP地址、端口、密码等。

方案1 properties文件配置

rcloud项目通过读取properties文件的形式实现,文件中包含所有可以配置的参数。rcloud项目会每隔一段时间查询一次参数,如果参数改变,会对应进行参数的修改。

方案2 Docker环境变量

Docker容器在运行时可以输入给定配置文件xxx.env

方案3 专门的参数配置服务器

设计一个专门的参数配置服务器,其他的应用服务器通过HTTP长轮询来获取数据。开源框架:携程Apollo

HTTP长轮询:HTTP Client注册Listener,参数配置服务器会推送配置过来。

2.10 固件升级表设计

  • 设备相关

    FotaLoaders表:下载器的信息,包括IMEI、ISMI、在线状态、

3、硬件难点

3.1 时钟同步

局域网内的始终同步,实现多节点的时钟同步,使得数据时间戳对其。

方案:使用UDP广播时钟信息,认为在数据传输过程中的延时相同。如果本次没有接收到也没有关系,因为认为延时相同,上一次接收的时间是有延时的,这一次别人接收到的也是有延时的,两者延时相同。只要主时钟的时间变化不大就行(一般来说漂移很小)。

最终实现了平均70+us,最大600+us的偏差。


欢迎关注我的微信公众号

互联网矿工

funpeefun

Search

    Post Directory