ESP-Nowをためす

電池動作での長時間化を狙って、ESP8266/ESP32での直接通信モードのESP-Nowを試してみます。

ESP-NowはEspressifの説明によれば、「ESP-NOWは、Espressifによって開発されたさらに別のプロトコルであり、Wi-Fiを使用せずに複数のデバイスが相互に通信できるようにします。このプロトコルは、ワイヤレスマウスによく使用される低電力2.4GHzワイヤレス接続に似ています。したがって、デバイス間のペアリングは、通信の前に必要です。ペアリングが完了すると、接続は安全でピアツーピアになり、ハンドシェイクは必要ありません。」(Google翻訳による)ということで、Wi-Fiのような複雑なプロトコルではないので、通信にかかる時間が短く=消費電力が削減されることが期待できます。

ESP-Nowのドキュメントはこちらになるのですが、今回は簡単にArduino環境のライブラリSimpleEspNowConnection(ライブラリマネージャからインストールできる)を使って、送信側にESP8266、受信側にESP32で動作させました。ESP-Nowでespressifから提供されているAPIはESP8266とESP32で異なっているのですが、SimpleEspNowConnectionはその差分も吸収してくれます。

想定する使い方は、電池で動くセンサー側から、PCもしくはRaspberry Piに接続されたホスト側に測定データを送信する、という使い方です。本来、ESP-Nowは双方向で通信ができるのですが、今回はこの使い方に絞った形でテストしてみました。

まずは受信側のコードです。これは、ライブラリのサンプルプログラム(SimpleEspNowConnectionServer.ino)をさらに簡略化したもので、ESP32で動作させました。

#include "SimpleEspNowConnection.h"

SimpleEspNowConnection simpleEspConnection(SimpleEspNowRole::SERVER);

String clientAddress;

void OnMessage(uint8_t* ad, const uint8_t* message, size_t len)
{
  Serial.printf("MESSAGE:[%d]%s from %s\n", len, (char *)message, simpleEspConnection.macToStr(ad).c_str());
}

void setup() 
{
  Serial.begin(115200);
  Serial.println();

  simpleEspConnection.begin();
  simpleEspConnection.setPairingBlinkPort(2);
  simpleEspConnection.onMessage(&OnMessage);  

  Serial.println(WiFi.macAddress());    
}

void loop() 
{
  simpleEspConnection.loop();
}

次に送信側のコードです。これも、ライブラリのサンプルプログラム(SimpleEspNowConnectionClient.ino)をさらに簡略化したもので、テストではESP8266で動作させました。

#include "SimpleEspNowConnection.h"

SimpleEspNowConnection simpleEspConnection(SimpleEspNowRole::CLIENT);

String serverAddress;

void OnSendError(uint8_t* ad)
{
  Serial.println("SENDING TO '"+simpleEspConnection.macToStr(ad)+"' WAS NOT POSSIBLE!");
}

void OnSendDone(uint8_t* ad)
{
  Serial.println("SENDING TO '"+simpleEspConnection.macToStr(ad)+"' WAS DONE!");
}

void setup() 
{
  Serial.begin(115200);
  Serial.println();

  Serial.println("Setup...");

  simpleEspConnection.begin();
  simpleEspConnection.setPairingBlinkPort(2);  

  serverAddress = "012345ABCDEF"; // 受信側のMACアドレス
  simpleEspConnection.setServerMac(serverAddress);
  simpleEspConnection.onSendError(&OnSendError);  
  simpleEspConnection.onSendDone(&OnSendDone);
  
  Serial.println(WiFi.macAddress());  
}

void loop() 
{
  static int n=1;
  static char buf[32];
  simpleEspConnection.loop();

  sprintf(buf,"%d",n++);
  simpleEspConnection.sendMessage(buf);
  delay(1000);
}

これで実際に動作させることができました。

送信に成功した場合、失敗した場合でそれぞれコールバック関数が呼ばれるので、それに応じた処理を記述することができそうです。例えば、成功した場合には2分間のディープスリープ、失敗した場合には5秒間のディープスリープとして再トライさせるなどの使い方になると思います。

