環境メーター 0.4.0

英国の MONK MAKES 社の CO2センサー for micro:bit を使った環境メーターを作成しました。 主な機能は以下の通りです。

PC と CO2センサーを接続した micro:bit をUSBで接続する予定だったのですが、 シリアル通信を PC と CO2センサーで切り替えられず、micro:bit を 2 台にすることにしました。

詳細については、下記のソースコードに沿って説明します。

実行結果

実行結果のスクリーンショットを以下に示します。 このバージョンで CO2 グラフの範囲を 200 からに変更しました。ひとつ前のバージョンでは 300 からだったのですが、このスクリーンショットのように 200ppm 台になることが分かったためです。

ソース

EnvironmentMeter.sb

プログラム全体は PVXC450.000-2 として発行してあります。 このページではこのプログラムを分解して説明します。このプログラムで If debug となっているところはデバッグ用の処理で、 削除しても問題ないのですが、まだ一応残してあります。

飽和水蒸気圧 $e[\mathrm{hPa}]$ は温度 $t[^\circ\mathrm{C}]$ のとき、 $$ e = 6.1078 \times 10^{\frac{7.5~t}{t + 237.3}} $$ 飽和水蒸気量 $a[\mathrm{g}/\mathrm{m}^3]$ は、 $$ a = \frac{216.7~e}{t + 273.15} $$ 容積絶対湿度 $vh[\mathrm{g}/\mathrm{m}^3]$ は相対湿度 $rh[\%]$ のとき、 $$ vh = a \times \frac{rh}{100} $$ で求められます。

' Environment Meter
version = "0.4.0"
' Copyright © 2021 Nonki Takahashi.  The MIT License.
' Last update 2021-05-20
' Program ID PVXC450.000-2

title = "Environment Meter"
GraphicsWindow.Title = title + " v" + version
debug = "False"
Init()
path = Program.Directory + "\EnvironmentMeter.log"
While "True"
  GetLine()
  If line <> "" Then
    rec = ""
    param = LDText.Split(line, ",")
    rec["year"] = Clock.Year
    rec["month"] = Clock.Month
    rec["day"] = Clock.Day
    rec["hour"] = Clock.Hour
    rec["min"] = Clock.Minute
    rec["sec"] = Clock.Second
    c = param[1]  ' [ppm]
    rec["co2"] = c
    t = param[2]  ' [℃]
    rec["temp"] = t
    rh = LDText.Replace(param[3], " ", "")  ' [%]
    rec["rh"] = rh
    GetWBGT() ' w [℃]
    Shapes.SetText(tmp, w + "℃")
    If (-30 < t) And (t < 50) Then
      Shapes.SetText(txt, (t * 1) + "℃")
      DrawTemp()
      ' saturated water vapor pressure [hPa]
      e = 6.1078 * Math.Power(10, 7.5 * t / (t + 237.3))
      ' saturated water vapor density [g/m^3]
      a = 216.7 * e / (t + 273.15)
    EndIf
    Shapes.SetText(co2, c + "ppm")
    If c < 400 Then
      hue = 120
    ElseIf 1000 < c Then
      hue = 0
    Else
      hue = (1000 - c) * 120 / 600
    EndIf
    rc = LDColours.HSLtoRGB(hue, 1, 0.5)
    LDShapes.PenColour(co2ring, rc)
    If (0 <= rh) And (rh <= 100) Then
      ' volumetric humidity [g/m^3]
      vh = Math.Round(a * rh / 100)
      Shapes.SetText(vhTxt, vh)
      vw = LDShapes.Width(vhTxt)
      Shapes.Move(vhTxt, 2 * tw + hw - 55 - vw, 10)
      Shapes.SetText(hum, rh + "%")
      _a = rh * 180 / 100
      LDShapes.RotateAbout(pointer, cx, cy, _a)
    EndIf
    date = rec["year"] + "/" + rec["month"] + "/" + rec["day"]
    time = rec["hour"] + ":" + rec["min"] + ":" + rec["sec"]
    GraphicsWindow.Title = title + " v" + version + " | " + date + " " + time
    If (c < 5000) And (_min <> rec["min"]) And (rh <= 100) Then
      File.AppendContents(path, rec)
      UpdateGraph()
      _min = rec["min"]
    EndIf
  EndIf
  Program.Delay(200)
EndWhile

Sub DrawTemp
  If temp <> "" Then
    Shapes.Remove(temp)
  EndIf
  GraphicsWindow.BrushColor = "#DD0000"
  GraphicsWindow.PenWidth = 0
  temp = Shapes.AddRectangle(4, (t + 30) * 4.5)
  Shapes.Move(temp, tw / 2 - 2, y1 + (50 - t) * 4.5)
