Lufaneng 6 months ago
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%')
}
}

@ -0,0 +1,203 @@
import { M9zService, LgnLatData, SunRiseSetData, SunRiseTimeData } from '../../utils/M9zService';
import { promptAction } from '@kit.ArkUI';
@Component
export struct ParameterSettings {
@Prop deviceId: string;
@State isLoading: boolean = false;
@State lgnLat: LgnLatData = new LgnLatData();
@State sunRiseSet: SunRiseSetData = new SunRiseSetData();
@State sunRiseTime: SunRiseTimeData = new SunRiseTimeData();
private m9zService: M9zService = M9zService.getInstance();
aboutToAppear() {
if (this.deviceId) {
this.refreshData();
}
}
refreshData() {
this.isLoading = true;
// Get LgnLat
this.m9zService.getLgnLat({
onSuccess: (data: LgnLatData): void => {
this.lgnLat = data;
},
onError: (err: string): void => {
// console.error('Get lgnlat failed', err);
}
});
// Get SunRiseSet
this.m9zService.getSunRiseSet({
onSuccess: (data: SunRiseSetData): void => {
this.sunRiseSet = data;
},
onError: (err: string): void => {}
});
// Get SunRiseTime
this.m9zService.getSunRiseTime({
onSuccess: (data: SunRiseTimeData): void => {
this.sunRiseTime = data;
this.isLoading = false;
},
onError: (err: string): void => {
this.isLoading = false;
}
});
}
syncLocation() {
// 模拟同步定位实际应调用定位API
const newData = new LgnLatData();
newData.lgn = 113.123456;
newData.lat = 23.123456;
newData.loc = '广东省广州市';
this.m9zService.setLgnLat(newData, {
onSuccess: (success: boolean): void => {
this.lgnLat = newData;
promptAction.showToast({ message: '同步成功' });
},
onError: (err: string): void => {
promptAction.showToast({ message: '同步失败:' + err });
}
});
}
setLgnLat() {
this.m9zService.setLgnLat(this.lgnLat, {
onSuccess: (success: boolean): void => {
promptAction.showToast({ message: '设置经纬度成功' });
},
onError: (err: string): void => {
promptAction.showToast({ message: '设置失败:' + err });
}
});
}
setSunRiseSet() {
this.m9zService.setSunRiseSet(this.sunRiseSet, {
onSuccess: (success: boolean): void => {
promptAction.showToast({ message: '设置偏差成功' });
},
onError: (err: string): void => {
promptAction.showToast({ message: '设置失败:' + err });
}
});
}
restartDevice() {
AlertDialog.show({
title: '确认',
message: '确定要重启设备吗?',
primaryButton: {
value: '取消',
action: (): void => {}
},
secondaryButton: {
value: '重启',
action: (): void => {
this.m9zService.restartDevice({
onSuccess: (val: boolean): void => { promptAction.showToast({ message: '发送重启命令成功' }); },
onError: (err: string): void => { promptAction.showToast({ message: '失败:' + err }); }
});
}
}
});
}
build() {
Column() {
// 经纬度设置
Column() {
Text('经纬度设置').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#22fdc8').width('100%').margin({bottom: 10})
Row() {
Text('经度: ').fontColor('#eaf6fc')
TextInput({ text: this.lgnLat.lgn.toString() })
.width(100)
.onChange((val) => { this.lgnLat.lgn = Number(val); })
.fontColor('#fff').backgroundColor('#333')
}.width('100%').margin({bottom: 5})
Row() {
Text('纬度: ').fontColor('#eaf6fc')
TextInput({ text: this.lgnLat.lat.toString() })
.width(100)
.onChange((val) => { this.lgnLat.lat = Number(val); })
.fontColor('#fff').backgroundColor('#333')
}.width('100%').margin({bottom: 10})
Row() {
Button('同步手机定位').fontSize(12).onClick(() => this.syncLocation()).margin({right: 10})
Button('保存').fontSize(12).onClick(() => this.setLgnLat())
}
}
.padding(10)
.backgroundColor('rgba(20, 38, 81, 0.95)')
.borderRadius(8)
.margin({bottom: 15})
.width('100%')
// 日出日落偏差
Column() {
Text('日出日落偏差 (分钟)').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#22fdc8').width('100%').margin({bottom: 10})
Row() {
Text('日出偏差: ').fontColor('#eaf6fc')
TextInput({ text: this.sunRiseSet.rise.toString() })
.width(80)
.onChange((val) => { this.sunRiseSet.rise = Number(val); })
.fontColor('#fff').backgroundColor('#333')
}.width('100%').margin({bottom: 5})
Row() {
Text('日落偏差: ').fontColor('#eaf6fc')
TextInput({ text: this.sunRiseSet.set.toString() })
.width(80)
.onChange((val) => { this.sunRiseSet.set = Number(val); })
.fontColor('#fff').backgroundColor('#333')
}.width('100%').margin({bottom: 10})
Row() {
Button('保存').fontSize(12).onClick(() => this.setSunRiseSet())
}
}
.padding(10)
.backgroundColor('rgba(20, 38, 81, 0.95)')
.borderRadius(8)
.margin({bottom: 15})
.width('100%')
// 当前日出日落时间
Column() {
Text('今日光照时间').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#22fdc8').width('100%').margin({bottom: 10})
Text(`日出: ${this.sunRiseTime.sunrise ? this.formatTime(this.sunRiseTime.sunrise) : '--:--'}`).fontColor('#eaf6fc')
Text(`日落: ${this.sunRiseTime.sunset ? this.formatTime(this.sunRiseTime.sunset) : '--:--'}`).fontColor('#eaf6fc')
}
.padding(10)
.backgroundColor('rgba(20, 38, 81, 0.95)')
.borderRadius(8)
.margin({bottom: 15})
.width('100%')
// 重启
Button('重启设备')
.backgroundColor('#fa5050')
.width('80%')
.margin({top: 20})
.onClick(() => this.restartDevice())
}.width('100%').padding(10)
}
formatTime(date: Date): string {
const h = String(date.getHours()).padStart(2, '0');
const m = String(date.getMinutes()).padStart(2, '0');
return `${h}:${m}`;
}
}

