FLIR AX8で遠隔監視する

## はじめに
会社の業務でAX8というサーモグラフィカメラを使ったので、その忘備録として、AX8からデータを取得する方法について記しておきます。

## FLIR AX8
FLIR AX8とは、フリアーシステムズの固定型サーモグラフィカメラです。
固定型として使うため、ディスプレイやバッテリーは搭載しておらず、もっぱら外部と通信して使うことを目的しています。
固定型の中では比較的安価かつ小型です。その代わり、サーモグラフィの解像度が80×60と低く、機能も限定的ですが、プロトタイプ的にソリューションを組み立てる場合は有力な候補となります。

このAX8を使って遠隔監視の仕組みを作る場合、どのようにしてAX8からデータを取得すればよいのでしょうか。
AX8には、温度が閾値を超えた場合はメールを送信したり、サーモグラフィ画像をFTPサーバに送信する機能があります。
本稿では、Modbus/TCPとREST APIを使った遠隔監視の方法を記します。

## Modbus/TCP
Modbus産業用プロトコルとして広く使われている通信方式です。
Modbus/TCPは、Modbusプロトコルをイーサネット上でやり取りします。
AX8は、Modbusスレーブとなり、Modbusマスターからの通信に対する応答として、データを送信することができます。

本稿ではModbus通信をするプログラムとして、Pythonを用い、ライブラリとしてpymodbusを選びました。

### 計測データを取得する
Python環境にpymodbusをインストールしておいてください。
また、AX8をパソコンを同じネットワーク上に接続し起動して置いてください。

その上で、以下のコードを実行します。
プログラム中の`AX8_HOST`は環境に合わせ適宜変更してください。

```python:基本的な計測データを取得
#!/usr/bin/env python
from pymodbus.client import ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian

# AX8に接続
AX8_HOST = '192.168.0.180'
client = ModbusTcpClient(AX8_HOST, port=502)
client.connect()

# レジスタ1001-1018のデータを読み込み
rr = client.read_input_registers(1000, 18, 1)
if rr.isError():
    print('error')
decoder = BinaryPayloadDecoder.fromRegisters(rr.registers, byteorder=Endian.BIG, wordorder=Endian.LITTLE)
print("flags 1:", decoder.decode_16bit_uint())
print("flags 2:", decoder.decode_16bit_uint())
print("flags 3:", decoder.decode_16bit_uint())
print("flags 4:", decoder.decode_16bit_uint())
print("delta temperature 1:", decoder.decode_32bit_float())
print("delta temperature 2:", decoder.decode_32bit_float())
print("delta temperature 3:", decoder.decode_32bit_float())
print("delta temperature 4:", decoder.decode_32bit_float())
print("delta temperature 5:", decoder.decode_32bit_float())
print("delta temperature 6:", decoder.decode_32bit_float())
print("internal camera temperature:", decoder.decode_32bit_float())

# レジスタ1019-1090のデータにある6個のbox・spotのペアを読み込み
for i in range(6):
    rr = client.read_input_registers(1018 + i * 12, 12, 1)
    if rr.isError():
        print('error')
    decoder = BinaryPayloadDecoder.fromRegisters(rr.registers, byteorder=Endian.BIG, wordorder=Endian.LITTLE)
    print(f"spot {i+1} temperature:", decoder.decode_32bit_float())
    print(f"box {i+1} min temperature:", decoder.decode_32bit_float())
    print(f"box {i+1} max temperature:", decoder.decode_32bit_float())
    print(f"box {i+1} averate temperature:", decoder.decode_32bit_float())
    print(f"spot {i+1} temperature valid state:", decoder.decode_16bit_uint())
    print(f"spot {i+1} min temperature valid state:", decoder.decode_16bit_uint())
    print(f"spot {i+1} max temperature valid state:", decoder.decode_16bit_uint())
    print(f"spot {i+1} avg temperature valid state:", decoder.decode_16bit_uint())

# 接続を閉じる
client.close()
```

