本文详细介绍如何开发一个基于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
      });
    }
  }
}

关键技术点:

  1. 死区判断:只有推动超过70%才触发,防止误操作
  2. 归一化坐标:将物理坐标转换为-1到1的标准值
  3. 双轴独立:水平和垂直方向分别判断,互不干扰
  4. 限制半径:确保摇杆球不会超出背景圆

三、双轴联动控制

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: 检查以下几点:

  1. 权限是否正确配置
  2. STM32蓝牙模块是否已开启
  3. 设备是否已配对
  4. SPP UUID是否匹配

Q2: 摇杆不响应?

A: 可能原因:

  1. 检查手势识别是否正确
  2. 确认 onDualAxisChange 回调是否设置
  3. 调整死区参数 deadZone

Q3: 指令发送延迟?

A: 优化建议:

  1. 只在指令变化时发送(已实现)
  2. 调整蓝牙MTU大小
  3. 优化STM32接收处理速度

Q4: 深色模式不生效?

A: 检查步骤:

  1. 确认 isDarkMode() 返回值
  2. 检查所有颜色是否都添加了判断
  3. 重新编译应用

Q5: 界面布局混乱?

A: 确认事项:

  1. 检查 orientation 配置
  2. 确认屏幕尺寸适配
  3. 调整卡片大小和间距

📚 总结

本教程详细介绍了HarmonyOS机械臂控制应用的完整开发流程,涵盖:

蓝牙通信:SPP协议、权限配置 ✅ 虚拟摇杆:手势识别、死区判断、双轴联动 ✅ 控制逻辑:指令映射、状态管理、防抖优化 ✅ 界面设计:卡片布局、配色方案、视觉效果 ✅ 深色模式:主题切换、颜色适配、用户体验

技术要点

  1. 双轴独立判断:水平和垂直方向分别处理,实现真正的联动控制
  2. 死区防抖:70%死区+指令去重,确保控制精度
  3. 状态实时反馈:动态颜色+Emoji图标,直观清晰
  4. 现代化UI:卡片+圆角+阴影,专业美观
  5. 深色模式:完整适配,护眼舒适

扩展方向

  • 🔧 添加姿态记录功能
  • 📊 增加数据可视化
  • 🎮 支持手柄控制
  • 🤖 集成AI视觉识别
  • 📱 多设备协同控制

🔗 参考资源


💬 交流讨论

如有问题欢迎评论区讨论,也可以:

  • 📧 邮箱联系1580195000@qq.com
  • 💬 技术交流
  • 🐛 提交:马上开源

感谢阅读!如果本文对你有帮助,请点赞收藏支持一下!

#HarmonyOS #机械臂控制 #蓝牙开发 #ArkTS #虚拟摇杆

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