USB温度計

はじめに

安価なUSB温度計が発売されています。銀色を秋葉原で安価に購入しました。
購入後しばらく放置していましたが、いつも電源が入りっぱなしのサーバに接続温度を測定することにしました。

Linux(CentOS)で温度測定

付属のCD-ROMにはWindowsのドライバ、APLが収録されていますが、Linux用はありません。
早速、先人の成果を調査すると、Ubuntuでの情報がありました。temperというAPLです。
ドライバはLinuxでは特にインストールすることなく、認識されます(HIDデバイスとして)。

# dmesg | grep TEMP
input: RDing TEMPerV1.2 as /class/input/input14
input,hidraw0: USB HID v1.10 Keyboard [RDing TEMPerV1.2] on usb-0000:00:12.0-1
hiddev96,hidraw96: USB HID v1.10 Device [RDing TEMPerV1.2] on usb-0000:00:12.0-1
# lsusb
Bus 003 Device 001: ID 0000:0000
Bus 006 Device 001: ID 0000:0000
Bus 002 Device 005: ID 0624:0248 Avocent Corp.
Bus 002 Device 001: ID 0000:0000
Bus 004 Device 016: ID 0c45:7401 Microdia
Bus 004 Device 003: ID 0518:0001 EzKEY Corp. USB to PS2 Adaptor v1.09
Bus 004 Device 002: ID 067b:2303 Prolific Technology, Inc. PL2303 Serial Port
Bus 004 Device 001: ID 0000:0000
Bus 005 Device 001: ID 0000:0000
Bus 001 Device 001: ID 0000:0000

gitでtemperを取得

  • APLはgitのレポジトリにあるため、まず、gitをインストールします。
# yum install git
Loaded plugins: fastestmirror, priorities, security
Loading mirror speeds from cached hostfile
* base: ftp.jaist.ac.jp
* extras: ftp.jaist.ac.jp
* rpmforge: ftp.kddilabs.jp
* updates: ftp.iij.ad.jp
126 packages excluded due to repository priority protections
Setting up Install Process
Resolving Dependencies
--> Running transaction check
---> Package git.i386 0:1.7.11.3-1.el5.rf set to be updated
--> Processing Dependency: perl-Git = 1.7.11.3-1.el5.rf for package: git
--> Processing Dependency: perl(Git) for package: git
--> Running transaction check
---> Package perl-Git.i386 0:1.7.11.3-1.el5.rf set to be updated
--> Processing Dependency: perl(SVN::Ra) for package: perl-Git
--> Processing Dependency: perl(SVN::Delta) for package: perl-Git
--> Processing Dependency: perl(SVN::Client) for package: perl-Git
--> Processing Dependency: perl(YAML::Any) for package: perl-Git
--> Processing Dependency: perl(SVN::Core) for package: perl-Git
--> Running transaction check
---> Package perl-YAML.noarch 0:0.72-1.el5.rf set to be updated
---> Package subversion-perl.i386 0:1.6.11-11.el5_9 set to be updated
--> Finished Dependency Resolution
Dependencies Resolved
==========================================================================================
Package                 Arch           Version                    Repository        Size
==========================================================================================
Installing:
git                     i386           1.7.11.3-1.el5.rf          rpmforge         8.0 M
Installing for dependencies:
perl-Git                i386           1.7.11.3-1.el5.rf          rpmforge          56 k
perl-YAML               noarch         0.72-1.el5.rf              rpmforge          84 k
subversion-perl         i386           1.6.11-11.el5_9            updates          1.0 M
Transaction Summary
==========================================================================================
Install       4 Package(s)
Upgrade       0 Package(s)
Total download size: 9.2 M
Is this ok [y/N]: y
Downloading Packages:
(1/4): perl-Git-1.7.11.3-1.el5.rf.i386.rpm                         |  56 kB     00:00
(2/4): perl-YAML-0.72-1.el5.rf.noarch.rpm                          |  84 kB     00:00
(3/4): subversion-perl-1.6.11-11.el5_9.i386.rpm                    | 1.0 MB     00:00
(4/4): git-1.7.11.3-1.el5.rf.i386.rpm                              | 8.0 MB     00:05
------------------------------------------------------------------------------------------
Total                                                     1.4 MB/s | 9.2 MB     00:06
Running rpm_check_debug
Running Transaction Test
Finished Transaction Test
Transaction Test Succeeded
Running Transaction
Installing     : perl-YAML                                                          1/4
Installing     : subversion-perl                                                    2/4
Installing     : git                                                                3/4
Installing     : perl-Git                                                           4/4
Installed:
git.i386 0:1.7.11.3-1.el5.rf
Dependency Installed:
perl-Git.i386 0:1.7.11.3-1.el5.rf              perl-YAML.noarch 0:0.72-1.el5.rf
subversion-perl.i386 0:1.6.11-11.el5_9
Complete!
  • いよいよ、temperを落としてきます。
# cd /opt
# git clone git://github.com/bitplane/temper.git
Cloning into 'temper'...
remote: Counting objects: 69, done.
remote: Compressing objects: 100% (42/42), done.
remote: Total 69 (delta 28), reused 61 (delta 24)
Receiving objects: 100% (69/69), 13.39 KiB, done.
Resolving deltas: 100% (28/28), done.
# ll
合計 4
drwxr-xr-x 3 root root 4096 10月 19 10:38 temper
# cd temper/

ビルド

  • makeします。
# make
gcc -Wall temper.c pcsensor.c -o temper -lusb

元の記事では、Ubuntuのため、

  • build-essential

をインストールしていますが、CentOSでは不要です。

動作確認

  • まず、素のままで動作確認します。
# ./temper
19-Oct-2013 01:48,28.403656

修正

  • 元記事にあるように、表示形式とローカライズ(GMT->JST)を行います。
# diff -c temper.c.org temper.c
*** temper.c.org        2013-10-19 10:49:15.000000000 +0900
--- temper.c    2013-10-19 11:38:10.000000000 +0900
***************
*** 41,50 ****
struct tm *utc;
time_t t;
t = time(NULL);
!               utc = gmtime(&t);
char dt[80];
!               strftime(dt, 80, "%d-%b-%Y %H:%M", utc);
                printf("%s,%f\n", dt, tempc);
                fflush(stdout);
