RK3399: 支援wifi/4g與乙太網共存
開發板晶片:RK3399 Android版本: 7.1.2
我們在Android開發板上開發一款App,Android開發板有wifi, 4G和乙太網介面,App通過乙太網與IP攝像頭連接獲取資料,App處理後通過wifi/4G網路將結果上報到雲端。因此,要求Android能同時連線乙太網(稱為內網)和外網。
熟悉Android的同學可能知道,預設情況下Android在同一時刻只能使用一種網路,比如手機同時連線4G和wifi,會優先使用wifi。因此,實現這個功能需要修改Android ROM,重新燒韌體。對於主要從事後端開發的我來講,看起來挑戰挺大的。下面整理了一下解決這個問題的過程和方法。
首先,制定了下面的方案和計劃:
- 下載開發板的官方編譯好的映象,熟悉韌體燒錄流程,因為後面會進行多次韌體燒錄進行測試。
- 下載開發板對應的原始碼編譯映象,確保編譯環境和流程的正確性,因為後面要修改Android的原始碼,得首先排除掉編譯過程中可能引入的錯誤。
- 重複“修改原始碼,編譯,燒錄韌體和測試”這個迴圈直到問題解決
0. 現狀
Android連線上wifi,此時可以正常訪問外網。插上網線後,可以訪問內網,但是不能訪問外網了。查詢資料後發現,Android內部對每種型別的網路有個評分,如果幾個不同型別的網路都能使用,會使用分數高的網路,檢視原始碼後發現乙太網的預設評分是70(可能不同的版本不一樣),高於wifi和4G網路的評分,因此插上網線後就預設使用乙太網訪問所有IP。很自然的,把乙太網的預設評分調為比其他網路型別都低就可以保證wifi/4G不會被踢掉。相關程式碼在EthernetNetworkFactory.java中,裡面有個靜態常量定義了乙太網的評分。
- private static final int NETWORK_SCORE = 70;
+ private static final int NETWORK_SCORE = 30; // changed from 70 by mjshi
重新編譯後燒錄韌體,發現插上網線後wifi依然可用,可以訪問外網,但是無法訪問內網。這符合預期,說明wifi沒有因為插上網線後被踢掉。
1. 第一次不太成功的嘗試
通過ifconfig可以看到乙太網卡和wifi都同時線上,猜測是路由表沒有配置好,所有IP的訪問請求都預設走wifi了,也許只需要增加一條路由記錄,讓訪問內網192.168.1.0網路的請求走乙太網就可以。
按照這個思路,先看看Android的路由表,考慮到Android是基於Linux的,因此直接使用Linux上route命令得到類似如下的路由表:
30.133.236.0/22 dev wlan0 proto kernel scope link src 30.133.237.132
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.2
咋一看,好像沒有問題啊,路由表中相應的路由記錄的啊。經過一番查詢資料後,發現Android使用了多級路由策略,簡單來講就是Android裡面有多張路由表,至於哪張路由表生效是由其優先順序決定的(參考這裡:https://my.oschina.net/u/1397402/blog/736806 )。
通過ip rule list
得到類似下面的路由策略(我刪除了一些不相干的記錄):
0: from all lookup local
10500: from all oif wlan0 lookup wlan0
.....
23000: from all fwmark 0x0/0xffff lookup main
32000: from all unreachable
可以看到系統中實際其作用的是第二條,最後面的wlan0就是路由表的名稱。
通過ip route list table wlan0
可以看到其內容,如下:
default via 30.133.239.254 dev wlan0 proto static
30.133.236.0/22 dev wlan0 proto static scope link
可以看到裡面沒有到內網的路由記錄,因此可以訪問外網,但是不能訪問內網。那怎麼辦呢,很自然的,在wlan0中增加一條到內網路由記錄,命令如下:
ip route add 192.168.1.0/24 dev eth0 proto static scope link table wlan0
此時,添加了上面的記錄後可以訪問內網和外網了。不過,這樣還沒有解決問題,因為這樣改了只是臨時可以用,如果系統重啟或者是通過4G上外網依然無法解決。因此,我們需要在Android系統中某個地方自動把將上面的路由記錄新增進去。
很自然想到,可以在wifi和4G網路剛連上的時候,對路由表進行修改,查詢了一些資料後在ConnectivityService.java中的對應地方加了一些程式碼,嘗試後不成功,主要原因是這一層是通過Android封裝的介面對路由表進行修改,有些限制。而且Android程式碼很大(解壓縮以後有25GB),不可能匯入到IDE裡面進行修改,修改程式碼效率很低。放棄這條路子,繼續查詢資料。
2. 第二次嘗試
查詢了資料瞭解到,Android可以在init.rc中定義service,然後設定指定的系統property就可以觸發service對應的指令碼。 於是,我先寫了指令碼來增加一條優先順序較高的路由表:
#!/system/bin/sh
if [ $# -ne 1 ];then
echo "usage: $0 [add|del]"
exit 1
fi
action=$1
if [ "$action" == "add" ];then
# avoid add the rule twice
ip rule list | grep '^9000:.*1'
if [ $? -ne 0 ];then
ip rule add from all table 1 pref 9000
fi
ifconfig eth0 192.168.1.2 netmask 255.255.255.0 up
ip route add 192.168.1.0/24 via 192.168.1.2 dev eth0 table 1
setprop eth.route ""
elif [ "$action" == "del" ];then
ip rule del pref 9000
setprop eth.route ""
else
echo "unsupported action: $action"
fi
這個指令碼很簡單,就是增加一個優先順序為9000的路由表名稱為1(好像只能命名為數字),裡面定義了到內網的路由記錄。這樣的好處是,我們不用修改預設的路由表(wlan0如果連的是wifi,wwan0如果連的是4G)。
然後在init.rc中定義了兩個services:
service add_eth_route /system/bin/eth_route.sh add
class main
disabled
oneshot
service del_eth_route /system/bin/eth_route.sh del
class main
disabled
oneshot
on property:eth.route=add
start add_eth_route
on property:eth.route=del
start del_eth_route
它的意思是說,如果系統屬性eth.route的值為add,則執行命令/system/bin/eth_route.sh add
。如果為del的話,則執行/system/bin/eth_route.sh del
最後,在java程式碼中找到乙太網卡插上網線和拔掉網線的地方,設定系統屬性eth.route為對應的值,程式碼還是在EthernetNetworkFactory中。至於是怎麼找到應該改在這裡,我是在logcat的日誌中發現eth0 link up
。
+ String ethRoutePropKey = "eth.route";
+ if (up) {
+ Log.d(TAG, "updateInterface: " + iface + " link up, setup route...");
+ SystemProperties.set(ethRoutePropKey, "add");
+ } else {
+ Log.d(TAG, "updateInterface: " + iface + " link down, removing route...");
+ SystemProperties.set(ethRoutePropKey, "del");
+ }
採用這個方案的好處是java中程式碼改動很少,這樣出錯的機率就小。
編譯、打包、燒錄韌體,期待能一次成功,結果是這個指令碼沒有被呼叫起來,又排查了很久在dmesg中找到了下面的錯誤日誌:
Service xxx does not have a SELinux domain defined
3. 測試
測試了以下場景
- 插上網線冷啟動
- 拔掉網線不能上內網,但外網訪問不受影響。插上網線後能上內網,外網也正常。
- 禁用wifi/4G,內網訪問不受影響。再次開啟wifi/4G,恢復正常 其中,在第三個case中,禁用wifi/4G,內網也不能訪問了,原因是此時只有乙太網可用,Android系統將乙太網提升為預設網路,會修改路由表,導致路由表錯亂(具體原因沒有去查了),實際上此時乙太網已經處於連線狀態且配置正確,沒有必要再去進行配置了。因此,改動很簡單,在對應的地方增加下面程式碼:
+ if (mEthernetCurrentState != EthernetManager.ETHER_STATE_DISCONNECTED) {
+ Log.d(TAG, "onRequestNetwork: " + mIface + "current state = " + mEthernetCurrentState);
+ return;
+ }
編譯,燒錄韌體,測試一切符合預期,可以把chrome中幾十個tab一下全關掉了,真爽!
4. 總結
解決問題總是有一些共有的套路的:
- 工具鏈一定先弄好,避免犯一些低階的錯誤,比如程式碼改動沒有生效
- 多看日誌,一開始可能日誌很多不知道從哪裡開始看,那就從後面往前看
- 搜尋,中英文關鍵字都可以用,這次很多資料就是在中文資料上找到的。
- 每次改動的內容要少一點,否則一下子改很多東西,也不知道是哪個改動在發生作用。
最後感謝很多朋友分享的文章,沒有你們的經驗分享,我也無法解決這個問題,因此整理成文希望能幫到更多的朋友。