互联网不景气了那就玩玩嵌入式吧,用纯.NET开辟并制作一个智能桌面机器人( ...

王國慶  金牌会员 | 2025-2-12 14:30:07 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 914|帖子 914|积分 2746

前言

前面两篇文章讲了.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 是一个完整的跨平台音视频解决方案,用于纪录、转换和流式处理音视频。它是目前最强盛的音视频处理开源软件之一,被广泛应用于视频网站、播放器、编码器等多种场景中。
舵机控制

舵机控制板固件相关先容


  • 起首我们象征性的看下舵机板子的固件代码,舵机控制板利用STM32F103尺度库硬件IIC+DMA的类似方案进行数据读写,有社区的人进行了优化,但是核心代码大体相同,改装的舵机板比舵机原始的只支持角度控制有更多的玩法。参考如下文档:
    STM32F103尺度库硬件IIC+DMA连续数据发送、吸取
    ,核心代码如下:
    1. // // Command handler
    2. void I2C_SlaveDMARxCpltCallback()
    3. {
    4.     ErrorStatus state;
    5.     float valF = *((float*) (i2cDataRx + 1));
    6.     i2cDataTx[0] = i2cDataRx[0];
    7.     switch (i2cDataRx[0])
    8.     {
    9.         case 0x01:  // Set angle
    10.         {
    11.             motor.dce.setPointPos = valF;
    12.             auto* b = (unsigned char*) &(motor.angle);
    13.             for (int i = 0; i < 4; i++)
    14.                 i2cDataTx[i + 1] = *(b + i);
    15.             break;
    16.         }
    17.         case 0x02: // Set velocity
    18.         {
    19.             motor.dce.setPointVel = valF;
    20.             auto* b = (unsigned char*) &(motor.velocity);
    21.             for (int i = 0; i < 4; i++)
    22.                 i2cDataTx[i + 1] = *(b + i);
    23.             break;
    24.         }
    25.         case 0x03: // Set torque
    26.         {
    27.             motor.SetTorqueLimit(valF);
    28.             auto* b = (unsigned char*) &(motor.angle);
    29.             for (int i = 0; i < 4; i++)
    30.                 i2cDataTx[i + 1] = *(b + i);
    31.             break;
    32.         }
    33.         case 0x11: // Get angle
    34.         {
    35.             auto* b = (unsigned char*) &(motor.angle);
    36.             for (int i = 0; i < 4; i++)
    37.                 i2cDataTx[i + 1] = *(b + i);
    38.             break;
    39.         }
    40.         case 0x12: // Get velocity
    41.         {
    42.             auto* b = (unsigned char*) &(motor.velocity);
    43.             for (int i = 0; i < 4; i++)
    44.                 i2cDataTx[i + 1] = *(b + i);
    45.             break;
    46.         }
    47.         case 0x21: // Set id
    48.         {
    49.             boardConfig.nodeId = i2cDataRx[1];
    50.             boardConfig.configStatus = CONFIG_COMMIT;
    51.             auto* b = (unsigned char*) &(motor.angle);
    52.             for (int i = 0; i < 4; i++)
    53.                 i2cDataTx[i + 1] = *(b + i);
    54.             break;
    55.         }
    56.         case 0x22: // Set kp
    57.         {
    58.             motor.dce.kp = valF;
    59.             boardConfig.dceKp = valF;
    60.             boardConfig.configStatus = CONFIG_COMMIT;
    61.             auto* b = (unsigned char*) &(motor.angle);
    62.             for (int i = 0; i < 4; i++)
    63.                 i2cDataTx[i + 1] = *(b + i);
    64.             break;
    65.         }
    66.         case 0x23: // Set ki
    67.         {
    68.             motor.dce.ki = valF;
    69.             boardConfig.dceKi = valF;
    70.             boardConfig.configStatus = CONFIG_COMMIT;
    71.             auto* b = (unsigned char*) &(motor.angle);
    72.             for (int i = 0; i < 4; i++)
    73.                 i2cDataTx[i + 1] = *(b + i);
    74.             break;
    75.         }
    76.         case 0x24: // Set kv
    77.         {
    78.             motor.dce.kv = valF;
    79.             boardConfig.dceKv = valF;
    80.             boardConfig.configStatus = CONFIG_COMMIT;
    81.             auto* b = (unsigned char*) &(motor.angle);
    82.             for (int i = 0; i < 4; i++)
    83.                 i2cDataTx[i + 1] = *(b + i);
    84.             break;
    85.         }
    86.         case 0x25: // Set kd
    87.         {
    88.             motor.dce.kd = valF;
    89.             boardConfig.dceKd = valF;
    90.             boardConfig.configStatus = CONFIG_COMMIT;
    91.             auto* b = (unsigned char*) &(motor.angle);
    92.             for (int i = 0; i < 4; i++)
    93.                 i2cDataTx[i + 1] = *(b + i);
    94.             break;
    95.         }
    96.         case 0x26: // Set torque limit
    97.         {
    98.             motor.SetTorqueLimit(valF);
    99.             boardConfig.toqueLimit = valF;
    100.             boardConfig.configStatus = CONFIG_COMMIT;
    101.             auto* b = (unsigned char*) &(motor.angle);
    102.             for (int i = 0; i < 4; i++)
    103.                 i2cDataTx[i + 1] = *(b + i);
    104.             break;
    105.         }
    106.         case 0x27: // Set init pos
    107.         {
    108.             boardConfig.initPos = valF;
    109.             boardConfig.configStatus = CONFIG_COMMIT;
    110.             auto* b = (unsigned char*) &(motor.angle);
    111.             for (int i = 0; i < 4; i++)
    112.                 i2cDataTx[i + 1] = *(b + i);
    113.             break;
    114.         }
    115.         case 0xff:
    116.             motor.SetEnable(i2cDataRx[1] != 0);
    117.             break;
    118.         default:
    119.             break;
    120.     }
    121.     do
    122.     {
    123.     state = Slave_Transmit(i2cDataTx,5,5000);
    124.     } while (state != SUCCESS);
    125.     if(i2cDataRx[0] == 0x21)
    126.     {
    127.         Set_ID(boardConfig.nodeId);
    128.     }
    129. }
    130. // Control loop
    131. void TIM14_PeriodElapsedCallback()
    132. {
    133.         // Read sensor data
    134.     LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_1);  
    135.     LL_ADC_REG_StartConversion(ADC1);
    136.    
    137.     motor.angle = motor.mechanicalAngleMin +
    138.                     (motor.mechanicalAngleMax - motor.mechanicalAngleMin) *
    139.                     ((float) adcData[0] - (float) motor.adcValAtAngleMin) /
    140.                     ((float) motor.adcValAtAngleMax - (float) motor.adcValAtAngleMin);
    141.     // Calculate PID
    142.     motor.CalcDceOutput(motor.angle, 0);
    143.     motor.SetPwm((int16_t) motor.dce.output);
    144. }
    复制代码
  • 固件控制指令对照图,这些指令是通过树莓派I2C引脚进行发送。

  • 个人的一些心得,控制板核心逻辑有个死循环,如果通讯不正常,会一直等候,以是如果树莓派的执行控制代码发送的不对,会出现I2C引脚超时的错误,这个各人操作的时候一定要记住接线是否正确,代码是否配置OK。
  • I2C设备都是并联到I2C总线上的,每个设备都有一个设备的ID,以是我们在和设备通讯的时候一定要指定设备的ID才能完成初始化。

