AirPodsの回転角をmyCobotの姿勢と同期。 pic.twitter.com/vNcBK1gYO1
— 君塚史高 (@ki_230) 2024年2月1日
AirPods → iPhoneアプリ → Express → python-shell → pymycobot → myCobotと繋いでいって、AirPodsの回転角とmyCobotの姿勢を同期させました。
需要があるかは謎ですが、ざっくりとしたソースコードを記載します。
iOSアプリ
以前つくったこちらのアプリをベースにしています。
変更点だけを記載しておくと、onDeviceMotionUpdatesの中にWebサーバへのポストの処理を入れています。
また、アップデートがあるたびにPOSTリクエストを送ってしまうとサーバが大変なので、最低でも500ms間を開けています。
App.tsx(抜粋)
useEffect(() => { const delay = 500; const handleDeviceMotionUpdates = onDeviceMotionUpdates((data) => { // 前回から500ms未満の場合はリターン if (Date.now() - lastUpdateTimeRef.current < delay) { return; } // Webサーバにセンサ値をPOSTする axios.post(String(process.env.API_URL), { pitch: data.attitude.pitchDeg || 0, roll: data.attitude.rollDeg || 0, yaw: data.attitude.yawDeg || 0 }).then(() => { lastUpdateTimeRef.current = Date.now(); }).catch((err) => { console.error(err); lastUpdateTimeRef.current = Date.now(); }); setPitch(data.attitude.pitch); setPitchDeg(data.attitude.pitchDeg); setRoll(data.attitude.roll); setRollDeg(data.attitude.rollDeg); setYaw(data.attitude.yaw); setYawDeg(data.attitude.yawDeg); setGravityX(data.gravity.x); setGravityY(data.gravity.y); setGravityZ(data.gravity.z); setRotationRateX(data.rotationRate.x); setRotationRateY(data.rotationRate.y); setRotationRateZ(data.rotationRate.z); setUserAccelerationX(data.userAcceleration.x); setUserAccelerationY(data.userAcceleration.y); setUserAccelerationZ(data.userAcceleration.z); }); return () => { handleDeviceMotionUpdates.remove(); }; }, []);
POSTにはaxiosを使いました。
なので、
import axios from 'axios';
と、モジュールのインポートも追加する必要があります。
結果として、
App.tsx(全文)
import axios from 'axios'; // 簡単にPOSTするために追加 import React, { useEffect, useRef, // 500ms間を開けるために追加 useState, } from 'react'; import {Button, SafeAreaView, StyleSheet, Text} from 'react-native'; import { requestPermission, onDeviceMotionUpdates, startListenDeviceMotionUpdates, stopDeviceMotionUpdates, } from 'react-native-headphone-motion'; const API_URL = 'http://localhost:3000'; // POSTするURLを入れる export default function App() { const lastUpdateTimeRef = useRef<number>(0); // 最後の更新時刻を保持するために追加 const [pitch, setPitch] = useState(0); const [pitchDeg, setPitchDeg] = useState(0); const [roll, setRoll] = useState(0); const [rollDeg, setRollDeg] = useState(0); const [yaw, setYaw] = useState(0); const [yawDeg, setYawDeg] = useState(0); const [gravityX, setGravityX] = useState(0); const [gravityY, setGravityY] = useState(0); const [gravityZ, setGravityZ] = useState(0); const [rotationRateX, setRotationRateX] = useState(0); const [rotationRateY, setRotationRateY] = useState(0); const [rotationRateZ, setRotationRateZ] = useState(0); const [userAccelerationX, setUserAccelerationX] = useState(0); const [userAccelerationY, setUserAccelerationY] = useState(0); const [userAccelerationZ, setUserAccelerationZ] = useState(0); useEffect(() => { const delay = 500; // 更新間隔を変数にいれておく const handleDeviceMotionUpdates = onDeviceMotionUpdates(data => { if (Date.now() - lastUpdateTimeRef.current < delay) { // 更新間隔に満たない場合にリターン return; } // Webサーバにセンサ値をPOST // 成功しても失敗してもlastUpdateTimeRefを更新 // なんとなくawaitは使用せず axios .post(String(API_URL), { pitch: data.attitude.pitchDeg || 0, roll: data.attitude.rollDeg || 0, yaw: data.attitude.yawDeg || 0, }) .then(() => { lastUpdateTimeRef.current = Date.now(); }) .catch(err => { console.error(err); lastUpdateTimeRef.current = Date.now(); }); setPitch(data.attitude.pitch); setPitchDeg(data.attitude.pitchDeg); setRoll(data.attitude.roll); setRollDeg(data.attitude.rollDeg); setYaw(data.attitude.yaw); setYawDeg(data.attitude.yawDeg); setGravityX(data.gravity.x); setGravityY(data.gravity.y); setGravityZ(data.gravity.z); setRotationRateX(data.rotationRate.x); setRotationRateY(data.rotationRate.y); setRotationRateZ(data.rotationRate.z); setUserAccelerationX(data.userAcceleration.x); setUserAccelerationY(data.userAcceleration.y); setUserAccelerationZ(data.userAcceleration.z); }); return () => { handleDeviceMotionUpdates.remove(); }; }, []); return ( <SafeAreaView style={styles.container}> <Button title={'requestPermission'} onPress={async () => { await requestPermission(); }} /> <Button title={'startListenDeviceMotionUpdates'} onPress={async () => { await startListenDeviceMotionUpdates(); }} /> <Button title={'stopDeviceMotionUpdates'} onPress={async () => { await stopDeviceMotionUpdates(); }} /> <Text>{lastUpdateTimeRef.current}</Text> <Text>{`pitch: ${pitch}`}</Text> <Text>{`pitchDeg: ${pitchDeg}`}</Text> <Text>{`roll: ${roll}`}</Text> <Text>{`rollDeg: ${rollDeg}`}</Text> <Text>{`yaw: ${yaw}`}</Text> <Text>{`yawDeg: ${yawDeg}`}</Text> <Text>{`gravityX: ${gravityX}`}</Text> <Text>{`gravityY: ${gravityY}`}</Text> <Text>{`gravityZ: ${gravityZ}`}</Text> <Text>{`rotationRateX: ${rotationRateX}`}</Text> <Text>{`rotationRateY: ${rotationRateY}`}</Text> <Text>{`rotationRateZ: ${rotationRateZ}`}</Text> <Text>{`userAccelerationX: ${userAccelerationX}`}</Text> <Text>{`userAccelerationY: ${userAccelerationY}`}</Text> <Text>{`userAccelerationZ: ${userAccelerationZ}`}</Text> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: 'white', }, });
というコードになりました。
本当は、API_URLはアプリ上から指定できた方が便利なのですが、スピード重視で実装してしまいました。
ウェブサーバ
ウェブサーバはMacにローカルサーバを立てました。
まず、myCobotを動かすために、こちらの設定をします。
本当はpythonでウェブサーバをつくれればスムーズだと思うのですが、僕のスキルセットではNode.jsでつくるのが一番早いので、Expressを使ってささっとサーバをつくります。
myCobotとの通信はpythonで行うので、その部分はpython-shellをつかって行うことにしました。
app.js
require('dotenv').config(); // 外部からmyCobotのポートを渡すために使用 const express = require('express'); const { PythonShell } = require('python-shell'); // myCobotとの通信用 const app = express(); const http = require('http').Server(app); const duration = 100; // アプリ側のdelay(500ms)よりも小さくしないと破綻する app.use(express.json()); app.post('/', (req, res) => { try { const angles = [0, 0, 0, 0, 0, 0]; // myCobotの関節はhttps://www.elephantrobotics.com/wp-content/uploads/2021/03/myCobot-User-Mannul-EN-V20210318.pdfの13ページを参照 // 配列には6個の関節を下から順番に収納する // 関節ごとに可動域が決まっているので超えないように制限する angles[0] = Math.max(-90, Math.min(req.body.yaw || 0, 90)); // J1 angles[3] = Math.max(-90, Math.min(req.body.pitch || 0, 90)); // J4 angles[5] = Math.max(-175, Math.min(req.body.roll || 0, 175)); // J6 // USBで接続されているmyCobotをpythonで指示を出す PythonShell.runString( `from pymycobot.mycobot import MyCobot; MyCobot('${ process.env.MY_COBOT_PORT }').send_angles([${ angles }], ${ duration })`, null, (err) => err && console.error(err) ); } catch (err) { console.error(err); } res.send(200); }); try { const angles = [0, 0, 0, 0, 0, 0]; // 起動時に姿勢をリセットする PythonShell.runString( `from pymycobot.mycobot import MyCobot; MyCobot('${ process.env.MY_COBOT_PORT }').send_angles([${ angles }], ${ duration })`, null, (err) => err && console.error(err) ); } catch(err) { console.error(err); } http.listen(3000, '0.0.0.0');
という感じです。
PythonShellでpymycobotを実行するので、app.jsの同階層にpymycobotのpymycobotディレクトリを設置する必要があります。
準備ができたら、PCとmyCobotを繋いで、
node app.js
を実行すればウェブサーバが起動し、POSTリクエストで受け取った、pitch、roll、yawをmyCobotに伝達します。
今回はiPhoneアプリからAirPodsのセンサ値をPOSTしていますが、POST元はなんでも大丈夫なので、こういうサーバをひとつ作っておくと、どこかで活用できる気がしています。
DEMO
AirPodsの回転角をmyCobotの姿勢と同期。 pic.twitter.com/vNcBK1gYO1
— 君塚史高 (@ki_230) 2024年2月1日