开源可视化引擎 Meta2d.js 实战 - 实时数据通信逻辑分析与实现

实时数据通信

实时数据通信,是物联网平台中必备的功能。可以实时、无刷新的将界面组件的重要属性值,如数据、文本、形状、行为、动画等进行双向传输和控制,实现设备数据实时监控、数据大屏展示以及数字孪生。

实时数据通信对于meta2d.js来说,就是远程服务器端发送给当前网页端的数据,实时更新到图纸上的一个或多个组件的属性值。图纸上的组件(s),也能够根据事件规则的设定进行实施反馈于转发。

一、通信流程

meta2d.js支持mqtt、websocket和http三种方式的数据实时通信协议。http采用的是定时请求远程api,获取响应数据后,更新组件的指定属性。mqtt和websocket协议,则依赖于 mqtt.js 实现。

通信流程大致如下:

  1. 建立连接
  2. 监听数据
  3. 更新数据

1、建立连接

建立连接,可以通过2种方式:打开图纸和调用api手动控制连接。

  1. 打开图纸文件。如果图纸文件数据中,包含有协议配置的连接信息,则会自动连接。在 meta2d库的core.js 中,open方法相关代码如下:

connectSocket方法内部,调用了3类协议的连接方法。

typescript 复制代码
  connectSocket() {
    this.connectWebsocket();
    this.connectMqtt();
    this.connectHttp();
  }

这3个函数主要逻辑就是读取 store.data中名为 mqtt、websocket、https 这3个对象的设置信息。如果存在这些对象,就读取配置信息,调用相关的实现函数进行连接服务器。代码实现并不复杂,不在此进行进一步介绍。

  1. 通过手动调用api

如果,你设置了相关协议的配置信息,可以直接手动调用connectSocket方法连接3个,或者单独调用其中的一个。

2、监听数据

对于mqtt和websocket来说,本质上是在mqtt.js 连接对象上,注册消息监听事件的自定义函数,来接收信息并使用自定义的业务逻辑。当然,我们通过额外的设置,可以增加自己的处理逻辑,比如下面代码,以mqtt协议为例:

typescript 复制代码
if (engine.mqttClient) {
  // 监听连接事件
  engine.mqttClient
    .on('connect', () => {
      console.info('连接Mqtt服务器成功: ', engine?.store.data.mqtt);
    })
    .on('message', (topic, message) => {
      try {
        console.info('收到Mqtt主题消息: ', topic, JSON.parse(message.toString()));
      } catch (e) {
        console.warn('收到Mqtt主题消息,但转换json格式失败: ', message.toString() + ' error: ' + e);
      }
    });
}

代码中,engine是meta2d对象的实例变量。其中内置的mqttClient属性,是建立连接后的 mqtt.js 对象。通过注册mqtt连接对象相关事件的监听函数,来实现自定义相关业务逻辑。

3、更新数据

上面章节已说明,注册了各类事件的自定义函数,会接收到协议通信的数据。而更新数据,就是在这些自定义函数内部中,将收到的数据,通过调用核心库的api,来更新图纸组件的属性值。

核心库的2个api至关重要:

1)根据组件的id、或者tag来查找组件,返回的是组件的数组对象。

typescript 复制代码
find(id:string): Pen[]

2)设置属性的值

typescript 复制代码
setValue(data: IValue, { render, doEvent, history, }?: {
    render?: boolean;
    doEvent?: boolean;
    history?: boolean;
}): void;

看一下参数 data 的类型 IValue定义:

typescript 复制代码
export declare type IValue = Pen & Partial<ChartData> & Partial<Record<'tag' | 'newId', string>> & {
    [key: string]: any;
};

可以看出,在setValue这个场景下(更新组件的属性值),data应该代表数据对象本身,也就是Pen对象。其中必须包含id或者tag属性,以便核心库确定更新的是哪个pen。

另一个需要说明的参数是 render 参数。如果组件的某个属性值更新后会产生外观上的变化,canvas界面需要实时反映出变化结果,就需要将其设置为true。

注意:每一次让canvas数据更新,都会产生计算量。那么一个建议就是,当一次更新多个组件的属性时,setValue中的render属性应设置为false,在完成所有组件的更新后,再调用api让canvas更新视图。

typescript 复制代码
for (let i = 0; i < selectedTarget.value.pen.length; i++) {
  const p = selectedTarget.value.pen[i];
  engineObj.value.setValue(
    {
      id: p.id,
      [name]: value,
    },
    {
      render: false,      // 更新后,不渲染canvas
      history: true,
    }
  );
}
engineObj.value.render(); // 渲染canvas

注:变量 selectedTarget 代表了选中的组件列表,engineObj代表了meta2d实例对象。