舵机控制代码编写

由于我做的独立版桌面机器人目前只用到了两个舵机,以是我选择了2号和3号ID的舵机进行控制。通过初始化I2C设备对象,进行通讯的建立,并进行角度的控制。示例代码是将舵机循环往复的运动180°,利用.NET IoT库编写,并在树莓派上部署利用,示例代码如下:
  1. using System.Device.I2c;
  2. try
  3. {
  4.     while (true)
  5.     {
  6.         using I2cDevice i2c = I2cDevice.Create(new I2cConnectionSettings(1, 0x02));
  7.         using I2cDevice i2c8 = I2cDevice.Create(new I2cConnectionSettings(1, 0x03));
  8.         byte[] writeBuffer = new byte[5] { 0xff, 0x01, 0x00, 0x00, 0x00 };
  9.         byte[] receiveData = new byte[5];
  10.         i2c.WriteRead(writeBuffer, receiveData);
  11.         i2c8.WriteRead(writeBuffer, receiveData);
  12.         for (int i = 0; i < 180; i += 1)
  13.         {
  14.             float angle = i;
  15.             byte[] angleBytes = BitConverter.GetBytes(angle);
  16.             writeBuffer[0] = 0x01;
  17.             Array.Copy(angleBytes, 0, writeBuffer, 1, angleBytes.Length);
  18.             i2c.WriteRead(writeBuffer, receiveData);
  19.             i2c8.WriteRead(writeBuffer, receiveData);
  20.             Thread.Sleep(20);
  21.         }
  22.         for (int i = 180; i > 0; i -= 1)
  23.         {
  24.             float angle = i;
  25.             byte[] angleBytes = BitConverter.GetBytes(angle);
  26.             writeBuffer[0] = 0x01;
  27.             Array.Copy(angleBytes, 0, writeBuffer, 1, angleBytes.Length);
  28.            i2c.WriteRead(writeBuffer, receiveData);
  29.             i2c8.WriteRead(writeBuffer, receiveData);
  30.             Thread.Sleep(20);
  31.         }
  32.         Console.WriteLine($"I2C 2 8 设备连接成功--{DateTime.Now.ToString("s")}");
  33.         foreach (var data in receiveData)
  34.         {
  35.             Console.Write($"{data}, ");
  36.         }
  37.         //Console.WriteLine();
  38.         //Thread.Sleep(500);      
  39.     }
  40. }
  41. catch (Exception ex)
  42. {
  43.     Console.WriteLine($"I2C 设备连接失败: {ex.Message}");
  44. }
  45. 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有提供剖析功能。
  1.         <ItemGroup>
  2.                 <PackageReference Include="SkiaSharp" Version="3.116.1" />
  3.                 <PackageReference Include="SkiaSharp.Skottie" Version="3.116.1" />
  4.                 <PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
  5.         </ItemGroup>
