masato-ka's diary

日々思ったこととか、やったことの備忘録。

AMG8833のデータを簡易サーモグラフィぽくブラウザで可視化(後編)

この記事について

この記事はAMG8833のデータを簡易サーモグラフィぽくブラウザで可視化(前編)の続きです。AMG8833のデータを使い、WEBブラウザ上でヒートマップ表示にチャレンジしています。

masato-ka.hatenablog.com

前回まではESP8266を使い、AMG8833からデータを取得するところまで紹介しました。今回はESP8266から受け取ったセンサデータをサーバ側の可視化について説明します。

サーバ側の構成について

 サーバ側の基本的な動作はESP8266からセンサデータをHTTPで受け取ります。受け取ったデータはそのままキューに送りつけます。また可視化用のブラウザからのリクエスト受け付けるとデータの可視化の非同期処理を起動します。この非同期処理は先ほどのセンサデータを送っているキューからデータを取り出し、ブラウザにSSE(Server sent event)で非同期にデータを送り続けます。前編で説明した通り、センサデータを逐次送りつけ、ブラウザの表示をリアルタイム表示させたいと思い、バックエンド(センサ受信処理)とフロントエンド(ブラウザの表示)を非同期通信で実装しました。サーバ側の受信スレッドを枯渇させないように、別スレッドとして、SSEの送信処理を実装しています。また、センサデータの受信スレッドとフロントエンドの送信処理をメッセージキューで同期させています。メッセージキューは別にサービスを建てたくなかったので、組みこみActiveMQを利用しています。  ヒートマップなどの表示処理はブラウザ側で実行します。サーバ側のバックエンドの処理はSpringBootで実装します。ブラウザ表示のフロントエンドはHTMLとAngularJSで実装しています。ブラウザは初回表示時にフロントエンドを取得し、その後、サーバからのSSEを受信する都度画面の描画を変更します。

f:id:masato-ka:20180106112641p:plain

今回実装したサーバ側コードは以下のリポジトリにアップしています。

github.com

バックエンド処理の実装

バックエンドはSpringBootで実装しています。

センサデータの受信

センサデータは[30.0,30.0,30.0,・・・・・・]JSON形式でPOSTリクエストで送信されます。これを以下のRestControllerで取得します。

@RestController
@RequestMapping("/api/v1/thermography")
public class Controller {

    private final JmsTemplate jmsTemplate;

    private final Queue queue;

    public Controller(JmsTemplate jmsTemplate, Queue queue) {
        this.jmsTemplate = jmsTemplate;
        this.queue = queue;
    }

    @PostMapping
    public void postThermoGraphyTelemetory(@RequestBody List<Double> payload) {
        jmsTemplate.convertAndSend(queue, payload); //(1)
    }

}
  • (1) 受け取ったデータはinmemory.queueと言う名前のキューに入れます。バックエンド側ではDoubleオブジェクトのリストとして表現されています。

ServerSentEvent

ServerSentEventの受信処理はRestContorollerに以下のエンドポイントを追加します。非同期処理のタイムアウト値は1分に設定します。(SSEが1分送信されなければタイムアウトします。)

    private final AsyncEmitData asyncEmitData;
    private final JmsTemplate jmsTemplate;

    private final Queue queue;

    public Controller(AsyncEmitData asyncEmitData, JmsTemplate jmsTemplate, Queue queue) {
        this.asyncEmitData = asyncEmitData;
        this.jmsTemplate = jmsTemplate;
        this.queue = queue;
    }

   @GetMapping("/sse")
    public SseEmitter getThermoGraphySSE() throws IOException, InterruptedException {
        SseEmitter emitter = new SseEmitter(60000L);
        log.info("do Async");
        asyncEmitData.streming(emitter);
        log.info("close Async");
        return emitter;
    }

実際に非同期処理を実施しているのはAsyncEmitData classで実装しています。

@Component
public class AsyncEmitData {

    private boolean isLock = true;

    private final JmsTemplate jmsTemplate;

    public AsyncEmitData(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }


    @Async
    public void streming(SseEmitter emitter) throws IOException, InterruptedException {

        jmsTemplate.setReceiveTimeout(0);// (1)
        while (true) {
            List<Double> result = (List<Double>) jmsTemplate.receiveAndConvert("inmemory.queue"); //(2)
            emitter.send(result); //(3)
            log.info("send event1");
            if (result == null) {
                break;
            }
        }
        log.info("complete sse session.");
        emitter.complete();
    }

}

