遥控器APP端自动重连方案

15 阅读2分钟

一、 遥控器链路通信拓扑分析

根据硬件原理图与架构图:

  1. 私有宽带链路(CX6531CD RF):通过 PCIe/以太网 PHY(RTL8125/RTL8201)映射为 Android 系统级网络接口(如 eth0),负责 高带宽双向通信(RTSP/RTP 高清图传视频流 + UDP/TCP 遥测数传数据)。
  2. 独立安全控制链路(GD32 MCU):摇杆(Hall sticks \ge 200Hz)、E-stop 急停、按键直通 MCU,MCU 与 CX6531CD 直接走 SBUS/UART,同时与 SLM580 走 UART 通信。
  3. 备份网络(4G Cat.4 / Wi-Fi):系统标准移动蜂窝/WLAN 接口。

因此,APP 端面对的“链路中断”并非单一网络断开,而是多层级、多通道的复合状态异常


二、 链路中断判定机制(多维联合判决)

为了防止网络偶发抖动导致的误判,APP 必须采用 “物理/驱动层 + 传输/应用层 + 视频流解码层” 的三层判定漏斗模型。

+------------------------------------------------------------------------+
| 物理/射频状态 (PHY Link Down, RSSI/SNR 跌零, CX6531 RF 丢包率 > 阈值)     | L1/L2
+-----------------------------------+------------------------------------+
                                    | 并发融合
+-----------------------------------v------------------------------------+
| 数传遥测心跳 (Heartbeat 超时, RTT 持续恶化, 序列号连续丢失)               | L4/L7
+-----------------------------------+------------------------------------+
                                    | 交叉验证
+-----------------------------------v------------------------------------+
| 图传流健康度 (Socket Read Timeout, PTS 停滞, 解码关键帧丢帧)             | Video Pipeline
+-----------------------------------+------------------------------------+
                                    | 决策输出
                         [ APP 状态机裁决 ]