二、协议配置说明

2.1 mqtt配置

乐吾乐官网文档 对mqtt协议有说明,但必须说,需要进一步说明,才能够更容易理解。

上面的params是mqtt协议连接的配置对象,其中,第一个参数 mqtt 的值 就是需要进一步说明的。

由于mqtt协议的实现其、是基于mqtt.js实现,故连接实际上是在websocket协议之上建立的。因此协议头是 ws:// 或 wss:// 开头。至于是否采用ssl,取决于你的设置(你可以自定义,来决定采用的协议头)。完整的url定义如下:

ws(s)://服务器地址:端口/上下文路径

以emq免费mqtt服务器为例,url就是这样的:

wss://broker.emqx.io:8084/mqtt

2.2 websocket配置

websocket协议的配置,只有一个,就是url,和上面mqtt的url定义结构完全相同,这取决于你采用的websocket服务器的具体的实现和要求。

2.3 http配置

注意是3个参数:url、httpTimeInterval、httpHeaders的设置:

注意一点,在最新的meta2d版本中,store.data中的https,被设计为一个对象数组。应该是为了支持多个http请求地址。同时,也兼容老版本中,store.data中的http对象。因此,设置和读取http的配置时,选你自己喜欢的就行。

三、数据结构

无论采用哪一种协议和远程数据接口进行通信,在meta2d的监听和更新数据逻辑上,数据结构上必须循序三类:

  1. 按组件id,更新属性值
  2. 按组件标签,更新属性值
  3. 按数据绑定id,更新属性值

这里说的数据结构,指的是远程服务器端,发送给网页端数据的结构,通常是一个json字符串,meta2d通过上面介绍的3类通信协议,可以收到这个字符串并解析成json对象后,调用核心api来更新组件的属性。

3.1 按组件id更新属性值的数据结构

typescript 复制代码
{
  id: 'pen1',
  text: 'new text'
}

如果需要一次性更新多个组件,结构是对象数组

typescript 复制代码
[{
  id: 'pen1',
  text: 'new text'
}]

3.2 按组件标签更新属性值的数据结构

typescript 复制代码
{
  tag: 'tag',      // tag的名字
  text: 'new text' // 属性及新的属性值
}

3.3 按数据绑定id更新数据的数据结构

typescript 复制代码
{
  dataId: 'id',     
  value: 'value' 
}

dataId,代表了一个关联关系。它将某个数据点和图纸上的某个属性进行了关联。例如:

某个设备的电压(数据点)的id是:id9527,通过关联操作,将其关联到id为 pen9527 的text属性。那么如果数据端发送了

typescript 复制代码
{
  dataId: 'id9527',     
  value: '200' 
}

meta2d就会将id为pen9527的text的值,更新为:200

四、自定义数据结构处理

当设计器页面接收到的数据,不是上面的结构的时候,基本上这是大概率会发生的。比如我们的后台api返回的通常是下面的架构:

typescript 复制代码
{
  code: 200,     
  data: {deviceId: 'afmc8976', temperature: 67, ...} 
}

遇到这样的情况,我们需要做的是

  1. 实现自定义结构解析函数
  2. 注册自定义函数

4.1 通信逻辑分析

实现自定义结构解析函数的主要作用,就将你的数据结构和上面章节介绍的3种结构进行转换,然后使用更新数据的方法。而注册自定义函数,就是定义引擎对象的socketFn函数(来自官网文档):

typescript 复制代码
meta2d.socketFn = (message, context) => {
  // Do sth
  meta2d.setValue(pen);
  
  //return false; //表示仅执行自定义的回调函数方法
  //return true; //表示除了执行自定义的回调方法外,还会执行核心库方法
};

我们来搞清楚实现自定义函数的逻辑。先来看 socketFn 的定义:

typescript 复制代码
socketFn: (
  e: string,
  // topic: string,
  context?: {
    meta2d?: Meta2d;
    type?: string;
    topic?: string;
    url?: string;
  }
) => boolean;

socketFn接收2个参数,e和context。翻看源码,我们会发现在socket建立连接和接收到消息时,会调用一个方法 socketCallback:

再看看socketCallback的定义:

typescript 复制代码
socketCallback(
    message: string,
    context?: { type?: string; topic?: string; url?: string }
  ) {
    this.store.emitter.emit('socket', { message, context });
    if (
      this.socketFn &&
      !this.socketFn(message, {
        meta2d: this,
        type: context.type,
        topic: context.topic,
        url: context.url,
      })
    ) {
      return;
    }

    ......
}

可以看到,socketCallback 会调用 socketFn 方法,将收到的字符串类型的消息,和上下文对象(包含引擎实例)传递给该方法。

