commit 0b93c86b9bbca0b313d60d968e69d3658d470b37 Author: Lufaneng Date: Fri Feb 13 19:49:32 2026 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2ff201 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/AppScope/app.json5 b/AppScope/app.json5 new file mode 100644 index 0000000..dccac72 --- /dev/null +++ b/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.bluetooth.myapplication", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} diff --git a/AppScope/resources/base/element/string.json b/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..1080233 --- /dev/null +++ b/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "MyApplication" + } + ] +} diff --git a/AppScope/resources/base/media/background.png b/AppScope/resources/base/media/background.png new file mode 100644 index 0000000..923f2b3 Binary files /dev/null and b/AppScope/resources/base/media/background.png differ diff --git a/AppScope/resources/base/media/foreground.png b/AppScope/resources/base/media/foreground.png new file mode 100644 index 0000000..eb94275 Binary files /dev/null and b/AppScope/resources/base/media/foreground.png differ diff --git a/AppScope/resources/base/media/layered_image.json b/AppScope/resources/base/media/layered_image.json new file mode 100644 index 0000000..fb49920 --- /dev/null +++ b/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/build-profile.json5 b/build-profile.json5 new file mode 100644 index 0000000..5fde02d --- /dev/null +++ b/build-profile.json5 @@ -0,0 +1,56 @@ +{ + "app": { + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "/Users/fanenglu/.ohos/config/default_MyApplication_zjQsOdlVcTqUDXAQawJCaJLrppdHc2GRKZtElYoZHFM=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001B63A7DEB8792EF7E2DE7B182B739BBF2EFC80F93C76E2EC26A4690528FA1F1CB3E2C8FC28823983", + "profile": "/Users/fanenglu/.ohos/config/default_MyApplication_zjQsOdlVcTqUDXAQawJCaJLrppdHc2GRKZtElYoZHFM=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "/Users/fanenglu/.ohos/config/default_MyApplication_zjQsOdlVcTqUDXAQawJCaJLrppdHc2GRKZtElYoZHFM=.p12", + "storePassword": "0000001B4FB71FC9328FAB65DF96485D11DEADBD2E3F81D37FF089A471DF666C8FAF3CC42E6031B5C59596" + } + } + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "targetSdkVersion": "6.0.2(22)", + "compatibleSdkVersion": "6.0.0(20)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/code-linter.json5 b/code-linter.json5 new file mode 100644 index 0000000..073990f --- /dev/null +++ b/code-linter.json5 @@ -0,0 +1,32 @@ +{ + "files": [ + "**/*.ets" + ], + "ignore": [ + "**/src/ohosTest/**/*", + "**/src/test/**/*", + "**/src/mock/**/*", + "**/node_modules/**/*", + "**/oh_modules/**/*", + "**/build/**/*", + "**/.preview/**/*" + ], + "ruleSet": [ + "plugin:@performance/recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@security/no-unsafe-aes": "error", + "@security/no-unsafe-hash": "error", + "@security/no-unsafe-mac": "warn", + "@security/no-unsafe-dh": "error", + "@security/no-unsafe-dsa": "error", + "@security/no-unsafe-ecdsa": "error", + "@security/no-unsafe-rsa-encrypt": "error", + "@security/no-unsafe-rsa-sign": "error", + "@security/no-unsafe-rsa-key": "error", + "@security/no-unsafe-dsa-key": "error", + "@security/no-unsafe-dh-key": "error", + "@security/no-unsafe-3des": "error" + } +} \ No newline at end of file diff --git a/entry/.gitignore b/entry/.gitignore new file mode 100644 index 0000000..e2713a2 --- /dev/null +++ b/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/entry/build-profile.json5 b/entry/build-profile.json5 new file mode 100644 index 0000000..6bd6457 --- /dev/null +++ b/entry/build-profile.json5 @@ -0,0 +1,33 @@ +{ + "apiType": "stageMode", + "buildOption": { + "resOptions": { + "copyCodeResource": { + "enable": false + } + } + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/entry/hvigorfile.ts b/entry/hvigorfile.ts new file mode 100644 index 0000000..b0e3a1a --- /dev/null +++ b/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/entry/obfuscation-rules.txt b/entry/obfuscation-rules.txt new file mode 100644 index 0000000..1e7e54e --- /dev/null +++ b/entry/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/source-obfuscation + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/entry/oh-package.json5 b/entry/oh-package.json5 new file mode 100644 index 0000000..248c3b7 --- /dev/null +++ b/entry/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": {} +} + diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000..091797f --- /dev/null +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,48 @@ +import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; + +const DOMAIN = 0x0000; + +export default class EntryAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + try { + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); + } catch (err) { + hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err)); + } + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); + return; + } + hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 0000000..8e4de99 --- /dev/null +++ b/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,16 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +const DOMAIN = 0x0000; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(DOMAIN, 'testTag', 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + await Promise.resolve(); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/BluetoothControlPage.ets b/entry/src/main/ets/pages/BluetoothControlPage.ets new file mode 100644 index 0000000..b4e1e5e --- /dev/null +++ b/entry/src/main/ets/pages/BluetoothControlPage.ets @@ -0,0 +1,995 @@ +/** + * 蓝牙设备控制页面 + * 功能:手动控制、参数设置、自动模式、报警浏览 + */ +import { router } from '@kit.ArkUI'; +import { connection, socket } from '@kit.ConnectivityKit'; +import { BusinessError } from '@kit.BasicServicesKit'; + +// 路由参数接口 +interface RouterParams { + deviceId: string; + deviceName: string; +} + +// 回路信息接口 +interface LoopInfo { + index: number; + name: string; + isOpen: boolean; + pwm: number; +} + +// Tab项接口 +interface TabItem { + name: string; + index: number; +} + +// 加载状态接口 +interface LoadingState { + deviceTime: boolean; + deviceInfo: boolean; + sunRiseTime: boolean; + settingDeviceInfo: boolean; +} + +@Entry +@Component +struct BluetoothControlPage { + @State deviceId: string = ''; + @State deviceName: string = ''; + @State activeTab: number = 0; + @State isManualMode: boolean = false; + @State deviceTime: string = '--:--:--'; + @State loops: LoopInfo[] = []; + @State currentBrightness: number = 50; + @State showBrightnessDialog: boolean = false; + @State selectedLoopIndex: number = -1; + @State sunriseTime: string = '--:--'; + @State sunsetTime: string = '--:--'; + @State isLoading: boolean = false; + @State toastMessage: string = ''; + @State showToast: boolean = false; + @State showBackDialog: boolean = false; + @State isConnected: boolean = false; + + // 加载状态 + @State dataLoading: LoadingState = { + deviceTime: false, + deviceInfo: false, + sunRiseTime: false, + settingDeviceInfo: false + }; + + private tabs: TabItem[] = [ + { name: '手动控制', index: 0 }, + { name: '参数设置', index: 1 }, + { name: '自动模式', index: 2 }, + { name: '报警浏览', index: 3 }, + ]; + + // SPP UUID (串口通信) + private readonly SPP_UUID: string = '00001101-0000-1000-8000-00805F9B34FB'; + private clientNumber: number = -1; + + aboutToAppear(): void { + // 获取路由参数 + const params = router.getParams() as RouterParams; + if (params) { + this.deviceId = params.deviceId || ''; + this.deviceName = params.deviceName || '未知设备'; + } + + // 初始化10个回路 + this.initLoops(); + + // 连接设备 + this.connectSpp(); + } + + aboutToDisappear(): void { + this.disconnectSpp(); + } + + // 显示Toast + showToastMessage(message: string): void { + this.toastMessage = message; + this.showToast = true; + setTimeout(() => { + this.showToast = false; + }, 2000); + } + + // 初始化回路 + initLoops(): void { + this.loops = []; + for (let i = 0; i < 10; i++) { + this.loops.push({ + index: i, + name: `#${i + 1}`, + isOpen: false, + pwm: 0, + }); + } + } + + // 连接 SPP(串口协议) + connectSpp(): void { + if (!this.deviceId) { + console.error('设备ID为空'); + return; + } + + try { + // 使用 SPP 连接 + socket.sppConnect(this.deviceId, { + uuid: this.SPP_UUID, + secure: true, + type: socket.SppType.SPP_RFCOMM + }, (err: BusinessError, clientNumber: number) => { + if (err) { + console.error('SPP 连接失败:', JSON.stringify(err)); + this.showToastMessage('SPP 连接失败'); + return; + } + + this.clientNumber = clientNumber; + this.isConnected = true; + console.info('SPP 连接成功, clientNumber:', clientNumber); + + // 监听数据接收 + socket.on('sppRead', this.clientNumber, (data: ArrayBuffer) => { + this.handleReceivedData(data); + }); + + // 获取初始数据 + setTimeout(() => { + this.getDeviceTime(); + this.getManualControlInfo(); + }, 500); + }); + + } catch (err) { + console.error('SPP 连接异常:', JSON.stringify(err)); + this.showToastMessage('SPP 连接异常'); + } + } + + // 断开 SPP 连接 + disconnectSpp(): void { + if (this.clientNumber >= 0) { + try { + socket.off('sppRead', this.clientNumber); + socket.sppCloseClientSocket(this.clientNumber); + this.clientNumber = -1; + this.isConnected = false; + console.info('SPP 断开成功'); + } catch (err) { + console.error('SPP 断开失败:', JSON.stringify(err)); + } + } + } + + // 发送数据 + sendData(hexString: string): void { + console.log('hexString',hexString) + if (this.clientNumber < 0) { + console.error('未连接 SPP'); + return; + } + console.info('发送数据:', 1111); + try { + const data = this.hexStringToArrayBuffer(hexString); + socket.sppWrite(this.clientNumber, data); + console.info('发送数据:', hexString); + } catch (err) { + console.error('发送数据失败:', JSON.stringify(err)); + } + } + + // 处理接收到的数据 + handleReceivedData(data: ArrayBuffer): void { + const hexString = this.arrayBufferToHexString(data); + console.info('接收数据:', hexString); + + // 解析 M9Z 协议响应 + this.parseM9zResponse(hexString); + } + + // 解析 M9Z 响应 + parseM9zResponse(hexString: string): void { + const bytes = this.hexStringToBytes(hexString); + + // 验证帧格式 + if (bytes.length < 5 || bytes[0] !== 0xEE || bytes[bytes.length - 1] !== 0xFF) { + console.warn('无效的响应帧'); + return; + } + + const instruction = bytes[1]; + const cmdType = bytes[2]; + + // 检查是否为响应帧 (D7=1) + if ((instruction & 0x80) === 0) { + console.warn('非响应帧'); + return; + } + + const realInstruction = instruction & 0x7F; + + // 根据指令码处理 + switch (realInstruction) { + case 0x02: // 设备时间 + if (bytes.length >= 9 && cmdType === 0x01) { + const timestamp = this.bytesToUint32LE(bytes.slice(5, 9)); + const date = new Date(timestamp * 1000); + this.deviceTime = this.formatDateTime(date); + } + break; + case 0x01: // 手动控制 + if (bytes.length >= 20 && cmdType === 0x01) { + this.parseManualControlResponse(bytes); + } + break; + } + } + + // 解析手动控制响应 + parseManualControlResponse(bytes: number[]): void { + const disable = bytes[5] === 0x01; + this.isManualMode = disable; + + const relayBits = bytes[7] | (bytes[8] << 8); + + for (let i = 0; i < 10; i++) { + this.loops[i].isOpen = ((relayBits >> i) & 0x01) === 1; + this.loops[i].pwm = bytes[10 + i] || 0; + } + + // 触发UI更新 + this.loops = [...this.loops]; + } + + // 获取设备时间 + getDeviceTime(): void { + // M9Z 读取设备时间命令: EE 02 01 00 36 FF + const command = this.buildM9zCommand(0x02, 0x01, 0x00, []); + this.sendData(command); + } + + // 获取手动控制信息 + getManualControlInfo(): void { + // M9Z 读取手动控制命令: EE 01 01 00 35 FF + const command = this.buildM9zCommand(0x01, 0x01, 0x00, []); + this.sendData(command); + } + + // 同步设备时间 + syncDeviceTime(): void { + const timestamp = Math.floor(Date.now() / 1000); + const data = this.uint32ToBytesLE(timestamp); + const command = this.buildM9zCommand(0x02, 0x02, 0x00, data); + this.sendData(command); + + this.showToastMessage('同步时间中...'); + + setTimeout(() => { + this.getDeviceTime(); + }, 500); + } + + // 设置手动控制 + setManualControl(): void { + const data: number[] = new Array(15).fill(0); + + // Byte0: 手动控制使能 + data[0] = this.isManualMode ? 0x01 : 0x00; + data[1] = 0x00; + + // Byte2-3: 继电器状态位图 + let relayBits = 0; + for (let i = 0; i < 10; i++) { + if (this.loops[i].isOpen) { + relayBits |= (1 << i); + } + } + data[2] = relayBits & 0xFF; + data[3] = (relayBits >> 8) & 0xFF; + data[4] = 0x00; + + // Byte5-14: PWM值 + for (let i = 0; i < 10; i++) { + data[5 + i] = this.loops[i].pwm; + } + + const command = this.buildM9zCommand(0x01, 0x02, 0x00, data); + this.sendData(command); + } + + // 重启设备 (CMD: EE 7E 02 00 B3 FF) + restartDevice(): void { + const command = 'EE 7E 02 00 B3 FF'; + this.sendData(command); + this.showToastMessage('重启指令已发送'); + } + + // 构建 M9Z 命令 + buildM9zCommand(instruction: number, cmdType: number, index: number, data: number[]): string { + const payload: number[] = [instruction, cmdType, index]; + + if (data.length > 0) { + payload.push(data.length); + payload.push(...data); + } + + // 计算校验和 + let checksum = 0x33; + for (const byte of payload) { + checksum += byte; + } + checksum = checksum & 0xFF; + + // 构建完整帧 + const frame = [0xEE, ...payload, checksum, 0xFF]; + + return frame.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(''); + } + + // 切换回路状态 + toggleLoop(index: number): void { + if (!this.isManualMode) { + this.showToastMessage('请先切换到手动模式'); + return; + } + + this.loops[index].isOpen = !this.loops[index].isOpen; + this.loops = [...this.loops]; + this.setManualControl(); + } + + // 打开亮度调节 + openBrightnessDialog(index: number): void { + if (!this.isManualMode) { + this.showToastMessage('请先切换到手动模式'); + return; + } + + this.selectedLoopIndex = index; + this.currentBrightness = index >= 0 ? this.loops[index].pwm : 50; + this.showBrightnessDialog = true; + } + + // 确认亮度设置 + confirmBrightness(): void { + if (this.selectedLoopIndex >= 0) { + this.loops[this.selectedLoopIndex].pwm = this.currentBrightness; + } else { + // 全局设置 + for (let i = 0; i < 10; i++) { + this.loops[i].pwm = this.currentBrightness; + } + } + + this.loops = [...this.loops]; + this.showBrightnessDialog = false; + this.setManualControl(); + } + + // 全开 + turnOnAll(): void { + if (!this.isManualMode) { + this.showToastMessage('请先切换到手动模式'); + return; + } + + for (let i = 0; i < 10; i++) { + this.loops[i].isOpen = true; + } + this.loops = [...this.loops]; + this.setManualControl(); + } + + // 全关 + turnOffAll(): void { + if (!this.isManualMode) { + this.showToastMessage('请先切换到手动模式'); + return; + } + + for (let i = 0; i < 10; i++) { + this.loops[i].isOpen = false; + } + this.loops = [...this.loops]; + this.setManualControl(); + } + + // 切换模式 + toggleMode(): void { + this.isManualMode = !this.isManualMode; + this.setManualControl(); + } + + // 刷新数据 + refresh(): void { + this.getDeviceTime(); + this.getManualControlInfo(); + this.showToastMessage('刷新中...'); + } + + // 返回上一页 + goBack(): void { + this.showBackDialog = true; + } + + // 执行返回 + doGoBack(): void { + this.disconnectSpp(); + router.back(); + } + + // 工具方法 + hexStringToArrayBuffer(hexString: string): ArrayBuffer { + const cleanHex = hexString.replace(/\s+/g, ''); + const bytes = new Uint8Array(cleanHex.length / 2); + for (let i = 0; i < cleanHex.length; i += 2) { + bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16); + } + return bytes.buffer; + } + + arrayBufferToHexString(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + return Array.from(bytes).map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' '); + } + + hexStringToBytes(hexString: string): number[] { + const cleanHex = hexString.replace(/\s+/g, ''); + const bytes: number[] = []; + for (let i = 0; i < cleanHex.length; i += 2) { + bytes.push(parseInt(cleanHex.substr(i, 2), 16)); + } + return bytes; + } + + bytesToUint32LE(bytes: number[]): number { + return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + } + + uint32ToBytesLE(value: number): number[] { + return [ + value & 0xFF, + (value >> 8) & 0xFF, + (value >> 16) & 0xFF, + (value >> 24) & 0xFF + ]; + } + + formatDateTime(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + const h = String(date.getHours()).padStart(2, '0'); + const min = String(date.getMinutes()).padStart(2, '0'); + return `${y}-${m}-${d} ${h}:${min}`; + } + + // loading遮罩层 + @Builder + LoadingOverlay() { + if (this.dataLoading.deviceInfo || this.dataLoading.settingDeviceInfo) { + Stack() { + Rect() + .width('100%') + .height('100%') + .fill('rgba(11, 24, 48, 0.8)') + + Column() { + LoadingProgress() + .width(60) + .height(60) + .color('#22fdc8') + Text('数据加载中...') + .fontSize(14) + .fontColor('#eaf6fc') + .margin({ top: 10 }) + } + } + .width('100%') + .height('100%') + .position({ x: 0, y: 0 }) + .zIndex(999) + } + } + + // 构建手动控制页面 + @Builder + ManualControlContent() { + Stack() { + Column() { + // 设备信息 + Column() { + Text(`设备地址:${this.deviceId || 'N/A'}`) + .fontSize(12) + .fontColor('#b2ebf2') + .margin({ bottom: 4 }) + Text(`设备名称:${this.deviceName}`) + .fontSize(12) + .fontColor('#b2ebf2') + .margin({ bottom: 4 }) + Row() { + Text(`设备时间:${this.deviceTime}`) + .fontSize(12) + .fontColor('#eaf6fc') + if (this.dataLoading.deviceTime) { + LoadingProgress() + .width(12) + .height(12) + .color('#22fdc8') + .margin({ left: 4 }) + } + Blank() + Text(this.isConnected ? '已连接' : '未连接') + .fontSize(10) + .fontColor(this.isConnected ? '#4caf50' : '#ff5050') + } + .width('100%') + } + .width('100%') + .padding(10) + .backgroundColor('rgba(15, 30, 60, 0.4)') + .borderRadius(8) + .alignItems(HorizontalAlign.Start) + .margin({ bottom: 15 }) + + // 控制模式 + Row() { + Text('控制模式') + .fontSize(14) + .fontColor('#22fdc8') + Blank() + Button(this.isManualMode ? '手动模式' : '自动模式') + .fontSize(12) + .fontColor(this.isManualMode ? '#22fdc8' : '#b2ebf2') + .backgroundColor(this.isManualMode ? 'rgba(34, 253, 200, 0.1)' : 'rgba(20, 38, 81, 0.95)') + .border({ width: 1, color: this.isManualMode ? '#22fdc8' : '#213b67' }) + .borderRadius(4) + .padding({ left: 15, right: 15, top: 8, bottom: 8 }) + .onClick(() => this.toggleMode()) + } + .width('100%') + .margin({ bottom: 15 }) + + // 回路控制 + Row() { + Text('回路控制') + .fontSize(14) + .fontColor('#22fdc8') + if (this.dataLoading.deviceInfo) { + LoadingProgress() + .width(16) + .height(16) + .color('#22fdc8') + .margin({ left: 8 }) + } + } + .width('100%') + .margin({ bottom: 10 }) + + Grid() { + ForEach(this.loops, (loop: LoopInfo, index: number) => { + GridItem() { + Column() { + Row() { + Text('💡') + .fontSize(20) + } + .width(40) + .height(40) + .borderRadius(20) + .backgroundColor(loop.isOpen ? '#22fdc8' : '#2d3a51') + .border({ width: 1, color: loop.isOpen ? '#22fdc8' : '#3e7ba1' }) + .justifyContent(FlexAlign.Center) + + Text(`回路${index + 1}`) + .fontSize(12) + .fontColor('#eaf6fc') + .margin({ top: 4 }) + Text(loop.isOpen ? '开启' : '关闭') + .fontSize(10) + .fontColor('#b2ebf2') + } + .padding(8) + .onClick(() => this.toggleLoop(index)) + } + }) + } + .columnsTemplate('1fr 1fr 1fr 1fr 1fr') + .rowsGap(10) + .columnsGap(10) + .width('100%') + .margin({ bottom: 15 }) + + // 调光控制 + Text('调光控制') + .fontSize(14) + .fontColor('#22fdc8') + .width('100%') + .margin({ bottom: 10 }) + + Grid() { + ForEach(this.loops, (loop: LoopInfo, index: number) => { + GridItem() { + Column() { + Row() { + Text(`${loop.pwm}%`) + .fontSize(12) + .fontColor('#eaf6fc') + .fontWeight(FontWeight.Medium) + } + .width(50) + .height(50) + .borderRadius(25) + .backgroundColor(loop.pwm > 0 ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51') + .border({ width: 1, color: loop.pwm > 0 ? '#22fdc8' : '#3e7ba1' }) + .justifyContent(FlexAlign.Center) + + Text(`通道${index + 1}`) + .fontSize(10) + .fontColor('#b2ebf2') + .margin({ top: 4 }) + } + .onClick(() => this.openBrightnessDialog(index)) + } + }) + } + .columnsTemplate('1fr 1fr 1fr 1fr 1fr') + .rowsGap(10) + .columnsGap(10) + .width('100%') + .margin({ bottom: 15 }) + + // 快捷操作 + Row() { + Button('全开') + .fontSize(12) + .fontColor('#22fdc8') + .backgroundColor('rgba(34, 253, 200, 0.1)') + .border({ width: 1, color: '#22fdc8' }) + .borderRadius(4) + .layoutWeight(1) + .onClick(() => this.turnOnAll()) + + Button('全关') + .fontSize(12) + .fontColor('#fa5050') + .backgroundColor('rgba(250, 80, 80, 0.1)') + .border({ width: 1, color: '#fa5050' }) + .borderRadius(4) + .layoutWeight(1) + .margin({ left: 8 }) + .onClick(() => this.turnOffAll()) + + Button('一键调光') + .fontSize(12) + .fontColor('#ffcb4d') + .backgroundColor('rgba(255, 203, 77, 0.1)') + .border({ width: 1, color: '#ffcb4d' }) + .borderRadius(4) + .layoutWeight(1) + .margin({ left: 8 }) + .onClick(() => this.openBrightnessDialog(-1)) + } + .width('100%') + .margin({ bottom: 10 }) + + Row() { + Button(this.isLoading ? '同步中...' : '同步时间') + .fontSize(12) + .fontColor('#40a7fa') + .backgroundColor('rgba(64, 167, 250, 0.1)') + .border({ width: 1, color: '#40a7fa' }) + .borderRadius(4) + .layoutWeight(1) + .onClick(() => this.syncDeviceTime()) + + Button('刷新数据') + .fontSize(12) + .fontColor('#b2ebf2') + .backgroundColor('rgba(20, 38, 81, 0.95)') + .border({ width: 1, color: '#213b67' }) + .borderRadius(4) + .layoutWeight(1) + .margin({ left: 8 }) + .onClick(() => this.refresh()) + + Button('重启') + .fontSize(12) + .fontColor('#ff5050') + .backgroundColor('rgba(250, 80, 80, 0.1)') + .border({ width: 1, color: '#ff5050' }) + .borderRadius(4) + .layoutWeight(1) + .margin({ left: 8 }) + .onClick(() => this.restartDevice()) + } + .width('100%') + + // 日出日落时间 + Row() { + Text(`日出时间:${this.sunriseTime}`) + .fontSize(12) + .fontColor('#b2ebf2') + if (this.dataLoading.sunRiseTime) { + LoadingProgress() + .width(12) + .height(12) + .color('#22fdc8') + .margin({ left: 4 }) + } + Blank() + Text(`日落时间:${this.sunsetTime}`) + .fontSize(12) + .fontColor('#b2ebf2') + } + .width('100%') + .padding(10) + .backgroundColor('rgba(15, 30, 60, 0.4)') + .borderRadius(8) + .margin({ top: 15 }) + } + .width('100%') + + this.LoadingOverlay() + } + } + + // 构建参数设置页面 + @Builder + ParameterSettingsContent() { + Column() { + Text('参数设置') + .fontSize(16) + .fontColor('#22fdc8') + .margin({ bottom: 20 }) + + Text('功能开发中...') + .fontSize(14) + .fontColor('#8a9ba8') + } + .width('100%') + .padding(20) + .justifyContent(FlexAlign.Center) + } + + // 构建自动模式页面 + @Builder + AutoModeContent() { + Column() { + Text('自动模式') + .fontSize(16) + .fontColor('#22fdc8') + .margin({ bottom: 20 }) + + Text('功能开发中...') + .fontSize(14) + .fontColor('#8a9ba8') + } + .width('100%') + .padding(20) + .justifyContent(FlexAlign.Center) + } + + // 构建报警浏览页面 + @Builder + AlarmBrowserContent() { + Column() { + Text('报警浏览') + .fontSize(16) + .fontColor('#22fdc8') + .margin({ bottom: 20 }) + + Text('功能开发中...') + .fontSize(14) + .fontColor('#8a9ba8') + } + .width('100%') + .padding(20) + .justifyContent(FlexAlign.Center) + } + + build() { + Stack() { + Column() { + // Tab栏 + Row() { + ForEach(this.tabs, (tab: TabItem) => { + Column() { + Text(tab.name) + .fontSize(14) + .fontColor(this.activeTab === tab.index ? '#22fdc8' : '#b2ebf2') + .fontWeight(this.activeTab === tab.index ? FontWeight.Medium : FontWeight.Normal) + + if (this.activeTab === tab.index) { + Divider() + .strokeWidth(2) + .color('#22fdc8') + .width('100%') + .margin({ top: 8 }) + } + } + .layoutWeight(1) + .padding({ top: 12, bottom: 12 }) + .backgroundColor(this.activeTab === tab.index ? 'rgba(34, 253, 200, 0.1)' : 'transparent') + .onClick(() => { + this.activeTab = tab.index; + }) + }) + } + .width('100%') + .backgroundColor('rgba(20, 38, 81, 0.95)') + .border({ width: { bottom: 1 }, color: '#213b67' }) + + // 内容区域 + Scroll() { + Column() { + if (this.activeTab === 0) { + this.ManualControlContent() + } else if (this.activeTab === 1) { + this.ParameterSettingsContent() + } else if (this.activeTab === 2) { + this.AutoModeContent() + } else if (this.activeTab === 3) { + this.AlarmBrowserContent() + } + } + .padding(10) + } + .layoutWeight(1) + .scrollBar(BarState.Off) + } + .width('100%') + .height('100%') + .linearGradient({ + direction: GradientDirection.RightBottom, + colors: [['#0b1830', 0], ['#1d3553', 1]] + }) + + // Toast 提示 + if (this.showToast) { + Text(this.toastMessage) + .fontSize(14) + .fontColor('#ffffff') + .backgroundColor('rgba(0, 0, 0, 0.7)') + .padding({ left: 20, right: 20, top: 10, bottom: 10 }) + .borderRadius(20) + .position({ x: '50%', y: '80%' }) + .translate({ x: '-50%' }) + } + + // 亮度调节弹窗 + if (this.showBrightnessDialog) { + Column() { + Column() { + Text('亮度调节') + .fontSize(16) + .fontColor('#22fdc8') + .fontWeight(FontWeight.Medium) + .margin({ bottom: 20 }) + + Text(`${this.currentBrightness}%`) + .fontSize(32) + .fontColor('#eaf6fc') + .fontWeight(FontWeight.Bold) + .margin({ bottom: 15 }) + + Slider({ + value: this.currentBrightness, + min: 0, + max: 100, + step: 1 + }) + .trackColor('#2d3a51') + .selectedColor('#22fdc8') + .blockColor('#22fdc8') + .width('100%') + .onChange((value: number) => { + this.currentBrightness = value; + }) + + Row() { + Button('取消') + .fontSize(14) + .fontColor('#b2ebf2') + .backgroundColor('rgba(20, 38, 81, 0.95)') + .border({ width: 1, color: '#213b67' }) + .borderRadius(4) + .layoutWeight(1) + .onClick(() => { + this.showBrightnessDialog = false; + }) + + Button('确认') + .fontSize(14) + .fontColor('#22fdc8') + .backgroundColor('rgba(34, 253, 200, 0.1)') + .border({ width: 1, color: '#22fdc8' }) + .borderRadius(4) + .layoutWeight(1) + .margin({ left: 10 }) + .onClick(() => this.confirmBrightness()) + } + .width('100%') + .margin({ top: 20 }) + } + .width('80%') + .padding(20) + .backgroundColor('rgba(20, 38, 81, 0.98)') + .borderRadius(12) + .border({ width: 1, color: '#213b67' }) + } + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .justifyContent(FlexAlign.Center) + .onClick(() => { + this.showBrightnessDialog = false; + }) + } + + // 返回确认对话框 + if (this.showBackDialog) { + Column() { + Column() { + Text('提示') + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor('#eaf6fc') + .margin({ bottom: 15 }) + + Text('确定要断开蓝牙设备连接吗?') + .fontSize(14) + .fontColor('#b2ebf2') + .margin({ bottom: 20 }) + + Row() { + Button('取消') + .fontSize(14) + .fontColor('#999999') + .backgroundColor('rgba(255, 255, 255, 0.1)') + .borderRadius(4) + .layoutWeight(1) + .onClick(() => { + this.showBackDialog = false; + }) + + Button('断开') + .fontSize(14) + .fontColor('#ff5050') + .backgroundColor('rgba(255, 80, 80, 0.2)') + .borderRadius(4) + .layoutWeight(1) + .margin({ left: 15 }) + .onClick(() => { + this.showBackDialog = false; + this.doGoBack(); + }) + } + .width('100%') + } + .width('80%') + .padding(20) + .backgroundColor('rgba(20, 38, 81, 0.98)') + .borderRadius(12) + .border({ width: 1, color: '#213b67' }) + } + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .justifyContent(FlexAlign.Center) + } + } + .width('100%') + .height('100%') + } +} diff --git a/entry/src/main/ets/pages/BluetoothSearchPage.ets b/entry/src/main/ets/pages/BluetoothSearchPage.ets new file mode 100644 index 0000000..98f0e38 --- /dev/null +++ b/entry/src/main/ets/pages/BluetoothSearchPage.ets @@ -0,0 +1,511 @@ +/** + * 蓝牙设备搜索页面 + * 功能:搜索、配对、连接蓝牙设备 + */ +import { router } from '@kit.ArkUI'; +import { access, connection } from '@kit.ConnectivityKit'; +import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'; +import { BusinessError } from '@kit.BasicServicesKit'; + +// 设备信息接口 +interface BluetoothDevice { + deviceId: string; + name: string; + displayName: string; + address: string; + isPaired: boolean; + connected: boolean; +} + +@Entry +@Component +struct BluetoothSearchPage { + @State isSearching: boolean = false; + @State devices: BluetoothDevice[] = []; + @State bluetoothState: number = -1; // 0=关闭, 2=打开 + @State hasPermission: boolean = false; + @State connectedDeviceId: string = ''; + @State toastMessage: string = ''; + @State showToast: boolean = false; + @State showConfirmDialog: boolean = false; + @State confirmDialogDevice: BluetoothDevice | null = null; + + // 页面显示时初始化 + aboutToAppear(): void { + this.init(); + } + + // 页面销毁时清理 + aboutToDisappear(): void { + this.stopSearch(); + } + + // 初始化 + async init(): Promise { + await this.checkAndRequestPermission(); + await this.checkBluetoothState(); + this.registerBluetoothStateListener(); + } + + // 显示Toast提示 + showToastMessage(message: string): void { + this.toastMessage = message; + this.showToast = true; + setTimeout(() => { + this.showToast = false; + }, 2000); + } + + // 检查并请求蓝牙权限 + async checkAndRequestPermission(): Promise { + const permissions: Permissions[] = ['ohos.permission.ACCESS_BLUETOOTH']; + + try { + const atManager = abilityAccessCtrl.createAtManager(); + const context = getContext(this) as common.UIAbilityContext; + + // 请求权限 + const result = await atManager.requestPermissionsFromUser(context, permissions); + if (result.authResults[0] === 0) { + this.hasPermission = true; + console.info('蓝牙权限请求成功'); + return true; + } else { + this.hasPermission = false; + console.warn('蓝牙权限被拒绝'); + return false; + } + } catch (err) { + console.error('检查权限失败:', JSON.stringify(err)); + return false; + } + } + + // 检查蓝牙状态 + async checkBluetoothState(): Promise { + try { + const state = access.getState(); + this.bluetoothState = state; + console.info('蓝牙状态:', state); + return state; + } catch (err) { + console.error('获取蓝牙状态失败:', JSON.stringify(err)); + return -1; + } + } + + // 注册蓝牙状态监听 + registerBluetoothStateListener(): void { + try { + access.on('stateChange', (data: access.BluetoothState) => { + console.info('蓝牙状态变化:', data); + this.bluetoothState = data; + }); + } catch (err) { + console.error('注册蓝牙状态监听失败:', JSON.stringify(err)); + } + } + + // 打开蓝牙 + openBluetooth(): void { + try { + access.enableBluetooth(); + this.showToastMessage('正在打开蓝牙...'); + + // 等待蓝牙状态更新 + setTimeout(() => { + this.checkBluetoothState(); + }, 1000); + } catch (err) { + console.error('打开蓝牙失败:', JSON.stringify(err)); + this.showToastMessage('打开蓝牙失败'); + } + } + + // 开始搜索设备 + async startSearch(): Promise { + if (this.isSearching) { + return; + } + + // 检查权限 + if (!this.hasPermission) { + const granted = await this.checkAndRequestPermission(); + if (!granted) { + this.showToastMessage('请授予蓝牙权限'); + return; + } + } + + // 检查蓝牙状态 + const state = await this.checkBluetoothState(); + if (state !== access.BluetoothState.STATE_ON) { + this.showToastMessage('请先打开蓝牙'); + return; + } + + this.isSearching = true; + this.devices = []; + + this.showToastMessage('正在搜索设备...'); + + // 加载已配对设备 + this.loadPairedDevices(); + + // 注册设备发现监听 + try { + connection.on('bluetoothDeviceFind', (data: Array) => { + console.info('发现设备:', JSON.stringify(data)); + data.forEach((deviceId: string) => { + this.onDeviceFound(deviceId); + }); + }); + + // 开始发现设备 + connection.startBluetoothDiscovery(); + console.info('开始搜索蓝牙设备'); + + // 30秒后自动停止搜索 + setTimeout(() => { + this.stopSearch(); + if (this.devices.length > 0) { + this.showToastMessage(`找到 ${this.devices.length} 个设备`); + } else { + this.showToastMessage('未找到设备'); + } + }, 30000); + + } catch (err) { + console.error('开始搜索失败:', JSON.stringify(err)); + this.isSearching = false; + this.showToastMessage('搜索失败'); + } + } + + // 停止搜索 + stopSearch(): void { + this.isSearching = false; + + try { + connection.stopBluetoothDiscovery(); + connection.off('bluetoothDeviceFind'); + console.info('停止搜索'); + } catch (err) { + console.error('停止搜索失败:', JSON.stringify(err)); + } + } + + // 处理发现的设备 + onDeviceFound(deviceId: string): void { + // 检查设备是否已存在 + const exists = this.devices.find(d => d.deviceId === deviceId); + if (exists) { + return; + } + + try { + const deviceName = connection.getRemoteDeviceName(deviceId); + const displayName = deviceName || deviceId || '未知设备'; + + const deviceInfo: BluetoothDevice = { + deviceId: deviceId, + name: deviceName, + displayName: displayName, + address: deviceId, + isPaired: false, + connected: false, + }; + + this.devices = [...this.devices, deviceInfo]; + console.info('添加设备:', displayName, deviceId); + } catch (err) { + console.error('获取设备信息失败:', JSON.stringify(err)); + } + } + + // 加载已配对设备 + loadPairedDevices(): void { + try { + const pairedDevices = connection.getPairedDevices(); + console.info('已配对设备:', JSON.stringify(pairedDevices)); + + pairedDevices.forEach((deviceId: string) => { + const exists = this.devices.find(d => d.deviceId === deviceId); + if (!exists) { + const deviceName = connection.getRemoteDeviceName(deviceId); + const displayName = deviceName || deviceId || '未知设备'; + + const deviceInfo: BluetoothDevice = { + deviceId: deviceId, + name: deviceName, + displayName: displayName, + address: deviceId, + isPaired: true, + connected: false, + }; + + this.devices = [...this.devices, deviceInfo]; + console.info('添加已配对设备:', displayName); + } + }); + } catch (err) { + console.error('获取已配对设备失败:', JSON.stringify(err)); + } + } + + // 连接设备 + connectDevice(device: BluetoothDevice): void { + this.confirmDialogDevice = device; + this.showConfirmDialog = true; + } + + // 执行连接 + doConnect(device: BluetoothDevice): void { + this.showToastMessage('正在连接...'); + + try { + // 如果未配对,先进行配对 + if (!device.isPaired) { + connection.pairDevice(device.deviceId); + console.info('开始配对:', device.deviceId); + } + + // 更新设备状态 + device.connected = true; + device.isPaired = true; + this.connectedDeviceId = device.deviceId; + + // 保存最后连接的设备 + AppStorage.setOrCreate('lastBleAddress', device.deviceId); + + this.showToastMessage('连接成功'); + + // 跳转到控制页面 + setTimeout(() => { + this.navigateToControl(device); + }, 500); + + } catch (err) { + console.error('连接失败:', JSON.stringify(err)); + this.showToastMessage('连接失败,请重试'); + } + } + + // 跳转到控制页面 + navigateToControl(device: BluetoothDevice): void { + router.pushUrl({ + url: 'pages/BluetoothControlPage', + params: { + deviceId: device.deviceId, + deviceName: device.displayName + } + }).catch((err: BusinessError) => { + console.error('跳转失败:', JSON.stringify(err)); + }); + } + + // 判断是否为MAC地址格式 + isMacAddress(str: string): boolean { + if (!str) return false; + const macRegex = /^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$/; + return macRegex.test(str); + } + + build() { + Stack() { + Column() { + // 顶部搜索栏 + Row() { + Text('设备列表(鸿蒙)') + .fontSize(16) + .fontWeight(FontWeight.Medium) + .fontColor('#eaf6fc') + + Blank() + + Button(this.isSearching ? '搜索中...' : '搜索') + .fontSize(14) + .fontWeight(FontWeight.Medium) + .fontColor(this.isSearching ? '#eaf6fc' : '#0b1830') + .backgroundColor(this.isSearching ? 'rgba(34, 253, 200, 0.5)' : '#22fdc8') + .borderRadius(25) + .padding({ left: 20, right: 20, top: 8, bottom: 8 }) + .onClick(() => this.startSearch()) + } + .width('100%') + .padding(15) + .backgroundColor('rgba(15, 30, 60, 0.8)') + .borderRadius(8) + .margin({ bottom: 15 }) + + // 蓝牙状态提示 + if (this.bluetoothState !== access.BluetoothState.STATE_ON) { + Row() { + Text(this.bluetoothState === access.BluetoothState.STATE_OFF ? '蓝牙已关闭' : '蓝牙状态未知') + .fontSize(14) + .fontColor('#ff9800') + + Blank() + + Button('打开蓝牙') + .fontSize(13) + .fontWeight(FontWeight.Medium) + .fontColor('#ffffff') + .backgroundColor('#ff9800') + .borderRadius(25) + .padding({ left: 16, right: 16, top: 6, bottom: 6 }) + .onClick(() => this.openBluetooth()) + } + .width('100%') + .padding(12) + .backgroundColor('rgba(255, 152, 0, 0.15)') + .border({ width: 1, color: 'rgba(255, 152, 0, 0.5)' }) + .borderRadius(6) + .margin({ bottom: 15 }) + } + + // 设备列表 + if (this.devices.length === 0 && !this.isSearching) { + Text('暂无设备,请点击搜索按钮') + .fontSize(14) + .fontColor('#8a9ba8') + .margin({ top: 50 }) + } else { + List({ space: 10 }) { + ForEach(this.devices, (device: BluetoothDevice, index: number) => { + ListItem() { + Row() { + // 设备信息 + Column() { + Row() { + Text('设备名称:') + .fontSize(14) + .fontColor('#8a9ba8') + Text(device.displayName || device.name || '未知设备') + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor('#22fdc8') + .layoutWeight(1) + } + .width('100%') + .margin({ bottom: 4 }) + + Row() { + Text(this.isMacAddress(device.deviceId) ? 'MAC地址:' : '设备ID:') + .fontSize(12) + .fontColor('#8a9ba8') + Text(device.deviceId) + .fontSize(12) + .fontColor('#b2ebf2') + .layoutWeight(1) + } + .width('100%') + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + // 状态标签 + Text(device.isPaired ? '已配对' : '未配对') + .fontSize(12) + .fontColor(device.isPaired ? '#4caf50' : '#ff9800') + .backgroundColor(device.isPaired ? 'rgba(76, 175, 80, 0.1)' : 'rgba(255, 152, 0, 0.1)') + .border({ + width: 1, + color: device.isPaired ? '#4caf50' : '#ff9800' + }) + .borderRadius(25) + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + } + .width('100%') + .padding(15) + .backgroundColor('rgba(15, 30, 60, 0.8)') + .borderRadius(8) + .border({ width: 1, color: 'rgba(34, 253, 200, 0.2)' }) + .onClick(() => this.connectDevice(device)) + } + }) + } + .width('100%') + .layoutWeight(1) + } + } + .width('100%') + .height('100%') + .padding(10) + .linearGradient({ + direction: GradientDirection.RightBottom, + colors: [['#0b1830', 0], ['#1d3553', 1]] + }) + + // Toast 提示 + if (this.showToast) { + Text(this.toastMessage) + .fontSize(14) + .fontColor('#ffffff') + .backgroundColor('rgba(0, 0, 0, 0.7)') + .padding({ left: 20, right: 20, top: 10, bottom: 10 }) + .borderRadius(20) + .position({ x: '50%', y: '80%' }) + .translate({ x: '-50%' }) + } + + // 确认对话框 + if (this.showConfirmDialog && this.confirmDialogDevice) { + Column() { + Column() { + Text('提示') + .fontSize(18) + .fontWeight(FontWeight.Bold) + .fontColor('#eaf6fc') + .margin({ bottom: 15 }) + + Text(`确定要连接设备"${this.confirmDialogDevice.displayName}"吗?`) + .fontSize(14) + .fontColor('#b2ebf2') + .margin({ bottom: 20 }) + + Row() { + Button('取消') + .fontSize(14) + .fontColor('#999999') + .backgroundColor('rgba(255, 255, 255, 0.1)') + .borderRadius(4) + .layoutWeight(1) + .onClick(() => { + this.showConfirmDialog = false; + this.confirmDialogDevice = null; + }) + + Button('确定') + .fontSize(14) + .fontColor('#22fdc8') + .backgroundColor('rgba(34, 253, 200, 0.2)') + .borderRadius(4) + .layoutWeight(1) + .margin({ left: 15 }) + .onClick(() => { + if (this.confirmDialogDevice) { + this.doConnect(this.confirmDialogDevice); + } + this.showConfirmDialog = false; + this.confirmDialogDevice = null; + }) + } + .width('100%') + } + .width('80%') + .padding(20) + .backgroundColor('rgba(20, 38, 81, 0.98)') + .borderRadius(12) + .border({ width: 1, color: '#213b67' }) + } + .width('100%') + .height('100%') + .backgroundColor('rgba(0, 0, 0, 0.5)') + .justifyContent(FlexAlign.Center) + } + } + .width('100%') + .height('100%') + } +} diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..a868a79 --- /dev/null +++ b/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,159 @@ +/** + * 应用首页 + * 提供蓝牙设备控制入口 + */ +import { router } from '@kit.ArkUI'; +import { BusinessError } from '@kit.BasicServicesKit'; + +@Entry +@Component +struct Index { + @State message: string = '智能照明控制'; + + build() { + Column() { + // 顶部标题 + Text(this.message) + .fontSize(24) + .fontWeight(FontWeight.Bold) + .fontColor('#22fdc8') + .margin({ top: 60, bottom: 40 }) + + // Logo 或图标区域 + Column() { + Text('💡') + .fontSize(80) + } + .width(150) + .height(150) + .borderRadius(75) + .backgroundColor('rgba(34, 253, 200, 0.1)') + .border({ width: 2, color: '#22fdc8' }) + .justifyContent(FlexAlign.Center) + .margin({ bottom: 60 }) + + // 功能入口 + Column() { + // 蓝牙设备搜索 + Button() { + Row() { + Text('🔍') + .fontSize(24) + .margin({ right: 12 }) + Column() { + Text('蓝牙设备搜索') + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#eaf6fc') + Text('搜索并连接蓝牙设备') + .fontSize(12) + .fontColor('#8a9ba8') + .margin({ top: 4 }) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Text('→') + .fontSize(20) + .fontColor('#22fdc8') + } + .width('100%') + .padding({ left: 20, right: 20, top: 15, bottom: 15 }) + } + .width('100%') + .backgroundColor('rgba(15, 30, 60, 0.8)') + .border({ width: 1, color: 'rgba(34, 253, 200, 0.3)' }) + .borderRadius(12) + .onClick(() => { + router.pushUrl({ url: 'pages/BluetoothSearchPage' }).catch((err: BusinessError) => { + console.error('跳转失败:', JSON.stringify(err)); + }); + }) + + // 设备管理(占位) + Button() { + Row() { + Text('⚙️') + .fontSize(24) + .margin({ right: 12 }) + Column() { + Text('设备管理') + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#eaf6fc') + Text('管理已连接的设备') + .fontSize(12) + .fontColor('#8a9ba8') + .margin({ top: 4 }) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Text('→') + .fontSize(20) + .fontColor('#22fdc8') + } + .width('100%') + .padding({ left: 20, right: 20, top: 15, bottom: 15 }) + } + .width('100%') + .backgroundColor('rgba(15, 30, 60, 0.8)') + .border({ width: 1, color: 'rgba(34, 253, 200, 0.3)' }) + .borderRadius(12) + .margin({ top: 15 }) + .enabled(false) + .opacity(0.5) + + // 关于 + Button() { + Row() { + Text('ℹ️') + .fontSize(24) + .margin({ right: 12 }) + Column() { + Text('关于') + .fontSize(18) + .fontWeight(FontWeight.Medium) + .fontColor('#eaf6fc') + Text('版本信息和帮助') + .fontSize(12) + .fontColor('#8a9ba8') + .margin({ top: 4 }) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + + Text('→') + .fontSize(20) + .fontColor('#22fdc8') + } + .width('100%') + .padding({ left: 20, right: 20, top: 15, bottom: 15 }) + } + .width('100%') + .backgroundColor('rgba(15, 30, 60, 0.8)') + .border({ width: 1, color: 'rgba(34, 253, 200, 0.3)' }) + .borderRadius(12) + .margin({ top: 15 }) + .enabled(false) + .opacity(0.5) + } + .width('100%') + .padding({ left: 20, right: 20 }) + + // 底部版本信息 + Blank() + + Text('Version 1.0.0') + .fontSize(12) + .fontColor('#8a9ba8') + .margin({ bottom: 30 }) + } + .width('100%') + .height('100%') + .linearGradient({ + direction: GradientDirection.RightBottom, + colors: [['#0b1830', 0], ['#1d3553', 1]] + }) + } +} diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 new file mode 100644 index 0000000..e513578 --- /dev/null +++ b/entry/src/main/module.json5 @@ -0,0 +1,77 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "requestPermissions": [ + { + "name": "ohos.permission.ACCESS_BLUETOOTH", + "reason": "$string:bluetooth_permission_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, + { + "name": "ohos.permission.DISCOVER_BLUETOOTH", + "reason": "$string:bluetooth_permission_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + }, + { + "name": "ohos.permission.USE_BLUETOOTH", + "reason": "$string:bluetooth_permission_reason", + "usedScene": { + "abilities": ["EntryAbility"], + "when": "inuse" + } + } + ], + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "ohos.want.action.home" + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ] + } +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/color.json b/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000..3c71296 --- /dev/null +++ b/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/float.json b/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000..33ea223 --- /dev/null +++ b/entry/src/main/resources/base/element/float.json @@ -0,0 +1,8 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + } + ] +} diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000..cee1f63 --- /dev/null +++ b/entry/src/main/resources/base/element/string.json @@ -0,0 +1,20 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "智能照明控制应用" + }, + { + "name": "EntryAbility_desc", + "value": "智能照明控制" + }, + { + "name": "EntryAbility_label", + "value": "智能照明" + }, + { + "name": "bluetooth_permission_reason", + "value": "用于搜索、配对和连接蓝牙设备,实现智能照明控制功能" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/media/background.png b/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000..923f2b3 Binary files /dev/null and b/entry/src/main/resources/base/media/background.png differ diff --git a/entry/src/main/resources/base/media/foreground.png b/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000..97014d3 Binary files /dev/null and b/entry/src/main/resources/base/media/foreground.png differ diff --git a/entry/src/main/resources/base/media/layered_image.json b/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000..fb49920 --- /dev/null +++ b/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/entry/src/main/resources/base/media/startIcon.png b/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000..205ad8b Binary files /dev/null and b/entry/src/main/resources/base/media/startIcon.png differ diff --git a/entry/src/main/resources/base/profile/backup_config.json b/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000..78f40ae --- /dev/null +++ b/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/entry/src/main/resources/base/profile/main_pages.json b/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..41abdf0 --- /dev/null +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,7 @@ +{ + "src": [ + "pages/Index", + "pages/BluetoothSearchPage", + "pages/BluetoothControlPage" + ] +} diff --git a/entry/src/main/resources/dark/element/color.json b/entry/src/main/resources/dark/element/color.json new file mode 100644 index 0000000..79b11c2 --- /dev/null +++ b/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/entry/src/mock/mock-config.json5 b/entry/src/mock/mock-config.json5 new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/entry/src/mock/mock-config.json5 @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/entry/src/ohosTest/ets/test/Ability.test.ets b/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000..85c78f6 --- /dev/null +++ b/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/entry/src/ohosTest/ets/test/List.test.ets b/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000..794c7dc --- /dev/null +++ b/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/entry/src/ohosTest/module.json5 b/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000..b947666 --- /dev/null +++ b/entry/src/ohosTest/module.json5 @@ -0,0 +1,12 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "phone", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/entry/src/test/List.test.ets b/entry/src/test/List.test.ets new file mode 100644 index 0000000..bb5b5c3 --- /dev/null +++ b/entry/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/entry/src/test/LocalUnit.test.ets b/entry/src/test/LocalUnit.test.ets new file mode 100644 index 0000000..165fc16 --- /dev/null +++ b/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,33 @@ +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/hvigor/hvigor-config.json5 b/hvigor/hvigor-config.json5 new file mode 100644 index 0000000..fac94d0 --- /dev/null +++ b/hvigor/hvigor-config.json5 @@ -0,0 +1,23 @@ +{ + "modelVersion": "6.0.2", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + // "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/hvigorfile.ts b/hvigorfile.ts new file mode 100644 index 0000000..47113e2 --- /dev/null +++ b/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/oh-package-lock.json5 b/oh-package-lock.json5 new file mode 100644 index 0000000..c5a91ec --- /dev/null +++ b/oh-package-lock.json5 @@ -0,0 +1,28 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", + "@ohos/hypium@1.0.25": "@ohos/hypium@1.0.25" + }, + "packages": { + "@ohos/hamock@1.0.0": { + "name": "@ohos/hamock", + "version": "1.0.0", + "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har", + "registryType": "ohpm" + }, + "@ohos/hypium@1.0.25": { + "name": "@ohos/hypium", + "version": "1.0.25", + "integrity": "sha512-l6uO2pjl8HyEKdekLqQt7tUpWbDqX/42zoAzkagtUVZAW9jT6lMvbe54MVjoLxq/RwQGygRvi6j4GpypSMFSHw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.25.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/oh-package.json5 b/oh-package.json5 new file mode 100644 index 0000000..c95806d --- /dev/null +++ b/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "modelVersion": "6.0.2", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.25", + "@ohos/hamock": "1.0.0" + } +}