分类 服务端软件 下的文章

引用文字

准备工作

  • OAuth2 认证基本知识;
  • Odoo 14版本,单机部署,最好不使用 docker 环境;
  • Odoo OAuth for Keycloak 扩展;
  • Keycloak,配置好 Client 和 User;

工作原理

1. Odoo 插件安装

下载插件,https://github.com/OCA/server-auth/tree/14.0/auth_oidc,配置到 Odoo 外部扩展插件中,安装,具体步骤看插件 Readme 文件即可;

2. 配置 Odoo 支持 Keycloak 认证,按照下图例子参考配置 Odoo 即可;

oauth1.png

3. Keycloak 配置,以便支持 Odoo;

Clients 配置基本功能即可,可参考:https://yiqisoft.cn/blogs/iot_platform/243.html,尤其需要配置一个 Mapper,匹配 email 地址用:
oauth2.png
创建 Keycloak 内置用户即可;
oauth4.png

4. Odoo 用户配置

已知内置用户可以配置成 OAuth2 用户,这样内置用户可以登录 Odoo,OAuth2 用户也可以登录;
oauth3.png
内置用户可以不创建,第一次登录 OAuth2 服务器后会自动创建,此时新创建用户无法使用 Odoo 登录,需要发送认证email后创建密码;

成果演示

此二次开发主要是基于 ThingsBoard UI 界面上的 js 脚本开发,熟悉 js 开发的朋友应该很容易理解。
从 Dashboard 导出 csv 其实有好几种情况,1是 telemetry 数据导出,2是 attributes 数据导出。

准备工作

  • Chrome 浏览器
  • javascript 基础知识
  • ThingsBoard UI 二次开发基础知识
  • Dashbaord 开发经验

新建Dashboard

Telemetry 数据导出

1.png

导出csv文件内容:

timestamp,GasCon
2022-3-21 21:29:0,800
2022-3-21 21:44:0,800
2022-3-25 19:59:0,0

自定义 action,js脚本:

let $injector = widgetContext.$scope.$injector;
let attributeService = $injector.get(widgetContext
    .servicesMap.get('attributeService'));
let deviceService = $injector.get(widgetContext.servicesMap
    .get('deviceService'));

console.log(widgetContext);
let data = widgetContext.data;

let dataKeys = [];
// get all data keys to array
widgetContext.datasources[0].dataKeys.forEach(function(
    item) {
    dataKeys.push(item.name);
});

let fileTitle = "export-from-list-" + entityName;

function timeConverter(UNIX_timestamp) {
    var a = new Date(UNIX_timestamp);
    var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
        'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
    ];
    var year = a.getFullYear();
    var month = a.getMonth()+1;
    var date = a.getDate();
    var hour = a.getHours();
    var min = a.getMinutes();
    var sec = a.getSeconds();
    var time = year + '-' + month + '-' + date + ' ' +
        hour + ':' + min + ':' + sec;
    return time;
}

function exportCSV() {
    // first line: header
    var allLine = "";
    dataKeys.unshift("timestamp");
    allLine += dataKeys;
    allLine += "\n";

    var key_length = data.length; // 字段数目
    var limit = data[0]["data"].length; // 数据行数
    var line = new Array(limit + 1); // 包括首行
    for (var l = 0; l < limit; l++)
        line[l] = "";
    data.forEach(
        (item, idx) => {
            var item_data = item["data"];
            var dataKey = item["dataKey"];
            for (var j = 0; j < limit; j++) {
                if (idx == 0) {

                    line[j] += timeConverter(item_data[
                        j][0]);
                    line[j] += ",";
                }
                line[j] += item_data[j][1];
                if (idx < key_length - 1)
                    line[j] += ",";
            }
            line[j] += "\n";
        }
    );
    for (var k = 0; k < limit; k++) {
        allLine += line[k];
        allLine += "\n";
    }

    var exportedFilenmae = fileTitle + '.csv' ||
        'export.csv';

    var blob = new Blob([allLine], {
        type: 'text/csv;charset=utf-8;'
    });
    if (navigator.msSaveBlob) { // IE 10+
        navigator.msSaveBlob(blob,
            exportedFilenmae);
    } else {
        var link = document.createElement("a");
        if (link.download !==
            undefined) { // feature detection
            // Browsers that support HTML5 download attribute
            var url = URL.createObjectURL(blob);
            link.setAttribute("href", url);
            link.setAttribute("download",
                exportedFilenmae);
            link.style.visibility = 'hidden';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }
    }
}

