项目实战-外卖自提柜 1.项目介绍、协议制定

项目实战-外卖自提柜 1.项目介绍、协议制定
项目实战-外卖自提柜 2. CubeMX + FreeRTOS入门
项目实战-外卖自提柜 3. FreeRTOS主要API的应用
项目实战-外卖自提柜 4. FreeRTOS 堆栈分配、调试技巧
项目实战-外卖自提柜 5. ESP8266 01S配置与掉线处理
项目实战-外卖自提柜 6. 硬件工作与测试(原理图、PCB绘制、测试视频)

项目介绍

外卖自提柜,类似蜂巢之类的快递柜。
工作流程:

  • 外卖员通过手机APP扫描柜体上面的固定二维码,在APP中输入客户的手机号

  • 完成后,服务器向对应手机号发送含有取货密码的短信

  • 同时自动分配一个空柜子,向设备端发送一个开柜指令,内容包括,柜号、开柜密码等

  • 设备端收到开柜指令后开柜

  • 客户收到短信后凭密码取外卖,取完后设备端上报服务器取货成功的信息。

基本功能包括与服务器通信,控制开柜,显示信息,声音提示,验证码输入等等。

服务器和APP是别人做的,我做设备端,柜体用下面这种。
在这里插入图片描述

方案选型

方案:
MCU + WIFI模块 + GPRS模块 + 显示屏 + 键盘
选型:
stm32f103rbt6 + esp8266 + sim800 + lcd彩屏 + 矩阵键盘

一开始觉得这个项目so easy 烂大街 ,乍一看确实,这选型也太烂大街了(笑),如果说这是一道电赛题,几天也能弄出来,但做它却花了近两个月…至于原因嘛,做的时候是当一个产品 亲儿子 来做(虽然也不知道产品标准是啥),从系统稳定性、工程/代码规范来讲,很认真在做的。

工作流程

设备端主要工作流程如下:

  1. 硬件开机后与服务器连接,连接成功后,硬件自动向服务器发送注册指令, 包含本机的Id,服务器收到后会将该机器注册进来,进行监管。

  2. 当有客户想要存放时,会扫描硬件二维码获取机器Id,然后在App上打开某个格子,服务器会向该机器发送存货指令 , 包含要打开的机器Id,格子Id,存放模式,取货验证码等等,同时服务器会向取货的客户发送6位验证码短信。

  3. 机器接收到存柜存货指令后,尝试打开相应格子,并保存验证码,若打开成功,则发回给服务器开柜成功指令表示成功。否则返回开柜失败指令表示失败。

  4. 客户来取物品时,在机器上输入相应的六位密码,响应密码的格子就会自动打开,然后向服务器发送取货指令,报告格子被打开。

  5. 持续工作,设备需要每30s发送一次心跳指令

协议制定

协议部分雏形是做服务器的同学定的,这部分直接导致系统从裸奔变成跑FreeRTOS,也是个人认为整个项目最费劲的地方。

下面直接把跟做服务器同学的部分讨论贴上来
(聊天记录自己读了好几遍,相当给劲)

首次确定格式 帧头+数据长度+数据+校验和

Ta:帧头+长度+数据+校验和 的形式可以吗。帧头标志数据开始,长度标识了信息结束的位置,其实也可以看作是帧头作为不同信息的分隔符
——————————
:可以
——————————
Ta:还有一种情况是信息内部出现帧头格式,怎么识别。数据丢失也可能发生在头部和尾部
——————————
:只要不是单字节非ASCII数据,就不会连续四字节的0x0a,咱们数据都用short类型(两字节),帧头就用四个连续的0x0a就可以避免上面的情况了

到这时候,我依然打算裸奔(不跑RTOS),直到后面出现一个个问题:

