Claude MCP × WordPress連携でハマった落とし穴──wp_healthの403は「接続失敗」ではなかった

ClaudeのMCP(Model Context Protocol)を使ってWordPressと連携する環境を構築しました。昨日まで正常に動いていたのに、今日は突然つながらない──そんなトラブルを経験しました。結論から言うと、原因はClaudeの思い込みによるデバッグミス(とアプリケーションパスワードの保存忘れ)でした。同じ失敗を繰り返さないための記録として残します。

環境

  • サーバー:Xserver(hoscm.xsrv.jp)
  • MCPプラグイン:claudeus-wp-mcp
  • MCPユーザー権限:編集者(Editor)
  • 接続設定:wp-sites.json(Claude Desktop)

何が起きたか──事象の時系列

昨日まで正常に動作していたMCP連携が、今朝から突然失敗するようになりました。Claudeは接続確認のために wp_health__test_auth を繰り返し実行しましたが、すべて403エラーが返ってきました。

手順実施内容結果
①wp_health__test_auth で接続確認❌ 403エラー
②WP Fastest Cache を無効化❌ まだ403
③CloudSecure WP Security を無効化❌ まだ403
④Xserver WAF を調査❌ 関係なし
⑤アプリケーションパスワードを再生成❌ まだ失敗
⑥PowerShellで認証テスト✅ 通った
⑦「更新ボタン押し忘れ」に気づく原因判明
⑧パスワード正しく保存・再起動❌ まだ失敗
⑨wp_healthではなく通常エンドポイントで確認✅ 正常動作!

本当の原因は2つあった

原因①:アプリケーションパスワードの「保存ボタン押し忘れ」

WordPressのアプリケーションパスワードを再生成した後、プロフィール画面の「プロフィールを更新」ボタンを押さずに離脱していたため、新しいパスワードが保存されていませんでした。これが真の認証失敗の原因でした。

PowerShellで認証テストをしたところ通ったことで、この事実が判明しました。パスワード更新後は必ずブラウザ上で「更新完了」のメッセージを確認してください。

原因②:wp_healthエンドポイントは編集者権限では使えない

Claudeが接続確認に使い続けた wp_health__test_auth は、管理者権限が必要なエンドポイントです。このサイトのMCPユーザーは編集者権限しか持っていないため、パスワードが正しくても403が返り続けます。これは「接続失敗」ではなく「権限不足」による正常な拒否でした。

エンドポイント必要権限編集者での結果
get_taxonomies不要(公開情報)✅ 正常
get_posts編集者✅ 正常
create_post編集者✅ 正常
get_settings管理者❌ 403
wp_health__test_auth管理者❌ 403

無実だったプラグインたち

今回の騒動で無効化・調査したプラグインやセキュリティ設定は、すべて無関係でした。

  • WP Fastest Cache:.htaccessを書き換えていたが、無効化しても403は続いた → 無関係
  • CloudSecure WP Security:セキュリティプラグインだが、無効化しても403は続いた → 無関係
  • Xserver WAF:XSS・SQL・PHP等の6項目を確認したが、REST APIの権限制御とは無関係

最終確認として、両プラグインを再有効化した状態で get_taxonomies を実行したところ正常に動作しました。環境は最初から正常だったのです。

一番の失敗:Claudeの思い込みに振り回された

今回最大の問題は、Claudeが「wp_health = 接続確認ツール」「403 = 接続失敗」と思い込み、その前提を疑わなかったことです。

その結果、ユーザーは本来まったく不要だった以下の作業をさせられました。

  • プラグインの無効化・再有効化
  • WAFの各項目調査
  • アプリケーションパスワードの再生成
  • Claude Desktopの複数回再起動
  • .htaccessの内容確認

「低権限エンドポイントから順に確認する」という基本的なデバッグ手順を踏んでいれば、これらの作業のほとんどは不要でした。

正しいMCP接続確認の手順

今後の再発防止のために、正しいデバッグ手順をまとめます。

Step 1:低権限エンドポイントで疎通確認

get_taxonomies  ← 認証不要・公開情報。まずここから。

✅ 通る → ネットワーク・サーバーは正常

Step 2:編集者権限エンドポイントで認証確認

get_posts または create_post(下書き)← 認証が必要。

✅ 通る → 認証(アプリケーションパスワード)も正常
❌ 失敗 → パスワードの確認、保存忘れがないか確認

wp_health系は使わない

管理者権限が必要なため、編集者権限の環境では常に403が返ります。接続確認には使用しないこと。

読者・ユーザーへの教訓

MCP連携でトラブルが発生したとき、最初にClaudeに伝えるべき情報があります。

「このサイトのMCPユーザーは編集者権限です。接続確認は get_taxonomies など低権限エンドポイントから順に試してください。wp_health系は使わないでください。」

この一言をセッション冒頭に伝えるだけで、今回のような無駄なデバッグを防ぐことができます。AIは間違った前提で動き続けることがあります。ユーザーが正しい制約条件を最初に伝えることが、AI活用の重要なスキルです。

まとめ

項目内容
真の原因①アプリケーションパスワードの保存ボタン押し忘れ
真の原因②wp_healthは管理者権限が必要(編集者権限では使えない)
Claudeのミス403=接続失敗と思い込み、低権限エンドポイントから試さなかった
無関係だったものWP Fastest Cache・CloudSecure・Xserver WAF・.htaccess
再発防止策接続確認はget_taxonomiesから。セッション冒頭に権限を明示する

MCP連携は非常に便利な仕組みですが、AIの思い込みに振り回されないためには、ユーザー側からの適切な制約条件の提示が重要です。この記録が同じ環境で構築する方の参考になれば幸いです。

コメント

このパターンにはまった場合、初心者はAIに振り回されて数日間を棒に振る可能性があります。最悪の場合、放棄するかもしれません。いあー。まだまだ、MCPはエンジニアレベルの絶賛開発中ツールでしょうかね。 このようなトラブルをサクッと解決できる人しか生き残れないのか?。。。 厳しいな。

関連記事

Google Nest Hub で YouTube ライブが再生できない不具合(2026年3月)— 原因・範囲・対処法


2026年3月28日より、Google Nest Hub / Nest Hub Max で YouTube のライブ配信が正常に再生できない不具合を確認した。同じ症状が多数報告されているようです。本記事では、症状・影響範囲・原因の考察・対処法をまとめます。また、hsBox をご利用の方向けに、復旧まで の代替手段についても案内します。
⇒2026年3月31日午前6時50分時点で復旧していました。対処は何もしていませんが2026年4月7日午後6時00分時点でキャストファームウェアが更新され復旧していました。。追加調査情報は下記参照します。

症状

報告されている症状は主に以下の2パターンです。発生タイミングにより段階的に変化しています。

フェーズ症状
フェーズ1(発生状態)同じ1〜3秒の映像がループして繰り返し再生される
フェーズ2(※ときどき症状が変わる)「このコンテンツはご利用いただけません。後でもう一度お試しください」と表示される

影響範囲

この不具合は WNI(ウェザーニュース)固有の問題ではなく、YouTube ライブ全般に影響しています。

コンテンツ状況
WNI(ウェザーニュース)ライブ再生不可
その他の YouTube ライブ配信全般再生不可
YouTube の通常動画(録画済み)正常
スマートフォンでの YouTube ライブ 正常
Nest Hub 以外のデバイスへのキャスト 正常

操作方法(音声操作・スマホからキャスト)による違いはなく、どちらでも同じ症状が発生します。影響を受けるデバイスは Nest Hub / Nest Hub Max など Nest Hub シリーズ全般です。

原因の考察

