2019领克车展 Max Co币机诞生记

领克车展 Max Co币机诞生记
网上拿张现场图

先引用一篇时下2019上海车展文章的段落

上海车展新车满满,领克展台玩起了“骚操作”
https://chejiahao.autohome.com.cn/info/3658165


在7.1展台的另一个区域,我们却发现了一个与众不同的品牌,它就是领克。
今年的领克展台同样人山人海,别人车展上都在秀车,而领克却走出了"不寻常的路"。
这次领克展台主题围绕"全能进阶,不酷不耍",八个大字出发,
除了为消费者带来全新的产品外,更是将都市游乐场搬到了展台现场,通过场景化、沉浸式的体验让消费者体会到"不止于车"的独特品牌魅力。
进入展厅,仿佛置身一场潮流盛宴。超级蹦床、能量燃吧、赛道玩家、MAX扭蛋机……简直就是一场属于年轻人的潮流大趴。


"不寻常的路"、"全能进阶,不酷不耍"、"不止于车",打造一个车展游戏厅,而我在车展的贡献就是用双手孵化出了游戏厅的 Max Co币机。

来一个现场 Max Co币机 的位置

车是红花,我是绿叶,就是这样甘于在幕后默默奉献!

好,诞生记开始吧!

brief
活动流程概要:

现场用户手机端参与活动,获得币,生成二维码。
用户在出币机扫码识别,机器出币。
现场消费,余币存入。


兵马未动粮草先行,分析功能要求,分工协作,规划接口是非常重要的。
假设机器是已经有了,串口通信
那么创建三个进程
1号进程socket服务端和串口通信功能
2号进程socket客户端和外网通信功能
3号负责扫码输入与2号进程通信


本文主要围绕机器出币的环节进行分析

出币机实物实现的功能要求:
用户扫描生成好的二维码,出币机显示扫码内容;
将扫描到的二维码信息发送服务器查询验证;
通过验证则允许出币机出N个币;
出币结果发送回服务器保存;
没有通过验证则反馈给用户未通过原因。


//==========================================================
出币机项目外网接口规划

网络交互分为三个步骤:查询验证,请求出币锁定,反馈结果解锁

第一步,查询验证接口:

如果把二维码当作电子钞票存单
则二维码内的信息应当包括:
1兑换通讯网址,如http://xx.com

2存单币个数,如10

3存单号(Key),如(数据表id的md5)xxxx,通过key能查询到存单的信息

4需要的其他信息 other

字符串的形式如
http://xx.com?c=10&k=xxxx&o=other


这相当与是验证出币的通讯接口和发送的数据信息


接口反回json形式

正常时举例
{
"response":1,
"description":"接口正常",
"data":{
"shuliang":10,
"duihuan":0,
"lock":false
}
}


response是返回状态值,出币机程序根据值做出判断
description是描述
data是存单数据
数据里有 通过key查数据库得到的存单信息
shuliang 存单币的数量
duihuan 已经兑换的数量
lock 出币时的锁定状态
以上的示例说明 数量有10个,已经兑换0个
并且没有请求过出币锁定,出币机根据信息判断可以请求锁定,然后出币10个

下面的示例说明全部兑换完毕,出币机不能出币

{
"response":1,
"description":"接口正常",
"data":{
"shuliang":10,
"duihuan":10,
"lock":false
}
}

出错时
{
"response":0,
"description":"接口响应失败,服务器错误什么的"

}


第二步,请求出币锁定状态接口:
针对多台终端的考虑
需要禁止一码同时多机扫描的问题,增加一个接口限制
终端出币前先将状态锁住
出币完成反馈结果时再将状态解锁。

1通讯网址,如http://xx.com?action=lock

2兑换Key,如(数据表id的md5)xxxx

3出币数量 10

4出币机器id CB01

5服务器端验证的操作码mmmm (2 3 4 项的字符串连接起来,取其md5值 )如 MD5(xxxx10CB01) 


http://xx.com?action=lock&&c=10&mid=CB01&code=mmmm

后台检查shuliang>duihuan && !lock时返回示例

{
"response":1,
"description":"锁住成功",
"data":{
"shuliang":10,
"duihuan":0,
"lock":true
}
}
否则返回
{
"response":0,
"description":"重复请求出币锁定",
"data":{
"shuliang":10,
"duihuan":0,
"lock":true
}
}

{
"response":0,
"description":"余额不足",
"data":{
"shuliang":10,
"duihuan":10,
"lock":false
}
}
出错时
{
"response":0,
"description":"接口响应失败,服务器错误什么的"

}

第三步,结果反馈接口:


1通讯网址,如http://xx.com?action=save

2兑换Key,如(数据表id的md5)xxxx

3出币数量 10

4出币机器id CB01

5服务器端验证的操作码mmmm (2 3 4 项的字符串连接起来,取其md5值 )如 MD5(xxxx10CB01) 


 !!!!!!注意 3出币数量    !!!!!!!!!!!!! 它的值是出币机实际出来的数量>=0。数据库里duihuan的值应该累加上本次提交的值
本接口要实现解锁的功能

get发送字符串的形式如
http://xx.com?action=save&k=xxxx&c=10&mid=CB01&code=mmmm

返回示例
{
"response":1,
"description":"接口正常",
"data":{
"shuliang":10,
"duihuan":10,
"lock":false
}
}

出现错误时
{
"response":0,
"description":"接口响应失败,服务器错误什么的"

}
出现错误出币机本地记录错误日志。

//==========================================================

出币机项目内网和串口接口

出币机内嵌入pc机

出币单元是下位机,与pc串口通讯

pc屏幕显示从扫码到出币的交互过程。

分两部分
服务端
显示端

服务端实现一个socket server,并负责串口与下位机的连接通讯

显示端连接socket,负责发送扫码信息,验证和显示每个交互步骤的信息反馈。

上位机下位机串口通讯
通讯字节码数据协议
四个字节一个数据包

FX XX XX CC 
FX开头  
第一字节是命令
 二三字节是数据
CC验证字节是数据一二三字节的和

