分类 产品 下的文章

准备工作

当然,首先必须要有 ThingsBoard 二次开发基础,可以在本博客中搜索“二次开发”,你会得到很多提升。

增加签名用的 signature pad 包

https://www.npmjs.com/package/angular2-signaturepad,根据提示,加入到 package.json 中,确保模块运行正常。

新增用于签名的组建

手工创建或通过命令行创建,sign.component.ts, sign.component.html, sign.component.scss,并且加入到 module.ts

新增配套 service 处理

页面点击触发签名框弹出,保存签名图片到数据库的请求。

后端图片处理

后端需要将前端提交的数据保存到数据库,还需要将数据展现在前端页面。

代码示例

TS 文件

import { Component, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { ReportService } from '@core/http/report.service';
import { Report, ReportFill } from '@shared/models/report.models';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';

import { SignaturePad } from 'angular2-signaturepad';

export interface ReportSignObservationDialogData {
  report: Report;
}

@Component({
  selector: 'tb-report-sign-observation-dialog',
  templateUrl: './sign-report-observation-dialog.component.html',
  styleUrls: ['./sign-report-observation-dialog.component.scss'],
  providers: [{provide: ErrorStateMatcher, useExisting: ReportSignObservationDialogComponent}]
})
export class ReportSignObservationDialogComponent extends
  DialogComponent<ReportSignObservationDialogComponent, Report> implements OnInit, ErrorStateMatcher {
    @ViewChild(SignaturePad) signaturePad: SignaturePad;
    private signaturePadOptions: Object = { // passed through to szimek/signature_pad constructor
      'minWidth': 2,
      'canvasWidth': 400, // 弹出的窗口宽度
      'canvasHeight': 200, // 弹出的窗口高度
      'backgroundColor': 'rgb(240,240,240)' // 背景颜色
    };

  reportSignFormGroup: FormGroup;
  isReadOnly: boolean;
  reportSign: String;
  report: Report;
  submitted = false;

  constructor(protected store: Store<AppState>,
              protected router: Router,
              @Inject(MAT_DIALOG_DATA) public data: ReportSignObservationDialogData,
              private reportService: ReportService,
              @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
              public dialogRef: MatDialogRef<ReportSignObservationDialogComponent, Report>,
              public fb: FormBuilder) {
    super(store, router, dialogRef);
  }

  ngAfterViewInit() {
    // this.signaturePad is now available
    this.signaturePad.set('minWidth', 2); // set szimek/signature_pad options at runtime
    this.signaturePad.clear(); // invoke functions from szimek/signature_pad API
  }

  signatureImage;
  drawComplete() {
    // will be notified of szimek/signature_pad's onEnd event
    // console.log(this.signaturePad.toDataURL());
    this.reportSign = this.signaturePad.toDataURL();
    this.signatureImage=this.signaturePad.toDataURL(); // 将图片转换成 base64 码
  }

  drawStart() {
    // will be notified of szimek/signature_pad's onBegin event
    // console.log('begin drawing');
  }

  ngOnInit(): void {
    this.buildReportSign();
    this.loadReportSign();
  }

  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
    const customErrorState = !!(control && control.invalid && this.submitted);
    return originalErrorState || customErrorState;
  }

  buildReportSign() {
    this.reportSignFormGroup = this.fb.group({  
      file: ''
    });
  }

  loadReportSign() {
    this.reportSignFormGroup = this.fb.group({  
      file: ''
    });
  }

  cancel(): void {
    this.dialogRef.close(null);
  }

  save(): void {
    this.submitted = true;
    // console.log(this.data.report.id, this.reportSign);
    this.report = this.data.report;
    this.reportService.signReport(this.report, encodeURIComponent(this.reportSign.toString())).subscribe( // 调用保存签名的 service
      (report) => {
        this.dialogRef.close(this.report);
      }
    );
  }
}

HTML 文件

