From 975eab89b2ae3b4cac54a2982a51b8cc4bc7d28f Mon Sep 17 00:00:00 2001 From: Lufaneng Date: Sun, 15 Feb 2026 08:50:03 +0800 Subject: [PATCH] 1 --- .../main/ets/pages/BluetoothControlPage.ets | 79 +++++- .../main/ets/pages/BluetoothSearchPage.ets | 17 +- entry/src/main/ets/pages/Index.ets | 4 + .../main/ets/pages/components/AutoMode.ets | 256 +++++++++++++++--- .../ets/pages/components/ManualControl.ets | 33 ++- entry/src/main/ets/utils/M9zService.ets | 4 + 6 files changed, 330 insertions(+), 63 deletions(-) diff --git a/entry/src/main/ets/pages/BluetoothControlPage.ets b/entry/src/main/ets/pages/BluetoothControlPage.ets index fec3d26..40e73fe 100644 --- a/entry/src/main/ets/pages/BluetoothControlPage.ets +++ b/entry/src/main/ets/pages/BluetoothControlPage.ets @@ -13,10 +13,16 @@ struct BluetoothControlPage { @State isConnected: boolean = false; @State statusMessage: string = '正在连接...'; @State currentIndex: number = 0; + @State showExitDialog: boolean = false; private m9zService: M9zService = M9zService.getInstance(); private controller: TabsController = new TabsController(); + onBackPress() { + this.showExitDialog = true; + return true; + } + aboutToAppear() { const params = router.getParams() as Record; if (params) { @@ -50,6 +56,7 @@ struct BluetoothControlPage { } build() { + Stack() { Column() { // 顶部导航栏 Row() { @@ -59,7 +66,7 @@ struct BluetoothControlPage { .fontColor('#fff') .margin({ right: 16 }) .onClick(() => { - router.back(); + this.showExitDialog = true; }) Column() { @@ -67,9 +74,18 @@ struct BluetoothControlPage { .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor('#eaf6fc') - Text(this.statusMessage) - .fontSize(12) - .fontColor(this.isConnected ? '#22fdc8' : '#fa5050') + + Row() { + Text(this.deviceId) + .fontSize(12) + .fontColor('#a0b0c0') + .margin({ right: 10 }) + + Text(this.statusMessage) + .fontSize(12) + .fontColor(this.isConnected ? '#22fdc8' : '#fa5050') + } + .margin({ top: 4 }) } .alignItems(HorizontalAlign.Start) @@ -134,6 +150,61 @@ struct BluetoothControlPage { .width('100%') .height('100%') .backgroundColor('#0a1629') // 深蓝色背景 + + // Exit Dialog + if (this.showExitDialog) { + 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.showExitDialog = false; + }) + + Button('确定') + .fontSize(14) + .fontColor('#fa5050') + .backgroundColor('rgba(255, 80, 80, 0.2)') + .borderRadius(4) + .layoutWeight(1) + .margin({ left: 15 }) + .onClick(() => { + this.m9zService.disconnect(); + this.showExitDialog = false; + router.back(); + }) + } + .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) + .zIndex(9999) + } + } } @Builder TabBuilder(index: number, name: string) { diff --git a/entry/src/main/ets/pages/BluetoothSearchPage.ets b/entry/src/main/ets/pages/BluetoothSearchPage.ets index 98f0e38..d82102e 100644 --- a/entry/src/main/ets/pages/BluetoothSearchPage.ets +++ b/entry/src/main/ets/pages/BluetoothSearchPage.ets @@ -255,6 +255,7 @@ struct BluetoothSearchPage { // 连接设备 connectDevice(device: BluetoothDevice): void { + this.stopSearch(); this.confirmDialogDevice = device; this.showConfirmDialog = true; } @@ -316,21 +317,27 @@ struct BluetoothSearchPage { Column() { // 顶部搜索栏 Row() { - Text('设备列表(鸿蒙)') + Text('设备列表') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor('#eaf6fc') Blank() - Button(this.isSearching ? '搜索中...' : '搜索') + Button(this.isSearching ? '停止搜索' : '开始搜索') .fontSize(14) .fontWeight(FontWeight.Medium) - .fontColor(this.isSearching ? '#eaf6fc' : '#0b1830') - .backgroundColor(this.isSearching ? 'rgba(34, 253, 200, 0.5)' : '#22fdc8') + .fontColor(this.isSearching ? '#ff9800' : '#0b1830') + .backgroundColor(this.isSearching ? 'rgba(255, 152, 0, 0.2)' : '#22fdc8') .borderRadius(25) .padding({ left: 20, right: 20, top: 8, bottom: 8 }) - .onClick(() => this.startSearch()) + .onClick(() => { + if (this.isSearching) { + this.stopSearch(); + } else { + this.startSearch(); + } + }) } .width('100%') .padding(15) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index a868a79..df743b5 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -10,6 +10,10 @@ import { BusinessError } from '@kit.BasicServicesKit'; struct Index { @State message: string = '智能照明控制'; + aboutToAppear() { + router.replaceUrl({ url: 'pages/BluetoothSearchPage' }); + } + build() { Column() { // 顶部标题 diff --git a/entry/src/main/ets/pages/components/AutoMode.ets b/entry/src/main/ets/pages/components/AutoMode.ets index 839da90..0f412fd 100644 --- a/entry/src/main/ets/pages/components/AutoMode.ets +++ b/entry/src/main/ets/pages/components/AutoMode.ets @@ -1,4 +1,4 @@ -import { M9zService, TaskProgram, TaskInstruction, M9zCmd, ControlTask, TaskLoop } from '../../utils/M9zService'; +import { M9zService, TaskProgram, TaskInstruction, M9zCmd, ControlTask, TaskLoop, SunRiseTimeData } from '../../utils/M9zService'; import { promptAction } from '@kit.ArkUI'; @Observed @@ -16,6 +16,8 @@ export struct AutoMode { @State startTask: ControlTask = this.createEmptyTask(); @State stopTask: ControlTask = this.createEmptyTask(); @State middleTasks: ControlTask[] = []; + @State sunriseTime: string = '--:--:--'; + @State sunsetTime: string = '--:--:--'; @State isLoading: boolean = false; @State isSetting: boolean = false; @@ -117,6 +119,26 @@ export struct AutoMode { } }); } + + fetchSunRiseTime() { + 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.isLoading = false; + }, + onError: (err: string): void => { + this.isLoading = false; + } + }); + } + + 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}`; + } changeViewMode(index: number) { this.selectedViewMode = index; @@ -141,12 +163,29 @@ export struct AutoMode { const instructions: TaskInstruction[] = []; // 1. Time Instruction - const parts: string[] = task.execTime.split(':'); - if (parts.length >= 2) { - const h: number = Number(parts[0]); - const m: number = Number(parts[1]); - const minutes: number = h * 60 + m; - + let h = 0; + let m = 0; + + if (task.execTime.includes(' ')) { + // Format: YYYY-MM-DD HH:mm:ss + const timePart = task.execTime.split(' ')[1]; + const parts = timePart.split(':'); + if (parts.length >= 2) { + h = Number(parts[0]); + m = Number(parts[1]); + } + } else { + // Format: HH:mm (or HH:mm:ss) + const parts = task.execTime.split(':'); + if (parts.length >= 2) { + h = Number(parts[0]); + m = Number(parts[1]); + } + } + + const minutes: number = h * 60 + m; + + if (true) { // Always generate instruction if we have time const ins1: TaskInstruction = new TaskInstruction(); ins1.type = 0x01; ins1.param = task.timeType === 0 ? 0x01 : 0x02; @@ -455,59 +494,194 @@ export struct AutoMode { .width('100%') // Control Panel - Row() { + Column() { + // Current Mode Display + Column() { + Text('⚙️ 当前运行模式') + .fontColor('#b2ebf2') + .fontSize(14) + .width('100%') + .margin({ bottom: 8 }) + + Row() { + Text(`自动模式${this.currentMode + 1}`) + .fontColor('#22fdc8') + .fontSize(16) + .fontWeight(FontWeight.Bold) + } + .width('100%') + .padding(12) + .backgroundColor('rgba(34, 253, 200, 0.1)') + .borderRadius(8) + .border({ width: 1, color: '#22fdc8' }) + } + .width('100%') + .padding(10) + .backgroundColor('rgba(15, 30, 60, 0.4)') + .borderRadius(8) + .margin({ bottom: 15 }) + Button(`应用模式${this.selectedViewMode+1}到设备`) .onClick((): void => {this.changeDeviceMode();}) - .width('80%') - .backgroundColor('#22fdc8') - .fontColor('#000') + .width('100%') + .backgroundColor('#102a45') + .fontColor('#22fdc8') + .border({ width: 1, color: '#22fdc8' }) + .height(44) } .width('100%') .justifyContent(FlexAlign.Center) .padding(10) + // 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) + .margin({bottom: 20}) + // Dialog if(this.showTaskDialog) { Stack() { + // Mask Rect().width('100%').height('100%').fill('rgba(0,0,0,0.8)').onClick((): void => {this.showTaskDialog = false;}) + + // Content Column() { - Text('编辑任务').fontColor('#fff').fontSize(16).margin({bottom: 10}) - - // Loops - Text('选择开启回路').fontColor('#b2ebf2').margin({bottom: 5}).alignSelf(ItemAlign.Start) - Grid() { - ForEach([0,1,2,3,4,5,6,7,8,9], (idx: number) => { - GridItem() { - Text(`${idx+1}`) - .backgroundColor(this.editingLoops.includes(idx) ? '#22fdc8' : '#333') - .fontColor(this.editingLoops.includes(idx) ? '#000' : '#fff') - .width(30).height(30).borderRadius(15) - .textAlign(TextAlign.Center) - .onClick((): void => {this.toggleEditingLoop(idx);}) - } - }) - }.columnsTemplate('1fr 1fr 1fr 1fr 1fr').height(80) + Text('编辑任务').fontColor('#fff').fontSize(16).margin({bottom: 10}).fontWeight(FontWeight.Bold) - // Time - Row() { - Text('时间').fontColor('#b2ebf2') - TextInput({ text: this.editingTask.execTime }).width(100).backgroundColor('#333').fontColor('#fff') - .onChange((val: string): void => {this.editingTask.execTime = val;}) - }.width('100%').margin({ top: 10 }).justifyContent(FlexAlign.SpaceBetween) + Scroll() { + Column() { + // 1. Time Type + Text('时间类型').fontColor('#b2ebf2').margin({top: 10, bottom: 5}).alignSelf(ItemAlign.Start).fontSize(14) + Row() { + Button('定时时间') + .backgroundColor(this.editingTask.timeType === 0 ? '#22fdc8' : '#333') + .fontColor(this.editingTask.timeType === 0 ? '#000' : '#fff') + .onClick(() => { this.editingTask.timeType = 0; }) + .height(30).fontSize(12).margin({right: 10}) + + Button('经纬度时间') + .backgroundColor(this.editingTask.timeType === 1 ? '#22fdc8' : '#333') + .fontColor(this.editingTask.timeType === 1 ? '#000' : '#fff') + .onClick(() => { this.editingTask.timeType = 1; }) + .height(30).fontSize(12) + }.width('100%').justifyContent(FlexAlign.Start) + + // 2. Time Input + if (this.editingTask.timeType === 0) { + Text('执行时间').fontColor('#b2ebf2').margin({top: 15, bottom: 5}).alignSelf(ItemAlign.Start).fontSize(14) + Row() { + TextInput({ text: this.editingTask.execTime.split(' ')[1] ? this.editingTask.execTime.split(' ')[1].substring(0,5) : '00:00' }) + .width(120).height(40) + .backgroundColor('#333').fontColor('#fff') + .borderRadius(5) + .onChange((val: string) => { + // Simple validation/update. better to use TimePicker but native TimePicker takes much space. + // Assume user types HH:mm. we append to today's date prefix on save or keep full string. + // Reference Vue: uses picker. Here let's just use text input for simplicity or check if we can use TimePickerDialog + // For now update logic: + if (val.length === 5) { + const today = new Date(); + const y = today.getFullYear(); + const m = String(today.getMonth() + 1).padStart(2, '0'); + const d = String(today.getDate()).padStart(2, '0'); + this.editingTask.execTime = `${y}-${m}-${d} ${val}:00`; + } + }) + + Button('选择时间') + .onClick(() => { + try { + TimePickerDialog.show({ + useMilitaryTime: true, + onAccept: (value: TimePickerResult) => { + const today = new Date(); + const y = today.getFullYear(); + const m = String(today.getMonth() + 1).padStart(2, '0'); + const d = String(today.getDate()).padStart(2, '0'); + const h = String(value.hour).padStart(2, '0'); + const min = String(value.minute).padStart(2, '0'); + this.editingTask.execTime = `${y}-${m}-${d} ${h}:${min}:00`; + } + }) + } catch(e) { + console.error('TimePicker error', e); + } + }) + .margin({left: 10}).fontSize(12).height(30).backgroundColor('#2d3a51') + .fontColor('#22fdc8').border({width:1, color: '#22fdc8'}) + }.width('100%').justifyContent(FlexAlign.Start) + } + + // 3. Loop Selection + Text('选择开启回路').fontColor('#b2ebf2').margin({top: 15, bottom: 5}).alignSelf(ItemAlign.Start).fontSize(14) + Grid() { + ForEach([0,1,2,3,4,5,6,7,8,9], (idx: number) => { + GridItem() { + Text(`${idx+1}`) + .backgroundColor(this.editingLoops.includes(idx) ? '#22fdc8' : '#333') + .fontColor(this.editingLoops.includes(idx) ? '#000' : '#fff') + .width(30).height(30).borderRadius(15) + .textAlign(TextAlign.Center) + .onClick((): void => {this.toggleEditingLoop(idx);}) + } + }) + }.columnsTemplate('1fr 1fr 1fr 1fr 1fr').height(80) + + // 4. Brightness Settings for Selected Loops + if (this.editingLoops.length > 0) { + Text('调光设置').fontColor('#b2ebf2').margin({top: 15, bottom: 5}).alignSelf(ItemAlign.Start).fontSize(14) + List() { + ForEach(this.editingLoops, (loopIdx: number) => { + ListItem() { + Row() { + Text(`通道${loopIdx+1}`).fontColor('#fff').fontSize(12).width(50) + Slider({ + value: this.editingTask.loops[loopIdx].pwm || 0, + min: 0, + max: 100, + style: SliderStyle.OutSet + }) + .layoutWeight(1) + .trackColor('#333') + .selectedColor('#22fdc8') + .onChange((val: number) => { + this.editingTask.loops[loopIdx].pwm = val; + }) + Text(`${Math.round(this.editingTask.loops[loopIdx].pwm || 0)}%`) + .fontColor('#22fdc8').fontSize(12).width(40).textAlign(TextAlign.End) + }.width('100%').padding({top: 5, bottom: 5}) + } + }) + }.height(150) + } // end if + }.width('100%') + } // end Scroll + .layoutWeight(1) + .width('100%') // Buttons Row() { - Button('取消').onClick((): void => {this.showTaskDialog = false;}).backgroundColor('#666') - Button('保存').onClick((): void => {this.saveTask();}).backgroundColor('#22fdc8').fontColor('#000') - }.width('100%').justifyContent(FlexAlign.SpaceAround).margin({ top: 20 }) + Button('取消').onClick((): void => {this.showTaskDialog = false;}).backgroundColor('#666').width('40%') + Button('保存').onClick((): void => {this.saveTask();}).backgroundColor('#22fdc8').fontColor('#000').width('40%') + }.width('100%').justifyContent(FlexAlign.SpaceAround).margin({ top: 10, bottom: 10 }) } .backgroundColor('#1d3553') .padding(20) .width('90%') + .height('80%') // Taller dialog .borderRadius(10) }.position({x:0,y:0}).zIndex(100) } } + .padding({ left: 15, right: 15, top: 15, bottom: 80 }) if (this.isLoading) { Stack() { @@ -529,7 +703,15 @@ export struct AutoMode { Row() { Text(title).fontColor('#22fdc8').fontWeight(FontWeight.Bold) Blank() - Text(task.execTime).fontColor('#fff') + Column() { + Text(task.timeType === 0 ? '定时时间' : '经纬度时间') + .fontSize(10) + .fontColor('#aaa') + .alignSelf(ItemAlign.End) + Text(task.execTime) + .fontColor('#fff') + .alignSelf(ItemAlign.End) + } }.width('100%').margin({ bottom: 5 }) Row() { diff --git a/entry/src/main/ets/pages/components/ManualControl.ets b/entry/src/main/ets/pages/components/ManualControl.ets index 4ccbe1f..05a9cfe 100644 --- a/entry/src/main/ets/pages/components/ManualControl.ets +++ b/entry/src/main/ets/pages/components/ManualControl.ets @@ -23,6 +23,7 @@ export struct ManualControl { private m9zService: M9zService = M9zService.getInstance(); private timer: number = -1; + private currentDeviceDate: Date | null = null; aboutToAppear() { this.initLoops(); @@ -45,13 +46,12 @@ export struct ManualControl { } startTimer() { + if (this.timer > 0) clearInterval(this.timer); this.timer = setInterval((): void => { - // 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. + if (this.currentDeviceDate) { + this.currentDeviceDate.setSeconds(this.currentDeviceDate.getSeconds() + 1); + this.deviceTime = this.formatDateTime(this.currentDeviceDate); + } }, 1000); } @@ -72,6 +72,7 @@ export struct ManualControl { } refreshData() { + if (this.isLoading) return; this.isLoading = true; // 1. Get Manual Info @@ -91,6 +92,7 @@ export struct ManualControl { // 2. Get Device Time this.m9zService.getDeviceTime({ onSuccess: (date: Date): void => { + this.currentDeviceDate = date; this.deviceTime = this.formatDateTime(date); this.fetchSunRiseTime(); }, @@ -104,11 +106,13 @@ export struct ManualControl { // 3. Get SunRise/Set Time this.m9zService.getSunRiseTime({ onSuccess: (data: SunRiseTimeData): void => { + console.info('ManualControl: fetchSunRiseTime success:', JSON.stringify(data)); if (data.sunrise) this.sunriseTime = this.formatTimeOnly(data.sunrise); if (data.sunset) this.sunsetTime = this.formatTimeOnly(data.sunset); this.checkLoadingComplete(); }, onError: (err: string): void => { + console.error('ManualControl: fetchSunRiseTime error:', err); this.checkLoadingComplete(); } }); @@ -257,7 +261,8 @@ export struct ManualControl { onSuccess: (success: boolean): void => { this.showToastMessage('时间同步成功'); // Refresh display - this.deviceTime = this.formatDateTime(new Date()); + this.currentDeviceDate = new Date(); + this.deviceTime = this.formatDateTime(this.currentDeviceDate); }, onError: (err: string): void => this.showToastMessage('同步失败:' + err) }); @@ -294,22 +299,16 @@ export struct ManualControl { 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) + .padding(10) .backgroundColor('rgba(15, 30, 60, 0.4)') .borderRadius(8) - .margin({bottom: 20}) + .margin({bottom: 10}) // --- Control Mode --- Column() { @@ -367,7 +366,7 @@ export struct ManualControl { } .columnsTemplate('1fr 1fr 1fr 1fr 1fr') .rowsGap(15) - .height(160) + .height(190) } .width('100%') .margin({bottom: 20}) @@ -471,7 +470,7 @@ export struct ManualControl { .borderRadius(8) } .width('100%') - .padding(15) + .padding({ left: 15, right: 15, top: 15, bottom: 80 }) } .width('100%') .height('100%') diff --git a/entry/src/main/ets/utils/M9zService.ets b/entry/src/main/ets/utils/M9zService.ets index e177961..e8e64a6 100644 --- a/entry/src/main/ets/utils/M9zService.ets +++ b/entry/src/main/ets/utils/M9zService.ets @@ -466,6 +466,7 @@ export class M9zService { } private parseSunRiseTime(data: number[]): SunRiseTimeData { + console.info('M9zService: parseSunRiseTime raw data:', JSON.stringify(data)); const res = new SunRiseTimeData(); // 假设返回两个 uint32 时间戳 (日出,日落) if (data.length >= 8) { @@ -473,6 +474,9 @@ export class M9zService { const setTs = this.bytesToUint32LE(data.slice(4, 8)); res.sunrise = new Date(riseTs * 1000); res.sunset = new Date(setTs * 1000); + console.info('M9zService: parsed sunrise:', res.sunrise, 'sunset:', res.sunset); + } else { + console.warn('M9zService: parseSunRiseTime data length insufficient:', data.length); } return res; }