main
parent
0b93c86b9b
commit
d8186f7a10
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,44 @@
|
||||
@Component
|
||||
export struct AlarmBrowser {
|
||||
@Prop deviceId: string;
|
||||
@State alarms: string[] = [];
|
||||
|
||||
aboutToAppear() {
|
||||
// Bluetooth protocol does not support historical alarm retrieval.
|
||||
// We could potentially read current status, but simply showing a notice is safer for now.
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Text('告警信息')
|
||||
.fontSize(16)
|
||||
.fontColor('#22fdc8')
|
||||
.width('100%')
|
||||
.margin({ top: 20, bottom: 10 })
|
||||
|
||||
List() {
|
||||
if (this.alarms.length === 0) {
|
||||
ListItem() {
|
||||
Text('暂无告警信息 (蓝牙模式仅支持查看部分实时状态)')
|
||||
.fontColor('#999')
|
||||
.fontSize(12)
|
||||
.width('100%')
|
||||
.textAlign(TextAlign.Center)
|
||||
.padding(20)
|
||||
}
|
||||
} else {
|
||||
ForEach(this.alarms, (alarm: string) => {
|
||||
ListItem() {
|
||||
Text(alarm).fontColor('red')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
.padding(20)
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,522 @@
|
||||
import { M9zService, TaskProgram, TaskInstruction, M9zCmd, ControlTask, TaskLoop } 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;
|
||||
@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 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();
|
||||
|
||||
aboutToAppear() {
|
||||
if (this.deviceId) {
|
||||
this.refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.isLoading = true;
|
||||
// Get Mode
|
||||
this.m9zService.getDeviceMode({
|
||||
onSuccess: (mode: number): void => {
|
||||
this.currentMode = mode;
|
||||
},
|
||||
onError: (err: string): void => {
|
||||
console.error('Get mode failed', err);
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
this.m9zService.getTask(M9zCmd.StartTask, modeIndex, {
|
||||
onSuccess: (prog: TaskProgram): void => {
|
||||
const t: ControlTask | null = this.decompileTask(prog);
|
||||
this.startTask = t ? t : this.createEmptyTask();
|
||||
},
|
||||
onError: (err: string): void => {}
|
||||
});
|
||||
|
||||
// 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();
|
||||
},
|
||||
onError: (err: string): void => {}
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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() {
|
||||
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() {
|
||||
this.TaskCard('开始任务', this.startTask, (): void => {this.openEditDialog('start', -1);})
|
||||
}
|
||||
|
||||
ForEach(this.middleTasks, (task: ControlTask, index: number) => {
|
||||
ListItem() {
|
||||
this.TaskCard(`中间任务 ${index+1}`, task, (): void => {this.openEditDialog('middle', index);}, true, (): 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() {
|
||||
this.TaskCard('结束任务', this.stopTask, (): void => {this.openEditDialog('stop', -1);})
|
||||
}
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.width('100%')
|
||||
|
||||
// Control Panel
|
||||
Row() {
|
||||
Button(`应用模式${this.selectedViewMode+1}到设备`)
|
||||
.onClick((): void => {this.changeDeviceMode();})
|
||||
.width('80%')
|
||||
.backgroundColor('#22fdc8')
|
||||
.fontColor('#000')
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding(10)
|
||||
|
||||
// Dialog
|
||||
if(this.showTaskDialog) {
|
||||
Stack() {
|
||||
Rect().width('100%').height('100%').fill('rgba(0,0,0,0.8)').onClick((): void => {this.showTaskDialog = false;})
|
||||
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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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 })
|
||||
}
|
||||
.backgroundColor('#1d3553')
|
||||
.padding(20)
|
||||
.width('90%')
|
||||
.borderRadius(10)
|
||||
}.position({x:0,y:0}).zIndex(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Builder
|
||||
TaskCard(title: string, task: ControlTask, onEdit: () => void, isDeletable: boolean = false, onDelete?: () => void) {
|
||||
Column() {
|
||||
Row() {
|
||||
Text(title).fontColor('#22fdc8').fontWeight(FontWeight.Bold)
|
||||
Blank()
|
||||
Text(task.execTime).fontColor('#fff')
|
||||
}.width('100%').margin({ bottom: 5 })
|
||||
|
||||
Row() {
|
||||
ForEach(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(onEdit)
|
||||
if (isDeletable && onDelete) {
|
||||
Button('删除').fontSize(10).height(24).backgroundColor('red').margin({left: 10}).onClick(onDelete)
|
||||
}
|
||||
}.width('100%').justifyContent(FlexAlign.End).margin({ top: 5 })
|
||||
}
|
||||
.backgroundColor('rgba(20, 38, 81, 0.95)')
|
||||
.padding(10)
|
||||
.margin({ bottom: 10 })
|
||||
.borderRadius(5)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,467 @@
|
||||
import { M9zService, ManualData, LoopInfo } from '../../utils/M9zService';
|
||||
|
||||
@Component
|
||||
export struct ManualControl {
|
||||
@Prop deviceId: string;
|
||||
@State manualData: ManualData = new ManualData(); // 初始化 Class
|
||||
@State deviceTime: string = '--:--:--';
|
||||
@State isConnected: boolean = false;
|
||||
@State isLoading: boolean = false;
|
||||
@State selectedLoopIndex: number = -1;
|
||||
@State currentBrightness: number = 50;
|
||||
@State showBrightnessDialog: boolean = false;
|
||||
@State toastMessage: string = '';
|
||||
@State showToast: boolean = false;
|
||||
|
||||
private m9zService: M9zService = M9zService.getInstance();
|
||||
private timer: number = -1;
|
||||
|
||||
aboutToAppear() {
|
||||
this.initLoops();
|
||||
if (this.deviceId) {
|
||||
this.refreshData();
|
||||
this.startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
aboutToDisappear() {
|
||||
if (this.timer > 0) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
this.timer = setInterval((): void => {
|
||||
this.m9zService.getDeviceTime({
|
||||
onSuccess: (date: Date): void => {
|
||||
this.deviceTime = this.formatDateTime(date);
|
||||
},
|
||||
onError: (err: string): void => {
|
||||
// console.error('获取时间失败', err);
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
initLoops() {
|
||||
const loops: LoopInfo[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const loop = new LoopInfo();
|
||||
loop.index = i;
|
||||
loop.name = `#${i + 1}`;
|
||||
loop.isOpen = false;
|
||||
loop.pwm = 0;
|
||||
loops.push(loop);
|
||||
}
|
||||
const data = new ManualData();
|
||||
data.disable = false;
|
||||
data.loops = loops;
|
||||
this.manualData = data;
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
this.isLoading = true;
|
||||
this.m9zService.getManualControlInfo({
|
||||
onSuccess: (data: ManualData): void => {
|
||||
this.manualData = data;
|
||||
this.isLoading = false;
|
||||
},
|
||||
onError: (err: string): void => {
|
||||
this.showToastMessage('获取数据失败: ' + err);
|
||||
this.isLoading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleMode() {
|
||||
// Deep copy manualData
|
||||
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);
|
||||
});
|
||||
|
||||
this.m9zService.setManualControl(newData, {
|
||||
onSuccess: (success: boolean): void => {
|
||||
if (success) {
|
||||
this.manualData = newData; // Re-assign triggers UI update
|
||||
this.showToastMessage('模式切换成功');
|
||||
}
|
||||
},
|
||||
onError: (err: string): void => {
|
||||
this.showToastMessage('模式切换失败: ' + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleLoop(index: number) {
|
||||
if (!this.manualData.disable) { // disable=true 代表手动模式 (0x01)
|
||||
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
|
||||
newData.loops[index].isOpen = !newData.loops[index].isOpen;
|
||||
|
||||
this.m9zService.setManualControl(newData, {
|
||||
onSuccess: (success: boolean): void => {
|
||||
if (success) {
|
||||
this.manualData = newData;
|
||||
}
|
||||
},
|
||||
onError: (err: string): void => {
|
||||
this.showToastMessage('控制失败: ' + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openBrightnessDialog(index: number) {
|
||||
if (!this.manualData.disable) {
|
||||
this.showToastMessage('请先切换到手动模式');
|
||||
return;
|
||||
}
|
||||
this.selectedLoopIndex = index;
|
||||
if (index >= 0) {
|
||||
this.currentBrightness = this.manualData.loops[index].pwm;
|
||||
} else {
|
||||
this.currentBrightness = 50;
|
||||
}
|
||||
this.showBrightnessDialog = true;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
if (this.selectedLoopIndex >= 0) {
|
||||
newData.loops[this.selectedLoopIndex].pwm = this.currentBrightness;
|
||||
} else {
|
||||
// 全局调光
|
||||
for (let i = 0; i < 10; i++) {
|
||||
newData.loops[i].pwm = this.currentBrightness;
|
||||
}
|
||||
}
|
||||
|
||||
this.m9zService.setManualControl(newData, {
|
||||
onSuccess: (success: boolean): void => {
|
||||
if (success) {
|
||||
this.manualData = newData;
|
||||
this.showBrightnessDialog = false;
|
||||
this.showToastMessage('调光成功');
|
||||
}
|
||||
},
|
||||
onError: (err: string): void => {
|
||||
this.showToastMessage('调光失败: ' + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
this.sendControl(newData, '全开成功');
|
||||
}
|
||||
|
||||
turnOffAll(): 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 = false; // OFF
|
||||
loop.pwm = l.pwm;
|
||||
newData.loops.push(loop);
|
||||
});
|
||||
this.sendControl(newData, '全关成功');
|
||||
}
|
||||
|
||||
sendControl(newData: ManualData, successMsg: string): void {
|
||||
this.m9zService.setManualControl(newData, {
|
||||
onSuccess: (success: boolean): void => {
|
||||
if (success) {
|
||||
this.manualData = newData;
|
||||
this.showToastMessage(successMsg);
|
||||
}
|
||||
},
|
||||
onError: (err: string): void => {
|
||||
this.showToastMessage('操作失败: ' + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
syncDeviceTime(): void {
|
||||
this.m9zService.setDeviceTime(new Date(), {
|
||||
onSuccess: (): void => this.showToastMessage('时间同步成功'),
|
||||
onError: (err: string): void => this.showToastMessage('同步失败:' + err)
|
||||
});
|
||||
}
|
||||
|
||||
showToastMessage(msg: string): void {
|
||||
this.toastMessage = msg;
|
||||
this.showToast = true;
|
||||
setTimeout((): void => {
|
||||
this.showToast = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
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');
|
||||
const s = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${y}-${m}-${d} ${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);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
.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(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 })
|
||||
|
||||
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})
|
||||
|
||||
}
|
||||
.padding(15)
|
||||
.width('100%')
|
||||
|
||||
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%'})
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
.onChange((value: number): void => {
|
||||
this.currentBrightness = value;
|
||||
})
|
||||
.width('80%')
|
||||
|
||||
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%')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue