You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

738 lines
28 KiB

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<number, number[]> = new Map<number, number[]>();
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)
}
}