<form [formGroup]="reportSignFormGroup" (ngSubmit)="save()">
    <mat-toolbar color="primary">
        <h2 translate>report.sign-report</h2>
        <span fxFlex></span>
        <button mat-icon-button (click)="cancel()" type="button">
      <mat-icon class="material-icons">close</mat-icon>
    </button>
    </mat-toolbar>
    <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
    </mat-progress-bar>
    <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
    <div mat-dialog-content>
        <signature-pad [options]="signaturePadOptions" (onBeginEvent)="drawStart()" (onEndEvent)="drawComplete()"></signature-pad>
    </div>

    <div mat-dialog-actions fxLayoutAlign="end center">
        <button *ngIf="!isReadOnly" mat-raised-button color="primary" type="submit">
      {{ 'action.save' | translate }}
    </button>
        <button mat-button color="primary" type="button" [disabled]="(isLoading$ | async)" (click)="cancel()" cdkFocusInitial>
      {{ (isReadOnly ? 'action.close' : 'action.cancel') | translate }}
    </button>
    </div>
</form>

结果展示

签名提示:
sign.png

报告结果:
sign1.png

我们都知道蓝牙BLE设备只能与蓝牙相关设备通信,如果需要将蓝牙BLE设备接入物联网平台,那需要通过一些技术手段来实现协议和数据的转换。本文章,就是为这个解决方案提供一个指引。

准备工作

设备和软件相关

  • 蓝牙BLE广播设备,可以是一个iBeacon,这里用 YiTHE 人体温度计为例;
  • 蓝牙BLE网关,可以是一个通用的蓝牙网关或可以二次开发的网关;
  • ThingsBoard Gateway,需要二次开发;

蓝牙网关需要跟蓝牙设备之间已经建立连接和配合,具体配置要看蓝牙设备的协议是否被网关支持。

数据走向

  1. 蓝牙设备持续广播,将数据发送出来:02010613ff11aae6000055444757543035000000000000;
  2. 网关设备持续扫描广播,并解析广播数据,比如 YiTHE ;
  3. 蓝牙网关将数据发送到 ThingsBoard Gateway;
  4. ThingsBoard 端需要针对 iBeacon 数据解析,并入库。

协议转换

蓝牙网关端

为了区分自有设备,过滤掉其他厂家设备,需要在网关处开发过滤规则:

  • 根据广播数据长度过滤不相关数据;
  • 根据数据内容关键字过滤;

ThingsBoard 端

为了保证数据完整性,我们将原始数据直接发送到 TB ,然后,在规则链 中过滤并且分析数据,写入:

adv_data = msg.adv_data;

// 电池
battery_str = adv_data.substr(20, 2);
hex = "0x" + battery_str;
charValueBattery = String.fromCharCode(hex);
battery = charValueBattery.charCodeAt(0);
msg.battery = battery;

// 温度
temp_symbol = adv_data.substr(10, 2);
if (temp_symbol == "00") {
    symbol = -1;
} else if (temp_symbol == "11") {
    symbol = 1;
}
temp1_str = adv_data.substr(16, 2);
temp2_str = adv_data.substr(14, 2);
hex = "0x" + temp1_str + temp2_str;
charValueTemp = String.fromCharCode(hex);
temp = charValueTemp.charCodeAt(0);
msg.temp = symbol * temp / 10.0;

// 温度单位 摄氏度或华氏度
temp_unit_str = adv_data.substr(12, 2);
if (temp_unit_str == "aa") {
    unitCode = "C";
} else if (temp_unit_str == "bb") {
    unitCode = "F";
}
msg.temp_unit = unitCode;

// 按键标志
pressed_str = adv_data.substr(18, 2);
if (pressed_str == "01") {
    pressed = true;
} else if (pressed_str == "00") {
    pressed = false;
}
msg.pressed = pressed;

// 设备id
devcie_id_str = adv_data.substr(34, 12);
hex = "0x" + devcie_id_str;
charValueDeviceId = String.fromCharCode(hex);
device_id = charValueDeviceId.charCodeAt(0);
msg.device_id = device_id;

