专栏名称: dotNET跨平台
专注于.NET Core的技术传播。在这里你可以谈微软.NET,Mono的跨平台开发技术。在这里可以让你的.NET项目有新的思路,不局限于微软的技术栈,横跨Windows,Linux 主流平台
目录
相关文章推荐
人民日报  ·  迎财神!人民日报微信万元现金红包送不停 ·  3 天前  
人民日报  ·  【夜读】新一年做好这4件事,越来越有福气 ·  4 天前  
新华社  ·  破60亿! ·  4 天前  
51好读  ›  专栏  ›  dotNET跨平台

用.NET IoT库控制舵机并多方法播放表情

dotNET跨平台  · 公众号  ·  · 2025-01-31 08:00

正文

前言

前面两篇文章用纯.NET开发并制作一个智能桌面机器人(一):从.NET IoT入门开始 和 用纯.NET开发并制作一个智能桌面机器人(二):用.NET IoT库编写驱动控制两个屏幕讲了.NET IoT相关的知识点,以及硬件的GPIO的一些概念,还有点亮两个屏幕的方法,这些让大家对.NET的用途有了新的认识,那我们这回继续讲解.NET IoT的知识点,以及介绍一些好玩的东西,例如让视频通过机器人的屏幕播放起来,还有机器人的身体也能通过我们的代码控制动起来。大家感兴趣的话可以跟着我的文章继续下去,另外说下我B站更新了机器人相关视频,所以大家可以跟着观看制作,视频包含了机器人的组装和打印文件使用,点击图片即可跳转。

问题解答

大家看完这篇文章,大概对机器人的一些功能模块有了了解,大家肯定会有疑问,做这个机器人到底需要什么电路板,以及只用树莓派到底能够做到什么程度,我会挑一些大家可能会问的问题做一些解答。

1. 只用树莓派可以控制舵机吗?

只用树莓派控制舵机是OK,舵机本身是使用PWM的信号进行控制的,这个可以通过树莓派的引脚进行模拟,这个不在本文章的讨论范围内,有需要可以单独写一篇文章进行讲解。

2. 机器人的制作到底需要哪些电路板?

下图为完整的硬件相关的部分,大家可以大概的了解到机器人的电路构成。

目前机器人总共需要三块板子,一块是我设计的搭配树莓派使用的,另外两块是使用的ElectronBot精英版A2的一个舵机驱动板(用来改装舵机并且驱动舵机的运动),一个语音板子(包含麦克风,喇叭,和摄像头连接),这些大家都可以通过在闲鱼之类搜索ElectronBot相关的关键词买到,大家不要惧怕自己不会焊接电路板不能学习之类。即使大家买不到电路板,通过文章进行学习也是问题不大的,所以大家不要担心。

3. 如果想学习应该怎么样获得电路板?

这个现在网络上都有一站式创客电路板生产的平台,例如嘉立创(这个非广告因为这个是国内算是很成熟的平台了),我刚才提到的ElectronBot精英版A2和我的树莓派拓展板子都在立创的开源广场有提供,大家直接跟着下单就能够拿到电路板了,然后就可以购买芯片物料焊接了。

4. ElectronBot和我做的机器人有什么关系?

ElectronBot是稚晖君(B站一个有名的UP主)制作的一个开源的必须连接电脑的桌面机器人,我和网友在他的方案基础上优化了电路板出了一个ElectronBot精英版A2的版本,现在我通过用树莓派替换了ElectronBot的屏幕控制和舵机控制部分,实现了一个独立的版本,我为了省事,就借用了ElectronBot的两个电路板,省的自己设计了。

名词解释

1. 什么是舵机?

舵机是一种位置(角度)伺服的驱动器,适用于那些需要角度不断变化并可以保持的控制系统。舵机通过一瞬间的堵转扭力将舵盘进行转向,持续的时间短。最早以前在高档遥控玩具,如飞机、潜艇模型,遥控机器人中已经得到了普遍应用。如今通过技术的革新,加工工艺的升级,舵机在各行各业中已得到越来越广泛的应用。

