うたカモ技術ブログ

Linux OpenWrt

OpenWrtアプリケーション開発   LuCI用カスタムページの作成と追加(難易度Lv1)

post:     update: 

この記事で紹介するソースコードは自由に使っていただいて構いません。 アプリケーションの開発や自己学習にお役立て下さい。ただし、当ブログでは掲載するソースコードを流用・利用したことによる損害等につきましては 一切の責任を負いません。自己責任で利用お願いします。

今回はOpenWrtアプリケーション開発として、自作のカスタムページをWebUIのLuCIに追加する方法を紹介します。

あくまでOpenWrtの既存機能やアプリケーションはそのままで、LuCIを通してユーザーに提供するページ(HTMLファイルやLuaスクリプト)を新たに作成・追加します。

このようなことから、この記事で紹介するカスタムページの実装難易度は最も簡単なLv1と定義します。

この記事を読んで恩恵がある読者は以下のような方です。

  1. これからOpenWrtのアプリケーション開発を学びたい人
  2. 自分にとって使いやすいユーザーインタフェース(設定画面)を作りたい人
  3. バグや機種未対応で正常に動作しないサービスに対して、適切にサービスを有効化できる設定画面を追加したい人

※3番目の方は、サービスが動作しない問題が設定パターンである場合に限ります。設定パターン以外の場合、標準アプリ自体のバグ (または、それに準ずるバグ)と考えられるため、ここで紹介する情報だけでは解決できません。

興味があれば、ご参考ください。

作成中の関連記事について

現在、本記事の関連として以下の記事を作成中です。この記事も合わせ、実装難易度を3段階に分けて紹介します。

  • OpenWrtアプリケーション開発 自作スクリプトと連携するLuCI用プラグインの作成と追加(難易度Lv2)(作成中)
  • OpenWrtアプリケーション開発 自作UBUS対応アプリと連携するLuCI用プラグインの作成と追加(難易度Lv3)(作成中)

目次

  1. 実施環境と補足事項について
  2. サンプルのダウンロード
  3. 今回作成するLuCIカスタムページの概要
  4. カスタムページ作成の流れ
  5. 本題:開発準備
  6. 本題:カスタムページの作成
  7. 本題:カスタムページのパッケージ作成とインストール
  8. LuCIがシステム内部で利用するUCI操作プロセス(/sbin/rpcd)について
  9. 【おまけ】Luaスクリプト用UBUSモジュールでカントリコード一覧を取得する方法
  10. おわりに
  11. 参考文献

実施環境と補足事項について

この記事は以下の環境で実施した結果を元に作成しています。

#開発用PCの実行環境OS
ubuntu 22.04 LTS 64bit ※今回はUbuntuで開発しましたが、Windows(WSL2)上でもOKです。
#OpenWrtデバイス
  名称:Raspberry Pi3 Model B
  CPU:ARM Cortex-A53 (1.2GHz)
  SOC:Broadcom BCM2837
  RAM:1GB
  ストレージ(media):MicroSDカード
#OpenWrt
  OpenWrt 22.03
パッケージ
  luci - git-24.078.77116-81496b8
  luci-compat - git-24.078.77116-81496b8
  luci-lua-runtime - git-24.078.77116-81496b8

この記事で紹介するカスタムページの作成と追加方法に関して、機種依存はありません。 この記事ではRaspberry Pi3BをOpenWrtデバイスとしていますが、どんなデバイスでもOKです。(※LuCIがインストールされていることが前提です。)

[補足事項]
記事内で紹介するコンソール表記は次の通りです。

開発用PCのコンソール表記
ユーザー名とカレントディレクトリを色付けして表記します。
user:~/openwrt$ command
OpenWrtのコンソール表記
ユーザー名はroot@OpenWrtです。
root@OpenWrt:~# command

この記事が想定するOpenWrtデバイスの初期状態

この記事で想定するOpenWrtデバイスの状態は、OpenWrtがインストールされた直後の初期状態です。
IPアドレスに着目すれば、OpenWrtデバイス自身は192.168.1.1/24のIPアドレスを持ち、192.168.1.0/24のネットワークを管理しています。

サンプルのダウンロード

今回取り上げるLuCIのカスタムページ一式は私のGitHubリポジトリにあります。 パッケージ作成に必要なMakefileも揃った完全版です。

一応、この記事の内容に沿って1つずつ作業をしていけば、このリポジトリにあるものと同じものが作成できます。

記述ミスなどで躓いたら、以下のリポジトリをダウンロードして手っ取り早く動作を確認してみてください。

今回作成するLuCIカスタムページの概要

この記事で作成するLuCIのカスタムページは以下の動画で紹介するものです。例として、OpenWrt開発入門の第3回第4回で紹介したアクセスポイント設定をカスタムページ経由で実施しています。

内部構成としては、既存のOpenWrtのLuCI環境にカスタムページを追加しただけです。

ネットワーク設定とWi-Fi設定のカスタムページを作りますので、ネットワーク・Wi-Fi制御用アプリケーションのnetifdが 管理する/etc/config/network/etc/config/wirelessの各オプションの値を変更・適用できます。

