在不知不觉中,身边已经充斥着各种智能设备,很多人都过上了智能生活,很多事几乎都不用亲力亲为,不仅人们的生活被智能设备占领,宠物市场也是如此,尤其是宠物智能喂食器已经成为家喻户晓的智能设备,如今也正是宠物智能喂食器飞速发展和技术更成熟的时候,宠物智能喂食器未来是我市场前景将不可估量。近来由于新冠疫情的影响,宠物被独自放置在家中,宠物的温饱成了一个大问题,为了不让铲屎官白天工作的时候担心宠物会饿肚子,宠物喂食器应运而生。
宠物喂食器,基于涂鸦wifi模组和ITOP4412开发板,因为之前没有使用过单片机,本次采用的是在linux环境下进行开发,涂鸦VWXR2模组出厂已经烧录好固件,所以不需要开发,十分方便,该模组主要是进行数据转发,itop4412开发板主要是进行数据处理,和外设控制。
(1) 涂鸦VWXR2 WIFI通信模组
(2) 涂鸦12V电源板
(3) 涂鸦H桥电机驱动板
(4) ITOP4412开发板
(5) 385直流电机
2.1.1 通过涂鸦APP直接进行喂食;
2.1.2 定时喂食,当没有网络时,依旧可以根据设置的闹钟来自动喂食
2.1.3 小夜灯模式
2.1.4 语音唤醒
产品型号:TYDE5-VWXR2-MCU-1
产品型号:TYDE5-H-BRIDGE-1
产品型号:TYDE5-POWER-DC_DC-1
在涂鸦IOT平台找到 创建产品 -> 小家电 -> 宠物喂食器
这里可以选择公版面板,也可以选择自定义面板,当现有的需求公版面板不满足时,就需要使用自定义面板了; 这里我选择公版面板
后期根据我们下载的MCU SDK进行开发
首先下载完SDK以后,分析其源码,花了一些时间理清了大致的框架,用户主要做的是命令下发后,主控板接收到wifi模组转发的数据,经过SDK对数据的解析,然后根据对应的数据帧中的命令字
字段进入相应的功能处理函数,最后用户在功能函数中,对下发的数据进行处理,例如,当接收到客户端下发的快速喂食命令,便执行打开电机的操作。
这个SDK的移植相对简单,因为不涉及依赖库的问题,所以只需要使用交叉编译器进行编译然后使用nfs服务挂载到开发板即可,这里我使用的交叉编译器是arm-linux-4.4.3。
在linux中对于串口的读写,并不复杂,主要步骤 1、打开串口 2、对串口参数进行配置 3、对串口进行读写;
int uart_receive(void)
{
int ret ;
unsigned char c;
do{
ret = read(fd_wifi_uart, &c, 1);
}while(ret == -1 && errno == EINTR);
if (-1 == ret)
{
perror("read");
// return -1;
}
if (1 == ret)
uart_receive_input(c);
// printf("recv:0x%x\n",c);
return ret;
}
static void wifi_uart_write_data(unsigned char *in, unsigned short len)
{
if((NULL == in) || (0 == len)) {
return;
}
/* 串口发送数据*/
if (-1 == write(fd_wifi_uart, in, len))
{
perror("uart write");
return ;
}
}
因为喂食操作需要对电机进行操作,需要将电机的驱动编译到内核中去,但是电机驱动占用了我的摄像头驱动,所以只能先将摄像头驱动给砍掉,导致我的远程监控功能暂时搁浅,后期想方法解决。
对于喂食操作也相对简单,例如快速喂食,当APP下发指令后,开发板收到指令进入相应的功能函数,只需要打开电机或者关闭电机。
static unsigned char dp_download_quick_feed_handle(const unsigned char value[], unsigned short length)
{
//示例:当前DP类型为BOOL
unsigned char ret;
//0:关/1:开
unsigned char quick_feed;
quick_feed = mcu_get_dp_download_bool(value,length);
if(quick_feed == 0) {
//开关关
}else {
// 打开电机开关
//开关开
printf("quick feed\n");
motor_on('R', 10, 1); //打开电机
feed_status.quick_feed = 1;
slow_feed = 0;
}
//处理完DP数据后应有反馈
// 数据处理完上报
ret = mcu_dp_bool_update(DPID_QUICK_FEED,quick_feed);
if(ret == SUCCESS)
return SUCCESS;
else
return ERROR;
}
根据下发的value值,对电机转动的时长进行控制;
static unsigned char dp_download_manual_feed_handle(const unsigned char value[], unsigned short length)
{
//示例:当前DP类型为VALUE
unsigned char ret;
unsigned long manual_feed;
manual_feed = mcu_get_dp_download_value(value,length);
/*
//VALUE类型数据处理
*/
//喂食value份
motor_on('R', manual_feed * 10, 2); //打开电机
printf("manual feed value : %ld\n", manual_feed);
feed_status.manual_feed = manual_feed;
//处理完DP数据后应有反馈
ret = mcu_dp_value_update(DPID_MANUAL_FEED,manual_feed);
if(ret == SUCCESS)
return SUCCESS;
else
return ERROR;
}
要执行定时操作,需要对本地时钟进行同步,同步时钟需要先将protocol.h中的 SUPPORT_MCU_RTC_CHECK 宏打开
#define SUPPORT_MCU_RTC_CHECK //开启校时功能
1 然后进行时钟同步,因为时钟同步需要在wifi模组联网之后才能进行,这里 在get_wifi_status()获取wifi状态函数中,当 wifi状态为已连接上路由器且连接到云端时,开发板便向wifi模组发送时钟同步命令
case 4:
//wifi工作状态5
// 当wifi模组连接上路由器时,开发板向wifi模组获取格林威治时间
//向wifi模组发送 获取格林威治时间的指令 55 AA 03 0C 00 00 0E
//void wifi_uart_write_frame(unsigned char fr_type, unsigned char fr_ver, unsigned short len)
//wifi_uart_write_frame(0x03, 0x0c, 0x00);
printf("wifi连接上云端\n");
mcu_get_system_time(); //获取本地时间
获取本地时钟,然后进行时钟同步
void mcu_write_rtctime(unsigned char time_[])
{
// #error "请自行完成RTC时钟写入代码,并删除该行"
/*
Time[0] 为是否获取时间成功标志,为 0 表示失败,为 1表示成功
Time[1] 为年份,0x00 表示 2000 年
Time[2] 为月份,从 1 开始到12 结束
Time[3] 为日期,从 1 开始到31 结束
Time[4] 为时钟,从 0 开始到23 结束
Time[5] 为分钟,从 0 开始到59 结束
Time[6] 为秒钟,从 0 开始到59 结束
Time[7] 为星期,从 1 开始到 7 结束,1代表星期一
*/
if(time_[0] == 1) {
//正确接收到wifi模块返回的本地时钟数据
struct tm stpr;
stpr.tm_year = time_[1] + 100; //0x00 代表2000年 自1900年经过了多少年
stpr.tm_mon = time_[2] - 1; // 从0 - 11
stpr.tm_hour = time_[4]; // 格林威治时区比北京时间多了8小时
stpr.tm_mday = time_[3];
stpr.tm_min = time_[5];
stpr.tm_sec = time_[6];
printf("time[1] :0x%x\n",time_[1]);
printf("time[1] :0x%x\n",time_[2]);
printf("time[1] :0x%x\n",time_[3]);
printf("time[1] :0x%x\n",time_[4]);
printf("time[1] :0x%x\n",time_[5]);
printf("time[1] :0x%x\n",time_[6]);
struct timeval tv;
tv.tv_sec = mktime(&stpr); //将st结构体数据 解析为自1970年1月1日00:00:00以来的秒数
tv.tv_usec = 0;
settimeofday(&tv, NULL); //更新系统时间
printf("time set done\n");
time_t result = time(NULL);
printf("%s secs since the Epoch\n",
asctime(localtime(&result)));
}else {
//获取本地时钟数据出错,有可能是当前wifi模块未联网
fprintf(stderr, "get rtctime is error!\n");
}
}
当时钟同步完成后,接收到下发的喂食计划,对喂食计划raw数据进行解析,并且将定时的闹钟写入文件
static unsigned char dp_download_meal_plan_handle(const unsigned char value[], unsigned short length)
{
//示例:当前DP类型为RAW
unsigned char ret;
/*
//RAW类型数据处理
*/
bzero(feed_plan_buf.buf, 128);
bzero(&feed_plan, sizeof(feed_plan));
feed_num = length / 5;
lseek(feedplan_fd, 0, SEEK_SET);
unsigned int i = 0;
if (10 >= feed_num)
for (i = 0; i < feed_num; i++) //获取所有的喂食计划
{
get_week(value[0 + i*5], i); // 获取计划中的week
feed_plan[i].hour = value[1 + i*5];
feed_plan[i].minute = value[2 + i*5];
feed_plan[i].weight = value[3 + i*5];
feed_plan[i].swch = value[4 + i*5]; //开关
int ret = write(feedplan_fd, &feed_plan[i], sizeof(Feed_Plan));
printf("write ret %d\n", ret);
}
ftruncate(feedplan_fd, sizeof(Feed_Plan) * feed_num);
kill(cpid, SIGUSR1); // 更新完毕,向子进程发送新信号,使其获取新的喂食计划
my_memcpy(feed_plan_buf.buf, value, length); // 更新喂食计划
feed_plan_buf.length = length;
//喂食计划
//处理完DP数据后应有反馈
ret = mcu_dp_raw_update(DPID_MEAL_PLAN,value,length);
if(ret == SUCCESS)
return SUCCESS;
else
return ERROR;
}
这里的闹钟操作是由子进程,子进程主要是对定时计划进行操作,对于喂食计划的同步,是通过父子进程对文件的读写来进行的,当APP更新喂食计划后,父进程通过向子进程发送SIGUSR1信号,来唤醒子进程,然后子进程更新喂食计划,获取新的闹钟;
if (0 == cpid) //子进程
{
// 获取当前文件偏移量 如果非0,代表feedplan更新;
int file_seek; //获取当前文件偏移量
int i = 0;
int ret = 1;
get_feedplan();
printf("child get_feedplan success!\n");
if (signal(SIGALRM, sig_alrm) == SIG_ERR) //捕捉SIGALRM信号
{
perror("signal");
exit(EXIT_FAILURE);
}
if (signal(SIGUSR1, sig_usr) == SIG_ERR)// 捕捉父进程发送给子进程的SIGUSR1信号
{
perror("signal");
exit(EXIT_FAILURE);
}
while (1)
{
Alarm_clock();
}
// 子进程获取喂食计划;
}
因为H桥电路主要是负责电机的正反转,控制其转速的是PWM,在这里是使用GPIO的拉高与拉低来模拟PWM波
int motor_on(const unsigned char direction, unsigned short feed_num, unsigned short speed)
{
feed_status.feed_report = feed_num / 10;
if(mcu_dp_value_update(DPID_FEED_REPORT,feed_status.feed_report) == SUCCESS) //喂食结果上报
printf ("feed_report:%d\n", feed_status.feed_report);
if(direction == 'R')
{
printf ("slow_feed :%d\n", feed_status.slow_feed);
mcu_dp_enum_update(DPID_FEED_STATE,feeding);
while (feed_num)
{
ioctl(fd_moter, CMD_STEPMOTOR_C, HIGHT);
ioctl(fd_moter, CMD_STEPMOTOR_D, LOW);
usleep(3000);
ioctl(fd_moter, CMD_STEPMOTOR_C, LOW);
ioctl(fd_moter, CMD_STEPMOTOR_D, LOW);
if (feed_status.slow_feed == TRUE)
usleep(6000); //慢速喂食
else
usleep(3000); //快速喂食
feed_num--;
}
mcu_dp_enum_update(DPID_FEED_STATE,done);
}
set_motor_off();
}
这里我是将喂食记录上报,放在了motor控制中,当motor接收到下发的feed_num, 根据feed_num 来确定喂食的分量,然后对喂食分量进行上报
int motor_on(const unsigned char direction, unsigned short feed_num, unsigned short speed)
{
feed_status.feed_report = feed_num / 10;
if(mcu_dp_value_update(DPID_FEED_REPORT,feed_status.feed_report) == SUCCESS) //喂食结果上报
printf ("feed_report:%d\n", feed_status.feed_report);
if(direction == 'R')
{
printf ("slow_feed :%d\n", feed_status.slow_feed);
mcu_dp_enum_update(DPID_FEED_STATE,feeding);
while (feed_num)
{
ioctl(fd_moter, CMD_STEPMOTOR_C, HIGHT);
ioctl(fd_moter, CMD_STEPMOTOR_D, LOW);
usleep(3000);
ioctl(fd_moter, CMD_STEPMOTOR_C, LOW);
ioctl(fd_moter, CMD_STEPMOTOR_D, LOW);
if (feed_status.slow_feed == TRUE)
usleep(6000); //慢速喂食
else
usleep(3000); //快速喂食
feed_num--;
}
mcu_dp_enum_update(DPID_FEED_STATE,done);
}
set_motor_off();
}
由于手上的资源比较少,就用开发板上的LED来代替小夜灯,控制起来相对简单,这里不过多介绍
这里采用的是涂鸦宠物喂食器的公版模板