--- 41,52 ----
                struct tm *utc;
                time_t t;
                t = time(NULL);
!               //utc = gmtime(&t);
!               utc = localtime(&t);
                char dt[80];
!               //strftime(dt, 80, "%d-%b-%Y %H:%M", utc);
!               strftime(dt, 80, "%Y-%m-%d,%H:%M:%S", utc);
                printf("%s,%f\n", dt, tempc);
                fflush(stdout);
  • 修正が終了したら、リビルドを実施
# make
gcc -Wall temper.c pcsensor.c -o temper -lusb

再度動作を確認

# ./temper
2013-10-19,11:41:29,26.732019

次はWebからグラフを表示できるようにしましょう。

定期測定

定期的に温度測定をするため、シェルスクリプトをcronに登録します。

  • スクリプト
====================== /opt/temper/record.sh ================================
#! /bin/sh
/usr/bin/tail -n 300 /opt/temper/data/temper.dat > /opt/temper/data/temper.tmp
/opt/temper/temper >> /opt/temper/data/temper.tmp
/bin/mv -f /opt/temper/data/temper.tmp /opt/temper/data/temper.dat

301エントリーを最大保持します。

  • cronへの登録
# temparature logging
*/15 * * * * /opt/temper/record.sh

問題発生

temperでは、以下の問題があります。

単一のセンサしか制御できません。(2個入りのTEMPerは2個まで表示可)

  1. 時々エラーが発生し、正常な読み取りができないばかりか、デバイスの挿抜が行われたかのようにデバイス番号まで変更されます。
  2. 温度補正ができません。
  3. 特定のデバイスを指定して測定ができません。

1.2.については、temper同様、リトライ、補正処理を実施することで改善されます。ただし、エラーの発生によりデバイス番号が変更されるため、特定のデバイスを指定することができません。デバイスID、ベンダIDは同一製品では同一になり、かつデバイスIDがエラーの度に変更されるため、バス番号でしか、識別できません。ホストに複数のバスが存在する方法ですが、これ以外に識別しようがありません。

今回、pcsensorのソースを元に、temperのリトライ機能とバス番号(ホストコントローラ番号)識別機能盛り込み使用します。

pcsensorの入手

# wget http://blog.osakana.net/sw/pcsensor/pcsensor-1.0.2.tar.gz
--2013-10-20 22:53:59--  http://blog.osakana.net/sw/pcsensor/pcsensor-1.0.2.tar.gz
blog.osakana.net をDNSに問いあわせています... 222.229.47.46
blog.osakana.net|222.229.47.46|:80 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 12668 (12K) [application/x-gzip]
`pcsensor-1.0.2.tar.gz' に保存中
100%[========================================================================================>] 12,668      --.-K/s 時間 0.03s
2013-10-20 22:54:00 (401 KB/s) - `pcsensor-1.0.2.tar.gz' へ保存完了 [12668/12668]
#

ソースの修正

  • ソースを修正します修正ポイントは以下のとおりです。
  1. オプション名の変更(省略可能な引数を許可しない)
  2. リトライの実施
  3. 補正情報をオフセットとスケールに修正
  4. 出力形式をtemperに整合
  5. バス番号指定による特定デバイス情報のみの表示
  • まず、一部の定義をヘッダファイルを作成し、移動します。

pcsensor.h

#define VENDOR_ID  0x0c45
#define PRODUCT_ID 0x7401
#define INTERFACE1 0x00
#define INTERFACE2 0x01
#define MAX_DEV 4
#define RETRY_CNT 3
void message_dump( char *mess, char *data, int len );
int exec_usb_detach( int dev, int iInterface );
int setup_libusb_access( void );
int find_lvr_winusb( void );
int ini_control_transfer( int dev_num );
int control_transfer( int dev_num, const char *pquestion );
int interrupt_transfer( int dev_num );
int interrupt_read( int dev_num );
int interrupt_read_temperatura( int dev_num, float *tempC );
int bulk_transfer( int dev_num );
int get_temparatura( int dev_num );

pcsensor.c

/***************************************************************************************
 * pcsensor.c by Juan Carlos Perez (c) 2011 (cray@isp-sl.com)
 * based on Temper.c by Robert Kavaler (c) 2009 (relavak.com)
 * All rights reserved.
 *
 * Temper driver for linux. This program can be compiled either as a library
 * or as a standalone program (-DUNIT_TEST). The driver will work with some
 * TEMPer usb devices from RDing (www.PCsensor.com).
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *       * Redistributions of source code must retain the above copyright
 *         notice, this list of conditions and the following disclaimer.
 *
 * THIS SOFTWARE IS PROVIDED BY Juan Carlos Perez ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Robert kavaler BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************************/