安価なCO2センサの比較(4)

SCD30もSelf Calibrationが動作する連続稼働7日を越えましたので、改めて各CO2センサの比較をしてみたいと思います。

まず、CO2濃度が上昇していく局面です。

①の前は設置環境は無人かつ窓が開いている状態で値は安定しています。①で自分が入室、②で窓を閉めました。ここから値がどんどん上昇し、③で窓を開けて換気しました。

結果的には前回とよく似た結果です。パチもんのMH-Z19Bは相変わらず大きく外れた値が出ています。

次に、400ppm付近の挙動です。(下記④)

SCD30は400ppm以下の値もそのまま出してしまうようです。
MH-Z19Bは内部の計算結果が400ppm以下の場合には400ppmでクリップ、偽物のMH-Z19Bは400ppmを下回ったらとりあえず50足しとけという乱暴な挙動に見えます。

興味深いのはMH-Z19CとSCD41の挙動が結構似ているようにみえることです。結果も今回テストした2台に関してはほぼ同じ値が出ているように見えますし。

外気のCO2濃度は気象庁の資料(二酸化炭素濃度の経年変化二酸化炭素濃度の観測結果)を見ると、410〜420ppm前後ということになります。温室内や森の中などでは周囲より下がることがありますが、通常の環境では400ppmを下回ることはありません。

安価なCO2センサの比較(3)

現時点でのCO2センサの勝手な比較をしてみました。各センサの(ほぼ)同一条件での出力比較はこちらの記事を見てみてください。
方式としては測定値に信頼性がおけるNDIR方式のもののみです。(MOX方式のCCS811も試したことがありますが、自分的には使い物になりません)
また、Sensirionのセンサはまだ初期キャリブレーションが終わっていないので、後で評価を更新するかもしれません。