EndSub

Sub GetLine
  len = Text.GetLength(buf)
  If p <= len Then
    line = Text.GetSubText(buf, p, len - p + 1)
    nl = Text.GetIndexOf(line, CRLF)
    If 0 < nl Then
      line = Text.GetSubText(line, 1, nl - 1)
      p = p + nl + 1
    Else
      line = ""
    EndIf
  Else
    line = ""
  EndIf
EndSub

Sub GetWBGT
  ' param t - temperature [℃]
  ' param rh - relative humidity [%]
  ' return w - WBGT [℃]
  If (21 <= t) And (t <= 40) And (20 <= rh) And (rh <= 100) Then
    rem = Math.Remainder(h, 5)
    If rem = 0 Then
      w = wbgt[t][h]
    Else
      h1 = h - rem
      h2 = h1 + 5
      w1 = wbgt[t][h1]
      w2 = wbgt[t][h2]
      w = Math.Round(w1 + (w2 - w1) / (h2 - h1) * (h - h1))
    EndIf
  Else
    w = "N/A"
  EndIf
EndSub

サブルーチン Init では GraphicsWindow 上に表示する要素や、 シリアルポートやタイマーといったイベントなどを初期化します。

Sub Init
  CRLF = Text.GetCharacter(13) + Text.GetCharacter(10)
  InitWBGT()
  gw = 800
  tw = 200
  gh = 600
  GraphicsWindow.Width = gw
  GraphicsWindow.Height = gh
  GraphicsWindow.Top = 20
  GraphicsWindow.Left = 20
  bg = "#333333"
  GraphicsWindow.BackgroundColor = bg
  ' CO2 meter
  GraphicsWindow.PenColor = "Lime"
  GraphicsWindow.PenWidth = 10
  GraphicsWindow.BrushColor = "Transparent"
  co2ring = Shapes.AddEllipse(tw, tw)
  Shapes.Move(co2ring, tw, 10)
  GraphicsWindow.BrushColor = "White"
  GraphicsWindow.FontName = "Trebuchet MS"
  GraphicsWindow.FontSize = 40
  co2 = Shapes.AddText("0000ppm")
  Shapes.Move(co2, tw + 15, tw / 2 - 15)
  ' humidity meter
  GraphicsWindow.BrushColor = "#112233"
  hx = 2 * tw + 10
  hy = 10
  hw = gw - 2 * tw - 20
  hh = tw
  GraphicsWindow.FillRectangle(hx, hy, hw, hh)
  GraphicsWindow.PenWidth = 2
  GraphicsWindow.BrushColor = "White"
  hum = Shapes.AddText("50%")
  Shapes.Move(hum, 2 * tw + 20, 10)
  vhTxt = Shapes.AddText("?")
  vw = LDShapes.Width(vhTxt)
  Shapes.Move(vhTxt, 2 * tw + hw - 55 - vw, 10)
  GraphicsWindow.FontSize = 30
  GraphicsWindow.DrawText(2 * tw + hw - 55, 20, "g/㎥")
  pc = "#1111CC"
  GraphicsWindow.BrushColor = pc
  cs = 20
  cx = hx + hw / 2
  cy = hy + hh - cs
  GraphicsWindow.FillEllipse(cx - cs / 2, cy - cs / 2, cs, cs)
  pl = 100
  GraphicsWindow.PenColor = "White"
  r1 = 1.02
  r3 = 1.4
  r4 = 0.98
  r5 = 0.7
  as = pl * r4
  at = pl * (r4 - r5) / 2
  GraphicsWindow.BrushColor = "White"
  GraphicsWindow.PenWidth = 2
  GraphicsWindow.FontSize = 20
  i = 0
  For h = 0 To 100 Step 2
    a = h * 180 / 100
    _a = Math.GetRadians(a)
    If Math.Remainder(h, 10) = 0 Then
      x3 = cx - r3 * pl * Math.Cos(_a)
      y3 = cy - r3 * pl * Math.Sin(_a)
      GraphicsWindow.DrawText(x3 - 10, y3 - 12, h)
      r2 = 1.2
    Else
      r2 = 1.1
    EndIf
    i = i + 1
    x4[i] = cx - r4 * pl * Math.Cos(_a)
    y4[i] = cy - r4 * pl * Math.Sin(_a)
    x5[i] = cx - r5 * pl * Math.Cos(_a)
    y5[i] = cy - r5 * pl * Math.Sin(_a)
    x1 = cx - r1 * pl * Math.Cos(_a)
    y1 = cy - r1 * pl * Math.Sin(_a)
    x2 = cx - r2 * pl * Math.Cos(_a)
    y2 = cy - r2 * pl * Math.Sin(_a)
    GraphicsWindow.DrawLine(x1, y1, x2, y2)
  EndFor
  GraphicsWindow.PenWidth = 0
  dry = LDColours.HSLtoRGB(55, 1, 0.65)
  GraphicsWindow.BrushColor = dry
  For i = 1 To 21
    points[i][1] = x4[i]
    points[i][2] = y4[i]
  EndFor
  For i = 21 To 1 Step -1
    points[43 - i][1] = x5[i]
    points[43 - i][2] = y5[i]
  EndFor
  LDShapes.AddPolygon(points)
  GraphicsWindow.BrushColor = "Cyan"
  points = ""
  For i = 21 To 31
    points[i - 20][1] = x4[i]
    points[i - 20][2] = y4[i]
  EndFor
  For i = 31 To 21 Step -1
    points[43 - i][1] = x5[i]
    points[43 - i][2] = y5[i]
  EndFor
  LDShapes.AddPolygon(points)
  wet = LDColours.HSLtoRGB(185, 1, 0.35)
  GraphicsWindow.BrushColor = wet
  points = ""
  For i = 31 To 51
    points[i - 30][1] = x4[i]
    points[i - 30][2] = y4[i]
  EndFor
  For i = 51 To 31 Step -1
    points[73 - i][1] = x5[i]
    points[73 - i][2] = y5[i]
  EndFor
  LDShapes.AddPolygon(points)
  GraphicsWindow.PenWidth = 2
  GraphicsWindow.PenColor = pc
  pointer = Shapes.AddLine(cx, cy, cx - pl, cy)
  LDShapes.RotateAbout(pointer, cx, cy, 90)
  '  thermometer
  GraphicsWindow.BrushColor = "White"
  GraphicsWindow.FillRectangle(10, 10, tw - 20, gh - 20)
  GraphicsWindow.BrushColor = "Gray"
  GraphicsWindow.FontSize = 40
  txt = Shapes.AddText("")
  Shapes.Move(txt, 20, 10)
  GraphicsWindow.FontSize = 20
  GraphicsWindow.DrawText(20, 50, "WBGT")
  GraphicsWindow.FontSize = 30
  tmp = Shapes.AddText("")
  Shapes.Move(tmp, 20, 70)
  GraphicsWindow.FontSize = 40
  y1 = 138
  y2 = gh - 100
  t = 50
  GraphicsWindow.PenColor = "Gray"
  For y = y1 To y2 Step 9
    If Math.Remainder(y, 45) = 3 Then
      x1 = tw / 2 - 40
      x2 = tw / 2 + 40
      If t < 0 Then
        GraphicsWindow.DrawText(tw / 2 - 82, y - 40, t)
      Else
        GraphicsWindow.DrawText(tw / 2 + 20, y - 40, t)
      EndIf
      t = t - 10
    Else
      x1 = tw / 2 - 20
      x2 = tw / 2 + 20
    EndIf
    GraphicsWindow.DrawLine(x1, y, x2, y)
  EndFor 
  GraphicsWindow.BrushColor = "LightGray"
  GraphicsWindow.FillEllipse(tw / 2 - 15, gh - 60, 30, 30)
  GraphicsWindow.FillEllipse(tw / 2 - 5, 60, 10, 10)
  GraphicsWindow.FillRectangle(tw / 2 - 5, 65, 10, gh - 100)
  GraphicsWindow.BrushColor = "#DD0000"
  GraphicsWindow.FillEllipse(tw / 2 - 10, gh - 55, 20, 20)
  GraphicsWindow.FillRectangle(tw / 2 - 2, gh - 102, 4, 50)
  ' graph
  xx = tw
  xy = hy + hh + 10
  xw = gw - tw - 10
  xh = gh - hh - 30
  GraphicsWindow.BrushColor = "#222222"
  GraphicsWindow.FillRectangle(xx, xy, xw, xh)
  xx0 = xx + 70
  xx1 = xx + xw - 40
  xy0 = xy + xh - 30
  xy1 = xy + 20
  dxx = Math.Floor((xx1 - xx0) / 20)
  dxy = Math.Floor((xy1 - xy0) / 10)
  GraphicsWindow.PenColor = "#666666"
  GraphicsWindow.BrushColor = "#666666"
  sec = -600
  GraphicsWindow.FontSize = 14
  For xx_ = xx0 To xx1 Step dxx
    GraphicsWindow.DrawLine(xx_, xy0, xx_, xy1)
    If Math.Remainder(sec, 60) = 0 Then
      If sec = 0 Then
        min = "0min"
      Else
        min = sec / 60
      EndIf
      GraphicsWindow.DrawText(xx_ - 7, xy0 + 5, min)
    EndIf
    sec = sec + 30
  EndFor
  v0[1] = -50
  t = v0[1]
  v0[2] = 200
  c = v0[2]
  v0[3] =  0
  rh = v0[3]
  gc[1] = "#FF3333"
  gc[2] = "#00CC00"
  gc[3] = "#3333FF"
  key[1] = "temp"
  key[2] = "co2"
  key[3] = "rh"
  For xy_ = xy0 To xy1 Step dxy
    GraphicsWindow.DrawLine(xx0, xy_, xx1, xy_)
    GraphicsWindow.BrushColor = gc[1]
    GraphicsWindow.DrawText(xx + 5, xy_ - 10, t)
    t = t + 10
    GraphicsWindow.BrushColor = gc[2]
    GraphicsWindow.DrawText(xx + 30, xy_ - 10, c)
    c = c + 100
    GraphicsWindow.BrushColor = gc[3]
    GraphicsWindow.DrawText(xx1 + 5, xy_ - 10, rh)
    rh = rh + 10
  EndFor
  v1[1] = t - 10
  v1[2] = c - 100
  v1[3] = rh - 10
  p = 1 ' COM3 receive buffer pointer
  status = LDCommPort.OpenPort("COM4", 115200)
  If status <> "SUCCESS" Then
    TextWindow.WriteLine("status=" + status)
  EndIf
  LDCommPort.SetEncoding("Ascii")
  LDCommPort.DataReceived = OnDataReceived
  Timer.Interval = 20000
  Timer.Tick = OnTick
