×

ノイズ&温度モニター改め、タイトルを「オーディオヘルスモニター」へ改題しました。

どういうことかというと、チャッピーにそそのかされて、

Roon ROCKに限定せず、オーディオシステム全体のウェルネスを監視しようという、壮大かつ楽しげなスコープが生まれてしまったのです。例を挙げると、

  • 部屋の温湿度
  • パワーアンプ放熱器の温度
  • オーディオラック上・中・下段の温度
  • パワーアンプ出力端のDCオフセット量
  • 現在PLAY中の曲情報
  • 曲の再生音圧レベル (@500Hz) 等々。

モニタリング対象は、きりがない。本当にヘルスモニタリングするという用途以上に、楽しいから拡張する、に近い。

温度センサー選定

今回利用する外部温度センサーは、DHT11というもの。
ピン配置は画像のとおり「S, DC+, DC-」です。

ESP32スターターキットの中に付いてきたもの。ちなみにAruduinoスターターキットの中に入っていたセンサーもこれと同じ。つまり、初学の定番中の定番なのでしょう。

チャッピーも、最終的な精度には限界があるものの、手習いとしてはこれで十分と言っています。室温常温で使う限り、絶対精度もそんなに悪くない。(水銀型温度計とほぼ同じ温度を出してきます)

私が使っている ESP32 DevKit V1のピンアサインです。ひとくちにESP32といっても、型番にはかなり種類があるようで。個々において、サイズもピン配列も大きく違うのです。だからPIN確認は大事。

DHT11においてそれぞれ、

  • – : GND
  • + : 3V3
  • S : GPIO13

と繋ぎます。このセンサーの動作電位は+3.3vということ。
最初はシグナルをGPIO4へアサインしたのですが、これがなぜかうまく動かない。深追いすると例によってハマりそうだったので、とっととGPIO13へ移設しました。

DHTは黙っていて動くわけでもない。このセンサーを動かすためにはライブラリの追加インストールも必要です。
組み込み開発ではこのように、必要になったライブラリをどんどん追加でインストールしていくという形です。

今回は [DHT sensor] と入力して検索。[DHT sensor library by Adafruit] というライブラリを探して[インストール]します。

ESP32もAruduinoもオープンソースですから、プロの作ったものもその辺のお兄さんが自作したライブラリも等質かつ大量に転がっているわけです。特にDHT11のような標準品には大量の対象ライブラリがあります。その中から何を選ぶのか?

Adafruit とは、有名な大手開発元だそうです。利用実績も最多。だから、初学で使うならば実績のある大手ライブラリを選んでおけばまずは大丈夫、というわけですね。

ブレッドボードに載せて配線していきます。
無理に刺したんで、パーツをPCB上でひんまげてしまいました(笑)でも気にしない。あくまでこれは試作ですから。

ブレッドボードは裏側で図のような短絡がされており。この結線状態を想像しながら配線をしていけばOKです。
 

キットにはフックアップケーブルが大量に添付されているからそれを使って結線する。

簡単なコードを書いて動作確認します。
ウン、きちんと「Temperture」と「Humidity」の値が返ってきました。

では本番。いよいよこのセンサーの数値をウェブダッシュボード上へ反映していきましょう。

思えば、「ROCK近傍の温度とファンノイズを測りたい」から始まったプロジェクト。温度計ができてしまうと、目的の半分は完遂できたことになります。
 

センサー値をウェブ投影

最初に DHT.h ライブラリをインクルードします。こうして、プログラムは肥大化していくわけです。
完成形からお見せします。コード全文。

// Audio_Monitor_R2.ino
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <DHT.h>

// ===== WiFi設定 =====
const char* ssid     = "私のSSID";
const char* password = "私のパスワード";

// ===== DHT11 =====

#define DHTPIN 13
#define DHTTYPE DHT11

DHT dht(DHTPIN, DHTTYPE);

float currentTemp = 0;
float currentHumidity = 0;

String tempStatus = "OK";

const float TEMP_WARNING = 30.0;
const float TEMP_ERROR   = 35.0;

// ===== Roon状態変数 =====
String roonStatus     = "UNKNOWN";
String roonVersion    = "-";
String roonUptimeText = "-";
String rockVersion    = "-";
String rockUptime     = "-";
String storageInfo    = "-";
String storageFree    = "-";
String rockIP         = "-";

unsigned long lastRoonCheck = 0;

// ===== Webサーバー =====
WebServer server(80);

// =====================================
// uptime秒 → xxd xxh
// =====================================
String formatUptime(unsigned long sec)
{
  int days  = sec / 86400;
  int hours = (sec % 86400) / 3600;

  return String(days) + "d " +
         String(hours) + "h";
}

