分类 默认分类 下的文章

准备工作

当然,首先必须要有 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

本文所有内容都来自原创,思路开阔,敢想敢做,经过不懈努力,终于成功扩展 ThingsBoard 功能模块

先来看一段视频,体验一下新增的功能,前端增加的菜单和功能演示。
tb-v.mp4

前言

此次二次开发,主要是针对 ThingsBoard 新增功能模块,以报表功能为目的,提供一个可以独立与原有功能的 Report/报表 模块,咱们先这么来定义。就像 Device/设备、Asset/资产模块,增删改查必不可少,还需要兼容 ThingsBoard 原有操作习惯,还可以将 Report/报表 分配给特定的 Customer/客户。上图,这是开始的设想:
tb1.png

后端

代码结构

modified:   application/src/main/java/org/thingsboard/server/controller/BaseController.java
new file:   application/src/main/java/org/thingsboard/server/controller/ReportController.java
modified:   application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
modified:   application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
modified:   application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
modified:   application/src/main/resources/thingsboard.yml
new file:   common/dao-api/src/main/java/org/thingsboard/server/dao/report/ReportService.java
modified:   common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
modified:   common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/Report.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/ReportInfo.java
modified:   common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/id/ReportId.java
modified:   common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilterType.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/query/ReportSearchQueryFilter.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/query/ReportTypeFilter.java
new file:   common/data/src/main/java/org/thingsboard/server/common/data/report/ReportSearchQuery.java
new file:   dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractReportEntity.java
new file:   dao/src/main/java/org/thingsboard/server/dao/model/sql/ReportEntity.java
new file:   dao/src/main/java/org/thingsboard/server/dao/model/sql/ReportInfoEntity.java
new file:   dao/src/main/java/org/thingsboard/server/dao/report/ReportDao.java
new file:   dao/src/main/java/org/thingsboard/server/dao/report/ReportServiceImpl.java
modified:   dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
modified:   dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java
new file:   dao/src/main/java/org/thingsboard/server/dao/sql/report/JpaReportDao.java
new file:   dao/src/main/java/org/thingsboard/server/dao/sql/report/ReportRepository.java
modified:   pom.xml

根据 git 提供的log,看出来新增文件比修改文件多,也是此次二次开发的难点所在。

  • 新增文件:新增的文件主要以扩展功能模块为主,大概是这样:POJO(DTO,ID),Entity,Repository,Dao,Service/Impl,Controller,基本都是依葫芦画瓢;
  • 修改文件:因为要达到 ThingsBoard 原有的一些功能体验,势必要满足其实现逻辑,那就是找到所有的 Device 功能模块的做法,在所有需要的地方增加 Report 功能,无非就是一些新增枚举值,方法调用,新增 case 判断;

这个开发过程中,我发现,频繁读取一些 API,且都是需要数据库交互,如果用户数量大,不是一个好方法,这里举一个例子:

  • 获取实体类型频繁数据库交互
    types.png

调用后台 API 为: /api/report/types,追查代码发现其实是一次 SELECT,瞬间尴尬了

@Query("SELECT DISTINCT d.type FROM ReportEntity d WHERE d.tenantId = :tenantId")
List<String> findTenantReportTypes(@Param("tenantId") UUID tenantId);
  • WEB API,大概就是把 device 都替换成了 report,其他逻辑上没有做更改,为了配合前端的调用,这些都是必不可少的内容。因为用 device 衍生过来,把 cridentail 相关内容删除了。
    tb3.png

数据表

CREATE TABLE public.report
(
    id uuid NOT NULL,
    created_time bigint NOT NULL,
    additional_info character varying COLLATE pg_catalog."default",
    customer_id uuid,
    type character varying(255) COLLATE pg_catalog."default",
    name character varying(255) COLLATE pg_catalog."default",
    label character varying(255) COLLATE pg_catalog."default",
    search_text character varying(255) COLLATE pg_catalog."default",
    tenant_id uuid,
    CONSTRAINT report_pkey PRIMARY KEY (id),
    CONSTRAINT report_name_unq_key UNIQUE (tenant_id, name)
)
  • 如果你观察仔细,发现跟 device 表几乎一样,那是因为我们以 device 为原型做的二次开发。开发自己功能时,表的定义请随意啊。不过,name, type, search_text, id, customer_id, tenant_id, create_time 都是不能少的,因为框架业务逻辑有代码实现一些功能;
  • 建议增加几个不影响 ThingsBoard 框架工作的“扩展字段”,也可以直接使用 additional_info 来存储自己的数据,这个字段最长可以有 1G,足够放你需要的结构化或非结构化数据;类似:
{
    "hello": "world",
    "thingsboard": "iot",
    "company": "yiqisoft",
    "location": "shanghai",
    "protocol": ["coap", "mqtt", "http"]
}
  • 自由业务逻辑,针对“扩展字段”的操作,请随意,数据频繁修改的还是用自己的业务表,不要混为一谈。

