前言
又到了成胖子^_^每周一博的時間了.最近在學習openwrt luci方面的知識,為了貫穿整個知識體系,練習題目為:
通過頁面配置周期性地往/tmp/addtest文件寫入內容和時間戳
1.在web主頁面的下拉菜單做一個按鈕,進入設置頁面;
2.兩個設置項:輸入的內容和周期;
3,讀取/tmp/addtest中的內容並顯示在頁面上;
代碼已經開源,歡迎交流~
知識准備
源碼編譯及ipk生成
這部分網上相關文章很多,也可以參見拙作
LuCI
首先回答一個問題:什么是Luci?
>LuCI是OpenWrt上的Web管理界面,LuCI采用了MVC三層架構,使用Lua腳本開發.
簡單地說,Luci就是用來做openwrt的頁面的.不同於常見的html+css+javascript,Openwrt是用lua腳本語言開發的.
怎么開發一個頁面呢?
要開發一個新的功能頁面,開發者只要根據MVC框架寫些簡單的lua腳本,剩下的部分由openwrt為你自動完成.
說到MVC框架了,什么是MVC框架呢?
MVC是model+view+controller的簡寫.為了便於開發,openwrt將實現不同功能的lua腳本放在不同的文件夾中.請看下圖:
什么是controller控制器?
我們在這里設置功能在頁面的位置,同時設置點擊頁面后,將要調用的功能.是要去Model模型讀寫配置數據呢?還是要呈現一個靜態頁面,或者是直接執行lua腳本函數.
什么是model模型?
這里我們常用的是,通過cbi模塊和UCI(統一配置接口)進行交互.簡單地說,就是我們在這里將頁面和路由器里面的配置關聯起來,從而將頁面的設置寫到路由器當中.
什么是view視圖?
這個應該是最容易理解的,就是呈現的頁面的樣式,有點類似於傳統的html頁面.
上面說到了UCI(Unified Configuartion Interface),這是什么龜?
openwrt將配置用統一的格式書寫,放在規定的地方(/etc/config/),同時提供接口函數進行讀取和設置.
如果還不太明白,接着向下看.如果有可能跟着我動動手,相信你很快就會掌握:)
正文
我們先看下最終效果圖:
我們在頁面上面的System
下拉框的下面加了一個AddTest
按鈕,下面有兩個子選項:Set
和Info
.其中Set
用於選擇是否開啟功能,設置時間間隔和內容.Info
用於顯示/tmp/addtest
文件中的內容.
准備工作
首先,嗯~
你得有環境,得有電,有源碼,編譯過簡單的ipk.如果沒有,請回爐重造.
其次,建立相應的文件夾及文件.至於linux操作神馬的,我相信你一定沒有問題.
$mkdir -p ~/temp/addtest
$cd ~/temp/addtest
最終文件樹形圖
骨架已經有了,下面只需要往里面填肉了,是不是感覺很快~
不要管為什么要這樣,我們后面慢慢解釋.
controller
前面我們提到,controller主要用於控制頁面按鈕位置,以及調用的功能.首先來編輯這個文件.
$vim ~/temp/addtest/files/usr/lib/lua/luci/controller/addtest.lua
代碼如下:
module("luci.controller.addtest",package.seeall)
function index()
entry({"admin","system","addtest"},alias("admin","system","addtest","set"),_("AddTest"),99).index=true
entry({"admin","system","addtest","set"},cbi("addtest"),_("Set"),1)
entry({"admin","system","addtest","info"},call("action_info"),_("Info"),2)
end
function action_info()
if not nixio.fs.access("/tmp/addtest") then
return
end
local info = nixio.fs.readfile("/tmp/addtest")
luci.template.render("addtest_info",{info=info})
end
格式模板:
module("luci.controller.控制器名", package.seeall)
function index()
entry(路徑, 調用目標, _("顯示名稱"), 顯示順序)
end
這個腳本文件可以分為3塊:第1行,3~7行,9~16行
- 第1行
-
說明了模塊的名稱,本文在controller目錄下創建了
addtest.lua
文件,將模板中的控制器名替換為addtest
即可. - 第3行
- 第3~7行定義按鈕的位置,調用的功能,顯示名稱.其中第3行和第7行是固定的模板格式,不需要修改
- 第4行
-
entry表示添加新的模塊.
第一個參數{"admin","system","addtest"}
表示按鈕的位置.admin
表示我們這個功能只有以管理員身份登錄頁面才可以看到.system
表示一級菜單名,addtest
則是一級菜單下的子菜單.
第二個參數alias("admin","system","addtest","set")
表示調用的功能.這個按鈕沒有獨立的功能,而是將它關聯到它的下一級子菜單set
.
第三個參數_("AddTest")
表示顯示名稱,可選.如果頁面按鈕想做成中文,可以在這里設置.
第四個參數99
表示顯示順序的優先級,Luci根據這個值為同一父菜單的所有子菜單排序. - 第5行
-
第一個參數
{"admin","system","addtest","set"}
表示在addtest
下再增加一個子選項set
.
第二個參數cbi("addtest")
表示調用cbi模塊,這里將會調用到/usr/lib/lua/luci/model/cbi/addtest.lua
- 第6行
-
第二個參數
call("action_info")
表示執行指定方法,這里將會調用我們下面寫的acttion_info
函數. - 備注
-
關於
entry
第二個參數調用目標.我們還有一個template
沒有涉及,它表示訪問指定頁面.比如template(addtest_info)
將會直接訪問/usr/lib/lua/luci/view/addtest_info.htm
. - 9~16行
-
這里使用lua語言調用
nixio
接口寫了一個簡單的函數,首先判斷文件是否存在,然后讀取其中的內容賦值給變量info
,最后訪問指定頁面/usr/lib/lua/luci/view/addtest_info.htm
,同時將變量info
傳遞過去.
luci接口手冊
nixio接口手冊
UCI
UCI是openwrt的配置管理機制,它將配置統一放到/etc/config
文件夾下.詳細地介紹請參考這里.
下面來編輯這個文件
$vim ~/temp/addtest/files/etc/config/addtest
代碼如下:
config arguments
option interval ''
option content ''
Section開始語法: config '類型' '名字'
參數定義語法: option '鍵' '值'
列表定義語法: list '集合名字' '值'
簡單解釋下,我們在/etc/config
下新建一個名為addtest
的配置文件,其中類型為arguments
,名字省略.有兩個鍵,一個名為interval
用來存時間間隔.一個名為content
用來存准備周期性輸入的內容.
Model
在controller章節中,我們提到cbi
會調用到model
文件夾中的addtest.lua
文件.下面我們來編輯它.
$vim ~/temp/addtest/files/usr/lib/lua/luci/model/cbi/addtest.lua
代碼如下:
m=Map("addtest",translate("Luci practice"),translate("fat cheng's test"))
s=m:section(TypedSection,"arguments","")
s.addremove=true
s.anonymous=false
s:option(Flag,"enable",translate("Enable"))
s:option(Value,"interval",translate("Interval"))
s:option(Value,"content",translate("Content"))
local apply=luci.http.formvalue("cbi.apply")
if apply then
io.popen("/etc/init.d/addtestd restart")
end
return m
下面我們來解釋下這個文件.
- 第1行
-
模板
m = Map("配置文件文件名", "配置頁面標題", "配置頁面說明")
第一個參數:上一步我們新建配置文件/etc/config/addtest
.這里就是建立與配置文件的聯系.
第二,三兩個參數,則是頁面的主標題和副標題.還不清楚的話,翻上去看看最終效果圖,看看它們在哪里. - 第3行
-
在一個配置文件中可能有很多Section,所以我們需要創建與配置文件中我們想要的Section的聯系.
有兩種方式可以選擇:NamedSection(name,type,title,description)和TypedSection(type,title,description),前者根據配置文件中的Section名,而后者根據配置文件中的Section類型.我們選用了第二種. - 第4行
- 設定不允許增加或刪除Section
- 第5行
-
設定顯示Section的名稱,這里建議你可以試試設定為
true
,看看會發生什么. - 7~9行
-
接着則是建立與Section中的option之間的聯系.模板
s:option(交互形式,option鍵值,顯示名稱)
.
第一個參數:常見的交互形式有Value(文本框),ListValue(下拉框),Flag(選擇框).,不知道為啥我打不開 官方文檔,這里也可以 參考
第二個參數表示在配置文件中的option的鍵值
第三個參數表示,你希望在頁面上呈現的名稱.
創建后開發者無需考慮讀取以及寫入配置文件的問題,系統會自動處理. - 11~14行
-
系統會為我們在頁面上自動創建一些按鈕
Save&Apply
,Save
,Reset
.我們僅僅將配置寫入/etc/config
下對應的文件是不夠的,我們還希望可以根據這個配置進行一些操作.
這部分代碼的作用是,當你按下頁面的apply
按鈕后,相當於在串口shell下輸入/etc/init.d/addtestd restart
init.d
上一節我們已經可以讀寫配置了,怎么根據配置來進行操作呢?這是我們這一節要談的.我們來編輯~/temp/addtest/files/etc/init.d/addtestd
這個文件.
代碼如下:
#!/bin/sh /etc/rc.common
START=50
run_addtest()
{
local enable
config_get_bool enable $1 enable
if [ $enable ]; then
local interval
local content
config_get interval $1 interval
config_get content $1 content
addtest $interval $content
fi
}
start()
{
config_load addtest
config_foreach run_addtest arguments
}
stop()
{
result=`pidof addtest`
kill -9 $result
echo "addtest has stoped"
}
- 第1行
- Linux 系統根據 “#!” 及該字串后面的信息確定該文件的類型,表示這個文件需要由/bin/sh和/etc/rc.common來解釋執行.
- 第2行
- 表示啟動的優先級,這里暫時用不到
- 4~17行
-
是一個函數,主要作用是讀取
/etc/config/addtest
中的內容,然后根據是否打開開關在第15行將配置傳遞給可執行文件addtest
,由它根據配置執行指定的操作.
讀取配置的方法,我強烈推薦你閱讀 官方文檔,精煉而簡潔.
獲取布爾值類型:config_get_bool 變量名 Section名 Section參數名
獲取變量值:config_get 變量名 Section名 Section參數名
- 19~23行
-
對應於
/etc/init.d/addtestd start
.首先使用config_load 配置文件名
的方法載入配置文件,然后使用config_foreach 遍歷函數名 Section類型
的方法,遍歷配置文件中的Section. - 25~30行
-
對應於
/etc/init.d/addtestd stop
.找到addtest
這個進程的進程號,然后殺死它 - 備注
-
前一節提到的
/etc/init.d/addtestd restart
中的restart
命令,在/etc/rc.common
進行了定義,簡單來講就是先執行了stop
命令,再執行start
命令.
最后務必執行$sudo chmod 755 ~/temp/addtest/files/etc/init.d/addtestd
.
src
前一節,我們談到run_addtest
調用可執行文件addtest
,現在我們編輯這部分內容
$vim ~/temp/addtest/files/src/addtest.c
代碼如下:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int index;
for(index=0; index<10; index++)
{
FILE *fp=fopen("/tmp/addtest","at");
system("date >> /tmp/addtest");
fprintf(fp, "%s\n", argv[2]);
fclose(fp);
printf("interval=%d\n",atoi(argv[1]));
sleep( atoi(argv[1]) );
}
return 0;
}
這部分代碼比較簡短,我們不再解釋.需要掌握的點有:
1.
argc
和argv[]
的使用方法
2.fopen
函數,fclose
函數以及fprintf
函數的使用方法
3.system
函數的使用方法
4.sleep
函數和atoi
函數的使用方法,argv[1]
的類型為char
需要轉換為整型.
通過這個可執行文件,我們周期性地將時間戳和內容寫入了/tmp/addtest
文件.
最后我們寫一個簡單的Makefile:
$vim $vim ~/temp/addtest/files/src/Makefile
代碼如下:
addtest : addtest.o
$(CC) addtest.o -o addtest
addtest.o : addtest.c
$(CC) -c addtest.c
clean :
rm *.o addtest
View
上一節,我們已經根據配置將指定的內容周期性地寫入了/tmp/addtest
.在controller那一節,我們的函數action_info
讀取了/tmp/addtest
中的內容並訪問指定頁面/usr/lib/lua/luci/view/addtest_info.htm
,同時將讀取的內容通過變量info
傳遞過去.
下面我們來編輯這個頁面,
$vim ~/temp/addtest/files/usr/lib/lua/luci/view/addtest_info.htm
代碼如下:
<%+header%>
<h2><a id="content" name="content"><%:Addtest Info%></a></h2>
<div id="content_addtest_info">
<textarea readonly="readonly" wrap="off" rows="<%=info:cmatch("\n")+2%>" id="info"><%=info:pcdata()%></textarea>
</div>
<%+footer%>
這部分和傳統的html
很類似,我主要是根據其他頁面照貓畫虎,不是很美觀.有機會還要加強這個方面的學習.
Makefile
不知不覺,我們居然已經將代碼全部寫完了,竟還有點戀戀不舍呢.下面我們用一個Makefie
文件將它們打包生成一個ipk文件.
$vim ~/temp/addtest/Makefile
代碼如下:
include $(TOPDIR)/rules.mk
PKG_NAME:=addtest
PKG_VERSION=1.0
PKG_RELEASE:=1
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
define Package/addtest
SECTION:=utils
CATEGORY:=Utilities
TITLE:=Addtest--print something to /var/addtest
endef
define Package/addtest/description
It's a test,print something to /var/addtest cyclicaliy
endef
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
$(CP) ./src/* $(PKG_BUILD_DIR)/
endef
define Package/addtest/postinst
#!/bin/sh
rm -rf /tmp/luci*
endef
define Build/Configure
endef
define Build/Compile
$(call Build/Compile/Default)
endef
define Package/$(PKG_NAME)/install
$(CP) ./files/* $(1)/
$(INSTALL_DIR) $(1)/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/addtest $(1)/bin
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
Makefile的解釋,請參見拙作.我們這里稍作補充.
- 26~29行
-
由於luci會將模塊加載到
/tmp
目錄下運行,每次新加載luci模塊后,需要執行$rm -rf /tmp/luci*
.這里表示安裝了ipk之后,將會自動執行刪除命令,重新載入. - 39行
-
$(1)是傳入的參數,表示系統鏡像目錄,你可以將之視為路由器最后的文件系統.所以這句的意思就是將我們
files
下的內容拷貝到路由器的文件系統中.這也是我們為什么要建立一開始那么復雜的目錄樹的原因.
編譯&安裝
簡直像裹腳布一樣,又臭又長.不要說讀了,我自己寫的都快有點受不了了.讀到這里的人真是辛苦了,下面到了我們收獲果實的時候了.
將文件拷貝到源碼目錄的package
目錄下.其余部分,請參考拙作
$cp ~/temp/addtest ~/openwrt/package
把它拷貝到你的開發板中,試試看.
調試方法
我們當然希望可以一次成功,不過世間不如意之事十之八九.我來談談我自己的調試方法.
-
src
部分 -
src
文件下有Makefile
文件,你可以直接在編譯機上執行$make
生成可執行文件addtest
,然后在編譯機上src
目錄下執行$./addtest 參數1 參數2
.最后記得執行$make clean
. -
luci
部分 -
將ipk安裝到開發板后,可以通過串口或者ssh的方式登錄開發板,然后直接在開發板中修改文件內容,再執行
$rm -rf /tmp/luci*
.最后重新載入設備頁面.
尾記
不知不覺到了分手的時候,竟感覺有些憂桑呢.
不足
- 我自己剛接觸學習,難免很多不足
- 頁面輸入沒有防呆機制
多多包含:)
感謝
除了官方文檔之外,這兩篇博客給我很多指導:
開發OpenWrt路由器上LuCI的模塊, openwrt中luci學習筆記.
我的同事寧財神給我們做了luci的框架介紹,同時在我的調試過程中,給予我很多幫助.
最后感謝管工給出這樣一個練習題,雖然很小巧,居然可以貫通整個知識體系.我現在還是為他的高屋建瓴感到驚嘆.
Q&A
在整篇文章學習完成后,我們希望可以回答以下幾個問題:
1.MVC是什么?各部分有哪些功能?
2.怎么在頁面上指定位置做出一個子頁面.
3.怎么將配置寫入到路由器中,又怎么讀取?
4.頁面怎么和可執行文件關聯起來?或者通俗地說,頁面點了一下,開發板怎么就執行了命令.
5.ipk怎么生成,安裝過程中發生了什么?