カスタムページ作成の流れ

この記事では、流れに沿って前節で紹介したLuCIのカスタムページを作成・追加します。

先ず開発準備として、OpnWrtデバイスに対してファイル転送する環境を整えた後、カスタムページ用のHTMLファイルやLuaスクリプトを 流し込んで動作を確認します。

全ての動作確認が取れたら他のユーザーに配布できるように、それらのファイルをアプリケーションパッケージにまとめます。

このように、本筋のカスタムページの作成と追加を一番簡単な方法で先に説明し、最後にパッケージ化の方法を紹介することで 一通りの開発方法が分かる構成になっています。

それでは、次節から本題に入ります。

本題:開発準備

LuCIのカスタムページを作成する前に開発準備をしましょう。

準備1:OpenWrtのインストール 

当然ですが、まずはOpenWrtデバイスを用意しましょう。一応、私の記事にはRaspberry Pi1~4を対象にOpenWrtファームウェア をインストールする方法を紹介していますので、良ければ参考にしてみてください。

なお今回、LuCI(WebUI)は依存モジュールとしてluci-compatluci-lua-runtimeを利用しますので、メニューコンフィグの該当項目に チェックを入れてください。

準備2:OpenWrtデバイスと開発用PCを接続してコンソールにログインする

OpenWrtデバイスを用意できたら、次の図のようにLANケーブルでOpenWrtデバイスと開発用PCを繋いでください。

※図ではRaspberry PiがOpenWrtデバイスです。読者の方は、ご自身が持つOpenWrtデバイスとして考えてみてください。

OpenWrtデバイス(192.168.1.1/24)が管理するデフォルトのLANネットワークは192.168.1.0/24であり、SSHサーバーのdropbearが起動するようになっています。

また、SSHログインに必要な初期パスワード設定は無しのため、以下のコマンドを実行することでログインが成功します。

kamo@kamo:~$ ssh 192.168.1.1 -l root

準備3:SCPコマンドによるファイル転送方法を理解する

次に、UbuntuやWindowsで利用可能なSCPコマンドについて使い方を押さえましょう。

SCPコマンドは作成したカスタムページ(HTMLファイルやLuaスクリプト)をOpenWrtデバイスに転送するために使用します。

OpenWrtをインストールしたばかりのデバイス条件では、次のようにSCPコマンドを実行することでファイル転送が可能です。

kamo@kamo:~$ touch test-file
kamo@kamo:~$ echo helloworld > test-file
kamo@kamo:~$ scp ./test-file root@192.168.1.1:/root

上記は、開発用PCのカレントディレクトリ内のtest-fileをOpenWrtデバイス(192.168.1.1/24)の/rootディレクトリに転送した例です

これだけ押さえておけば、ひとまず大丈夫かと思います。もっと知りたい方はこちらを参照してみてください。

準備4:LuCIの変数キャッシュ機能をOFFにする

カスタムページをファイル転送して動作確認をする際に、LuCIのキャッシュ機能が最新Webページの表示を邪魔します。

そのため、LuCIのキャッシュ機能を表すUCIオプション(luci.ccache.enable)の値をOFFを示す0に変更してください。

root@OpenWrt:~# uci set luci.ccache.enable=0
root@OpenWrt:~# uci commit luci
root@OpenWrt:~# reboot

本題:カスタムページの作成

その1:説明ページの作成と追加

最初に、静的なHTMLページを作成しましょう。

今回作成するHTMLページはこの記事で作成するカスタムページの説明文を掲載したものです。 そのため、先ずは開発PC上で、次のHTMLファイル(desc.htm)を作成してください。

※LuCI内の既存HTMLファイルに倣い拡張子を「.htm」としています。

-- desc.htm
<%+header%> <!-- themeのheader部が紐付きます。 ThemeとはUIデザインのことです。-->

<h1>[Desc] luci-sample-app01</h1>

<p>
    This is the description page for luci-sample-app01.<br>
    luci-sample-app01 combines the wi-fi, router and access point configuration interfaces introduced in <a href="https://utakamo.com/article/openwrt/beginner/intro03.html">my blog article</a> into a single page.
</p>

<p>
    <strong>[Implementation difficulty Lv.1]</strong><br>
    This section allows you to modify the parameters in the OpenWrt network and wireless configuration files (/etc/config/network and /etc/config/wireless).
    In other words, it is a LuCI plugin that only manipulates the configuration files managed by the standard OpenWrt application.
</p>

<h2>What this app can do</h2>

<ul>
    <li>Wi-Fi Settings</li>
    <li>Router Settings</li>
    <li>AP Settings</li>
    </ul>

<h2>Page Link</h2>

<ul>
    <li>[Desc] luci-sample-app01  /cgi-bin/luci/my/luci-sample-app01/desc (this page)</li>
    <li>Wi-Fi Setting (kamo custom)  <a href="../../../../cgi-bin/luci/admin/network/custom-page/wireless"><device ip address>/cgi-bin/luci/admin/network/custom-page/wireless"</a></li>
    <li>Network Setting (kamo custom)  <a href="../../../../cgi-bin/luci/admin/network/custom-page/network"><device ip address>/cgi-bin/luci/admin/network/custom-page/network"</a></li>