1. 物理与驱动层判定(Fast Detection,200ms\le 200\text{ms}

  • 以太网/PHY 接口状态检测
    • Android 系统层:注册 ConnectivityManager.NetworkCallback 监听图传网口(eth0)的 onLostonCapabilitiesChanged
    • 底层 Netlink 广播/Sysfs 节点:监听 RTL8125/RTL8201 PHY 状态。若网线/内部通信链路物理断开(Link Down),系统服务瞬间捕获。
  • RF 射频链路健康指标监控(私有 HAL/AIDL 服务)
    • 系统开发提供专有 System Service,定期(如 100ms100\text{ms})轮询 CX6531CD 驱动节点,暴露:RSSI、SNR、RF Lock(锁频状态)、PER(误码率)
    • 快速判定规则:当 RF Lock == false 且连续 300ms300\text{ms} 未恢复,或 RSSI < -95 dBm 持续 500ms500\text{ms},判定为 “RF 射频层脱网”

2. 数传应用层判定(Heartbeat Sliding Window,1000ms\le 1000\text{ms}

  • 双向自适应滑动窗口心跳
    • APP 与机载端保持周期性 Ping-Pong 心跳(默认 200ms200\text{ms} 一次)。
    • 引入 序列号(Seq ID)与滑动窗口,统计最近 10 个心跳周期的丢失率。
  • 分级告警与判定阈值
    • 弱网(Degraded):RTT >300ms> 300\text{ms} 或连续丢失 2 个心跳包。
    • 中断(Link Lost):连续丢失 4 个心跳包(800ms\ge 800\text{ms},工业级阈值推荐 800ms1.2s800\text{ms}\sim 1.2\text{s})。

3. 视频流层判定(Video Pipeline Check,1500ms\le 1500\text{ms}

  • Socket / 解码管道检测
    • RTSP/RTP Socket 设置 SO_RCVTIMEO1.0s1.0\text{s}
    • MediaCodec 解码器 PTS 监控:APP 维护一个 PTS Watchdog,当表面渲染纹理(SurfaceTexture)在 1.2s1.2\text{s} 内未收到新的 Presentation Time Stamp 时,视为“图传冻结/中断”。

三、 APP 链路重连状态机设计(FSM)

APP 内部维护统一的通信链路状态机,避免无脑死循环重连导致系统资源耗尽或 UI 界面卡死。

   [ CONNECTED ]
         |
    (心跳连续丢失 / RF失锁)
         v
   [ LINK_WARNING ] (弱网降级: OSD 提示, 降低视频码率请求)
         |
    (超时达到 1.0s)
         v
   [ DISCONNECTED ] (链路中断: 触发 Fail-Safe 警告, 通知 MCU)
         |
    (启动重连策略)
         v
   [ RECONNECTING ] (退避重连, 通道探测)
    /              \
(成功恢复)      (连续 N 次重连失败)
  /                  \
[ CONNECTED ]      [ FAIL_SAFE_LOST ] (彻底失联: 引导搜星/返航/切 4G 应急)

状态转移与行为定义:

  1. CONNECTED(正常连接):全功能可用,实时更新 OSD 延迟、信号强度指示条。
  2. LINK_WARNING(链路劣化)
    • 触发原因:连续丢包率 >30%> 30\% 或延时持续增高。
    • APP 行为:主动向机载端发送降低分辨率/码率指令;UI 弹出黄色轻量弱网提示。
  3. DISCONNECTED(中断确认)
    • 触发原因:心跳超时达到 1.0s1.0\text{s} 或 RF Lock 丢失。
    • APP 行为:
      1. 画面冻结并置灰,叠加 “链路中断,正在重连...” HUD 警告;
      2. 触发震动马达(通过硬件原理图中的马达驱动 PWM 输出短震);
      3. 通过 UART 通知 Safety MCU(MCU 可接管或进入自主保活逻辑)。
  4. RECONNECTING(重连中)
    • 采用 截断二进制指数退避算法(Exponential Backoff with Jitter) 进行重连,防止网络端突发广播风暴。
    • 重试间隔计算公式Twait=min(Tmax,  Tbase×2attempt)±RandomJitterT_{\text{wait}} = \min(T_{\max}, \; T_{\text{base}} \times 2^{\text{attempt}}) \pm \text{RandomJitter} 推荐参数Tbase=200msT_{\text{base}} = 200\text{ms}, Tmax=2000msT_{\max} = 2000\text{ms}, Jitter =±50ms= \pm 50\text{ms}
    • 重连步骤:
      1. 重新解析与握手 Control Socket(优先恢复遥测/控制指令);
      2. 发送 TEARDOWN -> SETUP -> PLAY 重建 RTSP 视频 Session;
      3. 强制请求机载编码器发送 IDR 关键帧。
  5. FAIL_SAFE_LOST(失控保护)
    • 重连超过阈值(如 10s10\text{s}):APP 端判定为远距离失联,自动切换或弹出 “是否切换 4G 蜂窝链路接管”,界面切换为离线地图与机载设备最后已知 GPS 定位轨迹引导。

四、 核心代码架构实现方案

1. 链路状态仲裁管理器(Kotlin 协程架构)

class LinkQualityManager(
    private val context: Context,
    private val rfHalService: IRfHalService // 绑定高通SLM580系统层的AIDL服务
) {
    private val _linkState = MutableStateFlow<LinkState>(LinkState.Disconnected)
    val linkState: StateFlow<LinkState> = _linkState.asStateFlow()

    private var lastHeartbeatTimestamp = 0L
    private var lastPtsTimestamp = 0L
    private val HEARTBEAT_TIMEOUT_MS = 1000L
    private val PTS_STALL_TIMEOUT_MS = 1500L

    fun startMonitor(scope: CoroutineScope) {
        scope.launch(Dispatchers.Default) {
            while (isActive) {
                val now = SystemClock.elapsedRealtime()
                val rfInfo = rfHalService.currentRfInfo // 获取 CX6531 信号: RSSI, Lock, PER
                
                // 1. 物理层判定
                if (!rfInfo.isLocked) {
                    handleStateTransition(LinkState.Disconnected("RF Lock Lost"))
                    delay(200)
                    continue
                }

                // 2. 遥测应用层心跳判定
                val isHeartbeatTimeout = (now - lastHeartbeatTimestamp) > HEARTBEAT_TIMEOUT_MS

                // 3. 视频流 PTS 停滞判定
                val isVideoStalled = (now - lastPtsTimestamp) > PTS_STALL_TIMEOUT_MS

                when {
                    isHeartbeatTimeout -> {
                        handleStateTransition(LinkState.Disconnected("Heartbeat Timeout"))
                    }
                    isVideoStalled -> {
                        handleStateTransition(LinkState.Degraded("Video Stream Frozen"))
                    }
                    rfInfo.rssi < -90 -> {
                        handleStateTransition(LinkState.Degraded("Weak RF Signal"))
                    }
                    else -> {
                        handleStateTransition(LinkState.Connected)
                    }
                }
                delay(100) // 10Hz 轮询仲裁
            }
        }
    }

    fun onHeartbeatReceived(seqId: Long) {
        lastHeartbeatTimestamp = SystemClock.elapsedRealtime()
    }

    fun onVideoFrameDecoded(pts: Long) {
        lastPtsTimestamp = SystemClock.elapsedRealtime()
    }

    private fun handleStateTransition(newState: LinkState) {
        if (_linkState.value != newState) {
            _linkState.value = newState
            notifySystemAndMcu(newState)
        }
    }

    private fun notifySystemAndMcu(state: LinkState) {
        // 通过串口通知 MCU,并联动震动/指示灯
    }
}

2. 重连引擎实现(平滑重连与防抖)

class ReconnectionEngine(
    private val commandChannel: CommandChannel,
    private val videoPipeline: VideoPipeline
) {
    private var reconnectJob: Job? = null

    fun triggerReconnect(scope: CoroutineScope) {
        if (reconnectJob?.isActive == true) return

        reconnectJob = scope.launch(Dispatchers.IO) {
            var attempt = 0
            val baseDelay = 200L
            val maxDelay = 2000L

            while (isActive) {
                attempt++
                val jitter = (-50..50).random()
                val backoff = min(maxDelay, baseDelay * (1 shl min(attempt, 6))) + jitter

                Log.w("ReconEngine", "Reconnecting attempt #$attempt, backoff: ${backoff}ms")

                // Step 1: 尝试重连指令控制通道 (轻量级)
                val cmdSuccess = commandChannel.reconnect()

                if (cmdSuccess) {
                    // Step 2: 控制通道成功,立即请求机载端刷新关键帧,并恢复视频管线
                    val videoSuccess = videoPipeline.reconnectStream()
                    if (videoSuccess) {
                        Log.i("ReconEngine", "Link fully restored on attempt #$attempt")
                        break // 重连成功,跳出循环
                    }
                }

                delay(backoff)
            }
        }
    }

    fun cancel() {
        reconnectJob?.cancel()
        reconnectJob = null
    }
}

五、 Android 16 系统级深度保障与优化策略

作为搭载 Android 16 的工业级平台,APP 层的健壮性必须由底层 OS 提供协同支持:

  1. 进程常驻与保活(Keep-Alive)
    • 将遥控器主控 APP 设为系统签名应用(android:persistent="true"),加入 AOSP Doze 白名单 与系统后台无杀保护。
    • 配置实时调度策略:数传和重连线程提升至 Linux SCHED_FIFO 或设置实时优先级(Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO))。
  2. 网络接口独立绑定(Socket Multi-homing)
    • SLM580 同时具备 4G(rmnet)、Wi-Fi(wlan0)和 CX6531CD(eth0)。
    • APP 端建立 Socket 时,必须使用 Network.bindSocket(socket) 显式绑定到 eth0 网络,避免在图传链路断开时,Android 系统自动将 Socket 漂移到 4G 导致重连路由混乱。
  3. 断网与安全闭环(Safety MCU 同步)
    • 当 APP 判定彻底失联后,通过 UART 向 Safety MCU 发送 DISCONNECT_ALARM 帧。
    • Safety MCU 依据硬件协议触发机载端返航/停机保护,杜绝因 Android 应用层崩溃或重连中产生死锁而导致机载设备飞丢。