Lufaneng 6 months ago
parent d8186f7a10
commit 1e17bfe0a6

@ -41,8 +41,6 @@ struct BluetoothControlPage {
() => {
this.isConnected = true;
this.statusMessage = '已连接';
// 同步时间作为连接成功的标志性动作
this.m9zService.getDeviceTime({ onSuccess:()=>{}, onError:()=>{} });
},
(err: string) => {
this.isConnected = false;
@ -93,17 +91,17 @@ struct BluetoothControlPage {
if (this.isConnected) {
Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
TabContent() {
ManualControl({ deviceId: this.deviceId })
ManualControl({ deviceId: this.deviceId, currentTabIndex: this.currentIndex })
}
.tabBar(this.TabBuilder(0, '手动控制'))
TabContent() {
ParameterSettings({ deviceId: this.deviceId })
ParameterSettings({ deviceId: this.deviceId, currentTabIndex: this.currentIndex })
}
.tabBar(this.TabBuilder(1, '参数设置'))
TabContent() {
AutoMode({ deviceId: this.deviceId })
AutoMode({ deviceId: this.deviceId, currentTabIndex: this.currentIndex })
}
.tabBar(this.TabBuilder(2, '自动模式'))

@ -9,6 +9,7 @@ export class AutoModeHelper {
@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)
@ -28,12 +29,6 @@ export struct AutoMode {
private m9zService: M9zService = M9zService.getInstance();
aboutToAppear() {
if (this.deviceId) {
this.refreshData();
}
}
createEmptyTask(): ControlTask {
const loops: TaskLoop[] = [];
for(let i: number = 0; i < 10; i++) {
@ -50,44 +45,68 @@ export struct AutoMode {
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;
// Get Mode
// 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);
}
});
// 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
// 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 => {}
onError: (err: string): void => {
this.fetchStopTask(modeIndex);
}
});
}
// Stop Task
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 => {}
onError: (err: string): void => {
this.fetchMiddleTask(modeIndex);
}
});
}
// Middle Task
fetchMiddleTask(modeIndex: number) {
// 3. Middle Task
this.m9zService.getTask(M9zCmd.MiddleTask, modeIndex, {
onSuccess: (prog: TaskProgram): void => {
this.middleTasks = this.decompileMiddleTask(prog);
@ -395,6 +414,7 @@ export struct AutoMode {
}
build() {
Stack() {
Column() {
// Mode Tabs
Row() {
@ -488,7 +508,20 @@ export struct AutoMode {
}.position({x:0,y:0}).zIndex(100)
}
}
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)
}
}
}
@Builder
TaskCard(title: string, task: ControlTask, onEdit: () => void, isDeletable: boolean = false, onDelete?: () => void) {

@ -1,15 +1,23 @@
import { M9zService, ManualData, LoopInfo } from '../../utils/M9zService';
import { M9zService, ManualData, LoopInfo, SunRiseTimeData } from '../../utils/M9zService';
@Component
export struct ManualControl {
@Prop deviceId: string;
@State manualData: ManualData = new ManualData(); // 初始化 Class
@Prop @Watch('onTabChange') currentTabIndex: number = 0;
@State manualData: ManualData = new ManualData();
@State deviceTime: string = '--:--:--';
@State isConnected: boolean = false;
@State isLoading: boolean = false;
@State sunriseTime: string = '--:--:--';
@State sunsetTime: string = '--:--:--';
// Brightness Control
@State selectedLoopIndex: number = -1;
@State currentBrightness: number = 50;
@State showBrightnessDialog: boolean = false;
@State toastMessage: string = '';
@State showToast: boolean = false;
@ -24,6 +32,12 @@ export struct ManualControl {
}
}
onTabChange() {
if (this.currentTabIndex === 0 && this.deviceId) {
this.refreshData();
}
}
aboutToDisappear() {
if (this.timer > 0) {
clearInterval(this.timer);
@ -32,14 +46,12 @@ export struct ManualControl {
startTimer() {
this.timer = setInterval((): void => {
this.m9zService.getDeviceTime({
onSuccess: (date: Date): void => {
this.deviceTime = this.formatDateTime(date);
},
onError: (err: string): void => {
// console.error('获取时间失败', err);
}
});
// Local time update for UI clock if needed,
// but here we fetch device time periodically or just tick internally?
// The Vue app updates local time every second and valid device time + 1s.
// For now, let's keep fetching device time periodically or simplify.
// Let's just fetch device time every few seconds to keep it simple and accurate to device.
// Or better, fetch once and tick locally to reduce traffic.
}, 1000);
}
@ -54,29 +66,62 @@ export struct ManualControl {
loops.push(loop);
}
const data = new ManualData();
data.disable = false;
data.disable = false; // Default to Auto
data.loops = loops;
this.manualData = data;
}
refreshData() {
this.isLoading = true;
// 1. Get Manual Info
this.m9zService.getManualControlInfo({
onSuccess: (data: ManualData): void => {
this.manualData = data;
this.isLoading = false;
this.fetchDeviceTime();
},
onError: (err: string): void => {
this.showToastMessage('获取数据失败: ' + err);
this.isLoading = false;
this.fetchDeviceTime();
}
});
}
fetchDeviceTime() {
// 2. Get Device Time
this.m9zService.getDeviceTime({
onSuccess: (date: Date): void => {
this.deviceTime = this.formatDateTime(date);
this.fetchSunRiseTime();
},
onError: (err: string): void => {
this.fetchSunRiseTime();
}
});
}
fetchSunRiseTime() {
// 3. Get SunRise/Set Time
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.checkLoadingComplete();
},
onError: (err: string): void => {
this.checkLoadingComplete();
}
});
}
checkLoadingComplete() {
this.isLoading = false;
}
toggleMode() {
// Deep copy manualData
const newData = new ManualData();
newData.disable = !this.manualData.disable;
newData.disable = !this.manualData.disable; // Toggle
// Copy existing loops
this.manualData.loops.forEach((l: LoopInfo): void => {
const loop = new LoopInfo();
loop.index = l.index;
@ -89,7 +134,7 @@ export struct ManualControl {
this.m9zService.setManualControl(newData, {
onSuccess: (success: boolean): void => {
if (success) {
this.manualData = newData; // Re-assign triggers UI update
this.manualData = newData;
this.showToastMessage('模式切换成功');
}
},
@ -100,23 +145,12 @@ export struct ManualControl {
}
toggleLoop(index: number) {
if (!this.manualData.disable) { // disable=true 代表手动模式 (0x01)
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 = l.isOpen;
loop.pwm = l.pwm;
newData.loops.push(loop);
});
// Toggle
const newData = this.cloneManualData(this.manualData);
newData.loops[index].isOpen = !newData.loops[index].isOpen;
this.m9zService.setManualControl(newData, {
@ -146,21 +180,12 @@ export struct ManualControl {
}
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);
});
const newData = this.cloneManualData(this.manualData);
if (this.selectedLoopIndex >= 0) {
newData.loops[this.selectedLoopIndex].pwm = this.currentBrightness;
} else {
// 全局调光
// Global
for (let i = 0; i < 10; i++) {
newData.loops[i].pwm = this.currentBrightness;
}
@ -171,7 +196,6 @@ export struct ManualControl {
if (success) {
this.manualData = newData;
this.showBrightnessDialog = false;
this.showToastMessage('调光成功');
}
},
onError: (err: string): void => {
@ -185,16 +209,8 @@ export struct ManualControl {
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);
});
const newData = this.cloneManualData(this.manualData);
newData.loops.forEach(l => l.isOpen = true);
this.sendControl(newData, '全开成功');
}
@ -203,17 +219,23 @@ export struct ManualControl {
this.showToastMessage('请先切换到手动模式');
return;
}
const newData = this.cloneManualData(this.manualData);
newData.loops.forEach(l => l.isOpen = false);
this.sendControl(newData, '全关成功');
}
cloneManualData(src: ManualData): ManualData {
const newData = new ManualData();
newData.disable = this.manualData.disable;
this.manualData.loops.forEach((l: LoopInfo): void => {
newData.disable = src.disable;
src.loops.forEach((l: LoopInfo): void => {
const loop = new LoopInfo();
loop.index = l.index;
loop.name = l.name;
loop.isOpen = false; // OFF
loop.isOpen = l.isOpen;
loop.pwm = l.pwm;
newData.loops.push(loop);
});
this.sendControl(newData, '全关成功');
return newData;
}
sendControl(newData: ManualData, successMsg: string): void {
@ -232,7 +254,11 @@ export struct ManualControl {
syncDeviceTime(): void {
this.m9zService.setDeviceTime(new Date(), {
onSuccess: (): void => this.showToastMessage('时间同步成功'),
onSuccess: (success: boolean): void => {
this.showToastMessage('时间同步成功');
// Refresh display
this.deviceTime = this.formatDateTime(new Date());
},
onError: (err: string): void => this.showToastMessage('同步失败:' + err)
});
}
@ -255,163 +281,215 @@ export struct ManualControl {
return `${y}-${m}-${d} ${h}:${min}:${s}`;
}
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}`;
}
build() {
Stack() {
Scroll() {
Column() {
// 这里的UI结构参考 ManualControl.vue 和 BluetoothControlPage.ets 融合
// 设备信息
// --- Header Status ---
Column() {
Row() {
Text(`设备时间:${this.deviceTime}`)
.fontSize(14)
.fontColor('#eaf6fc')
Blank()
Button('刷新')
.fontSize(12)
.height(24)
.onClick(() => this.refreshData())
Text('设备号N/A').fontSize(12).fontColor('#b2ebf2')
}.width('100%').margin({bottom: 5})
Row() {
Text('设备名称:智能照明控制器').fontSize(12).fontColor('#b2ebf2')
}.width('100%').margin({bottom: 10})
// 模式切换
Row() {
Text(`设备时间:${this.deviceTime}`)
.fontSize(14).fontColor('#eaf6fc')
}.width('100%')
}
.width('100%')
.padding(15)
.backgroundColor('rgba(15, 30, 60, 0.4)')
.borderRadius(8)
.margin({bottom: 20})
// --- Control Mode ---
Column() {
Text('控制模式')
.fontSize(14)
.fontColor('#22fdc8')
Blank()
.width('100%')
.margin({ bottom: 10 })
.fontWeight(FontWeight.Medium)
Button(this.manualData.disable ? '手动模式' : '自动模式')
.fontSize(12)
.fontColor(this.manualData.disable ? '#22fdc8' : '#b2ebf2')
.width('100%')
.backgroundColor(this.manualData.disable ? 'rgba(34, 253, 200, 0.1)' : 'rgba(20, 38, 81, 0.95)')
.fontColor(this.manualData.disable ? '#22fdc8' : '#b2ebf2')
.border({ width: 1, color: this.manualData.disable ? '#22fdc8' : '#213b67' })
.onClick((): void => {
this.toggleMode();
})
}.width('100%').margin({bottom: 15})
.height(44)
.onClick(() => this.toggleMode())
}
.width('100%')
.margin({bottom: 20})
// 回路控制 Grid
// --- Circuit Control ---
Column() {
Text('回路控制')
.fontSize(14)
.fontColor('#22fdc8')
.width('100%')
.margin({ bottom: 10 })
.fontWeight(FontWeight.Medium)
Grid() {
ForEach(this.manualData.loops, (loop: LoopInfo): void => {
GridItem() {
Column() {
// Icon Circle
Text('💡')
.fontSize(20)
.fontSize(24)
.fontColor(loop.isOpen ? '#22fdc8' : '#666')
.backgroundColor(loop.isOpen ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51')
.width(40).height(40)
.borderRadius(20)
.width(50).height(50)
.textAlign(TextAlign.Center)
.lineHeight(40)
.borderRadius(25)
.backgroundColor(loop.isOpen ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51')
.border({ width: 1, color: loop.isOpen ? '#22fdc8' : '#3e7ba1' })
.margin({bottom: 8})
Text(loop.name)
.fontSize(12)
.fontColor('#eaf6fc')
.margin({top: 5})
Text(`回路${loop.index + 1}`)
.fontSize(12).fontColor('#eaf6fc')
Text(loop.isOpen ? '开启' : '关闭')
.fontSize(10).fontColor(loop.isOpen ? '#22fdc8' : '#999')
}
.onClick((): void => {
this.toggleLoop(loop.index);
})
.onClick(() => this.toggleLoop(loop.index))
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsGap(15)
.height(140)
.height(160)
}
.width('100%')
.margin({bottom: 20})
// 调光控制 Grid
// --- Dimming Control ---
Column() {
Text('调光控制')
.fontSize(14)
.fontColor('#22fdc8')
.width('100%')
.margin({ top: 15, bottom: 10 })
.margin({ bottom: 10 })
.fontWeight(FontWeight.Medium)
Grid() {
ForEach(this.manualData.loops, (loop: LoopInfo): void => {
GridItem() {
Column() {
// Value Circle
Stack() {
Circle({ width: 50, height: 50 })
.fill(loop.pwm > 0 ? 'rgba(34, 253, 200, 0.2)' : '#2d3a51')
.stroke(loop.pwm > 0 ? '#22fdc8' : '#3e7ba1')
.strokeWidth(1)
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' })
.fontSize(12).fontColor('#eaf6fc')
}
.margin({bottom: 8})
Text(`通道${loop.index + 1}`)
.fontSize(10)
.fontColor('#b2ebf2')
.margin({top: 5})
.fontSize(12).fontColor('#b2ebf2')
}
.onClick((): void => {
this.openBrightnessDialog(loop.index);
})
.onClick(() => this.openBrightnessDialog(loop.index))
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsGap(15)
.height(140)
.height(160)
}
.width('100%')
.margin({bottom: 20})
// 快捷操作
// --- Quick Actions ---
Row() {
Button('全开')
.fontSize(12)
.backgroundColor('#102a45') // Darker
.fontColor('#22fdc8')
.backgroundColor('rgba(34, 253, 200, 0.1)')
.border({ width: 1, color: '#22fdc8' })
.border({width: 1, color: '#22fdc8'})
.layoutWeight(1)
.onClick((): void => {
this.turnOnAll();
})
.height(40)
.onClick(() => this.turnOnAll())
Button('全关')
.fontSize(12)
.backgroundColor('#1a1520')
.fontColor('#fa5050')
.backgroundColor('rgba(250, 80, 80, 0.1)')
.border({ width: 1, color: '#fa5050' })
.border({width: 1, color: '#fa5050'})
.layoutWeight(1)
.margin({ left: 8 })
.onClick((): void => {
this.turnOffAll();
})
.height(40)
.margin({left: 10})
.onClick(() => this.turnOffAll())
Button('一键调光')
.fontSize(12)
.backgroundColor('#2a2515')
.fontColor('#ffcb4d')
.backgroundColor('rgba(255, 203, 77, 0.1)')
.border({ width: 1, color: '#ffcb4d' })
.border({width: 1, color: '#ffcb4d'})
.layoutWeight(1)
.margin({ left: 8 })
.onClick((): void => {
this.openBrightnessDialog(-1);
})
}
.width('100%')
.margin({ top: 15 })
.height(40)
.margin({left: 10})
.onClick(() => this.openBrightnessDialog(-1))
}.width('100%').margin({bottom: 15})
Row() {
Button('同步时间')
.fontSize(12)
.backgroundColor('#102035')
.fontColor('#40a7fa')
.backgroundColor('rgba(64, 167, 250, 0.1)')
.border({ width: 1, color: '#40a7fa' })
.border({width: 1, color: '#40a7fa'})
.layoutWeight(1)
.onClick((): void => {
this.syncDeviceTime();
})
}.width('100%').margin({top: 10})
.height(40)
.onClick(() => this.syncDeviceTime())
Button('刷新数据')
.backgroundColor('rgba(34, 253, 200, 0.1)')
.fontColor('#22fdc8')
.border({width: 1, color: '#22fdc8'})
.layoutWeight(1)
.height(40)
.margin({left: 10})
.onClick(() => this.refreshData())
}.width('100%').margin({bottom: 20})
// --- Bottom Info ---
Column() {
Row() {
Text(`日出时间:${this.sunriseTime}`).fontSize(12).fontColor('#b2ebf2').layoutWeight(1)
Text(`日落时间:${this.sunsetTime}`).fontSize(12).fontColor('#b2ebf2').layoutWeight(1)
}.width('100%')
}
.width('100%')
.padding(15)
.backgroundColor('rgba(15, 30, 60, 0.4)')
.borderRadius(8)
}
.width('100%')
.padding(15)
}
.width('100%')
.height('100%')
// --- Loading ---
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)
}
// --- Toast & Dialogs ---
if (this.showToast) {
Text(this.toastMessage)
.fontSize(12)
@ -423,16 +501,18 @@ export struct ManualControl {
}
if (this.showBrightnessDialog) {
// 简易弹窗
Stack() {
Rect().width('100%').height('100%').fill('rgba(0,0,0,0.5)').onClick((): void => {
// Mask
Rect().width('100%').height('100%').fill('rgba(0,0,0,0.6)').onClick(() => {
this.showBrightnessDialog = false;
})
// Content
Column() {
Text(this.selectedLoopIndex >= 0 ? `通道${this.selectedLoopIndex + 1} 亮度` : '全部通道 亮度')
.fontColor('#eaf6fc').margin({bottom: 20})
.fontColor('#eaf6fc').margin({bottom: 20}).fontSize(16)
Text(`${this.currentBrightness}%`).fontColor('#22fdc8').fontSize(24).margin({bottom: 20})
Text(`${this.currentBrightness}%`)
.fontColor('#22fdc8').fontSize(36).fontWeight(FontWeight.Bold).margin({bottom: 30})
Slider({
value: this.currentBrightness,
@ -440,28 +520,49 @@ export struct ManualControl {
max: 100,
style: SliderStyle.OutSet
})
.onChange((value: number): void => {
.trackColor('#2d3a51')
.selectedColor('#22fdc8')
.blockColor('#22fdc8')
.onChange((value: number) => {
this.currentBrightness = value;
})
.width('80%')
.width('90%')
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%')
Button('取消')
.onClick(() => {this.showBrightnessDialog = false;})
.backgroundColor('transparent')
.fontColor('#999')
.border({width: 1, color: '#666'})
.width(100)
Button('确定')
.onClick(() => {this.confirmBrightness();})
.backgroundColor('#22fdc8')
.fontColor('#0b1830')
.width(100)
}.margin({top: 40}).width('100%').justifyContent(FlexAlign.SpaceAround)
}
.width('85%')
.backgroundColor('#1d3553')
.padding(20)
.borderRadius(10)
.padding(30)
.borderRadius(16)
}
.position({x: 0, y: 0})
.zIndex(100)
.zIndex(999)
.width('100%')
.height('100%')
.alignContent(Alignment.Center)
}
}
.width('100%')
.height('100%')
}
@Builder
ParamRow(label: string, value: string) {
Row() {
Text(label).fontSize(10).fontColor('#b2ebf2')
Blank()
Text(value).fontSize(10).fontColor('#eaf6fc')
}.width('100%').margin({bottom: 5})
}
}

@ -4,6 +4,8 @@ import { promptAction } from '@kit.ArkUI';
@Component
export struct ParameterSettings {
@Prop deviceId: string;
@Prop @Watch('onTabChange') currentTabIndex: number = 0;
@State isLoading: boolean = false;
@State lgnLat: LgnLatData = new LgnLatData();
@State sunRiseSet: SunRiseSetData = new SunRiseSetData();
@ -13,6 +15,19 @@ export struct ParameterSettings {
aboutToAppear() {
if (this.deviceId) {
// initial load handled by onTabChange? No, initial index might be 0, this is 1. IF this is 1 initially?
// BluetoothControlPage sets index 0 default.
// If user goes to 1, onTabChange fires.
// If user starts at 0, this component might load but not show?
// Let's rely on onTabChange mostly, but for initial:
if (this.currentTabIndex === 1) {
this.refreshData();
}
}
}
onTabChange() {
if (this.currentTabIndex === 1 && this.deviceId) {
this.refreshData();
}
}
@ -20,25 +35,34 @@ export struct ParameterSettings {
refreshData() {
this.isLoading = true;
// Get LgnLat
// 1. Get LgnLat
this.m9zService.getLgnLat({
onSuccess: (data: LgnLatData): void => {
this.lgnLat = data;
this.fetchSunRiseSet();
},
onError: (err: string): void => {
// console.error('Get lgnlat failed', err);
this.fetchSunRiseSet();
}
});
}
// Get SunRiseSet
fetchSunRiseSet() {
// 2. Get SunRiseSet
this.m9zService.getSunRiseSet({
onSuccess: (data: SunRiseSetData): void => {
this.sunRiseSet = data;
this.fetchSunRiseTime();
},
onError: (err: string): void => {}
onError: (err: string): void => {
this.fetchSunRiseTime();
}
});
}
// Get SunRiseTime
fetchSunRiseTime() {
// 3. Get SunRiseTime
this.m9zService.getSunRiseTime({
onSuccess: (data: SunRiseTimeData): void => {
this.sunRiseTime = data;
@ -111,6 +135,8 @@ export struct ParameterSettings {
}
build() {
Stack() {
Scroll() {
Column() {
// 经纬度设置
Column() {
@ -195,6 +221,20 @@ export struct ParameterSettings {
}.width('100%').padding(10)
}
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)
}
}
}
formatTime(date: Date): string {
const h = String(date.getHours()).padStart(2, '0');
const m = String(date.getMinutes()).padStart(2, '0');

@ -248,21 +248,85 @@ export class M9zService {
return [FRAME_HEADER, ...payload, checksum, FRAME_FOOTER];
}
// 缓存接收到的数据,处理分包/粘包
private receiveBuffer: number[] = [];
// 处理接收到的数据
private handleReceivedData(data: ArrayBuffer): void {
const bytes = new Uint8Array(data);
console.info('接收数据:', this.uint8ArrayToHexString(bytes));
// console.info('接收原始数据:', this.uint8ArrayToHexString(bytes));
// 1. 追加到缓存
for (let i = 0; i < bytes.length; i++) {
this.receiveBuffer.push(bytes[i]);
}
// 简单验证帧格式
if (bytes.length < 5 || bytes[0] !== FRAME_HEADER || bytes[bytes.length - 1] !== FRAME_FOOTER) {
console.warn('无效的响应帧');
// 2. 循环处理缓存中的帧
while (this.receiveBuffer.length >= 5) { // 最小帧长 header+inst+type+idx+checksum+footer? No header+inst+type+idx+check+footer = 6?
// check buildM9zCommand: H(1)+inst(1)+type(1)+idx(1)+len(optional)+data+check(1)+F(1).
// Min read response: H I T I L[1] D[1] C F = 8 bytes?
// Min write response: H I T I L[1] S[1] C F = 8 bytes?
// Let's stick to Header search
// 寻找帧头
const headerIndex = this.receiveBuffer.indexOf(FRAME_HEADER);
if (headerIndex === -1) {
// 没有帧头,清空(或保留最后一点以防分包EE是必须的)
this.receiveBuffer = [];
return;
}
// 丢弃帧头前面的垃圾数据
if (headerIndex > 0) {
this.receiveBuffer.splice(0, headerIndex);
}
// 现在 buffer[0] 是 EE
// 检查是否有足够的长度读到长度字段?
// Read Response: [EE, Inst, 01, Idx, LEN, Data..., CS, FF]
// Write Response: [EE, Inst, 02, Idx, 01, Success, CS, FF]
if (this.receiveBuffer.length < 5) return; // Wait for more
const cmdType = this.receiveBuffer[2];
let frameLen = 0;
let dataLen = 0;
// 初步判定长度
// 读/写响应都有长度字段在 byte[4]
if (this.receiveBuffer.length < 5) return;
dataLen = this.receiveBuffer[4];
// 帧总长 = Header(1)+Inst(1)+Type(1)+Idx(1)+LenField(1) + Data(dataLen) + Check(1) + Footer(1)
// = 5 + dataLen + 2 = 7 + dataLen
frameLen = 7 + dataLen;
if (this.receiveBuffer.length < frameLen) {
// 数据不够,等待下次
return;
}
// 检查帧尾
if (this.receiveBuffer[frameLen - 1] !== FRAME_FOOTER) {
console.warn('帧尾校验失败,丢弃帧头');
this.receiveBuffer.shift(); // 丢弃 header重新寻找
continue;
}
// 提取完整一帧
const frame = this.receiveBuffer.slice(0, frameLen);
this.receiveBuffer.splice(0, frameLen); // 从缓存移除
console.info('处理完整帧:', frame.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' '));
this.processFrame(frame);
}
}
private processFrame(bytes: number[]): void {
const instruction = bytes[1];
const cmdType = bytes[2];
// 检查是否为响应帧 (D7=1) 且是对应的 instruction
// 检查是否为响应帧 (D7=1)
if ((instruction & 0x80) === 0) {
console.warn('非响应帧');
return;
@ -271,18 +335,11 @@ export class M9zService {
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;
if (cmdType === CMD_READ) {
// 格式: header(0) inst(1) cmdType(2) index(3) len(4) data... check(n-2) footer(n-1)
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]);
}
// data starts at 5
const payloadData = bytes.slice(5, 5 + len);
if (callbacks && callbacks.length > 0) {
try {
@ -292,12 +349,11 @@ export class M9zService {
console.error("解析错误", e);
callbacks.forEach(cb => cb.invokeError("解析错误"));
}
this.callbackMap.delete(realInstruction); // 清除回调
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;
} else if (cmdType === CMD_WRITE) {
// payload[0] is success
const success = bytes[5] === 0x01;
if (callbacks && callbacks.length > 0) {
if (success) {

Loading…
Cancel
Save