业务逻辑实现

tb2.png
对于自己的业务逻辑开发方面,建议不要改动 ThingsBoard 的代码结构,可以使用外挂一个 jar,来独立实现业务功能,这个具体要看业务复杂度,是不是需要与 ThingsBoard 耦合,可以查看前期我们发布的文章:如何无缝扩展 ThingsBoard 功能?原来二次开发如此简单!如何无缝扩展 ThingsBoard 功能?原来二次开发如此简单!【续】

前端

代码结构

modified:   ui-ngx/src/app/core/http/entity.service.ts
new file:   ui-ngx/src/app/core/http/report.service.ts
modified:   ui-ngx/src/app/core/services/menu.service.ts
modified:   ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts
modified:   ui-ngx/src/app/modules/home/dialogs/add-entities-to-customer-dialog.component.ts
modified:   ui-ngx/src/app/modules/home/dialogs/assign-to-customer-dialog.component.ts
modified:   ui-ngx/src/app/modules/home/dialogs/home-dialogs.service.ts
modified:   ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
modified:   ui-ngx/src/app/modules/home/pages/customer/customer.component.html
modified:   ui-ngx/src/app/modules/home/pages/customer/customers-table-config.resolver.ts
modified:   ui-ngx/src/app/modules/home/pages/home-pages.module.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report-routing.module.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report-table-header.component.html
new file:   ui-ngx/src/app/modules/home/pages/report/report-table-header.component.scss
new file:   ui-ngx/src/app/modules/home/pages/report/report-table-header.component.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report-tabs.component.html
new file:   ui-ngx/src/app/modules/home/pages/report/report-tabs.component.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report.component.html
new file:   ui-ngx/src/app/modules/home/pages/report/report.component.scss
new file:   ui-ngx/src/app/modules/home/pages/report/report.component.ts
new file:   ui-ngx/src/app/modules/home/pages/report/report.module.ts
new file:   ui-ngx/src/app/modules/home/pages/report/reports-table-config.resolver.ts
modified:   ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
modified:   ui-ngx/src/app/shared/components/entity/entity-subtype-autocomplete.component.ts
modified:   ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
modified:   ui-ngx/src/app/shared/components/entity/entity-subtype-select.component.ts
modified:   ui-ngx/src/app/shared/components/footer.component.html
modified:   ui-ngx/src/app/shared/models/alias.models.ts
modified:   ui-ngx/src/app/shared/models/entity-type.models.ts
new file:   ui-ngx/src/app/shared/models/id/report-id.ts
new file:   ui-ngx/src/app/shared/models/report.models.ts
modified:   ui-ngx/src/assets/locale/locale.constant-en_US.json
modified:   ui-ngx/src/assets/locale/locale.constant-zh_CN.json
modified:   ui-ngx/src/assets/logo_title_white.svg
modified:   ui-ngx/src/environments/environment.prod.ts
modified:   ui-ngx/src/environments/environment.ts
modified:   ui-ngx/src/index.html
new file:   ui-ngx/src/yiqisoft.ico
  • 先复制一份 ui-ngx/src/app/modules/home/pages/device 到 ui-ngx/src/app/modules/home/pages/report,就是组件要完整,文件名都改掉,内部都是替换操作: Device -> Report, device -> report, DEVICE -> REPORT,这样我们的 ReportModule 就做好了;
  • 在 home-pages.module.ts 载入 ReportModule ;
  • 在 ui-ngx/src/app/core/services/menu.service.ts 增加需要的菜单;
  • 根据 report 目录里面的内容,需要从 device 功能模块复制一些组件和 service,挺多的,自己慢慢找,也算是熟悉一下前端代码;
  • 汉化,就是修改 ui-ngx/src/assets/locale/ 目录下相应的语言包,很容易;

功能扩展过程

前期,我们发布了文章,如何进行 ThingsBoard 前端二次开发?超强 Angular 框架帮你忙。,介绍如果用 Angular 二次开发的要点,具体看开发者对于 Angular 框架掌握程度。