查询状态命令

F0 00 00 F0
返回
F0 XX XX CC

F0 00 00 F0 空闲状态
F0 00 01 F1 忙

空闲状态接受出币请求
执行出币
F1 0X XX CC

返回
F1 0X XX CC或 F1 FX XX CC或 F1 EX XX CC 或忙状态

F1 F 是成功完成出币  
F1 E 是完成部分出币

XXX 是12位的币数量

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
在以上基础上增加包头FF和包尾FE
FF XX XX XX CC FE

其中XX XX XX CC四个字节是上面说的数据
整个通讯包包含了 包头 数据 校验 包尾

通讯实际数据举例

查询状态命令
FF F0 00 00 F0 FE
返回
FF F0 XX XX CC FE

XX XX 为单片机内部运行的状态数值, 00 00 空闲状态。


空闲状态接受出币请求
执行出币(执行出5个币)
FF F1 00 05 F6 FE

返回

正常启动时
FF F1 00 05 F6 FE
正常结束
FF F1 F0 05 E6 FE
错误(只出3个币后中止)
FF F1 E0 03 D4 FE
忙时返回当前状态
FF F0 XX XX CC FE

socket通讯为json字串

{
action:出币
data:10

}


{
action:出币完成
data:10

}

{
action:部分出币
data:5

}

{
action:请求状态
data:0

}

{
action:返回状态
data:0

}

通讯json字符串以base64编码后同样加 头FF 尾FE 分隔

形式如 FF base64stringdata FE


尽管后期接口文档都会有改动,不过将来的事将来再说,到现在,有了规划,项目可以启动了。


事先的假设并不存在,所以我任务的第一步得创造一个能听从串口指挥出币的机器。

万能的X宝找,并没有这种机器,不过有能数币的机器,拿来改造吧。用现成的驱动电路,arduino做个控制板。

顺丰到付一个,一觉醒来,嗖~~到了,真快!

测试数币后马上拆机,

传感器就一个光眼,执行器就一个马达,


主控芯片信号给mos管控制马达正反转,马达带动孔盘转动,掉进孔内的币,被机械上的挤出结构挤出,挤出同时光眼传感器被触发,
计数并显示在数码管上,机器的只能用按键设定数币数量。

意料之中,只能抛弃原来主控,只利用主板的电机驱动部分。


开始电路分析,


电路图反编译,

幸好不是多层板,放大镜翻来覆去的看,出来了。

控制信号


打孔飞线改板,把需要的接口焊接好插针

arduino编程,主要逻辑:

接收到出币指令,正转。
中断触发计数,
满足出币数量,刹停,反馈出币数量。

正转持续3秒,若不出币,停,反转一下,停。反复
连续第4次不出币,刹停,反馈出币数量。

要快捷的开发还是使用rtos框架

概括的分几个任务,具体看源码
比如串口通信协议解析任务。
根据状态控制正反转的任务。
中断触发容易抖动的延时计数任务。

面包板上搭个结构。

视频

编程源码

#include <Arduino_FreeRTOS.h>
#include <semphr.h>

// add the FreeRTOS functions for Semaphores (or Flags).
#include "DataPkg.h"

// Declare a mutex Semaphore Handle which we will use to manage the Serial Port.
//声明信互斥信号量用它来管理串口
// It will be used to ensure only only one Task is accessing this resource at any time.
//某时刻只有一个任务能获取到串口通讯这个资源
SemaphoreHandle_t xSerialSemaphore;
//
int _status = 0; //运行状态
int _f = 1; //方向
int _setCount = 0; //出币数
int _empty = 0; //5秒内没有出币  的次数
int _tick_fz = 0; //反转的tick
int _tick_zh = 0; //正反转换时延时,status=10
int _speed = 0;
int _gonglv = 4; //x分之一
int work_finish = 0; //工作完成中止,工作未完成中止
bool _blink_enable = true;
int _blink_count = 0;
int _bring_up=0;//重启时_bring_up初始为0,启动成功为1
int cornCount = 0;//统计出币数量
bool chk_request=false;
int value_sigma=0;
int value_times=0;
void init_var(){
   _status = 0; //运行状态
   _f = 1; //方向
   _setCount = 0; //出币数
   _empty = 0; //5秒内没有出币  的次数
   _tick_fz = 0; //反转的tick
   _tick_zh = 5; //正反转换时延时,status=10
   _speed = 0;
   _gonglv = 4; //x分之一
   work_finish = 0; //工作完成中止,工作未完成中止
   _blink_enable = true;
   _blink_count = 0;//中断消抖延时计数

   cornCount = 0;
}
void power_on_run(){//开机bringup反转响应
  _tick_fz=60;
  _f = -1;  
  _status=12;
  
}
unsigned int _gonglv_count = 0;
//串口数据解析
unsigned char sbuf[16];
unsigned char pkg_data_buf[16];
unsigned char send_data_buf[6];//发送数据的容器
int pkg_data_len = 0; //不为0说明有命令
DataPkg pkg = DataPkg();
void on_pkg_data_ok(unsigned char arr[], int len) {
  for (int i = 0; i < len; i++) {
    pkg_data_buf[i] = arr[i];
  }
  pkg_data_len = len;
  /*
    if(_status==0){
    //解析数据,返回状态或出币
    }else{
    //返回忙状态。
    }
  */
}

// define two Tasks for TaskReadSerial & TaskCheckSpeed &TaskDriveMotor
//定义3个任务TaskReadSerial TaskCheckSpeed和 TaskDriveMotor

void TaskReadSerial( void *pvParameters );//解析命令开始动作
void TaskCheckSpeed( void *pvParameters );//检查出币速度,5秒无出币,反转
void TaskDriveMotor( void *pvParameters );//根据状态切换驱动信号
void TaskInerrupt0Effect( void *pvParameters );
void TaskBlink1Delay( void *pvParameters );//中断消抖延时


int lastcornCount = 0;

volatile int state = LOW;

