feat(scale): 新增 UDP 广播兜底发现机制并恢复开机自启原始方案

- 新增 UdpBroadcastSender/UdpBroadcastReceiver,子设备定时广播,主设备监听,与 mDNS 互为备份
- 恢复 BootReceiver 为直接启动 Activity 的原始方式,删除 BootService
- 移除 InitActivity 开机自启相关 Window flags
- 移除 PyTorch 依赖,缩减安装包体积
- 删除仓库中的 release apk 和 pt 模型文件
This commit is contained in:
2026-04-10 18:04:18 +08:00
parent 904ccdbb06
commit c72d30e88a
13 changed files with 542 additions and 473 deletions
@@ -31,9 +31,11 @@ object ScaleServiceManager {
private var mdnsRegister: MdnsRegisterManager? = null
private var wsServer: ScaleWebSocketServer? = null
private var udpSender: UdpBroadcastSender? = null
// 以下仅主设备使用
private var mdnsDiscovery: MdnsDiscoveryManager? = null
private var udpReceiver: UdpBroadcastReceiver? = null
private var wsClient: ScaleWebSocketClient? = null
private var aggregator: ScaleDataAggregator? = null
@@ -66,6 +68,10 @@ object ScaleServiceManager {
if (role == DeviceRole.MASTER) {
startMasterServices(context, deviceId)
} else {
// 子设备:启动 UDP 广播,作为 mDNS 的兜底发现机制
val localIp = getLocalIp(context)
udpSender = UdpBroadcastSender(deviceId, localIp).also { it.start() }
}
}
@@ -93,24 +99,35 @@ object ScaleServiceManager {
}
wsClient = client
// 统一的设备发现处理:mDNS 和 UDP 共用同一逻辑
fun onDeviceFound(remoteId: String, host: String, port: Int) {
if (remoteId == deviceId) return
Log.d(TAG, "发现子设备 $remoteId ($host:$port), 建立连接")
aggregator?.setDeviceIp(remoteId, host)
client.connect(remoteId, host, port)
}
// mDNS 发现:发现子设备 → 建立 WebSocket 连接;设备离线 → 断开连接并清除数据
mdnsDiscovery = MdnsDiscoveryManager(context).also { discovery ->
discovery.onDeviceFound = { remoteId, host, port ->
// 不连接自身
if (remoteId != deviceId) {
Log.d(TAG, "发现子设备 $remoteId, 建立连接")
// 记录子设备 IP,供 UI 展示使用
aggregator?.setDeviceIp(remoteId, host)
client.connect(remoteId, host, port)
}
onDeviceFound(remoteId, host, port)
}
discovery.onDeviceLost = { remoteId ->
Log.d(TAG, "子设备离线 $remoteId, 断开连接并清除数据")
client.disconnect(remoteId)
aggregator?.removeDevice(remoteId)
udpReceiver?.removeDevice(remoteId)
}
discovery.startDiscovery()
}
// UDP 广播接收:作为 mDNS 的兜底,发现 mDNS 未能触发的子设备
udpReceiver = UdpBroadcastReceiver().also { receiver ->
receiver.onDeviceFound = { remoteId, host, port ->
onDeviceFound(remoteId, host, port)
}
receiver.start()
}
}
/**
@@ -118,12 +135,16 @@ object ScaleServiceManager {
*/
fun stop() {
mdnsDiscovery?.stopDiscovery()
udpReceiver?.stop()
udpSender?.stop()
wsClient?.shutdown()
aggregator?.stop()
wsServer?.stop()
mdnsRegister?.unregister()
mdnsDiscovery = null
udpReceiver = null
udpSender = null
wsClient = null
aggregator = null
wsServer = null
@@ -0,0 +1,95 @@
package com.shuwei.dish.match.scale
import android.util.Log
import com.google.gson.Gson
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
/**
* UDP 广播接收器(主设备运行)
* 监听子设备发来的 UDP 广播包,解析后通过回调通知连接管理器
* 与 mDNS 互为备份,任意一条路发现子设备均可建立 WebSocket 连接
*/
class UdpBroadcastReceiver {
companion object {
private const val TAG = "UdpBroadcastReceiver"
private const val BUFFER_SIZE = 1024
}
private val gson = Gson()
private val executor = Executors.newSingleThreadExecutor()
/** 已发现的设备集合,避免重复触发回调,key=deviceId, value="host:port" */
private val knownDevices = ConcurrentHashMap<String, String>()
@Volatile
private var running = false
private var socket: DatagramSocket? = null
/**
* 发现新设备时的回调:(deviceId, host, port)
* 与 MdnsDiscoveryManager.onDeviceFound 签名一致,可共用同一处理逻辑
*/
var onDeviceFound: ((deviceId: String, host: String, port: Int) -> Unit)? = null
/**
* 开始监听 UDP 广播
*/
fun start() {
if (running) return
running = true
executor.execute { listenLoop() }
Log.d(TAG, "UDP 广播接收已启动, 监听端口=${UdpBroadcastSender.BROADCAST_PORT}")
}
private fun listenLoop() {
try {
socket = DatagramSocket(UdpBroadcastSender.BROADCAST_PORT)
val buf = ByteArray(BUFFER_SIZE)
while (running) {
val packet = DatagramPacket(buf, buf.size)
socket?.receive(packet) ?: break
val json = String(packet.data, 0, packet.length, Charsets.UTF_8)
handlePacket(json)
}
} catch (e: Exception) {
if (running) Log.w(TAG, "UDP 接收异常: ${e.message}")
} finally {
socket?.close()
socket = null
}
}
private fun handlePacket(json: String) {
try {
val payload = gson.fromJson(json, UdpBroadcastSender.Payload::class.java)
val key = "${payload.ip}:${payload.port}"
// 同一设备已通知过则跳过,避免每5秒重复触发连接
if (knownDevices.put(payload.deviceId, key) == key) return
Log.d(TAG, "UDP 发现设备: ${payload.deviceId} -> ${payload.ip}:${payload.port}")
onDeviceFound?.invoke(payload.deviceId, payload.ip, payload.port)
} catch (e: Exception) {
Log.w(TAG, "解析 UDP 广播包失败: ${e.message}")
}
}
/**
* 移除已知设备记录,设备离线时调用,确保设备重新上线后能再次触发回调
*/
fun removeDevice(deviceId: String) {
knownDevices.remove(deviceId)
}
/**
* 停止监听,释放资源
*/
fun stop() {
running = false
socket?.close()
executor.shutdownNow()
Log.d(TAG, "UDP 广播接收已停止")
}
}
@@ -0,0 +1,76 @@
package com.shuwei.dish.match.scale
import android.util.Log
import com.google.gson.Gson
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
/**
* UDP 广播发送器(子设备运行)
* 每隔固定间隔向局域网广播本机的 deviceId、IP、WebSocket 端口
* 作为 mDNS 的兜底发现机制,确保主设备能可靠发现子设备
*/
class UdpBroadcastSender(
private val deviceId: String,
private val localIp: String,
private val wsPort: Int = MdnsRegisterManager.WS_PORT
) {
companion object {
private const val TAG = "UdpBroadcastSender"
/** UDP 广播目标端口,与 UdpBroadcastReceiver 保持一致 */
const val BROADCAST_PORT = 8766
/** 广播间隔(秒) */
private const val INTERVAL_SEC = 5L
}
private val gson = Gson()
private val scheduler = Executors.newSingleThreadScheduledExecutor()
private var task: ScheduledFuture<*>? = null
/** 广播数据包结构 */
data class Payload(val deviceId: String, val ip: String, val port: Int)
/**
* 开始定时广播
*/
fun start() {
if (task != null) return
task = scheduler.scheduleWithFixedDelay({
sendBroadcast()
}, 0, INTERVAL_SEC, TimeUnit.SECONDS)
Log.d(TAG, "UDP 广播已启动, deviceId=$deviceId, ip=$localIp, port=$wsPort")
}
private fun sendBroadcast() {
try {
val payload = gson.toJson(Payload(deviceId, localIp, wsPort))
val data = payload.toByteArray(Charsets.UTF_8)
DatagramSocket().use { socket ->
socket.broadcast = true
val packet = DatagramPacket(
data, data.size,
InetAddress.getByName("255.255.255.255"),
BROADCAST_PORT
)
socket.send(packet)
}
} catch (e: Exception) {
Log.w(TAG, "UDP 广播发送失败: ${e.message}")
}
}
/**
* 停止广播,释放资源
*/
fun stop() {
task?.cancel(false)
task = null
scheduler.shutdownNow()
Log.d(TAG, "UDP 广播已停止")
}
}