分类 产品 下的文章

3轴加速度,Tri-Axis Accelerometer,G-sensor,有不同的说法,但是表达的意思是一样的,我们要实现物体移动检测,计步器,简单应用还是非常不错的,至于6轴或9轴,不在本文章覆盖范围。

准备工作

参考驱动

官方并没有给出完整的代码参考,不过在github找到了非常不错的示例,只不过代码是 linux 下的,需要移植到嵌入式需要一些工作。仓库地址:https://github.com/eclipse/upm ,完整的驱动在 /src/kxtj3 目录下,需要3个文件:kxtj3.c, kxtj3.h, kxtj3_registers.h。有一个 example 在 /examples/c/kxtj3.c 下。全部移植到你的工程代码目录中。

代码移植

example 文件分析

主要是根据自己的嵌入式平台,改一下适配。

#define SENSOR_ADDR 0x0f // 设备slave号,接口低电平是 0x0E,高电平是 0x0F
#define I2C_BUS 0  // 使用哪个 i2c
#define SAMPLE_COUNT 10 // 读取传感器次数

bool isStopped = false;

void signal_int_handler(int signo)
{
    if (signo == SIGINT)
        isStopped = true;
}

// 读取,打印
void print_acceleration_data(kxtj3_context dev)
{
    float wait_time = kxtj3_get_acceleration_sampling_period(dev) * SECOND_IN_MICRO_S;
    uint8_t sample_counter = 0;
    float x, y, z;
    while (sample_counter < SAMPLE_COUNT && !isStopped)
    {
        kxtj3_get_acceleration_data(dev, &x, &y, &z);
        printf("%.02f | %.02f | %.02f\n", x, y, z);
        usleep(wait_time);
        sample_counter++;
    }
}

int main(int argc, char **argv)
{
    signal(SIGINT, signal_int_handler);
    
    // i2c 初始化
    printf("Sensor init\n");
    kxtj3_context dev = kxtj3_init(I2C_BUS, SENSOR_ADDR);
    if (!dev)
    {
        printf("kxtj3_init() failed.\n");
        return -1;
    }

    // sensor 初始化
    printf("Setting settings:\nODR: 25 Hz\nResolution: High\nAcceleration range: 16g with 14bits");
    kxtj3_sensor_init(dev, KXTJ3_ODR_25, HIGH_RES, KXTJ3_RANGE_16G_14);
    printf("Showing acceleration data:\n");
    print_acceleration_data(dev);

    printf("Closing sensor\n");
    kxtj3_close(dev);
    return 0;
}

驱动文件分析

i2c 驱动相关

主要是 i2c 读取/写入的几个函数适配,其他函数不需要更改。

// Register Read/Write helper functions
static upm_result_t kxtj3_read_register(const kxtj3_context dev, uint8_t reg, uint8_t *data)
{
    int value = mraa_i2c_read_byte_data(dev->i2c, reg); // 适配字节读取,注意都是指针传递
    if (value == -1)
    {
        printf("%s: mraa_i2c_read_byte_data() failed.\n", __FUNCTION__);
        return UPM_ERROR_OPERATION_FAILED;
    }

    *data = (uint8_t)value;
    return UPM_SUCCESS;
}

static upm_result_t kxtj3_read_registers(const kxtj3_context dev, uint8_t reg, uint8_t *data, int len)
{
    if (mraa_i2c_read_bytes_data(dev->i2c, reg, data, len) != (int)len) // 适配多字节读取,注意都是这真传递
        return UPM_ERROR_OPERATION_FAILED;

    return UPM_SUCCESS;
}

static upm_result_t kxtj3_write_register(const kxtj3_context dev, uint8_t reg, uint8_t val)
{
    if (mraa_i2c_write_byte_data(dev->i2c, val, reg) != MRAA_SUCCESS) // 适配字节写入
    {
        printf("%s: mraa_i2c_write_byte_data() failed.\n", __FUNCTION__);
        return UPM_ERROR_OPERATION_FAILED;
    }

    return UPM_SUCCESS;
}

i2c 连接,其实就是指定你的 i2c 相关内容,可有可无,保留函数结构即可。

static bool kxtj3_check_mraa_i2c_connection(kxtj3_context dev, int bus, uint8_t addr)
{
    if (mraa_init() != MRAA_SUCCESS)
    {
        printf("%s: mraa_init() failed.\n", __FUNCTION__);
        kxtj3_close(dev);
        return false;
    }

    if (!(dev->i2c = mraa_i2c_init(bus)))
    {
        printf("%s: mraa_i2c_init() failed.\n", __FUNCTION__);
        kxtj3_close(dev);
        return false;
    }

    if (mraa_i2c_address(dev->i2c, addr))
    {
        printf("%s: mraa_i2c_address() failed.\n", __FUNCTION__);
        kxtj3_close(dev);
        return false;
    }

    return true;
}

初始化函数,很重要,这是入口,初始化了 kxtj3_context。

