• 本部分源码放置于

3.2.1 机械结构

lock2boxS

图10 箱体开合两种状态

机械结构的主体由锁体、锁片、锁舌三部分组成,材料选择7系铝合金,由铣床和线切割机加工制成。微型直流电机轴上齿轮与锁片上的齿条啮合,电机旋转带动锁片移动,使锁舌固定或脱离锁体,达到开解锁的目的。

lock2statuS

图11 机械结构示意图

电机采用6mm空心杯电机,带行星齿轮减速箱,减速比达到700:1,使减速后的出轴转速为47 rpm,扭矩200g/cm,能输出足够的力带动锁片。

motor

图12 电机参数与结构

3.2.2 电路结构

ele

图13 电路结构简图

设备端统一使用3.3V DC供电,霍尔传感器、蜂鸣器、电机驱动IC与ESP8266的GPIO引脚相连,霍尔传感器负责检测锁舌上的磁铁磁性以确定锁止状态,无源蜂鸣器负责在配网状态时发出提示音,电机驱动IC负责控制电机正反转从而实现开解锁。

a3144

图14 A3144开关霍尔传感器

设计选用A3144E开关霍尔传感器,当锁舌上的磁铁S面靠近传感器霍尔面时,输出低电平,相反的无感应时输出高电平。

mx1508

图15 MX1508典型应用图

MX1508是双路有刷直流马达驱动IC,该IC采用H 桥电路结构设计,低导通内阻MOSFET 功率开关管,内部集成续流二极管和带迟滞效应的过热保护电路 (TSD)。

表3-1 MX1508 逻辑真值表

INAx INBx OUTAx OUTBx 功能
L L Z Z 待机
H L H L 正转
L H L H 反转
H H L L 刹车

3.2.3 程序说明

ESP8266的Arduino框架提供基于SPIFFS的文件系统,SPIFFS专为小型系统设计用于嵌入式目标的SPI闪存设备,而在框架所规划的Flash布局中,文件系统与程序存储在同一闪存芯片上,烧写程序并不会修改文件系统内容。而且框架还提供借助Flash模拟的EEPROM,位于SPIFFS之后的闪存扇区。7

flash

图16 Arduino环境中使用的Flash布局

在本设计中,使用SPIFFS存放设备编号、设备MQTT认证密码以及服务器地址(AuthInfo.json文件,以JSON格式存储),使用EEPROM存放WiFi连接信息。这样烧录新设备或设备信息、服务器信息发生变化时,只需使用ESP8266FS工具将AuthInfo.json文件写入SPIFFS,不影响固件程序的正常使用,能做到指令与数据的分离。
设备上电启动后,调用EEPROM并读取第一位是否为空以此来判断有无WiFi连接信息,若无WiFi连接信息则进入SmartConfig配网模式,若存在WiFi连接信息则进入连接模式。在连接模式中读取WiFi连接信息并尝试连接,如果30s后未成功连接再转入SmartConfig配网模式,配网模式30s未成功配网再转回连接模式,如此循环。在配网模式下,蜂鸣器发出提示音提醒用户使用微信AirKiss配网,并在成功配网后保存WiFi连接信息,延迟1s后重启单片机,以此来解决配网后DHCP拿不到IP地址的Bug。
网络连接完毕后,紧接着读取SPIFFS中的AuthInfo.json文件,设置MQTT协议连接的相关参数,MQTT协议连接依靠PubSubClient库实现[ ]。这样就完成了setup阶段。

部分代码展示

void smartConfig()
{
  // 在STA模式下启动SmartConfig
  WiFi.mode(WIFI_STA);
  Serial.println("\r\nWait for Smartconfig");
  WiFi.beginSmartConfig();
  int i = 0;
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(1000);
    Serial.print(".");
    // 过程中播放提示音
    tonePlay();
    i+= 1;
    if (i==20) { return; }
  }
  // SmartConfig成功 保存WiFi信息并重启
  if (WiFi.smartConfigDone())
  {
    Serial.println("SmartConfig Success");
    Serial.printf("SSID:%s\r\n", WiFi.SSID().c_str());
    Serial.printf("PSW:%s\r\n", WiFi.psk().c_str());
    Serial.println(WiFi.localIP());

    strcpy(config.ssid, WiFi.SSID().c_str());
    strcpy(config.psw, WiFi.psk().c_str());
    saveConfig();

    delay(1000);
    ESP.restart();
  }
}

bool beginConfig()
{
  // STA模式 取出WiFi信息并尝试连接
  WiFi.mode(WIFI_STA);
  loadConfig();
  Serial.printf("SSID:%s\r\n", config.ssid);
  Serial.printf("PSW:%s\r\n", config.psw);
  Serial.println("\r\WiFi begin");
  WiFi.begin(config.ssid, config.psw);
  int i = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    i += 1;
    if (i==60) { return 0; }
  }
  return 1;
}

