feat: OTA部分对接代码提交

This commit is contained in:
2026-06-26 14:43:44 +08:00
parent 54e5427a77
commit 35c49bbb10
3 changed files with 506 additions and 51 deletions
+292 -29
View File
@@ -1,8 +1,15 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { onLoad, onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import {
connectDeviceWifiAPI,
getDeviceBatteryAPI,
getHardwareBoxTaskStatusAPI,
getHardwareBoxVersionAPI,
sendHardwareBoxUpdateAPI,
} from "@/apis";
const STATES = {
SCANNING: "SCANNING",
@@ -24,15 +31,29 @@ const connectingWifi = ref(null);
const connectInput = ref({ ssid: "", password: "" });
const connectMode = ref("secure"); // secure | open | manual
const connectError = ref("");
const connectStatusText = ref("");
const isSubmittingWifi = ref(false);
const keyboardHeight = ref(0);
// 控制密码输入框是否显示明文
const showPassword = ref(false);
// 刷新防抖标志:扫描进行中为 true,禁止重复点击;扫描结束(成功/失败)后重置为 false。
const isRefreshing = ref(false);
const isStartingUpdate = ref(false);
const routeOtaInfo = ref({
versionNumber: "",
resourceUrl: "",
});
const progress = ref(0);
let progressTimer = null;
let timeoutTimer = null;
let statusTimer = null;
let wifiConnectTimer = null;
let wifiConnectRequestId = 0;
let wifiConnectPollCount = 0;
const WIFI_CONNECT_POLL_INTERVAL = 2000;
const WIFI_CONNECT_MAX_POLL_COUNT = 15;
const WIFI_CONNECT_FAILED_TEXT = "连接失败,请检查WiFi密码或WiFi状态";
// 控制授权拒绝弹窗显示/隐藏
const wifiAuthDeniedVisible = ref(false);
@@ -182,7 +203,9 @@ const startScanning = () => {
});
};
// 选择列表中的 WiFi,并打开密码输入弹窗。
const selectWifi = (wifi) => {
cancelWifiConnectPolling();
connectingWifi.value = wifi;
connectInput.value = { ssid: wifi.SSID, password: "" };
connectMode.value = wifi.secure ? "secure" : "open";
@@ -190,7 +213,9 @@ const selectWifi = (wifi) => {
currentState.value = STATES.CONNECTING;
};
// 选择手动输入 WiFi,并打开手动输入弹窗。
const selectOther = () => {
cancelWifiConnectPolling();
connectingWifi.value = null;
connectInput.value = { ssid: "", password: "" };
connectMode.value = "manual";
@@ -198,7 +223,9 @@ const selectOther = () => {
currentState.value = STATES.CONNECTING;
};
// 关闭连接弹窗,并停止当前 WiFi 连接轮询。
const closeConnectSheet = () => {
cancelWifiConnectPolling();
connectError.value = "";
currentState.value = connectedWifi.value ? STATES.CONNECTED : STATES.LIST;
};
@@ -212,7 +239,9 @@ const ssidWarning = computed(() => {
return "";
});
// 判断当前是否禁止点击加入按钮,提交中也禁止重复点击。
const joinDisabled = computed(() => {
if (isSubmittingWifi.value) return true;
if (connectMode.value === "secure") return !connectInput.value.password;
if (connectMode.value === "manual") return !connectInput.value.ssid;
return false;
@@ -226,17 +255,107 @@ const wifiListScrollHeight = computed(() => {
return `${Math.min(itemCount * 92, maxHeight)}rpx`;
});
// 提交 WiFi 配置给游戏设备;后端接口未接入前只提示占位信息并停留在弹窗
const submitDeviceWifiConfig = ({ ssid, password }) => {
console.log("[OTA WiFi] submit device wifi config pending:", {
ssid,
hasPassword: !!password,
// 清理 WiFi 连接轮询定时器
const clearWifiConnectTimer = () => {
clearTimeout(wifiConnectTimer);
wifiConnectTimer = null;
wifiConnectPollCount = 0;
};
// 取消当前 WiFi 连接轮询,并恢复弹窗提交状态。
const cancelWifiConnectPolling = () => {
wifiConnectRequestId += 1;
clearWifiConnectTimer();
connectStatusText.value = "";
isSubmittingWifi.value = false;
uni.hideLoading();
};
// 判断设备是否已经在线且连接到 WiFi。
const isDeviceConnectedByWifi = (deviceStatus) => {
return deviceStatus?.online === true && String(deviceStatus?.netType || "").toLowerCase() === "wifi";
};
// 轮询设备电量接口,确认设备已经切换到 WiFi 在线状态。
const waitForDeviceWifiConnected = (requestId) => {
return new Promise((resolve) => {
const poll = async () => {
if (requestId !== wifiConnectRequestId) {
resolve(false);
return;
}
wifiConnectPollCount += 1;
try {
const deviceStatus = await getDeviceBatteryAPI();
if (requestId !== wifiConnectRequestId) {
resolve(false);
return;
}
if (isDeviceConnectedByWifi(deviceStatus)) {
resolve(true);
return;
}
} catch (err) {
if (requestId !== wifiConnectRequestId) {
resolve(false);
return;
}
}
if (wifiConnectPollCount >= WIFI_CONNECT_MAX_POLL_COUNT) {
resolve(false);
return;
}
wifiConnectTimer = setTimeout(poll, WIFI_CONNECT_POLL_INTERVAL);
};
poll();
});
connectError.value = "设备连接接口待接入";
uni.showToast({
title: "设备连接接口待接入",
icon: "none",
};
// 提交 WiFi 配置给游戏设备,并轮询确认设备真实连上 WiFi 后再展示成功。
const submitDeviceWifiConfig = async ({ ssid, password }) => {
if (isSubmittingWifi.value) return;
wifiConnectRequestId += 1;
const requestId = wifiConnectRequestId;
clearWifiConnectTimer();
isSubmittingWifi.value = true;
connectStatusText.value = "WiFi连接中...";
uni.showLoading({
title: "WiFi连接中",
mask: true,
});
try {
await connectDeviceWifiAPI(ssid, password);
const isConnected = await waitForDeviceWifiConnected(requestId);
if (requestId !== wifiConnectRequestId) return;
if (!isConnected) {
connectError.value = WIFI_CONNECT_FAILED_TEXT;
return;
}
connectedWifi.value = {
...(connectingWifi.value || {}),
SSID: ssid,
password,
secure: connectMode.value === "secure" || !!password,
};
connectError.value = "";
currentState.value = STATES.CONNECTED;
} catch (err) {
if (requestId === wifiConnectRequestId) {
connectError.value =
err?.code === -1 && err?.message ? err.message : WIFI_CONNECT_FAILED_TEXT;
}
} finally {
if (requestId === wifiConnectRequestId) {
clearWifiConnectTimer();
connectStatusText.value = "";
isSubmittingWifi.value = false;
uni.hideLoading();
}
}
};
// 校验用户输入并提交 WiFi 配置,不再把手机连接结果当作设备连接成功。
@@ -249,9 +368,18 @@ const joinNetwork = () => {
submitDeviceWifiConfig({ ssid, password });
};
const startUpdate = () => {
currentState.value = STATES.UPDATING;
progress.value = 0;
// 清理 OTA 更新相关定时器,避免页面退出或状态结束后继续执行。
const clearUpdateTimers = () => {
clearInterval(progressTimer);
clearTimeout(timeoutTimer);
clearTimeout(statusTimer);
progressTimer = null;
timeoutTimer = null;
statusTimer = null;
};
// 启动本地进度条动画,真实完成状态以后端任务轮询结果为准。
const startProgressAnimation = () => {
clearInterval(progressTimer);
progressTimer = setInterval(() => {
if (progress.value >= 90) {
@@ -261,28 +389,144 @@ const startUpdate = () => {
const increment = Math.max(0.5, 2 - progress.value / 60);
progress.value = Math.min(90, progress.value + increment);
}, 500);
clearTimeout(timeoutTimer);
timeoutTimer = setTimeout(() => {
if (currentState.value === STATES.UPDATING) {
clearInterval(progressTimer);
currentState.value = STATES.FAILED;
}
}, 5 * 60 * 1000);
};
const handleWsDone = () => {
clearInterval(progressTimer);
clearTimeout(timeoutTimer);
// 将 OTA 更新流程标记为失败并切换到失败页面。
const failUpdate = () => {
clearUpdateTimers();
isStartingUpdate.value = false;
currentState.value = STATES.FAILED;
};
// 将 OTA 更新流程标记为成功,进度补到 100% 后进入完成页面。
const completeUpdate = () => {
clearUpdateTimers();
isStartingUpdate.value = false;
progress.value = 100;
setTimeout(() => {
currentState.value = STATES.DONE;
}, 300);
};
// 轮询 OTA 更新任务状态,状态为处理中则继续轮询,成功或失败则结束流程。
const pollUpdateTaskStatus = (taskId) => {
clearTimeout(statusTimer);
statusTimer = setTimeout(async () => {
try {
const taskStatus = await getHardwareBoxTaskStatusAPI(taskId);
const status = Number(taskStatus?.status);
if (status === 2) {
completeUpdate();
return;
}
if (status === 3) {
failUpdate();
return;
}
if (status === 0 || status === 1) {
pollUpdateTaskStatus(taskId);
return;
}
failUpdate();
} catch (err) {
failUpdate();
}
}, 3000);
};
// 判断设备是否满足 OTA 更新条件,不满足时返回精确提示文案。
const getUpdateDisabledReason = (deviceStatus) => {
if (deviceStatus?.online !== true) return "设备已离线,请先开启设备并保持在线";
if (Number(deviceStatus?.battery) <= 20) return "设备电量不足,请充电至 20% 以上后再更新";
if (String(deviceStatus?.netType || "").toLowerCase() !== "wifi") return "设备当前未连接 WiFi,请先连接 WiFi 后再更新";
return "";
};
// 获取 OTA 更新版本信息,优先使用首页跳转传入的数据,没有传参时再请求后端版本接口。
const getOtaVersionInfo = async () => {
if (routeOtaInfo.value.versionNumber && routeOtaInfo.value.resourceUrl) {
return {
needUpdate: true,
versionNumber: routeOtaInfo.value.versionNumber,
resourceUrl: routeOtaInfo.value.resourceUrl,
};
}
return getHardwareBoxVersionAPI();
};
// 点击开始更新时先判断设备状态和版本信息,满足条件才发送 OTA 指令并开始轮询任务状态。
const startUpdate = async () => {
if (isStartingUpdate.value) return;
if (!connectedWifi.value) return;
isStartingUpdate.value = true;
try {
const deviceStatus = await getDeviceBatteryAPI();
const disabledReason = getUpdateDisabledReason(deviceStatus);
if (disabledReason) {
isStartingUpdate.value = false;
uni.showToast({
title: disabledReason,
icon: "none",
});
return;
}
let versionInfo;
try {
versionInfo = await getOtaVersionInfo();
} catch (err) {
isStartingUpdate.value = false;
uni.showToast({
title: "获取更新版本失败,请重试",
icon: "none",
});
return;
}
if (!versionInfo?.needUpdate) {
isStartingUpdate.value = false;
uni.showToast({
title: "当前已是最新版本",
icon: "none",
});
return;
}
currentState.value = STATES.UPDATING;
progress.value = 0;
startProgressAnimation();
timeoutTimer = setTimeout(() => {
if (currentState.value === STATES.UPDATING) {
failUpdate();
}
}, 5 * 60 * 1000);
const updateResult = await sendHardwareBoxUpdateAPI({
versionNumber: versionInfo.versionNumber,
wifiSsid: connectedWifi.value.SSID,
wifiPassword: connectedWifi.value.password || "",
resourceUrl: versionInfo.resourceUrl,
});
if (!updateResult?.taskId) {
failUpdate();
return;
}
pollUpdateTaskStatus(updateResult.taskId);
} catch (err) {
failUpdate();
}
};
// WebSocket 成功回调保留兜底能力,触发后直接按更新完成处理。
const handleWsDone = () => {
completeUpdate();
};
// WebSocket 失败回调保留兜底能力,触发后直接按更新失败处理。
const handleWsFail = () => {
clearInterval(progressTimer);
clearTimeout(timeoutTimer);
currentState.value = STATES.FAILED;
failUpdate();
};
const handleDone = () => {
@@ -322,6 +566,14 @@ const togglePasswordVisibility = () => {
});
};
// 页面加载时接收首页传入的 OTA 版本号和固件地址。
onLoad((options = {}) => {
routeOtaInfo.value = {
versionNumber: decodeURIComponent(options.versionNumber || ""),
resourceUrl: decodeURIComponent(options.resourceUrl || ""),
};
});
// 页面挂载后启动 WiFi 扫描流程。
onMounted(() => {
if (typeof uni.onKeyboardHeightChange === "function") {
@@ -341,8 +593,8 @@ onUnmounted(() => {
if (typeof uni.offKeyboardHeightChange === "function") {
uni.offKeyboardHeightChange(handleKeyboardHeightChange);
}
clearInterval(progressTimer);
clearTimeout(timeoutTimer);
cancelWifiConnectPolling();
clearUpdateTimers();
wx.offGetWifiList && wx.offGetWifiList();
});
</script>
@@ -524,6 +776,7 @@ onUnmounted(() => {
/>
</view>
</view>
<text v-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
<text v-if="connectError" class="connect-error">{{ connectError }}</text>
</block>
@@ -538,7 +791,8 @@ onUnmounted(() => {
<image src="../static/sicon/check.png" mode="aspectFit" style="width: 28rpx; height: 24rpx;" />
</view>
</view>
<text class="sheet-hint">该网络为开放网络点击 加入</text>
<text v-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
<text v-else class="sheet-hint">该网络为开放网络点击 加入</text>
<text v-if="connectError" class="connect-error">{{ connectError }}</text>
</block>
@@ -588,6 +842,7 @@ onUnmounted(() => {
</view>
</view>
<text v-if="ssidWarning" class="connect-error">{{ ssidWarning }}</text>
<text v-else-if="connectStatusText" class="connect-status">{{ connectStatusText }}</text>
<text v-else-if="connectError" class="connect-error">{{ connectError }}</text>
</block>
@@ -927,6 +1182,14 @@ onUnmounted(() => {
text-align: center;
margin-bottom: 24rpx;
}
.connect-status {
color: rgba(255, 255, 255, 0.72);
font-size: 26rpx;
line-height: 40rpx;
margin-top: 16rpx;
display: block;
text-align: center;
}
.connect-error {
color: rgba(254, 216, 71, 1);
font-size: 26rpx;