跳到主要内容

头域相关功能开发

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,初始化 TextureView 和 CameraManager:

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

创建预览 Surface 和 CaptureSession,并通过 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. 麦克风使用方法​

麦克风可通过 MediaRecorder 或 AudioRecord 采集音频。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. 设备兼容性:确保设备支持麦克风功能,并检查 AudioManager 和 MediaRecorder 相关能力。
  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 眼屏使用前需打开,应用退出或不再使用时需关闭。