复制代码
核心的剖析动画并转成Image的代码如下:
  1. using SixLabors.ImageSharp;
  2. using SixLabors.ImageSharp.PixelFormats;
  3. using SkiaSharp;
  4. using SkiaSharp.Skottie;
  5. namespace Verdure.LottieToImage;
  6. public class LottieToImage
  7. {
  8.     public static Image<Bgra32> RenderLottieFrame(Animation animation, double progress, int width, int height)
  9.     {
  10.         // 创建SKSurface用于渲染
  11.         using var bitmap = new SKBitmap(width, height);
  12.         using var canvas = new SKCanvas(bitmap);
  13.         // 清除背景
  14.         canvas.Clear(SKColors.Transparent);
  15.         animation.SeekFrameTime(progress);
  16.         animation.Render(canvas, new SKRect(0, 0, width, height));
  17.         // 将SKBitmap转换为byte数组
  18.         using var image = SKImage.FromBitmap(bitmap);
  19.         using var data = image.Encode(SKEncodedImageFormat.Png, 100);
  20.         var bytes = data.ToArray();
  21.         // 转换为ImageSharp格式
  22.         using var memStream = new MemoryStream(bytes);
  23.         return Image.Load<Bgra32>(memStream);
  24.     }
  25.     public static async Task SaveLottieFramesAsync(string lottieJsonPath, string outputDir, int width, int height)
  26.     {
  27.         Directory.CreateDirectory(outputDir);
  28.         // 读取Lottie JSON文件
  29.         var animation = Animation.Create(lottieJsonPath);
  30.         if (animation != null)
  31.         {
  32.             //帧数
  33.             var frameCount = animation.OutPoint;
  34.             for (int i = 0; i < frameCount; i++)
  35.             {
  36.                 var progress = animation.Duration.TotalSeconds / (frameCount - i);
  37.                 var frame = RenderLottieFrame(animation, progress, width, height);
  38.                 await frame.SaveAsPngAsync(Path.Combine(outputDir, $"frame_{i:D4}.png"));
  39.             }
  40.         }
  41.     }
  42. }
