【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十五)之【超级设备模拟器实战】多设备交互调试:像上帝一样俯瞰整个智能厨房

【超级设备模拟器实战】多设备交互调试:像上帝一样俯瞰整个智能厨房

专栏:HarmonyOS 6.1.0 全场景实战|手把手带你打造《灵犀厨房》AI 厨艺助手

摘要 :上一篇我们让菜谱在手机、平板、智慧屏之间"飞"了起来。但一个致命的工程问题随之浮现:当代码里同时存在流转设备和厨电设备,共7台"虚拟设备"时,它们的在线状态、连接事件、温度变化交织在一起,就像同时看7个电视频道------你根本追踪不过来。今天,我们将构建一个超级设备仪表盘,像空中交通管制塔一样,实时监控所有设备的一举一动。这不仅是调试工具,更是一种"全局视野"的架构思维。


一、从"单挑"到"团战":为什么我们需要一个"指挥塔"?

回望我们的战果:

  • 第13篇,我们创造了一个智能厨电模拟器 KitchenDeviceSimulator,管理着烤箱、电磁炉、冰箱、油烟机。
  • 第14篇,我们又创造了一个分布式流转管理器 DistributedFlowManager,管理着手机、平板、智慧屏。

两个管理器各自运行得很完美。但作为一个开发者,当你同时调试"点击流转按钮到平板"和"同时烤箱还在升温"这两个并发场景时,你需要在两份代码、两份日志之间来回横跳。这就好比一个交通管制员,看着两张独立的地图来指挥同一片空域的飞机------迟早要撞上。

金句:软件复杂度的爆发,不在于加了几个新模块,而在于模块之间的交互状态呈指数级增长。

今天,我们要构建的 SuperDeviceManagerSuperDevicePage,就是这片天空唯一的"指挥塔"。它将7台设备的状态统一管理,将分散的日志汇聚成一条清晰的事件时间线,让你像上帝一样俯瞰整个智能厨房。

二、核心原理:聚合而非替代,观测而非侵入

设计这个"指挥塔"时,我们坚持了一条关键原则:绝不修改 KitchenDeviceSimulatorDistributedFlowManager 的内部逻辑

这就像一个合格的经理,不会去替下属干活,而是通过收集各个下属的工作报告,汇总成一份全局周报。SuperDeviceManager 就是这个经理角色。它通过查询getUnifiedDevices())和事件订阅addLog())的方式,从两个子管理器中收集信息,然后为上层UI提供一个统一的、只读的视图。

这种"聚合而非替代"的模式,保证了架构的健壮性:即使某天你不用超级仪表盘了,底下两个模拟器依然可以独立工作。这种设计在软件工程中称为观测器模式,是不破坏原有系统结构的"上帝之眼"。

三、架构设计:统一设备管理器的"数字沙盘"

在动手写代码之前,我们先从架构高度看清 SuperDeviceManager 的位置和职责。
Business层
聚合查询
聚合查询
生成
生成
生成
调试仪表盘
SuperDevicePage
健康度卡片
分类设备列表
事件日志时间线
★ SuperDeviceManager 统一指挥塔
DistributedFlowManager

流转设备: 3台
KitchenDeviceSimulator

厨电设备: 4台
DeviceTopology 拓扑快照
UnifiedDevice 统一设备
DeviceLog 事件日志

架构解读SuperDeviceManager 处于两个子管理器的上一层 。它不是继承,而是组合 ------它持有对两个子管理器的引用,并通过它们的公开接口获取数据。这种"中介者模式"让复杂的多设备交互变得井然有序。未来如果新增设备类型(如智能微波炉),只需在 SuperDeviceManager 中增加对一个新模拟器的引用即可,扩展性极强。

四、关键知识点详解:调试面板的技术选型

在设计超级设备仪表盘时,我们有几个关键技术决策要提前想清楚。

决策点 可选方案 《灵犀厨房》选型 理由
设备状态获取方式 轮询 vs 事件驱动 事件驱动 + 手动刷新 轮询浪费性能;事件驱动精准记录变化。手动刷新作为兜底,保证数据一致性
日志存储策略 持久化 vs 内存环形队列 内存环形队列(50条上限) 调试场景时效性短,重启丢失可接受;环形队列防止内存泄漏
健康度判断标准 二态(在线/离线) vs 三态 三态(healthy/warning/degraded) 全部在线=healthy,部分离线=warning,全部离线=degraded。三态比二态更细腻,一眼看出严重程度
仪表盘入口位置 独立入口 vs 嵌入式 厨电页的🔬按钮 厨电页本身就在管理设备,从此处进入符合用户心智;不干扰普通用户的正常使用路径

