×閉じる
利用事例 mruby/c

温湿度管理を自動化する「温湿度ロガー」

しまねソフト研究開発センターでは、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/)。ただし、後述するとおり、開発時はケーブル一本で接続して非常に簡易に作業ができたが、基板の固定ができないため、最終成果物では半田付けとしている。

機器内側_02

ソフトウェア

本センサーは、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ファイル「ドキュメント(温湿度ロガー開発について)」をダウンロードする(PDF:766kB)

pdfファイル「パーツリスト(温湿度ロガー開発について)」をダウンロードする(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

 

このページのトップへ