#include <usb.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include "log_ctrl.h"
#include "pcsensor.h"
#define VERSION "1.0.3"
const static int ReqIntLen=8;
const static int ReqBulkLen=8;
const static int Endpoint_Int_in=0x82;          // endpoint 0x81 address for IN
const static int Endpoint_Int_out=0x00;         // endpoint 1 address for OUT
const static int Endpoint_Bulk_in=0x82;         // endpoint 0x81 address for IN
const static int Endpoint_Bulk_out=0x00;        // endpoint 1 address for OUT
const static int Timeout=5000;                  // Timeout in ms
const static char UTemperatura[] = { 0x01, 0x80, 0x33, 0x01, 0x00, 0x00, 0x00, 0x00 };
const static char UIni1[] = { 0x01, 0x82, 0x77, 0x01, 0x00, 0x00, 0x00, 0x00 };
const static char UIni2[] = { 0x01, 0x86, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00 };
static int Debug=0;
static int Disp_format=0;
static int Mrtg=0;
static float Scale=1.0;
static float Offset=0.0;
static int Disp_dev_name=0;
static int Disp_dev_list=0;
static int Target_bus=-1;
static int Max_dev = MAX_DEV;
static struct usb_bus *Usb_busses;
static usb_dev_handle *Handles[MAX_DEV];
static char *Devlist_bus[MAX_DEV];
static char *Devlist_device[MAX_DEV];
//==================================================================
//  概要:
//      USB通信メッセージのダンプを行います。
//
//  引数:
//      char    *mess           メッセージデータ
//      int     len             データ長(バイト)
//
//  戻り値:
//      (なし)
//
//==================================================================
void message_dump( char *mess, char *data, int len )
{
        int  i;
        char str[256];
        char *p;
        p = str;
        sprintf( p, "%s, Message length:%d, Data:%02X", mess, len, data[0] & 0xff );
        p += strlen( str );
        for( i=1; i<len; i++ )
        {
                sprintf( p, " %02X", data[i] & 0xff );
                p += 3;
        }
        log_write( LOG_DEBUG, "%s", str );
}
//==================================================================
//  概要:
//      Kernelドライバにすでに組み込まれているデバイスを引き剥がします。
//
//  引数:
//      int dev_no              デバイス番号
//      int interface           インタフェース番号
//
//  戻り値:
//      int     0:              正常終了
//              -1:             エラー
//
//==================================================================
int exec_usb_detach( int dev_no, int interface )
{
        int ret;
        ret = usb_detach_kernel_driver_np( Handles[dev_no], interface );
        if( ret )
        {
                if( errno == ENODATA )
                {
                        log_write( LOG_DEBUG, "Device:[%d] was already detached", dev_no );
                }
                else
                {
                        log_write( LOG_DEBUG,
                                "Detach failed, Device:[%d], %s[%d]",
                                dev_no,
                                strerror(errno),
                                errno
                        );
                }
                return( -1 );
        }
        else
        {
                log_write( LOG_DEBUG,
                        "Device[%d]: detach successful",
                        dev_no
                );
                return( 0 );
        }
}
//==================================================================
//  概要:
//      libusbを初期化し対象のセンサを検出します。
//      検出したデバイスをオープンし、各デバイスのハンドル等を取得します。
//
//  引数:
//      (なし)
//
//  戻り値:
//      int                     検出したデバイス数
//
//  グローバル変数操作:
//      Handles[i]              handle;
//      Devlist_bus[i]          bus->dirname;
//      Devlist_device[i]       dev->filename;
//==================================================================
int find_usb_sensor_dev( void )
{
        struct usb_bus *bus;
        struct usb_device *dev;
        usb_dev_handle *handle;
        int i;
        memset( Handles, 0, sizeof(Handles) );
        i = 0;
        for( bus=Usb_busses; bus; bus=bus->next )
        {
                for( dev=bus->devices; dev; dev=dev->next )
                {
                        if( dev->descriptor.idVendor == VENDOR_ID &&
                                dev->descriptor.idProduct == PRODUCT_ID
                        )
                        {
                                log_write( LOG_DEBUG,
                                        "Found usb device with Vendor Id: %04X and Product Id: %04X assign Device [%d]",
                                        VENDOR_ID,
                                        PRODUCT_ID,
                                        i
                                );
                                if( !(handle = usb_open(dev)) )
                                {
                                        log_write( LOG_ERR,
                                                "Could not open USB device"
                                        );
                                        continue;
                                }
                                Handles[i] = handle;
                                Devlist_bus[i] = bus->dirname;
                                Devlist_device[i] = dev->filename;
                                i++;
                                if( i == MAX_DEV )
                                {
                                        break;
                                }
                        }
                }
        }
        return(i);
}
//==================================================================
//  概要:
//      対象のUSBデバイスをアクセスできるようにします。
//      (コンフィグレーション)
//
//  引数:
//      (なし)
//
//  戻り値:
//      int                     セットアップしたデバイス数
//==================================================================
int setup_libusb_access( void )
{
        int i = 0;
        if( Debug )
        {
                usb_set_debug( 255 );
        }
        else
        {
                usb_set_debug( 0 );
        }
        usb_init();
        usb_find_busses();
        usb_find_devices();
        Usb_busses = usb_get_busses();
        if( !find_usb_sensor_dev() )
        {
                log_write( LOG_ERR, "Couldn't find the USB device" );
                return(0);
        }
        for( i=0; Handles[i] != NULL && i < Max_dev; i++ )
        {
                if( usb_set_configuration(Handles[i], 0x01 ) < 0 )
                {
                        if( exec_usb_detach( i, INTERFACE1 ) )
                        {
                                if( usb_set_configuration(Handles[i], INTERFACE1 ) < 0 )
                                {
                                        log_write( LOG_ERR,
                                                "Could not set configuration 1 on device:[%d]",
                                                 i
                                        );
                                        return(0);
                                }
                        }
                }
                if( usb_claim_interface(Handles[i], INTERFACE1) < 0 )
                {
                        exec_usb_detach( i, INTERFACE1 );
                        if( usb_claim_interface(Handles[i], INTERFACE1) < 0 )
                        {
                                log_write( LOG_ERR,
                                        "Could not claim, device:%d, interface:%d",
                                        i,
                                        INTERFACE1
                                );
                                return(0);
                        }
                }
                if( usb_claim_interface(Handles[i], INTERFACE2) < 0 )
                {
                        exec_usb_detach( i, INTERFACE2 );
                        if( usb_claim_interface(Handles[i], INTERFACE2) < 0 )
                        {
                                log_write( LOG_ERR,
                                        "Could not claim, device:%d, interface:%d",
                                        i,
                                        INTERFACE2
                                );
                                return(0);
                        }
                }
        }
        return(i);
}
//==================================================================
//  概要:
//      デフォルトコントロールパイプへの転送のためのメッセージをセットアップをします。
//
//  引数:
//      int     dev_num         デバイス番号
//
//  戻り値:
//      int     0:              成功
//              -1:             失敗
//==================================================================
int ini_control_transfer( int dev_num )
{
        int r;
        char str[256];
        char question[] = { 0x01,0x01 };
        sprintf( str, "ini_control_transfer(), dev:%d ", dev_num );
        message_dump( str, question, 2 );
        r = usb_control_msg( Handles[dev_num], 0x21, 0x09, 0x0201, 0x00, (char *) question, 2, Timeout);
        if( r < 0 )
        {
                log_write( LOG_ERR,
                        "USB control write failed:%s[%d]",
                        strerror(errno),
                        errno
                );
                return( -1 );
        }
        return( 0 );
}
//==================================================================
//  概要:
//      コントロール転送のためのメッセージをセットアップをします。
//  引数:
//      int     dev_num         デバイス番号
//      char    *question       メッセージ
//  戻り値:
//      int     0:              成功
//              -1:             失敗
//==================================================================
int control_transfer( int dev_num, const char *pquestion )
{
        int r;
        char str[256];
        char question[ReqIntLen];
        memcpy( question, pquestion, sizeof(question) );
        sprintf( str, "control_transfer(), dev:%d ", dev_num );
        message_dump( str, question, sizeof( question ) );
        r = usb_control_msg( Handles[dev_num], 0x21, 0x09, 0x0200, 0x01, (char *) question, ReqIntLen, Timeout );
        if( r < 0 )
        {
                log_write( LOG_ERR,
                        "USB control write failed:%s[%d]",
                        strerror(errno),
                        errno
                );
                return( -1 );
        }
        return( 0 );
}
//==================================================================
//  概要:
//      インタラプト転送によりメッセージを転送します。
//  引数:
//      int     dev_num         デバイス番号
//  戻り値:
//      int     0:              成功
//              -1:             失敗
//==================================================================
int interrupt_transfer( int dev_num )
{
        int r;
        int i;
        char str[256];
        char answer[ReqIntLen];
        char question[ReqIntLen];
        for( i=0;i<ReqIntLen; i++ )
        {
                question[i]=i;
        }
        sprintf( str, "interrupt_transfer():write, dev:%d ", dev_num );
        message_dump( str, question, ReqIntLen );
        r = usb_interrupt_write( Handles[dev_num], Endpoint_Int_out, question, ReqIntLen, Timeout );
        if( r < 0 )
        {
                log_write( LOG_ERR,
                        "USB interrupt write failed:%s[%d]",
                        strerror(errno),
                        errno
                );
                return( -1 );
        }
        r = usb_interrupt_read( Handles[dev_num], Endpoint_Int_in, answer, ReqIntLen, Timeout );
        if( r != ReqIntLen )
        {
                log_write( LOG_ERR,
                        "USB interrupt read failed:%s[%d]",
                        strerror(errno),
                        errno
                );
                return( -1 );
        }
        sprintf( str, "interrupt_transfer():read, dev:%d ", dev_num );
        message_dump( str, answer, ReqIntLen );
        usb_release_interface( Handles[dev_num], 0 );
        return( 0 );
}
//==================================================================
//  概要:
//      インタラプト転送によりメッセージのリードを行います。
//
//  引数:
//      int     dev_num         デバイス番号
//
//  戻り値:
//      int     0:      成功
//              -1:     失敗
//==================================================================
int interrupt_read( int dev_num )
{
        int r;
        char str[256];
        char answer[ReqIntLen];
        bzero( answer, ReqIntLen );
        r = usb_interrupt_read( Handles[dev_num], 0x82, answer, ReqIntLen, Timeout );
        if( r != ReqIntLen )
        {
                log_write( LOG_ERR,
                        "USB interrupt read failed:%s[%d]",
                        strerror(errno),
                        errno
                );
                return( -1 );
        }
        sprintf( str, "interrupt_read():read, dev:%d ", dev_num );
        message_dump( str, answer, ReqIntLen );
        return(0);
}
//==================================================================
//  概要:
//      温度を読み取ります。
//
//  引数:
//      int     dev_num         デバイス番号
//      float   *tempC          摂氏による読み取り温度
//
//  戻り値:
//      int     0:      成功
//              -1:     失敗
//==================================================================
int interrupt_read_temperatura( int dev_num, float *tempC )
{
        int r;
        int temperature;
        char str[256];
        char answer[ReqIntLen];
        bzero( answer, ReqIntLen );
        r = usb_interrupt_read( Handles[dev_num], 0x82, answer, ReqIntLen, Timeout );
        if( r != ReqIntLen )
        {
                log_write( LOG_ERR,
                        "USB interrupt read failed:%s[%d]",
                        strerror(errno),
                        errno
                );
                return( -1 );
        }
        sprintf( str, "interrupt_read():read, dev:%d ", dev_num );
        message_dump( str, answer, ReqIntLen );
        temperature = (answer[3] & 0xFF) + ((signed char)answer[2] << 8);
        *tempC = temperature * (125.0 / 32000.0) * Scale + Offset;
        return(0);
}
//==================================================================
//  概要:
//      バルク転送を行います。
//
//  引数:
//      int     dev_num         デバイス番号
//
//  戻り値:
//      int     0:      成功
//              -1:     失敗
//==================================================================
int bulk_transfer( int dev_num )
{
        int r;
        char str[256];
        char answer[ReqBulkLen];
        log_write( LOG_DEBUG, "Data write(NULL)" );
        r = usb_bulk_write( Handles[dev_num], Endpoint_Bulk_out, NULL, 0, Timeout );
        if( r < 0 )
        {
                log_write( LOG_ERR,
                        "USB bulk write failed:%s[%d]",
                        strerror(errno),
                        errno
                );
                return( -1 );
        }
        r = usb_bulk_read( Handles[dev_num], Endpoint_Bulk_in, answer, ReqBulkLen, Timeout );
        if( r != ReqBulkLen )
        {
                log_write( LOG_ERR,
                        "USB bulk read failed:%s[%d]",
                        strerror(errno),
                        errno
                );
                return( -1 );
        }
        sprintf( str, "bulk_transfer():read, dev:%d ", dev_num );
        message_dump( str, answer, ReqIntLen );
        usb_release_interface( Handles[dev_num], 0 );
        return( 0 );
}
//==================================================================
//  概要:
//      温度情報の取得
//
//  引数:
//      int     dev_num デバイス番号
//
//  戻り値:
//      int     0:      成功
//              -1:     失敗
//              -2:     デバイスが存在しない
//
//==================================================================
int get_temparatura( int dev_num )
{
        int     ret;
        float   tempc;
        struct  tm *local;
        time_t  t;
        if( (ret = setup_libusb_access()) == 0 )
        {
                return( 0 );
        }
        if( Handles[dev_num] == NULL )
        {
                return( 0 );
        }
        if( Disp_dev_list )
        {
                printf( "Device:%i is Bus_id:%s Device_id:%s \n",
                        dev_num,
                        Devlist_bus[ dev_num ],
                        Devlist_device[ dev_num ]
                );
        }
        else if( atoi(Devlist_bus[dev_num]) == Target_bus || Target_bus == -1 )
        {
                if( ini_control_transfer(  dev_num  ) ) return( -1 );
                if( control_transfer(  dev_num , UTemperatura ) ) return( -1 );
                if( interrupt_read(  dev_num  ) ) return( -1 );
                if( control_transfer(  dev_num , UIni1 ) ) return( -1 );
                if( interrupt_read(  dev_num  ) ) return( -1 );
                if( control_transfer(  dev_num , UIni2 ) ) return( -1 );
                if( interrupt_read(  dev_num  ) ) return( -1 );
                if( interrupt_read(  dev_num  ) ) return( -1 );
                if( control_transfer(  dev_num , UTemperatura ) ) return( -1 );
                if( interrupt_read_temperatura(  dev_num , &tempc ) ) return( -1 );
                t = time( NULL );
                local = localtime( &t );
                if( Mrtg )
                {
                        if( Disp_format & 0x02 )
                        {
                                printf( "%.4f\n", (9.0 / 5.0 * tempc + 32.0) );
                                printf( "%.4f\n", (9.0 / 5.0 * tempc + 32.0) );
                        }
                        else
                        {
                                printf( "%.4f\n", tempc );
                                printf( "%.4f\n", tempc );
                        }
                        printf( "%02d:%02d\n",
                                local->tm_hour,
                                local->tm_min
                        );
                        printf( "pcsensor\n" );
                }
                else
                {
                        printf( "%04d/%02d/%02d,%02d:%02d:%02d",
                                local->tm_year +1900,
                                local->tm_mon + 1,
                                local->tm_mday,
                                local->tm_hour,
                                local->tm_min,
                                local->tm_sec
                        );
                        if( Disp_dev_name > 0 )
                        {
                                printf( ",Bus:%s/Device:%s", Devlist_bus[ dev_num ], Devlist_device[ dev_num ] );
                        }
                        // printf( "Temperature" );
                        if( Disp_format == 0 )
                        {
                                printf( ",%.4fF", (9.0 / 5.0 * tempc + 32.0) );
                        }
                        if( Disp_format & 0x02 )
                        {
                                printf( ",%.4f", (9.0 / 5.0 * tempc + 32.0) );
                        }
                        if( Disp_format == 0 )
                        {
                                printf( ",%.4fC", tempc );
                        }
                        if( Disp_format & 0x01 )
                        {
                                printf( ",%.4f", tempc );
                        }
                        printf( "\n" );
                }
                usb_release_interface( Handles[ dev_num ], INTERFACE1 );
                usb_release_interface( Handles[ dev_num ], INTERFACE2 );
                usb_close( Handles[ dev_num ] );
        }
        return( 0 );
}
//==================================================================
//  概要:
//      メイン関数
//  引数:
//      int argc                コマンドラインの引数の数
//      char **argv             引数リスト
//  戻り値:
//      int                     実行結果
//
//==================================================================
int main( int argc, char **argv )
{
        int     c;
        int     i;
        int     ret;
        char    *tok;
        char    arg[256];
        strcpy( arg, argv[0] );
        for( i=1; i<argc; i++ )
        {
                strcat( arg, " " );
                strcat( arg, argv[i] );
        }
        log_write( LOG_INFO, "command = %s", arg );
        memset( Handles, 0, sizeof(Handles) );
        while ( (c = getopt (argc, argv, "mfcvhla:dB:")) != -1 )
        switch ( c )
        {
        case 'v':
                // 詳細な情報を表示します。
                Debug = 1;
                log_write( LOG_DEBUG, "option -v" );
                break;
        case 'c':
                // 摂氏表示を行います。
                Disp_format |= 0x01;    //Celsius
                log_write( LOG_DEBUG, "option -c" );
                break;
        case 'f':
                // 華氏表示を行います。
                Disp_format |= 0x02;    //Fahrenheit
                log_write( LOG_DEBUG, "option -f" );
                break;
        case 'm':
                // MRTG用のデータ形式を出力します。
                Mrtg=1;
                log_write( LOG_DEBUG, "option -m" );
                break;
        case 'd':
                // デバイスのバス/デバイスIDを含め表示します。
                Disp_dev_name++;
                log_write( LOG_DEBUG, "option -d" );
                break;
        case 'l':
                // デバイス一覧を表示する
                Disp_dev_list++;
                log_write( LOG_DEBUG, "option -l" );
                break;
        case 'B':
                // 対象のバスを指定します。
                if( (tok = strtok( optarg, "," )) == NULL )
                {
                        log_write( LOG_ERR, "option -B, Invalid option(0): %s", optarg );
                        exit( 1 );
                }
                Target_bus = atoi( tok );
                log_write( LOG_DEBUG, "option -B, target=%d", Target_bus );
                break;
        case 'a':
                // 温度補正データの設定をします。
                if( (tok = strtok( optarg, "," )) == NULL )
                {
                        log_write( LOG_ERR, "option -a, Invalid option(0): %s", optarg );
                        exit( 1 );
                }
                Scale = atof( tok );
                if( (tok = strtok( NULL, "," )) == NULL )
                {
                        log_write( LOG_ERR, "option -a, Invalid option(1): %s", optarg );
                        exit( 1 );
                }
                Offset = atof( tok );
                log_write( LOG_DEBUG, "option -a, scale=%f, offset=%f", Scale, Offset );
                break;
        case '?':
        case 'h':
                printf( "pcsensor version %s\n", VERSION );
                printf( "  Aviable options:\n ");
                printf( "   -h    help\n" );
                printf( "   -v    verbose\n" );
                printf( "   -c    output only in Celsius\n" );
                printf( "   -f    output only in Fahrenheit\n" );
                printf( "   -a <Scale>,<Offset>\n" );
                printf( "         compesate temparature: temp = (temp * Scale) - Offset\n" );
                printf( "   -m    output for Mrtg integration\n" );
                printf( "   -d    output with Bus and Device number\n" );
                printf( "   -l    display device list\n" );
                printf( "   -B <n>\n" );
                printf( "         specific bus number\n" );
                exit( 1 );
        default:
                if( isprint (optopt) )
                {
                        log_write( LOG_ERR, "Unknown option -%c", optopt );
                        fprintf( stderr, "Unknown option -%c\n", optopt );
                }
                else
                {
                        log_write( LOG_ERR, "Unknown option character %02X", optopt );
                        fprintf (stderr, "Unknown option character %02X.\n", optopt );
                }
                exit( 1 );
        }
        if( optind < argc )
        {
                log_write( LOG_ERR, "Non-option ARGV-elements" );
                fprintf( stderr, "Non-option ARGV-elements, try -h for help.\n" );
                exit( 1 );
        }
        for( i=0; i < Max_dev; i++ )
        {
                int retry = RETRY_CNT;
                while( retry )
                {
                        ret = get_temparatura( i );
                        if( ret ==  0 ) break;
                        sleep(3);
                        if( retry-- == 1)
                        {
                                log_write( LOG_ERR, "Unrecoverable error was detected." );
                                exit(1);
                        }
                }
        }
        exit(0);
}
  • log_ctrl.h
