HarmonyOS机械臂蓝牙控制应用开发完整教程
本文详细介绍了一个基于HarmonyOS的STM32三轴机械臂蓝牙控制应用开发方案。项目采用ArkTS框架实现,包含蓝牙通信管理、虚拟摇杆控制、三轴联动等核心功能。通过SPP蓝牙协议与STM32硬件交互,使用双虚拟摇杆分别控制底座360°旋转、关节270°俯仰和夹爪180°开合动作。应用采用现代化卡片式UI设计,支持深色模式适配,具有状态实时反馈、指令防抖优化等特点。文章详细讲解了技术架构、蓝牙通
本文详细介绍如何开发一个基于HarmonyOS的STM32机械臂蓝牙控制应用,包含虚拟摇杆实现、三轴联动控制、界面美化和深色模式适配等内容。
📋 目录
🎯 项目概述
本项目是一个完整的HarmonyOS应用,用于通过蓝牙控制基于STM32的三轴机械臂。应用采用现代化的卡片式UI设计,支持深色模式,提供流畅的操作体验。
项目特点
- ✅ 双摇杆控制:左摇杆双轴(底座+关节),右摇杆单轴(夹爪)
- ✅ 三轴联动:可同时控制三个舵机
- ✅ 实时反馈:状态显示、调试信息
- ✅ 现代UI:卡片式设计、圆角阴影
- ✅ 深色模式:完整的浅色/深色主题支持
控制方案
| 摇杆 | 轴向 | 舵机 | 控制 | 指令 |
|---|---|---|---|---|
| 左摇杆 | 水平 | 底座360° | 左右旋转 | 0x01/0x02 |
| 左摇杆 | 垂直 | 关节270° | 上下俯仰 | 0x03/0x04 |
| 右摇杆 | 水平 | 夹爪180° | 开合控制 | 0x05/0x06 |
🛠️ 技术栈
- 开发工具:DevEco Studio 4.0+
- 框架:ArkTS (HarmonyOS)
- UI组件:@Component装饰器、声明式UI
- 通信:SPP蓝牙协议
- 目标设备:STM32系列微控制器
🏗️ 系统架构
┌─────────────────────────────────────┐
│ HarmonyOS应用层 │
│ ┌─────────────┬─────────────────┐ │
│ │ 控制界面 │ 蓝牙管理器 │ │
│ │ RobotControl│ BluetoothManager│ │
│ └─────────────┴─────────────────┘ │
│ ┌─────────────┬─────────────────┐ │
│ │ 虚拟摇杆 │ 状态显示 │ │
│ │ Joystick │ StatusCard │ │
│ └─────────────┴─────────────────┘ │
└─────────────────────────────────────┘
↓ SPP蓝牙
┌─────────────────────────────────────┐
│ STM32硬件层 │
│ ┌─────────┬─────────┬─────────┐ │
│ │底座舵机 │关节舵机 │夹爪舵机 │ │
│ │ 360° │ 270° │ 180° │ │
│ └─────────┴─────────┴─────────┘ │
└─────────────────────────────────────┘
💡 核心功能实现
一、蓝牙通信
1.1 权限配置
在 module.json5 中添加蓝牙权限:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.ACCESS_BLUETOOTH",
"reason": "$string:bluetooth_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
1.2 蓝牙管理器
创建单例模式的蓝牙管理类:
export class BlueToothMgr {
private static instance: BlueToothMgr;
private clientNumber: number = -1;
private dataCallback?: (data: string) => void;
static Ins(): BlueToothMgr {
if (!BlueToothMgr.instance) {
BlueToothMgr.instance = new BlueToothMgr();
}
return BlueToothMgr.instance;
}
// 发送SPP数据
sendSPPData(data: Uint8Array): void {
if (this.clientNumber !== -1) {
bluetoothManager.sppWrite(
this.clientNumber,
data,
(err) => {
if (err) {
console.error('SPP写入失败:', err);
}
}
);
}
}
// 设置数据接收回调
setDataCallback(callback: (data: string) => void): void {
this.dataCallback = callback;
}
}
二、虚拟摇杆
2.1 摇杆组件设计
虚拟摇杆是整个控制系统的核心,需要支持:
- 手势拖动
- 死区判断
- 方向检测
- 双轴独立
2.2 数据结构定义
// 方向枚举
export enum JoystickDirection {
STOP = 0,
POSITIVE = 1, // 正向(右/下)
NEGATIVE = 2 // 负向(左/上)
}
// 双轴状态
export interface DualAxisState {
horizontal: JoystickDirection; // 水平方向
vertical: JoystickDirection; // 垂直方向
}
2.3 摇杆核心实现
@Component
export struct Joystick {
// 摇杆参数
@State centerX: number = 0;
@State centerY: number = 0;
@State knobX: number = 0;
@State knobY: number = 0;
private maxRadius: number = 80;
private deadZone: number = 0.7; // 死区70%
// 双轴回调
onDualAxisChange?: (state: DualAxisState) => void;
// 摇杆尺寸
private joystickSize: number = 250;
private knobSize: number = 100;
build() {
Stack() {
// 背景圆
Circle()
.width(this.joystickSize)
.height(this.joystickSize)
.fill("#E0E0E0")
.opacity(0.5)
// 十字辅助线
Column().width(2).height(this.joystickSize).backgroundColor("#CCCCCC")
Row().width(this.joystickSize).height(2).backgroundColor("#CCCCCC")
// 中心点
Circle().width(15).height(15).fill("#999999")
// 摇杆球
Circle()
.width(this.knobSize)
.height(this.knobSize)
.fill("#007DFF")
.position({ x: this.knobX, y: this.knobY })
.shadow({ radius: 15, color: "#00000050" })
}
.width(this.joystickSize)
.height(this.joystickSize)
.gesture(
PanGesture()
.onActionUpdate((event: GestureEvent) => {
this.handleDrag(event);
})
.onActionEnd(() => {
this.resetPosition();
})
)
}
// 处理拖动
private handleDrag(event: GestureEvent) {
let offsetX = event.offsetX;
let offsetY = event.offsetY;
let distance = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
// 限制在最大半径内
if (distance > this.maxRadius) {
let angle = Math.atan2(offsetY, offsetX);
offsetX = Math.cos(angle) * this.maxRadius;
offsetY = Math.sin(angle) * this.maxRadius;
}
// 更新位置
this.knobX = this.centerX + offsetX;
this.knobY = this.centerY + offsetY;
// 计算方向
let normalizedX = offsetX / this.maxRadius;
let normalizedY = offsetY / this.maxRadius;
let horizontal = this.calculateAxisDirection(normalizedX);
let vertical = this.calculateAxisDirection(normalizedY);
// 触发回调
if (this.onDualAxisChange) {
this.onDualAxisChange({ horizontal, vertical });
}
}
// 计算单轴方向
private calculateAxisDirection(normalized: number): JoystickDirection {
if (Math.abs(normalized) < this.deadZone) {
return JoystickDirection.STOP;
}
return normalized > 0 ? JoystickDirection.POSITIVE : JoystickDirection.NEGATIVE;
}
// 重置位置
private resetPosition() {
this.knobX = this.centerX;
this.knobY = this.centerY;
if (this.onDualAxisChange) {
this.onDualAxisChange({
horizontal: JoystickDirection.STOP,
vertical: JoystickDirection.STOP
});
}
}
}
关键技术点:
- 死区判断:只有推动超过70%才触发,防止误操作
- 归一化坐标:将物理坐标转换为-1到1的标准值
- 双轴独立:水平和垂直方向分别判断,互不干扰
- 限制半径:确保摇杆球不会超出背景圆
三、双轴联动控制
3.1 控制逻辑
左摇杆实现双轴联动的核心在于独立处理每个轴的状态:
// 处理双轴输入
handleDualAxis(state: DualAxisState) {
this.handleServo1(state.horizontal); // 水平 → 底座
this.handleServo2(state.vertical); // 垂直 → 关节
}
// 底座舵机(360°)
handleServo1(direction: JoystickDirection) {
let newCommand: number;
switch (direction) {
case JoystickDirection.NEGATIVE: // 左
newCommand = 0x02;
this.servo1Status = "反转";
break;
case JoystickDirection.POSITIVE: // 右
newCommand = 0x01;
this.servo1Status = "正转";
break;
default:
newCommand = 0x07;
this.servo1Status = "停止";
break;
}
// 只在指令变化时发送
if (newCommand !== this.currentServo1Command) {
this.currentServo1Command = newCommand;
this.sendCommand(newCommand);
}
}
// 关节舵机(270°)
handleServo2(direction: JoystickDirection) {
let newCommand: number;
switch (direction) {
case JoystickDirection.NEGATIVE: // 上
newCommand = 0x03;
this.servo2Status = "上升";
break;
case JoystickDirection.POSITIVE: // 下
newCommand = 0x04;
this.servo2Status = "下降";
break;
default:
newCommand = 0x08;
this.servo2Status = "停止";
break;
}
if (newCommand !== this.currentServo2Command) {
this.currentServo2Command = newCommand;
this.sendCommand(newCommand);
}
}
3.2 指令优化
防抖处理:只在指令真正变化时才发送,避免重复发送相同指令,减少蓝牙通信压力。
// 当前指令缓存
private currentServo1Command: number = 0x07;
private currentServo2Command: number = 0x08;
private currentServo3Command: number = 0x09;
// 发送前检查
if (newCommand !== this.currentServo1Command) {
this.currentServo1Command = newCommand;
this.sendCommand(newCommand);
}
四、界面设计
4.1 卡片式布局
采用现代化的卡片式设计,清晰分层:
Column() {
// ===== 顶部导航 =====
Row() {
// 返回按钮
Text("<").fontSize(30).onClick(() => {
router.replaceUrl({ url: 'pages/MainPage' })
})
// Logo展示
Row() {
Image($r('app.media.logo')).width(50).height(50)
Image($r('app.media.hnqc_logo')).width(120).height(120)
}
}
.backgroundColor(Color.White)
.shadow({ radius: 10 })
Scroll() {
Column() {
// ===== 状态卡片组 =====
Row() {
// 底座状态卡片
StatusCard("🔄", "底座", this.servo1Status, "360°")
// 关节状态卡片
StatusCard("↕️", "关节", this.servo2Status, "270°")
// 夹爪状态卡片
StatusCard("✋", "夹爪", this.servo3Status, "180°")
}
// ===== 主控摇杆卡片 =====
JoystickCard("🎮 主控摇杆", "双轴联动控制")
// ===== 夹爪摇杆卡片 =====
JoystickCard("🦾 夹爪控制", "精确抓取")
// ===== 调试信息卡片 =====
DebugCard()
}
}
}
4.2 状态卡片实现
@Component
struct StatusCard {
emoji: string;
title: string;
status: string;
angle: string;
build() {
Column() {
Text(this.emoji).fontSize(28)
Text(this.title).fontSize(12).fontColor("#7F8C8D")
Text(this.status)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(this.status === "停止" ? "#BDC3C7" : "#27AE60")
Text(this.angle).fontSize(10).fontColor("#BDC3C7")
}
.width('30%')
.padding(15)
.backgroundColor(Color.White)
.borderRadius(15)
.shadow({ radius: 8, color: "#00000010" })
}
}
4.3 视觉设计要点
配色方案:
// 功能色
底座运行: #27AE60 // 翠绿
关节运行: #3498DB // 天蓝
夹爪运行: #E67E22 // 橙色
停止状态: #BDC3C7 // 灰色
// 基础色
背景: #F5F6FA // 淡灰蓝
卡片: #FFFFFF // 纯白
文字: #2C3E50 // 深灰蓝
次要: #7F8C8D // 中灰
圆角阴影:
.borderRadius(15) // 卡片圆角
.shadow({ radius: 8, color: "#00000010" }) // 轻阴影
五、深色模式
5.1 深色模式检测
@Component
struct RobotControl {
@State isDark: boolean = false;
// 深色模式判断
isDarkMode(): boolean {
return this.isDark;
}
build() {
Column() {
// 所有颜色都适配深色模式
// ...
}
.backgroundColor(this.isDarkMode() ? "#1A1A1A" : "#F5F6FA")
}
}
5.2 配色适配
所有UI元素都需要添加深色模式判断:
// 背景色适配
.backgroundColor(this.isDarkMode() ? "#2C2C2C" : Color.White)
// 文字颜色适配
.fontColor(this.isDarkMode() ? "#E0E0E0" : "#2C3E50")
// 次要文字适配
.fontColor(this.isDarkMode() ? "#B0B0B0" : "#7F8C8D")
// 分隔线适配
.color(this.isDarkMode() ? "#404040" : "#ECF0F1")
// 阴影适配
.shadow({
radius: 8,
color: this.isDarkMode() ? "#00000030" : "#00000010"
})
5.3 深色模式切换
添加切换按钮(可选):
Button({ type: ButtonType.Circle }) {
Text(this.isDark ? "🌙" : "☀️").fontSize(20)
}
.backgroundColor(this.isDarkMode() ? "#3C3C3C" : "#ECF0F1")
.onClick(() => {
this.isDark = !this.isDark;
promptAction.showToast({
message: this.isDark ? "深色模式" : "浅色模式"
});
})
📦 完整代码
Joystick.ets(虚拟摇杆组件)
/**
* 虚拟摇杆组件(双轴版本)
*/
export enum JoystickDirection {
STOP = 0,
POSITIVE = 1,
NEGATIVE = 2
}
export interface DualAxisState {
horizontal: JoystickDirection;
vertical: JoystickDirection;
}
@Component
export struct Joystick {
@State centerX: number = 0;
@State centerY: number = 0;
@State knobX: number = 0;
@State knobY: number = 0;
private maxRadius: number = 80;
private deadZone: number = 0.7;
private joystickSize: number = 250;
private knobSize: number = 100;
onDualAxisChange?: (state: DualAxisState) => void;
private currentHorizontal: JoystickDirection = JoystickDirection.STOP;
private currentVertical: JoystickDirection = JoystickDirection.STOP;
build() {
Stack() {
Circle()
.width(this.joystickSize)
.height(this.joystickSize)
.fill("#E0E0E0")
.opacity(0.5)
Column().width(2).height(this.joystickSize).backgroundColor("#CCCCCC")
Row().width(this.joystickSize).height(2).backgroundColor("#CCCCCC")
Circle().width(15).height(15).fill("#999999")
Circle()
.width(this.knobSize)
.height(this.knobSize)
.fill("#007DFF")
.position({ x: this.knobX, y: this.knobY })
.shadow({ radius: 15, color: "#00000050" })
}
.width(this.joystickSize)
.height(this.joystickSize)
.onAreaChange((oldValue: Area, newValue: Area) => {
this.centerX = Number(newValue.width) / 2 - this.knobSize / 2;
this.centerY = Number(newValue.height) / 2 - this.knobSize / 2;
this.knobX = this.centerX;
this.knobY = this.centerY;
})
.gesture(
PanGesture()
.onActionUpdate((event: GestureEvent) => {
let offsetX = event.offsetX;
let offsetY = event.offsetY;
let distance = Math.sqrt(offsetX * offsetX + offsetY * offsetY);
if (distance > this.maxRadius) {
let angle = Math.atan2(offsetY, offsetX);
offsetX = Math.cos(angle) * this.maxRadius;
offsetY = Math.sin(angle) * this.maxRadius;
}
this.knobX = this.centerX + offsetX;
this.knobY = this.centerY + offsetY;
let normalizedX = offsetX / this.maxRadius;
let normalizedY = offsetY / this.maxRadius;
let horizontal = this.calculateAxisDirection(normalizedX);
let vertical = this.calculateAxisDirection(normalizedY);
if (horizontal !== this.currentHorizontal || vertical !== this.currentVertical) {
this.currentHorizontal = horizontal;
this.currentVertical = vertical;
if (this.onDualAxisChange) {
this.onDualAxisChange({ horizontal, vertical });
}
}
})
.onActionEnd(() => {
this.knobX = this.centerX;
this.knobY = this.centerY;
this.currentHorizontal = JoystickDirection.STOP;
this.currentVertical = JoystickDirection.STOP;
if (this.onDualAxisChange) {
this.onDualAxisChange({
horizontal: JoystickDirection.STOP,
vertical: JoystickDirection.STOP
});
}
})
)
}
private calculateAxisDirection(normalized: number): JoystickDirection {
if (Math.abs(normalized) < this.deadZone) {
return JoystickDirection.STOP;
}
return normalized > 0 ? JoystickDirection.POSITIVE : JoystickDirection.NEGATIVE;
}
}
RobotControl.ets(主控制界面)
/**
* 机械臂控制界面(竖屏美化版 + 深色模式)
*/
import { BlueToothMgr } from '../Server_layer/BluetoothManager';
import { Joystick, JoystickDirection, DualAxisState } from '../components/Joystick';
import { promptAction, router } from '@kit.ArkUI';
@Entry
@Component
struct RobotControl {
@StorageLink('SPPConnected') isConnected: boolean = false;
@StorageLink('CurrentDeviceId') deviceId: string = "";
@State debugMessages: string[] = [];
@State maxDebugMessages: number = 15;
@State servo1Status: string = "停止";
@State servo2Status: string = "停止";
@State servo3Status: string = "停止";
private currentServo1Command: number = 0x07;
private currentServo2Command: number = 0x08;
private currentServo3Command: number = 0x09;
private scroller: Scroller = new Scroller();
isDarkMode(): boolean {
return false; // 默认浅色,可改为true启用深色
}
aboutToAppear() {
if (!this.isConnected) {
promptAction.showToast({ message: "蓝牙未连接" });
setTimeout(() => router.back(), 1500);
return;
}
BlueToothMgr.Ins().setDataCallback((data: string) => {
this.addDebugMessage("收到: " + data);
});
this.addDebugMessage("=== 系统启动 ===");
}
aboutToDisappear() {
this.sendCommand(0x00); // 全部停止
}
addDebugMessage(msg: string) {
const time = new Date().toLocaleTimeString();
this.debugMessages.unshift(`[${time}] ${msg}`);
if (this.debugMessages.length > this.maxDebugMessages) {
this.debugMessages.pop();
}
}
sendCommand(cmd: number) {
BlueToothMgr.Ins().sendSPPData(new Uint8Array([cmd]));
const hex = "0x" + cmd.toString(16).toUpperCase().padStart(2, '0');
this.addDebugMessage("发送: " + hex);
}
handleDualAxis(state: DualAxisState) {
this.handleServo1(state.horizontal);
this.handleServo2(state.vertical);
}
handleServo1(direction: JoystickDirection) {
let cmd = this.currentServo1Command;
switch (direction) {
case JoystickDirection.NEGATIVE:
cmd = 0x02; this.servo1Status = "反转"; break;
case JoystickDirection.POSITIVE:
cmd = 0x01; this.servo1Status = "正转"; break;
default:
cmd = 0x07; this.servo1Status = "停止"; break;
}
if (cmd !== this.currentServo1Command) {
this.currentServo1Command = cmd;
this.sendCommand(cmd);
}
}
handleServo2(direction: JoystickDirection) {
let cmd = this.currentServo2Command;
switch (direction) {
case JoystickDirection.NEGATIVE:
cmd = 0x03; this.servo2Status = "上升"; break;
case JoystickDirection.POSITIVE:
cmd = 0x04; this.servo2Status = "下降"; break;
default:
cmd = 0x08; this.servo2Status = "停止"; break;
}
if (cmd !== this.currentServo2Command) {
this.currentServo2Command = cmd;
this.sendCommand(cmd);
}
}
handleServo3(direction: JoystickDirection) {
let cmd = this.currentServo3Command;
switch (direction) {
case JoystickDirection.NEGATIVE:
cmd = 0x06; this.servo3Status = "闭合"; break;
case JoystickDirection.POSITIVE:
cmd = 0x05; this.servo3Status = "张开"; break;
default:
cmd = 0x09; this.servo3Status = "停止"; break;
}
if (cmd !== this.currentServo3Command) {
this.currentServo3Command = cmd;
this.sendCommand(cmd);
}
}
build() {
Column() {
// 顶部导航
Row() {
Row() {
Text("<")
.onClick(() => router.replaceUrl({ url: 'pages/MainPage' }))
.fontSize(30)
.margin({ left: 10 })
.fontColor(this.isDarkMode() ? Color.White : "#2C3E50")
}.layoutWeight(1)
Row() {
Image($r('app.media.logo')).width(50).height(50)
Image($r('app.media.hnqc_logo')).width(120).height(120)
}
.justifyContent(FlexAlign.End)
.margin(5)
.layoutWeight(1)
}
.width('100%')
.height(70)
.backgroundColor(this.isDarkMode() ? "#1E1E1E" : Color.White)
.shadow({ radius: 10 })
Scroll(this.scroller) {
Column() {
// 状态卡片组
Row() {
this.StatusCard("🔄", "底座", this.servo1Status, "360°")
this.StatusCard("↕️", "关节", this.servo2Status, "270°")
this.StatusCard("✋", "夹爪", this.servo3Status, "180°")
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(15)
// 主控摇杆
this.JoystickCard(
"🎮 主控摇杆",
"双轴联动控制",
(state) => this.handleDualAxis(state),
true
)
// 夹爪摇杆
this.JoystickCard(
"🦾 夹爪控制",
"精确抓取",
(state) => this.handleServo3(state.horizontal),
false
)
// 调试信息
this.DebugCard()
}
}
.layoutWeight(1)
.backgroundColor(this.isDarkMode() ? "#1A1A1A" : "#F5F6FA")
}
.width('100%')
.height('100%')
.backgroundColor(this.isDarkMode() ? "#1A1A1A" : "#F5F6FA")
}
@Builder StatusCard(emoji: string, title: string, status: string, angle: string) {
Column() {
Text(emoji).fontSize(28)
Text(title).fontSize(12).fontColor(this.isDarkMode() ? "#B0B0B0" : "#7F8C8D")
Text(status)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(status === "停止" ?
(this.isDarkMode() ? "#6C6C6C" : "#BDC3C7") :
(title === "底座" ? "#27AE60" : title === "关节" ? "#3498DB" : "#E67E22"))
Text(angle).fontSize(10).fontColor(this.isDarkMode() ? "#808080" : "#BDC3C7")
}
.width('30%')
.padding(15)
.backgroundColor(this.isDarkMode() ? "#2C2C2C" : Color.White)
.borderRadius(15)
.shadow({ radius: 8, color: this.isDarkMode() ? "#00000030" : "#00000010" })
}
@Builder JoystickCard(title: string, subtitle: string, callback: (state: DualAxisState) => void, showTips: boolean) {
Column() {
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(this.isDarkMode() ? "#E0E0E0" : "#2C3E50")
.alignSelf(ItemAlign.Start)
Text(subtitle)
.fontSize(12)
.fontColor(this.isDarkMode() ? "#A0A0A0" : "#95A5A6")
.alignSelf(ItemAlign.Start)
.margin({ top: 5, bottom: 15 })
Joystick({ onDualAxisChange: callback })
if (showTips) {
Row() {
Text("← → 底座").fontSize(11).fontColor("#27AE60").layoutWeight(1)
Divider().vertical(true).height(20).color(this.isDarkMode() ? "#404040" : "#ECF0F1")
Text("↑ ↓ 关节").fontSize(11).fontColor("#3498DB").layoutWeight(1)
}.width('80%').margin({ top: 15 })
}
}
.width('100%')
.padding(20)
.margin({ top: 20, left: 15, right: 15 })
.backgroundColor(this.isDarkMode() ? "#2C2C2C" : Color.White)
.borderRadius(20)
.shadow({ radius: 15 })
}
@Builder DebugCard() {
Column() {
Row() {
Text("📊 调试信息")
.fontSize(14)
.fontColor(this.isDarkMode() ? "#E0E0E0" : "#2C3E50")
Blank()
Text("清空")
.fontSize(12)
.fontColor("#3498DB")
.padding(5)
.backgroundColor(this.isDarkMode() ? "#2C5282" : "#EBF5FB")
.borderRadius(5)
.onClick(() => {
this.debugMessages = [];
this.addDebugMessage("已清空");
})
}.width('100%')
Column() {
ForEach(this.debugMessages, (msg: string, idx: number) => {
Text(msg)
.fontSize(11)
.fontColor(this.isDarkMode() ? "#D0D0D0" : "#34495E")
.width('100%')
.padding(8)
.backgroundColor(idx % 2 === 0 ?
(this.isDarkMode() ? "#2C2C2C" : "#FFFFFF") :
(this.isDarkMode() ? "#383838" : "#F8F9FA"))
.borderRadius(5)
})
}.width('100%').maxHeight(200)
}
.width('100%')
.padding(15)
.margin({ top: 20, left: 15, right: 15, bottom: 20 })
.backgroundColor(this.isDarkMode() ? "#2C2C2C" : Color.White)
.borderRadius(20)
.shadow({ radius: 15 })
}
}
🚀 部署运行
步骤1:创建项目
# 使用DevEco Studio创建HarmonyOS应用项目
# 选择Empty Ability模板
步骤2:配置权限
在 entry/src/main/module.json5 中添加蓝牙权限(见上文)
步骤3:添加组件
# 创建目录结构
entry/src/main/ets/
├── components/
│ └── Joystick.ets # 摇杆组件
├── pages/
│ ├── MainPage.ets # 主页
│ └── RobotControl.ets # 控制页
└── Server_layer/
└── BluetoothManager.ets # 蓝牙管理
步骤4:添加资源
将Logo图片放入资源目录:
entry/src/main/resources/base/media/
├── logo.png
└── hnqc_logo.png
步骤5:配置竖屏
在 module.json5 中:
"abilities": [{
"orientation": "portrait" // 强制竖屏
}]
步骤6:编译运行
# 清理编译
hvigorw clean
# 编译HAP包
hvigorw assembleHap
# 安装到设备
hdc install entry-default-signed.hap
❓ 常见问题
Q1: 蓝牙连接失败?
A: 检查以下几点:
- 权限是否正确配置
- STM32蓝牙模块是否已开启
- 设备是否已配对
- SPP UUID是否匹配
Q2: 摇杆不响应?
A: 可能原因:
- 检查手势识别是否正确
- 确认
onDualAxisChange回调是否设置 - 调整死区参数
deadZone
Q3: 指令发送延迟?
A: 优化建议:
- 只在指令变化时发送(已实现)
- 调整蓝牙MTU大小
- 优化STM32接收处理速度
Q4: 深色模式不生效?
A: 检查步骤:
- 确认
isDarkMode()返回值 - 检查所有颜色是否都添加了判断
- 重新编译应用
Q5: 界面布局混乱?
A: 确认事项:
- 检查
orientation配置 - 确认屏幕尺寸适配
- 调整卡片大小和间距
📚 总结
本教程详细介绍了HarmonyOS机械臂控制应用的完整开发流程,涵盖:
✅ 蓝牙通信:SPP协议、权限配置 ✅ 虚拟摇杆:手势识别、死区判断、双轴联动 ✅ 控制逻辑:指令映射、状态管理、防抖优化 ✅ 界面设计:卡片布局、配色方案、视觉效果 ✅ 深色模式:主题切换、颜色适配、用户体验
技术要点
- 双轴独立判断:水平和垂直方向分别处理,实现真正的联动控制
- 死区防抖:70%死区+指令去重,确保控制精度
- 状态实时反馈:动态颜色+Emoji图标,直观清晰
- 现代化UI:卡片+圆角+阴影,专业美观
- 深色模式:完整适配,护眼舒适
扩展方向
- 🔧 添加姿态记录功能
- 📊 增加数据可视化
- 🎮 支持手柄控制
- 🤖 集成AI视觉识别
- 📱 多设备协同控制
🔗 参考资源
💬 交流讨论
如有问题欢迎评论区讨论,也可以:
- 📧 邮箱联系1580195000@qq.com
- 💬 技术交流
- 🐛 提交:马上开源
感谢阅读!如果本文对你有帮助,请点赞收藏支持一下! ⭐
#HarmonyOS #机械臂控制 #蓝牙开发 #ArkTS #虚拟摇杆
更多推荐
所有评论(0)