EndSub

最後のサブルーチン群では以下の処理を行います。サブルーチン群はアルファベット順に並べました。 サブルーチン OnDataReceived と OnTick はイベントハンドラであり、 ハードウェア的なイベントが発生したときに呼び出されます。

Sub InitWBGT
  _wbgt[40] = "29,30,31,32,33,34,35,35,36,37,38,39,40,41,42,43,44"
  _wbgt[39] = "28,29,30,31,32,33,34,35,35,36,37,38,39,40,41,42,43"
  _wbgt[38] = "28,28,29,30,31,32,33,34,35,35,36,37,38,39,40,41,42"
  _wbgt[37] = "27,28,29,29,30,31,32,33,33,35,35,36,37,38,39,40,41"
  _wbgt[36] = "26,27,28,29,29,30,31,32,33,34,34,35,36,37,38,39,39"
  _wbgt[35] = "25,26,27,28,29,29,30,31,32,33,33,34,35,36,37,38,38"
  _wbgt[34] = "25,25,26,27,28,29,29,30,31,32,33,33,34,35,36,37,37"
  _wbgt[33] = "24,25,25,26,27,28,28,29,30,31,32,32,33,34,35,35,36"
  _wbgt[32] = "23,24,25,25,26,27,28,28,29,30,31,31,32,33,34,34,35"
  _wbgt[31] = "22,23,24,24,25,26,27,27,28,29,30,30,31,32,33,33,34"
  _wbgt[30] = "21,22,23,24,24,25,26,27,27,28,29,29,30,31,32,32,33"
  _wbgt[29] = "21,21,22,23,24,24,25,26,26,27,28,29,29,30,31,31,32"
  _wbgt[28] = "20,21,21,22,23,23,24,25,25,26,27,28,28,29,30,30,31"
  _wbgt[27] = "19,20,21,21,22,23,23,24,25,25,26,27,27,28,29,29,30"
  _wbgt[26] = "18,19,20,20,21,22,22,23,24,24,25,26,26,27,28,28,29"
  _wbgt[25] = "18,18,19,20,20,21,22,22,23,23,24,25,25,26,27,27,28"
  _wbgt[24] = "17,18,18,19,19,20,21,21,22,22,23,24,24,25,26,26,27"
  _wbgt[23] = "16,17,17,18,18,19,19,20,21,22,22,23,23,24,25,25,26"
  _wbgt[22] = "15,16,17,17,18,18,19,19,20,21,21,22,22,23,24,24,25"
  _wbgt[21] = "15,15,16,16,17,17,18,19,19,20,20,21,21,22,23,23,24"
  For t = 40 To 21 Step -1
    w = LDText.Split(_wbgt[t], ",")
    i = 1
    For rh = 20 To 100 Step 5
      wbgt[t][rh] = w[i]
      i = i + 1
    EndFor
  EndFor