决策原则:调试工具的最高境界是"召之即来,挥之即去"。它应该在你需要时一触即达,但绝不影响正常用户的使用流程。在厨电页埋一个不起眼的🔬按钮,完美符合这一原则。

五、实战:构建超级设备"指挥塔"

现在,我们分四步走,从数据聚合层到可视化仪表盘,完成整个系统。

Step 1:定义统一设备"语言"------SuperDeviceManager

首先,在 business/SuperDeviceManager.ets 中定义统一的类型和聚合逻辑。

typescript 复制代码
// business/SuperDeviceManager.ets
import { DistributedFlowManager, FlowDevice, FlowDeviceType } from './DistributedFlowManager';
import { KitchenDeviceSimulator } from './KitchenDeviceSimulator';
import { KitchenDevice, DeviceType } from '../foundation/model/KitchenDevice';

// 设备大类
export enum DeviceCategory {
  PERSONAL = 'personal',  // 个人设备(手机/平板/智慧屏)
  KITCHEN = 'kitchen'     // 厨电设备
}

// 统一设备视图
export interface UnifiedDevice {
  deviceId: string;
  name: string;
  category: DeviceCategory;
  typeLabel: string;    // 如「智能烤箱」「MatePad Pro」
  isOnline: boolean;
  icon: string;
  detail: string;       // 实时详情,如「工作中 · 180°C」「待机」
}

// 设备拓扑快照
export interface DeviceTopology {
  localDeviceId: string;
  personalDevices: UnifiedDevice[];
  kitchenDevices: UnifiedDevice[];
  connectedCount: number;
  totalCount: number;
  snapshotTime: number;
}

// 健康报告
export interface DeviceHealthReport {
  totalDevices: number;
  connectedDevices: number;
  offlineDevices: number;
  status: 'healthy' | 'warning' | 'degraded';
  statusLabel: string; // 如「✅ 全设备正常」
}

// 设备事件日志
export interface DeviceLog {
  timestamp: number;
  deviceId: string;
  deviceName: string;
  event: string;        // 如「发现设备」「连接成功」「温度变化」
  detail: string;
  icon: string;         // 如 '🟢' '🔗' '🔍'
}

export class SuperDeviceManager {
  private flowManager: DistributedFlowManager = new DistributedFlowManager();
  private kitchenSimulator: KitchenDeviceSimulator = new KitchenDeviceSimulator();
  private logs: DeviceLog[] = []; // 环形队列,最多50条

  // 获取统一设备拓扑
  getTopology(): DeviceTopology {
    const personalDevices: UnifiedDevice[] = this.mapFlowDevices();
    const kitchenDevices: UnifiedDevice[] = this.mapKitchenDevices();
    const allDevices = [...personalDevices, ...kitchenDevices];
    const connectedCount = allDevices.filter(d => d.isOnline).length;

    console.info(`[SuperDevice] 生成拓扑快照: ${connectedCount}/${allDevices.length} 在线`);
    return {
      localDeviceId: 'PH:ON:E0:00:00:01',
      personalDevices,
      kitchenDevices,
      connectedCount,
      totalCount: allDevices.length,
      snapshotTime: Date.now()
    };
  }

  // 映射流转设备 → UnifiedDevice
  private mapFlowDevices(): UnifiedDevice[] {
    const devices = this.flowManager.getAvailableDevices();
    return devices.map(d => ({
      deviceId: d.deviceId,
      name: d.name,
      category: DeviceCategory.PERSONAL,
      typeLabel: d.type === FlowDeviceType.TABLET ? '平板' : '智慧屏',
      isOnline: true,
      icon: d.icon,
      detail: d.type === FlowDeviceType.TABLET ? '待流转 · 分步浏览' : '待流转 · 全屏教学'
    }));
  }

  // 映射厨电设备 → UnifiedDevice
  private mapKitchenDevices(): UnifiedDevice[] {
    const devices = this.kitchenSimulator.getAllDevices();
    return devices.map(d => {
      let detail = '';
      if (d.type === DeviceType.OVEN || d.type === DeviceType.INDUCTION_COOKER) {
        detail = d.status === 'working' ? `工作中 · ${d.temperature}°C` : `待机 · ${d.temperature}°C`;
      } else if (d.type === DeviceType.REFRIGERATOR) {
        detail = `冷藏 ${d.fridgeTemperature}°C`;
      } else {
        detail = '离线';
      }
      return {
        deviceId: d.deviceId,
        name: d.name,
        category: DeviceCategory.KITCHEN,
        typeLabel: d.type === DeviceType.OVEN ? '智能烤箱' : d.type === DeviceType.INDUCTION_COOKER ? '电磁炉' : d.type === DeviceType.REFRIGERATOR ? '冰箱' : '油烟机',
        isOnline: d.status !== 'offline',
        icon: d.type === DeviceType.OVEN ? '🔥' : d.type === DeviceType.INDUCTION_COOKER ? '♨️' : d.type === DeviceType.REFRIGERATOR ? '❄️' : '💨',
        detail
      };
    });
  }