复制代码
转成Image对象之后,就可以利用我们上一篇文章里的方法转成字节数组写入到屏幕了。这个各人有爱好可以查看我的项目代码里,有做demo测试。
桌面桌面机器人仓库地点
通过转换MP4格式文件进行播放

这一种方式我是事先通过ffmpeg剖析mp4的表情文件,然后将表情转换成屏幕直接显示的字节数组,并且序列化到json文件里,这样将剖析转换的部分的逻辑前置处理了,树莓派在播放表情的时候就可以很轻松了。
核心转换代码逻辑如下:
将视频帧转成图片的字节数组代码:
  1. using FFmpeg.NET;
  2. using FFmpegImageSharp.Models;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. namespace FFmpegImageSharp.Services;
  9. public class FrameExtractor
  10. {
  11.     public async Task<List<FrameData>> ExtractFramesAsync(string filePath)
  12.     {
  13.         var frames = new List<FrameData>();
  14.         var ffmpeg = new Engine("C:\\ffmpeg-n7.1-latest-win64-gpl-7.1\\ffmpeg-n7.1-latest-win64-gpl-7.1\\bin\\ffmpeg.exe"); // Specify the path to ffmpeg executable
  15.         var mediaFile = new InputFile(filePath); // Use a concrete class instead of MediaFile
  16.         var mediaInfo = await ffmpeg.GetMetaDataAsync(mediaFile, CancellationToken.None);
  17.         var duration = mediaInfo.Duration;
  18.         var frameRate = mediaInfo.VideoData.Fps;
  19.         var frameCount = (int)(duration.TotalSeconds * frameRate);
  20.         for (var i = 0; i < frameCount; i++)
  21.         {
  22.             var timestamp = TimeSpan.FromSeconds(i / frameRate);
  23.             var outputFilePath = $"frame_{i}.jpg";
  24.             var arguments = $"-i "{filePath}" -vf "select='eq(n\\,{i})'" -vsync vfr -q:v 2 "{outputFilePath}"";
  25.             await ffmpeg.ExecuteAsync(arguments, CancellationToken.None);
  26.             var frameImage = await File.ReadAllBytesAsync(outputFilePath);
  27.             var frameData = new FrameData
  28.             {
  29.                 ImageData = frameImage,
  30.                 Timestamp = timestamp
  31.             };
  32.             frames.Add(frameData);
  33.         }
  34.         return frames;
  35.     }
  36. }
复制代码
将图片字节数组转成显示屏需要的字节数组数据的代码如下:
  1. using FFmpegImageSharp.Models;
  2. using SixLabors.ImageSharp;
  3. using SixLabors.ImageSharp.PixelFormats;
  4. using SixLabors.ImageSharp.Processing;
  5. namespace FFmpegImageSharp.Services;
  6. public class ImageProcessor
  7. {
  8.     public byte[] ProcessImage(FrameData frame)
  9.     {
  10.         using (var image = Image.Load(frame.ImageData))
  11.         {
  12.             // Resize the image to 240x240
  13.             image.Mutate(x => x.Resize(240, 240));
  14.             // Create a new 320x240 image with a custom background color
  15.             using (var background = new Image<Bgra32>(320, 240, new Bgra32(0, 0, 0))) // Custom color: black
  16.             {
  17.                 // Calculate the position to center the 240x240 image on the 320x240 background
  18.                 var x = (background.Width - image.Width) / 2;
  19.                 var y = (background.Height - image.Height) / 2;
  20.                 // Draw the resized image onto the background
  21.                 background.Mutate(ctx => ctx.DrawImage(image, new Point(x, y), 1f));
  22.                 background.Mutate(x => x.Rotate(90));
  23.                 using Image<Bgr24> converted2inch4Image = background.CloneAs<Bgr24>();
  24.                 var byteList = GetImageBytes(converted2inch4Image);
  25.                 return byteList;
  26.                 // Save the processed image or perform further processing
  27.                 //background.Save($"path_to_save_processed_image_{DateTime.Now.Ticks}.png");
  28.             }
  29.         }
  30.     }
  31.     public byte[] GetImageBytes(Image<Bgr24> image, int xStart = 0, int yStart = 0)
  32.     {
  33.         int imwidth = image.Width;
  34.         int imheight = image.Height;
  35.         var pix = new byte[imheight * imwidth * 2];
  36.         for (int y = 0; y < imheight; y++)
  37.         {
  38.             for (int x = 0; x < imwidth; x++)
  39.             {
  40.                 var color = image[x, y];
  41.                 pix[(y * imwidth + x) * 2] = (byte)((color.R & 0xF8) | (color.G >> 5));
  42.                 pix[(y * imwidth + x) * 2 + 1] = (byte)(((color.G << 3) & 0xE0) | (color.B >> 3));
  43.             }
  44.         }
  45.         return pix;
  46.     }
  47. }