/********************************************************************************
 **                     Logging tool
 **     log_ctrl:
 **             General purpose logging tools for C language.
 **
 **
 *******************************************************************************/
#ifndef _LOG_CTRL_H
#define _LOG_CTRL_H 1
#ifndef _SYS_SYSLOG_H
#include <syslog.h>
#endif
/*****************************************************************************
 **     MACROS
 *****************************************************************************/
// ==========================================================================
//      log_write( LOG_LEVEL, <function_name>, <messages(format,arguments....)>
//
// ==========================================================================
#define log_write( level, fmt, arg... ) log_write_sub( level, "%s;%d;[%s()] " fmt, __FILE__, __LINE__, __FUNCTION__, ##arg )
/*****************************************************************************
 **     Define structs
 *****************************************************************************/
typedef struct _log_code
{
        char    *name;
        int     val;
} LOG_CODE;
/*****************************************************************************
 **     for reference only
 *****************************************************************************/
/* ============ Log Priorities (these are ordered) ============
#define LOG_EMERG       0       system is unusable
#define LOG_ALERT       1       action must be taken immediately
#define LOG_CRIT        2       critical conditions
#define LOG_ERR         3       error conditions
#define LOG_WARNING     4       warning conditions
#define LOG_NOTICE      5       normal but signification condition
#define LOG_INFO        6       informational
#define LOG_DEBUG       7       debug-level messages
 ============================================================== */
