柴火创客社区 · 10月17日 · 广东

创客项目秀|基于XIAO ESP32S3的Dao Clock项目

作者: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”。
在开始之前,我收集了这个项目所需的传感器的规格和信息。

项目编号描述数量价格
1DHT11-温度和湿度传感器贴片1$5.99
2R-1206 - 4.99K - 贴片1$0.014
3R-1206 - 1K - SMD 电阻器1$0.00606
4R-1206 - 499R - SMD 电阻器1$0.10
5C-1206 - 100nF - SMD 电容器1$0.26
6开关 - SW2 - B3SN-3112P - OMRON1$0.85
7NeoPixel x 12 - 5 毫米1五包$4.95
8LED-1206 绿色 - SMD1$0.382
9Seeed-Xiao - ESP32S3 - Sense1$13.99
10Grove - 光传感器 v1.21$3.90
11Grove - MP3 扬声器1$6.90

PS:整体的PCB设计花了我很多时间,由于设计难度大,我不得不调整相应的3D外观设计方案。
修改流程如下:

  1. Neopixel PCB方案调整:由于我的设计需要 12 个 Neopixel 串联,所以我创建了设计图,但我发现由于 PCB 的尺寸小,无法通过 CNC 雕刻来实现。因此,我更改了设计计划。
  2. 因设计缺陷导致的结构方案调整:在设计过程中,我发现我的方案可能存在缺陷:Neopixels的闪烁会干扰光传感器的正常运行,所以我改变了方案。
  3. PCB设计缺陷:在完成所有焊接工作并测试编程后,我发现传感器布局方向错误,导致DHT11损坏。我重新设计了 PCB 的一个版本。
  4. 跳线较多导致底部空间设计不足:这让我再次调整了我的3D设计方案。

我的电路设计分为两个主要部分。

  1. 一部分是用于固定 12 个 Neopixel 的上部模块、一个光传感器和一个 Xiao 摄像头组件。
  2. 另一部分是主控电路板,包括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 布局部分,我按照以下准则进行设计:

  1. 以合乎逻辑和直观的方式放置组件。
  2. 使用网格布局以确保正确的对齐和布线。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); // 默认 - 白色          
                      }          
                  }
推荐阅读
关注数
9152
内容数
48
深度服务产业的国际化双创平台
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息