別途@EnableAsyncアノテーションを指定します。

  • (1) JmsTemplateの受信処理はデフォルトで非同期処理のため、タイムアウトを0に指定し同期的に受信待ちするようにします。
  • (2) JmsTemplate.receiveAndConvertでinmemoru.queueのデータを待ち受けします。
  • (3) キューから取得したデータをそのままSSEとして送信します。またデータがnullであればサーバから切断します。

Embedded ActiveMQの利用

組みこみActiveMQの設定です。

@Configuration
@EnableJms
public class ActiveMqConfig {

    @Bean
    public Queue queue() {
        return new ActiveMQQueue("inmemory.queue");
    }

}
  • application.yml
spring:
  activemq:
    in-memory: true
    pool:
      enabled: false
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

ブラウザ側処理

ヒートマップの表示処理

ヒートマップの表示はTableを温度の値ごとにAngularJSで表示させます。

  • index.html
<!DOCTYPE html>
<html lang="en" ng-app="App">
<head>
    <meta charset="UTF-8">
    <title>Viaualization AMG88xx</title>
    <style>
        table {
            border: none;
        }
        td {
            width: 50px;
            height: 50px;
            border-style: none;
            border: none;
        }

        tr {
            border: none;
        }
    </style>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.6/angular.min.js"></script>
    <script type=text/javascript src="./js/controller.js"></script>
</head>
<body ng-controller="ApplicationController">
<center>
<h1>Visualization.</h1>
<table bgcolor="white" >
    <tr ng-repeat="line in image">
        <td ng-repeat="pixel in line" style="background-color: rgb({{pixel[0]}},{{pixel[1]}},{{pixel[2]}})">1</td>
    </tr>
</table>
</center>
</body>
</html>
  • controller.js
ar app = angular.module('App',[]);
var es = new EventSource('/api/v1/thermography/sse');

app.controller('ApplicationController', ['$scope',function($scope){

    es.addEventListener('message', function (event) {

        var acceptData = JSON.parse(event.data);

        var temp = []

        for (var i = 0; i <= 56; i += 8) {
            temp.push(acceptData.slice(i, i + 8));
        }
        $scope.image = [];
        $scope.image = temp.map(function (line) {
            return line.map(function (temp) {
                return translateTempToRGB(temp);
            })
        });
        $scope.$apply();
    });

    data = function () {
        $scope.image = [];
        var temp = [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]];

        $scope.image = temp.map(function (line) {
            return line.map(function (temp) {
                return translateTempToRGB(temp);
            })
        });
    }
    data();
}]);

SSEでデータを取得するたびにAngularJSで表示を切り替えています。温度の値をRGBに変換する処理translateTempTpRGBファンクションは以下のような実装です。

var translateTempToRGB = function(temp){
    var scale = 1 / 50.0;
    temp * scale;
    return colorRGBBar(temp * scale);
}

var sigmoid = function(x, gain, offset){
    var value = (x+offset)*gain;
    return ((Math.tanh(value*0.5)+1)*0.5);
}

var colorRGBBar = function(x){
    var gain = 10
    var offset_x = 0.2
    x = (x * 2) - 1
    red = sigmoid(x, gain, -1*offset_x)
    blue = 1-sigmoid(x, gain, offset_x)
    green = sigmoid(x, gain, 0.6) + (1-sigmoid(x,gain,-1*0.6))
    green = green - 1.0
    return [Math.floor(red*255),Math.floor(green*255),Math.floor(blue*255)];
}

動作

実際に動作させた動画は以下の通りです。わかりやすく氷をセンサの前にくぐらせています。また、チョキやパーなどもわかる程度にデータが取れています。


blog20180106

まとめ

今回はAMG8833のデータをESP8266で取得し、WEBブラウザで可視化するところまでやりました.AMG8833自体は指定温度での割り込み機能などがあり、センサとしては高度な処理ができそうです。また、補間処理をすることで8x8から画素を広げて表示することにもチャレンジしたいです。 サーバ側はフルスクラッチで実装していますが、クラウドサービスなどを使ってデータの取得部分を置き換えるようにしたいです。