基于RFID和Raspberry Pi的雨伞租赁系统设计

  • 这篇文章是机器人实验课的项目记录。这个项目的目标是制作一个校园雨伞租赁系统,师生只需要刷校园卡就可以从伞架借走一把伞,还伞的时候只需要刷卡就可以归还。功能看起来很简单,但让这个系统稳定合理的工作却花了我不少时间。

## 系统的工作流程 1. 用户需要在借伞前注册自己的IC卡信息,否则会在刷卡时提示该用户未注册,不能享受此服务。用户刷卡后读取IC卡信息,当确定该用户没有恶意不还伞记录并且之前没有借过伞后,会同意该用户的借伞请求,打开锁伞设备的开关,记录该用户已借伞。 2. 用户刷卡还伞时,同样打开开关,提示用户将伞放入设备内,确认放入后,关闭开关,此时若有开关无法正常闭合,或者检测出设备内物品与标准伞有出入,会提示用户重新放伞,直到检测到标准伞正确放入,记录用户已还伞。 3. 若用户在还伞截止时间前并未还伞,则将该用户列入黑名单,不再为该用户提供借伞服务。

P.S 目前还伞的检测还没有做,现在是还伞刷一下卡系统开锁,开完锁就会认为你把伞还了,我相信大家还是比较有素质的(´・_・`),附上一张很早以前画的流程图: 流程图

系统的硬件设计

硬件组成

  • Raspberry 2B+
  • RFID读卡模块
  • 稳压电源模块
  • 电磁铁

电路设计

由于Raspberry Pi本身已经集成了各种各样的引脚功能,所以电路设计上就没有很多值得提的地方。
220v交流电压输入经过稳压电路得到5v直流电压给Raspberry Pi和RFID读卡模块供电,12v直流电压提供给电磁铁。Raspberry Pi提供3.3v的高电平控制MOS管开关电磁铁回路。我主要负责软件,这些电路的事情就交给靠谱的小伙伴们了。

系统的软件设计

  • 软件设计部分是我负责的,所以我尽量说的详细一些。

软件环境

  • ubuntu 14.04 for ARM
  • wiringPi
  • MySQL

系统工作流程的再探究

租赁系统看起来很简单,要弄清楚的无非是下面的这几个问题:

  1. 来者何人?
  2. 借还是还?
  3. 能借给他不?

首先我们分析第一个问题:判断用户身份由很多种方法,我们选择了较为经济的一种方式—在食堂,水房,门禁随处可见的射频读卡技术。我们只需要阅读购买的RFID读卡模块的使用文档,由串口即可获得ID的信息,通过比对ID,第一个问题解决了。
第二个问题看似很无脑,但事实上正是为了可靠的解决第二个问题,我们需要多写50行左右的代码使用数据库。解释下使用数据库的原因:数据库的数据是存在硬盘(Raspi是SD卡)里的,而程序运行过程中开辟的空间是存在RAM里面的。如果我们不能把用户的借出归还标志存入磁盘,断电重启系统后会丢失数据。举个例子:老王昨天借了把伞,借完就停电了。第二天准备去还伞的老王看到机器又扔给他一把伞。这显然是需要避免的情况。而数据库技术很好的解决了这个问题。
第三个问题讲的是规则设定。在我们的系统中借伞时间超过两天则会被视为违规,取消你借伞的权利。为了便于课上演示,在实际程序中我调用的系统时间是分钟级,也就是借伞超过两分钟你就被ban了。
### 数据库的设计 数据库选用了比较流行的MySQL,具有良好的C语言接口。下面是数据库的建立过程: 首先在终端中连接数据库,并创建umbrella_db的数据库。

mysql -u root -p
create database umbrella_db;

由于下面的这个语句比较长,所以我们写一个小小的脚本来生成users表,建立createtable.sql文件,内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
create table users
(
id_0 int unsigned not null auto_increment primary key,
id_1 int unsigned not null,
id_2 int unsigned not null,
id_3 int unsigned not null,
id_4 int unsigned not null,
id_5 int unsigned not null,
id_6 int unsigned not null,
id_7 int unsigned not null,
id_8 int unsigned not null,
id_9 int unsigned not null,
id_10 int unsigned not null,
id_11 int unsigned not null,
id_12 int unsigned not null,
name char(8) not null,
return_state int unsigned not null,
time int unsigned not null
);

之后在MySQL控制台输入source createtable.sql即可,注意文件路径。
建立成功后的表单如图所示:
MySQL

程序设计

为了可能存在的读者按顺序贴代码,便于有兴趣的朋友验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <time.h>
#include <mysql/mysql.h>
#include <stdlib.h>
#include <string.h>
#include <wiringPi.h>
#include <wiringSerial.h>
#define ID_LENTH 12
MYSQL mysql;
MYSQL_RES *res = NULL;
MYSQL_ROW row;
int user_num;
struct User{
int userID[ID_LENTH];
char *returnFlag;
char *name;
int time;
}User[100];

这里是代码的头文件和变量定义部分,由于这个系统实在是太小了,没有采用分文件的形式,或者cpp面向对象的编程风格。time.h包含了我们需要使用的操作系统时间的函数。wiringPi.hwiringSerial.h是raspi的函数库,可以它们来控制pi的GPIO和串口功能,用于读卡和电磁铁的开关电路控制,也是软件和硬件之间的桥梁。mysql.h提供了丰富的MySQL数据库的C语言APi,我们可以方便的通过C语言操作数据库。
全局变量主要有一个用户结构体:包含了用户ID、借还状态、姓名、借出时间四个成员。还有一些MySQL的结构体。他们使用全局变量的原因在于几乎所有的函数都在使用他们,在函数间传递反而会让程序变的冗长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int idAuth(int *testID) {
int i,j,k = 0;
for(j = 0; j < user_num; j++) {
for(i = 0; i < ID_LENTH; i++) {
if(*(User[j].userID+i) != *(testID+i)) {
k = 0;
break;
}
k++;
}
if(k == ID_LENTH)
break;
}
if(k == ID_LENTH)
return j;
else
return -1;
}

这里是解决上述所谓问题1的代码,传入值为一个指针指向数组的首地址。这个值将由串口读回的数据传递。在系统启动时将数据库中的用户信息加载入内存,存入User[]数组结构体。这个函数遍历了所有数据库中用户的ID并和传入的数组比较,若配对成功则返回用户编号,配对失败返回-1。

1
2
3
4
5
6
int timeAuth(int time_O,int time_I) {
if(((time_I - time_O < 0 && time_I > 2) || time_I - time_O > 2) && time_O != 1000)
return -1;
else
return 0;
}

这是解决所谓问题3的代码,传入两个参数,借伞时间和还伞时间。若还伞时间与借伞时间相差的数值大与2,则认为违规。在tm结构体中p->tm_min表示离当前小时0分钟的时间。若遇到整点前借伞整点后还伞的情况,两值之差为负数,只验证还伞时间是否大与2即可。我们将用户初始时间均置为1000。在用户时间值为默认的情况下不会认为用户违规。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int db_connect() {
if (NULL == mysql_init(&mysql)) {
printf("mysql_init(): %s\n", mysql_error(&mysql));
return -1;
}
if (NULL == mysql_real_connect(&mysql,
"localhost",
"root",
"ubuntu",
"umbrella_db",
0,
NULL,
0)) {
printf("mysql_real_connect(): %s\n", mysql_error(&mysql));
return -1;
}
printf("Connected to MySQL \n");
}

连接数据库的函数,不用多说,每次对用户进行信息修改的时候都必须调用。如果连接不成功则返回-1并输出连接错误原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int read_db() {
char *query_str = NULL;
int rc,i,j,fields;
int rows;
db_connect();
query_str = "select * from users";
rc = mysql_real_query(&mysql, query_str, strlen(query_str));
if (0 != rc) {
printf("mysql_real_query(): %s\n", mysql_error(&mysql));
return -1;
}
res = mysql_store_result(&mysql);
if (NULL == res) {
printf("mysql_restore_result(): %s\n", mysql_error(&mysql));
return -1;
}
rows = mysql_num_rows(res);
fields = mysql_num_fields(res);
user_num = rows;
printf("The total users is: %d\n", rows);
for(j = 0; j < rows; j++) {
row = mysql_fetch_row(res);
for(i = 1; i < 13; i++) {
*(User[j].userID+i-1) = atoi(row[i]);
User[j].name = row[13];
User[j].returnFlag = row[14];
User[j].time = atoi(row[15]);
}
}
mysql_close(&mysql);
return 0;
}

读数据库信息函数。在程序启动后调用,rc = mysql_real_query(&mysql, query_str, strlen(query_str));提交请求,如果请求成功,则数据库的值会存放在row这个变量中。我们按照之前定义的数据库规则用户结构体赋值。完成后断开连接,返回整型数1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int write_db_re(int id,const char *re_state) {
char *str_1 = NULL;
char *str_2 = NULL;
char *str;
int rc;
str_1 = "UPDATE users SET return_state=";
str_2 = " WHERE id_0=";
char temp[10];
db_connect();
sprintf(temp,"%d",id+1);
strcat(strcat(strcat(strcpy(str,str_1),re_state),str_2),temp);
rc = mysql_real_query(&mysql, str, strlen(str));
if (0 != rc) {
printf("mysql_real_query(): %s\n", mysql_error(&mysql));
return -1;
}
mysql_close(&mysql);
return 0;
}

写数据库用户状态函数。传入参数为要修改的用户序号和用户借还伞的状态。我们需要构造相应的MySQL语句来实现对应的功能。在这里我使用了 strcpy(*str1,*str2) 函数来连接字符串。将完整的MySQL语句分为下面四部分:UPDATE users SET return_state=*re_stateWHERE id_0=id,其中id使用sprintf(temp,"%d",id+1);转换为字符串,连接后提交请求,验证成功后断开数据库连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int write_db_tm(int id,int time) {
char *str_1 = NULL;
char *str_2 = NULL;
char *str;
int rc;
str_1 = "UPDATE users SET time=";
str_2 = " WHERE id_0=";
char temp[10];
char tempNum[10];
db_connect();
sprintf(temp,"%d",id+1);
sprintf(tempNum,"%d",time);
strcat(strcat(strcat(strcpy(str,str_1),tempNum),str_2),temp);
rc = mysql_real_query(&mysql, str, strlen(str));
if (0 != rc) {
printf("mysql_real_query(): %s\n", mysql_error(&mysql));
return -1;
}
mysql_close(&mysql);
return 0;
}

和上面的内容几乎完全一样,只是将借还表示更改为了时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void user_function(int userNum,int reFlag) {
time_t timep;
struct tm *p;
int flag;
int time_I,time_O;
time(&timep);
p = gmtime(&timep);
system("clear");
if(timeAuth(User[userNum].time,p->tm_min) == 0) {
if (reFlag == 0) {
printf("Take Your Umbrella!,%s\n",User[userNum].name);
User[userNum].returnFlag = "1";
if(write_db_tm(userNum,p->tm_min) == 0)
printf("Time has been recorded\n");
if(write_db_re(userNum,"1") == 0)
printf("State has been updated\n");
}
else {
printf("%s:have a nice day!\n",User[userNum].name);
User[userNum].returnFlag = "0";
if(write_db_tm(userNum,1000) ==0 )
printf("Time record has been cleared\n");
if(write_db_re(userNum,"0") == 0)
printf("State has been updated\n");
}
delay(3000);
digitalWrite(userNum,HIGH);
delay(1000);
digitalWrite(userNum,LOW);
}
else {
delay(1000);
printf("Blocked user!\n");
}
}

用户功能部分,也是上面函数综合使用。传入用户序号和借还标志。首先判断用户是否被ban和用户借还伞,若借伞则屏幕显示相应内容,写数据库借伞标志,写借伞时间。若还伞屏幕显示相应内容,数据库写还伞标志,置时间为默认(1000)。结束后调用GPIO功能,打开相应位置的锁扣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int main(){
int fd;
char data[] = {0x03,0x08,0xC1,0x20,0x02,0x00,0x00,0x17};
int readData;
int ID[ID_LENTH];
int i;
wiringPiSetup();
pinMode(0,OUTPUT);
pinMode(1,OUTPUT);
pinMode(2,OUTPUT);
pinMode(3,OUTPUT);
pinMode(4,OUTPUT);
pinMode(5,INPUT);
fd = serialOpen("/dev/ttyAMA0",9600);
if(fd < 0) return 1;
system("clear");
printf("System is Running...\n");
delay(2000);
if(read_db() == 0) {
printf("Database is connected\n");
}
printf("Copyright OUC:Industrial Automation\n");
serialPuts(fd,data);
while(1) {
if(digitalRead(5) == 1) {
for(i = 0; i < ID_LENTH; i++) {
readData = serialGetchar(fd);
*(ID+i) = readData;
}
if(idAuth(ID) == -1){
system("clear");
printf("Permission denied!\n");
digitalWrite(0,LOW);
delay(4000);system("clear");
}
else{
user_function(idAuth(ID),strcmp(User[idAuth(ID)].returnFlag,"0"));
}
}
}
serialClose(fd);
return 0;
}

主函数部分,首先初始化GPIO功能,打开串口,给串口模块发自动读卡指令,打印一些无关紧要的信息,好像很炫酷的样子。在循环部分,检测读卡信号,上升沿触发。读串口数据,首先验证ID,若未注册显示相关信息。若比对成功就按照上文提到的流程处理。

小结

系统的运行图如下:
界面
用户借伞 实际效果 这里用LED小灯来测试GPIO电平翻转是否正确 >最后感慨一下:大概就是去年的这个时候我有了自己的Raspberry Pi,当时用了RPi.GPIO点亮了一盏小灯就开心的不行。现在终于可以熟练的使用Pi的两个GPIO库,可以自己用它做一个小系统了。当然做这个系统遇到了不少困难,好在Linux编程资料不少,