本篇文章是经过作者kimizuka授权,进行编辑转载。
引言
本篇文章主要记录,将 AirPods 通过 iPhone 应用连接到 Express,再通过 python-shell 连接到 pymycobot,最后与 myCobot 同步,实现 AirPods 的旋转角度与 myCobot 的姿态同步 。
虽然不确定是否有需求,但我会提供一个大致的源代码。
项目结构
这个项目主要运用到的技术有headphone-motion,web服务器,node.js的express框架,python-shell,pymycobot。
这里简要介绍一下这些技术。
- Headphone-Motion:Headphone Motion 是一个利用特定技术来追踪和利用用户头部运动的项目。虽然具体细节可能依赖于实现方式和所用的平台(如 iOS),它主要演示如何通过连接到设备的耳机(尤其是那些带有内置传感器的智能耳机)来捕获头部运动数据。我们看GitHub-anastasiadeana做的Headphone Motion Unity Plugin 比较直观的效果。GitHub - anastasiadevana/HeadphoneMotion: Unity plugin for Apple Headphone Motion API.
能够实时的追踪用户的头部运动,包括倾斜,旋转等动作。这也是本次项目较为核心的一个技术。
- web服务器:服务器的类型有很多种,它为其他应用程序或设备提供数据,服务或应用程序。服务器执行某些任务,如处理数据请求,托管网站,存储信息,运行企业应用程序。等等。本项目web服务器主要负责接受ios应用的头部运动数据,并且将这些数据传递给控制mycobot机械臂的脚本。
- express-node.js:Express 是一个快速、开放、最小化的 Web 应用程序框架,用于 Node.js。它被设计用来构建 Web 应用程序和 API。它允许开发者以非常快速和简便的方式设置中间件来响应 HTTP 请求,使得开发 Web 应用程序变得更加简单快捷。
GitHub - expressjs/express: Fast, unopinionated, minimalist web framework for node.
- pymycobot-python:pymycobot是一个Python库,专门为控制myCobot机械臂设计。这个库提供了一系列函数和接口,允许开发者通过Python脚本直接与myCobot机械臂进行通信和控制。使用pymycobot,开发者可以编写代码来控制机械臂的运动、调整其姿态、执行预设的动作序列等,使其在教育、研究、自动化等多种场景中具有广泛的应用可能性。
GitHub - elephantrobotics/pymycobot: This is a python API for ElephantRobotics product.
iOS 应用
这个应用是基于我之前创建的应用,使用react-native-haedphone-motion通过react Native IOS应用程序访问AirdPods中的传感器。一个有趣的项目,当你带着airpods长时间低头被检测的时候,就会有悲鸣的声音提醒你。
react-native-headphone-motionを使って、React Native製のiOSアプリでAirPods内のセンサにアクセスする 🎧 - みかづきブログ・カスタム
只是要注意更改点,我在 onDeviceMotionUpdates 中加入了向 Web 服务器发送 POST 请求的处理。另外,为了避免每次更新都发送 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,它能够发送异步HTTP请求到REST端点并处理相应。因此,还需要添加模块导入。
import axios from 'axios';
完整的代码
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会更方便,但是我出于对速度的考虑,就直接这样实现了。
Web服务器
我在Mac上建立了一个本地服务器。
首先,为了操作myCobot,我进行了以下设置,主要是适配mac的电脑,安装机械臂的驱动,更新mycobot 280的固件等一些操作都在这篇文章当中。
myCobotをPythonから動かすための準備をする 🤖 - みかづきブログ・カスタム
我认为如果能用Python创建Web服务器会更顺畅,但基于我的技能集,使用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; // 如果应用端的延迟(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目录。
GitHub - elephantrobotics/pymycobot: This is a python API for ElephantRobotics product.
准备好之后,将PC与myCobot连接,执行相应的操作即可启动Web服务器,并通过POST请求接收到的pitch、roll、yaw值传递给myCobot。虽然这次是从iPhone应用通过POST发送AirPods的传感器值,但POST的来源可以是任何地方,所以我觉得建立这样一个服务器,将来可能会有用武之地。
源代码