kxtj3_context kxtj3_init(int bus, uint8_t addr)
{
    kxtj3_context dev = (kxtj3_context)malloc(sizeof(struct _kxtj3_context));
    if (!dev)
        return NULL;

    dev->i2c = NULL;
    dev->interrupt_pin = NULL;

    if (!kxtj3_check_mraa_i2c_connection(dev, bus, addr)) // i2c 初始化
        return NULL;

    if (!kxtj3_check_who_am_i(dev)) // 读取设备号,必须返回 0x35 才能行
        return NULL;

    kxtj3_set_default_values(dev);

    kxtj3_set_odr_wakeup_function(dev, dev->odr_wakeup);
    kxtj3_sensor_init(dev, dev->odr, dev->res_mode, dev->g_range_mode);

    return dev;
}

中断唤醒MCU相关

自定义一个函数,配置中断触发的高低电平和清除模式,在系统初始化时调用即可。

upm_result_t kxtj3_interrupt_init(const kxtj3_context dev)
{
    kxtj3_enable_wakeup_interrupt(dev);
    kxtj3_enable_interrupt_pin(dev, ACTIVE_LOW, LATCH_UNTIL_CLEARED); // ACTIVE_LOW:低电平触发,LATCH_UNTIL_CLEARED:手工清除

    kxtj3_set_wakeup_threshold_g_value(dev, 0.1);  // 0.1 代表了 0.1g 的加速度,0.1g 基本可以使能拿起来就触发

    return UPM_SUCCESS;
}

实验效果

读出来的效果,基本就是三个指标,xyz坐标轴的状态值,自己打印出来即可;
中断触发唤醒MCU,这个也不好演示,自己试试能不能唤醒MCU。

ThingsBoard 移动端采用 flutter 来实现,支持 Android 和 iOS,当然,web 也可以。总体测试下来,还是算不错的,虽然还没有 release 版本,但是,完全可以使用。

准备工作

flutter 工作环境

参考官方手册,很简单,下载,解压缩,运行:flutter doctor ,一步步排查,直到没有错误。这里不多说,不是重点。

Android

需要提前准备好 Android 开发环境和SDK,具体请参考 Android 开发环境要求,主要是能跑起来环境就没问题。

iOS

前提是需要一台 macOS 的电脑,配置低了还不行,还需要安装很多软件环境,这个对普通开发者还是有难度,土豪除外。

源码分析

配置自己的服务器

lib/constants/app_constants.dart, thingsBoardApiEndpoint 改成你的服务器地址即可;

汉化过程

大概思路就是各种dart 文件,比如: lib/modules/profile/change_password_page.dart,修改密码页面,把字符串改成中文就好了,没什么其他要求。

编译

运行 flutter build apk,可简单了,结果就是这样:

jiekechoo@jiekechoo flutter_thingsboard_app % flutter build apk --no-tree-shake-icons

Building with sound null safety 

Running Gradle task 'assembleRelease'...                                
Running Gradle task 'assembleRelease'... Done                     212.8s
✓  Built build/app/outputs/flutter-apk/app-release.apk (22.8MB).

apk 文件在 build/app/outputs/flutter-apk/app-release.apk,拿到Android 手机上安装即可使用,非常简单。

中文汉化效果预览

登录

直接使用 ThingsBoard 上用户登录即可

tb-app-login.png

设备

tb-app.png

仪表板

tb-app1.png

2021.8.14 第一时间将 TB 3.3 的 OTA 功能进行源码分析,基本思路就是 应用了telemetry 和 attributes ,不复杂,自己实现的话也是要这样来做。这里,只是分析了 CoAP 协议层,其他类似。

1、CoAP 路径:

按照官方文档里面的路径是错误的,需要改成如下url路径才能获取固件:coap://localhost/fw/$access_token?title=$title&version=$versoin

具体文件位置:
/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java

2、checksum算法

默认支持 SHA256,如果设备端不支持或有其他算法请自取 enum 里面内容。
/common/data/src/main/java/org/thingsboard/server/common/data/ota/ChecksumAlgorithm.java
public enum ChecksumAlgorithm {

MD5,
SHA256,
SHA384,
SHA512,
CRC32,
MURMUR3_32,
MURMUR3_128

}

3、OTA Dashboard

自带了两个不错的查看 OTA 的dashboard,赶紧收藏起来。
firmware:
/application/src/main/data/json/demo/dashboards/firmware.json
software:
/application/src/main/data/json/demo/dashboards/software.json
firmware-dashboard.png

4、OTA 固件存储

firmware / software 文件使用 oid 格式存储,隐藏列,无法查看,具体详细内容请自行查看postgresql oid数据类型。

5、测试脚本

https://thingsboard.io/docs/user-guide/resources/firmware/ 目录下
http_firmware_client.py
mqtt_firmware_client.py
coap_firmware_client.py
基本可以用,但是脚步写的不是很好,有能力的可以自己改。

5、升级步骤

1、服务有一个属性,标记是不是要升级,设备定期去获取这个属性,有的话就下载,提交服务器“下载中”;
2、固定地址下载:包括固件名字和版本号,下载完成:提交服务器“已下载”;
3、设备验证固件包完整性,提交服务器“已验证”;
4、设备开始更新前,提交服务器“更新中”;
5、设备更新完成后,成功提交“已更新”,失败提交“失败”;

准备工作

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