void blink1()
{
  chk_request=true;
   value_sigma=0;
 value_times=0;
}
void corn_count_times(){
    _setCount -= 1;
    _speed += 1;
    _empty = 0;
    if (_setCount == 0) {
      _status += 100;
      _tick_fz = 0;
    }
    //old
    state = !state;
    cornCount += 1;  
}
void blink2(){
  if (_blink_enable) {
    _blink_enable = false; //禁止中断,中断消除抖动
    _blink_count = 0; //延时计数

    corn_count_times();

  }
}
//设置中断
void setupInerrupt0()
{
  //attachInterrupt(0, blink, CHANGE);
  //attachInterrupt(pin, ISR, mode)
  attachInterrupt(digitalPinToInterrupt(2), blink1, FALLING); //(recommended)
  //attachInterrupt(digitalPinToInterrupt(pin), ISR, mode); (recommended)
  //attachInterrupt(interrupt, ISR, mode); (not recommended)
  //attachInterrupt(pin, ISR, mode); (Not recommended. Arduino SAMD Boards, Uno WiFi Rev2, Due, 101 only)
  //mode:定义中断触发类型,有四种形式:
  //LOW:低电平触发;
  //CHANGE:电平变化触发;
  //RISING :上升沿触发(由LOW变为HIGH);
  //FALLING:下降沿触发(由HIGH变为LOW);
  //Due板子还支持高电平触发。
}

void setMotorPins() {
  //4,5正反方向 L298N控制方式, 4高5低正 4低5高反 45电平相同停止,同时0为安全电平
  pinMode(4, OUTPUT);
  digitalWrite(4, 0);
  pinMode(5, OUTPUT);
  digitalWrite(5, 0);

  //6789 H桥控制方式,拉低通电 对应电路板1234线 14 低电平正 23低电平反转12和34不能同时拉低否则短路!!! 同时为1为安全电平
  pinMode(6, OUTPUT);
  digitalWrite(6, 1);
  pinMode(7, OUTPUT);
  digitalWrite(7, 1);
  pinMode(8, OUTPUT);
  digitalWrite(8, 1);
  pinMode(9, OUTPUT);
  digitalWrite(9, 1);
}
void setMotorT() {

  //4,5正反方向 L298N控制方式, 4高5低正 4低5高反 45电平相同停止,同时0为安全电平
  digitalWrite(4, 0);
  digitalWrite(5, 0);
  //6789 H桥控制方式,拉低通电 对应电路板1234线,14 低电平正转23低电平反转  12和34不能同时拉低否则短路!!! 同时为1为安全电平
  digitalWrite(6, 1);
  digitalWrite(7, 1);
  digitalWrite(8, 1);
  digitalWrite(9, 1);


}
void setMotorSTOP() {
  //制动
  // L298N控制方式停默认制动
  digitalWrite(4, 0);
  digitalWrite(5, 0);
  //6789 H桥控制方式 79负极相通制动
  digitalWrite(6, 1);
  digitalWrite(7, 0);
  digitalWrite(8, 1);
  digitalWrite(9, 0);


}
void setMotorZ() {
  setMotorT();
  _gonglv_count++;
  if (_gonglv_count % _gonglv == 0) {
    digitalWrite(4, 1);

    digitalWrite(6, 0);
    digitalWrite(9, 0);
  }

}
void setMotorF() {
  setMotorT();
  _gonglv_count++;
  if (_gonglv_count % _gonglv == 0) {
    digitalWrite(5, 1);
    digitalWrite(7, 0);
    digitalWrite(8, 0);
  }


}
// the setup function runs once when you press reset or power the board
// 当上电或reset时setup函数只运行一次
void setup() {
  
  setMotorPins();
  delay(1000);
  setupInerrupt0();
  // initialize serial communication at 9600 bits per second:
  //初始化串口连接波特率
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB, on LEONARDO, MICRO, YUN, and other 32u4 based boards.
    //等待串口连接。需要LEONARDO, MICRO, YUN, 或其他 32u4板子上的usb接口
  }
  pkg.dispatch_event = &on_pkg_data_ok;
  int l = pkg.getDatLen();
  Serial.println(l);
  Serial.println(pkg.pkgLen);
  Serial.println("pkg ready!");

  // Semaphores are useful to stop a Task proceeding, where it should be paused to wait,
  //信号量在需要让一个任务进程停一会儿时很有用,它让任务处于暂停等待的状态,
  // because it is sharing a resource, such as the Serial port.
  //就像串口这样的共享资源
  // Semaphores should only be used whilst the scheduler is running, but we can set it up here.
  //信号量应该只在调度运行的时候使用,在这里我们可以对它进行设置。
  if ( xSerialSemaphore == NULL )  // Check to confirm that the Serial Semaphore has not already been created.//检查确认串口信号没被创建。
  {
    xSerialSemaphore = xSemaphoreCreateMutex();  // Create a mutex semaphore we will use to manage the Serial Port//创建信号来管理串口
    if ( ( xSerialSemaphore ) != NULL )
      xSemaphoreGive( ( xSerialSemaphore ) );  // Make the Serial Port available for use, by "Giving" the Semaphore.//调用xSemaphoreGive (“给”信号)使串口处于有效状态(没被占用)
  }
  Serial.println("SemaphoreGive");
  // Now set up Tasks to run independently.
  //现在设置多个个任务互不影响的运行。

  xTaskCreate(
    TaskInerrupt0Effect
    ,  (const portCHAR *) "Inerrupt0Effect"
    ,  128  // Stack size
    ,  NULL
    ,  2  // Priority
    ,  NULL );

  xTaskCreate(
    TaskBlink1Delay
    ,  (const portCHAR *) "TaskBlink1Delay"
    ,  128  // Stack size
    ,  NULL
    ,  3  // Priority
    ,  NULL );
  xTaskCreate(
    TaskDriveMotor
    ,  (const portCHAR *) "TaskDriveMotor"
    ,  128  // Stack size
    ,  NULL
    ,  2  // Priority
    ,  NULL );

  xTaskCreate(
    TaskCheckSpeed
    ,  (const portCHAR *) "TaskCheckSpeed"
    ,  128  // Stack size
    ,  NULL
    ,  1  // Priority
    ,  NULL );
  // Now the Task scheduler, which takes over control of scheduling individual Tasks, is automatically started.
  //现在,任务调度程序开始自动控制调度单个任务,它将自动启动。
  //*/
  Serial.println("tasks is running now.");
  /*_bring_up=1;
  _setCount = 10;
  _status = 10;
  _tick_zh = 5;*/
  power_on_run();
}