// 设备标志位
var result = [];
device_flag_str = adv_data.substr(22, 12);
for (var i = 0; i < device_flag_str.length / 2; i++) {
    var aa = device_flag_str.slice(i * 2, (i + 1) * 2);
    result.push(aa);
}

var device_flag = "";
for (var i = 0; i < result.length; i++) {
    device_flag += String.fromCharCode(parseInt(result[i],
        16));
}
msg.device_flag = device_flag;

return {
    msg: msg,
    metadata: metadata,
    msgType: msgType
};

需要解析数据,并写入msg,达到入库的作用。

{
    "msg": {
        "adv_data": "02010613ff11aae6000055444757543035000000000000",
        "battery": 85,
        "temp": 23,
        "temp_unit": "C",
        "pressed": false,
        "device_id": 0,
        "device_flag": "DGWT05"
    },
    "metadata": {
        "deviceType": "default",
        "deviceName": "Test Device",
        "ts": "1618149679239"
    },
    "msgType": "POST_TELEMETRY_REQUEST"
}

结果呈现

设备列表

yithe2.png

设备日志

yithe.png

视频信息如何作为物联网管理的一部分,一直是我们公司比较关注的一项内容,哪些内容应该被纳入物联网管理范畴?通过这篇文章,我们一起来探讨一下。

准备工作

  • 视频直播设备,比如:IPC 网络摄像机,或者可以模拟推流的ffmpeg,IPC 拍摄视频,自定义码流(比如500kbit/s),自定义分辨率(1080P),标准协议(RTMP);
  • Wi-Fi 或 有线网络是比较容易的选择,有4G当然更加灵活,这个根据具体需求来确定选择哪类传输方式;
  • 流媒体服务器,比如:YiMEDIA,可以实现流媒体服务支持,推流/拉流,状态查询;
  • 物联网插件,具备从流媒体服务器获取IPC信息,流信息,并且与物联网服务器交互,比如:YiCONNECT;

过程剖析

IPC 端

  • 首先,必须是 IPC,这样才能通过网络推流;
  • 其次,具备 RTMP 推流协议,如果只是支持 RTSP,需要一个转换器把 RTSP流 转换成 RTMP流 推送出去;
  • 再次,编码类型H264,分辨率看摄像头,一般都可以1080P,码流至少要达到 500kbps 以上,否则不一定能推流成功,且视频不清晰;
  • 再一次,如果需要音频上传,需要用 aac 编码;
  • 最后,成功推流。如果不行,建议咨询 IPC 厂家。

YiMEDIA 端

流媒体服务器,这里以我司 YiMEDIA 为基础,简单介绍一些。

  • 流媒体服务器开启 RTMP 通道,一般是 TCP 1935 端口号;
  • 开启推流验证,如果验证不通过,就断开推流客户端的连接,可以用随机数的方式,YiMEDIA 才有回调应用服务器确认连接安全性;
  • 推流/拉流 URL 建议采用: rtmp://[IPADDRESS]/[IPC]/[auth_key],或 rtmp://[IPADDRESS]/[IPC]/[MAIN/SUB]?key=[auth_key] ;
  • YiMEDIA 开启 NVR/DVR 功能,记录所有的视频信息;

YiCONNECT 端

为了将视频流数据状态和信息发送到 YiCONNECT,需要开发一个插件,获取 流信息 后,以 Thingsboard 能接收的方式发送。

  • 编写 Thingsboard gateway 应用程序,以便支持 视频流 信息的转发和解析;
开启 rest.json 服务
编写 YiMEDIA 丢过来的回调请求回应信息
  • 解析 [IPC] 和 [auth_key] 字段,并验证 auth_key 是否满足要求;
  • YiCONNECT 端新建 Gateway 网关设备,保持验证码一致;

结果预览

