【小白也能学】从挫败到突破,5天地狱式开发,如何用 AI 和 Arduino 打造属于自己的智能桌宠?——慢慢学AI144
写在前面
- Arduino 是做什么用的
- 我们理解它是用来降低复杂度,帮我们更好对接不同设备
- R 4 的板子和舵机是什么关系
- 舵机就是一个普通外设,流水的设备,铁打的板子
- AI 编程的瓶颈大概在哪里
- 如果错误提示 AI 没有遇到过
- 如果那个编程语言它不熟悉
- 我们自己不知道要干啥
这次的意外体会,上个月在编程上,基本上个人想法参与很少,完全让 AI 牵着走,效率高,效果好。最近几天慢慢浮现了过往的思考和经历,想要参与进去的想法多了很多,但是结果也更加不理想了。
这也是在印证前段时间,关于如何和 AI 相处,放下傲慢,让渡权力的想法。在一定阶段会特别想要参与进去,实际上这是徒劳的,需要找到更好的方式。
项目背景和起源
上半年的 AI 硬件中,有别于 AI Pin 这种“小玩意“,出现了一个桌面的小宠物——LOOI,有了 AI 的加持,它能在桌面上自由滑动,动作捕捉,人脸识别,偶尔发点小脾气,偶尔烘托点情侣之间的气氛,情绪价值算是给到了,技术上着实火了一把。
远在大洋彼岸,自诩是文科生的机器人大拿 — Garman ,敏锐捕捉到了它的陪伴价值,在现代快节奏生活中的情感寄托,在无数次分享中,他都强调了这点,这在技术人中很少见到。
然后 1500+的价格,对于很多人来说,内心的独白大概是: 我!没有情感诉求,没有!
Garman 自学 Python,自学 Arduino 编程,在经历过无数次尝试以后,无私贡献了自己的成果,把价格硬生生给打下来了,不足 200 就实现了 LOOI 自由。不过,门槛并不低,在一次分享以后,家里多一个吃灰的 R4 板子。
这次 AIPO 高校活动,鉴于 AI 编程共学的经验,做了 Arduino 的安装教程(因为当时以为这是主要的开发工具),有幸成为 Garman 老师的助手,在老师的眼皮底下,眼睁睁看着把线给插错了,不知道直播间的同学们进展如何。
回来以后,就想着 :
- Arduino 是要用汇编语言写吗?
- Python 在这个事中扮演什么样的角色
- 为什么安卓手机不行,只有 iOS 可以
- 刷机是什么意思
- 作为生产力工具的老鸟,希望能解决把串口号抄下来这个难题。
带着这些疑问开始了这次的征程,目前的成果大概是这样的:
目前还有 2 个问题没有解决: |
- 如何和手机联动,发信息过去
- 摄像头跟随
有兴趣的小伙伴留言,一起折腾完善。
源代码分析,各设备之间的交互逻辑
这次实践之前,完全没有 Arduino 操作经验,对硬件一窍不通。
分析现有代码逻辑—Claude
这部分基本是靠 claude 来实现的,下面是 Garman 老师的核心逻辑
老实说,上次看到这个图还是上一次,就在半年前,当时想着应该不难吧,才 3 个文件,硬啃也能啃下来呀,结果,楞是啃了半年,毫无进展,这次借助 claude,才稍微有了点眉目。
硬件部分完全小白,请移步 Garman 老师的共学视频。waytoagi 知识库搜索 Garman
整体逻辑
sequenceDiagram participant User as 用户 participant Chat as chat.py (Macbook) participant API as OpenAI API participant Face as face.py (iPhone) participant Head as head.py (Macbook) participant Servo as 舵机电机 User->>Chat: 输入对话内容 Chat->>API: 发送提示 API-->>Chat: 返回响应 Chat->>Chat: 解析JSON响应 Chat->>Chat: 生成语音 Chat->>Face: 发送颜文字和音频 Face->>Face: 显示颜文字 Face->>Face: 播放音频 Chat->>Head: 发送舵机位置 Head->>Head: 处理舵机位置 Head->>Servo: 控制舵机移动 Head->>Head: 更新图像显示
在原图基础上,重新梳理了一个时序图,从下面的箭头大概能看出来信息的流动情况。每一次会话的过程大概是这样的:
- 在电脑上同时启动 2 个程序,也就是 chat 和 head。在手机上启动face
- 在 chat 里面输入信息,它会和 AI 说,AI 就给了一段声音和一个表情符号
- 然后 chat 把它发给手机,手机上的 face 拿到这个信息之后,播放声音,显示一个表情符号
这样就结束了。然后我们再来看 head,它的作用是时刻捕捉摄像头,分析里面的人物动作,去调整舵机。
另外一个版本的流程图
graph LR A[用户输入] --> B[chat.py PC电脑] B --> C[OpenAI API] C --> B B --> D[face.py iPhone] B --> E[head.py PC电脑] E --> F[舵机电机] D --> G[显示颜文字] D --> H[播放音频] style A fill:#f0f8ff,stroke:#b0e0e6,stroke-width:2px style B fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style C fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style D fill:#fff0f5,stroke:#ffd9e6,stroke-width:2px style E fill:#f0fff0,stroke:#98fb98,stroke-width:2px style F fill:#fffaf0,stroke:#ffdab9,stroke-width:2px style G fill:#fff5ee,stroke:#ffa07a,stroke-width:2px style H fill:#f0e6ff,stroke:#d9b3ff,stroke-width:2px
3个模块的内部逻辑
Head (电脑端运行)
graph TD A[开始] --> B[初始化摄像头和Arduino] B --> C[启动socket监听线程] C --> D[进入主循环] D --> E{最近5秒内收到数据?} E -->|是| F[使用接收到的舵机位置] E -->|否| G[检测人脸并计算舵机位置] F --> H[更新舵机位置] G --> H H --> I[在图像上显示状态] I --> J[显示图像] J --> D style A fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style B fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style C fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style D fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style E fill:#fff0e6,stroke:#ffd9b3,stroke-width:2px style F fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style G fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style H fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style I fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style J fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px
Face(手机上运行)
graph TD A[开始] --> B[初始化socket] B --> C[启动receive_data线程] C --> D[初始化PyGame场景] D --> E{收到新数据?} E -->|是| F[更新text_to_display] F --> G[保存并播放音频] E -->|否| H[绘制当前文本] G --> H H --> I{触摸事件?} I -->|是| J[清理音频文件] J --> K[关闭场景] I -->|否| E K --> L[结束] style A fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style B fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style C fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style D fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style E fill:#fff0e6,stroke:#ffd9b3,stroke-width:2px style F fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style G fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style H fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style I fill:#fff0e6,stroke:#ffd9b3,stroke-width:2px style J fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style K fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style L fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px
Chat(电脑上运行)
graph TD A[开始] --> B[初始化OpenAI客户端] B --> C[进入主循环] C --> D{用户输入'quit'?} D -->|是| E[结束] D -->|否| F[发送提示到GPT-3.5-turbo] F --> G[解析JSON响应] G --> H[从响应生成语音] H --> I[发送颜文字和音频到iPhone] I --> J[发送舵机位置到head.py] J --> C style A fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style B fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style C fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style D fill:#fff0e6,stroke:#ffd9b3,stroke-width:2px style E fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style F fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style G fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style H fill:#e6fff2,stroke:#b3ffd9,stroke-width:2px style I fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px style J fill:#e6f3ff,stroke:#b3d9ff,stroke-width:2px
至此,整个机器人软件部分的逻辑算是理顺了。AI 编程的原则是能不编程最好不编,无奈在直播的时候,确实是通了,但是回家以后,就咋也不行。想着能不能不用 Python 来写呢?用 nodejs 能不能做到呢?
希望做成什么样
- 首先希望有一个 UI 界面,尽量不用打开命令行
- 其次所有的配置信息最好能抽离出来,方便后续切换
- 最好能看到一些中间过程,方便定位问题
开始改造
经过了 50 多个会话,大概搞清楚了它的逻辑
舵机和板子还有 Arduino 的关系是啥
为了方便排查,板子是否和电脑连接成功,是否能正常控制舵机,就想了一个办法,让 AI 帮忙写一个小程序来测试。也是因为这次经历,了解到它背后的逻辑。
Arduino 代码
1 |
|
它需要放在 arduino 的 ide 中执行
node 的代码 (arduino_servo_control.js)
1 | const { SerialPort } = require('serialport'); |
运行的时候,首先需要安装依赖包,然后运行
1 | npm install serialport |
最后执行的结果是这样的:
到底是什么样的逻辑呢,其实板子在死循环一直等消息
目前了解到的信息大概是这样的。
目前对它的理解大概是这样的:
- Arduino 板子存在的意义,是为了简化我们和不同设备打交道的难度
- 就是它帮我们做了很多对接的事
- 它像一个中转站
- Arduino 上只能有一个程序在运行,每次 upload 以后,原来的就被覆盖了
- 所以也就不存在版本的概念
- 也不存在不同程序之间干扰的情况
当然,这只是我们几个人的理解,但是也打开了一扇窗户,就是为上面这个简单的测试代码打了一个基础。
整体的程序架构分析
基于前面的界面逻辑,在 claude 的帮助下,很快就写出一个客户端程序,之所以选择客户端程序而不是网页,是因为需要和硬件打交道,浏览器里面的权限是受限的。这点在最初的 claude 交互中也提到了。
选择的客户端程序架构分为 javascript 的界面展示和 rust 的硬件控制。这里不要给唬住了,多了俩名词好像挺吓人,都是 AI 写的代码,我们只负责粘贴复制,复制粘贴,出错了让它改,因为,真的看不懂呀!
这个结构看不懂,不明白也不用担心,如果我们搞晕头了,告诉 AI 一声,现在给我们的文件,应该放哪里,让它给一下最新的文件结构就可以了。
这个过程就是个典型的体力活,除了无尽的挫败感,没有任何美感可言,除了多掉几根头发,几乎没啥收获。
在做的过程中,有些错误是反复出现的,因为改了改去会改坏,但是实在没办法理解它的逻辑,只能一遍一遍问 AI。
遇到的困难和体会
缘起
做这个项目的缘起和动机,是因为毕竟拖了半年多了,想要做点东西出来。另外毕竟 Garman 手把手教了一遍,还是希望能做出来。还有一个原因是希望把它的难度稍微降低一点下来,让云桌面能介入这个场景,尽量把软件部分的内容简化。
苦难经历-5 天的地狱生涯
这个项目前后做了 5 天,但是没有最终完成,实际上,目前的代码量已经远远超出最初的 python 版本,如果整体上都用 python 来实现可能会简单高效很多。
中间花费最大时间的,是关于摄像头捕捉的部分,python 版本使用的是 opencv,人脸识别的部分没有使用外部的内容。
但是换成 rust 和 nodejs 以后,
- 能满足 rust 的 opencv 我们的 mac 不支持,需要自己编译
- 而编译又用到了 cmake 工具,所以要先安装 cmake 工具,结果 pkg 包的方式无法安装
- 所以要先编译安装 cmake,为了编译它,又安装llvm
- 但是 brew 安装 llvm 的时候一直报错
- 于是又换了 macport 去安装他们
- 总算 llvm 是安装好了,但是用它去编译 opencv 版本又不行
- 原因是 mac 的版本太低了,opencv 用 brew 方式安装需要是 2024 年 1 月前
- 结果换了不下 10 个版本的 opencv 源码,每次编译都超过半小时,基本都在 60%左右报错
- 这个报错问 claude 也无济于事,就很崩溃
还好睡了一觉,回头看了自己当时和 claude 沟通的目标,是为了能实现人脸捕获,在了解了 arduino 原理以后,决定先放手,把 tts 和舵机控制的问题解决了,而这个过程中,又遇到 firmata 在 rust 支持不够理想的情况。
于是采用 Serial port 的方式通信,摒弃了 firmata,这才了解到 arduino 板子的原理。在 AI 的帮助下,完成了这部分代码。
等这些内容全部完成,回来解决摄像头的问题,就从容淡定了很多。
接下来的思考
首先还是先完成 Garman 老师的内容,注意到实际上手机端是开启了一个端口侦听,这样的话就可以考虑:
- 通过一个 App 来开启监听任务,因为 claude 的能力加持,写 App 和写一个 python 的难度,对我们大部分人来说,是一回事
- 通过一个网页,它呈现的是表情和播放语音,内容可以通过动态轮询或者长连接。
- 我们桌面的程序向那个网页发送信息
这样一来,下次共学通过云桌面降低难度就更可行了。
收获和体会
这次经历大部分还是在 AI 的帮助下完成的,但是涉及到深层次的问题排查和调优,不得不说,还是依赖了一部分过往的经历,虽然不多,但是还是有。大概能感觉到,问题可能出现在哪里。
虽然代码依然看不懂,因为 rust 的语法太过于复杂,而牵扯到 rust 的 javascript 也简单不到哪里去。整体上文件的布局等方面还是有了基本的概念。
我们很希望通过这个案例来阐述,AI 编程在极大降低难度,但是并没有达到我们的预期,完全小白,并不容易,虽然可能性很低,但总归不是 0,我们还是希望在把难度打下来这条路上,贡献一些力量。