  // 获取健康报告
  getHealthReport(): DeviceHealthReport {
    const topology = this.getTopology();
    const offlineCount = topology.totalCount - topology.connectedCount;
    let status: 'healthy' | 'warning' | 'degraded';
    let statusLabel: string;

    if (offlineCount === 0) {
      status = 'healthy';
      statusLabel = '✅ 全设备正常';
    } else if (offlineCount < topology.totalCount) {
      status = 'warning';
      statusLabel = `⚠️ ${offlineCount} 台离线`;
    } else {
      status = 'degraded';
      statusLabel = '❌ 全部离线';
    }

    return {
      totalDevices: topology.totalCount,
      connectedDevices: topology.connectedCount,
      offlineDevices: offlineCount,
      status,
      statusLabel
    };
  }

  // 添加事件日志(环形队列,最多50条)
  addLog(deviceId: string, deviceName: string, event: string, detail: string, icon: string): void {
    const log: DeviceLog = {
      timestamp: Date.now(),
      deviceId,
      deviceName,
      event,
      detail,
      icon
    };
    this.logs.unshift(log);
    if (this.logs.length > 50) {
      this.logs.pop(); // 移除最旧的
    }
    console.info(`[SuperDevice] 📋 日志: [${icon}] ${deviceName} - ${event}: ${detail}`);
  }

  // 获取日志历史
  getLogHistory(): DeviceLog[] {
    return [...this.logs];
  }

  // 清空日志
  clearLogs(): void {
    this.logs = [];
    console.info('[SuperDevice] 📋 日志已清空');
  }
}

核心点解读SuperDeviceManager 是一个典型的中介者 (Mediator)模式实现。它不做事,它只是把别人的事说清楚。mapFlowDevices()mapKitchenDevices() 两个私有方法将两种异构设备统一映射为 UnifiedDevice,抹平了差异。addLog 使用环形队列,保证日志内存不会无限增长。注意每个方法都有 console.info 输出,这在调试时形成双重保障:既有UI展示,又有Log窗口备份。

Step 2:构建"上帝视角"仪表盘------SuperDevicePage

pages/SuperDevicePage.ets 是本次的交付核心,它让开发者一眼看穿7台设备。

typescript 复制代码
// pages/SuperDevicePage.ets
import { SuperDeviceManager, DeviceCategory, DeviceHealthReport, UnifiedDevice, DeviceLog } from '../business/SuperDeviceManager';
import { router } from '@kit.ArkUI';

@Entry
@Component
struct SuperDevicePage {
  @State healthReport: DeviceHealthReport = {
    totalDevices: 0, connectedDevices: 0, offlineDevices: 0,
    status: 'healthy', statusLabel: '加载中...'
  };
  @State personalDevices: UnifiedDevice[] = [];
  @State kitchenDevices: UnifiedDevice[] = [];
  @State logs: DeviceLog[] = [];
  private manager: SuperDeviceManager = new SuperDeviceManager();

  aboutToAppear(): void {
    this.refreshDashboard();
  }

  // 全局刷新
  refreshDashboard(): void {
    const topology = this.manager.getTopology();
    this.personalDevices = topology.personalDevices;
    this.kitchenDevices = topology.kitchenDevices;
    this.healthReport = this.manager.getHealthReport();
    this.logs = this.manager.getLogHistory();
    
    this.manager.addLog('SYSTEM', '超级设备仪表盘', '手动刷新', '重新扫描设备拓扑', '🔍');
    console.info('[SuperDevicePage] 仪表盘已刷新');
  }

  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Button({ type: ButtonType.Circle, stateEffect: true }) {
          SymbolGlyph($r('sys.symbol.chevron_left')).fontSize(24)
        }
        .backgroundColor(Color.Transparent)
        .onClick(() => router.back())