exportCSV();

Attributes 数据导出

2.png

导出csv文件内容:

timestamp,name,detector_tag,area,floor,location,department,equipment_name,gas,setup_date,sensor_date,cal_date,cal_result,ip,hub_port,sub_hub_port,maintenance
2022-3-28 21:25:9,CL96-06,,,,,,,,,,,,,,,
2022-3-28 21:25:9,F-CL96-SRD013-HF,,,,,,,,,,,,,,,
2022-3-28 21:25:9,F-CL96-SRD033-C-NH3,,,,,,,,,,,,,,,
2022-3-28 21:25:9,F-CL96-SRD033-D-NH3,,,,,,,,,,,,,,,
2022-3-28 21:25:9,F-CL96-SRD037-CLF3,,,,,,,,,,,,,,,

自定义 action,js 脚本:

let $injector = widgetContext.$scope.$injector;
let attributeService = $injector.get(widgetContext
    .servicesMap.get('attributeService'));
let deviceService = $injector.get(widgetContext.servicesMap
    .get('deviceService'));

let data = widgetContext.data;
console.log(data);

let dataKeys = [];
// get all data keys to array
widgetContext.datasources[0].dataKeys.forEach(function(
    item) {
    dataKeys.push(item.name);
});

let fileTitle = "export-device-list";

function timeConverter(UNIX_timestamp) {
    var a = new Date(UNIX_timestamp);
    var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
        'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
    ];
    var year = a.getFullYear();
    var month = a.getMonth()+1;
    var date = a.getDate();
    var hour = a.getHours();
    var min = a.getMinutes();
    var sec = a.getSeconds();
    var time = year + '-' + month + '-' + date + ' ' +
        hour + ':' + min + ':' + sec;
    return time;
}

function exportCSV() {
    // first line: header
    var allLine = "";
    var key_length = dataKeys
        .length; // csv字段数目, 不含timestamp
    dataKeys.unshift("timestamp");
    allLine += dataKeys;
    // allLine += "\n";

    var limit = data.length / key_length; // csv数据行数
    var line = new Array(limit + 1); // csv总行数,包括首行
    for (var l = 0; l < limit; l++)
        line[l] = "";

    //var j = 0;
    data.forEach(
        (item, idx) => {
            var item_data = item["data"];

            if (idx % key_length == 0) {
                allLine += "\n"; // 换行
                allLine += timeConverter(item_data[0][0]); // timestamp
                allLine += ",";
            }

            allLine += item_data[0][1]; // value/值

            if ((idx+1) % key_length != 0)
                allLine += ",";
        }
    );
    
    allLine += "\n";
    // for (var k = 0; k < key_length; k++) {
    //     allLine += line[k];
    //     allLine += "\n";
    // }

    var exportedFilenmae = fileTitle + '.csv' ||
        'export.csv';

    var blob = new Blob([allLine], {
        type: 'text/csv;charset=utf-8;'
    });
    if (navigator.msSaveBlob) { // IE 10+
        navigator.msSaveBlob(blob,
            exportedFilenmae);
    } else {
        var link = document.createElement("a");
        if (link.download !==
            undefined) { // feature detection
            // Browsers that support HTML5 download attribute
            var url = URL.createObjectURL(blob);
            link.setAttribute("href", url);
            link.setAttribute("download",
                exportedFilenmae);
            link.style.visibility = 'hidden';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }
    }
}

exportCSV();

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 功能,其实在实施过程中,还是遇到很多问题,需要提前注意!

独立于 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 来管理,所以命名要独立;