はじめに
今年の 1 月末にmocopiが発売されてから、ネット上で様々なmocopiを使った記事や動画が投稿されています。
弊社でも発売開始直後に購入していたのですが、しばらく放置中々記事にすることができず、早数ヶ月経過してしまいました。ただ、せっかく購入したのに何もしないのはもったいないため、遅ればせながらmocopiを使って色々なことをしていきたいと思います。
今回は、まずmocopiから送信されてくるデータのパーサーを作り、今後の開発をスムーズにできるようにできるようなライブラリを作っていこうと思います。
実装について
実装例がないか探してみたところ、公式からパースするためのライブラリはなかったものの、以下の有志の方が作成したコードがありました。
GitHub - seagetch/mcp-receiver: Open source implementation of receiver plugin for mocopi motion tracking system.
Open source implementation of receiver plugin for mocopi motion tracking system. - GitHub - seagetch/mcp-receiver: Open source implementation of receiver plugin for mocopi motion tracking system.
https://github.com/seagetch/mcp-receiver
この方の実装言語は Python でしたが、自分の予定では RaspberryPi や Arduino などのマイコンと連携していきたいと思っているので、Rustで実装しました。
調査
いざ実装を始めて見たものの、そもそも UDP レシーバーを実装したことがなかったため、実際にmocopiの通信を読み取り、Wiresharkを使用してどのようなデータが受信できるか見てみるところから始めました。
mocopi からデータを受信する
まずは UDP 経由でデータを受信できるのか確かめます。
説明書どおりにmocopiを体に装着し、モーションキャプチャ画面まで表示します。
そして、PC の IP アドレスに向けて UDP を送信するように設定し、送信します。
上手くいくと、以下のように PC でデータを受信できます。
Wireshark を使用して受信したデータを確認
mocopi データをパース
前節でデータを送信の確認ができたので、これと解析レポートを見ながら、このデータをプログラム上で受信し、データを利用しやすいようにパースする処理を書いていきます。
普段はRustを書かないので、最初はかなり苦戦しましたが、以下のブログで紹介されていた nom というライブラリを使用することで、何とか実装することができました。
Rustでバイナリを読み書きするのに必要なクレート3選 - aptpod Tech Blog
研究開発グループの大久保です。 当社の製品の中にはC/C++で書かれたものが存在し、その中には独自のバイナリフォーマットを取り扱うものが存在します。既存のコードとやり取りするようなRustのプロジェクトを起こすためには、その独自のバイナリフォーマットをRustで取り扱えるようにしなければなりません。しかしながら、Rustの標準ライブラリの機能だけでは、バイナリの読み書きは意外と面倒になります。そのため、今回はRustでバイナリを扱うのならぜひ知っておきたいクレートを3つご紹介します。
https://tech.aptpod.co.jp/entry/2020/10/09/090000
GitHub - rust-bakery/nom: Rust parser combinator framework
Rust parser combinator framework. Contribute to rust-bakery/nom development by creating an account on GitHub.
https://github.com/rust-bakery/nom
以下が実装したコードです。構造体の定義を見るとわかりますが、元データの構造から若干変えているところがあります。
use std::{env};
use std::io::{Cursor};
use std::net::{UdpSocket};
use local_ip_address::local_ip;
use nom::bytes::complete::take;
use nom::number::complete::{le_u32};
use nom::error::Error;
type BoneId = u16;
type TransVal = f32;
#[derive(Debug, PartialEq)]
pub struct SkeletonPacket {
pub head: Head,
pub info: Info,
pub skeleton: Skeleton,
}
#[derive(Debug, PartialEq)]
pub struct Head {
pub format: String,
pub ver: u8,
}
#[derive(Debug, PartialEq)]
pub struct Info {
pub addr: u64,
pub port: u16,
}
#[derive(Debug, PartialEq)]
pub struct Skeleton {
pub bones: Vec<Bone>,
}
#[derive(Debug, PartialEq)]
pub struct Bone {
pub id: BoneId,
pub parent: BoneId,
pub trans: Transform,
}
#[derive(Debug, PartialEq)]
pub struct FramePacket {
pub head: Head,
pub info: Info,
pub frame: Frame,
}
#[derive(Debug, PartialEq)]
pub struct Frame {
pub num: u32,
pub time: u32,
pub bones: Vec<BoneTrans>,
}
#[derive(Debug, PartialEq)]
pub struct BoneTrans {
pub id: BoneId,
pub trans: Transform,
}
#[derive(Debug, PartialEq)]
pub struct Transform {
pub rot: Rotation,
pub pos: Position,
}
#[derive(Debug, PartialEq)]
pub struct Rotation {
pub x: TransVal,
pub y: TransVal,
pub z: TransVal,
pub w: TransVal,
}
#[derive(Debug, PartialEq)]
pub struct Position {
pub x: TransVal,
pub y: TransVal,
pub z: TransVal,
}
#[derive(Debug, PartialEq)]
pub struct Data<'a> {
pub len: u32,
pub name: String,
pub data: &'a [u8],
pub rem: &'a [u8],
}
fn parse_value(data: &[u8]) -> Data {
// lengthの長さは4bytesで固定
let (data, length) = le_u32::<_, Error<_>>(data).unwrap() as (&[u8], u32);
// nameは4bytesの文字列
let (data, name) = take::<_, _, Error<_>>(4usize)(data).unwrap();
let name_str = String::from_utf8(name.to_vec()).unwrap();
// valueの長さはlengthの値による
let (rem, data) = take::<_, _, Error<_>>(length)(data).unwrap();
return Data {
len: length,
name: name_str,
data,
rem,
};
}
fn parse_head(data: &[u8]) -> (u32, Head) {
let data = parse_value(data);
let len = data.len;
// ftyp
let data= parse_value(data.data);
let format = String::from_utf8(data.data.to_vec()).unwrap();
// vrsn
let data = parse_value(data.rem);
let ver = data.data[0];
(len, Head { format, ver })
}
fn parse_info(data: &[u8]) -> (u32, Info) {
let data = parse_value(data);
let len = data.len;
// ipad
let data = parse_value(data.data);
let addr = u64::from_le_bytes(data.data.try_into().unwrap());
// rcvp
let data = parse_value(data.rem);
let port = u16::from_le_bytes(data.data.try_into().unwrap());
(len, Info { addr, port })
}
fn parse_skeleton(data: &[u8]) -> (u32, Skeleton) {
// skdf
let data = parse_value(data);
let len = data.len;
// bons
let (_, bones) = parse_bones(data.data);
(len, Skeleton { bones: *bones })
}
fn parse_frame(data: &[u8]) -> (u32, Frame) {
// fram
let data = parse_value(data);
let len = data.len;
// fnum
let data = parse_value(data.data);
let num = u32::from_le_bytes(data.data.try_into().unwrap());
// time
let data = parse_value(data.rem);
let time = u32::from_le_bytes(data.data.try_into().unwrap());
// btrs
let (_, bones) = parse_bone_trans(data.rem);
(len, Frame { num, time, bones: *bones })
}
fn parse_bone_trans(data: &[u8]) -> (u32, Box<Vec<BoneTrans>>) {
// btrs
let btrs_data = parse_value(data);
let btrs_len = btrs_data.len;
// btrsの下にあるbtdtをparseしていく
let mut bones: Vec<BoneTrans> = Vec::new();
let mut read_bytes: u32 = 0;
loop {
let part = &btrs_data.data[(read_bytes as usize)..];
// btdt
let data = parse_value(part);
let len = data.len;
// bnid
let data = parse_value(data.data);
let id = u16::from_le_bytes(data.data.try_into().unwrap());
// tran
let (_, trans) = parse_trans(data.rem);
bones.push(BoneTrans { id, trans });
read_bytes += len + 8;
if read_bytes == btrs_len {
break;
}
}
(btrs_len, Box::new(bones))
}
fn parse_bones(data: &[u8]) -> (u32, Box<Vec<Bone>>) {
// bons
let bons_data = parse_value(data);
let bons_len = bons_data.len;
// bonsの下にあるbndtをparseしていく
let mut bones: Vec<Bone> = Vec::new();
let mut read_bytes: u32 = 0;
loop {
let part = &bons_data.data[(read_bytes as usize)..];
// bndt
let data = parse_value(part);
let len = data.len;
// bnid
let data = parse_value(data.data);
let id = u16::from_le_bytes(data.data.try_into().unwrap());
// pbid
let data = parse_value(data.rem);
let parent = u16::from_le_bytes(data.data.try_into().unwrap());
// tran
let (_, trans) = parse_trans(part);
bones.push(Bone { id, parent, trans });
read_bytes += len + 8;
if read_bytes == bons_len {
break;
}
}
(bons_len, Box::new(bones))
}
fn parse_trans(data: &[u8]) -> (u32, Transform) {
// tran
let data = parse_value(data);
let len = data.len;
// 28bytesのデータを4bytesごとに取り出す
let mut values = [0.0; 7];
for i in 0..6usize {
let v = data.data[i * 4..(i * 4 + 4)].to_vec();
values[i] = f32::from_le_bytes(v.try_into().unwrap());
}
(len, Transform {
rot: Rotation { x: values[0], y: values[1], z: values[2], w: values[3] },
pos: Position { x: values[4], y: values[5], z: values[6] },
})
}
fn main() -> () {
let args: Vec<String> = env::args().collect();
let local_ip = local_ip().unwrap();
let port = match args.get(1) {
Some(s) => s.clone(),
None => String::from("12351"),
};
let addr = format!("{:?}:{}", local_ip, port);
let socket = UdpSocket::bind(&addr).expect("couldn't bind socket");
println!("Successfully {} binding socket", &addr);
println!("Listening...");
let mut buff = Cursor::new([0u8; 2048]);
loop {
socket.recv_from(buff.get_mut()).expect("didn't receive data");
let mut data: &[u8] = buff.get_ref();
let (len, head) = parse_head(data);
data = &data[((len + 8) as usize)..];
let (len, info) = parse_info(data);
data = &data[((len + 8) as usize)..];
let name= parse_value(data).name;
if name == "skdf" {
let (_, skeleton) = parse_skeleton(data);
let packet = SkeletonPacket { head, info, skeleton };
dbg!(&packet);
} else {
let (_, frame) = parse_frame(data);
let packet = FramePacket { head, info, frame };
dbg!(&packet);
}
}
}
実行すると、以下のようにコンソールに出力されます。
fram パケットの例
パッケージ化
せっかくなので、ここまで来たらパッケージ化まで進めようということで、Cargo パッケージとして公開しました。もしよかったら使ってみてください。
mocopi_parser - crates.io: Rust Package Registry
A parser of streamed data from mocopi
https://crates.io/crates/mocopi_parser
パッケージのソースコードは会社の GitHub にリポジトリにプッシュしたので、そこで確認できます。
GitHub - FOURIER-Inc/mocopi-parser: a parser library for streamed data from mocopi.
a parser library for streamed data from mocopi. Contribute to FOURIER-Inc/mocopi-parser development by creating an account on GitHub.
https://github.com/FOURIER-Inc/mocopi-parser
まとめ
今回はRustでmocopiの受信データをパースするライブラリを作りました。
今のところこのライブラリを使ってやろうとしている案は、
- mocopiでゲームをプレイする
- mocopiでロボットを操作する
の二つを考えているので、アイデアが固まったらドンドン作っていこうと思います。
ちなみに、Arduino や RaspberryPi は C 言語での実装経験はあるものの、Rustはコンパイルできると聞いたことがある程度の知識なので、もしかしたらこのライブラリは使えないかもしれないです。🤷♂️
新しいメンバーを募集しています
Hirayama / Engineer
1997年生まれ、南伊豆出身。学生時代にC#で画像処理アプリケーションを作ったりしていました。業務では主にLaravelを使用してサーバーサイドのプログラミングをしています。趣味はドライブとシミュレーションゲーム。
関連記事