Mackerel + Raspberry Pi で 職場環境をモニタリング

Mackerel + Raspberry Pi で 職場環境をモニタリング

皆さんMackerelをご存知ですか?

Mackerel とは はてな社が提供しているサーバー監視のサービスで、
サーバーにエージェントと言われるモジュールを設置するだけで、サーバーの状況がブラウザからグラフィカルに確認できるサービスです。

また、監視ルールを設定しておくことにより、サーバーが特定の状態(CPU使用率90%以上など)になった場合アラートを通知したり
無料プランでも一部制限はありますが、サーバー5台まで監視可能など、お手軽、便利、太っ腹なサービスです。

今回は、そんな本来はサーバー監視のサービスであるMackerelを本来の用途以外に使ってみようと思います。

突然ですが、夏ですね

だんだん気候も夏めいて来た今日このごろ、やはり気になるのはオフィスの環境。
健康を害するような環境で仕事をしないためにも、暑さで仕事の効率が落ちないためにも職場環境には気を使いたいところです。

というわけで、今回は職場(でなくてもいいですが)の温度と湿度を計測したいと思います。

温度・湿度を測定する

では、実際に監視システムを構築していきたいと思います。

今回使うもの

  • Raspberry pi 2 Type B
  • 温湿度センサー
  • Raspberry pi と 温度センサー接続用のブレッドボードやジャンパ線
    • 温度センサーとRaspberry Pi をつなぐための線を必要な分用意します。
  • 以前に使用したズゴック(オプション)

ハードウェアの準備

Raspberry Pi と AM2320 を接続する

この実体配線図のようにAM2320をRaspberry Pi と接続します。
AM2320は型番のシルク印刷を表に見た場合、
左から

  • VDD <-> Pin #2 5V
  • SDA <-> Pin #3 SDA
  • GND <-> Pin #20 等 GND
  • SCL <-> Pin #5 SCL
    と接続します。

raspi_am2320
raspi_am2320

ソフトウエアの準備

Raspberry Pi で I2C を使えるようにする

今回使用する AM2320 という温湿度計モジュールは I2C インターフェース経由でデータを取得します。
RaspberryPIは初期状態ではI2Cが有効になっていないため、
こちらのページ https://blog.ymyzk.com/2015/02/enable-raspberry-pi-i2c/ を参考にし、I2Cを有効にします。

AM2320からデータを取得するプログラムを用意する

こちらのコード https://github.com/takagi/am2321 をベースにし、出力をMackerelに対応させたコードがこちらです。


