写在前面

  • Arduino 是做什么用的
    • 我们理解它是用来降低复杂度,帮我们更好对接不同设备
  • R 4 的板子和舵机是什么关系
    • 舵机就是一个普通外设,流水的设备,铁打的板子
  • AI 编程的瓶颈大概在哪里
    • 如果错误提示 AI 没有遇到过
    • 如果那个编程语言它不熟悉
    • 我们自己不知道要干啥

这次的意外体会,上个月在编程上,基本上个人想法参与很少,完全让 AI 牵着走,效率高,效果好。最近几天慢慢浮现了过往的思考和经历,想要参与进去的想法多了很多,但是结果也更加不理想了。
这也是在印证前段时间,关于如何和 AI 相处,放下傲慢,让渡权力的想法。在一定阶段会特别想要参与进去,实际上这是徒劳的,需要找到更好的方式。

项目背景和起源

上半年的 AI 硬件中,有别于 AI Pin 这种“小玩意“,出现了一个桌面的小宠物——LOOI,有了 AI 的加持,它能在桌面上自由滑动,动作捕捉,人脸识别,偶尔发点小脾气,偶尔烘托点情侣之间的气氛,情绪价值算是给到了,技术上着实火了一把。

LOOI桌面 LOOI 桌面

远在大洋彼岸,自诩是文科生的机器人大拿 — Garman ,敏锐捕捉到了它的陪伴价值,在现代快节奏生活中的情感寄托,在无数次分享中,他都强调了这点,这在技术人中很少见到。

然后 1500+的价格,对于很多人来说,内心的独白大概是: 我!没有情感诉求,没有!

Garman 自学 Python,自学 Arduino 编程,在经历过无数次尝试以后,无私贡献了自己的成果,把价格硬生生给打下来了,不足 200 就实现了 LOOI 自由。不过,门槛并不低,在一次分享以后,家里多一个吃灰的 R4 板子。

Garman版LOOI

这次 AIPO 高校活动,鉴于 AI 编程共学的经验,做了 Arduino 的安装教程(因为当时以为这是主要的开发工具),有幸成为 Garman 老师的助手,在老师的眼皮底下,眼睁睁看着把线给插错了,不知道直播间的同学们进展如何。

回来以后,就想着 :

  • Arduino 是要用汇编语言写吗?
  • Python 在这个事中扮演什么样的角色
  • 为什么安卓手机不行,只有 iOS 可以
  • 刷机是什么意思
  • 作为生产力工具的老鸟,希望能解决把串口号抄下来这个难题。

带着这些疑问开始了这次的征程,目前的成果大概是这样的:

image.png
image.png
image.png
image.png
image.png
image.png
目前还有 2 个问题没有解决:
  • 如何和手机联动,发信息过去
  • 摄像头跟随

有兴趣的小伙伴留言,一起折腾完善。

源代码分析,各设备之间的交互逻辑

这次实践之前,完全没有 Arduino 操作经验,对硬件一窍不通。

分析现有代码逻辑—Claude

这部分基本是靠 claude 来实现的,下面是 Garman 老师的核心逻辑
机器人核心组件

老实说,上次看到这个图还是上一次,就在半年前,当时想着应该不难吧,才 3 个文件,硬啃也能啃下来呀,结果,楞是啃了半年,毫无进展,这次借助 claude,才稍微有了点眉目。

硬件部分完全小白,请移步 Garman 老师的共学视频。waytoagi 知识库搜索 Garman

整体逻辑

在原图基础上,重新梳理了一个时序图,从下面的箭头大概能看出来信息的流动情况。每一次会话的过程大概是这样的:

  • 在电脑上同时启动 2 个程序,也就是 chat 和 head。在手机上启动face
  • 在 chat 里面输入信息,它会和 AI 说,AI 就给了一段声音和一个表情符号
  • 然后 chat 把它发给手机,手机上的 face 拿到这个信息之后,播放声音,显示一个表情符号