Google Nest Community への報告によると、「まだファームウェアがアップデートされていない Nest Hub では正常動作している」という証言があります。これはNest Hub の最新ファームウェアと YouTube ライブの配信方式との相性問題である可能性を示しています。

なお、現時点での参考ファームウェアバージョン(Nest Hub Max)は以下の通りです。

  • システムファームウェア:29.20251023.103.2201 ←(2026/3/31GoogleHomeで確認)
  • キャストファームウェア:3.78.508739 ← 同上
  • Fuchsiaバージョン 29.20251023.103.2102100   ←(2026/4/2実機設定で確認)
  • ソフトバージョン 70.94.11.871942728 ← 同上
  • Chromecastファームウェアバージョン 3.78.523914 ← 同上
    ※2026/4/7 19:00時点追記: 画像は荒いが復旧したようだ、復旧後のバージョンは下、同じバージョンのままで21:00時点で通常のレベルに復旧した。
    システムファームウェア:29.20251023.103.2102100 ←(2026/4/7GoogleHomeで確認)
  • キャストファームウェア:3.78.523914 ← 同上
  • Fuchsiaバージョン 29.20251023.103.2102100   ←(2026/4/7実機設定で確認)
  • ソフトバージョン 70.94.11.871942728 ← 同上
  • Chromecastファームウェアバージョン 3.78.523914 ← 同上

※追加情報:2026/3/30時点

Chromecast Ultra では発生していない⇒4/8時点でも発生していない

Google Pixel Tab でも発生を確認。 →4/8には復旧
  ただし、発生はライブをキャストした場合、YouTubeアプリで、
手動でライブを再生する場合は正常に再生できる。

Google はすでにシニアチームにエスカレーションして調査中とのことです。また、2026年1月にも同様の問題が数週間継続した後に自然解消した前例があります。

ファームウェアバージョンの確認方法

お使いの Nest Hub のファームウェアバージョンは以下の手順で確認できます。

Google Home アプリから確認する方法

  1. Google Home アプリを開く
  2. 対象の Nest Hub のタイルを長押し
  3. 右上の歯車アイコン(設定)をタップ
  4. 「デバイス情報」→「技術情報」でバージョンを確認

Nest Hub 本体から確認する方法

  1. 画面を上から下にスワイプ
  2. 歯車アイコン(設定)をタップ
  3. 「デバイス情報」でバージョンを確認

他サイトでの情報

次のサイトで関連する情報が公開されていました。

以下のリンクから、現在進行中の議論や最新のアップデート状況を確認できます。

現時点での対処法

残念ながら、ファクトリーリセット・Wi-Fi 再接続・アプリ再インストールなど、ユーザー側でできる操作では解決しないことが確認されています。Google 側の修正を待つのが現実的な対応です。

  • Google Nest Community のスレッドに「同様の問題あり」と投稿することで、修正の優先度が上がる可能性があります
  • 1月の前例では数週間後に自然解消しています

hsBox への強化提案:復旧までの代替手段

hsBox で WNI ライブや YouTube ライブをテレビ・スマートディスプレイに表示している場合、Google 側の修正が完了するまでの間、以下の代替手段の活用をご検討ください。

  • 別のキャスト先への変更:Chromecast built-in 対応の Android TV / Google TV へのキャストは正常に動作しています。Nest Hub 以外のデバイスが利用可能であれば、そちらへの振り替えが有効です
  • hsBox のメニュー・スケジュール機能で切り替え設定を追加:復旧情報を受け取り次第、元の Nest Hub へのキャストに戻す運用も可能です
  • 復旧の確認方法:Google Nest Community の該当スレッドを定期的に確認するか、本記事の更新をご確認ください

まとめ

今回の不具合は Google Nest Hub と YouTube ライブの組み合わせに特有のバグであり、hsBox 側の実装に問題はありません。Google の修正対応を待ちながら、必要に応じて代替手段を活用してください。

復旧情報が判明次第、本記事を更新します。

参考:hsBox で YouTube ライブをテレビにキャスト(hoscm.com)


🗓️ 追記:2026年4月更新──ダウングレード不可、YouTube側変更の隣

ファームウェアのダウングレードは可能か?

結論から言うと、ダウングレードは実貪上不可能です。Nest Hub は Fuchsia OS ベースのクローズドシステムであり、ファームウェアファイルを手動で適用する手段が公開されていないためです。Google 側からユーザーが下げる方法は提供されていません。

YouTube 側の変更が原因の可能性

当初は「Nest Hub のファームウェア側の問題」と見られていましたが、調査を進めると「YouTube 側の変更がトリガーになった」可能性が浮上してきました。

YouTube のライブストリーム配信方式には、主に DASH(Dynamic Adaptive Streaming over HTTP)と HLS(HTTP Live Streaming)の2つのプロトコルが使われています。2026年3月の開発者向けツール(yt-dlp)のバグ報告によると、YouTube 側で DASH 形式から HLS 形式への切り替えが発生していることが報告されており、Nest Hub の YouTube クライアントが新しい配信形式に未対応のままになっている可能性があります。

問題の構造:両社が絡んだ共責問題

対処主体内容可能性
Google(Nest Hub)側ファームウェアを更新してYouTubeの新プロトコルに対応高
YouTube側古いChromecastクライアントへの後方互換性を維持中
ユーザー側ダウングレード不可のためできることはないなし

両社のどちらかが修正を出すまで、ユーザー側でできることはありません。引き続き代替デバイス(Android TV / Google TV)へのキャストで運用してください。

現時点の状況(2026年4月時点)

  • 修正パッチ配信:❌ まだなし → 2026/4/7には配信されたもよう
  • Google 対応状況:シニアチームが調査中だが進展報告なし
  • 問題の長期化:一部ユーザーは1〜2か月前から発生中との報告もあり

関連記事

IT駆使(hsBox1.3活用)、テクノロジー駆使で水濡れ原因を謎解き! メカニズム新説へ?

以前に書いた「映像監視への、漏水状況の追加監視について2026/1/12」の記事で、示した漏水の謎についてほぼ事象を確認できたのでここでまとめておく。

写真の上部の中央やや右よりに、先の記事で書いた「水分検知シート」をテープ状に切って張り付けた。張り付けたのは昨年末で、上の画像の通り1/30にはまだ白色で水濡れは発生していなかった。
 しかし、先日確認したところ、明らかな水濡れが発生したことを確認した。

tape20260319

seat
水検知シート

赤色に変色しているところが、水で濡れたところである。この変色の状況から、いくつかの状況が読み取れる。それについては後で整理することにする。


水濡れ原因の検証・推定

さて、今回水濡れが発生した原因は、先に推定した漏水・吐水だったのだろうか?それを確認するため、水濡れが発生した時間を特定し、そこで何か起こっていたのかを確認しよう。

目視で、水濡れが発生していなかった記憶は、1月末くらいである。そこで、3月以降の画像記録をたどってみた。3月13-14日を除いて3月1日から3月19日まで、欠落なく10分毎の画像キャプチャができているようだ。3月13日13:40から3月14日18:20の間はキャプチャが欠損している。(※キャプチャ失敗に1日程度気が付かなかった。そこで対策としてhsBoxのステータスモニター機能を使うことにした)

3月14日18:30時点の画像では、水濡れが発生していたようである。3月13日13:30時点の画像はカメラが横倒しで対象個所を撮影できていない。遡るとカメラが横倒しになったのは3月13日10:20から10:30の間であった。 そして、3月13日10:20時点画像ですでに水濡れが発生していたと判断できた。どうも、水濡れが発生したのは2月の何処かのようだ。

2/1 ◎ 2/7◎ 2/9◎ 2/10 17:00〇 2/11 8:00× 2/11x 2/12 12:30× 2/15x 3/1×