Ta
因为数据传输可能出现两种情况,第一种是出错,第二种是丢失。不论哪种情况都需要数据重传,所以对于不管是出错还是丢失的数据都采取丢弃不响应的措施。而正确收到的数据需要返回一个回复帧用以响应
然后在自己这里设置一个定时器,如果超过一定时间没有收到响应就认为这个数据丢失或者出错了,就进行重传。重传三次以上都没有响应就关闭链接。
—————————————————————————————————
但加入重传就导致另一个问题,就是由于网络问题,回复帧丢失或者是回复帧传输较慢,发送端认为接收端未收到,但实际接收端收到了。发送端就会又进行重传,这时我们需要判断这条指令我们有没有执行过,不能重复执行。
所以如果要进行严格的数据安全保证: 第一个是定时检查,重传未响应数据 第二个是对于响应丢失或延迟,识别并排除重复指令
所以在我们这里实现就是,识别每一个指令,保存发送过的指令,超时重传处理,每个指令都要有回复帧。客户端和服务器端都需要实现这些功能,因为相互传输过程中都有可能出现出错,丢失
————————————————————————————————
我:
为解决上面的问题,我们增加一个计数值,我们双方每成功通信一次,计数加1,对于发送方来说,收到正确的应答算一次成功通信;对于接收方来说,收到的数据没有错误算一次成功通讯。
把这个计数值附加在每条指令的最后(校验和之前),这样发送者的计数值应该和接收者的计数值相同。如果出现了上面的那种情况,比如设备端已经收到数据,而由于回复帧有延时导致服务器误判断为超时,重发本条指令。这时设备端计数值已经加1了,而服务器的计数值未增加(此时设备端计数值=服务器计数值+1),则设备端判断为重复指令,忽略此次指令对应的执行动作,但照常回复
—————————————————————————————————
Ta:ok我觉得可行,我再想一下细节,明天同步完善一下
—————————————————————————————————
Ta:我昨天想了一下,计数值可能会出现问题。因为通信消息的发送并不是顺序进行的
比如,当前计数值为10,表示已经通信10次了。现在服务器向你发送多条开柜消息(多人在使用柜子),由于前面发送的还没收到应答消息,所以计数值还是10,那这些消息的消息的计数值就都=10+1=11。
另一种情况是,服务器发送开柜消息,通信号为11,在这段发送时间里,你那里也发送开柜信号,通信号也是11。因为咱俩计数值是一致的=10,我这里发送一条新消息11,你那里也是11
—————————————————————————————————
:我的理解是,虽然会同时使用柜子,但发送指令还是按顺序来的,服务器发完指令阻塞等待设备回复,这期间不对该设备发送指令,进行一次成功通讯后(收到成功应答信号),再进行下一次通讯
—————————————————————————————————
Ta:实际上并不是这样的,netty框架本身是异步的,当然也可以同步阻塞,但是阻塞的话,一个客户发送消息,如果消息丢了,需要超时重传,响应丢了需要超时重传,这个过程一直阻塞的话,其他人的手机界面一直转圈圈等待,效率太低了。netty就是为高效而生,如果每进行一步都要阻塞等待,那用最简单传统的socket通信就可以了。所以通信不能阻塞,通信必须可以并发
————————————————————————————————
Ta:假如我们设置一分钟的超时时间+四五位客户存东西+线路传送时间,那么最后一位存东西的不一直在等吗,相当于在排队
我们要的效果是,如果第一位存的消息没到达但是干扰不了第五位存的,第五位顺利到达了,第五位正常存取,第一位自己在等待
————————————————————————————
:懂了,这样的话,计数值不行。主要原因是计数值总在收到回复后才自增,导致后面数据的必须阻塞才能保证计数正确。
那另一种方案,计数值在每发送一个新数据时自增(新数据不包括错误重发的数据),这样计数值相当于一个数据帧的唯一ID,开一个数组/链表,内容为正在等待接收方回复的数据帧的ID,如果某数据帧收到正确的回复,那么将该数据帧对应的ID从数组/链表中移出。
当然这里需要判断这条回复帧是回复哪条数据帧的,所以回复帧中,需要增加数据帧ID的信息。
这样,发送端在每发送一个数据帧时,将该数据帧的ID计入数组/链表,同时开一个属于该数据帧的定时器,做超时判断。
————————————————————————————————
Ta:这样,如果服务器和客户端公用一个计数值,不好保证同步。服务器自己用一个计数器,从服务器发送出去的数据帧从0开始计数,客户端同理,使用自己的计数器。每发送一个指令就自增,保证自己发送的数据id在自己发送里是唯一的。同理我发送给你的数据指令id在你的接收里也是唯一的。