void loop()
{
  // Empty. Things are done in Tasks.
  //所有事情都在每个任务里做,这里留空。
}

/*--------------------------------------------------*/
/*---------------------- Tasks ---------------------*/
/*--------------------------------------------------*/
void TaskDriveMotor(void *pvParameters __attribute__((unused))) {
  for (;;) // A Task shall never return or exit.
  {
    if (_status > 0 && _status <= 10) {
      setMotorT();
      //设置停00
      if(_bring_up==0){
        _bring_up=1;
        _status += 20;
        
        continue;
      }
      if (_empty >= 3) {
        _status += 50;
        continue;

      }

      if (_tick_zh > 0) {
        //转换延时
        vTaskDelay((100L * configTICK_RATE_HZ) / 1000L);
        _tick_zh -= 1;
        continue;
      }

      if (_f == 1) {
        _status += 1; //11
      }
      if (_f == -1) {
        _status += 2; //12
      }
    }
    if (_status == 11) {

      //设置正  1  0
      setMotorZ();
      if (_f == -1) {
        _status -= 1; //10
        _tick_zh = 5;
      }
    }
    if (_status == 12) {

      //设置正  0  1
      setMotorF();
      _tick_fz -= 1;
      if (_tick_fz <= 0) {
        _f = 1;
      }
      if (_f == 1) {
        _status -= 2; //10
        _tick_zh = 5;
      }
    }
    if (_status > 90) {

      //设置  0  0

      setMotorT();
      setMotorSTOP();
      _status = 0; //0
      work_finish = 1;
    }
    
    if (_status > 40 && _status < 90) {
      
      //设置  0  0

      setMotorT();
      setMotorSTOP();
      //发送无币
      _status = 0; //0
      work_finish = 2;
      

    }
    if (_status > 20 && _status < 40) {
      
      //设置  0  0

      setMotorT();
      setMotorSTOP();
      //bringup后发送
      _status = 0; //0
      work_finish = 3;
      

    }
    if (_status == 0) {
      //设置  0  0
      setMotorT();
      setMotorSTOP();
      
    }
    vTaskDelay((30L * configTICK_RATE_HZ) / 1000L);

  }
}


void TaskCheckSpeed(void *pvParameters __attribute__((unused))) {
  for (;;) // A Task shall never return or exit.
  {
    if (_status == 11 && _f == 1) { //正转,并且没有请求反转的时候 检查出币
      _speed = 0;
      vTaskDelay((3000L * configTICK_RATE_HZ) / 1000L);//正转等3秒检测出币

      if (_speed == 0 && _status == 11) {
        _empty += 1;
        _tick_fz = 30; //反转时间
        _f = -1; //请求反转
      } else {
        _speed = 0;

      }
    } else {
      vTaskDelay((200L * configTICK_RATE_HZ) / 1000L);
    }
  }
}


void TaskInerrupt0Effect( void *pvParameters __attribute__((unused)) )  // This is a Task.
{
  int pin = 13;
  pinMode(pin, OUTPUT);
  for (;;)
  {
    digitalWrite(pin, state);
    if (lastcornCount != cornCount) {
      lastcornCount = cornCount;
      /*if ( xSemaphoreTake( xSerialSemaphore, ( TickType_t ) 5 ) == pdTRUE )
      {
        Serial.print("ZZZZ--------:");
        Serial.println(cornCount);

        xSemaphoreGive( xSerialSemaphore );
      }*/

    }
    vTaskDelay(10);
    
  }
}

