头域相关功能开发
1. 概述
GR-Head 是 GR3 机器人头部开发模块,运行于头部 RK3588 Android 主控。本文档说明头部基础硬件能力、Android 开发环境、设备连接方式,以及摄像头、麦克风、扬声器和 SPI 眼屏的基础使用方法。
| 元素 | 数值 |
|---|---|
| 主控平台 | RK3588 |
| 操作系统 | Android 12 |
| 默认 IP | 192.168.137.200 |
| 开发方式 | Android 应用开发 |
| 主要硬件 | RGB 摄像头、麦克风、扬声器、SPI 眼屏 |
2. 硬件能力
| 硬件 | 说明 |
|---|---|
| 单目 RGB 摄像头 | 支持图像采集,可用于预览、拍照和图像处理。支持硬件帧同步功能。 |
| 领夹式麦克风 | 支持音频采集。 |
| 扬声器系统 | 支持音频播放。 |
| SPI 眼屏 | 支持眼睛表情控制。 |
3. Android 开发环境
开发前需准备以下工具:
| 工具 | 说明 |
|---|---|
| Android Studio | Android 应用开发、编译和调试工具。 |
| JDK 11 | Android 工程构建环境。 |
adb | 连接设备、安装应用和查看日志。 |
scrcpy | 投屏显示 Android 运行画面。 |
Android Studio 下载地址:
https://developer.android.google.cn/studio?hl=zh-cn
配置完成后,确认终端可执行:
java -version
adb version
scrcpy --version
4. 设备连接与调试
头部 RK3588 默认 IP 为:
192.168.137.200
开发机需先连接机器人内部网络:
| 方式 | 说明 |
|---|---|
| 有线连接 | 网线一端接机器人背部网口,另一端接开发机网口。 |
| 无线连接 | 开发机连接机器人 Wi-Fi,例如 GR301AA0001_5G。 |
连接网络后执行:
adb connect 192.168.137.200:5555
adb devices
投屏调试:
scrcpy --no-audio
安装应用与查看日志:
adb install -r your_app.apk
adb logcat
5. Android 权限
在 AndroidManifest.xml 中添加所需的权限:
5.1 摄像头权限
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
5.2 麦克风权限
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.microphone" />
应用安装后,建议在系统设置中确认摄像头和麦克风权限已授权。
6. 摄像头使用方法
头部 RGB 摄像头可通过 Android Camera2 API 或传统 Camera API 调用。以下示例使用 Camera2 API 打开摄像头并显示实时画面。
6.1 创建 Camera Activity
创建 CameraActivity.kt,初始化 TextureView 和 CameraManager:
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.SurfaceTexture
import android.hardware.camera2.*
import android.os.*
import android.util.Log
import android.view.Surface
import android.view.TextureView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
class CameraActivity : AppCompatActivity() {
private lateinit var textureView: TextureView
private lateinit var cameraManager: CameraManager
private var cameraDevice: CameraDevice? = null
private var captureSession: CameraCaptureSession? = null
private var previewRequestBuilder: CaptureRequest.Builder? = null
private var currentCameraId: String? = null
private val mainHandler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textureView = TextureView(this)
setContentView(textureView)
cameraManager = getSystemService(CAMERA_SERVICE) as CameraManager
textureView.surfaceTextureListener = textureListener
}
6.2 监听 TextureView 状态
当 TextureView 可用时,打开摄像头并注册摄像头可用性监听:
private val textureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
Log.i(TAG, "SurfaceTexture available: ${width}x$height")
openCamera()
registerCameraAvailabilityCallback()
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
Log.i(TAG, "SurfaceTexture size changed: ${width}x$height")
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
Log.i(TAG, "SurfaceTexture destroyed")
closeCamera()
return true
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
}
6.3 注册摄像头可用性监听
通过 CameraManager.AvailabilityCallback 监听摄像头可用和不可用状态:
private fun registerCameraAvailabilityCallback() {
cameraManager.registerAvailabilityCallback(object : CameraManager.AvailabilityCallback() {
override fun onCameraAvailable(cameraId: String) {
Log.i(TAG, "Camera available: $cameraId")
}
override fun onCameraUnavailable(cameraId: String) {
Log.w(TAG, "Camera unavailable: $cameraId")
}
}, mainHandler)
}
6.4 打开前置摄像头
遍历系统摄像头列表,查找前置摄像头并打开:
private fun openCamera() {
try {
for (cameraId in cameraManager.cameraIdList) {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
currentCameraId = cameraId
break
}
}
if (currentCameraId == null) {
Log.e(TAG, "No camera found")
return
}
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 1)
return
}
Log.i(TAG, "Opening camera: $currentCameraId")
cameraManager.openCamera(currentCameraId!!, stateCallback, mainHandler)
} catch (e: Exception) {
Log.e(TAG, "openCamera error: ${e.message}", e)
}
}
6.5 处理 Camera 状态回调
摄像头打开成功后启动预览;如果断开或异常,则关闭摄像头资源:
private val stateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
Log.i(TAG, "Camera opened: ${camera.id}")
cameraDevice = camera
startPreview()
}
override fun onDisconnected(camera: CameraDevice) {
Log.w(TAG, "Camera disconnected: ${camera.id}")
camera.close()
cameraDevice = null
}
override fun onError(camera: CameraDevice, error: Int) {
Log.e(TAG, "Camera error ($error): ${camera.id}")
camera.close()
cameraDevice = null
}
}
6.6 启动摄像头预览
创建预览 Surface 和 CaptureSession,并通过 setRepeatingRequest() 持续显示实时画面:
private fun startPreview() {
val camera = cameraDevice ?: return
val surfaceTexture = textureView.surfaceTexture ?: return
try {
surfaceTexture.setDefaultBufferSize(1920, 1080)
val surface = Surface(surfaceTexture)
previewRequestBuilder =
camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
addTarget(surface)
set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
}
camera.createCaptureSession(listOf(surface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
Log.i(TAG, "CaptureSession configured")
captureSession = session
try {
session.setRepeatingRequest(previewRequestBuilder!!.build(), null, mainHandler)
} catch (e: CameraAccessException) {
Log.e(TAG, "setRepeatingRequest error: ${e.message}", e)
}
}
override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e(TAG, "CaptureSession configure failed")
}
},
mainHandler
)
} catch (e: Exception) {
Log.e(TAG, "startPreview error: ${e.message}", e)
}
}
6.7 释放摄像头资源
在页面暂停、恢复和销毁时处理摄像头生命周期:
private fun closeCamera() {
try {
captureSession?.close()
captureSession = null
cameraDevice?.close()
cameraDevice = null
} catch (e: Exception) {
Log.e(TAG, "closeCamera error: ${e.message}", e)
}
}
override fun onPause() {
super.onPause()
Log.i(TAG, "onPause: closing camera")
closeCamera()
}
override fun onResume() {
super.onResume()
Log.i(TAG, "onResume: try reopen camera")
if (textureView.isAvailable) {
openCamera()
}
}
override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy: releasing camera")
closeCamera()
}
companion object {
private const val TAG = "CameraActivity"
}
}
7. 麦克风使用方法
麦克风可通过 MediaRecorder 或 AudioRecord 采集音频。MediaRecorder 比较适合简单录音,AudioRecord 提供更底层的帧级控制能力,适用于需要实时音频处理或精细控制音频采集的场景。
7.1 MediaRecorder 录音
MediaRecorder 是一个高层接口,适合进行简单的音频录制操作。
在 AndroidManifest.xml 中添加录音权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
创建并配置 MediaRecorder:
public class AudioRecorderActivity extends AppCompatActivity {
private MediaRecorder mediaRecorder;
private String audioFilePath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_recorder);
Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);
audioFilePath = getExternalFilesDir(null).getAbsolutePath() + "/audio_record.aac";
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mediaRecorder.setOutputFile(audioFilePath);
startButton.setOnClickListener(v -> {
Log.i("wtf", "startRecording");
startRecording();
});
stopButton.setOnClickListener(v -> {
Log.i("wtf", "stopRecording");
stopRecording();
});
}
开始录音:
public void startRecording() {
try {
mediaRecorder.prepare();
mediaRecorder.start();
} catch (IOException e) {
System.out.print("启动有异常");
e.printStackTrace();
}
}
停止录音:
public void stopRecording() {
try {
mediaRecorder.stop();
mediaRecorder.release();
} catch (RuntimeException e) {
e.printStackTrace();
}
}
页面销毁时释放资源:
@Override
protected void onDestroy() {
super.onDestroy();
if (mediaRecorder != null) {
mediaRecorder.release();
}
}
}
也可以通过按钮或其他控件控制录音的开始和停止:
Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);
startButton.setOnClickListener(v -> startRecording());
stopButton.setOnClickListener(v -> stopRecording());
7.2 AudioRecord 获取 PCM
AudioRecord 是更底层的音频采集接口,适合低延迟录音、实时音频处理和自定义音频算法。
添加录音权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.microphone" />
初始化 AudioRecord:
public class AudioRecorderActivity extends AppCompatActivity {
private static final int SAMPLE_RATE_IN_HZ = 44100;
private AudioRecord audioRecord;
private boolean isRecording = false;
private Thread recordingThread;
private String audioFilePath;
private FileOutputStream fileOutputStream;
@Override
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_recorder);
Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);
int bufferSize = AudioRecord.getMinBufferSize(
SAMPLE_RATE_IN_HZ,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
);
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
SAMPLE_RATE_IN_HZ,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize
);
recordingThread = new Thread(new Runnable() {
@Override
public void run() {
recordAudio();
}
});
startButton.setOnClickListener(v -> {
Log.i("wtf", "startRecording");
startRecording();
});
stopButton.setOnClickListener(v -> {
Log.i("wtf", "stopRecording");
stopRecording();
});
}
开始录音:
public void startRecording() {
audioRecord.startRecording();
isRecording = true;
recordingThread.start();
}
停止录音:
public void stopRecording() {
isRecording = false;
audioRecord.stop();
audioRecord.release();
}
读取音频数据并保存为 WAV 文件:
private void recordAudio() {
String audioFileName = "audio_record_" + System.currentTimeMillis() + ".wav";
File audioFile = new File(getExternalFilesDir(null), audioFileName);
audioFilePath = audioFile.getAbsolutePath();
byte[] audioBuffer = new byte[1024];
int totalBytesRead = 0;
try (FileOutputStream fos = new FileOutputStream(audioFile)) {
writeWavHeader(fos, 0, 0);
while (isRecording) {
int read = audioRecord.read(audioBuffer, 0, audioBuffer.length);
if (read > 0) {
fos.write(audioBuffer, 0, read);
totalBytesRead += read;
}
}
updateWavHeader(audioFile, totalBytesRead);
} catch (IOException e) {
Log.e("AudioRecord", "File write failed: " + e.getMessage());
}
}
写入 WAV 文件头:
private void writeWavHeader(FileOutputStream out, long totalAudioLen, long totalDataLen) throws IOException {
long sampleRate = SAMPLE_RATE_IN_HZ;
int channels = 1;
int bitsPerSample = 16;
byte[] header = new byte[44];
header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';
header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';
header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0;
header[20] = 1; header[21] = 0;
header[22] = (byte) channels; header[23] = 0;
header[24] = (byte) (sampleRate & 0xff);
header[25] = (byte) ((sampleRate >> 8) & 0xff);
header[26] = (byte) ((sampleRate >> 16) & 0xff);
header[27] = (byte) ((sampleRate >> 24) & 0xff);
long byteRate = sampleRate * channels * bitsPerSample / 8;
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
header[32] = (byte) (channels * bitsPerSample / 8);
header[33] = 0;
header[34] = (byte) bitsPerSample;
header[35] = 0;
header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}
更新 WAV 文件头中的数据大小:
private void updateWavHeader(File file, int totalAudioLen) {
try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
long totalDataLen = totalAudioLen + 36;
raf.seek(4);
raf.write((int) (totalDataLen & 0xff));
raf.write((int) ((totalDataLen >> 8) & 0xff));
raf.write((int) ((totalDataLen >> 16) & 0xff));
raf.write((int) ((totalDataLen >> 24) & 0xff));
raf.seek(40);
raf.write((int) (totalAudioLen & 0xff));
raf.write((int) ((totalAudioLen >> 8) & 0xff));
raf.write((int) ((totalAudioLen >> 16) & 0xff));
raf.write((int) ((totalAudioLen >> 24) & 0xff));
} catch (IOException e) {
Log.e("AudioRecord", "Error updating WAV header: " + e.getMessage());
}
}
页面销毁时释放资源:
@Override
protected void onDestroy() {
super.onDestroy();
if (audioRecord != null) {
audioRecord.release();
}
}
}
也可以通过按钮或其他控件控制录音的开始和停止:
Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);
startButton.setOnClickListener(v -> startRecording());
stopButton.setOnClickListener(v -> stopRecording());
如果需要保存录音数据,可以通过 AudioRecord 将音频流写入文件,也可以直接使用 MediaRecorder 保存为 .3gp 格式文件。
7.3 注意事项
- 权限问题:建议在系统设置中确认麦克风权限已授权,避免运行过程中弹出权限申请提示。
- 设备兼容性:确保设备支持麦克风功能,并检查
AudioManager和MediaRecorder相关能力。 - 线程管理:使用
AudioRecord时,需要在后台线程中读取音频数据,避免造成 ANR。 - 如果需要简单录音功能,可以使用
MediaRecorder。 - 如果需要更精细的控制,例如实时音频处理,可以使用
AudioRecord。
8. 扬声器使用方法
扬声器可通过 Android 音频接口播放本地音频文件。常用方式包括:
| 接口 | 说明 |
|---|---|
MediaPlayer | 播放常见音频文件。 |
AudioTrack | 播放 PCM 数据。 |
播放前应确认媒体音量、音频文件路径和文件格式正确。
以下示例使用 MediaPlayer 播放系统 默认通知音,可用于快速验证扬声器是否正常输出。
package com.example.head
import android.app.Activity
import android.media.MediaPlayer
import android.media.RingtoneManager
import android.os.Bundle
import android.widget.Button
class SpeakerPlayActivity : Activity() {
private var mediaPlayer: MediaPlayer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val playButton = Button(this).apply { text = "播放系统音频" }
setContentView(playButton)
playButton.setOnClickListener { playSystemSound() }
}
private fun playSystemSound() {
mediaPlayer?.release()
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
mediaPlayer = MediaPlayer.create(this, uri).apply {
setOnCompletionListener {
it.release()
mediaPlayer = null
}
start()
}
}
override fun onDestroy() {
mediaPlayer?.release()
mediaPlayer = null
super.onDestroy()
}
}
9. SPI 眼屏控制方法
SPI 眼屏支持微笑、星星眼、无语表情、闭眼、眨眼、眼球位置控制等。
9.1 集成方式
如需集成 SPI 眼屏控制库,可将 拓展包 libSpiScreen.aar 放入应用模块的 libs 目录。并在 build.gradle 中添加:
dependencies {
implementation(files("./libs/libSpiScreen.aar"))
}
9.2 眼球控制示例
EyeAction.openSpiEye()
//控制眼睛的方法
EyeAction.blinkEye() // 眨眼1次
EyeAction.blinkEye(240, 2) // 眨眼2次
EyeAction.updateEmotionSmile() // 微笑
EyeAction.closeEye() // 闭眼
EyeAction.updateEmotionWuYu() // 无语
EyeAction.updateEmotionStar() // 星星眼
// 控制眼球位置,x:-100-100,y:-100-100 中心点0 y下正 x右正
//两个眼球位置同时控制
EyeAction.updateBothScleraXY( e0.x, e0.y, e1.x, e1.y )
//控制眼睛位置,0左眼 1右眼
EyeAction.updateScleraXY(0, e0.x, e0.y)
//关闭spi屏幕
EyeAction.closeSpiEye()
10. 注意事项
- 摄像头和麦克风需先获得系统权限。
- 摄像头、麦克风和扬声器可能被其他应用占用,调试异常时需检查资源占用情况。
- 使用摄像头、录音和眼屏后,应在生命周期结束时释放资源。
AudioRecord采集需在后台线程中执行。- SPI 眼屏使用前需打开,应用退出或不再使用时需关闭。