</ul>

<h2>Reference</h2>
<p>
    The following references provide an implementation; developers familiar with OpenWrt should look here.
</p>
<ul>
    <li>luci Wiki:  <a href="https://github.com/openwrt/luci/wiki">https://github.com/openwrt/luci/wiki</a></li>
</ul>

<%+footer%> <!-- themeのfooter部が紐付きます。 -->

HTMLファイルの作成が完了したら、次にカスタムページのルーティング設定を管理するコントローラースクリプトをLua言語で書いていきます。

Webサービスのルーティング設定とは?

昔ながらの方法で作成されたWebサイトは、ドキュメントルートのサーバー内ディレクトリを基点として、 そのディレクトリ内に配置した各ページの絶対パスがそのページにアクセスするためのURLになります。

しかし、それではURLを決定するために常にファイル(コンテンツ)の配置関係を意識しなくてはならず、コンテンツ管理の自由度が低いです。

そのため、近年ではルーティング設定を管理するスクリプトを通して、URLとコンテンツ配置の関係を分離した方法が取られています。

OpenWrtのLuCIでもルーティング設定が存在し、それが以下のLuaスクリプト(module.lua)になります。

desc.htmと同様に、開発PC上に次のmodule.luaを作成してください。

-- module.lua
module("luci.controller.luci-sample-app01.module", package.seeall)

function index()
    -- 以下の記述で、次のURLでアクセスできるようになります。<ip address ex) 192.168.1.1>/cgi-bin/luci/utakamo/luci-sample-app01/desc
    entry({"utakamo", "luci-sample-app01", "desc"}, template("luci-sample-app01/desc"), "desc", 20).dependent=false
end

上記に掲載した2つのファイル(desc.htmとmodule.lua)を開発PC上で作成できたら、OpenWrtデバイス環境にアップロードします。

アップロードの準備として、OpenWrt環境上にアップロード先のディレクトリを作成する必要があります。

今回、ルーティング設定を管理するLuaスクリプトはmodule("luci.controller.luci-sample-app01", package.seeall) という文でLuCIにモジュールを公開しています。

そのため、OpenWrtデバイスのコンソール上で次のコマンドを実行することで、module.luaを管理するディレクトリ(/usr/lib/lua/luci/controller/luci-sample-app01)を作成します。

root@OpenWrt:~# mkdir -p /usr/lib/lua/luci/controller/luci-sample-app01
root@OpenWrt:~# mkdir -p /usr/lib/lua/luci/view/luci-sample-app01

作成できたら、今度は開発用PCのコンソール上で次のSCPコマンドを実行して、desc.htmとmodule.luaをOpenWrtデバイスの以下にアップロードします。

kamo@kamo:~$ scp ./module.lua root@192.168.1.1:/usr/lib/lua/luci/controller/luci-sample-app01
kamo@kamo:~$ scp ./desc.htm root@192.168.1.1:/usr/lib/lua/luci/view/luci-sample-app01

アップロード完了後、開発PCのWebブラウザーを開いてURL入力欄に「http://192.168.1.1/cgi-bin/luci/utakamo/luci-sample-app01/desc」と打ち込んでみてください。 すると、次のカスタムページ(desc.htm)が表示されます。

カスタムページ(desc.htm)がLuCIに反映されない場合

desc.htmとmodule.luaが以下のOpenWrtのディレクトリパスに格納されているか確認してください。

  • /usr/lib/lua/luci/view/luci-sample-app01/desc.htm
  • /usr/lib/lua/luci/controller/luci-sample-app01/module.lua

上記パス通りであれば、原因はキャッシュかもしれません。OpenWrtデバイスを再起動してください。

その2:ネットワーク設定ページの作成と追加

次はネットワーク設定用のページを追加します。

先ず最初に上記で作成したmodule.luaに対して、ネットワーク設定用ページ(network.lua)のルーティング設定を追記します。

-- module.lua
module("luci.controller.luci-sample-app01.module", package.seeall)

function index()
    entry({"utakamo", "luci-sample-app01", "desc"}, template("luci-sample-app01/desc"), "desc", 20).dependent=false
    -- 以下を追記する
    entry({"admin", "network", "custom-page", "network"}, cbi("luci-sample-app01/network"), "Network Setting (kamo custom)", 30).dependent=false
end

追記できたら、前回と同様に開発PCからOpenWrtデバイスに対してSCPコマンドを使用してmodule.luaをアップロードします。 ※OpenWrtデバイスに入っていた前回のmodule.luaは最新のmodule.luaに更新されます。

kamo@kamo:~$ scp ./module.lua root@192.168.1.1:/usr/lib/lua/luci/controller/luci-sample-app01

module.luaを更新できたら、次は肝心のネットワーク設定ページであるnetwork.luaを以下の内容で作成します。

-- network.lua

-- [execut_cmd function] https://utakamo.com/article/lua/execute-cmd-from-lua/index.html
execute_cmd = function(cmd)
    local handle = io.popen(cmd)
    local result = handle:read('*a')
    handle:close()
    
    return result