// =====================================
// Roon状態取得
// =====================================
void updateRoonStatus()
{
  HTTPClient http;

  http.begin("http://<ROCKのIP-Adress>/1/getstate");

  http.addHeader(
    "Content-Type",
    "application/x-www-form-urlencoded"
  );

  int httpCode = http.POST("");

  if (httpCode > 0)
  {
    String payload = http.getString();

    Serial.println("===== ROON JSON =====");
    Serial.println(payload);
    Serial.println("=====================");

    DynamicJsonDocument doc(8192);

    DeserializationError error =
      deserializeJson(doc, payload);

    if (!error)
    {
      bool running =
        doc["data"]["state"]["roon_running"];

      roonStatus =
        running ? "ONLINE" : "OFFLINE";

      roonVersion =
        doc["data"]["state"]["roon_version"]
          .as<String>();

      rockVersion =
        doc["data"]["device_display_version"]
          .as<String>();

      rockIP =
        doc["data"]["interfaces"][0]["state"]["ip"]
          .as<String>();

      unsigned long rockSec =
        String(doc["data"]["state"]["uptime"])
          .toInt();

      unsigned long roonSec =
        String(doc["data"]["state"]["roon_uptime"])
          .toInt();

      rockUptime =
        formatUptime(rockSec);

      roonUptimeText =
        formatUptime(roonSec);

      storageInfo =
        doc["data"]["state"]
           ["internal_storage"]["info"]
          .as<String>();

      uint64_t used =
        doc["data"]["state"]
           ["internal_storage"]["data_used"];

      uint64_t total =
        doc["data"]["state"]
           ["internal_storage"]["data_total"];

      float freeGB =
        (float)(total - used)
        / 1024.0
        / 1024.0;

      float totalGB =
        (float)total
        / 1024.0
        / 1024.0;

      int percent =
        ((total - used) * 100) / total;

      storageFree =
        String((int)freeGB)
        + " GB / "
        + String((int)totalGB)
        + " GB ("
        + String(percent)
        + "% available)";
    }
    else
    {
      roonStatus = "JSON ERROR";
    }
  }
  else
  {
    roonStatus = "NO RESPONSE";
  }

  http.end();
}

// =====================================
// DHT11取得
// =====================================

void updateEnvironment()
{
  float h = dht.readHumidity();
  float t = dht.readTemperature();

  if (!isnan(h) && !isnan(t))
  {
    currentHumidity = h;
    currentTemp = t;
  }
  
  if (t >= TEMP_ERROR)
  {
    tempStatus = "ERROR";
  }
  else if (t >= TEMP_WARNING)
  {
    tempStatus = "WARNING";
  }
  else
  {
    tempStatus = "OK";
  }
}

// =====================================
// トップページ
// =====================================
void handleRoot()
{
  String html;

  html += "<html>";
  html += "<head>";

  html += "<title>Audio System Monitoring</title>";

  html += "<meta http-equiv='refresh' content='30'>";

  html += "<style>";

  html += "body{font-family:Arial;margin:20px;}";

  html += "table{width:100%;border-collapse:collapse;}";

  html += "td{padding:8px;border-bottom:1px solid #cccccc;}";

  html += ".ok{color:green;font-weight:bold;}";
  html += ".ng{color:red;font-weight:bold;}";
  html += ".warning{color:orange;font-weight:bold;}";

  html += "</style>";

  html += "</head>";
  html += "<body>";

  html += "<h1>Audio System Monitoring</h1>";

  html += "<hr>";
  html += "<img src = 'https://keroyon-audio.com/wp-content/uploads/2026/06/audio_dashboard.jpg' width=100%>";

  html += "<h2>System Health : ";

  if (roonStatus == "ONLINE")
  {
    html += "<span class='ok'>OK</span>";
  }
  else
  {
    html += "<span class='ng'>ERROR</span>";
  }

  html += "</h2>";

  html += "<p>Audit since update : ";
  html += String(millis() / 60000);
  html += " min.</p>";

  html += "<h2>Roon ROCK</h2>";

  html += "<table>";

  html += "<tr>";
  html += "<td>Roon OS</td>";
  html += "<td class='ok'>OK</td>";
  html += "<td>Version : ";
  html += rockVersion;
  html += " Running : ";
  html += rockUptime;
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>Roon Server</td>";
  html += "<td class='ok'>OK</td>";
  html += "<td>Version : ";
  html += roonVersion;
  html += " Running : ";
  html += roonUptimeText;
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>Music Storage</td>";
  html += "<td class='ok'>OK</td>";
  html += "<td>";
  html += storageFree;
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>ROCK IP Address</td>";
  html += "<td></td>";
  html += "<td>";
  html += rockIP;
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>ROCK-side Temp.</td>";
  if(tempStatus == "OK")
  {
    html += "<td class='ok'>OK</td>";
  }
  else if(tempStatus == "WARNING")
  {
    html += "<td class='warning'>WARNING</td>";
  }
  else
  {
    html += "<td class='ng'>NG</td>";
  }
  html += "<td>";
  html += String(currentTemp,1);
  html += " degrees";
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>ROCK-side Humidity</td>";
  html += "<td class='ok'>OK</td>";
  html += "<td>";
  html += String(currentHumidity,1);
  html += " %";
  html += "</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>External Fan Noise</td>";
  html += "<td class='ng'>NG</td>";
  html += "<td>Sensor Not Installed</td>";
  html += "</tr>";

  html += "<tr>";
  html += "<td>Playback Now</td>";
  html += "<td></td>";
  html += "<td>Not Implemented</td>";
  html += "</tr>";

  html += "</table>";

  html += "<hr>";

  html += "<small>";
  html += "ESP32 : ";
  html += WiFi.localIP().toString();

  html += " | RSSI ";
  html += String(WiFi.RSSI());

  html += " dBm";

  html += " | Heap ";
  html += String(ESP.getFreeHeap());

  html += "</small>";

  html += "</body>";
  html += "</html>";

  server.send(200, "text/html", html);
}

