最近在学习OpenWrt,需要在OpenWrt的WEB界面增加内容,本文将讲述修改OpenWrt的过程和其中遇到的问题。
一、WEB界面开发
LuCI是OpenWrt上的Web管理界面,LuCI采用了MVC三层架构,使用Lua脚本开发,所以开发LuCI的配置界面不需要编辑任何的Html代码,除非想自己单独去创建网页(View层),否则我们基本上只需要修改Model层就可以了。
首先我们讲述如何在web界面增加一个新的选项,如下图“System”旁边的“SZ-Loogson”选项卡。
在文件系统目录“/usr/lib/lua/luci/controller/admin”下创建loogson.lua文件,文件内容如下:
-
module("luci.controller.admin.loogson", package.seeall)
-
-
function index()
-
entry({"admin", "loogson"}, alias("admin", "loogson", "loogson"), _("SZ-Loogson"), 30).index = true
-
-
entry({"admin", "loogson", "loogson"}, cbi("admin_loogson/loogson"), _("BoardType"), 1)
-
-
entry({"admin", "loogson", "control"}, cbi("admin_loogson/control"), _("Control"), 2)
-
-
end
说明:
1) lua单行注释使用“--”,类似于C语言的“//”,多行注释时,“--[[”类似C语言中的“/*”,“]]--”类似C语言中的“*/”。
2) 第1行定义了模块的入口。即“/usr/lib/lua/luci/controller/admin”所示的目录下建立一个loogson.lua文件。如果程序比较多,可能分为好几个模块,那么可以在controller下再建立一个子目录,比如controller/loogsonapp/,那么就可以写成“luci.controller.loogsonapp.loogson”。
3) 从第3行到10行即是function index函数,该函数定义了SZ-Loogson下各个选项卡。
entry表示添加一个新的模块入口,entry的定义如下,其中后两项都是可以为空:
-
entry(path, target, title=nil, order=nil)
“path”是访问的路径,路径是按字符串数组给定的,比如路径按如下方式写“{"admin", "loogson", "control"}”,那么就可以在浏览器里访问“”来访问这个脚本。其中的“admin”表示为管理员添加脚本,“loogson”即为一级菜单名,“control”为菜单项名。系统会自动在对应的菜单中生成菜单项。比如想在“System”菜单下创建一个菜单项,那么一级菜单名可以写为“system”。
“target”为调用目标,调用目标分为三种,分别是执行指定方法(Action)、访问指定页面(Views)以及调用CBI Module。
-
第一种可以直接调用指定的函数,比如点击菜单项就直接重启路由器等等,比如写为“call("function_name")”,然后在该lua文件下编写名为function_name的函数就可以调用了。
-
第二种可以访问指定的页面,比如写为“template("myapp/mymodule")”就可以调用/usr/lib/lua/luci/view/myapp/mymodule.htm文件了。
-
第三种主要应用在配置界面,比如写为“cbi("myapp/mymodule")”就可以调用/usr/lib/lua/luci/model/cbi/myapp/mymodule.lua文件了。
title和order是针对管理员菜单的,其中的title即是显示在网页上的内容。这里我们创建“/usr/lib/lua/luci/controller/loogson.lua”文件,定义我们的入口为“loogson”。
4) 从上面的程序可以看出,在“SZ-Loogson”选项卡下共有两个分选项,名称分别为“BoardType”和“Control”。根据cbi指示的目录,在“/usr/lib/lua/luci/model/cbi/admin_loogson”目录下有loogson.lua和control.lua两个文件,两个界面类似,我们选取"Control"界面讲述。
当点击上图中的"Control"选项时,即可进入如下界面:
此界面对应的程序在“/usr/lib/lua/luci/model/cbi/admin_loogson/control.lua”下,具体内容为:
-
require("luci.sys")
-
require("luci.sys.zoneinfo")
-
require("luci.tools.webadmin")
-
require("luci.fs")
-
require("luci.config")
-
-
local m, s, o
-
-
m = Map("loogson", translate("Control"), translate("This is design by Davied Huang, in order to control loogson board, such as led、beep、and adc."))
-
m:chain("luci")
-
-
s = m:section(TypedSection, "controlboard", translate("Control Board"))
-
s.anonymous = true
-
s.addremove = false
-
-
-
s:tab("led", translate("Control LED"))
-
s:tab("beep", translate("Control Beep"))
-
--s:tab("adc", translate("Control Adc"))
-
-
--
-
-- LED
-
--
-
o = s:taboption("led", ListValue, "lednum", translate("LED NUM:"))
-
o.default = 0
-
o.datatype = "uinteger"
-
o:value(0, translate("LED0"))
-
o:value(1, translate("LED1"))
-
o:value(2, translate("LED2"))
-
-
o = s:taboption("led", ListValue, "ledstatus", translate("LED STATUS:"))
-
o.default = 1 --off status
-
o.datatype = "uinteger"
-
o:value(0, translate("LED ON"))
-
o:value(1, translate("LED OFF"))
-
-
-
--
-
-- BEEP
-
--
-
o = s:taboption("beep", ListValue, "beepstatus", translate("BEEP STATUS:"))
-
o.default = 1 --off status
-
o.datatype = "uinteger"
-
o:value(0, translate("ON"))
-
o:value(1, translate("OFF"))
-
-
o = s:taboption("beep", Value, "beepfreq", translate("BEEP FREQ:"))
-
o.datatype = "uinteger"
-
-
-
local apply = luci.http.formvalue("cbi.apply")
-
if apply then
-
io.popen("/etc/init.d/loogson restart")
-
end
-
-
-
return m
说明:
1) 此处我们使用UCI(Unified Configuration Interface,统一配置接口)开发方式,第7行定义了三个局部变量。
2) 第9行引用了一个“Map”调用,该调用的定义为:
-
m = Map("配置文件文件名", "配置页面标题", "配置页面说明")
第一个参数为配置文件存储的文件名,不包含路径,比如按上述创建的话,应该写为“loogson”,配置文件的存储地址为:/etc/config。第二与第三个参数是用在来页面上显示的,如图所示。
3) 接下来需要创建与配置文件中对应的Section,Section分为两种,NamedSection和TypedSection,前者根据配置文件中的Section名,而后者根据配置文件中的Section类型,这里我们使用后者。我们设定不显示Section的名称(“s.anonymous = true”),以及不允许增加或删除Section(“s.addremove = false”)。controlboard即为1)中配置文件其中的一个配置。
4) 接下来我们定义了两个选项卡,分别为“Control LED”和“Control Beep”。如上图所示。
5) 对于LED选项卡的程序为24-35行。首先创建LED的Section中不同内容的交互(创建Option),常见的有Value(文本框)、ListValue(下拉框)、Flag(选择框)等。创建Option的过程非常简单,而且创建后系统会无需考虑读取以及写入配置文件的问题,系统都会自动处理。25行定义了Option的默认值,26行定义了它的数据类型,此处为整形。27到29定义了它的三个取值,比如如果你选“LED1”的话,实际写到配置文件中的值为1。
6) 对于使用UCI的方式,首先需要创建对应的配置文件(如果配置文件不存在的话,访问配置页面将会报错),格式即为linux配置文件的格式,文件存储在文件系统“/etc/config”目录下,对于本文即在“/etc/config/loogson”,内容如下:
-
config boardinfo
-
option ipaddr1 '192.168.123.212'
-
option netmask1 '255.0.0.0'
-
option boardname '1'
-
-
config controlboard
-
option beepfreq '100'
-
option beepstatus '0'
-
option lednum '2'
-
option ledstatus '0
7) 应用配置以后希望配置立即生效,51行到54行的代码就是判断是否点击了“应用”按钮,如果点击了“应用”按钮就执行默认的脚本程序。
二、OpenWrt添加模块(package)
OpenWrt是一个比较完善的嵌入式Linux开发平台,在无线路由器应用上已有100多个软件包。人们可以在其基础上增加软件包,以扩大其应用范围。
2.1、准备工作
OpenWrt在增加软件方面极其方便,按照OpenWrt的约定就可以很简单完成。加入的软件包可以是网上下载的开源软件或自行开发的软件。加入软件包需要在package目录下创建一个目录。然后创建一个Makefile与OpenWrt建立联系,Makefile需要遵循OpenWrt的约定。另外可以创建一个patchs目录保存patch文件,对下载的源代码进行修改。
由于本文所建立的模块是基于luci的,所以在OpenWrt的“package/feeds/luci”目录下建立“szloogson”目录。
2.2、模块Makefile
在“szloogson”目录下建立一个“Makefile”文件,该文件的如下所示:
-
#
-
# Copyright (C) 2010-2014 Davied Huang Wich
-
#
-
# This is free software, licensed under the GNU General Public License v2.
-
# See /LICENSE for more information.
-
#
-
-
include $(TOPDIR)/rules.mk
-
-
-
PKG_NAME:=luci-app-szloogson
-
PKG_VERSION=1.0
-
PKG_RELEASE:=1
-
-
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
-
-
include $(INCLUDE_DIR)/package.mk
-
-
define Package/luci-app-szloogson
-
SECTION:=luci
-
CATEGORY:=LuCI
-
SUBMENU:=3. Applications
-
TITLE:=shenzhou loogson for LuCI
-
PKGARCH:=all
-
endef
-
-
define Package/luci-app-njitclient/description
-
This package contains LuCI configuration pages for shenzhou loogson.
-
endef
-
-
define Build/Prepare
-
mkdir -p $(PKG_BUILD_DIR)
-
$(CP) ./src/* $(PKG_BUILD_DIR)/
-
endef
-
-
define Build/Configure
-
endef
-
-
define Build/Compile
-
$(MAKE) -C $(PKG_BUILD_DIR) \
-
CC="$(TARGET_CC)" \
-
CROSS_COMPILE="$(TARGET_CROSS)" \
-
ARCH="$(ARCH)"
-
endef
-
-
define Package/luci-app-szloogson/install
-
#install shell
-
$(INSTALL_DIR) $(1)/etc/init.d
-
$(INSTALL_BIN) ./files/loogson.init $(1)/etc/init.d/loogson
-
#install config
-
$(INSTALL_DIR) $(1)/etc/config
-
$(INSTALL_CONF) ./files/loogson.config $(1)/etc/config/loogson
-
#install execute bin
-
$(INSTALL_DIR) $(1)/usr/sbin
-
$(INSTALL_BIN) $(PKG_BUILD_DIR)/gsc3280_led $(1)/usr/sbin/gsc3280_led
-
#install luci
-
mkdir -p $(1)/usr/lib/lua/luci/controller/admin
-
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller
-
$(INSTALL_DATA) ./src/luci/controller/admin/loogson.lua $(1)/usr/lib/lua/luci/controller/admin/loogson.lua
-
-
mkdir -p $(1)/usr/lib/lua/luci/model/cbi/admin_loogson
-
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/model/cbi
-
$(INSTALL_DATA) ./src/luci/model/cbi/admin_loogson/* $(1)/usr/lib/lua/luci/model/cbi/admin_loogson/
-
endef
-
-
$(eval $(call BuildPackage,luci-app-szloogson)
说明:
2.2.1、第8行“$(TOPDIR)/rules.mk”
“$(TOPDIR)/rules.mk”一般在Makefile的开头,定义一些包的基本信息。
软件包的信息均以“PKG_”开头,其意思和作用如下:
PKG_NAME:软件包名称,将在menuconfig和ipkg可以看到。
PKG_VERSION:软件版本号。
PKG_RELEASE:Makefile的版本号
PKG_SOURCE:源代码的文件名。
PKG_SOURCE_URL:源代码的下载网站位置。@SF表示在sourceforge网站,@GNU表示在GNU网站,还有@GNOME、@KERNEL。获取方式可以为:git、svn、cvs、hg、bzr等。有关下载方法可参考$(INCLUDE_DIR)/download.mk和$(SCRIPT_DIR) /download.pl。由于本文使用的是自己开发的代码,所以没有此项。
PKG_MD5SUM:源代码文件的效验码。用于核对软件包是否下载正确。
PKG_CAT:源代码文件的解压方法。包括zcat, bzcat, unzip等。
PKG_BUILD_DIR:软件包编译目录。它的父目录为$(BUILD_DIR)。如果不指定,默认为$(BUILD_DIR)/$( PKG_NAME)-$( PKG_VERSION)。
还有一些有关源代码的定义如下:
PKG_SOURCE_SUBDIR
PKG_SOURCE_PROTO
PKG_SOURCE_MIRROR
PKG_MIRROR_MD5SUM
PKG_SOURCE_VERSION 2.2.2、第17行“include $(INCLUDE_DIR)/package.mk”
“include $(INCLUDE_DIR)/package.mk”一般在软件包的基本信息完成后再引入,他定义了用户态软件包的规则。
编译包分为用户态和内核模块,用户态软件包使用Package,内核模块使用KernelPackage。“$(INCLUDE_DIR)/kernel.mk”文件对于软件包为内核时不可缺少,“$(INCLUDE_DIR)/package.mk”应用在用户态。接下来讲述用户态软件包。用户程序的编译包以“Package/”开头,然后接着软件名,在Package定义中的软件名可以与软件包名不一样,而且可以多个定义。
2.2.3、第19行”define Package/luci-app-szloogson
包的名称为”luci-app-szloogson“。
接下来定义的包括:
SECTION:包的类型,预留。
CATEGORY:分类,在menuconfig的菜单下将可以找到。
SUBMENU:包在make menuconfig的位置,此处即在”LuCi/3. Applications“下。
TITLE:用于软件包的简短描述,将显示在”make menuconfig“中。
DESCRIPTION:用于软件包的详细描述,已放弃使用。如果使用DESCRIPTION将会提示“error DESCRIPTION:= is obsolete, use Package/PKG_NAME/description”。
URL:软件包的下载位置。
MAINTAINER:维护者选项。
DEPENDS:与其他软件的依赖。即如编译或安装需要其他软件时需要说明。如果存在多个依赖,则每个依赖需用空格分开。依赖前使用+号表示默认显示,即对象沒有选中时也会显示,使用@则默认为不显示,即当依赖对象选中后才显示。
2.2.4、第27行”define Package/luci-app-szloogson/description“
软件包的详细描述,取代前面提到的DESCRIPTION详细描述。此处定义的信息将显示在”make menuconfig“中。
2.2.5、第31行”define Build/Prepare“
编译准备方法,对于网上下载的软件包不需要再描述。对于非网上下载或自行开发的软件包必须说明编译准备方法。本文所用的准备方法就是首先创建软件包目录,然后将源码拷贝到刚刚创建的目录中。按OpenWrt的习惯,一般把自己设计的程序全部放在src目录下。
2.2.6、第36行"define Build/Configure”
Build/Configure:在Automake中需要进行“./configure”,所以本配置方法主要针对需要配置的软件包而设计,一般自行开发的软件包可以不在这里说明。本文设计的package由自己写makefile,所以此处没有定义。
2.2.7、第39行”define Build/Compile“
编译方法,没有特别说明的可以不予以定义。如果不定义将使用默认的编译方法Build/Compile/Default。
自行开发的软件包可以考虑使用下面的定义:
-
define Build/Compile
-
$(MAKE) -C $(PKG_BUILD_DIR) \
-
$(TARGET_CONFIGURE_OPTS) CFLAGS="$(TARGET_CFLAGS) -I$(LINUX_DIR)/include"
-
Endef
本文此处指定了交叉编译器和体系结构。
2.2.8、第46行”define Package/luci-app-szloogson/install“
软件包的安装方法,包括一系列拷贝编译好的文件到指定位置。调用时会带一个参数,就是嵌入系统的镜像文件系统目录,因此$(1)表示嵌入系统的镜像目录。一般可以采用下面的方法:
-
define Package/luci-app-szloogson/install
-
$(INSTALL_DIR) $(1)/usr/bin
-
$(INSTALL_BIN) $(PKG_BUILD_DIR)/gsc3280_led $(1)/usr/sbin/
-
endef
INSTALL_DIR、INSTALL_BIN在”$(TOPDIR)/rules.mk“文件中定义,所以本Makefile必须引入$(TOPDIR)/rules.mk文件。
INSTALL_DIR :=install -d -m0755:创建所属用戶可读写、执行,其他用戶可读可执行的目录。
INSTALL_BIN:=install -m0755:编译好的文件到镜像文件目录。
安装文件放在files子目录下,不要与源代码文件目录“src”混在一起,以提高可读性。
如果用户态软件在boot时要自动运行,则需要在安装方法说明中增加自动运行的脚本文件安装和配置文件安裝方法。
例如:
-
define Package/luci-app-szloogson/install
-
#install shell
-
$(INSTALL_DIR) $(1)/etc/init.d
-
$(INSTALL_BIN) ./files/loogson.init $(1)/etc/init.d/loogson
-
#install config
-
$(INSTALL_DIR) $(1)/etc/config
-
$(INSTALL_CONF) ./files/loogson.config $(1)/etc/config/loogson
-
endef
使用清晰的文件扩展名,更方便安裝识别文件。
Package/$(PKG_NAME)/preinst
软件包安装前处理方法,使用脚本语言,因此定义的第一行需要下面的格式
#!/bin/sh:调用时带入的参数为嵌入式系统的镜像目录。
Package/$(PKG_NAME)/postinst:软件包安装后处理方法,使用脚本语言。
Package/$(PKG_NAME)/prerm:软件包删除前处理方法,使用脚本语言
Package/$(PKG_NAME)/postrm:软件包删除后处理方法,使用脚本语言
程序接下来安装luci文件。
2.2.9、第66行”$(eval $(call BuildPackage,luci-app-szloogson))“
完成前面定义后,必须使用eval函数实现各种定义。其格式为:
对于一般软件包:$(eval $(call Package,$(PKG_NAME)))
或对于内核模块:$(eval $(call KernelPackage,$(PKG_NAME)))
如果一个软件包有多个程序,例如:一个应用程序有自己的内核模块,上面使用的“PKG_NAME”需要灵活变通。eval函数可能设计多个。也可以当成多个软件包处理。
2.2.10、本文没有用到的
Package/$(PKG_NAME)/conffiles:本包安装的配置文件,一行一个。如果文件结尾使用“/”,则表示为目录。用于备份配置文件说明,在sysupgrade命令执行时将会用到。
2.3、内核模块包定义
Linux分为内核态和用户态。开发者开发的内核部分可以直接加入Linux的Kernel程序,也可以生成内核模块以便需要时装入内核。OpenWrt一般希望开发者生成内核模块,在Linux启动后自动装载或手工使用insmod命令装载。内核模块使用“KernelPackage”开头,其他与一般软件包基本相同。
在内核模块定义中增加“SUBMENU”表示子菜单位置,在“$(INCLUDE)/kernel.mk”对内核模块定义了CATEGORY为kernel modules,所以内核模块在menuconfig中的主菜单为kernel modules,然后有下一级子菜单$(SUBMENU)。在子菜单下可以看到以kmod-$( PKG_NAME)项目。
DEFAULT表示直接编入内核或产生内核模块,y表示直接编入内核,m表示产生内核模块。
AUTOLOAD表示自动装入内核,一般表示方法为:
AUTOLOAD:=$(call AutoLoad, $(PRIORITY),$(AUTOLOAD_MODS))
AutoLoad的第一个参数$(PRIORITY)为优先级,01为最优先,99为最后装载。有关自动装载可以在/etc/modules.d目录下看到,第二个参数$(AUTOLOAD_MODS)模块名,每个模块名以空格符分隔。即可同时装载多个内核模块。
在开发过程最好不要使用自动装载,需经过严格调试后再使用,可以减轻调试的工作量。
用户态的软件包中沒有内核模块的“AUTOLOAD”参数。如果软件需要在boot时自动运行,则需要在“/etc/init.d”增加相应的脚本文件。 脚本文件需要START参数,说明在boot时的优先级,如果在boot启动后再启动,则需要STOP参数。如果STOP参数存在,其值必须大于START。由“/etc/rc.d/S10boot”知道,装载内核模块的优先级为10,需要使用自己设计的内核模块的程序其START的值必须大于10。同样由“/etc/rc.d/S40network”知道,使用网络通信的程序其START的值必须大于40。
2.4、脚本文件
脚本文件需要start()和stop()两个函数,start()是执行程序,stop()是关闭程序。关闭程序一般需要执行killall命令。
在(一)中我们讨论了点击“应用”后执行的脚本文件在“/etc/init.d/loogson”目录下,程序如下:
-
#!/bin/sh /etc/rc.common
-
# (C) 2014 openwrt.org
-
# add by Davied Huang
-
-
START=50
-
-
LED_BIN="/usr/sbin/gsc3280_led"
-
-
-
control_board()
-
{
-
local ledstatus, lednum;
-
-
config_get_bool ledstatus $1 ledstatus
-
-
config_get lednum $1 lednum
-
-
echo "${lednum} ${ledstatus}"
-
${LED_BIN} ${lednum} ${ledstatus}
-
}
-
-
start() {
-
config_load loogson
-
config_foreach control_board controlboard
-
}
-
-
stop() {
-
config_load loogson
-
#config_foreach stop_instance controlboard
-
}
说明:
1) 在“start”函数中,首先使用“config_load”函数加载“/etc/config/”目录下的loogson配置文件。
2) “config_foreach”遍历"/etc/init.d/loogson"配置文件中的Section,并且执行"control_board"函数。
3) 在"control_board"函数中,使用“config_get_bool”获得操作LED的开关状态,config_get获得操作第几个LED。
4) "config_load"、“config_foreach”、“config_get”和“config_get_bool”等函数由其他脚本提供,可以直接使用。
5) 最后执行可执行文件,后面加上可执行文件需要的参数。该可执行文件的源码为:
-
#include
-
#include
-
#include
-
#include
-
-
int main(int argc,char *argv[])
-
{
-
int fd;
-
-
if (argc != 3) {
-
printf("wrong cmd!\n");
-
}
-
fd = open("/dev/led", O_RDWR);
-
if(fd == -1){
-
printf("open led failed!\n");
-
}
-
ioctl(fd, argv[1], argv[2]);
-
-
close(fd);
-
return 0;
-
}
三、参考资料
官方说明文档: http://luci.subsignal.org/trac/wiki/Documentation
LuCI上配置Makefile:
http://luci.subsignal.org/trac/wiki/Documentation/Modules
CBI文档:
http://luci.subsignal.org/trac/wiki/Documentation/CBI
Luci模块说明文档:
http://luci.subsignal.org/trac/wiki/Documentation/ModulesHowTo
Luci类库的函数定义和使用说明:
UCI接口在脚本文件中的官方说明:
http://wiki.openwrt.org/doc/devel/config-scripting