/* ============ Log Facilityes (these are ordered) ============
#define LOG_KERN        (0<<3)  kernel messages
#define LOG_USER        (1<<3)  random user-level messages
#define LOG_MAIL        (2<<3)  mail system
#define LOG_DAEMON      (3<<3)  system daemons
#define LOG_AUTH        (4<<3)  security/authorization messages
#define LOG_SYSLOG      (5<<3)  messages generated internally by syslogd
#define LOG_LPR         (6<<3)  line printer subsystem
#define LOG_NEWS        (7<<3)  network news subsystem
#define LOG_UUCP        (8<<3)  UUCP subsystem
#define LOG_CRON        (9<<3)  UUCP subsystem
#define LOG_AUTHPRIV    (10<<3) UUCP subsystem
#define LOG_FTP         (11<<3) UUCP subsystem
#define LOG_NTP         (12<<3) UUCP subsystem
#define LOG_AUDIT       (13<<3) UUCP subsystem
#define LOG_ALERT       (14<<3) UUCP subsystem
#define LOG_CLOCK       (15<<3) UUCP subsystem
#define LOG_LOCAL0      (16<<3) reserved for local use
#define LOG_LOCAL1      (17<<3) reserved for local use
#define LOG_LOCAL2      (18<<3) reserved for local use
#define LOG_LOCAL3      (19<<3) reserved for local use
#define LOG_LOCAL4      (20<<3) reserved for local use
#define LOG_LOCAL5      (21<<3) reserved for local use
#define LOG_LOCAL6      (22<<3) reserved for local use
#define LOG_LOCAL7      (23<<3) reserved for local use
 ============================================================== */