YiMEDIA 状态查看

  • 客户端状态
    clients.png
  • 流状态
    streams.png

YiCONNECT

  • 设备列表表
    devices.png
  • 视频流列表
    videos.png

图像预览

可以通过 VLC 播放器播放:

    1. rtmp 流: rtmp://ipaddress/ON400H16DM0FZ20210308002/high,基本处于实时播放,1-3秒
    2. flv 格式: http://ipaddress/ON400H16DM0FZ20210308002/high.flv,准实时播放,3-5秒
    3. m3u8 格式:http://ipaddress/ON400H16DM0FZ20210308002/high.m3u8,延时比较大,几秒到几十秒
    • 4G 摄像头抓图
      4g.png
    • 半夜红外线开启抓图
      midnight.jpg

    方案详解

    YiCAMERA-solution.png

    此次移植工作以中国移动 ML302 模组为基础,亿琪软件 YiDTU 设备为原型,开发基于 Modbus Master 应用;
    传统模式下,DTU 都是以透传为主要工作,所有的业务需求都是从服务器端发出,而 YiDTU 是作为边缘计算的角色,在工业现场就已经把数据处理做完;

    yigate-yidtu.png

    准备工作

    硬件设备

    • YiDTU 设备一台
      亿琪软件 YiDTU 设备内置 UART 转 RS485 模块,对外连接 RS485 双线设备,如:YiSensor 模拟量采集模块;对于开发而言,那就是主要针对 UART 串口操作;
    • Micro USB 线一根
      用于操作 YiDTU 设备:AT 串口操作,烧录操作;
    • RS485 设备若干
      可用 USB 转 RS485 转换器代替(需配合 modbus slave 软件使用);有支持 Modbus Slave 设备最好;准备了两个:模拟量 4-20mA 和 开关量各一个;
    • 9~30V 直流电源一个
      支持最大 2A 电流的直流电源一个,用于给 YiDTU 设备供电;
    • 将以上硬件设备连接好

    软件 SDK

    • 从中国移动物联网公司获取 ML302 相关 SDK,;
    • 确保 SDK 编译正常;
    • 烧录基本应用成功;

    移植过程

    Modbus Master 库

    • https://github.com/jiekechoo/ModBus-Master.git 获取源代码,使用 STM32 环境编译成功,确保你的环境没问题;
    • 将 Modbus_Master 目录移植到 ML302 工程中 src/demo/modbus 下,根据需求删除不必要的组件功能;
    • ML302 环境下编译通过,确保没有错误出现;
    • Modbus 枚举值要重新定义
    typedef enum
    {
      // Modbus function codes for bit access
      ku8MBReadCoils = 0x01,          ///< Modbus function 0x01 Read Coils
      ku8MBReadDiscreteInputs = 0x02, ///< Modbus function 0x02 Read Discrete Inputs
      ku8MBWriteSingleCoil = 0x05,    ///< Modbus function 0x05 Write Single Coil
      ku8MBWriteMultipleCoils = 0x0F, ///< Modbus function 0x0F Write Multiple Coils
    
      // Modbus function codes for 16 bit access
      ku8MBReadHoldingRegisters = 0x03,      ///< Modbus function 0x03 Read Holding Registers
      ku8MBReadInputRegisters = 0x04,        ///< Modbus function 0x04 Read Input Registers
      ku8MBWriteSingleRegister = 0x06,       ///< Modbus function 0x06 Write Single Register
      ku8MBWriteMultipleRegisters = 0x10,    ///< Modbus function 0x10 Write Multiple Registers
      ku8MBMaskWriteRegister = 0x16,         ///< Modbus function 0x16 Mask Write Register
      ku8MBReadWriteMultipleRegisters = 0x17 ///< Modbus function 0x17 Read Write Multiple Registers
    } modbusFunc;

    串口接收支持

    • 串口接收任务中,将收到的内容复制到全局变量中,目的是把 Modbus Slave 回复的内容获取到,交给 Modbus 组件使用;
    extern uint8_t u8ModbusADU[1024];
    extern uint8_t u8ModbusADUSize;
    
    // 接收处理函数中
    {
                u8ModbusADUSize = uart_buf_len;
                memcpy(u8ModbusADU, uart_buf, u8ModbusADUSize);
    }
    • 接收完成后,创建一个 信号量,通知 Modbus 组件来处理;
    {
                    uart_buf_len = 0;
                    recv_count = 0;
                    osSignalSet(OC_Main_TaskHandle, 0x0004);
    }

    Modbus 组件

    • 将 Modbus 组件中接收处理替换成信号量等待,超时重置接收缓冲区;
      // 串口收到数据信号量,2000ms 超时
      osEvent osevent = osSignalWait(0x0004, ku16MBResponseTimeout);
      if (osevent.status == osEventTimeout)
      {
        u8MBStatus = ku8MBResponseTimedOut;
        memset(u8ModbusADU, 0, sizeof(u8ModbusADU));
        u8ModbusADUSize = 0;
      }
    • Modbus 发送函数重写
    uint8_t Modbus_Master_Write(uint8_t *buf, uint8_t length)
    {
      //  if(HAL_UART_Transmit(&huart2 ,(uint8_t *)buf,length,0xff))
      if (cm_uart_send_no_cache(OPENCPU_MAIN_URAT, (uint8_t *)buf, length, 0xff) == 0)
      {
        return HAL_ERROR;
      }
      else
      {
        return HAL_OK;
      }
    }
    • 如果有 CS 控制口 IO,需要自定义相关函数
    #if 0
      // transmit request RS485接口是需要每次发送前改变接口的模式
      
      if (_preTransmission)
      {
        _preTransmission();
      }
    #endif
    
      //串口发送数据
      Modbus_Master_Write(u8ModbusADU, u8ModbusADUSize);
      u8ModbusADUSize = 0;
    
    #if 0
    // transmit request RS485接口是需要每次发送后改变接口的模式
          if (_postTransmission)
      {
        _postTransmission();
      }
    #endif

    移植过程比较简单,细节需要一定的嵌入式和 C 语言基础。

    效果展示

    在 YiDTU 业务逻辑中增加相应的功能模块,把数据传输到云平台,这样,就可以及时获取现场的模拟量数据。
    PSX_20210119_151717.jpg

    前期,发表过多个系列文章关于如何无缝扩展 ThingsBoard 功能,其实在实施过程中,还是遇到很多问题,需要提前注意!

    独立于 ThingsBoard 外的功能模块注意要点

    • 功能模块需要能够独立运行,不管是服务也好,controller 也罢,都要能够正常运行,就类似单元测试跑通,才可以融入到 ThingsBoard 中;
    • 数据表要独立成 sql 文件,便于移植到 ThingsBoard 库中,最好不要跟原有数据表有关联或冲突,尤其不能冲突;

      • 调用系统库表,新建的 Entity 要注意 column 名字于原有一样,使用 @Column(name = "foo_bar"),否则 Bean 建立失败;
      • 如果 Entity 有嵌套调用 POJO,需要将 Entity 设置成 @Proxy(lazy = false),关闭懒加载,否则无法查询数据;
    • 打包功能模块,需要将 Spring Boot 项目 target 中的 .original 文件安装到本地 maven 库,或者修改 pom.xml 文件来改变 maven 打包规则;
    • 功能模块中的附件,图片,pdf 模板,直接在功能模块 jar 包中,不需要单独导入 ThingsBoard 中;
    • Controller 要单独命名,不能与 ThingsBoard 冲突,API 路径可以复用原有路径,比如:原有是 /api/report/foo,可以增加新的 /api/report/bar;
    • Domain/POJO/Repository等,要独立命名,因为我们以 maven jar 包方式融入 ThingsBoard,都交给 spring 来管理,所以命名要独立;