基于ESP32的办公室电子宠物:物联网环境感知与交互系统实践
1. 项目概述:当“电子宠物”走进办公室
最近在GitHub上看到一个挺有意思的项目,叫opencroc/cube-pets-office。光看名字,你可能会联想到小时候玩的电子宠物机,或者是一些桌面小摆件。没错,这个项目的核心,就是为现代办公室环境,创造一种全新的、数字化的“电子宠物”体验。但它又不止于此,它把“宠物”这个概念,从单纯的娱乐,延伸到了办公场景下的环境感知、互动提醒,甚至团队氛围的数字化构建上。
简单来说,cube-pets-office是一个运行在小型硬件(比如树莓派、ESP32等)上的软件项目。它通过连接各种传感器(温湿度、光照、噪音、人体红外等),实时感知办公室的物理环境状态,并将这些数据转化为一个或多个虚拟“立方体宠物”的行为和状态。这些宠物会显示在一块小屏幕上,它们可能会因为办公室太吵而“捂耳朵”,因为光线太暗而“打瞌睡”,或者因为长时间无人活动而显得“孤单”。团队成员可以通过简单的物理交互(比如敲击设备、按按钮)或者网络请求来与宠物互动,喂食、玩耍,改变它的心情,从而间接地关注和改善办公环境。
这个项目巧妙地用“宠物”这个极具亲和力的载体,解决了几个办公室里的“隐形”痛点:环境质量无人关心、团队缺乏轻量级互动话题、远程办公下办公室“存在感”缺失。它适合谁呢?首先是喜欢折腾硬件的极客和创客,想做个既有趣又有用的小玩意儿;其次是团队管理者或行政,希望用低成本、高趣味性的方式提升办公体验;最后,任何对物联网、嵌入式开发、数据可视化感兴趣的朋友,都能从中找到学习和实践的乐趣。接下来,我就结合自己的理解和一些常见的实践,来深度拆解一下这个项目的实现思路、技术要点以及那些“做了才知道”的坑。
2. 项目整体设计与核心思路拆解
2.1 从“宠物”到“环境感知代理”的设计哲学
这个项目的精髓在于其设计理念的转换。它没有做一个功能复杂的办公室监控系统,那样太生硬、有压迫感;而是选择了一个情感化的切入点——宠物。宠物的状态(开心、困倦、活跃、生病)天然就是环境数据的绝佳可视化载体。比如,温度传感器读数28°C,在监控系统里只是一个数字,但在宠物世界里,它可以表现为“宠物吐着舌头散热”的动画。噪音分贝过高,宠物会做出“烦躁捂耳”的动作。这种设计极大地降低了数据的理解门槛,并激发了人们主动去改善环境的意愿,因为你是在“照顾”一个生命体,而不是在“处理”一个报警。
在方案选型上,项目必然走向“微控制器+传感器+显示屏”的经典物联网架构。为什么不是直接用电脑跑个程序?因为我们需要一个独立的、低功耗的、可以摆放在办公室任何角落的实体设备,这能增强其作为“宠物”和“环境成员”的实体感和存在感。树莓派Zero W或ESP32系列是理想的选择,它们性能足够驱动一块小屏和多个传感器,且功耗低,支持Wi-Fi,便于远程管理和数据上传。
2.2 核心模块与数据流设计
整个系统的数据流可以清晰地划分为三层:感知层、逻辑层和表现层。
感知层负责采集原始环境数据。通常会包含以下传感器:
- 温湿度传感器(如DHT22):监测办公室舒适度。湿度过低(空调房常见)可能导致宠物“皮肤干燥”,温度过高则可能“精神萎靡”。
- 声音传感器(或麦克风模块):监测环境噪音。持续高分贝噪音会让宠物“焦虑”,短暂的欢声笑语则可能让它“雀跃”。这里需要注意,为了避免隐私问题,通常只采集声音的振幅(分贝)或频率特征,而不是录制和分析具体语音内容。
- 光照传感器(如BH1750):监测光线强度。光线充足时宠物“活力四射”,昏暗时则“昏昏欲睡”。
- 人体红外传感器(如HC-SR501):监测人员活动。长时间无人经过,宠物会进入“孤独”或“休眠”状态;检测到活动则“醒来”互动。
逻辑层是项目的大脑,运行在主控制器上。它的核心是一个“宠物状态机”。每个传感器数据经过标准化处理后,会被映射为影响宠物“心情值”、“活力值”、“健康值”等内在属性的因子。例如:
当前噪音分贝 = 70dB 噪音舒适阈值 = 55dB 心情衰减量 = (70 - 55) * 0.1 // 每超出阈值1dB,心情值每秒减0.1 宠物心情值 = max(0, 宠物心情值 - 心情衰减量)同时,逻辑层还要处理交互输入(如按钮按下代表“喂食”,网络API调用代表“玩耍”),并更新宠物状态。状态机需要精心设计权重和衰减函数,让宠物的行为变化既灵敏又不过于频繁,显得自然。
表现层即显示屏(如SPI接口的IPS TFT屏)和可能的简单声光输出(LED、蜂鸣器)。逻辑层将最终的宠物状态(如“开心_80%”、“饥饿_30%”)传递给表现层,表现层根据预设的动画帧和规则,渲染出对应的宠物形象和场景。一个复杂的表现层可能会包含几十种不同的动画状态和过渡效果。
注意:在传感器选型时,务必考虑办公室环境的特殊性。例如,人体红外传感器对于静态办公的人员检测可能不灵敏,可能需要结合声音传感器进行判断。光照传感器要避免被设备自身的屏幕光或台灯直射干扰。这些细节决定了宠物反应是否“真实可信”。
3. 硬件选型、环境搭建与核心代码解析
3.1 硬件清单与连接要点
要复现这样一个项目,你需要准备以下核心硬件。这里我以性价比和易用性较高的ESP32开发板和1.8寸TFT屏为例进行说明。
| 组件 | 推荐型号 | 功能 | 大致成本 | 连接接口/备注 |
|---|---|---|---|---|
| 主控制器 | ESP32 DevKit V1 | 运行主程序,连接所有外设 | ¥30-50 | 核心,需支持Wi-Fi |
| 显示屏 | ST7735 1.8寸 SPI TFT | 显示宠物动画和状态 | ¥20-30 | SPI接口,节省IO口 |
| 温湿度 | DHT22 (AM2302) | 检测温度、湿度 | ¥15-25 | 单总线,需上拉电阻 |
| 环境光 | BH1750 | 检测光照强度(Lux) | ¥5-10 | I2C接口 |
| 声音 | KY-037 或 MAX9814 | 检测环境噪音强度 | ¥5-15 | 模拟输出(AO)接ADC引脚 |
| 人体红外 | HC-SR501 | 检测运动 | ¥5-10 | 数字输出(DO) |
| 交互按钮 | 轻触开关 x 3 | 喂食、玩耍、切换 | ¥2 | 接GPIO并下拉 |
| 电源 | 5V 2A MicroUSB电源 | 为整个系统供电 | 自有 | 稳定供电很重要 |
接线示意图(核心部分):
ESP32 GPIO18 -> TFT SCLK ESP32 GPIO23 -> TFT MOSI (SDA) ESP32 GPIO5 -> TFT DC (寄存器/数据选择) ESP32 GPIO4 -> TFT RST (复位) ESP32 GPIO2 -> TFT CS (片选) ESP32 GPIO16 -> DHT22 DATA ESP32 3.3V -> DHT22 VCC, BH1750 VCC, 传感器VCC ESP32 GND -> 所有GND ESP32 GPIO21 -> BH1750 SDA (I2C) ESP32 GPIO22 -> BH1750 SCL (I2C) ESP32 GPIO34 -> 声音传感器 AO (ADC1通道6) ESP32 GPIO35 -> HC-SR501 DO (ADC1通道7, 仅作数字输入) ESP32 GPIO15 -> 按钮1 (喂食), 接下拉电阻到GND ESP32 GPIO13 -> 按钮2 (玩耍), 接下拉电阻到GND实操心得:ESP32的ADC引脚(GPIO34-39)只能作为输入,且内部无上拉下拉电阻。连接按钮时,务必使用外部下拉电阻(如10KΩ)到GND,以确保引脚稳定读取低电平,防止因浮空产生误触发。对于DHT22,数据线需要接一个4.7KΩ - 10KΩ的上拉电阻到3.3V。
3.2 开发环境搭建与核心库
软件层面,我们使用Arduino IDE或PlatformIO进行开发。PlatformIO在库管理上更强大,推荐使用。
首先,需要安装以下核心库(在PlatformIO的platformio.ini中配置或Arduino库管理中搜索安装):
TFT_eSPI:驱动ST7735等TFT屏幕的强大图形库,需手动配置引脚。DHT sensor library:用于读取DHT11/DHT22数据。Adafruit BH1750:用于读取BH1750光照传感器。ArduinoJson:如果需要通过网络API交换数据,用于处理JSON。
配置TFT_eSPI库:这是第一个坑。找到Arduino库安装目录下的TFT_eSPI文件夹,编辑User_Setup.h文件。你需要根据屏幕驱动芯片和你的接线,注释掉不用的驱动,并正确设置引脚。例如:
// 选择驱动芯片,取消注释你的屏幕型号 #define ST7735_DRIVER // 我的屏幕是ST7735 // 定义ESP32的引脚 #define TFT_CS 4 // Chip select control pin #define TFT_DC 5 // Data Command control pin #define TFT_RST 2 // Reset pin (could connect to ESP32 RST pin) // 定义SPI时钟频率 #define SPI_FREQUENCY 27000000 // 27MHz, 根据屏幕性能调整保存后,在代码中#include <TFT_eSPI.h>即可使用。
3.3 宠物状态机与传感器数据融合的核心代码逻辑
这是项目的灵魂。我们不会贴出全部代码,但会拆解最核心的状态机逻辑和数据融合方法。
第一步:定义宠物属性结构体
struct PetState { String mood; // “HAPPY”, “SAD”, “ANGRY”, “SLEEPY”, “NORMAL” float moodValue; // 0.0 - 100.0,心情数值 float energy; // 0.0 - 100.0, 能量值 float hunger; // 0.0 - 100.0, 饥饿值,随时间增长 float health; // 0.0 - 100.0, 健康值,受环境影响 // ... 其他属性如清洁度、亲密度等 }; PetState myPet;第二步:传感器数据采集与标准化
void readSensors() { // 读取光照 float lux = lightMeter.readLightLevel(); // 标准化到0-1的影响因子,假设500lux为最舒适 float lightFactor = constrain(abs(lux - 500) / 500.0, 0, 1); // 光照偏离越大,因子越接近1,表示负面影响越大 // 读取噪音(模拟值) int soundRaw = analogRead(SOUND_PIN); // 假设安静时值<100, 吵闹时>800 float noiseFactor = constrain((soundRaw - 100) / 700.0, 0, 1); // 读取温湿度 float temp = dht.readTemperature(); float humi = dht.readHumidity(); // 计算温湿度舒适度因子(简化版) float tempComfort = 1.0 - constrain(abs(temp - 22.0) / 10.0, 0, 1); //22度为理想温度 float humiComfort = 1.0 - constrain(abs(humi - 50.0) / 50.0, 0, 1); //50%为理想湿度 float envComfortFactor = (tempComfort + humiComfort) / 2.0; // 检测运动 bool hasMotion = digitalRead(MOTION_PIN) == HIGH; }第三步:状态更新逻辑(每秒执行一次)
void updatePetState(float lightFactor, float noiseFactor, float envComfortFactor, bool hasMotion) { // 1. 基础衰减:饥饿随时间增加,能量随时间减少 myPet.hunger += HUNGER_RATE_PER_SEC; myPet.energy -= ENERGY_DECAY_PER_SEC; // 2. 环境因素影响健康值 float envImpact = (lightFactor + noiseFactor + (1.0 - envComfortFactor)) / 3.0; myPet.health -= envImpact * HEALTH_DECAY_RATE; myPet.health = constrain(myPet.health, 0, 100); // 3. 综合环境与自身状态,计算心情值 float baseMoodChange = 0; if (envImpact > 0.7) baseMoodChange -= 2.0; //环境很差,心情大跌 else if (envImpact < 0.3) baseMoodChange += 0.5; //环境舒适,心情微涨 if (myPet.hunger > 80) baseMoodChange -= 1.5; //很饿 if (myPet.energy < 20) baseMoodChange -= 1.0; //很累 if (hasMotion) baseMoodChange += 0.2; //有人活动,宠物感到被关注 myPet.moodValue += baseMoodChange; myPet.moodValue = constrain(myPet.moodValue, 0, 100); // 4. 根据心情值等,确定当前情绪状态 updateMoodState(); } void updateMoodState() { if (myPet.energy < 15) { myPet.mood = “SLEEPY”; } else if (myPet.moodValue > 75) { myPet.mood = “HAPPY”; } else if (myPet.moodValue < 30) { myPet.mood = “SAD”; } else if (myPet.health < 50) { myPet.mood = “SICK”; } else { myPet.mood = “NORMAL”; } }第四步:交互处理
void checkInteractions() { if (digitalRead(BUTTON_FEED) == HIGH) { myPet.hunger = max(0, myPet.hunger - 30); //喂食减少饥饿 myPet.moodValue += 10; //喂食提升心情 triggerAnimation(“FEEDING”); } if (digitalRead(BUTTON_PLAY) == HIGH) { myPet.energy = max(0, myPet.energy - 15); //玩耍消耗能量 myPet.moodValue += 15; //玩耍大幅提升心情 triggerAnimation(“PLAYING”); } }这个逻辑框架清晰地展示了如何将冰冷的传感器数据,转化为有情感的宠物行为。你可以调整各种速率因子(HUNGER_RATE_PER_SEC)、阈值和计算公式,来塑造不同性格的宠物(比如一只“懒散”的宠物能量衰减更快,一只“敏感”的宠物受噪音影响更大)。
4. 动画渲染、显示优化与网络功能拓展
4.1 基于状态机的动画系统实现
有了宠物状态,下一步就是让它“动”起来。在资源有限的嵌入式设备上,我们需要一个高效的动画系统。通常采用“状态-动画帧”映射的方式。
首先,准备素材。你可以用任何绘图软件(如Aseprite, Photoshop)绘制一组像素风格的宠物精灵图(Sprite Sheet)。每个状态(HAPPY, SAD, SLEEPY, EATING, PLAYING…)对应一组动画帧。例如,“IDLE”状态可能是4帧的呼吸动画,“HAPPY”状态可能是6帧的跳跃动画。
然后,实现一个简单的动画播放器:
// 定义动画结构 struct Animation { String name; const uint16_t *frameData; // 指向帧数据数组的指针 int frameCount; int frameDelay; // 每帧延迟(ms) bool loop; }; // 假设你已经将位图数据转换为16位RGB565数组 extern const uint16_t anim_idle_frames[4][128*128]; // 示例:4帧,128x128分辨率 extern const uint16_t anim_happy_frames[6][128*128]; Animation animIdle = {“IDLE”, &anim_idle_frames[0][0], 4, 200, true}; Animation animHappy = {“HAPPY”, &anim_happy_frames[0][0], 6, 150, true}; Animation* currentAnim = &animIdle; int currentFrame = 0; unsigned long lastFrameTime = 0; void renderPet() { // 1. 根据宠物状态选择动画 String targetAnimName = myPet.mood; if (isPlaying) targetAnimName = “PLAYING”; // 交互动画优先 // ... 其他逻辑决定最终动画 // 2. 切换动画(如果状态变了) if (targetAnimName != currentAnim->name) { if (targetAnimName == “HAPPY”) currentAnim = &animHappy; else if (targetAnimName == “IDLE”) currentAnim = &animIdle; // ... 其他状态 currentFrame = 0; } // 3. 定时播放当前动画的帧 if (millis() - lastFrameTime > currentAnim->frameDelay) { lastFrameTime = millis(); // 将帧数据推送到屏幕的特定位置 tft.pushImage(20, 20, 128, 128, (uint16_t*)currentAnim->frameData + (currentFrame * 128 * 128)); currentFrame++; if (currentFrame >= currentAnim->frameCount) { if (currentAnim->loop) currentFrame = 0; else { // 非循环动画播放完毕,切回默认状态动画 currentAnim = getMoodAnimation(myPet.mood); currentFrame = 0; } } } }注意事项:将大量位图数据存储在ESP32的Flash或PSRAM中是一个挑战。务必使用
PROGMEM关键字将常量数据存储在Flash中,以节省宝贵的RAM。同时,考虑使用压缩算法(如RLE)或降低颜色深度(从16位降至8位索引色)来减小图像体积。对于复杂的动画,128x128分辨率可能已经对ESP32造成压力,需要测试帧率。
4.2 网络功能拓展:让宠物连接世界
基本的本地交互已经很有趣,但加上网络功能,能让cube-pets-office的玩法提升一个维度。ESP32强大的Wi-Fi能力让这变得简单。
1. Web服务器状态面板:你可以让ESP32启动一个简单的Web服务器。同事们在浏览器输入设备的IP地址,就能看到一个状态面板,显示当前办公室的温湿度、噪音等级,以及宠物的实时心情、饥饿值,甚至可以点击网页按钮进行远程喂食。
#include <WiFi.h> #include <WebServer.h> WebServer server(80); void handleRoot() { String html = “<html><body>”; html += “<h1>办公室宠物状态</h1>”; html += “<p>心情:” + myPet.mood + “ (“ + String(myPet.moodValue) + “%)</p>”; html += “<p>温度:” + String(dht.readTemperature()) + “ °C</p>”; html += “<button onclick=\”fetch(‘/feed’)\”>远程喂食</button>”; html += “</body></html>”; server.send(200, “text/html”, html); } void handleFeed() { myPet.hunger -= 30; server.send(200, “text/plain”, “Feeding OK!”); } void setup() { // ... 其他初始化 WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) delay(500); Serial.println(WiFi.localIP()); // 打印IP地址 server.on(“/”, handleRoot); server.on(“/feed”, handleFeed); server.begin(); } void loop() { server.handleClient(); // ... 原有的主循环逻辑 }2. MQTT上报与集成:这是更高级的玩法。让ESP32作为MQTT客户端,将传感器数据和宠物状态发布到MQTT Broker(如部署在本地树莓派上的Mosquitto,或使用云服务)。然后,你可以:
- 用Node-RED创建一个仪表板,可视化整个办公室的环境数据和宠物情绪历史。
- 设置自动化规则:当宠物连续1小时“SAD”时,Node-RED自动发送一条提醒到团队Slack频道:“办公室环境似乎让宠物不开心了,看看是不是太闷热或太吵了?”
- 多个办公室的宠物数据可以汇聚到一起,进行“宠物心情排行榜”,增加团队间的趣味互动。
3. 物理外观与结构设计:为了让“宠物”更有实体感,一个定制的外壳必不可少。你可以使用3D打印设计一个立方体(呼应项目名中的“cube”)外壳,将屏幕嵌入一面,传感器探头巧妙布置在周围。电源线可以从底部接入。一个精心设计的外壳,能极大提升项目的完整度和摆在桌上的观赏性。
5. 常见问题、调试技巧与项目优化
在实际动手制作的过程中,你几乎一定会遇到下面这些问题。这里我把踩过的坑和解决方案整理出来,希望能帮你节省大量时间。
5.1 硬件与传感器相关问题
问题1:DHT22读取失败或返回NaN。
- 排查:这是最常见的问题。首先检查接线,数据线是否接了上拉电阻(4.7KΩ-10KΩ)。其次,DHT22对时序要求严格,确保在
setup()中调用dht.begin()后,留有至少1秒的初始化时间(delay(1000))。最后,读取函数之间需要至少2秒的间隔,连续快速读取会导致失败。 - 解决:实现一个健壮的读取函数,包含错误重试机制。
float readDHTTemperature() { float t = dht.readTemperature(); for (int i=0; i<3 && isnan(t); i++) { // 重试3次 delay(2000); t = dht.readTemperature(); } return t; }
问题2:TFT屏幕白屏、花屏或不显示。
- 排查:99%是
TFT_eSPI库的User_Setup.h文件配置错误。请仔细核对:- 是否正确定义了驱动芯片(如
#define ST7735_DRIVER)? - 引脚定义(
TFT_CS,TFT_DC,TFT_RST)是否和你的实际接线一致? - 是否正确定义了屏幕分辨率(
#define TFT_WIDTH 128,#define TFT_HEIGHT 160)?很多1.8寸屏是128x160。 - 尝试降低
SPI_FREQUENCY(如改为20000000),过高的频率可能导致通信不稳定。
- 是否正确定义了驱动芯片(如
- 解决:使用库中提供的示例图形测试程序(如
TFT_eSPI -> examples -> 320 x 240 -> TFT_Graphicstest),在确认硬件连接无误后,用示例程序来验证你的配置。示例能跑通,再移植到自己的项目。
问题3:声音传感器数值不稳定,无声音时也有底噪。
- 排查:模拟传感器容易受到电源噪声干扰。用
analogRead读取其在安静环境下的值,这个值就是底噪。 - 解决:在代码中设置一个阈值,低于该阈值的读数视为无效噪音。可以通过计算一段时间内的平均值和动态范围来校准。
const int SOUND_THRESHOLD = 200; // 根据实测调整 int soundValue = analogRead(SOUND_PIN); if (soundValue > SOUND_THRESHOLD) { // 处理有效声音 float noiseLevel = map(soundValue, SOUND_THRESHOLD, 4095, 0, 100); // ESP32 ADC为12位,0-4095 }
5.2 软件与逻辑问题
问题4:宠物状态变化过于频繁,显得“神经质”。
- 原因:状态更新逻辑的敏感度参数设置过高,或者传感器数据波动大直接映射到了状态上。
- 解决:
- 数据平滑(滤波):对传感器读数进行滑动平均滤波,减少毛刺。
#define FILTER_SIZE 10 float soundReadings[FILTER_SIZE]; int soundIndex = 0; float getFilteredSound() { soundReadings[soundIndex] = analogRead(SOUND_PIN); soundIndex = (soundIndex + 1) % FILTER_SIZE; float sum = 0; for (int i=0; i<FILTER_SIZE; i++) sum += soundReadings[i]; return sum / FILTER_SIZE; } - 状态滞后(Hysteresis):为状态切换设置不同的进入和退出阈值。例如,从“HAPPY”切换到“SAD”需要心情值低于25,但从“SAD”恢复为“HAPPY”则需要心情值高于40。这能防止状态在边界附近来回跳动。
- 降低更新频率:不必每秒都根据传感器数据大幅更新心情。可以每5-10秒计算一次环境对心情的累积影响。
- 数据平滑(滤波):对传感器读数进行滑动平均滤波,减少毛刺。
问题5:动画播放卡顿,主循环变慢。
- 原因:
pushImage或复杂的图形操作耗时过长,阻塞了主循环,导致传感器读取和逻辑更新不及时。 - 解决:
- 使用双缓冲(如果库支持):
TFT_eSPI支持创建内存中的帧缓冲区(TFT_eSprite),先在缓冲区里完成所有绘制,然后一次性pushSprite到屏幕,效率更高。 - 优化图像数据:如前所述,使用
PROGMEM存储,并考虑压缩。 - 非阻塞式定时:确保所有定时操作(如动画帧切换、传感器读取)都使用
millis()进行非阻塞判断,而不是用delay()。 - 降低动画帧率或分辨率:对于嵌入式设备,15-20 FPS已经足够流畅,不必追求30 FPS。
- 使用双缓冲(如果库支持):
5.3 项目优化与进阶思路
当你解决了基本问题后,可以考虑以下优化来提升体验:
- 低功耗设计:如果想让设备摆脱电源线,使用电池供电。可以在深夜或长时间无人时(通过人体红外判断),让ESP32进入深度睡眠(Deep Sleep)模式,仅定时唤醒检查,大幅延长续航。
- 离线存储状态:使用ESP32的Preferences库或EEPROM,在断电前将宠物的状态(心情、饥饿值等)保存到Flash中。下次上电时读取,让宠物拥有“记忆”,体验更连贯。
- 多宠物系统:在逻辑上维护多个宠物实例,它们共享环境数据但拥有独立的性格参数(如对噪音的耐受度不同)。通过按钮切换显示的宠物,或者让它们在屏幕上共处、互动。
- 集成更多传感器:加入空气质量传感器(如SGP30)、气压传感器,让宠物对更丰富的环境因素产生反应。甚至加入RFID读卡器,让同事刷卡“认领”喂养,增加归属感。
这个项目的魅力在于,它有一个简单的核心,但扩展的边界非常广阔。从硬件焊接、软件调试,到逻辑设计、外观制作,每一步都充满动手的乐趣和解决问题的成就感。它不仅仅是一个玩具,更是一个微型的、充满情感的物联网系统原型。当你看到自己创造的“小生命”在屏幕上因为办公室环境的变化而做出反应,并且同事们开始主动关心它、通过它来调节空调或提醒大家降低音量时,那种感觉是非常奇妙的。
