- 机器人
- 嵌入式
- 硬件开发
- 移动开发
【免费下载链接】foc-wheel-legged-robot
Open source materials for a novel structured legged robot, including mechanical design, electronic design, algorithm simulation, and software development. | 一个新型结构的轮腿机器人开源资料,包含机械设计、电子设计、算法仿真、软件开发等材料
本文基于 foc-wheel-legged-robot 仓库中android/子模块的官方文档(README_en.md),结合 MainActivity.java、CtrlView.java、ctrl-proxy.py 等源码,完整还原这套 Android 遥控应用的显示方案、图传连接时序与蓝牙配网协议。读完本文,你将理解如何仅用一个重写的 WebView 同时承载视频显示与触摸遥控,以及如何通过 BLE 文本协议让图传系统自动入网,掌握一套可直接借鉴的"App + Linux 图传 + 嵌入式主控"三级无线链路设计。
一、模块定位与三大功能
android/是轮腿机器人项目的子模块,基于 Android Studio 开发(工程名BalanceBot,applicationId 为com.skythinker.balancebot),提供三项核心功能:
- 读取用户触摸输入,向机器人发送遥控指令;
- 拉取图传系统(linux-fpv/ 模块)的视频流并显示在界面上;
- 配合图传系统进行蓝牙配网(Bluetooth pairing),使图传系统自动接入指定 WiFi。
项目同时提供了预编译安装包 balancebot.apk,可直接安装使用;如需二次开发,仓库保留了完整的 Gradle 工程(settings.gradle、build.gradle)。
二、显示方案:重写 WebView 实现"视频 + 控件"双层叠加
文档指出:由于图传系统输出的 MJPG 视频流可以直接在各种浏览器中访问,因此 App 直接利用一个 WebView 作为主显示视图,仅需少量 HTML + JavaScript 代码即可显示视频流;同时重写 WebView 的onDraw方法,在 HTML 页面上方叠加绘制摇杆、滑块等控件,并通过onTouchEvent方法监听用户触摸输入。
2.1 视频流页面 streamview.html
视频流页面位于 assets/streamview.html,其关键实现:
- 通过
setServerIP(ip)函数把视频源切换为http://<ip>:8090/camera.mjpg(即图传系统上 ffserver 的 MJPG 推流地址,与 linux-fpv/README_en.md 中camera.mjpg端点一致); - 使用
<img>标签直接承载 MJPG 流,并在onerror时回退到本地 transparent.png 透明图,用于占位; - 每 3 秒定时刷新一次图片地址,并通过 WebView JS 接口
webview.jsCallback("setVideoState", "true"/"false")向 Native 层反馈视频是否连通; - 页面内以
ASPECT_RATIO = 4/3自适应缩放视频区域。
对应的 Native 侧代码在 CtrlView.java 中:构造函数里启用 JavaScript、加载本地页面并注册交互接口(L62-L64):
getSettings().setJavaScriptEnabled(true); //启用JavaScript loadUrl("file:///android_asset/streamview.html"); //加载本地HTML页面 addJavascriptInterface(this, "webview"); //创建WebView交互接口jsCallback方法(L67-L76)收到setVideoState消息后更新videoConnected标志并invalidate()重绘,这就是界面上"视频状态灯"的数据来源。当配网拿到图传 IP 后,setServerIP方法(L226-L234)会执行loadUrl("javascript:setServerIP('<ip>')")反向调用 HTML 函数完成视频源切换。
2.2 叠加控件的绘制与触摸
CtrlView继承自WebView,onDraw先调用super.onDraw(canvas)绘制 WebView 内容,再依次叠加绘制(L80-L125):
- 状态标识 + 配置按钮:左上角圆角矩形内显示蓝牙/视频连接状态图标(bt_on.png / video_off.png 等);
- 摇杆:加载 rocker.png 底图,按
rockerValueX/Y偏移绘制摇杆中心点; - 滑块:横向圆角轨道 + 圆形滑块,用于调节腿部长度;
- 左下角文字提示:显示"正在扫描设备…"、"WiFi连接成功…"等状态文字。
setSize方法(L237-L250)根据屏幕宽高计算各控件矩形区域:摇杆位于右下角,滑块位于其上方,配置按钮固定在左上角 (100, 0)-(200, 100)。
onTouchEvent(L128-L223)实现了一套轻量级命中检测:
- 按下时判断触点落在摇杆圆内、滑条内还是配置按钮内,分别置位
rockerDown/sliderDown/configDown; - 拖动摇杆时做了越界收缩(把落点拉回圆内)并归一化到 -1.0~1.0;同时存在死区逻辑——当
|rockerValueY/rockerValueX| < 0.4时把纵向分量清零(或横向清零),保证摇杆在"纯前进"或"纯转向"两个轴向上输出更干净; - 摇杆抬起即归零复位,滑块则保持松手位置;
- 配置按钮在按下区域内侧松开才触发
callback.onUserInput("click", "config"),交回MainActivity弹出操作菜单。
这套"HTML 画视频 + Canvas 画控件 + 统一 onTouchEvent 分发"的做法,避免了为视频帧做 Java 层解码,是文档所述显示方案的精髓。
三、图传连接方案:视频流 + UDP 遥控双链路
文档提示:图传系统的通信链路细节请先参阅 linux-fpv/README_en.md 中的 Connection Scheme 一节。该方案是"即插即用"的双场景设计:
- 不使用图传:App 直接通过蓝牙与主控模块(ESP32,蓝牙名
BalanceBot)收发遥控数据; - 使用图传:图传系统(NanoPI,蓝牙名
NanoPI)一方面与 App 通过局域网交互,另一方面通过蓝牙连接主控模块,把 App 发来的遥控数据转发给主控,同时把视频流推给 App。此时 App 与机器人无需处于蓝牙范围内,只需同在一个局域网即可。
注:图中"上位机"表示本 App,"运动控制模块"表示机器人主控模块。
3.1 视频链路:App 直接 HTTP 拉流
视频不需要经过蓝牙或 UDP,App 中的 WebView 直接向http://<NanoPI IP>:8090/camera.mjpg发起 HTTP 请求获取 MJPG 流(对应 streamview.html 中setServerIP的实现)。推流端由 NanoPI 上的mjpg-ffserver服务提供(见 linux-fpv/scripts/ffserver.conf)。
3.2 UDP 遥控链路:10Hz 的 4 字节遥控帧
遥控数据走 UDP。App 侧由 WifiClient.java 封装:本地绑定端口15000,默认目标端口5000,内部用阻塞式DatagramSocket.receive线程轮询接收(L15-L17、L28-L46)。
MainActivity中创建一个独立发送线程(MainActivity.java L225-L256),以 100ms 周期(10Hz)构造遥控帧:
final byte BT_FRAME_HEADER = (byte) 0xAA; //遥控数据帧头 byte[] btData = new byte[]{ BT_FRAME_HEADER, (byte) ((ctrlView.rockerValueX + 1) * 100), (byte) ((ctrlView.rockerValueY + 1) * 100), (byte) ((ctrlView.sliderValue + 1) * 100) }; if(btCtrl.isConnected) { btCtrl.sendBytes(btData); //蓝牙模式:直发主控模块 } else { wifiClient.sendBytes(btData); //图传模式:发往NanoPI的UDP服务器 }帧格式与主控端解析完全对应。ESP32 侧 main.cpp 的蓝牙写特征回调(L786-L795)按如下规则解码:
| 字节 | 含义 | 主控端解码(main.cpp) |
|---|---|---|
| data[0] | 帧头0xAA | 长度必须为 4,否则丢弃 |
| data[1] | 摇杆 X(转向) | target.yawSpeedCmd = -(data[1]/100.0f - 1) * 1.5f |
| data[2] | 摇杆 Y(前进) | target.speedCmd = (data[2]/100.0f - 1) * 1.0f |
| data[3] | 滑块(腿长) | target.legLength = (data[3]/200.0f) * 0.02f + 0.07f(约 0.07~0.09 m) |
另外0xAB单字节帧表示"触发起立",App 在配置菜单中选择了"发送站立指令"后,会根据当前模式经 UDP 或蓝牙发出0xAB(MainActivity.java L56-L62),主控端则创建Ctrl_StandupPrepareTask任务执行站立流程(main.cpp L792-L795)。
3.3 NanoPI 侧的 UDP↔蓝牙桥
在图传模式下,UDP 数据并不直接到达 ESP32,而是先到达 NanoPI 上运行的 ctrl-proxy.py。该脚本:
- 绑定
0.0.0.0:5000创建 UDP 服务器(L27-L33); - 收到 App 数据后进入"遥控数据转发模式"(
is_proxy_mode = True),并顺带service mjpg-ffserver start启动视频推流(L112-L115); - 通过 bleak 扫描并连接名为
BalanceBot的下位机(L141-L160),把 UDP 队列里的帧写入 ESP32 的 TX 特征(4c9a0002-...),并通过start_notify订阅其 RX 特征(4c9a0003-...)把下行数据经 UDP 转发回 App(L124-L127); - 超过 5 秒未收到 UDP 消息则退出转发模式并停止推流服务(L118-L121),实现"人不在、视频关"的省电逻辑。
因此,图传模式下整条链路为:App(UDP:15000) → NanoPI(UDP:5000) → BLE → ESP32;视频链路为App(WebView/HTTP) ← ffserver:8090 ← ffmpeg ← ov5640 摄像头。
四、蓝牙配网方案:BLE 文本协议驱动图传入网
文档定义:蓝牙配网指本程序先通过蓝牙直接连接到图传系统,向其发送网络信息,使其连接到指定的 WiFi 网络,此后即可通过网络连接到图传系统,整个过程自动完成。
4.1 双 GATT 客户端与两组 UUID
MainActivity中实际创建了两个BLE 客户端实例(Bluetooth.java 封装标准 GATT 客户端):
| 实例 | 目标设备 | 蓝牙名 | Service UUID | 写特征(App→设备) | Notify 特征(设备→App) |
|---|---|---|---|---|---|
btCtrl | ESP32 主控 | BalanceBot | 4c9a0001-fb2e-432e-92ec-c6f5316b4689 | 4c9a0002-... | 4c9a0003-... |
btUpper | NanoPI 图传 | NanoPI | 4c9a0011-fb2e-432e-92ec-c6f5316b4689 | 4c9a0013-... | 4c9a0012-... |
这三组 UUID 与固件端定义一一对应:ESP32 侧见 main.cpp L25-L28 的UUID_SERVICE/UUID_RX_CHARA/UUID_TX_CHARA与BLE_NAME "BalanceBot";NanoPI 侧见 ctrl-proxy.py L21-L24(bless库创建的 GATT Server,notify 特征4c9a0012、write 特征4c9a0013)。
Bluetooth.java的实现细节值得借鉴:
- 连接成功后调用
setPreferredPhy(PHY_LE_2M, ...)协商 2M PHY(L64),降低遥控链路时延; - 服务发现后向
00002902-...(CCCD)写入ENABLE_NOTIFICATION_VALUE开启订阅(L94-L97); - 首次扫描到设备后把 MAC 地址存入
SharedPreferences(key 为"MAC " + 设备名,L133-L135),下次connectDevice直接按 MAC 连接而不再全量扫描(L158-L162);选择"进行连接配置"时会调用clearMacInfo()强制重新扫描(MainActivity L77-L80); - 连接断开后 1 秒自动重连(L75-L80),保证链路的自愈能力。
4.2 配网协议:一组约定的文本命令
App 与 NanoPI 之间的 BLE 数据是 UTF-8 文本命令,由btUpper的回调处理(MainActivity.java L114-L185),与 ctrl-proxy.py 的on_self_write(L228-L256)严格对应:
| 方向 | 命令 | 触发与含义 |
|---|---|---|
| App → NanoPI | SCAN_WIFI | 请求扫描周边 WiFi;配网模式下连接成功即发送(L122-L124) |
| NanoPI → App | WIFI_LIST:<名1>\t<名2>... | 扫描结果,App 弹出列表让用户选择(L136-L154) |
| App → NanoPI | CONN_WIFI:<SSID>\t<密码> | 用户选定网络并输入密码后发送(L148) |
| NanoPI → App | CONN_WIFI_OK/CONN_WIFI_ERR | WiFi 连接结果反馈;失败则 App 提示并重扫(L155-L165) |
| App → NanoPI | GET_IP | 配网成功后延时 1 秒请求对端 IP(L157-L162) |
| NanoPI → App | SELF_IP:<ip> | 通过get_ip_address("wlan0")回报自身 IP(ctrl-proxy.py L233) |
收到SELF_IP后,App 执行收尾动作(L166-L182):wifiClient.setServerAddress(ip, 5000)设置 UDP 目标、ctrlView.setServerIP(ip)切换视频源、断开 BLE,至此配网流程闭环,遥控帧与视频流均走网络。此外 NanoPI 端还实现了SET_AP命令(设置热点模式 SSID/密码,ctrl-proxy.py L250-L255),当前 App 版本未调用,属于预留能力。
非配网状态下,App 连接NanoPI后直接发送GET_IP获取 IP(L125-L128),即可快速进入图传模式——这正是"再次打开 App 无需重新配网"的机制。
五、主要文件清单与工程配置
文档列出的关键文件如下(已逐一与仓库核对):
| 文件 | 职责 |
|---|---|
| balancebot.apk | 预编译安装包,可直接安装 |
| MainActivity.java | 主 Activity:创建各模块对象、处理用户菜单事件、10Hz 发送遥控帧 |
| Bluetooth.java | BLE GATT 客户端:扫描、连接、MAC 缓存、特征读写与订阅 |
| WifiClient.java | UDP 客户端:本地 15000 端口、阻塞接收线程 |
| CtrlView.java | 主视图,重写自 WebView,负责界面叠加绘制与触摸分发 |
| AndroidManifest.xml | 权限与 Activity 声明 |
| res/drawable | 摇杆底图、状态图标等图片资源 |
| streamview.html | WebView 视频流页面 |
工程层面的可考证信息(app/build.gradle、gradle-wrapper.properties):
compileSdk 32、minSdk 26、targetSdk 32,Java 8 语法级别,Gradle 7.2(Android Studio 对应 2021 年之后的版本即可打开);- 依赖仅
androidx.appcompat、material、constraintlayout三件套,没有引入任何第三方蓝牙/网络库——BLE 用系统 GATT API,网络用原生DatagramSocket,这一点在阅读源码时容易高估其复杂度,实际非常轻量。
AndroidManifest.xml 的权限声明体现了对 Android 版本差异的处理,与 MainActivity 中的运行时申请逻辑(L214-L222)配合:
- Android 12(API 31)及以上:
BLUETOOTH_SCAN(声明neverForLocation)+BLUETOOTH_CONNECT; - API 30 及以下:传统
BLUETOOTH/BLUETOOTH_ADMIN(maxSdkVersion="30"); - 蓝牙扫描所需的位置权限:
ACCESS_COARSE_LOCATION/ACCESS_FINE_LOCATION(对应源码中 S 以下申请精确定位、Q 以下申请粗略定位的分支); INTERNET+usesCleartextTraffic="true"(视频流为明文 HTTP);MainActivity声明为横屏(screenOrientation="landscape"),界面即按横屏布局设计。
六、使用说明(继承原文档并补充)
- 给机器人上电,等待图传系统启动完成(约 1 min);
- 打开本程序,点击左上角的配置按钮,菜单包含四项:
切换为蓝牙模式、切换为图传模式、进行连接配置、发送站立指令(见 MainActivity.java L53):- 不使用图传:点选"切换为蓝牙模式",等待连接成功(App 扫描并连接名为
BalanceBot的 ESP32 设备); - 使用图传:点选"切换为图传模式",然后再次点击配置按钮选择"进行连接配置",按要求选择网络并输入密码,等待连接成功(即 4.2 节的完整配网流程);
- 不使用图传:点选"切换为蓝牙模式",等待连接成功(App 扫描并连接名为
- 左上角配置按钮由灰色变亮即表示连接成功(蓝牙模式亮蓝牙图标、图传模式亮视频图标),此时即可使用摇杆控制机器人。
补充两点源码可证实的使用细节:
- 模式持久化:所选模式会写入
SharedPreferences的useVideoMode键(L100),下次启动 App 时自动按上次模式连接对应设备(L260-L266),无需重复选择; - 站立指令:机器人趴倒后,可在菜单中选择"发送站立指令"发送
0xAB帧,触发主控的站立准备任务; - 摇杆 X 轴控制转向(主控端按 1.5 倍增益换算),Y 轴控制前进后退,底部滑块实时调节腿部长度(映射为 0.07~0.09 m 的腿长目标值),松开摇杆即停止。
七、小结:可复用的设计要点
从这份实现中可以提炼出对类似"机器人远程调试 App"项目有参考价值的几条设计决策:
- 显示零成本化:把 MJPG 推流端点设计成标准 HTTP 资源,App 用 WebView 直接当视频容器,HTML 只写 60 行以内的代码,Native 层不碰任何视频解码;
- 单帧定长协议:遥控数据压缩为 1 字节帧头 + 3 字节量化参数(
(值+1)*100的无符号化技巧让 float 区间 [-1,1] 落入一个字节),10Hz 发送,UDP 与 BLE 两条通道共用同一帧格式,主控端解析逻辑零分支差异; - 配网即协议:BLE 通道被复用为"带外管理通道",用
SCAN_WIFI / CONN_WIFI / GET_IP这类简单文本命令完成入网与 IP 发现,避免了 App 与图传系统之间需要人工输入 IP 的繁琐步骤; - 链路自愈:BLE 断线 1 秒重连、HTML 页 3 秒心跳刷新视频源、UDP 5 秒超时自动退出转发模式,三处自愈逻辑共同降低了现场调试成本。
以上全部行为均可在仓库中按文件路径逐一验证:App 端四个 Java 源文件 + 一个 HTML 页面,Linux 端一个 Python 脚本,固件端main.cpp中的蓝牙解码回调,共同构成了这套轮腿机器人的完整远程操控链路。
- 机器人
- 嵌入式
- 硬件开发
- 移动开发
【免费下载链接】foc-wheel-legged-robot
Open source materials for a novel structured legged robot, including mechanical design, electronic design, algorithm simulation, and software development. | 一个新型结构的轮腿机器人开源资料,包含机械设计、电子设计、算法仿真、软件开发等材料
相关推荐
FOC 轮腿机器人 Android 遥控 App:触摸遥控、MJPG 图传显示与蓝牙自动配网实现解析
FOC 轮腿机器人 Android 遥控 App:触摸遥控、MJPG 图传显示与蓝牙自动配网实现解析 本文围绕 android/README.md 讲解 foc
机器人嵌入式硬件开发移动开发QGroundControl虚拟摇杆与遥控器配置:多种控制方式详解
QGroundControl虚拟摇杆与遥控器配置:多种控制方式详解 QGroundControl作为跨平台无人机地面控制站,提供了多种灵活的控制方式,让用户能够
无人机智能硬件高级应用:使用cross-de-zh-roberta-sentence-transformer构建智能客服系统
高级应用:使用cross de zh roberta sentence transformer构建智能客服系统 在全球化时代,企业需要跨越语言障碍提供高效服务。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考