void TaskBlink1Delay( void *pvParameters __attribute__((unused)) )  // This is a Task.
{
  for (;;)
  {

    if(chk_request){  
      if(digitalRead(2)==HIGH){
      value_sigma++;
      }
      value_times++;
      if(float(value_sigma)/float(value_times)>0.5&&value_times>=4){
        chk_request=false;
        //计数
        corn_count_times();
      }
      
    }
    if (!_blink_enable) {
      _blink_count++;
      if (_blink_count >= 8) {
        _blink_enable = true;
      }

    } else {

    }
    while (Serial.available()) {
      //byte s=Serial.read();
      int numdata = Serial.readBytes(sbuf, 1);
      //Serial.write(sbuf,numdata);
      pkg.writeArrayToBuf(sbuf, numdata);
      //Serial.write('\n');
      //for(byte i=0;i<numdata;i++){

    }
    //判断命令
    if (pkg_data_len > 0) {
      pkg_data_len = 0;

      bool need_reply = false;
      send_data_buf[0] = 0xff;
      send_data_buf[1] = 0x00;
      send_data_buf[2] = 0x00;
      send_data_buf[3] = 0x00;
      send_data_buf[4] = 0x00;
      send_data_buf[5] = 0xfe;
      //三个字节一个命令
      //F0 00 00查询状态
      if (pkg_data_buf[0] == 0xF0) {
        send_data_buf[1] = 0xF0;
        send_data_buf[2] = 0x00;
        send_data_buf[3] = _status;
        send_data_buf[4] = (send_data_buf[1] + send_data_buf[2] + send_data_buf[3]) & 0xff;
        need_reply = true;
      }
      //F1 0X XX 请求出币----------------------------------------------!!!!!!!!!!!!!!
      if (pkg_data_buf[0] == 0xF1) {
        if (_status == 0) {
          send_data_buf[1] = 0xF1;
          send_data_buf[2] = pkg_data_buf[1];
          send_data_buf[3] = pkg_data_buf[2];
          send_data_buf[4] = (send_data_buf[1] + send_data_buf[2] + send_data_buf[3]) & 0xff;
          init_var();
          _setCount = ((pkg_data_buf[1] & 0x0F) << 8 )+ pkg_data_buf[2];
          _status = 10;  
          Serial.write(_setCount);        
          need_reply = true;
        } else {
          send_data_buf[1] = 0xF0;
          send_data_buf[2] = 0x00;
          send_data_buf[3] = _status;
          send_data_buf[4] = (send_data_buf[1] + send_data_buf[2] + send_data_buf[3]) & 0xff;
          need_reply = true;
        }
      }
      if (need_reply) {
        need_reply = false;
        //串口发送数据
        if ( xSemaphoreTake( xSerialSemaphore, ( TickType_t ) 5 ) == pdTRUE )
        {
          Serial.write(send_data_buf, 6);
          xSemaphoreGive( xSerialSemaphore );
        }
      }

    }
    if (work_finish > 0) {
      send_data_buf[0] = 0xff;
      send_data_buf[1] = 0x00;
      send_data_buf[2] = 0x00;
      send_data_buf[3] = 0x00;
      send_data_buf[4] = 0x00;
      send_data_buf[5] = 0xfe;
      if (work_finish == 1) {
        work_finish = 0;
        send_data_buf[1] = 0xF1;
        send_data_buf[2] = 0xF0|(0x0F & (cornCount >> 8)); //成功完成出币
        send_data_buf[3] = 0xFF & (cornCount);
        send_data_buf[4] = (send_data_buf[1] + send_data_buf[2] + send_data_buf[3]) & 0xff;

      } else if(work_finish == 2){
        work_finish = 0;
        send_data_buf[1] = 0xF1;
        send_data_buf[2] = 0xE0|(0x0F & (cornCount >> 8)); //部分完成出币(币数量不够)
        send_data_buf[3] = 0xFF & (cornCount);
        send_data_buf[4] = (send_data_buf[1] + send_data_buf[2] + send_data_buf[3]) & 0xff;

      }else if(work_finish == 3){//bringup 完成主动发送一次空闲状态
        work_finish = 0;
        send_data_buf[1] = 0xF0;
        send_data_buf[2] = 0x00; 
        send_data_buf[3] = 0x00;
        send_data_buf[4] = (send_data_buf[1] + send_data_buf[2] + send_data_buf[3]) & 0xff;

      }
      work_finish = 0;
      //串口发送数据
      if ( xSemaphoreTake( xSerialSemaphore, ( TickType_t ) 5 ) == pdTRUE )
      {
        Serial.write(send_data_buf, 6);
        xSemaphoreGive( xSerialSemaphore );
      }
      
    }
    vTaskDelay(1);//15*n  ms
  }
}

验证程序。
视频

真实驱动装机测试,控制板内部控制出5个币

视频

好啦,完成硬件编程再来用万能的python搞搞软件,废话不说直接上马。

python开发1号进程部分源码


import socketserver
import socket
import struct
from socketserver import StreamRequestHandler as SRH
from time import ctime
import time

from cbj_serial_data_pkg import CBJ_SerialDataPkg
from cbj_serial_data_pkg_parser import CBJ_SerialDataPkg_Parser
from serails_mng import SerialOP
from event import *

from jsonconfig import loadConfig
from base64_json_pkg import Base64JsonPkgData
from fffe_pkg_parser import FFFEPkgParser
import json
from log_saver import *
logging_init("TCP_SERVER","logs")

tcp_server_host = '127.0.0.1'
tcp_server_port = 9999
serials_config=["COM21", 112500]#
#外部参数加载
setting=loadConfig("config.json")
tcp_server_host=setting['tcp_server_host']
tcp_server_port=setting['tcp_server_port']
serials_config=setting['serials_config']

#串口数据包解析
serial_pkg = CBJ_SerialDataPkg_Parser()
#base64 json 数据转换
b64json_data=Base64JsonPkgData()
#socket 包解析
skt_pgk=FFFEPkgParser()

#dat_arr=b64json_data.encodeToPkg("{\"abc\":\"我们\",\"o\":{\"abc\":456}}")
#启动串口设备并检查通讯

#
queue = []
def closeallsock():
    print("closeallsock", queue)
    for sockhandle in queue:
        sock=sockhandle.request
        print(sock)
        sockhandle.izclosed = 1
        sock.shutdown(2)
        sock.close()

    for sockhandle in queue:
        try:
            queue.remove(sockhandle)
        except ValueError:
            pass
def sendallsock(data):
    for sockhandle in queue:
        try:
            sockhandle.wfile.write(data)
        except ValueError:
            pass

wait_bringup=0
# 接收到串口发来的数据包
def on_serial_data_ok(e):
    # 判断serial数据
    # 创建分发json数据
    # print(senddata)
    # 分发给客户端
    d=e.data
    jsondata={}
    if(d[0]==0xF0):
        #状态
        jsondata["action"]="status"
        jsondata["data"] = d[2]
        global wait_bringup
        wait_bringup=1
        pass
    if (d[0] == 0xF1):
        # 完成反馈状态
        jsondata["data"] = ((d[1]&0x0F)<<8)+d[2]
        if(d[1] ==0):
            jsondata["action"] = "workstart"
        elif (d[1]&0xF0 == 0xF0):
            #全部
            jsondata["action"] = "workfinish"
        elif(d[1]&0xF0 == 0xE0):
            #部分
            jsondata["action"] = "worktimeout"

    log_info("from serial:%s"%str(jsondata))
    # 转换为base64串
    arr_b64 = b64json_data.encodeObjToPkg(jsondata)
    # 转换为socket包FFxxxxFE
    skt_data = skt_pgk.makeToPkg(arr_b64)
    #print(arr_b64)
    sendallsock(skt_data)
    pass