EndSub

Sub OnDataReceived
  buf = Text.Append(buf, LDCommPort.RXAll())
EndSub

Sub OnTick
  LDCommPort.TXString(":")
EndSub

Sub UpdateGraph
  ' param rec - current record
  n = n + 1
  If debug Then
    TextWindow.WriteLine("n = " + n)
  EndIf
  GraphicsWindow.PenWidth = 2
  ' remove graph
  If 11 < n Then
    For g = 1 To 3
      Shapes.Remove(l[n - 11][g])
      If debug Then
        TextWindow.WriteLine("- l[" + (n - 11) + "]")
      EndIf
      l[n - 11][g] = ""
      log[n - 11][g] = ""
    EndFor
  EndIf
  ' move graph
  If 2 < n Then
    m = Math.Max(1, n - 10)
    If debug Then
      TextWindow.WriteLine("m = " + m)
    EndIf
    For i = m To n - 2
      For g = 1 To 3
        _x1 = xx1 - (xx1 - xx0) * (n - i) / 10
        _y1 = log[i][g]
        _x2 = xx1 - (xx1 - xx0) * (n - i - 1) / 10
        _y2 = log[i + 1][g]
        LDShapes.MoveLine(l[i][g], _x1, _y1, _x2, _y2)
        If debug Then
          TextWindow.WriteLine("< l[" + i + "][" + g + "] = (" + _x1 + "," + _y1 + "," + _x2 + "," + _y2 + ")")
        EndIf
      EndFor
    EndFor
  EndIf
  ' add graph
  For g = 1 To 3
    value = rec[key[g]]
    Value2Y()
    log[n][g] = xy
    If debug Then
      TextWindow.WriteLine("log[" + n + "][" + g + "] = " + xy)
    EndIf
  EndFor
  If 1 < n Then
    For g = 1 To 3
      GraphicsWindow.PenColor = gc[g]
      _x1 = xx1 - (xx1 - xx0) / 10
      _y1 = log[n - 1][g]
      _x2 = xx1
      _y2 = log[n][g]
      l[n - 1][g] = Shapes.AddLine(_x1, _y1, _x2, _y2)
      Shapes.SetOpacity(l[n - 1][g], 80)
      If debug Then
        TextWindow.WriteLine("+ l[" + (n - 1) + "][" + g + "] = (" + _x1 + "," + _y1 + "," + _x2 + "," + _y2 + ")")
      EndIf
    EndFor
  EndIf
