初初稿
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/unpackage
|
||||||
15
.hbuilderx/launch.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"version" : "1.0",
|
||||||
|
"configurations" : [
|
||||||
|
{
|
||||||
|
"customPlaygroundType" : "local",
|
||||||
|
"playground" : "custom",
|
||||||
|
"type" : "uni-app:app-android"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customPlaygroundType" : "local",
|
||||||
|
"playground" : "custom",
|
||||||
|
"type" : "uni-app:app-ios"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
98
App.uvue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<script lang="uts">
|
||||||
|
import {BleLib,MyApiResult,BleScanResult,BleServices} from "@/uni_modules/android-ble"
|
||||||
|
import * as Ble from "@/src/BluetoothManager.uts"
|
||||||
|
import * as McmChannel from "@/src/mcm/McmChannel.uts"
|
||||||
|
|
||||||
|
// #ifdef APP-ANDROID || APP-HARMONY
|
||||||
|
let firstBackTime = 0
|
||||||
|
// #endif
|
||||||
|
export default {
|
||||||
|
|
||||||
|
globalData: {
|
||||||
|
ble: new BleLib(), // 蓝牙全局变量
|
||||||
|
// 接收蓝牙数据的回调
|
||||||
|
setBleNotity(){
|
||||||
|
getApp().globalData.ble.onNotityReadBleData(getApp().globalData.ble.getSericUUID(),
|
||||||
|
getApp().globalData.ble.getNotityUUID(),
|
||||||
|
true,
|
||||||
|
function(res:MyApiResult){
|
||||||
|
// console.log(res)
|
||||||
|
uni.$emit("bleData", res)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
},
|
||||||
|
// 连接蓝牙
|
||||||
|
ConnectBle(address: string, autoReconnect: boolean){
|
||||||
|
getApp().globalData.ble.connect(address, autoReconnect, function(res:MyApiResult){
|
||||||
|
// console.log(res)
|
||||||
|
if(res.type==0){
|
||||||
|
getApp().globalData.ble.scanServices(function(resSevice:MyApiResult){
|
||||||
|
// console.log(resSevice);
|
||||||
|
// 显式选中 CAIIC-MCM 的 CLS 服务组合(自动识别可能选错)
|
||||||
|
Ble.selectService("0000CA10")
|
||||||
|
getApp().globalData.setBleNotity()
|
||||||
|
|
||||||
|
// getApp().globalData.ble.readRssi(function(b:MyApiResult){
|
||||||
|
// console.log(b)
|
||||||
|
// })
|
||||||
|
// console.log(getApp().globalData.ble.getwriteUUID());
|
||||||
|
// console.log(getApp().globalData.ble.getNotityUUID());
|
||||||
|
uni.$emit("bleState",res.type)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}else{
|
||||||
|
uni.$emit("bleState",res.type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onLaunch: function () {
|
||||||
|
console.log('App Launch')
|
||||||
|
// 注册 CAIIC-MCM 协议通道的全局蓝牙事件监听
|
||||||
|
McmChannel.attach()
|
||||||
|
},
|
||||||
|
onShow: function () {
|
||||||
|
console.log('App Show')
|
||||||
|
},
|
||||||
|
onHide: function () {
|
||||||
|
console.log('App Hide')
|
||||||
|
},
|
||||||
|
// #ifdef APP-ANDROID || APP-HARMONY
|
||||||
|
onLastPageBackPress: function () {
|
||||||
|
console.log('App LastPageBackPress')
|
||||||
|
if (firstBackTime == 0) {
|
||||||
|
uni.showToast({
|
||||||
|
title: 'Press Exit Application again',
|
||||||
|
position: 'bottom',
|
||||||
|
})
|
||||||
|
firstBackTime = Date.now()
|
||||||
|
setTimeout(() => {
|
||||||
|
firstBackTime = 0
|
||||||
|
}, 2000)
|
||||||
|
} else if (Date.now() - firstBackTime < 2000) {
|
||||||
|
firstBackTime = Date.now()
|
||||||
|
uni.exit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// #endif
|
||||||
|
onExit: function () {
|
||||||
|
console.log('App Exit')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/*每个页面公共css */
|
||||||
|
.uni-row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uni-column {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
api/httpInstance.uts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { getToken } from "../common/auth"
|
||||||
|
import { clearAllData } from "../common/auth"
|
||||||
|
|
||||||
|
// 可选后台服务器列表
|
||||||
|
export type ServerItem = {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SERVER_LIST: ServerItem[] = [
|
||||||
|
{ name: "正式 ssl (evmtapp.clenergy.com)", url: "https://evmtapp.clenergy.com:20096" } as ServerItem,
|
||||||
|
{ name: "测试 (47.107.245.120)", url: "http://47.107.245.120:30081" } as ServerItem,
|
||||||
|
{ name: "DEBUG (192.168.13.158)", url: "http://192.168.13.158:30081" } as ServerItem
|
||||||
|
]
|
||||||
|
|
||||||
|
let recordBaseUrl: string | null = null
|
||||||
|
|
||||||
|
// 读取当前后台服务器地址(内存 + 本地),未选择过则默认第一个
|
||||||
|
export function getBaseUrl(): string {
|
||||||
|
if (recordBaseUrl != null && (recordBaseUrl as string).length > 0) {
|
||||||
|
return recordBaseUrl as string
|
||||||
|
}
|
||||||
|
const t = uni.getStorageSync('serverBaseUrl') as string | null
|
||||||
|
if (t != null && t.length > 0) {
|
||||||
|
recordBaseUrl = t
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
return SERVER_LIST[0].url
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存后台服务器地址(内存 + 本地),下次启动直接加载
|
||||||
|
export function setBaseUrl(url: string) {
|
||||||
|
recordBaseUrl = url
|
||||||
|
uni.setStorageSync('serverBaseUrl', url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
|
||||||
|
|
||||||
|
// 请求参数类型
|
||||||
|
export type HttpRequest = {
|
||||||
|
url: string
|
||||||
|
method?: HttpMethod
|
||||||
|
data?: any
|
||||||
|
header?: UTSJSONObject // 这里用 UTSJSONObject
|
||||||
|
}
|
||||||
|
|
||||||
|
// 封装请求
|
||||||
|
export function $http(params: HttpRequest): Promise<UTSJSONObject> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
// 1. 准备 header
|
||||||
|
let baseHeader: UTSJSONObject = params.header ?? {} as UTSJSONObject
|
||||||
|
|
||||||
|
baseHeader["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
// 2. 读 token,有就加 Authorization
|
||||||
|
const token = getToken()
|
||||||
|
if (token != null && token.length > 0) {
|
||||||
|
baseHeader["Authorization"] = token
|
||||||
|
}
|
||||||
|
// console.log("url = ", getBaseUrl() + params.url)
|
||||||
|
// console.log("method = ", params.method)
|
||||||
|
// console.log("data = ", params.data)
|
||||||
|
// console.log("header = ", baseHeader)
|
||||||
|
|
||||||
|
// 3. 发请求
|
||||||
|
uni.request({
|
||||||
|
url: getBaseUrl() + params.url,
|
||||||
|
method: params.method ?? "GET",
|
||||||
|
data: params.data ?? {} as UTSJSONObject,
|
||||||
|
header: baseHeader,
|
||||||
|
success: (res) => {
|
||||||
|
if(res.statusCode == 401) {
|
||||||
|
clearAllData()
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/login/login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const result: UTSJSONObject = {
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
data: res.data
|
||||||
|
} as UTSJSONObject
|
||||||
|
resolve(result)
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
80
api/requestApi.uts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { $http } from "@/api/httpInstance.uts"
|
||||||
|
|
||||||
|
export type RegisteredUserReq = {
|
||||||
|
userName: string,
|
||||||
|
userPassword: string,
|
||||||
|
rndcode: string
|
||||||
|
}
|
||||||
|
// 获取pinCode
|
||||||
|
export async function GetPindCodeReqApi(params: RegisteredUserReq): Promise<UTSJSONObject>{
|
||||||
|
const body: UTSJSONObject = {
|
||||||
|
userName: params.userName,
|
||||||
|
userPassword: params.userPassword,
|
||||||
|
rndcode: params.rndcode
|
||||||
|
} as UTSJSONObject
|
||||||
|
|
||||||
|
const res = await $http({
|
||||||
|
url: "/ocpp-mtapp-api/api/v1/users/getPinCode",
|
||||||
|
method: "POST",
|
||||||
|
data: body
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册用户
|
||||||
|
export async function RegisteredUserReqApi(params: RegisteredUserReq): Promise<UTSJSONObject>{
|
||||||
|
const body: UTSJSONObject = {
|
||||||
|
userName: params.userName,
|
||||||
|
userPassword: params.userPassword,
|
||||||
|
rndcode: params.rndcode
|
||||||
|
} as UTSJSONObject
|
||||||
|
|
||||||
|
const res = await $http({
|
||||||
|
url: "/ocpp-mtapp-api/api/v1/users/add",
|
||||||
|
method: "POST",
|
||||||
|
data: body
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoginReq = {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
// 登录请求
|
||||||
|
export async function LoginReqApi(params: LoginReq): Promise<UTSJSONObject>{
|
||||||
|
const body: UTSJSONObject = {
|
||||||
|
username: params.username,
|
||||||
|
password: params.password,
|
||||||
|
expirationTime: 0
|
||||||
|
} as UTSJSONObject
|
||||||
|
|
||||||
|
const res = await $http({
|
||||||
|
url: "/ocpp-mtapp-api/api/v1/auth/login",
|
||||||
|
method: "POST",
|
||||||
|
data: body
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModifyUserInfoReq = {
|
||||||
|
name?: string | null,
|
||||||
|
phone?: string | null,
|
||||||
|
password?: string | null,
|
||||||
|
nickName?: string | null
|
||||||
|
}
|
||||||
|
// 修改用户信息
|
||||||
|
export async function ModifyUserInfoReqApi(params: ModifyUserInfoReq): Promise<UTSJSONObject>{
|
||||||
|
const body: UTSJSONObject = {
|
||||||
|
name: params.name,
|
||||||
|
phone: params.phone,
|
||||||
|
password: params.password
|
||||||
|
} as UTSJSONObject
|
||||||
|
|
||||||
|
const res = await $http({
|
||||||
|
url: `/ocpp-mtapp-api/api/v1/users/${params.name}`,
|
||||||
|
method: "GET",
|
||||||
|
data: body
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
32
api/responseData.uts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
// 注册用户信息
|
||||||
|
export type RegisteredUserResponse = {
|
||||||
|
result: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录应答
|
||||||
|
export type LoginResponse = {
|
||||||
|
refresh_token: string
|
||||||
|
token_type: string
|
||||||
|
username: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改用户信息应答
|
||||||
|
export type ModifyUserInfoResponse = {
|
||||||
|
userName: string | null,
|
||||||
|
userPassword: string | null,
|
||||||
|
salt: string | null,
|
||||||
|
sessionKey: string | null,
|
||||||
|
nickName: string | null,
|
||||||
|
avatarUrl: string | null,
|
||||||
|
gender: string | null,
|
||||||
|
country: string | null,
|
||||||
|
province: string | null,
|
||||||
|
city: string | null,
|
||||||
|
language: string | null,
|
||||||
|
created: string | null,
|
||||||
|
timestamp: string | null,
|
||||||
|
email: string | null,
|
||||||
|
phone: string | null
|
||||||
|
}
|
||||||
164
common/auth.uts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
let recordToken: string | null = null
|
||||||
|
let recordUserName: string | null = null
|
||||||
|
let recordCharging: string | null = null
|
||||||
|
|
||||||
|
// 保存 token(内存 + 本地)
|
||||||
|
export function setToken(token: string) {
|
||||||
|
recordToken = token
|
||||||
|
uni.setStorageSync('loginToken', token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 token
|
||||||
|
export function getToken(): string | null {
|
||||||
|
if (recordToken != null) {
|
||||||
|
return recordToken
|
||||||
|
}
|
||||||
|
const t = uni.getStorageSync('loginToken') as string | null
|
||||||
|
recordToken = t ?? null
|
||||||
|
return recordToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录时清掉
|
||||||
|
export function clearAllData() {
|
||||||
|
recordToken = null
|
||||||
|
recordUserName = null
|
||||||
|
recordCharging = null
|
||||||
|
uni.removeStorageSync('loginToken')
|
||||||
|
uni.removeStorageSync('loginUserName')
|
||||||
|
uni.removeStorageSync('ChargingFlag')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkToken(): boolean {
|
||||||
|
const t = getToken()
|
||||||
|
if(t == null || (t.length) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 保存用户名
|
||||||
|
export function setUserName(userName: string) {
|
||||||
|
recordUserName = userName
|
||||||
|
uni.setStorageSync('loginUserName', userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取用户名
|
||||||
|
export function getUserName(): string | null {
|
||||||
|
if (recordUserName != null) {
|
||||||
|
return recordUserName
|
||||||
|
}
|
||||||
|
const t = uni.getStorageSync('loginUserName') as string | null
|
||||||
|
recordUserName = t ?? null
|
||||||
|
return recordUserName
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 保存充电标记
|
||||||
|
export function setCharging(flag: string) {
|
||||||
|
recordCharging = flag
|
||||||
|
uni.setStorageSync('ChargingFlag', flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取充电标记
|
||||||
|
export function getCharging(): string | null {
|
||||||
|
if (recordCharging != null) {
|
||||||
|
return recordCharging
|
||||||
|
}
|
||||||
|
const t = uni.getStorageSync('ChargingFlag') as string | null
|
||||||
|
recordCharging = t ?? null
|
||||||
|
return recordCharging
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除充电标记
|
||||||
|
export function clearCharging() {
|
||||||
|
recordCharging = null
|
||||||
|
uni.removeStorageSync('ChargingFlag')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存登录账号密码(记住密码)
|
||||||
|
export function setSavedAccount(account: string, password: string) {
|
||||||
|
uni.setStorageSync('savedAccount', account)
|
||||||
|
uni.setStorageSync('savedPassword', password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取保存的账号
|
||||||
|
export function getSavedAccount(): string {
|
||||||
|
const t = uni.getStorageSync('savedAccount') as string | null
|
||||||
|
return t ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取保存的密码
|
||||||
|
export function getSavedPassword(): string {
|
||||||
|
const t = uni.getStorageSync('savedPassword') as string | null
|
||||||
|
return t ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除保存的账号密码
|
||||||
|
export function clearSavedAccount() {
|
||||||
|
uni.removeStorageSync('savedAccount')
|
||||||
|
uni.removeStorageSync('savedPassword')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 历史账号记录
|
||||||
|
export type AccountRecord = {
|
||||||
|
account: string,
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACCOUNT_HISTORY_MAX = 5
|
||||||
|
|
||||||
|
// 读取历史账号(最新登录的在前)
|
||||||
|
export function getAccountHistory(): AccountRecord[] {
|
||||||
|
const result: AccountRecord[] = []
|
||||||
|
const raw = uni.getStorageSync('accountHistory') as string | null
|
||||||
|
if (raw == null || raw.length == 0) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(raw) as UTSJSONObject[] | null
|
||||||
|
if (arr == null) return result
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
const obj = arr[i]
|
||||||
|
const acc = obj["account"] as string | null
|
||||||
|
const pwd = obj["password"] as string | null
|
||||||
|
if (acc != null && pwd != null) {
|
||||||
|
const rec: AccountRecord = { account: acc, password: pwd }
|
||||||
|
result.push(rec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log('parse accountHistory error = ', e)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存一条历史账号(同账号去重置顶,最多保留 5 条)
|
||||||
|
export function addAccountHistory(account: string, password: string) {
|
||||||
|
const old = getAccountHistory()
|
||||||
|
const list: AccountRecord[] = []
|
||||||
|
const first: AccountRecord = { account: account, password: password }
|
||||||
|
list.push(first)
|
||||||
|
for (let i = 0; i < old.length && list.length < ACCOUNT_HISTORY_MAX; i++) {
|
||||||
|
const item = old[i]
|
||||||
|
if (item.account != account) {
|
||||||
|
list.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uni.setStorageSync('accountHistory', JSON.stringify(list))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除指定账号的历史记录
|
||||||
|
export function removeAccountHistory(account: string) {
|
||||||
|
const old = getAccountHistory()
|
||||||
|
const list: AccountRecord[] = []
|
||||||
|
for (let i = 0; i < old.length; i++) {
|
||||||
|
const item = old[i]
|
||||||
|
if (item.account != account) {
|
||||||
|
list.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uni.setStorageSync('accountHistory', JSON.stringify(list))
|
||||||
|
}
|
||||||
|
|
||||||
41
common/bleInfo.uts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
let bleName: string | null = null
|
||||||
|
let bleMac: string | null = null
|
||||||
|
|
||||||
|
// 保存蓝牙名字
|
||||||
|
export function setName(name: string) {
|
||||||
|
bleName = name
|
||||||
|
uni.setStorageSync('bleName', name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取蓝牙名字
|
||||||
|
export function getName(): string | null {
|
||||||
|
if (bleName != null) {
|
||||||
|
return bleName
|
||||||
|
}
|
||||||
|
const t = uni.getStorageSync('bleName') as string | null
|
||||||
|
bleName = t ?? null
|
||||||
|
return bleName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存蓝牙mac
|
||||||
|
export function setMac(mac: string) {
|
||||||
|
bleMac = mac
|
||||||
|
uni.setStorageSync('bleMac', mac)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取蓝牙mac
|
||||||
|
export function getMac(): string | null {
|
||||||
|
if (bleMac != null) {
|
||||||
|
return bleMac
|
||||||
|
}
|
||||||
|
const t = uni.getStorageSync('bleMac') as string | null
|
||||||
|
bleMac = t ?? null
|
||||||
|
return bleMac
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAllData() {
|
||||||
|
bleName = null
|
||||||
|
bleMac = null
|
||||||
|
uni.removeStorageSync('bleName')
|
||||||
|
uni.removeStorageSync('bleMac')
|
||||||
|
}
|
||||||
52
docs/修改记录_2026-07-17_服务器切换与记住密码.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# 修改记录:后台服务器切换 + 登录记住密码
|
||||||
|
|
||||||
|
日期:2026-07-17
|
||||||
|
|
||||||
|
本次未提交的修改主要包含两块功能:**我的页面可切换后台服务器**、**登录页记住密码**,另附登录页图标颜色调整。
|
||||||
|
|
||||||
|
## 1. 后台服务器动态切换
|
||||||
|
|
||||||
|
### `api/httpInstance.uts`
|
||||||
|
|
||||||
|
- 原固定常量 `BASE_URL` 移除,改为可配置的服务器列表 `SERVER_LIST`:
|
||||||
|
- 正式 ssl(evmtapp.clenergy.com:20096,默认)
|
||||||
|
- 正式(47.107.245.120:30081)
|
||||||
|
- 测试 ssl(miniapp.test.springecloud.com:20095)
|
||||||
|
- 测试(192.168.13.243:30081)
|
||||||
|
- 新增 `getBaseUrl()`:按「内存缓存 → 本地存储(`serverBaseUrl`)→ 列表第一项」的顺序取当前服务器地址。
|
||||||
|
- 新增 `setBaseUrl(url)`:同时写入内存和本地存储,下次启动直接生效。
|
||||||
|
- `$http()` 请求地址由 `BASE_URL + params.url` 改为 `getBaseUrl() + params.url`。
|
||||||
|
|
||||||
|
### `pages/tabbar/my.uvue`
|
||||||
|
|
||||||
|
- 「我的」页面新增「后台服务器」卡片,显示当前服务器名称。
|
||||||
|
- 点击弹出 ActionSheet 选择服务器,选择后调用 `setBaseUrl()` 保存并 Toast 提示。
|
||||||
|
- `onShow` 中调用 `initServerList()` 回显当前选择。
|
||||||
|
|
||||||
|
## 2. 登录页记住密码
|
||||||
|
|
||||||
|
### `common/auth.uts`
|
||||||
|
|
||||||
|
新增 4 个本地存储方法:
|
||||||
|
|
||||||
|
- `setSavedAccount(account, password)`:保存账号密码(`savedAccount` / `savedPassword`)。
|
||||||
|
- `getSavedAccount()` / `getSavedPassword()`:读取,无则返回空串。
|
||||||
|
- `clearSavedAccount()`:清除保存的账号密码。
|
||||||
|
|
||||||
|
### `pages/login/login.uvue`
|
||||||
|
|
||||||
|
- 密码登录表单下方新增「remember password」勾选行(`rememberPwd`)。
|
||||||
|
- `onLoad` 时若本地已保存密码,则自动勾选并回填账号、密码。
|
||||||
|
- 登录成功后:勾选则 `setSavedAccount()` 保存,未勾选则 `clearSavedAccount()` 清除。
|
||||||
|
- 顺带调整:输入框及眼睛图标的 `uni-icons` 统一加上 `color="#3A6FA1"`,并新增 `.remember-row` / `.remember-text` 样式。
|
||||||
|
|
||||||
|
## 3. 其他新增文件(未跟踪)
|
||||||
|
|
||||||
|
- `static/androidApp22.png`
|
||||||
|
- `static/miniApp22.png`
|
||||||
|
- `uni_modules/android-ble/utssdk/mp-weixin.zip`
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 密码以**明文**存储在本地 storage(`savedPassword`),仅适合内部测试用途;如需对外发布,建议改为加密存储或仅保存账号。
|
||||||
|
- 服务器地址本地存储 key 为 `serverBaseUrl`,清除应用数据后会恢复默认(列表第一项)。
|
||||||
31
docs/修改记录_2026-07-17_登录页历史账号选择.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# 修改记录:登录页历史账号选择
|
||||||
|
|
||||||
|
日期:2026-07-17
|
||||||
|
|
||||||
|
本次修改在登录页新增**历史账号记录**功能:登录成功的账号密码都会保存到本地历史列表,下次登录时可直接选择回填,无需再次输入。
|
||||||
|
|
||||||
|
## 1. 历史账号存储层
|
||||||
|
|
||||||
|
### `common/auth.uts`
|
||||||
|
|
||||||
|
新增 `AccountRecord` 类型(`account` + `password`)及 3 个方法:
|
||||||
|
|
||||||
|
- `getAccountHistory()`:读取历史账号列表(最新登录的在前)。数据以 JSON 字符串存于本地存储 key `accountHistory`,解析失败时安全返回空数组。
|
||||||
|
- `addAccountHistory(account, password)`:保存一条历史记录,同账号去重并置顶,最多保留 5 条(`ACCOUNT_HISTORY_MAX`)。
|
||||||
|
- `removeAccountHistory(account)`:删除指定账号的历史记录。
|
||||||
|
|
||||||
|
## 2. 登录页 UI 与逻辑
|
||||||
|
|
||||||
|
### `pages/login/login.uvue`
|
||||||
|
|
||||||
|
- 密码登录表单「remember password」下方新增「saved accounts (n)」折叠行,点击展开/收起历史账号列表(`historyOpen`);无历史记录时整个入口不显示。
|
||||||
|
- 点击列表中某条账号:自动回填账号 + 密码,自动勾选「remember password」,并收起列表(`onPickHistory`)。
|
||||||
|
- 每条记录右侧「✕」可单独删除该条历史(`onRemoveHistory`,`@tap.stop` 阻止触发选中)。
|
||||||
|
- `onLoad` 时调用 `getAccountHistory()` 加载历史列表(`historyList`)。
|
||||||
|
- 密码登录成功后无条件调用 `addAccountHistory()` 写入历史;原有单账号「记住密码」(`savedAccount` / `savedPassword`)逻辑保持不变。
|
||||||
|
- 新增样式:`.history-row` / `.history-text` / `.history-list` / `.history-item` / `.history-account` / `.history-del`。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 历史账号的密码以**明文**存储在本地 storage(`accountHistory`),与已有 `savedPassword` 相同,仅适合内部测试用途;如需对外发布,建议改为加密存储或仅保存账号。
|
||||||
|
- 历史列表上限 5 条,超出时最旧的记录被移除;清除应用数据后历史记录丢失。
|
||||||
77
docs/修改记录_2026-09-02_母乳杯蓝牙改造.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# 修改记录 2026-09-02:母乳杯(CAIIC-MCM)蓝牙改造
|
||||||
|
|
||||||
|
将原充电桩 App 改造为母乳杯设备 App:通过蓝牙显示设备参数(温度、风扇转速)、
|
||||||
|
查询设备系统运行状态、本地固件文件经 BLE OTA 升级设备。
|
||||||
|
设备协议依据 `D:\caic-workspace\mothercup\mcm-ddc-04\docs\ble_protocol.md`(CAIIC BLE 通信协议 V1.0)。
|
||||||
|
|
||||||
|
## 协议要点(设备侧已实现)
|
||||||
|
|
||||||
|
- GATT:Service `0000CA10-CA11-4B11-8000-CA11CA11CA11`,下行写特征 `...CA11...`
|
||||||
|
(**仅 Write Without Response**),上行 notify `...CA12...`;广播名 `CAIIC-MCM`;建议 MTU=247。
|
||||||
|
- 二进制帧(小端):`[0xCA, TYPE, SEQ, LEN(u16), PAYLOAD, CRC16-CCITT(0x1021/初值0xFFFF)]`,
|
||||||
|
开销 7 字节,payload ≤480;一帧可跨多次 BLE 写入,设备按字节流重组。
|
||||||
|
- 帧类型:0x01 CLI_REQ / 0x02 CLI_RSP / 0x03 CLI_RSP_END;0x10 OTA_BEGIN /
|
||||||
|
0x11 OTA_DATA / 0x12 OTA_END / 0x1F OTA_RSP。
|
||||||
|
- OTA:双 bank 直写,设备 4KB 扇区缓冲,满扇区回 OTA_RSP(0x11) 作流控;status=2 按应答
|
||||||
|
offset 重发重同步;OTA_END 整镜像 CRC32(zlib/IEEE)校验通过后设备 200ms 自动复位切 bank。
|
||||||
|
|
||||||
|
## 新增文件
|
||||||
|
|
||||||
|
- `src/mcm/Crc.uts` — CRC16-CCITT 与 CRC32-IEEE(zlib)实现。注意 uts number 位运算按
|
||||||
|
32 位有符号处理,CRC32 中间值保持在 Int 范围、无符号右移用 `(x>>1)&0x7FFFFFFF`,
|
||||||
|
多项式用补码字面量 `-306674912`(0xEDB88320)。
|
||||||
|
- `src/mcm/FrameCodec.uts` — 帧常量、`encodeFrame` 组帧、`FrameDecoder` 字节流重组
|
||||||
|
(找 SOF → 头 → payload+CRC,错帧静默丢弃重找)、`pushU32LE/readU32LE`(纯算术实现,
|
||||||
|
避免大数值位运算符号问题)。
|
||||||
|
- `src/mcm/McmChannel.uts` — 连接级单例通道:`attach()` 挂全局 `bleData/bleState` 事件;
|
||||||
|
`sendFrame` 写队列串行化、整帧按 (att_mtu-3) 切片、写类型 WRITE_NO_RESPONSE;
|
||||||
|
`sendCli` CLI 命令透传串行执行(聚合 CLI_RSP 至 CLI_RSP_END,超时/断开兜底);
|
||||||
|
OTA_RSP 转发给 OtaManager。所有 Promise 只 resolve 不 reject。
|
||||||
|
- `src/mcm/McmCommands.uts` — 业务命令:version/sysinfo/bankinfo/temp/fan,
|
||||||
|
命令名集中在顶部常量。**温度/风扇命令(默认 temp/fan)固件尚未实现,解析失败页面显示 `--`**。
|
||||||
|
- `src/mcm/OtaManager.uts` — OTA 状态机:BEGIN→按 4KB 扇区发 DATA 等 ack(流控/重同步)→END;
|
||||||
|
断连或 `abort()` 协作式中止;进度/状态回调。
|
||||||
|
- `pages/tabbar/sysStatus.uvue` — 系统状态页:version/bankinfo 概要 + sysinfo 原文控制台
|
||||||
|
(运行时间/任务数/heap/任务表),下拉刷新。
|
||||||
|
- `pages/tabbar/ota.uvue` — 固件升级页:手动输入本地 bin 路径(默认
|
||||||
|
`/sdcard/Download/embeddedSrc.bin`),UTS 直调 Android `java.io.File/FileInputStream`
|
||||||
|
读文件(`#ifdef APP-ANDROID`);显示大小与 CRC32;输入目标版本号(如 1.00.02→0x010002);
|
||||||
|
进度条 + 日志;升级期间禁止断连/退出,离开页面自动中止。
|
||||||
|
|
||||||
|
## 修改文件
|
||||||
|
|
||||||
|
- `src/BluetoothManager.uts` — 新增 `sendDataBytes(data, writeType, cb)`(显式写类型,
|
||||||
|
设备下行特征必须 writeType=1)与 `selectService(uuidPrefix)`(按服务 UUID 前缀
|
||||||
|
`0000CA10` 选择 service+notify+write 组合,规避自动识别选错)。
|
||||||
|
- `App.uvue` — `ConnectBle()` 在 scanServices 后先 `selectService("0000CA10")` 再订阅 notify;
|
||||||
|
`onLaunch` 调 `McmChannel.attach()`。
|
||||||
|
- `pages/tabbar/home.uvue` — 重写为设备主页:扫描(过滤 CAIIC-MCM)/连接/断开;
|
||||||
|
连接后温度、风扇转速每 2s 轮询显示,固件版本与运行 Bank 展示;notify 订阅成功
|
||||||
|
(bleData type=1000)后协商 MTU=247。
|
||||||
|
- `pages/tabbar/my.uvue` — 删除充电/离线行为入口(原已注释),启用"CLI 调试终端"入口。
|
||||||
|
- `pages/bleTest/bluetoothOperation.uvue` — 重写为 CLI 调试终端:inline 命令输入 +
|
||||||
|
应答文本流展示,复用全局 ConnectBle 连接流程。
|
||||||
|
- `pages.json` — tabbar 改为 设备/状态/升级/my 四 tab(图标复用 static/tabbar 现有资源);
|
||||||
|
移除 charge/order 及全部充电页面注册。
|
||||||
|
- `manifest.json` — 应用名改为"母乳杯助手";iOS 蓝牙用途文案改为母乳杯描述。
|
||||||
|
- `api/requestApi.uts` — 仅保留用户类接口(登录/注册/pinCode/修改资料),删除全部
|
||||||
|
OCPP 充电与蓝牙绑定分享接口。
|
||||||
|
- `api/responseData.uts` — 仅保留用户类响应类型。
|
||||||
|
|
||||||
|
## 删除
|
||||||
|
|
||||||
|
- 目录:`pages/startCharge/`、`pages/stopCharge/`、`pages/orderDetails/`、
|
||||||
|
`pages/offlineBehavior/`、`pages/tabbar/charge.uvue`、`pages/tabbar/order.uvue`、
|
||||||
|
`src/espBlufi/`(充电桩 JSON 分片协议,与 CAIIC 二进制帧协议不兼容)。
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
|
||||||
|
- **固件需新增温度/风扇 CLI 命令**(设备仓库 `D:\caic-workspace\mothercup`,
|
||||||
|
在 `cli_core.c` 命令表注册;当前 APP 侧默认命令名 `temp`/`fan`,定名不同改
|
||||||
|
`McmCommands.uts` 顶部常量即可)。
|
||||||
|
- **云端固件接口未接入**:本期为本地 bin 文件升级;`api/httpInstance.uts`($http/token/
|
||||||
|
服务器切换)保留,后续按约定接口(查询最新版本 + 下载 URL)接入 ota 页。
|
||||||
|
- uni-app x 无 CLI 构建手段,需在 HBuilderX 编译并真机(Android)验证:
|
||||||
|
version/sysinfo/bankinfo 显示、temp/fan(待固件)、本地 bin OTA 后 bankinfo 确认切 bank。
|
||||||
|
- OTA 大数据量发送走 Write Without Response + 扇区级 ack 流控,若实测丢包导致
|
||||||
|
status=2 重同步频繁,可在 OtaManager 扇区内加小窗口节流。
|
||||||
20
index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/main"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9
main.uts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import App from './App.uvue'
|
||||||
|
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
return {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
97
manifest.json
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"name": "smartassister",
|
||||||
|
"appid": "__UNI__884AC41",
|
||||||
|
"description": "",
|
||||||
|
"versionName": "1.0.1",
|
||||||
|
"versionCode": "110",
|
||||||
|
"uni-app-x": {},
|
||||||
|
/* 快应用特有相关 */
|
||||||
|
"quickapp": {},
|
||||||
|
/* 小程序特有相关 */
|
||||||
|
"mp-weixin": {
|
||||||
|
"appid": "wx50ffc3eab938ff4c",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": true,
|
||||||
|
"minified": true
|
||||||
|
},
|
||||||
|
"usingComponents": true,
|
||||||
|
"permission": {}
|
||||||
|
},
|
||||||
|
"mp-alipay": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"mp-baidu": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"mp-toutiao": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"uniStatistics": {
|
||||||
|
"enable": false
|
||||||
|
},
|
||||||
|
"vueVersion": "3",
|
||||||
|
"app": {
|
||||||
|
"distribute": {
|
||||||
|
"icons": {
|
||||||
|
"android": {
|
||||||
|
"hdpi": "",
|
||||||
|
"xhdpi": "",
|
||||||
|
"xxhdpi": "",
|
||||||
|
"xxxhdpi": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app-android": {
|
||||||
|
"distribute": {
|
||||||
|
"modules": {},
|
||||||
|
"icons": {
|
||||||
|
"hdpi": "/static/androidApp.png",
|
||||||
|
"xhdpi": "/static/androidApp.png",
|
||||||
|
"xxhdpi": "/static/androidApp.png",
|
||||||
|
"xxxhdpi": "/static/androidApp.png"
|
||||||
|
},
|
||||||
|
"splashScreens": {
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app-ios": {
|
||||||
|
"distribute": {
|
||||||
|
"modules": {
|
||||||
|
"uni-location": {},
|
||||||
|
"uni-barcode-scanning": {}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"appstore": "/static/androidApp.png"
|
||||||
|
},
|
||||||
|
"splashScreens": {},
|
||||||
|
"privacyDescription": {
|
||||||
|
"NSBluetoothPeripheralUsageDescription": "请允许使用蓝牙功能。该功能用于:连接母乳杯设备,显示温度、风扇转速等运行参数,查询设备系统状态,并通过蓝牙为设备升级固件。",
|
||||||
|
"NSBluetoothAlwaysUsageDescription": "请允许使用蓝牙功能。该功能用于:连接母乳杯设备,显示温度、风扇转速等运行参数,查询设备系统状态,并通过蓝牙为设备升级固件。",
|
||||||
|
"NSLocationWhenInUseUsageDescription": "Please allow the use of location services to identify the position of charging stations. This app promises that this function will not be used for any other purposes.",
|
||||||
|
"NSLocalNetworkUsageDescription": "Please allow the use of network connectivity for communication between the app and the cloud service backend to enable charging-related features. This app promises that it will only be used to complete charging-related services with the cloud backend and will not be utilized for any other purposes.",
|
||||||
|
"NSCameraUsageDescription": "Please allow the use of the camera function. This function is solely used for scanning the QR codes on charging piles to obtain information corresponding to the QR codes. This app promises that the function will not be used for any other purposes."
|
||||||
|
},
|
||||||
|
"urltypes": "",
|
||||||
|
"capabilities": {
|
||||||
|
"entitlements": {
|
||||||
|
"com.apple.developer.associated-domains": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"urlschemewhitelist": "",
|
||||||
|
"UIBackgroundModes": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"router": {
|
||||||
|
"mode": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
pages.json
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"pages": [ //pages数组中第一项表示应用启动页,参考:https://doc.dcloud.net.cn/uni-app-x/collocation/pagesjson.html
|
||||||
|
{
|
||||||
|
"path": "pages/tabbar/home",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "母乳杯设备",
|
||||||
|
"navigationBarBackgroundColor": "#3A6FA1",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/tabbar/sysStatus",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "系统状态",
|
||||||
|
"navigationBarBackgroundColor": "#3A6FA1",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/tabbar/ota",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "固件升级",
|
||||||
|
"navigationBarBackgroundColor": "#3A6FA1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/tabbar/my",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "my",
|
||||||
|
"navigationBarBackgroundColor": "#3A6FA1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/register/register",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "register",
|
||||||
|
"navigationStyle": "default",
|
||||||
|
"navigationBarBackgroundColor": "#3A6FA1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/login",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "login",
|
||||||
|
"navigationStyle": "default",
|
||||||
|
"navigationBarBackgroundColor": "#3A6FA1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/displayUserInfo/displayUserInfo",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "user info",
|
||||||
|
"navigationBarBackgroundColor": "#3A6FA1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/bleTest/bluetoothList/bluetoothList",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "bluetooth list",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/bleTest/bluetoothOperation/bluetoothOperation",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "CLI 调试终端"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#999999",
|
||||||
|
"selectedColor": "#3A6FA1",
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"pagePath": "pages/tabbar/home",
|
||||||
|
"text": "设备",
|
||||||
|
"iconPath": "/static/tabbar/homeDefault.png",
|
||||||
|
"selectedIconPath": "/static/tabbar/homeActive.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/tabbar/sysStatus",
|
||||||
|
"text": "状态",
|
||||||
|
"iconPath": "/static/tabbar/mapDefault.png",
|
||||||
|
"selectedIconPath": "/static/tabbar/mapActive.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/tabbar/ota",
|
||||||
|
"text": "升级",
|
||||||
|
"iconPath": "/static/tabbar/chargeDefault.png",
|
||||||
|
"selectedIconPath": "/static/tabbar/chargeActive.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/tabbar/my",
|
||||||
|
"text": "my",
|
||||||
|
"iconPath": "/static/tabbar/myDefault.png",
|
||||||
|
"selectedIconPath": "/static/tabbar/myActive.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "uni-app x",
|
||||||
|
"navigationBarBackgroundColor": "#F8F8F8",
|
||||||
|
"backgroundColor": "#F8F8F8",
|
||||||
|
"navigationStyle": "default"
|
||||||
|
},
|
||||||
|
"uniIdRouter": {}
|
||||||
|
}
|
||||||
142
pages/bleTest/bluetoothList/bluetoothList.uvue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
<scroll-view style="flex:1">
|
||||||
|
<!-- #endif -->
|
||||||
|
<view class="page">
|
||||||
|
<view v-for="(item, index) in scanArray" :key="index" class="card">
|
||||||
|
<view class="cardBox" @tap="onLinkBluetooth(item)">
|
||||||
|
<view class="leftIcon">
|
||||||
|
<uni-icons type="paperclip" size="20"/>
|
||||||
|
</view>
|
||||||
|
<view class="rightContent">
|
||||||
|
<text>{{ item.device.name }}</text>
|
||||||
|
<text>{{ item.device.address }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
</scroll-view>
|
||||||
|
<!-- #endif -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import {BleLib,MyApiResult,BleScanResult,BleServices,ScanRssiBR,WNuuid} from "@/uni_modules/android-ble"
|
||||||
|
import * as Ble from "../../../src/BluetoothManager"
|
||||||
|
|
||||||
|
const scanArray = ref<BleScanResult[]>([]) // 蓝牙扫描结果
|
||||||
|
const refreshing = ref<boolean>(false)
|
||||||
|
|
||||||
|
// 连接蓝牙
|
||||||
|
function onLinkBluetooth(item: BleScanResult) {
|
||||||
|
console.log(item.device.name)
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/bleTest/bluetoothOperation/bluetoothOperation?address=${encodeURIComponent(item.device.address)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBleStarScan() {
|
||||||
|
console.log("list")
|
||||||
|
if (refreshing.value) return
|
||||||
|
refreshing.value = true
|
||||||
|
|
||||||
|
const scanMs = 10*1000
|
||||||
|
scanArray.value = []
|
||||||
|
Ble.startScan(scanMs,function(res:MyApiResult){
|
||||||
|
// type 为0 扫描成功 10001 表示无蓝牙权限 10002 表示蓝牙未开启
|
||||||
|
// console.log(res.type)
|
||||||
|
if(res.type==0){
|
||||||
|
var scan = res.data as BleScanResult;
|
||||||
|
scanArray.value.push(scan);
|
||||||
|
}else {
|
||||||
|
// 处理扫描失败的情况(如没有蓝牙权限或蓝牙未开启)
|
||||||
|
if (res.type == 10003) {
|
||||||
|
uni.showToast({
|
||||||
|
title: 'No Bluetooth permission',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
} else if (res.type == 10002) {
|
||||||
|
uni.showToast({
|
||||||
|
title: 'Bluetooth is not turned on',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 只控制“动画”,不影响扫描继续
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshing.value = false
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}, scanMs + 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======= 生命周期 ========
|
||||||
|
onLoad((options: UTSJSONObject | null) => {
|
||||||
|
if (options != null) {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onPageShow(() => {
|
||||||
|
onBleStarScan()
|
||||||
|
})
|
||||||
|
|
||||||
|
onPageHide(() => {
|
||||||
|
Ble.stopScan()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
console.log("stop")
|
||||||
|
Ble.stopScan()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
onPullDownRefresh(() => {
|
||||||
|
onBleStarScan()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 20rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
/* 阴影效果 */
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
/* 横向布局:左图右文 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardBox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftIcon {
|
||||||
|
width: 28px; /* 左侧固定宽度,避免挤压右侧 */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightContent {
|
||||||
|
flex: 1; /* 右侧撑满剩余宽度 */
|
||||||
|
min-width: 0; /* 很关键:让 text-overflow 生效 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.displyType {
|
||||||
|
color: #0000ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
321
pages/bleTest/bluetoothOperation/bluetoothOperation.uvue
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<!-- 消息列表(可上下滑动) -->
|
||||||
|
<scroll-view class="msgList" scroll-y="true" :scroll-top="scrollTop">
|
||||||
|
<view v-for="(it, idx) in msgList" :key="idx" class="msgCard" :class="it.dir">
|
||||||
|
<view class="msgHeader">
|
||||||
|
<text v-if="it.dir == 'tx'" class="msgTag">{{ 'TX' }}</text>
|
||||||
|
<text v-else-if="it.dir == 'rx'" class="msgTag">{{ 'RX' }}</text>
|
||||||
|
<text v-else class="msgTag">{{ '' }}</text>
|
||||||
|
<text class="msgTime">{{ it.time }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="msgBody">{{ it.text }}</text>
|
||||||
|
</view>
|
||||||
|
<view style="height: 20px;"></view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 底部:命令输入 + 按钮 -->
|
||||||
|
<view class="bottomPanel">
|
||||||
|
<view class="cmdRow">
|
||||||
|
<input class="cmdInput" v-model="cmdText" placeholder="输入 CLI 命令,如 help / sysinfo" :disabled="!bleIsConnect" />
|
||||||
|
<button type="primary" size="mini" class="sendBtn" :disabled="!bleIsConnect || sending" @tap="onSendCmd">{{ sending ? '...' : 'send' }}</button>
|
||||||
|
</view>
|
||||||
|
<view class="btnRow">
|
||||||
|
<button
|
||||||
|
type="warn"
|
||||||
|
@click="onConnect"
|
||||||
|
:disabled="bleIsConnect"
|
||||||
|
class="btnType"
|
||||||
|
>connect</button>
|
||||||
|
<button
|
||||||
|
type="warn"
|
||||||
|
@click="onDisConnect"
|
||||||
|
:disabled="!bleIsConnect"
|
||||||
|
class="btnType"
|
||||||
|
>disconnect</button>
|
||||||
|
<button
|
||||||
|
type="primary"
|
||||||
|
@click="onGetStatus"
|
||||||
|
:disabled="!bleIsConnect"
|
||||||
|
class="btnType"
|
||||||
|
>rssi</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { MyApiResult } from "@/uni_modules/android-ble"
|
||||||
|
import * as Ble from "../../../src/BluetoothManager"
|
||||||
|
import * as McmChannel from "../../../src/mcm/McmChannel.uts"
|
||||||
|
import * as McmCmd from "../../../src/mcm/McmCommands.uts"
|
||||||
|
import { CliResult } from "../../../src/mcm/McmChannel.uts"
|
||||||
|
|
||||||
|
const bleAddress = ref('')
|
||||||
|
const bleIsConnect = ref(false) // 蓝牙连接标志 true-已连接 false-未连接
|
||||||
|
const cmdText = ref("")
|
||||||
|
const sending = ref(false)
|
||||||
|
|
||||||
|
type MsgItem = {
|
||||||
|
dir: string
|
||||||
|
text: string
|
||||||
|
time: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgList = ref<Array<MsgItem>>([] as Array<MsgItem>)
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
|
||||||
|
// ===========================功能函数
|
||||||
|
function nowStr(): string {
|
||||||
|
const d = new Date()
|
||||||
|
const hh = d.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = d.getMinutes().toString().padStart(2, '0')
|
||||||
|
const ss = d.getSeconds().toString().padStart(2, '0')
|
||||||
|
return `${hh}:${mm}:${ss}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushMsg(dir: string, text: string) {
|
||||||
|
const item: MsgItem = {
|
||||||
|
dir: dir,
|
||||||
|
text: text,
|
||||||
|
time: nowStr()
|
||||||
|
}
|
||||||
|
msgList.value.push(item)
|
||||||
|
scrollTop.value = scrollTop.value + 9999
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================蓝牙事件
|
||||||
|
function onBleDataListener(res: MyApiResult) {
|
||||||
|
if (res.type == 1000) {
|
||||||
|
// notify 订阅成功:协商 MTU 247
|
||||||
|
Ble.setMtu(247, (mtuRes: MyApiResult) => {
|
||||||
|
if (mtuRes.type == 0) {
|
||||||
|
McmChannel.setMtuSize(247)
|
||||||
|
pushMsg("message", "set MTU 247")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
bleIsConnect.value = true
|
||||||
|
pushMsg("message", "notify subscribed, ready")
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBleStatueListener(state: number) {
|
||||||
|
if (state == 10000) {
|
||||||
|
pushMsg("message", "connect failed")
|
||||||
|
bleIsConnect.value = false
|
||||||
|
uni.hideLoading()
|
||||||
|
} else if (state == 10001) {
|
||||||
|
pushMsg("message", "disconnected")
|
||||||
|
bleIsConnect.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================蓝牙操作
|
||||||
|
function onDisConnect() {
|
||||||
|
if (Ble.isConnected()) {
|
||||||
|
Ble.disConnect()
|
||||||
|
}
|
||||||
|
bleIsConnect.value = false
|
||||||
|
McmChannel.resetChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接蓝牙(走全局 ConnectBle:connect → scanServices → 选中 CA10 服务 → 订阅 notify)
|
||||||
|
function onConnect() {
|
||||||
|
if (Ble.isConnected()) {
|
||||||
|
uni.hideLoading()
|
||||||
|
bleIsConnect.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({
|
||||||
|
title: "Connecting...",
|
||||||
|
mask: true
|
||||||
|
})
|
||||||
|
pushMsg("start", "connect: " + bleAddress.value)
|
||||||
|
getApp().globalData.ConnectBle(bleAddress.value, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送 CLI 命令
|
||||||
|
async function onSendCmd() {
|
||||||
|
const cmd = cmdText.value.trim()
|
||||||
|
if (cmd.length == 0) return
|
||||||
|
sending.value = true
|
||||||
|
pushMsg("tx", cmd)
|
||||||
|
cmdText.value = ""
|
||||||
|
const r: CliResult = await McmCmd.execCli(cmd, 8000)
|
||||||
|
sending.value = false
|
||||||
|
if (r.status == 0) {
|
||||||
|
pushMsg("rx", r.text.length > 0 ? r.text : "(ok)")
|
||||||
|
} else if (r.status == 1) {
|
||||||
|
pushMsg("message", "未知命令或行超长")
|
||||||
|
} else {
|
||||||
|
pushMsg("message", "命令失败 status=" + r.status + (r.text.length > 0 ? (", 已收: " + r.text) : ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGetStatus() {
|
||||||
|
Ble.readRssi(function(res: MyApiResult) {
|
||||||
|
pushMsg("message", res.message + ": " + res.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======= 生命周期 ========
|
||||||
|
let subscribed = false
|
||||||
|
function subscribe() {
|
||||||
|
if (subscribed) return
|
||||||
|
uni.$on("bleData", onBleDataListener)
|
||||||
|
uni.$on("bleState", onBleStatueListener)
|
||||||
|
subscribed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsubscribe() {
|
||||||
|
if (!subscribed) return
|
||||||
|
uni.$off("bleData", onBleDataListener)
|
||||||
|
uni.$off("bleState", onBleStatueListener)
|
||||||
|
subscribed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((options: UTSJSONObject | null) => {
|
||||||
|
if (options != null) {
|
||||||
|
const address = options["address"] as string | null
|
||||||
|
if (address != null) {
|
||||||
|
bleAddress.value = address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subscribe()
|
||||||
|
// 已连接(如从设备页过来)则直接进入可用状态
|
||||||
|
if (Ble.isConnected()) {
|
||||||
|
bleIsConnect.value = true
|
||||||
|
pushMsg("message", "already connected: " + Ble.getConnectMac())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onPageShow(() => {
|
||||||
|
subscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onPageHide(() => {
|
||||||
|
unsubscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
unsubscribe()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部面板 */
|
||||||
|
.bottomPanel {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 0 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmdRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmdInput {
|
||||||
|
flex: 1;
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sendBtn {
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnType {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 消息列表占满剩余高度 */
|
||||||
|
.msgList {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 120px;
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.msgCard {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msgCard.tx {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-color: #3c9cff;
|
||||||
|
}
|
||||||
|
.msgCard.rx {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-color: #67c23a;
|
||||||
|
}
|
||||||
|
.msgCard.message {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-color: #7e50c2;
|
||||||
|
}
|
||||||
|
.msgCard.start {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-color: #ff5500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msgHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msgTag {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msgTime {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msgBody {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
pages/displayUserInfo/displayUserInfo.uvue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
<scroll-view style="flex:1">
|
||||||
|
<!-- #endif -->
|
||||||
|
<view class="page">
|
||||||
|
<button class="buttonSize" @tap="OnLogOut">logout</button>
|
||||||
|
</view>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
</scroll-view>
|
||||||
|
<!-- #endif -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { clearAllData } from '../../common/auth';
|
||||||
|
function OnLogOut() {
|
||||||
|
clearAllData()
|
||||||
|
console.log("clear data")
|
||||||
|
uni.switchTab({
|
||||||
|
url: "/pages/tabbar/my",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page{
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonSize {
|
||||||
|
background-color: #6d91b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
408
pages/login/login.uvue
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
<scroll-view style="flex:1">
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<view class="page">
|
||||||
|
<view class="login-tabs">
|
||||||
|
<!-- 左:密码登录 -->
|
||||||
|
<view
|
||||||
|
class="login-tab-item"
|
||||||
|
@click="switchTab(1)"
|
||||||
|
>
|
||||||
|
<text class="login-tab-text" :class="currentTab == 1 ? 'active-text' : '' ">Password login</text>
|
||||||
|
<view class="login-tab-underline" v-if="currentTab == 1"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 右:短信登录 -->
|
||||||
|
<view
|
||||||
|
class="login-tab-item"
|
||||||
|
@click="switchTab(0)"
|
||||||
|
>
|
||||||
|
<text class="login-tab-text" :class="currentTab == 0 ? 'active-text' : '' ">SMS login</text>
|
||||||
|
<view class="login-tab-underline" v-if="currentTab == 0"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="page-box">
|
||||||
|
<!-- SMS login 表单 -->
|
||||||
|
<view v-if="currentTab == 0">
|
||||||
|
<view class="input-wrapper">
|
||||||
|
<uni-icons type="person" size="20" color="#3A6FA1" class="input-icon" />
|
||||||
|
<input
|
||||||
|
class="inputType"
|
||||||
|
type="text"
|
||||||
|
v-model="phoneNumber"
|
||||||
|
prefixIcon="person"
|
||||||
|
placeholder="mobile phone number"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="input-wrapper">
|
||||||
|
<uni-icons type="locked" size="20" color="#3A6FA1" class="input-icon" />
|
||||||
|
<input
|
||||||
|
class="inputType"
|
||||||
|
v-model="verificationCode"
|
||||||
|
prefixIcon="locked"
|
||||||
|
placeholder="verification code"
|
||||||
|
/>
|
||||||
|
<view class="code-btn" @click="onGetCode"> get code </view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Password login 表单 -->
|
||||||
|
<view v-else>
|
||||||
|
<view class="input-wrapper">
|
||||||
|
<uni-icons type="person" size="20" color="#3A6FA1" class="input-icon" />
|
||||||
|
<input
|
||||||
|
class="inputType"
|
||||||
|
type="text"
|
||||||
|
v-model="account"
|
||||||
|
prefixIcon="person"
|
||||||
|
placeholder="account"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="input-wrapper">
|
||||||
|
<uni-icons type="locked" size="20" color="#3A6FA1" class="input-icon" />
|
||||||
|
<input
|
||||||
|
class="inputType"
|
||||||
|
type="text"
|
||||||
|
:password="!showPwd"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
<!-- 右侧“眼睛”按钮 -->
|
||||||
|
<view class="toggle" @tap="showPwd = !showPwd">
|
||||||
|
<uni-icons :type="showPwd ? 'eye' : 'eye-filled'" size="20" color="#3A6FA1" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 记住密码 -->
|
||||||
|
<view class="remember-row" @tap="rememberPwd = !rememberPwd">
|
||||||
|
<uni-icons :type="rememberPwd ? 'checkbox-filled' : 'circle'" size="18" :color="rememberPwd ? '#3A6FA1' : '#999999'" />
|
||||||
|
<text class="remember-text">remember password</text>
|
||||||
|
</view>
|
||||||
|
<!-- 历史账号 -->
|
||||||
|
<view class="history-row" v-if="historyList.length > 0" @tap="historyOpen = !historyOpen">
|
||||||
|
<uni-icons :type="historyOpen ? 'up' : 'down'" size="16" color="#3A6FA1" />
|
||||||
|
<text class="history-text">saved accounts ({{ historyList.length }})</text>
|
||||||
|
</view>
|
||||||
|
<view class="history-list" v-if="historyOpen && historyList.length > 0">
|
||||||
|
<view class="history-item" v-for="(item, idx) in historyList" :key="idx" @tap="onPickHistory(item)">
|
||||||
|
<uni-icons type="person" size="16" color="#3A6FA1" />
|
||||||
|
<text class="history-account">{{ item.account }}</text>
|
||||||
|
<view class="history-del" @tap.stop="onRemoveHistory(item)">
|
||||||
|
<uni-icons type="closeempty" size="16" color="#999999" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="buttonType">
|
||||||
|
<button class="buttonSize" @click="LoginRequest">login</button>
|
||||||
|
<text class="registerText" @click="onRegister">Don't have an account yet? Quick registration</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
</scroll-view>
|
||||||
|
<!-- #endif -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { LoginReqApi } from '../../api/requestApi'
|
||||||
|
import { setToken, setUserName, setSavedAccount, getSavedAccount, getSavedPassword, clearSavedAccount, AccountRecord, getAccountHistory, addAccountHistory, removeAccountHistory } from '../../common/auth'
|
||||||
|
|
||||||
|
const account = ref("")
|
||||||
|
const password = ref("")
|
||||||
|
const currentTab = ref(1)
|
||||||
|
const phoneNumber = ref("")
|
||||||
|
const verificationCode = ref("")
|
||||||
|
const showPwd = ref(false)
|
||||||
|
const rememberPwd = ref(false)
|
||||||
|
const historyList = ref<AccountRecord[]>([])
|
||||||
|
const historyOpen = ref(false)
|
||||||
|
|
||||||
|
// 页面加载时回填已保存的账号密码
|
||||||
|
onLoad(() => {
|
||||||
|
const savedPassword = getSavedPassword()
|
||||||
|
if (savedPassword.length > 0) {
|
||||||
|
rememberPwd.value = true
|
||||||
|
account.value = getSavedAccount()
|
||||||
|
password.value = savedPassword
|
||||||
|
}
|
||||||
|
historyList.value = getAccountHistory()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选择历史账号回填
|
||||||
|
function onPickHistory(item: AccountRecord) {
|
||||||
|
account.value = item.account
|
||||||
|
password.value = item.password
|
||||||
|
rememberPwd.value = true
|
||||||
|
historyOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除一条历史账号
|
||||||
|
function onRemoveHistory(item: AccountRecord) {
|
||||||
|
removeAccountHistory(item.account)
|
||||||
|
historyList.value = getAccountHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(index: number) {
|
||||||
|
if (currentTab.value !== index) {
|
||||||
|
currentTab.value = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGetCode() {
|
||||||
|
// TODO: 这里写请求验证码的逻辑
|
||||||
|
uni.showToast({
|
||||||
|
title: "Send verification code",
|
||||||
|
icon: "none"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async function GetSmsLoginReq(){
|
||||||
|
uni.showToast({
|
||||||
|
title: "in development",
|
||||||
|
icon: "error"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRegister() {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: "/pages/register/register"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetPasswordLoginReq(){
|
||||||
|
try {
|
||||||
|
const res = await LoginReqApi({
|
||||||
|
username: account.value,
|
||||||
|
password: password.value
|
||||||
|
})
|
||||||
|
// console.log("res = ", JSON.stringify((res)))
|
||||||
|
const statusCode = res["statusCode"] as number
|
||||||
|
if(statusCode == 200) {
|
||||||
|
const data = res["data"] as UTSJSONObject
|
||||||
|
const token = data["token"] as string | null
|
||||||
|
const userName = data["username"] as string | null
|
||||||
|
if(token != null && token.length > 0){
|
||||||
|
setToken(token)
|
||||||
|
}
|
||||||
|
if(userName != null && userName.length > 0) {
|
||||||
|
setUserName(userName)
|
||||||
|
}
|
||||||
|
// 勾选了记住密码则保存账号密码,否则清除
|
||||||
|
if(rememberPwd.value) {
|
||||||
|
setSavedAccount(account.value, password.value)
|
||||||
|
} else {
|
||||||
|
clearSavedAccount()
|
||||||
|
}
|
||||||
|
// 保存到历史账号,方便下次直接选择
|
||||||
|
addAccountHistory(account.value, password.value)
|
||||||
|
uni.switchTab({
|
||||||
|
url: "/pages/tabbar/my"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const data = res["data"] as UTSJSONObject
|
||||||
|
const msg = (data["result"] as string | null) ?? "login fail"
|
||||||
|
uni.showToast({
|
||||||
|
title: msg,
|
||||||
|
icon: 'error',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log('login error = ', e)
|
||||||
|
uni.showToast({
|
||||||
|
title: "login error",
|
||||||
|
icon: "error",
|
||||||
|
duration: 2000,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function LoginRequest() {
|
||||||
|
if(currentTab.value == 0) {
|
||||||
|
GetSmsLoginReq()
|
||||||
|
}
|
||||||
|
else if(currentTab.value == 1) {
|
||||||
|
GetPasswordLoginReq()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page{
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.buttonType {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 40rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 80%;
|
||||||
|
}
|
||||||
|
.buttonSize {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
margin: 40rpx auto;
|
||||||
|
background-color: #6d91b3;
|
||||||
|
}
|
||||||
|
.input-wrapper {
|
||||||
|
width: 600rpx;
|
||||||
|
margin: 20rpx auto;
|
||||||
|
border-bottom: 10rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 2rpx solid #6d91b3;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputType {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 32rpx;
|
||||||
|
padding-left: 70rpx;
|
||||||
|
padding-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 10rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 20rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-box {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 15rpx;
|
||||||
|
}
|
||||||
|
.login-tabs {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
padding: 0 60rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab-item {
|
||||||
|
margin-right: 100rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start; /* 让文字和下划线左对齐 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #6d91b3; /* 未选中浅色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中时文字变深、加粗 */
|
||||||
|
.active-text {
|
||||||
|
color: #3c2c14;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下划线小块 */
|
||||||
|
.login-tab-underline {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
width: 100rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
background-color: #3A6FA1;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
border-width: 1rpx;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registerText {
|
||||||
|
margin-top: 30rpx;
|
||||||
|
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3A6FA1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 记住密码 */
|
||||||
|
.remember-row {
|
||||||
|
width: 600rpx;
|
||||||
|
margin: 20rpx auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666666;
|
||||||
|
margin-left: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 历史账号 */
|
||||||
|
.history-row {
|
||||||
|
width: 600rpx;
|
||||||
|
margin: 10rpx auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #3A6FA1;
|
||||||
|
margin-left: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
width: 600rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 2rpx solid #e6e6e6;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-account {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-del {
|
||||||
|
padding: 6rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
344
pages/register/register.uvue
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
<scroll-view style="flex:1">
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<view class="page">
|
||||||
|
<view v-if="registerFlag == false">
|
||||||
|
<view class="textType">
|
||||||
|
<text >email</text>
|
||||||
|
</view>
|
||||||
|
<view class="input-wrapper">
|
||||||
|
<uni-icons type="person" size="20" class="input-icon" />
|
||||||
|
<input
|
||||||
|
class="inputType"
|
||||||
|
type="text"
|
||||||
|
v-model="account"
|
||||||
|
prefixIcon="person"
|
||||||
|
placeholder="email"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="textType">
|
||||||
|
<text >pinCode</text>
|
||||||
|
</view>
|
||||||
|
<view class="input-wrapper">
|
||||||
|
<uni-icons type="starhalf" size="20" class="input-icon" />
|
||||||
|
<input
|
||||||
|
class="inputType"
|
||||||
|
type="text"
|
||||||
|
v-model="pinCode"
|
||||||
|
prefixIcon="person"
|
||||||
|
placeholder="pinCode"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
class="code-btn"
|
||||||
|
:class="{ disabled: isCountingDown }"
|
||||||
|
@tap="onGetCodeTap"
|
||||||
|
>
|
||||||
|
<text class="textCode">{{ isCountingDown ? `${countdown}s` : "get code" }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="buttonType">
|
||||||
|
<button class="buttonSize" @tap="onNextStep">next</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else>
|
||||||
|
<view class="textType">
|
||||||
|
<text>password</text>
|
||||||
|
</view>
|
||||||
|
<view class="input-wrapper">
|
||||||
|
<uni-icons type="locked" size="20" class="input-icon" />
|
||||||
|
<input
|
||||||
|
class="inputType"
|
||||||
|
type="text"
|
||||||
|
:password="!showPwd"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
<!-- 右侧“眼睛”按钮 -->
|
||||||
|
<view class="toggle" @tap="showPwd = !showPwd">
|
||||||
|
<uni-icons :type="showPwd ? 'eye' : 'eye-filled'" size="20" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="textType">
|
||||||
|
<text>enter the password again</text>
|
||||||
|
</view>
|
||||||
|
<view class="input-wrapper">
|
||||||
|
<uni-icons type="locked" size="20" class="input-icon" />
|
||||||
|
<input
|
||||||
|
class="inputType"
|
||||||
|
type="text"
|
||||||
|
:password="!showAgainPwd"
|
||||||
|
v-model="passwordAgain"
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
<!-- 右侧“眼睛”按钮 -->
|
||||||
|
<view class="toggle" @tap="showAgainPwd = !showAgainPwd">
|
||||||
|
<uni-icons :type="showAgainPwd ? 'eye' : 'eye-filled'" size="20" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="textTip">
|
||||||
|
<text class="textSize">tips: The password should be at least six characters long and include both uppercase and lowercase letters as well as numbers.</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="buttonType">
|
||||||
|
<button class="buttonSize" @tap="GetRegisteredUserReq">register</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
</scroll-view>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { RegisteredUserReqApi, GetPindCodeReqApi } from '../../api/requestApi'
|
||||||
|
|
||||||
|
const showPwd = ref(false)
|
||||||
|
const showAgainPwd = ref(false)
|
||||||
|
|
||||||
|
const account = ref("")
|
||||||
|
const password = ref("")
|
||||||
|
const passwordAgain = ref("")
|
||||||
|
const pinCode = ref("")
|
||||||
|
const resPinCode = ref("")
|
||||||
|
const registerFlag = ref(false)
|
||||||
|
|
||||||
|
async function GetpindCode() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await GetPindCodeReqApi({
|
||||||
|
userName: account.value,
|
||||||
|
userPassword: password.value,
|
||||||
|
rndcode: "123"
|
||||||
|
})
|
||||||
|
// console.log("res = ", res)
|
||||||
|
const statusCode = res["statusCode"] as number
|
||||||
|
const data = res["data"] as UTSJSONObject
|
||||||
|
if(statusCode == 200) {
|
||||||
|
resPinCode.value = (data["pinCode"] as string) ?? ""
|
||||||
|
uni.showToast({
|
||||||
|
title: "send pinCode success",
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const msg = (data["result"] as string | null) ?? "The user has registered"
|
||||||
|
uni.showToast({
|
||||||
|
title: msg + ": " + statusCode,
|
||||||
|
icon: 'error',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function GetRegisteredUserReq(){
|
||||||
|
if(password.value != passwordAgain.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "The password is inconsistent",
|
||||||
|
icon: 'error',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await RegisteredUserReqApi({
|
||||||
|
userName: account.value,
|
||||||
|
userPassword: password.value,
|
||||||
|
rndcode: "123"
|
||||||
|
})
|
||||||
|
const statusCode = res["statusCode"] as number
|
||||||
|
const data = res["data"] as UTSJSONObject
|
||||||
|
// console.log("res = ", res)
|
||||||
|
if(statusCode == 200) {
|
||||||
|
const msg = (data["result"] as string | null) ?? "Registration successful"
|
||||||
|
uni.showToast({
|
||||||
|
title: msg,
|
||||||
|
icon: "success",
|
||||||
|
duration: 2000,
|
||||||
|
mask: true
|
||||||
|
})
|
||||||
|
uni.redirectTo({
|
||||||
|
url: "/pages/login/login"
|
||||||
|
})
|
||||||
|
} else{
|
||||||
|
const msg = (data["result"] as string | null) ?? "The user has registered"
|
||||||
|
uni.showToast({
|
||||||
|
title: msg + ": " + statusCode,
|
||||||
|
icon: 'error',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNextStep() {
|
||||||
|
console.log("pinCode.value = ", pinCode.value)
|
||||||
|
console.log("resPinCode.value = ", resPinCode.value)
|
||||||
|
if (pinCode.value.length == 0 || pinCode.value != resPinCode.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "pindCode error",
|
||||||
|
icon: 'error',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
registerFlag.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const isCountingDown = ref(false)
|
||||||
|
const countdown = ref(0)
|
||||||
|
let countdownTimer: number = 0
|
||||||
|
|
||||||
|
function startCountdown(sec: number) {
|
||||||
|
// 防止重复启动
|
||||||
|
if (countdownTimer != 0) {
|
||||||
|
clearInterval(countdownTimer)
|
||||||
|
countdownTimer = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
isCountingDown.value = true
|
||||||
|
countdown.value = sec
|
||||||
|
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(countdownTimer)
|
||||||
|
countdownTimer = 0
|
||||||
|
isCountingDown.value = false
|
||||||
|
countdown.value = 0
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onGetCodeTap() {
|
||||||
|
// 倒计时期间不允许再点
|
||||||
|
if (isCountingDown.value) return
|
||||||
|
if(account.value.length == 0) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "please enter email",
|
||||||
|
icon: 'error',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 先调你的接口
|
||||||
|
await GetpindCode()
|
||||||
|
startCountdown(60) // 60秒倒计时
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page{
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonType {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonSize {
|
||||||
|
width: 90%;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
background-color: #6d91b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonBack {
|
||||||
|
width: 80%;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 90%;
|
||||||
|
height: 80rpx;
|
||||||
|
margin: 20rpx auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputType {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-left: 80rpx; /* 给左边图标腾位置 */
|
||||||
|
padding-right: 230rpx;
|
||||||
|
border: 2rpx solid #ddd;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 20rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 20rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textType {
|
||||||
|
width: 600rpx;
|
||||||
|
margin: 10rpx auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textTip {
|
||||||
|
width: 90%;
|
||||||
|
margin: 20rpx auto;
|
||||||
|
}
|
||||||
|
.textSize {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #6d91b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn {
|
||||||
|
display: flex;
|
||||||
|
transform: none;
|
||||||
|
position: absolute;
|
||||||
|
right: 5rpx;
|
||||||
|
top: 5rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
height: 70rpx;
|
||||||
|
width: 200rpx;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textCode {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #6d91b3;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
429
pages/tabbar/home.uvue
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
<scroll-view style="flex:1">
|
||||||
|
<!-- #endif -->
|
||||||
|
<view class="page">
|
||||||
|
|
||||||
|
<!-- 连接状态卡片 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="statusRow">
|
||||||
|
<view class="statusDot" :class="connected ? 'dotOn' : 'dotOff'"></view>
|
||||||
|
<text class="statusText">{{ connected ? '已连接' : (connecting ? '连接中...' : '未连接') }}</text>
|
||||||
|
<text class="deviceText" v-if="connected">{{ deviceName }} ({{ deviceMac }})</text>
|
||||||
|
</view>
|
||||||
|
<view class="btnRow">
|
||||||
|
<button class="btn" size="mini" v-if="!connected && !connecting" @tap="onBleStarScan">{{ scanning ? '扫描中...' : '扫描设备' }}</button>
|
||||||
|
<button class="btn btnWarn" size="mini" v-if="connected" @tap="onDisconnect">断开连接</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 参数显示 -->
|
||||||
|
<view class="card paramCard" v-if="connected">
|
||||||
|
<view class="paramItem">
|
||||||
|
<text class="paramLabel">温度</text>
|
||||||
|
<text class="paramValue">{{ temperature }}<text class="paramUnit"> ℃</text></text>
|
||||||
|
</view>
|
||||||
|
<view class="paramItem">
|
||||||
|
<text class="paramLabel">风扇转速</text>
|
||||||
|
<text class="paramValue">{{ fanSpeed }}<text class="paramUnit"> RPM</text></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 设备信息 -->
|
||||||
|
<view class="card" v-if="connected">
|
||||||
|
<view class="infoRow">
|
||||||
|
<text class="infoLabel">固件版本</text>
|
||||||
|
<text class="infoValue">{{ fwVersion }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="infoRow">
|
||||||
|
<text class="infoLabel">运行 Bank</text>
|
||||||
|
<text class="infoValue">{{ bankInfo }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 扫描结果列表 -->
|
||||||
|
<view v-if="!connected">
|
||||||
|
<view v-for="(item, index) in scanArray" :key="index" class="card">
|
||||||
|
<view class="cardBox" @tap="onLinkBluetooth(item)">
|
||||||
|
<view class="leftIcon">
|
||||||
|
<uni-icons type="paperclip" size="20"/>
|
||||||
|
</view>
|
||||||
|
<view class="rightContent">
|
||||||
|
<text>{{ item.device.name }}</text>
|
||||||
|
<text>{{ item.device.address }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!scanning && scanArray.length == 0" class="emptyBox">
|
||||||
|
<text class="emptyText">未发现 CAIIC-MCM 设备,点击"扫描设备"重试</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
</scroll-view>
|
||||||
|
<!-- #endif -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { MyApiResult, BleScanResult } from "@/uni_modules/android-ble"
|
||||||
|
import * as Ble from "../../src/BluetoothManager"
|
||||||
|
import * as McmChannel from "../../src/mcm/McmChannel.uts"
|
||||||
|
import * as McmCmd from "../../src/mcm/McmCommands.uts"
|
||||||
|
import * as BleINfo from "../../common/bleInfo.uts"
|
||||||
|
|
||||||
|
const DEVICE_NAME_FILTER = "CAIIC-MCM"
|
||||||
|
const POLL_INTERVAL = 2000
|
||||||
|
|
||||||
|
const scanArray = ref<BleScanResult[]>([])
|
||||||
|
const scanning = ref<boolean>(false)
|
||||||
|
const connecting = ref<boolean>(false)
|
||||||
|
const connected = ref<boolean>(false)
|
||||||
|
const deviceName = ref<string>("")
|
||||||
|
const deviceMac = ref<string>("")
|
||||||
|
const temperature = ref<string>("--")
|
||||||
|
const fanSpeed = ref<string>("--")
|
||||||
|
const fwVersion = ref<string>("--")
|
||||||
|
const bankInfo = ref<string>("--")
|
||||||
|
|
||||||
|
let pollTimer = 0
|
||||||
|
let pageVisible = false
|
||||||
|
|
||||||
|
// ================= 参数轮询 =================
|
||||||
|
|
||||||
|
async function pollParams() {
|
||||||
|
if (!connected.value || !pageVisible) return
|
||||||
|
const temp = await McmCmd.getTemperature()
|
||||||
|
temperature.value = temp != null ? temp.toString() : "--"
|
||||||
|
if (!connected.value || !pageVisible) return
|
||||||
|
const fan = await McmCmd.getFanSpeed()
|
||||||
|
fanSpeed.value = fan != null ? fan.toString() : "--"
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (pollTimer != 0) {
|
||||||
|
clearTimeout(pollTimer)
|
||||||
|
pollTimer = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
stopPolling()
|
||||||
|
// 注意:uts 的 setTimeout 不接受 async 回调,用 then 链式调度
|
||||||
|
pollTimer = setTimeout(() => {
|
||||||
|
pollParams().then(() => {
|
||||||
|
if (connected.value && pageVisible) {
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, POLL_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 设备信息 =================
|
||||||
|
|
||||||
|
async function loadDeviceInfo() {
|
||||||
|
const v = await McmCmd.getVersion()
|
||||||
|
if (v != null) fwVersion.value = v
|
||||||
|
const b = await McmCmd.getBankInfo()
|
||||||
|
if (b != null) {
|
||||||
|
// bankinfo 原文较长,取首个 "APP1/APP2" 关键词展示
|
||||||
|
bankInfo.value = b.indexOf("APP2") >= 0 ? "APP2" : (b.indexOf("APP1") >= 0 ? "APP1" : b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 蓝牙事件 =================
|
||||||
|
|
||||||
|
async function onBleDataListener(res: MyApiResult) {
|
||||||
|
if (res.type == 1000) {
|
||||||
|
// notify 订阅成功:协商 MTU 247(写切片 244),iOS 无效保持 20
|
||||||
|
Ble.setMtu(247, (resMtu: MyApiResult) => {
|
||||||
|
if (resMtu.type == 0) {
|
||||||
|
McmChannel.setMtuSize(247)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
connected.value = true
|
||||||
|
connecting.value = false
|
||||||
|
deviceMac.value = Ble.getConnectMac()
|
||||||
|
loadDeviceInfo()
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisConnectUi() {
|
||||||
|
connected.value = false
|
||||||
|
connecting.value = false
|
||||||
|
stopPolling()
|
||||||
|
temperature.value = "--"
|
||||||
|
fanSpeed.value = "--"
|
||||||
|
fwVersion.value = "--"
|
||||||
|
bankInfo.value = "--"
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBleStatueListener(state: number) {
|
||||||
|
console.log("home bleState = ", state)
|
||||||
|
if (state == 0) {
|
||||||
|
connecting.value = false
|
||||||
|
} else if (state == 10000) { // 连接失败
|
||||||
|
connecting.value = false
|
||||||
|
connected.value = false
|
||||||
|
uni.showToast({ title: '连接失败', icon: 'error' })
|
||||||
|
} else if (state == 10001) { // 已断开
|
||||||
|
onDisConnectUi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 扫描与连接 =================
|
||||||
|
|
||||||
|
function onBleStarScan() {
|
||||||
|
if (scanning.value) return
|
||||||
|
if (!Ble.isEnabled()) {
|
||||||
|
Ble.openBluetooth((res: MyApiResult) => {
|
||||||
|
if (res.type == 0) onBleStarScan()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scanning.value = true
|
||||||
|
scanArray.value = []
|
||||||
|
const scanMs = 10 * 1000
|
||||||
|
Ble.startScan(scanMs, function(res: MyApiResult) {
|
||||||
|
if (res.type == 0) {
|
||||||
|
const scan = res.data as BleScanResult
|
||||||
|
const name = scan.device.name ?? ""
|
||||||
|
if (name.indexOf(DEVICE_NAME_FILTER) < 0) return // 只显示母乳杯设备
|
||||||
|
// 去重
|
||||||
|
for (let i = 0; i < scanArray.value.length; i++) {
|
||||||
|
if (scanArray.value[i].device.address == scan.device.address) return
|
||||||
|
}
|
||||||
|
scanArray.value.push(scan)
|
||||||
|
} else if (res.type == 10003) {
|
||||||
|
uni.showToast({ title: '无蓝牙权限', icon: 'none' })
|
||||||
|
} else if (res.type == 10002) {
|
||||||
|
uni.showToast({ title: '蓝牙未开启', icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
scanning.value = false
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}, scanMs + 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLinkBluetooth(item: BleScanResult) {
|
||||||
|
Ble.stopScan()
|
||||||
|
scanning.value = false
|
||||||
|
connecting.value = true
|
||||||
|
deviceName.value = item.device.name ?? DEVICE_NAME_FILTER
|
||||||
|
BleINfo.setName(deviceName.value)
|
||||||
|
BleINfo.setMac(item.device.address)
|
||||||
|
getApp().globalData.ConnectBle(item.device.address, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnect() {
|
||||||
|
stopPolling()
|
||||||
|
Ble.disConnect()
|
||||||
|
onDisConnectUi()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 生命周期 =================
|
||||||
|
|
||||||
|
let subscribed = false
|
||||||
|
function subscribe() {
|
||||||
|
if (subscribed) return
|
||||||
|
uni.$on("bleData", onBleDataListener)
|
||||||
|
uni.$on("bleState", onBleStatueListener)
|
||||||
|
subscribed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsubscribe() {
|
||||||
|
if (!subscribed) return
|
||||||
|
uni.$off("bleData", onBleDataListener)
|
||||||
|
uni.$off("bleState", onBleStatueListener)
|
||||||
|
subscribed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((options: UTSJSONObject | null) => {
|
||||||
|
subscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
subscribe()
|
||||||
|
pageVisible = true
|
||||||
|
if (Ble.isConnected()) {
|
||||||
|
if (!connected.value) {
|
||||||
|
// 应用内已连接(如从其他页返回)
|
||||||
|
connected.value = true
|
||||||
|
deviceMac.value = Ble.getConnectMac()
|
||||||
|
deviceName.value = BleINfo.getName() ?? ""
|
||||||
|
loadDeviceInfo()
|
||||||
|
}
|
||||||
|
startPolling()
|
||||||
|
} else {
|
||||||
|
onBleStarScan()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
pageVisible = false
|
||||||
|
Ble.stopScan()
|
||||||
|
stopPolling()
|
||||||
|
unsubscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
pageVisible = false
|
||||||
|
Ble.stopScan()
|
||||||
|
stopPolling()
|
||||||
|
unsubscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onPullDownRefresh(() => {
|
||||||
|
if (!connected.value) {
|
||||||
|
onBleStarScan()
|
||||||
|
} else {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusDot {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
.dotOn { background-color: #4cd964; }
|
||||||
|
.dotOff { background-color: #cccccc; }
|
||||||
|
|
||||||
|
.statusText {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceText {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #888888;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: #3A6FA1;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.btnWarn {
|
||||||
|
background-color: #dd524d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paramCard {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paramItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paramLabel {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paramValue {
|
||||||
|
font-size: 52rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3A6FA1;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paramUnit {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoValue {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333333;
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardBox {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftIcon {
|
||||||
|
width: 28px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightContent {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyBox {
|
||||||
|
padding: 60rpx 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyText {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
252
pages/tabbar/my.uvue
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
<scroll-view style="flex:1">
|
||||||
|
<!-- #endif -->
|
||||||
|
<view class="my-page">
|
||||||
|
<!-- 个人卡片信息 -->
|
||||||
|
<view class="card" @tap="onCardTap">
|
||||||
|
<view v-if="isLogin == true">
|
||||||
|
<image class="card-img" src="/static/user.png" mode="aspectFill" />
|
||||||
|
</view>
|
||||||
|
<view v-else>
|
||||||
|
<image class="card-img" src="/static/userDefault.png" mode="aspectFill" />
|
||||||
|
</view>
|
||||||
|
<view class="card-body">
|
||||||
|
<text class="card-title">{{userInfo}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 后台服务器选择 -->
|
||||||
|
<view class="card server-card" @tap="onServerTap">
|
||||||
|
<text class="server-label">后台服务器</text>
|
||||||
|
<view class="server-value">
|
||||||
|
<text class="server-name">{{currentServerName}}</text>
|
||||||
|
<uni-icons type="bottom" size="16" color="#3A6FA1" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="verType">
|
||||||
|
<text class="colorType">{{ "version: 1.0.1" }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="cardBle">
|
||||||
|
<!-- CLI 调试终端 -->
|
||||||
|
<view class="cardBox" @tap="onBluetooth">
|
||||||
|
<uni-icons type="link" size="20" class="input-icon" />
|
||||||
|
<text class="textType">CLI 调试终端</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
</scroll-view>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { checkToken, getUserName } from '../../common/auth';
|
||||||
|
import { SERVER_LIST, getBaseUrl, setBaseUrl } from '../../api/httpInstance';
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import {BleLib,MyApiResult,BleScanResult,BleServices,ScanRssiBR,WNuuid} from "@/uni_modules/android-ble"
|
||||||
|
import * as Ble from "../../src/BluetoothManager"
|
||||||
|
|
||||||
|
const isLogin = ref(false)
|
||||||
|
const userInfo = ref("login/register")
|
||||||
|
|
||||||
|
// 后台服务器选择
|
||||||
|
const serverNames = ref<string[]>([])
|
||||||
|
const currentServerName = ref("")
|
||||||
|
|
||||||
|
function initServerList() {
|
||||||
|
const names: string[] = []
|
||||||
|
SERVER_LIST.forEach((item) => {
|
||||||
|
names.push(item.name)
|
||||||
|
})
|
||||||
|
serverNames.value = names
|
||||||
|
// 恢复上次保存的选择,没有则默认第一个
|
||||||
|
const url = getBaseUrl()
|
||||||
|
SERVER_LIST.forEach((item) => {
|
||||||
|
if (item.url == url) {
|
||||||
|
currentServerName.value = item.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onServerTap() {
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: serverNames.value,
|
||||||
|
success: (res) => {
|
||||||
|
const idx = res.tapIndex
|
||||||
|
if (idx >= 0 && idx < SERVER_LIST.length) {
|
||||||
|
// 保存用户选择(内存 + 本地存储),下次启动直接加载
|
||||||
|
setBaseUrl(SERVER_LIST[idx].url)
|
||||||
|
currentServerName.value = SERVER_LIST[idx].name
|
||||||
|
uni.showToast({
|
||||||
|
title: '已切换到: ' + SERVER_LIST[idx].name,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshLoginState() {
|
||||||
|
const name = getUserName()
|
||||||
|
if(name != null && name.length > 0) {
|
||||||
|
isLogin.value = true
|
||||||
|
userInfo.value = name
|
||||||
|
} else {
|
||||||
|
isLogin.value = false
|
||||||
|
userInfo.value = "login/register"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCardTap () {
|
||||||
|
// 已登录
|
||||||
|
if(isLogin.value){
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/displayUserInfo/displayUserInfo'
|
||||||
|
})
|
||||||
|
|
||||||
|
} else { // 未登录
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/login'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI 调试终端(先扫描选择设备,连接后进入终端)
|
||||||
|
function onBluetooth() {
|
||||||
|
var open=Ble.isEnabled();// true 为蓝牙开启 false 为关闭
|
||||||
|
if(open) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: "/pages/bleTest/bluetoothList/bluetoothList"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: 'please turn on bluetooth',
|
||||||
|
icon: 'error',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======= 生命周期 ========
|
||||||
|
onLoad((options: UTSJSONObject | null) => {
|
||||||
|
if (options != null) {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
console.log("my")
|
||||||
|
refreshLoginState()
|
||||||
|
initServerList()
|
||||||
|
})
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.my-page {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card {
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 20rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
/* 阴影效果 */
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
/* 横向布局:左图右文 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧图片 */
|
||||||
|
.card-img {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧文字区域 */
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题 */
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.cardBle {
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 20rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
/* 阴影效果 */
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
/* 横向布局:左图右文 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardBox {
|
||||||
|
padding: 35rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textType {
|
||||||
|
font-size: 25rpx;
|
||||||
|
}
|
||||||
|
.verType {
|
||||||
|
margin-top: 500rpx;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.colorType{
|
||||||
|
color: #3A6FA1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 后台服务器选择 */
|
||||||
|
.server-card {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-value {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-name {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #3A6FA1;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
380
pages/tabbar/ota.uvue
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
<scroll-view style="flex:1">
|
||||||
|
<!-- #endif -->
|
||||||
|
<view class="page">
|
||||||
|
|
||||||
|
<view class="card" v-if="!connected">
|
||||||
|
<text class="tipText">设备未连接,请先在"设备"页连接 CAIIC-MCM</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-if="connected">
|
||||||
|
<!-- 当前设备信息 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="infoRow">
|
||||||
|
<text class="infoLabel">当前固件版本</text>
|
||||||
|
<text class="infoValue">{{ curVersion }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="infoRow">
|
||||||
|
<text class="infoLabel">运行 Bank</text>
|
||||||
|
<text class="infoValue">{{ bankInfo }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 固件文件 -->
|
||||||
|
<view class="card">
|
||||||
|
<text class="sectionTitle">固件文件(Keil 产物 bin,≤224KB)</text>
|
||||||
|
<input class="input" v-model="filePath" placeholder="/sdcard/Download/embeddedSrc.bin" :disabled="upgrading" />
|
||||||
|
<view class="btnRow">
|
||||||
|
<button class="btn" size="mini" :disabled="upgrading" @tap="onLoadFile">读取文件</button>
|
||||||
|
</view>
|
||||||
|
<view class="infoRow" v-if="fileLoaded">
|
||||||
|
<text class="infoLabel">文件大小</text>
|
||||||
|
<text class="infoValue">{{ fileSize }} 字节</text>
|
||||||
|
</view>
|
||||||
|
<view class="infoRow" v-if="fileLoaded">
|
||||||
|
<text class="infoLabel">镜像 CRC32</text>
|
||||||
|
<text class="infoValue">0x{{ fileCrcHex }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 目标版本 -->
|
||||||
|
<view class="card">
|
||||||
|
<text class="sectionTitle">目标版本号(如 1.00.02)</text>
|
||||||
|
<input class="input" v-model="versionText" placeholder="1.00.02" :disabled="upgrading" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 升级控制 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="progressBox">
|
||||||
|
<view class="progressBar" :style="'width:' + progress + '%'"></view>
|
||||||
|
</view>
|
||||||
|
<text class="progressText">{{ progress }}%</text>
|
||||||
|
<view class="btnRow">
|
||||||
|
<button class="btn" size="mini" v-if="!upgrading" :disabled="!fileLoaded" @tap="onStartOta">开始升级</button>
|
||||||
|
<button class="btn btnWarn" size="mini" v-if="upgrading" @tap="onAbortOta">中止升级</button>
|
||||||
|
</view>
|
||||||
|
<text class="warnText" v-if="upgrading">升级中请勿断开连接或退出应用</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日志 -->
|
||||||
|
<view class="card" v-if="logs.length > 0">
|
||||||
|
<text class="sectionTitle">升级日志</text>
|
||||||
|
<text class="logText" v-for="(log, index) in logs" :key="index">{{ log }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
</scroll-view>
|
||||||
|
<!-- #endif -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { MyApiResult } from "@/uni_modules/android-ble"
|
||||||
|
import * as Ble from "../../src/BluetoothManager"
|
||||||
|
import * as McmCmd from "../../src/mcm/McmCommands.uts"
|
||||||
|
import * as Ota from "../../src/mcm/OtaManager.uts"
|
||||||
|
import { OtaCallbacks } from "../../src/mcm/OtaManager.uts"
|
||||||
|
import { crc32Ieee } from "../../src/mcm/Crc.uts"
|
||||||
|
|
||||||
|
// #ifdef APP-ANDROID
|
||||||
|
import File from 'java.io.File'
|
||||||
|
import FileInputStream from 'java.io.FileInputStream'
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
const connected = ref<boolean>(false)
|
||||||
|
const curVersion = ref<string>("--")
|
||||||
|
const bankInfo = ref<string>("--")
|
||||||
|
const filePath = ref<string>("/sdcard/Download/embeddedSrc.bin")
|
||||||
|
const fileLoaded = ref<boolean>(false)
|
||||||
|
const fileSize = ref<number>(0)
|
||||||
|
const fileCrcHex = ref<string>("")
|
||||||
|
const versionText = ref<string>("")
|
||||||
|
const upgrading = ref<boolean>(false)
|
||||||
|
const progress = ref<number>(0)
|
||||||
|
const logs = ref<string[]>([])
|
||||||
|
|
||||||
|
let imageBytes = [] as number[]
|
||||||
|
|
||||||
|
function addLog(msg: string) {
|
||||||
|
logs.value.push(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 文件读取(Android 本地文件) =================
|
||||||
|
|
||||||
|
function readFileBytes(path: string): number[] | null {
|
||||||
|
// #ifdef APP-ANDROID
|
||||||
|
try {
|
||||||
|
const f = new File(path)
|
||||||
|
if (!f.exists()) return null
|
||||||
|
const len: Int = f.length().toInt()
|
||||||
|
if (len <= 0) return null
|
||||||
|
const fis = new FileInputStream(f)
|
||||||
|
const buf = new ByteArray(len)
|
||||||
|
let readTotal: Int = 0
|
||||||
|
while (readTotal < len) {
|
||||||
|
const n = fis.read(buf, readTotal, len - readTotal)
|
||||||
|
if (n <= 0) break
|
||||||
|
readTotal += n
|
||||||
|
}
|
||||||
|
fis.close()
|
||||||
|
if (readTotal != len) return null
|
||||||
|
const out = [] as number[]
|
||||||
|
var i: Int = 0
|
||||||
|
while (i < len) {
|
||||||
|
out.push(buf[i].toInt() & 0xFF)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
} catch (e) {
|
||||||
|
console.log("readFileBytes error", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-ANDROID
|
||||||
|
return null
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLoadFile() {
|
||||||
|
const bytes = readFileBytes(filePath.value.trim())
|
||||||
|
if (bytes == null) {
|
||||||
|
fileLoaded.value = false
|
||||||
|
uni.showToast({ title: '文件读取失败,请检查路径', icon: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (bytes.length > 0x38000) {
|
||||||
|
fileLoaded.value = false
|
||||||
|
uni.showToast({ title: '文件超过 224KB 上限', icon: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imageBytes = bytes
|
||||||
|
fileSize.value = bytes.length
|
||||||
|
fileCrcHex.value = crc32Ieee(bytes).toString(16)
|
||||||
|
fileLoaded.value = true
|
||||||
|
addLog("已加载文件: " + bytes.length + " 字节, CRC32=0x" + fileCrcHex.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 版本号解析 "1.00.02" -> 0x010002 =================
|
||||||
|
|
||||||
|
function parseVersion(v: string): number | null {
|
||||||
|
const parts = v.trim().split(".")
|
||||||
|
if (parts.length != 3) return null
|
||||||
|
const a = parseInt(parts[0])
|
||||||
|
const b = parseInt(parts[1])
|
||||||
|
const c = parseInt(parts[2])
|
||||||
|
if (isNaN(a) || isNaN(b) || isNaN(c)) return null
|
||||||
|
if (a < 0 || a > 255 || b < 0 || b > 255 || c < 0 || c > 255) return null
|
||||||
|
return a * 65536 + b * 256 + c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= OTA =================
|
||||||
|
|
||||||
|
async function onStartOta() {
|
||||||
|
const version = parseVersion(versionText.value)
|
||||||
|
if (version == null) {
|
||||||
|
uni.showToast({ title: '版本号格式错误(如 1.00.02)', icon: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!Ble.isConnected()) {
|
||||||
|
uni.showToast({ title: '设备未连接', icon: 'error' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
upgrading.value = true
|
||||||
|
progress.value = 0
|
||||||
|
logs.value = []
|
||||||
|
addLog("开始升级,目标版本 " + versionText.value)
|
||||||
|
const cbs: OtaCallbacks = {
|
||||||
|
onProgress: (percent: number) => {
|
||||||
|
progress.value = percent
|
||||||
|
},
|
||||||
|
onState: (msg: string) => {
|
||||||
|
addLog(msg)
|
||||||
|
}
|
||||||
|
} as OtaCallbacks
|
||||||
|
const ok = await Ota.start(imageBytes, version, cbs)
|
||||||
|
upgrading.value = false
|
||||||
|
if (ok) {
|
||||||
|
uni.showToast({ title: '升级成功,设备将自动复位', icon: 'success' })
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '升级失败', icon: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAbortOta() {
|
||||||
|
Ota.abort()
|
||||||
|
addLog("用户请求中止 ...")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 设备信息 =================
|
||||||
|
|
||||||
|
async function loadDeviceInfo() {
|
||||||
|
const v = await McmCmd.getVersion()
|
||||||
|
curVersion.value = v != null ? v : "--"
|
||||||
|
const b = await McmCmd.getBankInfo()
|
||||||
|
if (b != null) {
|
||||||
|
bankInfo.value = b.indexOf("APP2") >= 0 ? "APP2" : (b.indexOf("APP1") >= 0 ? "APP1" : "--")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 蓝牙状态监听 =================
|
||||||
|
|
||||||
|
function onBleStatueListener(state: number) {
|
||||||
|
if (state == 10000 || state == 10001) {
|
||||||
|
connected.value = false
|
||||||
|
if (upgrading.value) {
|
||||||
|
addLog("连接断开,升级中止")
|
||||||
|
upgrading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscribed = false
|
||||||
|
function subscribe() {
|
||||||
|
if (subscribed) return
|
||||||
|
uni.$on("bleState", onBleStatueListener)
|
||||||
|
subscribed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsubscribe() {
|
||||||
|
if (!subscribed) return
|
||||||
|
uni.$off("bleState", onBleStatueListener)
|
||||||
|
subscribed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 生命周期 =================
|
||||||
|
|
||||||
|
onLoad((options: UTSJSONObject | null) => {
|
||||||
|
subscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
subscribe()
|
||||||
|
connected.value = Ble.isConnected()
|
||||||
|
if (connected.value) {
|
||||||
|
loadDeviceInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
unsubscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
unsubscribe()
|
||||||
|
// 离开页面时若仍在升级则中止(升级期间不建议离开)
|
||||||
|
if (Ota.isRunning()) {
|
||||||
|
Ota.abort()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipText {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999999;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #888888;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 16rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #888888;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoValue {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333333;
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: #3A6FA1;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.btnWarn {
|
||||||
|
background-color: #dd524d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBox {
|
||||||
|
height: 24rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #3A6FA1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressText {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #3A6FA1;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warnText {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #dd524d;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logText {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #333333;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
196
pages/tabbar/sysStatus.uvue
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
<scroll-view style="flex:1">
|
||||||
|
<!-- #endif -->
|
||||||
|
<view class="page">
|
||||||
|
|
||||||
|
<view class="card" v-if="!connected">
|
||||||
|
<text class="tipText">设备未连接,请先在"设备"页连接 CAIIC-MCM</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-if="connected">
|
||||||
|
<!-- 概要 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="infoRow">
|
||||||
|
<text class="infoLabel">固件版本</text>
|
||||||
|
<text class="infoValue">{{ version }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="infoRow">
|
||||||
|
<text class="infoLabel">Bank 信息</text>
|
||||||
|
<text class="infoValue">{{ bankInfo }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="btnRow">
|
||||||
|
<button class="btn" size="mini" :disabled="loading" @tap="refreshAll">
|
||||||
|
{{ loading ? '查询中...' : '刷新' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- sysinfo 控制台输出 -->
|
||||||
|
<view class="card">
|
||||||
|
<text class="consoleTitle">sysinfo(系统运行状态)</text>
|
||||||
|
<text class="consoleText">{{ sysInfo }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
<!-- #ifdef APP -->
|
||||||
|
</scroll-view>
|
||||||
|
<!-- #endif -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="uts">
|
||||||
|
import { MyApiResult } from "@/uni_modules/android-ble"
|
||||||
|
import * as Ble from "../../src/BluetoothManager"
|
||||||
|
import * as McmCmd from "../../src/mcm/McmCommands.uts"
|
||||||
|
|
||||||
|
const connected = ref<boolean>(false)
|
||||||
|
const loading = ref<boolean>(false)
|
||||||
|
const version = ref<string>("--")
|
||||||
|
const bankInfo = ref<string>("--")
|
||||||
|
const sysInfo = ref<string>("--")
|
||||||
|
|
||||||
|
async function refreshAll() {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
if (!Ble.isConnected()) {
|
||||||
|
connected.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connected.value = true
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const v = await McmCmd.getVersion()
|
||||||
|
version.value = v != null ? v : "--"
|
||||||
|
|
||||||
|
const b = await McmCmd.getBankInfo()
|
||||||
|
bankInfo.value = b != null ? b.trim() : "--"
|
||||||
|
|
||||||
|
const s = await McmCmd.getSysInfo()
|
||||||
|
sysInfo.value = s != null ? s : "--"
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 蓝牙状态监听 =================
|
||||||
|
|
||||||
|
function onBleStatueListener(state: number) {
|
||||||
|
if (state == 10000 || state == 10001) {
|
||||||
|
connected.value = false
|
||||||
|
version.value = "--"
|
||||||
|
bankInfo.value = "--"
|
||||||
|
sysInfo.value = "--"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subscribed = false
|
||||||
|
function subscribe() {
|
||||||
|
if (subscribed) return
|
||||||
|
uni.$on("bleState", onBleStatueListener)
|
||||||
|
subscribed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsubscribe() {
|
||||||
|
if (!subscribed) return
|
||||||
|
uni.$off("bleState", onBleStatueListener)
|
||||||
|
subscribed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 生命周期 =================
|
||||||
|
|
||||||
|
onLoad((options: UTSJSONObject | null) => {
|
||||||
|
subscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
subscribe()
|
||||||
|
if (Ble.isConnected()) {
|
||||||
|
refreshAll()
|
||||||
|
} else {
|
||||||
|
connected.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
unsubscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
unsubscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
onPullDownRefresh(() => {
|
||||||
|
refreshAll()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipText {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999999;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #888888;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoValue {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #333333;
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnRow {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: #3A6FA1;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consoleTitle {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #888888;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.consoleText {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #333333;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
platformConfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
// 参考链接 https://doc.dcloud.net.cn/uni-app-x/tutorial/ls-plugin.html#setting
|
||||||
|
{
|
||||||
|
"targets": [
|
||||||
|
"APP-ANDROID"
|
||||||
|
]
|
||||||
|
}
|
||||||
171
src/BluetoothManager.uts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import {BleLib,MyApiResult,BleScanResult,BleServices,ScanRssiBR,WNuuid,ScanPara,WriteData} from "@/uni_modules/android-ble"
|
||||||
|
|
||||||
|
// 统一拿到全局 ble 单例(仍然是 App.globalData.ble)
|
||||||
|
function ble(): BleLib {
|
||||||
|
const app = getApp()
|
||||||
|
return app.globalData.ble as BleLib
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机蓝牙是否打开 true-打开 false-未打开
|
||||||
|
export function isEnabled(): boolean {
|
||||||
|
return ble().isEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听系统蓝牙开关状态 res.data的值为true-打开 false-关闭
|
||||||
|
export function OpenStateListener(cb: (res: MyApiResult) => void) {
|
||||||
|
ble().onBtOpenStateListener(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始蓝牙扫描
|
||||||
|
export function startScan(timeoutMs: number, cb: (res: MyApiResult) => void) {
|
||||||
|
// ble().startScanBleDevice(timeoutMs, cb) // 旧版本
|
||||||
|
ble().onStartScanBle({
|
||||||
|
scantime:timeoutMs, // 超时时间 ms
|
||||||
|
showEmptyName:0, // 0表示 不返回空的名称 1 表示可返回空名称蓝牙
|
||||||
|
btNameFilter:"", // showEmptyName 为0有效 代表过滤相关的名字 为空表示不过滤
|
||||||
|
onScanResult:(cb)
|
||||||
|
} as ScanPara)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 蓝牙扫描时监听蓝牙广播信号变化 实时回掉 多次
|
||||||
|
export function scanDataListener(cb: (res: MyApiResult) => void) {
|
||||||
|
ble().onScanDataListener(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止蓝牙扫描
|
||||||
|
export function stopScan() {
|
||||||
|
ble().stopScanBle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 蓝牙是否已连接
|
||||||
|
export function isConnected(): boolean {
|
||||||
|
return ble().isConnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取已连接设备id
|
||||||
|
export function getConnectMac(): string {
|
||||||
|
return ble().getConnectMac()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开安卓蓝牙
|
||||||
|
export function openBluetooth(cb: (res: MyApiResult) => void) {
|
||||||
|
ble().openBtBluetooth(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 连接蓝牙
|
||||||
|
export function connect(mac: string, autoReconnect: boolean, cb: (res: MyApiResult) => void) {
|
||||||
|
ble().connect(mac, autoReconnect, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置蓝牙超时时间
|
||||||
|
export function connectTimeout(timeout: number): void {
|
||||||
|
ble().setBtConnectTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置自动连接间隔
|
||||||
|
export function setBtAutoConnectTime(timeout: number): void {
|
||||||
|
ble().setBtAutoConnectTime(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消自动连接蓝牙
|
||||||
|
export function cancelAutoConnectBt(): void {
|
||||||
|
ble().cancelAutoConnectBt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接蓝牙后需要获扫描蓝牙与特征值
|
||||||
|
export function scanServices(cb: (res: MyApiResult) => void) {
|
||||||
|
ble().scanServices(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择可用服务
|
||||||
|
export function setSelectUUID(len: number): void {
|
||||||
|
ble().setSelectUUID(len) // 长度不可超过
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可用写通知服务
|
||||||
|
export function getUUIDs(): string {
|
||||||
|
return ble().getUUIDs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断开蓝牙
|
||||||
|
export function disConnect() {
|
||||||
|
ble().close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启服务消息通知
|
||||||
|
export function onNotityReadBleData(readNotification: boolean , cb: (res: MyApiResult) => void) {
|
||||||
|
const cBle = ble()
|
||||||
|
cBle.onNotityReadBleData(cBle.getSericUUID(), cBle.getNotityUUID(), readNotification, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取自动识别读写服务uuid
|
||||||
|
export function getSericUUID(): string {
|
||||||
|
return ble().getSericUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取自动识别通知uuid
|
||||||
|
export function getNotityUUID(): string {
|
||||||
|
return ble().getNotityUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取自动识写入uuid
|
||||||
|
export function getwriteUUID(): string {
|
||||||
|
return ble().getwriteUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 发送数据 16进制数组 b:number[]=[0x55,0xff,0xAA] as number[];
|
||||||
|
export function sendData(data: number[], cb: (res: MyApiResult) => void) {
|
||||||
|
const b = ble()
|
||||||
|
b.writeDataToBle(b.getSericUUID(), b.getwriteUUID(), data, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送数据(显式写类型):writeType 0=WRITE(有响应) 1=WRITE_NO_RESPONSE(无响应)
|
||||||
|
// CAIIC-MCM 设备的下行特征仅支持 Write Without Response,必须用 writeType=1
|
||||||
|
export function sendDataBytes(data: number[], writeType: number, cb: (res: MyApiResult) => void) {
|
||||||
|
const b = ble()
|
||||||
|
const wd: WriteData = {
|
||||||
|
serviceId: b.getServiceUUID(),
|
||||||
|
characteristicId: b.getWriteUUID(),
|
||||||
|
writeType: writeType,
|
||||||
|
data: data,
|
||||||
|
fenbao: false
|
||||||
|
} as WriteData
|
||||||
|
b.sendData(wd, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按服务 UUID 前缀选择 service+notify+write 组合(如 "0000CA10"),找到返回 true
|
||||||
|
export function selectService(serviceUuidPrefix: string): boolean {
|
||||||
|
const b = ble()
|
||||||
|
const list = b.getUUIDs() as any as WNuuid[]
|
||||||
|
if (list == null) return false
|
||||||
|
const prefix = serviceUuidPrefix.toUpperCase()
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
if (list[i].serviceId.toUpperCase().indexOf(prefix) == 0) {
|
||||||
|
b.setSelectUUID(i)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送16进制字符数据 var d="00FFAABB"
|
||||||
|
export function sendHexString(data: string, cb: (res: MyApiResult) => void) {
|
||||||
|
const b = ble()
|
||||||
|
b.writeStringDataToBle(b.getSericUUID(), b.getwriteUUID(), data, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置mtu(仅安卓)
|
||||||
|
export function setMtu(len: number, cb: (res: MyApiResult) => void) {
|
||||||
|
ble().setMtu(len, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取rssi 信号 需要蓝牙已经连接
|
||||||
|
export function readRssi(cb: (res: MyApiResult) => void) {
|
||||||
|
ble().readRssi(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字符转16进制拼接字符 string2ByteStrWithCharset
|
||||||
|
// 进制拼接字符转文本 byte2StringWithCharset
|
||||||
|
// 判断对端蓝牙设备的虚拟MAC地址是否有效(鸿蒙) isValidRandomDeviceId
|
||||||
49
src/commonFunctions.uts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
export function bytesToAscii(bytes: number[]): string {
|
||||||
|
let s = ''
|
||||||
|
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i] & 0xFF)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
export function asciiToBytes(s: string): Array<number> {
|
||||||
|
const out = [] as Array<number>
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
const c = s.charCodeAt(i)
|
||||||
|
out.push(c == null ? 0 : c)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToHex(bytes: number[]): string {
|
||||||
|
let s = ''
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
const v = bytes[i] & 0xFF
|
||||||
|
s += (v < 16 ? '0' : '') + v.toString(16)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// "4D 00 00 01 01" => 0x4d 0x00 0x00 0x01 0x01
|
||||||
|
export function parseHexBytes(input: string): Array<number> | null {
|
||||||
|
let s = input.trim()
|
||||||
|
if (s.length == 0) return [] as Array<number>
|
||||||
|
|
||||||
|
// 逗号、换行、制表符 全部变空格(全局)
|
||||||
|
s = s.replace(/[,\n\t]/g, " ")
|
||||||
|
|
||||||
|
// 去掉 0x / 0X(全局)
|
||||||
|
s = s.replace(/0x/ig, "")
|
||||||
|
|
||||||
|
const parts = s.split(" ")
|
||||||
|
const out = [] as Array<number>
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const p = parts[i].trim()
|
||||||
|
if (p.length == 0) continue
|
||||||
|
|
||||||
|
const token = (p.length == 1) ? ("0" + p) : p
|
||||||
|
const v = parseInt(token, 16)
|
||||||
|
if (isNaN(v) || v < 0 || v > 255) return null
|
||||||
|
out.push(v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
47
src/mcm/Crc.uts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* CRC 校验算法(CAIIC BLE 协议 V1.0)
|
||||||
|
* - crc16Ccitt: 帧校验,CRC16-CCITT poly 0x1021 初值 0xFFFF
|
||||||
|
* - crc32Ieee : 固件镜像校验,zlib CRC32 poly 0xEDB88320 初值/异或出 0xFFFFFFFF
|
||||||
|
*
|
||||||
|
* 注意:uts number 位运算按 32 位有符号处理,CRC32 中间值全部保持在 Int 范围内,
|
||||||
|
* 无符号右移用 (x >> 1) & 0x7FFFFFFF 实现;最终返回值转为无符号(0..2^32-1)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 0xEDB88320 的 32 位补码形式(避免字面量超出 Int 范围)
|
||||||
|
const CRC32_POLY = -306674912
|
||||||
|
|
||||||
|
export function crc16Ccitt(data: number[], offset: number, len: number): number {
|
||||||
|
let crc = 0xFFFF
|
||||||
|
for (let i = offset; i < offset + len; i++) {
|
||||||
|
crc = crc ^ ((data[i] & 0xFF) << 8)
|
||||||
|
for (let k = 0; k < 8; k++) {
|
||||||
|
if ((crc & 0x8000) != 0) {
|
||||||
|
crc = ((crc << 1) ^ 0x1021) & 0xFFFF
|
||||||
|
} else {
|
||||||
|
crc = (crc << 1) & 0xFFFF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc
|
||||||
|
}
|
||||||
|
|
||||||
|
export function crc32Ieee(data: number[]): number {
|
||||||
|
let crc = -1 // 0xFFFFFFFF
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
crc = crc ^ (data[i] & 0xFF)
|
||||||
|
for (let k = 0; k < 8; k++) {
|
||||||
|
const shifted = (crc >> 1) & 0x7FFFFFFF
|
||||||
|
if ((crc & 1) != 0) {
|
||||||
|
crc = shifted ^ CRC32_POLY
|
||||||
|
} else {
|
||||||
|
crc = shifted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crc = crc ^ -1
|
||||||
|
// 转为无符号返回值(避免后续 >> 算术右移出问题)
|
||||||
|
if (crc < 0) {
|
||||||
|
return crc + 4294967296
|
||||||
|
}
|
||||||
|
return crc
|
||||||
|
}
|
||||||
113
src/mcm/FrameCodec.uts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* CAIIC BLE 协议 V1.0 帧编解码
|
||||||
|
* 帧格式(小端):[0]=0xCA SOF, [1]=TYPE, [2]=SEQ, [3:4]=LEN(u16 LE),
|
||||||
|
* [5..]=PAYLOAD(LEN 字节), 末尾 2B CRC16-CCITT(覆盖 TYPE..PAYLOAD, LE)
|
||||||
|
* 帧开销 7 字节(头 5 + CRC 2),payload 上限 480。
|
||||||
|
* 一个协议帧可跨多次 BLE 写入传输,接收侧按字节流重组。
|
||||||
|
*/
|
||||||
|
import { crc16Ccitt } from "./Crc.uts"
|
||||||
|
|
||||||
|
export const FRAME_SOF = 0xCA
|
||||||
|
export const FRAME_OVERHEAD = 7
|
||||||
|
export const MAX_PAYLOAD = 480
|
||||||
|
|
||||||
|
// 帧类型
|
||||||
|
export const TYPE_CLI_REQ = 0x01 // 手机→设备 命令文本(≤63B)
|
||||||
|
export const TYPE_CLI_RSP = 0x02 // 设备→手机 应答文本分块
|
||||||
|
export const TYPE_CLI_RSP_END = 0x03 // 设备→手机 {status u8} 0=ok 1=未知命令/行超长
|
||||||
|
export const TYPE_OTA_BEGIN = 0x10 // {total_size u32, image_crc32 u32, version u32}
|
||||||
|
export const TYPE_OTA_DATA = 0x11 // {offset u32, data…}
|
||||||
|
export const TYPE_OTA_END = 0x12 // {crc32 u32}
|
||||||
|
export const TYPE_OTA_RSP = 0x1F // {cmd_echo u8, status u8, offset u32}
|
||||||
|
|
||||||
|
export type BleFrame = {
|
||||||
|
type: number,
|
||||||
|
seq: number,
|
||||||
|
payload: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组帧:SOF + TYPE + SEQ + LEN + PAYLOAD + CRC16 */
|
||||||
|
export function encodeFrame(type: number, seq: number, payload: number[]): number[] {
|
||||||
|
const len = payload.length
|
||||||
|
const out = [] as number[]
|
||||||
|
out.push(FRAME_SOF)
|
||||||
|
out.push(type & 0xFF)
|
||||||
|
out.push(seq & 0xFF)
|
||||||
|
out.push(len & 0xFF)
|
||||||
|
out.push((len >> 8) & 0xFF)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
out.push(payload[i] & 0xFF)
|
||||||
|
}
|
||||||
|
// CRC 覆盖 TYPE..PAYLOAD,即 out 中索引 [1, 5+len)
|
||||||
|
const crc = crc16Ccitt(out, 1, 4 + len)
|
||||||
|
out.push(crc & 0xFF)
|
||||||
|
out.push((crc >> 8) & 0xFF)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字节流组包器:跨 notify 分片重组,CRC 错/超长静默丢弃并重新找 SOF */
|
||||||
|
export class FrameDecoder {
|
||||||
|
buf = [] as number[]
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.buf = [] as number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
feed(data: number[]): BleFrame[] {
|
||||||
|
const frames = [] as BleFrame[]
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
this.buf.push(data[i] & 0xFF)
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
// 重新找 SOF
|
||||||
|
while (this.buf.length > 0 && this.buf[0] != FRAME_SOF) {
|
||||||
|
this.buf.shift()
|
||||||
|
}
|
||||||
|
if (this.buf.length < 5) break
|
||||||
|
const len = this.buf[3] | (this.buf[4] << 8)
|
||||||
|
if (len > MAX_PAYLOAD) {
|
||||||
|
this.buf.shift() // 非法长度,丢弃该 SOF 继续找
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const total = 5 + len + 2
|
||||||
|
if (this.buf.length < total) break // 帧未收完,等下一包
|
||||||
|
const crc = crc16Ccitt(this.buf, 1, 4 + len)
|
||||||
|
const rxCrc = this.buf[5 + len] | (this.buf[5 + len + 1] << 8)
|
||||||
|
if (crc != rxCrc) {
|
||||||
|
this.buf.shift() // CRC 错,丢弃该 SOF 重新找
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const payload = [] as number[]
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
payload.push(this.buf[5 + i])
|
||||||
|
}
|
||||||
|
frames.push({
|
||||||
|
type: this.buf[1],
|
||||||
|
seq: this.buf[2],
|
||||||
|
payload: payload
|
||||||
|
} as BleFrame)
|
||||||
|
this.buf.splice(0, total)
|
||||||
|
}
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 小端 u32 打包(纯算术实现,避免大数值位运算的符号问题) */
|
||||||
|
export function pushU32LE(out: number[], v: number) {
|
||||||
|
let x = v % 4294967296
|
||||||
|
out.push(x % 256)
|
||||||
|
x = Math.floor(x / 256)
|
||||||
|
out.push(x % 256)
|
||||||
|
x = Math.floor(x / 256)
|
||||||
|
out.push(x % 256)
|
||||||
|
x = Math.floor(x / 256)
|
||||||
|
out.push(x % 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 小端 u32 解包(纯算术实现) */
|
||||||
|
export function readU32LE(data: number[], offset: number): number {
|
||||||
|
return (data[offset] & 0xFF) +
|
||||||
|
(data[offset + 1] & 0xFF) * 256 +
|
||||||
|
(data[offset + 2] & 0xFF) * 65536 +
|
||||||
|
(data[offset + 3] & 0xFF) * 16777216
|
||||||
|
}
|
||||||
209
src/mcm/McmChannel.uts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* CAIIC-MCM 连接级通道(单例)
|
||||||
|
* - 经 uni.$on("bleData") 接收原始字节,FrameDecoder 字节流重组后按帧类型分发
|
||||||
|
* - sendFrame:整帧按 (att_mtu-3) 切片串行写入(写特征为 Write Without Response)
|
||||||
|
* - sendCli :CLI 命令透传,串行执行(协议要求一条命令应答完再发下一条)
|
||||||
|
* - OTA_RSP 帧转发给 OtaManager 注册的 handler
|
||||||
|
*
|
||||||
|
* 约定:所有接口的 Promise 只 resolve 不 reject,失败通过返回值表达(uts 兼容性考虑)。
|
||||||
|
*/
|
||||||
|
import * as Ble from "../BluetoothManager.uts"
|
||||||
|
import { MyApiResult } from "@/uni_modules/android-ble"
|
||||||
|
import {
|
||||||
|
FrameDecoder, BleFrame, encodeFrame,
|
||||||
|
TYPE_CLI_REQ, TYPE_CLI_RSP, TYPE_CLI_RSP_END, TYPE_OTA_RSP
|
||||||
|
} from "./FrameCodec.uts"
|
||||||
|
import { asciiToBytes, bytesToAscii } from "../commonFunctions.uts"
|
||||||
|
|
||||||
|
export type CliResult = {
|
||||||
|
text: string, // 聚合的应答文本
|
||||||
|
status: number // 0=ok 1=未知命令/行超长 -1=超时 -2=命令超长 -3=发送失败 -4=连接断开
|
||||||
|
}
|
||||||
|
|
||||||
|
type CliJob = {
|
||||||
|
textChunks: string[],
|
||||||
|
resolve: (r: CliResult) => void,
|
||||||
|
timer: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLI_MAX_CMD_BYTES = 63
|
||||||
|
|
||||||
|
let txSeq = 0
|
||||||
|
let maxWriteBytes = 20 // 单次 BLE 写入上限 = att_mtu - 3,未协商 MTU 时按 20 兼容
|
||||||
|
let decoder = new FrameDecoder()
|
||||||
|
let attached = false
|
||||||
|
|
||||||
|
let cliJob: CliJob | null = null
|
||||||
|
let cliChain: Promise<boolean> = Promise.resolve(true)
|
||||||
|
let writeChain: Promise<boolean> = Promise.resolve(true)
|
||||||
|
|
||||||
|
let otaRspHandler: ((payload: number[]) => void) | null = null
|
||||||
|
let disconnectHandler: (() => void) | null = null
|
||||||
|
|
||||||
|
// ================= 接收 =================
|
||||||
|
|
||||||
|
function dispatchFrame(frame: BleFrame) {
|
||||||
|
if (frame.type == TYPE_CLI_RSP) {
|
||||||
|
const job = cliJob
|
||||||
|
if (job != null) {
|
||||||
|
job.textChunks.push(bytesToAscii(frame.payload))
|
||||||
|
}
|
||||||
|
} else if (frame.type == TYPE_CLI_RSP_END) {
|
||||||
|
const job = cliJob
|
||||||
|
if (job != null) {
|
||||||
|
cliJob = null
|
||||||
|
clearTimeout(job.timer)
|
||||||
|
let text = ""
|
||||||
|
for (let i = 0; i < job.textChunks.length; i++) {
|
||||||
|
text += job.textChunks[i]
|
||||||
|
}
|
||||||
|
const status = frame.payload.length > 0 ? frame.payload[0] : 1
|
||||||
|
job.resolve({ text: text, status: status } as CliResult)
|
||||||
|
}
|
||||||
|
} else if (frame.type == TYPE_OTA_RSP) {
|
||||||
|
const h = otaRspHandler
|
||||||
|
if (h != null) {
|
||||||
|
h(frame.payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBleData(res: MyApiResult) {
|
||||||
|
if (res.type != 0) return
|
||||||
|
const data = res.data as number[]
|
||||||
|
const frames = decoder.feed(data)
|
||||||
|
for (let i = 0; i < frames.length; i++) {
|
||||||
|
dispatchFrame(frames[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBleState(state: number) {
|
||||||
|
// 10000=连接失败 10001=已断开(见 home 页既有约定)
|
||||||
|
if (state == 10000 || state == 10001) {
|
||||||
|
resetChannel()
|
||||||
|
const h = disconnectHandler
|
||||||
|
if (h != null) h()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 断连/异常时复位通道:挂起的 CLI 命令以 -4 结束 */
|
||||||
|
export function resetChannel() {
|
||||||
|
decoder.reset()
|
||||||
|
txSeq = 0
|
||||||
|
const job = cliJob
|
||||||
|
if (job != null) {
|
||||||
|
cliJob = null
|
||||||
|
clearTimeout(job.timer)
|
||||||
|
let text = ""
|
||||||
|
for (let i = 0; i < job.textChunks.length; i++) {
|
||||||
|
text += job.textChunks[i]
|
||||||
|
}
|
||||||
|
job.resolve({ text: text, status: -4 } as CliResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 生命周期 =================
|
||||||
|
|
||||||
|
/** 注册全局蓝牙事件监听(幂等,App onLaunch 时调用一次) */
|
||||||
|
export function attach() {
|
||||||
|
if (attached) return
|
||||||
|
attached = true
|
||||||
|
uni.$on("bleData", (res: any) => onBleData(res as MyApiResult))
|
||||||
|
uni.$on("bleState", (state: any) => onBleState(state as number))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 记录协商后的 att_mtu,写切片大小 = mtu - 3(协议上限 244) */
|
||||||
|
export function setMtuSize(mtu: number) {
|
||||||
|
let w = mtu - 3
|
||||||
|
if (w > 244) w = 244
|
||||||
|
if (w < 20) w = 20
|
||||||
|
maxWriteBytes = w
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMaxWriteBytes(): number {
|
||||||
|
return maxWriteBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOtaRspHandler(h: ((payload: number[]) => void) | null) {
|
||||||
|
otaRspHandler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDisconnectHandler(h: (() => void) | null) {
|
||||||
|
disconnectHandler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 发送 =================
|
||||||
|
|
||||||
|
function sendOnce(chunk: number[]): Promise<MyApiResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 设备下行特征仅支持 Write Without Response → writeType=1
|
||||||
|
Ble.sendDataBytes(chunk, 1, (res: MyApiResult) => resolve(res))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendFrameImpl(type: number, payload: number[]): Promise<boolean> {
|
||||||
|
const frame = encodeFrame(type, txSeq & 0xFF, payload)
|
||||||
|
txSeq = (txSeq + 1) & 0xFF
|
||||||
|
let offset = 0
|
||||||
|
while (offset < frame.length) {
|
||||||
|
const chunkLen = Math.min(frame.length - offset, maxWriteBytes)
|
||||||
|
const chunk = [] as number[]
|
||||||
|
for (let i = 0; i < chunkLen; i++) {
|
||||||
|
chunk.push(frame[offset + i])
|
||||||
|
}
|
||||||
|
const res = await sendOnce(chunk)
|
||||||
|
if (res.type != 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
offset += chunkLen
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发送一个协议帧(写队列串行化,帧间不会交错) */
|
||||||
|
export function sendFrame(type: number, payload: number[]): Promise<boolean> {
|
||||||
|
const p = writeChain.then((): Promise<boolean> => sendFrameImpl(type, payload))
|
||||||
|
writeChain = p
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendCliImpl(cmd: string, timeoutMs: number): Promise<CliResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const payload = asciiToBytes(cmd)
|
||||||
|
if (payload.length > CLI_MAX_CMD_BYTES) {
|
||||||
|
resolve({ text: "", status: -2 } as CliResult)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const job: CliJob = {
|
||||||
|
textChunks: [] as string[],
|
||||||
|
resolve: resolve,
|
||||||
|
timer: 0
|
||||||
|
} as CliJob
|
||||||
|
job.timer = setTimeout(() => {
|
||||||
|
if (cliJob == job) {
|
||||||
|
cliJob = null
|
||||||
|
let text = ""
|
||||||
|
for (let i = 0; i < job.textChunks.length; i++) {
|
||||||
|
text += job.textChunks[i]
|
||||||
|
}
|
||||||
|
resolve({ text: text, status: -1 } as CliResult)
|
||||||
|
}
|
||||||
|
}, timeoutMs)
|
||||||
|
cliJob = job
|
||||||
|
sendFrame(TYPE_CLI_REQ, payload).then((ok) => {
|
||||||
|
if (!ok && cliJob == job) {
|
||||||
|
cliJob = null
|
||||||
|
clearTimeout(job.timer)
|
||||||
|
resolve({ text: "", status: -3 } as CliResult)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CLI 命令透传:串行执行,聚合 CLI_RSP 文本直到 CLI_RSP_END;只 resolve 不 reject */
|
||||||
|
export function sendCli(cmd: string, timeoutMs: number = 3000): Promise<CliResult> {
|
||||||
|
attach()
|
||||||
|
const p = cliChain.then((): Promise<CliResult> => sendCliImpl(cmd, timeoutMs))
|
||||||
|
cliChain = p.then((): boolean => true)
|
||||||
|
return p
|
||||||
|
}
|
||||||
65
src/mcm/McmCommands.uts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* CAIIC-MCM 业务命令封装(基于 CLI 透传)
|
||||||
|
* 命令名集中在顶部常量,固件侧命令定名后只需改这里。
|
||||||
|
* 注意:固件当前已实现 version/sysinfo/bankinfo;
|
||||||
|
* 温度/风扇命令(默认 temp/fan)待固件新增,解析失败返回 null 由页面显示 "--"。
|
||||||
|
*/
|
||||||
|
import { sendCli, CliResult } from "./McmChannel.uts"
|
||||||
|
|
||||||
|
const CMD_VERSION = "version"
|
||||||
|
const CMD_SYSINFO = "sysinfo"
|
||||||
|
const CMD_BANKINFO = "bankinfo"
|
||||||
|
const CMD_TEMPERATURE = "temp"
|
||||||
|
const CMD_FAN_SPEED = "fan"
|
||||||
|
|
||||||
|
/** 从应答文本中提取第一个数值(整数或小数),提取不到返回 null */
|
||||||
|
function parseFirstNumber(text: string): number | null {
|
||||||
|
const m = text.match(/-?\d+(\.\d+)?/)
|
||||||
|
if (m == null) return null
|
||||||
|
const m0 = m[0]
|
||||||
|
if (m0 == null) return null
|
||||||
|
const v = parseFloat(m0)
|
||||||
|
if (isNaN(v)) return null
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 固件版本号原文,如 "V1.00.01";失败返回 null */
|
||||||
|
export async function getVersion(): Promise<string | null> {
|
||||||
|
const r: CliResult = await sendCli(CMD_VERSION)
|
||||||
|
if (r.status != 0) return null
|
||||||
|
const t = r.text.trim()
|
||||||
|
return t.length > 0 ? t : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 系统运行状态原文(运行时间/任务数/heap/任务表),失败返回 null */
|
||||||
|
export async function getSysInfo(): Promise<string | null> {
|
||||||
|
const r: CliResult = await sendCli(CMD_SYSINFO, 5000)
|
||||||
|
if (r.status != 0) return null
|
||||||
|
return r.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/** bank 信息原文(当前运行 bank 与 bootsetting),失败返回 null */
|
||||||
|
export async function getBankInfo(): Promise<string | null> {
|
||||||
|
const r: CliResult = await sendCli(CMD_BANKINFO)
|
||||||
|
if (r.status != 0) return null
|
||||||
|
return r.text
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 温度(单位由固件定义,℃),失败返回 null */
|
||||||
|
export async function getTemperature(): Promise<number | null> {
|
||||||
|
const r: CliResult = await sendCli(CMD_TEMPERATURE)
|
||||||
|
if (r.status != 0) return null
|
||||||
|
return parseFirstNumber(r.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 风扇转速(RPM),失败返回 null */
|
||||||
|
export async function getFanSpeed(): Promise<number | null> {
|
||||||
|
const r: CliResult = await sendCli(CMD_FAN_SPEED)
|
||||||
|
if (r.status != 0) return null
|
||||||
|
return parseFirstNumber(r.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通用 CLI 命令(调试终端用) */
|
||||||
|
export async function execCli(cmd: string, timeoutMs: number = 5000): Promise<CliResult> {
|
||||||
|
return await sendCli(cmd, timeoutMs)
|
||||||
|
}
|
||||||
214
src/mcm/OtaManager.uts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* CAIIC-MCM BLE OTA 升级状态机(协议第 5 节:双 bank 直写)
|
||||||
|
* 流程:OTA_BEGIN{size,crc32,version} → 等 OTA_RSP(0x10,ok)
|
||||||
|
* → OTA_DATA{offset,data} 按 4KB 扇区发送,满扇区等 OTA_RSP(0x11,ok) 作流控
|
||||||
|
* (status=2 时按应答 offset 重发重同步)
|
||||||
|
* → OTA_END{crc32} → 等 OTA_RSP(0x12,ok) → 设备 200ms 后自动复位切 bank
|
||||||
|
* 约定:Promise 只 resolve 不 reject;断连或 abort() 协作式中止。
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
sendFrame, setOtaRspHandler, setDisconnectHandler
|
||||||
|
} from "./McmChannel.uts"
|
||||||
|
import {
|
||||||
|
TYPE_OTA_BEGIN, TYPE_OTA_DATA, TYPE_OTA_END,
|
||||||
|
MAX_PAYLOAD, pushU32LE, readU32LE
|
||||||
|
} from "./FrameCodec.uts"
|
||||||
|
import { crc32Ieee } from "./Crc.uts"
|
||||||
|
|
||||||
|
export type OtaCallbacks = {
|
||||||
|
onProgress: (percent: number) => void, // 0~100
|
||||||
|
onState: (msg: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type OtaAck = {
|
||||||
|
received: boolean, // false=超时
|
||||||
|
status: number, // 0 ok / 1 bad_frame / 2 bad_state(offset 乱序) / 3 size_too_big / 4 crc_fail / 5 flash_fail
|
||||||
|
offset: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type OtaWaiter = {
|
||||||
|
echo: number,
|
||||||
|
timer: number,
|
||||||
|
resolve: (ack: OtaAck) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const BANK_MAX = 0x38000 // 224KB bank 上限
|
||||||
|
const SECTOR_SIZE = 4096 // 设备 4KB 扇区缓冲,攒满回 ack
|
||||||
|
const DATA_PER_FRAME = MAX_PAYLOAD - 4 // OTA_DATA 帧数据段上限(4 字节 offset + 476 数据 = 480)
|
||||||
|
|
||||||
|
let waiter: OtaWaiter | null = null
|
||||||
|
let aborted = false
|
||||||
|
let running = false
|
||||||
|
|
||||||
|
function statusText(status: number): string {
|
||||||
|
switch (status) {
|
||||||
|
case 1: return "bad_frame"
|
||||||
|
case 2: return "bad_state"
|
||||||
|
case 3: return "size_too_big"
|
||||||
|
case 4: return "crc_fail"
|
||||||
|
case 5: return "flash_fail"
|
||||||
|
}
|
||||||
|
return "unknown(" + status + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOtaRsp(payload: number[]) {
|
||||||
|
// payload: {cmd_echo u8, status u8, offset u32 LE}
|
||||||
|
if (payload.length < 6) return
|
||||||
|
const w = waiter
|
||||||
|
if (w == null) return
|
||||||
|
if (payload[0] != w.echo) return // 非当前等待的应答(如末扇区 ack 迟到),忽略
|
||||||
|
waiter = null
|
||||||
|
clearTimeout(w.timer)
|
||||||
|
w.resolve({ received: true, status: payload[1], offset: readU32LE(payload, 2) } as OtaAck)
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitRsp(echo: number, timeoutMs: number): Promise<OtaAck> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const w: OtaWaiter = { echo: echo, timer: 0, resolve: resolve } as OtaWaiter
|
||||||
|
w.timer = setTimeout(() => {
|
||||||
|
if (waiter == w) {
|
||||||
|
waiter = null
|
||||||
|
resolve({ received: false, status: -1, offset: 0 } as OtaAck)
|
||||||
|
}
|
||||||
|
}, timeoutMs)
|
||||||
|
waiter = w
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRunning(): boolean {
|
||||||
|
return running
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 协作式中止:发送循环在下一扇区边界停止 */
|
||||||
|
export function abort() {
|
||||||
|
aborted = true
|
||||||
|
const w = waiter
|
||||||
|
if (w != null) {
|
||||||
|
waiter = null
|
||||||
|
clearTimeout(w.timer)
|
||||||
|
w.resolve({ received: false, status: -2, offset: 0 } as OtaAck)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(image: number[], version: number, cb: OtaCallbacks): Promise<boolean> {
|
||||||
|
const total = image.length
|
||||||
|
const crc = crc32Ieee(image)
|
||||||
|
|
||||||
|
// 1. OTA_BEGIN
|
||||||
|
const pb = [] as number[]
|
||||||
|
pushU32LE(pb, total)
|
||||||
|
pushU32LE(pb, crc)
|
||||||
|
pushU32LE(pb, version)
|
||||||
|
cb.onState("发送 OTA_BEGIN(" + total + " 字节)...")
|
||||||
|
if (!await sendFrame(TYPE_OTA_BEGIN, pb)) {
|
||||||
|
cb.onState("OTA_BEGIN 发送失败")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let ack = await waitRsp(TYPE_OTA_BEGIN, 5000)
|
||||||
|
if (!ack.received) {
|
||||||
|
cb.onState(ack.status == -2 ? "升级已中止" : "等待 OTA_BEGIN 应答超时")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (ack.status != 0) {
|
||||||
|
cb.onState("设备拒绝升级: " + statusText(ack.status))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. OTA_DATA:按 4KB 扇区发送,满扇区等 ack(流控)
|
||||||
|
let offset = 0
|
||||||
|
while (offset < total) {
|
||||||
|
if (aborted) {
|
||||||
|
cb.onState("升级已中止")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const sectorEnd = Math.min(offset + SECTOR_SIZE, total)
|
||||||
|
let pos = offset
|
||||||
|
while (pos < sectorEnd) {
|
||||||
|
const n = Math.min(sectorEnd - pos, DATA_PER_FRAME)
|
||||||
|
const payload = [] as number[]
|
||||||
|
pushU32LE(payload, pos)
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
payload.push(image[pos + i])
|
||||||
|
}
|
||||||
|
if (!await sendFrame(TYPE_OTA_DATA, payload)) {
|
||||||
|
cb.onState("OTA_DATA 写入失败 offset=" + pos)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
pos += n
|
||||||
|
cb.onProgress(Math.floor(pos * 100 / total))
|
||||||
|
}
|
||||||
|
if (sectorEnd - offset >= SECTOR_SIZE) {
|
||||||
|
// 满扇区:设备擦写完成后回 OTA_RSP(0x11)(擦写期间关中断数十 ms,超时给足)
|
||||||
|
ack = await waitRsp(TYPE_OTA_DATA, 8000)
|
||||||
|
if (!ack.received) {
|
||||||
|
cb.onState(ack.status == -2 ? "升级已中止" : "等待扇区应答超时 offset=" + offset)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (ack.status == 2) {
|
||||||
|
// offset 乱序:从设备期望的 offset 重发重同步
|
||||||
|
offset = ack.offset
|
||||||
|
cb.onState("重同步至 offset=" + offset)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (ack.status != 0) {
|
||||||
|
cb.onState("设备写入错误: " + statusText(ack.status))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset = sectorEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. OTA_END:设备 flush 尾部并整镜像回读校验 CRC32
|
||||||
|
const pe = [] as number[]
|
||||||
|
pushU32LE(pe, crc)
|
||||||
|
cb.onState("发送 OTA_END,设备校验镜像 ...")
|
||||||
|
if (!await sendFrame(TYPE_OTA_END, pe)) {
|
||||||
|
cb.onState("OTA_END 发送失败")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ack = await waitRsp(TYPE_OTA_END, 15000)
|
||||||
|
if (!ack.received) {
|
||||||
|
cb.onState(ack.status == -2 ? "升级已中止" : "等待 OTA_END 应答超时")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (ack.status != 0) {
|
||||||
|
cb.onState("镜像校验失败: " + statusText(ack.status))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cb.onProgress(100)
|
||||||
|
cb.onState("升级成功,设备约 200ms 后自动复位运行新固件,请重新连接")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始 OTA 升级
|
||||||
|
* @param image 固件 bin 文件全部字节
|
||||||
|
* @param version 版本号 u32,如 0x010002 表示 V1.00.02
|
||||||
|
*/
|
||||||
|
export async function start(image: number[], version: number, cb: OtaCallbacks): Promise<boolean> {
|
||||||
|
if (running) {
|
||||||
|
cb.onState("已有升级任务进行中")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (image.length < 4 || image.length > BANK_MAX) {
|
||||||
|
cb.onState("镜像大小非法(4 ~ " + BANK_MAX + " 字节)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
running = true
|
||||||
|
aborted = false
|
||||||
|
setOtaRspHandler((payload: number[]) => onOtaRsp(payload))
|
||||||
|
setDisconnectHandler(() => {
|
||||||
|
aborted = true
|
||||||
|
abort()
|
||||||
|
})
|
||||||
|
const ok = await run(image, version, cb)
|
||||||
|
running = false
|
||||||
|
setOtaRspHandler(null)
|
||||||
|
setDisconnectHandler(null)
|
||||||
|
const w = waiter
|
||||||
|
if (w != null) {
|
||||||
|
waiter = null
|
||||||
|
clearTimeout(w.timer)
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
BIN
static/androidApp.png
Normal file
|
After Width: | Height: | Size: 604 KiB |
BIN
static/androidApp22.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
static/charge/UnAvailable.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/charge/Unkonw.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/charge/addDevice.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
static/charge/charging.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
static/charge/connectGun.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
static/charge/error.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/charge/finishing.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
static/charge/insertGun.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
static/charge/noDevice.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/charge/reserve.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
static/charge/scanCode.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
static/logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
static/miniApp.png
Normal file
|
After Width: | Height: | Size: 604 KiB |
BIN
static/miniApp22.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
static/tabbar/chargeActive.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
static/tabbar/chargeDefault.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
static/tabbar/homeActive.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/tabbar/homeDefault.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
static/tabbar/mapActive.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/tabbar/mapDefault.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/tabbar/myActive.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
static/tabbar/myDefault.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
static/tabbar/orderActive.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
static/tabbar/orderDefault.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
static/tabbar/scanCode.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/user.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
static/userDefault.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
76
uni.scss
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 这里是uni-app内置的常用样式变量
|
||||||
|
*
|
||||||
|
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||||
|
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||||
|
*
|
||||||
|
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 颜色变量 */
|
||||||
|
|
||||||
|
/* 行为相关颜色 */
|
||||||
|
$uni-color-primary: #007aff;
|
||||||
|
$uni-color-success: #4cd964;
|
||||||
|
$uni-color-warning: #f0ad4e;
|
||||||
|
$uni-color-error: #dd524d;
|
||||||
|
|
||||||
|
/* 文字基本颜色 */
|
||||||
|
$uni-text-color:#333;//基本色
|
||||||
|
$uni-text-color-inverse:#fff;//反色
|
||||||
|
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||||
|
$uni-text-color-placeholder: #808080;
|
||||||
|
$uni-text-color-disable:#c0c0c0;
|
||||||
|
|
||||||
|
/* 背景颜色 */
|
||||||
|
$uni-bg-color:#ffffff;
|
||||||
|
$uni-bg-color-grey:#f8f8f8;
|
||||||
|
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||||
|
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||||
|
|
||||||
|
/* 边框颜色 */
|
||||||
|
$uni-border-color:#c8c7cc;
|
||||||
|
|
||||||
|
/* 尺寸变量 */
|
||||||
|
|
||||||
|
/* 文字尺寸 */
|
||||||
|
$uni-font-size-sm:12px;
|
||||||
|
$uni-font-size-base:14px;
|
||||||
|
$uni-font-size-lg:16px;
|
||||||
|
|
||||||
|
/* 图片尺寸 */
|
||||||
|
$uni-img-size-sm:20px;
|
||||||
|
$uni-img-size-base:26px;
|
||||||
|
$uni-img-size-lg:40px;
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
$uni-border-radius-sm: 2px;
|
||||||
|
$uni-border-radius-base: 3px;
|
||||||
|
$uni-border-radius-lg: 6px;
|
||||||
|
$uni-border-radius-circle: 50%;
|
||||||
|
|
||||||
|
/* 水平间距 */
|
||||||
|
$uni-spacing-row-sm: 5px;
|
||||||
|
$uni-spacing-row-base: 10px;
|
||||||
|
$uni-spacing-row-lg: 15px;
|
||||||
|
|
||||||
|
/* 垂直间距 */
|
||||||
|
$uni-spacing-col-sm: 4px;
|
||||||
|
$uni-spacing-col-base: 8px;
|
||||||
|
$uni-spacing-col-lg: 12px;
|
||||||
|
|
||||||
|
/* 透明度 */
|
||||||
|
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||||
|
|
||||||
|
/* 文章场景相关 */
|
||||||
|
$uni-color-title: #2C405A; // 文章标题颜色
|
||||||
|
$uni-font-size-title:20px;
|
||||||
|
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||||
|
$uni-font-size-subtitle:26px;
|
||||||
|
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||||
|
$uni-font-size-paragraph:15px;
|
||||||
170
uni_modules/android-ble/changelog.md
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
## 1.1.73(2026-08-15)
|
||||||
|
优化用户反馈插件存在的问题
|
||||||
|
## 1.1.72(2026-08-12)
|
||||||
|
优化
|
||||||
|
## 1.1.71(2026-08-08)
|
||||||
|
优化demo
|
||||||
|
## 1.1.70(2026-08-05)
|
||||||
|
优化ios 连接
|
||||||
|
## 1.1.69(2026-07-14)
|
||||||
|
修复用户反馈Android的闪退问题
|
||||||
|
## 1.1.68(2026-07-07)
|
||||||
|
优化用户反馈的闪退问题
|
||||||
|
## 1.1.67(2026-07-03)
|
||||||
|
增加 Android iOS 鸿蒙 onIndicate 真实订阅与数据读取功能
|
||||||
|
优化 Android indicate 特征判断与回调
|
||||||
|
优化 iOS 与鸿蒙 indicate 订阅成功失败和数据回调
|
||||||
|
## 1.1.66(2026-06-17)
|
||||||
|
优化ios 与鸿蒙断开连接
|
||||||
|
## 1.1.65(2026-06-15)
|
||||||
|
优化群里反馈的闪退问题
|
||||||
|
## 1.1.64(2026-06-14)
|
||||||
|
增加Android 鸿蒙 微信小程序扫描通过uuid过滤
|
||||||
|
优化鸿蒙onReadData
|
||||||
|
其它群中反馈问题优化
|
||||||
|
## 1.1.63(2026-05-20)
|
||||||
|
优化ble连接多连接
|
||||||
|
修复已知问题
|
||||||
|
## 1.1.62(2026-04-12)
|
||||||
|
1.增加主动读取蓝牙数据
|
||||||
|
2.优化Android 分包发送 数据较大的时候 出现回掉多次的问题
|
||||||
|
3.优化ios 蓝牙多连接
|
||||||
|
4 其他优化
|
||||||
|
## 1.1.61(2026-03-18)
|
||||||
|
增加安卓phy 2m 模式设置
|
||||||
|
修复鸿蒙获取信号
|
||||||
|
ios 增加扫描通过uuid 过滤
|
||||||
|
## 1.1.60(2026-03-17)
|
||||||
|
1 增加多蓝牙连接 收消息时候回掉蓝牙对应的uuid 信息
|
||||||
|
2.增加ios 返回广播中蓝牙的名称 其它端不返回
|
||||||
|
3.增加扫描多个名称过滤
|
||||||
|
4,其它优化
|
||||||
|
## 1.1.59(2026-02-02)
|
||||||
|
修复鸿蒙端自动连接多次回掉问题
|
||||||
|
## 1.1.58(2026-01-20)
|
||||||
|
优化ios发送字节数据错误
|
||||||
|
## 1.1.57(2026-01-19)
|
||||||
|
优化蓝牙发送数据 优化数据分包 蓝牙连接
|
||||||
|
优化已知问题等
|
||||||
|
## 1.1.56(2026-01-12)
|
||||||
|
优化ios mtu
|
||||||
|
## 1.1.55(2026-01-08)
|
||||||
|
优化鸿蒙mtu 设置
|
||||||
|
## 1.1.54(2025-12-30)
|
||||||
|
优化蓝牙扫描与连接
|
||||||
|
## 1.1.53(2025-12-29)
|
||||||
|
优化蓝牙扫描
|
||||||
|
## 1.1.52(2025-12-20)
|
||||||
|
修复安卓编译错误
|
||||||
|
## 1.1.51(2025-12-19)
|
||||||
|
优化
|
||||||
|
## 1.1.50(2025-12-19)
|
||||||
|
优化连接 多端错误码同步等
|
||||||
|
## 1.1.49(2025-12-18)
|
||||||
|
优化
|
||||||
|
## 1.1.48(2025-12-18)
|
||||||
|
优化鸿蒙蓝牙扫描与Android 连接
|
||||||
|
## 1.1.47(2025-12-17)
|
||||||
|
优化
|
||||||
|
## 1.1.46(2025-12-15)
|
||||||
|
优化鸿蒙
|
||||||
|
## 1.1.45(2025-12-09)
|
||||||
|
you hua ios connect
|
||||||
|
## 1.1.44(2025-12-04)
|
||||||
|
优化鸿蒙发送
|
||||||
|
## 1.1.43(2025-12-04)
|
||||||
|
优化Android 蓝牙权限
|
||||||
|
## 1.1.42(2025-11-23)
|
||||||
|
优化
|
||||||
|
## 1.1.41(2025-11-19)
|
||||||
|
优化
|
||||||
|
## 1.1.40(2025-11-18)
|
||||||
|
优化ios
|
||||||
|
## 1.1.39(2025-11-18)
|
||||||
|
优化
|
||||||
|
## 1.1.38(2025-11-15)
|
||||||
|
优化链接
|
||||||
|
## 1.1.37(2025-11-14)
|
||||||
|
优化鸿蒙端
|
||||||
|
## 1.1.36(2025-11-11)
|
||||||
|
优化
|
||||||
|
## 1.1.35(2025-11-11)
|
||||||
|
优化
|
||||||
|
## 1.1.34(2025-10-25)
|
||||||
|
优化蓝牙扫描问题
|
||||||
|
## 1.1.33(2025-10-24)
|
||||||
|
优化
|
||||||
|
## 1.1.32(2025-09-17)
|
||||||
|
优化
|
||||||
|
## 1.1.31(2025-09-12)
|
||||||
|
优化
|
||||||
|
## 1.1.30(2025-09-11)
|
||||||
|
增加微信小程序
|
||||||
|
## 1.1.29(2025-09-01)
|
||||||
|
增加鸿蒙 其它优化
|
||||||
|
## 1.1.28(2025-08-31)
|
||||||
|
优化
|
||||||
|
## 1.1.27(2025-06-06)
|
||||||
|
优化
|
||||||
|
## 1.1.26(2025-05-30)
|
||||||
|
优化
|
||||||
|
## 1.1.25(2025-05-22)
|
||||||
|
优化
|
||||||
|
## 1.1.24(2025-05-20)
|
||||||
|
优化
|
||||||
|
## 1.1.23(2025-05-20)
|
||||||
|
优化
|
||||||
|
## 1.1.22(2025-05-18)
|
||||||
|
优化
|
||||||
|
## 1.1.21(2025-05-10)
|
||||||
|
优化
|
||||||
|
## 1.1.20(2025-05-10)
|
||||||
|
优化iOS端
|
||||||
|
## 1.1.19(2025-05-07)
|
||||||
|
优化
|
||||||
|
## 1.1.18(2025-05-07)
|
||||||
|
优化
|
||||||
|
## 1.1.17(2025-05-06)
|
||||||
|
优化
|
||||||
|
## 1.1.16(2025-05-03)
|
||||||
|
优化
|
||||||
|
## 1.1.15(2025-04-30)
|
||||||
|
优化
|
||||||
|
## 1.1.14(2025-04-29)
|
||||||
|
优化
|
||||||
|
## 1.1.13(2025-04-29)
|
||||||
|
优化
|
||||||
|
## 1.1.12(2025-04-22)
|
||||||
|
优化
|
||||||
|
## 1.1.11(2025-04-21)
|
||||||
|
优化
|
||||||
|
## 1.1.10(2025-04-11)
|
||||||
|
优化Android蓝牙
|
||||||
|
## 1.1.9(2025-04-10)
|
||||||
|
优化蓝牙
|
||||||
|
## 1.1.8(2025-04-08)
|
||||||
|
优化低版本不申请定位权限问题
|
||||||
|
## 1.1.7(2025-04-07)
|
||||||
|
优化
|
||||||
|
## 1.1.6(2025-04-04)
|
||||||
|
优化
|
||||||
|
## 1.1.5(2025-04-04)
|
||||||
|
优化
|
||||||
|
## 1.1.2(2025-03-18)
|
||||||
|
增加ios
|
||||||
|
## 1.1.1(2025-03-17)
|
||||||
|
优化
|
||||||
|
## 1.0.9(2025-02-28)
|
||||||
|
优化
|
||||||
|
## 1.0.8(2025-02-11)
|
||||||
|
优化
|
||||||
|
## 1.0.7(2025-01-03)
|
||||||
|
修复编译报错问题
|
||||||
|
## 1.0.6(2024-11-19)
|
||||||
|
修复上次问题
|
||||||
|
## 1.0.5(2024-11-19)
|
||||||
|
修复uniapp 问题
|
||||||
|
## 1.0.4(2024-10-08)
|
||||||
|
修复偶现链接失败的bug
|
||||||
|
## 1.0.3(2024-09-26)
|
||||||
|
修复已知bug
|
||||||
1
uni_modules/android-ble/encrypt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Πϊρ<EFBFBD>τ,ΨΒtµ <C2B5>&ΐ‘qΨά:ϊSK<53>'«Δξv–Η,¨”
ςψF<CF88>³›vQ<76>½P•ΜN<03>Η®'_hΩg47C·CΘTόhjI›:<3A>ΔX~t<>Δ—t0D©2§ΜiαΥ³ Σδy\d°Αΐ%ύ<>σΖΨ<CE96><CEA8>ΟM@t*ΎλϊςQγ?ΓΏµs<C2B5><73>IVΙ%P
|
||||||
126
uni_modules/android-ble/old.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 开启服务消息通知(旧)
|
||||||
|
### onNotityReadBleData
|
||||||
|
参数1 通知服务uuid
|
||||||
|
|
||||||
|
参数2 通知属性 uuid
|
||||||
|
|
||||||
|
参数3 true 开启通知读取 false 关闭通知读取
|
||||||
|
|
||||||
|
参数4 回掉结果res.type 为0 表示通知数据 1000 表示订阅成功 1001 表示 订阅失败
|
||||||
|
|
||||||
|
uniappx
|
||||||
|
~~~
|
||||||
|
lib.onNotityReadBleData(lib.getSericUUID(),
|
||||||
|
lib.getNotityUUID(),
|
||||||
|
true,
|
||||||
|
function(res:MyApiResult){
|
||||||
|
if(res.type==0){
|
||||||
|
console.log(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
~~~
|
||||||
|
uniapp
|
||||||
|
~~~
|
||||||
|
lib.onNotityReadBleData(lib.getSericUUID(),
|
||||||
|
lib.getNotityUUID(),
|
||||||
|
true,
|
||||||
|
function(res){
|
||||||
|
if(res.type==0){
|
||||||
|
console.log(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### 发送数据(弃用)
|
||||||
|
### writeDataToBle
|
||||||
|
参数1 服务uuid
|
||||||
|
|
||||||
|
参数2 写入属性uuid
|
||||||
|
|
||||||
|
参数3 16进制数组
|
||||||
|
|
||||||
|
参数4 写入回掉
|
||||||
|
|
||||||
|
uniappx
|
||||||
|
~~~
|
||||||
|
var b:number[]=[0x55,0xff,oxAA] as number[];
|
||||||
|
lib.writeDataToBle(
|
||||||
|
lib.getSericUUID(),
|
||||||
|
lib.getwriteUUID(),
|
||||||
|
b,
|
||||||
|
function(res:MyApiResult){
|
||||||
|
console.log(res)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
~~~
|
||||||
|
|
||||||
|
|
||||||
|
~~~
|
||||||
|
var b:number[]=[0x55,0xff,oxAA] as number[];
|
||||||
|
lib.writeDataToBleWithType(
|
||||||
|
lib.getSericUUID(),
|
||||||
|
lib.getwriteUUID(),
|
||||||
|
b,
|
||||||
|
0,
|
||||||
|
function(res:MyApiResult){
|
||||||
|
console.log(res)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
~~~
|
||||||
|
uniapp
|
||||||
|
~~~
|
||||||
|
var b=[0x55,0xff,oxAA] ;
|
||||||
|
lib.writeDataToBle(
|
||||||
|
lib.getSericUUID(),
|
||||||
|
lib.getwriteUUID(),
|
||||||
|
b,
|
||||||
|
function(res){
|
||||||
|
console.log(res)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
~~~
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 发送16进制字符数据(弃用)
|
||||||
|
### writeStringDataToBle
|
||||||
|
参数1 服务uuid
|
||||||
|
|
||||||
|
参数2 写入属性uuid
|
||||||
|
|
||||||
|
参数3 16进制字符串
|
||||||
|
|
||||||
|
参数4 回掉写入结果
|
||||||
|
|
||||||
|
uniappx
|
||||||
|
~~~
|
||||||
|
var d="00FFAABB"
|
||||||
|
lib.writeStringDataToBle(
|
||||||
|
lib.getSericUUID(),
|
||||||
|
lib.getwriteUUID(),
|
||||||
|
d,
|
||||||
|
function(res:MyApiResult){
|
||||||
|
console.log(res)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
~~~
|
||||||
|
uniapp
|
||||||
|
~~~~
|
||||||
|
var d="00FFAABB"
|
||||||
|
lib.writeStringDataToBle(
|
||||||
|
lib.getSericUUID(),
|
||||||
|
lib.getwriteUUID(),
|
||||||
|
d,
|
||||||
|
function(res){
|
||||||
|
console.log(res)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
~~~~
|
||||||
111
uni_modules/android-ble/package.json
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"id": "android-ble",
|
||||||
|
"displayName": "Ble低功耗蓝牙uts插件 支持安卓ios 鸿蒙 微信小程序(2025插件大赛三等奖)",
|
||||||
|
"version": "1.1.73",
|
||||||
|
"description": "蓝牙 ble UTS 插件 支持安卓ios 鸿蒙 微信小程序,蓝牙扫描、蓝牙广播 连接 多蓝牙、读取数据 发送数据,蓝牙状态监听、连接状态、数据写入 支持GBK编码 格式转换等",
|
||||||
|
"keywords": [
|
||||||
|
"BLE",
|
||||||
|
"蓝牙",
|
||||||
|
"低功耗蓝牙",
|
||||||
|
"多蓝牙连接",
|
||||||
|
"bluetooth"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.98",
|
||||||
|
"uni-app": "^4.07",
|
||||||
|
"uni-app-x": "^4.07"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "uts",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "66.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "888.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "无",
|
||||||
|
"permissions": " <uses-permission android:name=\"android.permission.BLUETOOTH_SCAN\" />\n <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>\n <uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"/>\n <uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\"/>\n <uses-permission android:name=\"android.permission.BLUETOOTH\"/>\n <uses-permission android:name=\"android.permission.BLUETOOTH_ADVERTISE\" />"
|
||||||
|
},
|
||||||
|
"npmurl": "",
|
||||||
|
"darkmode": "√",
|
||||||
|
"i18n": "√",
|
||||||
|
"widescreen": "√"
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"client": {
|
||||||
|
"uni-app": {
|
||||||
|
"vue": {
|
||||||
|
"vue2": "-",
|
||||||
|
"vue3": "-"
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"safari": "-",
|
||||||
|
"chrome": "-"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"vue": "√",
|
||||||
|
"nvue": "√",
|
||||||
|
"android": {
|
||||||
|
"extVersion": "",
|
||||||
|
"minVersion": "21"
|
||||||
|
},
|
||||||
|
"ios": "√",
|
||||||
|
"harmony": "√"
|
||||||
|
},
|
||||||
|
"mp": {
|
||||||
|
"weixin": "√",
|
||||||
|
"alipay": "-",
|
||||||
|
"toutiao": "-",
|
||||||
|
"baidu": "-",
|
||||||
|
"kuaishou": "-",
|
||||||
|
"jd": "-",
|
||||||
|
"harmony": "-",
|
||||||
|
"qq": "-",
|
||||||
|
"lark": "-",
|
||||||
|
"xhs": "-"
|
||||||
|
},
|
||||||
|
"quickapp": {
|
||||||
|
"huawei": "-",
|
||||||
|
"union": "-"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uni-app-x": {
|
||||||
|
"web": {
|
||||||
|
"safari": "-",
|
||||||
|
"chrome": "-"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"android": {
|
||||||
|
"extVersion": "",
|
||||||
|
"minVersion": "21"
|
||||||
|
},
|
||||||
|
"ios": "√",
|
||||||
|
"harmony": "√"
|
||||||
|
},
|
||||||
|
"mp": {
|
||||||
|
"weixin": "√"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "x",
|
||||||
|
"aliyun": "x",
|
||||||
|
"alipay": "x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extends": {
|
||||||
|
"sn": "AcF3Eo4MCUoCe6Q513VMY+Y6GBtfln8RTEF0kiwL7ae84o3c+KmUKJlF3Fl7kzYpjilw9Lt9erESBkB/qL2pTBr+xP/aWJP3GzC3oHmsg5e7n6fH4in9Mg2quSV5DOseKObFgIDddAiirAi++uXYJy9ElBlBC4WQH+OGw7UqVtbc7x7lQ2LMEh2xOw1nEFrTThc5TaNi6Mr9Jm5bOr2GRx0hTCIW8WSummY7Cs5/nELcJXEBegufNwHv62lF9WBaGoUKwYLGxEkCQDmCgX/yyA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1240
uni_modules/android-ble/readme.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="com.xtf.ble">
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"
|
||||||
|
/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
|
||||||
|
</manifest>
|
||||||
7
uni_modules/android-ble/utssdk/app-android/Result.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
+ž5¶µ52W×ŇĄiyi7YŁá±Řużőö <EFBFBD>މžp^w”‡<EFBFBD>µîYä@?ŕ+`¬¶ĹýĎŠĐX<EFBFBD>ľjp©)
|
||||||
|
:Ţ\ĺP±č˛Bëw/D
š_üŘ*;ÝŤK.S 8vZ6MvŤ~
Î(KČR]űÝĆđ್H`@\ŚDĹFąMĐXܧŚ^vů\şcŢZ6˝renôەǤ ŹĆ&Ťx7%¨ŁÓp±ęšŃB§¤JĎŹÁÖ<EFBFBD>űĽ<EFBFBD>‚Á”š<EFBFBD>oTUI=ĄLMçăü%ĎžiŐDl/y—ÂR:GüF‹Ë…Řűęęé”ĆJUÎZ–ő°gţx)uŐb 8–›ŮŘ#"BrÓą›lŢ\éF´µŚN~OF5<46>Y•d4ÝÓ›ôç˝<C3A7>•“J„Č&Pş¶éM;ÖËŘ?='Wť
˛ă¸˙\PŚćţŢ»„qžn]ľ›Ę=qn•tÓň×3^¸ůóŰY„ęÔΕĚmMçŘjĆü
.
|
||||||
|
p\ň5žv̤˘ŹMżŽ}›t0
|
||||||
|
äaĽ‡ěnľVéďŔAŚ:Ĺ83.M0“Ý3IÚžAawëoŰ:Úżčţ˛vuČ4¸˘˛*¨'Ť†Ż9ŇčEśÇ
|
||||||
|
ż†…Ăĺ,Łš?őx+óË?ŚU¬é><EFBFBD>g‘±Jťçë •q÷¦'<EFBFBD>¦ĂJm’ĚĽ6(§ęŠĎyČUB-)µ+mu’âž›–k`_fO©ÂşcÔˇ¨‹rJńl˙Ő¨)ýNěb„{`I»ČCŃ ĐíCa$<EFBFBD>‘Äu
|
||||||
|
ý¨ŠÍ3žôt%<;µJÖ %3ŃXWfŰ˝ÁĽ«Š˘jó¸z7ŞcßľKHty=‘Źäw®‹ŚĐćăőÍÚ
|
||||||
|
ÎdëbÖP<EFBFBD>ç•G&Çű:č^zŽMc eÁEŢĎî…T÷<EFBFBD>ˇË€˘‰0knYĚÜ%řżŠBZé–ˇb2}EüZs
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
¶L׃±k<EFBFBD>ֽ™£ָ™iY6ֶ/ƒq<EFBFBD>vpDׂ±yo
|
||||||
|
;b*r‚?י¢:<EFBFBD>^צzֹo|<EFBFBD>Tn[6heֶ¸¹כ†ׁ<EFBFBD>]ם<EFBFBD><EFBFBD>“<EFBFBD>H^¯÷ש<EFBFBD>ת3±9’ב›<EFBFBD>]צב—ר¬¡Wֵ<EFBFBD>0<EFBFBD><EFBFBD>¹ָB<EFBFBD>ע²˜<EFBFBD>{םf<§IiCדu=”uƒG±%J<EFBFBD>¨)@<EFBFBD>‚<EFBFBD>ֿ<EFBFBD>tqצ|׀װ‹‰‡“ <EFBFBD>—·<EFBFBD>÷יƒDK<EFBFBD><EFBFBD>.<EFBFBD>"4מל´<EFBFBD><EFBFBD>¥…‘‡G<‡h`ֵצl:^Ggֵ₪&ד(צ<>µהeחֻ*'סN<D7A1>ש<EFBFBD>םןץװ§_w¼¹m$
|
||||||
|
gד‚ש\j¬װ«CS††©“<EFBFBD>Md1ֱףG<EFBFBD>‰e>ךS<EFBFBD>NLd<EFBFBD>ƒ׳9װ¡§¨₪·
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
ЖLгБk<EFBFBD>Э<EFBFBD>ЃШ<EFBFBD>iY6Ц/<EFBFBD>q<EFBFBD>vpDвБyo
|
||||||
|
;b3<EFBFBD>ЕцЇkщК<EFBFBD>%`мBЕцt dx <EFBFBD>В<EFBFBD>ZHtяSg%№5цx<EFBFBD>S<EFBFBD>s<EFBFBD>СY<EFBFBD>>?ЊБrљ5<EFBFBD>ТѕaiШоЛ|'GHч<EFBFBD>в5дй<EFBFBD>ўyЪЮ,5тNЖИ§iїєw<EFBFBD>]И=y<EFBFBD> ,6MНЊЕЅї<EFBFBD>ѕ'FіЁ<EFBFBD>?щљy*Ѓяш3]C)<EFBFBD>Н"Ў=mђа<D192><D0B0>VЪgwNћ<4E>5<EFBFBD>qAKЛ[!Х(/<2F>d/М^\мМ9}<7D>K%
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
¶LÓ±k<EFBFBD>Í™£È™iY6Æ.wüeIO¡QNIZ?Ѳñ|®nVeO<EFBFBD>öV³çO|WOPVZse5éíökÔL9<ZGC‘»ú×[9Ô⺵€bAǸušKçê²¢“ªS*Úæ%zmD0ˆ®æêPCoH‡õ>É…¢ªòoÿZIMâ&1Ö R/ˆ¥µŠË!yà4ƳÞ#½Ë’c³©úüÉ¥•ÿ$/:î@òÕAÖªÜê<EFBFBD>—þõê‚AšÔŸé¼žíQ¿w,+·±<EFBFBD>ú<EFBFBD>!6ñÝG¤Á<EFBFBD>죄JùHÕÚŸ÷0ùUÇ{Bcl<EFBFBD>AÌ
|
||||||
|
¾t ï€<EFBFBD>@HÌ<EFBFBD>–Üm/Nwù
|
||||||
@ -0,0 +1 @@
|
|||||||
|
¶LÓ±kŹÍ™ŁČ™iY6Ć.wüeIOˇQNIZ?ѲńE ă‘×ţ´NXËŠ˛_Ě‹úCbŇ<EFBFBD>Nâč–z<EFBFBD>Z;h<EFBFBD>k¸2ŮuýĐ
KDyQ@RR¬í† ˘0ֺҜ‹ÔČ6ŻŔZÖšd]|¸7Iě3ĹxI{zÁŻË†"ާĆGá
|
||||||
@ -0,0 +1 @@
|
|||||||
|
¶LÓ±kŹÍ™ŁČ™iY6Ćú73ޤi[źa’{ţ^™b_Şřâ<EFBFBD>(je$*\˛R
ŚűŇL(d}
>ł°98>^ŕ훇uęŤ>n;Ę=Ť]b”1GŽ/WşĺOzďŚ;f>üę¤m<‹*ŐN꼙5LQjŚ˝ź‘t)ŽW~Ć—}.O<EFBFBD>´ă-˘O§ýös±„;ßâDú;DuqŰ<EFBFBD>y<EFBFBD>=Ý52A]Ĺţ˙f’Š«…š,“÷Nwd)±i¨˛lÂłĚô€q+Źa÷Ë:lÉ
|
||||||