复制代码
主步伐序列化表情到json数据的代码如下:
  1. using System.Text.Json;
  2. using FFmpegImageSharp.Models;
  3. using FFmpegImageSharp.Services;
  4. using Microsoft.Extensions.DependencyInjection;
  5. var serviceProvider = new ServiceCollection()
  6.             .AddSingleton<FrameExtractor>()
  7.             .AddSingleton<StreamFrameExtractor>()
  8.             .AddSingleton<ImageProcessor>()
  9.             .BuildServiceProvider();
  10. var frameExtractor = serviceProvider.GetRequiredService<FrameExtractor>();
  11. //var streamFrameExtractor = serviceProvider.GetRequiredService<StreamFrameExtractor>();
  12. var imageProcessor = serviceProvider.GetRequiredService<ImageProcessor>();
  13. var videoFilePath = "anger.mp4"; // Update with your video file path
  14. var data = new FrameMetaData
  15. {
  16.     Name = Path.GetFileNameWithoutExtension(videoFilePath),
  17.     FileName = videoFilePath,
  18.     Width = 240,
  19.     Height = 320
  20. };
  21. var frames = await frameExtractor.ExtractFramesAsync(videoFilePath);
  22. foreach (var frame in frames)
  23. {
  24.     var list = imageProcessor.ProcessImage(frame);
  25.     data.FrameDatas.Add(list);
  26. }
  27. // JSON serialization
  28. await File.WriteAllTextAsync($"{data.Name}.json", JsonSerializer.Serialize(data));
  29. // JSON deserialization
  30. var deserializedData = JsonSerializer.Deserialize<FrameMetaData>(await File.ReadAllTextAsync($"{data.Name}.json"));
  31. // Verify deserialization
  32. Console.WriteLine($"Name: {deserializedData?.Name}, Width: {deserializedData?.Width}, Height: {deserializedData?.Height}");
  33. Console.WriteLine("Frame extraction and processing completed. Metadata saved to frame_metadata.json.");
复制代码
通过上面的代码就可以制作出一个表情文件了,源代码在我另外的仓库里,然后通过桌面机器人仓库的代码反序列化并且播放就好了。结果如下:

总结感悟

这篇文章先容了两个方面的东西,内容也算是比较长了,个人感悟,有的时候方案自己感觉挺好的,但是实行的时候有的时候结果不是很理想,感觉我们有时候要多准备几个方案,当验证完其中一个不行的时候也可以及时的切换方案,这样也不至于让我们发急了。固然如果有大佬可以或许直接想出完美的方案,那就最好不外了。
最近国产的大语言模型deepseek公司都给美国带来了不小的压力,目测2025年大语言模型应该会有很大的进步了,希望可以或许对我们的生活的各个方面带来质的改变吧,我们无法选择生命的长度,却可以主宰生命的宽度与高度。希望各人新年都能有新的进步。
参考推荐文档项目如下:


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

王國慶

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表