end

split = function(inputString, delimiter)
    local result = {}
    local pattern = string.format("([^%s]+)", delimiter)
    
    for match in inputString:gmatch(pattern) do
        table.insert(result, match)
    end
    
    return result
end

m = Map("network", "Network Setting (kamo custom)")

s = m:section(TypedSection, "interface")
s.addremove = true
function s:filter(value)
    return value ~= "loopback" and value
end 
s:depends("proto", "static")
s:depends("proto", "dhcp")

p = s:option(ListValue, "proto", "Protocol")
p:value("static", "static")
p:value("dhcp", "dhcp")
p.default = "static"

d = s:option(ListValue, "device", "Device")
active_ifs = execute_cmd("ip link show up | awk '/^[0-9]+: / {print substr($2, 1, length($2)-1)}'")
active_iflist = split(active_ifs, '\n')

for _, v in ipairs(active_iflist) do
    d:value(v, v)
end

s:option(Value, "ipaddr", "ip"):depends("proto", "static")

s:option(Value, "netmask", "Netmask"):depends("proto", "static")

return m

ソースコードの内容について

上記のnetwork.luaはLuCI用のCBI(Configuration Bind Interface)関数を利用して、 UCI(Unified Configuration Interface)とWebUIのインタフェースをバインドしています。

詳細については、表示される設定画面の各インタフェースの対応を比較するか、ChatGPTや BingChatで聞いてみてください。

なお、システムが認識している優先・無線インタフェースを取得するために、以下の記事で紹介した 方法(execute_cmd関数)を使用しています。

Luaプログラミング Luaから外部ソフトウェアを実行して結果を取得する方法

興味があれば、参照してみてください。

network.luaを作成できたら、OpenWrtデバイスにアップロードします。

前回同様に、予めOpenWrtデバイス側でnetwork.luaを格納する以下のディレクトリを作成します。

root@OpenWrt:~# mkdir -p /usr/lib/lua/luci/model/cbi/luci-sample-app01

ディレクトリを作成できたら、開発PC上で作成したnetwork.luaをOpenWrtデバイス環境にアップロードします。

kamo@kamo:~$ scp ./network.lua root@192.168.1.1:/usr/lib/lua/luci/model/cbi/luci-sample-app01

アップロード完了後、開発PCのWebブラウザーを開いてURL入力欄に「http://192.168.1.1/cgi-bin/luci/admin/network/custom-page/network」と打ち込んでみてください。 すると、ログイン画面が表示されますので、必要であればUsernameとPasswordを入力します。(OpenWrtインストール直後はUsernameは「root」、パスワードは無し(入力しない)です。)

ログインが成功すると、自作のネットワーク設定ページ(network.lua)が表示されます。

その3:Wi-Fi設定ページの作成と追加

次はWWi-Fi設定用のページを追加します。

先ず最初に上記で作成したmodule.luaに対して、ネットワーク設定用ページ(wireless.lua)のルーティング設定を追記します。

-- module.lua
module("luci.controller.luci-sample-app01.module", package.seeall)

function index()
    entry({"utakamo", "luci-sample-app01", "desc"}, template("luci-sample-app01/desc"), "desc", 20).dependent=false

    -- 以下を2行を追記する
    entry({"admin", "network", "custom-page"}, firstchild(), "CUSTOM PAGE", 30).dependent=false
    entry({"admin", "network", "custom-page", "wireless"}, cbi("luci-sample-app01/wireless"), "Wi-Fi Setting (kamo custom)", 30).dependent=false

    entry({"admin", "network", "custom-page", "network"}, cbi("luci-sample-app01/network"), "Network Setting (kamo custom)", 30).dependent=false
end    

追記できたら、前回と同様に開発PCからOpenWrtデバイスに対してSCPコマンドを使用してmodule.luaをアップロードします。 ※OpenWrtデバイスに入っていた前回のmodule.luaは最新のmodule.luaに更新されます。

kamo@kamo:~$ scp ./module.lua root@192.168.1.1:/usr/lib/lua/luci/controller/luci-sample-app01

module.luaを更新できたら、次は肝心のWi-Fi設定ページであるwireless.luaを以下の内容で作成します。

-- wireless.lua

ubus = require("ubus")

-- [References] https://openwrt.org/docs/techref/ubus#lua_module_for_ubus
ubus_call = function(path, method, json_param)

    local conn = ubus.connect()

    if not conn then
        return
    end

    local result = conn:call(path, method, json_param)

    return result
end

m = Map("wireless", "Wi-Fi Setting (kamo custom)")
radio = m:section(TypedSection, "wifi-device")
radio.addremove = true
function radio:filter(value)
    return value
end 

wave = radio:option(ListValue, "disabled", "Wi-Fi Carrier Wave")
wave:value("1", "OFF")
wave:value("0", "ON")

country_code = radio:option(ListValue, "country", "COUNTRY")
wlan = ubus_call("iwinfo", "devices", {})

