LUCI界面
OpenWrt 的界面其实就是网页界面,默认是由 uhttpd 服务器承载,之所以叫做 LUCI ,因为这是使用 Lua 脚本编写的控制界面,全称 Lua Unified Configuration Interface,当然目前已经不再使用 Lua 脚本了,从 OpenWrt 19.07.4 版开始,界面已经切换为使用 JavaScript 脚本来编写,其拥有更便利的页面控件,页面自由度也大大提高,因为脚本交由客户端运行,页面流畅度自然也比 Lua 界面高出不少。
下面仅介绍 OpenWrt 的 JavaScript 脚本界面规范。
完整的界面文件结构
以源代码目录的文件结构为例,一个基本的界面程序应当具备这样的目录文件结构。
openwrt ┕feeds ┕luci ┕applications ┕luci-app-name #界面程序的主目录 ┕htdocs ┊ ┕luci-static ┊ ┕resources ┊ ┕view ┊ ┕name.js # JavaScript 脚本界面文件。 ┕po ┊ ┕zh_Hans # 此目录名称对应简体中文。 ┊ ┕name.po # 界面语言翻译文件。 ┕root ┊ ┕etc ┊ ┊ ┕uci-defaults ┊ ┊ ┕luci-app-name # 软件安装完毕后默认执行的脚本(一次性脚本),可选。 ┊ ┕usr ┊ ┕share ┊ ┕luci ┊ ┊ ┕menu.d ┊ ┊ ┕luci-app-name.json # 界面菜单,在系统菜单中的名称、顺序等。 ┊ ┕rpcd ┊ ┕acl.d ┊ ┕luci-app-name.json # 权限控制文件,管控界面能执行的各类操作。 ┕Makefile # 编译文件。
界面程序的 Makefile 编写指南
# # Copyright (C) 2020 OpenWrt.org # # This is free software, licensed under the GNU General Public License v2. # See /LICENSE for more information. # 注释信息,可选。 # 所有的可选项,不需要时可以直接不写。 # 加载相关规则文件,必需。 include $(TOPDIR)/rules.mk # 在 OpenWrt 编译菜单中显示的标题,必需 LUCI_TITLE:=My package - LuCI interface # 依赖关系,可选 LUCI_DEPENDS:=+luci-mod-admin-full # 是否要限制硬件平台,可选 LUCI_PKGARCH:=all # 版本号,可选 PKG_VERSION:=1.0 # 修订版本号,可选 PKG_RELEASE:=1 # 标记日期,可选 PKG_DATE:=20201130 # 作者信息,可选 PKG_MAINTAINER:=OpenWrt-Life <admin@ikghx.com> # 软件许可信息,可选 PKG_LICENSE:=Apache-2.0 # 加载相关规则文件,必需。 include ../../luci.mk # 下面一行是 Luci 界面专用调用标识,必需,如果缺失会导致不会被加入 OpenWrt 的编译菜单中。 # call BuildPackage - OpenWrt buildroot signature
JavaScript 脚本界面
至于如何编写完整的 JavaScript 脚本界面,不在本指南范围内,你应该先去学会通用的 JavaScript 编写知识,这里只额外说明 OpenWrt 平台上特有的一些接口或注意事项。
以源代码中已有的 Samba4 的 JavaScript 脚本界面为例。
'use strict'; //使用紧凑格式,编译时会自动压缩此脚本。 'require view'; 'require fs'; 'require form'; 'require tools.widgets as widgets'; //上面这些都是声明的接口调用,OpenWrt下有效。 return view.extend({ load: function() { return Promise.all([ L.resolveDefault(fs.stat('/sbin/block'), null), //读取文件状态。 L.resolveDefault(fs.stat('/etc/config/fstab'), null), L.resolveDefault(fs.stat('/usr/sbin/nmbd'), {}), L.resolveDefault(fs.stat('/usr/sbin/samba'), {}), L.resolveDefault(fs.stat('/usr/sbin/winbindd'), {}), L.resolveDefault(fs.exec('/usr/sbin/smbd', ['-V']), null), //执行命令,获取版本号。 ]); }, render: function(stats) { var m, s, o, v; v = ''; m = new form.Map('samba4', _('Network Shares-samba4')); //关联配置文件/etc/config/samba4,括号内为页面标题名称。 if (stats[5] && stats[5].code === 0) { v = stats[5].stdout.trim(); } s = m.section(form.TypedSection, 'samba', 'Samba ' + v); //配置名为 samba 的子配置节点,此处也为页面说明,这里仅用于显示samba的版本号了。 s.anonymous = true; //隐藏配置文件中的 Section 节点名称。 s.tab('general', _('General Settings')); //子菜单,选项卡界面。 s.tab('template', _('Edit Template')); s.taboption('general', form.Flag, 'enable', _('Enable')); //复选框选项,此选项位于'general'子菜单下。 s.taboption('general', widgets.NetworkSelect, 'interface', _('Interface'), //这是菜单名称 _('Listen only on the given interface or, if unspecified, on lan'));//这是菜单注释,详细说明。 o = s.taboption('general', form.Value, 'workgroup', _('Workgroup')); o.placeholder = 'WORKGROUP'; //占位符,用于提示用户应该输入什么样的字符。 o = s.taboption('general', form.Value, 'description', _('Description')); o.placeholder = 'Samba4 on OpenWrt'; s.taboption('general', form.Flag, 'enable_extra_tuning', _('Enable extra Tuning'), _('Enable some community driven tuning parameters, that may improve write speeds and better operation via WiFi.')); s.taboption('general', form.Flag, 'allow_legacy_protocols', _('Allow legacy protocols'), _('Allow connection using smb(v1) protocol.')); s.taboption('general', form.Flag, 'disable_async_io', _('Force synchronous I/O'), _('On lower-end devices may increase speeds, by forceing synchronous I/O instead of the default asynchronous.')); s.taboption('general', form.Flag, 'macos', _('Enable macOS compatible shares'), _('Enables Apple\'s AAPL extension globally and adds macOS compatibility options to all shares.')); o = s.taboption('general', form.Value, 'nice', _('Scheduling priority'), _('Set the scheduling priority of the spawned process.')); o.datatype = 'range(-20,19)'; //限制此输入框的格式,只允许输入-20至19的数字。 o.default = '0'; //此选项的默认值。 o.rmempty = false; //是否允许为空值。false则表示否,不允许为空值。 if (stats[2].type === 'file') { s.taboption('general', form.Flag, 'disable_netbios', _('Disable Netbios')) } //符合判断条件才会显示出来的菜单。 if (stats[3].type === 'file') { s.taboption('general', form.Flag, 'disable_ad_dc', _('Disable Active Directory Domain Controller')) } if (stats[4].type === 'file') { s.taboption('general', form.Flag, 'disable_winbind', _('Disable Winbind')) } o = s.taboption('template', form.TextValue, '_tmpl', _(''), _("This is the content of the file '/etc/samba/smb.conf.template' from which your samba configuration will be generated. " + "Values enclosed by pipe symbols ('|') should not be changed. They get their values from the 'General Settings' tab.")); //这句话太长,不方便阅读,可以切断,用 + 号连接,这样程序仍然会认为这是一句话。 o.rows = 20; //行高 o.cfgvalue = function(section_id) { return fs.trimmed('/etc/samba/smb.conf.template'); }; //读取指定的文件。 o.write = function(section_id, formvalue) { return fs.write('/etc/samba/smb.conf.template', formvalue.trim().replace(/\r\n/g, '\n') + '\n'); }; //写入数据到指定的文件。 s = m.section(form.TableSection, 'sambashare', _('Shared Directories'), _('Please add directories to share. Each directory refers to a folder on a mounted device.'));//配置名为 sambashare 的子配置节点 s.anonymous = true; s.addremove = true; //允许添加或删除此配置节点 s.option(form.Value, 'name', _('Name')); o = s.option(form.Value, 'path', _('Path')); if (stats[0] && stats[1]) { o.titleref = L.url('admin', 'system', 'mounts'); } o = s.option(form.Flag, 'browseable', _('Browse-able')); o.enabled = 'yes'; //使复选框选项使用指定的参数,勾选则写入参数 yes o.disabled = 'no'; //使复选框选项使用指定的参数,不勾选则写入参数 no o.default = 'yes'; o = s.option(form.Flag, 'read_only', _('Read-only')); o.enabled = 'yes'; o.disabled = 'no'; o.default = 'no'; // smb.conf default is 'yes' o.rmempty = false; s.option(form.Flag, 'force_root', _('Force Root')); o = s.option(form.Value, 'users', _('Allowed users')); o.rmempty = true; o = s.option(form.Flag, 'guest_ok', _('Allow guests')); o.enabled = 'yes'; o.disabled = 'no'; o.default = 'yes'; // smb.conf default is 'no' o.rmempty = false; o = s.option(form.Flag, 'guest_only', _('Guests only')); o.enabled = 'yes'; o.disabled = 'no'; o.default = 'no'; o = s.option(form.Flag, 'inherit_owner', _('Inherit owner')); o.enabled = 'yes'; o.disabled = 'no'; o.default = 'no'; o = s.option(form.Value, 'create_mask', _('Create mask')); o.maxlength = 4; //限制字符长度,4表示最多只允许4个英文字符长度。 o.default = '0666'; // smb.conf default is '0744' o.placeholder = '0666'; o.rmempty = false; o = s.option(form.Value, 'dir_mask', _('Directory mask')); o.maxlength = 4; o.default = '0777'; // smb.conf default is '0755' o.placeholder = '0777'; o.rmempty = false; o = s.option(form.Value, 'vfs_objects', _('Vfs objects')); o.rmempty = true; s.option(form.Flag, 'timemachine', _('Apple Time-machine share')); o = s.option(form.Value, 'timemachine_maxsize', _('Time-machine size in GB')); o.rmempty = true; o.maxlength = 5; return m.render(); } });
界面脚本与配置文件是对应关系,将关联的配置文件 /etc/config/samba4 内容贴出来,两相对照才能更准确的理解各个参数的意义。
config samba option name 'OpenWrt' option workgroup 'WORKGROUP' option description 'Samba on OpenWrt' option charset 'UTF-8' option enable '1' option disable_ad_dc '1' option disable_winbind '1' option interface 'lan' option nice '0' config sambashare option name 'sda' option path '/mnt/sda3' option read_only 'no' option force_root '1' option guest_ok 'yes' option create_mask '0666' option dir_mask '0777'
通过上面的实例,你应该已经初步知晓了, OpenWrt 当前的 JavaScript 脚本界面的运行规则了,下面介绍一些常用的其它脚本规则。
获取网络接口的选项
'require tools.widgets as widgets';//申明调用接口 //获取接口的网络名称,例如 lan wan wan6 s.option('general', widgets.NetworkSelect, 'interface', _('Interface'));
'require tools.widgets as widgets';//申明调用接口 //获取接口的设备名称,例如 eth0 br-lan s.option('general', widgets.DeviceSelect, 'interface', _('Interface name'));
获取系统用户列表
'require tools.widgets as widgets';//申明调用接口 //获取操作系统内的用户列表。 s.option('general', widgets.UserSelect, 'user', _('Run daemon as user'));
配置一个下拉列表框,只允许用户选择预设的参数。
'require form';//申明调用接口 o = s.option(form.ListValue, 'leasetime', _('Lease time')); o.value('1h', _('One hour')); //下拉列表框将显示预设的选项名称,此名称可以使用翻译文件进行转换。 o.value('2h'); //此选项将直接显示为 2h o.value('1d', _('One day')); o.value('7d', _('A week')); o.rmempty = true;//允许此选项为空值,即允许此选项不存在。
配置一个下拉列表框,允许用户选择预设的参数,也允许用户自行输入参数。
'require form';//申明调用接口 o = s.option(form.Value, 'leasetime', _('Lease time')); o.value('1h', _('One hour')); //下拉列表框将显示预设的选项名称,此名称可以使用翻译文件进行转换。 o.value('2h'); //此选项将直接显示为 2h o.value('1d', _('One day')); o.value('7d', _('A week')); o.rmempty = true;//允许此选项为空值,即允许此选项不存在。
配置一个动态列表,允许自由添加删除多个参数,展示一个实例样板。
'require form';//申明调用接口 o = s.taboption('general', form.DynamicList, 'address', _('Static address'), _('List of domains to force to an IP address')); o.optional = true;//表示此选项为可选属性。 o.placeholder = '/openwrt.xyz/192.168.9.1'; //占位符,用于提示用户应该输入什么样的字符。
配置含多个子菜单的选项卡界面
'require form';//申明调用接口 o = s.tab("general", _("General Settings")); o = s.tab("advanced", _('Advanced Settings')); o = s.taboption('general', form.Flag, "enabled", _("Enabled")); o = s.taboption('advanced', form.Flag, "enable", _("Enable"));
配置选项关联另外的某个选项,当关联的选项为指定的值时才显示。
o.depends("advanced", "1"); //关联选项 advanced 参数为 1 时,此选项才会显示。
限制选项参数格式的常用类型
o.datatype = 'uinteger';//只允许输入正整数。 o.datatype = 'range(-20,19)';//只允许输入范围内的数值,(只允许输入-20至19的数字) o.datatype = 'list(string)';//限制此选项的格式为列表值,一般用于配合动态列表。 o.datatype = 'directory';//只允许输入路径格式。 o.datatype = 'string';//允许任意字符组合。(此为默认值,即如果不指定数据类型就默认是这个。) o.datatype = 'hostname';//限制参数为主机名。 o.datatype = 'host';//限制参数为网站域名。 o.datatype = 'port';//只允许输入端口号。 o.datatype = 'portrange';//限制参数为端口范围,即允许的书写格式如: 1025-65535 o.datatype = 'ipaddr';//限制参数为ip地址。 o.datatype = 'ip4addr';//限制参数为ipv4地址。 o.datatype = 'ip6addr';//限制参数为ipv6地址。 o.datatype = 'ipaddrport';//限制参数为ip地址加端口,书写格式如:127.0.0.1:80 o.datatype = 'max(1024)';//限制参数最大值。
界面语言翻译文件
在对应的语言目录下创建 po 翻译文件即可。
# # OpenWrt-Life <admin@ikghx.com>, 2020. # # msgid 为源语言文字。 # msgstr 为翻译后的文字。 # 这是可选注释,标记这个词句在源代码 JavaScript 脚本中的第几行。 #: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:97 msgid "Allow guests" msgstr "允许匿名用户" # 对于比较长的语句,为了方便阅读源码,可以断句书写,见下面的实例。 # 但需要注意,不允许出现连续两个空格,否则会导致翻译失效。 #: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:46 msgid "" "Enables Apple's AAPL extension globally and adds macOS compatibility options " "to all shares." msgstr "全局启用 Apple 的 AAPL 扩展,并为所有共享添加 macOS 兼容性选项。" #: applications/luci-app-samba4/htdocs/luci-static/resources/view/samba4.js:63 msgid "" "This is the content of the file '/etc/samba/smb.conf.template' from which " "your samba configuration will be generated. Values enclosed by pipe symbols " "('|') should not be changed. They get their values from the 'General " "Settings' tab." msgstr "" "这是将从其上生成 samba 配置的文件“/etc/samba/smb.conf.template”的内容。由管道" "符(“|”)包围的值不应更改。它们将从“常规设置”标签中获取其值。" # 翻译文件的冲突优先级。 # 当同一个源词句被翻译为不同的词句,则最后安装的会优先显示于所有界面。 # 例如单词 “Enable”,有 A、B、C 软件界面都含有这个源词语,但翻译词语却各不相同。 # 例如 A 翻译为“启用”,B 翻译为“打开”,C 翻译为“开启” # A 安装后,“Enable”显示为“启用” # B 安装后,所有界面的“Enable”将显示为“打开” # C 安装后,所有界面的“Enable”将显示为“开启” # 所以为了避免出现这种翻译混乱,请各位开发者用词准确且专业规范。 # 翻译文件的复用。 # 当一个界面所含的源词句与系统中已安装的翻译文件相同, # 则即使这个界面不提供任何翻译文件,界面的相关词句仍然能显示已有的翻译词句。
默认执行脚本
在 uci-defaults 目录下可以放置脚本文件,用于在 ipk 安装完毕后自动执行命令,当然也可以不用它,目前建议至少用于重载 rpcd 服务,以便新安装的界面能顺利显示,否则就必须关闭浏览器重新登录才能显示新安装的界面菜单。
#!/bin/sh /etc/init.d/rpcd reload exit 0
界面菜单
在 menu.d 目录下的 json 文件为软件的界面菜单文件,用于控制软件在系统菜单中的名称、顺序等。
下面用几个实例介绍 json 界面菜单文件的书写规范。
{ "admin/services/samba4": { //表示位于系统菜单的“服务”下。 "title": "Network Shares-samba4", //菜单标题,可以由翻译文件转换。 "action": { "type": "view", //类型view,表示默认的调用格式。 "path": "samba4" // JavaScript 脚本的路径,此处表示文件在 view 目录下,不用写后缀名。 }, "depends": { //关联依赖项。 "acl": [ "luci-app-samba4" ], //权限控制文件的名称,不用写后缀名。 "uci": { "samba4": true } //表示关联 uci 配置文件 samba4,当配置文件存在才能显示界面菜单。(/etc/config/samba4) } } }
{ "admin/network/sqm": { //表示位于系统菜单的“网络”下。 "title": "SQM QoS", "order": 59,//菜单排序,数字越大排序越靠后,如果与其它界面数值相同,则以字母顺序分先后。 "action": { "type": "view", "path": "network/sqm" // JavaScript 脚本的路径,此处表示文件在 view/network 目录下。 }, "depends": { "acl": [ "luci-app-sqm" ] } } }
{ "admin/troubleshooting": { //表示一级菜单。 "title": "Troubleshooting", "order": 80, "action": { "type": "firstchild" //表示此菜单不存在则自动创建。 } }, "admin/troubleshooting/packet_capture": { //表示位于系统菜单的“troubleshooting”下。 "title": "Packet Capture", "order": 1, "action": { "type": "view", "path": "packet_capture/tcpdump" }, "depends" : { "acl": [ "luci-app-packet-capture" ], "uci": { "packet_capture": true }, "fs": { "/usr/libexec/packet_capture": "executable", //表示需要读取到这些文件存在且可执行。 "/usr/libexec/packet_capture_start": "executable", "/usr/libexec/packet_capture_stop": "executable" } } } }
权限控制文件
OpenWrt 为 JavaScript 脚本界面引入的权限控制机制,用于管制脚本界面能执行哪些系统操作。
{ "luci-app-samba4": { //这个权限控制文件的名称。 "description": "Grant access to LuCI app samba4", //描述,可由翻译文件转换。 "read": { //允许读取操作。 "file": { //表示关联对象为文件 "/etc/samba/smb.conf.template": [ "read" ], //表示允许读取这个文件。 "/usr/sbin/smbd": [ "exec" ] //表示允许执行此文件,只能读取数据。 }, "uci": [ "samba4" ] //表示允许读取uci配置文件 samba4(/etc/config/samba4) }, "write": { //允许写入操作。 "file": { "/etc/samba/smb.conf.template": [ "write" ] //允许写入这个文件。 }, "uci": [ "samba4" ] //表示允许写入uci配置文件 samba4 } } }
{ "luci-app-sqm": { "description": "Grant UCI access for luci-app-sqm", "read": { "file": { "/var/run/sqm/available_qdiscs": [ "list" ], //表示在这个目录下执行 list 命令。 "/usr/lib/sqm/*.qos.help": [ "read" ] }, "uci": [ "sqm" ], "ubus": { //表示允许 ubus 命令调用 "file": [ "read", "list" ], //允许 file 项目调用 read 和 list 命令。 "luci": [ "setInitAction" ] //允许界面调用 setInitAction } }, "write": { "uci": [ "sqm" ] } } }
{ "luci-app-packet-capture": { "description": "Grant access to tcpdump ubus object", "read": { "cgi-io": [ "download", "exec" ], //允许执行的 cgi-io 操作。 "ubus": { "tcpdump": [ "*" ], //允许通过 ubus 调用 tcpdump 执行任意命令。 "luci": [ "getProcessList" ] //允许获取 luci 流程列表。 }, "uci": [ "packet_capture", "system" ], "file": { "/tmp/capture.pcap": [ "read" ] } }, "write": { "uci": [ "packet_capture" ], "file": { "/usr/libexec/packet_capture_start": [ "exec" ], //允许执行此文件,并可写入数据。 "/usr/libexec/packet_capture_stop": [ "exec" ], "/usr/libexec/packet_capture": [ "exec" ], "/tmp/capture.pcap": [ "write" ] //允许写入此文件。 } } } }
界面调试技巧
开发软件界面的时候,经常需要查看界面改动后的实际效果,难道必须要编译为 ipk 文件再安装吗?并不是,通过上面的学习,你应该了解到界面文件其实都是纯文本状态,编译过程只是压缩打包而已,并不是为了编译成二进制文件,所以只需要把文件放入对应的系统目录,然后刷新浏览器页面,或者清空浏览器缓存即可。
# 以 samba4 为例,将相关文件放入对应的系统目录即可。 # JavaScript 脚本界面 /www/luci-static/resources/view/samba4.js # UCI 配置文件 /etc/config/samba4 # 界面菜单文件 /usr/share/luci/menu.d/luci-app-samba4.json # 权限控制文件 /usr/share/rpcd/acl.d/luci-app-samba4.json # 无法直接显示 po 翻译文件,需要转换为 lmo /usr/lib/lua/luci/i18n/samba4.zh-cn.lmo
OpenWrt Procd系统初始化和守护程序管理
在 OpenWrt 上要让一个程序正常运行,自然也需要一个启动脚本来提供服务,不然就只能使用命令行操控了,OpenWrt 下提供了一款类似于 systemd 的进程管理守护程序,称之为 Procd,其提供了非常强大的脚本功能,对于程序运行大有助益。
OpenWrt官网关于 Procd脚本的说明:OpenWrt Project: procd init script parameters
以程序 memcached 为例,演示传统启动脚本和 Procd 脚本的区别。
#!/bin/sh /etc/rc.common # Copyright (C) 2010-2011 OpenWrt.org # 这是传统启动脚本实例。 # 开机自启动时的顺序,1-99,数字越大,启动越晚。 START=80 start_instance () { local section="$1" config_get user "$section" 'user' # 获取配置文件参数 config_get maxconn "$section" 'maxconn' config_get listen "$section" 'listen' config_get port "$section" 'port' config_get memory "$section" 'memory' config_get options "$section" 'options' service_start /usr/bin/memcached -d -u $user \ -c $maxconn -l $listen \ -p $port -m $memory $options # 启动参数 } start() { config_load 'memcached' config_foreach start_instance 'memcached' } stop() { service_stop /usr/bin/memcached }
#!/bin/sh /etc/rc.common # Copyright (C) 2010-2011 OpenWrt.org # 这是 Procd 脚本的实例。 START=80 STOP=10 # 使用 PROCD 的标识,必需。 USE_PROCD=1 PROG=/usr/bin/memcached start_instance () { local section="$1" config_get user "$section" 'user' # 获取配置文件参数 config_get maxconn "$section" 'maxconn' config_get listen "$section" 'listen' config_get port "$section" 'port' config_get memory "$section" 'memory' config_get options "$section" 'options' config_get_bool enable "$section" enable 0 [ "$enable" -gt 0 ] || return 1 # 获取 UCI 配置文件中的 enable 参数,以判断是否应该启动。 procd_open_instance procd_set_param command "$PROG" # 程序可执行文件路径。 procd_append_param command -u $user # 附加参数 procd_append_param command -c $maxconn procd_append_param command -l $listen procd_append_param command -p $port procd_append_param command -m $memory procd_append_param command $options procd_set_param respawn # 守护进程,当进程出现故障未响应时自动重载。 procd_set_param stdout 1 # 将输出信息转发至系统日志。 procd_set_param stderr 1 # 将错误日志转发至系统日志。 procd_close_instance } start_service() { config_load 'memcached' config_foreach start_instance 'memcached' } service_triggers() { procd_add_reload_trigger "memcached" # 监控 UCI 配置文件,当文件发生变化时自动重载程序。 # 例如通过界面勾选启用,再点击保存并应用后,则程序就能自动运行。 }
搭建OpenWrt本地软件源
开发软件过程中,免不了要经常进行实机测试,难道要搭建一个 HTTP 服务器来提供安装源吗?并不需要,经验丰富的开发者应该已经想到了,那就是直接使用 OpenWrt 自带的 uhttpd 来作为 HTTP 服务器,并不需要任何额外设置,只需要将软件源目录软链接至 www 目录即可。
首先通过 FTP 或其它各种方式,将软件源目录上传至路由器磁盘空间。
然后使用软链接命令,将软件源目录链接至 www 目录即可。
ln -s /mnt/sda3/soft /www/soft
此时可以打开浏览器测试,看是否能成功访问。
接着再修改软件包的 “OPKG 配置” 里的软件源地址。
然后就可以开始使用了。
文章来源:https://iyzm.net/openwrt/624.html
本文章由作者:佐须之男 整理编辑,原文地址: OpenWrt达人教程之 LuCI2开发入门指南