至此我们就实现了自定义的数据结构的实时数据通信了。

4.2 扩展自定义数据结构解析函数

上面的方法,是将自定义函数固话到前端代码中。这种方式最少有2个不足:

  1. 如果需要修改则需要重新修改、边缘并发布到服务器上,维护成本其实并不低。
  2. 如果在自定义函数内部,需要调用我自己程序中相关的上下文对象,比如,我用的是quasar框架,我希望在收到某些消息时,调用quasar提供的消息处理对象,弹窗消息提示。这样的情景,该怎么办?

这里介绍一个方法,可以解决上面2个问题。

实现思路,是将函数体实现的内容代码,由前端程序中,迁移到页面编辑器中。编写一个方法,将内容代码的字符串转换为socketFn要求的类型,并增加传入自定义类型的对象。

在我的另一篇文档开源可视化引擎 Meta2d.js 实战 - 集成monaco编辑器 介绍了如何在页面集成编辑器编写前端代码。基于此在程序中能够获取到编辑器的内容,也就是一个 js代码字符串。我们要做的就是,将字符串转换为函数类型。大概的代码是这样:

typescript 复制代码
// 注册自定义js处理函数
if (!StringUtil.isBlank(customJs) {
  engine.socketFn = (e, context) => {
    if (e && typeof e === 'string' && customJs) {
      executeJsExpressions(customJs, { message: e, context, notify: NotifyUtil });
    }
    return true;
  };
}

上面代码中,customJs是编辑器的内容,这比较好理解。重点是executeJsExpressions方法,此方法接收2个参数:代表实现自定义数据结构解析的函数代码字符串,和一个对象。这个对象包含了收到的消息,meta2d上下文对象,以及扩展所需要的用于提示的对象。当然,如果需要你可以增加任何你所需要的对象进去。

来看一下executeJsExpressions的具体实现,可以直接使用。

typescript 复制代码
// 执行多行js表达式,每行表达式必须以分号结尾/分割
export const executeJsExpressions = (
  // 表达式字符串,一般为编辑器/文本域中的值
  expressions: string,
  // 要传入的上下文对象 key 代表代码中引用的对象名,value为实际的对象实例。如 context: { 'context': any, 'message': string, notify: NotifyUtil}
  context: { [key: string]: any },
  // 每个表达式区分的分隔符,默认为分号
  separator = ';'
): any[] => {
  const tmpStat = expressions.replace(/;/g, separator);
  const func = new Function(...Object.keys(context), tmpStat);
  return func(...Object.values(context));
};

这里需要说明的是:将一个字符串转换为函数类型,存在潜在的安全风险,在此不再赘述,各位自行把握.

至此,实时数据通信逻辑分析与实现介绍完毕。

相关推荐
EterNity_TiMe_2 小时前
【论文复现】STM32设计的物联网智能鱼缸
stm32·单片机·嵌入式硬件·物联网·学习·性能优化
Amarantine、沐风倩✨3 小时前
研发工程师---物联网+AI方向
人工智能·物联网
7yewh5 小时前
嵌入式硬件杂谈(一)-推挽 开漏 高阻态 上拉电阻
驱动开发·stm32·嵌入式硬件·mcu·物联网·硬件架构·pcb工艺
7yewh15 小时前
嵌入式硬件电子电路设计(五)MOS管详解(NMOS、PMOS、三极管跟mos管的区别)
stm32·嵌入式硬件·mcu·物联网·硬件架构·硬件工程·pcb工艺
小刘同学-很乖16 小时前
MQTT从入门到精通之 MQTT 客户端编程
spring boot·stm32·物联网·iot
树莓集团1 天前
以数字产业园区规划为笔,绘智慧城市新篇章
大数据·人工智能·科技·物联网·智慧城市·媒体
极客小张1 天前
基于STM32的智能宠物自动喂食器设计思路:TCP\HTTP、Node.js技术
stm32·单片机·物联网·tcp/ip·node.js·毕业设计·宠物
B站计算机毕业设计超人1 天前
计算机毕业设计Python+Neo4j中华古诗词可视化 古诗词智能问答系统 古诗词数据分析 古诗词情感分析 PyTorch Tensorflow LSTM
pytorch·python·深度学习·机器学习·知识图谱·neo4j·数据可视化
B站计算机毕业设计超人1 天前
计算机毕业设计Python+大模型斗鱼直播可视化 直播预测 直播爬虫 直播数据分析 直播大数据 大数据毕业设计 机器学习 深度学习
爬虫·python·深度学习·机器学习·数据分析·课程设计·数据可视化
AI服务老曹2 天前
建立更及时、更有效的安全生产优化提升策略的智慧油站开源了
大数据·人工智能·物联网·开源·音视频