利点欠点独断評価
SCD30
(Sensirion)
電源に対する要求が緩い
単体で扱いやすい
ユニバーサル基板にも載せやすい
千石マルツなどの国内の通販でも買える
低い濃度領域で400ppm以下の値が返ってくる。
初期のオートキャリブレーションの期間が長い(1週間連続通電が必要)
価格が高め。
SCD41
(Sensirion)
大きさが非常に小さい
電源に対する要求が緩い
公式ドライバソフトウェアが公開されている(ラズパイ、Arduino、組込)
省電力の間欠動作モードがある(電池動作向け)
SCD41単体での入手がまだできない?
SCD41単体でははんだ付けが困難
(評価キットで入手するのがおすすめ)
オートキャリブレーションの期間は不明(調査中)
MH-Z19B
(Winsen)
安価($20程度)
オートキャリブレーションの期間が短い(24時間)
ディスコン済み?
電源の要求条件が厳しい(5V±0.1V)
端子配置が一般的なユニバーサル基板のピッチにあってない
MH-Z19C
(Winsen)
安価で入手性良好(2480円で秋月で買える
オートキャリブレーションの期間が短い(24時間)
AliExpressに直営店出てます
電源の要求条件が厳しい(5V±0.1V)
端子配置が一般的なユニバーサル基板のピッチにあってない
MH-Z19Bの
パチもの
精度が極めて悪くて使い物にならない
価格も本物と変わらない
AliExpressで本物のMH-Z19Bの写真が貼ってあるのに送られてきたのはこれ
(Aliで買うならWinsenのShopで買いましょう)
×

秋月にはMH-Z14Bも取り扱いがあるようですが、入手していないので掲載していません。

安価なCO2センサの比較(2)

実際に手持ちのNDIR方式のCO2センサを概ね一箇所にまとめて置いて6時間ほどかけて比較してみました。場所は窓に近くで窓から50cmくらいのところで、自分が作業している椅子からは背後方向1mくらいのところです。対象は

  • Sensirion SCD30(注:キャリブレーションの期間が明らかに終わっていません)
  • Sensirion SCD41(これも稼働を始めて2日程度です)
  • Winsen MH-Z19B
  • Winsen MH-Z19C
  • MH-Z19Bのパチもの(グラフではFAKE19B)

の5つです。早速、結果から。

①の前は部屋の窓を少し開けた状態。①で窓を大きく開けました。②で窓を完全に締めました。③で暑くなってきたのでエアコンで冷房を入れました。④でエアコンを切って、窓を全開にして空気を入れ替えです。

②まではどれも同じくらいの値ですが、SCD30は外の風(ごく弱く、ほぼ無風です)の向きなどで影響されているのか、キャリブレーションが終わっていないからなのか、時々値が変動しています。窓を完全に閉めて部屋を締め切ると自分の呼気でCO2濃度が上昇を始めます。③でエアコンを稼働すると、エアコンのON/OFFに連動するのか、Sensirionのセンサーは定期的に値が上下しています。④で窓を開けると、一斉に値が下がり、400〜500ppmまで下がります。パチもんのMH-Z19Bだけは、値が大幅に異なっていて、他のセンサーでは2600ppm前後に達していても、こいつだけは1750ppm程度になっています。

というわけで、

パチもんのMH-Z19Bは使えねぇ

ということがよくわかりました。

安価なCO2センサの比較(1)

Sensirion製センサも動くようにしたので、それぞれどんな関係にあるか、見たくなってきました。
ですので、以前作った電源周波数観測用のサーバプログラムを改造して、chart.jsでグラフを描かせるようにしてみました。

本当はリアルタイム更新のほうが見た目には面白いのですが、 asyncio を使いこなせていないせいか、うまく行かず、Javascriptで強制リロードする方式にしました。

各センサからのデータ収集はUSBシリアル経由で行っていますが、今までは数値だけだったり、デバッグ情報が混ざっていたりとバラバラだったので、フォーマットを以下のように規定しました。

$(センサ名) Co2:(CO2濃度値 ppm) ・・・(その他の情報)

各USBシリアルから入ってくるデータを読んで、蓄積、定期的にHTML出力するコードとしました。(とにかく動けばいいや、なのでテキトーです)

# -*- coding:utf-8 -*-
#
import os.path,sys
import serial,time,re

# webserverは
#   python -m http.server 8080
# で別途起動

hist = {}        # データを保持する変数
hlen = 60000       # データを保持する数

devs = (
    '/dev/ttyUSB0',
    '/dev/ttyUSB1',
    '/dev/ttyUSB2',
    '/dev/ttyUSB3',
    '/dev/ttyUSB4',
    '/dev/ttyACM0',
    '/dev/ttyACM1',
    '/dev/ttyACM2',
    '/dev/ttyACM3',
    '/dev/ttyACM4' 
  )

ports = []

header = """<!DOCTYPE HTML>
<html lang="ja">
 
<head>
  <meta charset="utf-8">
 <title>CO2センサ比較</title>
</head>
 
<body>
  <h1>CO2センサ比較</h1>
  時刻はサーバプログラムがデータを受信した時刻です。データは約1分ごとに自動更新しています。30秒で自動リロードしますが、リロードのタイミングとデータの自動更新のタイミングがぶつかると表示の更新が停止する場合があります。<BR>
  センサーのサンプル数は各1台です。入手経路も入手時期も取り付けの構造もまちまちです。また、気分で改造したりします。
  <HR>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.js"></script>
    <canvas id="Chart1" width="auto" height="auto"></canvas>
    <script>
      var context = document.getElementById('Chart1');
      var Chart1 = new Chart(context, {
        type: 'scatter',
          data: {
            datasets: [
"""
footer = """
              ]},
          options:{
            scales:{
              xAxes: [{
                gridLines: {
                  color: "rgba(255, 0, 0, 0.2)", 
                  zeroLineColor: "black"    },
                type: 'time',
                time: {
                  unit: 'minute',
                  displayFormats: {
                    minute: 'H:mm'
                  },
                },
              }],
              yAxes: [{
                ticks :{
                  userCallback: function(tick) {
                    return tick.toString() + 'ppm'
                  }
                },
                gridLines: {
                  color: "rgba(0, 0, 255, 0.2)",
                  zeroLineColor: "black"
                },
              }]
            },
            animation: false
          }
      });
    </script>
    <script>
    // 30秒に一回リロード
      setTimeout("location.reload(true)",30000);
    </script>
</body>
</html>
"""
prepare = """<!DOCTYPE HTML>
<html lang="ja">
 
<head>
  <meta charset="utf-8">
 <title>CO2センサ比較</title>
</head>
 
<body>
  <h1>準備中です。1分程度で開始します</h1>
  <script>
  // 1分=60秒に一回リロード
    setTimeout("location.reload(true)",60000);
  </script>
</body>
</html>
"""

m1 = re.compile('^\$([A-Z0-9]+)\sCo2:([0-9]+)')

ctbl = (
  'rgba(  0,  0,255,0.8)',
  'rgba(255,  0,  0,0.8)',
  'rgba(  0,128,  0,0.8)',
  'rgba(255,  0,255,0.8)',
  'rgba(  0,128,128,0.8)',
  'rgba(128,128,  0,0.8)',
  'rgba( 64, 64, 64,0.8)'
)

def recieve():
  mkht = time.time()

  for p in ports :
    p.rts = False
    p.reset_input_buffer()
    p.rts = True

  while True:
    for p in ports :
      data = None
      if p.inWaiting() > 0 :
        try :
          data = p.readline().decode().rstrip('\r\n')
        except :
          data = None
          pass
        finally :
          pass
        #print(time.time(),data)
        r1 = m1.match(data)
        if r1 :
          #print(r1.group(1),r1.group(2),time.time())
          sensor = r1.group(1)
          data = float(r1.group(2))
          tstamp = time.time()
        else :
          data = None

      # 配列にデータを追加
      if data is not None :
        if not sensor in hist  :
          hist[sensor] = []
        hist[sensor].append((tstamp,data))
        while len(hist[sensor]) > hlen :
          del(hist[sensor][0])

    # 定期的にHTMLファイルを生成する。
    if time.time() - mkht > 10 :  # 前回ファイル出力から10秒以上経過している場合
      mkht = time.time()
      s = ""
      cidx = 0
      for k in hist.keys():
        s += """
              {
                label: \"""" + k + """\",
                showLine: true ,
                lineTension: 0 ,
                fill: false ,
                borderColor: \"""" + ctbl[cidx] + """\",
                borderWidth: 1,
                pointRadius: 1,
                data :[
"""
        cidx += 1
        for i in hist[k]:
          if mkht - i[0] < 3600*6 :    # 過去20分以内のデータに限り出力する
            s += "{{ x: {:.0f} , y: {} }},".format(i[0]*1000,i[1])
        s = s.rstrip(',')
        s += "]},"
      s = s.rstrip(',')
      with open('index.html',mode='w') as f:
        f.write(header + s + footer)

  for p in ports :
    p.close()

#
if __name__ == "__main__":
  for p in devs :
    if os.path.exists(p) :
      ports.append(serial.Serial(p , baudrate=115200, parity=serial.PARITY_NONE, rtscts=False))
  with open('index.html',mode='w') as f:
    f.write(prepare)
  time.sleep(5)
  recieve()

これをRaspberry Pi 3上で動かして、データ収集しながらHTML生成をさせました。

すべてのセンサを自室内の10cmほど開けてある窓に近いところに置いて比較してみました。

SCD30は変動が大きく、自分がそちらを向くと1mくらいあっても呼気に反応するのか、あるいはちょっとした風向きの変化でCO2濃度が変化する(外気があたる or 室内の空気があたるかが変わる)のか、すぐに数値が上昇します。その他のものは概ね似たような傾向を示しています。

このあと、窓を開けたまま風呂に入って無人の時間がしばらく経過して戻ってきたところです。

やはり、SCD30が極めて敏感に反応しているのですが、400ppmを切ってしまっているので、まだオートキャリブレーションが正常に機能していないのだと思います。(SCD41もまだ稼働時間は24時間以下です)

しばらく様子見するしかなさそうです。

Sensirion製CO2センサ

テストしただけで使っていなかったSensirion製SCD30と新規に購入したSCD41を動かしてみました。SCD30の方はテスト時と同じ構成です。

写真ではわかりませんが、写真の上半分の側面と上面に通気用の穴をたくさん開けてあります。また、小さく作ることができたので、小さなモバイルバッテリーで持ち運びもできそうです。
ソフトウェアはArduino環境でSparkFunのSCD30用のライブラリと、液晶制御はtomozhさんのST7032iライブラリを使っています。

一方、SCD41は間欠動作する省電力動作モードがあるようなので、将来的にディープスリープ併用で電池で動かすことを考え、以前作った自作のESP-WROOM-02搭載ボードに載せようと思ったのですが、肝心のESP-WROOM-02がありませんでした。ですので、暫定で秋月のESP-WROOM-02ボードにつないでありますが、Wi-Fiは使っていません。

ソフトウェアの方はSensirionからRaspberry PiやArduino、組み込み用のライブラリが提供されているので、Arduinoのライブラリをそのまま使っています。プログラムもほぼサンプルそのままで、シリアルに出力するのみになっています。

CASIOの超古いラベルプリンタKL-E11をお手軽軽量化

ゴールデンウイークで自宅の片づけをしていたら、大昔に買ったCASIOのラベルプリンタKL-E11が未開封のラベルカートリッジと一緒に出てきました。いつ買ったかも覚えていないのですが、20年くらい前ではないでしょうか。

ACアダプタが電気用品安全法のPSEマークではなく、電気用品取締法の「▽+〒」マークというあたりからも、いかに古いかがわかります。ちなみに蛍光オレンジのテープは残り無しだった。

調べてみると、なんと、今でも印刷に必要なWindows10用のアプリケーション(ラベル印刷ソフトBA-100PCラベル印刷ユーティリティBA-P20の2種類)とプリンタドライバがCASIOのWebサイトで提供されています。(プリンタドライバはラベル印刷ソフトなどとは別にインストールする必要があることに注意)

しかし、このACアダプタがとんでもなく重いのです。そうです。今となっては絶滅したトランスと整流回路だけが入っている、スイッチング電源ではないACアダプタです。ですので、出力7.5Vと書いてありますが、無負荷だと11.5Vくらい電圧があります。重さも本体より重い約400gです。一方で、本体側はDC9V 9Wと書いてあります。
ということは、適当にこのあたりの電圧を突っ込んでやれば動きそうです。そこで手元にあった秋月のDC9VのスイッチングACアダプタを挿そうとしたのですが大きさが合いません。でも、この形状はどこかで見たことがあると思ってジャンク箱を探したら、JEITAの電圧区分3のDCプラグのようで手持ちの変換ケーブルのプラグと同じ大きさでした。

さらによく考えると、Φ2.1mmのプラグのついた9V出力のUSB電源ケーブルを先日買ったことを思い出しました。

これらを組み合わせて(おそらく)15年くらいの時を超えて無事に出力ができました。しかも重たいACアダプタともおさらばできて万々歳です。なお、このワンダーキットの変換ケーブルWK-J3は値段が倍くらいになってますが未だに現役で売っているようです。また、秋月電子でもJEITAの電圧区分3の統一極性プラグを打っているようですので、秋月電子で9VのACアダプタと一緒に買ってきてプラグを付け替えるという手もありそうです。

セットできるテープの幅は18mmまでで、印刷できる幅は12mmくらいまでのようですが、自宅でたまに使う分にはまだまだ使えそうです。もちろん、自宅の中で圧倒的に現役最古のPC周辺機器になりました(笑)

変な挙動

電源周波数計測が変な変動を捉えた・・・といっても、本当に周波数が変動したのではなく、ちょうどスレッショルド付近にノイズが乗っただけだと思う。

ただ、これが特異なのは45秒近くの間ノイズがのり続けたこと。

いったい、どんな波形だったのか気になります。