import { M9zService, TaskProgram, TaskInstruction, M9zCmd, ControlTask, TaskLoop, SunRiseTimeData } from '../../utils/M9zService'; import { promptAction } from '@kit.ArkUI'; @Observed export class AutoModeHelper { // Helper to avoid Observed restrictions if any } @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) @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; // Dialog state @State showTaskDialog: boolean = false; @State editingTaskType: string = ''; // 'start', 'stop', 'middle' @State editingTaskIndex: number = -1; @State editingTask: ControlTask = this.createEmptyTask(); @State editingLoops: number[] = []; // selected loop indices private m9zService: M9zService = M9zService.getInstance(); createEmptyTask(): ControlTask { const loops: TaskLoop[] = []; for(let i: number = 0; i < 10; i++) { const loop: TaskLoop = new TaskLoop(); loop.index = i; loop.turnOn = false; loop.pwm = 0; loops.push(loop); } const task: ControlTask = new ControlTask(); task.execTime = '00:00'; task.timeType = 0; task.loops = loops; 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; // 1. Get Mode this.m9zService.getDeviceMode({ onSuccess: (mode: number): void => { this.currentMode = mode; // 2. Get Tasks this.getTasksForMode(this.selectedViewMode); }, onError: (err: string): void => { console.error('Get mode failed', err); this.getTasksForMode(this.selectedViewMode); } }); } getTasksForMode(modeIndex: number) { this.isLoading = true; // 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 => { this.fetchStopTask(modeIndex); } }); } 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 => { this.fetchMiddleTask(modeIndex); } }); } fetchMiddleTask(modeIndex: number) { // 3. Middle Task this.m9zService.getTask(M9zCmd.MiddleTask, modeIndex, { onSuccess: (prog: TaskProgram): void => { this.middleTasks = this.decompileMiddleTask(prog); this.isLoading = false; }, onError: (err: string): void => { this.isLoading = false; } }); } 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; this.refreshData(); } changeDeviceMode() { // Set device mode to current view mode this.m9zService.setDeviceMode(this.selectedViewMode, { onSuccess: (val: boolean): void => { this.currentMode = this.selectedViewMode; promptAction.showToast({ message: '模式切换成功' }); }, onError: (err: string): void => { promptAction.showToast({ message: '切换失败: ' + err }); } }); } // Compiler Logic compileTask(task: ControlTask): TaskProgram { const instructions: TaskInstruction[] = []; // 1. Time Instruction 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; ins1.value = minutes; instructions.push(ins1); } // 2. Output Instructions // Group by PWM const pwmMap: Map = new Map(); let openMask: number = 0; let closeMask: number = 0; task.loops.forEach((loop: TaskLoop) => { if (loop.turnOn) { openMask |= (1 << loop.index); } else { closeMask |= (1 << loop.index); } if (loop.pwm > 0) { const list: number[] = pwmMap.get(loop.pwm) || []; list.push(loop.index); pwmMap.set(loop.pwm, list); } }); // PWM Instructions pwmMap.forEach((indices: number[], pwm: number) => { let mask: number = 0; indices.forEach((idx: number) => { mask |= (1 << idx); }); const ins: TaskInstruction = new TaskInstruction(); ins.type = 0x04; ins.param = 0x03; ins.value = (mask << 12) | pwm; instructions.push(ins); }); // Relay Instructions if (openMask > 0) { const ins: TaskInstruction = new TaskInstruction(); ins.type = 0x04; ins.param = 0x01; ins.value = (openMask << 12) | 0x01; instructions.push(ins); } if (closeMask > 0) { const ins: TaskInstruction = new TaskInstruction(); ins.type = 0x04; ins.param = 0x01; ins.value = (closeMask << 12) | 0x00; instructions.push(ins); } const prog: TaskProgram = new TaskProgram(); prog.index = this.selectedViewMode; prog.instructions = instructions; return prog; } compileMiddleTasks(tasks: ControlTask[]): TaskProgram { const allInstructions: TaskInstruction[] = []; // Sort tasks by time? The user should ensure order, simplified here. tasks.forEach((task: ControlTask) => { const prog: TaskProgram = this.compileTask(task); prog.instructions.forEach((ins: TaskInstruction) => { allInstructions.push(ins); }); }); // Add Loop/GOTO instruction at end const gotoIns: TaskInstruction = new TaskInstruction(); gotoIns.type = 0x02; // GOTO gotoIns.param = 0x01; // Enable gotoIns.value = 0; // Jump to 0, count 0 allInstructions.push(gotoIns); const prog: TaskProgram = new TaskProgram(); prog.index = this.selectedViewMode; prog.instructions = allInstructions; return prog; } // Decompiler Logic decompileTask(prog: TaskProgram): ControlTask | null { if (!prog.instructions || prog.instructions.length === 0) { return null; } const task: ControlTask = this.createEmptyTask(); let hasTime: boolean = false; for (const ins of prog.instructions) { if (ins.type === 0x01) { // CMP hasTime = true; const minutes: number = ins.value; const h: number = Math.floor(minutes / 60); const m: number = minutes % 60; task.execTime = `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`; task.timeType = (ins.param === 0x01) ? 0 : 1; } else if (ins.type === 0x04) { // OUTPUT if (ins.param === 0x01) { // Relay const isOpen: boolean = (ins.value & 1) === 1; const mask: number = (ins.value >> 12) & 0xFFF; for (let i: number = 0; i < 10; i++) { if ((mask >> i) & 1) { task.loops[i].turnOn = isOpen; } } } else if (ins.param === 0x03) { // PWM const pwm: number = ins.value & 0xFF; const mask: number = (ins.value >> 12) & 0xFFF; for (let i: number = 0; i < 10; i++) { if ((mask >> i) & 1) { task.loops[i].pwm = pwm; } } } } } return hasTime ? task : null; } decompileMiddleTask(prog: TaskProgram): ControlTask[] { const tasks: ControlTask[] = []; let currentTask: ControlTask | null = null; for (const ins of prog.instructions) { if (ins.type === 0x01) { // New Time instruction = New Task if (currentTask) { tasks.push(currentTask); } currentTask = this.createEmptyTask(); const minutes: number = ins.value; const h: number = Math.floor(minutes / 60); const m: number = minutes % 60; currentTask.execTime = `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`; if (ins.param === 0x01 || ins.param === 0x05) { currentTask.timeType = 0; } else { currentTask.timeType = 1; } } else if (ins.type === 0x04 && currentTask) { if (ins.param === 0x01) { // Relay const isOpen: boolean = (ins.value & 1) === 1; const mask: number = (ins.value >> 12) & 0xFFF; for (let i: number = 0; i < 10; i++) { if ((mask >> i) & 1) { currentTask.loops[i].turnOn = isOpen; } } } else if (ins.param === 0x03) { // PWM const pwm: number = ins.value & 0xFF; const mask: number = (ins.value >> 12) & 0xFFF; for (let i: number = 0; i < 10; i++) { if ((mask >> i) & 1) { currentTask.loops[i].pwm = pwm; } } } } } if (currentTask) { tasks.push(currentTask); } return tasks; } // --- UI Actions --- openEditDialog(type: string, index: number) { if (type === 'start') { // Deep copy const t: ControlTask = this.startTask; const copy: ControlTask = this.createEmptyTask(); copy.execTime = (t.execTime.includes(' ') ? t.execTime.split(' ')[1] : t.execTime).substring(0, 5); copy.timeType = t.timeType; t.loops.forEach((l: TaskLoop, i: number) => { copy.loops[i].turnOn = l.turnOn; copy.loops[i].pwm = l.pwm; }); this.editingTask = copy; } else if (type === 'stop') { const t: ControlTask = this.stopTask; const copy: ControlTask = this.createEmptyTask(); copy.execTime = (t.execTime.includes(' ') ? t.execTime.split(' ')[1] : t.execTime).substring(0, 5); copy.timeType = t.timeType; t.loops.forEach((l: TaskLoop, i: number) => { copy.loops[i].turnOn = l.turnOn; copy.loops[i].pwm = l.pwm; }); this.editingTask = copy; } else if (type === 'middle') { const t: ControlTask = index >= 0 ? this.middleTasks[index] : this.createEmptyTask(); const copy: ControlTask = this.createEmptyTask(); copy.execTime = (t.execTime.includes(' ') ? t.execTime.split(' ')[1] : t.execTime).substring(0, 5); copy.timeType = t.timeType; t.loops.forEach((l: TaskLoop, i: number) => { copy.loops[i].turnOn = l.turnOn; copy.loops[i].pwm = l.pwm; }); this.editingTask = copy; } this.editingTaskType = type; this.editingTaskIndex = index; this.editingLoops = []; this.editingTask.loops.forEach((l: TaskLoop) => { if (l.turnOn) { this.editingLoops.push(l.index); } }); this.showTaskDialog = true; } toggleEditingLoop(index: number) { const idx: number = this.editingLoops.indexOf(index); if (idx >= 0) { this.editingLoops.splice(idx, 1); } else { this.editingLoops.push(index); } } saveTask() { // Apply editing loops this.editingTask.loops.forEach((l: TaskLoop) => { l.turnOn = this.editingLoops.includes(l.index); }); if (this.editingTaskType === 'start') { this.startTask = this.editingTask; const prog: TaskProgram = this.compileTask(this.startTask); this.m9zService.setTask(M9zCmd.StartTask, prog, { onSuccess:():void=>{}, onError:(e:string):void=>{} }); } else if (this.editingTaskType === 'stop') { this.stopTask = this.editingTask; const prog: TaskProgram = this.compileTask(this.stopTask); this.m9zService.setTask(M9zCmd.StopTask, prog, { onSuccess:():void=>{}, onError:(e:string):void=>{} }); } else if (this.editingTaskType === 'middle') { if (this.editingTaskIndex >= 0) { this.middleTasks[this.editingTaskIndex] = this.editingTask; } else { this.middleTasks.push(this.editingTask); // Add new } const prog: TaskProgram = this.compileMiddleTasks(this.middleTasks); this.m9zService.setTask(M9zCmd.MiddleTask, prog, { onSuccess:():void=>{}, onError:(e:string):void=>{} }); } this.showTaskDialog = false; promptAction.showToast({ message: '任务保存成功' }); } deleteMiddleTask(index: number) { this.middleTasks.splice(index, 1); const prog: TaskProgram = this.compileMiddleTasks(this.middleTasks); this.m9zService.setTask(M9zCmd.MiddleTask, prog, { onSuccess:():void=>{ promptAction.showToast({ message: '删除成功' }); }, onError:(err:string):void=> { promptAction.showToast({ message: '删除失败' }); } }); } build() { Stack() { Column() { // Mode Tabs Row() { ForEach([0,1,2], (mode: number) => { Button(`模式${mode+1}`) .backgroundColor(this.selectedViewMode === mode ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51') .fontColor('#eaf6fc') .onClick((): void => {this.changeViewMode(mode);}) .layoutWeight(1) }) }.width('100%').margin({ bottom: 10 }) List() { ListItem() { TaskItem({ title: '开始任务', task: this.startTask, onEdit: (): void => { this.openEditDialog('start', -1); } }) } ForEach(this.middleTasks, (task: ControlTask, index: number) => { ListItem() { TaskItem({ title: `中间任务 ${index+1}`, task: task, onEdit: (): void => { this.openEditDialog('middle', index); }, isDeletable: true, onDelete: (): void => { this.deleteMiddleTask(index); } }) } }) ListItem() { Button('+ 添加中间任务') .width('100%') .backgroundColor('rgba(34, 253, 200, 0.1)') .fontColor('#22fdc8') .onClick((): void => {this.openEditDialog('middle', -1);}) // -1 for new .margin({ top: 10, bottom: 10 }) } ListItem() { TaskItem({ title: '结束任务', task: this.stopTask, onEdit: (): void => { this.openEditDialog('stop', -1); } }) } } .layoutWeight(1) .width('100%') // Control Panel 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('100%') .backgroundColor('#102a45') .fontColor('#22fdc8') .border({ width: 1, color: '#22fdc8' }) .height(44) } .width('100%') .justifyContent(FlexAlign.Center) .padding(10) // 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}).fontWeight(FontWeight.Bold) 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.includes(' ') ? this.editingTask.execTime.split(' ')[1].substring(0,5) : this.editingTask.execTime }) .width(120).height(40) .backgroundColor('#333').fontColor('#fff') .borderRadius(5) .onChange((val: string) => { if (val.length === 5) { this.editingTask.execTime = val; } }) Button('选择时间') .onClick(() => { try { TimePickerDialog.show({ useMilitaryTime: true, onAccept: (value: TimePickerResult) => { const h = String(value.hour).padStart(2, '0'); const min = String(value.minute).padStart(2, '0'); this.editingTask.execTime = `${h}:${min}`; } }) } 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').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() { 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) } } } } @Component struct TaskItem { @Prop title: string; @ObjectLink task: ControlTask; onEdit: () => void = () => {}; @Prop isDeletable: boolean = false; onDelete: () => void = () => {}; build() { Column() { Row() { Text(this.title).fontColor('#22fdc8').fontWeight(FontWeight.Bold) Blank() Column() { Text(this.task.timeType === 0 ? '定时时间' : '经纬度时间') .fontSize(10) .fontColor('#aaa') .alignSelf(ItemAlign.End) Text((this.task.execTime.includes(' ') ? this.task.execTime.split(' ')[1] : this.task.execTime).substring(0, 5)) .fontColor('#fff') .alignSelf(ItemAlign.End) .onAppear(() => { console.info(`TaskItem: ${this.title} time=${this.task.execTime}`); }) } }.width('100%').margin({ bottom: 5 }) Row() { ForEach(this.task.loops, (loop: TaskLoop) => { if (loop.turnOn) { Text(`${loop.index+1}`).fontSize(10).fontColor('#22fdc8').margin({right: 2}) } }) }.width('100%') Row() { Button('编辑').fontSize(10).height(24).onClick(this.onEdit) if (this.isDeletable && this.onDelete) { Button('删除').fontSize(10).height(24).backgroundColor('red').margin({left: 10}).onClick(this.onDelete) } }.width('100%').justifyContent(FlexAlign.End).margin({ top: 5 }) } .backgroundColor('rgba(20, 38, 81, 0.95)') .padding(10) .margin({ bottom: 10 }) .borderRadius(5) } }