確認の結果、2/10 17:00と2/11 7:10の間に発生したようだ。

つぎは、2/11 20:00時点の画像である。

yoru
2/11 20:00

この時点で、水濡れはすでに発生しているにもかかわらず、この画像では確認できない。暗視状態(白黒)の画像では、赤外光で撮像するため赤くなっていても暗くうつることはない。つまり、暗がりでは水濡れを検知テープの状態から判別することができないのである。

遡って、暗視画像を確認すると、2/10 5:50時点では撮影範囲内には特に変化は見られなかった。しかし6:00時点で上部に水のシミが映り込み、7:00まで1時間かけてじわじわ広がっていく様子が確認できた。そのシミの範囲と、変色部分の位置は一致しており、このシミが水であったことを確認できた。

2/10 6:00ころの天気が何であったか確認してみよう。気象庁記録では、前日夜から午前10時ころまで雨であった。気温は7度前後、風速は2m/sである。この場所は、上にベランダが張り出しているので雨がかからないので、これまでコンクリート奥から漏れ出てきたと推定していた。しかし、他の仮説も立てる必要があるかもしれない。

また、画像:tape20260319 これは、2/11時点の濡れ状態から、さらに1回ほど濡れが追加されているように見える。
 その1つは、小さな霧状の水滴がついたように見えることによる。この小さな点は、上から貼り付けたメンディングテープの部分には発生しておらず。霧吹きのように振りかけられたように見える。上と同様に、発生した日時と、その時の天候を確認した。

3/16◎ 3/17◎ 3/18◎ 3/19×

こちらも夜間に発生したようだ。3/18 17:00から3/19 7:00の間で、細かい赤い点が発生し、メンディングテープの位置がくっきりと見えるようになっている。
この日も3/18から3/19にかけて雨が降っており気温は11℃程度、風速は3m/sちょっとだった。2/11とは風向きが逆で吹き込んでくる方向だった。霧雨状の水滴がついたと考える。

この状況から、一見濡れなさそうなこの面も雨の降り方と風速と風向きしだいで濡れることがあることが確認できた。

水濡れの原因調査は、水検知シート+hsBoxで

水検知シートだけだと濡れたことを確認できただろうが、いつ濡れたか、その原因の特定までは難しかったかもしれない。定点観測的に監視カメラでキャプチャし続けたことで、濡れたタイミングを特定して、ほぼ確実な原因を推定できた。

▼新説:構築 → 次の分析へ

残念ながら新たに確認できたのは、一見濡れないと思われた場所も状況によっては濡れる可能性があることが示されたことにとどまる。新しい仮説を組み立てるほどの状況にはない。とにかく、今年の秋から冬にかけては雨が少なかった。これが、今回の調査が進まなかった、つまり問題事象が発生しにくかったことに効いているかもしれない。


関連記事

なぜ壁は膨らんだのか?IT駆使で迫るコンクリート劣化の原因調査 ~2から6週間分 中間まとめ ~

昨年12月に本格着手して、さまざまな角度でITを駆使しながら進めてきています。以下は、ここまでに確認できた結果の要点の書き出してまとめます。 当初計画した調査環境の整備が整ってから2週間ほどしか経っていませんが、当初の仮説とは少し異なる新たな仮説を立てています。

ATOMCAM2撮影、画像キャプチャ(コンクリート劣化)


カメラでの定点観測は2025/12/23より運用開始: 上のとおり、キャプチャ開始時点から、後述のAEセンサー、温度センサー(熱電対)を追加しています。また、上右よりの2本のテープ状のものは水濡れ検知シールです。水に濡れると下の写真のように赤色に変わり乾いても赤のままなので、水が垂れてきたことがあったかどうか分かります。今のところ、水は出てきていません。同様に熱電対を入れた塩ビ管内にも。このシールを入れたので、壁の内側で結露が発生したかどうかを判別できるでしょう。

mizu
水検知シール

MCR-4TCでの温度測定

温度測定
温度測定

今のところ気温が一瞬0℃になりましたが、コンクリート裏はそこまでは下がっていません。気温が下がってコンクリート内の温度が連動するところもあれば、連動しないところもあります。単純に外気温だけがコンクリート裏側の温度に効いているわけではないようです。

 また、場所によって外気温の影響が異なるように見えます。

AE測定

[ EEPROM Data Analysis Report ]
Type | Date Time (UTC/Local) | Millis (raw) | Details
--------------------------------------------------------------------------------
SYNC | 2026-01-14 13:25:51 | 325120 | Time Synchronized (Unix:1768364751)
DATA | 2026-01-14 14:25:45 | 3919360 | CH0:0, CH1:19, CH2:0, CH3:0
DATA | 2026-01-15 06:45:30 | 62704128 | CH0:0, CH1:8, CH2:0, CH3:0
DATA | 2026-01-16 00:26:07 | 126341632 | CH0:0, CH1:8, CH2:0, CH3:1
DATA | 2026-01-16 06:11:56 | 147090432 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-16 07:11:10 | 150644736 | CH0:0, CH1:0, CH2:3, CH3:7
DATA | 2026-01-16 10:10:08 | 161382912 | CH0:0, CH1:16, CH2:0, CH3:0
DATA | 2026-01-16 10:16:08 | 161742848 | CH0:0, CH1:7, CH2:0, CH3:0
DATA | 2026-01-16 10:57:17 | 164211968 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-16 12:33:41 | 169995520 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-16 12:47:02 | 170796544 | CH0:0, CH1:7, CH2:0, CH3:0
DATA | 2026-01-17 16:21:30 | 270064640 | CH0:0, CH1:11, CH2:1, CH3:1
DATA | 2026-01-18 01:01:31 | 301265664 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-18 05:01:11 | 315645696 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-18 12:22:00 | 342094336 | CH0:0, CH1:1, CH2:6, CH3:6
DATA | 2026-01-19 00:38:54 | 386308608 | CH0:0, CH1:45, CH2:0, CH3:0
DATA | 2026-01-19 06:01:51 | 405686016 | CH0:0, CH1:8, CH2:0, CH3:0
DATA | 2026-01-20 12:54:55 | 516869376 | CH0:0, CH1:6, CH2:0, CH3:0
DATA | 2026-01-20 18:41:19 | 537653504 | CH0:0, CH1:7, CH2:0, CH3:0
DATA | 2026-01-22 01:14:26 | 647640576 | CH0:0, CH1:7, CH2:0, CH3:3
DATA | 2026-01-25 12:50:54 | 948628992 | CH0:0, CH1:9, CH2:0, CH3:2
DATA | 2026-01-29 15:48:20 | 1304874752 | CH0:0, CH1:23, CH2:3, CH3:4
RSET | 2026-01-30 12:55:41 | 0 | System Boot / Reset Detected
SYNC | 2026-01-30 12:58:24 | 162304 | Time Synchronized (Unix:1769745504)
--------------------------------------------------------------------------------
Analysis Finished.

顕著なAE信号検出はなさそうで、たまに何かを検出してはいますが、ほとんど発生していないようです。1/16がちょっと多い感じがしますが、今のところ顕著な問題は起きていなさそうです。

コンクリート壁穿孔工事関連

熱電対を設置した状況からそれぞれの場所ごとにコンクリート壁と、壁内の土の間に隙間があり、場所によってその隙間が、約5mmから約48mmと大きなばらつきがあるように見えます。

中間まとめ 

 今のところ、新たに見えた事象は、コンクリート裏に隙間があるようにみえるということです。その隙間と、コンクリート奥の温度に関連性があるように見えます。つまり、隙間が大きいほど、外気温が下がっても温度が下がりにくそうということです。

 それとは別に隙間に関係なく、タイミングによってどの面の温度が低くなるのかが変わりそうです。