这一段讨论,增加了一个头疼的需求:

要保证当设备发送一条指令时,等待该条指令被应答的时间内,不影响新的指令的发送

我还是以系统裸奔为前提,来寻找解决办法:

每发送一条指令,我需要为该条指令创建一个独立的的软件定时器,用以等待接收方对该条指令的回复,同时不能阻塞系统的运行。

软件定时器是可行的,但我只能提前在程序里创建好规定数量的定时器,这样软件定时器的个数是确定的,而极端恶劣环境下(有N多条指令要等待发送),那至少从理论上,这种方法不是最优解。
(最优解是:只要单片机的RAM还富裕,我就可以一直创建,直到榨干)

当然我可以动态 申请/释放 内存来 创建/删除 软件定时器,不过这个点子是我决定使用FreeRTOS之后才想到的,当然这就像自己去造轮子,毕竟RTOS实现多任务也通过动态管理内存。

一直在搬砖的我选择继续搬…

相比之前,增加了FrameId,每一帧都有自己唯一的FrameId,用以辨识重复的指令。

嘿,至此,数据格式完全确定了:

帧头 + Length + CmdId + DevId + Content + FrameId + 校验和

  • 帧头:0x0a 0x0a 0x0a 0x0a
  • Length :指令字节数总长度,包括其本身和校验和,两个字节的无符号short类型,顺序为 [低字节,高字节]
  • CmdId: 指令的Id , 一个字节的无符号byte类型
  • DevId: 目标设备的Id,两个字节的无符号short类型,顺序为 [低字节,高字节]
  • Content : 该条指令包含的详细信息
  • FrameId:每一帧的唯一Id,两字节无符号short类型,顺序为[低字节,高字节]
  • 校验和:一字节有符号byte类型

不同指令的Content不同:

  1. 注册帧000:设备向服务器发送的认证信息,在服务器上注册该设备
    Content为空

  2. 回复帧001:回复数据正确
    Content为空

  3. 心跳帧002:心跳保持
    Content为空

  4. 存货开柜帧003:服务器向设备发送存货开柜指令
    Content内容包含:
    -CellId:机器格子的编号,要开启的格子。两个字节的无符号short类型,顺序为 [低字节,高字节]
    -Mode:代表存储的模式(常温,保温,制冷),一个字节的无符号byte
    -PassWord:表示存储密码,六个字节的char字符串,顺序即为密码顺序
    -SendAddress:表示存件者的id,11个字节的电话号码,char字符串,顺序即为号码顺序
    -ReceiveAddress:表示取件者的id,含义同上

  5. 开柜成功帧004:设备开柜成功
    Content内容与指令003相同

  6. 开柜失败帧005:设备开柜失败
    Content包含:
    -SendAddress : 表示存件者的id,11个字节的电话号码,char字符串,顺序即为号码顺序

  7. 取货帧006:客户取货成功
    Content包含:
    -CellId:机器格子的编号,要开启的格子。两个字节的无符号short类型,顺序为 [低字节,高字节]

前期工作完成了,开始写程序。

项目实战-外卖自提柜 1.项目介绍、协议制定
项目实战-外卖自提柜 2. CubeMX + FreeRTOS入门
项目实战-外卖自提柜 3. FreeRTOS主要API的应用
项目实战-外卖自提柜 4. FreeRTOS 堆栈分配、调试技巧
项目实战-外卖自提柜 5. ESP8266 01S配置与掉线处理
项目实战-外卖自提柜 6. 硬件工作与测试(原理图、PCB绘制、测试视频)

猜你喜欢

转载自blog.csdn.net/weixin_44578655/article/details/105945891