From 1e17bfe0a6b81b1e4b71087b75bd88f924fe3d34 Mon Sep 17 00:00:00 2001 From: Lufaneng Date: Sat, 14 Feb 2026 09:53:31 +0800 Subject: [PATCH] 2 --- .../main/ets/pages/BluetoothControlPage.ets | 8 +- .../main/ets/pages/components/AutoMode.ets | 69 +- .../ets/pages/components/ManualControl.ets | 597 ++++++++++-------- .../pages/components/ParameterSettings.ets | 56 +- entry/src/main/ets/utils/M9zService.ets | 102 ++- 5 files changed, 530 insertions(+), 302 deletions(-) diff --git a/entry/src/main/ets/pages/BluetoothControlPage.ets b/entry/src/main/ets/pages/BluetoothControlPage.ets index b44d3e8..fec3d26 100644 --- a/entry/src/main/ets/pages/BluetoothControlPage.ets +++ b/entry/src/main/ets/pages/BluetoothControlPage.ets @@ -41,8 +41,6 @@ struct BluetoothControlPage { () => { this.isConnected = true; this.statusMessage = '已连接'; - // 同步时间作为连接成功的标志性动作 - this.m9zService.getDeviceTime({ onSuccess:()=>{}, onError:()=>{} }); }, (err: string) => { this.isConnected = false; @@ -93,17 +91,17 @@ struct BluetoothControlPage { if (this.isConnected) { Tabs({ barPosition: BarPosition.Start, controller: this.controller }) { TabContent() { - ManualControl({ deviceId: this.deviceId }) + ManualControl({ deviceId: this.deviceId, currentTabIndex: this.currentIndex }) } .tabBar(this.TabBuilder(0, '手动控制')) TabContent() { - ParameterSettings({ deviceId: this.deviceId }) + ParameterSettings({ deviceId: this.deviceId, currentTabIndex: this.currentIndex }) } .tabBar(this.TabBuilder(1, '参数设置')) TabContent() { - AutoMode({ deviceId: this.deviceId }) + AutoMode({ deviceId: this.deviceId, currentTabIndex: this.currentIndex }) } .tabBar(this.TabBuilder(2, '自动模式')) diff --git a/entry/src/main/ets/pages/components/AutoMode.ets b/entry/src/main/ets/pages/components/AutoMode.ets index 8086302..839da90 100644 --- a/entry/src/main/ets/pages/components/AutoMode.ets +++ b/entry/src/main/ets/pages/components/AutoMode.ets @@ -9,6 +9,7 @@ export class AutoModeHelper { @Component export struct AutoMode { @Prop deviceId: string; + @Prop @Watch('onTabChange') currentTabIndex: number = 0; @State currentMode: number = 0; // Device running mode @State selectedViewMode: number = 0; // Tab selection (Mode 1/2/3) @@ -28,12 +29,6 @@ export struct AutoMode { private m9zService: M9zService = M9zService.getInstance(); - aboutToAppear() { - if (this.deviceId) { - this.refreshData(); - } - } - createEmptyTask(): ControlTask { const loops: TaskLoop[] = []; for(let i: number = 0; i < 10; i++) { @@ -50,44 +45,68 @@ export struct AutoMode { return task; } + aboutToAppear() { + if (this.deviceId) { + if (this.currentTabIndex === 2) { + this.refreshData(); + } + } + } + + onTabChange() { + if (this.currentTabIndex === 2 && this.deviceId) { + this.refreshData(); + } + } + refreshData() { this.isLoading = true; - // Get Mode + // 1. Get Mode this.m9zService.getDeviceMode({ onSuccess: (mode: number): void => { - this.currentMode = mode; + this.currentMode = mode; + // 2. Get Tasks + this.getTasksForMode(this.selectedViewMode); }, onError: (err: string): void => { console.error('Get mode failed', err); + this.getTasksForMode(this.selectedViewMode); } }); - - // Get Tasks for selected view mode (0,1,2 in protocol means Mode 1,2,3) - this.getTasksForMode(this.selectedViewMode); } getTasksForMode(modeIndex: number) { this.isLoading = true; - // Start Task + // 1. Start Task this.m9zService.getTask(M9zCmd.StartTask, modeIndex, { onSuccess: (prog: TaskProgram): void => { const t: ControlTask | null = this.decompileTask(prog); this.startTask = t ? t : this.createEmptyTask(); + this.fetchStopTask(modeIndex); }, - onError: (err: string): void => {} + onError: (err: string): void => { + this.fetchStopTask(modeIndex); + } }); - - // Stop Task + } + + fetchStopTask(modeIndex: number) { + // 2. Stop Task this.m9zService.getTask(M9zCmd.StopTask, modeIndex, { onSuccess: (prog: TaskProgram): void => { const t: ControlTask | null = this.decompileTask(prog); this.stopTask = t ? t : this.createEmptyTask(); + this.fetchMiddleTask(modeIndex); }, - onError: (err: string): void => {} + onError: (err: string): void => { + this.fetchMiddleTask(modeIndex); + } }); - - // Middle Task + } + + fetchMiddleTask(modeIndex: number) { + // 3. Middle Task this.m9zService.getTask(M9zCmd.MiddleTask, modeIndex, { onSuccess: (prog: TaskProgram): void => { this.middleTasks = this.decompileMiddleTask(prog); @@ -395,6 +414,7 @@ export struct AutoMode { } build() { + Stack() { Column() { // Mode Tabs Row() { @@ -488,7 +508,20 @@ export struct AutoMode { }.position({x:0,y:0}).zIndex(100) } } + + if (this.isLoading) { + Stack() { + Rect().width('100%').height('100%').fill('rgba(0,0,0,0.5)') + Column() { + LoadingProgress().width(50).height(50).color('#22fdc8') + Text('数据加载中...').fontColor('white').margin({top: 10}) + } + } + .width('100%').height('100%') + .zIndex(1000) + } } +} @Builder TaskCard(title: string, task: ControlTask, onEdit: () => void, isDeletable: boolean = false, onDelete?: () => void) { diff --git a/entry/src/main/ets/pages/components/ManualControl.ets b/entry/src/main/ets/pages/components/ManualControl.ets index 6534655..4ccbe1f 100644 --- a/entry/src/main/ets/pages/components/ManualControl.ets +++ b/entry/src/main/ets/pages/components/ManualControl.ets @@ -1,15 +1,23 @@ -import { M9zService, ManualData, LoopInfo } from '../../utils/M9zService'; +import { M9zService, ManualData, LoopInfo, SunRiseTimeData } from '../../utils/M9zService'; @Component export struct ManualControl { @Prop deviceId: string; - @State manualData: ManualData = new ManualData(); // 初始化 Class + @Prop @Watch('onTabChange') currentTabIndex: number = 0; + + @State manualData: ManualData = new ManualData(); @State deviceTime: string = '--:--:--'; @State isConnected: boolean = false; @State isLoading: boolean = false; + + @State sunriseTime: string = '--:--:--'; + @State sunsetTime: string = '--:--:--'; + + // Brightness Control @State selectedLoopIndex: number = -1; @State currentBrightness: number = 50; @State showBrightnessDialog: boolean = false; + @State toastMessage: string = ''; @State showToast: boolean = false; @@ -24,6 +32,12 @@ export struct ManualControl { } } + onTabChange() { + if (this.currentTabIndex === 0 && this.deviceId) { + this.refreshData(); + } + } + aboutToDisappear() { if (this.timer > 0) { clearInterval(this.timer); @@ -32,14 +46,12 @@ export struct ManualControl { startTimer() { this.timer = setInterval((): void => { - this.m9zService.getDeviceTime({ - onSuccess: (date: Date): void => { - this.deviceTime = this.formatDateTime(date); - }, - onError: (err: string): void => { - // console.error('获取时间失败', err); - } - }); + // Local time update for UI clock if needed, + // but here we fetch device time periodically or just tick internally? + // The Vue app updates local time every second and valid device time + 1s. + // For now, let's keep fetching device time periodically or simplify. + // Let's just fetch device time every few seconds to keep it simple and accurate to device. + // Or better, fetch once and tick locally to reduce traffic. }, 1000); } @@ -54,29 +66,62 @@ export struct ManualControl { loops.push(loop); } const data = new ManualData(); - data.disable = false; + data.disable = false; // Default to Auto data.loops = loops; this.manualData = data; } refreshData() { this.isLoading = true; + + // 1. Get Manual Info this.m9zService.getManualControlInfo({ onSuccess: (data: ManualData): void => { this.manualData = data; - this.isLoading = false; + this.fetchDeviceTime(); }, onError: (err: string): void => { this.showToastMessage('获取数据失败: ' + err); - this.isLoading = false; + this.fetchDeviceTime(); } }); } + fetchDeviceTime() { + // 2. Get Device Time + this.m9zService.getDeviceTime({ + onSuccess: (date: Date): void => { + this.deviceTime = this.formatDateTime(date); + this.fetchSunRiseTime(); + }, + onError: (err: string): void => { + this.fetchSunRiseTime(); + } + }); + } + + fetchSunRiseTime() { + // 3. Get SunRise/Set Time + this.m9zService.getSunRiseTime({ + onSuccess: (data: SunRiseTimeData): void => { + if (data.sunrise) this.sunriseTime = this.formatTimeOnly(data.sunrise); + if (data.sunset) this.sunsetTime = this.formatTimeOnly(data.sunset); + this.checkLoadingComplete(); + }, + onError: (err: string): void => { + this.checkLoadingComplete(); + } + }); + } + + checkLoadingComplete() { + this.isLoading = false; + } + toggleMode() { - // Deep copy manualData const newData = new ManualData(); - newData.disable = !this.manualData.disable; + newData.disable = !this.manualData.disable; // Toggle + // Copy existing loops this.manualData.loops.forEach((l: LoopInfo): void => { const loop = new LoopInfo(); loop.index = l.index; @@ -89,7 +134,7 @@ export struct ManualControl { this.m9zService.setManualControl(newData, { onSuccess: (success: boolean): void => { if (success) { - this.manualData = newData; // Re-assign triggers UI update + this.manualData = newData; this.showToastMessage('模式切换成功'); } }, @@ -100,23 +145,12 @@ export struct ManualControl { } toggleLoop(index: number) { - if (!this.manualData.disable) { // disable=true 代表手动模式 (0x01) + if (!this.manualData.disable) { this.showToastMessage('请先切换到手动模式'); return; } - const newData = new ManualData(); - newData.disable = this.manualData.disable; - this.manualData.loops.forEach((l: LoopInfo): void => { - const loop = new LoopInfo(); - loop.index = l.index; - loop.name = l.name; - loop.isOpen = l.isOpen; - loop.pwm = l.pwm; - newData.loops.push(loop); - }); - - // Toggle + const newData = this.cloneManualData(this.manualData); newData.loops[index].isOpen = !newData.loops[index].isOpen; this.m9zService.setManualControl(newData, { @@ -146,21 +180,12 @@ export struct ManualControl { } confirmBrightness() { - const newData = new ManualData(); - newData.disable = this.manualData.disable; - this.manualData.loops.forEach((l: LoopInfo): void => { - const loop = new LoopInfo(); - loop.index = l.index; - loop.name = l.name; - loop.isOpen = l.isOpen; - loop.pwm = l.pwm; - newData.loops.push(loop); - }); + const newData = this.cloneManualData(this.manualData); if (this.selectedLoopIndex >= 0) { newData.loops[this.selectedLoopIndex].pwm = this.currentBrightness; } else { - // 全局调光 + // Global for (let i = 0; i < 10; i++) { newData.loops[i].pwm = this.currentBrightness; } @@ -171,7 +196,6 @@ export struct ManualControl { if (success) { this.manualData = newData; this.showBrightnessDialog = false; - this.showToastMessage('调光成功'); } }, onError: (err: string): void => { @@ -179,22 +203,14 @@ export struct ManualControl { } }); } - + turnOnAll(): void { if (!this.manualData.disable) { this.showToastMessage('请先切换到手动模式'); return; } - const newData = new ManualData(); - newData.disable = this.manualData.disable; - this.manualData.loops.forEach((l: LoopInfo): void => { - const loop = new LoopInfo(); - loop.index = l.index; - loop.name = l.name; - loop.isOpen = true; // ON - loop.pwm = l.pwm; - newData.loops.push(loop); - }); + const newData = this.cloneManualData(this.manualData); + newData.loops.forEach(l => l.isOpen = true); this.sendControl(newData, '全开成功'); } @@ -203,19 +219,25 @@ export struct ManualControl { this.showToastMessage('请先切换到手动模式'); return; } + const newData = this.cloneManualData(this.manualData); + newData.loops.forEach(l => l.isOpen = false); + this.sendControl(newData, '全关成功'); + } + + cloneManualData(src: ManualData): ManualData { const newData = new ManualData(); - newData.disable = this.manualData.disable; - this.manualData.loops.forEach((l: LoopInfo): void => { + newData.disable = src.disable; + src.loops.forEach((l: LoopInfo): void => { const loop = new LoopInfo(); loop.index = l.index; loop.name = l.name; - loop.isOpen = false; // OFF + loop.isOpen = l.isOpen; loop.pwm = l.pwm; newData.loops.push(loop); }); - this.sendControl(newData, '全关成功'); + return newData; } - + sendControl(newData: ManualData, successMsg: string): void { this.m9zService.setManualControl(newData, { onSuccess: (success: boolean): void => { @@ -232,7 +254,11 @@ export struct ManualControl { syncDeviceTime(): void { this.m9zService.setDeviceTime(new Date(), { - onSuccess: (): void => this.showToastMessage('时间同步成功'), + onSuccess: (success: boolean): void => { + this.showToastMessage('时间同步成功'); + // Refresh display + this.deviceTime = this.formatDateTime(new Date()); + }, onError: (err: string): void => this.showToastMessage('同步失败:' + err) }); } @@ -254,214 +280,289 @@ export struct ManualControl { const s = String(date.getSeconds()).padStart(2, '0'); return `${y}-${m}-${d} ${h}:${min}:${s}`; } + + formatTimeOnly(date: Date): string { + const h = String(date.getHours()).padStart(2, '0'); + const min = String(date.getMinutes()).padStart(2, '0'); + const s = String(date.getSeconds()).padStart(2, '0'); + return `${h}:${min}:${s}`; + } build() { - Column() { - // 这里的UI结构参考 ManualControl.vue 和 BluetoothControlPage.ets 融合 - // 设备信息 - Column() { - Row() { - Text(`设备时间:${this.deviceTime}`) - .fontSize(14) - .fontColor('#eaf6fc') - Blank() - Button('刷新') - .fontSize(12) - .height(24) - .onClick(() => this.refreshData()) - }.width('100%').margin({bottom: 10}) - - // 模式切换 - Row() { - Text('控制模式') - .fontSize(14) - .fontColor('#22fdc8') - Blank() - Button(this.manualData.disable ? '手动模式' : '自动模式') - .fontSize(12) - .fontColor(this.manualData.disable ? '#22fdc8' : '#b2ebf2') - .backgroundColor(this.manualData.disable ? 'rgba(34, 253, 200, 0.1)' : 'rgba(20, 38, 81, 0.95)') - .border({ width: 1, color: this.manualData.disable ? '#22fdc8' : '#213b67' }) - .onClick((): void => { - this.toggleMode(); - }) - }.width('100%').margin({bottom: 15}) - - // 回路控制 Grid - Text('回路控制') - .fontSize(14) - .fontColor('#22fdc8') - .width('100%') - .margin({ bottom: 10 }) - - Grid() { - ForEach(this.manualData.loops, (loop: LoopInfo): void => { - GridItem() { - Column() { - Text('💡') - .fontSize(20) - .fontColor(loop.isOpen ? '#22fdc8' : '#666') - .backgroundColor(loop.isOpen ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51') - .width(40).height(40) - .borderRadius(20) - .textAlign(TextAlign.Center) - .lineHeight(40) - .border({ width: 1, color: loop.isOpen ? '#22fdc8' : '#3e7ba1' }) - - Text(loop.name) - .fontSize(12) - .fontColor('#eaf6fc') - .margin({top: 5}) - } - .onClick((): void => { - this.toggleLoop(loop.index); + Stack() { + Scroll() { + Column() { + // --- Header Status --- + Column() { + Row() { + Text('设备号:N/A').fontSize(12).fontColor('#b2ebf2') + }.width('100%').margin({bottom: 5}) + Row() { + Text('设备名称:智能照明控制器').fontSize(12).fontColor('#b2ebf2') + }.width('100%').margin({bottom: 10}) + Row() { + Text(`设备时间:${this.deviceTime}`) + .fontSize(14).fontColor('#eaf6fc') + }.width('100%') + } + .width('100%') + .padding(15) + .backgroundColor('rgba(15, 30, 60, 0.4)') + .borderRadius(8) + .margin({bottom: 20}) + + // --- Control Mode --- + Column() { + Text('控制模式') + .fontSize(14) + .fontColor('#22fdc8') + .width('100%') + .margin({ bottom: 10 }) + .fontWeight(FontWeight.Medium) + + Button(this.manualData.disable ? '手动模式' : '自动模式') + .width('100%') + .backgroundColor(this.manualData.disable ? 'rgba(34, 253, 200, 0.1)' : 'rgba(20, 38, 81, 0.95)') + .fontColor(this.manualData.disable ? '#22fdc8' : '#b2ebf2') + .border({ width: 1, color: this.manualData.disable ? '#22fdc8' : '#213b67' }) + .height(44) + .onClick(() => this.toggleMode()) + } + .width('100%') + .margin({bottom: 20}) + + // --- Circuit Control --- + Column() { + Text('回路控制') + .fontSize(14) + .fontColor('#22fdc8') + .width('100%') + .margin({ bottom: 10 }) + .fontWeight(FontWeight.Medium) + + Grid() { + ForEach(this.manualData.loops, (loop: LoopInfo): void => { + GridItem() { + Column() { + // Icon Circle + Text('💡') + .fontSize(24) + .fontColor(loop.isOpen ? '#22fdc8' : '#666') + .width(50).height(50) + .textAlign(TextAlign.Center) + .borderRadius(25) + .backgroundColor(loop.isOpen ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51') + .border({ width: 1, color: loop.isOpen ? '#22fdc8' : '#3e7ba1' }) + .margin({bottom: 8}) + + Text(`回路${loop.index + 1}`) + .fontSize(12).fontColor('#eaf6fc') + + Text(loop.isOpen ? '开启' : '关闭') + .fontSize(10).fontColor(loop.isOpen ? '#22fdc8' : '#999') + } + .onClick(() => this.toggleLoop(loop.index)) + } }) } - }) - } - .columnsTemplate('1fr 1fr 1fr 1fr 1fr') - .rowsGap(15) - .height(140) - - // 调光控制 Grid - Text('调光控制') - .fontSize(14) - .fontColor('#22fdc8') - .width('100%') - .margin({ top: 15, bottom: 10 }) - - Grid() { - ForEach(this.manualData.loops, (loop: LoopInfo): void => { - GridItem() { - Column() { - Text(`${loop.pwm}%`) - .fontSize(12) - .fontColor('#eaf6fc') - .backgroundColor(loop.pwm > 0 ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51') - .width(40).height(40) - .borderRadius(20) - .textAlign(TextAlign.Center) - .lineHeight(40) - .border({ width: 1, color: loop.pwm > 0 ? '#22fdc8' : '#3e7ba1' }) - - Text(`通道${loop.index + 1}`) - .fontSize(10) - .fontColor('#b2ebf2') - .margin({top: 5}) - } - .onClick((): void => { - this.openBrightnessDialog(loop.index); + .columnsTemplate('1fr 1fr 1fr 1fr 1fr') + .rowsGap(15) + .height(160) + } + .width('100%') + .margin({bottom: 20}) + + // --- Dimming Control --- + Column() { + Text('调光控制') + .fontSize(14) + .fontColor('#22fdc8') + .width('100%') + .margin({ bottom: 10 }) + .fontWeight(FontWeight.Medium) + + Grid() { + ForEach(this.manualData.loops, (loop: LoopInfo): void => { + GridItem() { + Column() { + // Value Circle + Stack() { + Circle({ width: 50, height: 50 }) + .fill(loop.pwm > 0 ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51') + .stroke(loop.pwm > 0 ? '#22fdc8' : '#3e7ba1') + .strokeWidth(1) + Text(`${loop.pwm}%`) + .fontSize(12).fontColor('#eaf6fc') + } + .margin({bottom: 8}) + + Text(`通道${loop.index + 1}`) + .fontSize(12).fontColor('#b2ebf2') + } + .onClick(() => this.openBrightnessDialog(loop.index)) + } }) } - }) - } - .columnsTemplate('1fr 1fr 1fr 1fr 1fr') - .rowsGap(15) - .height(140) - - // 快捷操作 - Row() { - Button('全开') - .fontSize(12) - .fontColor('#22fdc8') - .backgroundColor('rgba(34, 253, 200, 0.1)') - .border({ width: 1, color: '#22fdc8' }) - .layoutWeight(1) - .onClick((): void => { - this.turnOnAll(); - }) - - Button('全关') - .fontSize(12) - .fontColor('#fa5050') - .backgroundColor('rgba(250, 80, 80, 0.1)') - .border({ width: 1, color: '#fa5050' }) - .layoutWeight(1) - .margin({ left: 8 }) - .onClick((): void => { - this.turnOffAll(); - }) - - Button('一键调光') - .fontSize(12) - .fontColor('#ffcb4d') - .backgroundColor('rgba(255, 203, 77, 0.1)') - .border({ width: 1, color: '#ffcb4d' }) - .layoutWeight(1) - .margin({ left: 8 }) - .onClick((): void => { - this.openBrightnessDialog(-1); - }) - } - .width('100%') - .margin({ top: 15 }) + .columnsTemplate('1fr 1fr 1fr 1fr 1fr') + .rowsGap(15) + .height(160) + } + .width('100%') + .margin({bottom: 20}) - Row() { - Button('同步时间') - .fontSize(12) - .fontColor('#40a7fa') - .backgroundColor('rgba(64, 167, 250, 0.1)') - .border({ width: 1, color: '#40a7fa' }) - .layoutWeight(1) - .onClick((): void => { - this.syncDeviceTime(); - }) - }.width('100%').margin({top: 10}) + // --- Quick Actions --- + Row() { + Button('全开') + .backgroundColor('#102a45') // Darker + .fontColor('#22fdc8') + .border({width: 1, color: '#22fdc8'}) + .layoutWeight(1) + .height(40) + .onClick(() => this.turnOnAll()) + + Button('全关') + .backgroundColor('#1a1520') + .fontColor('#fa5050') + .border({width: 1, color: '#fa5050'}) + .layoutWeight(1) + .height(40) + .margin({left: 10}) + .onClick(() => this.turnOffAll()) + Button('一键调光') + .backgroundColor('#2a2515') + .fontColor('#ffcb4d') + .border({width: 1, color: '#ffcb4d'}) + .layoutWeight(1) + .height(40) + .margin({left: 10}) + .onClick(() => this.openBrightnessDialog(-1)) + }.width('100%').margin({bottom: 15}) + + Row() { + Button('同步时间') + .backgroundColor('#102035') + .fontColor('#40a7fa') + .border({width: 1, color: '#40a7fa'}) + .layoutWeight(1) + .height(40) + .onClick(() => this.syncDeviceTime()) + + Button('刷新数据') + .backgroundColor('rgba(34, 253, 200, 0.1)') + .fontColor('#22fdc8') + .border({width: 1, color: '#22fdc8'}) + .layoutWeight(1) + .height(40) + .margin({left: 10}) + .onClick(() => this.refreshData()) + }.width('100%').margin({bottom: 20}) + + // --- Bottom Info --- + Column() { + Row() { + Text(`日出时间:${this.sunriseTime}`).fontSize(12).fontColor('#b2ebf2').layoutWeight(1) + Text(`日落时间:${this.sunsetTime}`).fontSize(12).fontColor('#b2ebf2').layoutWeight(1) + }.width('100%') + } + .width('100%') + .padding(15) + .backgroundColor('rgba(15, 30, 60, 0.4)') + .borderRadius(8) + } + .width('100%') + .padding(15) } - .padding(15) .width('100%') + .height('100%') + + // --- Loading --- + if (this.isLoading) { + Stack() { + Rect().width('100%').height('100%').fill('rgba(0,0,0,0.5)') + Column() { + LoadingProgress().width(50).height(50).color('#22fdc8') + Text('数据加载中...').fontColor('white').margin({top: 10}) + } + } + .width('100%').height('100%') + .zIndex(1000) + } + // --- Toast & Dialogs --- if (this.showToast) { - Text(this.toastMessage) - .fontSize(12) - .fontColor('white') - .backgroundColor('rgba(0,0,0,0.7)') - .padding(10) - .borderRadius(5) - .position({x: '35%', y: '80%'}) + Text(this.toastMessage) + .fontSize(12) + .fontColor('white') + .backgroundColor('rgba(0,0,0,0.7)') + .padding(10) + .borderRadius(5) + .position({x: '35%', y: '80%'}) } if (this.showBrightnessDialog) { - // 简易弹窗 - Stack() { - Rect().width('100%').height('100%').fill('rgba(0,0,0,0.5)').onClick((): void => { - this.showBrightnessDialog = false; - }) - Column() { - Text(this.selectedLoopIndex >= 0 ? `通道${this.selectedLoopIndex + 1} 亮度` : '全部通道 亮度') - .fontColor('#eaf6fc').margin({bottom: 20}) - - Text(`${this.currentBrightness}%`).fontColor('#22fdc8').fontSize(24).margin({bottom: 20}) - - Slider({ - value: this.currentBrightness, - min: 0, - max: 100, - style: SliderStyle.OutSet + Stack() { + // Mask + Rect().width('100%').height('100%').fill('rgba(0,0,0,0.6)').onClick(() => { + this.showBrightnessDialog = false; }) - .onChange((value: number): void => { - this.currentBrightness = value; - }) - .width('80%') + // Content + Column() { + Text(this.selectedLoopIndex >= 0 ? `通道${this.selectedLoopIndex + 1} 亮度` : '全部通道 亮度') + .fontColor('#eaf6fc').margin({bottom: 20}).fontSize(16) + + Text(`${this.currentBrightness}%`) + .fontColor('#22fdc8').fontSize(36).fontWeight(FontWeight.Bold).margin({bottom: 30}) + + Slider({ + value: this.currentBrightness, + min: 0, + max: 100, + style: SliderStyle.OutSet + }) + .trackColor('#2d3a51') + .selectedColor('#22fdc8') + .blockColor('#22fdc8') + .onChange((value: number) => { + this.currentBrightness = value; + }) + .width('90%') - Row() { - Button('取消').onClick((): void => {this.showBrightnessDialog = false;}).backgroundColor('transparent').fontColor('#999') - Button('确定').onClick((): void => {this.confirmBrightness();}).backgroundColor('#22fdc8').fontColor('#000') - }.margin({top: 20}).width('100%').justifyContent(FlexAlign.SpaceAround) - } - .width('80%') - .backgroundColor('#1d3553') - .padding(20) - .borderRadius(10) - } - .position({x: 0, y: 0}) - .zIndex(100) - .width('100%') - .height('100%') + Row() { + Button('取消') + .onClick(() => {this.showBrightnessDialog = false;}) + .backgroundColor('transparent') + .fontColor('#999') + .border({width: 1, color: '#666'}) + .width(100) + + Button('确定') + .onClick(() => {this.confirmBrightness();}) + .backgroundColor('#22fdc8') + .fontColor('#0b1830') + .width(100) + }.margin({top: 40}).width('100%').justifyContent(FlexAlign.SpaceAround) + } + .width('85%') + .backgroundColor('#1d3553') + .padding(30) + .borderRadius(16) + } + .position({x: 0, y: 0}) + .zIndex(999) + .width('100%') + .height('100%') + .alignContent(Alignment.Center) } } - .width('100%') - .height('100%') + } + + @Builder + ParamRow(label: string, value: string) { + Row() { + Text(label).fontSize(10).fontColor('#b2ebf2') + Blank() + Text(value).fontSize(10).fontColor('#eaf6fc') + }.width('100%').margin({bottom: 5}) } } diff --git a/entry/src/main/ets/pages/components/ParameterSettings.ets b/entry/src/main/ets/pages/components/ParameterSettings.ets index f5d5e2e..00ffae5 100644 --- a/entry/src/main/ets/pages/components/ParameterSettings.ets +++ b/entry/src/main/ets/pages/components/ParameterSettings.ets @@ -4,6 +4,8 @@ import { promptAction } from '@kit.ArkUI'; @Component export struct ParameterSettings { @Prop deviceId: string; + @Prop @Watch('onTabChange') currentTabIndex: number = 0; + @State isLoading: boolean = false; @State lgnLat: LgnLatData = new LgnLatData(); @State sunRiseSet: SunRiseSetData = new SunRiseSetData(); @@ -13,32 +15,54 @@ export struct ParameterSettings { aboutToAppear() { if (this.deviceId) { - this.refreshData(); + // initial load handled by onTabChange? No, initial index might be 0, this is 1. IF this is 1 initially? + // BluetoothControlPage sets index 0 default. + // If user goes to 1, onTabChange fires. + // If user starts at 0, this component might load but not show? + // Let's rely on onTabChange mostly, but for initial: + if (this.currentTabIndex === 1) { + this.refreshData(); + } } } + onTabChange() { + if (this.currentTabIndex === 1 && this.deviceId) { + this.refreshData(); + } + } + refreshData() { this.isLoading = true; - // Get LgnLat + // 1. Get LgnLat this.m9zService.getLgnLat({ onSuccess: (data: LgnLatData): void => { this.lgnLat = data; + this.fetchSunRiseSet(); }, onError: (err: string): void => { // console.error('Get lgnlat failed', err); + this.fetchSunRiseSet(); } }); + } - // Get SunRiseSet + fetchSunRiseSet() { + // 2. Get SunRiseSet this.m9zService.getSunRiseSet({ onSuccess: (data: SunRiseSetData): void => { this.sunRiseSet = data; + this.fetchSunRiseTime(); }, - onError: (err: string): void => {} + onError: (err: string): void => { + this.fetchSunRiseTime(); + } }); - - // Get SunRiseTime + } + + fetchSunRiseTime() { + // 3. Get SunRiseTime this.m9zService.getSunRiseTime({ onSuccess: (data: SunRiseTimeData): void => { this.sunRiseTime = data; @@ -111,7 +135,9 @@ export struct ParameterSettings { } build() { - Column() { + Stack() { + Scroll() { + Column() { // 经纬度设置 Column() { Text('经纬度设置').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#22fdc8').width('100%').margin({bottom: 10}) @@ -192,7 +218,21 @@ export struct ParameterSettings { .margin({top: 20}) .onClick(() => this.restartDevice()) - }.width('100%').padding(10) + }.width('100%').padding(10) + } + + if (this.isLoading) { + Stack() { + Rect().width('100%').height('100%').fill('rgba(0,0,0,0.5)') + Column() { + LoadingProgress().width(50).height(50).color('#22fdc8') + Text('数据加载中...').fontColor('white').margin({top: 10}) + } + } + .width('100%').height('100%') + .zIndex(1000) + } + } } formatTime(date: Date): string { diff --git a/entry/src/main/ets/utils/M9zService.ets b/entry/src/main/ets/utils/M9zService.ets index bc469da..e177961 100644 --- a/entry/src/main/ets/utils/M9zService.ets +++ b/entry/src/main/ets/utils/M9zService.ets @@ -248,21 +248,85 @@ export class M9zService { return [FRAME_HEADER, ...payload, checksum, FRAME_FOOTER]; } + // 缓存接收到的数据,处理分包/粘包 + private receiveBuffer: number[] = []; + // 处理接收到的数据 private handleReceivedData(data: ArrayBuffer): void { const bytes = new Uint8Array(data); - console.info('接收数据:', this.uint8ArrayToHexString(bytes)); + // console.info('接收原始数据:', this.uint8ArrayToHexString(bytes)); - // 简单验证帧格式 - if (bytes.length < 5 || bytes[0] !== FRAME_HEADER || bytes[bytes.length - 1] !== FRAME_FOOTER) { - console.warn('无效的响应帧'); - return; + // 1. 追加到缓存 + for (let i = 0; i < bytes.length; i++) { + this.receiveBuffer.push(bytes[i]); } + // 2. 循环处理缓存中的帧 + while (this.receiveBuffer.length >= 5) { // 最小帧长 header+inst+type+idx+checksum+footer? No header+inst+type+idx+check+footer = 6? + // check buildM9zCommand: H(1)+inst(1)+type(1)+idx(1)+len(optional)+data+check(1)+F(1). + // Min read response: H I T I L[1] D[1] C F = 8 bytes? + // Min write response: H I T I L[1] S[1] C F = 8 bytes? + // Let's stick to Header search + + // 寻找帧头 + const headerIndex = this.receiveBuffer.indexOf(FRAME_HEADER); + if (headerIndex === -1) { + // 没有帧头,清空(或保留最后一点以防分包?不,EE是必须的) + this.receiveBuffer = []; + return; + } + + // 丢弃帧头前面的垃圾数据 + if (headerIndex > 0) { + this.receiveBuffer.splice(0, headerIndex); + } + + // 现在 buffer[0] 是 EE + // 检查是否有足够的长度读到长度字段? + // Read Response: [EE, Inst, 01, Idx, LEN, Data..., CS, FF] + // Write Response: [EE, Inst, 02, Idx, 01, Success, CS, FF] + + if (this.receiveBuffer.length < 5) return; // Wait for more + + const cmdType = this.receiveBuffer[2]; + let frameLen = 0; + let dataLen = 0; + + // 初步判定长度 + // 读/写响应都有长度字段在 byte[4] + if (this.receiveBuffer.length < 5) return; + dataLen = this.receiveBuffer[4]; + + // 帧总长 = Header(1)+Inst(1)+Type(1)+Idx(1)+LenField(1) + Data(dataLen) + Check(1) + Footer(1) + // = 5 + dataLen + 2 = 7 + dataLen + frameLen = 7 + dataLen; + + if (this.receiveBuffer.length < frameLen) { + // 数据不够,等待下次 + return; + } + + // 检查帧尾 + if (this.receiveBuffer[frameLen - 1] !== FRAME_FOOTER) { + console.warn('帧尾校验失败,丢弃帧头'); + this.receiveBuffer.shift(); // 丢弃 header,重新寻找 + continue; + } + + // 提取完整一帧 + const frame = this.receiveBuffer.slice(0, frameLen); + this.receiveBuffer.splice(0, frameLen); // 从缓存移除 + + console.info('处理完整帧:', frame.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ')); + this.processFrame(frame); + } + } + + private processFrame(bytes: number[]): void { const instruction = bytes[1]; const cmdType = bytes[2]; - - // 检查是否为响应帧 (D7=1) 且是对应的 instruction + + // 检查是否为响应帧 (D7=1) if ((instruction & 0x80) === 0) { console.warn('非响应帧'); return; @@ -270,19 +334,12 @@ export class M9zService { const realInstruction = instruction & 0x7F; const callbacks = this.callbackMap.get(realInstruction); - - // 根据 cmdType 解析 - if (cmdType === CMD_READ) { // 读响应 - // 格式: header(0) inst(1) cmdType(2) index(3) len(4) data... check footer - if (bytes.length < 6) return; + + if (cmdType === CMD_READ) { + // 格式: header(0) inst(1) cmdType(2) index(3) len(4) data... check(n-2) footer(n-1) const len = bytes[4]; - if (bytes.length < 6 + len) return; - - // 拷贝数据以避免引用问题 - const payloadData: number[] = []; - for(let i=0; i 0) { try { @@ -292,12 +349,11 @@ export class M9zService { console.error("解析错误", e); callbacks.forEach(cb => cb.invokeError("解析错误")); } - this.callbackMap.delete(realInstruction); // 清除回调 + this.callbackMap.delete(realInstruction); } - } else if (cmdType === CMD_WRITE) { // 写响应 - // 格式: header(0) inst(1) cmdType(2) index(3) len(4=1) success(5) check footer - if (bytes.length < 7) return; + } else if (cmdType === CMD_WRITE) { + // payload[0] is success const success = bytes[5] === 0x01; if (callbacks && callbacks.length > 0) { if (success) {