温度測定が終わる4月頃に、熱電対をとおした塩ビパイプを取り出して、内視鏡カメラで壁の裏側の隙間の状態を調査しようと考えています。

今年はほとんど雨降っておらず、乾ききっている感じなので問題事象は発生しにくそうです。

新たな仮説

これまでの仮説はすべての面が均等にアイスレンズが成長するように考えていましたが、片面だけ発生するケースもありそうな感触です。つまり、東面と南面で交互に発生し隙間が広がったり、逆に押されてほとんどなくなったりすることが繰り返されたのかもしれません。

さらにここまでの仮説で将来発生することが予想されていた西面北寄りでの事象発生ですが、下の写真のように新たに発生していることを確認しました。

crack
ひび割れ

ここまでの仮説を補強する材料と言ってよいでしょう。 

関連記事

ATOM CAM2のカメラ画像キャプチャをルーター越えの別セグメントからhsBox1.3でトライ

ルーターの向こう側にあるATOM CAM2の画像をキャプチャしたかったのですが、これまでツールではできていませんでした。今回トライした結果を考慮して、以前に公開したツールの今後の強化項目にしていきます。

cam
cam

どうやるか、これまでの課題も踏まえて、検討してみましょう。
スマホで、rtspでのアクセスURLを確認できるので、これにアクセスできるように環境を整えてみます。

方法1 ルーティングでATOMCAM2にアクセスできるようにする

hsBoxや PC側の設定(ルーティング設定)で、ATOM CAM2に向けてつなげ、ルーターで対象のパケットを通すように許可する。
※hsBox1.3にはシステム設定に「ルーティング設定」機能があり、ルーティングの制御支援機能があるので、これを利用できます。

確認したルーターには。指定したパケットを許可する機能がなく、この方法での動作確認はできていません。
このパターンの場合、ツールの強化ポイントはIP指定できるようにすることです。
また、ルーターのDHCPでの割り当てIP固定化機能もあったほうがよいですね。

方法2 ルーターのDMZ機能を利用してアクセスできるようにする

確認したルーターには。DMZ指定する機能はありますが、1つのIPしか指定できませんでした。すでにスマートスピーカにこのIPを割り当て済みだったので、この方法での動作確認はできていません。
このパターンの場合、ツールの強化ポイントはIP指定できるようにすることも挙げられますが、MACアドレスにルータのMACを指定すれば既存実装でも操作するはずです。

方法3 ルーターのポートオープン機能を使ってアクセスできるようにする

確認したルーターにはこの機能があったので、試してみました。
暫定的に既存のツールを、指定したIPの画像キャプチャーをするように改造して動作確認してみました。IPにルーターのIPを指定することで、難なく画像キャプチャーに成功しました。

複数のATOM CAM2がこのルーターの向こう側にある場合は、ポート番号が被らないように調整する必要が出てくるので、IP指定に加えてポート番号指定機能もあったほうが良いですね。ただ、IP指定を IP:PORTの形式で指定できるように拡張しするだけで対応できるでしょう。
また、ルーターのDHCPでの割り当てIP固定化機能もあったほうがよいですね。

関連記事

温度測定点設置壁裏の温度測定準備完了。見えてきた「隙間」を内視鏡カメラ「YPC99」でどう捉えるか?

これまでにコンクリート劣化の原因調査に関して様々な調査の準備をしてきましたが、2026/1/14、1/21にコンクリート壁への穿孔と温度測定定点設置を行い最終的な調査準備が完了しました。

コンクリート壁の3面に穴をあけ壁の裏側の土の温度を測定できるように熱電対を設置しました。設置後の外観は次の通り。

東面

南面

西面

塩ビ管を奥まで通して、先端にわずかに出した熱電対の先が、その先の土にあたるように設置しています。ぱっと見で、塩ビ管の出方に違いがあることが分かるでしょう。それぞれの準備した塩ビ管の長さと出っ張り具合から、それぞれの場所でコンクリート壁の内側での塩ビ管の出っ張り具合を検討してみました。壁の厚みは設計値では未確認ですが270mmとされています。

場所塩ビ管長さ
切断時測定
それぞれの管の出っ張り長壁裏突出長(推定値)
東側約350mm
32mm
約48mm
南側約350mm
60mm
約20mm
西側約300mm
25mm
約5mm

推定ですが、少なくとも東面については、コンクリート壁内面と、土の間にはある程度の隙間がありそうです。 熱電対設置前に事前確認しておけばよかったのですが、温度測定観測が終了した後に調査してみましょう。 以前に別の調査のために用意した内視鏡カメラYPC99があるので、これで確認することにします。

YPC99
YPC99 内視鏡カメラ

カメラの先は100mmくらい棒状で自由度は低いのですが、線の長さを合わせると450mmほどはあるので、コンクリート壁の穴を通して、隙間の具合がどんな感じなのかは見ることができると思います。

関連記事

hsBoxで、ATOM Cam 2の映像をキャプチャで発生した問題と課題と対処策について

約1か月連続キャプチャできていましたが、キャプチャできなくなる事象が発生したのでその原因調査と対策をしていきます。そして恒久対策に向けた対策案も検討して、以前に公開したツールの今後の強化項目にしていきます。

cam
cam

調査 その1

まず、状況整理ですが、キャプチャは、1回のcron起動で、3つのATOM CAM2を順にキャプチャしてファイル保存するように実装していました。一番最初にキャプチャするカメラ画像は10:00にキャプチャしたものが最後で、そのほかの2つは、14:40が最後でした。つまり、1つ目のキャプチャに失敗しても2つめ以降のキャプチャはできていましたが、その後3つともキャプチャできなくなっています。運用点検のため、hsBoxを再起動したタイミングあたりでキャプチャできなくなっていたようです。
 まずは、切り分けしていくためウォークスルー方式で問題となりそうなところを順にチェックしていきます。まずは、NASのマウント状況を確認しました。マウントはできており問題ありませんでした。
次に、ATOM CAM2のIP探索の確認です。 スマホでATOM CAM2のIPを確認して、そのIPが取れているかを、arpコマンドで確認します。 すると、3台のカメラともにMACが取れていないことが判明した。

暫定対処 その1

それぞれの、IPにpingを打ってみます。 それぞれ、反応がありMACがとれました。これにより、14:40が最後だった2台のカメラのキャプチャが復旧しました。
 残りの1台は、NAS上へのフォルダ作成には成功したものの、画像ファイルがまだ置かれていない状況になっています。

根本対策 その1

 今後の強化策です。 既存実装でもブロードキャストでpingを打つ実装を入れていますが、ブロードキャストではarpに反映されないのが問題のようです。そこで、MACが取れなかったとき、DHCPで割り振られる範囲に対して明示的に個別にpingを打つ実装を追加することにします。

調査 その2

まだ復旧できていない1台分のカメラについて調査を行います。

実装を、最後にキャプチャできたタイミングで何かあったかを確認してみましたが、特に問題はないようです。手動で、FFMPEGコマンドを実行すると、次の結果が返ってきました。

> python3 .\cap_once.py
FFmpeg failed: Command '['ffmpeg', '-rtsp_transport', 'tcp', '-i', 'rtsp://**:***@192.168.*.*/live', '-ss', '00:00:01', '-vframes', '1', '-q:v', '2', '-y', '\\\\192.168.*.*\\***\\2026-01-25\\20260125_124510.jpg']' timed out after 20 seconds

タイムアウトしていますが、pingは通っている。 前のセッションが残っていて接続待ちの状態になっている?のかという説はあるが、 スマホからの確認では動作している。 スマホでの画像表示と、rtspは別動作なので、rtsp側だけの問題(セッション残留?)の可能性がある。