// =====================================
// setup
// =====================================
void setup()
{
  Serial.begin(115200);

  delay(3000);

  dht.begin();
  Serial.println("DHT11 READY");

  Serial.println();
  Serial.println("BOOT OK");

  WiFi.mode(WIFI_STA);

  Serial.println("CONNECTING...");

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.println("WiFi CONNECTED");

  Serial.print("IP ADDRESS: ");
  Serial.println(WiFi.localIP());

  updateRoonStatus();

  server.on("/", handleRoot);

  server.begin();

  Serial.println("WEB SERVER STARTED");
}

// =====================================
// loop
// =====================================
void loop()
{
  server.handleClient();

  if (millis() - lastRoonCheck > 10000)
  {
    lastRoonCheck = millis();

    updateRoonStatus();
    updateEnvironment();

    Serial.println("ROON CHECK");
    Serial.println(roonStatus);
    Serial.println(roonVersion);
  }
}

そして、成果物がこちら。

実にキレイ。
七転八倒することなく、意外にすんなりと実装が終わりました。

ポイント

文字列を見ていただければわかりますが、温度がオレンジ色で「WARNING」となっています。

温度を三段階で評価して警告する判定も入れたのです。

  • OK : green
  • WARNING : orange
  • NG : red

今はデバッグのために閾値を「25度」と低めにとっていますので、室温27度でも「WARNING」と表示されているわけです。最終的には30度/38度くらいに閾値を設けようと考えています。

湿度の意義

オーディオファイルのみなさんは、

室温や湿度がどんな音質インパクトがあるのか

くらいはご存じですよね? それこそ、オーディオ専用ケーブルのそれとは比較にならないほどの甚大な有意差となって音質差で表出する対象です。装置に対しても大きなインパクトがあるが、ヒト聴覚(受容特性)に対してはもっと大きな影響がある。だから、ROCKに限らぬ室温や湿度と音質の相関を眺めながらモニターするのは意義がありますね。

最初の構想からすると、残りは「ファンノイズの音圧測定」だけとなりました。
ただ・・・このプロジェクトは掘れば掘るほど奥行きが拡がってしまい、遠大な展開が待っていそうなのです。つづく

シリコンパワー ノートPC用メモリ DDR4-2400(PC4-19200) 8GB×1枚 260Pin 1.2V CL17 SP008GBSFU240B02

Synology NASを拡張した時に入れたメモリーがコレ!永久保証の上、レビューも高評価。もちろん正常に動作しており、速度余裕も生まれて快適です。

フィリップス 電動歯ブラシ ソニッケアー 3100シリーズ (軽量) HX3673/33 ホワイト 【Amazon.co.jp限定・2024年モデル】

歯の健康を考えるのならPhilipsの電動歯ブラシがお勧めです。歯科医の推奨も多いみたいです。高価なモデルも良いですが、最安価なモデルでも十分に良さを体感できる。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

投稿者

KeroYon

関連投稿

メヒシバ退治 (4)やっと少し光明が?

我が家の芝生は、3年越しで コガネムシ幼虫食害〜それによるメヒシバの大量繁殖 によって壊滅的大ダメー...

ノイズ&温度モニター (4)ウェブのROCK情報をRich化

前回。ESP32を利用した簡易ウェブサーバーが立ち上がったわけですが。今回は、ここを少しリッチにして...

ノイズ&温度モニター (3)ウェブサーバーを立ち上げる

Roon ROCK を外部から、温度とファンノイズの両面でモニタリングするSTBを構想中。一挙に解に...

ノイズ&温度モニター (2)シリアルポートの監視、そしてWi-Fiのノード化へ

タイトルとは裏腹に、なかなか本題へはいかない。今日もESP32の学習です。 ちゃっぴーの助言により、...

YP-D7 (9)木製ベースの設計図を引き直し

当初、アルミブロック削り出しで考えていたYP-D7アームのアームベース。 しかし、素材温存の簡素化設...

タグ:

梅酒 2瓶目ハチミツ

2つ目の瓶を購入し、梅酒の仕込みが完了。左が黒糖梅酒。左がはちみつ梅酒です。1瓶目以上に、巨大で高級...