Lufaneng 6 months ago
parent 1e17bfe0a6
commit 975eab89b2

@ -13,10 +13,16 @@ struct BluetoothControlPage {
@State isConnected: boolean = false;
@State statusMessage: string = '正在连接...';
@State currentIndex: number = 0;
@State showExitDialog: boolean = false;
private m9zService: M9zService = M9zService.getInstance();
private controller: TabsController = new TabsController();
onBackPress() {
this.showExitDialog = true;
return true;
}
aboutToAppear() {
const params = router.getParams() as Record<string, string>;
if (params) {
@ -50,6 +56,7 @@ struct BluetoothControlPage {
}
build() {
Stack() {
Column() {
// 顶部导航栏
Row() {
@ -59,7 +66,7 @@ struct BluetoothControlPage {
.fontColor('#fff')
.margin({ right: 16 })
.onClick(() => {
router.back();
this.showExitDialog = true;
})
Column() {
@ -67,9 +74,18 @@ struct BluetoothControlPage {
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#eaf6fc')
Text(this.statusMessage)
.fontSize(12)
.fontColor(this.isConnected ? '#22fdc8' : '#fa5050')
Row() {
Text(this.deviceId)
.fontSize(12)
.fontColor('#a0b0c0')
.margin({ right: 10 })
Text(this.statusMessage)
.fontSize(12)
.fontColor(this.isConnected ? '#22fdc8' : '#fa5050')
}
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
@ -134,6 +150,61 @@ struct BluetoothControlPage {
.width('100%')
.height('100%')
.backgroundColor('#0a1629') // 深蓝色背景
// Exit Dialog
if (this.showExitDialog) {
Column() {
Column() {
Text('提示')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#eaf6fc')
.margin({ bottom: 15 })
Text('是否断开连接并退出?')
.fontSize(14)
.fontColor('#b2ebf2')
.margin({ bottom: 20 })
Row() {
Button('取消')
.fontSize(14)
.fontColor('#999999')
.backgroundColor('rgba(255, 255, 255, 0.1)')
.borderRadius(4)
.layoutWeight(1)
.onClick(() => {
this.showExitDialog = false;
})
Button('确定')
.fontSize(14)
.fontColor('#fa5050')
.backgroundColor('rgba(255, 80, 80, 0.2)')
.borderRadius(4)
.layoutWeight(1)
.margin({ left: 15 })
.onClick(() => {
this.m9zService.disconnect();
this.showExitDialog = false;
router.back();
})
}
.width('100%')
}
.width('80%')
.padding(20)
.backgroundColor('rgba(20, 38, 81, 0.98)')
.borderRadius(12)
.border({ width: 1, color: '#213b67' })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.5)')
.justifyContent(FlexAlign.Center)
.zIndex(9999)
}
}
}
@Builder TabBuilder(index: number, name: string) {

@ -255,6 +255,7 @@ struct BluetoothSearchPage {
// 连接设备
connectDevice(device: BluetoothDevice): void {
this.stopSearch();
this.confirmDialogDevice = device;
this.showConfirmDialog = true;
}
@ -316,21 +317,27 @@ struct BluetoothSearchPage {
Column() {
// 顶部搜索栏
Row() {
Text('设备列表(鸿蒙)')
Text('设备列表')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#eaf6fc')
Blank()
Button(this.isSearching ? '搜索中...' : '搜索')
Button(this.isSearching ? '停止搜索' : '开始搜索')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor(this.isSearching ? '#eaf6fc' : '#0b1830')
.backgroundColor(this.isSearching ? 'rgba(34, 253, 200, 0.5)' : '#22fdc8')
.fontColor(this.isSearching ? '#ff9800' : '#0b1830')
.backgroundColor(this.isSearching ? 'rgba(255, 152, 0, 0.2)' : '#22fdc8')
.borderRadius(25)
.padding({ left: 20, right: 20, top: 8, bottom: 8 })
.onClick(() => this.startSearch())
.onClick(() => {
if (this.isSearching) {
this.stopSearch();
} else {
this.startSearch();
}
})
}
.width('100%')
.padding(15)

@ -10,6 +10,10 @@ import { BusinessError } from '@kit.BasicServicesKit';
struct Index {
@State message: string = '智能照明控制';
aboutToAppear() {
router.replaceUrl({ url: 'pages/BluetoothSearchPage' });
}
build() {
Column() {
// 顶部标题

@ -1,4 +1,4 @@
import { M9zService, TaskProgram, TaskInstruction, M9zCmd, ControlTask, TaskLoop } from '../../utils/M9zService';
import { M9zService, TaskProgram, TaskInstruction, M9zCmd, ControlTask, TaskLoop, SunRiseTimeData } from '../../utils/M9zService';
import { promptAction } from '@kit.ArkUI';
@Observed
@ -16,6 +16,8 @@ export struct AutoMode {
@State startTask: ControlTask = this.createEmptyTask();
@State stopTask: ControlTask = this.createEmptyTask();
@State middleTasks: ControlTask[] = [];
@State sunriseTime: string = '--:--:--';
@State sunsetTime: string = '--:--:--';
@State isLoading: boolean = false;
@State isSetting: boolean = false;
@ -118,6 +120,26 @@ export struct AutoMode {
});
}
fetchSunRiseTime() {
this.m9zService.getSunRiseTime({
onSuccess: (data: SunRiseTimeData): void => {
if (data.sunrise) this.sunriseTime = this.formatTimeOnly(data.sunrise);
if (data.sunset) this.sunsetTime = this.formatTimeOnly(data.sunset);
this.isLoading = false;
},
onError: (err: string): void => {
this.isLoading = false;
}
});
}
formatTimeOnly(date: Date): string {
const h = String(date.getHours()).padStart(2, '0');
const min = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${h}:${min}:${s}`;
}
changeViewMode(index: number) {
this.selectedViewMode = index;
this.refreshData();
@ -141,12 +163,29 @@ export struct AutoMode {
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;
let h = 0;
let m = 0;
if (task.execTime.includes(' ')) {
// Format: YYYY-MM-DD HH:mm:ss
const timePart = task.execTime.split(' ')[1];
const parts = timePart.split(':');
if (parts.length >= 2) {
h = Number(parts[0]);
m = Number(parts[1]);
}
} else {
// Format: HH:mm (or HH:mm:ss)
const parts = task.execTime.split(':');
if (parts.length >= 2) {
h = Number(parts[0]);
m = Number(parts[1]);
}
}
const minutes: number = h * 60 + m;
if (true) { // Always generate instruction if we have time
const ins1: TaskInstruction = new TaskInstruction();
ins1.type = 0x01;
ins1.param = task.timeType === 0 ? 0x01 : 0x02;
@ -455,59 +494,194 @@ export struct AutoMode {
.width('100%')
// Control Panel
Row() {
Column() {
// Current Mode Display
Column() {
Text('⚙️ 当前运行模式')
.fontColor('#b2ebf2')
.fontSize(14)
.width('100%')
.margin({ bottom: 8 })
Row() {
Text(`自动模式${this.currentMode + 1}`)
.fontColor('#22fdc8')
.fontSize(16)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding(12)
.backgroundColor('rgba(34, 253, 200, 0.1)')
.borderRadius(8)
.border({ width: 1, color: '#22fdc8' })
}
.width('100%')
.padding(10)
.backgroundColor('rgba(15, 30, 60, 0.4)')
.borderRadius(8)
.margin({ bottom: 15 })
Button(`应用模式${this.selectedViewMode+1}到设备`)
.onClick((): void => {this.changeDeviceMode();})
.width('80%')
.backgroundColor('#22fdc8')
.fontColor('#000')
.width('100%')
.backgroundColor('#102a45')
.fontColor('#22fdc8')
.border({ width: 1, color: '#22fdc8' })
.height(44)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(10)
// 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)
.margin({bottom: 20})
// Dialog
if(this.showTaskDialog) {
Stack() {
// Mask
Rect().width('100%').height('100%').fill('rgba(0,0,0,0.8)').onClick((): void => {this.showTaskDialog = false;})
// Content
Column() {
Text('编辑任务').fontColor('#fff').fontSize(16).margin({bottom: 10})
// 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)
Text('编辑任务').fontColor('#fff').fontSize(16).margin({bottom: 10}).fontWeight(FontWeight.Bold)
Scroll() {
Column() {
// 1. Time Type
Text('时间类型').fontColor('#b2ebf2').margin({top: 10, bottom: 5}).alignSelf(ItemAlign.Start).fontSize(14)
Row() {
Button('定时时间')
.backgroundColor(this.editingTask.timeType === 0 ? '#22fdc8' : '#333')
.fontColor(this.editingTask.timeType === 0 ? '#000' : '#fff')
.onClick(() => { this.editingTask.timeType = 0; })
.height(30).fontSize(12).margin({right: 10})
Button('经纬度时间')
.backgroundColor(this.editingTask.timeType === 1 ? '#22fdc8' : '#333')
.fontColor(this.editingTask.timeType === 1 ? '#000' : '#fff')
.onClick(() => { this.editingTask.timeType = 1; })
.height(30).fontSize(12)
}.width('100%').justifyContent(FlexAlign.Start)
// 2. Time Input
if (this.editingTask.timeType === 0) {
Text('执行时间').fontColor('#b2ebf2').margin({top: 15, bottom: 5}).alignSelf(ItemAlign.Start).fontSize(14)
Row() {
TextInput({ text: this.editingTask.execTime.split(' ')[1] ? this.editingTask.execTime.split(' ')[1].substring(0,5) : '00:00' })
.width(120).height(40)
.backgroundColor('#333').fontColor('#fff')
.borderRadius(5)
.onChange((val: string) => {
// Simple validation/update. better to use TimePicker but native TimePicker takes much space.
// Assume user types HH:mm. we append to today's date prefix on save or keep full string.
// Reference Vue: uses picker. Here let's just use text input for simplicity or check if we can use TimePickerDialog
// For now update logic:
if (val.length === 5) {
const today = new Date();
const y = today.getFullYear();
const m = String(today.getMonth() + 1).padStart(2, '0');
const d = String(today.getDate()).padStart(2, '0');
this.editingTask.execTime = `${y}-${m}-${d} ${val}:00`;
}
})
Button('选择时间')
.onClick(() => {
try {
TimePickerDialog.show({
useMilitaryTime: true,
onAccept: (value: TimePickerResult) => {
const today = new Date();
const y = today.getFullYear();
const m = String(today.getMonth() + 1).padStart(2, '0');
const d = String(today.getDate()).padStart(2, '0');
const h = String(value.hour).padStart(2, '0');
const min = String(value.minute).padStart(2, '0');
this.editingTask.execTime = `${y}-${m}-${d} ${h}:${min}:00`;
}
})
} catch(e) {
console.error('TimePicker error', e);
}
})
.margin({left: 10}).fontSize(12).height(30).backgroundColor('#2d3a51')
.fontColor('#22fdc8').border({width:1, color: '#22fdc8'})
}.width('100%').justifyContent(FlexAlign.Start)
}
// 3. Loop Selection
Text('选择开启回路').fontColor('#b2ebf2').margin({top: 15, bottom: 5}).alignSelf(ItemAlign.Start).fontSize(14)
Grid() {
ForEach([0,1,2,3,4,5,6,7,8,9], (idx: number) => {
GridItem() {
Text(`${idx+1}`)
.backgroundColor(this.editingLoops.includes(idx) ? '#22fdc8' : '#333')
.fontColor(this.editingLoops.includes(idx) ? '#000' : '#fff')
.width(30).height(30).borderRadius(15)
.textAlign(TextAlign.Center)
.onClick((): void => {this.toggleEditingLoop(idx);})
}
})
}.columnsTemplate('1fr 1fr 1fr 1fr 1fr').height(80)
// 4. Brightness Settings for Selected Loops
if (this.editingLoops.length > 0) {
Text('调光设置').fontColor('#b2ebf2').margin({top: 15, bottom: 5}).alignSelf(ItemAlign.Start).fontSize(14)
List() {
ForEach(this.editingLoops, (loopIdx: number) => {
ListItem() {
Row() {
Text(`通道${loopIdx+1}`).fontColor('#fff').fontSize(12).width(50)
Slider({
value: this.editingTask.loops[loopIdx].pwm || 0,
min: 0,
max: 100,
style: SliderStyle.OutSet
})
.layoutWeight(1)
.trackColor('#333')
.selectedColor('#22fdc8')
.onChange((val: number) => {
this.editingTask.loops[loopIdx].pwm = val;
})
Text(`${Math.round(this.editingTask.loops[loopIdx].pwm || 0)}%`)
.fontColor('#22fdc8').fontSize(12).width(40).textAlign(TextAlign.End)
}.width('100%').padding({top: 5, bottom: 5})
}
})
}.height(150)
} // end if
}.width('100%')
} // end Scroll
.layoutWeight(1)
.width('100%')
// Buttons
Row() {
Button('取消').onClick((): void => {this.showTaskDialog = false;}).backgroundColor('#666')
Button('保存').onClick((): void => {this.saveTask();}).backgroundColor('#22fdc8').fontColor('#000')
}.width('100%').justifyContent(FlexAlign.SpaceAround).margin({ top: 20 })
Button('取消').onClick((): void => {this.showTaskDialog = false;}).backgroundColor('#666').width('40%')
Button('保存').onClick((): void => {this.saveTask();}).backgroundColor('#22fdc8').fontColor('#000').width('40%')
}.width('100%').justifyContent(FlexAlign.SpaceAround).margin({ top: 10, bottom: 10 })
}
.backgroundColor('#1d3553')
.padding(20)
.width('90%')
.height('80%') // Taller dialog
.borderRadius(10)
}.position({x:0,y:0}).zIndex(100)
}
}
.padding({ left: 15, right: 15, top: 15, bottom: 80 })
if (this.isLoading) {
Stack() {
@ -529,7 +703,15 @@ export struct AutoMode {
Row() {
Text(title).fontColor('#22fdc8').fontWeight(FontWeight.Bold)
Blank()
Text(task.execTime).fontColor('#fff')
Column() {
Text(task.timeType === 0 ? '定时时间' : '经纬度时间')
.fontSize(10)
.fontColor('#aaa')
.alignSelf(ItemAlign.End)
Text(task.execTime)
.fontColor('#fff')
.alignSelf(ItemAlign.End)
}
}.width('100%').margin({ bottom: 5 })
Row() {

@ -23,6 +23,7 @@ export struct ManualControl {
private m9zService: M9zService = M9zService.getInstance();
private timer: number = -1;
private currentDeviceDate: Date | null = null;
aboutToAppear() {
this.initLoops();
@ -45,13 +46,12 @@ export struct ManualControl {
}
startTimer() {
if (this.timer > 0) clearInterval(this.timer);
this.timer = setInterval((): void => {
// 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.
if (this.currentDeviceDate) {
this.currentDeviceDate.setSeconds(this.currentDeviceDate.getSeconds() + 1);
this.deviceTime = this.formatDateTime(this.currentDeviceDate);
}
}, 1000);
}
@ -72,6 +72,7 @@ export struct ManualControl {
}
refreshData() {
if (this.isLoading) return;
this.isLoading = true;
// 1. Get Manual Info
@ -91,6 +92,7 @@ export struct ManualControl {
// 2. Get Device Time
this.m9zService.getDeviceTime({
onSuccess: (date: Date): void => {
this.currentDeviceDate = date;
this.deviceTime = this.formatDateTime(date);
this.fetchSunRiseTime();
},
@ -104,11 +106,13 @@ export struct ManualControl {
// 3. Get SunRise/Set Time
this.m9zService.getSunRiseTime({
onSuccess: (data: SunRiseTimeData): void => {
console.info('ManualControl: fetchSunRiseTime success:', JSON.stringify(data));
if (data.sunrise) this.sunriseTime = this.formatTimeOnly(data.sunrise);
if (data.sunset) this.sunsetTime = this.formatTimeOnly(data.sunset);
this.checkLoadingComplete();
},
onError: (err: string): void => {
console.error('ManualControl: fetchSunRiseTime error:', err);
this.checkLoadingComplete();
}
});
@ -257,7 +261,8 @@ export struct ManualControl {
onSuccess: (success: boolean): void => {
this.showToastMessage('时间同步成功');
// Refresh display
this.deviceTime = this.formatDateTime(new Date());
this.currentDeviceDate = new Date();
this.deviceTime = this.formatDateTime(this.currentDeviceDate);
},
onError: (err: string): void => this.showToastMessage('同步失败:' + err)
});
@ -294,22 +299,16 @@ export struct ManualControl {
Column() {
// --- Header Status ---
Column() {
Row() {
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)
.padding(10)
.backgroundColor('rgba(15, 30, 60, 0.4)')
.borderRadius(8)
.margin({bottom: 20})
.margin({bottom: 10})
// --- Control Mode ---
Column() {
@ -367,7 +366,7 @@ export struct ManualControl {
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsGap(15)
.height(160)
.height(190)
}
.width('100%')
.margin({bottom: 20})
@ -471,7 +470,7 @@ export struct ManualControl {
.borderRadius(8)
}
.width('100%')
.padding(15)
.padding({ left: 15, right: 15, top: 15, bottom: 80 })
}
.width('100%')
.height('100%')

@ -466,6 +466,7 @@ export class M9zService {
}
private parseSunRiseTime(data: number[]): SunRiseTimeData {
console.info('M9zService: parseSunRiseTime raw data:', JSON.stringify(data));
const res = new SunRiseTimeData();
// 假设返回两个 uint32 时间戳 (日出,日落)
if (data.length >= 8) {
@ -473,6 +474,9 @@ export class M9zService {
const setTs = this.bytesToUint32LE(data.slice(4, 8));
res.sunrise = new Date(riseTs * 1000);
res.sunset = new Date(setTs * 1000);
console.info('M9zService: parsed sunrise:', res.sunrise, 'sunset:', res.sunset);
} else {
console.warn('M9zService: parseSunRiseTime data length insufficient:', data.length);
}
return res;
}

Loading…
Cancel
Save