暫定対処 その2

セッション残留の問題だと仮定して、rtspの再起動での復旧を試みる。スマホでの操作で一度「PCで再生」をオフにして再起動してみる。
 この操作を行うとrtsp用のユーザとパスワードが更新された。新たなユーザとパスワードを反映すると、キャプチャに成功し、 自動キャプチャは復旧した。

問題の原因は、残留セッションとみてよいようだ。 これは、ATOM CAM2側で何とかしてほしいが、 録画の都合(解像度の高い録画をしたい)から古いバージョンのファームウェアを使用している。このため最新パッチを適用することができず、ATOM CAM2側での”残留セッション”の改善は望めない。そこで、hsBox側での別の改善策を取ることにする。

その2の問題の 改善策案

”残留セッション”問題の対処方針は、上の”暫定対処”の問題を早期検知し、できるだけ手間をかけずに復旧できるようにするというものである。

 復旧手順を明記した内容での問題検知通知を出し、スマホ操作で「PCで再生」を再起動し、あらたなユーザ名・パスワードをGUI設定画面で更新するというものである。
とりあえず今時点での簡便な復旧策はこの辺でしょう。

他に良い案があれば、コメント投稿をお願いします。


関連記事

https://mic.or.jp/info/2026/01/30/cam-4/

hsBox1.3で、太陽光発電機器故障を検知して通知してみよう

「2025年12月22日で太陽光発電 通知メールがなくなる件。 代替として自前で収集した情報をLINEに通知してみた」をさらに高度化を検討してみた。
 過去の実装(太陽光発電システムの故障判断アルゴリズム(その1))で、故障検知を高精度(6年間で5回検知・誤検知なし。1件はパワコンブレーカーダウン、4件はデータアップロードネットワークのダウン)で実装できていましたが、さきのソーラーフロンティアのサービス終了でこの検知システムが使えなくなりました。そこで、新たな検知システムの実装を、先に公開してシステムを強化して、検討してみます。

 ちなみに、公開済みの発電量収集代替実装では、従来のソーラーフロンティアの発電量との誤差は±2%程度なので、3か所ともに発電量収集代替実装を導入して、既存の検知システムにデータを流し込めば高精度での判定を継続できるようになります。しかし、通常、太陽光発電システムを複数保有しているほぼないので、この方式はほぼ使えません。そこで、ここでは1システムだけであっても判定することができる仕組みを検討してみます。

SolarAlert_
太陽光発電システム 故障通知

故障検知実現の設計方針

最終的には、人が判断するが、できるだけ判定精度を上げたい。過去の高精度判定システム構築での知見を反映して設計する。ポイントは次の通り。
・快晴時の発電量は安定しており、きれいなカーブを描く。
   →判定はこの部分のデータを使う。
・雲がある晴れの日のデータは変化が激しい。
・発電量の小さい日の発電量のばらつきが大きい。
   →判定には使用せず、参考値として通知する。
・四季の変化での発電量の補正が必要
→過去実装では、実績のピークデータをもとに関数化して組み込み
想定発電量と比較し、判定するかどうかを切り分けした。
・外部情報として最も近い気象庁観測点のデータを使用する。
・太陽光パネルの様々な設置場所ごとの違いによる想定発電量を単純な計算式ではなく学習方式を採用する。

使用するデータは、以前に公開したParquet形式で保存したものとする。

実装例 


import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import os
import sys
import argparse
import requests
from bs4 import BeautifulSoup

# ==================== 設定エリア ====================
NAS_PATH = r"/mnt/nas/PowerData" #★
MODEL_PATH = os.path.join(NAS_PATH, "learning_model.parquet") # 学習データ保存先
IFTTT_WEBHOOK_URL0 = "https://maker.ifttt.com/trigger/{event}/with/key/{your_key}"
IFTTT_EVENT_NAME = "message_to_myline" #★
your_key = "************" #★
IFTTT_WEBHOOK_URL = IFTTT_WEBHOOK_URL0.replace("{your_key}", your_key)

# 判定しきい値 (期待値の50%以下が2時間続いたら故障疑いなど)
THRESHOLD_RATIO = 0.5 #★
# ====================================================

def send_line_message(title: str, body: str, timestamp: str = "-"):
payload = {"value1": title, "value2": body, "value3": timestamp}
url = IFTTT_WEBHOOK_URL.replace("{event}", IFTTT_EVENT_NAME)
try:
response = requests.post(url, json=payload, timeout=10)
return response.status_code == 200
except Exception as e:
print(f"LINE通知失敗: {e}")
return False

def get_jma_sunshine_hourly(target_date: datetime):
"""気象庁HPから10分ごとの日照時間を取得し1時間単位(0.0-1.0)で返す"""
url = (f"https://www.data.jma.go.jp/stats/etrn/view/10min_s1.php?"
f"prec_no=**&block_no=****&year={target_date.year}&month={target_date.month}&day={target_date.day}&view=p2") #★
try:
res = requests.get(url, timeout=10)
res.encoding = res.apparent_encoding
soup = BeautifulSoup(res.text, 'html.parser')
rows = soup.find_all('tr', class_='mtx')
sunshine_10min = []
for row in rows:
cols = row.find_all('td')
if len(cols) > 10:
val = cols[9].text.strip() # 日照時間の列
sunshine_10min.append(float(val) if val else 0.0)
# 1時間(6個)ごとに合計(最大1.0)
return [sum(sunshine_10min[i:i+6]) for i in range(0, len(sunshine_10min), 6)]
except Exception as e:
print(f"気象庁データ取得失敗: {e}")
return [None] * 24

def update_learning_model(target_date: datetime, hourly_actual: list, hourly_sun: list):
"""日照が良い時のデータを『快晴時の正解』として学習モデルを更新"""
month = target_date.month
try:
if os.path.exists(MODEL_PATH):
model_df = pd.read_parquet(MODEL_PATH)
else:
# 初期モデル: 全月0で作成
model_df = pd.DataFrame(0.0, index=range(1, 13), columns=[f"h_{i}" for i in range(24)])

updated = False
for h in range(7, 18): # 発電時間帯のみ
# 日照率が0.9以上かつ、これまでの学習値より高い(または初データ)場合に更新
if hourly_sun[h] and hourly_sun[h] >= 0.9:
current_val = model_df.loc[month, f"h_{h}"]
if hourly_actual[h] > current_val:
model_df.loc[month, f"h_{h}"] = hourly_actual[h]
updated = True

if updated:
model_df.to_parquet(MODEL_PATH)
print("学習モデルを更新しました。")
except Exception as e:
print(f"学習モデル更新失敗: {e}")

def load_day_data(target_date: datetime) -> pd.DataFrame:
date_str = target_date.strftime("%Y%m%d")
file_path = fr"{NAS_PATH}/power_{date_str}.parquet"
if not os.path.exists(file_path):
raise FileNotFoundError(f"データが見つかりません: {file_path}")
return pd.read_parquet(file_path)

def main():
parser = argparse.ArgumentParser()
parser.add_argument("--da", type=int, default=0)
args = parser.parse_args()

base_date = datetime.now() + timedelta(days=args.da)
date_label = base_date.strftime("%Y/%m/%d")

try:
df = load_day_data(base_date)
# 1時間ごとの発電量(value1)リスト作成
df['hour'] = pd.to_datetime(df['timestamp']).dt.hour # timestamp列があると想定
hourly_actual = df.groupby('hour')['value1'].mean().reindex(range(24), fill_value=0.0).tolist()
except Exception as e:
print(f"データ読み込みエラー: {e}")
return