bool loadAuthInfo()
{
  // 从SPIFFS中打开AuthInfo.json文件
  if (!SPIFFS.begin())
  {
    Serial.println("Failed to mount file system");
    return false;
  }

  File InfoFile = SPIFFS.open("/AuthInfo.json", "r");
  if (!InfoFile) {
    Serial.println("Failed to open config file");
    return false;
  }

  size_t size = InfoFile.size();
  if (size > 1024) {
    Serial.println("Config file size is too large");
    return false;
  }

  // 分配一个缓冲区 并将文件内容载入
  std::unique_ptr<char[]> buf(new char[size]);
  InfoFile.readBytes(buf.get(), size);

  // StaticJsonBuffer 静态分配内存(in the stack) DynamicJsonBuffer 动态分配内存(on the heap)
  // DynamicJsonBuffer  jsonBuffer;
  StaticJsonBuffer<200> jsonBuffer;
  JsonObject& json = jsonBuffer.parseObject(buf.get());

  if (!json.success()) {
    Serial.println("Failed to parse config file");
    return false;
  }

  // json解码 取出数据
  mqtt_server = json["mqtt_server"];
  String n = json["lockno"];
  lockno = (char *)malloc(n.length() * sizeof(char));
  strcpy(lockno, n.c_str());
  String p = json["lpassword"];
  lpassword = (char *)malloc(p.length() * sizeof(char));
  strcpy(lpassword, p.c_str());

  return true;
}

void setup()
{
  // 打开串口通讯 用于调试
  Serial.begin(115200);
  Serial.println("Start module");

  // 输出VCC电压 用于调试
  Serial.print("System voltage(mV): ");
  Serial.println(ESP.getVcc());

  // 接口定义
  // pinMode(ClearPin, INPUT_PULLUP);
  pinMode(HallPin, INPUT_PULLUP);
  pinMode(M_IN1, OUTPUT);
  pinMode(M_IN2, OUTPUT);

  // 进入连接网络循环
  EEPROM.begin(1024);
  // attachInterrupt(digitalPinToInterrupt(ClearPin), clearConfig , FALLING);
  while(EEPROM.read(0) == 0) 
  { 
    smartConfig();
  }

  while(!beginConfig())
  {
    smartConfig();
  }

  Serial.println(WiFi.localIP());

  // 载入SPIFFS存储的数据
  if (!loadAuthInfo()) {
    Serial.println("Failed to load config");
    ESP.restart();
  } else {
    Serial.println("Config loaded");
  }

  //设置MQTT连接参数
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

程序进入loop阶段后与云端MQTT Broker建立连接并订阅 /lockno/call ,根据接收到call指令完成相应动作:执行开锁、报告状态、OTA固件更新。开关锁动作通过改变电机驱动IC(MX1508)对应引脚的电平进而使电机转动来实现,定时执行的readval函数负责检测霍尔传感器以报告锁止状态,并在锁舌插入后自动执行关锁动作。OTA(Over the Air)更新指不通过串行端口烧录而直接用WiFi在随时随地实现固件更新的过程[ ],在本设计中的思路是通过下达OTA固件更新指令在指定时间点完成全网设备统一固件更新。

ota

图17 OTA固件更新过程 - 内存视图

部分代码展示

void callback(char* topic, byte* payload, unsigned int length) {
  // 对接收到的命令判断处理
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  switch(payload[0])
  {
    case 'u':
      UNLOCK();
      break;
    case 'e':
      EXPORT();
      break;
    case 'o':
      OTA();
      break;
  }
}

void reconnect() {
  while (!client.connected()) 
  {
    Serial.print("Attempting MQTT connection...");
    // MQTT连接凭据和遗嘱消息
    if (client.connect(lockno,lockno,lpassword,buildTopic("statu"),0,1,"-2")) 
    {
      // 上线订阅call并输出状态
      Serial.println("connected");
      client.subscribe(buildTopic("call"));
   // client.subscribe(buildTopic("ping"));
   // EXPORT();
      readval();
    } 
    else 
    {
      // 若因网络问题无法上线 则重连WiFi
      Serial.print("failed, rc=");
      Serial.println(client.state());
      if (client.state() == -2)
      {
        WiFi.reconnect();
        Serial.println(WiFi.localIP());
      }
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void UNLOCK()
{
  // 执行解锁
  Serial.println("UNLOCK");
  digitalWrite(M_IN1,LOW);
  digitalWrite(M_IN2,HIGH);
  delay(250);
  digitalWrite(M_IN1,LOW);
  digitalWrite(M_IN2,LOW);
}

void EXPORT()
{
  // 输出状态
  Serial.println("EXPORT");
  char* sc = (char *)malloc(1 * sizeof(char));
  sprintf(sc, "%d", statu);
  client.publish(buildTopic("statu"), sc, true);
}

void OTA()
{
  // OTA更新
  t_httpUpdate_return ret = ESPhttpUpdate.update("http://****/lock/firmware");
  switch(ret) {
    case HTTP_UPDATE_FAILED:
      Serial.println("[update] Update failed.");
      break;
    case HTTP_UPDATE_OK:
      Serial.println("[update] Update ok.");
      break;
  }
}

void readval()
{
  // 采集霍尔传感器状态
  int Hall = 0;
  for (int i=0;i<5;i++) { 
    Hall += digitalRead(HallPin);
    delay(10);
  }
  if (Hall == 5) {
  // Serial.println("1*5,UNLOCK");
    if (statu != 0) {
      statu = 0;
      EXPORT();
    }
    return ;
  }
  if (Hall == 0) {
  // Serial.println("0*5,LOCK");
    if (statu != 1) {
      if (statu == 0) {
        // 执行上锁
        digitalWrite(M_IN1,HIGH);
        digitalWrite(M_IN2,LOW);
        delay(250);
        digitalWrite(M_IN1,LOW);
        digitalWrite(M_IN2,LOW);
      }
      statu = 1;
      EXPORT();
    }
    return ;
  }
}

void loop()
{
  // 建立MQTT连接 处理传入消息
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  // 定时执行readval
long now = millis();
  if (now - lastMsg > 1000) {
     lastMsg = now;
     readval();
  }  
}