跳到主要内容

GR-Media

1. 概述

GR-Media 是机器人媒体操作的路由运行时。运行时服务 fouriermedia 通过 DDS 接收一个简单的媒体操作 ID,然后按通道转发:

  • LED 动作通过 /opt/fftai/fouriermedia/bin/cfg/media_leds_action.json 配置,并通过 UDP 发给灯控。
  • 眼睛动作通过 /opt/fftai/fouriermedia/bin/cfg/media_eyes_action.json 配置,并通过 DDS topic head_eyes_cmd 转发给头部。
  • 声音动作通过 DDS topic head_sound_cmd 转发给头部。
元素数值
运行时应用名fouriermedia
Domain ID123
输入消息fourier_msgs/msg/MediaSignalCmd
输入 topicmedia_signal_led_cmdmedia_signal_eyes_cmdmedia_signal_sound_cmd
头部输出 topichead_eyes_cmdhead_sound_cmd

2. DDS 接口

2.1 Topics

订阅

Topic消息类型说明
media_signal_led_cmdfourier_msgs/msg/MediaSignalCmdLED ID。ID 必须存在于 media_leds_action.json
media_signal_eyes_cmdfourier_msgs/msg/MediaSignalCmd眼睛 ID。ID 必须存在于 media_eyes_action.json
media_signal_sound_cmdfourier_msgs/msg/MediaSignalCmd声音 ID,会原样转发为 SoundControlCmd.sound_id

发布 / 转发

Topic / 输出消息类型说明
head_eyes_cmdfourier_msgs/msg/EyesControlGroupCmd眼睛动作解析完成后发布。
head_sound_cmdfourier_msgs/msg/SoundControlCmd收到声音动作后发布。

2.2 IDL 定义

MediaSignalCmd

Idl
struct MediaSignalCmd {
std_msgs::msg::TracerHeader header;
int32 action; // action ID
};

EyesControlGroupCmd

Idl
struct EyesControlCmd {
int32 eyes_index; // 0 = 左眼,1 = 右眼
int32 emoji_id;
int32 x; // -100..100
int32 y; // -100..100
};

struct EyesControlGroupCmd {
std_msgs::msg::TracerHeader header;
sequence<EyesControlCmd> eyes_cmd;
};

SoundControlCmd

Idl
struct SoundControlCmd {
std_msgs::msg::TracerHeader header;
int32 sound_id;
};

3. 动作 ID

3.1 LED

Action ID含义
0警告红,常亮
1警告红,呼吸
2警告橙,常亮
3警告橙,呼吸
4正常白,常亮
5正常白,呼吸
6工作蓝,常亮
7工作蓝,呼吸
8工作紫,常亮
9工作紫,呼吸
10触摸粉,常亮

3.2 眼睛屏幕

以下 ID 来自 media_eyes_action.json

Action ID含义
0居中 / 默认
1向上
2微笑
3向下
4向右
5向左
6右上
7左上
8右下
9左下

3.3 声音动作

声音动作 ID 会直接作为 SoundControlCmd.sound_id 转发,实际音频映射由头部声音服务处理。

4. 基础示例

4.1 Python —— 发送媒体动作

下面脚本会向指定通道发布一个 MediaSignalCmd。运行前确认 fouriermedia 已启动。

Python
import argparse
import time

from fourierdds_py import DDSInterface
import fourier_msgs.msg.MediaSignalCmd as MediaSignalCmd

TOPICS = {
"led": "media_signal_led_cmd",
"eyes": "media_signal_eyes_cmd",
"sound": "media_signal_sound_cmd",
}

parser = argparse.ArgumentParser()
parser.add_argument("channel", choices=sorted(TOPICS))
parser.add_argument("action", type=int)
parser.add_argument("--domain", type=int, default=123)
args = parser.parse_args()

dds = DDSInterface(domain_id=args.domain)
pub = dds.create_publisher(
MediaSignalCmd.MediaSignalCmdPubSubType,
TOPICS[args.channel],
)

# 等待 DDS discovery,避免第一帧过早发送。
time.sleep(1.0)

msg = MediaSignalCmd.MediaSignalCmd()
msg.action(args.action)

print(f"[publish] channel={args.channel} topic={TOPICS[args.channel]} action={args.action}")
pub.publish(msg)
time.sleep(0.2)
dds.close()

运行示例:

Bash
# LED:工作蓝,常亮
python3 send_media_signal.py led 6

# 眼睛:微笑
python3 send_media_signal.py eyes 2

# 声音:向头部转发 sound_id=1
python3 send_media_signal.py sound 1

