カテゴリー: 気象

  • 0-3.8V風速計

    0-3.8V風速計

    中国製マニュアル無しブラックボックス風速計を調べて見たら
    一番安価だが一番手間と出費が掛かった話

    風速計全部調べてやる!!という気になって勢いで0-3.8Vタイプも調達してみた。
    0-3.8Vという意味が良く分からないのだが、もしかしてモーターの出力が±1.9Vという事なのか?
    とっても気になるのでgoogleさんに聞いてみた。(以下)

    という具合な全くそつがない返答でした。
    普通に聞いていると全部正解に聞こえるのでAIは無条件で信用すると怖いなと思いました。
    果たして本当の意味は何なんでしょうか?
    中国製の製品は、けっこうこうゆう風に説明なしの商品が多いので困ることが多い。
    恐らくそうゆうのがあの国では普通の事なのだろう、我々とは考え方が違うようだ。
    まあいいやとりあえずいじってみよう

    風速監視センサー
    ブランド MUNDFE
    購入先 アマゾン 購入価格 ¥2,746

    風速計では最安な商品なので全く期待はしていませんが、どんなもの?という好奇心からの注文でし
    した。
    届いた第一印象は、何これ という感じ
    本体から2本のリードが出ているだけのシンプルな構造、プラスチック製のカップも別のプラ機種と
    比べても若干分厚く重そうな感じがする。
    想像した感じでは、
    ・2本のリードしか無い->電源が不要
    ・もしかしてブラシレスモーターか?
    そこで試しに2本のリードに電源電圧を徐々に掛けてみる、すると羽がくるくると時計方向に回転を
    始めた逆に電圧を加えると今度は半時計方向に回り出した。
    確信->ブラシレスモーターが入っている(回路は無いと見た)
    しかし、最初から気になったのは本体に無造作についているねじの事

    気になるねじ

    よく見るとねじが締まっていることにより、プロペラが微妙に偏芯しているような
    これはモーターを無理やり固定しているねじに違いないと思い、緩めてみたらそのとうりでプロペラが
    上方に引き抜くことが出来ました。
    そうしたら案の定ご本尊のブラシモーターが現れました。

    出てきたDCモーター
    上画像は260モーター 良く似ている確信はないが
    後で全く別物と分かる

    モーターに刻印は見つからないが、外見と形状から検索してみると260という型式のモーターが一番近いことが分かった。

    という事で中身が判明致しました。
    今度は、さてこれをどう料理するか?という事になりました。

    1,カップは時計方向しか回らないという事では無いので出力は直流でマイナスにも振れるということを
    考慮する。
    2,ある程度のスパーク的な高電圧も発生するだろうし、ノイズも相当な筈、ADコンバーターに入れる
     前の段階のノイズ処理が重要事項。
    3,低回転付近で信号がノイズに勝たないと回転を検出できない可能性がある、そもそも検出出来る
     のか?。

    説明書もWeb上でのデーターも全く見つからない代物なのでこれを風速計に仕上げるには、実際に風
    を当てて既知の風速計データーと比較決定するしか方法が無い。

    そこで調べて見た
    中国製のブラシモーター(またはDCモーター)を利用した風速計の回転数は、基本的には風速に比例(直線的な比例関係)する。
    これは、風によってプロペラやカップが回る力が風速に比例するためです。しかし、厳密には様々な要因で比例関係からズレが生じるため、実用上の注意点がある。

    1. 比例する理由
    ・回転式(風杯・プロペラ)の原理: 風速計の羽(カップ)は、風が強くなるほど速く回る構造にな
     っている。
    ・DCモーターの特性: ブラシモーターを発電機として利用する場合、その回転数(RPM)に比例した
     出力電圧が得られるため、電圧を測定することで風速を計算できる。
    2. 比例関係からズレる要因
    ・「理論上は比例」しますが、実機では以下の要因で非線形になることがある。
    ・機械的摩擦(フリクション): モーター内部のブラシや軸受(ベアリング)の摩擦が、特に微風時
     に抵抗となり、低い風速域では正確に比例しなくなる(回転数が低くなる)傾向がある。
    ・始動風速の限界: 風が非常に弱い場合、摩擦に負けてモーターが回転せず、0m/sと表示されてしま
     いまう。
    ・慣性(応答性): 風が急激に変化した場合、プロペラの重さにより、実際の風速の変化に回転数が
     追いつかない(応答が遅れる)ことがあります。
    ・空気抵抗: 風が極めて速い場合、空気抵抗が増大し、直線的な比例関係から誤差(負の偏差)が生
     じやすくなる。
    3. 中国製製品における注意点
    ・中国製の安価な風速計(特にDIY向けや低価格な産業用センサ)は、構造が単純なため、上記の
    「摩擦」の影響を受けやすい場合がある。
     校正: 低価格品は個体ごとの摩擦の差が大きく、出荷時の校正が不十分な場合、低風速で誤差が大き
     い可能性があります。
     高精度が必要な場合: 1m/s以下の低風速や、常に変動する気象観測などの高精度な用途には、プロ
     ペラ式ではなく「超音波式」や「熱線式(ホットワイヤー)」が推奨されている。
     結論:
     目安として風速を計測する用途(送風機や換気扇の風速チェックなど)には問題なく使えるが、
     ±1m/s以下の精密な数値が必要な場合は、定期的な校正または信頼性の高い機器の選択が必要。

    以上の事からカップの回転数が糸口と考え回転数と検出電圧の関係を調べる事とした。

    回転数と電圧測定

    Prextor シナノケンシと聞いて懐かしいと思うのは私だけか?
    安価な回転装置を探したらこれが見つかった。
    普段の生活に全く必要のない品物だし高額出費で痛い

    回転数-出力電圧 測定装置
    Plexmotion  発振機内蔵スピードコントローラとステッピングモータと電源
    CSA-UR42D3-PS 購入先 Plexmotionオンラインショップ
    購入価格 ¥34,650
    実験室スタンド
    購入先 アマゾン ¥2,869

    風速計を実験スタンドにセットして風速計の2芯出力をサンワのデジタルマルチメーターで計測した
    ところ
    100rpm->0.1V
    200rpm->0.2V
    300rpm->0.3V
    400rpm->0.4V

    500rpm->0.5V

    1000rpm->1.0V
    1500rpm->1.48V
    1800rpm以上はシナノケンシが対応していないようなので測れませんでした。
    また1000rpm以上になるとステッピングモーターと風速計をゴム状の回転計アタッチメイントで摩擦接
    触させているのでその影響による機械的回転誤差の影響もあり高回転域の測定値自体の信頼性は低くなっている。(恐らく摩擦ロスにより測定値が実際より少なく記録されている可能性がある)
    後半は若干誤差が出ますがほぼ計算されたような値が出てきました、
    いやどうもこれは計算されて作られているようです、すばらしい。
    ちなみに気になる低速の領域
    1rpm->0.001V
    2rpm->0.002V
    3rpm->0.003V
    と極めて正確に線形比例して計測されました。

    喜んだのも束の間で逆回転を調べたら出力レベルが正回転に比べてかなり低いし、540rpm付近から
    グラフ形状が極端に変わり方程式が2つ必要な感じです。
    (正逆非対称)これだと正回転と逆回転で違う計算式を用意する必要が出てきた。

    サンワ デジタルマルチメーターで計測

    実験回路でテスト

    使用部品

    ADS1015使用 PGA機能搭載12bitADコンバーター
    購入先 秋月電子 購入価格 ¥540
    積層セラミックコンデンサー 0.1μF50V C0G 5mm 購入先 秋月電子 購入価格 ¥250 (10)
    ショットキーバリアダイオード 40V1A 1N5819 購入先 秋月電子 購入価格 ¥100 (10)
    小型 金属皮膜抵抗 1/4W1kΩ
    購入先 秋月電子
    購入価格 ¥250 (100)

    その他 ESP-WROOM-02 、 3.3V三端子レギュレーター1Aか以上のもの、DC5Vアダプターなどが必
    要です。

    上記のような回路を組みました。
    ADCは秋月電子製「AE-ADS1015」を使用します。
    I2Cラインのプルアップ抵抗、ADR端子、ALT端子、電源パスコンはそれぞれ基板内部で適切に処理さ
    れているので外付け部品無しでそのまま使用できます。
    (I2Sプルアップ機能を動作させるには内部半田でジャンプ必要)
    アドレスはデフォルトで0x48です。
    モーター出力をADS1015に差動入力します、サージ電圧保護の為のクランプダイオード、ノイズ除去
    のコンデンサ、1kΩと0.1uFで高域ノイズ除去用ローパスフィルタを形成しています。
    スピードコントローラで極めて正確な回転数0-1000rpmを与えたときに読み取れる電圧を取得して電
    圧-回転数の関係を記録します。
    比例関係が成立していれば、ADコンバーターの電圧読み取り値 X 1000 = 回転数RPM で回転数を求め
    風速値変換係数 Kを乗ずれば風速が求まります。(Kは別途の方法で求める)

    実際には微妙に線形が崩れているようなので、1-1000rpmの各点の電圧と回転数を測定をした上で近
    似曲線の方程式を求めます。
    これは最小二乗法を用いて、測定値と近似曲線の誤差(残差)の二乗和が最小になるような多項式を
    自動的に算出する方法になります。
    1000点の手作業のデーター取りですから全部採るには1カ月位は要します。
    それは辛いので10回転ごとに1000回転まで100ポイント計測する事にした。(手抜き)

    それでは以下のスクリプトを実行してデーターを取ります、読み取りはMQTT経由でNODE-REDフロ
    ー内で受信します。
    適当なタイミングで「記録」ボタンをクリックすると測定電圧値がカンマ区切り(cvs方式)で記録され
    ます、記録した結果を曲線方程式を求めるpythonスクリプト内にデーターとして貼り付けを行い
    計算を実行します。
    正転と逆転で大分計測値が違うので方程式は2つ以上必要になります。




    データー収集用スクリプト

    電源を入れて無回転時0.0以外の例えば0.001Vを受信したらスクリプト中のoffset値を-0.001にする
    float offset = -0.001; // 無風時補正(測定値に-0.001が加算されて0.0になる)

    #include <ESP8266WiFi.h>
    #include <PubSubClient.h>
    #include <Wire.h>
    #include <math.h>
    #define ADS1015_ADDR 0x48
    
    // =========================
    // WiFi
    // =========================
    const char* ssid = "your_ssid";
    const char* password = "your_passwd";
    
    // =========================
    // MQTT
    // =========================
    const char* mqttServer = "192.168.1.200";//Your MQTT Server Address
    const int mqttPort = 1883;
    const char* pubTopic = "wind_3v/data";
    
    WiFiClient espClient;
    PubSubClient client(espClient);
    
    unsigned long lastPublish = 0;
    
    // =========================
    // パラメータ(調整用)
    // =========================
    float offset = -0.001;      // 無風時補正(要調整)
    int avgSamples = 100;      // 平均回数
    
    // =========================
    // WiFi
    // =========================
    void setupWiFi() {
      WiFi.config(IPAddress(192,168,1,100),
                  IPAddress(192,168,1,1),
                  IPAddress(255,255,255,0));//Your IP Address
    
      WiFi.begin(ssid, password);
    
      unsigned long start = millis();
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        if (millis() - start > 15000) ESP.restart();
      }
    }
    
    // =========================
    // MQTT
    // =========================
    void reconnect() {
      while (!client.connected()) {
        if (client.connect("wind_3v")) {
          // OK
        } else {
          delay(1000);
        }
      }
    }
    
    // =========================
    // ADS1015
    // =========================
    void writeRegister(uint8_t reg, uint8_t msb, uint8_t lsb) {
      Wire.beginTransmission(ADS1015_ADDR);
      Wire.write(reg);
      Wire.write(msb);
      Wire.write(lsb);
      Wire.endTransmission();
    }
    
    int16_t readADC() {
      Wire.beginTransmission(ADS1015_ADDR);
      Wire.write(0x00);
      Wire.endTransmission();
    
      Wire.requestFrom(ADS1015_ADDR, 2);
      if (Wire.available() < 2) return 0;
    
      int16_t val = (Wire.read() << 8) | Wire.read();
    
      // ADS1015は12bitなので右シフト
      val = val >> 4;
    
      return fabs(val);//絶対値で求めている
    }
    
    // =========================
    // 電圧取得(平均化)
    // =========================
    float readVoltage() {
      float sum = 0;
    
      for (int i = 0; i < avgSamples; i++) {
        sum += readADC()* 0.001;//   ADS1015: 1mV/LSB
        delayMicroseconds(300);
      }
    
      float V = sum / avgSamples;
    
      // オフセット補正
      V += offset;
    
    
      return V;
    }
    
    // =========================
    // setup
    // =========================
    void setup() {
      Wire.begin();
    
      // ADS1015設定
      // 差動 A0-A1 / ±2.048V / 連続 / 3300SPS
      writeRegister(0x01, 0b00000100, 0b11000011);
      setupWiFi();
      client.setServer(mqttServer, mqttPort);
    }
    
    // =========================
    // loop
    // =========================
    void loop() {
    
      if (!client.connected()) reconnect();
      client.loop();
    
      float V = readVoltage();
    
      // rpmに変換
      float rpm = V * 1000.0;//線形動作すると仮定した回転数(参考値)
    
      if (millis() - lastPublish > 1500) {
    
        char payload[256];
        snprintf(payload, sizeof(payload),
          "{\"voltage\":%.4f,\"rpm\":%.1f}",
          V,
          rpm
        );
    
        client.publish(pubTopic, payload);
    
        lastPublish = millis();
      }
    }



    データー収集用 NODE-REDフロー

    [{"id":"80e05128ccf9cad2","type":"tab","label":"風速計 0-3V データー受信フロー","disabled":false,"info":"","env":[]},{"id":"787b35d0a058e59d","type":"mqtt in","z":"80e05128ccf9cad2","name":"","topic":"wind_3v/data","qos":"2","datatype":"auto-detect","broker":"cf1776747da46a7c","nl":false,"rap":true,"rh":0,"inputs":0,"x":210,"y":200,"wires":[["36f09488631e1c32"]]},{"id":"49aad1d32b93c301","type":"debug","z":"80e05128ccf9cad2","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":800,"y":140,"wires":[]},{"id":"7b75e7af0742d1e3","type":"function","z":"80e05128ccf9cad2","name":"電圧 (少数3桁)","func":"var data = msg.payload.voltage;\nmsg.payload = Math.round(data * 1000) / 1000;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":200,"wires":[["49aad1d32b93c301","29674e90f423f542"]]},{"id":"36f09488631e1c32","type":"json","z":"80e05128ccf9cad2","name":"","property":"payload","action":"obj","pretty":false,"x":370,"y":200,"wires":[["7b75e7af0742d1e3","a5561e5f48496b2d"]]},{"id":"91d893bb84d4d3de","type":"function","z":"80e05128ccf9cad2","name":"カンマ区切り 10個目で折り返す","func":"// 現在のカウントを読み込む\nlet count = flow.get('csv_count') || 0;\n\ncount++;\nlet output = msg.payload + \",\"; // 常に数値のあとにカンマを付ける\n\nif (count >= 10) {\n    output += \"\\n\"; // 10個目なら改行コードを追加\n    count = 0;      // カウントをリセット\n}\n\n// カウントを保存\nflow.set('csv_count', count);\n\nmsg.payload = output;\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":560,"y":300,"wires":[["5b2ea7893b6ccf40"]]},{"id":"5b2ea7893b6ccf40","type":"file","z":"80e05128ccf9cad2","name":"","filename":"/home/pi/wind_3v/voltage.csv","filenameType":"str","appendNewline":false,"createDir":false,"overwriteFile":"false","encoding":"none","x":880,"y":300,"wires":[[]]},{"id":"93e6b7d4b0f5a298","type":"inject","z":"80e05128ccf9cad2","name":"データー記録","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"1","payloadType":"num","x":590,"y":80,"wires":[["29674e90f423f542"]]},{"id":"29674e90f423f542","type":"function","z":"80e05128ccf9cad2","name":"ゲート","func":"// 状態を保持する変数(ボタンが押されたかどうか)\nlet ready = flow.get('ready') || false;\n\n// 1. Injectノードからの信号が来た場合\nif (msg.topic === \"trigger\") {\n    flow.set('ready', true);\n    return null; // まだデータは送らない\n}\n\n// 2. データの発生源からデータが来た場合\nif (ready === true) {\n    flow.set('ready', false); // ゲートを閉じる\n    return msg; // データを次のフロー(10列整形)へ送る\n}\n\nreturn null; // ボタンが押されていなければ破棄\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":730,"y":200,"wires":[["91d893bb84d4d3de"]]},{"id":"a5561e5f48496b2d","type":"debug","z":"80e05128ccf9cad2","name":"debug 4","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":560,"y":160,"wires":[]},{"id":"cf1776747da46a7c","type":"mqtt-broker","name":"","broker":"192.168.1.200","port":1883,"clientid":"","autoConnect":true,"usetls":false,"protocolVersion":4,"keepalive":60,"cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""}]

    近似2次方程式を求める(Jupyter note book上で行う作業)

    回転数に対する電圧の読みを記録します。
    最低でも0-1000回転程度は必要です。
    以下のスクリプトを実行して多項式を求めます。
    その2次方程式を風速計スクリプトに組み込み完成です。
    正転用と逆転用の2つ(今回のものは3つ必要)が必要です。

    正転用(時計回り) pythonスクリプト

    測定した電圧値データーを#電圧voltage = np.array([])内にコピペする。
    (先頭に0.0,を追加する事を忘れないこと)
    回転数はrpm = np.arange(先頭値,最終値+1,間隔値)で入力する。

    !python3 -m pip install --upgrade pip
    !pip install -U scikit-learn
    !pip install matplotlib
    
    from sklearn.linear_model import LinearRegression
    import numpy as np
    import matplotlib.pyplot as plt
    from sklearn.linear_model import LinearRegression
    from sklearn.preprocessing import PolynomialFeatures
    from sklearn.pipeline import make_pipeline
    from sklearn.metrics import r2_score
    
    # 1. データの準備 
    # 回転数(RPM) - 0-1000 0から1000迄10おき
    rpm = rpm = np.arange(0, 1001, 10)
                             
    # 電圧(V)
    voltage = np.array([0.0,0.009,0.017,0.031,0.039,0.045,0.063,0.065,0.078,0.087,0.096,
    0.107,0.116,0.124,0.138,0.142,0.152,0.166,0.171,0.183,0.194,
    0.203,0.212,0.224,0.23,0.246,0.247,0.258,0.275,0.28,0.293,
    0.299,0.308,0.316,0.33,0.339,0.34,0.357,0.362,0.374,0.379,
    0.391,0.401,0.412,0.416,0.424,0.436,0.44,0.451,0.46,0.476,
    0.484,0.492,0.501,0.499,0.515,0.513,0.528,0.537,0.549,0.553,
    0.571,0.574,0.589,0.596,0.602,0.611,0.623,0.623,0.632,0.637,
    0.642,0.651,0.647,0.653,0.656,0.655,0.662,0.669,0.674,0.687,
    0.69,0.699,0.697,0.709,0.714,0.715,0.721,0.729,0.73,0.737,
    0.746,0.751,0.755,0.763,0.773,0.776,0.784,0.795,0.794,0.795,])       
    
     # 重要:電圧→RPM の式を作る
    X = voltage.reshape(-1, 1)
    y = rpm
    
    # 2次式でフィッティング
    model = make_pipeline(PolynomialFeatures(degree=2), LinearRegression())
    model.fit(X, y)
    
    # 係数取得
    lin_reg = model.named_steps['linearregression']
    coef = lin_reg.coef_
    intercept = lin_reg.intercept_
    
    print("----- 変換式 -----")
    print(f"RPM = {coef[2]:.8f} * V^2 + {coef[1]:.8f} * V + {intercept:.8f}")
    
    # 精度
    rpm_pred = model.predict(X)
    r2 = r2_score(y, rpm_pred)
    print("R2 =", r2)
    
    # グラフ
    v_range = np.linspace(min(voltage), max(voltage), 200).reshape(-1, 1)
    rpm_range = model.predict(v_range)
    
    plt.scatter(voltage, rpm, color='red', s=5)
    plt.plot(v_range, rpm_range, color='blue')
    plt.xlabel("Voltage (V)")
    plt.ylabel("RPM")
    plt.grid(True)
    plt.show()

    実行例

    正方向(時計回り)

    逆転用(反時計回り)

    1回目の結果

    ここれは。。。悲しい感じの結果となった。
    曲線形状が0~400rpm付近迄は比較的直線的だが、400~1000rpmあたりは見た目にも完全な2次曲線
    になっています。

    しょうが無いので逆回転のみ0~400rpmと401~1000rpmの方程式を分けて2つ作ることにした。

    逆転用(0-400rpmのグラフ(1回目の結果を400回転で分けた)

    逆転用(401-1000rpmのグラフ(1回目の結果を400回転で分けた)





    2次方程式のスクリプトへの組み込み方法について

    正回転の場合の例 求めた値を代入する

    // RPM = a*V^2 + b*V + c
    #define A1_COEF 10681.36804861
    #define B1_COEF -1773.02825101
    #define C1_COEF 119.59619983
    #define K_WIND 0.018   // RPM → m/s 現在は仮の値です
    
    //
    //
    //
    
    float voltage = readVoltage();
    
    // ---------- ここが変換核心 ----------
    float rpm = A1_COEF * voltage * voltage
                  + B1_COEF * voltage
                  + C1_COEF;
    
    if (rpm < 0) rpm = 0;
        float wind_speed = rpm * K_WIND;
        
        









    完成品は以下の様になります。
    Nomal TypeとProの2とうりあります。

    0-3.8Vタイプ風速計 完成スクリプト Nomal Type

    K値は仮の値なので実際に実験して求める必要があります。
    身近な方法では車で一定速度で走行してその時の風速計の回転数(RPM)から K = 風速/RPM
    で求める方法です。
    運転速度と風速の関係は、例: 時速40km -> 11.11m/s なので この場合 K = 11.11/RPMで求めます。
    この部分に設置する。 #define K_WIND 0.018

    風速計停止の状態で風速が0になるようここを設定して下さい -> #define ZERO_OFFSET   -0.001

    #include <ESP8266WiFi.h>
    #include <PubSubClient.h>
    #include <Wire.h>
    #include <math.h>
    
    #define ADS1015_ADDR 0x48
    #define LSB_VOLTAGE   0.001 //ADS1015: 1mV/LSB
    #define LED 16
    
    unsigned long lastSample = 0;
    uint8_t count = 0;
    bool dir = true;//モーター右回転->true 左回転->false
    bool status = true;
    
    // =========================
    // パラメータ(調整用)
    // =========================
    #define ZERO_OFFSET   -0.001   // 無風オフセット(要調整)
    int avgSamples = 100;       // 平均回数
    #define K_WIND 0.018        // RPM → m/s 現在は仮の値です
    float point = 0.27;        //L回転 関数切替ポイント 400rpm(0.27V)
    
    // =========================
    // 2次関数
    // RPM = a*V^2 + b*V + c
    // =========================
    
    #define A1_COEF 568.44271651
    #define B1_COEF 745.50276351
    #define C1_COEF 23.93921350
    
    #define A2_COEF 2512.58376334
    #define B2_COEF 639.18639503
    #define C2_COEF 12.12971571
    
    #define A3_COEF 84689.25132864
    #define B3_COEF -47637.29654340
    #define C3_COEF 7134.56500783
    
    
    // =========================
    // WiFi
    // =========================
    #define WIFI_SSID "your_ssid"
    #define WIFI_PWD  "your_passwd"
    const char* mqtt_server = "192.168.1.200";//Your MQTT Server Address
    const int mqttPort = 1883;
    WiFiClient espClient;
    PubSubClient client(espClient);
    
    // =========================
    // WiFi
    // =========================
    void setupWiFi() {
      WiFi.config(IPAddress(192,168,1,100),
                  IPAddress(192,168,1,1),
                  IPAddress(255,255,255,0));//Your IP Address
      WiFi.begin(WIFI_SSID, WIFI_PWD);
    
      unsigned long start = millis();
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        if (millis() - start > 15000) ESP.restart();
      }
    }
    
    // =========================
    // MQTT
    // =========================
    void reconnect() {
      while (!client.connected()) {
        if (client.connect("wind_speed")) {
          // OK
        } else {
          delay(1000);
        }
      }
    }
    
    // =========================
    // ADS1015
    // =========================
    void writeRegister(uint8_t reg, uint8_t msb, uint8_t lsb) {
      Wire.beginTransmission(ADS1015_ADDR);
      Wire.write(reg);
      Wire.write(msb);
      Wire.write(lsb);
      Wire.endTransmission();
    }
    
    float readVoltageRaw() {
      Wire.beginTransmission(ADS1015_ADDR);
      Wire.write(0x00);
      Wire.endTransmission();
    
      Wire.requestFrom(ADS1015_ADDR, 2);
      if (Wire.available() < 2) return 0;
    
      int16_t raw = (Wire.read() << 8) | Wire.read();
    
      // ADS1015は12bitなので右シフト
      raw = raw >> 4;
      dir = (raw < 0) ? false : true;
      
      return fabs(raw * LSB_VOLTAGE);//   ADS1015: 1mV/LSB
    }
    
    // =========================
    // 電圧取得(平均化)
    // =========================
    float readVoltage() {
      float sum = 0;
    
      status = dir;
      for (int i = 0; i < avgSamples; i++) {
        sum += readVoltageRaw();
        delayMicroseconds(300);
      }
      if (dir != status) sum = 0;
    
      float v = sum / avgSamples;
    
      // オフセット補正
      v += ZERO_OFFSET;
    
      return v;
    }
    
    
    // =========================
    // 2次方程式 voltage -> rpm
    // =========================
    float conv (float voltage){
      float r;
      if (dir){
             r = A1_COEF * voltage * voltage
                  + B1_COEF * voltage
                  + C1_COEF;
      }else{
        if(voltage <= point){ //0-400rpm
             r = A2_COEF * voltage * voltage
                  + B2_COEF * voltage
                  + C2_COEF;
        }else{ //401-1000rpm
             r = A3_COEF * voltage * voltage
                  + B3_COEF * voltage
                  + C3_COEF;
        }
      }
      return r;
    }
    
    
    // =========================
    // setup
    // =========================
    void setup() {
    
      Serial.begin(115200);
      Wire.begin(4, 5);
    
      // ADS1015設定
      // 差動 A0-A1 / ±2.048V / 連続 / 3300SPS
      writeRegister(0x01, 0b00000100, 0b11000011);
      setupWiFi();
      client.setServer(mqtt_server, mqttPort);
      pinMode(LED, OUTPUT);
    }
    
    
    
    // =========================
    // loop
    // =========================
    void loop() {
    
      if (!client.connected()) reconnect();
      client.loop();
    
      if (millis() - lastSample >= 1000) {
        float voltage = readVoltage();
        // rpmに変換
        float rpm = conv (voltage);
        float wind_speed = rpm * K_WIND;
    
        if (wind_speed < 0.5) wind_speed = 0;
        // ------------------------------------
    
        count++;
        if (count > 3) {
          char msg[16];
          dtostrf(wind_speed, 6, 2, msg);
    
          client.publish("wind_speed", msg);
    
          Serial.print("V=");
          Serial.print(voltage, 4);
          Serial.print(" RPM=");
          Serial.print(rpm, 1);
          Serial.print(" Wind=");
          Serial.println(msg);
    
          count = 0;
        }
    
        digitalWrite(LED, !digitalRead(LED));
        lastSample = millis();
      }
    }


    受信用NODE-RED フロー


    ダッシュボード2

    こんな感じです
    [{"id":"c31a01828a7d6717","type":"tab","label":"0-3.8V 式風速計","disabled":false,"info":"","env":[]},{"id":"74c02f018504dd99","type":"mqtt in","z":"c31a01828a7d6717","name":"","topic":"wind_speed","qos":"2","datatype":"auto-detect","broker":"61e45e3515b67f80","nl":false,"rap":true,"rh":0,"inputs":0,"x":190,"y":200,"wires":[["0f0fe984d5cf198c","4d6fca769c847061"]]},{"id":"32d1e716c05ef4f4","type":"ui-text","z":"c31a01828a7d6717","group":"a2862a7d5d758378","order":1,"width":0,"height":0,"name":"風速","label":"風速","format":"{{msg.payload}}","layout":"row-spread","style":true,"font":"Arial,Arial,Helvetica,sans-serif","fontSize":"24","color":"#fafafa","wrapText":false,"className":"","value":"payload","valueType":"msg","x":530,"y":200,"wires":[]},{"id":"0f0fe984d5cf198c","type":"debug","z":"c31a01828a7d6717","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":440,"y":340,"wires":[]},{"id":"4d6fca769c847061","type":"function","z":"c31a01828a7d6717","name":"単位付与","func":"var temp = msg.payload;\nmsg.payload = temp + \"m/s\";\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":200,"wires":[["32d1e716c05ef4f4"]]},{"id":"61e45e3515b67f80","type":"mqtt-broker","name":"","broker":"192.168.1.200","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":4,"keepalive":60,"cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"a2862a7d5d758378","type":"ui-group","name":"風速","page":"cef20d4917f1c9a7","width":6,"height":1,"order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"cef20d4917f1c9a7","type":"ui-page","name":"気象","ui":"debf650e7f1343d3","path":"/page1","icon":"home","layout":"grid","theme":"a45c7f2adf09be21","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"debf650e7f1343d3","type":"ui-base","name":"画面名","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":1,"showDisconnectNotification":true,"allowInstall":true},{"id":"a45c7f2adf09be21","type":"ui-theme","name":"気象","colors":{"surface":"#505050","primary":"#505050","bgPage":"#505050","groupBg":"#5f5f5f","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"c683216e03e10ce1","type":"global-config","env":[],"modules":{"@flowfuse/node-red-dashboard":"1.30.2"}}]



    0-3.8Vタイプ風速計 完成スクリプト Pro仕様

    K値は仮の値なので実際に実験して求める必要があります。
    身近な方法では車で一定速度で走行してその時の風速計の回転数(RPM)から K = 風速/RPM
    で求める方法です。
    運転速度と風速の関係は、例: 時速40km -> 11.11m/s なので この場合 K = 11.11/RPMで求めます。
    この部分に設置する。 #define K_WIND 0.018

    風速計停止の状態で風速が0になるようここを設定して下さい -> #define ZERO_OFFSET   0.013

    3秒平均・10分平均・最大瞬間風速+ JSON
    更に機能を増やしたタイプです。

    送信JSON例
    {
    “wind_now”: 3.21,
    “wind_3s”: 3.05,
    “wind_10m”: 2.88,
    “gust_10m”: 6.42
    }

    #include <ESP8266WiFi.h>
    #include <PubSubClient.h>
    #include <Wire.h>
    #include <math.h>
    
    #define ADS1015_ADDR 0x48
    #define LSB_VOLTAGE   0.001 //ADS1015: 1mV/LSB
    #define LED 16
    
    unsigned long lastSample = 0;
    uint8_t count = 0;
    bool dir = true;//モーター右回転->true 左回転->false
    bool status = true;
    
    // =========================
    // パラメータ(調整用)
    // =========================
    #define ZERO_OFFSET   0.001   // 無風オフセット(要調整)
    int avgSamples = 100;       // 平均回数
    #define K_WIND 0.018        // RPM → m/s 現在は仮の値です
    float point = 0.27;        //L回転 関数切替ポイント 400rpm(0.27V)
    
    // =========================
    // 2次関数
    // RPM = a*V^2 + b*V + c
    // =========================
    
    #define A1_COEF 568.44271651
    #define B1_COEF 745.50276351
    #define C1_COEF 23.93921350
    
    #define A2_COEF 2512.58376334
    #define B2_COEF 639.18639503
    #define C2_COEF 12.12971571
    
    #define A3_COEF 84689.25132864
    #define B3_COEF -47637.29654340
    #define C3_COEF 7134.56500783
    
    // =========================
    // WiFi
    // =========================
    #define WIFI_SSID "your_ssid"
    #define WIFI_PWD  "your_passwd"
    const char* mqtt_server = "192.168.1.200";//Your MQTT Server Address
    const int mqttPort = 1883;
    WiFiClient espClient;
    PubSubClient client(espClient);
    
    // =========================
    // WiFi
    // =========================
    void setupWiFi() {
      WiFi.config(IPAddress(192,168,1,100),
                  IPAddress(192,168,1,1),
                  IPAddress(255,255,255,0));//Your IP Address
      WiFi.begin(WIFI_SSID, WIFI_PWD);
    
      unsigned long start = millis();
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        if (millis() - start > 15000) ESP.restart();
      }
    }
    
    // =========================
    // MQTT
    // =========================
    void reconnect() {
      while (!client.connected()) {
        if (client.connect("weather")) {
          // OK
        } else {
          delay(1000);
        }
      }
    }
    
    
    // =========================
    // 平均用
    // =========================
    float wind_3s_buf[3];
    float wind_10m_buf[600];
    
    int idx3 = 0;
    int idx10 = 0;
    
    float gust_max = 0;
    
    
    // =========================
    // ADS1015
    // =========================
    void writeRegister(uint8_t reg, uint8_t msb, uint8_t lsb) {
      Wire.beginTransmission(ADS1015_ADDR);
      Wire.write(reg);
      Wire.write(msb);
      Wire.write(lsb);
      Wire.endTransmission();
    }
    
    float readVoltageRaw() {
      Wire.beginTransmission(ADS1015_ADDR);
      Wire.write(0x00);
      Wire.endTransmission();
    
      Wire.requestFrom(ADS1015_ADDR, 2);
      if (Wire.available() < 2) return 0;
    
      int16_t raw = (Wire.read() << 8) | Wire.read();
    
      // ADS1015は12bitなので右シフト
      raw = raw >> 4;
      dir = (raw < 0) ? false : true;
      
      return fabs(raw * LSB_VOLTAGE);//   ADS1015: 1mV/LSB
    }
    
    
    
    // =========================
    // 電圧取得(平均化)
    // =========================
    float readVoltage() {
      float sum = 0;
    
      status = dir;
      for (int i = 0; i < avgSamples; i++) {
        sum += readVoltageRaw();
        delayMicroseconds(300);
      }
      if (dir != status) sum = 0;
    
      float v = sum / avgSamples;
    
      // オフセット補正
      v += ZERO_OFFSET;
    
      return v;
    }
    
    // =========================
    // 平均計算
    // =========================
    float average(float *buf, int size) {
      float sum = 0;
      for (int i = 0; i < size; i++) sum += buf[i];
      return sum / size;
    }
    
    
    
    // =========================
    // 2次方程式 voltage -> rpm
    // =========================
    float conv (float voltage){
      float r;
      if (dir){
             r = A1_COEF * voltage * voltage
                  + B1_COEF * voltage
                  + C1_COEF;
      }else{
        if(voltage <= point){ //0-400rpm
             r = A2_COEF * voltage * voltage
                  + B2_COEF * voltage
                  + C2_COEF;
        }else{ //401-1000rpm
             r = A3_COEF * voltage * voltage
                  + B3_COEF * voltage
                  + C3_COEF;
        }
      }
      return r;
    }
    
    // =========================
    // setup
    // =========================
    void setup() {
    
      Serial.begin(115200);
      Wire.begin(4, 5);
    
      // ADS1015設定
      // 差動 A0-A1 / ±2.048V / 連続 / 3300SPS
      writeRegister(0x01, 0b00000100, 0b11000011);
      setupWiFi();
      client.setServer(mqtt_server, mqttPort);
      pinMode(LED, OUTPUT);
    
      // バッファ初期化
      for(int i=0;i<3;i++) wind_3s_buf[i]=0;
      for(int i=0;i<600;i++) wind_10m_buf[i]=0;
    }
    
    // =========================
    // loop
    // =========================
    void loop() {
    
      if (!client.connected()) reconnect();
      client.loop();
    
      if (millis() - lastSample >= 1000) {
        float voltage = readVoltage();
        // rpmに変換
        float rpm = conv (voltage);
        float wind_now = rpm * K_WIND;
    
        if (wind_now < 0.5) wind_now = 0;
    
        // 最大瞬間風速
        if (wind_now > gust_max) gust_max = wind_now;
    
        // バッファ保存
        wind_3s_buf[idx3] = wind_now;
        wind_10m_buf[idx10] = wind_now;
    
        idx3 = (idx3 + 1) % 3;
        idx10 = (idx10 + 1) % 600;
    
        float wind_3s = average(wind_3s_buf, 3);
        float wind_10m = average(wind_10m_buf, 600);
    
        // JSON作成
        char json[128];
        snprintf(json, sizeof(json),
          "{\"wind_now\":%.2f,\"wind_3s\":%.2f,\"wind_10m\":%.2f,\"gust_10m\":%.2f}",
          wind_now, wind_3s, wind_10m, gust_max);
    
        client.publish("weather/wind", json);
    
        Serial.println(json);
    
        digitalWrite(LED, !digitalRead(LED));
        lastSample = millis();
      }
    }






    受信用NODE-RED フロー

    こんな感じです

    フロー

    [{"id":"a1acabfbc3a80144","type":"tab","label":"0-3.8V 式風速計","disabled":false,"info":"","env":[]},{"id":"8f281f5aae74e438","type":"mqtt in","z":"a1acabfbc3a80144","name":"","topic":"weather/wind","qos":"2","datatype":"auto-detect","broker":"61e45e3515b67f80","nl":false,"rap":true,"rh":0,"inputs":0,"x":190,"y":200,"wires":[["70e10f970868701c","24ebe8f3dbf7c1d0"]]},{"id":"70e10f970868701c","type":"json","z":"a1acabfbc3a80144","name":"","property":"payload","action":"obj","pretty":false,"x":370,"y":200,"wires":[["b962b7ddda0979a9","85f1ef230ca1b9f1","0fd12f9e1cd7cdcc","a42fed982fd59f07"]]},{"id":"b962b7ddda0979a9","type":"function","z":"a1acabfbc3a80144","name":"wind3s","func":"var temp = msg.payload.wind_3s;\nmsg.payload = temp + \" m/s\";\nreturn msg;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":200,"wires":[["92f3df073f1cb6e2"]]},{"id":"85f1ef230ca1b9f1","type":"function","z":"a1acabfbc3a80144","name":"avg10","func":"var temp = msg.payload.wind_10m;\nmsg.payload = temp + \" m/s\";\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":240,"wires":[["1fe9ce9dfe6305c6"]]},{"id":"0fd12f9e1cd7cdcc","type":"function","z":"a1acabfbc3a80144","name":"max","func":"var temp = msg.payload.gust_10m;\nmsg.payload = temp + \" m/s\";\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":280,"wires":[["d2567d54a95a6d62"]]},{"id":"92f3df073f1cb6e2","type":"ui-text","z":"a1acabfbc3a80144","group":"a2862a7d5d758378","order":2,"width":0,"height":0,"name":"3秒平均風速","label":"3秒平均風速","format":"{{msg.payload}}","layout":"row-spread","style":true,"font":"Arial,Arial,Helvetica,sans-serif","fontSize":"24","color":"#fafafa","wrapText":false,"className":"","value":"payload","valueType":"msg","x":690,"y":200,"wires":[]},{"id":"1fe9ce9dfe6305c6","type":"ui-text","z":"a1acabfbc3a80144","group":"a2862a7d5d758378","order":3,"width":0,"height":0,"name":"10分平均風速","label":"10分平均風速","format":"{{msg.payload}}","layout":"row-spread","style":true,"font":"Arial,Arial,Helvetica,sans-serif","fontSize":"24","color":"#fafafa","wrapText":false,"className":"","value":"payload","valueType":"msg","x":700,"y":240,"wires":[]},{"id":"d2567d54a95a6d62","type":"ui-text","z":"a1acabfbc3a80144","group":"a2862a7d5d758378","order":4,"width":0,"height":0,"name":"10分最大風速","label":"10分最大風速","format":"{{msg.payload}}","layout":"row-spread","style":true,"font":"Arial,Arial,Helvetica,sans-serif","fontSize":"24","color":"#fafafa","wrapText":false,"className":"","value":"payload","valueType":"msg","x":700,"y":280,"wires":[]},{"id":"24ebe8f3dbf7c1d0","type":"debug","z":"a1acabfbc3a80144","name":"debug 1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":440,"wires":[]},{"id":"a42fed982fd59f07","type":"debug","z":"a1acabfbc3a80144","name":"debug 2","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":480,"y":360,"wires":[]},{"id":"61e45e3515b67f80","type":"mqtt-broker","name":"","broker":"192.168.1.200","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":4,"keepalive":60,"cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"a2862a7d5d758378","type":"ui-group","name":"風速","page":"cef20d4917f1c9a7","width":6,"height":1,"order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"cef20d4917f1c9a7","type":"ui-page","name":"気象","ui":"debf650e7f1343d3","path":"/page1","icon":"home","layout":"grid","theme":"a45c7f2adf09be21","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"debf650e7f1343d3","type":"ui-base","name":"画面名","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":1,"showDisconnectNotification":true,"allowInstall":true},{"id":"a45c7f2adf09be21","type":"ui-theme","name":"気象","colors":{"surface":"#505050","primary":"#505050","bgPage":"#505050","groupBg":"#5f5f5f","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"2bdec586c1d32b58","type":"global-config","env":[],"modules":{"@flowfuse/node-red-dashboard":"1.30.2"}}]

    組み立て

    未来工業 PVKボックス
    PVK-BOK 
    購入先 アマゾン ¥703
    蓋を底にして取付用のM4ねじ穴を4か所あける
    後ろ面にケーブル用の穴2箇所開ける 
    前面にLED用の穴あける
    完成品
    ステンレススリムヘッド小ねじ M4×10 (10本入り) 
    購入先 アマゾン ¥576 (10本)
    ステンレス鋼製 M4x0.7mm ギザギザ付きフランジ六角ロックナット
    購入先 アマゾン ¥399 (12本)
    ネオジム磁石 耐荷重8kg
    直径20mm 
    購入先 アマゾン ¥ 1,699 (10個)
    100 x 100 x1.6mm厚鉄板
    購入先 横山テクノ
    購入価格 ¥370
    鉄板を裏面にM4 2cmx2本
    皿ねじで固定する
    トラスコ中山 コルゲートチューブ 内径7.4mm x10m
    電源ケーブル配線カバーに
    使用 
    購入先 アマゾン ¥1,209
    固定台側にネオジム磁石
    x2個をM4ねじで固定する

    未来工業のケースを使用します。
    防水とは言ってないですが、特に問題は無いでしょう。
    上部のねじ穴はパテで塞ぎます。
    逆さにしたので底部をシリコンなどで塞ぎます。
    風速計からのケーブルは、最短でカット接続します、引っ張りまわすとノイズが乗ります。
    天板の風速計固定ねじはM4x10mm x4本 後部ケーブル穴は風速計用5mmφ 電源ケーブル用は
    内径7.4mmのコルゲートチューブが入るサイズに。
    前面LED穴は3mmφ

    固定用に100x100x1.6mmの鉄板を風速計の裏面にM4皿ねじ2本で固定する。
    固定台側にはネオジム磁石2個をM4ねじで固定する。

    完成品と運用の様子

    製作後の感想

    手間のかかる子程かわいいとは良く言ったものでこの風速計は単純構造で、使えるものにするには非
    常に手間がかかる代物だが、同時に学べる部分も多く、製作前よりも好感を持てるようになった。
    ブラシモーターの平均寿命は3000時間程度らしいので半年後位に使えなくなる可能性もあるので換え
    用のモーターは存在するのか調べて見た。
    外見は「206」というモーターに似ているので本家マブチモーターの206と中国製206モーター両方
    を取り寄せて互換性の有り無しを確認した。
    結果は、全く別物でした。
    もし、マブチの206を補修に使用する場合には、物理的サイズは全く同じなので問題は無いが、特性
    が異なるのでAMPのゲインから調整する必要がある、マブチモーター206の方が電圧が低い(1/3位)の
    で増幅度をもう少し上げる必要がある。
    特性についても未知なので1から測定して近似方程式を求める必要がある。
    そこまでする人は居ないはずなので(私以外は)新しいのを購入した方が早い。
    正回転のみで良いならかなり線形性があるので回転数はx1000でも差ほど誤差は無いと思う、ただ
    し、風速計は残念ながら逆にも回るのでこの結果はちょっと残念。
    一応作ったもののこの風速計の正確性はかなり?なので特にお勧めはしない機種でした。

    既知の風速によるK値の測定はまだだが、サンバートラックに風速計を付けて走っているのを見かけ
    たら間違いなく私です。

    今迄すべての方式の中国製風速計を購入して実際に製作して来たなかでこの風速計プロジェクトに残
    る最後の課題は「風洞実験」だ。
    まさかそこまではやらないと思うが?