×

前回。ESP32を利用した簡易ウェブサーバーが立ち上がったわけですが。今回は、ここを少しリッチにして環境整備しておこうと思います。

具体的には、ROCKサーバーにアクセスして情報を取得し、ROCKのステータスをここに表示してやろうという構想です。

オーディオ・ヘルスメーターという構想

最初は「ROCKの外側で温度や騒音が簡便に監視できればいいや」くらいのつもりで始めたプロジェクト。しかし、進めるうちにどんどん欲が出てきてしまったのです。
このウェブページを、我が家のオーディオの「総合ヘルスモニタリング」ページにしてしまおうと。

例えばですが。HttpでRoon ROCKを叩くと、ROCKの内部ステータスが記述されたjsonを取得できることが判ったのです。例えば、

  • OSのバージョンと稼働状態
  • Roon Serverのバージョンと稼働状態
  • 内部ストレージの種類
  • 内部ストレージの残り容量
  • 接続中のIPアドレス

などです。単にROCKの外部温度や外部ノイズを測定して表示するだけでなく、こうした内部ステータスも一緒に表示すれば、「総合的ROCKダッシュボード」とできます。

さらにさらに。他のAPI経由で取得した情報を徐々にここへ足していけば。統合オーディオ・ヘルスボードとできます。室内温度・湿度・気圧なども一緒に表示すれば、気候や室内環境とオーディオ音質との相関も体感できるかも知れません。

少し大袈裟な言い方をすれば、ESP32を使って「オーディオ用のIoTハブを作ってやれ」という構想に替わってしまったのですね。

まずは、簡単なコードを書いて、ROCKのPayloadでjsonにどんな情報が詰まっているのかを確認してみました。すると様々な情報量が混ざっていることが分かった。

===== ROON JSON ===== {"status":"Success","data":{"device_vendor":"Roon Labs","device_model":"Roon Optimized Core Kit","device_serial":"1C697AA9D2F9","device_display_version":"2.1 (build 271) production","device_machine_version":"200100271","state":{"tailscale":{"enabled":false},"mounted_data":true,"has_data":true,"roon_running":true,"uptime":"1837823","roon_version":"2.67 (build 1661) production","roon_uptime":"556282","has_ffmpeg":true,"has_vendor":false,"vendor_version":"Unknown","vendor_running":true,"vendor_uptime":"1837818","vendor_can_stop":"","data_used":4479344,"data_total":462571972,"internal_storage":{"id":"sda","info":"4000GB, Samsung SSD 860 ","mounted":true,"data_used":1284615424,"data_total":3876033380},"cd":{"status":"no_cd_rom"}},"interfaces":[{"interface":"eth0","name":"Ethernet","config":{"enabled":true,"use_dhcp":true,"static_ip":"","static_netmask":"","static_gateway":"","static_dns":""},"state":{"ip":"192.168.x.xxx","netmask":"255.255.255.0","gateway":"192.168.x.xxx","dns":"192.168.x.xxx"}}]}} ===================== ROON CHECK ONLINE 2.67 (build 1661) production

なんのことはない。これって、ROCKのダッシュボード上で目にする情報群と同じものです。つまり、ROCKダッシュボードもこのjsonをレンダリングしているだけ、ということが判ってきた。

あとは、私もこのJsonを分解して、自前のウェブページ上へ展開表示すればいいだけだと分かった。レンダリングのコードはちゃっぴーに描いてもらいます。一発で動きました。

コード全文(サンプル):

#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

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

// ===== 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アドレス>/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();
}

// =====================================
// トップページ
// =====================================
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 += "</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>External Temp.</td>";
  html += "<td class='ng'>NG</td>";
  html += "<td>Sensor Not Installed</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);

  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();

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

そして、出来上がったWebページがこちらです:

もお〜、カッコよすぎでしょう(笑)

Roon ROCKの各種のステータスが一目瞭然。また、ESP32のステータスも下部に出ている。これらの情報をもって、システムヘルスを診断するというダッシュボードになっています。
ただしまだ温度計や騒音計は付けていませんから。入れ物だけを作っている感じです。

あとは、ESP32へ外部センサーを接続し、それを可視化するだけ。

そして、Roon以外のAPIも少し存在を探ってみる。
例えば、他のIoTであるWiiM、miniDSPなど。あとは湿度計や気圧計もターゲットになってくる。

例えばですけれども。
パワーアンプの出力端をセンシングし、「DCオフセットが通常よりも高めでっせ。」
な〜んてアラームだって出そうと思えば出せるのです。

放熱器へ温度センサー直付けすれば、パワーアンプの温度モニタリングだって出来ちゃう。

バラバラじゃなくて、1枚のモニタリング画面で全体を俯瞰できるところに価値があるのです。
夢は拡がりますねえ。実に楽しい取り組みになってきました。

次回はいよいよ、外部センサー(温度センサー)を繋げてみます。巧く動くのかな?

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

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

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

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

コメントを残す

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

投稿者

KeroYon

関連投稿

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

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

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

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

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

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

タグ:

梅酒 2瓶目ハチミツ

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

タグ:

今年も梅酒を仕込むのです(二瓶)

そう。 昨年の梅酒造りの大成功に味を占めた私は、今年も梅酒を仕込むのです。 しかも今年は2瓶も。 ワ...

ノイズ&温度モニター (1)ESP32初チャレンジ

ずいぶん前に各種パーツの詰まったAruduinoの詰め合わせセットキットを買っていました。でも、それ...