        Text('🔬 超级设备仪表盘')
          .fontSize(18).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center)

        Button('刷新')
          .fontSize(14)
          .onClick(() => this.refreshDashboard())
      }
      .width('100%').padding(16)

      // 可滚动内容区
      Scroll() {
        Column() {
          // 1. 健康度卡片
          this.buildHealthCard()

          // 2. 设备分类列表
          this.buildDeviceSection('📱 个人设备', this.personalDevices)
          Divider().strokeWidth(1).color('#E0E0E0').margin({ left: 16, right: 16 })
          this.buildDeviceSection('🔧 厨电设备', this.kitchenDevices)

          // 3. 事件日志
          this.buildLogSection()
        }
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%').backgroundColor('#F8F8F8')
  }

  // 健康度卡片
  @Builder
  buildHealthCard() {
    Column() {
      Text(this.healthReport.statusLabel)
        .fontSize(24).fontWeight(FontWeight.Bold)
        .fontColor(this.healthReport.status === 'healthy' ? '#4CAF50' :
                   this.healthReport.status === 'warning' ? '#FF9800' : '#F44336')
        .padding({ top: 16 })

      Row() {
        this.buildStatItem('在线', `${this.healthReport.connectedDevices}/${this.healthReport.totalDevices}`)
        this.buildStatItem('离线', `${this.healthReport.offlineDevices}`)
        this.buildStatItem('快照时间', new Date(Date.now()).toLocaleTimeString())
      }
      .width('100%').justifyContent(FlexAlign.SpaceEvenly).padding(16)
    }
    .width('90%').borderRadius(16).backgroundColor(Color.White)
    .shadow({ radius: 8, color: '#10000000' }).margin(16)
  }

  @Builder
  buildStatItem(label: string, value: string) {
    Column() {
      Text(value).fontSize(28).fontWeight(FontWeight.Bold).fontColor('#333')
      Text(label).fontSize(12).fontColor('#999').margin({ top: 4 })
    }
  }

  // 设备分组列表
  @Builder
  buildDeviceSection(title: string, devices: UnifiedDevice[]) {
    Column() {
      Text(title).fontSize(16).fontWeight(FontWeight.Bold).padding(16)
      ForEach(devices, (device: UnifiedDevice) => {
        Row() {
          Text(device.icon).fontSize(28)
          Column() {
            Text(device.name).fontSize(15).fontWeight(FontWeight.Medium)
            Text(device.typeLabel).fontSize(12).fontColor('#999')
          }.layoutWeight(1).margin({ left: 12 })
          Text(device.detail).fontSize(12).fontColor('#666').margin({ right: 8 })
          Circle({ width: 10, height: 10 })
            .fill(device.isOnline ? Color.Green : Color.Red)
        }
        .width('100%').padding(14)
        .borderRadius(12).backgroundColor(Color.White).margin({ left: 16, right: 16, bottom: 8 })
      })
    }
  }

  // 事件日志时间线
  @Builder
  buildLogSection() {
    Column() {
      Row() {
        Text('📋 事件日志').fontSize(16).fontWeight(FontWeight.Bold)
        Blank()
        Button('清空').fontSize(12).height(28).backgroundColor(Color.Transparent)
          .onClick(() => {
            this.manager.clearLogs();
            this.logs = [];
          })
      }
      .width('100%').padding(16)

      ForEach(this.logs, (log: DeviceLog) => {
        Row() {
          Text(log.icon).fontSize(16)
          Column() {
            Text(`${log.deviceName} · ${log.event}`).fontSize(13).fontWeight(FontWeight.Medium)
            Text(log.detail).fontSize(11).fontColor('#999')
          }.layoutWeight(1).margin({ left: 8 })
          Text(new Date(log.timestamp).toLocaleTimeString()).fontSize(10).fontColor('#BBB')
        }
        .width('100%').padding({ left: 16, right: 16, top: 8, bottom: 8 })
        .borderRadius(8).backgroundColor(Color.White).margin({ left: 16, right: 16, bottom: 4 })
      })

      if (this.logs.length === 0) {
        Text('暂无事件日志,点击刷新开始扫描').fontSize(14).fontColor('#999').padding(24).textAlign(TextAlign.Center)
      }
    }
  }
}

核心点解读SuperDevicePagebuild 方法分为三个清晰区域:健康度卡片、设备分组列表、事件日志时间线。每个区域都通过 @Builder 方法独立构建,保持 build 主干清晰。aboutToAppear 自动触发首次刷新,确保页面打开即显示最新数据。注意日志部分使用了 scrollable 配合 layoutWeight(1),确保长时间列表可以流畅滚动,而顶部健康卡片保持固定可见。

Step 3:在厨电页埋入入口,打通开发者动线

pages/KitchenDevicePage.ets 的导航栏增加一个极简入口:

