Android 车载中控屏远程调试:用 FRP 把 ADB 从客户现场接回研发环境
工程车辆上的 Android 中控屏通常需要连接串口、CAN、USB、网络和各种外围设备,很多问题只有整车安装后才会暴露。公司环境难以完整复现现场条件,客户又很难协助执行 shell、导日志或复现调试动作,因此需要让研发能够直接远程获得 ADB + Shell + Logcat + 屏幕控制 能力。
整体方案
局域网内可以直接开启 ADB TCP:
adb tcpip 5555
但现场设备通常位于 Wi-Fi、4G 或运营商 NAT 后,没有公网 IP。ADB over TCP/IP 只解决了 ADB 网络化,没有解决公网访问问题。
最终链路:
研发电脑 → 公网 frps → Android frpc → 127.0.0.1:5555 → adbd
核心就是把现场设备的 5555 端口通过 FRP 映射到研发侧,再继续使用 Android 原有调试工具:
adb connect
adb shell
adb logcat
scrcpy
FRP:
FRP 服务端
公网服务器运行 frps。
docker-compose.yml:
version: '3'
services:
frps:
image: snowdreamtech/frps
container_name: frps
restart: always
network_mode: host
volumes:
- ./frps.toml:/etc/frp/frps.toml
frps.toml:
bindPort = 7000
auth.token = "tkoen"
webServer.addr = "0.0.0.0"
webServer.port = 7500
webServer.user = "user"
webServer.password = "password"
7000 用于 frpc 回连,7500 用于查看 FRP 状态。服务器安全组和防火墙需要放行对应端口,实际部署应更换示例中的 Token、用户名和密码。
Android 独立远程调试 App
没有把 FRP 集成进业务 App,而是单独做一个无界面的系统调试 App,只负责:
- 监听开机广播
- 开启 ADB TCP
- 启动
frpc - 设备重启后自动恢复远程调试链路
这样可以让远程调试能力与业务 App 解耦,避免业务版本更新影响调试链路。
将 FRP 封装成 Android AAR
Android 端不重新实现 FRP,而是复用 FRP 的 Go 实现,并通过 gomobile 封装成 AAR。
Go 侧提供一个简单入口:
package frp
import (
"context"
"fmt"
"github.com/fatedier/frp/client"
"github.com/fatedier/frp/pkg/config"
"github.com/fatedier/frp/pkg/config/v1/validation"
"github.com/fatedier/frp/pkg/util/log"
"github.com/fatedier/frp/server"
"os"
)
func RunClient(cfgFile string) {
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFile, true)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if isLegacyFormat {
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, please use yaml/json/toml format instead!\n")
}
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
svr, err := client.NewService(client.ServiceOptions{
Common: cfg,
ProxyCfgs: proxyCfgs,
VisitorCfgs: visitorCfgs,
ConfigFilePath: cfgFile,
})
if err != nil {
fmt.Println(err)
os.Exit(1)
}
svr.Run(context.Background())
}
func RunServer(cfgFile string) {
cfg, isLegacyFormat, err := config.LoadServerConfig(cfgFile, true)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if isLegacyFormat {
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, please use yaml/json/toml format instead!\n")
}
warning, err := validation.ValidateServerConfig(cfg)
if warning != nil {
fmt.Printf("WARNING: %v\n", warning)
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
svr, err := server.NewService(cfg)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
log.Infof("frps started successfully")
svr.Run(context.Background())
}
Android 端只需要调用 RunClient,不需要关心 FRP 内部实现。
安装 gomobile:
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
go get golang.org/x/mobile/bind
生成 AAR:
gomobile bind -androidapi 21 .
生成:
frp.aar
frp-sources.jar
将 frp.aar 放入 Android 项目的 libs:
implementation(files("libs/frp.aar"))
Android 侧准备好 FRP 配置文件后调用:
Frp.runClient(configFilePath)
开机自动开启 ADB TCP
FRP 映射之前,需要先让 adbd 监听 TCP 5555:
setprop service.adb.tcp.port 5555
stop adbd
start adbd
独立 App 收到开机广播后执行这一步,再启动 frpc:
Android 开机
→ 开启 adbd:5555
→ 启动 frpc
→ frpc 连接公网 frps
→ 映射 5555
→ 研发侧 adb connect
这样设备掉电或重启后可以自动恢复链路,不需要现场人员重新输入命令。
研发侧连接
假设现场设备的 5555 被映射到研发侧可访问的 10000:
adb connect 127.0.0.1:10000
确认设备:
adb devices
进入 Shell:
adb shell
查看日志:
adb logcat
安装 APK:
adb install -r app.apk
查看并操作设备界面可以使用 scrcpy:
scrcpy
最终研发侧可以直接完成:
ADB → Shell → Logcat → APK 安装 → 界面查看 → 远程操作
最终链路
Android 中控屏
├── adbd :5555
├── 独立调试 App
│ ├── BootReceiver
│ ├── 开启 ADB TCP
│ └── 启动 FRP AAR
│
└── frpc
↓
Internet
↓
frps 公网服务器
↓
研发电脑
├── adb connect
├── adb shell
├── adb logcat
└── scrcpy
这套方案没有重新实现远程调试协议,只通过 FRP 打通 ADB TCP 链路,再复用 Android 原有的 ADB 和 scrcpy 工具链。