if #wlan.devices >= 1 then
    countrylist = ubus_call("iwinfo", "countrylist", {device = wlan.devices[1]})
    for _, item in ipairs(countrylist.results) do
        country_code:value(item.code, item.country)
    end
end

txpower = radio:option(ListValue, "txpower", "TXPOWER")

if #wlan.devices >= 1 then

    txpowerlist = ubus_call("iwinfo", "txpowerlist", {device = wlan.devices[1]})

    for _, result in ipairs(txpowerlist.results) do
	if (result.dbm >= 6) and (result.dbm <= 10) then
            txpower:value(result.mw, result.dbm .. "dBm")
        end
    end
end

default_radio = m:section(TypedSection, "wifi-iface")
default_radio.addremove = true
function default_radio:filter(value)
    return value
end

-- [References] https://openwrt.org/docs/guide-user/network/wifi/basic#common_options
encryption = default_radio:option(ListValue, "encryption", "ENCRYPTION")
encryption:value('none', 'no authentication')
encryption:value('psk+tkip+ccmp', 'WPA Personal (PSK)')
encryption:value('psk2', 'WPA2 Personal (PSK)')
encryption:value('sae', 'WPA3 Personal (SAE)')

default_radio:option(Value, "ssid", "SSID")
key = default_radio:option(Value, "key", "KEY")
key:depends("encryption", 'psk+tkip+ccmp')
key:depends("encryption", 'psk2')
key:depends("encryption", 'sae')
key.password = true

return m

wireless.luaを作成できたら、OpenWrtデバイスにアップロードします。

格納先であるOpenWrtデバイスのディレクトリは上記で既に作っていますので、次のSCPコマンドで開発PC上のwireless.luaをOpenWrtデバイスにアップロードします。

kamo@kamo:~$ scp ./wireless.lua root@192.168.1.1:/usr/lib/lua/luci/model/cbi/luci-sample-app01

これで、今回のカスタムページの作成と追加は完了です。

一連の動作を見てみます。

先ず、開発PCのWebブラウザーのURL入力覧に「http://192.168.1.1」と打ち込んでみてください。 ログイン画面が表示されますので、必要であればUsernameとPasswordを入力します。(OpenWrtインストール直後はUsernameは「root」、パスワードは無し(入力しない)です。)

すると、LuCIの管理画面トップが表示されますので、「Network」プルダウンメニューの中から「CUSTOM PAGE」をクリックします。

これで、今回作成したネットワーク設定ページが表示されます。

また、ネットワーク設定ページのタブからWi-Fi設定ページにも遷移できることが分かります。

これら設定ページの各項目は対象のUCIコンフィグレーションファイル(/etc/config/network、 /etc/config/wireless)と紐づいているため、UI上の項目を通した設定変更が反映されます。

本題:カスタムページのパッケージ作成とインストール

ここでは、前節までで作成したカスタムページ(LuaスクリプトとHTMLファイル)を第三者に配布するために、 luci-sample-app01という名前でパッケージ化してみます。

手順1:パッケージ作成のためのディレクトリ管理

最初に、前節で作成した各カスタムページ用のLuaスクリプトとHTMLファイルを次のディレクトリ・ファイル構成になるように整理します。 ※以下の図にあるMakefileは次節で作成します。

上図のディレクトリ構成で各カスタムページが管理されているものとして、 パッケージ作成用のMakefileを記述します。

手順2:パッケージ用Makefileの作成

手順1で作成したUtakamoStudyApps/webui/luci-sample-app01直下に次のMakefileを作成します。

#~/UtakamoStudyApps/webui/luci-sample-app01/Makefile
include $(TOPDIR)/rules.mk

PKG_NAME:=luci-sample-app01
PKG_VERSION:=1.0
PKG_RELEASE:=1

SOURCE_DIR:=./files/luasrc

LUA_LIBRARYDIR = /usr/lib/lua
LUCI_LIBRARYDIR = $(LUA_LIBRARYDIR)/luci
LUCI_MODULEDIR = $(LUCI_LIBRARYDIR)/controller
LUCI_MODELDIR = $(LUCI_LIBRARYDIR)/model/cbi
LUCI_VIEWDIR = $(LUCI_LIBRARYDIR)/view

include $(INCLUDE_DIR)/package.mk

define Package/luci-sample-app01
    CATEGORY:=utakamo
    SECTION:=utakamo
    TITLE:=luci sample application
endef

define Build/Compile
endef

define Package/luci-sample-app01/install
	$(INSTALL_DIR) $(1)$(LUCI_MODULEDIR)/luci-sample-app01
	$(INSTALL_DATA) $(SOURCE_DIR)/controller/module.lua $(1)$(LUCI_MODULEDIR)/luci-sample-app01
	$(INSTALL_DIR) $(1)$(LUCI_MODELDIR)/luci-sample-app01
	$(INSTALL_DATA) $(SOURCE_DIR)/model/cbi/network.lua $(1)$(LUCI_MODELDIR)/luci-sample-app01
	$(INSTALL_DATA) $(SOURCE_DIR)/model/cbi/wireless.lua $(1)$(LUCI_MODELDIR)/luci-sample-app01
	$(INSTALL_DIR) $(1)$(LUCI_VIEWDIR)/luci-sample-app01
	$(INSTALL_DATA) $(SOURCE_DIR)/view/cbi/desc.htm $(1)$(LUCI_VIEWDIR)/luci-sample-app01