#接收到客户端发来的数据包
def on_skt_data_ok(e):

    #print("on_skt_data_ok",e.data)
    #解析json数据
    err, jsondata = b64json_data.decodeFromPkg(e.data)
    #print(err)
    #print(jsondata)
    if(err==0):
        log_info("from sktclient:%s" % str(jsondata))
        #print("action" in jsondata)
        #print(jsondata["action"]=="getwork")
        if "action" in jsondata :
            if(jsondata["action"] == "getwork" ):
                if "data" in jsondata:
                    if(jsondata["data"] > 0):
                        print("出币动作",jsondata["data"])
                        sop.d.setDataToOutCorn(jsondata["data"])
                        sop.serialSend()
                        pass
            elif(jsondata["action"] == "getstatus"):
                sop.d.setDataToCheckStatus()
                sop.serialSend()
                pass

    #创建下发数据
    #dat_arr = b64json_data.encodeToPkg(json.dumps(jsondata))
    # sop.testCreateDataToSend()
    # sop.serialSendData([0xff,0xf0,0,0,0xf0,0xfe])
    #下发

    pass
#监听
serial_pkg.add_event_listener(PKGEvent.PKG_DATA_OK, on_serial_data_ok)
skt_pgk.add_event_listener(PKGEvent.PKG_DATA_OK, on_skt_data_ok)
sop = SerialOP(serials_config[0], serials_config[1],serial_pkg)
#TCP服务端
class Servers(SRH):
    def handle(self):
        self.izclosed=0
        log_info('got connection from %s'% str(self.client_address))
        #self.request.setblocking(0)#设置为非组赛模式
        strdata = 'connection %s:%s at %s succeed!' % (tcp_server_host, tcp_server_port, ctime())
        self.wfile.write(strdata.encode())
        queue.append(self)
        while True:
            try:
                data = self.request.recv(8192)  # .decode()
                #print(data)
            except socket.error:
                self.request.shutdown(2)
                self.request.close()
                print("eeee")
                break

            dataLen = len(data)
            #print(dataLen,data)
            if (dataLen > 0):
                '''if(dataLen>pkg.pkgLen):
                    ndata=data[dataLen-pkg.pkgLen:dataLen]
                    print("!!! Too Many Data, Ignore length:%d" % (dataLen-pkg.pkgLen))
                    dataLen = pkg.pkgLen
                else:
                    ndata = data'''
                ndata = data
                arraydata = struct.unpack(str(dataLen) + "B", ndata)
                skt_pgk.writeArrayToBuf(arraydata)
                #print("Recv Total length:%d" % dataLen, len(arraydata))
            else:
                self.request.shutdown(2)
                self.request.close()
                print("empty")
                break

        log_info(' %s closed' % str(self.client_address))
        queue.remove(self)

if __name__ == '__main__':
    log_info("正在等待出币机启动")
    while (wait_bringup == 0):
        print(wait_bringup)
        time.sleep(1)
    log_info("出币机启动成功")
    tst=0
    if(tst):
        print("测试开始")

        #测试
        #创建json
        jsondata = {}
        jsondata["action"] = "getstatus"
        jsondata["data"] = 22
        #转换为base64串
        arr_b64=b64json_data.encodeObjToPkg(jsondata)
        #转换为socket包FFxxxxFE
        skt_data=skt_pgk.makeToPkg(arr_b64)
        print(skt_data)
        # 模拟接收
        #skt_pgk.writeArrayToBuf([1, 254, 255, 3, 43, 254, 3, 41, 254, 255, 3, 43, 254, 3, 41, 254, 255, 3, 43, 254, 255, 8])
        #skt_pgk.writeArrayToBuf([255,101, 121, 74, 104, 89, 109, 77, 105, 79, 105, 76, 109, 105, 74, 72, 107, 117, 54, 119, 105, 76, 67, 74, 118, 73, 106, 112, 55, 73, 109, 70, 105, 89, 121, 73, 54, 78, 68, 85, 50, 102, 88, 48, 61,254])
        skt_pgk.writeArrayToBuf(skt_data)


    log_info("server is running....")
    server = socketserver.ThreadingTCPServer((tcp_server_host, tcp_server_port), Servers)
    server.daemon_threads=True
    server.serve_forever()
    closeallsock()


2号进程部分源码

#coding=utf-8
import socket,select,threading,sys
import struct
from event import *
from base64_json_pkg import Base64JsonPkgData
from fffe_pkg_parser import FFFEPkgParser
#from web_api import *
import web_api
from jsonconfig import loadConfig
import input_qrcode_str
import input_qrcode_recver
from log_saver import *
import sound_player
import string
logging_init("TCP_CLIENT","logs")
# 外部参数加载
setting = loadConfig("config.json")
#print (setting["url_ck"],setting["url_lk"],setting["url_sv"])
web_api.url_ck=setting["url_ck"]
web_api.url_lk=setting["url_lk"]
web_api.url_sv=setting["url_sv"]
web_api.url_ad=setting["url_ad"]
#存币使能
enable_add=setting["enable_add"]
#print(web_api.url_ck, web_api.url_lk)
client_addr = (setting["tcp_server_host"],setting["tcp_server_port"])  # equals server_addr()
#print(client_addr)
CBJ_id=setting["CBJ_id"]
log_info(CBJ_id)


no_error=True
scan_is_ok=False
action_status=0
corn_cont_send=0
corn_count=0
qr_code="67acc2862ffb3fb3f0219eec2569df0fa88bdd4714f161f57e81f1d41e90311645a9d498b916d250"
qr_code="67acc2862ffb3fb32d0ba295b7ec2916a88bdd4714f161f54965d8aa8986d75845a9d498b916d250"
remark= "出币反馈"
#监听二维码输入事件
def on_qr_ok(e):
    global enable_add
    global qr_code
    global scan_is_ok
    global action_status
    log_info("接收二维码"+str(e.data))
    if(not scan_is_ok):
        log_info("系统开始响应")
        qr_code=e.data#"67acc2862ffb3fb32d0ba295b7ec2916a88bdd4714f161f54965d8aa8986d75845a9d498b916d250"

        #这里添加了新的规则  add$  开头的码为存币请求
        #检查是否是存币码,如果是action_status=1000进入存币流程
        ck=qr_code.split("$", 1)
        if (len(ck)==2 and ck[0]=="add" and enable_add):
            action_status=1000

        scan_is_ok=True
    else:
        log_info("系统忙,请稍后扫码")