# 気象庁データ取得
hourly_sun = get_jma_sunshine_hourly(base_date)

# 学習モデルの更新
update_learning_model(base_date, hourly_actual, hourly_sun)

# 故障検知
is_anomaly = False
anomaly_details = []
if os.path.exists(MODEL_PATH):
model_df = pd.read_parquet(MODEL_PATH)
month = base_date.month

for h in range(8, 17): # 影の影響が少ない時間帯を重点チェック
sun = hourly_sun[h]
if sun and sun > 0.3: # ある程度の日照がある場合
expected = model_df.loc[month, f"h_{h}"] * sun
actual = hourly_actual[h]
if actual < expected * THRESHOLD_RATIO:
is_anomaly = True
anomaly_details.append(f"{h}時(実測{actual:.1f}kW/期待{expected:.1f}kW)")

# メッセージ作成
total_gen = sum(hourly_actual) * (10/60) # 10分データの場合の概算
status_msg = "✅ 正常" if not is_anomaly else "⚠️ 故障の疑い"

message = f"⚡️ {date_label} 発電実績 ({status_msg})\n" \
f"総発電量:{total_gen:.2f} kWh\n"

if is_anomaly:
message += "\n【異常検知】\n" + "\n".join(anomaly_details)
send_line_message("太陽光発電アラート", message, "ALERT")
else:
send_line_message("太陽光発電データ", message, "OK")

print(message)

if __name__ == "__main__":
main()

上の実装は、あくまでもサンプルです。 特に”★”印のある行は環境に合わせて修正してください。

このコードで検証していますが、かなり過敏で誤検知が多いです。
閾値調整で、精度を上げることになると思いますが、過去実装での故障判定ロジックを超える精度にはならなさそうです。 誤検知をゼロにするには、十分に学習させたうえで、1日中快晴だった日のデータでのみで判定する必要がありそうです。

他に改善案などあればコメントをお願いします。

関連記事

 

」

AE測定、長期ロギングシステム構築 (その7) 運用開始編

前回までで、AEイベントを収集するセンサーから信号を取り出し、Arduinoで複数チャンネルのAD変換してイベントをとらえる事前検討が完了しました。ここでは、長期ロギングに向けて最終的な統合をしていきます。

ArduinoIDE
ArduinoIDE

 長期ロギング用の実装を事前準備していたのですが、実際にロギングを開始してみるといくつもの新たな課題を確認しました。それらの課題はつぎのとおりです。

  • Flashへのイベントデータの記録ができない。
  • EEPROMなどのデータ回収でavrdude.exeを使うとリセットが発生する
  • シリアルモニタを使うとON/OFFでリセットが発生する。
  • 閾値を超えていないイベントが記録される。
  • ロギング用プログラムを起動した直後にイベントが記録される。
  • 隣のチャンネルの信号の影響を受けているように見える。

これらの課題について、以下のように検討して、対処していきました。

Flashへのイベントデータの記録ができない。

できるだけ多くのイベントデータを記録できるようにEEPROMではなく、Flashに記録する方針でしたが、動作検証するとこのArduinoは、細工しないとFlashに記録できないことが判明しました。 次の件の課題もあり、Flashへの記録はあきらめて、EEPROMを目いっぱい活用することにしました。イベントデータは1ブロック8byteに圧縮する仕様としました。

EEPROMなどのデータ回収でavrdude.exeを使うとリセットが発生する

 avrdude.exeを使うには、一度リセットしなければならないことが判明しました。検討の結果、データ回収、などのすべての運用に必要な機能を、イベントロギングと並行で処理できるように実装することにしました。

シリアルモニタを使うとON/OFFでリセットが発生する。

シリアルモニタは、ON/OFFで、リセット信号を送信する仕様になっているようだ。シリアルモニタでデータ回収することも検討していたが、データを吸い上げる実装もロギングツール内に実装することにしました。

閾値を超えていないイベントが記録される。

これはいくつかバグや後述の項目の問題が関わっていました。
まず、最初の実装では、閾値判定したデータそのものがイベントのそのチャンネルでの最大値に反映されておらず、さらに閾値を越えた後に配列データの0クリアを行う構造になっており、閾値越え検出から本計測開始までに余計な時間がかかっていました。そこで、サンプリングの高速化と閾値越えから本計測開始までの時間を最小化できるように、簡易オシロで採用したリングバッファ構造を採用しました。

ロギング用プログラムを起動した直後にイベントが記録される。

リセット直後(ほぼ同時に)に閾値を越えていないイベントが複数回検出された。毎回検出されるわけではないが、記録量引きの少ないEEPROMを無駄に消費するので、これの対策を検討した。これが前段の件と関連して発生していた。どうもAD変換のレジスタにリセット直前の電位が残っていることがあるようだ。そこで、リセット直後の約1秒間はイベント情報をEEPROMに書き込まない仕様に変更しました。

隣のチャンネルの信号の影響を受けているように見える。

隣のセンサーに影響が出ないように衝撃を与えた時に隣のチャンネルの信号の影響を受けているように見えた。当初、AD変換で直前のチャネルの電位の影響を受けて信号を拾っているのではないかと調整を試したが改善しなかった。 顧みると実験室で検証したときよりより多くの隣のチャンネルの影響を受けているようだった。試しに実験室内での計測したチャンネル切り替えで隣のチャンネルの影響を受けていなかったプログラムに戻して検証すると確かに、実験室の時より隣のチャンネルの影響が大きいことを確認した。この結果から、AD変換チャンネル切り替えの問題ではなく、物理的な信号ラインの クロストークが発生していると判断した。 フィールドへ配置する際に配線を束ねており、実験室に比べて電磁的クロストークが発生しやすい状況になったと推測されるそこで、信号ライン同士をできるだけ物理的に離すように調整した。 それでもある程度影響が残るので、可能な範囲で波形観測もできるように、リングバッファのほかに波形保存用のウインドウバッファを使う仕様に拡張しました。RAMではあるが、最後の2回については保存できるようにしました。

最終ロギング用 Arduinoスケッチ

#include <EEPROM.h>
// 2026/01/08 update
//
const int NUM_CHANS = 4;
const int W_NUM = 2; // ウインドウ数
const int DISPLAY_POINTS = 50; // 画面に表示したい総行数
const int PRE_TRIGGER_SAMPLES = 5; // そのうちの20%(過去分)
const int POST_TRIGGER_SAMPLES = DISPLAY_POINTS - PRE_TRIGGER_SAMPLES; // 残りの80%(未来分)

const unsigned long RECORD_WINDOW_MS = 1000;
const int EEPROM_MAX = 1024;
const int LIMITSC = 500; // シリアルチェックのサンプルカウント間隔
const int BUFFER_SIZE = 1100; // バッファサイズ (1.1KB)
const int WBUFFER_SIZE = W_NUM * NUM_CHANS * DISPLAY_POINTS; // ウインドバッファサイズ (0.4KB) 2回x4chx50点
const int THRESHOLD = 5; // 閾値 (8bit: 0-255) ※約2.5V


int currentAddr = 0;
bool isMonitoring = false;
uint8_t maxValues[NUM_CHANS];
unsigned long windowStartTime;
unsigned long windowTime[W_NUM];

int samplecount = 0;
int sw = 0; //ウインド書き込み位置

byte buffer[BUFFER_SIZE];
byte wbuffer[WBUFFER_SIZE];
int head = 0; // バッファ書き込み位置

void setup() {
Serial.begin(115200);
ADCSRA &= ~(bit(ADPS2) | bit(ADPS1) | bit(ADPS0));
ADCSRA |= bit(ADPS2);

findNextAddr();

// 起動記録 (01: Reset)
saveEventLog(0x01, 0);

Serial.println(F("SYSTEM_READY"));
}