/*****************************************************************************
 **     Prototype definitions
 *****************************************************************************/
int  get_log_facility( char *facility_name );
void set_log_facility( int code );
void log_write_sub( int level, const char* format, ... );
// =========================================== End of codes ======================================================
#endif
  • log_ctrl.c
/********************************************************************************
 **                     Logging tool
 **     log_ctrl:
 **             General purpose logging tools for C language.
 **
 **
 *******************************************************************************/
/*****************************************************************************
 **     includes header files
 *****************************************************************************/
#define _GNU_SOURCE
#include <string.h>
#include <syslog.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include "log_ctrl.h"
/*****************************************************************************
 **     Global viables but only use in Logging tool
 *****************************************************************************/
static char *LOG_LEVEL_STR[] =
{
        "EMERG",
        "ALERT",
        "CRIT",
        "ERR",
        "WARNING",
        "NOTICE",
        "INFO",
        "DEBUG"
};
// ==========================================================================
//      Log facility code is permitted following codes.
//              subset of RFC 3164
// ==========================================================================
LOG_CODE FACILITY_NAMES[] =
{
        { "cron",       LOG_CRON },
        { "daemon",     LOG_DAEMON },
        { "user",       LOG_USER },
        { "local0",     LOG_LOCAL0 },
        { "local1",     LOG_LOCAL1 },
        { "local2",     LOG_LOCAL2 },
        { "local3",     LOG_LOCAL3 },
        { "local4",     LOG_LOCAL4 },
        { "local5",     LOG_LOCAL5 },
        { "local6",     LOG_LOCAL6 },
        { "local7",     LOG_LOCAL7 },
        { NULL,         -1 }
};
int LOG_facility_code = LOG_USER;       // default facility
/*****************************************************************************
 **
 ** [Name]
 **     get_log_facility
 ** [Description]
 **     get logfacility code
 ** [Arguments]
 **     char *facility_name     facility name
 ** [Return Value]
 **     int LOG_facility_code   -1:error,other:facility code
 **
 *****************************************************************************/