4.2 Python —— 观察头部输出

眼睛和声音通道会被 GR-Media 转发到头部 DDS topic。联调时可以用下面脚本验证转发结果。

Python
import time

from fourierdds_py import DDSInterface, SubscriberQosProfile
import fourier_msgs.msg.HeadMediaCmd as HeadMediaCmd

dds = DDSInterface(domain_id=123)


def seq_items(seq):
size = seq.size() if hasattr(seq, "size") else len(seq)
return [seq[i] for i in range(size)]


def on_eyes(msg):
for eye in seq_items(msg.eyes_cmd()):
print(
"[eyes]",
"index:", eye.eyes_index(),
"emoji:", eye.emoji_id(),
"x:", eye.x(),
"y:", eye.y(),
)


def on_sound(msg):
print("[sound] sound_id:", msg.sound_id())


dds.create_subscription(
HeadMediaCmd.EyesControlGroupCmdPubSubType,
"head_eyes_cmd",
on_eyes,
qos_profile=SubscriberQosProfile.default(),
)
dds.create_subscription(
HeadMediaCmd.SoundControlCmdPubSubType,
"head_sound_cmd",
on_sound,
qos_profile=SubscriberQosProfile.default(),
)

while True:
time.sleep(0.1)

4.3 C++ —— 发送媒体动作

C++
#include <MediaSignalCmd/MediaSignalCmdPubSubTypes.hpp>
#include "fourier_dds/dds_node.hpp"
#include "fourier_dds/dds_publisher.hpp"

#include <chrono>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <unordered_map>

int main(int argc, char** argv) {
if (argc != 3) {
std::cerr << "usage: " << argv[0] << " <led|eyes|sound> <action_id>\n";
return 1;
}

const std::unordered_map<std::string, std::string> topics = {
{"led", "media_signal_led_cmd"},
{"eyes", "media_signal_eyes_cmd"},
{"sound", "media_signal_sound_cmd"},
};

const std::string channel = argv[1];
const auto it = topics.find(channel);
if (it == topics.end()) {
std::cerr << "unknown channel: " << channel << "\n";
return 1;
}

const int action = std::atoi(argv[2]);
auto node = std::make_shared<fourier_dds::DdsNode>(123);
auto pub = std::make_shared<
fourier_dds::DdsPublisher<fourier_msgs::msg::MediaSignalCmdPubSubType>>(
node,
it->second);

std::this_thread::sleep_for(std::chrono::seconds(1));

fourier_msgs::msg::MediaSignalCmd msg;
msg.action(action);

std::cout << "[publish] channel=" << channel
<< " topic=" << it->second
<< " action=" << action << std::endl;
pub->publish(msg);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return 0;
}

编译示例:

Bash
g++ -std=c++17 send_media_signal.cpp -o send_media_signal \
-I/opt/fftai/include/fourier_dds \
-I/opt/fftai/fourier_dds_msgs/include/fourier_dds_msgs \
-L/opt/fftai/fourier_dds_msgs/lib \
-Wl,-rpath,/opt/fftai/fourier_dds_msgs/lib \
-lMediaSignalCmd \
-lfastdds \
-lfastcdr \
-pthread

运行示例:

Bash
./send_media_signal eyes 2

5. 进阶 —— 运行时流程

5.1 LED 流程

  1. media_signal_led_cmd 发布 MediaSignalCmd.action
  2. fouriermediacfg/media_leds_action.json 中查找该 ID。
  3. 如果动作存在,运行时将 LED 配置转换成 JSON。
  4. JSON 通过 UDP 发送到 执行侧。

5.2 眼睛流程

  1. media_signal_eyes_cmd 发布 MediaSignalCmd.action
  2. fouriermediacfg/media_eyes_action.json 中查找该 ID。
  3. 运行时生成左右眼各一条 EyesControlCmd
  4. 结果发布到 head_eyes_cmd

5.3 声音流程

  1. media_signal_sound_cmd 发布 MediaSignalCmd.action
  2. fouriermedia 将 action ID 写入 SoundControlCmd.sound_id
  3. 结果发布到 head_sound_cmd

6. 故障排查与注意事项

发布后无效果

确认 fouriermedia 已启动、DDS domain 为 123,并且发送到了正确通道。例如 LED 命令必须发到 media_signal_led_cmd,不要发到眼睛或声音 topic。

LED 或眼睛 action ID 无效

LED 和眼睛动作都依赖配置文件。若 ID 不存在于 media_leds_action.jsonmedia_eyes_action.json,运行时会打印 warning 并忽略该命令。