void loop() {
// 1. シリアル通信処理 (バイナリ同期プロトコル対応)
if (samplecount > LIMITSC) {
handleSerial();
samplecount = 0;
} else { ++samplecount; }

// --- 1. 定常サンプリング ---
int val = analogRead(A0 + (head % NUM_CHANS));
buffer[head] = (byte)(val >> 2);

// --- 2. トリガー判定 ---
if ( buffer[head] > THRESHOLD) {
capture(head);
// 送信が終わったら head をリセットして、次のトリガーに備える
}

head = (head + 1) % BUFFER_SIZE;

/*
// 2. 監視ロジック
unsigned long now = millis();
if (!isMonitoring) {
for (int ch = 0; ch < NUM_CHANS; ch++) {
uint8_t val = analogRead(A0 + ch ) >> 2;
if (val > THRESHOLDS[ch]) {
isMonitoring = true;
windowStartTime = now;
maxValues[ch] = val;
break;
}
}
} else {
for (int ch = 0; ch < NUM_CHANS; ch++) {
uint8_t val = analogRead(A0 + ch ) >> 2;
if (val > maxValues[ch]) maxValues[ch] = val;
}
if (now - windowStartTime >= RECORD_WINDOW_MS) {
saveLog03(now);
for (int i = 0; i < NUM_CHANS; i++) maxValues[i] = 0;
isMonitoring = false;
}
}
*/

}

//ウインドウバッファへのコピーとイベント登録
void capture(int tIndex) {
unsigned long now = millis();
// 1. 未来分をサンプリング
for (int i = 0; i < (POST_TRIGGER_SAMPLES * NUM_CHANS); i++) {
head = (head + 1) % BUFFER_SIZE;
int v = analogRead(A0 + (head % NUM_CHANS));
buffer[head] = (byte)(v >> 2);
}

// 2. ウイドウコピー開始位置の計算
int startOffset = (tIndex - (PRE_TRIGGER_SAMPLES * NUM_CHANS) + BUFFER_SIZE * 2) % BUFFER_SIZE;
startOffset = (startOffset / NUM_CHANS) * NUM_CHANS;
windowTime[sw] = now;

// 3. ウイドウコピー
for (int i = 0; i < NUM_CHANS; i++) maxValues[i] = 0;
for (int i = 0; i < DISPLAY_POINTS; i++) {
for (int ch = 0; ch < NUM_CHANS; ch++) {
int idx = (startOffset + (i * NUM_CHANS) + ch) % BUFFER_SIZE;
int wi = (sw * NUM_CHANS * DISPLAY_POINTS + (i * NUM_CHANS) + ch) % WBUFFER_SIZE;
wbuffer[wi] = buffer[idx];
if (buffer[idx] > maxValues[ch]) maxValues[ch] = buffer[idx];
}
//

}


saveLog03(now);
sw = (sw + 1) % W_NUM;
}



// --- EEPROM記録関数 ---
void findNextAddr() {
currentAddr = 0;
while (currentAddr <= EEPROM_MAX - 8) {
uint8_t tag = EEPROM.read(currentAddr);
if (tag != 0x01 && tag != 0x02 && tag != 0x03) break;
currentAddr += 8;
}
}

// 種別01(Reset), 02(Sync)用
// Syncの場合は data に UnixTime を入れる
void saveEventLog(uint8_t type, uint32_t data) {
if (currentAddr > EEPROM_MAX - 8) return;
EEPROM.update(currentAddr + 0, type);
// データ4バイト分 (UnixTimeなど)
EEPROM.update(currentAddr + 1, (uint8_t)(data & 0xFF));
EEPROM.update(currentAddr + 2, (uint8_t)((data >> 8) & 0xFF));
EEPROM.update(currentAddr + 3, (uint8_t)((data >> 16) & 0xFF));
EEPROM.update(currentAddr + 4, (uint8_t)((data >> 24) & 0xFF));
// millis上位3バイト
unsigned long ms = millis();
EEPROM.update(currentAddr + 5, (uint8_t)(ms >> 24));
EEPROM.update(currentAddr + 6, (uint8_t)(ms >> 16));
EEPROM.update(currentAddr + 7, (uint8_t)(ms >> 8));
currentAddr += 8;
}

// 種別03(Measurement)用
void saveLog03(unsigned long timestamp) {
if ((currentAddr > EEPROM_MAX - 8) || millis() < 1025 ) return;
EEPROM.update(currentAddr + 0, 0x03);
for(int i=0; i<NUM_CHANS; i++) EEPROM.update(currentAddr + 1 + i, maxValues[i]);
EEPROM.update(currentAddr + 5, (uint8_t)(timestamp >> 24));
EEPROM.update(currentAddr + 6, (uint8_t)(timestamp >> 16));
EEPROM.update(currentAddr + 7, (uint8_t)(timestamp >> 8));
currentAddr += 8;
}

void handleSerial() {
if (Serial.available() > 0) {
uint8_t firstByte = Serial.peek();

// バイナリ同期パケット (0x02) の処理
if (firstByte == 0x02) {
uint8_t buf[6];
if (Serial.readBytes(buf, 6) == 6) {
uint8_t checksum = (buf[1] + buf[2] + buf[3] + buf[4]) & 0xFF;
if (checksum == buf[5]) {
uint32_t unixTime = ((uint32_t)buf[4] << 24) | ((uint32_t)buf[3] << 16) | ((uint32_t)buf[2] << 8) | buf[1];
saveEventLog(0x02, unixTime);
Serial.println(F("OK: Sync Success"));
} else {
Serial.println(F("Error: Checksum Mismatch"));
}
}
}
// テキストコマンドの処理 (既存のRead/Clear)
else {
char cmd = Serial.read();
if (cmd == 'D') { // Dump
Serial.println(F("START_DUMP"));
for (int i = 0; i < EEPROM_MAX; i++) {
uint8_t b = EEPROM.read(i);
if (b < 16) Serial.print('0');
Serial.print(b, HEX);
if ((i + 1) % 16 == 0) Serial.println();
}
Serial.println(F("END_DUMP"));
}
else if (cmd == 'C') { // Clear
for (int i = 0; i < EEPROM_MAX; i++) EEPROM.update(i, 0xFF);
currentAddr = 0;
Serial.println(F("OK: EEPROM Cleared"));
}
else if (cmd == 'P') { // Peek Get 波形取得
Serial.println(F("START_PDUMP"));
// 時間情報の出力 (ここはOK)
for (int i = 0; i < W_NUM; i++) {
Serial.print(windowTime[i]);
Serial.println(); // 改行があった方が良いかもしれません
}
// 波形データの出力
// wbufferは [ウィンドウ数 * ポイント数 * チャンネル数] の1次元配列
for (int i = 0; i < WBUFFER_SIZE; i += NUM_CHANS) { // NUM_CHANSずつ進める
for (int ch = 0; ch < NUM_CHANS; ch++) {
Serial.print(wbuffer[i + ch]);
if (ch < NUM_CHANS - 1) Serial.print(",");
}
Serial.println();
}
Serial.println(F("END_PDUMP"));
}
}
}
}

上記のArduinoスケッチのほかに、PC側のPowerShellツール(同期、EEPROMログ回収、EEPROMクリア、波形データ回収)を用意しています。

読みだした生データ(EEPROM)

02E7085F690004AA0300000009004B42
01000000000000000308000000000000
0286325F690000380100000000000000
02C3395F690000400337000000000313
034600000000123F03030C000100189E
03000306000018AD03030602020018BE
03040702030018BFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

読みだしたデータ(波形分)(太文字は追記コメント)