#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h> /* for O_RDWR */
#include <string.h> /* for memcpy */
#include <linux/i2c-dev.h> /* for I2C_SLAVE */
#include <time.h>
#include <inttypes.h>
#include <math.h>
/* define room id for mackrel*/
#define ROOM_ID 1
/* I2C character device */
#define I2C_DEVICE "/dev/i2c-0"
/* I2C address of AM2321 sensor in 7 bits
* - the first 7 bits should be passed to ioctl system call
* because the least 1 bit of the address represents read/write
* and the i2c driver takes care of it
*/
#define AM2321_ADDR (0xB8 >> 1)
/*
* udelay function
*/
long timeval_to_usec( struct timeval tm ) {
return tm.tv_sec * 1000000 + tm.tv_usec;
}
void udelay( long us ) {
struct timeval current;
struct timeval start;
gettimeofday( &start, NULL );
do {
gettimeofday( &current, NULL );
} while( timeval_to_usec( current ) - timeval_to_usec( start ) < us );
}
/*
* CRC16
*/
unsigned short crc16( unsigned char *ptr, unsigned char len ) {
unsigned short crc = 0xFFFF;
unsigned char i;
while( len-- )
{
crc ^= *ptr++;
for( i = 0; i < 8; i++ ) {
if( crc & 0x01 ) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
unsigned char crc16_low( unsigned short crc ) {
return crc & 0xFF;
}
unsigned char crc16_high( unsigned short crc ) {
return crc >> 8;
}
/*
* st_am2321 Structure
*/
typedef struct {
unsigned char data[8];
} st_am2321;
void __check_crc16( st_am2321 measured ) {
unsigned short crc_m, crc_s;
crc_m = crc16( measured.data, 6 );
crc_s = (measured.data[7] << 8) + measured.data[6];
if ( crc_m != crc_s ) {
fprintf( stderr, "am2321: CRC16 does not match\n" );
exit( 1 );
}
return;
}
st_am2321 __st_am2321( unsigned char* data ) {
st_am2321 result;
memcpy( result.data, data, 8 );
__check_crc16( result );
return result;
}
void am2321_dump( st_am2321 measured ) {
printf( "[ 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x ]\n",
measured.data[0], measured.data[1],
measured.data[2], measured.data[3],
measured.data[4], measured.data[5],
measured.data[6], measured.data[7] );
return;
}
short __am2321_temperature( st_am2321 measured ) {
return (measured.data[4] << 8) + measured.data[5];
}
short am2321_temperature_integral( st_am2321 measured ) {
return __am2321_temperature( measured ) / 10;
}
short am2321_temperature_fraction( st_am2321 measured ) {
return __am2321_temperature( measured ) % 10;
}
short __am2321_humidity( st_am2321 measured ) {
return (measured.data[2] << 8) + measured.data[3];
}
short am2321_humidity_integral( st_am2321 measured ) {
return __am2321_humidity( measured ) / 10;
}
short am2321_humidity_fraction( st_am2321 measured ) {
return __am2321_humidity( measured ) % 10;
}
/*
* Measurement function
*/
st_am2321 am2321() {
int fd;
int ret;
int retry_cnt;
unsigned char data[8];
/* open I2C device */
fd = open( I2C_DEVICE, O_RDWR );
if ( fd < 0 ) {
perror( "am2321(1)" );
exit( 1 );
}
/* set address of I2C device in 7 bits */
ret = ioctl( fd, I2C_SLAVE, AM2321_ADDR );
if ( ret < 0 ) {
perror( "am2321(2)" );
exit( 2 );
}
retry_cnt = 0;
retry:
/* wake I2C device up */
write( fd, NULL, 0);
/* write measurement request */
data[0] = 0x03; data[1] = 0x00; data[2] = 0x04;
ret = write( fd, data, 3 );
if ( ret < 0 && retry_cnt++ < 5 ) {
fprintf( stderr, "am2321: retry\n" );
udelay( 1000 );
goto retry;
}
if ( ret < 0 ) {
perror( "am2321(3)" );
exit( 3 );
}
/* wait for having measured */
udelay( 1500 );
/* read measured result */
memset( data, 0x00, 8 );
ret = read( fd, data, 8 );
if ( ret < 0 ) {
perror( "am2321(4)" );
exit( 4 );
}
/* close I2C device */
close( fd );
return __st_am2321( data );
}
st_am2321 am2321_stub() {
unsigned char data[] = { 0x03, 0x04, 0x01, 0x41,
0x00, 0xEA, 0x21, 0x8F };
return __st_am2321( data );
}
/*
* Print functions
*/
void print_am2321( st_am2321 measured ) {
printf( "%d.%d %d.%d\n",
am2321_temperature_integral( measured ),
am2321_temperature_fraction( measured ),
am2321_humidity_integral( measured ),
am2321_humidity_fraction( measured ) );
return;
}
void print_am2321_human_readable( st_am2321 measured ) {
printf( "Temperature %d.%d [C]\n",
am2321_temperature_integral( measured ),
am2321_temperature_fraction( measured ) );
printf( "Humidity %d.%d [%%]\n",
am2321_humidity_integral( measured ),
am2321_humidity_fraction( measured ) );
return;
}
void print_am2321_mackerel_readable( st_am2321 measured ) {
time_t epoch_time;
epoch_time = time(NULL);
double temp = am2321_temperature_integral( measured );
if( am2321_temperature_fraction( measured ) > 0 ){
temp = temp + ( am2321_temperature_fraction( measured ) / pow( 10.0, (int) log10(am2321_temperature_fraction( measured )) +1 ) );
}
double humd = am2321_humidity_integral( measured );
if( am2321_humidity_fraction( measured ) > 0 ){
humd = humd + ( am2321_humidity_fraction( measured ) / pow( 10.0, (int) log10(am2321_humidity_fraction( measured )) +1 ) );
}
//0.81Td+0.01H(0.99Td-14.3)+46.3
double di = ( 0.81 * temp ) + (( 0.01 * humd ) * (( 0.99 * temp ) - 14.3)) + 46.3;
printf( "room.%d.temperatre\t%0.02f\t%d\n", ROOM_ID, temp, (uintmax_t)epoch_time);
printf( "room.%d.humidity\t%0.02f\t%d\n", ROOM_ID, humd, (uintmax_t)epoch_time);
printf( "room.%d.discomfortindex\t%0.02f\t%d\n", ROOM_ID, di, (uintmax_t)epoch_time);
return;
}
/*
* Main
*/
#define OPT_HUMAN_READABLE 0x1
#define OPT_STUB 0x2
#define OPT_MACKEREL_READABLE 0x4
int print_help() {
fprintf( stderr,
"Usage: am2321 [-r] [-s]\n"
"Get temperature and humidity measured with Aosong's AM2321 sensor.\n"
" -r human readable output\n"
" -m mackerel readable output\n"
" -s not measure with AM2321, instead stub of 23.4[C] and 32.1[%]\n" );
exit( 1 );
}
int parse_options( int argc, char* argv[]) {
int options = 0;
int flags = 0;
while( 1+flags < argc && argv[1+flags][0] == '-' ) {
switch( argv[1+flags][1] ) {
case 'r': options |= OPT_HUMAN_READABLE; break;
case 'm': options |= OPT_MACKEREL_READABLE; break;
case 's': options |= OPT_STUB; break;
default:
fprintf( stderr, "am2321: Unsupported option \"%s\"\n", argv[1+flags] );
print_help();
exit( 1 );
}
flags++;
}
return options;
}
int is_opt_human_readable( int options ) {
return options & OPT_HUMAN_READABLE;
}
int is_opt_mackerel_readable( int options ) {
return options & OPT_MACKEREL_READABLE;
}
int is_opt_stub( int options ) {
return options & OPT_STUB;
}
int main( int argc, char* argv[] ) {
int options;
st_am2321 measured;
/* parse the given options */
options = parse_options( argc, argv );
/* measure temperature and humidity */
measured = ! is_opt_stub( options ) ? am2321() : am2321_stub();
/* print measured temperature and humidity */
if ( is_opt_human_readable( options ) ) {
print_am2321_human_readable( measured );
} else if (is_opt_mackerel_readable( options ) ){
print_am2321_mackerel_readable( measured );
} else {
print_am2321( measured );
}
return 0;
}
view raw am2321.c hosted with ❤ by GitHub

これをRaspberryPI上でコンパイルし、パスの通った場所に配置します。

1
2
3
4
5
6
curl -LO https://gist.githubusercontent.com/yenjoji/40d135519a0741d3718b/raw/5c7835651a539f16f3446108e15aa482f6c2111f/am2321.c
gcc -lm -o am2321 am2321.c
chmod a+x am2321
mv am2321 /usr/local/bin

Mackerelとの連携

Mackerelに登録してオーガニゼーションを作成する

http://help-ja.mackerel.io/entry/getting-started
の手順にそって、オーガニゼーションを作成します。

maeckrel-agent をインストールする

パッケージをダウンロードし、インストールします。

1
2
curl -LO http://file.mackerel.io/agent/deb/mackerel-agent_latest.all.deb
sudo dpkg -i mackerel-agent_latest.all.deb

ARM 版バイナリに実行ファイルを置き換える

通常ならば、パッケージをインストールすれば完了ですが、
RasperryPiはもともとMackerelが想定しているCPUとアーキテクチャが違うためかそのままではうまく起動しません。
そのため、RaspberryPiのCPUにあったアーキテクチャのMackerelの実行ファイルで上書きします。

1
2
3
4
5
curl -LO https://github.com/mackerelio/mackerel-agent/releases/download/v0.17.1/mackerel-agent_linux_arm.tar.gz
tar xf mackerel-agent_linux_arm.tar.gz
sudo mv /usr/local/bin/mackerel-agent /usr/local/bin/mackerel-agent.org
sudo mv mackerel-agent_linux_arm/mackerel-agent /usr/local/bin/mackerel-agent

maeckrel-agentの設定

インストールが完了したので、設定をしていきます。

設定ファイルに apiKeyとカスタムメトリクスの設定を追加します。
/etc/mackerel-agent/mackerel-agent.conf に オーガニゼーションの画面に表示されているAPIKEYと
以下のカスタムメトリクスの設定を追加してください。

1
2
3
# Get room status
[plugin.metrics.temperature]
command = "/usr/local/bin/am2321 -m"

mackerel-agent 起動

以上で設定がひと通り完了しましたので、エージェントを起動します。

1
sudo /etc/init.d/mackerel-agent start

これで、先ほど作成したオーガニゼーションに自動的にホストが追加され、
mackerelのデフォルトの監視項目と、温湿度計のデータが記録されていくようになります。

mackerel-room
mackerel-room

アラートの設定

無事に温度と湿度の記録が始まりました。
しかしながらこれだけでは、職場が危機な状況になっても気づくことが出来ません。

ということで、職場の労働環境を監視する尺度として、不快指数を使って職場環境をモニタリングしたいと思います。

実は先程のMackerelカスタムメトリクス取得プログラムには、温度、湿度以外に不快指数も取得できるようにしてあります。
なので、手順通りに設定した場合は、すでにカスタムメトリクス上に不快指数が記録されていると思います。

wikipediaによると、日本人は不快指数が77を超えた辺りから一部の方々が不快を覚え始め、80を超えるとみんなが不快感を感じるそうなので、
この値を超えた場合に通知が来るように設定したいと思います。

  1. Macerel管理画面から Monitor メニューをクリックし、監視ルールを追加ボタンをクリックします。
    mackerel-monitor_001
    mackerel-monitor_001
  2. ポップアップしたウィンドウに監視条件を入力し作成ボタンをクリックします。
    今回は不快指数(custom.room.1.discomfortindex)を選択し、77でWarning、80でCriticalとなるように設定します。
    mackerel-monitor_002
    mackerel-monitor_002
  3. 作成した関し条件が一覧に表示されていれば成功です。
    これで不快指数がしきい値を超えるた際にメール通知がされるようになりました。また、チャットなどメール以外の通知方法も用意されています。(私はHipChatにも通知しています。)
    momongar-discomfortindex
    momongar-discomfortindex

ズゴックと連携する

以前作成した、ズゴックXFDですが、
チームのみんながちゃんとテストが通ることを確認してからソースコードをPushするため、ほぼ活躍する機会がありません。
このままだと ただ職場にガンプラをおいている人になってしまう ので、
ズゴックの存在意義を上げるべく温度計を連携させたいと思います。

温度計の実装

  1. ドリルで大まかな穴を開けた後、ニッパーで穴をつなぎ
    DSC_0093
    DSC_0093
  2. カッターで凹凸を整えます
    DSC_0094
    DSC_0094
  3. 温度計を開けた穴にはめ込み、配線を足経由で踵から外部に引き出せば完成です。
    DSC_0101
    DSC_0101
  4. 組み立てるとこんなかんじになります。
    DSC_0108
    DSC_0108
    DSC_0105
    DSC_0105

結果

晴れてズゴックに温度計が付き、職場の状況をモニタリングすることが可能になりました。

これで、自分の居室の不快指数を計測し放題です。
計測したところで、特に快適になったりはしないのは残念ですが、
計測データを元に現場のリーダーに職場環境のカイゼンをお願いする材料くらいにはなるはずです。

ただ、残念ながら、温度計を付けてもズゴック見た目に変化がないので、
相変わらず傍から見るとガンダム好きな人にしか見えないという点は今後の課題とします。

みなさんもMackerelと様々なセンサーを組み合わせて遊んでみるのはいかがでしょうか?

参考URL

Raspberry Pi の I2C を有効化する方法 (2015年版)

https://blog.ymyzk.com/2015/02/enable-raspberry-pi-i2c/

Raspberry Pi で湿度センサ AM2321 を使う

http://technology-memo.seesaa.net/article/404719464.html

Mackerelについて

http://qiita.com/ariarijp/items/838628e121b051524309

不快指数

https://ja.wikipedia.org/wiki/%E4%B8%8D%E5%BF%AB%E6%8C%87%E6%95%B0