英国の MONK MAKES 社の CO2センサー for micro:bit を使った環境メーターを作成しました。 主な機能は以下の通りです。
PC と CO2センサーを接続した micro:bit をUSBで接続する予定だったのですが、 シリアル通信を PC と CO2センサーで切り替えられず、micro:bit を 2 台にすることにしました。
詳細については、下記のソースコードに沿って説明します。
実行結果のスクリーンショットを以下に示します。 このバージョンで CO2 グラフの範囲を 200 からに変更しました。ひとつ前のバージョンでは 300 からだったのですが、このスクリーンショットのように 200ppm 台になることが分かったためです。
プログラム全体は 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
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)
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"