int get_log_facility( char *facility_name )
{
        char *str;
        int i = 0;
        int code = -1;
        while( (str = FACILITY_NAMES[i].name) != NULL )
        {
                if( strcmp( str, facility_name ) == 0 )
                {
                        code = FACILITY_NAMES[i].val;
                        break;
                }
                i++;
        }
        return( code );
}
/*****************************************************************************
 **
 ** [Name]
 **     set_log_facility
 ** [Description]
 **     set logfacility code
 ** [Arguments]
 **     char *facility_name     facility name
 ** [Return Value]
 **     int LOG_facility_code   -1:error,other:facility code
 **
 *****************************************************************************/
void set_log_facility( int code )
{
        LOG_facility_code = code;
}
/*****************************************************************************
 **
 ** [Name]
 **     log_write
 ** [Description]
 **     writes messages in logfile.
 ** [Arguments]
 **     int level                       log level
 **     const char* format, ...         printf like format
 ** [Return Value]
 **     none
 **
 *****************************************************************************/
void log_write_sub( int level, const char* format, ... )
{
        char *buff;
        int size;
        va_list ap;
        va_start(ap, format);
        size = vasprintf( &buff, format, ap );
        va_end( ap );
        openlog( "pcsensor", LOG_PID, LOG_MAKEPRI(LOG_facility_code, LOG_DEBUG) );
        syslog( level, "%s;%s\n", LOG_LEVEL_STR[level], buff );
        closelog();
        free( buff );
}
// =========================================== End of codes ======================================================
  • Makefileも修正します。
all:    pcsensor
CC     = gcc
CFLAGS = -O2 -Wall -g
pcsensor:       pcsensor.o log_ctrl.o -lusb
        ${CC} ${CFLAGS} -lusb -o pcsensor pcsensor.o log_ctrl.o
pcsensor.o:     pcsensor.c pcsensor.h
        ${CC}  ${CFLAGS} -DUNIT_TEST -c -o pcsensor.o pcsensor.c
log_ctrl.o:     log_ctrl.c log_ctrl.h
        ${CC} ${CFLAGS} -c -o log_ctrl.o log_ctrl.c
clean:
        rm -f pcsensor *.o
rules-install:                  # must be superuser to do this
        cp 99-tempsensor.rules /etc/udev/rules.d

ビルド

  • Makeします。
# make
gcc  -O2 -Wall -g -DUNIT_TEST -c -o pcsensor.o pcsensor.c
gcc -O2 -Wall -g -c -o log_ctrl.o log_ctrl.c
gcc -O2 -Wall -g -lusb -o pcsensor pcsensor.o log_ctrl.o
#

動作確認

  • デバイスリストを表示します。
# ./pcsensor -l
Device:0 is Bus_id:004 Device_id:100
Device:1 is Bus_id:005 Device_id:003
#
  • バス番号を指定します。
# ./pcsensor -c -B 4
2013/10/25,23:07:33,22.6250
#

以上で先のtemperと互換になりました。

Webで時系列温度表示

グラフ表示の方法

Webでグラフを表示するには、既存のSNMPマネージャに統合して表示する方法が一般的のようですが、

  • 今後のWebアプリケーションの勉強のために、Javascriptsで記述。
  • スタンドアロンで動作する(GoogleのAPI等は使用しない)

という方針で作成します。ネットでしらべましたが、「highcharts」が「簡単」、「きれい」という点でよさそうなので使用します

highchartsの入手

  • highcharts のページよりダウンロードします。非商用用途ではCCライセンスが適用されます。
    商用利用では有料です。
  • ダウンロードページよりダウンロードします。表示されるダウンロードボタンをクリックするとZIPファイルがダウンロードできます。
  • ファイルを解凍すると以下のように展開されます。
# ll
合計 12824
-rw-r--r--  1 vod users 13080205 10月 19 18:16 Highcharts-3.0.6.zip
drwxr-xr-x 59 vod users     4096 10月  4 15:24 examples
drwxr-xr-x  5 vod users     4096 10月  4 15:24 exporting-server
drwxr-xr-x  2 vod users     4096 10月  4 15:24 gfx
drwxr-xr-x  2 vod users     4096 10月  4 15:24 graphics
-rw-r--r--  1 vod users     4989 10月  4 15:24 index.htm
drwxr-xr-x  5 vod users     4096 10月  4 15:24 js
  • このうち使用するのは
./exporting-server/phantomjs/jquery.1.9.1.min.js
./exporting-server/phantomjs/highcharts.js

の2個のファイルです。

コード

では、早速試験コードです。ファイルアクセスはPHPで行っています。

====================== index.php ================================
<?php
    ini_set( 'display_errors', 1 );    // エラー出力する場合
    ini_set( 'error_reporting', E_ALL | E_STRICT );
    $Temp_data = array (
                array( "Server Room",   "/opt/temper/data/temper2.dat", 1.0287, 0.0 ),
                array( "External Air",  "/opt/temper/data/temper.dat",  1.0287, 0.0 )
    );