typescript 复制代码
// pages/KitchenDevicePage.ets (修改片段)
import { router } from '@kit.ArkUI';

// 在导航栏 Row 中添加
Button('🔬')
  .fontSize(14).height(32).width(36)
  .backgroundColor('#F0F0F0').borderRadius(16)
  .onClick(() => {
    console.info('[KitchenDevicePage] 开发者点击🔬,跳转超级设备仪表盘');
    router.pushUrl({ url: 'pages/SuperDevicePage' });
  })

变化点解读:这是对第13篇厨电页的一个精巧扩展。一个不起眼的🔬按钮,就像汽车的"OBD诊断接口"------普通用户永远不会注意它,但开发者在需要时,一插即用,全局设备状态尽收眼底。这是"开发者体验"的极致体现。

Step 4:注册路由,确保页面可达

src/main/resources/base/profile/main_pages.json 中注册新页面:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/MainContainer",
    "pages/RecipeDetailPage",
    "pages/KitchenDevicePage",
    "pages/ProfilePage",
    "pages/HealthDashboardPage",
    "pages/SuperDevicePage"
  ]
}

核心点解读 :在 HarmonyOS Stage 模型中,每个页面必须在 main_pages.json 中显式注册。这是路由生效的前提,也是应用安全性的保障。

六、运行与结果验证

现在,让我们运行 App,体验从厨电页到超级仪表盘的完整调试流程。

操作步骤

  1. 启动 App,进入"🔧 厨电"Tab。

  2. 点击右上角的🔬仪表盘按钮。

  3. 观察超级设备仪表盘加载。

  4. 查看设备分类列表、事件日志。

  5. 点击"刷新"按钮,观察日志更新。

  6. 返回厨电页,连接一台离线厨电。

    点击连接设备:

  7. 再次进入仪表盘,刷新后观察健康度变化。

    查看事件日志:

    解读 :首次加载时显示6/7在线(油烟机离线),健康状态为⚠️。当用户在厨电页连接油烟机后再次刷新,变为7/7在线,健康状态切换为✅。日志时间线精确记录了每一次刷新事件。这证明了 SuperDeviceManager 成功地聚合了两个子管理器的状态,并将其以统一视图呈现。

七、本阶段总结与下篇预告

今天,我们为《灵犀厨房》构建了一个俯瞰全局的"指挥塔"。我们设计了一个统一设备管理器 ,将流转设备和厨电设备的状态聚合成一张清晰的数字沙盘;我们实现了一个超级设备仪表盘,包含健康度卡片、分类设备列表和事件日志时间线三大模块;我们还在厨电页埋入了一个优雅的开发者入口,让调试能力一触即达。

回顾今天的核心要点:

  • 聚合而非替代SuperDeviceManager 通过组合模式聚合两个子管理器,不侵入原有逻辑
  • 观测而非侵入:统一仪表盘是纯粹的观测层,不影响任何业务功能
  • 环形队列:50条上限的事件日志,兼顾调试需求与内存安全
  • 三态健康度:healthy / warning / degraded,比简单的在线/离线更具信息量

但智慧厨房还缺最后一块拼图:当你双手沾满面粉,根本无法触摸屏幕时,怎么办?

下篇预告 :我们将进入《灵犀厨房》的"语音"维度------语音合成播报。让烹饪步骤用温暖的AI语音自动读出来,真正解放你的双手,实现"听步骤做菜"的无感交互。


📚 本系列持续更新中:下一篇将让你彻底解放双手,用耳朵"听"菜谱,敬请期待。

🔗 专栏入口:[《HarmonyOS 6.1 全场景实战》合集]


相关推荐
若兰幽竹5 小时前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十四)之【分布式流转】让菜谱“飞”:手机选、平板看、智慧屏播的全场景秘诀
分布式·华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹1 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十三)之【智能厨电模拟】用代码“凭空”创造智能厨房:《灵犀厨房》的全场景前奏
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹1 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十二)之【营养分析引擎】计算个性化卡路里建议:给《灵犀厨房》装上“营养大脑”
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹3 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战之补充【架构进化】灵犀厨房四层分层设计:给鸿蒙 App 搭一副坚不可摧的骨架
架构·鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹9 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(三):ArkTS 高效开发:TypeScript 核心与 API 23 新规
harmonyos·鸿蒙系统·harmonyos6.1.0
若兰幽竹18 天前
【HarmonyOS 6.1 全场景实战】开篇词:打造消除“吃饭焦虑”的《灵犀厨房》
harmonyos·鸿蒙开发·华为鸿蒙系统