2. 什么是I2C通讯?

I2C(Inter-Integrated Circuit),读作:I方C,是一种同步、多主多从架构、双向双线的串行通信总线,通常应用于短距离、低速通信场景,广泛用于微控制器和各种外围设备之间的通信。它使用两条线路:串行数据线(SDA)和串行时钟线(SCL)进行双向传输。

3. 什么是lottie动画?

Lottie 是一种轻量级的基于 JSON 的动画格式,可以在任何设备或浏览器上播放。设计师和开发人员广泛使用它来改善网站和应用程序的交互。Lottie 的矢量结构允许用户在不失去图像质量或增加文件大小的情况下缩放动画。

4. 什么是ffmpeg?

FFmpeg 是一个完整的跨平台音视频解决方案,用于记录、转换和流式处理音视频。它是目前最强大的音视频处理开源软件之一,被广泛应用于视频网站、播放器、编码器等多种场景中。

舵机控制

舵机控制板固件相关介绍

  1. 首先我们象征性的看下舵机板子的固件代码,舵机控制板使用STM32F103标准库硬件IIC+DMA的类似方案进行数据读写,有社区的人进行了优化,但是核心代码大体相同,改装的舵机板比舵机原始的只支持角度控制有更多的玩法。参考如下文档:
    STM32F103标准库硬件IIC+DMA连续数据发送、接收
    ,核心代码如下:

    // // Command handler
    void I2C_SlaveDMARxCpltCallback()
    {
    ErrorStatus state;

    float valF = *((float*) (i2cDataRx + 1));

    i2cDataTx[0] = i2cDataRx[0];
    switch (i2cDataRx[0])
    {
    case 0x01: // Set angle
    {
    motor.dce.setPointPos = valF;
    auto* b = (unsigned char*) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x02: // Set velocity
    {
    motor.dce.setPointVel = valF;
    auto* b = (unsigned char*) &(motor.velocity);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x03: // Set torque
    {
    motor.SetTorqueLimit(valF);
    auto* b = (unsigned char*) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x11: // Get angle
    {
    auto* b = (unsigned char*) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x12: // Get velocity
    {
    auto* b = (unsigned char*) &(motor.velocity);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x21: // Set id
    {
    boardConfig.nodeId = i2cDataRx[1];
    boardConfig.configStatus = CONFIG_COMMIT;
    auto* b = (unsigned char*) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x22: // Set kp
    {
    motor.dce.kp = valF;
    boardConfig.dceKp = valF;
    boardConfig.configStatus = CONFIG_COMMIT;
    auto* b = (unsigned char*) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x23: // Set ki
    {
    motor.dce.ki = valF;
    boardConfig.dceKi = valF;
    boardConfig.configStatus = CONFIG_COMMIT;
    auto* b = (unsigned char*) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x24: // Set kv
    {
    motor.dce.kv = valF;
    boardConfig.dceKv = valF;
    boardConfig.configStatus = CONFIG_COMMIT;
    auto* b = (unsigned char*) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x25: // Set kd
    {
    motor.dce.kd = valF;
    boardConfig.dceKd = valF;
    boardConfig.configStatus = CONFIG_COMMIT;
    auto* b = (unsigned char*) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x26: // Set torque limit
    {
    motor.SetTorqueLimit(valF);
    boardConfig.toqueLimit = valF;
    boardConfig.configStatus = CONFIG_COMMIT;
    auto* b = (unsigned char *) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0x27: // Set init pos
    {
    boardConfig.initPos = valF;
    boardConfig.configStatus = CONFIG_COMMIT;
    auto* b = (unsigned char*) &(motor.angle);
    for (int i = 0; i < 4; i++)
    i2cDataTx[i + 1] = *(b + i);
    break;
    }
    case 0xff:
    motor.SetEnable(i2cDataRx[1] != 0);
    break;
    default:
    break;
    }
    do
    {
    state = Slave_Transmit(i2cDataTx,5,5000);
    } while (state != SUCCESS);
    if(i2cDataRx[0] == 0x21)
    {
    Set_ID(boardConfig.nodeId);
    }

    }


    // Control loop
    void TIM14_PeriodElapsedCallback()
    {
    // Read sensor data
    LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_1);
    LL_ADC_REG_StartConversion(ADC1);

    motor.angle = motor.mechanicalAngleMin +
    (motor.mechanicalAngleMax - motor.mechanicalAngleMin) *
    ((float) adcData[0] - (float) motor.adcValAtAngleMin) /
    ((float) motor.adcValAtAngleMax - (float) motor.adcValAtAngleMin);
    // Calculate PID
    motor.CalcDceOutput(motor.angle, 0);
    motor.SetPwm((int16_t) motor.dce.output);
    }
  2. 固件控制指令对照图,这些指令是通过树莓派I2C引脚进行发送。

  3. 个人的一些心得,控制板核心逻辑有个死循环,如果通讯不正常,会一直等待,所以如果树莓派的执行控制代码发送的不对,会出现I2C引脚超时的错误,这个大家操作的时候一定要记住接线是否正确,代码是否配置OK。

  4. I2C设备都是并联到I2C总线上的,每个设备都有一个设备的ID,所以我们在和设备通讯的时候一定要指定设备的ID才能完成初始化。

舵机控制代码编写

由于我做的独立版桌面机器人目前只用到了两个舵机,所以我选择了2号和3号ID的舵机进行控制。通过初始化I2C设备对象,进行通讯的建立,并进行角度的控制。示例代码是将舵机循环往复的运动180°,使用.NET IoT库编写,并在树莓派上部署使用,示例代码如下:

using System.Device.I2c;

try
{
while (true)
{
using I2cDevice i2c = I2cDevice.Create(new I2cConnectionSettings(1, 0x02));

using I2cDevice i2c8 = I2cDevice.Create(new I2cConnectionSettings(1, 0x03));

byte[] writeBuffer = new byte[5] { 0xff, 0x01, 0x00, 0x00, 0x00 };
byte[] receiveData = new byte[5];

i2c.WriteRead(writeBuffer, receiveData);

i2c8.WriteRead(writeBuffer, receiveData);

for (int i = 0; i < 180; i += 1)
{
float angle = i;

byte[] angleBytes = BitConverter.GetBytes(angle);

writeBuffer[0] = 0x01;
Array.Copy(angleBytes, 0, writeBuffer, 1, angleBytes.Length);

i2c.WriteRead(writeBuffer, receiveData);
i2c8.WriteRead(writeBuffer, receiveData);
Thread.Sleep(20);
}
for (int i = 180; i > 0; i -= 1)
{
float angle = i;

byte[] angleBytes = BitConverter.GetBytes(angle);

writeBuffer[0] = 0x01;
Array.Copy(angleBytes, 0, writeBuffer, 1, angleBytes.Length);

i2c.WriteRead(writeBuffer, receiveData);
i2c8.WriteRead(writeBuffer, receiveData);
Thread.Sleep(20);
}

Console.WriteLine($"I2C 2 8 设备连接成功--{DateTime.Now.ToString("s")}");
foreach (var data in receiveData)
{
Console.Write($"{data}, ");
}
//Console.WriteLine();
//Thread.Sleep(500);
}
}
catch (Exception ex)
{
Console.WriteLine($"I2C 设备连接失败: {ex.Message}");
}

Console.ReadLine();

控制代码看起来很简单,但是这里有个坑,就是大家也看到了一个奇怪的地方,就是为什么发送数据的时候要用WriteRead这个方法,而不是先write再Read这样的操作。其实这里也卡住我了,我翻了固件的源码,我怀疑是因为舵机版子的速度太快了,导致读写的区分不大,如果我只是写入数据再读取会导致循环卡住,这里我是推测,我翻了.NET IoT的这个I2C通讯的源码,然后我用了WriteRead这个方法测试,发现通讯是OK的,如果有大佬能给出更详细的解答,欢迎评论区给大家科普一下。到这里舵机的控制就算是完成了,具体更详细的控制大家可以根据控制指令手册进行编写测试。

舵机测试

下图标出了树莓派的I2C引脚位置,这两个引脚和舵机控制板的I2C引脚进行接线就可以通讯了,舵机板子需要供电,而且舵机板子的地线要和树莓派板子共地,如果是其他的I2C设备也是一样,例如陀螺仪,I2C屏幕。

如果接线OK,代码运行OK,正常情况下会看到舵机旋转的样子。

看到这里大家有什么疑问可以在评论区讨论。

多种方式播放表情

这篇文章的篇幅有点长,上面我们讲了舵机的控制,上一篇文章我们调通了屏幕的显示,但是只显示图片其实不够生动的,如果我们能够配上表情的播放那就生动多了。

解析lottie动画文件进行播放

上面的名词解释我们解释了什么是lottie动画,那我们就直接看代码吧,这个lottie动画目前我在树莓派上进行解析不是很流畅,所以只是作为知识讲解,大家如果是树莓派4或者5应该性能很好,解析起来应该不费劲,而且如果代码能够优化一些应该也可以流畅。

我的做法是通过使用一些解析库,能够解析lottie动画,提取出帧数据,然后解析成ImageSharp的Image类,然后转换成字节数组就可以进行播放了。下面是我找到的社区的一些开源库,SkiaSharp.Skottie有提供解析功能。

	
Include="SkiaSharp" Version="3.116.1" />
Include="SkiaSharp.Skottie" Version="3.116.1" />
Include="SixLabors.ImageSharp" Version="3.1.6" />

核心的解析动画并转成Image的代码如下:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SkiaSharp;
using SkiaSharp.Skottie;

namespace Verdure.LottieToImage;

public class LottieToImage
{
public static Image RenderLottieFrame(Animation animation, double progress, int width, int height)
{

// 创建SKSurface用于渲染
using var bitmap = new SKBitmap(width, height);
using var canvas = new SKCanvas(bitmap);

// 清除背景
canvas.Clear(SKColors.Transparent);

animation.SeekFrameTime(progress);
animation.Render(canvas, new SKRect(0, 0, width, height));

// 将SKBitmap转换为byte数组
using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
var bytes = data.ToArray();

// 转换为ImageSharp格式
using var memStream = new MemoryStream(bytes);
return Image.Load(memStream);
}

public static async Task SaveLottieFramesAsync(string lottieJsonPath, string outputDir, int width, int height)
{
Directory.CreateDirectory(outputDir);
// 读取Lottie JSON文件
var animation = Animation.Create(lottieJsonPath);
if (animation != null)
{
//帧数
var frameCount = animation.OutPoint;
for (int i = 0; i < frameCount; i++)
{
var progress = animation.Duration.TotalSeconds / (frameCount - i);
var frame = RenderLottieFrame(animation, progress, width, height);
await frame.SaveAsPngAsync(Path.Combine(outputDir, $"frame_{i:D4}.png"));
}
}
}
}

转成Image对象之后,就可以使用我们上一篇文章里的方法转成字节数组写入到屏幕了。这个大家有兴趣可以查看我的项目代码里,有做demo测试。

桌面桌面机器人仓库地址

通过转换MP4格式文件进行播放

这一种方式我是事先通过ffmpeg解析mp4的表情文件,然后将表情转换成屏幕直接显示的字节数组,并且序列化到json文件里,这样将解析转换的部分的逻辑前置处理了,树莓派在播放表情的时候就可以很轻松了。
核心转换代码逻辑如下:

将视频帧转成图片的字节数组代码:

using FFmpeg.NET;
using FFmpegImageSharp.Models;
using System;
using System.Collections.Generic;
using System.IO;





请到「今天看啥」查看全文