功能扩展建议

  • 像上面的实体类型获取,增加功能特别不容易,因为代码实现是通过枚举值来 switch/case 来判断,那么新增功能模块时,需要改动太大了,如果能够独立到每个功能模块的组件中,那就方便多了。但是,前端代码就会有很多重复。为了扩展 REPORT,必须要找到所有的 case,上代码:
    switch (entity.id.entityType as EntityType | string) {
      case 'function':
        materialIcon = 'functions';
        break;
      case EntityType.DEVICE:
        materialIcon = 'devices_other';
        break;
      case EntityType.REPORT:
        materialIcon = 'equalizer';
        break;
      case EntityType.ASSET:
        materialIcon = 'domain';
        break;
    }
  • 更改 LOGO,需要使用工具做一个 svg 矢量图,替换 ui-ngx/src/assets/logo_title_white.svg 即可;
  • 更改 Swagger 显示内容需要改 thingsboard.yml;

欢迎大家一起来加入 ThingsBoard 讨论,可以在下方留言给作者。其实,我是最不建议在 ThingsBoard 上改动,我崇尚无缝扩展,但是太难了,所以才有了本文的发表,把自己的业务融入进去是一次尝试。当然,有能力的朋友可以自己独立做业务系统,对于简单物联网应用,何必重复制造轮子呢?

  • 新建 docker-compose.yml
version: '2'
services:
  web:
    image: odoo:14.0
    depends_on:
      - db
    ports:
      - "8069:8069"
    volumes:
      - odoo-web-data:/var/lib/odoo
      - ./config:/etc/odoo
      - ./addons:/mnt/extra-addons
  db:
    image: postgres:10
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_PASSWORD=odoo
      - POSTGRES_USER=odoo
      - PGDATA=/var/lib/postgresql/data/pgdata
    volumes:
      - odoo-db-data:/var/lib/postgresql/data/pgdata
volumes:
  odoo-web-data:
  odoo-db-data:
  • 启动 docker
docker-compose up -d
<VirtualHost *:80>
    DocumentRoot "/var/www/html/odoo.cn"
    ServerName www.odoo.cn
    RewriteEngine on
    RewriteCond %{SERVER_PORT} !^443$
    RewriteRule ^(.*)$ https://%{SERVER_NAME}$1 [L,R]
    <Directory "/var/www/html/odoo.cn">
        allow from all
        Options None
        Require all granted
    </Directory>
</VirtualHost>
<VirtualHost *:443>
    DocumentRoot "/var/www/html/odoo.cn"
    ServerName www.odoo.cn
    RewriteEngine on
    <Directory "/var/www/html/odoo.cn">
        allow from all
        Options FollowSymLinks
        Require all granted
        AllowOverride All 
    </Directory>
    SSLEngine on
    SSLCertificateFile /etc/httpd/conf.d/www.odoo.cn_public.crt
    SSLCertificateKeyFile /etc/httpd/conf.d/www.odoo.cn.key
    SSLCertificateChainFile /etc/httpd/conf.d/www.odoo.cn_chain.crt
    ProxyRequests Off
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8069/
    ProxyPassReverse / http://127.0.0.1:8069/
    ProxyErrorOverride off
</VirtualHost>
  • 打开浏览器 http://www.odoo.cn ,直接访问域名,注意要使用 https 需申请域名 ssl 证书

产品特性

YiAIR-86-印刷-2.png

  • 无线远传:NB-IoT全网通
  • 干电池供电:2节AA电池(5号)
  • 静态功耗:< 20uA,可用1年*
  • 温/湿度精度:±0.2℃/±3%
  • 私有化 IoT 平台:多租户管理
  • 尺寸:86 x 86 x 30mm

适用范围

  • 供暖、冷链、厂房、仓库
  • 办公室、会议室、学校、幼儿园
  • 酒店、医院、商场、咖啡厅
  • 酒吧、餐馆、地铁、地下室

多租户

多租户.png

仪表板

dashboard.png

  • 编辑 docker-compose.yml
version: "3.7"
services:
  db:
    image: postgres:11.6
    restart: always
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      PGDATA: /var/lib/postgresql/data
    volumes:
      - db-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
 
  pgadmin:
    image: dpage/pgadmin4
    restart: always
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@yiqisoft.cn
      PGADMIN_DEFAULT_PASSWORD: talent
      PGADMIN_LISTEN_PORT: 80
    ports:
      - "8088:80"
    volumes:
      - pgadmin-data:/var/lib/pgadmin
    links:
      - "db:pgsql-server"
volumes:
  db-data:
  pgadmin-data:

注意:文件保存在本地 volumes 中,如果需要再次使用,请保存!

  • 启动
docker-compose up -d
  • 登录 pgAdmin 4

打开本地浏览器 http://localhost:8080 , 使用用户名 admin@yiqisoft.cn 和密码 talent 登录