作者:Chexin Yang
来自:Fab academy
Fab Academy,源自麻省理工学院的先进教育项目,致力于培养具有全球视野的创新者和制造者。通过密集学习和实践,学生们掌握了从基础到高级的制造技术,学习了如何将创意转化为现实。今天,小编将给大家带来其中一位Fab Academy毕业生 Chenxin Yang的结课项目:Dao Clock。这个项目产品系统可以实时监控环境变化,并在检测到预定的唤醒信号时触发警报或其他唤醒机制。让我们一起深入了解这个有趣的项目!
项目介绍
项目背景
当闹钟响起时,人们并真的不想起床。取而代之的是,会经常根据环境条件来决定何时真正离开床铺。这促使作者构思了一种名为“懒惰时钟”的设备,旨在能根据周围环境中自然唤醒,称之为“自然唤醒”的概念。
因此,计划使用各种传感器来捕捉环境中的自然变化,例如声音、光线和温度,并将这些数据转换为适合机器学习模型的样本数据。接下来,将设计和训练一个轻量级的机器学习模型,该模型能够将特定的环境变化识别为唤醒用户的信号。
项目名字:
通过进一步思考,我从中国传统文化中学会了尊重自然。在中国古代,人们使用日晷记录时间,日晷通过自然光的变化来显示时间。
日晷的定义:十天干十二地枝
十二生肖时辰:紫、周、阴、毛、陈、思、吴、魏、申、优、徐、海
添加图片注释,不超过 140 字(可选)
这为我的表盘设计提供了灵感。我选择了“道”作为“Dao Clock”的象征,Dao Clock是时间和自然的象征。
项目草图:
在道钟的设计中,我将使用光传感器、麦克风、湿度传感器和温度传感器作为输入设备。并使用 Seeed Xiao 作为处理器
当达到合适的环境时,它会从 SD 卡中读取以播放我最喜欢的音乐,并通过作为输出设备的扬声器唤醒我。
对于外壳,我将使用透明的塑料材料,确保这些传感器在运行过程中不受影响。此外,它的吸盘将用于将道钟连接到我的窗户上。
材料清单及外观设计
道钟外观结构包含它包括三个部分:透明顶盖、中间支撑结构以及底部结构
外观材料清单:
名称 | 数量 | 描述 |
---|---|---|
透明顶盖 | 1 | 透明材料用于保护设备并防止组件暴露在外。选择这种材料是为了展示 Neopixel 照明 效果,同时让光传感器有效运行。 |
中间支撑 | 1 | 圆柱形保护层经过专门设计,用于支撑PCB_Part1的组件,并防止Neopixels在操作过程中干扰光传感器。 |
底座结构 | 1 | 底座用于保护组件,并为放置电池提供空间。 |
热熔铜螺母 | 4 | 嵌入塑料中的金属嵌件,提供耐用、可重复使用的螺纹孔 |
结构设计
1、 底座设计:
因为我想让这个有趣的产品好看,找到可以直接用来完成这个项目的标准件,所以我在淘宝上找到了我想要的吸盘。根据这个吸盘提供的尺寸,我设计了底座并使其适合我的底座。
2、 外壳设计
由于我需要使用各种传感器来检测环境变化,尤其是光线传感器和温度传感器,所以我选择将外壳设计成透明样式,让光线通过。同时,我考虑在某些位置打孔,以确保麦克风和扬声器不受干扰地工作。
3、 外壳与底座之间的连接结构
我设计了底座与外壳之间的连接结构,以确保传感器可以方便地连接。我设计了嵌入式凹槽,使外壳和底座的安装更加精确。我选择使用螺钉和螺母进行连接,所以我设计了螺丝孔。
需要说明的是,由于可能需要拆卸外壳进行调试和测试,因此我在外壳上设计了热熔螺母工艺。这使得连接外壳和底座变得容易,而且更加坚固。
制造
完成设计后,我需要使用满足特定外观要求的材料。我希望透明的顶盖和底座具有更吸引人的外观,因此我通过3D打印服务提供商进行制造。我自己打印了中间支撑部件并尝试组装。
我将尝试将透明外壳、支撑部件和底座组装在一起,以确认它们是否可以正确组装。但是,我发现中间支撑部件存在严重错误。因此,我使用其他工具修剪零件的接口,最后设法安装了它们。
本项目使用了热熔枪、热熔螺母和相应的螺钉。重要的是,在完成壳体孔的设计后,最关键的一点是确保螺丝不错位。如果它们安装不正确,整个程序集将受到损害并变得无法使用。
您可以看到安装后螺丝的特写以及四个螺丝被正确固定的最终外观。
组装
我在透明盖板和 PCB 上设计了凹槽,以确保它们精确地卡入相应的位置。这种设计保证了螺丝孔对准的准确性。
从图片中可以看出,盖板和PCB安装好后,螺丝孔清晰,螺母可见。
此外,组装完顶盖和底盖后,中间留有一个小缝隙,以便于用六角扳手拧紧螺丝。
一旦螺钉固定,组装部件之间的间隙就很小。
PCB设计
我的电路设计分为两个主要部分:
一部分是用于固定 12 个 Neopixel 的上部模块、一个光传感器和一个小摄像头组件(以下简称“PCB\_Part1”。
另一部分是主控电路板,包括XIAO ESP32S3、接口连接、扬声器、电池连接组件(以下简称“PCB\_Part2”。
在开始之前,我收集了这个项目所需的传感器的规格和信息。
项目编号 | 描述 | 数量 | 价格 |
---|---|---|---|
1 | DHT11-温度和湿度传感器贴片 | 1 | $5.99 |
2 | R-1206 - 4.99K - 贴片 | 1 | $0.014 |
3 | R-1206 - 1K - SMD 电阻器 | 1 | $0.00606 |
4 | R-1206 - 499R - SMD 电阻器 | 1 | $0.10 |
5 | C-1206 - 100nF - SMD 电容器 | 1 | $0.26 |
6 | 开关 - SW2 - B3SN-3112P - OMRON | 1 | $0.85 |
7 | NeoPixel x 12 - 5 毫米 | 1 | 五包$4.95 |
8 | LED-1206 绿色 - SMD | 1 | $0.382 |
9 | Seeed-Xiao - ESP32S3 - Sense | 1 | $13.99 |
10 | Grove - 光传感器 v1.2 | 1 | $3.90 |
11 | Grove - MP3 扬声器 | 1 | $6.90 |
PS:整体的PCB设计花了我很多时间,由于设计难度大,我不得不调整相应的3D外观设计方案。
修改流程如下:
- Neopixel PCB方案调整:由于我的设计需要 12 个 Neopixel 串联,所以我创建了设计图,但我发现由于 PCB 的尺寸小,无法通过 CNC 雕刻来实现。因此,我更改了设计计划。
- 因设计缺陷导致的结构方案调整:在设计过程中,我发现我的方案可能存在缺陷:Neopixels的闪烁会干扰光传感器的正常运行,所以我改变了方案。
- PCB设计缺陷:在完成所有焊接工作并测试编程后,我发现传感器布局方向错误,导致DHT11损坏。我重新设计了 PCB 的一个版本。
- 跳线较多导致底部空间设计不足:这让我再次调整了我的3D设计方案。
我的电路设计分为两个主要部分。
- 一部分是用于固定 12 个 Neopixel 的上部模块、一个光传感器和一个 Xiao 摄像头组件。
- 另一部分是主控电路板,包括XIAO ESP32S3、接口连接、扬声器、电池连接等组件。
这是我第一部分最初的设计:
突然间,我发现这个设计存在一个关键问题,我无法用小型CNC进行处理,因为走线必须非常细。我们的CNC雕刻钻头只能达到0.4mm的尺寸。
我咨询了Salman导师,他的最终建议是使用带有 12 个 LED 的预制 Neopixels 组件。因此,我重新调整了总体设计方案。
最后,我使用跳线将它们焊接在一起来连接必要的传感器组件。我利用支撑结构,尽可能合理地布置其上的传感器,形成一个新的组件。在设计涉及NeoPixel环的输出时,我最初使用了自己的PCB设计。但是,我在跟踪路由方面遇到了重大问题,因为我们使用了 12 个 NeoPixel,而根据文档,每个 NeoPixel 都需要添加一个电容器。由于走线宽度的限制,这使得使用 CNC 雕刻 PCB 变得具有挑战性。在我的导师 Salman 的建议下,他为我提供了一个预制的 12 NeoPixel 环,让我可以使用现成的模块来完成我的输出设计。
在切换到 12 NeoPixel 环形方法后,我重新设计了支撑结构以适应此组件。通过使用万用表测试每个组件的连接性,我确保没有短路问题。
最终PCB\_Part1设计如下:
在完成第一个PCB设计后,我进行了程序测试并发现DHT11运行不正常,我闻到了燃烧的味道。我意识到一定在某个地方短路了。在多次尝试排除故障并烧坏另一个 DHT11 传感器后,我发现电路板设计中的一个缺陷导致我错误地连接引脚,从而损坏了 DHT11 组件。
在从原理图到PCB的转换过程中,发生了一个小错误:我发现Xiao没有相应的足迹。因此,我进入组件编辑器以再次匹配适当的组件。
PCB元件布局
在 PCB 布局部分,我按照以下准则进行设计:
- 以合乎逻辑和直观的方式放置组件。
- 使用网格布局以确保正确的对齐和布线。PCB 组件布局是设计过程中的重要一步, 因为它有助于确保 PCB 可制造且功能正常.
第一步是以合乎逻辑和直观的方式放置组件。我将首先将它们分开,以尽量减少电线交叉。
PCB板外形设计:
最终,我实现了我想要的设计。
PCB制造
首先,是将Gerber文件转换为G代码文件,具体有以下两个步骤:
1、 将Gerber文件转换为可识别的PCB照片打印图像。
首先,我们需要从PCB设计接口生成制造文件:
请记住调整PCB设计规则,因为我们对PCB使用CNC加工,并注意 CNC 加工可能的最低精度。
然后将Gerber文件上传到在线工具Gerber2PNG以生成PCB照片打印图像。我们需要获取 3 个图像:Traces、outline 和 Drill。
PCB照片打印图像:
2、 从照片打印图像生成G代码文件。
这一步中,我们需要使用Modsproject(mods CE (modsproject.org))的 G-code Mill 2D PCB
在设置加工文件之前,请注意正在使用的雕刻和钻孔工具的参数。雕刻工具:尖端直径--0.2mm 角度--20°,钻孔工具:0.8mm。
最终,我们可以获得G代码文件的预览。
加工PCB
- 材料固定:使用双面胶带或虎钳固定待加工的PCB材料。
- 工具装载:选择合适的 V 形钻头或钻头,将它们安装在主轴上,并使用主轴扳手将它们锁定到位。
- 设备初始化:使用 Mach3 软件打开机器电源,清除警报,并设置工具的启动位置。
- G代码加载:将G代码加载到Mach3中,在加工前进行最终设置,例如调整主轴转速和进给率。
- 开始加工:启动机器开始加工,根据需要调整进给率并检查加工质量。
最后,我们可以看到完成的PCB。
材料清单
完成PCB加工后,我用万用表测试了PCB电路的连通性,以确保没有短路。具体方法涉及利用万用表的电路连通性测试功能,用正负极探头逐一测试所有导线。
这是Dao Clock的BOM(物料清单):
元件焊接
烙铁:我使用了额定温度为 400°C 的烙铁。
- 焊料:我使用了 2mm 厚的焊料。
注意:焊接时要非常小心,以确保使用适量的焊料,从而实现组件的电气连接。
完成主要部件的焊接后,我开始焊接XIAO ESP32S3。以下是XIAO ESP32S3 的引脚分配手册。另外,请务必在电源引脚位置放置绝缘贴纸,以防止短路。
完成此电路板后,我使用飞线将其他组件焊接在一起,包括光敏电阻和 Neopixels 环。
代码程序
总迭代次数:
该项目总共经历了 20 次迭代。
输入组件:
- 光敏电阻和DHT11:用于环境数据的传感器。
- WiFi连接:用于时间同步。
输出组件:
NeoPixel 戒指:用于信息的视觉显示。
发展历程:
- 广泛的迭代:该项目涉及许多漫长的迭代。
- Bug修复:在整个开发过程中,我们发现并纠正了许多错误。
具体代码如下:
#include
#include
#include
#include
#define PIN_NEOPIXEL D9 // NeoPixel连接的GPIO引脚D9
#define NUMPIXELS 12
#define DELAYVAL 150
#define LIGHT_SENSOR_PIN A8 // 光线传感器连接的模拟输入引脚A8
#define DHTPIN D6 // DHT11传感器连接的GPIO引脚D6
#define DHTTYPE DHT11 // DHT11传感器类型
#define WIFI_SSID "4536251" // Wi-Fi网络名
#define WIFI_PASSWORD "EACTJB159357" // Wi-Fi密码
Adafruit_NeoPixel pixels(NUMPIXELS, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
DHT dht(DHTPIN, DHTTYPE);
void setup() {
Serial.begin(115200);
pixels.begin(); // 初始化NeoPixel条
pinMode(LIGHT_SENSOR_PIN, INPUT); // 设置光线传感器引脚为输入
dht.begin(); // 初始化DHT11传感器
connectToWiFi(); // 连接到Wi-Fi网络
configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov"); // 配置时区为北京时间UTC+8
}
void loop() {
int lightLevel = analogRead(LIGHT_SENSOR_PIN); // 读取光线强度
int brightness = map(lightLevel, 800, 4095, 0, 255); // 映射光线强度到亮度值
pixels.setBrightness(brightness); // 设置LED亮度
showHourlyLED(); // 显示常规时辰LED
printSensorData(); // 打印传感器数据
delay(5000); // 每10s更新一次
}
void connectToWiFi() {
Serial.println("Attempting to connect to WiFi network...");
Serial.print("Connecting to ");
Serial.println(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int ledIndex = 0; // 初始LED索引
while (WiFi.status() != WL_CONNECTED) {
delay(150);
Serial.print(".");
lightLEDInSequence(ledIndex); // 点亮LED形成转圈效果
ledIndex = (ledIndex + 1) % NUMPIXELS; // 更新LED索引
}
// WiFi连接成功后,所有LED长亮5秒
pixels.fill(pixels.Color(0, 255, 0)); // 设置所有LED为绿色
pixels.show();
delay(5000); // 长亮5秒
Serial.println("");
Serial.println("WiFi connected.");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
void lightLEDInSequence(int ledIndex) {
pixels.clear();
pixels.setPixelColor(ledIndex, pixels.Color(50, 50, 50)); // 设置为柔弱的白光
pixels.show();
}
void showHourlyLED() {
float temp = dht.readTemperature(); // 读取温度值(摄氏度)
float humidity = dht.readHumidity(); // 读取湿度百分比
if (temp > 18 && temp < 37 && humidity > 30 && humidity < 60) {
breatheLights(); // 在舒适条件下展示呼吸灯效果
} else {
time_t now = time(nullptr);
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
return;
}
// 打印当前时间
char buffer[20];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
Serial.print("Synced time: ");
Serial.println(buffer);
int hour = timeinfo.tm_hour;
int shichenIndex = (hour + 1) / 2 % 12; // 计算当前时辰
uint32_t color = getColorForShichen(shichenIndex); // 获取当前时辰的颜色
pixels.clear();
for (int i = 0; i <= shichenIndex; i++) {
pixels.setPixelColor(i, color); // 使用统一颜色设置点亮的LED
}
pixels.show();
}
}
void breatheLights() {
time_t now = time(nullptr);
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
return;
}
int hour = timeinfo.tm_hour % 12; // 获取12小时制的小时数
uint32_t color = getColorForShichen((hour == 0 ? 12 : hour) - 1); // 获取当前时辰的颜色
int maxBrightness = 255; // 最大亮度值
int minBrightness = 30; // 最小亮度值
int steps = 30; // 控制波浪流动的步长
// 提取颜色的红、绿、蓝组成部分
uint8_t r = (uint8_t)((color >> 16) & 0xFF);
uint8_t g = (uint8_t)((color >> 8) & 0xFF);
uint8_t b = (uint8_t)(color & 0xFF);
// 增加亮度的循环
for (int brightness = minBrightness; brightness <= maxBrightness; brightness += 5) {
for (int i = 0; i <= hour; i++) {
pixels.setPixelColor(i, pixels.Color(r * brightness / maxBrightness, g * brightness / maxBrightness, b * brightness / maxBrightness));
}
pixels.show();
delay(30); // 控制波浪速度
}
// 减少亮度的循环
for (int brightness = maxBrightness; brightness >= minBrightness; brightness -= 5) {
for (int i = 0; i <= hour; i++) {
pixels.setPixelColor(i, pixels.Color(r * brightness / maxBrightness, g * brightness / maxBrightness, b * brightness / maxBrightness));
}
pixels.show();
delay(30); // 控制波浪速度
}
}
uint32_t getCurrentHourColor() {
time_t now = time(nullptr);
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
return pixels.Color(255, 255, 255); // 默认白色
}
int hour = timeinfo.tm_hour % 12; // 获取12小时制的小时数
return getColorForShichen((hour == 0 ? 12 : hour) - 1); // 获取当前时辰的颜色
}
void printSensorData() {
float temp = dht.readTemperature(); // 读取温度值(摄氏度)
float humidity = dht.readHumidity(); // 读取湿度百分比
int lightLevel = analogRead(LIGHT_SENSOR_PIN); // 读取光线强度
Serial.println("Current sensor readings:");
Serial.printf("Temperature: %.2f C\n", temp);
Serial.printf("Humidity: %.2f %%\n", humidity);
Serial.printf("Light level: %d\n", lightLevel);
}
uint32_t getColorForShichen(int index) {
switch (index) {
case 0: return pixels.Color(0, 0, 0); // 子时 - 玄黑
case 1: return pixels.Color(74, 66, 102); // 丑时 - 黛色
case 2: return pixels.Color(61, 59, 79); // 寅时 - 鸦青
case 3: return pixels.Color(237, 87, 54); // 卯时 - 妃色
case 4: return pixels.Color(255, 241, 67); // 辰时 - 藤黄
case 5: return pixels.Color(249, 144, 111); // 巳时 - 酡颜
case 6: return pixels.Color(157, 41, 51); // 午时 - 胭脂
case 7: return pixels.Color(72, 192, 163); // 未时 - 天水碧
case 8: return pixels.Color(217, 182, 17); // 申时 - 秋香
case 9: return pixels.Color(115, 151, 171); // 酉时 - 花青
case 10: return pixels.Color(93, 81, 60); // 戌时 - 相思灰
case 11: return pixels.Color(214, 236, 240); // 亥时 - 月白
default: return pixels.Color(255, 255, 255); // 默认 - 白色
}
}