1621795     ←1件目のイベント検出 millis
1621745 ←2件目のイベント検出 millis
0,1,0,0 ←ここから1件目の波形データ
0,0,0,0
0,0,1,3
0,1,0,0
4,4,0,0
1,7,2,0 ←ここで閾値越え
0,2,2,0
0,0,2,1
0,0,1,2
1,0,0,1
3,0,0,0
2,1,0,0
2,3,0,0
1,4,1,0
0,4,0,0
0,1,1,0
0,0,0,0
0,0,0,1
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0 ←ここのへんから2件目の波形データ
0,3,0,0
0,0,1,1
0,0,0,1
3,0,0,0
2,6,0,0 ←ここで閾値越え
0,4,1,0
0,2,2,1
0,0,1,1
0,0,1,2
1,0,0,1
1,0,0,1
3,1,0,0
2,2,0,0
1,2,0,0
0,3,0,0
0,1,0,0
0,1,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0
0,0,0,0

本番運用開始直前の検証結果

[ EEPROM Data Analysis Report ]
Type | Date Time (UTC/Local) | Millis (raw) | Details
--------------------------------------------------------------------------------
SYNC | 2026-01-08 10:31:19 | 305664 | Time Synchronized (Unix:1767835879)
DATA | 2026-01-08 11:48:25 | 4932096 | CH0:0, CH1:0, CH2:0, CH3:9
RSET | 2026-01-08 13:28:39 | 0 | System Boot / Reset Detected
DATA | 2026-01-08 13:28:39 | 0 | CH0:8, CH1:0, CH2:0, CH3:0
SYNC | 2026-01-08 13:28:54 | 14336 | Time Synchronized (Unix:1767846534)
RSET | 2026-01-08 13:59:30 | 0 | System Boot / Reset Detected
SYNC | 2026-01-08 13:59:47 | 16384 | Time Synchronized (Unix:1767848387)
DATA | 2026-01-08 14:02:52 | 201472 | CH0:55, CH1:0, CH2:0, CH3:0
DATA | 2026-01-08 14:19:26 | 1195776 | CH0:70, CH1:0, CH2:0, CH3:0
DATA | 2026-01-08 14:26:23 | 1613312 | CH0:3, CH1:12, CH2:0, CH3:1
DATA | 2026-01-08 14:26:27 | 1617152 | CH0:0, CH1:3, CH2:6, CH3:0
DATA | 2026-01-08 14:26:32 | 1621504 | CH0:3, CH1:6, CH2:2, CH3:2
DATA | 2026-01-08 14:26:32 | 1621760 | CH0:4, CH1:7, CH2:2, CH3:3
--------------------------------------------------------------------------------
Analysis Finished.

イベント情報は、上のログ解析結果のように確認できるようになりました。
14:00以降は、センサーを叩いて信号を検知できるか検証しました。
センサーの固定方法(ピエゾ素子の振動を貼り付けたボンドが押さえている?)の問題か、実験室より感度が鈍くなったような気がします。本番運用と変更して、残りのセンサーを使って感度を上げられないかを別途検討してみます。

当初の計画どおりとまでは言えませんが、これで、本番運用を開始できました。


関連記事

AE測定、長期間ロギングシステム構築 (その6) 最終検証編

前回までで、センサーから信号を取り出し、Arduinoで複数チャンネルのAD変換してイベントをとらえる事前検討が完了しました。ここでは、長期ロギングに向けて最終的な調整をしていきます。チャンネルごとにセンサーの設置状況も違うので、センサーごとに異なる閾値を設定したほうが良いと推測されます。
 

閾値検討用元データの取得用 Arduinoスケッチ

const int NUM_CHANS = 4;
const int MAX_HIST_INDEX = 200;
unsigned int histogram[MAX_HIST_INDEX + 1][NUM_CHANS];

// タイムアウト設定 (2時間 = 7,200,000ms)
const unsigned long TIMEOUT_MS = 7200000;
unsigned long startTime;

void setup() {
Serial.begin(115200);
ADCSRA &= ~(bit(ADPS2) | bit(ADPS1) | bit(ADPS0));
ADCSRA |= bit(ADPS2);

for (int i = 0; i <= MAX_HIST_INDEX; i++) {
for (int j = 0; j < NUM_CHANS; j++) histogram[i][j] = 0;
}

Serial.println(F("Long-term Noise Analysis Started..."));
Serial.println(F("Target: 20,000 non-zero samples OR 2 hours."));
startTime = millis();
}

void loop() {
bool limitReached = false;
unsigned long nonZeroCount = 0; // 0以外の総サンプル数

while (!limitReached) {
// タイムアウトチェック
if (millis() - startTime >= TIMEOUT_MS) {
Serial.println(F("Time limit reached (2 hours)."));
limitReached = true;
break;
}

for (int ch = 0; ch < NUM_CHANS; ch++) {
int rawVal = analogRead(A0 + ch) >> 2;

// 0以外の場合のみカウント
if (rawVal > 0) {
int index = (rawVal > MAX_HIST_INDEX) ? MAX_HIST_INDEX : rawVal;
histogram[index][ch]++;
nonZeroCount++;

// 0以外のサンプルが合計20,000に達したら終了
if (nonZeroCount >= 20000) {
Serial.println(F("Non-zero sample limit reached."));
limitReached = true;
break;
}
}
}
}

// 結果の出力
Serial.println(F("Value,CH0,CH1,CH2,CH3"));
for (int i = 1; i <= MAX_HIST_INDEX; i++) { // 0は除外して出力
Serial.print(i);
if (i == MAX_HIST_INDEX) Serial.print("+");
for (int ch = 0; ch < NUM_CHANS; ch++) {
Serial.print(",");
Serial.print(histogram[i][ch]);
}
Serial.println();
}

Serial.print(F("Total Non-Zero Samples: ")); Serial.println(nonZeroCount);
Serial.print(F("Elapsed Time (ms): ")); Serial.println(millis() - startTime);
Serial.println(F("--- Analysis Finished ---"));
while (1);
}

開発環境(室内)での事前検証結果

Long-term Noise Analysis Started...
Target: 20,000 non-zero samples OR 2 hours.
Time limit reached (2 hours).
Value,CH0,CH1,CH2,CH3
1,0,2,2,1
2,0,2,0,0
3,0,0,0,1
4,0,0,0,0
5,0,0,0,0
6,0,0,0,0
7,0,0,0,0
8,0,0,0,0
9,0,0,0,0
10,0,0,0,0
11,0,0,0,0
12,0,0,0,0
13,0,0,0,0


186,0,0,0,0
187,0,0,0,0
188,0,0,0,0
189,0,0,0,0
190,0,0,0,0
191,0,0,0,0
192,0,0,0,0
193,0,0,0,0
194,0,0,0,0
195,0,0,0,0
196,0,0,0,0
197,0,0,0,0
198,0,0,0,0
199,0,0,0,0
200+,0,0,0,0
Total Non-Zero Samples: 8
Elapsed Time (ms): 7200214
--- Analysis Finished ---

この結果から、2時間放置してもノイズ信号のMax値は3であり、少しだけ余裕を見て閾値を5に設定すればよさそうだと判断しました。 そして次はフィールドテストへシフトしました。

フィールドテストでの追加検討

 機器類を整理して、実環境に配置して、閾値を再検討してみました。2時間の放置検証して、事前検証と同じ結果を得ました。 チャンネル別に閾値を設定する想定でしたが、かなり低い閾値設定なので、すべてのチャンネルとも5に設定することにします。

 これで、長期ロギングに向けた最終検討に移行できそうです。
次回は、最終検証、本番運用に移行していきます。


関連記事