温湿度管理を自動化する「温湿度ロガー」
しまねソフト研究開発センターでは、IoT分野における先端技術支援や研究活動の一環として、mruby/cを使ったIoTデバイスの開発・製作を行っております。
このページでは、労働安全衛生や品質管理の観点で温湿度管理が必要とされる環境で、手書きでの温湿度管理と記録を自動化するニーズに対して、簡易にデータを計測・収集するための温湿度ロガーの開発を行いましたので、その取組事例をご紹介いたします。
目次
1.概要 |
2.内容 |
2-1.ハードウェア |
2-2.ソフトウェア |
2-3.稼働試験 |
2-4.試験結果 |
2-5.基盤回路図 |
2-6.ドキュメント |
2-7.サンプルコード |
2-8.注意事項 |
2-9.執筆者紹介 |
3.お問い合わせ先 |
概要
目的
- 労働安全衛生や品質管理の観点で温湿度管理が必要とされる場所(工場・ビル、倉庫・保管庫など)で、温度や湿度の管理と記録が必要とされる場所は少なくありません。
- しかしながら、労働環境や作業現場の温度・湿度の測定と記録を、管理者が紙の手書き作業で行う等の労力を要するとともに、温湿度の記録に測定し忘れや記入漏れ等の人的ミスが生じるといった課題があります。
- そこで、上記の課題を解決するため、より簡単かつ低コストで様々な労働環境・作業現場の温湿度管理を自動化する温湿度ロガーを開発しました。
こんな方にオススメ
- 温湿度の測定・記録を、紙で手書きではなくデータで自動測定・記録を行いたい方
- 温湿度ロガーでのデータ測定を、測定環境や場所に合わせて測定頻度を柔軟に変更したい方
- 温湿度ロガーで計測した環境データを、自社管理・運用するシステムに連携させたい方
- ラダー言語ではなく高級言語(Ruby)を用いたプログラミングに興味がある方
温湿度ロガーについて
- 今回、mruby/cを標準搭載するマイコンボード「RBoard」に温湿度センサーを接続し、同センサーによって温度・湿度のデータを測定して、一定時間ごとにWi-Fi経由でサーバへ送信するものです。
- これにより、様々な労働環境や作業現場において、温度・湿度の環境データを自動記録(ロギング)するとともに、計測頻度を増やせば環境の変化を細かく・早期に把握できます。
- なお、この「温湿度ロガー」を活用することで、予め設定した温度・湿度の異常を通知するIoTシステムの構築にも寄与します(※別途、システムが必要となります)。
内容
今回、東裕人専門研究員の執筆によって、温湿度ロガーの開発におけるハードウェアおよびソフトウェア、稼働試験と結果、基板回路図に関するドキュメントを公開いたします。併せて、サンプルコードを公開しておりますので、予め注意事項をご確認の上、参照ください。
ハードウェア
全体の構成は、温湿度センサーで取得した測定値を一定時間ごとにWi-Fi経由でサーバへ送信するものとする。温湿度センサーは、Grove規格で接続が可能な製品の中から、比較的測定確度が良いと思われるセンシリオン社製の温湿度センサーを搭載した、SeeedStudio社製101020592「Grove - I2C High Accuracy Temp&Humi Sensor(SHT35)」を選定した(https://www.seeedstudio.com/Grove-I2C-High-Accuracy-Temp-Humi-Sensor-SHT35.html)。
センサーは、外気温に直接触れる事が重要なため、ケース外へ取付している。また、温湿度センサーは経年劣化が早い可能性があるので、センサー部分のみの交換容易性を考えて、ケース外取付かつコネクタでの接続としている。
▼センサー仕様抜粋 (いずれも代表値)
温度 | ±0.1°C |
湿度 | ±1.5%RH |
Wi-Fiモジュールは、Grove規格で接続が可能な製品の中から、入手性がよくスイッチサイエンスにて購入可能なCRESCENT-014を選定した(https://www.switch-science.com/catalog/5795/)。ただし、後述するとおり、開発時はケーブル一本で接続して非常に簡易に作業ができたが、基板の固定ができないため、最終成果物では半田付けとしている。
ソフトウェア
本センサーは、I2Cバスを使って、測定値をデジタル値で直接取得できるため、mruby/c標準のI2Cクラスを使ってプログラミングが可能である。
【データ取得部抜粋】
ADRS_SHT35 = 0x45
$i2c = I2C.new()
$i2c.write( ADRS_SHT35, 0x2c, 0x06 )
sleep_ms( 12 )
res = $i2c.read( ADRS_SHT35, 6 )
サーバーへのデータ送信は、http post プロトコルでREST-APIで受け付けるサーバーに対してデータ送信を行う事を想定した。
【送信例】
URL
http://api.example.jp/datalogger.rb?ctrl=temp_humi&action=post"
POST DATA
{"type":"TempHumi", "mac_address":"4C:55:CC:18:43:BD",
"temperature":20.5, "humidity":48.3}
稼働結果
約1分に一度のデータ送信、18650セルを使ったバッテリー駆動という条件のもとで、約11時間稼働した。
基盤回路図
Wi-Fiモジュールの固定のため、Arduino用ユニバーサル基板を使い、半田付けでWi-Fiモジュールの取付を行っている。センサーモジュールは、Groveケーブルを使ってRBoardのI2Cコネクタと接続する。以下に回路図を示す。
ドキュメント
この温湿度ロガー開発に関するドキュメントは、以下のPDFファイルでご覧いただけます。
併せて、温湿度ロガー開発で用いたパーツリストをご覧いただけます。
「ドキュメント(温湿度ロガー開発について)」をダウンロードする(PDF:766kB)
「パーツリスト(温湿度ロガー開発について)」をダウンロードする(PDF:161kB)
サンプルコード
main.rb
# coding: utf-8
#
# Sensirion SHT35 温湿度センサを使った温湿度ロガーサンプル
#
# Copyright (C) 2022 Shimane IT Open-Innovation Center.
#
# This file is distributed under BSD 3-Clause License.
#
POST_URL = "http://example.jp/cgi-bin/datalogger.rb?ctrl=temphumi&action=post"
ADRS_SHT35 = 0x45
SLEEP_TIME = 60 # 一回測定ごとにスリープする時間 (秒)
GPIO_WIFI_RESET = 2 # WiFi reset pin に繋いだ GPIO Pin 番号
def to_uint16( b1, b2 )
return (b1 << 8 | b2)
end
def crc8(data)
crc = 0xff
data.each_byte {|b|
crc ^= b
8.times {
crc <<= 1
crc ^= 0x31 if crc > 0xff
crc &= 0xff
}
}
return crc
end
##
# SHT35 initialize
#
#@return [Hash] Device identify result.
#
def sht35_init()
data = {}
$i2c.write( ADRS_SHT35, 0x30, 0xa2 ) # Reset
res = $i2c.read( ADRS_SHT35, 3, 0xf3, 0x2d ) # Read Status
if crc8(res[0,2]) != res.getbyte(2)
data[:init] = :ERROR
else
data[:init] = :OK
end
return data
end
##
# SHT35 measure
#
#@param [Hash] data container
#@return [Hash] data container
#@return [Nil] error.
#
def sht35_meas( data = {} )
return nil if data[:init] == :ERROR
$i2c.write( ADRS_SHT35, 0x2c, 0x06 )
sleep_ms( 12 )
res = $i2c.read( ADRS_SHT35, 6 )
# check CRC
s2 = ""
res.each_byte {|byte|
if s2.length == 2
return nil if crc8( s2 ) != byte
s2 = ""
else
s2 << byte
end
}
st = to_uint16( res.getbyte(0), res.getbyte(1) ).to_f
srh = to_uint16( res.getbyte(3), res.getbyte(4) ).to_f
data[:temperature] = -45 + 175 * st / 65535
data[:humidity] = 100 * srh / 65535
return data
end
#
# send data to server
#
def send_data( data )
if !$wifi.wait_for_connect()
wifi_hard_reset()
return nil
end
res = $wifi.http_post_json( POST_URL, data )
$wifi.reboot() if !res
return res
end
#
# WiFiモジュールのハードウェアリセット
#
def wifi_hard_reset()
$wifi_reset.write( 0 )
sleep 1
$wifi_reset.write( 1 )
end
#
# main
#
sleep 2
puts "START PROGRAM"
$i2c = I2C.new()
$wifi_reset = GPIO.new( GPIO_WIFI_RESET )
$wifi_reset.setmode( 0 )
$wifi_reset.write( 1 )
$wifi = AMW037.new( UART.new( 115200 ))
#
# WiFi module setup
#
#$wifi.setup_module("SSID", "PASSWORD"); $wifi.reboot; sleep 1000
while !(mac_address = $wifi.mac_address())
puts "WiFi mac address error."
wifi_hard_reset()
end
puts "WiFi mac address is #{mac_address}"
sht35 = sht35_init()
if sht35[:init] != :OK
puts "Sensor not detected."
return
end
puts "Sensor SHT35 detected."
send_count = 1
while true
next if !sht35_meas( sht35 )
printf("Temperature:%5.1f C Humidity:%3.0f %%\n",
sht35[:temperature], sht35[:humidity] )
data = {:type => "TempHumi",
:mac_address => mac_address,
:temperature => sht35[:temperature],
:humidity => sht35[:humidity],
:count=>send_count }
if send_data( data )
send_count += 1
puts "Data send OK."
else
puts "ERROR: Data send failed."
end
sleep SLEEP_TIME
end
amw037.rb
# coding: utf-8
#
# Silicon Labs WiFi module AMW037 control class.
#
# Copyright (C) 2015-2022 Shimane IT Open-Innovation Center.
#
# This file is distributed under BSD 3-Clause License.
#
##
# debug print
#
def dp( s )
puts s
end
class Hash
def to_json_tiny
ret = "{"
flag_comma = false
self.each {|k,v|
ret << ", " if flag_comma
ret << %!"#{k}":#{v.inspect}!
flag_comma = true
}
ret << "}"
end
end
##
# AMW037 Class
#
class AMW037
attr_reader :is_read_timeout
##
# constructor
#
def initialize( node = nil )
@rfm = node || UART.new( 1 )
clear_buffer()
end
##
# clear tx/rx buffer
#
def clear_buffer()
@rfm.clear_tx_buffer()
@rfm.clear_rx_buffer()
@is_read_timeout = false
end
##
# send any command
#
def command( cmd )
dp ">>>SEND \"#{cmd}\""
@rfm.write("#{cmd}\r\n")
end
##
# get string with timeout
#
def get_string( timeout = 1000 )
@is_read_timeout = false
cnt = 0
timeout /= 10
while cnt < timeout
txt = @rfm.gets()
return txt if txt
cnt += 1
sleep_ms 10
end
@is_read_timeout = true
return nil
end
##
# chat
#
#@param [String] send message.
#@param [Integer] timeout timeout (ms)
#@return [String] result message
#@return [nil] seaquence error.
#@return [false] status code error.
#
def chat( send, timeout = 1000 )
if send
dp ">>>C:SEND \"#{send}\""
@rfm.write("#{send}\r\n")
end
# (see)
# https://docs.zentri.com/zentrios/wl/latest/serial-interface#response-format
@res_code = get_string( timeout )
dp "<<<C:RES #{@res_code.inspect}"
return nil if !@res_code || @res_code[0] != "R"
@res_code.chomp!
len = @res_code[2,5].to_i
@res_text = nil
cnt = 0
if len > 0
while (cnt += 1) < 100
@res_text = @rfm.read( len )
break if @res_text
sleep_ms 10
end
end
@is_read_timeout = (cnt == 100)
if @res_text
@res_text.chomp!
dp "<<<C:TEXT #{@res_text.inspect}"
else
@res_text = ""
end
return false if !@res_code.start_with?("R0")
return @res_text
end
##
# reboot
#
def reboot()
command("\r\nreboot")
sleep 5
clear_buffer()
end
##
# get MAC address
#
#@return [String] mac address
#@return [nil] retry error.
#
def mac_address()
mac = nil
retry_count = 10
while !mac
clear_buffer()
command("get wlan.mac")
while mac = get_string()
dp "<<<RECV #{mac.inspect}"
break if mac.size == 19
end
return nil if (retry_count -= 1) == 0
sleep 1
end
return mac.chomp
end
##
# setup module
#
def setup_module( ssid = nil, passkey = nil )
# factory reset.
mac = mac_address()
@rfm.write("factory_reset #{mac}\r\n")
puts ">>> SEND: factory_reset #{mac}"
sleep_ms 5000
puts "<<< RECV: " + @rfm.read_nonblock(1000).to_s
# to machine friendly mode.
# and set wlan parameter
cmds = <<-EOL.split("\n")
set setup.gpio.control_gpio -1
set system.print_level 0
set system.cmd.header_enabled 1
set system.cmd.prompt_enabled 0
set system.cmd.echo off
set wlan.hide_passkey 1
set wlan.auto_join.enabled 1
EOL
cmds << "set wlan.ssid #{ssid}\n" if ssid
cmds << "set wlan.passkey #{passkey}\n" if passkey
cmds << "save\n"
cmds.each {|cmd|
@rfm.write( cmd + "\r\n" )
puts ">>> SEND: " + cmd
sleep_ms 500
puts "<<< RECV: " + @rfm.read_nonblock(1000).to_s
}
end
##
# web setup mode
#
def setup_web()
return chat("setup web") == "In progress"
end
##
# start WiFi connection
#
def start_connect()
return if chat("network_up") == "In progress"
sleep_ms 100
clear_buffer()
end
##
# connected now?
#
def is_connected()
return chat("get wlan.network.status") == "2"
end
##
# wait for connect
#
#@return [Boolean] connect / disconnect
#
def wait_for_connect()
return true if is_connected()
retry_cnt = 0
while retry_cnt < 3
start_connect()
cnt = 0
while (cnt += 1) < 60
return true if is_connected()
sleep 1
end
retry_cnt += 1
end
return false
end
##
# HTTPサーバーへ、JSONデータをPOSTする
#
#@param [String] url URL (e.g. http://example.com/cgi-bin/sample.cgi)
#@param [Hash] data data hash.
#@return [Array<Integer,String>] returned status and contents.
#@return [Nil] error.
#
def http_post_json( url, data )
data_json = data.to_json_tiny()
handle = chat("http_post -o #{url} -l #{data_json.size} application/json", 10000)
return nil if !handle
command("stream_write #{handle} #{data_json.size}")
@rfm.write(data_json)
ret = nil
while !ret
break if !chat(nil)
status_code = chat("http_read_status #{handle}", 30000)
break if !status_code
status_code = status_code.to_i
contents = read_stream( handle, 1000 )
break if !contents
ret = [status_code, contents]
end
chat("stream_close #{handle}")
return ret
end
##
# Read data from stream
#
#@param [String,Integer] handle handle
#@param [Integer] max_len maximum length of return size.
#
def read_stream( handle, max_len = nil )
ret = ""
cnt = 0
while true
dp ""
res = chat("stream_poll #{handle} -r")
break if !res
status,size = res.split(",") # get status and data size
size = size.to_i
# break if no data status continue 5 seconds.
# because ZentriOS cannot detect closed HTTP stream
if status == "0"
break if (cnt += 1) > 5
sleep 1
next
end
cnt = 0
# read chunk data. max 1000 bytes.
# https://docs.zentri.com/zentrios/wl/1.5/cmd/commands#stream-read
while size > 0
size2 = size
size2 = 1000 if size2 > 1000
size -= size2
command("stream_read #{handle} #{size2}")
res = get_string() # "Rxxxxxx"
return nil if !res || !res.start_with?("R0")
# read size2 bytes.
cnt2 = 0
while size2 > 0
if data = @rfm.read_nonblock(size2)
dp ">>> Readed size #{data.size}"
ret << data if !max_len || ret.size < max_len
size2 -= data.size
cnt2 = 0
else
return nil if (cnt2 += 1) > 100
sleep_ms 10
end
end
get_string() # skip CRLF
end
break if status == "2" # break if connection has closed.
end
if max_len && ret.size > max_len
return ret[0, max_len]
end
return ret
end
end
注意事項
- 本事例の掲載情報の閲覧及び利用により、利用者自身、もしくは第三者が被った損害に対して、直接的、間接的を問わず、しまねソフト研究開発センターは責任を負いかねます。
- 本事例の内容を実践する中で用意された機器やパーツについてのご質問は、それぞれの機器やパーツの提供元にお問い合わせをお願いします。なお、機器やパーツの仕様は、本事例の公開当時のものです。
執筆者紹介
しまねソフト研究開発センター
東 裕人(Higashi Hirohito)/ 専門研究員
しまねソフト研究開発センターの専門研究員として、IoT分野で活用が期待できる小型デバイス向け開発言語「mruby/c」の研究開発、企業や大学・高専などとの共同研究、県内ITエンジニアの技術相談対応などの活動を行っています。詳しいプロフィールはこちら。
*このページで公開されている情報は2022年3月31日時点のものです。
お問い合わせ先
しまねソフト研究開発センター(担当:渡部)
Phone:0852-61-2225
Email:itoc@s-itoc.jp