luci架构
LuCI 架构采用了MVC(Model-View-Controller)设计模式,各个目录的作用如下:
- model(模型): 位于 /usr/lib/lua/luci/model 下,存放了与系统配置相关的模型脚本。这些脚本负责与底层系统的交互,如读取、验证和更新配置文件。模型层封装了对系统资源的访问和业务逻辑处理。
- view(视图): 位于 /usr/lib/lua/luci/view 下,包含了各类HTML模板文件,它们使用Lua脚本来动态生成页面内容。视图层负责将模型中的数据以合适的方式呈现给用户,如显示网络设置、系统状态等信息。
- controller(控制器): 位于 /usr/lib/lua/luci/controller 下,包含了处理HTTP请求的Lua脚本,也就是路由和控制器脚本。控制器层接收用户的HTTP请求,调用相应的模型获取数据,然后选择合适的视图进行渲染,将处理结果返回给用户。
LuCI 将网页中的每一个菜单视作一个节点,当用户通过浏览器点击节点,向路由器发起请求,LuCI 会从 controller 目录下的 index.lua 开始组织这些节点。index.lua 中定义了根节点 root,其他所有的节点都挂在这个根节点上。
通俗来讲,可以将 LuCI 管理界面看作一棵树状结构的菜单系统。管理路由器设置的网页就像一棵大树,每一层菜单就是一个节点。
当点击网页上的任何一个菜单选项时,就像是在触摸大树上的一片叶子或枝干。这时,浏览器会发送一个请求给路由器,告诉它想访问哪个菜单节点。
LuCI 框架接收到这个请求后,会从一个叫做 controller 目录下的 index.lua 文件开始处理。这个 index.lua 文件就好比是整棵树的根部,它定义了整个菜单系统的基础结构和访问规则。
在这个根节点之下,LuCI 会根据在 index.lua 中定义的 entry 函数逐步组织起其他的子菜单节点。也就是说,每当你在 index.lua 中添加一个类似于 entry({"admin", "example", "first"}, call("first_action"), "First") 的语句时,你就相当于在菜单树上挂了一个新的子节点——"First"。
当用户点击网页上的 "First" 菜单时,LuCI 就会调用与之关联的 first_action 函数,呈现相应的页面内容或者执行相应的操作。就这样,通过一步步递归展开,整个复杂的菜单系统就被建立起来了,并能够在用户交互时动态响应用户的请求。
为界面添加节点
entry(path, callback, title, order):用于定义 LuCI 管理界面的菜单项及其关联的操作。
path 指定该节点的位置(例如 node1.node2.node3)
target 指定当该节点被调度(即用户点击)时的行为,主要有三种:call、template 和 cbi。
title:标题,即我们在网页中看到的菜单
order:同一级节点之间的顺序,越小越靠前,反之越靠后(可选)
Map (config, title, description)
config:配置文件的层级名称,比如 example 对应 /etc/config/exampl
title:配置界面的标题,即我们在网页中看到的菜单
description:配置界面的描述信息
s = m:section(TypedSection, "example_instance", "Section Title", "Section Description")
TypedSection 或 SimpleSection:Section 的类型,TypedSection 可以为区段分配类型,并支持默认配置。
example_instance:区段实例的名称,用于区分配置文件中的不同区段实例。
Section Title:区段在界面上显示的标题。
Section Description(可选):区段的描述信息。
o = s:option(Value, "option_name", "Option Label", "Option Description")
Value 或其他选项类型(如 ListValue、Flag 等):选项的数据类型。
option_name:选项在配置文件中的键名。
Option Label:选项在界面上显示的标签文本。
Option Description(可选):选项的描述信息
1.通过call
进入 /usr/lib/lua/luci/controller/ 目录下,创建 lua 脚本文件 example.lua,其内容如下
--[[module 是 LuCI 自定义的函数,用于定义一个新的模块。这里的模块名为 "luci.controller.example",
表示这是一个 LuCI 控制器模块,主要用于定义路由和处理用户请求。
package.seeall 是 Lua 标准库中的一个函数,它用于打开模块内的全局访问权限。
这意味着在这个模块内部定义的所有函数和变量都将被视为全局的,可以从外部访问]]
module("luci.controller.example", package.seeall)
--[[定义了 LuCI 控制器的入口点(entry points)。
entry({"admin", "example"}, firstchild(), "Example", 60) 表示注册一个路由。
当用户在 Web 管理界面访问 /admin/example 时,LuCI 将会调用 firstchild() 函数来决定下一步跳转的位置。
这里 firstchild() 通常是指导航菜单的第一个子页面,即指向 "admin", "example", "first" 的路由。
'模板', 60 分别表示在菜单中显示的标题("模板")和菜单排序优先级(60)。]]
--[[这里的 firstchild() 是一种特殊的回调函数引用。
当LuCI接收到匹配到该路由 "admin", "example" 的请求时,它不会直接执行某个特定的动作函数,
而是查找该路由下第一个有效的子节点(即 "admin", "example", * 中的 * 部分),
并将控制权传递给这个子节点对应的处理函数]]
--
function index()
entry({"admin", "example"}, firstchild(),"模板", 60)
entry({"admin", "example", "first"}, call("first_action"), "第一")
entry({"admin", "example", "second"}, call("second_action"), "第二")
end
--[[当用户访问 /admin/example/first 时,LuCI 将调用 first_action() 函数进行处理。
luci.template.render("header") 表示渲染一个名为 "header" 的模板文件,
通常这个模板文件包含了页面的头部元素,如导航栏、样式表引用等。
这个模板文件位于 /usr/lib/lua/luci/view,如果在其他目录下则更改参数为header.htm路径
luci.http.write("<h1>Hello World</h1>") 用于直接向客户端发送 HTML 数据,
这里是发送一个 <h1> 标签,显示 "Hello World",即页面的主要内容。]]
function first_action()
luci.template.render("header")
luci.http.write("<h1> 一级标题 hello</h1>")
luci.http.write("<h2> 二级标题 hello</h2>")
end
function second_action()
luci.template.render("header")
luci.http.write("<h1> 一级标题 hello</h1>")
luci.http.write("<h2> 二级标题 hello</h2>")
end
刷新网页
2.通过template
更改example.lua文件
module("luci.controller.example", package.seeall)
--[[更改call为template 在view目录下直接调用html页面
添加order参数为子菜单排序
order参数加不加引号系统都会识别为数字]]
function index()
entry({"admin", "example"}, firstchild(),"模板", 60)
entry({ "admin", "example", "third" }, template("example/third"), "第三","30")
entry({ "admin", "example", "fourth" }, template("example/fourth"), "第四","35")
entry({"admin", "example", "first"}, call("first_action"), "第一","10")
entry({"admin", "example", "second"}, call("second_action"), "第二","20")
end
function first_action()
luci.template.render("header")
luci.http.write("<h1> 一级标题 hello</h1>")
luci.http.write("<h2> 二级标题 hello</h2>")
end
function second_action()
luci.template.render("header")
luci.http.write("<h1> 一级标题 hello</h1>")
luci.http.write("<h2> 二级标题 hello</h2>")
end
创建模板目录 /usr/lib/lua/luci/view/example/在模板目录下创建文件 third.htm/fourth.htm其内容如下
--third
<%+header%>
<h1>Hello World</h1>
--fourth
<%+header%>
<h1>Hello World</h1>
刷新网页
3.通过cbi
更改example.lua文件
module("luci.controller.example", package.seeall)
function index()
entry({ "admin", "example" }, firstchild(), "模板", 60)
entry({ "admin", "example", "third" }, template("example/third"), "第三", "30")
entry({ "admin", "example", "fourth" }, template("example/fourth"), "第四", "35")
entry({ "admin", "example", "first" }, call("first_action"), "第一", "10")
entry({ "admin", "example", "second" }, call("second_action"), "第二", "20")
--[[
如果配置文件/etc/config/example 存在,则创建 Example 的子节点,
当节点被调度时,LuCI 会将
/usr/lib/lua/luci/model/cbi/example/fifth.lua 这个脚本转换成 html 页面
发给客户端。
--]]
if nixio.fs.access("/etc/config/example")
then
entry({ "admin", "example", "fifth" }, cbi("example/fifth"), "第五", 40)
end
end
function first_action()
luci.template.render("header")
luci.http.write("<h1> 一级标题 hello</h1>")
luci.http.write("<h2> 二级标题 hello</h2>")
end
function second_action()
luci.template.render("header")
luci.http.write("<h1> 一级标题 hello</h1>")
luci.http.write("<h2> 二级标题 hello</h2>")
end
创建配置文件/etc/config/example
新建 lua 脚本文件:/usr/lib/lua/luci/model/cbi/example/fifth.lua,内容如下
--[[
Map (config, title, description)
]]
m = Map("example", "cbi示例", "这是cbi的一个非常简单的例子")
return m
刷新网页
修改/usr/lib/lua/luci/model/cbi/example/fifth.lua
m = Map("example", "cbi示例", "这是cbi的一个非常简单的例子")
--[[
在 Map 对象 m 中创建一个名为 s 的 Section(配置区段),类型为 TypedSection。
第一个参数 TypedSection 表示这是一个带有类型属性的配置区段;
第二个参数 是区段的名称,这也将成为 /etc/config/example 文件中区段的标识;
第三个参数 是区段的标题;
第四个参数 是区段的描述信息。
]]
s = m:section(TypedSection, "example", "模板", "此部分为模板")
--[[
设置 Section 对象 s 允许用户在界面上添加和移除区段实例。
]]
s.addremove = true
--[[
设置 Section 对象 s 不是匿名区段,意味着每个实例都需要在配置文件中拥有一个唯一的标识(id)。
]]
s.anonymous = false
--[[
在 Section 对象 s 中创建一个名为 n 的 Option(选项),类型为 Value(值类型)。
第一个参数 Value 表示这是一个可以输入任意值的选项;
第二个参数 "num" 是选项在配置文件中的键名;
第三个参数 "Number" 是选项在界面上显示的标签文本。
]]
n = s:option(Value, "num", "Number")
--[[
设置 Option 对象 n 允许用户清空其值,如果用户在界面上删除了输入值,保存时也会将配置文件中的相应值清空。
]]
n.rmempty = true
return m
刷新网页
在文本框中输入 first,然后单击 Add,如下所示
在 Number 后面随便输入一个数字,比如 12,然后单击 Save & Apply,如下所示
在路由器开发板上查看一些配置文件
n.rmempty = true表示当用户对该选项的输入值为空值时,LuCI 会将该选项从配置文件中移除。
将 Number 的值删除,再单击 Save & Apply
现在再来查看配置文件
添加启动脚本
在 OpenWrt系统中,LuCI 作为 Web 管理界面,允许用户通过网页图形界面编辑系统的各项配置。当用户在 LuCI 中修改了配置,并单击“Save & Apply”按钮后,会发生以下过程:
LuCI 会首先将用户在网页上所做的更改保存到对应的配置文件中。例如,网络相关的配置会保存到 /etc/config/network 文件。(上述保存到创建的配置文件/etc/config/example)
保存完成后,LuCI 通常会调用相应的 UCI(Unified Configuration Interface)命令行工具(如 ubus 或 uci)来通知系统配置已更改,并触发重新加载配置
系统收到配置更改通知后,会根据配置文件的变化情况,调用相应的启动脚本执行配置更新操作。这些启动脚本通常位于 /etc/init.d/ 目录下,例如对于网络配置,对应的启动脚本就是 /etc/init.d/network,如果为上述示例在/etc/init.d/目录下新建启动脚本文件,当示例的配置文件变化后,在/etc/init.d/目录下新建的启动脚本也会运行。
启动脚本接收到诸如 reload 的参数后,会停止当前的相关服务,应用新的配置,然后再启动这些服务,从而使更改生效。
为配置文件 example 创建一个启动脚本/etc/init.d/example,同时为其添加可执行权限。其内容如下:
#!/bin/sh /etc/rc.common
START=50
start()
{
echo "start example" > /dev/ttyS0
}
reload()
{
echo "reload example" > /dev/ttyS0
}
LuCI 通过以配置文件名作为参数调用/sbin/luci-reload 来使配置生效,而 luci-reload 会解析另一个配置文件 /etc/config/ucitrack,需要将 example 添加进去。用vi打开/etc/config/ucitrack,在最后添加如下内容:
config example
option init example
当用户在 LuCI 管理界面单击 "Save & Apply" 保存并应用配置变更后,LuCI 通常会执行 /sbin/luci-reload 命令,并传入对应的配置模块名称(即执行/sbin/luci-reload example)。luci-reload 会识别出与该模块相关的启动脚本,并调用其 reload 函数来重新加载配置并应用更改。
这里,example表示要跟踪的配置文件,init 选项指定了与该配置文件关联的启动脚本名称(即 /etc/init.d/example)
当 /etc/config/example 文件发生变化时,ucitrack 会监测到变化并调用 /etc/init.d/example 脚本的 reload 函数(如果存在),以重新加载并应用新的配置。这样就能确保当用户通过 LuCI 管理界面单击 "Save & Apply" 后,配置变更能够被系统及时识别并应用。
为了确保 /etc/init.d/example 脚本在系统启动时被自动运行,以及能够响应 luci-reload 命令,需要执行以下命令启用该服务:
chmod +x /etc/init.d/example #首先确保有执行权限 否则报错Permission denied
root@OpenWrt:/# /etc/init.d/example enable
这条命令会创建一个符号链接,将 /etc/init.d/example 链接到 /etc/rc.d/S50example(这里的数字 50 可能根据实际的 START 变量值有所不同),这样每次启动或执行 reload 时,系统都会自动运行 /etc/init.d/example 脚本中的相应函数。
当使用 reload 参数调用启动脚本时,其目的是让系统在不完全重启服务的情况下,仅重新加载并应用新的配置。这对于许多服务来说是十分重要的,因为它可以在不影响服务整体运行的前提下实现配置的热更新。例如,在网络配置变更时,调用 /etc/init.d/network reload 就可以让系统在不关闭网络连接的前提下,重新读取 /etc/config/network 配置文件并按照新配置调整网络设置。这样做的好处是可以避免因服务重启带来的短暂网络中断。相比之下,如果是使用其他参数(如 start 或 stop),它们分别代表启动或停止服务,这会导致服务状态的显著改变,很可能会影响到依赖于该服务的其他功能。而 reload 参数提供了更平滑的配置过渡方式,更适合于实时生效的配置更新场景。
现在单击网页中的 Save & Apply,打开串口助手可以看到开发板中输出了如下内容:(配置更改才会执行,即填入不同数字才会输出reload example )
reload example
说明确实执行了/etc/init.d/example 中的 reload 函数。
将软件包添加到路由器(以dtu程序为例)
dtu文件夹下Makefile
include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/package.mk #引入全局变量规则和包构建规则
PKG_NAME:=dtu
PKG_VERSION:=1.0
define Package/$(PKG_NAME)
CATEGORY:=My Package
#DEPENDS:= 如果需要依赖其他软件包,在这里添加
TITLE:=DTU program
endef #定义包的基本信息,包括名称 版本号 类别 标题
#编译前执行
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
$(CP) ./src/* $(PKG_BUILD_DIR)/
endef #在编译前运行 将src源文件复制到编译目录PKG_BUILD_DIR下
#安装时执行
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/dtu $(1)/bin/
endef #创建bin目录,将PKG_BUILD_DIR下可执行文件安装到/bin下
$(eval $(call BuildPackage,dtu))
将源文件放入src文件并在src下创建Makefile文件
all:
$(CC) dtu.c -o dtu
make menuconfig勾选dtu,M生成单独软件包,*包含进固件
运行 make ./package/dtu/compile V=s后在/bin/packages/mipsel_24kc/base下生成dtu_1.0_mipsel_24kc.ipk 运行scp dtu_1.0_mipsel_24kc.ipk root@192.168.1.1:/root 将软件包拷贝到root目录下
运行opkg install dtu_1.0_mipsel_24kc.ipk安装软件包 在/bin下生成dtu可执行程序
实现软件包开机自启
在路由器上实现
在/etc/init.d下创建脚本server_init
#!/bin/sh /etc/rc.common
START=99
STOP=10
start()
{
/bin/dtu &
}
stop()
{
killall dtu
}
添加执行权限
chmod +x server_init
在/etc/rc.d/rc*.d下创建链接 系统开机后会按照预定的优先级依次启动
./server_init enable
reboot重启ps查看
在软件包上实现
在package/dtu下创建files文件夹储存启动脚本文件server_init
在dtu目录下的Makefile文件中添加代码,将files下的启动脚本文件拷贝到/etc/init.d下
并启动开机启动
#安装时执行
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/bin $(1)/etc/init.d/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/dtu $(1)/bin/
$(INSTALL_BIN) ./files/server_init $(1)/etc/init.d/
endef
#创建bin目录和/etc/init.d/目录,将PKG_BUILD_DIR下可执行文件安装到/bin下
#将server_init脚本安装到/etc/init.d/下
#安装后执行
define Package/$(PKG_NAME)/postinst
#!/bin/sh
# check if we are on real system
if [ -z "$${IPKG_INSTROOT}" ]; then
echo "Enabling rc.d symlink for dtu"
if [ -e /etc/rc.d/S??dtu ];then
rm /etc/rc.d/S??dtu
fi
if [ -e /etc/rc.d/K??dtu ];then
rm /etc/rc.d/K??dtu
fi
/etc/init.d/server_init enable
fi
exit 0
endef #设置开机自启动,检查是否存在S启动脚本和K关闭脚本
#存在则关闭,并创建新的服务
#卸载前执行
define Package/$(PKG_NAME)/prerm
#!/bin/sh
# check if we are on real system
if [ -z "$${IPKG_INSTROOT}" ]; then
echo "Removing rc.d symlink for dtu"
/etc/init.d/server_init disable
fi
exit 0
endef #判断是否为真实的系统环境(非临时安装环境)
#并禁用server_init服务
相关资料
hostname():获取主机名。
loadavg():获取系统负载平均值。
luci.model.uci模块:
cursor():创建一个UCI数据库游标。
changes():获取最近的UCI更改。
apply():应用UCI配置更改。
游标对象的 get、set、add、delete 等方法用于操作UCI配置。
luci.template模块:
render(template, ...):渲染指定的Lua模板文件。
process(template, context):处理模板并输出内容。
luci.util模块:
split(str, sep):分割字符串。
trim(s):去除字符串两侧的空白字符。
ip.IPv4(ipstr):IP地址解析。
ip.IPv6(ipstr):IPv6地址解析。
软件包:
PKG_NAME - 用于指定软件包的名称,通常是唯一标识该软件包的关键字符串。
PKG_VERSION - 表示软件包的版本号,这是构建系统用来区分不同版本软件的重要信息。
PKG_RELEASE - 这是编译发布的版本信息,可能反映了软件包在同一个版本基础上的不同编译版本或修订版本,比如补丁级别。
PKG_BUILD_DIR - 指定编译该软件包的工作目录,默认情况下,会在构建系统的临时目录(如$(BUILD_DIR))下为每个软件包创建一个单独的子目录,子目录名由软件包名和版本号组成。
PKG_SOURCE - 指定要下载的源代码包的文件名,构建系统会根据这个信息从指定位置下载源代码。
PKG_SOURCE_URL - 提供源代码包的下载地址,构建系统会从这个URL下载指定的源码包。
PKG_MD5SUM - 源代码包的MD5校验和,用于验证下载的源代码包是否完整无误。
PKG_CAT - 指定解压源代码包的方式,比如使用zcat解压.gz文件,bzcat解压.bz2文件,或者unzip解压.zip文件。
PKG_BUILD_DEPENDS - 指定该软件包在编译过程中依赖的其他软件包,这些依赖的软件包必须先于当前软件包被成功编译。这通常用于编译时依赖关系,与运行时依赖(DEPENDS)有所区别,尽管两者语法可能相似。
SECTION - 软件包分类,目前未使用,未来可能会引入分类功能。
CATEGORY - 指定软件包在menuconfig菜单中的位置,如果这个类别之前未被使用过,menuconfig会自动创建一个新的菜单来容纳该类别的软件包。
TITLE - 软件包的简短描述,用于在menuconfig中显示软件包的基本信息。
URL - 提供软件包源代码的官方网站或仓库地址,方便开发者获取更多信息。
MAINTAINER - 软件包的维护者联系信息,便于用户报告问题或寻求帮助。
DEPENDS - 定义软件包在编译和安装时所需的依赖关系。具体语法包括:
- DEPENDS:=+foo:当前软件包和foo软件包一起联动,一荣俱荣,一损俱损,如果当前软件包被选中或取消选中,foo软件包的状态也会随之同步改变。
- DEPENDS:=foo:当前软件包依赖于foo软件包,只有当foo软件包被选中时,当前软件包才会出现在menuconfig中。
- DEPENDS:=@FOO:当前软件包依赖于全局配置项CONFIG_FOO,只有当CONFIG_FOO被启用时,当前软件包才会出现在menuconfig中。
- DEPENDS:=+FOO:bar:当前软件包和bar软件包联动,同时取决于全局配置项CONFIG_FOO,当CONFIG_FOO被启用时,当前软件包依赖于bar软件包。
- DEPENDS:=@FOO:bar:当前软件包是否依赖bar软件包取决于全局配置项CONFIG_FOO,只有当CONFIG_FOO被启用且bar软件包被选中时,当前软件包才会出现在menuconfig中。
o:value("wan","WAN")
在 OpenWrt/LEDE LuCI 的上下文中,o:value("wan","WAN") 这行代码的作用是在表单选项 o 中添加一个选项值。这里的 o 是一个 Value 类型的表单选项对象。
- "wan" 是选项的值,即当用户选择这个选项时,表单提交时实际存储的数据值。
- "WAN" 是选项的显示文本,即用户在界面上看到的文字描述。
固件的etc的文件受源代码哪些文件的影响?
OpenWrt固件中/etc目录下的文件受到源代码树中多个位置的影响:
- 基础系统配置文件:
-
- /package/base-files/files/etc/:这个目录包含了OpenWrt的基本系统配置文件,如/etc/config/*系列的网络配置文件、防火墙规则文件、系统启动脚本等。
- 特定软件包的配置文件:
-
- 各个软件包目录下的files/etc/子目录:例如,当编译安装某个软件包时,该软件包的源代码目录中可能包含files/etc文件夹,其中的配置文件会在编译打包阶段被复制到固件的/etc目录下。
feeds是软件包仓库的目录,那package是什么
在OpenWRT环境中,feeds 和 package 目录都与软件包管理有关,它们的作用有所不同:
- feeds:
-
- 是一个或多个软件包仓库的集合,这些仓库通常位于远程服务器上或者本地文件系统中,并且通过版本控制系统(如Git)维护。feeds中包含了一系列预编译的软件包定义,它们提供了扩展OpenWRT功能的各种附加软件包。当执行./scripts/feeds update和./scripts/feeds install命令时,feeds中的软件包信息会被下载到本地的feeds目录中,但实际的源代码并不会直接放置在feeds目录下。
- package:
-
- 位于OpenWRT源代码树的顶级目录或其子目录中,它存储的是OpenWRT系统本身自带的软件包源代码以及由feeds安装后提取出来的第三方软件包源代码。当你通过feeds安装了某个软件包后,相应的源代码将会被解压并放入到package目录下相应的位置,以便在编译OpenWRT固件时能够将这些软件包一同编译进去。
简而言之,在OpenWRT编译流程中,feeds是软件包仓库列表和软件包元数据的来源,而package则是实际存放编译时所需软件包源代码的地方。