这样就结束了。然后我们再来看 head,它的作用是时刻捕捉摄像头,分析里面的人物动作,去调整舵机。

另外一个版本的流程图

3个模块的内部逻辑

Head (电脑端运行)

Face(手机上运行)

Chat(电脑上运行)

至此,整个机器人软件部分的逻辑算是理顺了。AI 编程的原则是能不编程最好不编,无奈在直播的时候,确实是通了,但是回家以后,就咋也不行。想着能不能不用 Python 来写呢?用 nodejs 能不能做到呢?

希望做成什么样

  • 首先希望有一个 UI 界面,尽量不用打开命令行
  • 其次所有的配置信息最好能抽离出来,方便后续切换
  • 最好能看到一些中间过程,方便定位问题

开始改造

image.png

image.png

经过了 50 多个会话,大概搞清楚了它的逻辑

舵机和板子还有 Arduino 的关系是啥

为了方便排查,板子是否和电脑连接成功,是否能正常控制舵机,就想了一个办法,让 AI 帮忙写一个小程序来测试。也是因为这次经历,了解到它背后的逻辑。

Arduino 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <Firmata.h>

#include <Servo.h>



Servo myservo; // 创建舵机对象

int servoPin = 9; // 舵机连接的引脚

int minAngle = 0; // 最小角度

int maxAngle = 180; // 最大角度

int delayTime = 1000; // 每次移动后的延迟时间(毫秒)

boolean isRunning = true; // 控制舵机是否运行



void setup() {

Firmata.setFirmwareVersion(FIRMATA_FIRMWARE_MAJOR_VERSION, FIRMATA_FIRMWARE_MINOR_VERSION);

Firmata.attach(ANALOG_MESSAGE, analogWriteCallback);

Firmata.begin(57600);

myservo.attach(servoPin); // 将舵机对象附加到指定引脚

Serial.begin(9600); // 初始化串口通信,用于控制

Serial.println("输入 's' 停止舵机,输入 'r' 重新启动舵机");

}



void loop() {

while(Firmata.available()) {

Firmata.processInput();

}

// 检查串口输入

if (Serial.available() > 0) {

char input = Serial.read();

if (input == 's') {

isRunning = false;

Serial.println("舵机已停止");

} else if (input == 'r') {

isRunning = true;

Serial.println("舵机已重新启动");

}

}

if (isRunning) {

// 移动舵机到最小角度

myservo.write(minAngle);

delay(delayTime);

// 移动舵机到最大角度

myservo.write(maxAngle);

delay(delayTime);

}

}



void analogWriteCallback(byte pin, int value) {

if (pin == servoPin) {

myservo.write(value);

}

}

它需要放在 arduino 的 ide 中执行
image.png
image.png

node 的代码 (arduino_servo_control.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const { SerialPort } = require('serialport');

const { ReadlineParser } = require('@serialport/parser-readline');



// 替换为你的串口名称(例如 COM3 或 /dev/ttyUSB0)

const port = new SerialPort({ path: '/dev/cu.usbmodemF0F5BD543E182', baudRate: 9600 });

const parser = port.pipe(new ReadlineParser({ delimiter: '\n' }));



port.on('open', () => {

console.log('串口已打开');



// 发送指令让舵机移动到指定位置 (例如 X: 45度, Y: 135度)

const position = '45,135\n';

port.write(position, (err) => {

if (err) {

return console.log('Error on write: ', err.message);

}

console.log('已发送指令: 移动到位置 45,135');

});

});



port.on('error', (err) => {

console.log('Error: ', err.message);

});



parser.on('data', (data) => {

console.log('收到来自Arduino的数据: ', data);

});

运行的时候,首先需要安装依赖包,然后运行

1
2
npm install serialport
node arduino_servo_control.js

最后执行的结果是这样的:

测试代码执行情况

到底是什么样的逻辑呢,其实板子在死循环一直等消息

目前了解到的信息大概是这样的。

板子和设备的关系

目前对它的理解大概是这样的:

  • 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,我们还是希望在把难度打下来这条路上,贡献一些力量。