ウェブブラウザ上で確認したときの現在値に対して、Modbus/TCPで取得した結果は以下です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105381/57c53d4a-6869-3099-a067-972868815e8f.png)

実行結果は以下です。
```
flags 1: 4097
flags 2: 0
flags 3: 0
flags 4: 0
delta temperature 1: 4.049993991851807
delta temperature 2: 0.04999389499425888
delta temperature 3: 0.0
delta temperature 4: 0.0
delta temperature 5: 0.0
delta temperature 6: 0.0
internal camera temperature: 311.04327392578125
spot 1 temperature: 289.9499816894531
box 1 min temperature: 287.54998779296875
box 1 max temperature: 291.75
box 1 averate temperature: 289.3500061035156
spot 1 temperature valid state: 1
spot 1 min temperature valid state: 1
spot 1 max temperature valid state: 1
spot 1 avg temperature valid state: 1
...割愛
```

flags1～3のそれぞれのビットは以下を意味します。
なお、flags4は予約領域であり、現在のところ意味はありません。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105381/3ab872fc-fc61-8901-e09c-60eadf72449e.png)
>
> [AX8のマニュアル](https://www.ads-tec.co.jp/wp/wp-content/uploads/2022/06/Users-manual_FLIR-AX-series_t559913-ja-jp_20210913.pdf)より

例えば、flags1が4097でしたが、ビット表記にすると`0001 0000 0000 0001`となり、Bit0とBit12が1となっています。
Bit0は表から「Auto NUC」を意味するので、どうやら自動的にNUC（キャリブレーション）が行われるようです。
Bit12は「Image Freeze」であり、画像更新の停止を意味します。

次に、「delta temperature 1」は計測値同士を演算して出力する値（温度差）です。
ここでは、「Box max 1」-「Box min 1」としていました。
値が一致していない気もしますが、まあ大体あってます。

「internal camera temperature」はカメラ内部温度です。
以降、温度の値が出てきますが、単位は絶対温度です。
今回の「internal camera temperature」の場合、摂氏に変換すると、

311.04327392578125 - 275.15 = 37.89327392578125 ≒ 37.9

となります。計算結果は小数第二位を丸めて小数第一位までで表すのがおすすめです。

「～temperature valid state」は温度データの状態を表します。
それぞれ以下の意味を表します。
| 値 | 意味 |
|--|---|
| 0 | Undefined: 未定義 |
| 1 | In the acceptable range: 許容範囲内 |
| 2 | Less than the acceptable range: 許容範囲以下 |
| 3 | More than the acceptable range: 許容範囲以上 |
| 4 | Outside the acceptable range: 許容範囲外 |
| 5 | Outside calibration: キャリブレーション外 |
| 6 | Unstable temperature: 温度不安定 |
| 7 | Temperature is compensated with delta correction: よくわかりません（涙 |

今回の場合は1であり、許容範囲内の温度データなので、出力された温度データは計測結果として利用可能です。
この値が0の場合は、ボックスやスポットの設定をしてないことを意味します。

### より詳細なデータを取得する
Modbus/TCPではユニットIDを指定することは意味がないのが普通ですが、先のプログラムはユニットIDとして1を指定しなければなりません。

実は、このユニットIDを1以外に変えることで、様々なデータを取得できます。
詳細は以下のリンクをご確認ください。

https://flir.custhelp.com/app/answers/detail/a_id/1234/~/reading-the-roi-%28box%2C-spot%2C-diff%29-status-over-modbus-tcp

例えば、Box1の最高温度のX・Y座標を取得するには以下のプログラムとなります。
なおユニットIDは、`read_input_registers`の第3引数で指定します。

```python:Modbusでより詳細なデータを取得
from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient('192.168.0.180', port=502)
client.connect()

rr = client.read_input_registers(4000, 1, 109)
print("Enable Local Object Parameter Values:", rr.registers[0])
rr = client.read_input_registers(4301, 2, 109)
print("Box Max Temp. Position X:", rr.registers[0], rr.registers[1])
rr = client.read_input_registers(4321, 2, 109)
print("Box Max Temp. Position Y:", rr.registers[0], rr.registers[1])

# 接続を閉じる
client.close()
```

実行結果は以下です。

```
Enable Local Object Parameter Values: 1
Box Max Temp. Position X: 51 0
Box Max Temp. Position Y: 37 0
```

### 撮影する
Modbus/TCPでレジスタを書き込むことで、撮影することができます。
レジスタ番号400001の6ビット目「Save Image」が0から1に変化したときに、AX8は保存を行います。
よって、以下のようにします。
```python:AX8で撮影
#!/usr/bin/env python
from pymodbus.client import ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian

# AX8に接続
client = ModbusTcpClient('192.168.0.180', port=502)
client.connect()

# レジスタ400001を取得
rr = client.read_input_registers(0, 1, 1)
if rr.isError():
    print('error')
print(rr.registers[0])

# 6ビット目「Save Image」を0にして、レジスタに書き込み
hr = client.write_registers(0, [rr.registers[0]&65503], 1)
if hr.isError():
    print('error')

# 6ビット目「Save Image」を1にして、レジスタに書き込み -> 撮影！
hr = client.write_registers(0, [rr.registers[0]|32], 1)
if hr.isError():
    print('error')

# 6ビット目「Save Image」を0にして、レジスタに書き込み
hr = client.write_registers(0, [rr.registers[0]&65503], 1)
if hr.isError():
    print('error')

# 接続を閉じる
client.close()
```


## REST API
AX8はファームウェアアップデートによりREST APIに対応します。
ユーザ登録は必要ですが、FLIR社のウェブサイトより、現時点（2023年11月）で1.52.16のファームウェアをダウンロードできます。

Modbus/TCPよりも簡単にデータや画像も取得できるため、IoTゲートウェイでAX8のデータを取得するなら、REST APIを用いたほうがいいです。

画像データを取得するには、以下のURLにアクセスします。（IPアドレスは利用環境により変えてください）

```url
http://192.168.0.180/api/image/current?imgformat=JPEG
```

また、Boxによる範囲指定の温度監視の結果を得るには、以下のURLにアクセスします。

```url
http://192.168.1.180/api/box/1.json?pretty=true&tempUnit=C
```

```json:Box 1 ROI
{
    "active": true,
    "tempUnit": "C",
    "avgT": 16.3,
    "avgValid": "=",
    "maxT": 18.6,
    "maxPos": [
        51,
        37
    ],
    "maxValid": "=",
    "minT": 14.5,
    "minPos": [
        39,
        21
    ],
    "minValid": "=",
    "pos": [
        26,
        20
    ],
    "size": [
        26,
        20
    ]
}
```
URLで`tempUnit=C`と指定することで、摂氏温度で出力してくれます。
絶対温度は日常生活の温度から離れすぎており、直感的にわかりにくいため、摂氏で出力し記録したほうが後々便利です。

より詳細な利用方法は、FLIRのサイトをご確認ください。
ただし、AX8は簡易的な機能しか実装されていないので、リンク先のすべてのREST APIが使えるわけではありません。

https://flir.custhelp.com/app/answers/detail/a_id/3602/~/getting-started-using-rest-api-with-automation-cameras


## AX8を使う上での注意点
使っていて気をつけたい点を述べます。

### 消費電力
常時2～3Wの電力を消費します。
それだけ本体の温度も上昇するため、常時運用する場合、AX8に付属の放熱ブラケットを装着しましょう。

あるいは、使うときだけPoEの電源供給をするなどして、電力削減の工夫をしましょう。

### 放射率
サーモグラフィカメラは測定対象の表面の放射率の影響を受けます。
必ず測定対象の放射率を求め、AX8に設定した上で計測しましょう。

放射率とかよく分からない場合は、放射率が1.0に近い黒体テープを測定対象に貼るのがおすすめです。