跳到主要内容

头域相关功能开发

1. 概述

GR-Head 是 GR3 机器人头部开发模块,运行于头部 RK3588 Android 主控。本文档说明头部基础硬件能力、Android 开发环境、设备连接方式,以及摄像头、麦克风、扬声器和 SPI 眼屏的基础使用方法。

元素数值
主控平台RK3588
操作系统Android 12
默认 IP192.168.137.200
开发方式Android 应用开发
主要硬件RGB 摄像头、麦克风、扬声器、SPI 眼屏

2. 硬件能力

硬件说明
单目 RGB 摄像头支持图像采集,可用于预览、拍照和图像处理。支持硬件帧同步功能。
领夹式麦克风支持音频采集。
扬声器系统支持音频播放。
SPI 眼屏支持眼睛表情控制。

3. Android 开发环境

开发前需准备以下工具:

工具说明
Android StudioAndroid 应用开发、编译和调试工具。
JDK 11Android 工程构建环境。
adb连接设备、安装应用和查看日志。
scrcpy投屏显示 Android 运行画面。

Android Studio 下载地址:

Text
https://developer.android.google.cn/studio?hl=zh-cn

配置完成后,确认终端可执行:

Bash
java -version
adb version
scrcpy --version

4. 设备连接与调试

头部 RK3588 默认 IP 为:

Text
192.168.137.200

开发机需先连接机器人内部网络:

方式说明
有线连接网线一端接机器人背部网口,另一端接开发机网口。
无线连接开发机连接机器人 Wi-Fi,例如 GR301AA0001_5G

连接网络后执行:

Bash
adb connect 192.168.137.200:5555
adb devices

投屏调试:

Bash
scrcpy --no-audio

安装应用与查看日志:

Bash
adb install -r your_app.apk
adb logcat

5. Android 权限

在 AndroidManifest.xml 中添加所需的权限:

5.1 摄像头权限

XML
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

5.2 麦克风权限

XML
<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,初始化 TextureViewCameraManager

Kotlin
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 可用时,打开摄像头并注册摄像头可用性监听:

Kotlin
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 监听摄像头可用和不可用状态:

Kotlin
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 打开前置摄像头

遍历系统摄像头列表,查找前置摄像头并打开:

Kotlin
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 状态回调

摄像头打开成功后启动预览;如果断开或异常,则关闭摄像头资源:

Kotlin
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 启动摄像头预览

创建预览 SurfaceCaptureSession,并通过 setRepeatingRequest() 持续显示实时画面:

Kotlin
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 释放摄像头资源

在页面暂停、恢复和销毁时处理摄像头生命周期:

Kotlin
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. 麦克风使用方法

麦克风可通过 MediaRecorderAudioRecord 采集音频。MediaRecorder 比较适合简单录音,AudioRecord 提供更底层的帧级控制能力,适用于需要实时音频处理或精细控制音频采集的场景。

7.1 MediaRecorder 录音

MediaRecorder 是一个高层接口,适合进行简单的音频录制操作。

AndroidManifest.xml 中添加录音权限:

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

Java
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();
});
}

开始录音:

Java
public void startRecording() {
try {
mediaRecorder.prepare();
mediaRecorder.start();
} catch (IOException e) {
System.out.print("启动有异常");
e.printStackTrace();
}
}

停止录音:

Java
public void stopRecording() {
try {
mediaRecorder.stop();
mediaRecorder.release();
} catch (RuntimeException e) {
e.printStackTrace();
}
}

页面销毁时释放资源:

Java
@Override
protected void onDestroy() {
super.onDestroy();
if (mediaRecorder != null) {
mediaRecorder.release();
}
}
}

也可以通过按钮或其他控件控制录音的开始和停止:

Java
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 是更底层的音频采集接口,适合低延迟录音、实时音频处理和自定义音频算法。

添加录音权限:

XML
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.microphone" />

初始化 AudioRecord

Java
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();
});
}

开始录音:

Java
public void startRecording() {
audioRecord.startRecording();
isRecording = true;
recordingThread.start();
}

停止录音:

Java
public void stopRecording() {
isRecording = false;
audioRecord.stop();
audioRecord.release();
}

读取音频数据并保存为 WAV 文件:

Java
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 文件头:

Java
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 文件头中的数据大小:

Java
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());
}
}

页面销毁时释放资源:

Java
@Override
protected void onDestroy() {
super.onDestroy();
if (audioRecord != null) {
audioRecord.release();
}
}
}

也可以通过按钮或其他控件控制录音的开始和停止:

Java
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 注意事项

  1. 权限问题:建议在系统设置中确认麦克风权限已授权,避免运行过程中弹出权限申请提示。
  2. 设备兼容性:确保设备支持麦克风功能,并检查 AudioManagerMediaRecorder 相关能力。
  3. 线程管理:使用 AudioRecord 时,需要在后台线程中读取音频数据,避免造成 ANR。
  4. 如果需要简单录音功能,可以使用 MediaRecorder
  5. 如果需要更精细的控制,例如实时音频处理,可以使用 AudioRecord

8. 扬声器使用方法

扬声器可通过 Android 音频接口播放本地音频文件。常用方式包括:

接口说明
MediaPlayer播放常见音频文件。
AudioTrack播放 PCM 数据。

播放前应确认媒体音量、音频文件路径和文件格式正确。

以下示例使用 MediaPlayer 播放系统默认通知音,可用于快速验证扬声器是否正常输出。

Kotlin
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 中添加:

Gradle
dependencies {
implementation(files("./libs/libSpiScreen.aar"))
}

9.2 眼球控制示例

Kotlin

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 眼屏使用前需打开,应用退出或不再使用时需关闭。