input_qrcode_recver.input_qr_EVT.add_event_listener(PKGEvent.PKG_DATA_OK, on_qr_ok)

#检查CBJ状态反馈事件
checkBusyEVT=EventDispatcher()
def on_not_busy_ok(e):
    global action_status
    log_info("出币空闲状态"+str(e.data))
    if(scan_is_ok and action_status==1):
        action_status=2
checkBusyEVT.add_event_listener(PKGEvent.PKG_DATA_OK, on_not_busy_ok)


# base64 json 数据转换
b64json_data = Base64JsonPkgData()
# socket 包解析
skt_pgk = FFFEPkgParser()

#接收到服务端发来的数据包
def on_skt_data_ok(e):
    global action_status
    global corn_count
    global checkBusyEVT
    #print("on_skt_data_ok",e.data)
    #解析json数据
    err, jsondata = b64json_data.decodeFromPkg(e.data)
    #print(err)
    #print(jsondata)
    if(err==0):
        #print("action" in jsondata)
        #print(jsondata["action"]=="getwork")
        if "action" in jsondata :
            if(jsondata["action"] == "workstart" ):
                if "data" in jsondata:
                    if(jsondata["data"] > 0):
                        log_info("正在执行出币动作"+str(jsondata["data"]))
                        action_status=10
                pass
            elif(jsondata["action"] == "workfinish"):
                if "data" in jsondata:
                    log_info("成功完成执行出币动作"+str(jsondata["data"]))
                    action_status=100
                    corn_count=jsondata["data"]
                pass
            elif (jsondata["action"] == "worktimeout"):
                if "data" in jsondata:
                    log_info("超时,部分出币动作完成"+str(jsondata["data"]))
                    action_status=101
                    corn_count = jsondata["data"]
                pass
            elif (jsondata["action"] == "status"):
                if "data" in jsondata:
                    log_info("出币机状态"+str(jsondata["data"]))
                    if(jsondata["data"]==0):
                        checkBusyEVT.dispatch_event(PKGEvent(PKGEvent.PKG_DATA_OK, 0))
                pass
#监听
skt_pgk.add_event_listener(PKGEvent.PKG_DATA_OK, on_skt_data_ok)
# 倾听其他成员谈话
def listening(cs):
    global no_error
    inputs = [cs]
    while True:
        rlist,wlist,elist = select.select(inputs, [], [],0.5)
        # client socket就是用来收发数据的, 由于只有这个waitable 对象, 所以不必迭代
        if cs in rlist:
            try:
                # 打印从服务器收到的数据
                data=cs.recv(1024)
            except socket.error:
                no_error = False
                log_error("socket is error")
                exit()

            dataLen = len(data)
            if (dataLen > 0):
                ndata = data
                arraydata = struct.unpack(str(dataLen) + "B", ndata)
                skt_pgk.writeArrayToBuf(arraydata)
                #print("Recv Total length:%d" % dataLen, len(arraydata))

# 发言
def speak(cs):
    global no_error
    while True:
        try:
            data = input()
        except Exception as e:
            log_error( "can't input")
            no_error = False
            exit()
        # if data == "exit":
        #     cs.close()
        #     break
        try:
            cs.send(data.encode())
        except Exception as e:
            no_error = False
            exit()


def main():
    import time
    # client socket
    cs = socket.socket()
    notconn=1
    while(notconn):
        try:
            cs.connect(client_addr)
            log_info("连接服务器成功")
            notconn=0
        except:
            log_info("无法连接服务器,,,10秒后重连")
            time.sleep(10)

    # 分别启动听和说线程
    t = threading.Thread(target=listening,args=(cs,))  # 注意当元组中只有一个元素的时候需要这样写, 否则会被认为是其原来的类型
    t.daemon=True
    t.start()

    t1 = threading.Thread(target=speak,args=(cs,))
    t1.daemon=True
    t1.start()


    # 创建json
    jsondata = {}
    jsondata["action"] = "getstatus"
    jsondata["data"] = 22
    # 转换为base64串
    arr_b64 = b64json_data.encodeObjToPkg(jsondata)
    # 转换为socket包FFxxxxFE
    skt_data = skt_pgk.makeToPkg(arr_b64)
    slp=0.5
    global scan_is_ok
    global action_status
    global corn_cont_send
    #接口参数需要

    global qr_code
    global corn_count
    global remark
    global CBJ_id
    while no_error:
        if(not scan_is_ok):
            #67acc2862ffb3fb32d0ba295b7ec2916a88bdd4714f161f54965d8aa8986d75845a9d498b916d250
            # print("等待扫描")
            time.sleep(slp)
        else:
            if(action_status==0):
                # 根据二维码准备好数据
                action_status=1
                # 创建json发送验出币机是否空闲的请求
                jsondata = {}
                jsondata["action"] = "getstatus"
                jsondata["data"] = 0
                # 转换为base64串
                arr_b64 = b64json_data.encodeObjToPkg(jsondata)
                # 转换为socket包FFxxxxFE
                skt_data = skt_pgk.makeToPkg(arr_b64)
                cs.send(skt_data)
            elif(action_status==1):
                time.sleep(slp)
                #等待出币机回答是否空闲的状态
            elif(action_status==2):
                action_status = 3
                time.sleep(slp)
                #进入网络验证
                log_info("调用网络验证")
                d = {'data': qr_code}
                web_api.do_start_actions(web_api.url_ck, web_api.url_lk,None,d)

            elif(action_status==3):
                #等待网络验证
                time.sleep(slp)
            elif (action_status == 4):
                action_status = 5
                # 等待网络验证
                jsondata = {}
                jsondata["action"] = "getwork"
                jsondata["data"] = corn_cont_send
                # 转换为base64串
                arr_b64 = b64json_data.encodeObjToPkg(jsondata)
                # 转换为socket包FFxxxxFE
                skt_data = skt_pgk.makeToPkg(arr_b64)
                cs.send(skt_data)
            elif (action_status == 5):
                # 等待出币
                time.sleep(slp)
            elif (action_status == 10):
                # 等待出币
                time.sleep(slp)
            elif (action_status == 100 or action_status == 101 ):
                action_status = 200
                log_info("调用发送出币结果")

                d = {'data': qr_code, 'mid': CBJ_id, 'status': 1, 'coTotal': corn_count, 'remark': remark}
                web_api.do_stop_actions(web_api.url_sv, None, d)

                time.sleep(slp)
            elif (action_status == 200):
                # 等待保存
                time.sleep(slp)
            elif (action_status >= 500 and action_status < 1000):
                # 取币流程结束保存结束
                action_status =0
                scan_is_ok=0
                time.sleep(slp)
            elif (action_status ==1000):
                action_status = 1001
                # 调用存币接口
                log_info("正在请求存入一个币")
                tst_post_data_ad = {'data': qr_code, 'coTotal': 1}
                web_api.do_add_actions(web_api.url_ad, None, tst_post_data_ad)

            elif (action_status ==1001):
                #等待存币返回
                time.sleep(slp)
            elif (action_status >=1500  and action_status < 2000):
                #存币流程结束成功
                action_status = 0
                scan_is_ok = 0
                time.sleep(slp)
        #cs.send(skt_data)
        #time.sleep(slp)

        #time.sleep(slp)
    cs.shutdown(socket.SHUT_RDWR)
    cs.close()