?>
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="./js/jquery.min.js"></script>
<script type="text/javascript" src="./js/highcharts.js"></script>
<script type="text/javascript">
    var chart;
    var options =
    {
    // 以下によりグラフのオプションを設定
        // グラフ全体の設定
        chart:
        {
            renderTo: 'container',        // (String)グラフを描写するHTMLの要素をIDで指定 します。
            defaultSeriesType: 'line',    // デフォルトのグラフタイプの設定
                                          //     line:    折れ線グラフ
                                          //     area:    折れ線の下ぬりつぶし
                                          //     spline:    曲線
                                          //     column:棒グラフ・縦
                                          //     bar:    棒グラフ・横
                                          //     pie:    円グラフ
                                          //     scatter:散布図
            // backgroundColor: null,     // (String)全体の背景色
            borderWidth: 1                // (Number)グラフの枠線を指定します。1 以上の数 値を指定すると、
                                          //     グラフ全体を囲む枠線が現れます。
            // borderRadius: ????,        // (Number)カドの丸まり具合を指定出来ます。
            // borderColor: '#ffffff',    // (String)グラフの枠線の色を指定します。
            // plotBackgroundColor: null  // (String)プロット部分の背景色を指定します。
            // inverted:                  // (Boolean)true と指定すると、横軸がy軸、縦軸がx軸になります。
            // zoomType:                  // (String)zoomType: "x" とすると、x軸をマウスで拡大可能になります。
                                          //     ”x”の他に”y”,”xy”とも指定できます。
        },
        // タイトルの設定
        title:
        {
            text: '温度グラフ',    // (String)タイトルの文字列を指定します。
            align: 'center'        // (String)タイトルの位置を指定します。
                                   //    左寄せ:    “left”
                                   //     中央揃え:        “center”
                                   //     右寄せ:        “right”
            //style:               // (CSS Object)タイトルの細かいスタイルを指定します。
                                   //     ただし位置揃えは text-align を使わずに
                                   //     上に書いたalignを使った方が良いとのことです。
            //floating:            // (Boolena)true にするとタイトルがプロット部分に入り込みます。
        },
        // サブタイトルの設定
        subtitle:
        {
            text: '(試行中)',
            align: 'center'
        },
        // 凡例の設定
        legend:
        {
            layout: 'vertical',    // (String)複数の系列を表示する際の表示方向を指定します。
                                   //    タテに並べる:vertical
                                   //     ヨコに並べる:horizontal
            align: 'right',        // (String)凡例の位置を指定します。
                                   //     left
                                   //     center
                                   //     right
            verticalAlign: 'top',  //
            // reversed            // (Boolean)true にすると系列の表示順が逆になります。
                                   //    もっと複雑に指定したい場合は、データ系列 series  の
                                   //    legendIndex を指定します。
            // borderWidth         // (Number) 枠線の太さを指定します。
            // borderColor         // (String) 枠線の色を指定します。
            // borderRadius        // (Number) 枠線の角まるめを指定します。
            // backgroundColor     // (String) 枠線の背景色を指定します。
            floating: true         // (Boolena)true にするとタイトルがプロット部分に入り込みます。
        },
/*
        // グラフにマウスオーバーすると出てくるポップアップの表示設定
        tooltip:
        {
            formatter: function()
            {
                return( this.y + '℃' );
            },
            enabled:true
        },
*/
        // X軸
        xAxis:
        {
            type: 'datetime',    // (String) 軸の種類を指定します。
                                 //    linear:        (デフォルト)
                                 //    datetime:     時系列
                                 //    logarithmic     対数目盛
            title:
            {
                text: 'date/time'    // (String) 軸のタイトルを指定します。
            }
            // max                   // (Number) 軸の最大値を指定します。
            // minx                  // (Number) 軸の最小値を指定します。
            // lineWidth             // (Number) 軸の太さを指定します。
            // lineColor             // (String) 軸の太さを指定します。
            //allowDecimals: false,  // (Boolean) グラフの目盛りに小数を含む値を使うことを許可するかを設定します。
            // categories            // (Array) 軸の目盛りを、数値ではなく「月曜日、火曜日...」などの
                                     //    指定形式にします。String型の配列を指定します。
        },
        // Y軸
        yAxis:
        {
            type: 'linear',
            title:
            {
                text: '℃'
                // style:
                // {
                //    color: '#4572A7'
                // }
            },
            //labels:
            //{
            //    formatter: function()
            //    {
            //        return( this.value / 10 + "0");
            //    }
            //},
            max: 30,
            min: 10,
            allowDecimals: false
        },
        // データ系列
        series:
        [
        <?php
            for( $i=0; $i<count($Temp_data); $i++ )
            //for( $i=0; $i<2; $i++ )
            {
                if( $i == 0 )
                {
                    echo "    {\n";
                }
                else
                {
                    echo "            ,\n";
                    echo "            {\n";
                }
                echo "                name: '" . $Temp_data[$i][0] . "',\n";    // 名前を 設定
                echo "                // color: '#000000',\n";        // 色の設定
                echo "                type: 'line',\n";               // グラフのタイプを 指定します。
                echo "                data: [\n";
                $handle = fopen( $Temp_data[$i][1], "r" );
                if( ($line_data = fgetcsv( $handle, 0, "," )) != FALSE )
                {
                    $plot_date = explode( '/', $line_data[0] );
                    $plot_time = explode( ':', $line_data[1] );
                    $temp = $line_data[2] * $Temp_data[$i][2] + $Temp_data[$i][3];
                    echo "                    [Date.UTC( " . $plot_date[0] . ", " . $plot_date[1] . " - 1, " . $plot_date[2] . ", ";
                    echo $plot_time[0] . ", " . $plot_time[1] . ", " . $plot_time[2] . " ), ";
                    echo $temp . ']';
                }
                while( ($line_data = fgetcsv( $handle, 0, "," )) != FALSE )
                {
                    $plot_date = explode( '/', $line_data[0] );
                    $plot_time = explode( ':', $line_data[1] );
                    $temp = $line_data[2] * $Temp_data[$i][2] + $Temp_data[$i][3];
                    echo ", [Date.UTC( " . $plot_date[0] . ", " . $plot_date[1] . " - 1, " . $plot_date[2] . ", ";
                    echo                 $plot_time[0] . ", " . $plot_time[1] . ", " . $plot_time[2] . " ), ";
                    echo                 $temp . ']';
                }
                echo "\n";
                echo "                ]\n";
                echo "            }\n";
                fclose( $handle );
            }
        ?>
        ]
    };
    // jQueryのコードはDocumentがReady状態になったタイミングで実行させます。
    // 通常よく使われるbodyのonloadなどでは、ページ上の画像などもロードされ、
    // レンダリングもされた状態で動作し始めるために、ユーザのオペレーションが
    // 開始されてしまう可能性があります。
    // 逆にheadやbodyの中でパース時に実行させてしまっては、DOMが利用可能な
    // 状態になっていない場合があるため、思わぬ不具合を発生させる要因になります。
    $(document).ready(
        function ()
        {
            chart = new Highcharts.Chart(options);
        }
    );
</script>
<title>Temper</title>
</head>
<body>
    <h1 align="center">温度監視</h1>
    <div id="container" style="width:1000px; height:400px; margin:0 auto"></div>
</body>
</html>

表示例

Temper


添付ファイル: fileTemper.JPG 1185件 [詳細]

トップ   編集 編集(GUI) 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-03-11 (土) 19:37:04 (2595d)