@ -0,0 +1,594 @@
import { socket } from '@kit.ConnectivityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { util } from '@kit.ArkTS';
// 协议常量
const FRAME_HEADER = 0xEE;
const FRAME_FOOTER = 0xFF;
const CMD_READ = 0x01;
const CMD_WRITE = 0x02;
// 指令码
export enum M9zCmd {
Manual = 0x01, // 手动控制
DeviceTime = 0x02, // 设备时间
DeviceOutStatus = 0x04, // 输出状态
LgnLat = 0x05, // 经纬度
SunRiseSet = 0x06, // 日出日落偏差
Mode = 0x0B, // 模式选择
DeviceStatus = 0x10, // 设备状态
DeviceConfig = 0x11, // 设备配置
SunRiseTime = 0x12, // 日出日落时间
StartTask = 0x13, // 开始任务
MiddleTask = 0x14, // 中间任务
StopTask = 0x15, // 结束任务
SubLoopParameters = 0x17,// 回路分控器参数
RelayStatus = 0x19, // 继电器状态
Restart = 0x7E, // 重启
Save = 0x7F // 保存配置
}
// 接口定义
export class LoopInfo {
index: number = 0;
name: string = '';
isOpen: boolean = false;
pwm: number = 0;
}
export class ManualData {
disable: boolean = false; // false=自动, true=手动
loops: LoopInfo[] = [];
}
export class DeviceStatus {
mode: number = 0; // 0=自动, 1=手动
deviceTime: Date = new Date();
relays: boolean[] = [];
pwmOutputs: number[] = [];
}
export class LgnLatData {
lgn: number = 0;
lat: number = 0;
loc: string = '';
}
export class SunRiseSetData {
rise: number = 0;
set: number = 0;
}
export class SunRiseTimeData {
sunrise: Date | null = null;
sunset: Date | null = null;
}
export class TaskInstruction {
type: number = 0;
param: number = 0;
value: number = 0;
}
export class TaskProgram {
index: number = 0;
instructions: TaskInstruction[] = [];
}
// Data structures for UI/Logic representation
@Observed
export class TaskLoop {
index: number = 0;
turnOn: boolean = false;
pwm: number = 0;
}
@Observed
export class ControlTask {
execTime: string = '00:00';
timeType: number = 0; // 0=Timer, 1=Delay
loops: TaskLoop[] = [];
}
// 定义联合类型以避免使用 any
export type ServiceData = boolean | number | Date | ManualData | LgnLatData | SunRiseSetData | SunRiseTimeData | TaskProgram | DeviceStatus;
export interface Callback<T> {
onSuccess: (data: T) => void;
onError: (err: string) => void;
}
// 内部使用的通用回调包装类 - Exported to satisfy strict ArkTS Map generic constraints
export class GeneralCallbackWrapper {
private successCallback: (data: ServiceData) => void;
private errorCallback: (err: string) => void;
constructor(success: (data: ServiceData) => void, error: (err: string) => void) {
this.successCallback = success;
this.errorCallback = error;
}
public invokeSuccess(data: ServiceData): void {
this.successCallback(data);
}
public invokeError(err: string): void {
this.errorCallback(err);
}
}
export class M9zService {
private static instance: M9zService;
private clientNumber: number = -1;
private isConnected: boolean = false;
private readonly SPP_UUID: string = '00001101-0000-1000-8000-00805F9B34FB';
// Explicitly typed Map initialization
private callbackMap: Map<number, GeneralCallbackWrapper[]> = new Map<number, GeneralCallbackWrapper[]>();
private constructor() {}
public static getInstance(): M9zService {
if (!M9zService.instance) {
M9zService.instance = new M9zService();
}
return M9zService.instance;
}
// 连接设备
public connect(deviceId: string, onConnect: () => void, onError: (err: string) => void): void {
if (this.isConnected) {
onConnect();
return;
}
try {
socket.sppConnect(deviceId, {
uuid: this.SPP_UUID,
secure: true,
type: socket.SppType.SPP_RFCOMM
}, (err: BusinessError, clientNumber: number) => {
if (err) {
console.error('SPP 连接失败:', JSON.stringify(err));
onError('连接失败: ' + err.message);
return;
}
this.clientNumber = clientNumber;
this.isConnected = true;
console.info('SPP 连接成功, clientNumber:', clientNumber);
// 监听数据接收
socket.on('sppRead', this.clientNumber, (data: ArrayBuffer) => {
this.handleReceivedData(data);
});
onConnect();
});
} catch (err) {
console.error('SPP 连接异常:', JSON.stringify(err));
onError('连接异常: ' + JSON.stringify(err));
}
}
// 断开连接
public disconnect(): void {
if (this.clientNumber >= 0) {
try {
socket.off('sppRead', this.clientNumber);
socket.sppCloseClientSocket(this.clientNumber);
this.clientNumber = -1;
this.isConnected = false;
console.info('SPP 断开成功');
} catch (err) {
console.error('SPP 断开失败:', JSON.stringify(err));
}
}
}
public getIsConnected(): boolean {
return this.isConnected;
}
// 注册回调
private registerCallback<T extends ServiceData>(instruction: number, callback: Callback<T>): void {
if (!this.callbackMap.has(instruction)) {
this.callbackMap.set(instruction, []);
}
// 创建包装对象 - explicitly typed
const wrapper: GeneralCallbackWrapper = new GeneralCallbackWrapper(
(data: ServiceData): void => {
callback.onSuccess(data as T);
},
(err: string): void => {
callback.onError(err);
}
);
this.callbackMap.get(instruction)?.push(wrapper);
}
// 发送数据
private sendFrame(frame: number[]): void {
if (this.clientNumber < 0) {
console.error('未连接 SPP');
return;
}
try {
const u8Array = new Uint8Array(frame);
const data = u8Array.buffer;
socket.sppWrite(this.clientNumber, data);
// Use helper with Uint8Array to avoid structural typing issues with ArrayBuffer
console.info('发送数据:', this.uint8ArrayToHexString(u8Array));
} catch (err) {
console.error('发送数据失败:', JSON.stringify(err));
}
}
// 构建 M9Z 命令
private buildM9zCommand(instruction: number, cmdType: number, index: number, data: number[]): number[] {
const payload: number[] = [instruction, cmdType, index];
// CMD_WRITE 且 data不为空时写入长度
if (cmdType === CMD_WRITE && data.length > 0) {
payload.push(data.length); // 写入命令中包含数据长度
payload.push(...data);
} else if (cmdType === CMD_READ) {
// 读取命令一般不需要data长度字段data本身为空
}
// 计算校验和
let checksum = 0x33;
for (const byte of payload) {
checksum += byte;
}
checksum = checksum & 0xFF;
return [FRAME_HEADER, ...payload, checksum, FRAME_FOOTER];
}
// 处理接收到的数据
private handleReceivedData(data: ArrayBuffer): void {
const bytes = new Uint8Array(data);
console.info('接收数据:', this.uint8ArrayToHexString(bytes));
// 简单验证帧格式
if (bytes.length < 5 || bytes[0] !== FRAME_HEADER || bytes[bytes.length - 1] !== FRAME_FOOTER) {
console.warn('无效的响应帧');
return;
}
const instruction = bytes[1];
const cmdType = bytes[2];
// 检查是否为响应帧 (D7=1) 且是对应的 instruction
if ((instruction & 0x80) === 0) {
console.warn('非响应帧');
return;
}
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;
const len = bytes[4];
if (bytes.length < 6 + len) return;
// 拷贝数据以避免引用问题
const payloadData: number[] = [];
for(let i=0; i<len; i++) {
payloadData.push(bytes[5+i]);
}
if (callbacks && callbacks.length > 0) {
try {
const parsedData = this.parseData(realInstruction, payloadData);
callbacks.forEach(cb => cb.invokeSuccess(parsedData));
} catch (e) {
console.error("解析错误", e);
callbacks.forEach(cb => cb.invokeError("解析错误"));
}
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;
const success = bytes[5] === 0x01;
if (callbacks && callbacks.length > 0) {
if (success) {
callbacks.forEach(cb => cb.invokeSuccess(true));
} else {
callbacks.forEach(cb => cb.invokeError("设备返回失败"));
}
this.callbackMap.delete(realInstruction);
}
}
}
private parseData(instruction: number, data: number[]): ServiceData {
switch (instruction) {
case M9zCmd.DeviceTime: // 0x02
return this.parseDeviceTime(data);
case M9zCmd.Manual: // 0x01
return this.parseManualData(data);
case M9zCmd.DeviceStatus: // 0x10
return this.parseDeviceStatus(data);
case M9zCmd.LgnLat: // 0x05
return this.parseLgnLat(data);
case M9zCmd.SunRiseSet: // 0x06
return this.parseSunRiseSet(data);
case M9zCmd.StartTask:
case M9zCmd.MiddleTask:
case M9zCmd.StopTask:
return this.parseTask(data);
case M9zCmd.SunRiseTime:
return this.parseSunRiseTime(data);
case M9zCmd.Mode:
return data.length > 0 ? data[0] : 0; // 模式
default:
return 0; // 默认返回
}
}
// --- 解析具体数据 ---
private parseDeviceTime(data: number[]): Date {
if (data.length < 4) return new Date();
const timestamp = this.bytesToUint32LE(data);
return new Date(timestamp * 1000);
}
private parseManualData(data: number[]): ManualData {
// Byte0: disable (manual=1)
const disable = data[0] === 0x01;
// Byte2-3: RelayBits
const relayBits = data[2] | (data[3] << 8);
// Byte5-14: PWM
const loops: LoopInfo[] = [];
for (let i = 0; i < 10; i++) {
const loop = new LoopInfo();
loop.index = i;
loop.name = `#${i + 1}`;
loop.isOpen = ((relayBits >> i) & 0x01) === 1;
loop.pwm = data[5 + i] || 0;
loops.push(loop);
}
const result = new ManualData();
result.disable = disable;
result.loops = loops;
return result;
}
private parseDeviceStatus(data: number[]): DeviceStatus {
// Byte0: Mode
const mode = data[0];
// Byte1-4: Time
const timestamp = this.bytesToUint32LE(data.slice(1, 5));
// Byte5-6: RelayBits
const relayBits = data[5] | (data[6] << 8);
const relays: boolean[] = [];
for (let i = 0; i < 10; i++) {
relays.push(((relayBits >> i) & 0x01) === 1);
}
// Byte8-17: PWM
const pwmOutputs: number[] = [];
for (let i = 0; i < 10; i++) {
pwmOutputs.push(data[8 + i]);
}
const status = new DeviceStatus();
status.mode = mode;
status.deviceTime = new Date(timestamp * 1000);
status.relays = relays;
status.pwmOutputs = pwmOutputs;
return status;
}
private parseLgnLat(data: number[]): LgnLatData {
const lgn = this.bytesToUint32LE(data.slice(0, 4)) / 1000000;
const lat = this.bytesToUint32LE(data.slice(4, 8)) / 1000000;
const res = new LgnLatData();
res.lgn = lgn;
res.lat = lat;
res.loc = '';
return res;
}
private parseSunRiseSet(data: number[]): SunRiseSetData {
let rise = data[0];
if (rise > 128) rise = rise - 256; // 转有符号
let set = data[1];
if (set > 128) set = set - 256;
const res = new SunRiseSetData();
res.rise = rise;
res.set = set;
return res;
}
private parseSunRiseTime(data: number[]): SunRiseTimeData {
const res = new SunRiseTimeData();
// 假设返回两个 uint32 时间戳 (日出,日落)
if (data.length >= 8) {
const riseTs = this.bytesToUint32LE(data.slice(0, 4));
const setTs = this.bytesToUint32LE(data.slice(4, 8));
res.sunrise = new Date(riseTs * 1000);
res.sunset = new Date(setTs * 1000);
}
return res;
}
private parseTask(data: number[]): TaskProgram {
// Byte0: Size
const size = data[0];
const instructions: TaskInstruction[] = [];
let offset = 1;
for (let i = 0; i < size; i++) {
if (offset + 4 > data.length) break;
const val = this.bytesToUint32LE(data.slice(offset, offset + 4));
const type = (val >> 28) & 0x0F;
let param = 0;
let value = 0;
if (type === 0x01 || type === 0x03 || type === 0x04) {
param = (val >> 24) & 0x0F;
value = val & 0x00FFFFFF;
} else if (type === 0x02) { // GOTO
param = (val >> 24) & 0x0F;
value = val & 0x00FFFFFF;
}
const ins = new TaskInstruction();
ins.type = type;
ins.param = param;
ins.value = value;
instructions.push(ins);
offset += 4;
}
const prog = new TaskProgram();
prog.index = 0;
prog.instructions = instructions;
return prog;
}
// --- 公共 API ---
public getDeviceTime(cb: Callback<Date>): void {
this.registerCallback(M9zCmd.DeviceTime, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.DeviceTime, CMD_READ, 0x00, []));
}
public setDeviceTime(date: Date, cb: Callback<boolean>): void {
const timestamp = Math.floor(date.getTime() / 1000);
const data = this.uint32ToBytesLE(timestamp);
this.registerCallback(M9zCmd.DeviceTime, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.DeviceTime, CMD_WRITE, 0x00, data));
}
public getManualControlInfo(cb: Callback<ManualData>): void {
this.registerCallback(M9zCmd.Manual, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.Manual, CMD_READ, 0x00, []));
}
public setManualControl(data: ManualData, cb: Callback<boolean>): void {
// Explicit number array initialization to avoid "any[]"
const payload: number[] = [];
for(let i=0; i<15; i++) payload.push(0);
payload[0] = data.disable ? 0x01 : 0x00;
let relayBits = 0;
data.loops.forEach(loop => {
if (loop.isOpen) relayBits |= (1 << loop.index);
});
payload[2] = relayBits & 0xFF;
payload[3] = (relayBits >> 8) & 0xFF;
data.loops.forEach(loop => {
payload[5 + loop.index] = loop.pwm;
});
this.registerCallback(M9zCmd.Manual, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.Manual, CMD_WRITE, 0x00, payload));
}
public getLgnLat(cb: Callback<LgnLatData>): void {
this.registerCallback(M9zCmd.LgnLat, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.LgnLat, CMD_READ, 0x00, []));
}
public setLgnLat(data: LgnLatData, cb: Callback<boolean>): void {
// 构造12字节数据
const lgnInt = Math.round(data.lgn * 1000000);
const latInt = Math.round(data.lat * 1000000);
const locInt = 8 * 3600 * 1000000; // 默认东八区? 暂时简化处理
const payload: number[] = [
...this.uint32ToBytesLE(lgnInt),
...this.uint32ToBytesLE(latInt),
...this.uint32ToBytesLE(locInt)
];
this.registerCallback(M9zCmd.LgnLat, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.LgnLat, CMD_WRITE, 0x00, payload));
}
public getSunRiseSet(cb: Callback<SunRiseSetData>): void {
this.registerCallback(M9zCmd.SunRiseSet, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.SunRiseSet, CMD_READ, 0x00, []));
}
public setSunRiseSet(data: SunRiseSetData, cb: Callback<boolean>): void {
const payload = [
data.rise < 0 ? data.rise + 256 : data.rise,
data.set < 0 ? data.set + 256 : data.set
];
this.registerCallback(M9zCmd.SunRiseSet, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.SunRiseSet, CMD_WRITE, 0x00, payload));
}
public getSunRiseTime(cb: Callback<SunRiseTimeData>): void {
this.registerCallback(M9zCmd.SunRiseTime, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.SunRiseTime, CMD_READ, 0x00, []));
}
public getDeviceMode(cb: Callback<number>): void {
this.registerCallback(M9zCmd.Mode, cb); // Mode read returns single byte
this.sendFrame(this.buildM9zCommand(M9zCmd.Mode, CMD_READ, 0x00, []));
}
public setDeviceMode(mode: number, cb: Callback<boolean>): void {
this.registerCallback(M9zCmd.Mode, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.Mode, CMD_WRITE, 0x00, [mode]));
}
public restartDevice(cb: Callback<boolean>): void {
this.registerCallback(M9zCmd.Restart, cb);
this.sendFrame(this.buildM9zCommand(M9zCmd.Restart, CMD_WRITE, 0x00, []));
}
// 获取任务
public getTask(cmd: M9zCmd, index: number, cb: Callback<TaskProgram>): void {
this.registerCallback(cmd, cb);
this.sendFrame(this.buildM9zCommand(cmd, CMD_READ, index, []));
}
// 设置任务
public setTask(cmd: M9zCmd, task: TaskProgram, cb: Callback<boolean>): void {
// 构建 payload
// Byte0: size
// Instructions... (4 bytes each)
const payload: number[] = [task.instructions.length];
task.instructions.forEach(ins => {
// ins.type (4bit) | ins.param (4bit) | ins.value (24bit)
const val = ((ins.type & 0xF) << 28) |
((ins.param & 0xF) << 24) |
(ins.value & 0xFFFFFF);
payload.push(...this.uint32ToBytesLE(val));
});
this.registerCallback(cmd, cb);
this.sendFrame(this.buildM9zCommand(cmd, CMD_WRITE, task.index, payload));
}
// 工具方法
private uint8ArrayToHexString(bytes: Uint8Array): string {
return Array.from(bytes).map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
}
private bytesToUint32LE(bytes: number[]): number {
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
}
private uint32ToBytesLE(value: number): number[] {
return [
value & 0xFF,
(value >> 8) & 0xFF,
(value >> 16) & 0xFF,
(value >> 24) & 0xFF
];
}
}
Loading…
Cancel
Save