サイト内検索

第4話でご紹介するデモは、マイコンボードに内蔵された加速度センサー(IMU)により、活動の変化を検出し、それをNVIDIA NemoClawで取り扱う方法を示すものです。活動は、人の動きや、製造装置の振動など、何でも良く、特に限定しておりませんが、短時間に急激に変化することを想定しております。そのため、AIエージェントが、活動の変化にリアルタイムに反応すべきユースケースを想定しており、その点が3話までのデモと大きく異なります。

システム構成

IMUとしてはmicro:bitに内蔵のものを利用しました。micro:bitからNVIDIA DGX Spark™へは、BLE UARTでセンサーデータを送信し、micro:bit MQTTブリッジで、MQTTプロトコルに変換してパブリッシュされます。データは、Node-REDフローの中でサブスクライブされ、活動の変化を検出します。検出された変化イベントをリアルタイムにNemoClawへ通知する目的で、NemoClawメッセージサーバーを設けています。

モジュール

役割

micro:bit (IMU)

センサーデータの発信元

micro:bit MQTTブリッジ

BLE UART → MQTT変換

Mosquitto

MQTT Broker

Node-RED

センサーデータ処理・活動変化検出・ダッシュボード

NemoClaw メッセージサーバー

Node-RED → NemoClaw間のAPI

vLLMサーバー

ローカルLLMの実行

NemoClaw

中心的なAIエージェント、SKILL.mdに基づく行動

Discord

ユーザーへの通知チャネル

リアルタイムイベント通知

イベント通知の問題点(※本連載執筆時における)

・ ポーリング方式ではリアルタイム性に課題がある
NemoClaw
の中核となるOpenClawでは、前話のデモで紹介したように、Cronジョブ機能を用いて一定間隔で外部イベントの発生をポーリングします。しかし、異常検出などリアルタイム性が求められる用途には不向きなため、外部プログラムからNemoClawへプッシュ型でイベントを通知する仕組みが必要です。

・ Webhook機能を利用できない
本来はOpenClawのWebhook機能を利用する方法が考えられますが、本連載執筆時点ではNemoClawからユーザーが利用できないため、別の方法を検討する必要があります。

・ Discordを利用したボット間通信ができない
Discordチャネルなど、メッセージチャネルからOpenClawに入力されるメッセージに対して、AIエージェントは即座に反応できるため、当初、メッセージチャネルでイベントを通知することを考えました。しかし、この方法では、イベント通知を発信する側もDiscordボットとなり、メッセージ受信側に既に存在するDiscordボットとのボット間通信になってしまいます。このような通信はDiscordでは禁止されています。

採用したイベント通知方法

・ サンドボックス内のコマンドを実行できる nemoclaw sandbox exec というコマンドがあります。

・ この機能を用いて、サンドボックス内で openclaw agent --agent main --message ユーザーメッセージ を実行すると、即座にNemoClaw内のAIエージェントにメッセージを配信できます。

・ このコマンドをDockerコンテナ内で動作しているNode-REDから実行するため、APIエンドポイント化します。

・ この方法で配信されたメッセージに対しては、AIエージェントが返信先を判断できないため、SKILLで返信先となるDiscordチャネルのチャネルIDを指定する必要があります。

 

IMUデータ送信

IMUデータをBLE UARTで送信するコードは以下のとおりです。

Node-REDフロー

Node-REDフローで以下の機能を実現しました。

・ MQTT
サブスクライバー
・ ダッシュボード(チャート表示)
・ 活動モニター(変化検出)

ダッシュボード

ダッシュボードで、リアルタイムにIMUセンサーの値をモニターできます。

活動モニター

活動をモニターし変化を検出するコードは、Node-RED関数で実装しました。リアルタイム性を確保する観点から、この機能をNemoClaw内部に配置するのではなく、検出された変化イベントの取り扱いをNemoClawのAIエージェントに任せるのが良いと思います。以下が、活動をモニターする関数です。

// ===== function ノード: 動作切り替わり検知 =====
// 入力 msg.payload: { accl_x, accl_y, accl_z, gyro_x, gyro_y, gyro_z }  (1サンプル)
// 出力: 切り替わりが起きた時だけ msg を送出
const WIN = env.get("AD_WINDOW");           // 窓サイズ(サンプル)
const HOP = env.get("AD_HOP");              // 何サンプルごとに判定するか
const MIN_HOLD = env.get("AD_MIN_HOLD");    // 状態確定に必要な連続回数
// しきい値(キャリブレーション値で上書き推奨)
const TH_HIGH = flow.get("thHigh") ?? 200.0;
const TH_LOW = flow.get("thLow") ?? 10.0;
// --- リングバッファ取得 ---
let buf = flow.get("imuBuffer") || [];
const p = msg.payload;
const accMag = Math.sqrt(p.accel.x * p.accel.x + p.accel.y * p.accel.y + p.accel.z * p.accel.z);
buf.push({accMag});
if (buf.length > WIN) buf.shift();   // 古いものを捨てる
flow.set("imuBuffer", buf);
// --- HOPごと & 窓が満ちた時だけ評価 ---
let cnt = (flow.get("hopCount") || 0) + 1;
flow.set("hopCount", cnt);
if (buf.length < WIN || cnt < HOP) return null;
flow.set("hopCount", 0);
// --- 特徴量(加速度の標準偏差 + 角速度平均) ---
const accArr = buf.map(b => b.accMag);
const mean = accArr.reduce((a, b) => a + b, 0) / accArr.length;
const variance = accArr.reduce((a, b) => a + (b - mean) ** 2, 0) / accArr.length;
const accStd = Math.sqrt(variance);
const score = accStd;
flow.set("score", score);
// --- ヒステリシス + 最小保持で状態判定 ---
let st = flow.get("state") ?? 0;   // 0=STILL, 1=ACTIVE
let cand = flow.get("cand") ?? 0;
let hold = flow.get("hold") ?? 0;
let target = (st === 0) ? (score > TH_HIGH ? 1 : 0)
    : (score < TH_LOW ? 0 : 1);
if (target === cand) hold++; else { cand = target; hold = 1; }
let event = null;
if (cand !== st && hold >= MIN_HOLD) {
    const label = (cand === 1) ? "STILL->ACTIVE" : "ACTIVE->STILL";
    st = cand;
    event = {
        payload: { label },
        topic: "motion/event"
    };
}
flow.set("state", st);
flow.set("cand", cand);
flow.set("hold", hold);
return event;

AIエージェントからの通知

SKILLでDiscordチャネルのIDを指定して、通知方法を指示しているので、活動の変化が検出されれば、ユーザーからの問い合わせ不要で通知されます。

お問い合わせ



第4話では、加速度センサー(IMU)を利用して活動の変化を検出し、AIエージェントへリアルタイムに通知する方法について解説しました。
第5話では、これまでの連載内容を振り返るとともに、NemoClawでIoTセンサーデータを活用するうえでの今後の課題について解説します。