endef

$(eval $(call BuildPackage,luci-sample-app01))

今回のMakefileはコンパイルが必要なソースファイルは含みませんので、define Build/Compileの文は空の状態です。

define Package/luci-sample-app01/installの文は、各ファイルをインストール先のOpenWrtデバイス環境と対になったパッケージ環境($(1) にコピーする処理です。

これで手順1で各ディレクトリの中に格納したHTMLファイルやLuaスクリプトが、$(1)で示されるパッケージ環境 の各ディレクトリ階層に配置されるようになります。

パッケージのビルド

次は、上記で作成したパッケージ作成用のMakefileをOpenWrtのbuildrootにfeed情報としてインストールしてビルドします。

まず、カスタムページ(Luaスクリプト・HTMLファイル)とMakefileを格納したディレクトリの親ディレクトリである UtakamoStudyAppsまでのフルパスを~/openwrt/feeds.confの内容として以下のように記述します。

kamo@kamo:~/openwrt$ nano feeds.conf
# feeds.confの記載例
# パッケージパスはご自身の環境を確認の上、記述してください
# src-link [フィード名] [パッケージパス]
src-link utakamo /home/kamo/UtakamoStudyApps

次に、以下のコマンドでbuildrootでカスタムページのMakefileを読み込ませ、utakamo feed情報としてインストールします。

kamo@kamo:~/openwrt$./scripts/feeds update -a
kamo@kamo:~/openwrt$./scripts/feeds install -a -p utakamo

正常にfeed情報がインストールできたら、make menuconfigで設定画面を表示します。

kamo@kamo:~/openwrt$ make menuconfig

一覧の中に「utakamo」という項目がありますので、これを選択します。

すると、「utakamo」の中に今回作成したLuCIのカスタムページ用パッケージの「luci-sample-app01」がありますので、これにチェックを入れます。

その後、変更を保存して設定画面(メニューコンフィグ)を終了します。※Exitを選択すると、終了する前に変更内容を保存するか聞かれますので「YES」を選んでください。

コンソールに操作が戻ってきますので、次のMakeコマンドでluci-sample-app01パッケージを作成してください。

kamo@kamo:~/openwrt$ make package/luci-sample-app01/compile

正常にパッケージ作成が完了すると、openwrt/bin/packages/<chip name>/utakamoに作成されたパッケージ本体(.ipk)があります。 ※今回、私はRaspberry Pi3用のパッケージを作成しましたので、パッケージ本体が格納されるディレクトリパスは~/openwrt/bin/packages/aarch64_cortex-a53/utakamoです。

kamo@kamo:~/openwrt/bin/packages/aarch64_cortex-a53/utakamo$ ls
Packages                                        Packages.gz                                    
Packages.manifest                               Packages.sig                                           
index.json                                      luci-sample-app01_1.0-1_aarch64_cortex-a53.ipk

これでパッケージ作成は完了です。

パッケージのインストール

上記で作成したパッケージをOpenWrtデバイスに転送してインストールします。

SCPコマンドを次のように使用することで、はOpenWrtデバイスに転送できます。 ※もちろん、いろいろなファイル転送のいづれかで作成したパッケージをOpenWrtデバイス環境に転送することができればOKです。

kamo@kamo:~/openwrt/bin/packages/aarch64_cortex-a53/utakamo$ scp ./luci-sample-app01_1.0-1_aarch64_cortex-a53.ipk root@192.168.1.1:/root

OpenWrtデバイスにパッケージを転送後、OpenWrtのコンソール上で次のコマンドを実行することでインストールします。

root@OpenWrt~:# opkg install luci-sample-app01_1.0-1_aarch64_cortex-a53.ipk

以上で、LuCI用カスタムページの作成と追加からパッケージ作成について紹介しました。

LuCIがシステム内部で利用するUCI操作プロセス(/sbin/rpcd)について

LuCIのページ上でユーザーに提供する設定項目の大半は、OpenWrtアプリケーションの環境変数であるUCIコンフィグレーションファイルと紐づいています。

例えば、ユーザーがLuCIのネットワーク設定ページからIPアドレスなどを変更すると、対応するUCIコンフィグレーションファイル(例:/etc/config/network)に変更内容が書き込まれ、 そのコンフィグレーションファイルを環境変数とするOpenWrtアプリケーション(例:netifd)が現在の設定を読み込み直し、ユーザーによる設定変更がシステムに適用されます。

UCIコンフィグレーションファイルに対する直接的な操作はUCIコマンド(/usr/bin/uci)がサポートします。ユーザー自身がOpenWrtデバイスの コンソール画面からroot@OpenWrt:~# uci set network.lan.ipaddr=192.168.2.1などのように実行することでも 設定に変更を加えることが可能です。

UCIコマンド(/usr/bin/uci)の詳細について

以下の記事で、内部の仕組みや使い方について紹介しています。この節の説明についてより深い理解を得たい方、興味がある方は参考にしていただければと思います。

そのため、コンソール上でUCIコマンドを利用して設定内容を頻繁に変更している人から見ると、 「LuCIもユーザーによる設定変更を受けたら、内部でUCIコマンド(/usr/bin/uci)を使ってUCIコンフィグレーションファイルを操作している」と思うかもしれません。

しかし実際には、LuCIはこのUCIコマンド(/usr/bin/uci)をシステム内部で利用していません。

LuCIがシステム内部で利用しているのは、UCIコマンド(/usr/bin/uci)と同等な機能を持ち、UBUSによるプロセス間通信とセッション 管理機能を持つRPC対応版のUCIコマンド(/sbin/rpcd)です。

このRPC対応版のUCIコマンド(/sbin/rpcd)は、いつ来るか分からないUCI操作リクエストのためにシステムブート時から稼働しているUBUS対応アプリケーション(daemon)でもあります。

RPC対応版UCIコマンドのクライアントはネットワークを通して繋がり、LuCIによってUI提供されているユーザーPC(ホスト)です。そのため、同時に複数台のクライアントがLuCIによる設定ページを 通してUCI操作をすることを想定して、セッション管理をサポートしています。

詳細な動作については以下の通りです。

LuCIが使用するRPC対応版UCIコマンドの仕組み(処理の流れ)

ここでは、2台のクライアントデバイス(PC)がLuCIのWebUIにアクセスして、それぞれがIPの設定変更をしたと仮定します。

このとき、それぞれのクライアントデバイスからIPアドレスの設定変更要求を受けたLuCIは、UCI操作プロセスの/usr/bin/rpcd に対して、UBUS経由で設定変更を通知します。

※LuCIは内部でubus call uci set '{"config":"network","section":"wan","type":"interface","match":"","values":{"ipaddr":"192.168.3.1"},"ubus_rpc_session":<session>}' などのUBUSコマンドを実行しています。

このとき、各デバイスのIP設定の変更は、それらデバイスのセッション番号に紐づいたステージング領域(RAM)に一時保存されます。

セッション番号に紐づいたステージング領域は、/usr/bin/rpcdによって動的に割り当てられた領域です。

上図では、2台のクライアントデバイスの各セッション(Session A、Session B)ごとにステージング領域(①、②)が 動的に作成され、その中に変更内容である192.168.3.1192.168.2.1が格納されています。

ステージング領域にある任意のIP設定はUCIオブジェクトのapplyメソッドを利用することでコンフィグ(今回では/etc/config/network)にマージすることができます。 これにより、対応アプリケーションの環境変数として使用できます。

上図では、Session Aの変更内容である192.168.3.1をapplyメソッドの実行によって適用しています。 applyメソッドは、タイムアウト時間内にネットワーク通信が確認できない場合、元の設定状態にロールバックできます。

ネットワーク通信が確認できた場合は、設定変更を確定します。

これにより、設定変更がフラッシュ領域のコンフィグ(今回の場合は/etc/config/network)に書き込まれます。

このように、LuCI経由で実施される設定変更はUBUSとUCI操作プロセスにより、実現されます。

【おまけ】ユーザーPCとLuCIのUBUS通信を見てみる

上記で説明したLuCIとユーザーPCのやり取りの一部はUBUSのmonitorコマンドで見ることができます。

確認するには、OpenWrtデバイス側のコンソールで次のUBUSコマンドを実行します。

root@OpenWrt:~# ubus monitor

後は、LuCIを通して何らかの設定を変更すれば、その時の通信内容(UBUSへのリクエスト含む)がログとして出力されます。

次はネットワークのIPアドレス設定を192.168.1.1から192.168.2.1に変更したときのUBUS monitorコマンドのログ出力の抜粋です。 (IPアドレスの設定項目に192.168.2.1を入力して、SAVEボタンを押した時の内容になります。)

root@OpenWrt:~# ubus monitor

...省略...

: {"objpath":"uci"}
-> ce806aa2 #00000000           data: {"objpath":"uci","objid":2098094064,"objtype":208355174,"signature":{"configs":{},"get":{"config":3,"section":3,"option":3,"type":3,"match":2,"ubus_rpc_session":3},"state":{"config":3,"section":3,"option":3,"type":3,"match":2,"ubus_rpc_session":3},"add":{"config":3,"type":3,"name":3,"values":2,"ubus_rpc_session":3},"set":{"config":3,"section":3,"type":3,"match":2,"values":2,"ubus_rpc_session":3},"delete":{"config":3,"section":3,"type":3,"match":2,"option":3,"options":1,"ubus_rpc_session":3},"rename":{"config":3,"section":3,"option":3,"name":3,"ubus_rpc_session":3},"order":{"config":3,"sections":1,"ubus_rpc_session":3},"changes":{"config":3,"ubus_rpc_session":3},"revert":{"config":3,"ubus_rpc_session":3},"commit":{"config":3,"ubus_rpc_session":3},"apply":{"rollback":7,"timeout":5,"ubus_rpc_session":3},"confirm":{"ubus_rpc_session":3},"rollback":{"ubus_rpc_session":3},"reload_config":{}}}
-> ce806aa2 #00000000         status: {"status":0}
#以下のログからUCIオブジェクトのsetメソッドがセッション番号[12f8439dc10ee9d90a81180472116bfa]に使用されたことが分かる
<- ce806aa2 #7d0e5ff0         invoke: {"objid":2098094064,"method":"set","data":{"ubus_rpc_session":"12f8439dc10ee9d90a81180472116bfa","config":"network","values":{"ipaddr":"192.168.2.1"},"section":"lan"}}
-> b7d95343 #ce806aa2         invoke: {"objid":2098094064,"method":"set","data":{"ubus_rpc_session":"12f8439dc10ee9d90a81180472116bfa","config":"network","values":{"ipaddr":"192.168.2.1"},"section":"lan"},"user":"root","group":"root"}
<- b7d95343 #ce806aa2         status: {"status":0,"objid":2098094064}
-> ce806aa2 #7d0e5ff0         status: {"status":0,"objid":2098094064}

...省略...

このログを見ると、LuCIがUCI操作プロセス(/sbin/rpcd)を利用してUCIコンフィグレーションファイルに対する操作をしていることが分かります。

上記の内容では、セッション番号[12f8439dc10ee9d90a81180472116bfa]によるnetworkコンフィグへの変更(192.168.2.1)がUCIオブジェクトのsetメソッドによって実施されたことが分かります。

setメソッドはステージング領域に変更情報を書き込む機能なので、セッション番号に紐づいた動的メモリ内に変更差分が記録されたことが推測できます。

このことを裏付けるために、ステージング領域の内容を見るchangesメソッドを実行してみます。

root@OpenWrt:~# ubus call uci changes '{"config":"network", "ubus_rpc_session":"12f8439dc10ee9d90a81180472116bfa"}'
{
        "changes": [
                [
                        "set",
                        "lan",
                        "ipaddr",
                        "192.168.2.1"
                ]
        ]
}

上記から、セッション番号に紐づいたステージング領域にIPアドレスの変更が入っていることが確認できます。

【おまけ】Luaスクリプト用UBUSモジュールでカントリコード一覧を取得する方法

UBUSによるプロセス間通信を利用して、iwinfoオブジェクトからカントリーコードを取得するLuaスクリプトを掲載します。

これは、カスタムページのWi-Fi設定画面(wireless.lua)を作る際にできたテストコードです。もったいないので掲載することにしました。

この処理はカントリコード用のプルダウンメニューに各コードに対応する国名を表示するために利用しています。

OpenWrtデバイス上のどこでも良いので、以下のスクリプトを置いて実行してみると国情報の一覧が取得できます。

-- country.lua
ubus  = require("ubus")

ubus_call = function(path, method, json_param)

    local conn = ubus.connect()

    if not conn then
        return
    end

    local result = conn:call(path, method, json_param)

    return result
end

wlan = ubus_call("iwinfo", "devices", {})

for _, value in ipairs(wlan.devices) do
    print("--->" .. value)
end

countrylist = {}

if #wlan.devices >= 1 then
    countrylist = ubus_call("iwinfo", "countrylist", {device = wlan.devices[1]})
else
    return
end

print("wlan.devices length = " .. #wlan.devices)
print("wlan.devies[1] = " .. wlan.devices[1])
print("countrylist length = " .. #countrylist)

for _, item in ipairs(countrylist.results) do
    print(item.country .. ' ---> ' .. item.code)
end

上記のLuaスクリプトは、OpenWrtのコンソール上で次のUBUSコマンドを実行したときと同等の結果になります。

# 例)interface : wlan0で国情報を取得する例
root@OpenWrt:~# ubus call iwinfo countrylist '{"device":"wlan0"}'
# 引数のWLANインタフェースはシステムが認識しているものである必要があります。
# 次のUBUSコマンドでWLANインタフェースの一覧が取得できます。
# root@OpenWrt:~# ubus call iwinfo devices

このように、UBUSを使用してWi-Fiドライバーにカントリーコードとして解釈できる設定値の組を教えてもらうことにより、 Wi-Fiチップ&ドライバーの差異をLuCIのWebUI層で抽象化することができます。

ただし、このような情報を提供してくれるかはハードウェア・ドライバーのサポートに依存します。

おわりに

今回は、LuCI用のカスタムページを作成して追加する方法を紹介しました。

この記事の内容で、UIを自分好みのものにカスタマイズできるようになります。

しかし、より深くアプリケーション開発をしたいと考えると、今回の知識では物足りません。

自作ソフトウェア(スクリプト含む)とLuCIとのプロセス間通信(IPC/RPC)も押さえておく必要があります。

さらに深い知識を理解して、OpenWrtのアプリケーション開発をしたい方は以下の記事をご参考ください。

  1. OpenWrtアプリケーション開発 自作スクリプトと連携するLuCI用プラグインの作成と追加(難易度Lv2)(作成中)
  2. OpenWrtアプリケーション開発 自作UBUS対応アプリと連携するLuCI用プラグインの作成と追加(難易度Lv3)(作成中)

参考文献