reset按键和ipget按键在openwrt中的处理逻辑
一、reset 按键
1、概述
(1)根据客户要求,重启长按3秒,红绿灯常亮,重启。
(2)长按reset键,然后上电开机,绿灯闪烁,红灯灭,恢复出厂设置。
2、分析
要获取reset按键pressed的持续时间,通过按下按键的时间来处理重启和复位的动作。
reset按键的处理主要在 openwrt/packages/base-file/file/etc/rc.button/reset脚本
//没有修改前的reset脚本
#!/bin/sh
. /lib/functions.sh
OVERLAY="$( grep ' /overlay ' /proc/mounts )"
case "$ACTION" in
pressed)
[ -z "$OVERLAY" ] && return 0
return 5
;;
timeout)
. /etc/diag.sh
set_state failsafe
;;
released)
if [ "$SEEN" -lt 1 ]
then
echo "REBOOT" > /dev/console
sync
reboot
elif [ "$SEEN" -ge 5 -a -n "$OVERLAY" ]
then
echo "FACTORY RESET" > /dev/console
echo "0" > /sys/class/leds/red\:status/brightness
jffs2reset -y && reboot &
fi
;;
esac
return 0
根据以上脚本我们可以看出来通过变量SEEN的值是来判断按键press的时间。但是在pressed条件下打印变量SEEN的值,发现pressed下SEEN的值和released状态下的SEEN值是不一样的,pressed下的SEEN的值并不是按键从按下到此时的时间,而按键released后SEEN的值是正确的,这代表只有在按键释放后才能获取按键按下的持续时间。这和我们的要求完全不符。
我们在按键按下的状态下也要获取到从按下到此时的时间,那么就要看驱动中怎么去处理了?
INIT_DELAYED_WORK(&bdev->work, gpio_keys_polled_poll);
static void gpio_keys_polled_poll(struct work_struct *work)
{
struct gpio_keys_button_dev *bdev =
container_of(work, struct gpio_keys_button_dev, work.work);
int i;
for (i = 0; i < bdev->pdata->nbuttons; i++) {
struct gpio_keys_button_data *bdata = &bdev->data[i];
if (bdata->gpiod)
gpio_keys_handle_button(bdata);
}
gpio_keys_polled_queue_work(bdev);
}
有驱动代码可知,gpio_keys_polled_poll被放在工作队列中处理,gpio_keys_polled_poll中一直在调用gpio_keys_handle_button函数,我们主要分析这个函数:
static void gpio_keys_handle_button(struct gpio_keys_button_data *bdata)
{
unsigned int type = bdata->b->type ?: EV_KEY;
int state = gpio_button_get_value(bdata);
unsigned long seen = jiffies; //jiffies是系统启动到此时的时钟中断次数。
pr_debug(PFX "event type=%u, code=%u, pressed=%d\n",
type, bdata->b->code, state);
/* is this the initialization state? */
if (bdata->last_state == -1) { //按键状态初始化
/*
* Don't advertise unpressed buttons on initialization.
* Just save their state and continue otherwise this
* can cause OpenWrt to enter failsafe.
*/
if (type == EV_KEY && state == 0)
goto set_state;
/*
* But we are very interested in pressed buttons and
* initial switch state. These will be reported to
* userland.
*/
} else if (bdata->last_state == state) { //判断上次状态和这次按键状态是否一致,一致则返回。
/* reset asserted counter (only relevant for polled keys) */
bdata->count = 0;
return;
}
if (bdata->count < bdata->threshold) {
bdata->count++;
return;
}
if (bdata->seen == 0)
bdata->seen = seen;
//按键在按下和释放动作会处理button_hotplug_create_event将按键事件通过
//broadcast_uevent发送消息到用户层
button_hotplug_create_event(button_map[bdata->map_entry].name, type,
(seen - bdata->seen) / HZ, state);
//按键按下时会记录时钟中断次数
bdata->seen = seen;
set_state:
bdata->last_state = state;
bdata->count = 0;
}
变量释义:
jiffies:
Linux内核使用全局变量jiffies记录系统自从启动以来的滴答数。在系统启动的时初始化jiffies为0,在每个时钟中断处理例程中该值会加1。假如HZ=1000,每隔1ms系统会发生一次时钟中断,也就是说每隔1ms jiffies的值会加1,也就是说1s内jiffies的值也是HZ,所以系统启动的时间就是: jiffies/HZ
HZ:
Linux内核每隔固定周期都会发生时钟中断, 而HZ代表系统在1s中发生时钟中断的次数。如果HZ=1000,则系统在1s之内会发生1000次时钟中断。
逻辑基本上是这样的:
按键按下-->执行一次button_hotplug_create_event发消息给用户层—>记录下系统启动到此时的时钟中断次数(bdata->seen = seen;)代表 按键按下时的时间----》按键释放后--》执行一次button_hotplug_create_event,此函数的参数 (seen - bdata->seen) / HZ 就是通过(释放时的系统中断次数-按下时 记录的时钟中断次数)/(HZ代表系统在1s中发生时钟中断的次数) = 按键按下的时间。
static int button_hotplug_create_event(const char *name, unsigned int type,
unsigned long seen, int pressed)
{
struct bh_event *event;
pr_debug(PFX "create event, name=%s, seen=%lu, pressed=%d,resetflag=%d\n",
name, seen, pressed,resetflag);
event = kzalloc(sizeof(*event), GFP_KERNEL);
if (!event)
return -ENOMEM;
event->name = name;
event->type = type;
event->seen = seen;
event->action = pressed ? "pressed" : "released";
INIT_WORK(&event->work, (void *)(void *)button_hotplug_work);
schedule_work(&event->work);
return 0;
}
button_hotplug_create_event会记录按键name,type,seen按下的时间,action按键的动作,
button_hotplug_fill_event会填充各个环境变量,通过broadcast_uevent发到用户层。
static int button_hotplug_fill_event(struct bh_event *event)
{
int ret;
ret = bh_event_add_var(event, 0, "HOME=%s", "/");
if (ret)
return ret;
ret = bh_event_add_var(event, 0, "PATH=%s",
"/sbin:/bin:/usr/sbin:/usr/bin");
if (ret)
return ret;
ret = bh_event_add_var(event, 0, "SUBSYSTEM=%s", "button");
if (ret)
return ret;
ret = bh_event_add_var(event, 0, "ACTION=%s", event->action);
if (ret)
return ret;
ret = bh_event_add_var(event, 0, "BUTTON=%s", event->name);
if (ret)
return ret;
if (event->type == EV_SW) {
ret = bh_event_add_var(event, 0, "TYPE=%s", "switch");
if (ret)
return ret;
}
ret = bh_event_add_var(event, 0, "SEEN=%ld", event->seen);
if (ret)
return ret;
ret = bh_event_add_var(event, 0, "SEQNUM=%llu", uevent_next_seqnum());
return ret;
}
reset脚本会对上面变量判断执行相应的动作。
现在就有个问题:
驱动层只在按键按下和释放的时候只发送一次消息给用户层进行按键处理,按键长按的时候不会一直发消息给用户层进行处理动作。所以要对代码进行修改。主要有两个需求要满足:
(1)在长按时一直给用户层发消息去更新长按的时间,释放后不用一直发。
(2)用户层要获取pressed状态下的长按时间。
(3)重启的长按和复位的长按状态还不一样,重启判断上一次按键状态和这次状态不一致代表按下或者释放,复位键是开机后一直在按着。
3、代码编写
3.1 、长按时一直给用户层发消息
由函数gpio_keys_handle_button分析得知,按键长按时由于判断上一次的状态和这一次的状态一致,如果state一直就renturn返回了,没有button_hotplug_create_event去发消息。
else if (bdata->last_state == state) { /* reset asserted counter (only relevant for polled keys) */ bdata->count = 0; return; }
这里状态包含两种:两次都是pressed状态、两次都是released状态,我们只处理长按(pressed)状态,所以修改如下:
else if (bdata->last_state == state) { /* reset asserted counter (only relevant for polled keys) */ if(strcmp(button_map[bdata->map_entry].name,"reset")==0) { if(!state) { bdata->count = 0; return; } } else { bdata->count = 0; return; } }
3.2 pressed状态下应用层要获取到长按的时间
(1)首先要记录按键按下时的初始时间
分析得知当按键last state为0(released)状态,这一次为1(pressed)状态的时候代表按键按下,即
else if(bdata->last_state != state) { if(((0==bdata->last_state)&&(1==state))&& (strcmp(button_map[bdata->map_entry].name,"reset")==0)) bdata->seen = seen; }
(2)长按时一直发送消息给用户层
这样就可以根据 (seen - bdata->seen) / HZ就可以计算出长按的时间。
button_hotplug_create_event(button_map[bdata->map_entry].name, type, (seen - bdata->seen) / HZ, state,resetflag);
长按时button_hotplug_create_event就一直发消息给用户层更新长按时间。
注意发消息后的bdata->seen的记录要关掉。
button_hotplug_create_event(button_map[bdata->map_entry].name, type, (seen - bdata->seen) / HZ, state); if(strcmp(button_map[bdata->map_entry].name,"reset")!=0) bdata->seen = seen;
(3)复位过程分析
reset的复位过程是开机的过程中一直长按着,到加载hotpug驱动,在加载驱动时按键状态有个初始值为-1,那么bdata->last_state = -1,state为1时,表示我按键一直按着,此时resetflag置为1。
static void gpio_keys_handle_button(struct gpio_keys_button_data *bdata) { unsigned int type = bdata->b->type ?: EV_KEY; int state = gpio_button_get_value(bdata); unsigned long seen = jiffies; pr_debug(PFX "event type=%u, code=%u, pressed=%d\n", type, bdata->b->code, state); /* is this the initialization state? */ if (bdata->last_state == -1) { /* * Don't advertise unpressed buttons on initialization. * Just save their state and continue otherwise this * can cause OpenWrt to enter failsafe. */ if (type == EV_KEY && state == 0) { goto set_state; } else if(((type == EV_KEY)&&(1==state))&& (strcmp(button_map[bdata->map_entry].name,"reset")==0)) { resetflag = 1; } /* * But we are very interested in pressed buttons and * initial switch state. These will be reported to * userland. */ }
按键释放后resetflag置0:
else if(bdata->last_state != state) { resetflag = 0; if(((0==bdata->last_state)&&(1==state))&& (strcmp(button_map[bdata->map_entry].name,"reset")==0)) bdata->seen = seen; }
复位消息发送:
if(bdata->last_time == 0) { bdata->last_time = (seen - bdata->seen) / HZ; } //判断reset键进入处理,且(seen - bdata->seen) / HZ !=0 排除短按(短按要一直发),1s发1次,会导致短按时pressed发一次,released状态可能会收不到,因为两次的时间可能都为0. if((strcmp(button_map[bdata->map_entry].name,"reset")==0)&&((seen - bdata->seen) / HZ !=0)) { //只有每次时间更新时才发消息,不能一直发。 if(bdata->last_time!=(seen - bdata->seen) / HZ) { button_hotplug_create_event(button_map[bdata->map_entry].name, type, (seen - bdata->seen) / HZ, state,resetflag); bdata->last_time=(seen - bdata->seen) / HZ; } } else { button_hotplug_create_event(button_map[bdata->map_entry].name, type, (seen - bdata->seen) / HZ, state,resetflag); }
3.3 reset脚本处理
(1)长按等于3s时红绿灯亮重启,因为开机复位时也要长按,所以驱动会传一个环境变量RESETFLAG判断是重启还是复位。
(2)复位的时间主要是根据系统的启动过程设定的,开机后,热插拔驱动加载后,判断长按状态,并长按超过5s,这里的5s是等procd进程启动后应用层才能接收到驱动发过来的消息。procd模块的处理机制解析。
#!/bin/sh . /lib/functions.sh OVERLAY="$( grep ' /overlay ' /proc/mounts )" case "$ACTION" in pressed) #[ -z "$OVERLAY" ] && return 0 if [ "$SEEN" -eq 3 -a "$RESETFLAG" -ne 1 ] then echo none > /sys/class/leds/red\:status/trigger echo none > /sys/class/leds/green\:pwr/trigger echo 255 > /sys/class/leds/red\:status/brightness echo 255 > /sys/class/leds/green\:pwr/brightness echo "REBOOT" > /dev/console sync reboot //这里的5s是要等procd进程启动之后,内核才能发消息给应用层,OVERLAY变量会判断是否挂载overlay文件系统, 当主板有装flash时会挂载,复位会清掉rootfs_data elif [ "$SEEN" -eq 5 -a "$RESETFLAG" -eq 1 -a -n "$OVERLAY" ] then echo none > /sys/class/leds/red\:status/trigger echo 0 > /sys/class/leds/red\:status/brightness echo 256 > /sys/class/leds/green\:pwr/delay_off echo 256 > /sys/class/leds/green\:pwr/delay_on echo timer > /sys/class/leds/green\:pwr/trigger echo "FACTORY RESET" > /dev/console jffs2reset -y && reboot & fi return 5 ;; timeout) . /etc/diag.sh set_state failsafe ;; released) ;; esac return 0
二、ipget按键
1、概述
如图,是客户要求ipget 按键的功能,同样要有长按功能。
2、分析
(1)短按汇报IP,因为长按过程中会汇报ip,所以,将汇报ip做成按键released下汇报ip
(2)长按ipget button 切换闪灯和正常状态,即获取红绿灯状态,如果是闪灯状态,则长按5s切为正常状态,反之亦然。
3、编写代码
(1)首先要ipget button长按时要1s发一次消息给用户层,更新用户层的长按时间。
(2)之前在hotplug.c中做报ip的动作不太合理,所以目前最新的做法时,将报ip的包安装进系统中,生成一个可执行文件,然后ipget脚本去调用可执行文件去发ip和mac地址即可。
在openwrt/package/utils下把报ip的源文件及makefile编写好。
//package/utils/sendip/src/sendip.c
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#define PORT 54321
static int button_ipget(void)
{
int sock = -1;
struct ifreq ifr;
const int opt = 1;
int nb = 0;
struct sockaddr_in addrto;
char mac_addr[30];
char ip_addr[30];
time_t now;
struct tm *tm_now;
char *datetime;
char msg[100];
time(&now);
tm_now = localtime(&now);
datetime = asctime(tm_now);
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
printf("socket error\n");
return -1;
}
bzero(&ifr, sizeof(ifr));
strcpy(ifr.ifr_name, "br-lan");
if (ioctl(sock, SIOCGIFADDR, &ifr) < 0)
printf("ioctl error!");
sprintf(ip_addr,"IP Address:%s",inet_ntoa(((struct sockaddr_in*)&(ifr.ifr_addr))->sin_addr));
if( (ioctl( sock, SIOCGIFHWADDR, &ifr)) < 0 ){
printf("mac ioctl error\n");
return -1;
}
sprintf(mac_addr, "MAC Address:%02x:%02x:%02x:%02x:%02x:%02x",
(unsigned char)ifr.ifr_hwaddr.sa_data[0],
(unsigned char)ifr.ifr_hwaddr.sa_data[1],
(unsigned char)ifr.ifr_hwaddr.sa_data[2],
(unsigned char)ifr.ifr_hwaddr.sa_data[3],
(unsigned char)ifr.ifr_hwaddr.sa_data[4],
(unsigned char)ifr.ifr_hwaddr.sa_data[5]
);
strcat(msg,mac_addr);
sprintf(msg,"%s %s Datetime:%s",ip_addr,mac_addr,datetime);
nb = setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char *)&opt, sizeof(opt));
if(nb == -1)
{
printf("set socket error...\n");
return -1;
}
bzero(&addrto, sizeof(struct sockaddr_in));
addrto.sin_family=AF_INET;
addrto.sin_addr.s_addr=htonl(INADDR_BROADCAST);
addrto.sin_port=htons(PORT);
int nlen=sizeof(addrto);
int ret=sendto(sock, msg, strlen(msg), 0, (struct sockaddr*)&addrto, nlen);
if(ret<0)
{
printf("send error....\n");
}
close(sock);
return 0;
}
int main(void)
{
if(button_ipget()!=0)
{
printf("get IP addr error!\n");
}
else
{
printf("get IP addr success!\n");
}
return 0;
}
package/utils/sendip/src/Makefile
CC = gcc
CFLAGS = -Wall
OBJS = sendip.o
all: sendip
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
sendip: $(OBJS)
$(CC) -o $@ $(OBJS)
clean:
rm -f sendip *.o
package/utils/sendip/Makefile
#
# Copyright (C) 2007-2008 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=sendip
PKG_VERSION:=1.0.00
PKG_RELEASE:=1
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
define Package/sendip
SECTION:=utils
DEFAULT:=y
CATEGORY:=Utilities
DEPENDS:=
TITLE:=send ip address and mac address
endef
define Package/sendip/description
send ip and mac to ipollo tools
endef
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
$(CP) ./src/* $(PKG_BUILD_DIR)/
endef
define Package/sendip/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/sendip $(1)/usr/bin/
endef
$(eval $(call BuildPackage,sendip))
执行make menuconfig --->Utilitiies中就可以看到ipget的工具
生成固件升级后会在/usr/bin下面生成一个sendip的可执行文件。ipget脚本去执行sendip发ip和mac.以及对于闪灯的处理
root@OpenWrt:~# cat /etc/rc.button/ipget #!/bin/sh . /lib/functions.sh echo "SEEN=$SEEN" > /dev/console greenStatus="$( grep '\[timer\]' /sys/class/leds/green\:pwr/trigger )" redStatus="$( grep '\[timer\]' /sys/class/leds/red\:status/trigger )" case "$ACTION" in pressed) if [ "$SEEN" -eq 5 ] then if [ -n "$greenStatus" -a -n "$redStatus" ] then echo none > /sys/class/leds/red\:status/trigger echo none > /sys/class/leds/green\:pwr/trigger else echo timer > /sys/class/leds/red\:status/trigger echo timer > /sys/class/leds/green\:pwr/trigger fi fi return 5 ;; timeout) . /etc/diag.sh set_state failsafe ;; released) ./usr/bin/sendip ;; esac
补充 :
脚本中pressed中的返回值为多少,代表按下后几秒后执行timeout,所以长按5s可以在press中return 5,在长按5s后超时,执行timeout.