def on_start_ok(e):
    global action_status
    global corn_cont_send
    log_info("main通知下位机出币"+str(e.data))
    corn_cont_send=e.data[2]
    action_status = 4

web_api.startActionEVT.add_event_listener(PKGEvent.PKG_DATA_OK, on_start_ok)
def on_stop_ok(e):
    global action_status
    global corn_cont_send
    global scan_is_ok
    log_info("main通知重新开始"+str(e.data))
    #这里可以根据返回的data做出响应
    code_num=e.data[0]


    if (code_num >= 10000 and code_num < 20000):
        sound_player.playSoundByCodeNumber(code_num)  # 声音反馈
        action_status = 500
        pass
    elif(code_num >= 20000 and code_num < 30000):
        sound_player.playSoundByCodeNumber(code_num)  # 声音反馈
        action_status = 500
        pass
    elif (code_num >= 30000 and code_num < 40000):
        sound_player.playSoundByCodeNumber(code_num)  # 声音反馈
        action_status = 500
        pass
    elif (code_num >= 40000 ):
        if(code_num == 40000 ):
            if(int(e.data[2])>0):
                sound_player.playSoundByCodeNumber(40001)  # 声音反馈
                log_info("存入%s个币" % e.data[2])
            else:
                sound_player.playSoundByCodeNumber(40002)  # 声音反馈
                log_info("0效果的存币操作")
        else:
            sound_player.playSoundByCodeNumber(40003)  # 声音反馈
            log_info("存币操作错误")
            pass
        action_status = 1500

    else:
        action_status = 500
    #scan_is_ok = False

web_api.stopActionEVT.add_event_listener(PKGEvent.PKG_DATA_OK, on_stop_ok)


if __name__ == "__main__":
    #global scan_is_ok
    scan_is_ok=False
    log_info("创建接收二维码扫描线程")

    #t = threading.Thread(target=input_qrcode_str.listening_input)
    t = threading.Thread(target=input_qrcode_recver.recv_input_str)
    t.daemon = True
    t.start()
    log_info("创建客户端线程")
    main()

3号进程源码

import mmap
import contextlib
import time
import file_lock
def init_file():
    try:

        f = open("test1.dat", "w")
        file_lock.lock(f, file_lock.LOCK_EX)
        f.write('\x00' * 1024)

        file_lock.unlock(f)
        f.close()
    except Exception as ex:
        print("Error Info: %s" % (str(ex)))
    finally:
        print("init ok")
        f.close()
init_file()
print("-----")
time.sleep(1)
#with open("test1.dat", "w") as f:
    #f.write('\x00' * 1024)
while 1:
    #with open('test1.dat', 'r+') as f:

    try:
        data = input("please input:\n")

        f = open("test1.dat", "r+")
        file_lock.lock(f, file_lock.LOCK_EX)
        #print(f.fileno())

        with contextlib.closing(mmap.mmap(f.fileno(), 1024, access=mmap.ACCESS_WRITE)) as m:
            #print(data)
            m.seek(0)
            s = "" + data
            #buf = 1024*b'0'#相当于bytes(1024) ,不可修改
            buf=bytearray(1024)#可修改
            a=s.encode("utf-8")
            buf[:len(a)]=a
            #print(buf)

            m.write(buf)
            m.flush()
    except Exception as ex:
        print("Error Info: %s" % (str(ex)))
    finally:

        file_lock.unlock(f)
        f.close()
        print("input ok")

    time.sleep(2)

软硬件联调测试没问题,开启批量生产模式。

批量焊接控制板改主板。


由于第一块拆机的主板长时间运行发现mos管有漏电,单片机给了信号也拉不低,出现电机不转的情况,不能因小失大,毁了活动现场就杯具了,干脆每块板换了mos管。


增加了光眼上拉电阻,消抖电容,运行时我心里更踏实一些。


最后原来主板按键的接口改为跟控制板连接的接口,这样免去了在主板打孔焊针的工作。

做了两天的手工,十几个主控主板终于完工


机器的外壳很重要,要高端大气上档次,选用稳重的黑色喷涂。满足人机工程学,内部还要有合理的布局。
购置好了电料,直奔工厂车间安装。

工厂内逐一进行系统调试。

一次一个视频

开始组装,接线拧螺丝。干到手抽筋。

新烤好的蛋糕出炉啦!


质检环节,调试出币视频

打包物流发出啦!

宝贝诞生了!你的职责就是助力领克车展活动,你我都是在默默无闻的幕后工作,共同加油!希望大家能喜欢,谢谢!

发布了78 篇原创文章 · 获赞 76 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/qq_38288618/article/details/89484503
max