EndSub

Sub Value2Y
  ' param value
  ' param g - 1 if temp, 2 if co2, 3 if rh
  ' return xy
  xy = xy0 - (xy0 - xy1) * (value - v0[g]) / (v1[g] - v0[g])
EndSub

environment-meter

micro:bit v2 用のプログラムです。プログラムは GitHub に公開しました。 CO2センサーPC と USB(シリアル通信)で接続され、環境データを無線で v1 に送ります。


function readCO2 () {
    co2 = Math.round(COZIR.readCo2())
}
function readTemp () {
    temp = Math.round(COZIR.readTemp())
}
function readRH () {
    rh = Math.round(COZIR.readRh())
}
function sendParam () {
    radio.sendString("" + co2 + "," + temp + "," + rh)
}
input.onButtonPressed(Button.A, function () {
    readCO2()
    basic.showNumber(co2)
})
input.onButtonPressed(Button.B, function () {
    readTemp()
    basic.showNumber(temp)
})
input.onButtonPressed(Button.AB, function () {
    readRH()
    basic.showNumber(rh)
})
radio.onReceivedString(function (receivedString) {
    if (receivedString == ":") {
        readCO2()
        readTemp()
        readRH()
        sendParam()
    }
})
let temp = 0
let co2 = 0
let rh = 0
radio.setGroup(1)
serial.redirect(
SerialPin.P0,
SerialPin.P1,
BaudRate.BaudRate9600
)
basic.pause(500)

receiver

micro:bit v1 用のプログラムです。プログラムは GitHub に公開しました。 PC と USB(シリアル通信)で接続され、v2 からの環境データを無線で受け取り、PCに送ります。


input.onButtonPressed(Button.A, function () {
    basic.showString(line)
})
radio.onReceivedString(function (receivedString) {
    line = receivedString
    serial.writeLine(line)
})
serial.onDataReceived(serial.delimiters(Delimiters.Colon), function () {
    radio.sendString(":")
})
let line = ""
radio.setGroup(1)
line = "N/A"

Copyright © 2021-2022 たかはしのんき. All rights reserved.